From acdaa6a5943eea10da1064b8e7cb2e1510f2f0f2 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Tue, 2 Jul 2024 13:22:37 -0400 Subject: [PATCH] Format all code / Add pre-commit format hook (#81) * Add format-guard * Format code --- .husky/pre-commit | 1 + .prettierrc | 4 + package-lock.json | 664 + package.json | 12 +- src/extensions/core/clipspace.ts | 284 +- src/extensions/core/colorPalette.ts | 1519 +- src/extensions/core/contextMenuFilter.ts | 273 +- src/extensions/core/dynamicPrompts.ts | 64 +- src/extensions/core/editAttention.ts | 275 +- src/extensions/core/groupNode.ts | 2589 +- src/extensions/core/groupNodeManage.ts | 145 +- src/extensions/core/groupOptions.ts | 477 +- src/extensions/core/invertMenuScrolling.ts | 60 +- src/extensions/core/keybinds.ts | 114 +- src/extensions/core/linkRenderMode.ts | 38 +- src/extensions/core/maskeditor.ts | 1982 +- src/extensions/core/nodeTemplates.ts | 754 +- src/extensions/core/noteNode.ts | 90 +- src/extensions/core/rerouteNode.js | 526 +- src/extensions/core/saveImageExtraOutput.ts | 58 +- src/extensions/core/simpleTouchSupport.ts | 173 +- src/extensions/core/slotDefaults.js | 159 +- src/extensions/core/snapToGrid.ts | 332 +- src/extensions/core/uploadAudio.ts | 70 +- src/extensions/core/uploadImage.ts | 12 +- src/extensions/core/webcamCapture.ts | 220 +- src/extensions/core/widgetInputs.js | 1565 +- src/lib/litegraph.core.js | 27493 +++++++++--------- src/lib/litegraph.extensions.js | 27 +- src/scripts/api.ts | 972 +- src/scripts/app.ts | 5471 ++-- src/scripts/changeTracker.js | 454 +- src/scripts/defaultGraph.ts | 250 +- src/scripts/domWidget.ts | 631 +- src/scripts/logging.ts | 433 +- src/scripts/pnginfo.ts | 187 +- src/scripts/ui.ts | 425 +- src/scripts/ui/components/asyncDialog.js | 100 +- src/scripts/ui/components/button.js | 278 +- src/scripts/ui/components/buttonGroup.js | 64 +- src/scripts/ui/components/popup.js | 223 +- src/scripts/ui/components/splitButton.js | 71 +- src/scripts/ui/dialog.ts | 13 +- src/scripts/ui/draggableList.ts | 36 +- src/scripts/ui/imagePreview.ts | 13 +- src/scripts/ui/menu/index.js | 36 +- src/scripts/ui/menu/queueButton.js | 18 +- src/scripts/ui/menu/viewList.js | 17 +- src/scripts/ui/menu/viewQueue.js | 6 +- src/scripts/ui/menu/workflows.js | 132 +- src/scripts/ui/settings.ts | 42 +- src/scripts/ui/spinner.ts | 1 - src/scripts/ui/toggleSwitch.ts | 5 +- src/scripts/ui/userSelection.ts | 49 +- src/scripts/utils.ts | 30 +- src/scripts/widgets.ts | 246 +- src/scripts/workflows.js | 112 +- src/types/apiTypes.ts | 253 +- src/types/colorPalette.ts | 62 +- src/types/comfy.d.ts | 147 +- src/types/comfyWorkflow.ts | 107 +- src/types/litegraph.d.ts | 2767 +- 62 files changed, 28225 insertions(+), 25406 deletions(-) create mode 100644 .husky/pre-commit create mode 100644 .prettierrc diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..2312dc587 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..f444ae604 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": true, + "trailingComma": "es5" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 30a6f457a..97768d3c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,9 +20,12 @@ "@types/node": "^20.14.8", "babel-plugin-transform-import-meta": "^2.2.1", "babel-plugin-transform-rename-import": "^2.3.0", + "husky": "^9.0.11", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "lint-staged": "^15.2.7", + "prettier": "^3.3.2", "ts-jest": "^29.1.4", "ts-node": "^10.9.2", "tsx": "^4.15.6", @@ -3884,6 +3887,87 @@ "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "dev": true }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3929,6 +4013,12 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3941,6 +4031,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4380,6 +4479,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4577,6 +4682,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -4739,6 +4856,21 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true, + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6758,12 +6890,293 @@ "immediate": "~3.0.5" } }, + "node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/lint-staged": { + "version": "15.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.7.tgz", + "integrity": "sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==", + "dev": true, + "dependencies": { + "chalk": "~5.3.0", + "commander": "~12.1.0", + "debug": "~4.3.4", + "execa": "~8.0.1", + "lilconfig": "~3.1.1", + "listr2": "~8.2.1", + "micromatch": "~4.0.7", + "pidtree": "~0.6.0", + "string-argv": "~0.3.2", + "yaml": "~2.4.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.3.tgz", + "integrity": "sha512-Lllokma2mtoniUOS94CcOErHWAug5iu7HOmDrvWgpw8jyQH2fomgB+7lZS4HWZxytUuQwkGOwe49FvwVaA85Xw==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.0.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -6788,6 +7201,147 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, + "node_modules/log-update": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^7.0.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7140,6 +7694,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -7233,6 +7799,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -7510,6 +8091,22 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7520,6 +8117,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, "node_modules/rollup": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", @@ -7659,6 +8262,46 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7723,6 +8366,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -8850,6 +9502,18 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 8c7b685c1..c7d4029a7 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "npm run typecheck && vite build", "zipdist": "node scripts/zipdist.js", "typecheck": "tsc --noEmit", + "format": "prettier --write 'src/**/*.{js,ts,tsx}'", "test": "npm run build && jest", "test:generate": "npx tsx tests-ui/setup", "test:browser": "npx playwright test", @@ -21,9 +22,12 @@ "@types/node": "^20.14.8", "babel-plugin-transform-import-meta": "^2.2.1", "babel-plugin-transform-rename-import": "^2.3.0", + "husky": "^9.0.11", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "lint-staged": "^15.2.7", + "prettier": "^3.3.2", "ts-jest": "^29.1.4", "ts-node": "^10.9.2", "tsx": "^4.15.6", @@ -36,5 +40,11 @@ "dotenv": "^16.4.5", "zod": "^3.23.8", "zod-validation-error": "^3.3.0" + }, + "lint-staged": { + "src/**/*.{js,ts,tsx}": [ + "prettier --write", + "git add" + ] } -} +} \ No newline at end of file diff --git a/src/extensions/core/clipspace.ts b/src/extensions/core/clipspace.ts index 175ad6bfd..ff5b307dd 100644 --- a/src/extensions/core/clipspace.ts +++ b/src/extensions/core/clipspace.ts @@ -3,164 +3,182 @@ import { ComfyDialog, $el } from "../../scripts/ui"; import { ComfyApp } from "../../scripts/app"; export class ClipspaceDialog extends ComfyDialog { - static items = []; - static instance = null; + static items = []; + static instance = null; - static registerButton(name, contextPredicate, callback) { - const item = - $el("button", { - type: "button", - textContent: name, - contextPredicate: contextPredicate, - onclick: callback - }) + static registerButton(name, contextPredicate, callback) { + const item = $el("button", { + type: "button", + textContent: name, + contextPredicate: contextPredicate, + onclick: callback, + }); - ClipspaceDialog.items.push(item); - } + ClipspaceDialog.items.push(item); + } - static invalidatePreview() { - if(ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0) { - const img_preview = document.getElementById("clipspace_preview") as HTMLImageElement; - if(img_preview) { - img_preview.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src; - img_preview.style.maxHeight = "100%"; - img_preview.style.maxWidth = "100%"; - } - } - } + static invalidatePreview() { + if ( + ComfyApp.clipspace && + ComfyApp.clipspace.imgs && + ComfyApp.clipspace.imgs.length > 0 + ) { + const img_preview = document.getElementById( + "clipspace_preview" + ) as HTMLImageElement; + if (img_preview) { + img_preview.src = + ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src; + img_preview.style.maxHeight = "100%"; + img_preview.style.maxWidth = "100%"; + } + } + } - static invalidate() { - if(ClipspaceDialog.instance) { - const self = ClipspaceDialog.instance; - // allow reconstruct controls when copying from non-image to image content. - const children = $el("div.comfy-modal-content", [ self.createImgSettings(), ...self.createButtons() ]); + static invalidate() { + if (ClipspaceDialog.instance) { + const self = ClipspaceDialog.instance; + // allow reconstruct controls when copying from non-image to image content. + const children = $el("div.comfy-modal-content", [ + self.createImgSettings(), + ...self.createButtons(), + ]); - if(self.element) { - // update - self.element.removeChild(self.element.firstChild); - self.element.appendChild(children); - } - else { - // new - self.element = $el("div.comfy-modal", { parent: document.body }, [children,]); - } + if (self.element) { + // update + self.element.removeChild(self.element.firstChild); + self.element.appendChild(children); + } else { + // new + self.element = $el("div.comfy-modal", { parent: document.body }, [ + children, + ]); + } - if(self.element.children[0].children.length <= 1) { - self.element.children[0].appendChild($el("p", {}, ["Unable to find the features to edit content of a format stored in the current Clipspace."])); - } + if (self.element.children[0].children.length <= 1) { + self.element.children[0].appendChild( + $el("p", {}, [ + "Unable to find the features to edit content of a format stored in the current Clipspace.", + ]) + ); + } - ClipspaceDialog.invalidatePreview(); - } - } + ClipspaceDialog.invalidatePreview(); + } + } - constructor() { - super(); - } + constructor() { + super(); + } - createButtons() { - const buttons = []; + createButtons() { + const buttons = []; - for(let idx in ClipspaceDialog.items) { - const item = ClipspaceDialog.items[idx]; - if(!item.contextPredicate || item.contextPredicate()) - buttons.push(ClipspaceDialog.items[idx]); - } + for (let idx in ClipspaceDialog.items) { + const item = ClipspaceDialog.items[idx]; + if (!item.contextPredicate || item.contextPredicate()) + buttons.push(ClipspaceDialog.items[idx]); + } - buttons.push( - $el("button", { - type: "button", - textContent: "Close", - onclick: () => { this.close(); } - }) - ); + buttons.push( + $el("button", { + type: "button", + textContent: "Close", + onclick: () => { + this.close(); + }, + }) + ); - return buttons; - } + return buttons; + } - createImgSettings() { - if(ComfyApp.clipspace.imgs) { - const combo_items = []; - const imgs = ComfyApp.clipspace.imgs; + createImgSettings() { + if (ComfyApp.clipspace.imgs) { + const combo_items = []; + const imgs = ComfyApp.clipspace.imgs; - for(let i=0; i < imgs.length; i++) { - combo_items.push($el("option", {value:i}, [`${i}`])); - } + for (let i = 0; i < imgs.length; i++) { + combo_items.push($el("option", { value: i }, [`${i}`])); + } - const combo1 = $el("select", - {id:"clipspace_img_selector", onchange:(event) => { - ComfyApp.clipspace['selectedIndex'] = event.target.selectedIndex; - ClipspaceDialog.invalidatePreview(); - } }, combo_items); + const combo1 = $el( + "select", + { + id: "clipspace_img_selector", + onchange: (event) => { + ComfyApp.clipspace["selectedIndex"] = event.target.selectedIndex; + ClipspaceDialog.invalidatePreview(); + }, + }, + combo_items + ); - const row1 = - $el("tr", {}, - [ - $el("td", {}, [$el("font", {color:"white"}, ["Select Image"])]), - $el("td", {}, [combo1]) - ]); + const row1 = $el("tr", {}, [ + $el("td", {}, [$el("font", { color: "white" }, ["Select Image"])]), + $el("td", {}, [combo1]), + ]); + const combo2 = $el( + "select", + { + id: "clipspace_img_paste_mode", + onchange: (event) => { + ComfyApp.clipspace["img_paste_mode"] = event.target.value; + }, + }, + [ + $el("option", { value: "selected" }, "selected"), + $el("option", { value: "all" }, "all"), + ] + ) as HTMLSelectElement; + combo2.value = ComfyApp.clipspace["img_paste_mode"]; - const combo2 = $el("select", - {id:"clipspace_img_paste_mode", onchange:(event) => { - ComfyApp.clipspace['img_paste_mode'] = event.target.value; - } }, - [ - $el("option", {value:'selected'}, 'selected'), - $el("option", {value:'all'}, 'all') - ]) as HTMLSelectElement; - combo2.value = ComfyApp.clipspace['img_paste_mode']; + const row2 = $el("tr", {}, [ + $el("td", {}, [$el("font", { color: "white" }, ["Paste Mode"])]), + $el("td", {}, [combo2]), + ]); - const row2 = - $el("tr", {}, - [ - $el("td", {}, [$el("font", {color:"white"}, ["Paste Mode"])]), - $el("td", {}, [combo2]) - ]); + const td = $el( + "td", + { align: "center", width: "100px", height: "100px", colSpan: "2" }, + [$el("img", { id: "clipspace_preview", ondragstart: () => false }, [])] + ); - const td = $el("td", {align:'center', width:'100px', height:'100px', colSpan:'2'}, - [ $el("img",{id:"clipspace_preview", ondragstart:() => false},[]) ]); + const row3 = $el("tr", {}, [td]); - const row3 = - $el("tr", {}, [td]); + return $el("table", {}, [row1, row2, row3]); + } else { + return []; + } + } - return $el("table", {}, [row1, row2, row3]); - } - else { - return []; - } - } + createImgPreview() { + if (ComfyApp.clipspace.imgs) { + return $el("img", { id: "clipspace_preview", ondragstart: () => false }); + } else return []; + } - createImgPreview() { - if(ComfyApp.clipspace.imgs) { - return $el("img",{id:"clipspace_preview", ondragstart:() => false}); - } - else - return []; - } + show() { + const img_preview = document.getElementById("clipspace_preview"); + ClipspaceDialog.invalidate(); - show() { - const img_preview = document.getElementById("clipspace_preview"); - ClipspaceDialog.invalidate(); - - this.element.style.display = "block"; - } + this.element.style.display = "block"; + } } app.registerExtension({ - name: "Comfy.Clipspace", - init(app) { - app.openClipspace = - function () { - if(!ClipspaceDialog.instance) { - ClipspaceDialog.instance = new ClipspaceDialog(); - ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate; - } + name: "Comfy.Clipspace", + init(app) { + app.openClipspace = function () { + if (!ClipspaceDialog.instance) { + ClipspaceDialog.instance = new ClipspaceDialog(); + ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate; + } - if(ComfyApp.clipspace) { - ClipspaceDialog.instance.show(); - } - else - app.ui.dialog.show("Clipspace is Empty!"); - }; - } -}); \ No newline at end of file + if (ComfyApp.clipspace) { + ClipspaceDialog.instance.show(); + } else app.ui.dialog.show("Clipspace is Empty!"); + }; + }, +}); diff --git a/src/extensions/core/colorPalette.ts b/src/extensions/core/colorPalette.ts index bb0c6fd6d..fea9d8e4d 100644 --- a/src/extensions/core/colorPalette.ts +++ b/src/extensions/core/colorPalette.ts @@ -5,789 +5,854 @@ import type { ColorPalettes } from "/types/colorPalette"; // Manage color palettes const colorPalettes: ColorPalettes = { - "dark": { - "id": "dark", - "name": "Dark (Default)", - "colors": { - "node_slot": { - "CLIP": "#FFD500", // bright yellow - "CLIP_VISION": "#A8DADC", // light blue-gray - "CLIP_VISION_OUTPUT": "#ad7452", // rusty brown-orange - "CONDITIONING": "#FFA931", // vibrant orange-yellow - "CONTROL_NET": "#6EE7B7", // soft mint green - "IMAGE": "#64B5F6", // bright sky blue - "LATENT": "#FF9CF9", // light pink-purple - "MASK": "#81C784", // muted green - "MODEL": "#B39DDB", // light lavender-purple - "STYLE_MODEL": "#C2FFAE", // light green-yellow - "VAE": "#FF6E6E", // bright red - "NOISE": "#B0B0B0", // gray - "GUIDER": "#66FFFF", // cyan - "SAMPLER": "#ECB4B4", // very soft red - "SIGMAS": "#CDFFCD", // soft lime green - "TAESD": "#DCC274", // cheesecake - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=", - "CLEAR_BACKGROUND_COLOR": "#222", - "NODE_TITLE_COLOR": "#999", - "NODE_SELECTED_TITLE_COLOR": "#FFF", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#AAA", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#333", - "NODE_DEFAULT_BGCOLOR": "#353535", - "NODE_DEFAULT_BOXCOLOR": "#666", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#FFF", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, + dark: { + id: "dark", + name: "Dark (Default)", + colors: { + node_slot: { + CLIP: "#FFD500", // bright yellow + CLIP_VISION: "#A8DADC", // light blue-gray + CLIP_VISION_OUTPUT: "#ad7452", // rusty brown-orange + CONDITIONING: "#FFA931", // vibrant orange-yellow + CONTROL_NET: "#6EE7B7", // soft mint green + IMAGE: "#64B5F6", // bright sky blue + LATENT: "#FF9CF9", // light pink-purple + MASK: "#81C784", // muted green + MODEL: "#B39DDB", // light lavender-purple + STYLE_MODEL: "#C2FFAE", // light green-yellow + VAE: "#FF6E6E", // bright red + NOISE: "#B0B0B0", // gray + GUIDER: "#66FFFF", // cyan + SAMPLER: "#ECB4B4", // very soft red + SIGMAS: "#CDFFCD", // soft lime green + TAESD: "#DCC274", // cheesecake + }, + litegraph_base: { + BACKGROUND_IMAGE: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=", + CLEAR_BACKGROUND_COLOR: "#222", + NODE_TITLE_COLOR: "#999", + NODE_SELECTED_TITLE_COLOR: "#FFF", + NODE_TEXT_SIZE: 14, + NODE_TEXT_COLOR: "#AAA", + NODE_SUBTEXT_SIZE: 12, + NODE_DEFAULT_COLOR: "#333", + NODE_DEFAULT_BGCOLOR: "#353535", + NODE_DEFAULT_BOXCOLOR: "#666", + NODE_DEFAULT_SHAPE: "box", + NODE_BOX_OUTLINE_COLOR: "#FFF", + DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", + DEFAULT_GROUP_FONT: 24, - "WIDGET_BGCOLOR": "#222", - "WIDGET_OUTLINE_COLOR": "#666", - "WIDGET_TEXT_COLOR": "#DDD", - "WIDGET_SECONDARY_TEXT_COLOR": "#999", + WIDGET_BGCOLOR: "#222", + WIDGET_OUTLINE_COLOR: "#666", + WIDGET_TEXT_COLOR: "#DDD", + WIDGET_SECONDARY_TEXT_COLOR: "#999", - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA", - }, - "comfy_base": { - "fg-color": "#fff", - "bg-color": "#202020", - "comfy-menu-bg": "#353535", - "comfy-input-bg": "#222", - "input-text": "#ddd", - "descrip-text": "#999", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#4e4e4e", - "tr-even-bg-color": "#222", - "tr-odd-bg-color": "#353535", - "content-bg": "#4e4e4e", - "content-fg": "#fff", - "content-hover-bg": "#222", - "content-hover-fg": "#fff", - } - }, - }, - "light": { - "id": "light", - "name": "Light", - "colors": { - "node_slot": { - "CLIP": "#FFA726", // orange - "CLIP_VISION": "#5C6BC0", // indigo - "CLIP_VISION_OUTPUT": "#8D6E63", // brown - "CONDITIONING": "#EF5350", // red - "CONTROL_NET": "#66BB6A", // green - "IMAGE": "#42A5F5", // blue - "LATENT": "#AB47BC", // purple - "MASK": "#9CCC65", // light green - "MODEL": "#7E57C2", // deep purple - "STYLE_MODEL": "#D4E157", // lime - "VAE": "#FF7043", // deep orange - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/gif;base64,R0lGODlhZABkALMAAAAAAP///+vr6+rq6ujo6Ofn5+bm5uXl5d3d3f///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAABkAGQAAAT/UMhJq7046827HkcoHkYxjgZhnGG6si5LqnIM0/fL4qwwIMAg0CAsEovBIxKhRDaNy2GUOX0KfVFrssrNdpdaqTeKBX+dZ+jYvEaTf+y4W66mC8PUdrE879f9d2mBeoNLfH+IhYBbhIx2jkiHiomQlGKPl4uZe3CaeZifnnijgkESBqipqqusra6vsLGys62SlZO4t7qbuby7CLa+wqGWxL3Gv3jByMOkjc2lw8vOoNSi0czAncXW3Njdx9Pf48/Z4Kbbx+fQ5evZ4u3k1fKR6cn03vHlp7T9/v8A/8Gbp4+gwXoFryXMB2qgwoMMHyKEqA5fxX322FG8tzBcRnMW/zlulPbRncmQGidKjMjyYsOSKEF2FBlJQMCbOHP6c9iSZs+UnGYCdbnSo1CZI5F64kn0p1KnTH02nSoV3dGTV7FFHVqVq1dtWcMmVQZTbNGu72zqXMuW7danVL+6e4t1bEy6MeueBYLXrNO5Ze36jQtWsOG97wIj1vt3St/DjTEORss4nNq2mDP3e7w4r1bFkSET5hy6s2TRlD2/mSxXtSHQhCunXo26NevCpmvD/UU6tuullzULH76q92zdZG/Ltv1a+W+osI/nRmyc+fRi1Xdbh+68+0vv10dH3+77KD/i6IdnX669/frn5Zsjh4/2PXju8+8bzc9/6fj27LFnX11/+IUnXWl7BJfegm79FyB9JOl3oHgSklefgxAC+FmFGpqHIYcCfkhgfCohSKKJVo044YUMttggiBkmp6KFXw1oII24oYhjiDByaKOOHcp3Y5BD/njikSkO+eBREQAAOw==", - "CLEAR_BACKGROUND_COLOR": "lightgray", - "NODE_TITLE_COLOR": "#222", - "NODE_SELECTED_TITLE_COLOR": "#000", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#444", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#F7F7F7", - "NODE_DEFAULT_BGCOLOR": "#F5F5F5", - "NODE_DEFAULT_BOXCOLOR": "#CCC", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#000", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.1)", - "DEFAULT_GROUP_FONT": 24, + LINK_COLOR: "#9A9", + EVENT_LINK_COLOR: "#A86", + CONNECTING_LINK_COLOR: "#AFA", + }, + comfy_base: { + "fg-color": "#fff", + "bg-color": "#202020", + "comfy-menu-bg": "#353535", + "comfy-input-bg": "#222", + "input-text": "#ddd", + "descrip-text": "#999", + "drag-text": "#ccc", + "error-text": "#ff4444", + "border-color": "#4e4e4e", + "tr-even-bg-color": "#222", + "tr-odd-bg-color": "#353535", + "content-bg": "#4e4e4e", + "content-fg": "#fff", + "content-hover-bg": "#222", + "content-hover-fg": "#fff", + }, + }, + }, + light: { + id: "light", + name: "Light", + colors: { + node_slot: { + CLIP: "#FFA726", // orange + CLIP_VISION: "#5C6BC0", // indigo + CLIP_VISION_OUTPUT: "#8D6E63", // brown + CONDITIONING: "#EF5350", // red + CONTROL_NET: "#66BB6A", // green + IMAGE: "#42A5F5", // blue + LATENT: "#AB47BC", // purple + MASK: "#9CCC65", // light green + MODEL: "#7E57C2", // deep purple + STYLE_MODEL: "#D4E157", // lime + VAE: "#FF7043", // deep orange + }, + litegraph_base: { + BACKGROUND_IMAGE: + "data:image/gif;base64,R0lGODlhZABkALMAAAAAAP///+vr6+rq6ujo6Ofn5+bm5uXl5d3d3f///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAABkAGQAAAT/UMhJq7046827HkcoHkYxjgZhnGG6si5LqnIM0/fL4qwwIMAg0CAsEovBIxKhRDaNy2GUOX0KfVFrssrNdpdaqTeKBX+dZ+jYvEaTf+y4W66mC8PUdrE879f9d2mBeoNLfH+IhYBbhIx2jkiHiomQlGKPl4uZe3CaeZifnnijgkESBqipqqusra6vsLGys62SlZO4t7qbuby7CLa+wqGWxL3Gv3jByMOkjc2lw8vOoNSi0czAncXW3Njdx9Pf48/Z4Kbbx+fQ5evZ4u3k1fKR6cn03vHlp7T9/v8A/8Gbp4+gwXoFryXMB2qgwoMMHyKEqA5fxX322FG8tzBcRnMW/zlulPbRncmQGidKjMjyYsOSKEF2FBlJQMCbOHP6c9iSZs+UnGYCdbnSo1CZI5F64kn0p1KnTH02nSoV3dGTV7FFHVqVq1dtWcMmVQZTbNGu72zqXMuW7danVL+6e4t1bEy6MeueBYLXrNO5Ze36jQtWsOG97wIj1vt3St/DjTEORss4nNq2mDP3e7w4r1bFkSET5hy6s2TRlD2/mSxXtSHQhCunXo26NevCpmvD/UU6tuullzULH76q92zdZG/Ltv1a+W+osI/nRmyc+fRi1Xdbh+68+0vv10dH3+77KD/i6IdnX669/frn5Zsjh4/2PXju8+8bzc9/6fj27LFnX11/+IUnXWl7BJfegm79FyB9JOl3oHgSklefgxAC+FmFGpqHIYcCfkhgfCohSKKJVo044YUMttggiBkmp6KFXw1oII24oYhjiDByaKOOHcp3Y5BD/njikSkO+eBREQAAOw==", + CLEAR_BACKGROUND_COLOR: "lightgray", + NODE_TITLE_COLOR: "#222", + NODE_SELECTED_TITLE_COLOR: "#000", + NODE_TEXT_SIZE: 14, + NODE_TEXT_COLOR: "#444", + NODE_SUBTEXT_SIZE: 12, + NODE_DEFAULT_COLOR: "#F7F7F7", + NODE_DEFAULT_BGCOLOR: "#F5F5F5", + NODE_DEFAULT_BOXCOLOR: "#CCC", + NODE_DEFAULT_SHAPE: "box", + NODE_BOX_OUTLINE_COLOR: "#000", + DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.1)", + DEFAULT_GROUP_FONT: 24, - "WIDGET_BGCOLOR": "#D4D4D4", - "WIDGET_OUTLINE_COLOR": "#999", - "WIDGET_TEXT_COLOR": "#222", - "WIDGET_SECONDARY_TEXT_COLOR": "#555", + WIDGET_BGCOLOR: "#D4D4D4", + WIDGET_OUTLINE_COLOR: "#999", + WIDGET_TEXT_COLOR: "#222", + WIDGET_SECONDARY_TEXT_COLOR: "#555", - "LINK_COLOR": "#4CAF50", - "EVENT_LINK_COLOR": "#FF9800", - "CONNECTING_LINK_COLOR": "#2196F3", - }, - "comfy_base": { - "fg-color": "#222", - "bg-color": "#DDD", - "comfy-menu-bg": "#F5F5F5", - "comfy-input-bg": "#C9C9C9", - "input-text": "#222", - "descrip-text": "#444", - "drag-text": "#555", - "error-text": "#F44336", - "border-color": "#888", - "tr-even-bg-color": "#f9f9f9", - "tr-odd-bg-color": "#fff", - "content-bg": "#e0e0e0", - "content-fg": "#222", - "content-hover-bg": "#adadad", - "content-hover-fg": "#222" - } - }, - }, - "solarized": { - "id": "solarized", - "name": "Solarized", - "colors": { - "node_slot": { - "CLIP": "#2AB7CA", // light blue - "CLIP_VISION": "#6c71c4", // blue violet - "CLIP_VISION_OUTPUT": "#859900", // olive green - "CONDITIONING": "#d33682", // magenta - "CONTROL_NET": "#d1ffd7", // light mint green - "IMAGE": "#5940bb", // deep blue violet - "LATENT": "#268bd2", // blue - "MASK": "#CCC9E7", // light purple-gray - "MODEL": "#dc322f", // red - "STYLE_MODEL": "#1a998a", // teal - "UPSCALE_MODEL": "#054A29", // dark green - "VAE": "#facfad", // light pink-orange - }, - "litegraph_base": { - "NODE_TITLE_COLOR": "#fdf6e3", // Base3 - "NODE_SELECTED_TITLE_COLOR": "#A9D400", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#657b83", // Base00 - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#094656", - "NODE_DEFAULT_BGCOLOR": "#073642", // Base02 - "NODE_DEFAULT_BOXCOLOR": "#839496", // Base0 - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#fdf6e3", // Base3 - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, + LINK_COLOR: "#4CAF50", + EVENT_LINK_COLOR: "#FF9800", + CONNECTING_LINK_COLOR: "#2196F3", + }, + comfy_base: { + "fg-color": "#222", + "bg-color": "#DDD", + "comfy-menu-bg": "#F5F5F5", + "comfy-input-bg": "#C9C9C9", + "input-text": "#222", + "descrip-text": "#444", + "drag-text": "#555", + "error-text": "#F44336", + "border-color": "#888", + "tr-even-bg-color": "#f9f9f9", + "tr-odd-bg-color": "#fff", + "content-bg": "#e0e0e0", + "content-fg": "#222", + "content-hover-bg": "#adadad", + "content-hover-fg": "#222", + }, + }, + }, + solarized: { + id: "solarized", + name: "Solarized", + colors: { + node_slot: { + CLIP: "#2AB7CA", // light blue + CLIP_VISION: "#6c71c4", // blue violet + CLIP_VISION_OUTPUT: "#859900", // olive green + CONDITIONING: "#d33682", // magenta + CONTROL_NET: "#d1ffd7", // light mint green + IMAGE: "#5940bb", // deep blue violet + LATENT: "#268bd2", // blue + MASK: "#CCC9E7", // light purple-gray + MODEL: "#dc322f", // red + STYLE_MODEL: "#1a998a", // teal + UPSCALE_MODEL: "#054A29", // dark green + VAE: "#facfad", // light pink-orange + }, + litegraph_base: { + NODE_TITLE_COLOR: "#fdf6e3", // Base3 + NODE_SELECTED_TITLE_COLOR: "#A9D400", + NODE_TEXT_SIZE: 14, + NODE_TEXT_COLOR: "#657b83", // Base00 + NODE_SUBTEXT_SIZE: 12, + NODE_DEFAULT_COLOR: "#094656", + NODE_DEFAULT_BGCOLOR: "#073642", // Base02 + NODE_DEFAULT_BOXCOLOR: "#839496", // Base0 + NODE_DEFAULT_SHAPE: "box", + NODE_BOX_OUTLINE_COLOR: "#fdf6e3", // Base3 + DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", + DEFAULT_GROUP_FONT: 24, - "WIDGET_BGCOLOR": "#002b36", // Base03 - "WIDGET_OUTLINE_COLOR": "#839496", // Base0 - "WIDGET_TEXT_COLOR": "#fdf6e3", // Base3 - "WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1", // Base1 + WIDGET_BGCOLOR: "#002b36", // Base03 + WIDGET_OUTLINE_COLOR: "#839496", // Base0 + WIDGET_TEXT_COLOR: "#fdf6e3", // Base3 + WIDGET_SECONDARY_TEXT_COLOR: "#93a1a1", // Base1 - "LINK_COLOR": "#2aa198", // Solarized Cyan - "EVENT_LINK_COLOR": "#268bd2", // Solarized Blue - "CONNECTING_LINK_COLOR": "#859900", // Solarized Green - }, - "comfy_base": { - "fg-color": "#fdf6e3", // Base3 - "bg-color": "#002b36", // Base03 - "comfy-menu-bg": "#073642", // Base02 - "comfy-input-bg": "#002b36", // Base03 - "input-text": "#93a1a1", // Base1 - "descrip-text": "#586e75", // Base01 - "drag-text": "#839496", // Base0 - "error-text": "#dc322f", // Solarized Red - "border-color": "#657b83", // Base00 - "tr-even-bg-color": "#002b36", - "tr-odd-bg-color": "#073642", - "content-bg": "#657b83", - "content-fg": "#fdf6e3", - "content-hover-bg": "#002b36", - "content-hover-fg": "#fdf6e3" - } - }, - }, - "arc": { - "id": "arc", - "name": "Arc", - "colors": { - "node_slot": { - "BOOLEAN": "", - "CLIP": "#eacb8b", - "CLIP_VISION": "#A8DADC", - "CLIP_VISION_OUTPUT": "#ad7452", - "CONDITIONING": "#cf876f", - "CONTROL_NET": "#00d78d", - "CONTROL_NET_WEIGHTS": "", - "FLOAT": "", - "GLIGEN": "", - "IMAGE": "#80a1c0", - "IMAGEUPLOAD": "", - "INT": "", - "LATENT": "#b38ead", - "LATENT_KEYFRAME": "", - "MASK": "#a3bd8d", - "MODEL": "#8978a7", - "SAMPLER": "", - "SIGMAS": "", - "STRING": "", - "STYLE_MODEL": "#C2FFAE", - "T2I_ADAPTER_WEIGHTS": "", - "TAESD": "#DCC274", - "TIMESTEP_KEYFRAME": "", - "UPSCALE_MODEL": "", - "VAE": "#be616b" - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABcklEQVR4nO3YMUoDARgF4RfxBqZI6/0vZqFn0MYtrLIQMFN8U6V4LAtD+Jm9XG/v30OGl2e/AP7yevz4+vx45nvgF/+QGITEICQGITEIiUFIjNNC3q43u3/YnRJyPOzeQ+0e220nhRzReC8e7R7bbdvl+Jal1Bs46jEIiUFIDEJiEBKDkBhKPbZT6qHdptRTu02p53DUYxASg5AYhMQgJAYhMZR6bKfUQ7tNqad2m1LP4ajHICQGITEIiUFIDEJiKPXYTqmHdptST+02pZ7DUY9BSAxCYhASg5AYhMRQ6rGdUg/tNqWe2m1KPYejHoOQGITEICQGITEIiaHUYzulHtptSj2125R6Dkc9BiExCIlBSAxCYhASQ6nHdko9tNuUemq3KfUcjnoMQmIQEoOQGITEICSGUo/tlHpotyn11G5T6jkc9RiExCAkBiExCIlBSAylHtsp9dBuU+qp3abUczjqMQiJQUgMQmIQEoOQGITE+AHFISNQrFTGuwAAAABJRU5ErkJggg==", - "CLEAR_BACKGROUND_COLOR": "#2b2f38", - "NODE_TITLE_COLOR": "#b2b7bd", - "NODE_SELECTED_TITLE_COLOR": "#FFF", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#AAA", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#2b2f38", - "NODE_DEFAULT_BGCOLOR": "#242730", - "NODE_DEFAULT_BOXCOLOR": "#6e7581", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#FFF", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 22, - "WIDGET_BGCOLOR": "#2b2f38", - "WIDGET_OUTLINE_COLOR": "#6e7581", - "WIDGET_TEXT_COLOR": "#DDD", - "WIDGET_SECONDARY_TEXT_COLOR": "#b2b7bd", - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA" - }, - "comfy_base": { - "fg-color": "#fff", - "bg-color": "#2b2f38", - "comfy-menu-bg": "#242730", - "comfy-input-bg": "#2b2f38", - "input-text": "#ddd", - "descrip-text": "#b2b7bd", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#6e7581", - "tr-even-bg-color": "#2b2f38", - "tr-odd-bg-color": "#242730", - "content-bg": "#6e7581", - "content-fg": "#fff", - "content-hover-bg": "#2b2f38", - "content-hover-fg": "#fff" - } - }, - }, - "nord": { - "id": "nord", - "name": "Nord", - "colors": { - "node_slot": { - "BOOLEAN": "", - "CLIP": "#eacb8b", - "CLIP_VISION": "#A8DADC", - "CLIP_VISION_OUTPUT": "#ad7452", - "CONDITIONING": "#cf876f", - "CONTROL_NET": "#00d78d", - "CONTROL_NET_WEIGHTS": "", - "FLOAT": "", - "GLIGEN": "", - "IMAGE": "#80a1c0", - "IMAGEUPLOAD": "", - "INT": "", - "LATENT": "#b38ead", - "LATENT_KEYFRAME": "", - "MASK": "#a3bd8d", - "MODEL": "#8978a7", - "SAMPLER": "", - "SIGMAS": "", - "STRING": "", - "STYLE_MODEL": "#C2FFAE", - "T2I_ADAPTER_WEIGHTS": "", - "TAESD": "#DCC274", - "TIMESTEP_KEYFRAME": "", - "UPSCALE_MODEL": "", - "VAE": "#be616b" - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFu2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUwNDFhMmZjLTEzNzQtMTk0ZC1hZWY4LTYxMzM1MTVmNjUwMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MDQxYTJmYy0xMzc0LTE5NGQtYWVmOC02MTMzNTE1ZjY1MDAiIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz73jWg/AAAAyUlEQVR42u3WKwoAIBRFQRdiMb1idv9Lsxn9gEFw4Dbb8JCTojbbXEJwjJVL2HKwYMGCBQuWLbDmjr+9zrBGjHl1WVcvy2DBggULFizTWQpewSt4HzwsgwULFiwFr7MUvMtS8D54WLBgGSxYCl7BK3iXZbBgwYIFC5bpLAWv4BW8Dx6WwYIFC5aC11kK3mUpeB88LFiwDBYsBa/gFbzLMliwYMGCBct0loJX8AreBw/LYMGCBUvB6ywF77IUvA8eFixYBgsWrNfWAZPltufdad+1AAAAAElFTkSuQmCC", - "CLEAR_BACKGROUND_COLOR": "#212732", - "NODE_TITLE_COLOR": "#999", - "NODE_SELECTED_TITLE_COLOR": "#e5eaf0", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#bcc2c8", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#2e3440", - "NODE_DEFAULT_BGCOLOR": "#161b22", - "NODE_DEFAULT_BOXCOLOR": "#545d70", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#e5eaf0", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, - "WIDGET_BGCOLOR": "#2e3440", - "WIDGET_OUTLINE_COLOR": "#545d70", - "WIDGET_TEXT_COLOR": "#bcc2c8", - "WIDGET_SECONDARY_TEXT_COLOR": "#999", - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA" - }, - "comfy_base": { - "fg-color": "#e5eaf0", - "bg-color": "#2e3440", - "comfy-menu-bg": "#161b22", - "comfy-input-bg": "#2e3440", - "input-text": "#bcc2c8", - "descrip-text": "#999", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#545d70", - "tr-even-bg-color": "#2e3440", - "tr-odd-bg-color": "#161b22", - "content-bg": "#545d70", - "content-fg": "#e5eaf0", - "content-hover-bg": "#2e3440", - "content-hover-fg": "#e5eaf0", - } - }, - }, - "github": { - "id": "github", - "name": "Github", - "colors": { - "node_slot": { - "BOOLEAN": "", - "CLIP": "#eacb8b", - "CLIP_VISION": "#A8DADC", - "CLIP_VISION_OUTPUT": "#ad7452", - "CONDITIONING": "#cf876f", - "CONTROL_NET": "#00d78d", - "CONTROL_NET_WEIGHTS": "", - "FLOAT": "", - "GLIGEN": "", - "IMAGE": "#80a1c0", - "IMAGEUPLOAD": "", - "INT": "", - "LATENT": "#b38ead", - "LATENT_KEYFRAME": "", - "MASK": "#a3bd8d", - "MODEL": "#8978a7", - "SAMPLER": "", - "SIGMAS": "", - "STRING": "", - "STYLE_MODEL": "#C2FFAE", - "T2I_ADAPTER_WEIGHTS": "", - "TAESD": "#DCC274", - "TIMESTEP_KEYFRAME": "", - "UPSCALE_MODEL": "", - "VAE": "#be616b" - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==", - "CLEAR_BACKGROUND_COLOR": "#040506", - "NODE_TITLE_COLOR": "#999", - "NODE_SELECTED_TITLE_COLOR": "#e5eaf0", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#bcc2c8", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#161b22", - "NODE_DEFAULT_BGCOLOR": "#13171d", - "NODE_DEFAULT_BOXCOLOR": "#30363d", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#e5eaf0", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, - "WIDGET_BGCOLOR": "#161b22", - "WIDGET_OUTLINE_COLOR": "#30363d", - "WIDGET_TEXT_COLOR": "#bcc2c8", - "WIDGET_SECONDARY_TEXT_COLOR": "#999", - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA" - }, - "comfy_base": { - "fg-color": "#e5eaf0", - "bg-color": "#161b22", - "comfy-menu-bg": "#13171d", - "comfy-input-bg": "#161b22", - "input-text": "#bcc2c8", - "descrip-text": "#999", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#30363d", - "tr-even-bg-color": "#161b22", - "tr-odd-bg-color": "#13171d", - "content-bg": "#30363d", - "content-fg": "#e5eaf0", - "content-hover-bg": "#161b22", - "content-hover-fg": "#e5eaf0", - } - }, - } + LINK_COLOR: "#2aa198", // Solarized Cyan + EVENT_LINK_COLOR: "#268bd2", // Solarized Blue + CONNECTING_LINK_COLOR: "#859900", // Solarized Green + }, + comfy_base: { + "fg-color": "#fdf6e3", // Base3 + "bg-color": "#002b36", // Base03 + "comfy-menu-bg": "#073642", // Base02 + "comfy-input-bg": "#002b36", // Base03 + "input-text": "#93a1a1", // Base1 + "descrip-text": "#586e75", // Base01 + "drag-text": "#839496", // Base0 + "error-text": "#dc322f", // Solarized Red + "border-color": "#657b83", // Base00 + "tr-even-bg-color": "#002b36", + "tr-odd-bg-color": "#073642", + "content-bg": "#657b83", + "content-fg": "#fdf6e3", + "content-hover-bg": "#002b36", + "content-hover-fg": "#fdf6e3", + }, + }, + }, + arc: { + id: "arc", + name: "Arc", + colors: { + node_slot: { + BOOLEAN: "", + CLIP: "#eacb8b", + CLIP_VISION: "#A8DADC", + CLIP_VISION_OUTPUT: "#ad7452", + CONDITIONING: "#cf876f", + CONTROL_NET: "#00d78d", + CONTROL_NET_WEIGHTS: "", + FLOAT: "", + GLIGEN: "", + IMAGE: "#80a1c0", + IMAGEUPLOAD: "", + INT: "", + LATENT: "#b38ead", + LATENT_KEYFRAME: "", + MASK: "#a3bd8d", + MODEL: "#8978a7", + SAMPLER: "", + SIGMAS: "", + STRING: "", + STYLE_MODEL: "#C2FFAE", + T2I_ADAPTER_WEIGHTS: "", + TAESD: "#DCC274", + TIMESTEP_KEYFRAME: "", + UPSCALE_MODEL: "", + VAE: "#be616b", + }, + litegraph_base: { + BACKGROUND_IMAGE: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABcklEQVR4nO3YMUoDARgF4RfxBqZI6/0vZqFn0MYtrLIQMFN8U6V4LAtD+Jm9XG/v30OGl2e/AP7yevz4+vx45nvgF/+QGITEICQGITEIiUFIjNNC3q43u3/YnRJyPOzeQ+0e220nhRzReC8e7R7bbdvl+Jal1Bs46jEIiUFIDEJiEBKDkBhKPbZT6qHdptRTu02p53DUYxASg5AYhMQgJAYhMZR6bKfUQ7tNqad2m1LP4ajHICQGITEIiUFIDEJiKPXYTqmHdptST+02pZ7DUY9BSAxCYhASg5AYhMRQ6rGdUg/tNqWe2m1KPYejHoOQGITEICQGITEIiaHUYzulHtptSj2125R6Dkc9BiExCIlBSAxCYhASQ6nHdko9tNuUemq3KfUcjnoMQmIQEoOQGITEICSGUo/tlHpotyn11G5T6jkc9RiExCAkBiExCIlBSAylHtsp9dBuU+qp3abUczjqMQiJQUgMQmIQEoOQGITE+AHFISNQrFTGuwAAAABJRU5ErkJggg==", + CLEAR_BACKGROUND_COLOR: "#2b2f38", + NODE_TITLE_COLOR: "#b2b7bd", + NODE_SELECTED_TITLE_COLOR: "#FFF", + NODE_TEXT_SIZE: 14, + NODE_TEXT_COLOR: "#AAA", + NODE_SUBTEXT_SIZE: 12, + NODE_DEFAULT_COLOR: "#2b2f38", + NODE_DEFAULT_BGCOLOR: "#242730", + NODE_DEFAULT_BOXCOLOR: "#6e7581", + NODE_DEFAULT_SHAPE: "box", + NODE_BOX_OUTLINE_COLOR: "#FFF", + DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", + DEFAULT_GROUP_FONT: 22, + WIDGET_BGCOLOR: "#2b2f38", + WIDGET_OUTLINE_COLOR: "#6e7581", + WIDGET_TEXT_COLOR: "#DDD", + WIDGET_SECONDARY_TEXT_COLOR: "#b2b7bd", + LINK_COLOR: "#9A9", + EVENT_LINK_COLOR: "#A86", + CONNECTING_LINK_COLOR: "#AFA", + }, + comfy_base: { + "fg-color": "#fff", + "bg-color": "#2b2f38", + "comfy-menu-bg": "#242730", + "comfy-input-bg": "#2b2f38", + "input-text": "#ddd", + "descrip-text": "#b2b7bd", + "drag-text": "#ccc", + "error-text": "#ff4444", + "border-color": "#6e7581", + "tr-even-bg-color": "#2b2f38", + "tr-odd-bg-color": "#242730", + "content-bg": "#6e7581", + "content-fg": "#fff", + "content-hover-bg": "#2b2f38", + "content-hover-fg": "#fff", + }, + }, + }, + nord: { + id: "nord", + name: "Nord", + colors: { + node_slot: { + BOOLEAN: "", + CLIP: "#eacb8b", + CLIP_VISION: "#A8DADC", + CLIP_VISION_OUTPUT: "#ad7452", + CONDITIONING: "#cf876f", + CONTROL_NET: "#00d78d", + CONTROL_NET_WEIGHTS: "", + FLOAT: "", + GLIGEN: "", + IMAGE: "#80a1c0", + IMAGEUPLOAD: "", + INT: "", + LATENT: "#b38ead", + LATENT_KEYFRAME: "", + MASK: "#a3bd8d", + MODEL: "#8978a7", + SAMPLER: "", + SIGMAS: "", + STRING: "", + STYLE_MODEL: "#C2FFAE", + T2I_ADAPTER_WEIGHTS: "", + TAESD: "#DCC274", + TIMESTEP_KEYFRAME: "", + UPSCALE_MODEL: "", + VAE: "#be616b", + }, + litegraph_base: { + BACKGROUND_IMAGE: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFu2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUwNDFhMmZjLTEzNzQtMTk0ZC1hZWY4LTYxMzM1MTVmNjUwMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MDQxYTJmYy0xMzc0LTE5NGQtYWVmOC02MTMzNTE1ZjY1MDAiIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz73jWg/AAAAyUlEQVR42u3WKwoAIBRFQRdiMb1idv9Lsxn9gEFw4Dbb8JCTojbbXEJwjJVL2HKwYMGCBQuWLbDmjr+9zrBGjHl1WVcvy2DBggULFizTWQpewSt4HzwsgwULFiwFr7MUvMtS8D54WLBgGSxYCl7BK3iXZbBgwYIFC5bpLAWv4BW8Dx6WwYIFC5aC11kK3mUpeB88LFiwDBYsBa/gFbzLMliwYMGCBct0loJX8AreBw/LYMGCBUvB6ywF77IUvA8eFixYBgsWrNfWAZPltufdad+1AAAAAElFTkSuQmCC", + CLEAR_BACKGROUND_COLOR: "#212732", + NODE_TITLE_COLOR: "#999", + NODE_SELECTED_TITLE_COLOR: "#e5eaf0", + NODE_TEXT_SIZE: 14, + NODE_TEXT_COLOR: "#bcc2c8", + NODE_SUBTEXT_SIZE: 12, + NODE_DEFAULT_COLOR: "#2e3440", + NODE_DEFAULT_BGCOLOR: "#161b22", + NODE_DEFAULT_BOXCOLOR: "#545d70", + NODE_DEFAULT_SHAPE: "box", + NODE_BOX_OUTLINE_COLOR: "#e5eaf0", + DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", + DEFAULT_GROUP_FONT: 24, + WIDGET_BGCOLOR: "#2e3440", + WIDGET_OUTLINE_COLOR: "#545d70", + WIDGET_TEXT_COLOR: "#bcc2c8", + WIDGET_SECONDARY_TEXT_COLOR: "#999", + LINK_COLOR: "#9A9", + EVENT_LINK_COLOR: "#A86", + CONNECTING_LINK_COLOR: "#AFA", + }, + comfy_base: { + "fg-color": "#e5eaf0", + "bg-color": "#2e3440", + "comfy-menu-bg": "#161b22", + "comfy-input-bg": "#2e3440", + "input-text": "#bcc2c8", + "descrip-text": "#999", + "drag-text": "#ccc", + "error-text": "#ff4444", + "border-color": "#545d70", + "tr-even-bg-color": "#2e3440", + "tr-odd-bg-color": "#161b22", + "content-bg": "#545d70", + "content-fg": "#e5eaf0", + "content-hover-bg": "#2e3440", + "content-hover-fg": "#e5eaf0", + }, + }, + }, + github: { + id: "github", + name: "Github", + colors: { + node_slot: { + BOOLEAN: "", + CLIP: "#eacb8b", + CLIP_VISION: "#A8DADC", + CLIP_VISION_OUTPUT: "#ad7452", + CONDITIONING: "#cf876f", + CONTROL_NET: "#00d78d", + CONTROL_NET_WEIGHTS: "", + FLOAT: "", + GLIGEN: "", + IMAGE: "#80a1c0", + IMAGEUPLOAD: "", + INT: "", + LATENT: "#b38ead", + LATENT_KEYFRAME: "", + MASK: "#a3bd8d", + MODEL: "#8978a7", + SAMPLER: "", + SIGMAS: "", + STRING: "", + STYLE_MODEL: "#C2FFAE", + T2I_ADAPTER_WEIGHTS: "", + TAESD: "#DCC274", + TIMESTEP_KEYFRAME: "", + UPSCALE_MODEL: "", + VAE: "#be616b", + }, + litegraph_base: { + BACKGROUND_IMAGE: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==", + CLEAR_BACKGROUND_COLOR: "#040506", + NODE_TITLE_COLOR: "#999", + NODE_SELECTED_TITLE_COLOR: "#e5eaf0", + NODE_TEXT_SIZE: 14, + NODE_TEXT_COLOR: "#bcc2c8", + NODE_SUBTEXT_SIZE: 12, + NODE_DEFAULT_COLOR: "#161b22", + NODE_DEFAULT_BGCOLOR: "#13171d", + NODE_DEFAULT_BOXCOLOR: "#30363d", + NODE_DEFAULT_SHAPE: "box", + NODE_BOX_OUTLINE_COLOR: "#e5eaf0", + DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", + DEFAULT_GROUP_FONT: 24, + WIDGET_BGCOLOR: "#161b22", + WIDGET_OUTLINE_COLOR: "#30363d", + WIDGET_TEXT_COLOR: "#bcc2c8", + WIDGET_SECONDARY_TEXT_COLOR: "#999", + LINK_COLOR: "#9A9", + EVENT_LINK_COLOR: "#A86", + CONNECTING_LINK_COLOR: "#AFA", + }, + comfy_base: { + "fg-color": "#e5eaf0", + "bg-color": "#161b22", + "comfy-menu-bg": "#13171d", + "comfy-input-bg": "#161b22", + "input-text": "#bcc2c8", + "descrip-text": "#999", + "drag-text": "#ccc", + "error-text": "#ff4444", + "border-color": "#30363d", + "tr-even-bg-color": "#161b22", + "tr-odd-bg-color": "#13171d", + "content-bg": "#30363d", + "content-fg": "#e5eaf0", + "content-hover-bg": "#161b22", + "content-hover-fg": "#e5eaf0", + }, + }, + }, }; const id = "Comfy.ColorPalette"; const idCustomColorPalettes = "Comfy.CustomColorPalettes"; const defaultColorPaletteId = "dark"; const els: { select: HTMLSelectElement | null } = { - select: null, -} + select: null, +}; // const ctxMenu = LiteGraph.ContextMenu; app.registerExtension({ - name: id, - addCustomNodeDefs(node_defs) { - const sortObjectKeys = (unordered) => { - return Object.keys(unordered).sort().reduce((obj, key) => { - obj[key] = unordered[key]; - return obj; - }, {}); - }; + name: id, + addCustomNodeDefs(node_defs) { + const sortObjectKeys = (unordered) => { + return Object.keys(unordered) + .sort() + .reduce((obj, key) => { + obj[key] = unordered[key]; + return obj; + }, {}); + }; - function getSlotTypes() { - var types = []; + function getSlotTypes() { + var types = []; - const defs = node_defs; - for (const nodeId in defs) { - const nodeData = defs[nodeId]; + const defs = node_defs; + for (const nodeId in defs) { + const nodeData = defs[nodeId]; - var inputs = nodeData["input"]["required"]; - if (nodeData["input"]["optional"] !== undefined) { - inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]) - } + var inputs = nodeData["input"]["required"]; + if (nodeData["input"]["optional"] !== undefined) { + inputs = Object.assign( + {}, + nodeData["input"]["required"], + nodeData["input"]["optional"] + ); + } - for (const inputName in inputs) { - const inputData = inputs[inputName]; - const type = inputData[0]; + for (const inputName in inputs) { + const inputData = inputs[inputName]; + const type = inputData[0]; - if (!Array.isArray(type)) { - types.push(type); - } - } + if (!Array.isArray(type)) { + types.push(type); + } + } - for (const o in nodeData["output"]) { - const output = nodeData["output"][o]; - types.push(output); - } - } + for (const o in nodeData["output"]) { + const output = nodeData["output"][o]; + types.push(output); + } + } - return types; - } + return types; + } - function completeColorPalette(colorPalette) { - var types = getSlotTypes(); + function completeColorPalette(colorPalette) { + var types = getSlotTypes(); - for (const type of types) { - if (!colorPalette.colors.node_slot[type]) { - colorPalette.colors.node_slot[type] = ""; - } - } + for (const type of types) { + if (!colorPalette.colors.node_slot[type]) { + colorPalette.colors.node_slot[type] = ""; + } + } - colorPalette.colors.node_slot = sortObjectKeys(colorPalette.colors.node_slot); + colorPalette.colors.node_slot = sortObjectKeys( + colorPalette.colors.node_slot + ); - return colorPalette; - } + return colorPalette; + } - const getColorPaletteTemplate = async () => { - let colorPalette = { - "id": "my_color_palette_unique_id", - "name": "My Color Palette", - "colors": { - "node_slot": {}, - "litegraph_base": {}, - "comfy_base": {} - } - }; + const getColorPaletteTemplate = async () => { + let colorPalette = { + id: "my_color_palette_unique_id", + name: "My Color Palette", + colors: { + node_slot: {}, + litegraph_base: {}, + comfy_base: {}, + }, + }; - // Copy over missing keys from default color palette - const defaultColorPalette = colorPalettes[defaultColorPaletteId]; - for (const key in defaultColorPalette.colors.litegraph_base) { - if (!colorPalette.colors.litegraph_base[key]) { - colorPalette.colors.litegraph_base[key] = ""; - } - } - for (const key in defaultColorPalette.colors.comfy_base) { - if (!colorPalette.colors.comfy_base[key]) { - colorPalette.colors.comfy_base[key] = ""; - } - } + // Copy over missing keys from default color palette + const defaultColorPalette = colorPalettes[defaultColorPaletteId]; + for (const key in defaultColorPalette.colors.litegraph_base) { + if (!colorPalette.colors.litegraph_base[key]) { + colorPalette.colors.litegraph_base[key] = ""; + } + } + for (const key in defaultColorPalette.colors.comfy_base) { + if (!colorPalette.colors.comfy_base[key]) { + colorPalette.colors.comfy_base[key] = ""; + } + } - return completeColorPalette(colorPalette); - }; + return completeColorPalette(colorPalette); + }; - const getCustomColorPalettes = (): ColorPalettes => { - return app.ui.settings.getSettingValue(idCustomColorPalettes, {}); - }; + const getCustomColorPalettes = (): ColorPalettes => { + return app.ui.settings.getSettingValue(idCustomColorPalettes, {}); + }; - const setCustomColorPalettes = (customColorPalettes: ColorPalettes) => { - return app.ui.settings.setSettingValue(idCustomColorPalettes, customColorPalettes); - }; + const setCustomColorPalettes = (customColorPalettes: ColorPalettes) => { + return app.ui.settings.setSettingValue( + idCustomColorPalettes, + customColorPalettes + ); + }; - const addCustomColorPalette = async (colorPalette) => { - if (typeof (colorPalette) !== "object") { - alert("Invalid color palette."); - return; - } + const addCustomColorPalette = async (colorPalette) => { + if (typeof colorPalette !== "object") { + alert("Invalid color palette."); + return; + } - if (!colorPalette.id) { - alert("Color palette missing id."); - return; - } + if (!colorPalette.id) { + alert("Color palette missing id."); + return; + } - if (!colorPalette.name) { - alert("Color palette missing name."); - return; - } + if (!colorPalette.name) { + alert("Color palette missing name."); + return; + } - if (!colorPalette.colors) { - alert("Color palette missing colors."); - return; - } + if (!colorPalette.colors) { + alert("Color palette missing colors."); + return; + } - if (colorPalette.colors.node_slot && typeof (colorPalette.colors.node_slot) !== "object") { - alert("Invalid color palette colors.node_slot."); - return; - } + if ( + colorPalette.colors.node_slot && + typeof colorPalette.colors.node_slot !== "object" + ) { + alert("Invalid color palette colors.node_slot."); + return; + } - const customColorPalettes = getCustomColorPalettes(); - customColorPalettes[colorPalette.id] = colorPalette; - setCustomColorPalettes(customColorPalettes); + const customColorPalettes = getCustomColorPalettes(); + customColorPalettes[colorPalette.id] = colorPalette; + setCustomColorPalettes(customColorPalettes); - for (const option of els.select.childNodes) { - if ((option as HTMLOptionElement).value === "custom_" + colorPalette.id) { - els.select.removeChild(option); - } - } + for (const option of els.select.childNodes) { + if ( + (option as HTMLOptionElement).value === + "custom_" + colorPalette.id + ) { + els.select.removeChild(option); + } + } - els.select.append($el("option", { - textContent: colorPalette.name + " (custom)", - value: "custom_" + colorPalette.id, - selected: true - })); + els.select.append( + $el("option", { + textContent: colorPalette.name + " (custom)", + value: "custom_" + colorPalette.id, + selected: true, + }) + ); - setColorPalette("custom_" + colorPalette.id); - await loadColorPalette(colorPalette); - }; + setColorPalette("custom_" + colorPalette.id); + await loadColorPalette(colorPalette); + }; - const deleteCustomColorPalette = async (colorPaletteId) => { - const customColorPalettes = getCustomColorPalettes(); - delete customColorPalettes[colorPaletteId]; - setCustomColorPalettes(customColorPalettes); + const deleteCustomColorPalette = async (colorPaletteId) => { + const customColorPalettes = getCustomColorPalettes(); + delete customColorPalettes[colorPaletteId]; + setCustomColorPalettes(customColorPalettes); - for (const opt of els.select.childNodes) { - const option = opt as HTMLOptionElement; - if (option.value === defaultColorPaletteId) { - option.selected = true; - } + for (const opt of els.select.childNodes) { + const option = opt as HTMLOptionElement; + if (option.value === defaultColorPaletteId) { + option.selected = true; + } - if (option.value === "custom_" + colorPaletteId) { - els.select.removeChild(option); - } - } + if (option.value === "custom_" + colorPaletteId) { + els.select.removeChild(option); + } + } - setColorPalette(defaultColorPaletteId); - await loadColorPalette(getColorPalette()); - }; + setColorPalette(defaultColorPaletteId); + await loadColorPalette(getColorPalette()); + }; - const loadColorPalette = async (colorPalette) => { - colorPalette = await completeColorPalette(colorPalette); - if (colorPalette.colors) { - // Sets the colors of node slots and links - if (colorPalette.colors.node_slot) { - // @ts-ignore - Object.assign(app.canvas.default_connection_color_byType, colorPalette.colors.node_slot); - // @ts-ignore - Object.assign(LGraphCanvas.link_type_colors, colorPalette.colors.node_slot); - } - // Sets the colors of the LiteGraph objects - if (colorPalette.colors.litegraph_base) { - // Everything updates correctly in the loop, except the Node Title and Link Color for some reason - app.canvas.node_title_color = colorPalette.colors.litegraph_base.NODE_TITLE_COLOR; - app.canvas.default_link_color = colorPalette.colors.litegraph_base.LINK_COLOR; + const loadColorPalette = async (colorPalette) => { + colorPalette = await completeColorPalette(colorPalette); + if (colorPalette.colors) { + // Sets the colors of node slots and links + if (colorPalette.colors.node_slot) { + Object.assign( + // @ts-ignore + app.canvas.default_connection_color_byType, + colorPalette.colors.node_slot + ); + Object.assign( + // @ts-ignore + LGraphCanvas.link_type_colors, + colorPalette.colors.node_slot + ); + } + // Sets the colors of the LiteGraph objects + if (colorPalette.colors.litegraph_base) { + // Everything updates correctly in the loop, except the Node Title and Link Color for some reason + app.canvas.node_title_color = + colorPalette.colors.litegraph_base.NODE_TITLE_COLOR; + app.canvas.default_link_color = + colorPalette.colors.litegraph_base.LINK_COLOR; - for (const key in colorPalette.colors.litegraph_base) { - if (colorPalette.colors.litegraph_base.hasOwnProperty(key) && LiteGraph.hasOwnProperty(key)) { - LiteGraph[key] = colorPalette.colors.litegraph_base[key]; - } - } - } - // Sets the color of ComfyUI elements - if (colorPalette.colors.comfy_base) { - const rootStyle = document.documentElement.style; - for (const key in colorPalette.colors.comfy_base) { - rootStyle.setProperty('--' + key, colorPalette.colors.comfy_base[key]); - } - } - app.canvas.draw(true, true); - } - }; + for (const key in colorPalette.colors.litegraph_base) { + if ( + colorPalette.colors.litegraph_base.hasOwnProperty(key) && + LiteGraph.hasOwnProperty(key) + ) { + LiteGraph[key] = colorPalette.colors.litegraph_base[key]; + } + } + } + // Sets the color of ComfyUI elements + if (colorPalette.colors.comfy_base) { + const rootStyle = document.documentElement.style; + for (const key in colorPalette.colors.comfy_base) { + rootStyle.setProperty( + "--" + key, + colorPalette.colors.comfy_base[key] + ); + } + } + app.canvas.draw(true, true); + } + }; - const getColorPalette = (colorPaletteId?) => { - if (!colorPaletteId) { - colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); - } + const getColorPalette = (colorPaletteId?) => { + if (!colorPaletteId) { + colorPaletteId = app.ui.settings.getSettingValue( + id, + defaultColorPaletteId + ); + } - if (colorPaletteId.startsWith("custom_")) { - colorPaletteId = colorPaletteId.substr(7); - let customColorPalettes = getCustomColorPalettes(); - if (customColorPalettes[colorPaletteId]) { - return customColorPalettes[colorPaletteId]; - } - } + if (colorPaletteId.startsWith("custom_")) { + colorPaletteId = colorPaletteId.substr(7); + let customColorPalettes = getCustomColorPalettes(); + if (customColorPalettes[colorPaletteId]) { + return customColorPalettes[colorPaletteId]; + } + } - return colorPalettes[colorPaletteId]; - }; + return colorPalettes[colorPaletteId]; + }; - const setColorPalette = (colorPaletteId) => { - app.ui.settings.setSettingValue(id, colorPaletteId); - }; + const setColorPalette = (colorPaletteId) => { + app.ui.settings.setSettingValue(id, colorPaletteId); + }; - const fileInput = $el("input", { - type: "file", - accept: ".json", - style: { display: "none" }, - parent: document.body, - onchange: () => { - const file = fileInput.files[0]; - if (file.type === "application/json" || file.name.endsWith(".json")) { - const reader = new FileReader(); - reader.onload = async () => { - await addCustomColorPalette(JSON.parse(reader.result as string)); - }; - reader.readAsText(file); - } - }, - }) as HTMLInputElement; + const fileInput = $el("input", { + type: "file", + accept: ".json", + style: { display: "none" }, + parent: document.body, + onchange: () => { + const file = fileInput.files[0]; + if (file.type === "application/json" || file.name.endsWith(".json")) { + const reader = new FileReader(); + reader.onload = async () => { + await addCustomColorPalette(JSON.parse(reader.result as string)); + }; + reader.readAsText(file); + } + }, + }) as HTMLInputElement; - app.ui.settings.addSetting({ - id, - name: "Color Palette", - type: (name, setter, value) => { - const options = [ - ...Object.values(colorPalettes).map(c => $el("option", { - textContent: c.name, - value: c.id, - selected: c.id === value - })), - ...Object.values(getCustomColorPalettes()).map(c => $el("option", { - textContent: `${c.name} (custom)`, - value: `custom_${c.id}`, - selected: `custom_${c.id}` === value - })), - ]; + app.ui.settings.addSetting({ + id, + name: "Color Palette", + type: (name, setter, value) => { + const options = [ + ...Object.values(colorPalettes).map((c) => + $el("option", { + textContent: c.name, + value: c.id, + selected: c.id === value, + }) + ), + ...Object.values(getCustomColorPalettes()).map((c) => + $el("option", { + textContent: `${c.name} (custom)`, + value: `custom_${c.id}`, + selected: `custom_${c.id}` === value, + }) + ), + ]; - els.select = $el("select", { - style: { - marginBottom: "0.15rem", - width: "100%", - }, - onchange: (e) => { - setter(e.target.value); - } - }, options) as HTMLSelectElement; + els.select = $el( + "select", + { + style: { + marginBottom: "0.15rem", + width: "100%", + }, + onchange: (e) => { + setter(e.target.value); + }, + }, + options + ) as HTMLSelectElement; - return $el("tr", [ - $el("td", [ - $el("label", { - for: id.replaceAll(".", "-"), - textContent: "Color palette", - }), - ]), - $el("td", [ - els.select, - $el("div", { - style: { - display: "grid", - gap: "4px", - gridAutoFlow: "column", - }, - }, [ - $el("input", { - type: "button", - value: "Export", - onclick: async () => { - const colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); - const colorPalette = await completeColorPalette(getColorPalette(colorPaletteId)); - const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string - const blob = new Blob([json], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: colorPaletteId + ".json", - style: { display: "none" }, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - }, - }), - $el("input", { - type: "button", - value: "Import", - onclick: () => { - fileInput.click(); - } - }), - $el("input", { - type: "button", - value: "Template", - onclick: async () => { - const colorPalette = await getColorPaletteTemplate(); - const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string - const blob = new Blob([json], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: "color_palette.json", - style: { display: "none" }, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - } - }), - $el("input", { - type: "button", - value: "Delete", - onclick: async () => { - let colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); + return $el("tr", [ + $el("td", [ + $el("label", { + for: id.replaceAll(".", "-"), + textContent: "Color palette", + }), + ]), + $el("td", [ + els.select, + $el( + "div", + { + style: { + display: "grid", + gap: "4px", + gridAutoFlow: "column", + }, + }, + [ + $el("input", { + type: "button", + value: "Export", + onclick: async () => { + const colorPaletteId = app.ui.settings.getSettingValue( + id, + defaultColorPaletteId + ); + const colorPalette = await completeColorPalette( + getColorPalette(colorPaletteId) + ); + const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = $el("a", { + href: url, + download: colorPaletteId + ".json", + style: { display: "none" }, + parent: document.body, + }); + a.click(); + setTimeout(function () { + a.remove(); + window.URL.revokeObjectURL(url); + }, 0); + }, + }), + $el("input", { + type: "button", + value: "Import", + onclick: () => { + fileInput.click(); + }, + }), + $el("input", { + type: "button", + value: "Template", + onclick: async () => { + const colorPalette = await getColorPaletteTemplate(); + const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = $el("a", { + href: url, + download: "color_palette.json", + style: { display: "none" }, + parent: document.body, + }); + a.click(); + setTimeout(function () { + a.remove(); + window.URL.revokeObjectURL(url); + }, 0); + }, + }), + $el("input", { + type: "button", + value: "Delete", + onclick: async () => { + let colorPaletteId = app.ui.settings.getSettingValue( + id, + defaultColorPaletteId + ); - if (colorPalettes[colorPaletteId]) { - alert("You cannot delete a built-in color palette."); - return; - } + if (colorPalettes[colorPaletteId]) { + alert("You cannot delete a built-in color palette."); + return; + } - if (colorPaletteId.startsWith("custom_")) { - colorPaletteId = colorPaletteId.substr(7); - } + if (colorPaletteId.startsWith("custom_")) { + colorPaletteId = colorPaletteId.substr(7); + } - await deleteCustomColorPalette(colorPaletteId); - } - }), - ]), - ]), - ]) - }, - defaultValue: defaultColorPaletteId, - async onChange(value) { - if (!value) { - return; - } + await deleteCustomColorPalette(colorPaletteId); + }, + }), + ] + ), + ]), + ]); + }, + defaultValue: defaultColorPaletteId, + async onChange(value) { + if (!value) { + return; + } - let palette = colorPalettes[value]; - if (palette) { - await loadColorPalette(palette); - } else if (value.startsWith("custom_")) { - value = value.substr(7); - let customColorPalettes = getCustomColorPalettes(); - if (customColorPalettes[value]) { - palette = customColorPalettes[value]; - await loadColorPalette(customColorPalettes[value]); - } - } + let palette = colorPalettes[value]; + if (palette) { + await loadColorPalette(palette); + } else if (value.startsWith("custom_")) { + value = value.substr(7); + let customColorPalettes = getCustomColorPalettes(); + if (customColorPalettes[value]) { + palette = customColorPalettes[value]; + await loadColorPalette(customColorPalettes[value]); + } + } - let { BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR } = palette.colors.litegraph_base; - if (BACKGROUND_IMAGE === undefined || CLEAR_BACKGROUND_COLOR === undefined) { - const base = colorPalettes["dark"].colors.litegraph_base; - BACKGROUND_IMAGE = base.BACKGROUND_IMAGE; - CLEAR_BACKGROUND_COLOR = base.CLEAR_BACKGROUND_COLOR; - } - // @ts-ignore - // litegraph.extensions.js - app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR); - }, - }); - }, + let { BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR } = + palette.colors.litegraph_base; + if ( + BACKGROUND_IMAGE === undefined || + CLEAR_BACKGROUND_COLOR === undefined + ) { + const base = colorPalettes["dark"].colors.litegraph_base; + BACKGROUND_IMAGE = base.BACKGROUND_IMAGE; + CLEAR_BACKGROUND_COLOR = base.CLEAR_BACKGROUND_COLOR; + } + // @ts-ignore + // litegraph.extensions.js + app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR); + }, + }); + }, }); diff --git a/src/extensions/core/contextMenuFilter.ts b/src/extensions/core/contextMenuFilter.ts index fc33ef820..39f1c1a42 100644 --- a/src/extensions/core/contextMenuFilter.ts +++ b/src/extensions/core/contextMenuFilter.ts @@ -1,150 +1,171 @@ -import {app} from "../../scripts/app"; +import { app } from "../../scripts/app"; // Adds filtering to combo context menus const ext = { - name: "Comfy.ContextMenuFilter", - init() { - const ctxMenu = LiteGraph.ContextMenu; - // @ts-ignore - // TODO Very hacky way to modify Litegraph behaviour. Fix this later. - LiteGraph.ContextMenu = function (values, options) { - const ctx = ctxMenu.call(this, values, options); + name: "Comfy.ContextMenuFilter", + init() { + const ctxMenu = LiteGraph.ContextMenu; + // @ts-ignore + // TODO Very hacky way to modify Litegraph behaviour. Fix this later. + LiteGraph.ContextMenu = function (values, options) { + const ctx = ctxMenu.call(this, values, options); - // If we are a dark menu (only used for combo boxes) then add a filter input - if (options?.className === "dark" && values?.length > 10) { - const filter = document.createElement("input"); - filter.classList.add("comfy-context-menu-filter"); - filter.placeholder = "Filter list"; - this.root.prepend(filter); + // If we are a dark menu (only used for combo boxes) then add a filter input + if (options?.className === "dark" && values?.length > 10) { + const filter = document.createElement("input"); + filter.classList.add("comfy-context-menu-filter"); + filter.placeholder = "Filter list"; + this.root.prepend(filter); - const items = Array.from(this.root.querySelectorAll(".litemenu-entry")) as HTMLElement[]; - let displayedItems = [...items]; - let itemCount = displayedItems.length; + const items = Array.from( + this.root.querySelectorAll(".litemenu-entry") + ) as HTMLElement[]; + let displayedItems = [...items]; + let itemCount = displayedItems.length; - // We must request an animation frame for the current node of the active canvas to update. - requestAnimationFrame(() => { - // @ts-ignore - const currentNode = LGraphCanvas.active_canvas.current_node; - const clickedComboValue = currentNode.widgets - ?.filter(w => w.type === "combo" && w.options.values.length === values.length) - .find(w => w.options.values.every((v, i) => v === values[i])) - ?.value; + // We must request an animation frame for the current node of the active canvas to update. + requestAnimationFrame(() => { + // @ts-ignore + const currentNode = LGraphCanvas.active_canvas.current_node; + const clickedComboValue = currentNode.widgets + ?.filter( + (w) => + w.type === "combo" && w.options.values.length === values.length + ) + .find((w) => + w.options.values.every((v, i) => v === values[i]) + )?.value; - let selectedIndex = clickedComboValue ? values.findIndex(v => v === clickedComboValue) : 0; - if (selectedIndex < 0) { - selectedIndex = 0; - } - let selectedItem = displayedItems[selectedIndex]; - updateSelected(); + let selectedIndex = clickedComboValue + ? values.findIndex((v) => v === clickedComboValue) + : 0; + if (selectedIndex < 0) { + selectedIndex = 0; + } + let selectedItem = displayedItems[selectedIndex]; + updateSelected(); - // Apply highlighting to the selected item - function updateSelected() { - selectedItem?.style.setProperty("background-color", ""); - selectedItem?.style.setProperty("color", ""); - selectedItem = displayedItems[selectedIndex]; - selectedItem?.style.setProperty("background-color", "#ccc", "important"); - selectedItem?.style.setProperty("color", "#000", "important"); - } + // Apply highlighting to the selected item + function updateSelected() { + selectedItem?.style.setProperty("background-color", ""); + selectedItem?.style.setProperty("color", ""); + selectedItem = displayedItems[selectedIndex]; + selectedItem?.style.setProperty( + "background-color", + "#ccc", + "important" + ); + selectedItem?.style.setProperty("color", "#000", "important"); + } - const positionList = () => { - const rect = this.root.getBoundingClientRect(); + const positionList = () => { + const rect = this.root.getBoundingClientRect(); - // If the top is off-screen then shift the element with scaling applied - if (rect.top < 0) { - const scale = 1 - this.root.getBoundingClientRect().height / this.root.clientHeight; - const shift = (this.root.clientHeight * scale) / 2; - this.root.style.top = -shift + "px"; - } - } + // If the top is off-screen then shift the element with scaling applied + if (rect.top < 0) { + const scale = + 1 - + this.root.getBoundingClientRect().height / + this.root.clientHeight; + const shift = (this.root.clientHeight * scale) / 2; + this.root.style.top = -shift + "px"; + } + }; - // Arrow up/down to select items - filter.addEventListener("keydown", (event) => { - switch (event.key) { - case "ArrowUp": - event.preventDefault(); - if (selectedIndex === 0) { - selectedIndex = itemCount - 1; - } else { - selectedIndex--; - } - updateSelected(); - break; - case "ArrowRight": - event.preventDefault(); - selectedIndex = itemCount - 1; - updateSelected(); - break; - case "ArrowDown": - event.preventDefault(); - if (selectedIndex === itemCount - 1) { - selectedIndex = 0; - } else { - selectedIndex++; - } - updateSelected(); - break; - case "ArrowLeft": - event.preventDefault(); - selectedIndex = 0; - updateSelected(); - break; - case "Enter": - selectedItem?.click(); - break; - case "Escape": - this.close(); - break; - } - }); + // Arrow up/down to select items + filter.addEventListener("keydown", (event) => { + switch (event.key) { + case "ArrowUp": + event.preventDefault(); + if (selectedIndex === 0) { + selectedIndex = itemCount - 1; + } else { + selectedIndex--; + } + updateSelected(); + break; + case "ArrowRight": + event.preventDefault(); + selectedIndex = itemCount - 1; + updateSelected(); + break; + case "ArrowDown": + event.preventDefault(); + if (selectedIndex === itemCount - 1) { + selectedIndex = 0; + } else { + selectedIndex++; + } + updateSelected(); + break; + case "ArrowLeft": + event.preventDefault(); + selectedIndex = 0; + updateSelected(); + break; + case "Enter": + selectedItem?.click(); + break; + case "Escape": + this.close(); + break; + } + }); - filter.addEventListener("input", () => { - // Hide all items that don't match our filter - const term = filter.value.toLocaleLowerCase(); - // When filtering, recompute which items are visible for arrow up/down and maintain selection. - displayedItems = items.filter(item => { - const isVisible = !term || item.textContent.toLocaleLowerCase().includes(term); - item.style.display = isVisible ? "block" : "none"; - return isVisible; - }); + filter.addEventListener("input", () => { + // Hide all items that don't match our filter + const term = filter.value.toLocaleLowerCase(); + // When filtering, recompute which items are visible for arrow up/down and maintain selection. + displayedItems = items.filter((item) => { + const isVisible = + !term || item.textContent.toLocaleLowerCase().includes(term); + item.style.display = isVisible ? "block" : "none"; + return isVisible; + }); - selectedIndex = 0; - if (displayedItems.includes(selectedItem)) { - selectedIndex = displayedItems.findIndex(d => d === selectedItem); - } - itemCount = displayedItems.length; + selectedIndex = 0; + if (displayedItems.includes(selectedItem)) { + selectedIndex = displayedItems.findIndex( + (d) => d === selectedItem + ); + } + itemCount = displayedItems.length; - updateSelected(); + updateSelected(); - // If we have an event then we can try and position the list under the source - if (options.event) { - let top = options.event.clientY - 10; + // If we have an event then we can try and position the list under the source + if (options.event) { + let top = options.event.clientY - 10; - const bodyRect = document.body.getBoundingClientRect(); - const rootRect = this.root.getBoundingClientRect(); - if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) { - top = Math.max(0, bodyRect.height - rootRect.height - 10); - } + const bodyRect = document.body.getBoundingClientRect(); + const rootRect = this.root.getBoundingClientRect(); + if ( + bodyRect.height && + top > bodyRect.height - rootRect.height - 10 + ) { + top = Math.max(0, bodyRect.height - rootRect.height - 10); + } - this.root.style.top = top + "px"; - positionList(); - } - }); + this.root.style.top = top + "px"; + positionList(); + } + }); - requestAnimationFrame(() => { - // Focus the filter box when opening - filter.focus(); + requestAnimationFrame(() => { + // Focus the filter box when opening + filter.focus(); - positionList(); - }); - }) - } + positionList(); + }); + }); + } - return ctx; - }; + return ctx; + }; - LiteGraph.ContextMenu.prototype = ctxMenu.prototype; - }, -} + LiteGraph.ContextMenu.prototype = ctxMenu.prototype; + }, +}; app.registerExtension(ext); diff --git a/src/extensions/core/dynamicPrompts.ts b/src/extensions/core/dynamicPrompts.ts index f342e1b12..f1cabba6f 100644 --- a/src/extensions/core/dynamicPrompts.ts +++ b/src/extensions/core/dynamicPrompts.ts @@ -7,42 +7,46 @@ import { app } from "../../scripts/app"; * Strips C-style line and block comments from a string */ function stripComments(str) { - return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,''); + return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ""); } app.registerExtension({ - name: "Comfy.DynamicPrompts", - nodeCreated(node) { - if (node.widgets) { - // Locate dynamic prompt text widgets - // Include any widgets with dynamicPrompts set to true, and customtext - const widgets = node.widgets.filter( - (n) => n.dynamicPrompts - ); - for (const widget of widgets) { - // Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node - widget.serializeValue = (workflowNode, widgetIndex) => { - let prompt = stripComments(widget.value); - while (prompt.replace("\\{", "").includes("{") && prompt.replace("\\}", "").includes("}")) { - const startIndex = prompt.replace("\\{", "00").indexOf("{"); - const endIndex = prompt.replace("\\}", "00").indexOf("}"); + name: "Comfy.DynamicPrompts", + nodeCreated(node) { + if (node.widgets) { + // Locate dynamic prompt text widgets + // Include any widgets with dynamicPrompts set to true, and customtext + const widgets = node.widgets.filter((n) => n.dynamicPrompts); + for (const widget of widgets) { + // Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node + widget.serializeValue = (workflowNode, widgetIndex) => { + let prompt = stripComments(widget.value); + while ( + prompt.replace("\\{", "").includes("{") && + prompt.replace("\\}", "").includes("}") + ) { + const startIndex = prompt.replace("\\{", "00").indexOf("{"); + const endIndex = prompt.replace("\\}", "00").indexOf("}"); - const optionsString = prompt.substring(startIndex + 1, endIndex); - const options = optionsString.split("|"); + const optionsString = prompt.substring(startIndex + 1, endIndex); + const options = optionsString.split("|"); - const randomIndex = Math.floor(Math.random() * options.length); - const randomOption = options[randomIndex]; + const randomIndex = Math.floor(Math.random() * options.length); + const randomOption = options[randomIndex]; - prompt = prompt.substring(0, startIndex) + randomOption + prompt.substring(endIndex + 1); - } + prompt = + prompt.substring(0, startIndex) + + randomOption + + prompt.substring(endIndex + 1); + } - // Overwrite the value in the serialized workflow pnginfo - if (workflowNode?.widgets_values) - workflowNode.widgets_values[widgetIndex] = prompt; + // Overwrite the value in the serialized workflow pnginfo + if (workflowNode?.widgets_values) + workflowNode.widgets_values[widgetIndex] = prompt; - return prompt; - }; - } - } - }, + return prompt; + }; + } + } + }, }); diff --git a/src/extensions/core/editAttention.ts b/src/extensions/core/editAttention.ts index b9908e9cf..e7c475be0 100644 --- a/src/extensions/core/editAttention.ts +++ b/src/extensions/core/editAttention.ts @@ -3,142 +3,159 @@ import { app } from "../../scripts/app"; // Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys app.registerExtension({ - name: "Comfy.EditAttention", - init() { - const editAttentionDelta = app.ui.settings.addSetting({ - id: "Comfy.EditAttention.Delta", - name: "Ctrl+up/down precision", - type: "slider", - attrs: { - min: 0.01, - max: 0.5, - step: 0.01, - }, - defaultValue: 0.05, - }); + name: "Comfy.EditAttention", + init() { + const editAttentionDelta = app.ui.settings.addSetting({ + id: "Comfy.EditAttention.Delta", + name: "Ctrl+up/down precision", + type: "slider", + attrs: { + min: 0.01, + max: 0.5, + step: 0.01, + }, + defaultValue: 0.05, + }); - function incrementWeight(weight, delta) { - const floatWeight = parseFloat(weight); - if (isNaN(floatWeight)) return weight; - const newWeight = floatWeight + delta; - if (newWeight < 0) return "0"; - return String(Number(newWeight.toFixed(10))); + function incrementWeight(weight, delta) { + const floatWeight = parseFloat(weight); + if (isNaN(floatWeight)) return weight; + const newWeight = floatWeight + delta; + if (newWeight < 0) return "0"; + return String(Number(newWeight.toFixed(10))); + } + + function findNearestEnclosure(text, cursorPos) { + let start = cursorPos, + end = cursorPos; + let openCount = 0, + closeCount = 0; + + // Find opening parenthesis before cursor + while (start >= 0) { + start--; + if (text[start] === "(" && openCount === closeCount) break; + if (text[start] === "(") openCount++; + if (text[start] === ")") closeCount++; + } + if (start < 0) return false; + + openCount = 0; + closeCount = 0; + + // Find closing parenthesis after cursor + while (end < text.length) { + if (text[end] === ")" && openCount === closeCount) break; + if (text[end] === "(") openCount++; + if (text[end] === ")") closeCount++; + end++; + } + if (end === text.length) return false; + + return { start: start + 1, end: end }; + } + + function addWeightToParentheses(text) { + const parenRegex = /^\((.*)\)$/; + const parenMatch = text.match(parenRegex); + + const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/; + const floatMatch = text.match(floatRegex); + + if (parenMatch && !floatMatch) { + return `(${parenMatch[1]}:1.0)`; + } else { + return text; + } + } + + function editAttention(event) { + const inputField = event.composedPath()[0]; + const delta = parseFloat(editAttentionDelta.value); + + if (inputField.tagName !== "TEXTAREA") return; + if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return; + if (!event.ctrlKey && !event.metaKey) return; + + event.preventDefault(); + + let start = inputField.selectionStart; + let end = inputField.selectionEnd; + let selectedText = inputField.value.substring(start, end); + + // If there is no selection, attempt to find the nearest enclosure, or select the current word + if (!selectedText) { + const nearestEnclosure = findNearestEnclosure(inputField.value, start); + if (nearestEnclosure) { + start = nearestEnclosure.start; + end = nearestEnclosure.end; + selectedText = inputField.value.substring(start, end); + } else { + // Select the current word, find the start and end of the word + const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t"; + + while ( + !delimiters.includes(inputField.value[start - 1]) && + start > 0 + ) { + start--; + } + + while ( + !delimiters.includes(inputField.value[end]) && + end < inputField.value.length + ) { + end++; + } + + selectedText = inputField.value.substring(start, end); + if (!selectedText) return; } + } - function findNearestEnclosure(text, cursorPos) { - let start = cursorPos, end = cursorPos; - let openCount = 0, closeCount = 0; + // If the selection ends with a space, remove it + if (selectedText[selectedText.length - 1] === " ") { + selectedText = selectedText.substring(0, selectedText.length - 1); + end -= 1; + } - // Find opening parenthesis before cursor - while (start >= 0) { - start--; - if (text[start] === "(" && openCount === closeCount) break; - if (text[start] === "(") openCount++; - if (text[start] === ")") closeCount++; - } - if (start < 0) return false; + // If there are parentheses left and right of the selection, select them + if ( + inputField.value[start - 1] === "(" && + inputField.value[end] === ")" + ) { + start -= 1; + end += 1; + selectedText = inputField.value.substring(start, end); + } - openCount = 0; - closeCount = 0; + // If the selection is not enclosed in parentheses, add them + if ( + selectedText[0] !== "(" || + selectedText[selectedText.length - 1] !== ")" + ) { + selectedText = `(${selectedText})`; + } - // Find closing parenthesis after cursor - while (end < text.length) { - if (text[end] === ")" && openCount === closeCount) break; - if (text[end] === "(") openCount++; - if (text[end] === ")") closeCount++; - end++; - } - if (end === text.length) return false; + // If the selection does not have a weight, add a weight of 1.0 + selectedText = addWeightToParentheses(selectedText); - return { start: start + 1, end: end }; + // Increment the weight + const weightDelta = event.key === "ArrowUp" ? delta : -delta; + const updatedText = selectedText.replace( + /\((.*):(\d+(?:\.\d+)?)\)/, + (match, text, weight) => { + weight = incrementWeight(weight, weightDelta); + if (weight == 1) { + return text; + } else { + return `(${text}:${weight})`; + } } + ); - function addWeightToParentheses(text) { - const parenRegex = /^\((.*)\)$/; - const parenMatch = text.match(parenRegex); - - const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/; - const floatMatch = text.match(floatRegex); - - if (parenMatch && !floatMatch) { - return `(${parenMatch[1]}:1.0)`; - } else { - return text; - } - }; - - function editAttention(event) { - const inputField = event.composedPath()[0]; - const delta = parseFloat(editAttentionDelta.value); - - if (inputField.tagName !== "TEXTAREA") return; - if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return; - if (!event.ctrlKey && !event.metaKey) return; - - event.preventDefault(); - - let start = inputField.selectionStart; - let end = inputField.selectionEnd; - let selectedText = inputField.value.substring(start, end); - - // If there is no selection, attempt to find the nearest enclosure, or select the current word - if (!selectedText) { - const nearestEnclosure = findNearestEnclosure(inputField.value, start); - if (nearestEnclosure) { - start = nearestEnclosure.start; - end = nearestEnclosure.end; - selectedText = inputField.value.substring(start, end); - } else { - // Select the current word, find the start and end of the word - const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t"; - - while (!delimiters.includes(inputField.value[start - 1]) && start > 0) { - start--; - } - - while (!delimiters.includes(inputField.value[end]) && end < inputField.value.length) { - end++; - } - - selectedText = inputField.value.substring(start, end); - if (!selectedText) return; - } - } - - // If the selection ends with a space, remove it - if (selectedText[selectedText.length - 1] === " ") { - selectedText = selectedText.substring(0, selectedText.length - 1); - end -= 1; - } - - // If there are parentheses left and right of the selection, select them - if (inputField.value[start - 1] === "(" && inputField.value[end] === ")") { - start -= 1; - end += 1; - selectedText = inputField.value.substring(start, end); - } - - // If the selection is not enclosed in parentheses, add them - if (selectedText[0] !== "(" || selectedText[selectedText.length - 1] !== ")") { - selectedText = `(${selectedText})`; - } - - // If the selection does not have a weight, add a weight of 1.0 - selectedText = addWeightToParentheses(selectedText); - - // Increment the weight - const weightDelta = event.key === "ArrowUp" ? delta : -delta; - const updatedText = selectedText.replace(/\((.*):(\d+(?:\.\d+)?)\)/, (match, text, weight) => { - weight = incrementWeight(weight, weightDelta); - if (weight == 1) { - return text; - } else { - return `(${text}:${weight})`; - } - }); - - inputField.setRangeText(updatedText, start, end, "select"); - } - window.addEventListener("keydown", editAttention); - }, + inputField.setRangeText(updatedText, start, end, "select"); + } + window.addEventListener("keydown", editAttention); + }, }); diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index f1ba5982a..f9592d0ff 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -7,1314 +7,1469 @@ import type { LGraphNode } from "/types/litegraph"; const GROUP = Symbol(); const Workflow = { - InUse: { - Free: 0, - Registered: 1, - InWorkflow: 2, - }, - isInUseGroupNode(name) { - const id = `workflow/${name}`; - // Check if lready registered/in use in this workflow - // @ts-ignore - if (app.graph.extra?.groupNodes?.[name]) { - // @ts-ignore - if (app.graph._nodes.find((n) => n.type === id)) { - return Workflow.InUse.InWorkflow; - } else { - return Workflow.InUse.Registered; - } - } - return Workflow.InUse.Free; - }, - storeGroupNode(name, data) { - // @ts-ignore - let extra = app.graph.extra; - // @ts-ignore - if (!extra) app.graph.extra = extra = {}; - let groupNodes = extra.groupNodes; - if (!groupNodes) extra.groupNodes = groupNodes = {}; - groupNodes[name] = data; - }, + InUse: { + Free: 0, + Registered: 1, + InWorkflow: 2, + }, + isInUseGroupNode(name) { + const id = `workflow/${name}`; + // Check if lready registered/in use in this workflow + // @ts-ignore + if (app.graph.extra?.groupNodes?.[name]) { + // @ts-ignore + if (app.graph._nodes.find((n) => n.type === id)) { + return Workflow.InUse.InWorkflow; + } else { + return Workflow.InUse.Registered; + } + } + return Workflow.InUse.Free; + }, + storeGroupNode(name, data) { + // @ts-ignore + let extra = app.graph.extra; + // @ts-ignore + if (!extra) app.graph.extra = extra = {}; + let groupNodes = extra.groupNodes; + if (!groupNodes) extra.groupNodes = groupNodes = {}; + groupNodes[name] = data; + }, }; class GroupNodeBuilder { - nodes: LGraphNode[]; - nodeData: any; + nodes: LGraphNode[]; + nodeData: any; - constructor(nodes) { - this.nodes = nodes; - } + constructor(nodes) { + this.nodes = nodes; + } - build() { - const name = this.getName(); - if (!name) return; + build() { + const name = this.getName(); + if (!name) return; - // Sort the nodes so they are in execution order - // this allows for widgets to be in the correct order when reconstructing - this.sortNodes(); + // Sort the nodes so they are in execution order + // this allows for widgets to be in the correct order when reconstructing + this.sortNodes(); - this.nodeData = this.getNodeData(); - Workflow.storeGroupNode(name, this.nodeData); + this.nodeData = this.getNodeData(); + Workflow.storeGroupNode(name, this.nodeData); - return { name, nodeData: this.nodeData }; - } + return { name, nodeData: this.nodeData }; + } - getName() { - const name = prompt("Enter group name"); - if (!name) return; - const used = Workflow.isInUseGroupNode(name); - switch (used) { - case Workflow.InUse.InWorkflow: - alert( - "An in use group node with this name already exists embedded in this workflow, please remove any instances or use a new name." - ); - return; - case Workflow.InUse.Registered: - if (!confirm("A group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?")) { - return; - } - break; - } - return name; - } + getName() { + const name = prompt("Enter group name"); + if (!name) return; + const used = Workflow.isInUseGroupNode(name); + switch (used) { + case Workflow.InUse.InWorkflow: + alert( + "An in use group node with this name already exists embedded in this workflow, please remove any instances or use a new name." + ); + return; + case Workflow.InUse.Registered: + if ( + !confirm( + "A group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?" + ) + ) { + return; + } + break; + } + return name; + } - sortNodes() { - // Gets the builders nodes in graph execution order - const nodesInOrder = app.graph.computeExecutionOrder(false); - this.nodes = this.nodes - .map((node) => ({ index: nodesInOrder.indexOf(node), node })) - .sort((a, b) => a.index - b.index || a.node.id - b.node.id) - .map(({ node }) => node); - } + sortNodes() { + // Gets the builders nodes in graph execution order + const nodesInOrder = app.graph.computeExecutionOrder(false); + this.nodes = this.nodes + .map((node) => ({ index: nodesInOrder.indexOf(node), node })) + .sort((a, b) => a.index - b.index || a.node.id - b.node.id) + .map(({ node }) => node); + } - getNodeData() { - const storeLinkTypes = (config) => { - // Store link types for dynamically typed nodes e.g. reroutes - for (const link of config.links) { - const origin = app.graph.getNodeById(link[4]); - const type = origin.outputs[link[1]].type; - link.push(type); - } - }; + getNodeData() { + const storeLinkTypes = (config) => { + // Store link types for dynamically typed nodes e.g. reroutes + for (const link of config.links) { + const origin = app.graph.getNodeById(link[4]); + const type = origin.outputs[link[1]].type; + link.push(type); + } + }; - const storeExternalLinks = (config) => { - // Store any external links to the group in the config so when rebuilding we add extra slots - config.external = []; - for (let i = 0; i < this.nodes.length; i++) { - const node = this.nodes[i]; - if (!node.outputs?.length) continue; - for (let slot = 0; slot < node.outputs.length; slot++) { - let hasExternal = false; - const output = node.outputs[slot]; - let type = output.type; - if (!output.links?.length) continue; - for (const l of output.links) { - const link = app.graph.links[l]; - if (!link) continue; - if (type === "*") type = link.type; + const storeExternalLinks = (config) => { + // Store any external links to the group in the config so when rebuilding we add extra slots + config.external = []; + for (let i = 0; i < this.nodes.length; i++) { + const node = this.nodes[i]; + if (!node.outputs?.length) continue; + for (let slot = 0; slot < node.outputs.length; slot++) { + let hasExternal = false; + const output = node.outputs[slot]; + let type = output.type; + if (!output.links?.length) continue; + for (const l of output.links) { + const link = app.graph.links[l]; + if (!link) continue; + if (type === "*") type = link.type; - if (!app.canvas.selected_nodes[link.target_id]) { - hasExternal = true; - break; - } - } - if (hasExternal) { - config.external.push([i, slot, type]); - } - } - } - }; + if (!app.canvas.selected_nodes[link.target_id]) { + hasExternal = true; + break; + } + } + if (hasExternal) { + config.external.push([i, slot, type]); + } + } + } + }; - // Use the built in copyToClipboard function to generate the node data we need - const backup = localStorage.getItem("litegrapheditor_clipboard"); - try { - // @ts-ignore - // TODO Figure out if copyToClipboard is really taking this param - app.canvas.copyToClipboard(this.nodes); - const config = JSON.parse(localStorage.getItem("litegrapheditor_clipboard")); + // Use the built in copyToClipboard function to generate the node data we need + const backup = localStorage.getItem("litegrapheditor_clipboard"); + try { + // @ts-ignore + // TODO Figure out if copyToClipboard is really taking this param + app.canvas.copyToClipboard(this.nodes); + const config = JSON.parse( + localStorage.getItem("litegrapheditor_clipboard") + ); - storeLinkTypes(config); - storeExternalLinks(config); + storeLinkTypes(config); + storeExternalLinks(config); - return config; - } finally { - localStorage.setItem("litegrapheditor_clipboard", backup); - } - } + return config; + } finally { + localStorage.setItem("litegrapheditor_clipboard", backup); + } + } } export class GroupNodeConfig { - name: string; - nodeData: any; - inputCount: number; - oldToNewOutputMap: {}; - newToOldOutputMap: {}; - oldToNewInputMap: {}; - oldToNewWidgetMap: {}; - newToOldWidgetMap: {}; - primitiveDefs: {}; - widgetToPrimitive: {}; - primitiveToWidget: {}; - nodeInputs: {}; - outputVisibility: any[]; - nodeDef: any; - inputs: any[]; - linksFrom: {}; - linksTo: {}; - externalFrom: {}; + name: string; + nodeData: any; + inputCount: number; + oldToNewOutputMap: {}; + newToOldOutputMap: {}; + oldToNewInputMap: {}; + oldToNewWidgetMap: {}; + newToOldWidgetMap: {}; + primitiveDefs: {}; + widgetToPrimitive: {}; + primitiveToWidget: {}; + nodeInputs: {}; + outputVisibility: any[]; + nodeDef: any; + inputs: any[]; + linksFrom: {}; + linksTo: {}; + externalFrom: {}; - constructor(name, nodeData) { - this.name = name; - this.nodeData = nodeData; - this.getLinks(); + constructor(name, nodeData) { + this.name = name; + this.nodeData = nodeData; + this.getLinks(); - this.inputCount = 0; - this.oldToNewOutputMap = {}; - this.newToOldOutputMap = {}; - this.oldToNewInputMap = {}; - this.oldToNewWidgetMap = {}; - this.newToOldWidgetMap = {}; - this.primitiveDefs = {}; - this.widgetToPrimitive = {}; - this.primitiveToWidget = {}; - this.nodeInputs = {}; - this.outputVisibility = []; - } + this.inputCount = 0; + this.oldToNewOutputMap = {}; + this.newToOldOutputMap = {}; + this.oldToNewInputMap = {}; + this.oldToNewWidgetMap = {}; + this.newToOldWidgetMap = {}; + this.primitiveDefs = {}; + this.widgetToPrimitive = {}; + this.primitiveToWidget = {}; + this.nodeInputs = {}; + this.outputVisibility = []; + } - async registerType(source = "workflow") { - this.nodeDef = { - output: [], - output_name: [], - output_is_list: [], - output_is_hidden: [], - name: source + "/" + this.name, - display_name: this.name, - category: "group nodes" + ("/" + source), - input: { required: {} }, + async registerType(source = "workflow") { + this.nodeDef = { + output: [], + output_name: [], + output_is_list: [], + output_is_hidden: [], + name: source + "/" + this.name, + display_name: this.name, + category: "group nodes" + ("/" + source), + input: { required: {} }, - [GROUP]: this, - }; + [GROUP]: this, + }; - this.inputs = []; - const seenInputs = {}; - const seenOutputs = {}; - for (let i = 0; i < this.nodeData.nodes.length; i++) { - const node = this.nodeData.nodes[i]; - node.index = i; - this.processNode(node, seenInputs, seenOutputs); - } + this.inputs = []; + const seenInputs = {}; + const seenOutputs = {}; + for (let i = 0; i < this.nodeData.nodes.length; i++) { + const node = this.nodeData.nodes[i]; + node.index = i; + this.processNode(node, seenInputs, seenOutputs); + } - for (const p of this.#convertedToProcess) { - p(); - } - this.#convertedToProcess = null; - await app.registerNodeDef("workflow/" + this.name, this.nodeDef); - } + for (const p of this.#convertedToProcess) { + p(); + } + this.#convertedToProcess = null; + await app.registerNodeDef("workflow/" + this.name, this.nodeDef); + } - getLinks() { - this.linksFrom = {}; - this.linksTo = {}; - this.externalFrom = {}; + getLinks() { + this.linksFrom = {}; + this.linksTo = {}; + this.externalFrom = {}; - // Extract links for easy lookup - for (const l of this.nodeData.links) { - const [sourceNodeId, sourceNodeSlot, targetNodeId, targetNodeSlot] = l; + // Extract links for easy lookup + for (const l of this.nodeData.links) { + const [sourceNodeId, sourceNodeSlot, targetNodeId, targetNodeSlot] = l; - // Skip links outside the copy config - if (sourceNodeId == null) continue; + // Skip links outside the copy config + if (sourceNodeId == null) continue; - if (!this.linksFrom[sourceNodeId]) { - this.linksFrom[sourceNodeId] = {}; - } - if (!this.linksFrom[sourceNodeId][sourceNodeSlot]) { - this.linksFrom[sourceNodeId][sourceNodeSlot] = []; - } - this.linksFrom[sourceNodeId][sourceNodeSlot].push(l); + if (!this.linksFrom[sourceNodeId]) { + this.linksFrom[sourceNodeId] = {}; + } + if (!this.linksFrom[sourceNodeId][sourceNodeSlot]) { + this.linksFrom[sourceNodeId][sourceNodeSlot] = []; + } + this.linksFrom[sourceNodeId][sourceNodeSlot].push(l); - if (!this.linksTo[targetNodeId]) { - this.linksTo[targetNodeId] = {}; - } - this.linksTo[targetNodeId][targetNodeSlot] = l; - } + if (!this.linksTo[targetNodeId]) { + this.linksTo[targetNodeId] = {}; + } + this.linksTo[targetNodeId][targetNodeSlot] = l; + } - if (this.nodeData.external) { - for (const ext of this.nodeData.external) { - if (!this.externalFrom[ext[0]]) { - this.externalFrom[ext[0]] = { [ext[1]]: ext[2] }; - } else { - this.externalFrom[ext[0]][ext[1]] = ext[2]; - } - } - } - } + if (this.nodeData.external) { + for (const ext of this.nodeData.external) { + if (!this.externalFrom[ext[0]]) { + this.externalFrom[ext[0]] = { [ext[1]]: ext[2] }; + } else { + this.externalFrom[ext[0]][ext[1]] = ext[2]; + } + } + } + } - processNode(node, seenInputs, seenOutputs) { - const def = this.getNodeDef(node); - if (!def) return; + processNode(node, seenInputs, seenOutputs) { + const def = this.getNodeDef(node); + if (!def) return; - const inputs = { ...def.input?.required, ...def.input?.optional }; + const inputs = { ...def.input?.required, ...def.input?.optional }; - this.inputs.push(this.processNodeInputs(node, seenInputs, inputs)); - if (def.output?.length) this.processNodeOutputs(node, seenOutputs, def); - } + this.inputs.push(this.processNodeInputs(node, seenInputs, inputs)); + if (def.output?.length) this.processNodeOutputs(node, seenOutputs, def); + } - getNodeDef(node) { - const def = globalDefs[node.type]; - if (def) return def; + getNodeDef(node) { + const def = globalDefs[node.type]; + if (def) return def; - const linksFrom = this.linksFrom[node.index]; - if (node.type === "PrimitiveNode") { - // Skip as its not linked - if (!linksFrom) return; + const linksFrom = this.linksFrom[node.index]; + if (node.type === "PrimitiveNode") { + // Skip as its not linked + if (!linksFrom) return; - let type = linksFrom["0"][0][5]; - if (type === "COMBO") { - // Use the array items - const source = node.outputs[0].widget.name; - const fromTypeName = this.nodeData.nodes[linksFrom["0"][0][2]].type; - const fromType = globalDefs[fromTypeName]; - const input = fromType.input.required[source] ?? fromType.input.optional[source]; - type = input[0]; - } + let type = linksFrom["0"][0][5]; + if (type === "COMBO") { + // Use the array items + const source = node.outputs[0].widget.name; + const fromTypeName = this.nodeData.nodes[linksFrom["0"][0][2]].type; + const fromType = globalDefs[fromTypeName]; + const input = + fromType.input.required[source] ?? fromType.input.optional[source]; + type = input[0]; + } - const def = (this.primitiveDefs[node.index] = { - input: { - required: { - value: [type, {}], - }, - }, - output: [type], - output_name: [], - output_is_list: [], - }); - return def; - } else if (node.type === "Reroute") { - const linksTo = this.linksTo[node.index]; - if (linksTo && linksFrom && !this.externalFrom[node.index]?.[0]) { - // Being used internally - return null; - } + const def = (this.primitiveDefs[node.index] = { + input: { + required: { + value: [type, {}], + }, + }, + output: [type], + output_name: [], + output_is_list: [], + }); + return def; + } else if (node.type === "Reroute") { + const linksTo = this.linksTo[node.index]; + if (linksTo && linksFrom && !this.externalFrom[node.index]?.[0]) { + // Being used internally + return null; + } - let config = {}; - let rerouteType = "*"; - if (linksFrom) { - for (const [, , id, slot] of linksFrom["0"]) { - const node = this.nodeData.nodes[id]; - const input = node.inputs[slot]; - if (rerouteType === "*") { - rerouteType = input.type; - } - if (input.widget) { - const targetDef = globalDefs[node.type]; - const targetWidget = targetDef.input.required[input.widget.name] ?? targetDef.input.optional[input.widget.name]; + let config = {}; + let rerouteType = "*"; + if (linksFrom) { + for (const [, , id, slot] of linksFrom["0"]) { + const node = this.nodeData.nodes[id]; + const input = node.inputs[slot]; + if (rerouteType === "*") { + rerouteType = input.type; + } + if (input.widget) { + const targetDef = globalDefs[node.type]; + const targetWidget = + targetDef.input.required[input.widget.name] ?? + targetDef.input.optional[input.widget.name]; - const widget = [targetWidget[0], config]; - const res = mergeIfValid( - { - widget, - }, - targetWidget, - false, - null, - widget - ); - // @ts-ignore - config = res?.customConfig ?? config; - } - } - } else if (linksTo) { - const [id, slot] = linksTo["0"]; - rerouteType = this.nodeData.nodes[id].outputs[slot].type; - } else { - // Reroute used as a pipe - for (const l of this.nodeData.links) { - if (l[2] === node.index) { - rerouteType = l[5]; - break; - } - } - if (rerouteType === "*") { - // Check for an external link - const t = this.externalFrom[node.index]?.[0]; - if (t) { - rerouteType = t; - } - } - } + const widget = [targetWidget[0], config]; + const res = mergeIfValid( + { + widget, + }, + targetWidget, + false, + null, + widget + ); + // @ts-ignore + config = res?.customConfig ?? config; + } + } + } else if (linksTo) { + const [id, slot] = linksTo["0"]; + rerouteType = this.nodeData.nodes[id].outputs[slot].type; + } else { + // Reroute used as a pipe + for (const l of this.nodeData.links) { + if (l[2] === node.index) { + rerouteType = l[5]; + break; + } + } + if (rerouteType === "*") { + // Check for an external link + const t = this.externalFrom[node.index]?.[0]; + if (t) { + rerouteType = t; + } + } + } - // @ts-ignore - config.forceInput = true; - return { - input: { - required: { - [rerouteType]: [rerouteType, config], - }, - }, - output: [rerouteType], - output_name: [], - output_is_list: [], - }; - } + // @ts-ignore + config.forceInput = true; + return { + input: { + required: { + [rerouteType]: [rerouteType, config], + }, + }, + output: [rerouteType], + output_name: [], + output_is_list: [], + }; + } - console.warn("Skipping virtual node " + node.type + " when building group node " + this.name); - } + console.warn( + "Skipping virtual node " + + node.type + + " when building group node " + + this.name + ); + } - getInputConfig(node, inputName, seenInputs, config, extra?) { - const customConfig = this.nodeData.config?.[node.index]?.input?.[inputName]; - let name = customConfig?.name ?? node.inputs?.find((inp) => inp.name === inputName)?.label ?? inputName; - let key = name; - let prefix = ""; - // Special handling for primitive to include the title if it is set rather than just "value" - if ((node.type === "PrimitiveNode" && node.title) || name in seenInputs) { - prefix = `${node.title ?? node.type} `; - key = name = `${prefix}${inputName}`; - if (name in seenInputs) { - name = `${prefix}${seenInputs[name]} ${inputName}`; - } - } - seenInputs[key] = (seenInputs[key] ?? 1) + 1; + getInputConfig(node, inputName, seenInputs, config, extra?) { + const customConfig = this.nodeData.config?.[node.index]?.input?.[inputName]; + let name = + customConfig?.name ?? + node.inputs?.find((inp) => inp.name === inputName)?.label ?? + inputName; + let key = name; + let prefix = ""; + // Special handling for primitive to include the title if it is set rather than just "value" + if ((node.type === "PrimitiveNode" && node.title) || name in seenInputs) { + prefix = `${node.title ?? node.type} `; + key = name = `${prefix}${inputName}`; + if (name in seenInputs) { + name = `${prefix}${seenInputs[name]} ${inputName}`; + } + } + seenInputs[key] = (seenInputs[key] ?? 1) + 1; - if (inputName === "seed" || inputName === "noise_seed") { - if (!extra) extra = {}; - extra.control_after_generate = `${prefix}control_after_generate`; - } - if (config[0] === "IMAGEUPLOAD") { - if (!extra) extra = {}; - extra.widget = this.oldToNewWidgetMap[node.index]?.[config[1]?.widget ?? "image"] ?? "image"; - } + if (inputName === "seed" || inputName === "noise_seed") { + if (!extra) extra = {}; + extra.control_after_generate = `${prefix}control_after_generate`; + } + if (config[0] === "IMAGEUPLOAD") { + if (!extra) extra = {}; + extra.widget = + this.oldToNewWidgetMap[node.index]?.[config[1]?.widget ?? "image"] ?? + "image"; + } - if (extra) { - config = [config[0], { ...config[1], ...extra }]; - } + if (extra) { + config = [config[0], { ...config[1], ...extra }]; + } - return { name, config, customConfig }; - } + return { name, config, customConfig }; + } - processWidgetInputs(inputs, node, inputNames, seenInputs) { - const slots = []; - const converted = new Map(); - const widgetMap = (this.oldToNewWidgetMap[node.index] = {}); - for (const inputName of inputNames) { - let widgetType = app.getWidgetType(inputs[inputName], inputName); - if (widgetType) { - const convertedIndex = node.inputs?.findIndex((inp) => inp.name === inputName && inp.widget?.name === inputName); - if (convertedIndex > -1) { - // This widget has been converted to a widget - // We need to store this in the correct position so link ids line up - converted.set(convertedIndex, inputName); - widgetMap[inputName] = null; - } else { - // Normal widget - const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]); - this.nodeDef.input.required[name] = config; - widgetMap[inputName] = name; - this.newToOldWidgetMap[name] = { node, inputName }; - } - } else { - // Normal input - slots.push(inputName); - } - } - return { converted, slots }; - } + processWidgetInputs(inputs, node, inputNames, seenInputs) { + const slots = []; + const converted = new Map(); + const widgetMap = (this.oldToNewWidgetMap[node.index] = {}); + for (const inputName of inputNames) { + let widgetType = app.getWidgetType(inputs[inputName], inputName); + if (widgetType) { + const convertedIndex = node.inputs?.findIndex( + (inp) => inp.name === inputName && inp.widget?.name === inputName + ); + if (convertedIndex > -1) { + // This widget has been converted to a widget + // We need to store this in the correct position so link ids line up + converted.set(convertedIndex, inputName); + widgetMap[inputName] = null; + } else { + // Normal widget + const { name, config } = this.getInputConfig( + node, + inputName, + seenInputs, + inputs[inputName] + ); + this.nodeDef.input.required[name] = config; + widgetMap[inputName] = name; + this.newToOldWidgetMap[name] = { node, inputName }; + } + } else { + // Normal input + slots.push(inputName); + } + } + return { converted, slots }; + } - checkPrimitiveConnection(link, inputName, inputs) { - const sourceNode = this.nodeData.nodes[link[0]]; - if (sourceNode.type === "PrimitiveNode") { - // Merge link configurations - const [sourceNodeId, _, targetNodeId, __] = link; - const primitiveDef = this.primitiveDefs[sourceNodeId]; - const targetWidget = inputs[inputName]; - const primitiveConfig = primitiveDef.input.required.value; - const output = { widget: primitiveConfig }; - const config = mergeIfValid(output, targetWidget, false, null, primitiveConfig); - primitiveConfig[1] = config?.customConfig ?? inputs[inputName][1] ? { ...inputs[inputName][1] } : {}; + checkPrimitiveConnection(link, inputName, inputs) { + const sourceNode = this.nodeData.nodes[link[0]]; + if (sourceNode.type === "PrimitiveNode") { + // Merge link configurations + const [sourceNodeId, _, targetNodeId, __] = link; + const primitiveDef = this.primitiveDefs[sourceNodeId]; + const targetWidget = inputs[inputName]; + const primitiveConfig = primitiveDef.input.required.value; + const output = { widget: primitiveConfig }; + const config = mergeIfValid( + output, + targetWidget, + false, + null, + primitiveConfig + ); + primitiveConfig[1] = + config?.customConfig ?? inputs[inputName][1] + ? { ...inputs[inputName][1] } + : {}; - let name = this.oldToNewWidgetMap[sourceNodeId]["value"]; - name = name.substr(0, name.length - 6); - primitiveConfig[1].control_after_generate = true; - primitiveConfig[1].control_prefix = name; + let name = this.oldToNewWidgetMap[sourceNodeId]["value"]; + name = name.substr(0, name.length - 6); + primitiveConfig[1].control_after_generate = true; + primitiveConfig[1].control_prefix = name; - let toPrimitive = this.widgetToPrimitive[targetNodeId]; - if (!toPrimitive) { - toPrimitive = this.widgetToPrimitive[targetNodeId] = {}; - } - if (toPrimitive[inputName]) { - toPrimitive[inputName].push(sourceNodeId); - } - toPrimitive[inputName] = sourceNodeId; + let toPrimitive = this.widgetToPrimitive[targetNodeId]; + if (!toPrimitive) { + toPrimitive = this.widgetToPrimitive[targetNodeId] = {}; + } + if (toPrimitive[inputName]) { + toPrimitive[inputName].push(sourceNodeId); + } + toPrimitive[inputName] = sourceNodeId; - let toWidget = this.primitiveToWidget[sourceNodeId]; - if (!toWidget) { - toWidget = this.primitiveToWidget[sourceNodeId] = []; - } - toWidget.push({ nodeId: targetNodeId, inputName }); - } - } + let toWidget = this.primitiveToWidget[sourceNodeId]; + if (!toWidget) { + toWidget = this.primitiveToWidget[sourceNodeId] = []; + } + toWidget.push({ nodeId: targetNodeId, inputName }); + } + } - processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs) { - this.nodeInputs[node.index] = {}; - for (let i = 0; i < slots.length; i++) { - const inputName = slots[i]; - if (linksTo[i]) { - this.checkPrimitiveConnection(linksTo[i], inputName, inputs); - // This input is linked so we can skip it - continue; - } + processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs) { + this.nodeInputs[node.index] = {}; + for (let i = 0; i < slots.length; i++) { + const inputName = slots[i]; + if (linksTo[i]) { + this.checkPrimitiveConnection(linksTo[i], inputName, inputs); + // This input is linked so we can skip it + continue; + } - const { name, config, customConfig } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]); + const { name, config, customConfig } = this.getInputConfig( + node, + inputName, + seenInputs, + inputs[inputName] + ); - this.nodeInputs[node.index][inputName] = name; - if(customConfig?.visible === false) continue; - - this.nodeDef.input.required[name] = config; - inputMap[i] = this.inputCount++; - } - } + this.nodeInputs[node.index][inputName] = name; + if (customConfig?.visible === false) continue; - processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs) { - // Add converted widgets sorted into their index order (ordered as they were converted) so link ids match up - const convertedSlots = [...converted.keys()].sort().map((k) => converted.get(k)); - for (let i = 0; i < convertedSlots.length; i++) { - const inputName = convertedSlots[i]; - if (linksTo[slots.length + i]) { - this.checkPrimitiveConnection(linksTo[slots.length + i], inputName, inputs); - // This input is linked so we can skip it - continue; - } + this.nodeDef.input.required[name] = config; + inputMap[i] = this.inputCount++; + } + } - const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName], { - defaultInput: true, - }); + processConvertedWidgets( + inputs, + node, + slots, + converted, + linksTo, + inputMap, + seenInputs + ) { + // Add converted widgets sorted into their index order (ordered as they were converted) so link ids match up + const convertedSlots = [...converted.keys()] + .sort() + .map((k) => converted.get(k)); + for (let i = 0; i < convertedSlots.length; i++) { + const inputName = convertedSlots[i]; + if (linksTo[slots.length + i]) { + this.checkPrimitiveConnection( + linksTo[slots.length + i], + inputName, + inputs + ); + // This input is linked so we can skip it + continue; + } - this.nodeDef.input.required[name] = config; - this.newToOldWidgetMap[name] = { node, inputName }; + const { name, config } = this.getInputConfig( + node, + inputName, + seenInputs, + inputs[inputName], + { + defaultInput: true, + } + ); - if (!this.oldToNewWidgetMap[node.index]) { - this.oldToNewWidgetMap[node.index] = {}; - } - this.oldToNewWidgetMap[node.index][inputName] = name; + this.nodeDef.input.required[name] = config; + this.newToOldWidgetMap[name] = { node, inputName }; - inputMap[slots.length + i] = this.inputCount++; - } - } + if (!this.oldToNewWidgetMap[node.index]) { + this.oldToNewWidgetMap[node.index] = {}; + } + this.oldToNewWidgetMap[node.index][inputName] = name; - #convertedToProcess = []; - processNodeInputs(node, seenInputs, inputs) { - const inputMapping = []; + inputMap[slots.length + i] = this.inputCount++; + } + } - const inputNames = Object.keys(inputs); - if (!inputNames.length) return; + #convertedToProcess = []; + processNodeInputs(node, seenInputs, inputs) { + const inputMapping = []; - const { converted, slots } = this.processWidgetInputs(inputs, node, inputNames, seenInputs); - const linksTo = this.linksTo[node.index] ?? {}; - const inputMap = (this.oldToNewInputMap[node.index] = {}); - this.processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs); + const inputNames = Object.keys(inputs); + if (!inputNames.length) return; - // Converted inputs have to be processed after all other nodes as they'll be at the end of the list - this.#convertedToProcess.push(() => this.processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs)); + const { converted, slots } = this.processWidgetInputs( + inputs, + node, + inputNames, + seenInputs + ); + const linksTo = this.linksTo[node.index] ?? {}; + const inputMap = (this.oldToNewInputMap[node.index] = {}); + this.processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs); - return inputMapping; - } + // Converted inputs have to be processed after all other nodes as they'll be at the end of the list + this.#convertedToProcess.push(() => + this.processConvertedWidgets( + inputs, + node, + slots, + converted, + linksTo, + inputMap, + seenInputs + ) + ); - processNodeOutputs(node, seenOutputs, def) { - const oldToNew = (this.oldToNewOutputMap[node.index] = {}); + return inputMapping; + } - // Add outputs - for (let outputId = 0; outputId < def.output.length; outputId++) { - const linksFrom = this.linksFrom[node.index]; - // If this output is linked internally we flag it to hide - const hasLink = linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId]; - const customConfig = this.nodeData.config?.[node.index]?.output?.[outputId]; - const visible = customConfig?.visible ?? !hasLink; - this.outputVisibility.push(visible); - if (!visible) { - continue; - } + processNodeOutputs(node, seenOutputs, def) { + const oldToNew = (this.oldToNewOutputMap[node.index] = {}); - oldToNew[outputId] = this.nodeDef.output.length; - this.newToOldOutputMap[this.nodeDef.output.length] = { node, slot: outputId }; - this.nodeDef.output.push(def.output[outputId]); - this.nodeDef.output_is_list.push(def.output_is_list[outputId]); + // Add outputs + for (let outputId = 0; outputId < def.output.length; outputId++) { + const linksFrom = this.linksFrom[node.index]; + // If this output is linked internally we flag it to hide + const hasLink = + linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId]; + const customConfig = + this.nodeData.config?.[node.index]?.output?.[outputId]; + const visible = customConfig?.visible ?? !hasLink; + this.outputVisibility.push(visible); + if (!visible) { + continue; + } - let label = customConfig?.name; - if (!label) { - label = def.output_name?.[outputId] ?? def.output[outputId]; - const output = node.outputs.find((o) => o.name === label); - if (output?.label) { - label = output.label; - } - } + oldToNew[outputId] = this.nodeDef.output.length; + this.newToOldOutputMap[this.nodeDef.output.length] = { + node, + slot: outputId, + }; + this.nodeDef.output.push(def.output[outputId]); + this.nodeDef.output_is_list.push(def.output_is_list[outputId]); - let name = label; - if (name in seenOutputs) { - const prefix = `${node.title ?? node.type} `; - name = `${prefix}${label}`; - if (name in seenOutputs) { - name = `${prefix}${node.index} ${label}`; - } - } - seenOutputs[name] = 1; + let label = customConfig?.name; + if (!label) { + label = def.output_name?.[outputId] ?? def.output[outputId]; + const output = node.outputs.find((o) => o.name === label); + if (output?.label) { + label = output.label; + } + } - this.nodeDef.output_name.push(name); - } - } + let name = label; + if (name in seenOutputs) { + const prefix = `${node.title ?? node.type} `; + name = `${prefix}${label}`; + if (name in seenOutputs) { + name = `${prefix}${node.index} ${label}`; + } + } + seenOutputs[name] = 1; - static async registerFromWorkflow(groupNodes, missingNodeTypes) { - const clean = app.clean; - app.clean = function () { - for (const g in groupNodes) { - try { - LiteGraph.unregisterNodeType("workflow/" + g); - } catch (error) {} - } - app.clean = clean; - }; + this.nodeDef.output_name.push(name); + } + } - for (const g in groupNodes) { - const groupData = groupNodes[g]; + static async registerFromWorkflow(groupNodes, missingNodeTypes) { + const clean = app.clean; + app.clean = function () { + for (const g in groupNodes) { + try { + LiteGraph.unregisterNodeType("workflow/" + g); + } catch (error) {} + } + app.clean = clean; + }; - let hasMissing = false; - for (const n of groupData.nodes) { - // Find missing node types - if (!(n.type in LiteGraph.registered_node_types)) { - missingNodeTypes.push({ - type: n.type, - hint: ` (In group node 'workflow/${g}')`, - }); + for (const g in groupNodes) { + const groupData = groupNodes[g]; - missingNodeTypes.push({ - type: "workflow/" + g, - action: { - text: "Remove from workflow", - callback: (e) => { - delete groupNodes[g]; - e.target.textContent = "Removed"; - e.target.style.pointerEvents = "none"; - e.target.style.opacity = 0.7; - }, - }, - }); + let hasMissing = false; + for (const n of groupData.nodes) { + // Find missing node types + if (!(n.type in LiteGraph.registered_node_types)) { + missingNodeTypes.push({ + type: n.type, + hint: ` (In group node 'workflow/${g}')`, + }); - hasMissing = true; - } - } + missingNodeTypes.push({ + type: "workflow/" + g, + action: { + text: "Remove from workflow", + callback: (e) => { + delete groupNodes[g]; + e.target.textContent = "Removed"; + e.target.style.pointerEvents = "none"; + e.target.style.opacity = 0.7; + }, + }, + }); - if (hasMissing) continue; + hasMissing = true; + } + } - const config = new GroupNodeConfig(g, groupData); - await config.registerType(); - } - } + if (hasMissing) continue; + + const config = new GroupNodeConfig(g, groupData); + await config.registerType(); + } + } } export class GroupNodeHandler { - node; - groupData; - innerNodes: any; - - constructor(node) { - this.node = node; - this.groupData = node.constructor?.nodeData?.[GROUP]; - - this.node.setInnerNodes = (innerNodes) => { - this.innerNodes = innerNodes; - - for (let innerNodeIndex = 0; innerNodeIndex < this.innerNodes.length; innerNodeIndex++) { - const innerNode = this.innerNodes[innerNodeIndex]; - - for (const w of innerNode.widgets ?? []) { - if (w.type === "converted-widget") { - w.serializeValue = w.origSerializeValue; - } - } - - innerNode.index = innerNodeIndex; - innerNode.getInputNode = (slot) => { - // Check if this input is internal or external - const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot]; - if (externalSlot != null) { - return this.node.getInputNode(externalSlot); - } - - // Internal link - const innerLink = this.groupData.linksTo[innerNode.index]?.[slot]; - if (!innerLink) return null; - - const inputNode = innerNodes[innerLink[0]]; - // Primitives will already apply their values - if (inputNode.type === "PrimitiveNode") return null; - - return inputNode; - }; - - innerNode.getInputLink = (slot) => { - const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot]; - if (externalSlot != null) { - // The inner node is connected via the group node inputs - const linkId = this.node.inputs[externalSlot].link; - let link = app.graph.links[linkId]; - - // Use the outer link, but update the target to the inner node - // @ts-ignore - // TODO: Fix this - link = { - ...link, - target_id: innerNode.id, - target_slot: +slot, - }; - return link; - } - - let link = this.groupData.linksTo[innerNode.index]?.[slot]; - if (!link) return null; - // Use the inner link, but update the origin node to be inner node id - link = { - origin_id: innerNodes[link[0]].id, - origin_slot: link[1], - target_id: innerNode.id, - target_slot: +slot, - }; - return link; - }; - } - }; - - this.node.updateLink = (link) => { - // Replace the group node reference with the internal node - link = { ...link }; - const output = this.groupData.newToOldOutputMap[link.origin_slot]; - let innerNode = this.innerNodes[output.node.index]; - let l; - while (innerNode?.type === "Reroute") { - l = innerNode.getInputLink(0); - innerNode = innerNode.getInputNode(0); - } - - if (!innerNode) { - return null; - } - - if (l && GroupNodeHandler.isGroupNode(innerNode)) { - return innerNode.updateLink(l); - } - - link.origin_id = innerNode.id; - link.origin_slot = l?.origin_slot ?? output.slot; - return link; - }; - - this.node.getInnerNodes = () => { - if (!this.innerNodes) { - this.node.setInnerNodes( - this.groupData.nodeData.nodes.map((n, i) => { - const innerNode = LiteGraph.createNode(n.type); - innerNode.configure(n); - // @ts-ignore - innerNode.id = `${this.node.id}:${i}`; - return innerNode; - }) - ); - } - - this.updateInnerWidgets(); - - return this.innerNodes; - }; - - this.node.recreate = async () => { - const id = this.node.id; - const sz = this.node.size; - const nodes = this.node.convertToNodes(); - - const groupNode = LiteGraph.createNode(this.node.type); - groupNode.id = id; - - // Reuse the existing nodes for this instance - groupNode.setInnerNodes(nodes); - groupNode[GROUP].populateWidgets(); - app.graph.add(groupNode); - groupNode.size = [Math.max(groupNode.size[0], sz[0]), Math.max(groupNode.size[1], sz[1])]; - - // Remove all converted nodes and relink them - groupNode[GROUP].replaceNodes(nodes); - return groupNode; - }; - - this.node.convertToNodes = () => { - const addInnerNodes = () => { - const backup = localStorage.getItem("litegrapheditor_clipboard"); - // Clone the node data so we dont mutate it for other nodes - const c = { ...this.groupData.nodeData }; - c.nodes = [...c.nodes]; - const innerNodes = this.node.getInnerNodes(); - let ids = []; - for (let i = 0; i < c.nodes.length; i++) { - let id = innerNodes?.[i]?.id; - // Use existing IDs if they are set on the inner nodes - if (id == null || isNaN(id)) { - id = undefined; - } else { - ids.push(id); - } - c.nodes[i] = { ...c.nodes[i], id }; - } - localStorage.setItem("litegrapheditor_clipboard", JSON.stringify(c)); - app.canvas.pasteFromClipboard(); - localStorage.setItem("litegrapheditor_clipboard", backup); - - const [x, y] = this.node.pos; - let top; - let left; - // Configure nodes with current widget data - const selectedIds = ids.length ? ids : Object.keys(app.canvas.selected_nodes); - const newNodes = []; - for (let i = 0; i < selectedIds.length; i++) { - const id = selectedIds[i]; - const newNode = app.graph.getNodeById(id); - const innerNode = innerNodes[i]; - newNodes.push(newNode); - - if (left == null || newNode.pos[0] < left) { - left = newNode.pos[0]; - } - if (top == null || newNode.pos[1] < top) { - top = newNode.pos[1]; - } - - if (!newNode.widgets) continue; - - const map = this.groupData.oldToNewWidgetMap[innerNode.index]; - if (map) { - const widgets = Object.keys(map); - - for (const oldName of widgets) { - const newName = map[oldName]; - if (!newName) continue; - - const widgetIndex = this.node.widgets.findIndex((w) => w.name === newName); - if (widgetIndex === -1) continue; - - // Populate the main and any linked widgets - if (innerNode.type === "PrimitiveNode") { - for (let i = 0; i < newNode.widgets.length; i++) { - newNode.widgets[i].value = this.node.widgets[widgetIndex + i].value; - } - } else { - const outerWidget = this.node.widgets[widgetIndex]; - const newWidget = newNode.widgets.find((w) => w.name === oldName); - if (!newWidget) continue; - - newWidget.value = outerWidget.value; - for (let w = 0; w < outerWidget.linkedWidgets?.length; w++) { - newWidget.linkedWidgets[w].value = outerWidget.linkedWidgets[w].value; - } - } - } - } - } - - // Shift each node - for (const newNode of newNodes) { - newNode.pos = [newNode.pos[0] - (left - x), newNode.pos[1] - (top - y)]; - } - - return { newNodes, selectedIds }; - }; - - const reconnectInputs = (selectedIds) => { - for (const innerNodeIndex in this.groupData.oldToNewInputMap) { - const id = selectedIds[innerNodeIndex]; - const newNode = app.graph.getNodeById(id); - const map = this.groupData.oldToNewInputMap[innerNodeIndex]; - for (const innerInputId in map) { - const groupSlotId = map[innerInputId]; - if (groupSlotId == null) continue; - const slot = node.inputs[groupSlotId]; - if (slot.link == null) continue; - const link = app.graph.links[slot.link]; - if (!link) continue; - // connect this node output to the input of another node - const originNode = app.graph.getNodeById(link.origin_id); - originNode.connect(link.origin_slot, newNode, +innerInputId); - } - } - }; - - const reconnectOutputs = (selectedIds) => { - for (let groupOutputId = 0; groupOutputId < node.outputs?.length; groupOutputId++) { - const output = node.outputs[groupOutputId]; - if (!output.links) continue; - const links = [...output.links]; - for (const l of links) { - const slot = this.groupData.newToOldOutputMap[groupOutputId]; - const link = app.graph.links[l]; - const targetNode = app.graph.getNodeById(link.target_id); - const newNode = app.graph.getNodeById(selectedIds[slot.node.index]); - newNode.connect(slot.slot, targetNode, link.target_slot); - } - } - }; - - const { newNodes, selectedIds } = addInnerNodes(); - reconnectInputs(selectedIds); - reconnectOutputs(selectedIds); - app.graph.remove(this.node); - - return newNodes; - }; - - const getExtraMenuOptions = this.node.getExtraMenuOptions; - this.node.getExtraMenuOptions = function (_, options) { - getExtraMenuOptions?.apply(this, arguments); - - let optionIndex = options.findIndex((o) => o.content === "Outputs"); - if (optionIndex === -1) optionIndex = options.length; - else optionIndex++; - options.splice( - optionIndex, - 0, - null, - { - content: "Convert to nodes", - callback: () => { - return this.convertToNodes(); - }, - }, - { - content: "Manage Group Node", - callback: () => { - new ManageGroupDialog(app).show(this.type); - }, - } - ); - }; - - // Draw custom collapse icon to identity this as a group - const onDrawTitleBox = this.node.onDrawTitleBox; - this.node.onDrawTitleBox = function (ctx, height, size, scale) { - onDrawTitleBox?.apply(this, arguments); - - const fill = ctx.fillStyle; - ctx.beginPath(); - ctx.rect(11, -height + 11, 2, 2); - ctx.rect(14, -height + 11, 2, 2); - ctx.rect(17, -height + 11, 2, 2); - ctx.rect(11, -height + 14, 2, 2); - ctx.rect(14, -height + 14, 2, 2); - ctx.rect(17, -height + 14, 2, 2); - ctx.rect(11, -height + 17, 2, 2); - ctx.rect(14, -height + 17, 2, 2); - ctx.rect(17, -height + 17, 2, 2); - - ctx.fillStyle = this.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR; - ctx.fill(); - ctx.fillStyle = fill; - }; - - // Draw progress label - const onDrawForeground = node.onDrawForeground; - const groupData = this.groupData.nodeData; - node.onDrawForeground = function (ctx) { - const r = onDrawForeground?.apply?.(this, arguments); - if (+app.runningNodeId === this.id && this.runningInternalNodeId !== null) { - const n = groupData.nodes[this.runningInternalNodeId]; - if(!n) return; - const message = `Running ${n.title || n.type} (${this.runningInternalNodeId}/${groupData.nodes.length})`; - ctx.save(); - ctx.font = "12px sans-serif"; - const sz = ctx.measureText(message); - ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR; - ctx.beginPath(); - ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); - ctx.fill(); - - ctx.fillStyle = "#fff"; - ctx.fillText(message, 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); - ctx.restore(); - } - }; - - // Flag this node as needing to be reset - const onExecutionStart = this.node.onExecutionStart; - this.node.onExecutionStart = function () { - this.resetExecution = true; - return onExecutionStart?.apply(this, arguments); - }; - - const self = this; - const onNodeCreated = this.node.onNodeCreated; - this.node.onNodeCreated = function () { - if (!this.widgets) { - return; - } - const config = self.groupData.nodeData.config; - if (config) { - for (const n in config) { - const inputs = config[n]?.input; - for (const w in inputs) { - if (inputs[w].visible !== false) continue; - const widgetName = self.groupData.oldToNewWidgetMap[n][w]; - const widget = this.widgets.find((w) => w.name === widgetName); - if (widget) { - widget.type = "hidden"; - widget.computeSize = () => [0, -4]; - } - } - } - } - - return onNodeCreated?.apply(this, arguments); - }; - - function handleEvent(type, getId, getEvent) { - const handler = ({ detail }) => { - const id = getId(detail); - if (!id) return; - const node = app.graph.getNodeById(id); - if (node) return; - - const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == id); - if (innerNodeIndex > -1) { - this.node.runningInternalNodeId = innerNodeIndex; - api.dispatchEvent(new CustomEvent(type, { detail: getEvent(detail, this.node.id + "", this.node) })); - } - }; - api.addEventListener(type, handler); - return handler; - } - - const executing = handleEvent.call( - this, - "executing", - (d) => d, - (d, id, node) => id - ); - - const executed = handleEvent.call( - this, - "executed", - (d) => d?.node, - (d, id, node) => ({ ...d, node: id, merge: !node.resetExecution }) - ); - - const onRemoved = node.onRemoved; - this.node.onRemoved = function () { - onRemoved?.apply(this, arguments); - api.removeEventListener("executing", executing); - api.removeEventListener("executed", executed); - }; - - this.node.refreshComboInNode = (defs) => { - // Update combo widget options - for (const widgetName in this.groupData.newToOldWidgetMap) { - const widget = this.node.widgets.find((w) => w.name === widgetName); - if (widget?.type === "combo") { - const old = this.groupData.newToOldWidgetMap[widgetName]; - const def = defs[old.node.type]; - const input = def?.input?.required?.[old.inputName] ?? def?.input?.optional?.[old.inputName]; - if (!input) continue; - - widget.options.values = input[0]; - - if (old.inputName !== "image" && !widget.options.values.includes(widget.value)) { - widget.value = widget.options.values[0]; - widget.callback(widget.value); - } - } - } - }; - } - - updateInnerWidgets() { - for (const newWidgetName in this.groupData.newToOldWidgetMap) { - const newWidget = this.node.widgets.find((w) => w.name === newWidgetName); - if (!newWidget) continue; - - const newValue = newWidget.value; - const old = this.groupData.newToOldWidgetMap[newWidgetName]; - let innerNode = this.innerNodes[old.node.index]; - - if (innerNode.type === "PrimitiveNode") { - innerNode.primitiveValue = newValue; - const primitiveLinked = this.groupData.primitiveToWidget[old.node.index]; - for (const linked of primitiveLinked ?? []) { - const node = this.innerNodes[linked.nodeId]; - const widget = node.widgets.find((w) => w.name === linked.inputName); - - if (widget) { - widget.value = newValue; - } - } - continue; - } else if (innerNode.type === "Reroute") { - const rerouteLinks = this.groupData.linksFrom[old.node.index]; - if (rerouteLinks) { - for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) { - const node = this.innerNodes[targetNodeId]; - const input = node.inputs[targetSlot]; - if (input.widget) { - const widget = node.widgets?.find((w) => w.name === input.widget.name); - if (widget) { - widget.value = newValue; - } - } - } - } - } - - const widget = innerNode.widgets?.find((w) => w.name === old.inputName); - if (widget) { - widget.value = newValue; - } - } - } - - populatePrimitive(node, nodeId, oldName, i, linkedShift) { - // Converted widget, populate primitive if linked - const primitiveId = this.groupData.widgetToPrimitive[nodeId]?.[oldName]; - if (primitiveId == null) return; - const targetWidgetName = this.groupData.oldToNewWidgetMap[primitiveId]["value"]; - const targetWidgetIndex = this.node.widgets.findIndex((w) => w.name === targetWidgetName); - if (targetWidgetIndex > -1) { - const primitiveNode = this.innerNodes[primitiveId]; - let len = primitiveNode.widgets.length; - if (len - 1 !== this.node.widgets[targetWidgetIndex].linkedWidgets?.length) { - // Fallback handling for if some reason the primitive has a different number of widgets - // we dont want to overwrite random widgets, better to leave blank - len = 1; - } - for (let i = 0; i < len; i++) { - this.node.widgets[targetWidgetIndex + i].value = primitiveNode.widgets[i].value; - } - } - return true; - } - - populateReroute(node, nodeId, map) { - if (node.type !== "Reroute") return; - - const link = this.groupData.linksFrom[nodeId]?.[0]?.[0]; - if (!link) return; - const [, , targetNodeId, targetNodeSlot] = link; - const targetNode = this.groupData.nodeData.nodes[targetNodeId]; - const inputs = targetNode.inputs; - const targetWidget = inputs?.[targetNodeSlot]?.widget; - if (!targetWidget) return; - - const offset = inputs.length - (targetNode.widgets_values?.length ?? 0); - const v = targetNode.widgets_values?.[targetNodeSlot - offset]; - if (v == null) return; - - const widgetName = Object.values(map)[0]; - const widget = this.node.widgets.find((w) => w.name === widgetName); - if (widget) { - widget.value = v; - } - } - - populateWidgets() { - if (!this.node.widgets) return; - - for (let nodeId = 0; nodeId < this.groupData.nodeData.nodes.length; nodeId++) { - const node = this.groupData.nodeData.nodes[nodeId]; - const map = this.groupData.oldToNewWidgetMap[nodeId] ?? {}; - const widgets = Object.keys(map); - - if (!node.widgets_values?.length) { - // special handling for populating values into reroutes - // this allows primitives connect to them to pick up the correct value - this.populateReroute(node, nodeId, map); - continue; - } - - let linkedShift = 0; - for (let i = 0; i < widgets.length; i++) { - const oldName = widgets[i]; - const newName = map[oldName]; - const widgetIndex = this.node.widgets.findIndex((w) => w.name === newName); - const mainWidget = this.node.widgets[widgetIndex]; - if (this.populatePrimitive(node, nodeId, oldName, i, linkedShift) || widgetIndex === -1) { - // Find the inner widget and shift by the number of linked widgets as they will have been removed too - const innerWidget = this.innerNodes[nodeId].widgets?.find((w) => w.name === oldName); - linkedShift += innerWidget?.linkedWidgets?.length ?? 0; - } - if (widgetIndex === -1) { - continue; - } - - // Populate the main and any linked widget - mainWidget.value = node.widgets_values[i + linkedShift]; - for (let w = 0; w < mainWidget.linkedWidgets?.length; w++) { - this.node.widgets[widgetIndex + w + 1].value = node.widgets_values[i + ++linkedShift]; - } - } - } - } - - replaceNodes(nodes) { - let top; - let left; - - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - if (left == null || node.pos[0] < left) { - left = node.pos[0]; - } - if (top == null || node.pos[1] < top) { - top = node.pos[1]; - } - - this.linkOutputs(node, i); - app.graph.remove(node); - } - - this.linkInputs(); - this.node.pos = [left, top]; - } - - linkOutputs(originalNode, nodeId) { - if (!originalNode.outputs) return; - - for (const output of originalNode.outputs) { - if (!output.links) continue; - // Clone the links as they'll be changed if we reconnect - const links = [...output.links]; - for (const l of links) { - const link = app.graph.links[l]; - if (!link) continue; - - const targetNode = app.graph.getNodeById(link.target_id); - const newSlot = this.groupData.oldToNewOutputMap[nodeId]?.[link.origin_slot]; - if (newSlot != null) { - this.node.connect(newSlot, targetNode, link.target_slot); - } - } - } - } - - linkInputs() { - for (const link of this.groupData.nodeData.links ?? []) { - const [, originSlot, targetId, targetSlot, actualOriginId] = link; - const originNode = app.graph.getNodeById(actualOriginId); - if (!originNode) continue; // this node is in the group - originNode.connect(originSlot, this.node.id, this.groupData.oldToNewInputMap[targetId][targetSlot]); - } - } - - static getGroupData(node) { - return (node.nodeData ?? node.constructor?.nodeData)?.[GROUP]; - } - - static isGroupNode(node) { - return !!node.constructor?.nodeData?.[GROUP]; - } - - static async fromNodes(nodes) { - // Process the nodes into the stored workflow group node data - const builder = new GroupNodeBuilder(nodes); - const res = builder.build(); - if (!res) return; - - const { name, nodeData } = res; - - // Convert this data into a LG node definition and register it - const config = new GroupNodeConfig(name, nodeData); - await config.registerType(); - - const groupNode = LiteGraph.createNode(`workflow/${name}`); - // Reuse the existing nodes for this instance - groupNode.setInnerNodes(builder.nodes); - groupNode[GROUP].populateWidgets(); - app.graph.add(groupNode); - - // Remove all converted nodes and relink them - groupNode[GROUP].replaceNodes(builder.nodes); - return groupNode; - } + node; + groupData; + innerNodes: any; + + constructor(node) { + this.node = node; + this.groupData = node.constructor?.nodeData?.[GROUP]; + + this.node.setInnerNodes = (innerNodes) => { + this.innerNodes = innerNodes; + + for ( + let innerNodeIndex = 0; + innerNodeIndex < this.innerNodes.length; + innerNodeIndex++ + ) { + const innerNode = this.innerNodes[innerNodeIndex]; + + for (const w of innerNode.widgets ?? []) { + if (w.type === "converted-widget") { + w.serializeValue = w.origSerializeValue; + } + } + + innerNode.index = innerNodeIndex; + innerNode.getInputNode = (slot) => { + // Check if this input is internal or external + const externalSlot = + this.groupData.oldToNewInputMap[innerNode.index]?.[slot]; + if (externalSlot != null) { + return this.node.getInputNode(externalSlot); + } + + // Internal link + const innerLink = this.groupData.linksTo[innerNode.index]?.[slot]; + if (!innerLink) return null; + + const inputNode = innerNodes[innerLink[0]]; + // Primitives will already apply their values + if (inputNode.type === "PrimitiveNode") return null; + + return inputNode; + }; + + innerNode.getInputLink = (slot) => { + const externalSlot = + this.groupData.oldToNewInputMap[innerNode.index]?.[slot]; + if (externalSlot != null) { + // The inner node is connected via the group node inputs + const linkId = this.node.inputs[externalSlot].link; + let link = app.graph.links[linkId]; + + // Use the outer link, but update the target to the inner node + // @ts-ignore + // TODO: Fix this + link = { + ...link, + target_id: innerNode.id, + target_slot: +slot, + }; + return link; + } + + let link = this.groupData.linksTo[innerNode.index]?.[slot]; + if (!link) return null; + // Use the inner link, but update the origin node to be inner node id + link = { + origin_id: innerNodes[link[0]].id, + origin_slot: link[1], + target_id: innerNode.id, + target_slot: +slot, + }; + return link; + }; + } + }; + + this.node.updateLink = (link) => { + // Replace the group node reference with the internal node + link = { ...link }; + const output = this.groupData.newToOldOutputMap[link.origin_slot]; + let innerNode = this.innerNodes[output.node.index]; + let l; + while (innerNode?.type === "Reroute") { + l = innerNode.getInputLink(0); + innerNode = innerNode.getInputNode(0); + } + + if (!innerNode) { + return null; + } + + if (l && GroupNodeHandler.isGroupNode(innerNode)) { + return innerNode.updateLink(l); + } + + link.origin_id = innerNode.id; + link.origin_slot = l?.origin_slot ?? output.slot; + return link; + }; + + this.node.getInnerNodes = () => { + if (!this.innerNodes) { + this.node.setInnerNodes( + this.groupData.nodeData.nodes.map((n, i) => { + const innerNode = LiteGraph.createNode(n.type); + innerNode.configure(n); + // @ts-ignore + innerNode.id = `${this.node.id}:${i}`; + return innerNode; + }) + ); + } + + this.updateInnerWidgets(); + + return this.innerNodes; + }; + + this.node.recreate = async () => { + const id = this.node.id; + const sz = this.node.size; + const nodes = this.node.convertToNodes(); + + const groupNode = LiteGraph.createNode(this.node.type); + groupNode.id = id; + + // Reuse the existing nodes for this instance + groupNode.setInnerNodes(nodes); + groupNode[GROUP].populateWidgets(); + app.graph.add(groupNode); + groupNode.size = [ + Math.max(groupNode.size[0], sz[0]), + Math.max(groupNode.size[1], sz[1]), + ]; + + // Remove all converted nodes and relink them + groupNode[GROUP].replaceNodes(nodes); + return groupNode; + }; + + this.node.convertToNodes = () => { + const addInnerNodes = () => { + const backup = localStorage.getItem("litegrapheditor_clipboard"); + // Clone the node data so we dont mutate it for other nodes + const c = { ...this.groupData.nodeData }; + c.nodes = [...c.nodes]; + const innerNodes = this.node.getInnerNodes(); + let ids = []; + for (let i = 0; i < c.nodes.length; i++) { + let id = innerNodes?.[i]?.id; + // Use existing IDs if they are set on the inner nodes + if (id == null || isNaN(id)) { + id = undefined; + } else { + ids.push(id); + } + c.nodes[i] = { ...c.nodes[i], id }; + } + localStorage.setItem("litegrapheditor_clipboard", JSON.stringify(c)); + app.canvas.pasteFromClipboard(); + localStorage.setItem("litegrapheditor_clipboard", backup); + + const [x, y] = this.node.pos; + let top; + let left; + // Configure nodes with current widget data + const selectedIds = ids.length + ? ids + : Object.keys(app.canvas.selected_nodes); + const newNodes = []; + for (let i = 0; i < selectedIds.length; i++) { + const id = selectedIds[i]; + const newNode = app.graph.getNodeById(id); + const innerNode = innerNodes[i]; + newNodes.push(newNode); + + if (left == null || newNode.pos[0] < left) { + left = newNode.pos[0]; + } + if (top == null || newNode.pos[1] < top) { + top = newNode.pos[1]; + } + + if (!newNode.widgets) continue; + + const map = this.groupData.oldToNewWidgetMap[innerNode.index]; + if (map) { + const widgets = Object.keys(map); + + for (const oldName of widgets) { + const newName = map[oldName]; + if (!newName) continue; + + const widgetIndex = this.node.widgets.findIndex( + (w) => w.name === newName + ); + if (widgetIndex === -1) continue; + + // Populate the main and any linked widgets + if (innerNode.type === "PrimitiveNode") { + for (let i = 0; i < newNode.widgets.length; i++) { + newNode.widgets[i].value = + this.node.widgets[widgetIndex + i].value; + } + } else { + const outerWidget = this.node.widgets[widgetIndex]; + const newWidget = newNode.widgets.find( + (w) => w.name === oldName + ); + if (!newWidget) continue; + + newWidget.value = outerWidget.value; + for (let w = 0; w < outerWidget.linkedWidgets?.length; w++) { + newWidget.linkedWidgets[w].value = + outerWidget.linkedWidgets[w].value; + } + } + } + } + } + + // Shift each node + for (const newNode of newNodes) { + newNode.pos = [ + newNode.pos[0] - (left - x), + newNode.pos[1] - (top - y), + ]; + } + + return { newNodes, selectedIds }; + }; + + const reconnectInputs = (selectedIds) => { + for (const innerNodeIndex in this.groupData.oldToNewInputMap) { + const id = selectedIds[innerNodeIndex]; + const newNode = app.graph.getNodeById(id); + const map = this.groupData.oldToNewInputMap[innerNodeIndex]; + for (const innerInputId in map) { + const groupSlotId = map[innerInputId]; + if (groupSlotId == null) continue; + const slot = node.inputs[groupSlotId]; + if (slot.link == null) continue; + const link = app.graph.links[slot.link]; + if (!link) continue; + // connect this node output to the input of another node + const originNode = app.graph.getNodeById(link.origin_id); + originNode.connect(link.origin_slot, newNode, +innerInputId); + } + } + }; + + const reconnectOutputs = (selectedIds) => { + for ( + let groupOutputId = 0; + groupOutputId < node.outputs?.length; + groupOutputId++ + ) { + const output = node.outputs[groupOutputId]; + if (!output.links) continue; + const links = [...output.links]; + for (const l of links) { + const slot = this.groupData.newToOldOutputMap[groupOutputId]; + const link = app.graph.links[l]; + const targetNode = app.graph.getNodeById(link.target_id); + const newNode = app.graph.getNodeById(selectedIds[slot.node.index]); + newNode.connect(slot.slot, targetNode, link.target_slot); + } + } + }; + + const { newNodes, selectedIds } = addInnerNodes(); + reconnectInputs(selectedIds); + reconnectOutputs(selectedIds); + app.graph.remove(this.node); + + return newNodes; + }; + + const getExtraMenuOptions = this.node.getExtraMenuOptions; + this.node.getExtraMenuOptions = function (_, options) { + getExtraMenuOptions?.apply(this, arguments); + + let optionIndex = options.findIndex((o) => o.content === "Outputs"); + if (optionIndex === -1) optionIndex = options.length; + else optionIndex++; + options.splice( + optionIndex, + 0, + null, + { + content: "Convert to nodes", + callback: () => { + return this.convertToNodes(); + }, + }, + { + content: "Manage Group Node", + callback: () => { + new ManageGroupDialog(app).show(this.type); + }, + } + ); + }; + + // Draw custom collapse icon to identity this as a group + const onDrawTitleBox = this.node.onDrawTitleBox; + this.node.onDrawTitleBox = function (ctx, height, size, scale) { + onDrawTitleBox?.apply(this, arguments); + + const fill = ctx.fillStyle; + ctx.beginPath(); + ctx.rect(11, -height + 11, 2, 2); + ctx.rect(14, -height + 11, 2, 2); + ctx.rect(17, -height + 11, 2, 2); + ctx.rect(11, -height + 14, 2, 2); + ctx.rect(14, -height + 14, 2, 2); + ctx.rect(17, -height + 14, 2, 2); + ctx.rect(11, -height + 17, 2, 2); + ctx.rect(14, -height + 17, 2, 2); + ctx.rect(17, -height + 17, 2, 2); + + ctx.fillStyle = this.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR; + ctx.fill(); + ctx.fillStyle = fill; + }; + + // Draw progress label + const onDrawForeground = node.onDrawForeground; + const groupData = this.groupData.nodeData; + node.onDrawForeground = function (ctx) { + const r = onDrawForeground?.apply?.(this, arguments); + if ( + +app.runningNodeId === this.id && + this.runningInternalNodeId !== null + ) { + const n = groupData.nodes[this.runningInternalNodeId]; + if (!n) return; + const message = `Running ${n.title || n.type} (${this.runningInternalNodeId}/${groupData.nodes.length})`; + ctx.save(); + ctx.font = "12px sans-serif"; + const sz = ctx.measureText(message); + ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR; + ctx.beginPath(); + ctx.roundRect( + 0, + -LiteGraph.NODE_TITLE_HEIGHT - 20, + sz.width + 12, + 20, + 5 + ); + ctx.fill(); + + ctx.fillStyle = "#fff"; + ctx.fillText(message, 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); + ctx.restore(); + } + }; + + // Flag this node as needing to be reset + const onExecutionStart = this.node.onExecutionStart; + this.node.onExecutionStart = function () { + this.resetExecution = true; + return onExecutionStart?.apply(this, arguments); + }; + + const self = this; + const onNodeCreated = this.node.onNodeCreated; + this.node.onNodeCreated = function () { + if (!this.widgets) { + return; + } + const config = self.groupData.nodeData.config; + if (config) { + for (const n in config) { + const inputs = config[n]?.input; + for (const w in inputs) { + if (inputs[w].visible !== false) continue; + const widgetName = self.groupData.oldToNewWidgetMap[n][w]; + const widget = this.widgets.find((w) => w.name === widgetName); + if (widget) { + widget.type = "hidden"; + widget.computeSize = () => [0, -4]; + } + } + } + } + + return onNodeCreated?.apply(this, arguments); + }; + + function handleEvent(type, getId, getEvent) { + const handler = ({ detail }) => { + const id = getId(detail); + if (!id) return; + const node = app.graph.getNodeById(id); + if (node) return; + + const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == id); + if (innerNodeIndex > -1) { + this.node.runningInternalNodeId = innerNodeIndex; + api.dispatchEvent( + new CustomEvent(type, { + detail: getEvent(detail, this.node.id + "", this.node), + }) + ); + } + }; + api.addEventListener(type, handler); + return handler; + } + + const executing = handleEvent.call( + this, + "executing", + (d) => d, + (d, id, node) => id + ); + + const executed = handleEvent.call( + this, + "executed", + (d) => d?.node, + (d, id, node) => ({ ...d, node: id, merge: !node.resetExecution }) + ); + + const onRemoved = node.onRemoved; + this.node.onRemoved = function () { + onRemoved?.apply(this, arguments); + api.removeEventListener("executing", executing); + api.removeEventListener("executed", executed); + }; + + this.node.refreshComboInNode = (defs) => { + // Update combo widget options + for (const widgetName in this.groupData.newToOldWidgetMap) { + const widget = this.node.widgets.find((w) => w.name === widgetName); + if (widget?.type === "combo") { + const old = this.groupData.newToOldWidgetMap[widgetName]; + const def = defs[old.node.type]; + const input = + def?.input?.required?.[old.inputName] ?? + def?.input?.optional?.[old.inputName]; + if (!input) continue; + + widget.options.values = input[0]; + + if ( + old.inputName !== "image" && + !widget.options.values.includes(widget.value) + ) { + widget.value = widget.options.values[0]; + widget.callback(widget.value); + } + } + } + }; + } + + updateInnerWidgets() { + for (const newWidgetName in this.groupData.newToOldWidgetMap) { + const newWidget = this.node.widgets.find((w) => w.name === newWidgetName); + if (!newWidget) continue; + + const newValue = newWidget.value; + const old = this.groupData.newToOldWidgetMap[newWidgetName]; + let innerNode = this.innerNodes[old.node.index]; + + if (innerNode.type === "PrimitiveNode") { + innerNode.primitiveValue = newValue; + const primitiveLinked = + this.groupData.primitiveToWidget[old.node.index]; + for (const linked of primitiveLinked ?? []) { + const node = this.innerNodes[linked.nodeId]; + const widget = node.widgets.find((w) => w.name === linked.inputName); + + if (widget) { + widget.value = newValue; + } + } + continue; + } else if (innerNode.type === "Reroute") { + const rerouteLinks = this.groupData.linksFrom[old.node.index]; + if (rerouteLinks) { + for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) { + const node = this.innerNodes[targetNodeId]; + const input = node.inputs[targetSlot]; + if (input.widget) { + const widget = node.widgets?.find( + (w) => w.name === input.widget.name + ); + if (widget) { + widget.value = newValue; + } + } + } + } + } + + const widget = innerNode.widgets?.find((w) => w.name === old.inputName); + if (widget) { + widget.value = newValue; + } + } + } + + populatePrimitive(node, nodeId, oldName, i, linkedShift) { + // Converted widget, populate primitive if linked + const primitiveId = this.groupData.widgetToPrimitive[nodeId]?.[oldName]; + if (primitiveId == null) return; + const targetWidgetName = + this.groupData.oldToNewWidgetMap[primitiveId]["value"]; + const targetWidgetIndex = this.node.widgets.findIndex( + (w) => w.name === targetWidgetName + ); + if (targetWidgetIndex > -1) { + const primitiveNode = this.innerNodes[primitiveId]; + let len = primitiveNode.widgets.length; + if ( + len - 1 !== + this.node.widgets[targetWidgetIndex].linkedWidgets?.length + ) { + // Fallback handling for if some reason the primitive has a different number of widgets + // we dont want to overwrite random widgets, better to leave blank + len = 1; + } + for (let i = 0; i < len; i++) { + this.node.widgets[targetWidgetIndex + i].value = + primitiveNode.widgets[i].value; + } + } + return true; + } + + populateReroute(node, nodeId, map) { + if (node.type !== "Reroute") return; + + const link = this.groupData.linksFrom[nodeId]?.[0]?.[0]; + if (!link) return; + const [, , targetNodeId, targetNodeSlot] = link; + const targetNode = this.groupData.nodeData.nodes[targetNodeId]; + const inputs = targetNode.inputs; + const targetWidget = inputs?.[targetNodeSlot]?.widget; + if (!targetWidget) return; + + const offset = inputs.length - (targetNode.widgets_values?.length ?? 0); + const v = targetNode.widgets_values?.[targetNodeSlot - offset]; + if (v == null) return; + + const widgetName = Object.values(map)[0]; + const widget = this.node.widgets.find((w) => w.name === widgetName); + if (widget) { + widget.value = v; + } + } + + populateWidgets() { + if (!this.node.widgets) return; + + for ( + let nodeId = 0; + nodeId < this.groupData.nodeData.nodes.length; + nodeId++ + ) { + const node = this.groupData.nodeData.nodes[nodeId]; + const map = this.groupData.oldToNewWidgetMap[nodeId] ?? {}; + const widgets = Object.keys(map); + + if (!node.widgets_values?.length) { + // special handling for populating values into reroutes + // this allows primitives connect to them to pick up the correct value + this.populateReroute(node, nodeId, map); + continue; + } + + let linkedShift = 0; + for (let i = 0; i < widgets.length; i++) { + const oldName = widgets[i]; + const newName = map[oldName]; + const widgetIndex = this.node.widgets.findIndex( + (w) => w.name === newName + ); + const mainWidget = this.node.widgets[widgetIndex]; + if ( + this.populatePrimitive(node, nodeId, oldName, i, linkedShift) || + widgetIndex === -1 + ) { + // Find the inner widget and shift by the number of linked widgets as they will have been removed too + const innerWidget = this.innerNodes[nodeId].widgets?.find( + (w) => w.name === oldName + ); + linkedShift += innerWidget?.linkedWidgets?.length ?? 0; + } + if (widgetIndex === -1) { + continue; + } + + // Populate the main and any linked widget + mainWidget.value = node.widgets_values[i + linkedShift]; + for (let w = 0; w < mainWidget.linkedWidgets?.length; w++) { + this.node.widgets[widgetIndex + w + 1].value = + node.widgets_values[i + ++linkedShift]; + } + } + } + } + + replaceNodes(nodes) { + let top; + let left; + + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (left == null || node.pos[0] < left) { + left = node.pos[0]; + } + if (top == null || node.pos[1] < top) { + top = node.pos[1]; + } + + this.linkOutputs(node, i); + app.graph.remove(node); + } + + this.linkInputs(); + this.node.pos = [left, top]; + } + + linkOutputs(originalNode, nodeId) { + if (!originalNode.outputs) return; + + for (const output of originalNode.outputs) { + if (!output.links) continue; + // Clone the links as they'll be changed if we reconnect + const links = [...output.links]; + for (const l of links) { + const link = app.graph.links[l]; + if (!link) continue; + + const targetNode = app.graph.getNodeById(link.target_id); + const newSlot = + this.groupData.oldToNewOutputMap[nodeId]?.[link.origin_slot]; + if (newSlot != null) { + this.node.connect(newSlot, targetNode, link.target_slot); + } + } + } + } + + linkInputs() { + for (const link of this.groupData.nodeData.links ?? []) { + const [, originSlot, targetId, targetSlot, actualOriginId] = link; + const originNode = app.graph.getNodeById(actualOriginId); + if (!originNode) continue; // this node is in the group + originNode.connect( + originSlot, + this.node.id, + this.groupData.oldToNewInputMap[targetId][targetSlot] + ); + } + } + + static getGroupData(node) { + return (node.nodeData ?? node.constructor?.nodeData)?.[GROUP]; + } + + static isGroupNode(node) { + return !!node.constructor?.nodeData?.[GROUP]; + } + + static async fromNodes(nodes) { + // Process the nodes into the stored workflow group node data + const builder = new GroupNodeBuilder(nodes); + const res = builder.build(); + if (!res) return; + + const { name, nodeData } = res; + + // Convert this data into a LG node definition and register it + const config = new GroupNodeConfig(name, nodeData); + await config.registerType(); + + const groupNode = LiteGraph.createNode(`workflow/${name}`); + // Reuse the existing nodes for this instance + groupNode.setInnerNodes(builder.nodes); + groupNode[GROUP].populateWidgets(); + app.graph.add(groupNode); + + // Remove all converted nodes and relink them + groupNode[GROUP].replaceNodes(builder.nodes); + return groupNode; + } } function addConvertToGroupOptions() { - function addConvertOption(options, index) { - const selected = Object.values(app.canvas.selected_nodes ?? {}); - const disabled = selected.length < 2 || selected.find((n) => GroupNodeHandler.isGroupNode(n)); - options.splice(index + 1, null, { - content: `Convert to Group Node`, - disabled, - callback: async () => { - return await GroupNodeHandler.fromNodes(selected); - }, - }); - } + function addConvertOption(options, index) { + const selected = Object.values(app.canvas.selected_nodes ?? {}); + const disabled = + selected.length < 2 || + selected.find((n) => GroupNodeHandler.isGroupNode(n)); + options.splice(index + 1, null, { + content: `Convert to Group Node`, + disabled, + callback: async () => { + return await GroupNodeHandler.fromNodes(selected); + }, + }); + } - function addManageOption(options, index) { - const groups = app.graph.extra?.groupNodes; - const disabled = !groups || !Object.keys(groups).length; - options.splice(index + 1, null, { - content: `Manage Group Nodes`, - disabled, - callback: () => { - new ManageGroupDialog(app).show(); - }, - }); - } + function addManageOption(options, index) { + const groups = app.graph.extra?.groupNodes; + const disabled = !groups || !Object.keys(groups).length; + options.splice(index + 1, null, { + content: `Manage Group Nodes`, + disabled, + callback: () => { + new ManageGroupDialog(app).show(); + }, + }); + } - // Add to canvas - // @ts-ignore - const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions; - // @ts-ignore - LGraphCanvas.prototype.getCanvasMenuOptions = function () { - const options = getCanvasMenuOptions.apply(this, arguments); - const index = options.findIndex((o) => o?.content === "Add Group") + 1 || options.length; - addConvertOption(options, index); - addManageOption(options, index + 1); - return options; - }; + // Add to canvas + // @ts-ignore + const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions; + // @ts-ignore + LGraphCanvas.prototype.getCanvasMenuOptions = function () { + const options = getCanvasMenuOptions.apply(this, arguments); + const index = + options.findIndex((o) => o?.content === "Add Group") + 1 || + options.length; + addConvertOption(options, index); + addManageOption(options, index + 1); + return options; + }; - // Add to nodes - // @ts-ignore - const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions; - // @ts-ignore - LGraphCanvas.prototype.getNodeMenuOptions = function (node) { - const options = getNodeMenuOptions.apply(this, arguments); - if (!GroupNodeHandler.isGroupNode(node)) { - const index = options.findIndex((o) => o?.content === "Outputs") + 1 || options.length - 1; - addConvertOption(options, index); - } - return options; - }; + // Add to nodes + // @ts-ignore + const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions; + // @ts-ignore + LGraphCanvas.prototype.getNodeMenuOptions = function (node) { + const options = getNodeMenuOptions.apply(this, arguments); + if (!GroupNodeHandler.isGroupNode(node)) { + const index = + options.findIndex((o) => o?.content === "Outputs") + 1 || + options.length - 1; + addConvertOption(options, index); + } + return options; + }; } const id = "Comfy.GroupNode"; let globalDefs; const ext = { - name: id, - setup() { - addConvertToGroupOptions(); - }, - async beforeConfigureGraph(graphData, missingNodeTypes) { - const nodes = graphData?.extra?.groupNodes; - if (nodes) { - await GroupNodeConfig.registerFromWorkflow(nodes, missingNodeTypes); - } - }, - addCustomNodeDefs(defs) { - // Store this so we can mutate it later with group nodes - globalDefs = defs; - }, - nodeCreated(node) { - if (GroupNodeHandler.isGroupNode(node)) { - node[GROUP] = new GroupNodeHandler(node); - } - }, - async refreshComboInNodes(defs) { - // Re-register group nodes so new ones are created with the correct options - Object.assign(globalDefs, defs); - const nodes = app.graph.extra?.groupNodes; - if (nodes) { - await GroupNodeConfig.registerFromWorkflow(nodes, {}); - } - } + name: id, + setup() { + addConvertToGroupOptions(); + }, + async beforeConfigureGraph(graphData, missingNodeTypes) { + const nodes = graphData?.extra?.groupNodes; + if (nodes) { + await GroupNodeConfig.registerFromWorkflow(nodes, missingNodeTypes); + } + }, + addCustomNodeDefs(defs) { + // Store this so we can mutate it later with group nodes + globalDefs = defs; + }, + nodeCreated(node) { + if (GroupNodeHandler.isGroupNode(node)) { + node[GROUP] = new GroupNodeHandler(node); + } + }, + async refreshComboInNodes(defs) { + // Re-register group nodes so new ones are created with the correct options + Object.assign(globalDefs, defs); + const nodes = app.graph.extra?.groupNodes; + if (nodes) { + await GroupNodeConfig.registerFromWorkflow(nodes, {}); + } + }, }; app.registerExtension(ext); diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts index 0e770fc69..b76ab38aa 100644 --- a/src/extensions/core/groupNodeManage.ts +++ b/src/extensions/core/groupNodeManage.ts @@ -5,7 +5,6 @@ import "./groupNodeManage.css"; import { app, type ComfyApp } from "../../scripts/app"; import type { LGraphNode, LGraphNodeConstructor } from "/types/litegraph"; - const ORDER: symbol = Symbol(); function merge(target, source) { @@ -26,11 +25,23 @@ function merge(target, source) { } export class ManageGroupDialog extends ComfyDialog { - tabs: Record<"Inputs" | "Outputs" | "Widgets", {tab: HTMLAnchorElement, page: HTMLElement}>; + tabs: Record< + "Inputs" | "Outputs" | "Widgets", + { tab: HTMLAnchorElement; page: HTMLElement } + >; selectedNodeIndex: number | null | undefined; selectedTab: keyof ManageGroupDialog["tabs"] = "Inputs"; selectedGroup: string | undefined; - modifications: Record>> = {}; + modifications: Record< + string, + Record< + string, + Record< + string, + { name?: string | undefined; visible?: boolean | undefined } + > + > + > = {}; nodeItems: any[]; app: ComfyApp; groupNodeType: LGraphNodeConstructor; @@ -86,7 +97,8 @@ export class ManageGroupDialog extends ComfyDialog { } getGroupData() { - this.groupNodeType = LiteGraph.registered_node_types["workflow/" + this.selectedGroup]; + this.groupNodeType = + LiteGraph.registered_node_types["workflow/" + this.selectedGroup]; this.groupNodeDef = this.groupNodeType.nodeData; this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType); } @@ -131,24 +143,39 @@ export class ManageGroupDialog extends ComfyDialog { this.changeNode(0); } else { const items = this.draggable.getAllItems(); - let index = items.findIndex(item => item.classList.contains("selected")); - if(index === -1) index = this.selectedNodeIndex; + let index = items.findIndex((item) => + item.classList.contains("selected") + ); + if (index === -1) index = this.selectedNodeIndex; this.changeNode(index, true); } const ordered = [...nodes]; this.draggable?.dispose(); this.draggable = new DraggableList(this.innerNodesList, "li"); - this.draggable.addEventListener("dragend", ({ detail: { oldPosition, newPosition } }) => { - if (oldPosition === newPosition) return; - ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]); - for (let i = 0; i < ordered.length; i++) { - this.storeModification({ nodeIndex: ordered[i].index, section: ORDER, prop: "order", value: i }); + this.draggable.addEventListener( + "dragend", + ({ detail: { oldPosition, newPosition } }) => { + if (oldPosition === newPosition) return; + ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]); + for (let i = 0; i < ordered.length; i++) { + this.storeModification({ + nodeIndex: ordered[i].index, + section: ORDER, + prop: "order", + value: i, + }); + } } - }); + ); } - storeModification(props: { nodeIndex?: number; section: symbol; prop: string; value: any }) { + storeModification(props: { + nodeIndex?: number; + section: symbol; + prop: string; + value: any; + }) { const { nodeIndex, section, prop, value } = props; const groupMod = (this.modifications[this.selectedGroup] ??= {}); const nodesMod = (groupMod.nodes ??= {}); @@ -165,7 +192,10 @@ export class ManageGroupDialog extends ComfyDialog { getEditElement(section, prop, value, placeholder, checked, checkable = true) { if (value === placeholder) value = ""; - const mods = this.modifications[this.selectedGroup]?.nodes?.[this.selectedNodeInnerIndex]?.[section]?.[prop]; + const mods = + this.modifications[this.selectedGroup]?.nodes?.[ + this.selectedNodeInnerIndex + ]?.[section]?.[prop]; if (mods) { if (mods.name != null) { value = mods.name; @@ -181,7 +211,11 @@ export class ManageGroupDialog extends ComfyDialog { placeholder, type: "text", onchange: (e) => { - this.storeModification({ section, prop, value: { name: e.target.value } }); + this.storeModification({ + section, + prop, + value: { name: e.target.value }, + }); }, }), $el("label", { textContent: "Visible" }, [ @@ -190,7 +224,11 @@ export class ManageGroupDialog extends ComfyDialog { checked, disabled: !checkable, onchange: (e) => { - this.storeModification({ section, prop, value: { visible: !!e.target.checked } }); + this.storeModification({ + section, + prop, + value: { visible: !!e.target.checked }, + }); }, }), ]), @@ -198,13 +236,20 @@ export class ManageGroupDialog extends ComfyDialog { } buildWidgetsPage() { - const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]; + const widgets = + this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]; const items = Object.keys(widgets ?? {}); const type = app.graph.extra.groupNodes[this.selectedGroup]; const config = type.config?.[this.selectedNodeInnerIndex]?.input; this.widgetsPage.replaceChildren( ...items.map((oldName) => { - return this.getEditElement("input", oldName, widgets[oldName], oldName, config?.[oldName]?.visible !== false); + return this.getEditElement( + "input", + oldName, + widgets[oldName], + oldName, + config?.[oldName]?.visible !== false + ); }) ); return !!items.length; @@ -223,7 +268,13 @@ export class ManageGroupDialog extends ComfyDialog { return; } - return this.getEditElement("input", oldName, value, oldName, config?.[oldName]?.visible !== false); + return this.getEditElement( + "input", + oldName, + value, + oldName, + config?.[oldName]?.visible !== false + ); }) .filter(Boolean) ); @@ -232,9 +283,12 @@ export class ManageGroupDialog extends ComfyDialog { buildOutputsPage() { const nodes = this.groupData.nodeData.nodes; - const innerNodeDef = this.groupData.getNodeDef(nodes[this.selectedNodeInnerIndex]); + const innerNodeDef = this.groupData.getNodeDef( + nodes[this.selectedNodeInnerIndex] + ); const outputs = innerNodeDef?.output ?? []; - const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex]; + const groupOutputs = + this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex]; const type = app.graph.extra.groupNodes[this.selectedGroup]; const config = type.config?.[this.selectedNodeInnerIndex]?.output; @@ -250,7 +304,14 @@ export class ManageGroupDialog extends ComfyDialog { if (!value || value === oldName) { value = ""; } - return this.getEditElement("output", slot, value, oldName, visible, checkable); + return this.getEditElement( + "output", + slot, + value, + oldName, + visible, + checkable + ); }) .filter(Boolean) ); @@ -258,13 +319,21 @@ export class ManageGroupDialog extends ComfyDialog { } show(type?) { - const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort((a, b) => a.localeCompare(b)); + const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort( + (a, b) => a.localeCompare(b) + ); - this.innerNodesList = $el("ul.comfy-group-manage-list-items") as HTMLUListElement; + this.innerNodesList = $el( + "ul.comfy-group-manage-list-items" + ) as HTMLUListElement; this.widgetsPage = $el("section.comfy-group-manage-node-page"); this.inputsPage = $el("section.comfy-group-manage-node-page"); this.outputsPage = $el("section.comfy-group-manage-node-page"); - const pages = $el("div", [this.widgetsPage, this.inputsPage, this.outputsPage]); + const pages = $el("div", [ + this.widgetsPage, + this.inputsPage, + this.outputsPage, + ]); this.tabs = [ ["Inputs", this.inputsPage], @@ -318,12 +387,20 @@ export class ManageGroupDialog extends ComfyDialog { { onclick: (e) => { // @ts-ignore - const node = app.graph._nodes.find((n) => n.type === "workflow/" + this.selectedGroup); + const node = app.graph._nodes.find( + (n) => n.type === "workflow/" + this.selectedGroup + ); if (node) { - alert("This group node is in use in the current workflow, please first remove these."); + alert( + "This group node is in use in the current workflow, please first remove these." + ); return; } - if (confirm(`Are you sure you want to remove the node: "${this.selectedGroup}"`)) { + if ( + confirm( + `Are you sure you want to remove the node: "${this.selectedGroup}"` + ) + ) { delete app.graph.extra.groupNodes[this.selectedGroup]; LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup); } @@ -416,16 +493,22 @@ export class ManageGroupDialog extends ComfyDialog { }, "Save" ), - $el("button.comfy-btn", { onclick: () => this.element.close() }, "Close"), + $el( + "button.comfy-btn", + { onclick: () => this.element.close() }, + "Close" + ), ]), ]); this.element.replaceChildren(outer); - this.changeGroup(type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]); + this.changeGroup( + type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0] + ); this.element.showModal(); this.element.addEventListener("close", () => { this.draggable?.dispose(); }); } -} \ No newline at end of file +} diff --git a/src/extensions/core/groupOptions.ts b/src/extensions/core/groupOptions.ts index 5bf1bf6ed..f1e86f067 100644 --- a/src/extensions/core/groupOptions.ts +++ b/src/extensions/core/groupOptions.ts @@ -1,262 +1,265 @@ -import {app} from "../../scripts/app"; +import { app } from "../../scripts/app"; function setNodeMode(node, mode) { - node.mode = mode; - node.graph.change(); + node.mode = mode; + node.graph.change(); } -function addNodesToGroup(group, nodes=[]) { - var x1, y1, x2, y2; - var nx1, ny1, nx2, ny2; - var node; +function addNodesToGroup(group, nodes = []) { + var x1, y1, x2, y2; + var nx1, ny1, nx2, ny2; + var node; - x1 = y1 = x2 = y2 = -1; - nx1 = ny1 = nx2 = ny2 = -1; + x1 = y1 = x2 = y2 = -1; + nx1 = ny1 = nx2 = ny2 = -1; - for (var n of [group._nodes, nodes]) { - for (var i in n) { - node = n[i] + for (var n of [group._nodes, nodes]) { + for (var i in n) { + node = n[i]; - nx1 = node.pos[0] - ny1 = node.pos[1] - nx2 = node.pos[0] + node.size[0] - ny2 = node.pos[1] + node.size[1] + nx1 = node.pos[0]; + ny1 = node.pos[1]; + nx2 = node.pos[0] + node.size[0]; + ny2 = node.pos[1] + node.size[1]; - if (node.type != "Reroute") { - ny1 -= LiteGraph.NODE_TITLE_HEIGHT; - } + if (node.type != "Reroute") { + ny1 -= LiteGraph.NODE_TITLE_HEIGHT; + } - if (node.flags?.collapsed) { - ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT; + if (node.flags?.collapsed) { + ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT; - if (node?._collapsed_width) { - nx2 = nx1 + Math.round(node._collapsed_width); - } - } - - if (x1 == -1 || nx1 < x1) { - x1 = nx1; - } - - if (y1 == -1 || ny1 < y1) { - y1 = ny1; - } - - if (x2 == -1 || nx2 > x2) { - x2 = nx2; - } - - if (y2 == -1 || ny2 > y2) { - y2 = ny2; - } + if (node?._collapsed_width) { + nx2 = nx1 + Math.round(node._collapsed_width); } + } + + if (x1 == -1 || nx1 < x1) { + x1 = nx1; + } + + if (y1 == -1 || ny1 < y1) { + y1 = ny1; + } + + if (x2 == -1 || nx2 > x2) { + x2 = nx2; + } + + if (y2 == -1 || ny2 > y2) { + y2 = ny2; + } } + } - var padding = 10; + var padding = 10; - y1 = y1 - Math.round(group.font_size * 1.4); + y1 = y1 - Math.round(group.font_size * 1.4); - group.pos = [x1 - padding, y1 - padding]; - group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2]; + group.pos = [x1 - padding, y1 - padding]; + group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2]; } app.registerExtension({ - name: "Comfy.GroupOptions", - setup() { - // @ts-ignore - const orig = LGraphCanvas.prototype.getCanvasMenuOptions; - // graph_mouse - // @ts-ignore - LGraphCanvas.prototype.getCanvasMenuOptions = function () { - const options = orig.apply(this, arguments); - const group = this.graph.getGroupOnPos(this.graph_mouse[0], this.graph_mouse[1]); - if (!group) { - options.push({ - content: "Add Group For Selected Nodes", - disabled: !Object.keys(app.canvas.selected_nodes || {}).length, - callback: () => { - // @ts-ignore - var group = new LiteGraph.LGraphGroup(); - addNodesToGroup(group, this.selected_nodes) - app.canvas.graph.add(group); - this.graph.change(); - } - }); + name: "Comfy.GroupOptions", + setup() { + // @ts-ignore + const orig = LGraphCanvas.prototype.getCanvasMenuOptions; + // graph_mouse + // @ts-ignore + LGraphCanvas.prototype.getCanvasMenuOptions = function () { + const options = orig.apply(this, arguments); + const group = this.graph.getGroupOnPos( + this.graph_mouse[0], + this.graph_mouse[1] + ); + if (!group) { + options.push({ + content: "Add Group For Selected Nodes", + disabled: !Object.keys(app.canvas.selected_nodes || {}).length, + callback: () => { + // @ts-ignore + var group = new LiteGraph.LGraphGroup(); + addNodesToGroup(group, this.selected_nodes); + app.canvas.graph.add(group); + this.graph.change(); + }, + }); - return options; - } + return options; + } - // Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date - group.recomputeInsideNodes(); - const nodesInGroup = group._nodes; + // Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date + group.recomputeInsideNodes(); + const nodesInGroup = group._nodes; - options.push({ - content: "Add Selected Nodes To Group", - disabled: !Object.keys(app.canvas.selected_nodes || {}).length, - callback: () => { - addNodesToGroup(group, this.selected_nodes) - this.graph.change(); - } - }); + options.push({ + content: "Add Selected Nodes To Group", + disabled: !Object.keys(app.canvas.selected_nodes || {}).length, + callback: () => { + addNodesToGroup(group, this.selected_nodes); + this.graph.change(); + }, + }); - // No nodes in group, return default options - if (nodesInGroup.length === 0) { - return options; - } else { - // Add a separator between the default options and the group options - options.push(null); - } + // No nodes in group, return default options + if (nodesInGroup.length === 0) { + return options; + } else { + // Add a separator between the default options and the group options + options.push(null); + } - // Check if all nodes are the same mode - let allNodesAreSameMode = true; - for (let i = 1; i < nodesInGroup.length; i++) { - if (nodesInGroup[i].mode !== nodesInGroup[0].mode) { - allNodesAreSameMode = false; - break; - } - } - - options.push({ - content: "Fit Group To Nodes", - callback: () => { - addNodesToGroup(group) - this.graph.change(); - } - }); - - options.push({ - content: "Select Nodes", - callback: () => { - this.selectNodes(nodesInGroup); - this.graph.change(); - this.canvas.focus(); - } - }); - - // Modes - // 0: Always - // 1: On Event - // 2: Never - // 3: On Trigger - // 4: Bypass - // If all nodes are the same mode, add a menu option to change the mode - if (allNodesAreSameMode) { - const mode = nodesInGroup[0].mode; - switch (mode) { - case 0: - // All nodes are always, option to disable, and bypass - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - break; - case 2: - // All nodes are never, option to enable, and bypass - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - break; - case 4: - // All nodes are bypass, option to enable, and disable - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - break; - default: - // All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - break; - } - } else { - // Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - } - - return options + // Check if all nodes are the same mode + let allNodesAreSameMode = true; + for (let i = 1; i < nodesInGroup.length; i++) { + if (nodesInGroup[i].mode !== nodesInGroup[0].mode) { + allNodesAreSameMode = false; + break; } - } + } + + options.push({ + content: "Fit Group To Nodes", + callback: () => { + addNodesToGroup(group); + this.graph.change(); + }, + }); + + options.push({ + content: "Select Nodes", + callback: () => { + this.selectNodes(nodesInGroup); + this.graph.change(); + this.canvas.focus(); + }, + }); + + // Modes + // 0: Always + // 1: On Event + // 2: Never + // 3: On Trigger + // 4: Bypass + // If all nodes are the same mode, add a menu option to change the mode + if (allNodesAreSameMode) { + const mode = nodesInGroup[0].mode; + switch (mode) { + case 0: + // All nodes are always, option to disable, and bypass + options.push({ + content: "Set Group Nodes to Never", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 2); + } + }, + }); + options.push({ + content: "Bypass Group Nodes", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 4); + } + }, + }); + break; + case 2: + // All nodes are never, option to enable, and bypass + options.push({ + content: "Set Group Nodes to Always", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 0); + } + }, + }); + options.push({ + content: "Bypass Group Nodes", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 4); + } + }, + }); + break; + case 4: + // All nodes are bypass, option to enable, and disable + options.push({ + content: "Set Group Nodes to Always", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 0); + } + }, + }); + options.push({ + content: "Set Group Nodes to Never", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 2); + } + }, + }); + break; + default: + // All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass + options.push({ + content: "Set Group Nodes to Always", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 0); + } + }, + }); + options.push({ + content: "Set Group Nodes to Never", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 2); + } + }, + }); + options.push({ + content: "Bypass Group Nodes", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 4); + } + }, + }); + break; + } + } else { + // Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass + options.push({ + content: "Set Group Nodes to Always", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 0); + } + }, + }); + options.push({ + content: "Set Group Nodes to Never", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 2); + } + }, + }); + options.push({ + content: "Bypass Group Nodes", + callback: () => { + for (const node of nodesInGroup) { + setNodeMode(node, 4); + } + }, + }); + } + + return options; + }; + }, }); diff --git a/src/extensions/core/invertMenuScrolling.ts b/src/extensions/core/invertMenuScrolling.ts index a6662bffc..38a36a269 100644 --- a/src/extensions/core/invertMenuScrolling.ts +++ b/src/extensions/core/invertMenuScrolling.ts @@ -4,34 +4,34 @@ import { app } from "../../scripts/app"; const id = "Comfy.InvertMenuScrolling"; app.registerExtension({ - name: id, - init() { - const ctxMenu = LiteGraph.ContextMenu; - const replace = () => { - // @ts-ignore - LiteGraph.ContextMenu = function (values, options) { - options = options || {}; - if (options.scroll_speed) { - options.scroll_speed *= -1; - } else { - options.scroll_speed = -0.1; - } - return ctxMenu.call(this, values, options); - }; - LiteGraph.ContextMenu.prototype = ctxMenu.prototype; - }; - app.ui.settings.addSetting({ - id, - name: "Invert Menu Scrolling", - type: "boolean", - defaultValue: false, - onChange(value) { - if (value) { - replace(); - } else { - LiteGraph.ContextMenu = ctxMenu; - } - }, - }); - }, + name: id, + init() { + const ctxMenu = LiteGraph.ContextMenu; + const replace = () => { + // @ts-ignore + LiteGraph.ContextMenu = function (values, options) { + options = options || {}; + if (options.scroll_speed) { + options.scroll_speed *= -1; + } else { + options.scroll_speed = -0.1; + } + return ctxMenu.call(this, values, options); + }; + LiteGraph.ContextMenu.prototype = ctxMenu.prototype; + }; + app.ui.settings.addSetting({ + id, + name: "Invert Menu Scrolling", + type: "boolean", + defaultValue: false, + onChange(value) { + if (value) { + replace(); + } else { + LiteGraph.ContextMenu = ctxMenu; + } + }, + }); + }, }); diff --git a/src/extensions/core/keybinds.ts b/src/extensions/core/keybinds.ts index 1d0365a48..1c22d556e 100644 --- a/src/extensions/core/keybinds.ts +++ b/src/extensions/core/keybinds.ts @@ -1,69 +1,73 @@ -import {app} from "../../scripts/app"; +import { app } from "../../scripts/app"; app.registerExtension({ - name: "Comfy.Keybinds", - init() { - const keybindListener = function (event) { - const modifierPressed = event.ctrlKey || event.metaKey; + name: "Comfy.Keybinds", + init() { + const keybindListener = function (event) { + const modifierPressed = event.ctrlKey || event.metaKey; - // Queue prompt using ctrl or command + enter - if (modifierPressed && event.key === "Enter") { - app.queuePrompt(event.shiftKey ? -1 : 0).then(); - return; - } + // Queue prompt using ctrl or command + enter + if (modifierPressed && event.key === "Enter") { + app.queuePrompt(event.shiftKey ? -1 : 0).then(); + return; + } - const target = event.composedPath()[0]; - if (["INPUT", "TEXTAREA"].includes(target.tagName)) { - return; - } + const target = event.composedPath()[0]; + if (["INPUT", "TEXTAREA"].includes(target.tagName)) { + return; + } - const modifierKeyIdMap = { - s: "#comfy-save-button", - o: "#comfy-file-input", - Backspace: "#comfy-clear-button", - d: "#comfy-load-default-button", - }; + const modifierKeyIdMap = { + s: "#comfy-save-button", + o: "#comfy-file-input", + Backspace: "#comfy-clear-button", + d: "#comfy-load-default-button", + }; - const modifierKeybindId = modifierKeyIdMap[event.key]; - if (modifierPressed && modifierKeybindId) { - event.preventDefault(); + const modifierKeybindId = modifierKeyIdMap[event.key]; + if (modifierPressed && modifierKeybindId) { + event.preventDefault(); - const elem = document.querySelector(modifierKeybindId); - elem.click(); - return; - } + const elem = document.querySelector(modifierKeybindId); + elem.click(); + return; + } - // Finished Handling all modifier keybinds, now handle the rest - if (event.ctrlKey || event.altKey || event.metaKey) { - return; - } + // Finished Handling all modifier keybinds, now handle the rest + if (event.ctrlKey || event.altKey || event.metaKey) { + return; + } - // Close out of modals using escape - if (event.key === "Escape") { - const modals = document.querySelectorAll(".comfy-modal"); - const modal = Array.from(modals).find(modal => window.getComputedStyle(modal).getPropertyValue("display") !== "none"); - if (modal) { - modal.style.display = "none"; - } + // Close out of modals using escape + if (event.key === "Escape") { + const modals = document.querySelectorAll(".comfy-modal"); + const modal = Array.from(modals).find( + (modal) => + window.getComputedStyle(modal).getPropertyValue("display") !== + "none" + ); + if (modal) { + modal.style.display = "none"; + } - [...document.querySelectorAll("dialog")].forEach(d => { - d.close(); - }); - } + [...document.querySelectorAll("dialog")].forEach((d) => { + d.close(); + }); + } - const keyIdMap = { - q: "#comfy-view-queue-button", - h: "#comfy-view-history-button", - r: "#comfy-refresh-button", - }; + const keyIdMap = { + q: "#comfy-view-queue-button", + h: "#comfy-view-history-button", + r: "#comfy-refresh-button", + }; - const buttonId = keyIdMap[event.key]; - if (buttonId) { - const button = document.querySelector(buttonId); - button.click(); - } - } + const buttonId = keyIdMap[event.key]; + if (buttonId) { + const button = document.querySelector(buttonId); + button.click(); + } + }; - window.addEventListener("keydown", keybindListener, true); - } + window.addEventListener("keydown", keybindListener, true); + }, }); diff --git a/src/extensions/core/linkRenderMode.ts b/src/extensions/core/linkRenderMode.ts index e8520d2b6..c269e4652 100644 --- a/src/extensions/core/linkRenderMode.ts +++ b/src/extensions/core/linkRenderMode.ts @@ -2,25 +2,25 @@ import { app } from "../../scripts/app"; const id = "Comfy.LinkRenderMode"; const ext = { - name: id, - async setup(app) { - app.ui.settings.addSetting({ - id, - name: "Link Render Mode", - defaultValue: 2, - type: "combo", - // @ts-ignore - options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({ - value: i, - text: m, - selected: i == app.canvas.links_render_mode, - })), - onChange(value) { - app.canvas.links_render_mode = +value; - app.graph.setDirtyCanvas(true); - }, - }); - }, + name: id, + async setup(app) { + app.ui.settings.addSetting({ + id, + name: "Link Render Mode", + defaultValue: 2, + type: "combo", + // @ts-ignore + options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({ + value: i, + text: m, + selected: i == app.canvas.links_render_mode, + })), + onChange(value) { + app.canvas.links_render_mode = +value; + app.graph.setDirtyCanvas(true); + }, + }); + }, }; app.registerExtension(ext); diff --git a/src/extensions/core/maskeditor.ts b/src/extensions/core/maskeditor.ts index 01ef4760d..39cc9c555 100644 --- a/src/extensions/core/maskeditor.ts +++ b/src/extensions/core/maskeditor.ts @@ -6,988 +6,1068 @@ import { ClipspaceDialog } from "./clipspace"; // Helper function to convert a data URL to a Blob object function dataURLToBlob(dataURL) { - const parts = dataURL.split(';base64,'); - const contentType = parts[0].split(':')[1]; - const byteString = atob(parts[1]); - const arrayBuffer = new ArrayBuffer(byteString.length); - const uint8Array = new Uint8Array(arrayBuffer); - for (let i = 0; i < byteString.length; i++) { - uint8Array[i] = byteString.charCodeAt(i); - } - return new Blob([arrayBuffer], { type: contentType }); + const parts = dataURL.split(";base64,"); + const contentType = parts[0].split(":")[1]; + const byteString = atob(parts[1]); + const arrayBuffer = new ArrayBuffer(byteString.length); + const uint8Array = new Uint8Array(arrayBuffer); + for (let i = 0; i < byteString.length; i++) { + uint8Array[i] = byteString.charCodeAt(i); + } + return new Blob([arrayBuffer], { type: contentType }); } function loadedImageToBlob(image) { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); - canvas.width = image.width; - canvas.height = image.height; + canvas.width = image.width; + canvas.height = image.height; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); - ctx.drawImage(image, 0, 0); + ctx.drawImage(image, 0, 0); - const dataURL = canvas.toDataURL('image/png', 1); - const blob = dataURLToBlob(dataURL); + const dataURL = canvas.toDataURL("image/png", 1); + const blob = dataURLToBlob(dataURL); - return blob; + return blob; } function loadImage(imagePath) { - return new Promise((resolve, reject) => { - const image = new Image(); + return new Promise((resolve, reject) => { + const image = new Image(); - image.onload = function() { - resolve(image); - }; + image.onload = function () { + resolve(image); + }; - image.src = imagePath; - }); + image.src = imagePath; + }); } async function uploadMask(filepath, formData) { - await api.fetchApi('/upload/mask', { - method: 'POST', - body: formData - }).then(response => {}).catch(error => { - console.error('Error:', error); - }); + await api + .fetchApi("/upload/mask", { + method: "POST", + body: formData, + }) + .then((response) => {}) + .catch((error) => { + console.error("Error:", error); + }); - ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image(); - ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = api.apiURL("/view?" + new URLSearchParams(filepath).toString() + app.getPreviewFormatParam() + app.getRandParam()); + ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]] = new Image(); + ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src = api.apiURL( + "/view?" + + new URLSearchParams(filepath).toString() + + app.getPreviewFormatParam() + + app.getRandParam() + ); - if(ComfyApp.clipspace.images) - ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath; + if (ComfyApp.clipspace.images) + ComfyApp.clipspace.images[ComfyApp.clipspace["selectedIndex"]] = filepath; - ClipspaceDialog.invalidatePreview(); + ClipspaceDialog.invalidatePreview(); } function prepare_mask(image, maskCanvas, maskCtx, maskColor) { - // paste mask data into alpha channel - maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height); - const maskData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height); + // paste mask data into alpha channel + maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height); + const maskData = maskCtx.getImageData( + 0, + 0, + maskCanvas.width, + maskCanvas.height + ); - // invert mask - for (let i = 0; i < maskData.data.length; i += 4) { - if(maskData.data[i+3] == 255) - maskData.data[i+3] = 0; - else - maskData.data[i+3] = 255; + // invert mask + for (let i = 0; i < maskData.data.length; i += 4) { + if (maskData.data[i + 3] == 255) maskData.data[i + 3] = 0; + else maskData.data[i + 3] = 255; - maskData.data[i] = maskColor.r; - maskData.data[i+1] = maskColor.g; - maskData.data[i+2] = maskColor.b; - } + maskData.data[i] = maskColor.r; + maskData.data[i + 1] = maskColor.g; + maskData.data[i + 2] = maskColor.b; + } - maskCtx.globalCompositeOperation = 'source-over'; - maskCtx.putImageData(maskData, 0, 0); + maskCtx.globalCompositeOperation = "source-over"; + maskCtx.putImageData(maskData, 0, 0); } class MaskEditorDialog extends ComfyDialog { - static instance = null; - static mousedown_x: number | null = null; - static mousedown_y: number | null = null; - - brush: HTMLDivElement; - maskCtx: any; - maskCanvas: HTMLCanvasElement; - brush_size_slider: HTMLDivElement; - brush_opacity_slider: HTMLDivElement; - colorButton: HTMLButtonElement; - saveButton: HTMLButtonElement; - zoom_ratio: number; - pan_x: number; - pan_y: number; - imgCanvas: HTMLCanvasElement; - last_display_style: string; - is_visible: boolean; - image: HTMLImageElement; - handler_registered: boolean; - brush_slider_input: HTMLInputElement; - cursorX: number; - cursorY: number; - mousedown_pan_x: number; - mousedown_pan_y: number; - last_pressure: number; - - - static getInstance() { - if(!MaskEditorDialog.instance) { - MaskEditorDialog.instance = new MaskEditorDialog(); - } - - return MaskEditorDialog.instance; - } - - is_layout_created = false; - - constructor() { - super(); - this.element = $el("div.comfy-modal", { parent: document.body }, - [ $el("div.comfy-modal-content", - [...this.createButtons()]), - ]); - } - - createButtons() { - return []; - } - - createButton(name, callback): HTMLButtonElement { - var button = document.createElement("button"); - button.style.pointerEvents = "auto"; - button.innerText = name; - button.addEventListener("click", callback); - return button; - } - - createLeftButton(name, callback) { - var button = this.createButton(name, callback); - button.style.cssFloat = "left"; - button.style.marginRight = "4px"; - return button; - } - - createRightButton(name, callback) { - var button = this.createButton(name, callback); - button.style.cssFloat = "right"; - button.style.marginLeft = "4px"; - return button; - } - - createLeftSlider(self, name, callback): HTMLDivElement { - const divElement = document.createElement('div'); - divElement.id = "maskeditor-slider"; - divElement.style.cssFloat = "left"; - divElement.style.fontFamily = "sans-serif"; - divElement.style.marginRight = "4px"; - divElement.style.color = "var(--input-text)"; - divElement.style.backgroundColor = "var(--comfy-input-bg)"; - divElement.style.borderRadius = "8px"; - divElement.style.borderColor = "var(--border-color)"; - divElement.style.borderStyle = "solid"; - divElement.style.fontSize = "15px"; - divElement.style.height = "21px"; - divElement.style.padding = "1px 6px"; - divElement.style.display = "flex"; - divElement.style.position = "relative"; - divElement.style.top = "2px"; - divElement.style.pointerEvents = "auto"; - self.brush_slider_input = document.createElement('input'); - self.brush_slider_input.setAttribute('type', 'range'); - self.brush_slider_input.setAttribute('min', '1'); - self.brush_slider_input.setAttribute('max', '100'); - self.brush_slider_input.setAttribute('value', '10'); - const labelElement = document.createElement("label"); - labelElement.textContent = name; - - divElement.appendChild(labelElement); - divElement.appendChild(self.brush_slider_input); - - self.brush_slider_input.addEventListener("change", callback); - - return divElement; - } - - createOpacitySlider(self, name, callback): HTMLDivElement { - const divElement = document.createElement('div'); - divElement.id = "maskeditor-opacity-slider"; - divElement.style.cssFloat = "left"; - divElement.style.fontFamily = "sans-serif"; - divElement.style.marginRight = "4px"; - divElement.style.color = "var(--input-text)"; - divElement.style.backgroundColor = "var(--comfy-input-bg)"; - divElement.style.borderRadius = "8px"; - divElement.style.borderColor = "var(--border-color)"; - divElement.style.borderStyle = "solid"; - divElement.style.fontSize = "15px"; - divElement.style.height = "21px"; - divElement.style.padding = "1px 6px"; - divElement.style.display = "flex"; - divElement.style.position = "relative"; - divElement.style.top = "2px"; - divElement.style.pointerEvents = "auto"; - self.opacity_slider_input = document.createElement('input'); - self.opacity_slider_input.setAttribute('type', 'range'); - self.opacity_slider_input.setAttribute('min', '0.1'); - self.opacity_slider_input.setAttribute('max', '1.0'); - self.opacity_slider_input.setAttribute('step', '0.01') - self.opacity_slider_input.setAttribute('value', '0.7'); - const labelElement = document.createElement("label"); - labelElement.textContent = name; - - divElement.appendChild(labelElement); - divElement.appendChild(self.opacity_slider_input); - - self.opacity_slider_input.addEventListener("input", callback); - - return divElement; - } - - setlayout(imgCanvas: HTMLCanvasElement, maskCanvas: HTMLCanvasElement) { - const self = this; - - // If it is specified as relative, using it only as a hidden placeholder for padding is recommended - // to prevent anomalies where it exceeds a certain size and goes outside of the window. - var bottom_panel = document.createElement("div"); - bottom_panel.style.position = "absolute"; - bottom_panel.style.bottom = "0px"; - bottom_panel.style.left = "20px"; - bottom_panel.style.right = "20px"; - bottom_panel.style.height = "50px"; - bottom_panel.style.pointerEvents = "none"; - - var brush = document.createElement("div"); - brush.id = "brush"; - brush.style.backgroundColor = "transparent"; - brush.style.outline = "1px dashed black"; - brush.style.boxShadow = "0 0 0 1px white"; - brush.style.borderRadius = "50%"; - // @ts-ignore - brush.style.MozBorderRadius = "50%"; - // @ts-ignore - brush.style.WebkitBorderRadius = "50%"; - brush.style.position = "absolute"; - brush.style.zIndex = "8889"; - brush.style.pointerEvents = "none"; - this.brush = brush; - this.element.appendChild(imgCanvas); - this.element.appendChild(maskCanvas); - this.element.appendChild(bottom_panel); - document.body.appendChild(brush); - - var clearButton = this.createLeftButton("Clear", () => { - self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height); - }); - - this.brush_size_slider = this.createLeftSlider(self, "Thickness", (event) => { - self.brush_size = event.target.value; - self.updateBrushPreview(self); - }); - - this.brush_opacity_slider = this.createOpacitySlider(self, "Opacity", (event) => { - self.brush_opacity = event.target.value; - if (self.brush_color_mode !== "negative") { - self.maskCanvas.style.opacity = self.brush_opacity.toString(); - } - }); - - this.colorButton = this.createLeftButton(this.getColorButtonText(), () => { - if (self.brush_color_mode === "black") { - self.brush_color_mode = "white"; - } - else if (self.brush_color_mode === "white") { - self.brush_color_mode = "negative"; - } - else { - self.brush_color_mode = "black"; - } - - self.updateWhenBrushColorModeChanged(); - }); - - var cancelButton = this.createRightButton("Cancel", () => { - document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown); - self.close(); - }); - - this.saveButton = this.createRightButton("Save", () => { - document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown); - self.save(); - }); - - this.element.appendChild(imgCanvas); - this.element.appendChild(maskCanvas); - this.element.appendChild(bottom_panel); - - bottom_panel.appendChild(clearButton); - bottom_panel.appendChild(this.saveButton); - bottom_panel.appendChild(cancelButton); - bottom_panel.appendChild(this.brush_size_slider); - bottom_panel.appendChild(this.brush_opacity_slider); - bottom_panel.appendChild(this.colorButton); - - imgCanvas.style.position = "absolute"; - maskCanvas.style.position = "absolute"; - - imgCanvas.style.top = "200"; - imgCanvas.style.left = "0"; - - maskCanvas.style.top = imgCanvas.style.top; - maskCanvas.style.left = imgCanvas.style.left; - - const maskCanvasStyle = this.getMaskCanvasStyle(); - maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode; - maskCanvas.style.opacity = maskCanvasStyle.opacity.toString(); - } - - async show() { - this.zoom_ratio = 1.0; - this.pan_x = 0; - this.pan_y = 0; - - if(!this.is_layout_created) { - // layout - const imgCanvas = document.createElement('canvas'); - const maskCanvas = document.createElement('canvas'); - - imgCanvas.id = "imageCanvas"; - maskCanvas.id = "maskCanvas"; - - this.setlayout(imgCanvas, maskCanvas); - - // prepare content - this.imgCanvas = imgCanvas; - this.maskCanvas = maskCanvas; - this.maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true }); - - this.setEventHandler(maskCanvas); - - this.is_layout_created = true; - - // replacement of onClose hook since close is not real close - const self = this; - const observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - if (mutation.type === 'attributes' && mutation.attributeName === 'style') { - if(self.last_display_style && self.last_display_style != 'none' && self.element.style.display == 'none') { - self.brush.style.display = "none"; - ComfyApp.onClipspaceEditorClosed(); - } - - self.last_display_style = self.element.style.display; - } - }); - }); - - const config = { attributes: true }; - observer.observe(this.element, config); - } - - // The keydown event needs to be reconfigured when closing the dialog as it gets removed. - document.addEventListener('keydown', MaskEditorDialog.handleKeyDown); - - if(ComfyApp.clipspace_return_node) { - this.saveButton.innerText = "Save to node"; - } - else { - this.saveButton.innerText = "Save"; - } - this.saveButton.disabled = false; - - this.element.style.display = "block"; - this.element.style.width = "85%"; - this.element.style.margin = "0 7.5%"; - this.element.style.height = "100vh"; - this.element.style.top = "50%"; - this.element.style.left = "42%"; - this.element.style.zIndex = "8888"; // NOTE: alert dialog must be high priority. - - await this.setImages(this.imgCanvas); - - this.is_visible = true; - } - - isOpened() { - return this.element.style.display == "block"; - } - - invalidateCanvas(orig_image, mask_image) { - this.imgCanvas.width = orig_image.width; - this.imgCanvas.height = orig_image.height; - - this.maskCanvas.width = orig_image.width; - this.maskCanvas.height = orig_image.height; - - let imgCtx = this.imgCanvas.getContext('2d', {willReadFrequently: true }); - let maskCtx = this.maskCanvas.getContext('2d', {willReadFrequently: true }); - - imgCtx.drawImage(orig_image, 0, 0, orig_image.width, orig_image.height); - prepare_mask(mask_image, this.maskCanvas, maskCtx, this.getMaskColor()); - } - - async setImages(imgCanvas) { - let self = this; - - const imgCtx = imgCanvas.getContext('2d', {willReadFrequently: true }); - const maskCtx = this.maskCtx; - const maskCanvas = this.maskCanvas; - - imgCtx.clearRect(0,0,this.imgCanvas.width,this.imgCanvas.height); - maskCtx.clearRect(0,0,this.maskCanvas.width,this.maskCanvas.height); - - // image load - const filepath = ComfyApp.clipspace.images; - - const alpha_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src) - alpha_url.searchParams.delete('channel'); - alpha_url.searchParams.delete('preview'); - alpha_url.searchParams.set('channel', 'a'); - let mask_image = await loadImage(alpha_url); - - // original image load - const rgb_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src); - rgb_url.searchParams.delete('channel'); - rgb_url.searchParams.set('channel', 'rgb'); - this.image = new Image(); - this.image.onload = function() { - maskCanvas.width = self.image.width; - maskCanvas.height = self.image.height; - - self.invalidateCanvas(self.image, mask_image); - self.initializeCanvasPanZoom(); - }; - this.image.src = rgb_url.toString(); - } - - initializeCanvasPanZoom() { - // set initialize - let drawWidth = this.image.width; - let drawHeight = this.image.height; - - let width = this.element.clientWidth; - let height = this.element.clientHeight; - - if (this.image.width > width) { - drawWidth = width; - drawHeight = (drawWidth / this.image.width) * this.image.height; - } - - if (drawHeight > height) { - drawHeight = height; - drawWidth = (drawHeight / this.image.height) * this.image.width; - } - - this.zoom_ratio = drawWidth/this.image.width; - - const canvasX = (width - drawWidth) / 2; - const canvasY = (height - drawHeight) / 2; - this.pan_x = canvasX; - this.pan_y = canvasY; - - this.invalidatePanZoom(); - } - - - invalidatePanZoom() { - let raw_width = this.image.width * this.zoom_ratio; - let raw_height = this.image.height * this.zoom_ratio; - - if(this.pan_x + raw_width < 10) { - this.pan_x = 10 - raw_width; - } - - if(this.pan_y + raw_height < 10) { - this.pan_y = 10 - raw_height; - } - - let width = `${raw_width}px`; - let height = `${raw_height}px`; - - let left = `${this.pan_x}px`; - let top = `${this.pan_y}px`; - - this.maskCanvas.style.width = width; - this.maskCanvas.style.height = height; - this.maskCanvas.style.left = left; - this.maskCanvas.style.top = top; - - this.imgCanvas.style.width = width; - this.imgCanvas.style.height = height; - this.imgCanvas.style.left = left; - this.imgCanvas.style.top = top; - } - - - setEventHandler(maskCanvas) { - const self = this; - - if(!this.handler_registered) { - maskCanvas.addEventListener("contextmenu", (event) => { - event.preventDefault(); - }); - - this.element.addEventListener('wheel', (event) => this.handleWheelEvent(self,event)); - this.element.addEventListener('pointermove', (event) => this.pointMoveEvent(self,event)); - this.element.addEventListener('touchmove', (event) => this.pointMoveEvent(self,event)); - - this.element.addEventListener('dragstart', (event) => { - if(event.ctrlKey) { - event.preventDefault(); - } - }); - - maskCanvas.addEventListener('pointerdown', (event) => this.handlePointerDown(self,event)); - maskCanvas.addEventListener('pointermove', (event) => this.draw_move(self,event)); - maskCanvas.addEventListener('touchmove', (event) => this.draw_move(self,event)); - maskCanvas.addEventListener('pointerover', (event) => { this.brush.style.display = "block"; }); - maskCanvas.addEventListener('pointerleave', (event) => { this.brush.style.display = "none"; }); - - document.addEventListener('pointerup', MaskEditorDialog.handlePointerUp); - - this.handler_registered = true; - } - } - - getMaskCanvasStyle() { - if (this.brush_color_mode === "negative") { - return { - mixBlendMode: "difference", - opacity: "1", - }; - } - else { - return { - mixBlendMode: "initial", - opacity: this.brush_opacity, - }; - } - } - - getMaskColor() { - if (this.brush_color_mode === "black") { - return { r: 0, g: 0, b: 0 }; - } - if (this.brush_color_mode === "white") { - return { r: 255, g: 255, b: 255 }; - } - if (this.brush_color_mode === "negative") { - // negative effect only works with white color - return { r: 255, g: 255, b: 255 }; - } - - return { r: 0, g: 0, b: 0 }; - } - - getMaskFillStyle() { - const maskColor = this.getMaskColor(); - - return "rgb(" + maskColor.r + "," + maskColor.g + "," + maskColor.b + ")"; - } - - getColorButtonText() { - let colorCaption = "unknown"; - - if (this.brush_color_mode === "black") { - colorCaption = "black"; - } - else if (this.brush_color_mode === "white") { - colorCaption = "white"; - } - else if (this.brush_color_mode === "negative") { - colorCaption = "negative"; - } - - return "Color: " + colorCaption; - } - - updateWhenBrushColorModeChanged() { - this.colorButton.innerText = this.getColorButtonText(); - - // update mask canvas css styles - - const maskCanvasStyle = this.getMaskCanvasStyle(); - this.maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode; - this.maskCanvas.style.opacity = maskCanvasStyle.opacity.toString(); - - // update mask canvas rgb colors - - const maskColor = this.getMaskColor(); - - const maskData = this.maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height); - - for (let i = 0; i < maskData.data.length; i += 4) { - maskData.data[i] = maskColor.r; - maskData.data[i+1] = maskColor.g; - maskData.data[i+2] = maskColor.b; - } - - this.maskCtx.putImageData(maskData, 0, 0); - } - - brush_opacity = 0.7; - brush_size = 10; - brush_color_mode = "black"; - drawing_mode = false; - lastx = -1; - lasty = -1; - lasttime = 0; - - static handleKeyDown(event) { - const self = MaskEditorDialog.instance; - if (event.key === ']') { - self.brush_size = Math.min(self.brush_size+2, 100); - self.brush_slider_input.value = self.brush_size; - } else if (event.key === '[') { - self.brush_size = Math.max(self.brush_size-2, 1); - self.brush_slider_input.value = self.brush_size; - } else if(event.key === 'Enter') { - self.save(); - } - - self.updateBrushPreview(self); - } - - static handlePointerUp(event) { - event.preventDefault(); - - this.mousedown_x = null; - this.mousedown_y = null; - - MaskEditorDialog.instance.drawing_mode = false; - } - - updateBrushPreview(self) { - const brush = self.brush; - - var centerX = self.cursorX; - var centerY = self.cursorY; - - brush.style.width = self.brush_size * 2 * this.zoom_ratio + "px"; - brush.style.height = self.brush_size * 2 * this.zoom_ratio + "px"; - brush.style.left = (centerX - self.brush_size * this.zoom_ratio) + "px"; - brush.style.top = (centerY - self.brush_size * this.zoom_ratio) + "px"; - } + static instance = null; + static mousedown_x: number | null = null; + static mousedown_y: number | null = null; + + brush: HTMLDivElement; + maskCtx: any; + maskCanvas: HTMLCanvasElement; + brush_size_slider: HTMLDivElement; + brush_opacity_slider: HTMLDivElement; + colorButton: HTMLButtonElement; + saveButton: HTMLButtonElement; + zoom_ratio: number; + pan_x: number; + pan_y: number; + imgCanvas: HTMLCanvasElement; + last_display_style: string; + is_visible: boolean; + image: HTMLImageElement; + handler_registered: boolean; + brush_slider_input: HTMLInputElement; + cursorX: number; + cursorY: number; + mousedown_pan_x: number; + mousedown_pan_y: number; + last_pressure: number; + + static getInstance() { + if (!MaskEditorDialog.instance) { + MaskEditorDialog.instance = new MaskEditorDialog(); + } + + return MaskEditorDialog.instance; + } + + is_layout_created = false; + + constructor() { + super(); + this.element = $el("div.comfy-modal", { parent: document.body }, [ + $el("div.comfy-modal-content", [...this.createButtons()]), + ]); + } + + createButtons() { + return []; + } + + createButton(name, callback): HTMLButtonElement { + var button = document.createElement("button"); + button.style.pointerEvents = "auto"; + button.innerText = name; + button.addEventListener("click", callback); + return button; + } + + createLeftButton(name, callback) { + var button = this.createButton(name, callback); + button.style.cssFloat = "left"; + button.style.marginRight = "4px"; + return button; + } + + createRightButton(name, callback) { + var button = this.createButton(name, callback); + button.style.cssFloat = "right"; + button.style.marginLeft = "4px"; + return button; + } + + createLeftSlider(self, name, callback): HTMLDivElement { + const divElement = document.createElement("div"); + divElement.id = "maskeditor-slider"; + divElement.style.cssFloat = "left"; + divElement.style.fontFamily = "sans-serif"; + divElement.style.marginRight = "4px"; + divElement.style.color = "var(--input-text)"; + divElement.style.backgroundColor = "var(--comfy-input-bg)"; + divElement.style.borderRadius = "8px"; + divElement.style.borderColor = "var(--border-color)"; + divElement.style.borderStyle = "solid"; + divElement.style.fontSize = "15px"; + divElement.style.height = "21px"; + divElement.style.padding = "1px 6px"; + divElement.style.display = "flex"; + divElement.style.position = "relative"; + divElement.style.top = "2px"; + divElement.style.pointerEvents = "auto"; + self.brush_slider_input = document.createElement("input"); + self.brush_slider_input.setAttribute("type", "range"); + self.brush_slider_input.setAttribute("min", "1"); + self.brush_slider_input.setAttribute("max", "100"); + self.brush_slider_input.setAttribute("value", "10"); + const labelElement = document.createElement("label"); + labelElement.textContent = name; + + divElement.appendChild(labelElement); + divElement.appendChild(self.brush_slider_input); + + self.brush_slider_input.addEventListener("change", callback); + + return divElement; + } + + createOpacitySlider(self, name, callback): HTMLDivElement { + const divElement = document.createElement("div"); + divElement.id = "maskeditor-opacity-slider"; + divElement.style.cssFloat = "left"; + divElement.style.fontFamily = "sans-serif"; + divElement.style.marginRight = "4px"; + divElement.style.color = "var(--input-text)"; + divElement.style.backgroundColor = "var(--comfy-input-bg)"; + divElement.style.borderRadius = "8px"; + divElement.style.borderColor = "var(--border-color)"; + divElement.style.borderStyle = "solid"; + divElement.style.fontSize = "15px"; + divElement.style.height = "21px"; + divElement.style.padding = "1px 6px"; + divElement.style.display = "flex"; + divElement.style.position = "relative"; + divElement.style.top = "2px"; + divElement.style.pointerEvents = "auto"; + self.opacity_slider_input = document.createElement("input"); + self.opacity_slider_input.setAttribute("type", "range"); + self.opacity_slider_input.setAttribute("min", "0.1"); + self.opacity_slider_input.setAttribute("max", "1.0"); + self.opacity_slider_input.setAttribute("step", "0.01"); + self.opacity_slider_input.setAttribute("value", "0.7"); + const labelElement = document.createElement("label"); + labelElement.textContent = name; + + divElement.appendChild(labelElement); + divElement.appendChild(self.opacity_slider_input); + + self.opacity_slider_input.addEventListener("input", callback); + + return divElement; + } + + setlayout(imgCanvas: HTMLCanvasElement, maskCanvas: HTMLCanvasElement) { + const self = this; + + // If it is specified as relative, using it only as a hidden placeholder for padding is recommended + // to prevent anomalies where it exceeds a certain size and goes outside of the window. + var bottom_panel = document.createElement("div"); + bottom_panel.style.position = "absolute"; + bottom_panel.style.bottom = "0px"; + bottom_panel.style.left = "20px"; + bottom_panel.style.right = "20px"; + bottom_panel.style.height = "50px"; + bottom_panel.style.pointerEvents = "none"; + + var brush = document.createElement("div"); + brush.id = "brush"; + brush.style.backgroundColor = "transparent"; + brush.style.outline = "1px dashed black"; + brush.style.boxShadow = "0 0 0 1px white"; + brush.style.borderRadius = "50%"; + // @ts-ignore + brush.style.MozBorderRadius = "50%"; + // @ts-ignore + brush.style.WebkitBorderRadius = "50%"; + brush.style.position = "absolute"; + brush.style.zIndex = "8889"; + brush.style.pointerEvents = "none"; + this.brush = brush; + this.element.appendChild(imgCanvas); + this.element.appendChild(maskCanvas); + this.element.appendChild(bottom_panel); + document.body.appendChild(brush); + + var clearButton = this.createLeftButton("Clear", () => { + self.maskCtx.clearRect( + 0, + 0, + self.maskCanvas.width, + self.maskCanvas.height + ); + }); + + this.brush_size_slider = this.createLeftSlider( + self, + "Thickness", + (event) => { + self.brush_size = event.target.value; + self.updateBrushPreview(self); + } + ); + + this.brush_opacity_slider = this.createOpacitySlider( + self, + "Opacity", + (event) => { + self.brush_opacity = event.target.value; + if (self.brush_color_mode !== "negative") { + self.maskCanvas.style.opacity = self.brush_opacity.toString(); + } + } + ); + + this.colorButton = this.createLeftButton(this.getColorButtonText(), () => { + if (self.brush_color_mode === "black") { + self.brush_color_mode = "white"; + } else if (self.brush_color_mode === "white") { + self.brush_color_mode = "negative"; + } else { + self.brush_color_mode = "black"; + } + + self.updateWhenBrushColorModeChanged(); + }); + + var cancelButton = this.createRightButton("Cancel", () => { + document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown); + self.close(); + }); + + this.saveButton = this.createRightButton("Save", () => { + document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown); + self.save(); + }); + + this.element.appendChild(imgCanvas); + this.element.appendChild(maskCanvas); + this.element.appendChild(bottom_panel); + + bottom_panel.appendChild(clearButton); + bottom_panel.appendChild(this.saveButton); + bottom_panel.appendChild(cancelButton); + bottom_panel.appendChild(this.brush_size_slider); + bottom_panel.appendChild(this.brush_opacity_slider); + bottom_panel.appendChild(this.colorButton); + + imgCanvas.style.position = "absolute"; + maskCanvas.style.position = "absolute"; + + imgCanvas.style.top = "200"; + imgCanvas.style.left = "0"; + + maskCanvas.style.top = imgCanvas.style.top; + maskCanvas.style.left = imgCanvas.style.left; + + const maskCanvasStyle = this.getMaskCanvasStyle(); + maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode; + maskCanvas.style.opacity = maskCanvasStyle.opacity.toString(); + } + + async show() { + this.zoom_ratio = 1.0; + this.pan_x = 0; + this.pan_y = 0; + + if (!this.is_layout_created) { + // layout + const imgCanvas = document.createElement("canvas"); + const maskCanvas = document.createElement("canvas"); + + imgCanvas.id = "imageCanvas"; + maskCanvas.id = "maskCanvas"; + + this.setlayout(imgCanvas, maskCanvas); + + // prepare content + this.imgCanvas = imgCanvas; + this.maskCanvas = maskCanvas; + this.maskCtx = maskCanvas.getContext("2d", { willReadFrequently: true }); + + this.setEventHandler(maskCanvas); + + this.is_layout_created = true; + + // replacement of onClose hook since close is not real close + const self = this; + const observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if ( + mutation.type === "attributes" && + mutation.attributeName === "style" + ) { + if ( + self.last_display_style && + self.last_display_style != "none" && + self.element.style.display == "none" + ) { + self.brush.style.display = "none"; + ComfyApp.onClipspaceEditorClosed(); + } + + self.last_display_style = self.element.style.display; + } + }); + }); + + const config = { attributes: true }; + observer.observe(this.element, config); + } + + // The keydown event needs to be reconfigured when closing the dialog as it gets removed. + document.addEventListener("keydown", MaskEditorDialog.handleKeyDown); + + if (ComfyApp.clipspace_return_node) { + this.saveButton.innerText = "Save to node"; + } else { + this.saveButton.innerText = "Save"; + } + this.saveButton.disabled = false; + + this.element.style.display = "block"; + this.element.style.width = "85%"; + this.element.style.margin = "0 7.5%"; + this.element.style.height = "100vh"; + this.element.style.top = "50%"; + this.element.style.left = "42%"; + this.element.style.zIndex = "8888"; // NOTE: alert dialog must be high priority. + + await this.setImages(this.imgCanvas); + + this.is_visible = true; + } + + isOpened() { + return this.element.style.display == "block"; + } + + invalidateCanvas(orig_image, mask_image) { + this.imgCanvas.width = orig_image.width; + this.imgCanvas.height = orig_image.height; + + this.maskCanvas.width = orig_image.width; + this.maskCanvas.height = orig_image.height; + + let imgCtx = this.imgCanvas.getContext("2d", { willReadFrequently: true }); + let maskCtx = this.maskCanvas.getContext("2d", { + willReadFrequently: true, + }); + + imgCtx.drawImage(orig_image, 0, 0, orig_image.width, orig_image.height); + prepare_mask(mask_image, this.maskCanvas, maskCtx, this.getMaskColor()); + } + + async setImages(imgCanvas) { + let self = this; + + const imgCtx = imgCanvas.getContext("2d", { willReadFrequently: true }); + const maskCtx = this.maskCtx; + const maskCanvas = this.maskCanvas; + + imgCtx.clearRect(0, 0, this.imgCanvas.width, this.imgCanvas.height); + maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); + + // image load + const filepath = ComfyApp.clipspace.images; + + const alpha_url = new URL( + ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src + ); + alpha_url.searchParams.delete("channel"); + alpha_url.searchParams.delete("preview"); + alpha_url.searchParams.set("channel", "a"); + let mask_image = await loadImage(alpha_url); + + // original image load + const rgb_url = new URL( + ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src + ); + rgb_url.searchParams.delete("channel"); + rgb_url.searchParams.set("channel", "rgb"); + this.image = new Image(); + this.image.onload = function () { + maskCanvas.width = self.image.width; + maskCanvas.height = self.image.height; + + self.invalidateCanvas(self.image, mask_image); + self.initializeCanvasPanZoom(); + }; + this.image.src = rgb_url.toString(); + } + + initializeCanvasPanZoom() { + // set initialize + let drawWidth = this.image.width; + let drawHeight = this.image.height; + + let width = this.element.clientWidth; + let height = this.element.clientHeight; + + if (this.image.width > width) { + drawWidth = width; + drawHeight = (drawWidth / this.image.width) * this.image.height; + } + + if (drawHeight > height) { + drawHeight = height; + drawWidth = (drawHeight / this.image.height) * this.image.width; + } + + this.zoom_ratio = drawWidth / this.image.width; + + const canvasX = (width - drawWidth) / 2; + const canvasY = (height - drawHeight) / 2; + this.pan_x = canvasX; + this.pan_y = canvasY; + + this.invalidatePanZoom(); + } + + invalidatePanZoom() { + let raw_width = this.image.width * this.zoom_ratio; + let raw_height = this.image.height * this.zoom_ratio; + + if (this.pan_x + raw_width < 10) { + this.pan_x = 10 - raw_width; + } + + if (this.pan_y + raw_height < 10) { + this.pan_y = 10 - raw_height; + } + + let width = `${raw_width}px`; + let height = `${raw_height}px`; + + let left = `${this.pan_x}px`; + let top = `${this.pan_y}px`; + + this.maskCanvas.style.width = width; + this.maskCanvas.style.height = height; + this.maskCanvas.style.left = left; + this.maskCanvas.style.top = top; + + this.imgCanvas.style.width = width; + this.imgCanvas.style.height = height; + this.imgCanvas.style.left = left; + this.imgCanvas.style.top = top; + } + + setEventHandler(maskCanvas) { + const self = this; + + if (!this.handler_registered) { + maskCanvas.addEventListener("contextmenu", (event) => { + event.preventDefault(); + }); + + this.element.addEventListener("wheel", (event) => + this.handleWheelEvent(self, event) + ); + this.element.addEventListener("pointermove", (event) => + this.pointMoveEvent(self, event) + ); + this.element.addEventListener("touchmove", (event) => + this.pointMoveEvent(self, event) + ); + + this.element.addEventListener("dragstart", (event) => { + if (event.ctrlKey) { + event.preventDefault(); + } + }); + + maskCanvas.addEventListener("pointerdown", (event) => + this.handlePointerDown(self, event) + ); + maskCanvas.addEventListener("pointermove", (event) => + this.draw_move(self, event) + ); + maskCanvas.addEventListener("touchmove", (event) => + this.draw_move(self, event) + ); + maskCanvas.addEventListener("pointerover", (event) => { + this.brush.style.display = "block"; + }); + maskCanvas.addEventListener("pointerleave", (event) => { + this.brush.style.display = "none"; + }); + + document.addEventListener("pointerup", MaskEditorDialog.handlePointerUp); + + this.handler_registered = true; + } + } + + getMaskCanvasStyle() { + if (this.brush_color_mode === "negative") { + return { + mixBlendMode: "difference", + opacity: "1", + }; + } else { + return { + mixBlendMode: "initial", + opacity: this.brush_opacity, + }; + } + } + + getMaskColor() { + if (this.brush_color_mode === "black") { + return { r: 0, g: 0, b: 0 }; + } + if (this.brush_color_mode === "white") { + return { r: 255, g: 255, b: 255 }; + } + if (this.brush_color_mode === "negative") { + // negative effect only works with white color + return { r: 255, g: 255, b: 255 }; + } + + return { r: 0, g: 0, b: 0 }; + } + + getMaskFillStyle() { + const maskColor = this.getMaskColor(); + + return "rgb(" + maskColor.r + "," + maskColor.g + "," + maskColor.b + ")"; + } + + getColorButtonText() { + let colorCaption = "unknown"; + + if (this.brush_color_mode === "black") { + colorCaption = "black"; + } else if (this.brush_color_mode === "white") { + colorCaption = "white"; + } else if (this.brush_color_mode === "negative") { + colorCaption = "negative"; + } + + return "Color: " + colorCaption; + } + + updateWhenBrushColorModeChanged() { + this.colorButton.innerText = this.getColorButtonText(); + + // update mask canvas css styles + + const maskCanvasStyle = this.getMaskCanvasStyle(); + this.maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode; + this.maskCanvas.style.opacity = maskCanvasStyle.opacity.toString(); + + // update mask canvas rgb colors + + const maskColor = this.getMaskColor(); + + const maskData = this.maskCtx.getImageData( + 0, + 0, + this.maskCanvas.width, + this.maskCanvas.height + ); + + for (let i = 0; i < maskData.data.length; i += 4) { + maskData.data[i] = maskColor.r; + maskData.data[i + 1] = maskColor.g; + maskData.data[i + 2] = maskColor.b; + } + + this.maskCtx.putImageData(maskData, 0, 0); + } + + brush_opacity = 0.7; + brush_size = 10; + brush_color_mode = "black"; + drawing_mode = false; + lastx = -1; + lasty = -1; + lasttime = 0; + + static handleKeyDown(event) { + const self = MaskEditorDialog.instance; + if (event.key === "]") { + self.brush_size = Math.min(self.brush_size + 2, 100); + self.brush_slider_input.value = self.brush_size; + } else if (event.key === "[") { + self.brush_size = Math.max(self.brush_size - 2, 1); + self.brush_slider_input.value = self.brush_size; + } else if (event.key === "Enter") { + self.save(); + } + + self.updateBrushPreview(self); + } + + static handlePointerUp(event) { + event.preventDefault(); + + this.mousedown_x = null; + this.mousedown_y = null; + + MaskEditorDialog.instance.drawing_mode = false; + } + + updateBrushPreview(self) { + const brush = self.brush; + + var centerX = self.cursorX; + var centerY = self.cursorY; + + brush.style.width = self.brush_size * 2 * this.zoom_ratio + "px"; + brush.style.height = self.brush_size * 2 * this.zoom_ratio + "px"; + brush.style.left = centerX - self.brush_size * this.zoom_ratio + "px"; + brush.style.top = centerY - self.brush_size * this.zoom_ratio + "px"; + } + + handleWheelEvent(self, event) { + event.preventDefault(); - handleWheelEvent(self, event) { - event.preventDefault(); - - if(event.ctrlKey) { - // zoom canvas - if(event.deltaY < 0) { - this.zoom_ratio = Math.min(10.0, this.zoom_ratio+0.2); - } - else { - this.zoom_ratio = Math.max(0.2, this.zoom_ratio-0.2); - } - - this.invalidatePanZoom(); - } - else { - // adjust brush size - if(event.deltaY < 0) - this.brush_size = Math.min(this.brush_size+2, 100); - else - this.brush_size = Math.max(this.brush_size-2, 1); - - this.brush_slider_input.value = this.brush_size.toString(); - - this.updateBrushPreview(this); - } - } - - pointMoveEvent(self, event) { - this.cursorX = event.pageX; - this.cursorY = event.pageY; - - self.updateBrushPreview(self); - - if(event.ctrlKey) { - event.preventDefault(); - self.pan_move(self, event); - } - - let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1; - - if(event.shiftKey && left_button_down) { - self.drawing_mode = false; - - const y = event.clientY; - let delta = (self.zoom_lasty - y)*0.005; - self.zoom_ratio = Math.max(Math.min(10.0, self.last_zoom_ratio - delta), 0.2); - - this.invalidatePanZoom(); - return; - } - } - - pan_move(self, event) { - if(event.buttons == 1) { - if(MaskEditorDialog.mousedown_x) { - let deltaX = MaskEditorDialog.mousedown_x - event.clientX; - let deltaY = MaskEditorDialog.mousedown_y - event.clientY; - - self.pan_x = this.mousedown_pan_x - deltaX; - self.pan_y = this.mousedown_pan_y - deltaY; - - self.invalidatePanZoom(); - } - } - } - - draw_move(self, event) { - if(event.ctrlKey || event.shiftKey) { - return; - } - - event.preventDefault(); - - this.cursorX = event.pageX; - this.cursorY = event.pageY; - - self.updateBrushPreview(self); - - let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1; - let right_button_down = [2, 5, 32].includes(event.buttons); - - if (!event.altKey && left_button_down) { - var diff = performance.now() - self.lasttime; - - const maskRect = self.maskCanvas.getBoundingClientRect(); - - var x = event.offsetX; - var y = event.offsetY - - if(event.offsetX == null) { - x = event.targetTouches[0].clientX - maskRect.left; - } - - if(event.offsetY == null) { - y = event.targetTouches[0].clientY - maskRect.top; - } - - x /= self.zoom_ratio; - y /= self.zoom_ratio; - - var brush_size = this.brush_size; - if(event instanceof PointerEvent && event.pointerType == 'pen') { - brush_size *= event.pressure; - this.last_pressure = event.pressure; - } - else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){ - // The firing interval of PointerEvents in Pen is unreliable, so it is supplemented by TouchEvents. - brush_size *= this.last_pressure; - } - else { - brush_size = this.brush_size; - } - - if(diff > 20 && !this.drawing_mode) - requestAnimationFrame(() => { - self.maskCtx.beginPath(); - self.maskCtx.fillStyle = this.getMaskFillStyle(); - self.maskCtx.globalCompositeOperation = "source-over"; - self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false); - self.maskCtx.fill(); - self.lastx = x; - self.lasty = y; - }); - else - requestAnimationFrame(() => { - self.maskCtx.beginPath(); - self.maskCtx.fillStyle = this.getMaskFillStyle(); - self.maskCtx.globalCompositeOperation = "source-over"; - - var dx = x - self.lastx; - var dy = y - self.lasty; - - var distance = Math.sqrt(dx * dx + dy * dy); - var directionX = dx / distance; - var directionY = dy / distance; - - for (var i = 0; i < distance; i+=5) { - var px = self.lastx + (directionX * i); - var py = self.lasty + (directionY * i); - self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false); - self.maskCtx.fill(); - } - self.lastx = x; - self.lasty = y; - }); - - self.lasttime = performance.now(); - } - else if((event.altKey && left_button_down) || right_button_down) { - const maskRect = self.maskCanvas.getBoundingClientRect(); - const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio; - const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio; - - var brush_size = this.brush_size; - if(event instanceof PointerEvent && event.pointerType == 'pen') { - brush_size *= event.pressure; - this.last_pressure = event.pressure; - } - else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){ - brush_size *= this.last_pressure; - } - else { - brush_size = this.brush_size; - } - - if(diff > 20 && !this.drawing_mode) // cannot tracking drawing_mode for touch event - requestAnimationFrame(() => { - self.maskCtx.beginPath(); - self.maskCtx.globalCompositeOperation = "destination-out"; - self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false); - self.maskCtx.fill(); - self.lastx = x; - self.lasty = y; - }); - else - requestAnimationFrame(() => { - self.maskCtx.beginPath(); - self.maskCtx.globalCompositeOperation = "destination-out"; - - var dx = x - self.lastx; - var dy = y - self.lasty; - - var distance = Math.sqrt(dx * dx + dy * dy); - var directionX = dx / distance; - var directionY = dy / distance; - - for (var i = 0; i < distance; i+=5) { - var px = self.lastx + (directionX * i); - var py = self.lasty + (directionY * i); - self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false); - self.maskCtx.fill(); - } - self.lastx = x; - self.lasty = y; - }); - - self.lasttime = performance.now(); - } - } - - handlePointerDown(self, event) { - if(event.ctrlKey) { - if (event.buttons == 1) { - MaskEditorDialog.mousedown_x = event.clientX; - MaskEditorDialog.mousedown_y = event.clientY; - - this.mousedown_pan_x = this.pan_x; - this.mousedown_pan_y = this.pan_y; - } - return; - } - - var brush_size = this.brush_size; - if(event instanceof PointerEvent && event.pointerType == 'pen') { - brush_size *= event.pressure; - this.last_pressure = event.pressure; - } - - if ([0, 2, 5].includes(event.button)) { - self.drawing_mode = true; - - event.preventDefault(); - - if(event.shiftKey) { - self.zoom_lasty = event.clientY; - self.last_zoom_ratio = self.zoom_ratio; - return; - } - - const maskRect = self.maskCanvas.getBoundingClientRect(); - const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio; - const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio; - - self.maskCtx.beginPath(); - if (!event.altKey && event.button == 0) { - self.maskCtx.fillStyle = this.getMaskFillStyle(); - self.maskCtx.globalCompositeOperation = "source-over"; - } else { - self.maskCtx.globalCompositeOperation = "destination-out"; - } - self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false); - self.maskCtx.fill(); - self.lastx = x; - self.lasty = y; - self.lasttime = performance.now(); - } - } - - async save() { - const backupCanvas = document.createElement('canvas'); - const backupCtx = backupCanvas.getContext('2d', {willReadFrequently:true}); - backupCanvas.width = this.image.width; - backupCanvas.height = this.image.height; - - backupCtx.clearRect(0,0, backupCanvas.width, backupCanvas.height); - backupCtx.drawImage(this.maskCanvas, - 0, 0, this.maskCanvas.width, this.maskCanvas.height, - 0, 0, backupCanvas.width, backupCanvas.height); - - // paste mask data into alpha channel - const backupData = backupCtx.getImageData(0, 0, backupCanvas.width, backupCanvas.height); - - // refine mask image - for (let i = 0; i < backupData.data.length; i += 4) { - if(backupData.data[i+3] == 255) - backupData.data[i+3] = 0; - else - backupData.data[i+3] = 255; - - backupData.data[i] = 0; - backupData.data[i+1] = 0; - backupData.data[i+2] = 0; - } - - backupCtx.globalCompositeOperation = 'source-over'; - backupCtx.putImageData(backupData, 0, 0); - - const formData = new FormData(); - const filename = "clipspace-mask-" + performance.now() + ".png"; - - const item = - { - "filename": filename, - "subfolder": "clipspace", - "type": "input", - }; - - if(ComfyApp.clipspace.images) - ComfyApp.clipspace.images[0] = item; - - if(ComfyApp.clipspace.widgets) { - const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image'); - - if(index >= 0) - ComfyApp.clipspace.widgets[index].value = item; - } - - const dataURL = backupCanvas.toDataURL(); - const blob = dataURLToBlob(dataURL); - - let original_url = new URL(this.image.src); - - type Ref = { filename: string, subfolder?: string, type?: string }; - - const original_ref: Ref = { filename: original_url.searchParams.get('filename') }; - - let original_subfolder = original_url.searchParams.get("subfolder"); - if(original_subfolder) - original_ref.subfolder = original_subfolder; - - let original_type = original_url.searchParams.get("type"); - if(original_type) - original_ref.type = original_type; - - formData.append('image', blob, filename); - formData.append('original_ref', JSON.stringify(original_ref)); - formData.append('type', "input"); - formData.append('subfolder', "clipspace"); - - this.saveButton.innerText = "Saving..."; - this.saveButton.disabled = true; - await uploadMask(item, formData); - ComfyApp.onClipspaceEditorSave(); - this.close(); - } + if (event.ctrlKey) { + // zoom canvas + if (event.deltaY < 0) { + this.zoom_ratio = Math.min(10.0, this.zoom_ratio + 0.2); + } else { + this.zoom_ratio = Math.max(0.2, this.zoom_ratio - 0.2); + } + + this.invalidatePanZoom(); + } else { + // adjust brush size + if (event.deltaY < 0) + this.brush_size = Math.min(this.brush_size + 2, 100); + else this.brush_size = Math.max(this.brush_size - 2, 1); + + this.brush_slider_input.value = this.brush_size.toString(); + + this.updateBrushPreview(this); + } + } + + pointMoveEvent(self, event) { + this.cursorX = event.pageX; + this.cursorY = event.pageY; + + self.updateBrushPreview(self); + + if (event.ctrlKey) { + event.preventDefault(); + self.pan_move(self, event); + } + + let left_button_down = + (window.TouchEvent && event instanceof TouchEvent) || event.buttons == 1; + + if (event.shiftKey && left_button_down) { + self.drawing_mode = false; + + const y = event.clientY; + let delta = (self.zoom_lasty - y) * 0.005; + self.zoom_ratio = Math.max( + Math.min(10.0, self.last_zoom_ratio - delta), + 0.2 + ); + + this.invalidatePanZoom(); + return; + } + } + + pan_move(self, event) { + if (event.buttons == 1) { + if (MaskEditorDialog.mousedown_x) { + let deltaX = MaskEditorDialog.mousedown_x - event.clientX; + let deltaY = MaskEditorDialog.mousedown_y - event.clientY; + + self.pan_x = this.mousedown_pan_x - deltaX; + self.pan_y = this.mousedown_pan_y - deltaY; + + self.invalidatePanZoom(); + } + } + } + + draw_move(self, event) { + if (event.ctrlKey || event.shiftKey) { + return; + } + + event.preventDefault(); + + this.cursorX = event.pageX; + this.cursorY = event.pageY; + + self.updateBrushPreview(self); + + let left_button_down = + (window.TouchEvent && event instanceof TouchEvent) || event.buttons == 1; + let right_button_down = [2, 5, 32].includes(event.buttons); + + if (!event.altKey && left_button_down) { + var diff = performance.now() - self.lasttime; + + const maskRect = self.maskCanvas.getBoundingClientRect(); + + var x = event.offsetX; + var y = event.offsetY; + + if (event.offsetX == null) { + x = event.targetTouches[0].clientX - maskRect.left; + } + + if (event.offsetY == null) { + y = event.targetTouches[0].clientY - maskRect.top; + } + + x /= self.zoom_ratio; + y /= self.zoom_ratio; + + var brush_size = this.brush_size; + if (event instanceof PointerEvent && event.pointerType == "pen") { + brush_size *= event.pressure; + this.last_pressure = event.pressure; + } else if ( + window.TouchEvent && + event instanceof TouchEvent && + diff < 20 + ) { + // The firing interval of PointerEvents in Pen is unreliable, so it is supplemented by TouchEvents. + brush_size *= this.last_pressure; + } else { + brush_size = this.brush_size; + } + + if (diff > 20 && !this.drawing_mode) + requestAnimationFrame(() => { + self.maskCtx.beginPath(); + self.maskCtx.fillStyle = this.getMaskFillStyle(); + self.maskCtx.globalCompositeOperation = "source-over"; + self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false); + self.maskCtx.fill(); + self.lastx = x; + self.lasty = y; + }); + else + requestAnimationFrame(() => { + self.maskCtx.beginPath(); + self.maskCtx.fillStyle = this.getMaskFillStyle(); + self.maskCtx.globalCompositeOperation = "source-over"; + + var dx = x - self.lastx; + var dy = y - self.lasty; + + var distance = Math.sqrt(dx * dx + dy * dy); + var directionX = dx / distance; + var directionY = dy / distance; + + for (var i = 0; i < distance; i += 5) { + var px = self.lastx + directionX * i; + var py = self.lasty + directionY * i; + self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false); + self.maskCtx.fill(); + } + self.lastx = x; + self.lasty = y; + }); + + self.lasttime = performance.now(); + } else if ((event.altKey && left_button_down) || right_button_down) { + const maskRect = self.maskCanvas.getBoundingClientRect(); + const x = + (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / + self.zoom_ratio; + const y = + (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / + self.zoom_ratio; + + var brush_size = this.brush_size; + if (event instanceof PointerEvent && event.pointerType == "pen") { + brush_size *= event.pressure; + this.last_pressure = event.pressure; + } else if ( + window.TouchEvent && + event instanceof TouchEvent && + diff < 20 + ) { + brush_size *= this.last_pressure; + } else { + brush_size = this.brush_size; + } + + if (diff > 20 && !this.drawing_mode) + // cannot tracking drawing_mode for touch event + requestAnimationFrame(() => { + self.maskCtx.beginPath(); + self.maskCtx.globalCompositeOperation = "destination-out"; + self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false); + self.maskCtx.fill(); + self.lastx = x; + self.lasty = y; + }); + else + requestAnimationFrame(() => { + self.maskCtx.beginPath(); + self.maskCtx.globalCompositeOperation = "destination-out"; + + var dx = x - self.lastx; + var dy = y - self.lasty; + + var distance = Math.sqrt(dx * dx + dy * dy); + var directionX = dx / distance; + var directionY = dy / distance; + + for (var i = 0; i < distance; i += 5) { + var px = self.lastx + directionX * i; + var py = self.lasty + directionY * i; + self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false); + self.maskCtx.fill(); + } + self.lastx = x; + self.lasty = y; + }); + + self.lasttime = performance.now(); + } + } + + handlePointerDown(self, event) { + if (event.ctrlKey) { + if (event.buttons == 1) { + MaskEditorDialog.mousedown_x = event.clientX; + MaskEditorDialog.mousedown_y = event.clientY; + + this.mousedown_pan_x = this.pan_x; + this.mousedown_pan_y = this.pan_y; + } + return; + } + + var brush_size = this.brush_size; + if (event instanceof PointerEvent && event.pointerType == "pen") { + brush_size *= event.pressure; + this.last_pressure = event.pressure; + } + + if ([0, 2, 5].includes(event.button)) { + self.drawing_mode = true; + + event.preventDefault(); + + if (event.shiftKey) { + self.zoom_lasty = event.clientY; + self.last_zoom_ratio = self.zoom_ratio; + return; + } + + const maskRect = self.maskCanvas.getBoundingClientRect(); + const x = + (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / + self.zoom_ratio; + const y = + (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / + self.zoom_ratio; + + self.maskCtx.beginPath(); + if (!event.altKey && event.button == 0) { + self.maskCtx.fillStyle = this.getMaskFillStyle(); + self.maskCtx.globalCompositeOperation = "source-over"; + } else { + self.maskCtx.globalCompositeOperation = "destination-out"; + } + self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false); + self.maskCtx.fill(); + self.lastx = x; + self.lasty = y; + self.lasttime = performance.now(); + } + } + + async save() { + const backupCanvas = document.createElement("canvas"); + const backupCtx = backupCanvas.getContext("2d", { + willReadFrequently: true, + }); + backupCanvas.width = this.image.width; + backupCanvas.height = this.image.height; + + backupCtx.clearRect(0, 0, backupCanvas.width, backupCanvas.height); + backupCtx.drawImage( + this.maskCanvas, + 0, + 0, + this.maskCanvas.width, + this.maskCanvas.height, + 0, + 0, + backupCanvas.width, + backupCanvas.height + ); + + // paste mask data into alpha channel + const backupData = backupCtx.getImageData( + 0, + 0, + backupCanvas.width, + backupCanvas.height + ); + + // refine mask image + for (let i = 0; i < backupData.data.length; i += 4) { + if (backupData.data[i + 3] == 255) backupData.data[i + 3] = 0; + else backupData.data[i + 3] = 255; + + backupData.data[i] = 0; + backupData.data[i + 1] = 0; + backupData.data[i + 2] = 0; + } + + backupCtx.globalCompositeOperation = "source-over"; + backupCtx.putImageData(backupData, 0, 0); + + const formData = new FormData(); + const filename = "clipspace-mask-" + performance.now() + ".png"; + + const item = { + filename: filename, + subfolder: "clipspace", + type: "input", + }; + + if (ComfyApp.clipspace.images) ComfyApp.clipspace.images[0] = item; + + if (ComfyApp.clipspace.widgets) { + const index = ComfyApp.clipspace.widgets.findIndex( + (obj) => obj.name === "image" + ); + + if (index >= 0) ComfyApp.clipspace.widgets[index].value = item; + } + + const dataURL = backupCanvas.toDataURL(); + const blob = dataURLToBlob(dataURL); + + let original_url = new URL(this.image.src); + + type Ref = { filename: string; subfolder?: string; type?: string }; + + const original_ref: Ref = { + filename: original_url.searchParams.get("filename"), + }; + + let original_subfolder = original_url.searchParams.get("subfolder"); + if (original_subfolder) original_ref.subfolder = original_subfolder; + + let original_type = original_url.searchParams.get("type"); + if (original_type) original_ref.type = original_type; + + formData.append("image", blob, filename); + formData.append("original_ref", JSON.stringify(original_ref)); + formData.append("type", "input"); + formData.append("subfolder", "clipspace"); + + this.saveButton.innerText = "Saving..."; + this.saveButton.disabled = true; + await uploadMask(item, formData); + ComfyApp.onClipspaceEditorSave(); + this.close(); + } } app.registerExtension({ - name: "Comfy.MaskEditor", - init(app) { - ComfyApp.open_maskeditor = - function () { - const dlg = MaskEditorDialog.getInstance(); - if(!dlg.isOpened()) { - dlg.show(); - } - }; + name: "Comfy.MaskEditor", + init(app) { + ComfyApp.open_maskeditor = function () { + const dlg = MaskEditorDialog.getInstance(); + if (!dlg.isOpened()) { + dlg.show(); + } + }; - const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0 - ClipspaceDialog.registerButton("MaskEditor", context_predicate, ComfyApp.open_maskeditor); - } + const context_predicate = () => + ComfyApp.clipspace && + ComfyApp.clipspace.imgs && + ComfyApp.clipspace.imgs.length > 0; + ClipspaceDialog.registerButton( + "MaskEditor", + context_predicate, + ComfyApp.open_maskeditor + ); + }, }); diff --git a/src/extensions/core/nodeTemplates.ts b/src/extensions/core/nodeTemplates.ts index 63dc9585e..fe813aa2a 100644 --- a/src/extensions/core/nodeTemplates.ts +++ b/src/extensions/core/nodeTemplates.ts @@ -24,404 +24,416 @@ const id = "Comfy.NodeTemplates"; const file = "comfy.templates.json"; class ManageTemplates extends ComfyDialog { - templates: any[]; - draggedEl: HTMLElement | null; - saveVisualCue: number | null; - emptyImg: HTMLImageElement; - importInput: HTMLInputElement; + templates: any[]; + draggedEl: HTMLElement | null; + saveVisualCue: number | null; + emptyImg: HTMLImageElement; + importInput: HTMLInputElement; - constructor() { - super(); - this.load().then((v) => { - this.templates = v; - }); + constructor() { + super(); + this.load().then((v) => { + this.templates = v; + }); - this.element.classList.add("comfy-manage-templates"); - this.draggedEl = null; - this.saveVisualCue = null; - this.emptyImg = new Image(); - this.emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; + this.element.classList.add("comfy-manage-templates"); + this.draggedEl = null; + this.saveVisualCue = null; + this.emptyImg = new Image(); + this.emptyImg.src = + "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; - this.importInput = $el("input", { - type: "file", - accept: ".json", - multiple: true, - style: { display: "none" }, - parent: document.body, - onchange: () => this.importAll(), - }) as HTMLInputElement; - } + this.importInput = $el("input", { + type: "file", + accept: ".json", + multiple: true, + style: { display: "none" }, + parent: document.body, + onchange: () => this.importAll(), + }) as HTMLInputElement; + } - createButtons() { - const btns = super.createButtons(); - btns[0].textContent = "Close"; - btns[0].onclick = (e) => { - clearTimeout(this.saveVisualCue); - this.close(); - }; - btns.unshift( - $el("button", { - type: "button", - textContent: "Export", - onclick: () => this.exportAll(), - }) - ); - btns.unshift( - $el("button", { - type: "button", - textContent: "Import", - onclick: () => { - this.importInput.click(); - }, - }) - ); - return btns; - } + createButtons() { + const btns = super.createButtons(); + btns[0].textContent = "Close"; + btns[0].onclick = (e) => { + clearTimeout(this.saveVisualCue); + this.close(); + }; + btns.unshift( + $el("button", { + type: "button", + textContent: "Export", + onclick: () => this.exportAll(), + }) + ); + btns.unshift( + $el("button", { + type: "button", + textContent: "Import", + onclick: () => { + this.importInput.click(); + }, + }) + ); + return btns; + } - async load() { - let templates = []; - if (app.storageLocation === "server") { - if (app.isNewUserSession) { - // New user so migrate existing templates - const json = localStorage.getItem(id); - if (json) { - templates = JSON.parse(json); - } - await api.storeUserData(file, json, { stringify: false }); - } else { - const res = await api.getUserData(file); - if (res.status === 200) { - try { - templates = await res.json(); - } catch (error) { - } - } else if (res.status !== 404) { - console.error(res.status + " " + res.statusText); - } - } - } else { - const json = localStorage.getItem(id); - if (json) { - templates = JSON.parse(json); - } - } + async load() { + let templates = []; + if (app.storageLocation === "server") { + if (app.isNewUserSession) { + // New user so migrate existing templates + const json = localStorage.getItem(id); + if (json) { + templates = JSON.parse(json); + } + await api.storeUserData(file, json, { stringify: false }); + } else { + const res = await api.getUserData(file); + if (res.status === 200) { + try { + templates = await res.json(); + } catch (error) {} + } else if (res.status !== 404) { + console.error(res.status + " " + res.statusText); + } + } + } else { + const json = localStorage.getItem(id); + if (json) { + templates = JSON.parse(json); + } + } - return templates ?? []; - } + return templates ?? []; + } - async store() { - if(app.storageLocation === "server") { - const templates = JSON.stringify(this.templates, undefined, 4); - localStorage.setItem(id, templates); // Backwards compatibility - try { - await api.storeUserData(file, templates, { stringify: false }); - } catch (error) { - console.error(error); - alert(error.message); - } - } else { - localStorage.setItem(id, JSON.stringify(this.templates)); - } - } + async store() { + if (app.storageLocation === "server") { + const templates = JSON.stringify(this.templates, undefined, 4); + localStorage.setItem(id, templates); // Backwards compatibility + try { + await api.storeUserData(file, templates, { stringify: false }); + } catch (error) { + console.error(error); + alert(error.message); + } + } else { + localStorage.setItem(id, JSON.stringify(this.templates)); + } + } - async importAll() { - for (const file of this.importInput.files) { - if (file.type === "application/json" || file.name.endsWith(".json")) { - const reader = new FileReader(); - reader.onload = async () => { - const importFile = JSON.parse(reader.result as string); - if (importFile?.templates) { - for (const template of importFile.templates) { - if (template?.name && template?.data) { - this.templates.push(template); - } - } - await this.store(); - } - }; - await reader.readAsText(file); - } - } + async importAll() { + for (const file of this.importInput.files) { + if (file.type === "application/json" || file.name.endsWith(".json")) { + const reader = new FileReader(); + reader.onload = async () => { + const importFile = JSON.parse(reader.result as string); + if (importFile?.templates) { + for (const template of importFile.templates) { + if (template?.name && template?.data) { + this.templates.push(template); + } + } + await this.store(); + } + }; + await reader.readAsText(file); + } + } - this.importInput.value = null; + this.importInput.value = null; - this.close(); - } + this.close(); + } - exportAll() { - if (this.templates.length == 0) { - alert("No templates to export."); - return; - } + exportAll() { + if (this.templates.length == 0) { + alert("No templates to export."); + return; + } - const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string - const blob = new Blob([json], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: "node_templates.json", - style: { display: "none" }, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - } + const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = $el("a", { + href: url, + download: "node_templates.json", + style: { display: "none" }, + parent: document.body, + }); + a.click(); + setTimeout(function () { + a.remove(); + window.URL.revokeObjectURL(url); + }, 0); + } - show() { - // Show list of template names + delete button - super.show( - $el( - "div", - {}, - this.templates.flatMap((t,i) => { - let nameInput; - return [ - $el( - "div", - { - dataset: { id: i.toString() }, - className: "tempateManagerRow", - style: { - display: "grid", - gridTemplateColumns: "1fr auto", - border: "1px dashed transparent", - gap: "5px", - backgroundColor: "var(--comfy-menu-bg)" - }, - ondragstart: (e) => { - this.draggedEl = e.currentTarget; - e.currentTarget.style.opacity = "0.6"; - e.currentTarget.style.border = "1px dashed yellow"; - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setDragImage(this.emptyImg, 0, 0); - }, - ondragend: (e) => { - e.target.style.opacity = "1"; - e.currentTarget.style.border = "1px dashed transparent"; - e.currentTarget.removeAttribute("draggable"); + show() { + // Show list of template names + delete button + super.show( + $el( + "div", + {}, + this.templates.flatMap((t, i) => { + let nameInput; + return [ + $el( + "div", + { + dataset: { id: i.toString() }, + className: "tempateManagerRow", + style: { + display: "grid", + gridTemplateColumns: "1fr auto", + border: "1px dashed transparent", + gap: "5px", + backgroundColor: "var(--comfy-menu-bg)", + }, + ondragstart: (e) => { + this.draggedEl = e.currentTarget; + e.currentTarget.style.opacity = "0.6"; + e.currentTarget.style.border = "1px dashed yellow"; + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setDragImage(this.emptyImg, 0, 0); + }, + ondragend: (e) => { + e.target.style.opacity = "1"; + e.currentTarget.style.border = "1px dashed transparent"; + e.currentTarget.removeAttribute("draggable"); - // rearrange the elements - this.element.querySelectorAll('.tempateManagerRow').forEach((el: HTMLElement,i) => { - var prev_i = Number.parseInt(el.dataset.id); + // rearrange the elements + this.element + .querySelectorAll(".tempateManagerRow") + .forEach((el: HTMLElement, i) => { + var prev_i = Number.parseInt(el.dataset.id); - if ( el == this.draggedEl && prev_i != i ) { - this.templates.splice(i, 0, this.templates.splice(prev_i, 1)[0]); - } - el.dataset.id = i.toString(); - }); - this.store(); - }, - ondragover: (e) => { - e.preventDefault(); - if ( e.currentTarget == this.draggedEl ) - return; + if (el == this.draggedEl && prev_i != i) { + this.templates.splice( + i, + 0, + this.templates.splice(prev_i, 1)[0] + ); + } + el.dataset.id = i.toString(); + }); + this.store(); + }, + ondragover: (e) => { + e.preventDefault(); + if (e.currentTarget == this.draggedEl) return; - let rect = e.currentTarget.getBoundingClientRect(); - if (e.clientY > rect.top + rect.height / 2) { - e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget.nextSibling); - } else { - e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget); - } - } - }, - [ - $el( - "label", - { - textContent: "Name: ", - style: { - cursor: "grab", - }, - onmousedown: (e) => { - // enable dragging only from the label - if (e.target.localName == 'label') - e.currentTarget.parentNode.draggable = 'true'; - } - }, - [ - $el("input", { - value: t.name, - dataset: { name: t.name }, - style: { - transitionProperty: 'background-color', - transitionDuration: '0s', - }, - onchange: (e) => { - clearTimeout(this.saveVisualCue); - var el = e.target; - var row = el.parentNode.parentNode; - this.templates[row.dataset.id].name = el.value.trim() || 'untitled'; - this.store(); - el.style.backgroundColor = 'rgb(40, 95, 40)'; - el.style.transitionDuration = '0s'; - // @ts-expect-error - // In browser env the return value is number. - this.saveVisualCue = setTimeout(function () { - el.style.transitionDuration = '.7s'; - el.style.backgroundColor = 'var(--comfy-input-bg)'; - }, 15); - }, - onkeypress: (e) => { - var el = e.target; - clearTimeout(this.saveVisualCue); - el.style.transitionDuration = '0s'; - el.style.backgroundColor = 'var(--comfy-input-bg)'; - }, - $: (el) => (nameInput = el), - }) - ] - ), - $el( - "div", - {}, - [ - $el("button", { - textContent: "Export", - style: { - fontSize: "12px", - fontWeight: "normal", - }, - onclick: (e) => { - const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string - const blob = new Blob([json], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: (nameInput.value || t.name) + ".json", - style: {display: "none"}, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - }, - }), - $el("button", { - textContent: "Delete", - style: { - fontSize: "12px", - color: "red", - fontWeight: "normal", - }, - onclick: (e) => { - const item = e.target.parentNode.parentNode; - item.parentNode.removeChild(item); - this.templates.splice(item.dataset.id*1, 1); - this.store(); - // update the rows index, setTimeout ensures that the list is updated - var that = this; - setTimeout(function (){ - that.element.querySelectorAll('.tempateManagerRow').forEach((el: HTMLElement,i) => { - el.dataset.id = i.toString(); - }); - }, 0); - }, - }), - ] - ), - ] - ) - ]; - }) - ) - ); - } + let rect = e.currentTarget.getBoundingClientRect(); + if (e.clientY > rect.top + rect.height / 2) { + e.currentTarget.parentNode.insertBefore( + this.draggedEl, + e.currentTarget.nextSibling + ); + } else { + e.currentTarget.parentNode.insertBefore( + this.draggedEl, + e.currentTarget + ); + } + }, + }, + [ + $el( + "label", + { + textContent: "Name: ", + style: { + cursor: "grab", + }, + onmousedown: (e) => { + // enable dragging only from the label + if (e.target.localName == "label") + e.currentTarget.parentNode.draggable = "true"; + }, + }, + [ + $el("input", { + value: t.name, + dataset: { name: t.name }, + style: { + transitionProperty: "background-color", + transitionDuration: "0s", + }, + onchange: (e) => { + clearTimeout(this.saveVisualCue); + var el = e.target; + var row = el.parentNode.parentNode; + this.templates[row.dataset.id].name = + el.value.trim() || "untitled"; + this.store(); + el.style.backgroundColor = "rgb(40, 95, 40)"; + el.style.transitionDuration = "0s"; + // @ts-expect-error + // In browser env the return value is number. + this.saveVisualCue = setTimeout(function () { + el.style.transitionDuration = ".7s"; + el.style.backgroundColor = "var(--comfy-input-bg)"; + }, 15); + }, + onkeypress: (e) => { + var el = e.target; + clearTimeout(this.saveVisualCue); + el.style.transitionDuration = "0s"; + el.style.backgroundColor = "var(--comfy-input-bg)"; + }, + $: (el) => (nameInput = el), + }), + ] + ), + $el("div", {}, [ + $el("button", { + textContent: "Export", + style: { + fontSize: "12px", + fontWeight: "normal", + }, + onclick: (e) => { + const json = JSON.stringify({ templates: [t] }, null, 2); // convert the data to a JSON string + const blob = new Blob([json], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = $el("a", { + href: url, + download: (nameInput.value || t.name) + ".json", + style: { display: "none" }, + parent: document.body, + }); + a.click(); + setTimeout(function () { + a.remove(); + window.URL.revokeObjectURL(url); + }, 0); + }, + }), + $el("button", { + textContent: "Delete", + style: { + fontSize: "12px", + color: "red", + fontWeight: "normal", + }, + onclick: (e) => { + const item = e.target.parentNode.parentNode; + item.parentNode.removeChild(item); + this.templates.splice(item.dataset.id * 1, 1); + this.store(); + // update the rows index, setTimeout ensures that the list is updated + var that = this; + setTimeout(function () { + that.element + .querySelectorAll(".tempateManagerRow") + .forEach((el: HTMLElement, i) => { + el.dataset.id = i.toString(); + }); + }, 0); + }, + }), + ]), + ] + ), + ]; + }) + ) + ); + } } app.registerExtension({ - name: id, - setup() { - const manage = new ManageTemplates(); + name: id, + setup() { + const manage = new ManageTemplates(); - const clipboardAction = async (cb) => { - // We use the clipboard functions but dont want to overwrite the current user clipboard - // Restore it after we've run our callback - const old = localStorage.getItem("litegrapheditor_clipboard"); - await cb(); - localStorage.setItem("litegrapheditor_clipboard", old); - }; + const clipboardAction = async (cb) => { + // We use the clipboard functions but dont want to overwrite the current user clipboard + // Restore it after we've run our callback + const old = localStorage.getItem("litegrapheditor_clipboard"); + await cb(); + localStorage.setItem("litegrapheditor_clipboard", old); + }; - // @ts-ignore - const orig = LGraphCanvas.prototype.getCanvasMenuOptions; - // @ts-ignore - LGraphCanvas.prototype.getCanvasMenuOptions = function () { - const options = orig.apply(this, arguments); + // @ts-ignore + const orig = LGraphCanvas.prototype.getCanvasMenuOptions; + // @ts-ignore + LGraphCanvas.prototype.getCanvasMenuOptions = function () { + const options = orig.apply(this, arguments); - options.push(null); - options.push({ - content: `Save Selected as Template`, - disabled: !Object.keys(app.canvas.selected_nodes || {}).length, - callback: () => { - const name = prompt("Enter name"); - if (!name?.trim()) return; + options.push(null); + options.push({ + content: `Save Selected as Template`, + disabled: !Object.keys(app.canvas.selected_nodes || {}).length, + callback: () => { + const name = prompt("Enter name"); + if (!name?.trim()) return; - clipboardAction(() => { - app.canvas.copyToClipboard(); - let data = localStorage.getItem("litegrapheditor_clipboard"); - data = JSON.parse(data); - const nodeIds = Object.keys(app.canvas.selected_nodes); - for (let i = 0; i < nodeIds.length; i++) { - const node = app.graph.getNodeById(Number.parseInt(nodeIds[i])); - // @ts-ignore - const nodeData = node?.constructor.nodeData; + clipboardAction(() => { + app.canvas.copyToClipboard(); + let data = localStorage.getItem("litegrapheditor_clipboard"); + data = JSON.parse(data); + const nodeIds = Object.keys(app.canvas.selected_nodes); + for (let i = 0; i < nodeIds.length; i++) { + const node = app.graph.getNodeById(Number.parseInt(nodeIds[i])); + // @ts-ignore + const nodeData = node?.constructor.nodeData; - let groupData = GroupNodeHandler.getGroupData(node); - if (groupData) { - groupData = groupData.nodeData; - // @ts-ignore - if (!data.groupNodes) { - // @ts-ignore - data.groupNodes = {}; - } - // @ts-ignore - data.groupNodes[nodeData.name] = groupData; - // @ts-ignore - data.nodes[i].type = nodeData.name; - } - } + let groupData = GroupNodeHandler.getGroupData(node); + if (groupData) { + groupData = groupData.nodeData; + // @ts-ignore + if (!data.groupNodes) { + // @ts-ignore + data.groupNodes = {}; + } + // @ts-ignore + data.groupNodes[nodeData.name] = groupData; + // @ts-ignore + data.nodes[i].type = nodeData.name; + } + } - manage.templates.push({ - name, - data: JSON.stringify(data), - }); - manage.store(); - }); - }, - }); + manage.templates.push({ + name, + data: JSON.stringify(data), + }); + manage.store(); + }); + }, + }); - // Map each template to a menu item - const subItems = manage.templates.map((t) => { - return { - content: t.name, - callback: () => { - clipboardAction(async () => { - const data = JSON.parse(t.data); - await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {}); - localStorage.setItem("litegrapheditor_clipboard", t.data); - app.canvas.pasteFromClipboard(); - }); - }, - }; - }); + // Map each template to a menu item + const subItems = manage.templates.map((t) => { + return { + content: t.name, + callback: () => { + clipboardAction(async () => { + const data = JSON.parse(t.data); + await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {}); + localStorage.setItem("litegrapheditor_clipboard", t.data); + app.canvas.pasteFromClipboard(); + }); + }, + }; + }); - subItems.push(null, { - content: "Manage", - callback: () => manage.show(), - }); + subItems.push(null, { + content: "Manage", + callback: () => manage.show(), + }); - options.push({ - content: "Node Templates", - submenu: { - options: subItems, - }, - }); + options.push({ + content: "Node Templates", + submenu: { + options: subItems, + }, + }); - return options; - }; - }, + return options; + }; + }, }); diff --git a/src/extensions/core/noteNode.ts b/src/extensions/core/noteNode.ts index a44f25203..f53c752d2 100644 --- a/src/extensions/core/noteNode.ts +++ b/src/extensions/core/noteNode.ts @@ -1,53 +1,55 @@ -import {app} from "../../scripts/app"; -import {ComfyWidgets} from "../../scripts/widgets"; +import { app } from "../../scripts/app"; +import { ComfyWidgets } from "../../scripts/widgets"; // Node that add notes to your project app.registerExtension({ - name: "Comfy.NoteNode", - registerCustomNodes() { - class NoteNode { - static category: string; - - // @ts-ignore - color=LGraphCanvas.node_colors.yellow.color; - // @ts-ignore - bgcolor=LGraphCanvas.node_colors.yellow.bgcolor; - // @ts-ignore - groupcolor = LGraphCanvas.node_colors.yellow.groupcolor; - properties: { text: string }; - serialize_widgets: boolean; - isVirtualNode: boolean; - collapsable: boolean; - title_mode: number; - - constructor() { - if (!this.properties) { - this.properties = { text: "" }; - } - // @ts-ignore - // Should we extends LGraphNode? - ComfyWidgets.STRING(this, "", ["", {default:this.properties.text, multiline: true}], app) - - this.serialize_widgets = true; - this.isVirtualNode = true; - - } + name: "Comfy.NoteNode", + registerCustomNodes() { + class NoteNode { + static category: string; + // @ts-ignore + color = LGraphCanvas.node_colors.yellow.color; + // @ts-ignore + bgcolor = LGraphCanvas.node_colors.yellow.bgcolor; + // @ts-ignore + groupcolor = LGraphCanvas.node_colors.yellow.groupcolor; + properties: { text: string }; + serialize_widgets: boolean; + isVirtualNode: boolean; + collapsable: boolean; + title_mode: number; + constructor() { + if (!this.properties) { + this.properties = { text: "" }; } - - // Load default visibility - - LiteGraph.registerNodeType( - "Note", - // @ts-ignore - Object.assign(NoteNode, { - title_mode: LiteGraph.NORMAL_TITLE, - title: "Note", - collapsable: true, - }) + ComfyWidgets.STRING( + // @ts-ignore + // Should we extends LGraphNode? + this, + "", + ["", { default: this.properties.text, multiline: true }], + app ); - NoteNode.category = "utils"; - }, + this.serialize_widgets = true; + this.isVirtualNode = true; + } + } + + // Load default visibility + + LiteGraph.registerNodeType( + "Note", + // @ts-ignore + Object.assign(NoteNode, { + title_mode: LiteGraph.NORMAL_TITLE, + title: "Note", + collapsable: true, + }) + ); + + NoteNode.category = "utils"; + }, }); diff --git a/src/extensions/core/rerouteNode.js b/src/extensions/core/rerouteNode.js index bb8666270..839bb8ef9 100644 --- a/src/extensions/core/rerouteNode.js +++ b/src/extensions/core/rerouteNode.js @@ -4,271 +4,311 @@ import { mergeIfValid, getWidgetConfig, setWidgetConfig } from "./widgetInputs"; // Node that allows you to redirect connections for cleaner graphs app.registerExtension({ - name: "Comfy.RerouteNode", - registerCustomNodes(app) { - class RerouteNode { - constructor() { - if (!this.properties) { - this.properties = {}; - } - this.properties.showOutputText = RerouteNode.defaultVisibility; - this.properties.horizontal = false; + name: "Comfy.RerouteNode", + registerCustomNodes(app) { + class RerouteNode { + constructor() { + if (!this.properties) { + this.properties = {}; + } + this.properties.showOutputText = RerouteNode.defaultVisibility; + this.properties.horizontal = false; - this.addInput("", "*"); - this.addOutput(this.properties.showOutputText ? "*" : "", "*"); + this.addInput("", "*"); + this.addOutput(this.properties.showOutputText ? "*" : "", "*"); - this.onAfterGraphConfigured = function () { - requestAnimationFrame(() => { - this.onConnectionsChange(LiteGraph.INPUT, null, true, null); - }); - }; + this.onAfterGraphConfigured = function () { + requestAnimationFrame(() => { + this.onConnectionsChange(LiteGraph.INPUT, null, true, null); + }); + }; - this.onConnectionsChange = function (type, index, connected, link_info) { - this.applyOrientation(); + this.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + this.applyOrientation(); - // Prevent multiple connections to different types when we have no input - if (connected && type === LiteGraph.OUTPUT) { - // Ignore wildcard nodes as these will be updated to real types - const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*")); - if (types.size > 1) { - const linksToDisconnect = []; - for (let i = 0; i < this.outputs[0].links.length - 1; i++) { - const linkId = this.outputs[0].links[i]; - const link = app.graph.links[linkId]; - linksToDisconnect.push(link); - } - for (const link of linksToDisconnect) { - const node = app.graph.getNodeById(link.target_id); - node.disconnectInput(link.target_slot); - } - } - } + // Prevent multiple connections to different types when we have no input + if (connected && type === LiteGraph.OUTPUT) { + // Ignore wildcard nodes as these will be updated to real types + const types = new Set( + this.outputs[0].links + .map((l) => app.graph.links[l].type) + .filter((t) => t !== "*") + ); + if (types.size > 1) { + const linksToDisconnect = []; + for (let i = 0; i < this.outputs[0].links.length - 1; i++) { + const linkId = this.outputs[0].links[i]; + const link = app.graph.links[linkId]; + linksToDisconnect.push(link); + } + for (const link of linksToDisconnect) { + const node = app.graph.getNodeById(link.target_id); + node.disconnectInput(link.target_slot); + } + } + } - // Find root input - let currentNode = this; - let updateNodes = []; - let inputType = null; - let inputNode = null; - while (currentNode) { - updateNodes.unshift(currentNode); - const linkId = currentNode.inputs[0].link; - if (linkId !== null) { - const link = app.graph.links[linkId]; - if (!link) return; - const node = app.graph.getNodeById(link.origin_id); - const type = node.constructor.type; - if (type === "Reroute") { - if (node === this) { - // We've found a circle - currentNode.disconnectInput(link.target_slot); - currentNode = null; - } else { - // Move the previous node - currentNode = node; - } - } else { - // We've found the end - inputNode = currentNode; - inputType = node.outputs[link.origin_slot]?.type ?? null; - break; - } - } else { - // This path has no input node - currentNode = null; - break; - } - } + // Find root input + let currentNode = this; + let updateNodes = []; + let inputType = null; + let inputNode = null; + while (currentNode) { + updateNodes.unshift(currentNode); + const linkId = currentNode.inputs[0].link; + if (linkId !== null) { + const link = app.graph.links[linkId]; + if (!link) return; + const node = app.graph.getNodeById(link.origin_id); + const type = node.constructor.type; + if (type === "Reroute") { + if (node === this) { + // We've found a circle + currentNode.disconnectInput(link.target_slot); + currentNode = null; + } else { + // Move the previous node + currentNode = node; + } + } else { + // We've found the end + inputNode = currentNode; + inputType = node.outputs[link.origin_slot]?.type ?? null; + break; + } + } else { + // This path has no input node + currentNode = null; + break; + } + } - // Find all outputs - const nodes = [this]; - let outputType = null; - while (nodes.length) { - currentNode = nodes.pop(); - const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || []; - if (outputs.length) { - for (const linkId of outputs) { - const link = app.graph.links[linkId]; + // Find all outputs + const nodes = [this]; + let outputType = null; + while (nodes.length) { + currentNode = nodes.pop(); + const outputs = + (currentNode.outputs ? currentNode.outputs[0].links : []) || []; + if (outputs.length) { + for (const linkId of outputs) { + const link = app.graph.links[linkId]; - // When disconnecting sometimes the link is still registered - if (!link) continue; + // When disconnecting sometimes the link is still registered + if (!link) continue; - const node = app.graph.getNodeById(link.target_id); - const type = node.constructor.type; + const node = app.graph.getNodeById(link.target_id); + const type = node.constructor.type; - if (type === "Reroute") { - // Follow reroute nodes - nodes.push(node); - updateNodes.push(node); - } else { - // We've found an output - const nodeOutType = - node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type - ? node.inputs[link.target_slot].type - : null; - if (inputType && inputType !== "*" && nodeOutType !== inputType) { - // The output doesnt match our input so disconnect it - node.disconnectInput(link.target_slot); - } else { - outputType = nodeOutType; - } - } - } - } else { - // No more outputs for this path - } - } + if (type === "Reroute") { + // Follow reroute nodes + nodes.push(node); + updateNodes.push(node); + } else { + // We've found an output + const nodeOutType = + node.inputs && + node.inputs[link?.target_slot] && + node.inputs[link.target_slot].type + ? node.inputs[link.target_slot].type + : null; + if ( + inputType && + inputType !== "*" && + nodeOutType !== inputType + ) { + // The output doesnt match our input so disconnect it + node.disconnectInput(link.target_slot); + } else { + outputType = nodeOutType; + } + } + } + } else { + // No more outputs for this path + } + } - const displayType = inputType || outputType || "*"; - const color = LGraphCanvas.link_type_colors[displayType]; + const displayType = inputType || outputType || "*"; + const color = LGraphCanvas.link_type_colors[displayType]; - let widgetConfig; - let targetWidget; - let widgetType; - // Update the types of each node - for (const node of updateNodes) { - // If we dont have an input type we are always wildcard but we'll show the output type - // This lets you change the output link to a different type and all nodes will update - node.outputs[0].type = inputType || "*"; - node.__outputType = displayType; - node.outputs[0].name = node.properties.showOutputText ? displayType : ""; - node.size = node.computeSize(); - node.applyOrientation(); + let widgetConfig; + let targetWidget; + let widgetType; + // Update the types of each node + for (const node of updateNodes) { + // If we dont have an input type we are always wildcard but we'll show the output type + // This lets you change the output link to a different type and all nodes will update + node.outputs[0].type = inputType || "*"; + node.__outputType = displayType; + node.outputs[0].name = node.properties.showOutputText + ? displayType + : ""; + node.size = node.computeSize(); + node.applyOrientation(); - for (const l of node.outputs[0].links || []) { - const link = app.graph.links[l]; - if (link) { - link.color = color; + for (const l of node.outputs[0].links || []) { + const link = app.graph.links[l]; + if (link) { + link.color = color; - if (app.configuringGraph) continue; - const targetNode = app.graph.getNodeById(link.target_id); - const targetInput = targetNode.inputs?.[link.target_slot]; - if (targetInput?.widget) { - const config = getWidgetConfig(targetInput); - if (!widgetConfig) { - widgetConfig = config[1] ?? {}; - widgetType = config[0]; - } - if (!targetWidget) { - targetWidget = targetNode.widgets?.find((w) => w.name === targetInput.widget.name); - } + if (app.configuringGraph) continue; + const targetNode = app.graph.getNodeById(link.target_id); + const targetInput = targetNode.inputs?.[link.target_slot]; + if (targetInput?.widget) { + const config = getWidgetConfig(targetInput); + if (!widgetConfig) { + widgetConfig = config[1] ?? {}; + widgetType = config[0]; + } + if (!targetWidget) { + targetWidget = targetNode.widgets?.find( + (w) => w.name === targetInput.widget.name + ); + } - const merged = mergeIfValid(targetInput, [config[0], widgetConfig]); - if (merged.customConfig) { - widgetConfig = merged.customConfig; - } - } - } - } - } + const merged = mergeIfValid(targetInput, [ + config[0], + widgetConfig, + ]); + if (merged.customConfig) { + widgetConfig = merged.customConfig; + } + } + } + } + } - for (const node of updateNodes) { - if (widgetConfig && outputType) { - node.inputs[0].widget = { name: "value" }; - setWidgetConfig(node.inputs[0], [widgetType ?? displayType, widgetConfig], targetWidget); - } else { - setWidgetConfig(node.inputs[0], null); - } - } + for (const node of updateNodes) { + if (widgetConfig && outputType) { + node.inputs[0].widget = { name: "value" }; + setWidgetConfig( + node.inputs[0], + [widgetType ?? displayType, widgetConfig], + targetWidget + ); + } else { + setWidgetConfig(node.inputs[0], null); + } + } - if (inputNode) { - const link = app.graph.links[inputNode.inputs[0].link]; - if (link) { - link.color = color; - } - } - }; + if (inputNode) { + const link = app.graph.links[inputNode.inputs[0].link]; + if (link) { + link.color = color; + } + } + }; - this.clone = function () { - const cloned = RerouteNode.prototype.clone.apply(this); - cloned.removeOutput(0); - cloned.addOutput(this.properties.showOutputText ? "*" : "", "*"); - cloned.size = cloned.computeSize(); - return cloned; - }; + this.clone = function () { + const cloned = RerouteNode.prototype.clone.apply(this); + cloned.removeOutput(0); + cloned.addOutput(this.properties.showOutputText ? "*" : "", "*"); + cloned.size = cloned.computeSize(); + return cloned; + }; - // This node is purely frontend and does not impact the resulting prompt so should not be serialized - this.isVirtualNode = true; - } + // This node is purely frontend and does not impact the resulting prompt so should not be serialized + this.isVirtualNode = true; + } - getExtraMenuOptions(_, options) { - options.unshift( - { - content: (this.properties.showOutputText ? "Hide" : "Show") + " Type", - callback: () => { - this.properties.showOutputText = !this.properties.showOutputText; - if (this.properties.showOutputText) { - this.outputs[0].name = this.__outputType || this.outputs[0].type; - } else { - this.outputs[0].name = ""; - } - this.size = this.computeSize(); - this.applyOrientation(); - app.graph.setDirtyCanvas(true, true); - }, - }, - { - content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default", - callback: () => { - RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility); - }, - }, - { - // naming is inverted with respect to LiteGraphNode.horizontal - // LiteGraphNode.horizontal == true means that - // each slot in the inputs and outputs are layed out horizontally, - // which is the opposite of the visual orientation of the inputs and outputs as a node - content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"), - callback: () => { - this.properties.horizontal = !this.properties.horizontal; - this.applyOrientation(); - }, - } - ); - } - applyOrientation() { - this.horizontal = this.properties.horizontal; - if (this.horizontal) { - // we correct the input position, because LiteGraphNode.horizontal - // doesn't account for title presence - // which reroute nodes don't have - this.inputs[0].pos = [this.size[0] / 2, 0]; - } else { - delete this.inputs[0].pos; - } - app.graph.setDirtyCanvas(true, true); - } + getExtraMenuOptions(_, options) { + options.unshift( + { + content: + (this.properties.showOutputText ? "Hide" : "Show") + " Type", + callback: () => { + this.properties.showOutputText = !this.properties.showOutputText; + if (this.properties.showOutputText) { + this.outputs[0].name = + this.__outputType || this.outputs[0].type; + } else { + this.outputs[0].name = ""; + } + this.size = this.computeSize(); + this.applyOrientation(); + app.graph.setDirtyCanvas(true, true); + }, + }, + { + content: + (RerouteNode.defaultVisibility ? "Hide" : "Show") + + " Type By Default", + callback: () => { + RerouteNode.setDefaultTextVisibility( + !RerouteNode.defaultVisibility + ); + }, + }, + { + // naming is inverted with respect to LiteGraphNode.horizontal + // LiteGraphNode.horizontal == true means that + // each slot in the inputs and outputs are layed out horizontally, + // which is the opposite of the visual orientation of the inputs and outputs as a node + content: + "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"), + callback: () => { + this.properties.horizontal = !this.properties.horizontal; + this.applyOrientation(); + }, + } + ); + } + applyOrientation() { + this.horizontal = this.properties.horizontal; + if (this.horizontal) { + // we correct the input position, because LiteGraphNode.horizontal + // doesn't account for title presence + // which reroute nodes don't have + this.inputs[0].pos = [this.size[0] / 2, 0]; + } else { + delete this.inputs[0].pos; + } + app.graph.setDirtyCanvas(true, true); + } - computeSize() { - return [ - this.properties.showOutputText && this.outputs && this.outputs.length - ? Math.max(75, LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40) - : 75, - 26, - ]; - } + computeSize() { + return [ + this.properties.showOutputText && this.outputs && this.outputs.length + ? Math.max( + 75, + LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + + 40 + ) + : 75, + 26, + ]; + } - static setDefaultTextVisibility(visible) { - RerouteNode.defaultVisibility = visible; - if (visible) { - localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true"; - } else { - delete localStorage["Comfy.RerouteNode.DefaultVisibility"]; - } - } - } + static setDefaultTextVisibility(visible) { + RerouteNode.defaultVisibility = visible; + if (visible) { + localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true"; + } else { + delete localStorage["Comfy.RerouteNode.DefaultVisibility"]; + } + } + } - // Load default visibility - RerouteNode.setDefaultTextVisibility(!!localStorage["Comfy.RerouteNode.DefaultVisibility"]); + // Load default visibility + RerouteNode.setDefaultTextVisibility( + !!localStorage["Comfy.RerouteNode.DefaultVisibility"] + ); - LiteGraph.registerNodeType( - "Reroute", - Object.assign(RerouteNode, { - title_mode: LiteGraph.NO_TITLE, - title: "Reroute", - collapsable: false, - }) - ); + LiteGraph.registerNodeType( + "Reroute", + Object.assign(RerouteNode, { + title_mode: LiteGraph.NO_TITLE, + title: "Reroute", + collapsable: false, + }) + ); - RerouteNode.category = "utils"; - }, + RerouteNode.category = "utils"; + }, }); diff --git a/src/extensions/core/saveImageExtraOutput.ts b/src/extensions/core/saveImageExtraOutput.ts index 5dccd2ac3..c63442265 100644 --- a/src/extensions/core/saveImageExtraOutput.ts +++ b/src/extensions/core/saveImageExtraOutput.ts @@ -3,33 +3,41 @@ import { applyTextReplacements } from "../../scripts/utils"; // Use widget values and dates in output filenames app.registerExtension({ - name: "Comfy.SaveImageExtraOutput", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "SaveImage") { - const onNodeCreated = nodeType.prototype.onNodeCreated; - // When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + name: "Comfy.SaveImageExtraOutput", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "SaveImage") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + // When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined; - const widget = this.widgets.find((w) => w.name === "filename_prefix"); - widget.serializeValue = () => { - return applyTextReplacements(app, widget.value); - }; + const widget = this.widgets.find((w) => w.name === "filename_prefix"); + widget.serializeValue = () => { + return applyTextReplacements(app, widget.value); + }; - return r; - }; - } else { - // When any other node is created add a property to alias the node - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + return r; + }; + } else { + // When any other node is created add a property to alias the node + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined; - if (!this.properties || !("Node name for S&R" in this.properties)) { - this.addProperty("Node name for S&R", this.constructor.type, "string"); - } + if (!this.properties || !("Node name for S&R" in this.properties)) { + this.addProperty( + "Node name for S&R", + this.constructor.type, + "string" + ); + } - return r; - }; - } - }, + return r; + }; + } + }, }); diff --git a/src/extensions/core/simpleTouchSupport.ts b/src/extensions/core/simpleTouchSupport.ts index 3175ea693..71551069d 100644 --- a/src/extensions/core/simpleTouchSupport.ts +++ b/src/extensions/core/simpleTouchSupport.ts @@ -4,108 +4,111 @@ let touchZooming; let touchCount = 0; app.registerExtension({ - name: "Comfy.SimpleTouchSupport", - setup() { - let zoomPos; - let touchTime; - let lastTouch; + name: "Comfy.SimpleTouchSupport", + setup() { + let zoomPos; + let touchTime; + let lastTouch; - function getMultiTouchPos(e) { - return Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY); - } + function getMultiTouchPos(e) { + return Math.hypot( + e.touches[0].clientX - e.touches[1].clientX, + e.touches[0].clientY - e.touches[1].clientY + ); + } - app.canvasEl.addEventListener( - "touchstart", - (e) => { - touchCount++; - lastTouch = null; - if (e.touches?.length === 1) { - // Store start time for press+hold for context menu - touchTime = new Date(); - lastTouch = e.touches[0]; - } else { - touchTime = null; - if (e.touches?.length === 2) { - // Store center pos for zoom - zoomPos = getMultiTouchPos(e); - app.canvas.pointer_is_down = false; - } - } - }, - true - ); + app.canvasEl.addEventListener( + "touchstart", + (e) => { + touchCount++; + lastTouch = null; + if (e.touches?.length === 1) { + // Store start time for press+hold for context menu + touchTime = new Date(); + lastTouch = e.touches[0]; + } else { + touchTime = null; + if (e.touches?.length === 2) { + // Store center pos for zoom + zoomPos = getMultiTouchPos(e); + app.canvas.pointer_is_down = false; + } + } + }, + true + ); - app.canvasEl.addEventListener("touchend", (e: TouchEvent) => { - touchZooming = false; - touchCount = e.touches?.length ?? touchCount - 1; - if (touchTime && !e.touches?.length) { - if ((new Date()).getTime() - touchTime > 600) { - try { - // hack to get litegraph to use this event - e.constructor = CustomEvent; - } catch (error) {} - // @ts-ignore - e.clientX = lastTouch.clientX; - // @ts-ignore - e.clientY = lastTouch.clientY; + app.canvasEl.addEventListener("touchend", (e: TouchEvent) => { + touchZooming = false; + touchCount = e.touches?.length ?? touchCount - 1; + if (touchTime && !e.touches?.length) { + if (new Date().getTime() - touchTime > 600) { + try { + // hack to get litegraph to use this event + e.constructor = CustomEvent; + } catch (error) {} + // @ts-ignore + e.clientX = lastTouch.clientX; + // @ts-ignore + e.clientY = lastTouch.clientY; - app.canvas.pointer_is_down = true; - // @ts-ignore - app.canvas._mousedown_callback(e); - } - touchTime = null; - } - }); + app.canvas.pointer_is_down = true; + // @ts-ignore + app.canvas._mousedown_callback(e); + } + touchTime = null; + } + }); - app.canvasEl.addEventListener( - "touchmove", - (e) => { - touchTime = null; - if (e.touches?.length === 2) { - app.canvas.pointer_is_down = false; - touchZooming = true; - // @ts-ignore - LiteGraph.closeAllContextMenus(); - // @ts-ignore - app.canvas.search_box?.close(); - const newZoomPos = getMultiTouchPos(e); + app.canvasEl.addEventListener( + "touchmove", + (e) => { + touchTime = null; + if (e.touches?.length === 2) { + app.canvas.pointer_is_down = false; + touchZooming = true; + // @ts-ignore + LiteGraph.closeAllContextMenus(); + // @ts-ignore + app.canvas.search_box?.close(); + const newZoomPos = getMultiTouchPos(e); - const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2; - const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2; + const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2; + const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2; - let scale = app.canvas.ds.scale; - const diff = zoomPos - newZoomPos; - if (diff > 0.5) { - scale *= 1 / 1.07; - } else if (diff < -0.5) { - scale *= 1.07; - } - app.canvas.ds.changeScale(scale, [midX, midY]); - app.canvas.setDirty(true, true); - zoomPos = newZoomPos; - } - }, - true - ); - }, + let scale = app.canvas.ds.scale; + const diff = zoomPos - newZoomPos; + if (diff > 0.5) { + scale *= 1 / 1.07; + } else if (diff < -0.5) { + scale *= 1.07; + } + app.canvas.ds.changeScale(scale, [midX, midY]); + app.canvas.setDirty(true, true); + zoomPos = newZoomPos; + } + }, + true + ); + }, }); // @ts-ignore const processMouseDown = LGraphCanvas.prototype.processMouseDown; // @ts-ignore LGraphCanvas.prototype.processMouseDown = function (e) { - if (touchZooming || touchCount) { - return; - } - return processMouseDown.apply(this, arguments); + if (touchZooming || touchCount) { + return; + } + return processMouseDown.apply(this, arguments); }; // @ts-ignore const processMouseMove = LGraphCanvas.prototype.processMouseMove; // @ts-ignore LGraphCanvas.prototype.processMouseMove = function (e) { - if (touchZooming || touchCount > 1) { - return; - } - return processMouseMove.apply(this, arguments); + if (touchZooming || touchCount > 1) { + return; + } + return processMouseMove.apply(this, arguments); }; diff --git a/src/extensions/core/slotDefaults.js b/src/extensions/core/slotDefaults.js index 905358cda..d2acf2bd0 100644 --- a/src/extensions/core/slotDefaults.js +++ b/src/extensions/core/slotDefaults.js @@ -3,89 +3,94 @@ import { ComfyWidgets } from "../../scripts/widgets"; // Adds defaults for quickly adding nodes with middle click on the input/output app.registerExtension({ - name: "Comfy.SlotDefaults", - suggestionsNumber: null, - init() { - LiteGraph.search_filter_enabled = true; - LiteGraph.middle_click_slot_add_default_node = true; - this.suggestionsNumber = app.ui.settings.addSetting({ - id: "Comfy.NodeSuggestions.number", - name: "Number of nodes suggestions", - type: "slider", - attrs: { - min: 1, - max: 100, - step: 1, - }, - defaultValue: 5, - onChange: (newVal, oldVal) => { - this.setDefaults(newVal); - } - }); - }, - slot_types_default_out: {}, - slot_types_default_in: {}, - async beforeRegisterNodeDef(nodeType, nodeData, app) { - var nodeId = nodeData.name; - var inputs = []; - inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs - for (const inputKey in inputs) { - var input = (inputs[inputKey]); - if (typeof input[0] !== "string") continue; + name: "Comfy.SlotDefaults", + suggestionsNumber: null, + init() { + LiteGraph.search_filter_enabled = true; + LiteGraph.middle_click_slot_add_default_node = true; + this.suggestionsNumber = app.ui.settings.addSetting({ + id: "Comfy.NodeSuggestions.number", + name: "Number of nodes suggestions", + type: "slider", + attrs: { + min: 1, + max: 100, + step: 1, + }, + defaultValue: 5, + onChange: (newVal, oldVal) => { + this.setDefaults(newVal); + }, + }); + }, + slot_types_default_out: {}, + slot_types_default_in: {}, + async beforeRegisterNodeDef(nodeType, nodeData, app) { + var nodeId = nodeData.name; + var inputs = []; + inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs + for (const inputKey in inputs) { + var input = inputs[inputKey]; + if (typeof input[0] !== "string") continue; - var type = input[0] - if (type in ComfyWidgets) { - var customProperties = input[1] - if (!(customProperties?.forceInput)) continue; //ignore widgets that don't force input - } + var type = input[0]; + if (type in ComfyWidgets) { + var customProperties = input[1]; + if (!customProperties?.forceInput) continue; //ignore widgets that don't force input + } - if (!(type in this.slot_types_default_out)) { - this.slot_types_default_out[type] = ["Reroute"]; - } - if (this.slot_types_default_out[type].includes(nodeId)) continue; - this.slot_types_default_out[type].push(nodeId); + if (!(type in this.slot_types_default_out)) { + this.slot_types_default_out[type] = ["Reroute"]; + } + if (this.slot_types_default_out[type].includes(nodeId)) continue; + this.slot_types_default_out[type].push(nodeId); - // Input types have to be stored as lower case - // Store each node that can handle this input type - const lowerType = type.toLocaleLowerCase(); - if (!(lowerType in LiteGraph.registered_slot_in_types)) { - LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] }; - } - LiteGraph.registered_slot_in_types[lowerType].nodes.push(nodeType.comfyClass); - } + // Input types have to be stored as lower case + // Store each node that can handle this input type + const lowerType = type.toLocaleLowerCase(); + if (!(lowerType in LiteGraph.registered_slot_in_types)) { + LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] }; + } + LiteGraph.registered_slot_in_types[lowerType].nodes.push( + nodeType.comfyClass + ); + } - var outputs = nodeData["output"]; - for (const key in outputs) { - var type = outputs[key]; - if (!(type in this.slot_types_default_in)) { - this.slot_types_default_in[type] = ["Reroute"];// ["Reroute", "Primitive"]; primitive doesn't always work :'() - } + var outputs = nodeData["output"]; + for (const key in outputs) { + var type = outputs[key]; + if (!(type in this.slot_types_default_in)) { + this.slot_types_default_in[type] = ["Reroute"]; // ["Reroute", "Primitive"]; primitive doesn't always work :'() + } - this.slot_types_default_in[type].push(nodeId); + this.slot_types_default_in[type].push(nodeId); - // Store each node that can handle this output type - if (!(type in LiteGraph.registered_slot_out_types)) { - LiteGraph.registered_slot_out_types[type] = { nodes: [] }; - } - LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass); + // Store each node that can handle this output type + if (!(type in LiteGraph.registered_slot_out_types)) { + LiteGraph.registered_slot_out_types[type] = { nodes: [] }; + } + LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass); - if(!LiteGraph.slot_types_out.includes(type)) { - LiteGraph.slot_types_out.push(type); - } - } - var maxNum = this.suggestionsNumber.value; - this.setDefaults(maxNum); - }, - setDefaults(maxNum) { + if (!LiteGraph.slot_types_out.includes(type)) { + LiteGraph.slot_types_out.push(type); + } + } + var maxNum = this.suggestionsNumber.value; + this.setDefaults(maxNum); + }, + setDefaults(maxNum) { + LiteGraph.slot_types_default_out = {}; + LiteGraph.slot_types_default_in = {}; - LiteGraph.slot_types_default_out = {}; - LiteGraph.slot_types_default_in = {}; - - for (const type in this.slot_types_default_out) { - LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[type].slice(0, maxNum); - } - for (const type in this.slot_types_default_in) { - LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[type].slice(0, maxNum); - } - } + for (const type in this.slot_types_default_out) { + LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[ + type + ].slice(0, maxNum); + } + for (const type in this.slot_types_default_in) { + LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[ + type + ].slice(0, maxNum); + } + }, }); diff --git a/src/extensions/core/snapToGrid.ts b/src/extensions/core/snapToGrid.ts index 5af1120db..fc248dbde 100644 --- a/src/extensions/core/snapToGrid.ts +++ b/src/extensions/core/snapToGrid.ts @@ -4,178 +4,192 @@ import { app } from "../../scripts/app"; /** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */ function roundVectorToGrid(vec) { - vec[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE); - vec[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE); - return vec; + vec[0] = + LiteGraph.CANVAS_GRID_SIZE * + Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE); + vec[1] = + LiteGraph.CANVAS_GRID_SIZE * + Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE); + return vec; } app.registerExtension({ - name: "Comfy.SnapToGrid", - init() { - // Add setting to control grid size - app.ui.settings.addSetting({ - id: "Comfy.SnapToGrid.GridSize", - name: "Grid Size", - type: "slider", - attrs: { - min: 1, - max: 500, - }, - tooltip: - "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.", - defaultValue: LiteGraph.CANVAS_GRID_SIZE, - onChange(value) { - LiteGraph.CANVAS_GRID_SIZE = +value; - }, - }); + name: "Comfy.SnapToGrid", + init() { + // Add setting to control grid size + app.ui.settings.addSetting({ + id: "Comfy.SnapToGrid.GridSize", + name: "Grid Size", + type: "slider", + attrs: { + min: 1, + max: 500, + }, + tooltip: + "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.", + defaultValue: LiteGraph.CANVAS_GRID_SIZE, + onChange(value) { + LiteGraph.CANVAS_GRID_SIZE = +value; + }, + }); - // After moving a node, if the shift key is down align it to grid - const onNodeMoved = app.canvas.onNodeMoved; - app.canvas.onNodeMoved = function (node) { - const r = onNodeMoved?.apply(this, arguments); + // After moving a node, if the shift key is down align it to grid + const onNodeMoved = app.canvas.onNodeMoved; + app.canvas.onNodeMoved = function (node) { + const r = onNodeMoved?.apply(this, arguments); - if (app.shiftDown) { - // Ensure all selected nodes are realigned - for (const id in this.selected_nodes) { - this.selected_nodes[id].alignToGrid(); - } - } + if (app.shiftDown) { + // Ensure all selected nodes are realigned + for (const id in this.selected_nodes) { + this.selected_nodes[id].alignToGrid(); + } + } - return r; - }; + return r; + }; - // When a node is added, add a resize handler to it so we can fix align the size with the grid - const onNodeAdded = app.graph.onNodeAdded; - app.graph.onNodeAdded = function (node) { - const onResize = node.onResize; - node.onResize = function () { - if (app.shiftDown) { - roundVectorToGrid(node.size); - } - return onResize?.apply(this, arguments); - }; - return onNodeAdded?.apply(this, arguments); - }; + // When a node is added, add a resize handler to it so we can fix align the size with the grid + const onNodeAdded = app.graph.onNodeAdded; + app.graph.onNodeAdded = function (node) { + const onResize = node.onResize; + node.onResize = function () { + if (app.shiftDown) { + roundVectorToGrid(node.size); + } + return onResize?.apply(this, arguments); + }; + return onNodeAdded?.apply(this, arguments); + }; - // Draw a preview of where the node will go if holding shift and the node is selected - // @ts-ignore - const origDrawNode = LGraphCanvas.prototype.drawNode; - // @ts-ignore - LGraphCanvas.prototype.drawNode = function (node, ctx) { - if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) { - const [x, y] = roundVectorToGrid([...node.pos]); - const shiftX = x - node.pos[0]; - let shiftY = y - node.pos[1]; + // Draw a preview of where the node will go if holding shift and the node is selected + // @ts-ignore + const origDrawNode = LGraphCanvas.prototype.drawNode; + // @ts-ignore + LGraphCanvas.prototype.drawNode = function (node, ctx) { + if ( + app.shiftDown && + this.node_dragged && + node.id in this.selected_nodes + ) { + const [x, y] = roundVectorToGrid([...node.pos]); + const shiftX = x - node.pos[0]; + let shiftY = y - node.pos[1]; - let w, h; - if (node.flags.collapsed) { - w = node._collapsed_width; - h = LiteGraph.NODE_TITLE_HEIGHT; - shiftY -= LiteGraph.NODE_TITLE_HEIGHT; - } else { - w = node.size[0]; - h = node.size[1]; - let titleMode = node.constructor.title_mode; - if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) { - h += LiteGraph.NODE_TITLE_HEIGHT; - shiftY -= LiteGraph.NODE_TITLE_HEIGHT; - } - } - const f = ctx.fillStyle; - ctx.fillStyle = "rgba(100, 100, 100, 0.5)"; - ctx.fillRect(shiftX, shiftY, w, h); - ctx.fillStyle = f; - } + let w, h; + if (node.flags.collapsed) { + w = node._collapsed_width; + h = LiteGraph.NODE_TITLE_HEIGHT; + shiftY -= LiteGraph.NODE_TITLE_HEIGHT; + } else { + w = node.size[0]; + h = node.size[1]; + let titleMode = node.constructor.title_mode; + if ( + titleMode !== LiteGraph.TRANSPARENT_TITLE && + titleMode !== LiteGraph.NO_TITLE + ) { + h += LiteGraph.NODE_TITLE_HEIGHT; + shiftY -= LiteGraph.NODE_TITLE_HEIGHT; + } + } + const f = ctx.fillStyle; + ctx.fillStyle = "rgba(100, 100, 100, 0.5)"; + ctx.fillRect(shiftX, shiftY, w, h); + ctx.fillStyle = f; + } - return origDrawNode.apply(this, arguments); - }; + return origDrawNode.apply(this, arguments); + }; - /** - * The currently moving, selected group only. Set after the `selected_group` has actually started - * moving. - */ - let selectedAndMovingGroup = null; + /** + * The currently moving, selected group only. Set after the `selected_group` has actually started + * moving. + */ + let selectedAndMovingGroup = null; - /** - * Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups` - * below) as well as handle the last move call from LiteGraph's `processMouseUp`. - */ - // @ts-ignore - const groupMove = LGraphGroup.prototype.move; - // @ts-ignore - LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) { - const v = groupMove.apply(this, arguments); - // When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group` - // too eagerly and we don't want to behave like we're moving until we get a delta. - if (!selectedAndMovingGroup && app.canvas.selected_group === this && (deltax || deltay)) { - selectedAndMovingGroup = this; - } + /** + * Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups` + * below) as well as handle the last move call from LiteGraph's `processMouseUp`. + */ + // @ts-ignore + const groupMove = LGraphGroup.prototype.move; + // @ts-ignore + LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) { + const v = groupMove.apply(this, arguments); + // When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group` + // too eagerly and we don't want to behave like we're moving until we get a delta. + if ( + !selectedAndMovingGroup && + app.canvas.selected_group === this && + (deltax || deltay) + ) { + selectedAndMovingGroup = this; + } - // LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want - // to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging` - // has been set to `false`. Essentially, this check here is the equivilant to calling an - // `LGraphGroup.prototype.onNodeMoved` if it had existed. - if (app.canvas.last_mouse_dragging === false && app.shiftDown) { - // After moving a group (while app.shiftDown), snap all the child nodes and, finally, - // align the group itself. - this.recomputeInsideNodes(); - for (const node of this._nodes) { - node.alignToGrid(); - } - // @ts-ignore - LGraphNode.prototype.alignToGrid.apply(this); - } - return v; - }; + // LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want + // to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging` + // has been set to `false`. Essentially, this check here is the equivilant to calling an + // `LGraphGroup.prototype.onNodeMoved` if it had existed. + if (app.canvas.last_mouse_dragging === false && app.shiftDown) { + // After moving a group (while app.shiftDown), snap all the child nodes and, finally, + // align the group itself. + this.recomputeInsideNodes(); + for (const node of this._nodes) { + node.alignToGrid(); + } + // @ts-ignore + LGraphNode.prototype.alignToGrid.apply(this); + } + return v; + }; - /** - * Handles drawing a group when, snapping the size when one is actively being resized tracking and/or - * drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for - * both. - */ - // @ts-ignore - const drawGroups = LGraphCanvas.prototype.drawGroups; - // @ts-ignore - LGraphCanvas.prototype.drawGroups = function (canvas, ctx) { - if (this.selected_group && app.shiftDown) { - if (this.selected_group_resizing) { - roundVectorToGrid(this.selected_group.size); - } else if (selectedAndMovingGroup) { - const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]); - const f = ctx.fillStyle; - const s = ctx.strokeStyle; - ctx.fillStyle = "rgba(100, 100, 100, 0.33)"; - ctx.strokeStyle = "rgba(100, 100, 100, 0.66)"; - ctx.rect(x, y, ...selectedAndMovingGroup.size); - ctx.fill(); - ctx.stroke(); - ctx.fillStyle = f; - ctx.strokeStyle = s; - } - } else if (!this.selected_group) { - selectedAndMovingGroup = null; - } - return drawGroups.apply(this, arguments); - }; + /** + * Handles drawing a group when, snapping the size when one is actively being resized tracking and/or + * drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for + * both. + */ + // @ts-ignore + const drawGroups = LGraphCanvas.prototype.drawGroups; + // @ts-ignore + LGraphCanvas.prototype.drawGroups = function (canvas, ctx) { + if (this.selected_group && app.shiftDown) { + if (this.selected_group_resizing) { + roundVectorToGrid(this.selected_group.size); + } else if (selectedAndMovingGroup) { + const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]); + const f = ctx.fillStyle; + const s = ctx.strokeStyle; + ctx.fillStyle = "rgba(100, 100, 100, 0.33)"; + ctx.strokeStyle = "rgba(100, 100, 100, 0.66)"; + ctx.rect(x, y, ...selectedAndMovingGroup.size); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = f; + ctx.strokeStyle = s; + } + } else if (!this.selected_group) { + selectedAndMovingGroup = null; + } + return drawGroups.apply(this, arguments); + }; - - /** Handles adding a group in a snapping-enabled state. */ - // @ts-ignore - const onGroupAdd = LGraphCanvas.onGroupAdd; - // @ts-ignore - LGraphCanvas.onGroupAdd = function() { - const v = onGroupAdd.apply(app.canvas, arguments); - if (app.shiftDown) { - // @ts-ignore - const lastGroup = app.graph._groups[app.graph._groups.length - 1]; - if (lastGroup) { - // @ts-ignore - roundVectorToGrid(lastGroup.pos); - // @ts-ignore - roundVectorToGrid(lastGroup.size); - } - } - return v; - }; - }, + /** Handles adding a group in a snapping-enabled state. */ + // @ts-ignore + const onGroupAdd = LGraphCanvas.onGroupAdd; + // @ts-ignore + LGraphCanvas.onGroupAdd = function () { + const v = onGroupAdd.apply(app.canvas, arguments); + if (app.shiftDown) { + // @ts-ignore + const lastGroup = app.graph._groups[app.graph._groups.length - 1]; + if (lastGroup) { + // @ts-ignore + roundVectorToGrid(lastGroup.pos); + // @ts-ignore + roundVectorToGrid(lastGroup.size); + } + } + return v; + }; + }, }); diff --git a/src/extensions/core/uploadAudio.ts b/src/extensions/core/uploadAudio.ts index cec7d74cb..28b4792af 100644 --- a/src/extensions/core/uploadAudio.ts +++ b/src/extensions/core/uploadAudio.ts @@ -11,10 +11,17 @@ function splitFilePath(path: string): [string, string] { if (folder_separator === -1) { return ["", path]; } - return [path.substring(0, folder_separator), path.substring(folder_separator + 1)]; + return [ + path.substring(0, folder_separator), + path.substring(folder_separator + 1), + ]; } -function getResourceURL(subfolder: string, filename: string, type: FolderType = "input"): string { +function getResourceURL( + subfolder: string, + filename: string, + type: FolderType = "input" +): string { const params = [ "filename=" + encodeURIComponent(filename), "type=" + type, @@ -54,7 +61,9 @@ async function uploadFile( } if (updateNode) { - audioUIWidget.element.src = api.apiURL(getResourceURL(...splitFilePath(path))); + audioUIWidget.element.src = api.apiURL( + getResourceURL(...splitFilePath(path)) + ); audioWidget.value = path; } } else { @@ -70,7 +79,9 @@ async function uploadFile( app.registerExtension({ name: "Comfy.AudioWidget", async beforeRegisterNodeDef(nodeType, nodeData) { - if (["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)) { + if ( + ["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass) + ) { nodeData.input.required.audioUI = ["AUDIO_UI"]; } }, @@ -82,7 +93,11 @@ app.registerExtension({ audio.classList.add("comfy-audio"); audio.setAttribute("name", "media"); - const audioUIWidget: DOMWidget = node.addDOMWidget(inputName, /* name=*/ "audioUI", audio); + const audioUIWidget: DOMWidget = node.addDOMWidget( + inputName, + /* name=*/ "audioUI", + audio + ); // @ts-ignore // TODO: Sort out the DOMWidget type. audioUIWidget.serialize = false; @@ -98,21 +113,27 @@ app.registerExtension({ const audios = message.audio; if (!audios) return; const audio = audios[0]; - audioUIWidget.element.src = api.apiURL(getResourceURL(audio.subfolder, audio.filename, audio.type)); + audioUIWidget.element.src = api.apiURL( + getResourceURL(audio.subfolder, audio.filename, audio.type) + ); audioUIWidget.element.classList.remove("empty-audio-widget"); - } + }; } return { widget: audioUIWidget }; - } - } + }, + }; }, onNodeOutputsUpdated(nodeOutputs: Record) { for (const [nodeId, output] of Object.entries(nodeOutputs)) { const node = app.graph.getNodeById(Number.parseInt(nodeId)); if ("audio" in output) { - const audioUIWidget = node.widgets.find((w) => w.name === "audioUI") as unknown as DOMWidget; + const audioUIWidget = node.widgets.find( + (w) => w.name === "audioUI" + ) as unknown as DOMWidget; const audio = output.audio[0]; - audioUIWidget.element.src = api.apiURL(getResourceURL(audio.subfolder, audio.filename, audio.type)); + audioUIWidget.element.src = api.apiURL( + getResourceURL(audio.subfolder, audio.filename, audio.type) + ); audioUIWidget.element.classList.remove("empty-audio-widget"); } } @@ -130,11 +151,17 @@ app.registerExtension({ return { AUDIOUPLOAD(node, inputName: string) { // The widget that allows user to select file. - const audioWidget: IWidget = node.widgets.find((w: IWidget) => w.name === "audio"); - const audioUIWidget: DOMWidget = node.widgets.find((w: IWidget) => w.name === "audioUI"); + const audioWidget: IWidget = node.widgets.find( + (w: IWidget) => w.name === "audio" + ); + const audioUIWidget: DOMWidget = node.widgets.find( + (w: IWidget) => w.name === "audioUI" + ); const onAudioWidgetUpdate = () => { - audioUIWidget.element.src = api.apiURL(getResourceURL(...splitFilePath(audioWidget.value))); + audioUIWidget.element.src = api.apiURL( + getResourceURL(...splitFilePath(audioWidget.value)) + ); }; // Initially load default audio file to audioUIWidget. if (audioWidget.value) { @@ -152,14 +179,19 @@ app.registerExtension({ } }; // The widget to pop up the upload dialog. - const uploadWidget = node.addWidget("button", inputName, /* value=*/"", () => { - fileInput.click(); - }); + const uploadWidget = node.addWidget( + "button", + inputName, + /* value=*/ "", + () => { + fileInput.click(); + } + ); uploadWidget.label = "choose file to upload"; uploadWidget.serialize = false; return { widget: uploadWidget }; - } - } + }, + }; }, }); diff --git a/src/extensions/core/uploadImage.ts b/src/extensions/core/uploadImage.ts index 7e28026ab..5fa8d19c9 100644 --- a/src/extensions/core/uploadImage.ts +++ b/src/extensions/core/uploadImage.ts @@ -4,10 +4,10 @@ import { ComfyNodeDef } from "/types/apiTypes"; // Adds an upload button to the nodes app.registerExtension({ - name: "Comfy.UploadImage", - async beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDef, app) { - if (nodeData?.input?.required?.image?.[1]?.image_upload === true) { - nodeData.input.required.upload = ["IMAGEUPLOAD"]; - } - }, + name: "Comfy.UploadImage", + async beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDef, app) { + if (nodeData?.input?.required?.image?.[1]?.image_upload === true) { + nodeData.input.required.upload = ["IMAGEUPLOAD"]; + } + }, }); diff --git a/src/extensions/core/webcamCapture.ts b/src/extensions/core/webcamCapture.ts index 8b6462a3b..c464cb497 100644 --- a/src/extensions/core/webcamCapture.ts +++ b/src/extensions/core/webcamCapture.ts @@ -4,123 +4,137 @@ import { api } from "../../scripts/api"; const WEBCAM_READY = Symbol(); app.registerExtension({ - name: "Comfy.WebcamCapture", - getCustomWidgets(app) { - return { - WEBCAM(node, inputName) { - let res; - node[WEBCAM_READY] = new Promise((resolve) => (res = resolve)); + name: "Comfy.WebcamCapture", + getCustomWidgets(app) { + return { + WEBCAM(node, inputName) { + let res; + node[WEBCAM_READY] = new Promise((resolve) => (res = resolve)); - const container = document.createElement("div"); - container.style.background = "rgba(0,0,0,0.25)"; - container.style.textAlign = "center"; + const container = document.createElement("div"); + container.style.background = "rgba(0,0,0,0.25)"; + container.style.textAlign = "center"; - const video = document.createElement("video"); - video.style.height = video.style.width = "100%"; + const video = document.createElement("video"); + video.style.height = video.style.width = "100%"; - const loadVideo = async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); - container.replaceChildren(video); + const loadVideo = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: false, + }); + container.replaceChildren(video); - setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes? - video.addEventListener("loadedmetadata", () => res(video), false); - video.srcObject = stream; - video.play(); - } catch (error) { - const label = document.createElement("div"); - label.style.color = "red"; - label.style.overflow = "auto"; - label.style.maxHeight = "100%"; - label.style.whiteSpace = "pre-wrap"; + setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes? + video.addEventListener("loadedmetadata", () => res(video), false); + video.srcObject = stream; + video.play(); + } catch (error) { + const label = document.createElement("div"); + label.style.color = "red"; + label.style.overflow = "auto"; + label.style.maxHeight = "100%"; + label.style.whiteSpace = "pre-wrap"; - if (window.isSecureContext) { - label.textContent = "Unable to load webcam, please ensure access is granted:\n" + error.message; - } else { - label.textContent = "Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n" + error.message; - } + if (window.isSecureContext) { + label.textContent = + "Unable to load webcam, please ensure access is granted:\n" + + error.message; + } else { + label.textContent = + "Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n" + + error.message; + } - container.replaceChildren(label); - } - }; + container.replaceChildren(label); + } + }; - loadVideo(); + loadVideo(); - return { widget: node.addDOMWidget(inputName, "WEBCAM", container) }; - }, - }; - }, - nodeCreated(node) { - if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return; + return { widget: node.addDOMWidget(inputName, "WEBCAM", container) }; + }, + }; + }, + nodeCreated(node) { + if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return; - let video; - const camera = node.widgets.find((w) => w.name === "image"); - const w = node.widgets.find((w) => w.name === "width"); - const h = node.widgets.find((w) => w.name === "height"); - const captureOnQueue = node.widgets.find((w) => w.name === "capture_on_queue"); + let video; + const camera = node.widgets.find((w) => w.name === "image"); + const w = node.widgets.find((w) => w.name === "width"); + const h = node.widgets.find((w) => w.name === "height"); + const captureOnQueue = node.widgets.find( + (w) => w.name === "capture_on_queue" + ); - const canvas = document.createElement("canvas"); + const canvas = document.createElement("canvas"); - const capture = () => { - canvas.width = w.value; - canvas.height = h.value; - const ctx = canvas.getContext("2d"); - ctx.drawImage(video, 0, 0, w.value, h.value); - const data = canvas.toDataURL("image/png"); + const capture = () => { + canvas.width = w.value; + canvas.height = h.value; + const ctx = canvas.getContext("2d"); + ctx.drawImage(video, 0, 0, w.value, h.value); + const data = canvas.toDataURL("image/png"); - const img = new Image(); - img.onload = () => { - node.imgs = [img]; - app.graph.setDirtyCanvas(true); - requestAnimationFrame(() => { - node.setSizeForImage?.(); - }); - }; - img.src = data; - }; + const img = new Image(); + img.onload = () => { + node.imgs = [img]; + app.graph.setDirtyCanvas(true); + requestAnimationFrame(() => { + node.setSizeForImage?.(); + }); + }; + img.src = data; + }; - const btn = node.addWidget("button", "waiting for camera...", "capture", capture); - btn.disabled = true; - btn.serializeValue = () => undefined; + const btn = node.addWidget( + "button", + "waiting for camera...", + "capture", + capture + ); + btn.disabled = true; + btn.serializeValue = () => undefined; - camera.serializeValue = async () => { - if (captureOnQueue.value) { - capture(); - } else if (!node.imgs?.length) { - const err = `No webcam image captured`; - alert(err); - throw new Error(err); - } + camera.serializeValue = async () => { + if (captureOnQueue.value) { + capture(); + } else if (!node.imgs?.length) { + const err = `No webcam image captured`; + alert(err); + throw new Error(err); + } - // Upload image to temp storage - const blob = await new Promise((r) => canvas.toBlob(r)); - const name = `${+new Date()}.png`; - const file = new File([blob], name); - const body = new FormData(); - body.append("image", file); - body.append("subfolder", "webcam"); - body.append("type", "temp"); - const resp = await api.fetchApi("/upload/image", { - method: "POST", - body, - }); - if (resp.status !== 200) { - const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`; - alert(err); - throw new Error(err); - } - return `webcam/${name} [temp]`; - }; + // Upload image to temp storage + const blob = await new Promise((r) => canvas.toBlob(r)); + const name = `${+new Date()}.png`; + const file = new File([blob], name); + const body = new FormData(); + body.append("image", file); + body.append("subfolder", "webcam"); + body.append("type", "temp"); + const resp = await api.fetchApi("/upload/image", { + method: "POST", + body, + }); + if (resp.status !== 200) { + const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`; + alert(err); + throw new Error(err); + } + return `webcam/${name} [temp]`; + }; - node[WEBCAM_READY].then((v) => { - video = v; - // If width isnt specified then use video output resolution - if (!w.value) { - w.value = video.videoWidth || 640; - h.value = video.videoHeight || 480; - } - btn.disabled = false; - btn.label = "capture"; - }); - }, + node[WEBCAM_READY].then((v) => { + video = v; + // If width isnt specified then use video output resolution + if (!w.value) { + w.value = video.videoWidth || 640; + h.value = video.videoHeight || 480; + } + btn.disabled = false; + btn.label = "capture"; + }); + }, }); diff --git a/src/extensions/core/widgetInputs.js b/src/extensions/core/widgetInputs.js index 38aefa7ca..fc4177923 100644 --- a/src/extensions/core/widgetInputs.js +++ b/src/extensions/core/widgetInputs.js @@ -9,792 +9,895 @@ const GET_CONFIG = Symbol(); const TARGET = Symbol(); // Used for reroutes to specify the real target widget export function getWidgetConfig(slot) { - return slot.widget[CONFIG] ?? slot.widget[GET_CONFIG](); + return slot.widget[CONFIG] ?? slot.widget[GET_CONFIG](); } function getConfig(widgetName) { - const { nodeData } = this.constructor; - return nodeData?.input?.required[widgetName] ?? nodeData?.input?.optional?.[widgetName]; + const { nodeData } = this.constructor; + return ( + nodeData?.input?.required[widgetName] ?? + nodeData?.input?.optional?.[widgetName] + ); } function isConvertableWidget(widget, config) { - return (VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) && !widget.options?.forceInput; + return ( + (VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) && + !widget.options?.forceInput + ); } function hideWidget(node, widget, suffix = "") { - if (widget.type?.startsWith(CONVERTED_TYPE)) return; - widget.origType = widget.type; - widget.origComputeSize = widget.computeSize; - widget.origSerializeValue = widget.serializeValue; - widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically - widget.type = CONVERTED_TYPE + suffix; - widget.serializeValue = () => { - // Prevent serializing the widget if we have no input linked - if (!node.inputs) { - return undefined; - } - let node_input = node.inputs.find((i) => i.widget?.name === widget.name); + if (widget.type?.startsWith(CONVERTED_TYPE)) return; + widget.origType = widget.type; + widget.origComputeSize = widget.computeSize; + widget.origSerializeValue = widget.serializeValue; + widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically + widget.type = CONVERTED_TYPE + suffix; + widget.serializeValue = () => { + // Prevent serializing the widget if we have no input linked + if (!node.inputs) { + return undefined; + } + let node_input = node.inputs.find((i) => i.widget?.name === widget.name); - if (!node_input || !node_input.link) { - return undefined; - } - return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; - }; + if (!node_input || !node_input.link) { + return undefined; + } + return widget.origSerializeValue + ? widget.origSerializeValue() + : widget.value; + }; - // Hide any linked widgets, e.g. seed+seedControl - if (widget.linkedWidgets) { - for (const w of widget.linkedWidgets) { - hideWidget(node, w, ":" + widget.name); - } - } + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + hideWidget(node, w, ":" + widget.name); + } + } } function showWidget(widget) { - widget.type = widget.origType; - widget.computeSize = widget.origComputeSize; - widget.serializeValue = widget.origSerializeValue; + widget.type = widget.origType; + widget.computeSize = widget.origComputeSize; + widget.serializeValue = widget.origSerializeValue; - delete widget.origType; - delete widget.origComputeSize; - delete widget.origSerializeValue; + delete widget.origType; + delete widget.origComputeSize; + delete widget.origSerializeValue; - // Hide any linked widgets, e.g. seed+seedControl - if (widget.linkedWidgets) { - for (const w of widget.linkedWidgets) { - showWidget(w); - } - } + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + showWidget(w); + } + } } function convertToInput(node, widget, config) { - hideWidget(node, widget); + hideWidget(node, widget); - const { type } = getWidgetType(config); + const { type } = getWidgetType(config); - // Add input and store widget config for creating on primitive node - const sz = node.size; - node.addInput(widget.name, type, { - widget: { name: widget.name, [GET_CONFIG]: () => config }, - }); + // Add input and store widget config for creating on primitive node + const sz = node.size; + node.addInput(widget.name, type, { + widget: { name: widget.name, [GET_CONFIG]: () => config }, + }); - for (const widget of node.widgets) { - widget.last_y += LiteGraph.NODE_SLOT_HEIGHT; - } + for (const widget of node.widgets) { + widget.last_y += LiteGraph.NODE_SLOT_HEIGHT; + } - // Restore original size but grow if needed - node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); + // Restore original size but grow if needed + node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); } function convertToWidget(node, widget) { - showWidget(widget); - const sz = node.size; - node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name)); + showWidget(widget); + const sz = node.size; + node.removeInput( + node.inputs.findIndex((i) => i.widget?.name === widget.name) + ); - for (const widget of node.widgets) { - widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT; - } + for (const widget of node.widgets) { + widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT; + } - // Restore original size but grow if needed - node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); + // Restore original size but grow if needed + node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); } function getWidgetType(config) { - // Special handling for COMBO so we restrict links based on the entries - let type = config[0]; - if (type instanceof Array) { - type = "COMBO"; - } - return { type }; + // Special handling for COMBO so we restrict links based on the entries + let type = config[0]; + if (type instanceof Array) { + type = "COMBO"; + } + return { type }; } function isValidCombo(combo, obj) { - // New input isnt a combo - if (!(obj instanceof Array)) { - console.log(`connection rejected: tried to connect combo to ${obj}`); - return false; - } - // New imput combo has a different size - if (combo.length !== obj.length) { - console.log(`connection rejected: combo lists dont match`); - return false; - } - // New input combo has different elements - if (combo.find((v, i) => obj[i] !== v)) { - console.log(`connection rejected: combo lists dont match`); - return false; - } + // New input isnt a combo + if (!(obj instanceof Array)) { + console.log(`connection rejected: tried to connect combo to ${obj}`); + return false; + } + // New imput combo has a different size + if (combo.length !== obj.length) { + console.log(`connection rejected: combo lists dont match`); + return false; + } + // New input combo has different elements + if (combo.find((v, i) => obj[i] !== v)) { + console.log(`connection rejected: combo lists dont match`); + return false; + } - return true; + return true; } export function setWidgetConfig(slot, config, target) { - if (!slot.widget) return; - if (config) { - slot.widget[GET_CONFIG] = () => config; - slot.widget[TARGET] = target; - } else { - delete slot.widget; - } + if (!slot.widget) return; + if (config) { + slot.widget[GET_CONFIG] = () => config; + slot.widget[TARGET] = target; + } else { + delete slot.widget; + } - if (slot.link) { - const link = app.graph.links[slot.link]; - if (link) { - const originNode = app.graph.getNodeById(link.origin_id); - if (originNode.type === "PrimitiveNode") { - if (config) { - originNode.recreateWidget(); - } else if(!app.configuringGraph) { - originNode.disconnectOutput(0); - originNode.onLastDisconnect(); - } - } - } - } + if (slot.link) { + const link = app.graph.links[slot.link]; + if (link) { + const originNode = app.graph.getNodeById(link.origin_id); + if (originNode.type === "PrimitiveNode") { + if (config) { + originNode.recreateWidget(); + } else if (!app.configuringGraph) { + originNode.disconnectOutput(0); + originNode.onLastDisconnect(); + } + } + } + } } -export function mergeIfValid(output, config2, forceUpdate, recreateWidget, config1) { - if (!config1) { - config1 = output.widget[CONFIG] ?? output.widget[GET_CONFIG](); - } +export function mergeIfValid( + output, + config2, + forceUpdate, + recreateWidget, + config1 +) { + if (!config1) { + config1 = output.widget[CONFIG] ?? output.widget[GET_CONFIG](); + } - if (config1[0] instanceof Array) { - if (!isValidCombo(config1[0], config2[0])) return; - } else if (config1[0] !== config2[0]) { - // Types dont match - console.log(`connection rejected: types dont match`, config1[0], config2[0]); - return; - } + if (config1[0] instanceof Array) { + if (!isValidCombo(config1[0], config2[0])) return; + } else if (config1[0] !== config2[0]) { + // Types dont match + console.log( + `connection rejected: types dont match`, + config1[0], + config2[0] + ); + return; + } - const keys = new Set([...Object.keys(config1[1] ?? {}), ...Object.keys(config2[1] ?? {})]); + const keys = new Set([ + ...Object.keys(config1[1] ?? {}), + ...Object.keys(config2[1] ?? {}), + ]); - let customConfig; - const getCustomConfig = () => { - if (!customConfig) { - if (typeof structuredClone === "undefined") { - customConfig = JSON.parse(JSON.stringify(config1[1] ?? {})); - } else { - customConfig = structuredClone(config1[1] ?? {}); - } - } - return customConfig; - }; + let customConfig; + const getCustomConfig = () => { + if (!customConfig) { + if (typeof structuredClone === "undefined") { + customConfig = JSON.parse(JSON.stringify(config1[1] ?? {})); + } else { + customConfig = structuredClone(config1[1] ?? {}); + } + } + return customConfig; + }; - const isNumber = config1[0] === "INT" || config1[0] === "FLOAT"; - for (const k of keys.values()) { - if (k !== "default" && k !== "forceInput" && k !== "defaultInput" && k !== "control_after_generate" && k !== "multiline") { - let v1 = config1[1][k]; - let v2 = config2[1]?.[k]; + const isNumber = config1[0] === "INT" || config1[0] === "FLOAT"; + for (const k of keys.values()) { + if ( + k !== "default" && + k !== "forceInput" && + k !== "defaultInput" && + k !== "control_after_generate" && + k !== "multiline" + ) { + let v1 = config1[1][k]; + let v2 = config2[1]?.[k]; - if (v1 === v2 || (!v1 && !v2)) continue; + if (v1 === v2 || (!v1 && !v2)) continue; - if (isNumber) { - if (k === "min") { - const theirMax = config2[1]?.["max"]; - if (theirMax != null && v1 > theirMax) { - console.log("connection rejected: min > max", v1, theirMax); - return; - } - getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.max(v1, v2); - continue; - } else if (k === "max") { - const theirMin = config2[1]?.["min"]; - if (theirMin != null && v1 < theirMin) { - console.log("connection rejected: max < min", v1, theirMin); - return; - } - getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2); - continue; - } else if (k === "step") { - let step; - if (v1 == null) { - // No current step - step = v2; - } else if (v2 == null) { - // No new step - step = v1; - } else { - if (v1 < v2) { - // Ensure v1 is larger for the mod - const a = v2; - v2 = v1; - v1 = a; - } - if (v1 % v2) { - console.log("connection rejected: steps not divisible", "current:", v1, "new:", v2); - return; - } + if (isNumber) { + if (k === "min") { + const theirMax = config2[1]?.["max"]; + if (theirMax != null && v1 > theirMax) { + console.log("connection rejected: min > max", v1, theirMax); + return; + } + getCustomConfig()[k] = + v1 == null ? v2 : v2 == null ? v1 : Math.max(v1, v2); + continue; + } else if (k === "max") { + const theirMin = config2[1]?.["min"]; + if (theirMin != null && v1 < theirMin) { + console.log("connection rejected: max < min", v1, theirMin); + return; + } + getCustomConfig()[k] = + v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2); + continue; + } else if (k === "step") { + let step; + if (v1 == null) { + // No current step + step = v2; + } else if (v2 == null) { + // No new step + step = v1; + } else { + if (v1 < v2) { + // Ensure v1 is larger for the mod + const a = v2; + v2 = v1; + v1 = a; + } + if (v1 % v2) { + console.log( + "connection rejected: steps not divisible", + "current:", + v1, + "new:", + v2 + ); + return; + } - step = v1; - } + step = v1; + } - getCustomConfig()[k] = step; - continue; - } - } + getCustomConfig()[k] = step; + continue; + } + } - console.log(`connection rejected: config ${k} values dont match`, v1, v2); - return; - } - } + console.log(`connection rejected: config ${k} values dont match`, v1, v2); + return; + } + } - if (customConfig || forceUpdate) { - if (customConfig) { - output.widget[CONFIG] = [config1[0], customConfig]; - } + if (customConfig || forceUpdate) { + if (customConfig) { + output.widget[CONFIG] = [config1[0], customConfig]; + } - const widget = recreateWidget?.call(this); - // When deleting a node this can be null - if (widget) { - const min = widget.options.min; - const max = widget.options.max; - if (min != null && widget.value < min) widget.value = min; - if (max != null && widget.value > max) widget.value = max; - widget.callback(widget.value); - } - } + const widget = recreateWidget?.call(this); + // When deleting a node this can be null + if (widget) { + const min = widget.options.min; + const max = widget.options.max; + if (min != null && widget.value < min) widget.value = min; + if (max != null && widget.value > max) widget.value = max; + widget.callback(widget.value); + } + } - return { customConfig }; + return { customConfig }; } let useConversionSubmenusSetting; app.registerExtension({ - name: "Comfy.WidgetInputs", - init() { - useConversionSubmenusSetting = app.ui.settings.addSetting({ - id: "Comfy.NodeInputConversionSubmenus", - name: "Node widget/input conversion sub-menus", - tooltip: "In the node context menu, place the entries that convert between input/widget in sub-menus.", - type: "boolean", - defaultValue: true, - }); - }, - async beforeRegisterNodeDef(nodeType, nodeData, app) { - // Add menu options to conver to/from widgets - const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; - nodeType.prototype.convertWidgetToInput = function (widget) { - const config = getConfig.call(this, widget.name) ?? [widget.type, widget.options || {}]; - if (!isConvertableWidget(widget, config)) return false; - convertToInput(this, widget, config); - return true; - }; - nodeType.prototype.getExtraMenuOptions = function (_, options) { - const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined; - - if (this.widgets) { - let toInput = []; - let toWidget = []; - for (const w of this.widgets) { - if (w.options?.forceInput) { - continue; - } - if (w.type === CONVERTED_TYPE) { - toWidget.push({ - content: `Convert ${w.name} to widget`, - callback: () => convertToWidget(this, w), - }); - } else { - const config = getConfig.call(this, w.name) ?? [w.type, w.options || {}]; - if (isConvertableWidget(w, config)) { - toInput.push({ - content: `Convert ${w.name} to input`, - callback: () => convertToInput(this, w, config), - }); - } - } - } - - //Convert.. main menu - if (toInput.length) { - if (useConversionSubmenusSetting.value) { - options.push({ - content: "Convert Widget to Input", - submenu: { - options: toInput, - }, - }); - } else { - options.push(...toInput, null); - } - } - if (toWidget.length) { - if (useConversionSubmenusSetting.value) { - options.push({ - content: "Convert Input to Widget", - submenu: { - options: toWidget, - }, - }); - } else { - options.push(...toWidget, null); - } - } - } - - return r; - }; - - nodeType.prototype.onGraphConfigured = function () { - if (!this.inputs) return; - - for (const input of this.inputs) { - if (input.widget) { - if (!input.widget[GET_CONFIG]) { - input.widget[GET_CONFIG] = () => getConfig.call(this, input.widget.name); - } - - // Cleanup old widget config - if (input.widget.config) { - if (input.widget.config[0] instanceof Array) { - // If we are an old converted combo then replace the input type and the stored link data - input.type = "COMBO"; - - const link = app.graph.links[input.link]; - if (link) { - link.type = input.type; - } - } - delete input.widget.config; - } - - const w = this.widgets.find((w) => w.name === input.widget.name); - if (w) { - hideWidget(this, w); - } else { - convertToWidget(this, input); - } - } - } - }; - - const origOnNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const r = origOnNodeCreated ? origOnNodeCreated.apply(this) : undefined; - - // When node is created, convert any force/default inputs - if (!app.configuringGraph && this.widgets) { - for (const w of this.widgets) { - if (w?.options?.forceInput || w?.options?.defaultInput) { - const config = getConfig.call(this, w.name) ?? [w.type, w.options || {}]; - convertToInput(this, w, config); - } - } - } - - return r; - }; - - const origOnConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = function () { - const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined; - if (!app.configuringGraph && this.inputs) { - // On copy + paste of nodes, ensure that widget configs are set up - for (const input of this.inputs) { - if (input.widget && !input.widget[GET_CONFIG]) { - input.widget[GET_CONFIG] = () => getConfig.call(this, input.widget.name); - const w = this.widgets.find((w) => w.name === input.widget.name); - if (w) { - hideWidget(this, w); - } - } - } - } - - return r; - }; - - function isNodeAtPos(pos) { - for (const n of app.graph._nodes) { - if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) { - return true; - } - } - return false; - } - - // Double click a widget input to automatically attach a primitive - const origOnInputDblClick = nodeType.prototype.onInputDblClick; - const ignoreDblClick = Symbol(); - nodeType.prototype.onInputDblClick = function (slot) { - const r = origOnInputDblClick ? origOnInputDblClick.apply(this, arguments) : undefined; - - const input = this.inputs[slot]; - if (!input.widget || !input[ignoreDblClick]) { - // Not a widget input or already handled input - if (!(input.type in ComfyWidgets) && !(input.widget[GET_CONFIG]?.()?.[0] instanceof Array)) { - return r; //also Not a ComfyWidgets input or combo (do nothing) - } - } - - // Create a primitive node - const node = LiteGraph.createNode("PrimitiveNode"); - app.graph.add(node); - - // Calculate a position that wont directly overlap another node - const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]]; - while (isNodeAtPos(pos)) { - pos[1] += LiteGraph.NODE_TITLE_HEIGHT; - } - - node.pos = pos; - node.connect(0, this, slot); - node.title = input.name; - - // Prevent adding duplicates due to triple clicking - input[ignoreDblClick] = true; - setTimeout(() => { - delete input[ignoreDblClick]; - }, 300); - - return r; - }; - - // Prevent connecting COMBO lists to converted inputs that dont match types - const onConnectInput = nodeType.prototype.onConnectInput; - nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { - const v = onConnectInput?.(this, arguments); - // Not a combo, ignore - if (type !== "COMBO") return v; - // Primitive output, allow that to handle - if (originNode.outputs[originSlot].widget) return v; - - // Ensure target is also a combo - const targetCombo = this.inputs[targetSlot].widget?.[GET_CONFIG]?.()?.[0]; - if (!targetCombo || !(targetCombo instanceof Array)) return v; - - // Check they match - const originConfig = originNode.constructor?.nodeData?.output?.[originSlot]; - if (!originConfig || !isValidCombo(targetCombo, originConfig)) { - return false; - } - - return v; - }; - }, - registerCustomNodes() { - const replacePropertyName = "Run widget replace on values"; - class PrimitiveNode { - constructor() { - this.addOutput("connect to widget input", "*"); - this.serialize_widgets = true; - this.isVirtualNode = true; - - if (!this.properties || !(replacePropertyName in this.properties)) { - this.addProperty(replacePropertyName, false, "boolean"); - } - } - - applyToGraph(extraLinks = []) { - if (!this.outputs[0].links?.length) return; - - function get_links(node) { - let links = []; - for (const l of node.outputs[0].links) { - const linkInfo = app.graph.links[l]; - const n = node.graph.getNodeById(linkInfo.target_id); - if (n.type == "Reroute") { - links = links.concat(get_links(n)); - } else { - links.push(l); - } - } - return links; - } - - let links = [...get_links(this).map((l) => app.graph.links[l]), ...extraLinks]; - let v = this.widgets?.[0].value; - if(v && this.properties[replacePropertyName]) { - v = applyTextReplacements(app, v); - } - - // For each output link copy our value over the original widget value - for (const linkInfo of links) { - const node = this.graph.getNodeById(linkInfo.target_id); - const input = node.inputs[linkInfo.target_slot]; - let widget; - if (input.widget[TARGET]) { - widget = input.widget[TARGET]; - } else { - const widgetName = input.widget.name; - if (widgetName) { - widget = node.widgets.find((w) => w.name === widgetName); - } - } - - if (widget) { - widget.value = v; - if (widget.callback) { - widget.callback(widget.value, app.canvas, node, app.canvas.graph_mouse, {}); - } - } - } - } - - refreshComboInNode() { - const widget = this.widgets?.[0]; - if (widget?.type === "combo") { - widget.options.values = this.outputs[0].widget[GET_CONFIG]()[0]; - - if (!widget.options.values.includes(widget.value)) { - widget.value = widget.options.values[0]; - widget.callback(widget.value); - } - } - } - - onAfterGraphConfigured() { - if (this.outputs[0].links?.length && !this.widgets?.length) { - if (!this.#onFirstConnection()) return; - - // Populate widget values from config data - if (this.widgets) { - for (let i = 0; i < this.widgets_values.length; i++) { - const w = this.widgets[i]; - if (w) { - w.value = this.widgets_values[i]; - } - } - } - - // Merge values if required - this.#mergeWidgetConfig(); - } - } - - onConnectionsChange(_, index, connected) { - if (app.configuringGraph) { - // Dont run while the graph is still setting up - return; - } - - const links = this.outputs[0].links; - if (connected) { - if (links?.length && !this.widgets?.length) { - this.#onFirstConnection(); - } - } else { - // We may have removed a link that caused the constraints to change - this.#mergeWidgetConfig(); - - if (!links?.length) { - this.onLastDisconnect(); - } - } - } - - onConnectOutput(slot, type, input, target_node, target_slot) { - // Fires before the link is made allowing us to reject it if it isn't valid - // No widget, we cant connect - if (!input.widget) { - if (!(input.type in ComfyWidgets)) return false; - } - - if (this.outputs[slot].links?.length) { - const valid = this.#isValidConnection(input); - if (valid) { - // On connect of additional outputs, copy our value to their widget - this.applyToGraph([{ target_id: target_node.id, target_slot }]); - } - return valid; - } - } - - #onFirstConnection(recreating) { - // First connection can fire before the graph is ready on initial load so random things can be missing - if (!this.outputs[0].links) { - this.onLastDisconnect(); - return; - } - const linkId = this.outputs[0].links[0]; - const link = this.graph.links[linkId]; - if (!link) return; - - const theirNode = this.graph.getNodeById(link.target_id); - if (!theirNode || !theirNode.inputs) return; - - const input = theirNode.inputs[link.target_slot]; - if (!input) return; - - let widget; - if (!input.widget) { - if (!(input.type in ComfyWidgets)) return; - widget = { name: input.name, [GET_CONFIG]: () => [input.type, {}] }; //fake widget - } else { - widget = input.widget; - } - - const config = widget[GET_CONFIG]?.(); - if (!config) return; - - const { type } = getWidgetType(config); - // Update our output to restrict to the widget type - this.outputs[0].type = type; - this.outputs[0].name = type; - this.outputs[0].widget = widget; - - this.#createWidget(widget[CONFIG] ?? config, theirNode, widget.name, recreating, widget[TARGET]); - } - - #createWidget(inputData, node, widgetName, recreating, targetWidget) { - let type = inputData[0]; - - if (type instanceof Array) { - type = "COMBO"; - } - - let widget; - if (type in ComfyWidgets) { - widget = (ComfyWidgets[type](this, "value", inputData, app) || {}).widget; - } else { - widget = this.addWidget(type, "value", null, () => {}, {}); - } - - if (targetWidget) { - widget.value = targetWidget.value; - } else if (node?.widgets && widget) { - const theirWidget = node.widgets.find((w) => w.name === widgetName); - if (theirWidget) { - widget.value = theirWidget.value; - } - } - - if (!inputData?.[1]?.control_after_generate && (widget.type === "number" || widget.type === "combo")) { - let control_value = this.widgets_values?.[1]; - if (!control_value) { - control_value = "fixed"; - } - addValueControlWidgets(this, widget, control_value, undefined, inputData); - let filter = this.widgets_values?.[2]; - if (filter && this.widgets.length === 3) { - this.widgets[2].value = filter; - } - } - - // Restore any saved control values - const controlValues = this.controlValues; - if(this.lastType === this.widgets[0].type && controlValues?.length === this.widgets.length - 1) { - for(let i = 0; i < controlValues.length; i++) { - this.widgets[i + 1].value = controlValues[i]; - } - } - - // When our value changes, update other widgets to reflect our changes - // e.g. so LoadImage shows correct image - const callback = widget.callback; - const self = this; - widget.callback = function () { - const r = callback ? callback.apply(this, arguments) : undefined; - self.applyToGraph(); - return r; - }; - - if (!recreating) { - // Grow our node if required - const sz = this.computeSize(); - if (this.size[0] < sz[0]) { - this.size[0] = sz[0]; - } - if (this.size[1] < sz[1]) { - this.size[1] = sz[1]; - } - - requestAnimationFrame(() => { - if (this.onResize) { - this.onResize(this.size); - } - }); - } - } - - recreateWidget() { - const values = this.widgets?.map((w) => w.value); - this.#removeWidgets(); - this.#onFirstConnection(true); - if (values?.length) { - for (let i = 0; i < this.widgets?.length; i++) this.widgets[i].value = values[i]; - } - return this.widgets?.[0]; - } - - #mergeWidgetConfig() { - // Merge widget configs if the node has multiple outputs - const output = this.outputs[0]; - const links = output.links; - - const hasConfig = !!output.widget[CONFIG]; - if (hasConfig) { - delete output.widget[CONFIG]; - } - - if (links?.length < 2 && hasConfig) { - // Copy the widget options from the source - if (links.length) { - this.recreateWidget(); - } - - return; - } - - const config1 = output.widget[GET_CONFIG](); - const isNumber = config1[0] === "INT" || config1[0] === "FLOAT"; - if (!isNumber) return; - - for (const linkId of links) { - const link = app.graph.links[linkId]; - if (!link) continue; // Can be null when removing a node - - const theirNode = app.graph.getNodeById(link.target_id); - const theirInput = theirNode.inputs[link.target_slot]; - - // Call is valid connection so it can merge the configs when validating - this.#isValidConnection(theirInput, hasConfig); - } - } - - #isValidConnection(input, forceUpdate) { - // Only allow connections where the configs match - const output = this.outputs[0]; - const config2 = input.widget[GET_CONFIG](); - return !!mergeIfValid.call(this, output, config2, forceUpdate, this.recreateWidget); - } - - #removeWidgets() { - if (this.widgets) { - // Allow widgets to cleanup - for (const w of this.widgets) { - if (w.onRemove) { - w.onRemove(); - } - } - - // Temporarily store the current values in case the node is being recreated - // e.g. by group node conversion - this.controlValues = []; - this.lastType = this.widgets[0]?.type; - for(let i = 1; i < this.widgets.length; i++) { - this.controlValues.push(this.widgets[i].value); - } - setTimeout(() => { delete this.lastType; delete this.controlValues }, 15); - this.widgets.length = 0; - } - } - - onLastDisconnect() { - // We cant remove + re-add the output here as if you drag a link over the same link - // it removes, then re-adds, causing it to break - this.outputs[0].type = "*"; - this.outputs[0].name = "connect to widget input"; - delete this.outputs[0].widget; - - this.#removeWidgets(); - } - } - - LiteGraph.registerNodeType( - "PrimitiveNode", - Object.assign(PrimitiveNode, { - title: "Primitive", - }) - ); - PrimitiveNode.category = "utils"; - }, + name: "Comfy.WidgetInputs", + init() { + useConversionSubmenusSetting = app.ui.settings.addSetting({ + id: "Comfy.NodeInputConversionSubmenus", + name: "Node widget/input conversion sub-menus", + tooltip: + "In the node context menu, place the entries that convert between input/widget in sub-menus.", + type: "boolean", + defaultValue: true, + }); + }, + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // Add menu options to conver to/from widgets + const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; + nodeType.prototype.convertWidgetToInput = function (widget) { + const config = getConfig.call(this, widget.name) ?? [ + widget.type, + widget.options || {}, + ]; + if (!isConvertableWidget(widget, config)) return false; + convertToInput(this, widget, config); + return true; + }; + nodeType.prototype.getExtraMenuOptions = function (_, options) { + const r = origGetExtraMenuOptions + ? origGetExtraMenuOptions.apply(this, arguments) + : undefined; + + if (this.widgets) { + let toInput = []; + let toWidget = []; + for (const w of this.widgets) { + if (w.options?.forceInput) { + continue; + } + if (w.type === CONVERTED_TYPE) { + toWidget.push({ + content: `Convert ${w.name} to widget`, + callback: () => convertToWidget(this, w), + }); + } else { + const config = getConfig.call(this, w.name) ?? [ + w.type, + w.options || {}, + ]; + if (isConvertableWidget(w, config)) { + toInput.push({ + content: `Convert ${w.name} to input`, + callback: () => convertToInput(this, w, config), + }); + } + } + } + + //Convert.. main menu + if (toInput.length) { + if (useConversionSubmenusSetting.value) { + options.push({ + content: "Convert Widget to Input", + submenu: { + options: toInput, + }, + }); + } else { + options.push(...toInput, null); + } + } + if (toWidget.length) { + if (useConversionSubmenusSetting.value) { + options.push({ + content: "Convert Input to Widget", + submenu: { + options: toWidget, + }, + }); + } else { + options.push(...toWidget, null); + } + } + } + + return r; + }; + + nodeType.prototype.onGraphConfigured = function () { + if (!this.inputs) return; + + for (const input of this.inputs) { + if (input.widget) { + if (!input.widget[GET_CONFIG]) { + input.widget[GET_CONFIG] = () => + getConfig.call(this, input.widget.name); + } + + // Cleanup old widget config + if (input.widget.config) { + if (input.widget.config[0] instanceof Array) { + // If we are an old converted combo then replace the input type and the stored link data + input.type = "COMBO"; + + const link = app.graph.links[input.link]; + if (link) { + link.type = input.type; + } + } + delete input.widget.config; + } + + const w = this.widgets.find((w) => w.name === input.widget.name); + if (w) { + hideWidget(this, w); + } else { + convertToWidget(this, input); + } + } + } + }; + + const origOnNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = origOnNodeCreated ? origOnNodeCreated.apply(this) : undefined; + + // When node is created, convert any force/default inputs + if (!app.configuringGraph && this.widgets) { + for (const w of this.widgets) { + if (w?.options?.forceInput || w?.options?.defaultInput) { + const config = getConfig.call(this, w.name) ?? [ + w.type, + w.options || {}, + ]; + convertToInput(this, w, config); + } + } + } + + return r; + }; + + const origOnConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + const r = origOnConfigure + ? origOnConfigure.apply(this, arguments) + : undefined; + if (!app.configuringGraph && this.inputs) { + // On copy + paste of nodes, ensure that widget configs are set up + for (const input of this.inputs) { + if (input.widget && !input.widget[GET_CONFIG]) { + input.widget[GET_CONFIG] = () => + getConfig.call(this, input.widget.name); + const w = this.widgets.find((w) => w.name === input.widget.name); + if (w) { + hideWidget(this, w); + } + } + } + } + + return r; + }; + + function isNodeAtPos(pos) { + for (const n of app.graph._nodes) { + if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) { + return true; + } + } + return false; + } + + // Double click a widget input to automatically attach a primitive + const origOnInputDblClick = nodeType.prototype.onInputDblClick; + const ignoreDblClick = Symbol(); + nodeType.prototype.onInputDblClick = function (slot) { + const r = origOnInputDblClick + ? origOnInputDblClick.apply(this, arguments) + : undefined; + + const input = this.inputs[slot]; + if (!input.widget || !input[ignoreDblClick]) { + // Not a widget input or already handled input + if ( + !(input.type in ComfyWidgets) && + !(input.widget[GET_CONFIG]?.()?.[0] instanceof Array) + ) { + return r; //also Not a ComfyWidgets input or combo (do nothing) + } + } + + // Create a primitive node + const node = LiteGraph.createNode("PrimitiveNode"); + app.graph.add(node); + + // Calculate a position that wont directly overlap another node + const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]]; + while (isNodeAtPos(pos)) { + pos[1] += LiteGraph.NODE_TITLE_HEIGHT; + } + + node.pos = pos; + node.connect(0, this, slot); + node.title = input.name; + + // Prevent adding duplicates due to triple clicking + input[ignoreDblClick] = true; + setTimeout(() => { + delete input[ignoreDblClick]; + }, 300); + + return r; + }; + + // Prevent connecting COMBO lists to converted inputs that dont match types + const onConnectInput = nodeType.prototype.onConnectInput; + nodeType.prototype.onConnectInput = function ( + targetSlot, + type, + output, + originNode, + originSlot + ) { + const v = onConnectInput?.(this, arguments); + // Not a combo, ignore + if (type !== "COMBO") return v; + // Primitive output, allow that to handle + if (originNode.outputs[originSlot].widget) return v; + + // Ensure target is also a combo + const targetCombo = this.inputs[targetSlot].widget?.[GET_CONFIG]?.()?.[0]; + if (!targetCombo || !(targetCombo instanceof Array)) return v; + + // Check they match + const originConfig = + originNode.constructor?.nodeData?.output?.[originSlot]; + if (!originConfig || !isValidCombo(targetCombo, originConfig)) { + return false; + } + + return v; + }; + }, + registerCustomNodes() { + const replacePropertyName = "Run widget replace on values"; + class PrimitiveNode { + constructor() { + this.addOutput("connect to widget input", "*"); + this.serialize_widgets = true; + this.isVirtualNode = true; + + if (!this.properties || !(replacePropertyName in this.properties)) { + this.addProperty(replacePropertyName, false, "boolean"); + } + } + + applyToGraph(extraLinks = []) { + if (!this.outputs[0].links?.length) return; + + function get_links(node) { + let links = []; + for (const l of node.outputs[0].links) { + const linkInfo = app.graph.links[l]; + const n = node.graph.getNodeById(linkInfo.target_id); + if (n.type == "Reroute") { + links = links.concat(get_links(n)); + } else { + links.push(l); + } + } + return links; + } + + let links = [ + ...get_links(this).map((l) => app.graph.links[l]), + ...extraLinks, + ]; + let v = this.widgets?.[0].value; + if (v && this.properties[replacePropertyName]) { + v = applyTextReplacements(app, v); + } + + // For each output link copy our value over the original widget value + for (const linkInfo of links) { + const node = this.graph.getNodeById(linkInfo.target_id); + const input = node.inputs[linkInfo.target_slot]; + let widget; + if (input.widget[TARGET]) { + widget = input.widget[TARGET]; + } else { + const widgetName = input.widget.name; + if (widgetName) { + widget = node.widgets.find((w) => w.name === widgetName); + } + } + + if (widget) { + widget.value = v; + if (widget.callback) { + widget.callback( + widget.value, + app.canvas, + node, + app.canvas.graph_mouse, + {} + ); + } + } + } + } + + refreshComboInNode() { + const widget = this.widgets?.[0]; + if (widget?.type === "combo") { + widget.options.values = this.outputs[0].widget[GET_CONFIG]()[0]; + + if (!widget.options.values.includes(widget.value)) { + widget.value = widget.options.values[0]; + widget.callback(widget.value); + } + } + } + + onAfterGraphConfigured() { + if (this.outputs[0].links?.length && !this.widgets?.length) { + if (!this.#onFirstConnection()) return; + + // Populate widget values from config data + if (this.widgets) { + for (let i = 0; i < this.widgets_values.length; i++) { + const w = this.widgets[i]; + if (w) { + w.value = this.widgets_values[i]; + } + } + } + + // Merge values if required + this.#mergeWidgetConfig(); + } + } + + onConnectionsChange(_, index, connected) { + if (app.configuringGraph) { + // Dont run while the graph is still setting up + return; + } + + const links = this.outputs[0].links; + if (connected) { + if (links?.length && !this.widgets?.length) { + this.#onFirstConnection(); + } + } else { + // We may have removed a link that caused the constraints to change + this.#mergeWidgetConfig(); + + if (!links?.length) { + this.onLastDisconnect(); + } + } + } + + onConnectOutput(slot, type, input, target_node, target_slot) { + // Fires before the link is made allowing us to reject it if it isn't valid + // No widget, we cant connect + if (!input.widget) { + if (!(input.type in ComfyWidgets)) return false; + } + + if (this.outputs[slot].links?.length) { + const valid = this.#isValidConnection(input); + if (valid) { + // On connect of additional outputs, copy our value to their widget + this.applyToGraph([{ target_id: target_node.id, target_slot }]); + } + return valid; + } + } + + #onFirstConnection(recreating) { + // First connection can fire before the graph is ready on initial load so random things can be missing + if (!this.outputs[0].links) { + this.onLastDisconnect(); + return; + } + const linkId = this.outputs[0].links[0]; + const link = this.graph.links[linkId]; + if (!link) return; + + const theirNode = this.graph.getNodeById(link.target_id); + if (!theirNode || !theirNode.inputs) return; + + const input = theirNode.inputs[link.target_slot]; + if (!input) return; + + let widget; + if (!input.widget) { + if (!(input.type in ComfyWidgets)) return; + widget = { name: input.name, [GET_CONFIG]: () => [input.type, {}] }; //fake widget + } else { + widget = input.widget; + } + + const config = widget[GET_CONFIG]?.(); + if (!config) return; + + const { type } = getWidgetType(config); + // Update our output to restrict to the widget type + this.outputs[0].type = type; + this.outputs[0].name = type; + this.outputs[0].widget = widget; + + this.#createWidget( + widget[CONFIG] ?? config, + theirNode, + widget.name, + recreating, + widget[TARGET] + ); + } + + #createWidget(inputData, node, widgetName, recreating, targetWidget) { + let type = inputData[0]; + + if (type instanceof Array) { + type = "COMBO"; + } + + let widget; + if (type in ComfyWidgets) { + widget = (ComfyWidgets[type](this, "value", inputData, app) || {}) + .widget; + } else { + widget = this.addWidget(type, "value", null, () => {}, {}); + } + + if (targetWidget) { + widget.value = targetWidget.value; + } else if (node?.widgets && widget) { + const theirWidget = node.widgets.find((w) => w.name === widgetName); + if (theirWidget) { + widget.value = theirWidget.value; + } + } + + if ( + !inputData?.[1]?.control_after_generate && + (widget.type === "number" || widget.type === "combo") + ) { + let control_value = this.widgets_values?.[1]; + if (!control_value) { + control_value = "fixed"; + } + addValueControlWidgets( + this, + widget, + control_value, + undefined, + inputData + ); + let filter = this.widgets_values?.[2]; + if (filter && this.widgets.length === 3) { + this.widgets[2].value = filter; + } + } + + // Restore any saved control values + const controlValues = this.controlValues; + if ( + this.lastType === this.widgets[0].type && + controlValues?.length === this.widgets.length - 1 + ) { + for (let i = 0; i < controlValues.length; i++) { + this.widgets[i + 1].value = controlValues[i]; + } + } + + // When our value changes, update other widgets to reflect our changes + // e.g. so LoadImage shows correct image + const callback = widget.callback; + const self = this; + widget.callback = function () { + const r = callback ? callback.apply(this, arguments) : undefined; + self.applyToGraph(); + return r; + }; + + if (!recreating) { + // Grow our node if required + const sz = this.computeSize(); + if (this.size[0] < sz[0]) { + this.size[0] = sz[0]; + } + if (this.size[1] < sz[1]) { + this.size[1] = sz[1]; + } + + requestAnimationFrame(() => { + if (this.onResize) { + this.onResize(this.size); + } + }); + } + } + + recreateWidget() { + const values = this.widgets?.map((w) => w.value); + this.#removeWidgets(); + this.#onFirstConnection(true); + if (values?.length) { + for (let i = 0; i < this.widgets?.length; i++) + this.widgets[i].value = values[i]; + } + return this.widgets?.[0]; + } + + #mergeWidgetConfig() { + // Merge widget configs if the node has multiple outputs + const output = this.outputs[0]; + const links = output.links; + + const hasConfig = !!output.widget[CONFIG]; + if (hasConfig) { + delete output.widget[CONFIG]; + } + + if (links?.length < 2 && hasConfig) { + // Copy the widget options from the source + if (links.length) { + this.recreateWidget(); + } + + return; + } + + const config1 = output.widget[GET_CONFIG](); + const isNumber = config1[0] === "INT" || config1[0] === "FLOAT"; + if (!isNumber) return; + + for (const linkId of links) { + const link = app.graph.links[linkId]; + if (!link) continue; // Can be null when removing a node + + const theirNode = app.graph.getNodeById(link.target_id); + const theirInput = theirNode.inputs[link.target_slot]; + + // Call is valid connection so it can merge the configs when validating + this.#isValidConnection(theirInput, hasConfig); + } + } + + #isValidConnection(input, forceUpdate) { + // Only allow connections where the configs match + const output = this.outputs[0]; + const config2 = input.widget[GET_CONFIG](); + return !!mergeIfValid.call( + this, + output, + config2, + forceUpdate, + this.recreateWidget + ); + } + + #removeWidgets() { + if (this.widgets) { + // Allow widgets to cleanup + for (const w of this.widgets) { + if (w.onRemove) { + w.onRemove(); + } + } + + // Temporarily store the current values in case the node is being recreated + // e.g. by group node conversion + this.controlValues = []; + this.lastType = this.widgets[0]?.type; + for (let i = 1; i < this.widgets.length; i++) { + this.controlValues.push(this.widgets[i].value); + } + setTimeout(() => { + delete this.lastType; + delete this.controlValues; + }, 15); + this.widgets.length = 0; + } + } + + onLastDisconnect() { + // We cant remove + re-add the output here as if you drag a link over the same link + // it removes, then re-adds, causing it to break + this.outputs[0].type = "*"; + this.outputs[0].name = "connect to widget input"; + delete this.outputs[0].widget; + + this.#removeWidgets(); + } + } + + LiteGraph.registerNodeType( + "PrimitiveNode", + Object.assign(PrimitiveNode, { + title: "Primitive", + }) + ); + PrimitiveNode.category = "utils"; + }, }); diff --git a/src/lib/litegraph.core.js b/src/lib/litegraph.core.js index 427a62b59..4854b6105 100644 --- a/src/lib/litegraph.core.js +++ b/src/lib/litegraph.core.js @@ -1,766 +1,744 @@ //packer version +(function (global) { + // ************************************************************* + // LiteGraph CLASS ******* + // ************************************************************* -(function(global) { - // ************************************************************* - // LiteGraph CLASS ******* - // ************************************************************* + /** + * The Global Scope. It contains all the registered node classes. + * + * @class LiteGraph + * @constructor + */ + + var LiteGraph = (global.LiteGraph = { + VERSION: 0.4, + + CANVAS_GRID_SIZE: 10, + + NODE_TITLE_HEIGHT: 30, + NODE_TITLE_TEXT_Y: 20, + NODE_SLOT_HEIGHT: 20, + NODE_WIDGET_HEIGHT: 20, + NODE_WIDTH: 140, + NODE_MIN_WIDTH: 50, + NODE_COLLAPSED_RADIUS: 10, + NODE_COLLAPSED_WIDTH: 80, + NODE_TITLE_COLOR: "#999", + NODE_SELECTED_TITLE_COLOR: "#FFF", + NODE_TEXT_SIZE: 14, + NODE_TEXT_COLOR: "#AAA", + NODE_SUBTEXT_SIZE: 12, + NODE_DEFAULT_COLOR: "#333", + NODE_DEFAULT_BGCOLOR: "#353535", + NODE_DEFAULT_BOXCOLOR: "#666", + NODE_DEFAULT_SHAPE: "box", + NODE_BOX_OUTLINE_COLOR: "#FFF", + DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", + DEFAULT_GROUP_FONT: 24, + + WIDGET_BGCOLOR: "#222", + WIDGET_OUTLINE_COLOR: "#666", + WIDGET_TEXT_COLOR: "#DDD", + WIDGET_SECONDARY_TEXT_COLOR: "#999", + + LINK_COLOR: "#9A9", + EVENT_LINK_COLOR: "#A86", + CONNECTING_LINK_COLOR: "#AFA", + + MAX_NUMBER_OF_NODES: 10000, //avoid infinite loops + DEFAULT_POSITION: [100, 100], //default node position + VALID_SHAPES: ["default", "box", "round", "card"], //,"circle" + + //shapes are used for nodes but also for slots + BOX_SHAPE: 1, + ROUND_SHAPE: 2, + CIRCLE_SHAPE: 3, + CARD_SHAPE: 4, + ARROW_SHAPE: 5, + GRID_SHAPE: 6, // intended for slot arrays + + //enums + INPUT: 1, + OUTPUT: 2, + + EVENT: -1, //for outputs + ACTION: -1, //for inputs + + NODE_MODES: ["Always", "On Event", "Never", "On Trigger"], // helper, will add "On Request" and more in the future + NODE_MODES_COLORS: ["#666", "#422", "#333", "#224", "#626"], // use with node_box_coloured_by_mode + ALWAYS: 0, + ON_EVENT: 1, + NEVER: 2, + ON_TRIGGER: 3, + + UP: 1, + DOWN: 2, + LEFT: 3, + RIGHT: 4, + CENTER: 5, + + LINK_RENDER_MODES: ["Straight", "Linear", "Spline"], // helper + STRAIGHT_LINK: 0, + LINEAR_LINK: 1, + SPLINE_LINK: 2, + + NORMAL_TITLE: 0, + NO_TITLE: 1, + TRANSPARENT_TITLE: 2, + AUTOHIDE_TITLE: 3, + VERTICAL_LAYOUT: "vertical", // arrange nodes vertically + + proxy: null, //used to redirect calls + node_images_path: "", + + debug: false, + catch_exceptions: true, + throw_errors: true, + allow_scripts: false, //if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits + registered_node_types: {}, //nodetypes by string + node_types_by_file_extension: {}, //used for dropping files in the canvas + Nodes: {}, //node types by classname + Globals: {}, //used to store vars between graphs + + searchbox_extras: {}, //used to add extra features to the search box + auto_sort_node_types: false, // [true!] If set to true, will automatically sort node types / categories in the context menus + + node_box_coloured_when_on: false, // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback + node_box_coloured_by_mode: false, // [true!] nodebox based on node mode, visual feedback + + dialog_close_on_mouse_leave: false, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false + dialog_close_on_mouse_leave_delay: 500, + + shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys + click_do_break_link_to: false, // [false!]prefer false, way too easy to break links + + search_hide_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false + search_filter_enabled: false, // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out] + search_show_all_on_open: true, // [true!] opens the results list when opening the search widget + + auto_load_slot_types: false, // [if want false, use true, run, get vars values to be statically set, than disable] nodes types and nodeclass association with node types need to be calculated, if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out] + + // set these values if not using auto_load_slot_types + registered_slot_in_types: {}, // slot types for nodeclass + registered_slot_out_types: {}, // slot types for nodeclass + slot_types_in: [], // slot types IN + slot_types_out: [], // slot types OUT + slot_types_default_in: [], // specify for each IN slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search + slot_types_default_out: [], // specify for each OUT slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search + + alt_drag_do_clone_nodes: false, // [true!] very handy, ALT click to clone and drag the new node + + do_add_triggers_slots: false, // [true!] will create and connect event slots when using action/events connections, !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this + + allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentially, one by one + + middle_click_slot_add_default_node: false, //[true!] allows to create and connect a ndoe clicking with the third button (wheel) + + release_link_on_empty_shows_menu: false, //[true!] dragging a link to empty space will open a menu, add from list, search or defaults + + pointerevents_method: "pointer", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) + // TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary) + + ctrl_shift_v_paste_connect_unselected_outputs: true, //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes + + // if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers. + // use this if you must have node IDs that are unique across all graphs and subgraphs. + use_uuids: false, /** - * The Global Scope. It contains all the registered node classes. - * - * @class LiteGraph - * @constructor + * Register a node class so it can be listed when the user wants to create a new one + * @method registerNodeType + * @param {String} type name of the node and path + * @param {Class} base_class class containing the structure of a node */ - var LiteGraph = (global.LiteGraph = { - VERSION: 0.4, + registerNodeType: function (type, base_class) { + if (!base_class.prototype) { + throw "Cannot register a simple object, it must be a class with a prototype"; + } + base_class.type = type; - CANVAS_GRID_SIZE: 10, + if (LiteGraph.debug) { + console.log("Node registered: " + type); + } - NODE_TITLE_HEIGHT: 30, - NODE_TITLE_TEXT_Y: 20, - NODE_SLOT_HEIGHT: 20, - NODE_WIDGET_HEIGHT: 20, - NODE_WIDTH: 140, - NODE_MIN_WIDTH: 50, - NODE_COLLAPSED_RADIUS: 10, - NODE_COLLAPSED_WIDTH: 80, - NODE_TITLE_COLOR: "#999", - NODE_SELECTED_TITLE_COLOR: "#FFF", - NODE_TEXT_SIZE: 14, - NODE_TEXT_COLOR: "#AAA", - NODE_SUBTEXT_SIZE: 12, - NODE_DEFAULT_COLOR: "#333", - NODE_DEFAULT_BGCOLOR: "#353535", - NODE_DEFAULT_BOXCOLOR: "#666", - NODE_DEFAULT_SHAPE: "box", - NODE_BOX_OUTLINE_COLOR: "#FFF", - DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", - DEFAULT_GROUP_FONT: 24, + const classname = base_class.name; - WIDGET_BGCOLOR: "#222", - WIDGET_OUTLINE_COLOR: "#666", - WIDGET_TEXT_COLOR: "#DDD", - WIDGET_SECONDARY_TEXT_COLOR: "#999", + const pos = type.lastIndexOf("/"); + base_class.category = type.substring(0, pos); - LINK_COLOR: "#9A9", - EVENT_LINK_COLOR: "#A86", - CONNECTING_LINK_COLOR: "#AFA", + if (!base_class.title) { + base_class.title = classname; + } - MAX_NUMBER_OF_NODES: 10000, //avoid infinite loops - DEFAULT_POSITION: [100, 100], //default node position - VALID_SHAPES: ["default", "box", "round", "card"], //,"circle" + //extend class + for (var i in LGraphNode.prototype) { + if (!base_class.prototype[i]) { + base_class.prototype[i] = LGraphNode.prototype[i]; + } + } - //shapes are used for nodes but also for slots - BOX_SHAPE: 1, - ROUND_SHAPE: 2, - CIRCLE_SHAPE: 3, - CARD_SHAPE: 4, - ARROW_SHAPE: 5, - GRID_SHAPE: 6, // intended for slot arrays - - //enums - INPUT: 1, - OUTPUT: 2, - - EVENT: -1, //for outputs - ACTION: -1, //for inputs - - NODE_MODES: ["Always", "On Event", "Never", "On Trigger"], // helper, will add "On Request" and more in the future - NODE_MODES_COLORS:["#666","#422","#333","#224","#626"], // use with node_box_coloured_by_mode - ALWAYS: 0, - ON_EVENT: 1, - NEVER: 2, - ON_TRIGGER: 3, - - UP: 1, - DOWN: 2, - LEFT: 3, - RIGHT: 4, - CENTER: 5, - - LINK_RENDER_MODES: ["Straight", "Linear", "Spline"], // helper - STRAIGHT_LINK: 0, - LINEAR_LINK: 1, - SPLINE_LINK: 2, - - NORMAL_TITLE: 0, - NO_TITLE: 1, - TRANSPARENT_TITLE: 2, - AUTOHIDE_TITLE: 3, - VERTICAL_LAYOUT: "vertical", // arrange nodes vertically - - proxy: null, //used to redirect calls - node_images_path: "", - - debug: false, - catch_exceptions: true, - throw_errors: true, - allow_scripts: false, //if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits - registered_node_types: {}, //nodetypes by string - node_types_by_file_extension: {}, //used for dropping files in the canvas - Nodes: {}, //node types by classname - Globals: {}, //used to store vars between graphs - - searchbox_extras: {}, //used to add extra features to the search box - auto_sort_node_types: false, // [true!] If set to true, will automatically sort node types / categories in the context menus - - node_box_coloured_when_on: false, // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback - node_box_coloured_by_mode: false, // [true!] nodebox based on node mode, visual feedback - - dialog_close_on_mouse_leave: false, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false - dialog_close_on_mouse_leave_delay: 500, - - shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys - click_do_break_link_to: false, // [false!]prefer false, way too easy to break links - - search_hide_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false - search_filter_enabled: false, // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out] - search_show_all_on_open: true, // [true!] opens the results list when opening the search widget - - auto_load_slot_types: false, // [if want false, use true, run, get vars values to be statically set, than disable] nodes types and nodeclass association with node types need to be calculated, if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out] - - // set these values if not using auto_load_slot_types - registered_slot_in_types: {}, // slot types for nodeclass - registered_slot_out_types: {}, // slot types for nodeclass - slot_types_in: [], // slot types IN - slot_types_out: [], // slot types OUT - slot_types_default_in: [], // specify for each IN slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search - slot_types_default_out: [], // specify for each OUT slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search - - alt_drag_do_clone_nodes: false, // [true!] very handy, ALT click to clone and drag the new node - - do_add_triggers_slots: false, // [true!] will create and connect event slots when using action/events connections, !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this - - allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentially, one by one - - middle_click_slot_add_default_node: false, //[true!] allows to create and connect a ndoe clicking with the third button (wheel) - - release_link_on_empty_shows_menu: false, //[true!] dragging a link to empty space will open a menu, add from list, search or defaults - - pointerevents_method: "pointer", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) - // TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary) - - ctrl_shift_v_paste_connect_unselected_outputs: true, //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes - - // if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers. - // use this if you must have node IDs that are unique across all graphs and subgraphs. - use_uuids: false, - - /** - * Register a node class so it can be listed when the user wants to create a new one - * @method registerNodeType - * @param {String} type name of the node and path - * @param {Class} base_class class containing the structure of a node - */ - - registerNodeType: function(type, base_class) { - if (!base_class.prototype) { - throw "Cannot register a simple object, it must be a class with a prototype"; + const prev = this.registered_node_types[type]; + if (prev) { + console.log("replacing node type: " + type); + } + if ( + !Object.prototype.hasOwnProperty.call(base_class.prototype, "shape") + ) { + Object.defineProperty(base_class.prototype, "shape", { + set: function (v) { + switch (v) { + case "default": + delete this._shape; + break; + case "box": + this._shape = LiteGraph.BOX_SHAPE; + break; + case "round": + this._shape = LiteGraph.ROUND_SHAPE; + break; + case "circle": + this._shape = LiteGraph.CIRCLE_SHAPE; + break; + case "card": + this._shape = LiteGraph.CARD_SHAPE; + break; + default: + this._shape = v; } - base_class.type = type; + }, + get: function () { + return this._shape; + }, + enumerable: true, + configurable: true, + }); - if (LiteGraph.debug) { - console.log("Node registered: " + type); + //used to know which nodes to create when dragging files to the canvas + if (base_class.supported_extensions) { + for (let i in base_class.supported_extensions) { + const ext = base_class.supported_extensions[i]; + if (ext && ext.constructor === String) { + this.node_types_by_file_extension[ext.toLowerCase()] = base_class; } + } + } + } - const classname = base_class.name; + this.registered_node_types[type] = base_class; + if (base_class.constructor.name) { + this.Nodes[classname] = base_class; + } + if (LiteGraph.onNodeTypeRegistered) { + LiteGraph.onNodeTypeRegistered(type, base_class); + } + if (prev && LiteGraph.onNodeTypeReplaced) { + LiteGraph.onNodeTypeReplaced(type, base_class, prev); + } - const pos = type.lastIndexOf("/"); - base_class.category = type.substring(0, pos); + //warnings + if (base_class.prototype.onPropertyChange) { + console.warn( + "LiteGraph node class " + + type + + " has onPropertyChange method, it must be called onPropertyChanged with d at the end" + ); + } - if (!base_class.title) { - base_class.title = classname; - } - - //extend class - for (var i in LGraphNode.prototype) { - if (!base_class.prototype[i]) { - base_class.prototype[i] = LGraphNode.prototype[i]; - } - } - - const prev = this.registered_node_types[type]; - if(prev) { - console.log("replacing node type: " + type); - } - if( !Object.prototype.hasOwnProperty.call( base_class.prototype, "shape") ) { - Object.defineProperty(base_class.prototype, "shape", { - set: function(v) { - switch (v) { - case "default": - delete this._shape; - break; - case "box": - this._shape = LiteGraph.BOX_SHAPE; - break; - case "round": - this._shape = LiteGraph.ROUND_SHAPE; - break; - case "circle": - this._shape = LiteGraph.CIRCLE_SHAPE; - break; - case "card": - this._shape = LiteGraph.CARD_SHAPE; - break; - default: - this._shape = v; - } - }, - get: function() { - return this._shape; - }, - enumerable: true, - configurable: true - }); - - - //used to know which nodes to create when dragging files to the canvas - if (base_class.supported_extensions) { - for (let i in base_class.supported_extensions) { - const ext = base_class.supported_extensions[i]; - if(ext && ext.constructor === String) { - this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class; - } - } - } - } - - this.registered_node_types[type] = base_class; - if (base_class.constructor.name) { - this.Nodes[classname] = base_class; - } - if (LiteGraph.onNodeTypeRegistered) { - LiteGraph.onNodeTypeRegistered(type, base_class); - } - if (prev && LiteGraph.onNodeTypeReplaced) { - LiteGraph.onNodeTypeReplaced(type, base_class, prev); - } - - //warnings - if (base_class.prototype.onPropertyChange) { - console.warn( - "LiteGraph node class " + - type + - " has onPropertyChange method, it must be called onPropertyChanged with d at the end" - ); - } - - // TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types - if (this.auto_load_slot_types) { - new base_class(base_class.title || "tmpnode"); - } - }, - - /** - * removes a node type from the system - * @method unregisterNodeType - * @param {String|Object} type name of the node or the node constructor itself - */ - unregisterNodeType: function(type) { - const base_class = - type.constructor === String - ? this.registered_node_types[type] - : type; - if (!base_class) { - throw "node type not found: " + type; - } - delete this.registered_node_types[base_class.type]; - if (base_class.constructor.name) { - delete this.Nodes[base_class.constructor.name]; - } - }, - - /** - * Save a slot type and his node - * @method registerSlotType - * @param {String|Object} type name of the node or the node constructor itself - * @param {String} slot_type name of the slot type (variable type), eg. string, number, array, boolean, .. - */ - registerNodeAndSlotType: function(type, slot_type, out){ - out = out || false; - const base_class = - type.constructor === String && - this.registered_node_types[type] !== "anonymous" - ? this.registered_node_types[type] - : type; - - const class_type = base_class.constructor.type; - - let allTypes = []; - if (typeof slot_type === "string") { - allTypes = slot_type.split(","); - } else if (slot_type == this.EVENT || slot_type == this.ACTION) { - allTypes = ["_event_"]; - } else { - allTypes = ["*"]; - } - - for (let i = 0; i < allTypes.length; ++i) { - let slotType = allTypes[i]; - if (slotType === "") { - slotType = "*"; - } - const registerTo = out - ? "registered_slot_out_types" - : "registered_slot_in_types"; - if (this[registerTo][slotType] === undefined) { - this[registerTo][slotType] = { nodes: [] }; - } - if (!this[registerTo][slotType].nodes.includes(class_type)) { - this[registerTo][slotType].nodes.push(class_type); - } - - // check if is a new type - if (!out) { - if (!this.slot_types_in.includes(slotType.toLowerCase())) { - this.slot_types_in.push(slotType.toLowerCase()); - this.slot_types_in.sort(); - } - } else { - if (!this.slot_types_out.includes(slotType.toLowerCase())) { - this.slot_types_out.push(slotType.toLowerCase()); - this.slot_types_out.sort(); - } - } - } - }, - - /** - * Create a new nodetype by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function. - * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. - * @method wrapFunctionAsNode - * @param {String} name node name with namespace (p.e.: 'math/sum') - * @param {Function} func - * @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type - * @param {String} return_type [optional] string with the return type, otherwise it will be generic - * @param {Object} properties [optional] properties to be configurable - */ - wrapFunctionAsNode: function( - name, - func, - param_types, - return_type, - properties - ) { - var params = Array(func.length); - var code = ""; - var names = LiteGraph.getParameterNames(func); - for (var i = 0; i < names.length; ++i) { - code += - "this.addInput('" + - names[i] + - "'," + - (param_types && param_types[i] - ? "'" + param_types[i] + "'" - : "0") + - ");\n"; - } - code += - "this.addOutput('out'," + - (return_type ? "'" + return_type + "'" : 0) + - ");\n"; - if (properties) { - code += - "this.properties = " + JSON.stringify(properties) + ";\n"; - } - var classobj = Function(code); - classobj.title = name.split("/").pop(); - classobj.desc = "Generated from " + func.name; - classobj.prototype.onExecute = function onExecute() { - for (var i = 0; i < params.length; ++i) { - params[i] = this.getInputData(i); - } - var r = func.apply(this, params); - this.setOutputData(0, r); - }; - this.registerNodeType(name, classobj); - }, - - /** - * Removes all previously registered node's types - */ - clearRegisteredTypes: function() { - this.registered_node_types = {}; - this.node_types_by_file_extension = {}; - this.Nodes = {}; - this.searchbox_extras = {}; - }, - - /** - * Adds this method to all nodetypes, existing and to be created - * (You can add it to LGraphNode.prototype but then existing node types wont have it) - * @method addNodeMethod - * @param {Function} func - */ - addNodeMethod: function(name, func) { - LGraphNode.prototype[name] = func; - for (var i in this.registered_node_types) { - var type = this.registered_node_types[i]; - if (type.prototype[name]) { - type.prototype["_" + name] = type.prototype[name]; - } //keep old in case of replacing - type.prototype[name] = func; - } - }, - - /** - * Create a node of a given type with a name. The node is not attached to any graph yet. - * @method createNode - * @param {String} type full name of the node class. p.e. "math/sin" - * @param {String} name a name to distinguish from other nodes - * @param {Object} options to set options - */ - - createNode: function(type, title, options) { - var base_class = this.registered_node_types[type]; - if (!base_class) { - if (LiteGraph.debug) { - console.log( - 'GraphNode type "' + type + '" not registered.' - ); - } - return null; - } - - var prototype = base_class.prototype || base_class; - - title = title || base_class.title || type; - - var node = null; - - if (LiteGraph.catch_exceptions) { - try { - node = new base_class(title); - } catch (err) { - console.error(err); - return null; - } - } else { - node = new base_class(title); - } - - node.type = type; - - if (!node.title && title) { - node.title = title; - } - if (!node.properties) { - node.properties = {}; - } - if (!node.properties_info) { - node.properties_info = []; - } - if (!node.flags) { - node.flags = {}; - } - if (!node.size) { - node.size = node.computeSize(); - //call onresize? - } - if (!node.pos) { - node.pos = LiteGraph.DEFAULT_POSITION.concat(); - } - if (!node.mode) { - node.mode = LiteGraph.ALWAYS; - } - - //extra options - if (options) { - for (var i in options) { - node[i] = options[i]; - } - } - - // callback - if ( node.onNodeCreated ) { - node.onNodeCreated(); - } - - return node; - }, - - /** - * Returns a registered node type with a given name - * @method getNodeType - * @param {String} type full name of the node class. p.e. "math/sin" - * @return {Class} the node class - */ - getNodeType: function(type) { - return this.registered_node_types[type]; - }, - - /** - * Returns a list of node types matching one category - * @method getNodeType - * @param {String} category category name - * @return {Array} array with all the node classes - */ - - getNodeTypesInCategory: function(category, filter) { - var r = []; - for (var i in this.registered_node_types) { - var type = this.registered_node_types[i]; - if (type.filter != filter) { - continue; - } - - if (category == "") { - if (type.category == null) { - r.push(type); - } - } else if (type.category == category) { - r.push(type); - } - } - - if (this.auto_sort_node_types) { - r.sort(function(a,b){return a.title.localeCompare(b.title)}); - } - - return r; - }, - - /** - * Returns a list with all the node type categories - * @method getNodeTypesCategories - * @param {String} filter only nodes with ctor.filter equal can be shown - * @return {Array} array with all the names of the categories - */ - getNodeTypesCategories: function( filter ) { - var categories = { "": 1 }; - for (var i in this.registered_node_types) { - var type = this.registered_node_types[i]; - if ( type.category && !type.skip_list ) - { - if(type.filter != filter) - continue; - categories[type.category] = 1; - } - } - var result = []; - for (var i in categories) { - result.push(i); - } - return this.auto_sort_node_types ? result.sort() : result; - }, - - //debug purposes: reloads all the js scripts that matches a wildcard - reloadNodes: function(folder_wildcard) { - var tmp = document.getElementsByTagName("script"); - //weird, this array changes by its own, so we use a copy - var script_files = []; - for (var i=0; i < tmp.length; i++) { - script_files.push(tmp[i]); - } - - var docHeadObj = document.getElementsByTagName("head")[0]; - folder_wildcard = document.location.href + folder_wildcard; - - for (var i=0; i < script_files.length; i++) { - var src = script_files[i].src; - if ( - !src || - src.substr(0, folder_wildcard.length) != folder_wildcard - ) { - continue; - } - - try { - if (LiteGraph.debug) { - console.log("Reloading: " + src); - } - var dynamicScript = document.createElement("script"); - dynamicScript.type = "text/javascript"; - dynamicScript.src = src; - docHeadObj.appendChild(dynamicScript); - docHeadObj.removeChild(script_files[i]); - } catch (err) { - if (LiteGraph.throw_errors) { - throw err; - } - if (LiteGraph.debug) { - console.log("Error while reloading " + src); - } - } - } - - if (LiteGraph.debug) { - console.log("Nodes reloaded"); - } - }, - - //separated just to improve if it doesn't work - cloneObject: function(obj, target) { - if (obj == null) { - return null; - } - var r = JSON.parse(JSON.stringify(obj)); - if (!target) { - return r; - } - - for (var i in r) { - target[i] = r[i]; - } - return target; - }, - - /* - * https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670 - */ - uuidv4: function() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,a=>(a^Math.random()*16>>a/4).toString(16)); - }, - - /** - * Returns if the types of two slots are compatible (taking into account wildcards, etc) - * @method isValidConnection - * @param {String} type_a - * @param {String} type_b - * @return {Boolean} true if they can be connected - */ - isValidConnection: function(type_a, type_b) { - if (type_a=="" || type_a==="*") type_a = 0; - if (type_b=="" || type_b==="*") type_b = 0; - if ( - !type_a //generic output - || !type_b // generic input - || type_a == type_b //same type (is valid for triggers) - || (type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION) - ) { - return true; - } - - // Enforce string type to handle toLowerCase call (-1 number not ok) - type_a = String(type_a); - type_b = String(type_b); - type_a = type_a.toLowerCase(); - type_b = type_b.toLowerCase(); - - // For nodes supporting multiple connection types - if (type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1) { - return type_a == type_b; - } - - // Check all permutations to see if one is valid - var supported_types_a = type_a.split(","); - var supported_types_b = type_b.split(","); - for (var i = 0; i < supported_types_a.length; ++i) { - for (var j = 0; j < supported_types_b.length; ++j) { - if(this.isValidConnection(supported_types_a[i],supported_types_b[j])){ - //if (supported_types_a[i] == supported_types_b[j]) { - return true; - } - } - } - - return false; - }, - - /** - * Register a string in the search box so when the user types it it will recommend this node - * @method registerSearchboxExtra - * @param {String} node_type the node recommended - * @param {String} description text to show next to it - * @param {Object} data it could contain info of how the node should be configured - * @return {Boolean} true if they can be connected - */ - registerSearchboxExtra: function(node_type, description, data) { - this.searchbox_extras[description.toLowerCase()] = { - type: node_type, - desc: description, - data: data - }; - }, - - /** - * Wrapper to load files (from url using fetch or from file using FileReader) - * @method fetchFile - * @param {String|File|Blob} url the url of the file (or the file itself) - * @param {String} type an string to know how to fetch it: "text","arraybuffer","json","blob" - * @param {Function} on_complete callback(data) - * @param {Function} on_error in case of an error - * @return {FileReader|Promise} returns the object used to - */ - fetchFile: function( url, type, on_complete, on_error ) { - var that = this; - if(!url) - return null; - - type = type || "text"; - if( url.constructor === String ) - { - if (url.substr(0, 4) == "http" && LiteGraph.proxy) { - url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3); - } - return fetch(url) - .then(function(response) { - if(!response.ok) - throw new Error("File not found"); //it will be catch below - if(type == "arraybuffer") - return response.arrayBuffer(); - else if(type == "text" || type == "string") - return response.text(); - else if(type == "json") - return response.json(); - else if(type == "blob") - return response.blob(); - }) - .then(function(data) { - if(on_complete) - on_complete(data); - }) - .catch(function(error) { - console.error("error fetching file:",url); - if(on_error) - on_error(error); - }); - } - else if( url.constructor === File || url.constructor === Blob) - { - var reader = new FileReader(); - reader.onload = function(e) - { - var v = e.target.result; - if( type == "json" ) - v = JSON.parse(v); - if(on_complete) - on_complete(v); - } - if(type == "arraybuffer") - return reader.readAsArrayBuffer(url); - else if(type == "text" || type == "json") - return reader.readAsText(url); - else if(type == "blob") - return reader.readAsBinaryString(url); - } - return null; - } - }); - - //timer that works everywhere - if (typeof performance != "undefined") { - LiteGraph.getTime = performance.now.bind(performance); - } else if (typeof Date != "undefined" && Date.now) { - LiteGraph.getTime = Date.now.bind(Date); - } else if (typeof process != "undefined") { - LiteGraph.getTime = function() { - var t = process.hrtime(); - return t[0] * 0.001 + t[1] * 1e-6; - }; - } else { - LiteGraph.getTime = function getTime() { - return new Date().getTime(); - }; - } - - //********************************************************************************* - // LGraph CLASS - //********************************************************************************* + // TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types + if (this.auto_load_slot_types) { + new base_class(base_class.title || "tmpnode"); + } + }, /** + * removes a node type from the system + * @method unregisterNodeType + * @param {String|Object} type name of the node or the node constructor itself + */ + unregisterNodeType: function (type) { + const base_class = + type.constructor === String ? this.registered_node_types[type] : type; + if (!base_class) { + throw "node type not found: " + type; + } + delete this.registered_node_types[base_class.type]; + if (base_class.constructor.name) { + delete this.Nodes[base_class.constructor.name]; + } + }, + + /** + * Save a slot type and his node + * @method registerSlotType + * @param {String|Object} type name of the node or the node constructor itself + * @param {String} slot_type name of the slot type (variable type), eg. string, number, array, boolean, .. + */ + registerNodeAndSlotType: function (type, slot_type, out) { + out = out || false; + const base_class = + type.constructor === String && + this.registered_node_types[type] !== "anonymous" + ? this.registered_node_types[type] + : type; + + const class_type = base_class.constructor.type; + + let allTypes = []; + if (typeof slot_type === "string") { + allTypes = slot_type.split(","); + } else if (slot_type == this.EVENT || slot_type == this.ACTION) { + allTypes = ["_event_"]; + } else { + allTypes = ["*"]; + } + + for (let i = 0; i < allTypes.length; ++i) { + let slotType = allTypes[i]; + if (slotType === "") { + slotType = "*"; + } + const registerTo = out + ? "registered_slot_out_types" + : "registered_slot_in_types"; + if (this[registerTo][slotType] === undefined) { + this[registerTo][slotType] = { nodes: [] }; + } + if (!this[registerTo][slotType].nodes.includes(class_type)) { + this[registerTo][slotType].nodes.push(class_type); + } + + // check if is a new type + if (!out) { + if (!this.slot_types_in.includes(slotType.toLowerCase())) { + this.slot_types_in.push(slotType.toLowerCase()); + this.slot_types_in.sort(); + } + } else { + if (!this.slot_types_out.includes(slotType.toLowerCase())) { + this.slot_types_out.push(slotType.toLowerCase()); + this.slot_types_out.sort(); + } + } + } + }, + + /** + * Create a new nodetype by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function. + * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. + * @method wrapFunctionAsNode + * @param {String} name node name with namespace (p.e.: 'math/sum') + * @param {Function} func + * @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type + * @param {String} return_type [optional] string with the return type, otherwise it will be generic + * @param {Object} properties [optional] properties to be configurable + */ + wrapFunctionAsNode: function ( + name, + func, + param_types, + return_type, + properties + ) { + var params = Array(func.length); + var code = ""; + var names = LiteGraph.getParameterNames(func); + for (var i = 0; i < names.length; ++i) { + code += + "this.addInput('" + + names[i] + + "'," + + (param_types && param_types[i] ? "'" + param_types[i] + "'" : "0") + + ");\n"; + } + code += + "this.addOutput('out'," + + (return_type ? "'" + return_type + "'" : 0) + + ");\n"; + if (properties) { + code += "this.properties = " + JSON.stringify(properties) + ";\n"; + } + var classobj = Function(code); + classobj.title = name.split("/").pop(); + classobj.desc = "Generated from " + func.name; + classobj.prototype.onExecute = function onExecute() { + for (var i = 0; i < params.length; ++i) { + params[i] = this.getInputData(i); + } + var r = func.apply(this, params); + this.setOutputData(0, r); + }; + this.registerNodeType(name, classobj); + }, + + /** + * Removes all previously registered node's types + */ + clearRegisteredTypes: function () { + this.registered_node_types = {}; + this.node_types_by_file_extension = {}; + this.Nodes = {}; + this.searchbox_extras = {}; + }, + + /** + * Adds this method to all nodetypes, existing and to be created + * (You can add it to LGraphNode.prototype but then existing node types wont have it) + * @method addNodeMethod + * @param {Function} func + */ + addNodeMethod: function (name, func) { + LGraphNode.prototype[name] = func; + for (var i in this.registered_node_types) { + var type = this.registered_node_types[i]; + if (type.prototype[name]) { + type.prototype["_" + name] = type.prototype[name]; + } //keep old in case of replacing + type.prototype[name] = func; + } + }, + + /** + * Create a node of a given type with a name. The node is not attached to any graph yet. + * @method createNode + * @param {String} type full name of the node class. p.e. "math/sin" + * @param {String} name a name to distinguish from other nodes + * @param {Object} options to set options + */ + + createNode: function (type, title, options) { + var base_class = this.registered_node_types[type]; + if (!base_class) { + if (LiteGraph.debug) { + console.log('GraphNode type "' + type + '" not registered.'); + } + return null; + } + + var prototype = base_class.prototype || base_class; + + title = title || base_class.title || type; + + var node = null; + + if (LiteGraph.catch_exceptions) { + try { + node = new base_class(title); + } catch (err) { + console.error(err); + return null; + } + } else { + node = new base_class(title); + } + + node.type = type; + + if (!node.title && title) { + node.title = title; + } + if (!node.properties) { + node.properties = {}; + } + if (!node.properties_info) { + node.properties_info = []; + } + if (!node.flags) { + node.flags = {}; + } + if (!node.size) { + node.size = node.computeSize(); + //call onresize? + } + if (!node.pos) { + node.pos = LiteGraph.DEFAULT_POSITION.concat(); + } + if (!node.mode) { + node.mode = LiteGraph.ALWAYS; + } + + //extra options + if (options) { + for (var i in options) { + node[i] = options[i]; + } + } + + // callback + if (node.onNodeCreated) { + node.onNodeCreated(); + } + + return node; + }, + + /** + * Returns a registered node type with a given name + * @method getNodeType + * @param {String} type full name of the node class. p.e. "math/sin" + * @return {Class} the node class + */ + getNodeType: function (type) { + return this.registered_node_types[type]; + }, + + /** + * Returns a list of node types matching one category + * @method getNodeType + * @param {String} category category name + * @return {Array} array with all the node classes + */ + + getNodeTypesInCategory: function (category, filter) { + var r = []; + for (var i in this.registered_node_types) { + var type = this.registered_node_types[i]; + if (type.filter != filter) { + continue; + } + + if (category == "") { + if (type.category == null) { + r.push(type); + } + } else if (type.category == category) { + r.push(type); + } + } + + if (this.auto_sort_node_types) { + r.sort(function (a, b) { + return a.title.localeCompare(b.title); + }); + } + + return r; + }, + + /** + * Returns a list with all the node type categories + * @method getNodeTypesCategories + * @param {String} filter only nodes with ctor.filter equal can be shown + * @return {Array} array with all the names of the categories + */ + getNodeTypesCategories: function (filter) { + var categories = { "": 1 }; + for (var i in this.registered_node_types) { + var type = this.registered_node_types[i]; + if (type.category && !type.skip_list) { + if (type.filter != filter) continue; + categories[type.category] = 1; + } + } + var result = []; + for (var i in categories) { + result.push(i); + } + return this.auto_sort_node_types ? result.sort() : result; + }, + + //debug purposes: reloads all the js scripts that matches a wildcard + reloadNodes: function (folder_wildcard) { + var tmp = document.getElementsByTagName("script"); + //weird, this array changes by its own, so we use a copy + var script_files = []; + for (var i = 0; i < tmp.length; i++) { + script_files.push(tmp[i]); + } + + var docHeadObj = document.getElementsByTagName("head")[0]; + folder_wildcard = document.location.href + folder_wildcard; + + for (var i = 0; i < script_files.length; i++) { + var src = script_files[i].src; + if (!src || src.substr(0, folder_wildcard.length) != folder_wildcard) { + continue; + } + + try { + if (LiteGraph.debug) { + console.log("Reloading: " + src); + } + var dynamicScript = document.createElement("script"); + dynamicScript.type = "text/javascript"; + dynamicScript.src = src; + docHeadObj.appendChild(dynamicScript); + docHeadObj.removeChild(script_files[i]); + } catch (err) { + if (LiteGraph.throw_errors) { + throw err; + } + if (LiteGraph.debug) { + console.log("Error while reloading " + src); + } + } + } + + if (LiteGraph.debug) { + console.log("Nodes reloaded"); + } + }, + + //separated just to improve if it doesn't work + cloneObject: function (obj, target) { + if (obj == null) { + return null; + } + var r = JSON.parse(JSON.stringify(obj)); + if (!target) { + return r; + } + + for (var i in r) { + target[i] = r[i]; + } + return target; + }, + + /* + * https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670 + */ + uuidv4: function () { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (a) => + (a ^ ((Math.random() * 16) >> (a / 4))).toString(16) + ); + }, + + /** + * Returns if the types of two slots are compatible (taking into account wildcards, etc) + * @method isValidConnection + * @param {String} type_a + * @param {String} type_b + * @return {Boolean} true if they can be connected + */ + isValidConnection: function (type_a, type_b) { + if (type_a == "" || type_a === "*") type_a = 0; + if (type_b == "" || type_b === "*") type_b = 0; + if ( + !type_a || //generic output + !type_b || // generic input + type_a == type_b || //same type (is valid for triggers) + (type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION) + ) { + return true; + } + + // Enforce string type to handle toLowerCase call (-1 number not ok) + type_a = String(type_a); + type_b = String(type_b); + type_a = type_a.toLowerCase(); + type_b = type_b.toLowerCase(); + + // For nodes supporting multiple connection types + if (type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1) { + return type_a == type_b; + } + + // Check all permutations to see if one is valid + var supported_types_a = type_a.split(","); + var supported_types_b = type_b.split(","); + for (var i = 0; i < supported_types_a.length; ++i) { + for (var j = 0; j < supported_types_b.length; ++j) { + if ( + this.isValidConnection(supported_types_a[i], supported_types_b[j]) + ) { + //if (supported_types_a[i] == supported_types_b[j]) { + return true; + } + } + } + + return false; + }, + + /** + * Register a string in the search box so when the user types it it will recommend this node + * @method registerSearchboxExtra + * @param {String} node_type the node recommended + * @param {String} description text to show next to it + * @param {Object} data it could contain info of how the node should be configured + * @return {Boolean} true if they can be connected + */ + registerSearchboxExtra: function (node_type, description, data) { + this.searchbox_extras[description.toLowerCase()] = { + type: node_type, + desc: description, + data: data, + }; + }, + + /** + * Wrapper to load files (from url using fetch or from file using FileReader) + * @method fetchFile + * @param {String|File|Blob} url the url of the file (or the file itself) + * @param {String} type an string to know how to fetch it: "text","arraybuffer","json","blob" + * @param {Function} on_complete callback(data) + * @param {Function} on_error in case of an error + * @return {FileReader|Promise} returns the object used to + */ + fetchFile: function (url, type, on_complete, on_error) { + var that = this; + if (!url) return null; + + type = type || "text"; + if (url.constructor === String) { + if (url.substr(0, 4) == "http" && LiteGraph.proxy) { + url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3); + } + return fetch(url) + .then(function (response) { + if (!response.ok) throw new Error("File not found"); //it will be catch below + if (type == "arraybuffer") return response.arrayBuffer(); + else if (type == "text" || type == "string") return response.text(); + else if (type == "json") return response.json(); + else if (type == "blob") return response.blob(); + }) + .then(function (data) { + if (on_complete) on_complete(data); + }) + .catch(function (error) { + console.error("error fetching file:", url); + if (on_error) on_error(error); + }); + } else if (url.constructor === File || url.constructor === Blob) { + var reader = new FileReader(); + reader.onload = function (e) { + var v = e.target.result; + if (type == "json") v = JSON.parse(v); + if (on_complete) on_complete(v); + }; + if (type == "arraybuffer") return reader.readAsArrayBuffer(url); + else if (type == "text" || type == "json") + return reader.readAsText(url); + else if (type == "blob") return reader.readAsBinaryString(url); + } + return null; + }, + }); + + //timer that works everywhere + if (typeof performance != "undefined") { + LiteGraph.getTime = performance.now.bind(performance); + } else if (typeof Date != "undefined" && Date.now) { + LiteGraph.getTime = Date.now.bind(Date); + } else if (typeof process != "undefined") { + LiteGraph.getTime = function () { + var t = process.hrtime(); + return t[0] * 0.001 + t[1] * 1e-6; + }; + } else { + LiteGraph.getTime = function getTime() { + return new Date().getTime(); + }; + } + + //********************************************************************************* + // LGraph CLASS + //********************************************************************************* + + /** * LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop. * supported callbacks: + onNodeAdded: when a new node is added to the graph @@ -772,1588 +750,1573 @@ * @param {Object} o data from previous serialization [optional] */ - function LGraph(o) { - if (LiteGraph.debug) { - console.log("Graph created"); - } - this.list_of_graphcanvas = null; - this.clear(); + function LGraph(o) { + if (LiteGraph.debug) { + console.log("Graph created"); + } + this.list_of_graphcanvas = null; + this.clear(); - if (o) { - this.configure(o); + if (o) { + this.configure(o); + } + } + + global.LGraph = LiteGraph.LGraph = LGraph; + + //default supported types + LGraph.supported_types = ["number", "string", "boolean"]; + + //used to know which types of connections support this graph (some graphs do not allow certain types) + LGraph.prototype.getSupportedTypes = function () { + return this.supported_types || LGraph.supported_types; + }; + + LGraph.STATUS_STOPPED = 1; + LGraph.STATUS_RUNNING = 2; + + /** + * Removes all nodes from this graph + * @method clear + */ + + LGraph.prototype.clear = function () { + this.stop(); + this.status = LGraph.STATUS_STOPPED; + + this.last_node_id = 0; + this.last_link_id = 0; + + this._version = -1; //used to detect changes + + //safe clear + if (this._nodes) { + for (var i = 0; i < this._nodes.length; ++i) { + var node = this._nodes[i]; + if (node.onRemoved) { + node.onRemoved(); } + } } - global.LGraph = LiteGraph.LGraph = LGraph; + //nodes + this._nodes = []; + this._nodes_by_id = {}; + this._nodes_in_order = []; //nodes sorted in execution order + this._nodes_executable = null; //nodes that contain onExecute sorted in execution order - //default supported types - LGraph.supported_types = ["number", "string", "boolean"]; + //other scene stuff + this._groups = []; - //used to know which types of connections support this graph (some graphs do not allow certain types) - LGraph.prototype.getSupportedTypes = function() { - return this.supported_types || LGraph.supported_types; - }; + //links + this.links = {}; //container with all the links - LGraph.STATUS_STOPPED = 1; - LGraph.STATUS_RUNNING = 2; + //iterations + this.iteration = 0; - /** - * Removes all nodes from this graph - * @method clear - */ + //custom data + this.config = {}; + this.vars = {}; + this.extra = {}; //to store custom data - LGraph.prototype.clear = function() { - this.stop(); - this.status = LGraph.STATUS_STOPPED; + //timing + this.globaltime = 0; + this.runningtime = 0; + this.fixedtime = 0; + this.fixedtime_lapse = 0.01; + this.elapsed_time = 0.01; + this.last_update_time = 0; + this.starttime = 0; - this.last_node_id = 0; - this.last_link_id = 0; + this.catch_errors = true; - this._version = -1; //used to detect changes + this.nodes_executing = []; + this.nodes_actioning = []; + this.nodes_executedAction = []; - //safe clear - if (this._nodes) { - for (var i = 0; i < this._nodes.length; ++i) { - var node = this._nodes[i]; - if (node.onRemoved) { - node.onRemoved(); - } - } - } + //subgraph_data + this.inputs = {}; + this.outputs = {}; - //nodes - this._nodes = []; - this._nodes_by_id = {}; - this._nodes_in_order = []; //nodes sorted in execution order - this._nodes_executable = null; //nodes that contain onExecute sorted in execution order + //notify canvas to redraw + this.change(); - //other scene stuff - this._groups = []; + this.sendActionToCanvas("clear"); + }; - //links - this.links = {}; //container with all the links + /** + * Attach Canvas to this graph + * @method attachCanvas + * @param {GraphCanvas} graph_canvas + */ - //iterations - this.iteration = 0; + LGraph.prototype.attachCanvas = function (graphcanvas) { + if (graphcanvas.constructor != LGraphCanvas) { + throw "attachCanvas expects a LGraphCanvas instance"; + } + if (graphcanvas.graph && graphcanvas.graph != this) { + graphcanvas.graph.detachCanvas(graphcanvas); + } - //custom data - this.config = {}; - this.vars = {}; - this.extra = {}; //to store custom data + graphcanvas.graph = this; - //timing - this.globaltime = 0; - this.runningtime = 0; - this.fixedtime = 0; - this.fixedtime_lapse = 0.01; - this.elapsed_time = 0.01; - this.last_update_time = 0; - this.starttime = 0; + if (!this.list_of_graphcanvas) { + this.list_of_graphcanvas = []; + } + this.list_of_graphcanvas.push(graphcanvas); + }; - this.catch_errors = true; + /** + * Detach Canvas from this graph + * @method detachCanvas + * @param {GraphCanvas} graph_canvas + */ + LGraph.prototype.detachCanvas = function (graphcanvas) { + if (!this.list_of_graphcanvas) { + return; + } - this.nodes_executing = []; - this.nodes_actioning = []; - this.nodes_executedAction = []; - - //subgraph_data - this.inputs = {}; - this.outputs = {}; + var pos = this.list_of_graphcanvas.indexOf(graphcanvas); + if (pos == -1) { + return; + } + graphcanvas.graph = null; + this.list_of_graphcanvas.splice(pos, 1); + }; - //notify canvas to redraw - this.change(); + /** + * Starts running this graph every interval milliseconds. + * @method start + * @param {number} interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate + */ - this.sendActionToCanvas("clear"); - }; + LGraph.prototype.start = function (interval) { + if (this.status == LGraph.STATUS_RUNNING) { + return; + } + this.status = LGraph.STATUS_RUNNING; - /** - * Attach Canvas to this graph - * @method attachCanvas - * @param {GraphCanvas} graph_canvas - */ + if (this.onPlayEvent) { + this.onPlayEvent(); + } - LGraph.prototype.attachCanvas = function(graphcanvas) { - if (graphcanvas.constructor != LGraphCanvas) { - throw "attachCanvas expects a LGraphCanvas instance"; - } - if (graphcanvas.graph && graphcanvas.graph != this) { - graphcanvas.graph.detachCanvas(graphcanvas); - } + this.sendEventToAllNodes("onStart"); - graphcanvas.graph = this; + //launch + this.starttime = LiteGraph.getTime(); + this.last_update_time = this.starttime; + interval = interval || 0; + var that = this; - if (!this.list_of_graphcanvas) { - this.list_of_graphcanvas = []; - } - this.list_of_graphcanvas.push(graphcanvas); - }; - - /** - * Detach Canvas from this graph - * @method detachCanvas - * @param {GraphCanvas} graph_canvas - */ - LGraph.prototype.detachCanvas = function(graphcanvas) { - if (!this.list_of_graphcanvas) { - return; - } - - var pos = this.list_of_graphcanvas.indexOf(graphcanvas); - if (pos == -1) { - return; - } - graphcanvas.graph = null; - this.list_of_graphcanvas.splice(pos, 1); - }; - - /** - * Starts running this graph every interval milliseconds. - * @method start - * @param {number} interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate - */ - - LGraph.prototype.start = function(interval) { - if (this.status == LGraph.STATUS_RUNNING) { - return; - } - this.status = LGraph.STATUS_RUNNING; - - if (this.onPlayEvent) { - this.onPlayEvent(); - } - - this.sendEventToAllNodes("onStart"); - - //launch - this.starttime = LiteGraph.getTime(); - this.last_update_time = this.starttime; - interval = interval || 0; - var that = this; - - //execute once per frame - if ( interval == 0 && typeof window != "undefined" && window.requestAnimationFrame ) { - function on_frame() { - if (that.execution_timer_id != -1) { - return; - } - window.requestAnimationFrame(on_frame); - if(that.onBeforeStep) - that.onBeforeStep(); - that.runStep(1, !that.catch_errors); - if(that.onAfterStep) - that.onAfterStep(); - } - this.execution_timer_id = -1; - on_frame(); - } else { //execute every 'interval' ms - this.execution_timer_id = setInterval(function() { - //execute - if(that.onBeforeStep) - that.onBeforeStep(); - that.runStep(1, !that.catch_errors); - if(that.onAfterStep) - that.onAfterStep(); - }, interval); - } - }; - - /** - * Stops the execution loop of the graph - * @method stop execution - */ - - LGraph.prototype.stop = function() { - if (this.status == LGraph.STATUS_STOPPED) { - return; - } - - this.status = LGraph.STATUS_STOPPED; - - if (this.onStopEvent) { - this.onStopEvent(); - } - - if (this.execution_timer_id != null) { - if (this.execution_timer_id != -1) { - clearInterval(this.execution_timer_id); - } - this.execution_timer_id = null; - } - - this.sendEventToAllNodes("onStop"); - }; - - /** - * Run N steps (cycles) of the graph - * @method runStep - * @param {number} num number of steps to run, default is 1 - * @param {Boolean} do_not_catch_errors [optional] if you want to try/catch errors - * @param {number} limit max number of nodes to execute (used to execute from start to a node) - */ - - LGraph.prototype.runStep = function(num, do_not_catch_errors, limit ) { - num = num || 1; - - var start = LiteGraph.getTime(); - this.globaltime = 0.001 * (start - this.starttime); - - var nodes = this._nodes_executable - ? this._nodes_executable - : this._nodes; - if (!nodes) { - return; - } - - limit = limit || nodes.length; - - if (do_not_catch_errors) { - //iterations - for (var i = 0; i < num; i++) { - for (var j = 0; j < limit; ++j) { - var node = nodes[j]; - if (node.mode == LiteGraph.ALWAYS && node.onExecute) { - //wrap node.onExecute(); - node.doExecute(); - } - } - - this.fixedtime += this.fixedtime_lapse; - if (this.onExecuteStep) { - this.onExecuteStep(); - } - } - - if (this.onAfterExecute) { - this.onAfterExecute(); - } - } else { - try { - //iterations - for (var i = 0; i < num; i++) { - for (var j = 0; j < limit; ++j) { - var node = nodes[j]; - if (node.mode == LiteGraph.ALWAYS && node.onExecute) { - node.onExecute(); - } - } - - this.fixedtime += this.fixedtime_lapse; - if (this.onExecuteStep) { - this.onExecuteStep(); - } - } - - if (this.onAfterExecute) { - this.onAfterExecute(); - } - this.errors_in_execution = false; - } catch (err) { - this.errors_in_execution = true; - if (LiteGraph.throw_errors) { - throw err; - } - if (LiteGraph.debug) { - console.log("Error during execution: " + err); - } - this.stop(); - } - } - - var now = LiteGraph.getTime(); - var elapsed = now - start; - if (elapsed == 0) { - elapsed = 1; - } - this.execution_time = 0.001 * elapsed; - this.globaltime += 0.001 * elapsed; - this.iteration += 1; - this.elapsed_time = (now - this.last_update_time) * 0.001; - this.last_update_time = now; - this.nodes_executing = []; - this.nodes_actioning = []; - this.nodes_executedAction = []; - }; - - /** - * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than - * nodes with only inputs. - * @method updateExecutionOrder - */ - LGraph.prototype.updateExecutionOrder = function() { - this._nodes_in_order = this.computeExecutionOrder(false); - this._nodes_executable = []; - for (var i = 0; i < this._nodes_in_order.length; ++i) { - if (this._nodes_in_order[i].onExecute) { - this._nodes_executable.push(this._nodes_in_order[i]); - } - } - }; - - //This is more internal, it computes the executable nodes in order and returns it - LGraph.prototype.computeExecutionOrder = function( - only_onExecute, - set_level + //execute once per frame + if ( + interval == 0 && + typeof window != "undefined" && + window.requestAnimationFrame ) { - var L = []; - var S = []; - var M = {}; - var visited_links = {}; //to avoid repeating links - var remaining_links = {}; //to a + function on_frame() { + if (that.execution_timer_id != -1) { + return; + } + window.requestAnimationFrame(on_frame); + if (that.onBeforeStep) that.onBeforeStep(); + that.runStep(1, !that.catch_errors); + if (that.onAfterStep) that.onAfterStep(); + } + this.execution_timer_id = -1; + on_frame(); + } else { + //execute every 'interval' ms + this.execution_timer_id = setInterval(function () { + //execute + if (that.onBeforeStep) that.onBeforeStep(); + that.runStep(1, !that.catch_errors); + if (that.onAfterStep) that.onAfterStep(); + }, interval); + } + }; - //search for the nodes without inputs (starting nodes) - for (var i = 0, l = this._nodes.length; i < l; ++i) { - var node = this._nodes[i]; - if (only_onExecute && !node.onExecute) { - continue; - } + /** + * Stops the execution loop of the graph + * @method stop execution + */ - M[node.id] = node; //add to pending nodes + LGraph.prototype.stop = function () { + if (this.status == LGraph.STATUS_STOPPED) { + return; + } - var num = 0; //num of input connections - if (node.inputs) { - for (var j = 0, l2 = node.inputs.length; j < l2; j++) { - if (node.inputs[j] && node.inputs[j].link != null) { - num += 1; - } - } - } + this.status = LGraph.STATUS_STOPPED; - if (num == 0) { - //is a starting node - S.push(node); - if (set_level) { - node._level = 1; - } - } //num of input links - else { - if (set_level) { - node._level = 0; - } - remaining_links[node.id] = num; - } + if (this.onStopEvent) { + this.onStopEvent(); + } + + if (this.execution_timer_id != null) { + if (this.execution_timer_id != -1) { + clearInterval(this.execution_timer_id); + } + this.execution_timer_id = null; + } + + this.sendEventToAllNodes("onStop"); + }; + + /** + * Run N steps (cycles) of the graph + * @method runStep + * @param {number} num number of steps to run, default is 1 + * @param {Boolean} do_not_catch_errors [optional] if you want to try/catch errors + * @param {number} limit max number of nodes to execute (used to execute from start to a node) + */ + + LGraph.prototype.runStep = function (num, do_not_catch_errors, limit) { + num = num || 1; + + var start = LiteGraph.getTime(); + this.globaltime = 0.001 * (start - this.starttime); + + var nodes = this._nodes_executable ? this._nodes_executable : this._nodes; + if (!nodes) { + return; + } + + limit = limit || nodes.length; + + if (do_not_catch_errors) { + //iterations + for (var i = 0; i < num; i++) { + for (var j = 0; j < limit; ++j) { + var node = nodes[j]; + if (node.mode == LiteGraph.ALWAYS && node.onExecute) { + //wrap node.onExecute(); + node.doExecute(); + } } - while (true) { - if (S.length == 0) { - break; - } - - //get an starting node - var node = S.shift(); - L.push(node); //add to ordered list - delete M[node.id]; //remove from the pending nodes - - if (!node.outputs) { - continue; - } - - //for every output - for (var i = 0; i < node.outputs.length; i++) { - var output = node.outputs[i]; - //not connected - if ( - output == null || - output.links == null || - output.links.length == 0 - ) { - continue; - } - - //for every connection - for (var j = 0; j < output.links.length; j++) { - var link_id = output.links[j]; - var link = this.links[link_id]; - if (!link) { - continue; - } - - //already visited link (ignore it) - if (visited_links[link.id]) { - continue; - } - - var target_node = this.getNodeById(link.target_id); - if (target_node == null) { - visited_links[link.id] = true; - continue; - } - - if ( - set_level && - (!target_node._level || - target_node._level <= node._level) - ) { - target_node._level = node._level + 1; - } - - visited_links[link.id] = true; //mark as visited - remaining_links[target_node.id] -= 1; //reduce the number of links remaining - if (remaining_links[target_node.id] == 0) { - S.push(target_node); - } //if no more links, then add to starters array - } - } + this.fixedtime += this.fixedtime_lapse; + if (this.onExecuteStep) { + this.onExecuteStep(); } + } - //the remaining ones (loops) - for (var i in M) { - L.push(M[i]); - } - - if (L.length != this._nodes.length && LiteGraph.debug) { - console.warn("something went wrong, nodes missing"); - } - - var l = L.length; - - //save order number in the node - for (var i = 0; i < l; ++i) { - L[i].order = i; - } - - //sort now by priority - L = L.sort(function(A, B) { - var Ap = A.constructor.priority || A.priority || 0; - var Bp = B.constructor.priority || B.priority || 0; - if (Ap == Bp) { - //if same priority, sort by order - return A.order - B.order; - } - return Ap - Bp; //sort by priority - }); - - //save order number in the node, again... - for (var i = 0; i < l; ++i) { - L[i].order = i; - } - - return L; - }; - - /** - * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. - * It doesn't include the node itself - * @method getAncestors - * @return {Array} an array with all the LGraphNodes that affect this node, in order of execution - */ - LGraph.prototype.getAncestors = function(node) { - var ancestors = []; - var pending = [node]; - var visited = {}; - - while (pending.length) { - var current = pending.shift(); - if (!current.inputs) { - continue; - } - if (!visited[current.id] && current != node) { - visited[current.id] = true; - ancestors.push(current); - } - - for (var i = 0; i < current.inputs.length; ++i) { - var input = current.getInputNode(i); - if (input && ancestors.indexOf(input) == -1) { - pending.push(input); - } - } - } - - ancestors.sort(function(a, b) { - return a.order - b.order; - }); - return ancestors; - }; - - /** - * Positions every node in a more readable manner - * @method arrange - */ - LGraph.prototype.arrange = function (margin, layout) { - margin = margin || 100; - - const nodes = this.computeExecutionOrder(false, true); - const columns = []; - for (let i = 0; i < nodes.length; ++i) { - const node = nodes[i]; - const col = node._level || 1; - if (!columns[col]) { - columns[col] = []; - } - columns[col].push(node); - } - - let x = margin; - - for (let i = 0; i < columns.length; ++i) { - const column = columns[i]; - if (!column) { - continue; - } - let max_size = 100; - let y = margin + LiteGraph.NODE_TITLE_HEIGHT; - for (let j = 0; j < column.length; ++j) { - const node = column[j]; - node.pos[0] = (layout == LiteGraph.VERTICAL_LAYOUT) ? y : x; - node.pos[1] = (layout == LiteGraph.VERTICAL_LAYOUT) ? x : y; - const max_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 1 : 0; - if (node.size[max_size_index] > max_size) { - max_size = node.size[max_size_index]; - } - const node_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 0 : 1; - y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT; - } - x += max_size + margin; - } - - this.setDirtyCanvas(true, true); - }; - - /** - * Returns the amount of time the graph has been running in milliseconds - * @method getTime - * @return {number} number of milliseconds the graph has been running - */ - LGraph.prototype.getTime = function() { - return this.globaltime; - }; - - /** - * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant - * @method getFixedTime - * @return {number} number of milliseconds the graph has been running - */ - - LGraph.prototype.getFixedTime = function() { - return this.fixedtime; - }; - - /** - * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct - * if the nodes are using graphical actions - * @method getElapsedTime - * @return {number} number of milliseconds it took the last cycle - */ - - LGraph.prototype.getElapsedTime = function() { - return this.elapsed_time; - }; - - /** - * Sends an event to all the nodes, useful to trigger stuff - * @method sendEventToAllNodes - * @param {String} eventname the name of the event (function to be called) - * @param {Array} params parameters in array format - */ - LGraph.prototype.sendEventToAllNodes = function(eventname, params, mode) { - mode = mode || LiteGraph.ALWAYS; - - var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes; - if (!nodes) { - return; - } - - for (var j = 0, l = nodes.length; j < l; ++j) { + if (this.onAfterExecute) { + this.onAfterExecute(); + } + } else { + try { + //iterations + for (var i = 0; i < num; i++) { + for (var j = 0; j < limit; ++j) { var node = nodes[j]; - - if ( - node.constructor === LiteGraph.Subgraph && - eventname != "onExecute" - ) { - if (node.mode == mode) { - node.sendEventToAllNodes(eventname, params, mode); - } - continue; + if (node.mode == LiteGraph.ALWAYS && node.onExecute) { + node.onExecute(); } + } - if (!node[eventname] || node.mode != mode) { - continue; - } - if (params === undefined) { - node[eventname](); - } else if (params && params.constructor === Array) { - node[eventname].apply(node, params); - } else { - node[eventname](params); - } - } - }; - - LGraph.prototype.sendActionToCanvas = function(action, params) { - if (!this.list_of_graphcanvas) { - return; + this.fixedtime += this.fixedtime_lapse; + if (this.onExecuteStep) { + this.onExecuteStep(); + } } - for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { - var c = this.list_of_graphcanvas[i]; - if (c[action]) { - c[action].apply(c, params); - } + if (this.onAfterExecute) { + this.onAfterExecute(); } - }; + this.errors_in_execution = false; + } catch (err) { + this.errors_in_execution = true; + if (LiteGraph.throw_errors) { + throw err; + } + if (LiteGraph.debug) { + console.log("Error during execution: " + err); + } + this.stop(); + } + } - /** - * Adds a new node instance to this graph - * @method add - * @param {LGraphNode} node the instance of the node - */ + var now = LiteGraph.getTime(); + var elapsed = now - start; + if (elapsed == 0) { + elapsed = 1; + } + this.execution_time = 0.001 * elapsed; + this.globaltime += 0.001 * elapsed; + this.iteration += 1; + this.elapsed_time = (now - this.last_update_time) * 0.001; + this.last_update_time = now; + this.nodes_executing = []; + this.nodes_actioning = []; + this.nodes_executedAction = []; + }; - LGraph.prototype.add = function(node, skip_compute_order) { - if (!node) { - return; + /** + * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than + * nodes with only inputs. + * @method updateExecutionOrder + */ + LGraph.prototype.updateExecutionOrder = function () { + this._nodes_in_order = this.computeExecutionOrder(false); + this._nodes_executable = []; + for (var i = 0; i < this._nodes_in_order.length; ++i) { + if (this._nodes_in_order[i].onExecute) { + this._nodes_executable.push(this._nodes_in_order[i]); + } + } + }; + + //This is more internal, it computes the executable nodes in order and returns it + LGraph.prototype.computeExecutionOrder = function ( + only_onExecute, + set_level + ) { + var L = []; + var S = []; + var M = {}; + var visited_links = {}; //to avoid repeating links + var remaining_links = {}; //to a + + //search for the nodes without inputs (starting nodes) + for (var i = 0, l = this._nodes.length; i < l; ++i) { + var node = this._nodes[i]; + if (only_onExecute && !node.onExecute) { + continue; + } + + M[node.id] = node; //add to pending nodes + + var num = 0; //num of input connections + if (node.inputs) { + for (var j = 0, l2 = node.inputs.length; j < l2; j++) { + if (node.inputs[j] && node.inputs[j].link != null) { + num += 1; + } + } + } + + if (num == 0) { + //is a starting node + S.push(node); + if (set_level) { + node._level = 1; + } + } //num of input links + else { + if (set_level) { + node._level = 0; + } + remaining_links[node.id] = num; + } + } + + while (true) { + if (S.length == 0) { + break; + } + + //get an starting node + var node = S.shift(); + L.push(node); //add to ordered list + delete M[node.id]; //remove from the pending nodes + + if (!node.outputs) { + continue; + } + + //for every output + for (var i = 0; i < node.outputs.length; i++) { + var output = node.outputs[i]; + //not connected + if ( + output == null || + output.links == null || + output.links.length == 0 + ) { + continue; } - //groups - if (node.constructor === LGraphGroup) { - this._groups.push(node); - this.setDirtyCanvas(true); - this.change(); - node.graph = this; - this._version++; - return; + //for every connection + for (var j = 0; j < output.links.length; j++) { + var link_id = output.links[j]; + var link = this.links[link_id]; + if (!link) { + continue; + } + + //already visited link (ignore it) + if (visited_links[link.id]) { + continue; + } + + var target_node = this.getNodeById(link.target_id); + if (target_node == null) { + visited_links[link.id] = true; + continue; + } + + if ( + set_level && + (!target_node._level || target_node._level <= node._level) + ) { + target_node._level = node._level + 1; + } + + visited_links[link.id] = true; //mark as visited + remaining_links[target_node.id] -= 1; //reduce the number of links remaining + if (remaining_links[target_node.id] == 0) { + S.push(target_node); + } //if no more links, then add to starters array } + } + } - //nodes - if (node.id != -1 && this._nodes_by_id[node.id] != null) { - console.warn( - "LiteGraph: there is already a node with this ID, changing it" - ); - if (LiteGraph.use_uuids) { - node.id = LiteGraph.uuidv4(); - } - else { - node.id = ++this.last_node_id; - } + //the remaining ones (loops) + for (var i in M) { + L.push(M[i]); + } + + if (L.length != this._nodes.length && LiteGraph.debug) { + console.warn("something went wrong, nodes missing"); + } + + var l = L.length; + + //save order number in the node + for (var i = 0; i < l; ++i) { + L[i].order = i; + } + + //sort now by priority + L = L.sort(function (A, B) { + var Ap = A.constructor.priority || A.priority || 0; + var Bp = B.constructor.priority || B.priority || 0; + if (Ap == Bp) { + //if same priority, sort by order + return A.order - B.order; + } + return Ap - Bp; //sort by priority + }); + + //save order number in the node, again... + for (var i = 0; i < l; ++i) { + L[i].order = i; + } + + return L; + }; + + /** + * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. + * It doesn't include the node itself + * @method getAncestors + * @return {Array} an array with all the LGraphNodes that affect this node, in order of execution + */ + LGraph.prototype.getAncestors = function (node) { + var ancestors = []; + var pending = [node]; + var visited = {}; + + while (pending.length) { + var current = pending.shift(); + if (!current.inputs) { + continue; + } + if (!visited[current.id] && current != node) { + visited[current.id] = true; + ancestors.push(current); + } + + for (var i = 0; i < current.inputs.length; ++i) { + var input = current.getInputNode(i); + if (input && ancestors.indexOf(input) == -1) { + pending.push(input); } + } + } - if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) { - throw "LiteGraph: max number of nodes in a graph reached"; + ancestors.sort(function (a, b) { + return a.order - b.order; + }); + return ancestors; + }; + + /** + * Positions every node in a more readable manner + * @method arrange + */ + LGraph.prototype.arrange = function (margin, layout) { + margin = margin || 100; + + const nodes = this.computeExecutionOrder(false, true); + const columns = []; + for (let i = 0; i < nodes.length; ++i) { + const node = nodes[i]; + const col = node._level || 1; + if (!columns[col]) { + columns[col] = []; + } + columns[col].push(node); + } + + let x = margin; + + for (let i = 0; i < columns.length; ++i) { + const column = columns[i]; + if (!column) { + continue; + } + let max_size = 100; + let y = margin + LiteGraph.NODE_TITLE_HEIGHT; + for (let j = 0; j < column.length; ++j) { + const node = column[j]; + node.pos[0] = layout == LiteGraph.VERTICAL_LAYOUT ? y : x; + node.pos[1] = layout == LiteGraph.VERTICAL_LAYOUT ? x : y; + const max_size_index = layout == LiteGraph.VERTICAL_LAYOUT ? 1 : 0; + if (node.size[max_size_index] > max_size) { + max_size = node.size[max_size_index]; } + const node_size_index = layout == LiteGraph.VERTICAL_LAYOUT ? 0 : 1; + y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT; + } + x += max_size + margin; + } - //give him an id - if (LiteGraph.use_uuids) { - if (node.id == null || node.id == -1) - node.id = LiteGraph.uuidv4(); + this.setDirtyCanvas(true, true); + }; + + /** + * Returns the amount of time the graph has been running in milliseconds + * @method getTime + * @return {number} number of milliseconds the graph has been running + */ + LGraph.prototype.getTime = function () { + return this.globaltime; + }; + + /** + * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant + * @method getFixedTime + * @return {number} number of milliseconds the graph has been running + */ + + LGraph.prototype.getFixedTime = function () { + return this.fixedtime; + }; + + /** + * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct + * if the nodes are using graphical actions + * @method getElapsedTime + * @return {number} number of milliseconds it took the last cycle + */ + + LGraph.prototype.getElapsedTime = function () { + return this.elapsed_time; + }; + + /** + * Sends an event to all the nodes, useful to trigger stuff + * @method sendEventToAllNodes + * @param {String} eventname the name of the event (function to be called) + * @param {Array} params parameters in array format + */ + LGraph.prototype.sendEventToAllNodes = function (eventname, params, mode) { + mode = mode || LiteGraph.ALWAYS; + + var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes; + if (!nodes) { + return; + } + + for (var j = 0, l = nodes.length; j < l; ++j) { + var node = nodes[j]; + + if (node.constructor === LiteGraph.Subgraph && eventname != "onExecute") { + if (node.mode == mode) { + node.sendEventToAllNodes(eventname, params, mode); } - else { - if (node.id == null || node.id == -1) { - node.id = ++this.last_node_id; - } else if (this.last_node_id < node.id) { - this.last_node_id = node.id; - } + continue; + } + + if (!node[eventname] || node.mode != mode) { + continue; + } + if (params === undefined) { + node[eventname](); + } else if (params && params.constructor === Array) { + node[eventname].apply(node, params); + } else { + node[eventname](params); + } + } + }; + + LGraph.prototype.sendActionToCanvas = function (action, params) { + if (!this.list_of_graphcanvas) { + return; + } + + for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { + var c = this.list_of_graphcanvas[i]; + if (c[action]) { + c[action].apply(c, params); + } + } + }; + + /** + * Adds a new node instance to this graph + * @method add + * @param {LGraphNode} node the instance of the node + */ + + LGraph.prototype.add = function (node, skip_compute_order) { + if (!node) { + return; + } + + //groups + if (node.constructor === LGraphGroup) { + this._groups.push(node); + this.setDirtyCanvas(true); + this.change(); + node.graph = this; + this._version++; + return; + } + + //nodes + if (node.id != -1 && this._nodes_by_id[node.id] != null) { + console.warn( + "LiteGraph: there is already a node with this ID, changing it" + ); + if (LiteGraph.use_uuids) { + node.id = LiteGraph.uuidv4(); + } else { + node.id = ++this.last_node_id; + } + } + + if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) { + throw "LiteGraph: max number of nodes in a graph reached"; + } + + //give him an id + if (LiteGraph.use_uuids) { + if (node.id == null || node.id == -1) node.id = LiteGraph.uuidv4(); + } else { + if (node.id == null || node.id == -1) { + node.id = ++this.last_node_id; + } else if (this.last_node_id < node.id) { + this.last_node_id = node.id; + } + } + + node.graph = this; + this._version++; + + this._nodes.push(node); + this._nodes_by_id[node.id] = node; + + if (node.onAdded) { + node.onAdded(this); + } + + if (this.config.align_to_grid) { + node.alignToGrid(); + } + + if (!skip_compute_order) { + this.updateExecutionOrder(); + } + + if (this.onNodeAdded) { + this.onNodeAdded(node); + } + + this.setDirtyCanvas(true); + this.change(); + + return node; //to chain actions + }; + + /** + * Removes a node from the graph + * @method remove + * @param {LGraphNode} node the instance of the node + */ + + LGraph.prototype.remove = function (node) { + if (node.constructor === LiteGraph.LGraphGroup) { + var index = this._groups.indexOf(node); + if (index != -1) { + this._groups.splice(index, 1); + } + node.graph = null; + this._version++; + this.setDirtyCanvas(true, true); + this.change(); + return; + } + + if (this._nodes_by_id[node.id] == null) { + return; + } //not found + + if (node.ignore_remove) { + return; + } //cannot be removed + + this.beforeChange(); //sure? - almost sure is wrong + + //disconnect inputs + if (node.inputs) { + for (var i = 0; i < node.inputs.length; i++) { + var slot = node.inputs[i]; + if (slot.link != null) { + node.disconnectInput(i); } + } + } - node.graph = this; - this._version++; - - this._nodes.push(node); - this._nodes_by_id[node.id] = node; - - if (node.onAdded) { - node.onAdded(this); + //disconnect outputs + if (node.outputs) { + for (var i = 0; i < node.outputs.length; i++) { + var slot = node.outputs[i]; + if (slot.links != null && slot.links.length) { + node.disconnectOutput(i); } + } + } - if (this.config.align_to_grid) { - node.alignToGrid(); + //node.id = -1; //why? + + //callback + if (node.onRemoved) { + node.onRemoved(); + } + + node.graph = null; + this._version++; + + //remove from canvas render + if (this.list_of_graphcanvas) { + for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { + var canvas = this.list_of_graphcanvas[i]; + if (canvas.selected_nodes[node.id]) { + delete canvas.selected_nodes[node.id]; } - - if (!skip_compute_order) { - this.updateExecutionOrder(); + if (canvas.node_dragged == node) { + canvas.node_dragged = null; } + } + } - if (this.onNodeAdded) { - this.onNodeAdded(node); - } + //remove from containers + var pos = this._nodes.indexOf(node); + if (pos != -1) { + this._nodes.splice(pos, 1); + } + delete this._nodes_by_id[node.id]; - this.setDirtyCanvas(true); - this.change(); + if (this.onNodeRemoved) { + this.onNodeRemoved(node); + } - return node; //to chain actions - }; + //close panels + this.sendActionToCanvas("checkPanels"); - /** - * Removes a node from the graph - * @method remove - * @param {LGraphNode} node the instance of the node - */ + this.setDirtyCanvas(true, true); + this.afterChange(); //sure? - almost sure is wrong + this.change(); - LGraph.prototype.remove = function(node) { - if (node.constructor === LiteGraph.LGraphGroup) { - var index = this._groups.indexOf(node); - if (index != -1) { - this._groups.splice(index, 1); - } - node.graph = null; - this._version++; - this.setDirtyCanvas(true, true); - this.change(); - return; - } + this.updateExecutionOrder(); + }; - if (this._nodes_by_id[node.id] == null) { - return; - } //not found + /** + * Returns a node by its id. + * @method getNodeById + * @param {Number} id + */ - if (node.ignore_remove) { - return; - } //cannot be removed + LGraph.prototype.getNodeById = function (id) { + if (id == null) { + return null; + } + return this._nodes_by_id[id]; + }; - this.beforeChange(); //sure? - almost sure is wrong + /** + * Returns a list of nodes that matches a class + * @method findNodesByClass + * @param {Class} classObject the class itself (not an string) + * @return {Array} a list with all the nodes of this type + */ + LGraph.prototype.findNodesByClass = function (classObject, result) { + result = result || []; + result.length = 0; + for (var i = 0, l = this._nodes.length; i < l; ++i) { + if (this._nodes[i].constructor === classObject) { + result.push(this._nodes[i]); + } + } + return result; + }; - //disconnect inputs - if (node.inputs) { - for (var i = 0; i < node.inputs.length; i++) { - var slot = node.inputs[i]; - if (slot.link != null) { - node.disconnectInput(i); - } - } - } + /** + * Returns a list of nodes that matches a type + * @method findNodesByType + * @param {String} type the name of the node type + * @return {Array} a list with all the nodes of this type + */ + LGraph.prototype.findNodesByType = function (type, result) { + var type = type.toLowerCase(); + result = result || []; + result.length = 0; + for (var i = 0, l = this._nodes.length; i < l; ++i) { + if (this._nodes[i].type.toLowerCase() == type) { + result.push(this._nodes[i]); + } + } + return result; + }; - //disconnect outputs - if (node.outputs) { - for (var i = 0; i < node.outputs.length; i++) { - var slot = node.outputs[i]; - if (slot.links != null && slot.links.length) { - node.disconnectOutput(i); - } - } - } + /** + * Returns the first node that matches a name in its title + * @method findNodeByTitle + * @param {String} name the name of the node to search + * @return {Node} the node or null + */ + LGraph.prototype.findNodeByTitle = function (title) { + for (var i = 0, l = this._nodes.length; i < l; ++i) { + if (this._nodes[i].title == title) { + return this._nodes[i]; + } + } + return null; + }; - //node.id = -1; //why? + /** + * Returns a list of nodes that matches a name + * @method findNodesByTitle + * @param {String} name the name of the node to search + * @return {Array} a list with all the nodes with this name + */ + LGraph.prototype.findNodesByTitle = function (title) { + var result = []; + for (var i = 0, l = this._nodes.length; i < l; ++i) { + if (this._nodes[i].title == title) { + result.push(this._nodes[i]); + } + } + return result; + }; - //callback - if (node.onRemoved) { - node.onRemoved(); - } - - node.graph = null; - this._version++; - - //remove from canvas render - if (this.list_of_graphcanvas) { - for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { - var canvas = this.list_of_graphcanvas[i]; - if (canvas.selected_nodes[node.id]) { - delete canvas.selected_nodes[node.id]; - } - if (canvas.node_dragged == node) { - canvas.node_dragged = null; - } - } - } - - //remove from containers - var pos = this._nodes.indexOf(node); - if (pos != -1) { - this._nodes.splice(pos, 1); - } - delete this._nodes_by_id[node.id]; - - if (this.onNodeRemoved) { - this.onNodeRemoved(node); - } - - //close panels - this.sendActionToCanvas("checkPanels"); - - this.setDirtyCanvas(true, true); - this.afterChange(); //sure? - almost sure is wrong - this.change(); - - this.updateExecutionOrder(); - }; - - /** - * Returns a node by its id. - * @method getNodeById - * @param {Number} id - */ - - LGraph.prototype.getNodeById = function(id) { - if (id == null) { - return null; - } - return this._nodes_by_id[id]; - }; - - /** - * Returns a list of nodes that matches a class - * @method findNodesByClass - * @param {Class} classObject the class itself (not an string) - * @return {Array} a list with all the nodes of this type - */ - LGraph.prototype.findNodesByClass = function(classObject, result) { - result = result || []; - result.length = 0; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].constructor === classObject) { - result.push(this._nodes[i]); - } - } - return result; - }; - - /** - * Returns a list of nodes that matches a type - * @method findNodesByType - * @param {String} type the name of the node type - * @return {Array} a list with all the nodes of this type - */ - LGraph.prototype.findNodesByType = function(type, result) { - var type = type.toLowerCase(); - result = result || []; - result.length = 0; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].type.toLowerCase() == type) { - result.push(this._nodes[i]); - } - } - return result; - }; - - /** - * Returns the first node that matches a name in its title - * @method findNodeByTitle - * @param {String} name the name of the node to search - * @return {Node} the node or null - */ - LGraph.prototype.findNodeByTitle = function(title) { - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].title == title) { - return this._nodes[i]; - } - } - return null; - }; - - /** - * Returns a list of nodes that matches a name - * @method findNodesByTitle - * @param {String} name the name of the node to search - * @return {Array} a list with all the nodes with this name - */ - LGraph.prototype.findNodesByTitle = function(title) { - var result = []; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].title == title) { - result.push(this._nodes[i]); - } - } - return result; - }; - - /** - * Returns the top-most node in this position of the canvas - * @method getNodeOnPos - * @param {number} x the x coordinate in canvas space - * @param {number} y the y coordinate in canvas space - * @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph - * @return {LGraphNode} the node at this position or null - */ - LGraph.prototype.getNodeOnPos = function(x, y, nodes_list, margin) { - nodes_list = nodes_list || this._nodes; - var nRet = null; - for (var i = nodes_list.length - 1; i >= 0; i--) { - var n = nodes_list[i]; - var skip_title = n.constructor.title_mode == LiteGraph.NO_TITLE; - if (n.isPointInside(x, y, margin, skip_title)) { - // check for lesser interest nodes (TODO check for overlapping, use the top) - /*if (typeof n == "LGraphGroup"){ + /** + * Returns the top-most node in this position of the canvas + * @method getNodeOnPos + * @param {number} x the x coordinate in canvas space + * @param {number} y the y coordinate in canvas space + * @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph + * @return {LGraphNode} the node at this position or null + */ + LGraph.prototype.getNodeOnPos = function (x, y, nodes_list, margin) { + nodes_list = nodes_list || this._nodes; + var nRet = null; + for (var i = nodes_list.length - 1; i >= 0; i--) { + var n = nodes_list[i]; + var skip_title = n.constructor.title_mode == LiteGraph.NO_TITLE; + if (n.isPointInside(x, y, margin, skip_title)) { + // check for lesser interest nodes (TODO check for overlapping, use the top) + /*if (typeof n == "LGraphGroup"){ nRet = n; }else{*/ - return n; - /*}*/ - } - } - return nRet; - }; - - /** - * Returns the top-most group in that position - * @method getGroupOnPos - * @param {number} x the x coordinate in canvas space - * @param {number} y the y coordinate in canvas space - * @return {LGraphGroup} the group or null - */ - LGraph.prototype.getGroupOnPos = function(x, y) { - for (var i = this._groups.length - 1; i >= 0; i--) { - var g = this._groups[i]; - if (g.isPointInside(x, y, 2, true)) { - return g; - } - } - return null; - }; - - /** - * Checks that the node type matches the node type registered, used when replacing a nodetype by a newer version during execution - * this replaces the ones using the old version with the new version - * @method checkNodeTypes - */ - LGraph.prototype.checkNodeTypes = function() { - var changes = false; - for (var i = 0; i < this._nodes.length; i++) { - var node = this._nodes[i]; - var ctor = LiteGraph.registered_node_types[node.type]; - if (node.constructor == ctor) { - continue; - } - console.log("node being replaced by newer version: " + node.type); - var newnode = LiteGraph.createNode(node.type); - changes = true; - this._nodes[i] = newnode; - newnode.configure(node.serialize()); - newnode.graph = this; - this._nodes_by_id[newnode.id] = newnode; - if (node.inputs) { - newnode.inputs = node.inputs.concat(); - } - if (node.outputs) { - newnode.outputs = node.outputs.concat(); - } - } - this.updateExecutionOrder(); - }; - - // ********** GLOBALS ***************** - - LGraph.prototype.onAction = function(action, param, options) { - this._input_nodes = this.findNodesByClass( - LiteGraph.GraphInput, - this._input_nodes - ); - for (var i = 0; i < this._input_nodes.length; ++i) { - var node = this._input_nodes[i]; - if (node.properties.name != action) { - continue; - } - //wrap node.onAction(action, param); - node.actionDo(action, param, options); - break; - } - }; - - LGraph.prototype.trigger = function(action, param) { - if (this.onTrigger) { - this.onTrigger(action, param); - } - }; - - /** - * Tell this graph it has a global graph input of this type - * @method addGlobalInput - * @param {String} name - * @param {String} type - * @param {*} value [optional] - */ - LGraph.prototype.addInput = function(name, type, value) { - var input = this.inputs[name]; - if (input) { - //already exist - return; - } - - this.beforeChange(); - this.inputs[name] = { name: name, type: type, value: value }; - this._version++; - this.afterChange(); - - if (this.onInputAdded) { - this.onInputAdded(name, type); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Assign a data to the global graph input - * @method setGlobalInputData - * @param {String} name - * @param {*} data - */ - LGraph.prototype.setInputData = function(name, data) { - var input = this.inputs[name]; - if (!input) { - return; - } - input.value = data; - }; - - /** - * Returns the current value of a global graph input - * @method getInputData - * @param {String} name - * @return {*} the data - */ - LGraph.prototype.getInputData = function(name) { - var input = this.inputs[name]; - if (!input) { - return null; - } - return input.value; - }; - - /** - * Changes the name of a global graph input - * @method renameInput - * @param {String} old_name - * @param {String} new_name - */ - LGraph.prototype.renameInput = function(old_name, name) { - if (name == old_name) { - return; - } - - if (!this.inputs[old_name]) { - return false; - } - - if (this.inputs[name]) { - console.error("there is already one input with that name"); - return false; - } - - this.inputs[name] = this.inputs[old_name]; - delete this.inputs[old_name]; - this._version++; - - if (this.onInputRenamed) { - this.onInputRenamed(old_name, name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Changes the type of a global graph input - * @method changeInputType - * @param {String} name - * @param {String} type - */ - LGraph.prototype.changeInputType = function(name, type) { - if (!this.inputs[name]) { - return false; - } - - if ( - this.inputs[name].type && - String(this.inputs[name].type).toLowerCase() == - String(type).toLowerCase() - ) { - return; - } - - this.inputs[name].type = type; - this._version++; - if (this.onInputTypeChanged) { - this.onInputTypeChanged(name, type); - } - }; - - /** - * Removes a global graph input - * @method removeInput - * @param {String} name - * @param {String} type - */ - LGraph.prototype.removeInput = function(name) { - if (!this.inputs[name]) { - return false; - } - - delete this.inputs[name]; - this._version++; - - if (this.onInputRemoved) { - this.onInputRemoved(name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - return true; - }; - - /** - * Creates a global graph output - * @method addOutput - * @param {String} name - * @param {String} type - * @param {*} value - */ - LGraph.prototype.addOutput = function(name, type, value) { - this.outputs[name] = { name: name, type: type, value: value }; - this._version++; - - if (this.onOutputAdded) { - this.onOutputAdded(name, type); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Assign a data to the global output - * @method setOutputData - * @param {String} name - * @param {String} value - */ - LGraph.prototype.setOutputData = function(name, value) { - var output = this.outputs[name]; - if (!output) { - return; - } - output.value = value; - }; - - /** - * Returns the current value of a global graph output - * @method getOutputData - * @param {String} name - * @return {*} the data - */ - LGraph.prototype.getOutputData = function(name) { - var output = this.outputs[name]; - if (!output) { - return null; - } - return output.value; - }; - - /** - * Renames a global graph output - * @method renameOutput - * @param {String} old_name - * @param {String} new_name - */ - LGraph.prototype.renameOutput = function(old_name, name) { - if (!this.outputs[old_name]) { - return false; - } - - if (this.outputs[name]) { - console.error("there is already one output with that name"); - return false; - } - - this.outputs[name] = this.outputs[old_name]; - delete this.outputs[old_name]; - this._version++; - - if (this.onOutputRenamed) { - this.onOutputRenamed(old_name, name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Changes the type of a global graph output - * @method changeOutputType - * @param {String} name - * @param {String} type - */ - LGraph.prototype.changeOutputType = function(name, type) { - if (!this.outputs[name]) { - return false; - } - - if ( - this.outputs[name].type && - String(this.outputs[name].type).toLowerCase() == - String(type).toLowerCase() - ) { - return; - } - - this.outputs[name].type = type; - this._version++; - if (this.onOutputTypeChanged) { - this.onOutputTypeChanged(name, type); - } - }; - - /** - * Removes a global graph output - * @method removeOutput - * @param {String} name - */ - LGraph.prototype.removeOutput = function(name) { - if (!this.outputs[name]) { - return false; - } - delete this.outputs[name]; - this._version++; - - if (this.onOutputRemoved) { - this.onOutputRemoved(name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - return true; - }; - - LGraph.prototype.triggerInput = function(name, value) { - var nodes = this.findNodesByTitle(name); - for (var i = 0; i < nodes.length; ++i) { - nodes[i].onTrigger(value); - } - }; - - LGraph.prototype.setCallback = function(name, func) { - var nodes = this.findNodesByTitle(name); - for (var i = 0; i < nodes.length; ++i) { - nodes[i].setTrigger(func); - } - }; - - //used for undo, called before any change is made to the graph - LGraph.prototype.beforeChange = function(info) { - if (this.onBeforeChange) { - this.onBeforeChange(this,info); - } - this.sendActionToCanvas("onBeforeChange", this); - }; - - //used to resend actions, called after any change is made to the graph - LGraph.prototype.afterChange = function(info) { - if (this.onAfterChange) { - this.onAfterChange(this,info); - } - this.sendActionToCanvas("onAfterChange", this); - }; - - LGraph.prototype.connectionChange = function(node, link_info) { - this.updateExecutionOrder(); - if (this.onConnectionChange) { - this.onConnectionChange(node); - } - this._version++; - this.sendActionToCanvas("onConnectionChange"); - }; - - /** - * returns if the graph is in live mode - * @method isLive - */ - - LGraph.prototype.isLive = function() { - if (!this.list_of_graphcanvas) { - return false; - } - - for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { - var c = this.list_of_graphcanvas[i]; - if (c.live_mode) { - return true; - } - } - return false; - }; - - /** - * clears the triggered slot animation in all links (stop visual animation) - * @method clearTriggeredSlots - */ - LGraph.prototype.clearTriggeredSlots = function() { - for (var i in this.links) { - var link_info = this.links[i]; - if (!link_info) { - continue; - } - if (link_info._last_time) { - link_info._last_time = 0; - } - } - }; - - /* Called when something visually changed (not the graph!) */ - LGraph.prototype.change = function() { - if (LiteGraph.debug) { - console.log("Graph changed"); - } - this.sendActionToCanvas("setDirty", [true, true]); - if (this.on_change) { - this.on_change(this); - } - }; - - LGraph.prototype.setDirtyCanvas = function(fg, bg) { - this.sendActionToCanvas("setDirty", [fg, bg]); - }; - - /** - * Destroys a link - * @method removeLink - * @param {Number} link_id - */ - LGraph.prototype.removeLink = function(link_id) { - var link = this.links[link_id]; - if (!link) { - return; - } - var node = this.getNodeById(link.target_id); - if (node) { - node.disconnectInput(link.target_slot); - } - }; - - //save and recover app state *************************************** - /** - * Creates a Object containing all the info about this graph, it can be serialized - * @method serialize - * @return {Object} value of the node - */ - LGraph.prototype.serialize = function() { - var nodes_info = []; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - nodes_info.push(this._nodes[i].serialize()); - } - - //pack link info into a non-verbose format - var links = []; - for (var i in this.links) { - //links is an OBJECT - var link = this.links[i]; - if (!link.serialize) { - //weird bug I havent solved yet - console.warn( - "weird LLink bug, link info is not a LLink but a regular object" - ); - var link2 = new LLink(); - for (var j in link) { - link2[j] = link[j]; - } - this.links[i] = link2; - link = link2; - } - - links.push(link.serialize()); - } - - var groups_info = []; - for (var i = 0; i < this._groups.length; ++i) { - groups_info.push(this._groups[i].serialize()); - } - - var data = { - last_node_id: this.last_node_id, - last_link_id: this.last_link_id, - nodes: nodes_info, - links: links, - groups: groups_info, - config: this.config, - extra: this.extra, - version: LiteGraph.VERSION - }; - - if(this.onSerialize) - this.onSerialize(data); - - return data; - }; - - /** - * Configure a graph from a JSON string - * @method configure - * @param {String} str configure a graph from a JSON string - * @param {Boolean} returns if there was any error parsing - */ - LGraph.prototype.configure = function(data, keep_old) { - if (!data) { - return; - } - - if (!keep_old) { - this.clear(); - } - - var nodes = data.nodes; - - //decode links info (they are very verbose) - if (data.links && data.links.constructor === Array) { - var links = []; - for (var i = 0; i < data.links.length; ++i) { - var link_data = data.links[i]; - if(!link_data) //weird bug - { - console.warn("serialized graph link data contains errors, skipping."); - continue; - } - var link = new LLink(); - link.configure(link_data); - links[link.id] = link; - } - data.links = links; - } - - //copy all stored fields - for (var i in data) { - if(i == "nodes" || i == "groups" ) //links must be accepted - continue; - this[i] = data[i]; - } - - var error = false; - - //create nodes - this._nodes = []; - if (nodes) { - for (var i = 0, l = nodes.length; i < l; ++i) { - var n_info = nodes[i]; //stored info - var node = LiteGraph.createNode(n_info.type, n_info.title); - if (!node) { - if (LiteGraph.debug) { - console.log( - "Node not found or has errors: " + n_info.type - ); - } - - //in case of error we create a replacement node to avoid losing info - node = new LGraphNode(); - node.last_serialization = n_info; - node.has_errors = true; - error = true; - //continue; - } - - node.id = n_info.id; //id it or it will create a new id - this.add(node, true); //add before configure, otherwise configure cannot create links - } - - //configure nodes afterwards so they can reach each other - for (var i = 0, l = nodes.length; i < l; ++i) { - var n_info = nodes[i]; - var node = this.getNodeById(n_info.id); - if (node) { - node.configure(n_info); - } - } - } - - //groups - this._groups.length = 0; - if (data.groups) { - for (var i = 0; i < data.groups.length; ++i) { - var group = new LiteGraph.LGraphGroup(); - group.configure(data.groups[i]); - this.add(group); - } - } - - this.updateExecutionOrder(); - - this.extra = data.extra || {}; - - if(this.onConfigure) - this.onConfigure(data); - - this._version++; - this.setDirtyCanvas(true, true); - return error; - }; - - LGraph.prototype.load = function(url, callback) { - var that = this; - - //from file - if(url.constructor === File || url.constructor === Blob) - { - var reader = new FileReader(); - reader.addEventListener('load', function(event) { - var data = JSON.parse(event.target.result); - that.configure(data); - if(callback) - callback(); - }); - - reader.readAsText(url); - return; - } - - //is a string, then an URL - var req = new XMLHttpRequest(); - req.open("GET", url, true); - req.send(null); - req.onload = function(oEvent) { - if (req.status !== 200) { - console.error("Error loading graph:", req.status, req.response); - return; - } - var data = JSON.parse( req.response ); - that.configure(data); - if(callback) - callback(); - }; - req.onerror = function(err) { - console.error("Error loading graph:", err); - }; - }; - - LGraph.prototype.onNodeTrace = function(node, msg, color) { - //TODO - }; - - //this is the class in charge of storing link information - function LLink(id, type, origin_id, origin_slot, target_id, target_slot) { - this.id = id; - this.type = type; - this.origin_id = origin_id; - this.origin_slot = origin_slot; - this.target_id = target_id; - this.target_slot = target_slot; - - this._data = null; - this._pos = new Float32Array(2); //center + return n; + /*}*/ + } + } + return nRet; + }; + + /** + * Returns the top-most group in that position + * @method getGroupOnPos + * @param {number} x the x coordinate in canvas space + * @param {number} y the y coordinate in canvas space + * @return {LGraphGroup} the group or null + */ + LGraph.prototype.getGroupOnPos = function (x, y) { + for (var i = this._groups.length - 1; i >= 0; i--) { + var g = this._groups[i]; + if (g.isPointInside(x, y, 2, true)) { + return g; + } + } + return null; + }; + + /** + * Checks that the node type matches the node type registered, used when replacing a nodetype by a newer version during execution + * this replaces the ones using the old version with the new version + * @method checkNodeTypes + */ + LGraph.prototype.checkNodeTypes = function () { + var changes = false; + for (var i = 0; i < this._nodes.length; i++) { + var node = this._nodes[i]; + var ctor = LiteGraph.registered_node_types[node.type]; + if (node.constructor == ctor) { + continue; + } + console.log("node being replaced by newer version: " + node.type); + var newnode = LiteGraph.createNode(node.type); + changes = true; + this._nodes[i] = newnode; + newnode.configure(node.serialize()); + newnode.graph = this; + this._nodes_by_id[newnode.id] = newnode; + if (node.inputs) { + newnode.inputs = node.inputs.concat(); + } + if (node.outputs) { + newnode.outputs = node.outputs.concat(); + } + } + this.updateExecutionOrder(); + }; + + // ********** GLOBALS ***************** + + LGraph.prototype.onAction = function (action, param, options) { + this._input_nodes = this.findNodesByClass( + LiteGraph.GraphInput, + this._input_nodes + ); + for (var i = 0; i < this._input_nodes.length; ++i) { + var node = this._input_nodes[i]; + if (node.properties.name != action) { + continue; + } + //wrap node.onAction(action, param); + node.actionDo(action, param, options); + break; + } + }; + + LGraph.prototype.trigger = function (action, param) { + if (this.onTrigger) { + this.onTrigger(action, param); + } + }; + + /** + * Tell this graph it has a global graph input of this type + * @method addGlobalInput + * @param {String} name + * @param {String} type + * @param {*} value [optional] + */ + LGraph.prototype.addInput = function (name, type, value) { + var input = this.inputs[name]; + if (input) { + //already exist + return; } - LLink.prototype.configure = function(o) { - if (o.constructor === Array) { - this.id = o[0]; - this.origin_id = o[1]; - this.origin_slot = o[2]; - this.target_id = o[3]; - this.target_slot = o[4]; - this.type = o[5]; - } else { - this.id = o.id; - this.type = o.type; - this.origin_id = o.origin_id; - this.origin_slot = o.origin_slot; - this.target_id = o.target_id; - this.target_slot = o.target_slot; + this.beforeChange(); + this.inputs[name] = { name: name, type: type, value: value }; + this._version++; + this.afterChange(); + + if (this.onInputAdded) { + this.onInputAdded(name, type); + } + + if (this.onInputsOutputsChange) { + this.onInputsOutputsChange(); + } + }; + + /** + * Assign a data to the global graph input + * @method setGlobalInputData + * @param {String} name + * @param {*} data + */ + LGraph.prototype.setInputData = function (name, data) { + var input = this.inputs[name]; + if (!input) { + return; + } + input.value = data; + }; + + /** + * Returns the current value of a global graph input + * @method getInputData + * @param {String} name + * @return {*} the data + */ + LGraph.prototype.getInputData = function (name) { + var input = this.inputs[name]; + if (!input) { + return null; + } + return input.value; + }; + + /** + * Changes the name of a global graph input + * @method renameInput + * @param {String} old_name + * @param {String} new_name + */ + LGraph.prototype.renameInput = function (old_name, name) { + if (name == old_name) { + return; + } + + if (!this.inputs[old_name]) { + return false; + } + + if (this.inputs[name]) { + console.error("there is already one input with that name"); + return false; + } + + this.inputs[name] = this.inputs[old_name]; + delete this.inputs[old_name]; + this._version++; + + if (this.onInputRenamed) { + this.onInputRenamed(old_name, name); + } + + if (this.onInputsOutputsChange) { + this.onInputsOutputsChange(); + } + }; + + /** + * Changes the type of a global graph input + * @method changeInputType + * @param {String} name + * @param {String} type + */ + LGraph.prototype.changeInputType = function (name, type) { + if (!this.inputs[name]) { + return false; + } + + if ( + this.inputs[name].type && + String(this.inputs[name].type).toLowerCase() == String(type).toLowerCase() + ) { + return; + } + + this.inputs[name].type = type; + this._version++; + if (this.onInputTypeChanged) { + this.onInputTypeChanged(name, type); + } + }; + + /** + * Removes a global graph input + * @method removeInput + * @param {String} name + * @param {String} type + */ + LGraph.prototype.removeInput = function (name) { + if (!this.inputs[name]) { + return false; + } + + delete this.inputs[name]; + this._version++; + + if (this.onInputRemoved) { + this.onInputRemoved(name); + } + + if (this.onInputsOutputsChange) { + this.onInputsOutputsChange(); + } + return true; + }; + + /** + * Creates a global graph output + * @method addOutput + * @param {String} name + * @param {String} type + * @param {*} value + */ + LGraph.prototype.addOutput = function (name, type, value) { + this.outputs[name] = { name: name, type: type, value: value }; + this._version++; + + if (this.onOutputAdded) { + this.onOutputAdded(name, type); + } + + if (this.onInputsOutputsChange) { + this.onInputsOutputsChange(); + } + }; + + /** + * Assign a data to the global output + * @method setOutputData + * @param {String} name + * @param {String} value + */ + LGraph.prototype.setOutputData = function (name, value) { + var output = this.outputs[name]; + if (!output) { + return; + } + output.value = value; + }; + + /** + * Returns the current value of a global graph output + * @method getOutputData + * @param {String} name + * @return {*} the data + */ + LGraph.prototype.getOutputData = function (name) { + var output = this.outputs[name]; + if (!output) { + return null; + } + return output.value; + }; + + /** + * Renames a global graph output + * @method renameOutput + * @param {String} old_name + * @param {String} new_name + */ + LGraph.prototype.renameOutput = function (old_name, name) { + if (!this.outputs[old_name]) { + return false; + } + + if (this.outputs[name]) { + console.error("there is already one output with that name"); + return false; + } + + this.outputs[name] = this.outputs[old_name]; + delete this.outputs[old_name]; + this._version++; + + if (this.onOutputRenamed) { + this.onOutputRenamed(old_name, name); + } + + if (this.onInputsOutputsChange) { + this.onInputsOutputsChange(); + } + }; + + /** + * Changes the type of a global graph output + * @method changeOutputType + * @param {String} name + * @param {String} type + */ + LGraph.prototype.changeOutputType = function (name, type) { + if (!this.outputs[name]) { + return false; + } + + if ( + this.outputs[name].type && + String(this.outputs[name].type).toLowerCase() == + String(type).toLowerCase() + ) { + return; + } + + this.outputs[name].type = type; + this._version++; + if (this.onOutputTypeChanged) { + this.onOutputTypeChanged(name, type); + } + }; + + /** + * Removes a global graph output + * @method removeOutput + * @param {String} name + */ + LGraph.prototype.removeOutput = function (name) { + if (!this.outputs[name]) { + return false; + } + delete this.outputs[name]; + this._version++; + + if (this.onOutputRemoved) { + this.onOutputRemoved(name); + } + + if (this.onInputsOutputsChange) { + this.onInputsOutputsChange(); + } + return true; + }; + + LGraph.prototype.triggerInput = function (name, value) { + var nodes = this.findNodesByTitle(name); + for (var i = 0; i < nodes.length; ++i) { + nodes[i].onTrigger(value); + } + }; + + LGraph.prototype.setCallback = function (name, func) { + var nodes = this.findNodesByTitle(name); + for (var i = 0; i < nodes.length; ++i) { + nodes[i].setTrigger(func); + } + }; + + //used for undo, called before any change is made to the graph + LGraph.prototype.beforeChange = function (info) { + if (this.onBeforeChange) { + this.onBeforeChange(this, info); + } + this.sendActionToCanvas("onBeforeChange", this); + }; + + //used to resend actions, called after any change is made to the graph + LGraph.prototype.afterChange = function (info) { + if (this.onAfterChange) { + this.onAfterChange(this, info); + } + this.sendActionToCanvas("onAfterChange", this); + }; + + LGraph.prototype.connectionChange = function (node, link_info) { + this.updateExecutionOrder(); + if (this.onConnectionChange) { + this.onConnectionChange(node); + } + this._version++; + this.sendActionToCanvas("onConnectionChange"); + }; + + /** + * returns if the graph is in live mode + * @method isLive + */ + + LGraph.prototype.isLive = function () { + if (!this.list_of_graphcanvas) { + return false; + } + + for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { + var c = this.list_of_graphcanvas[i]; + if (c.live_mode) { + return true; + } + } + return false; + }; + + /** + * clears the triggered slot animation in all links (stop visual animation) + * @method clearTriggeredSlots + */ + LGraph.prototype.clearTriggeredSlots = function () { + for (var i in this.links) { + var link_info = this.links[i]; + if (!link_info) { + continue; + } + if (link_info._last_time) { + link_info._last_time = 0; + } + } + }; + + /* Called when something visually changed (not the graph!) */ + LGraph.prototype.change = function () { + if (LiteGraph.debug) { + console.log("Graph changed"); + } + this.sendActionToCanvas("setDirty", [true, true]); + if (this.on_change) { + this.on_change(this); + } + }; + + LGraph.prototype.setDirtyCanvas = function (fg, bg) { + this.sendActionToCanvas("setDirty", [fg, bg]); + }; + + /** + * Destroys a link + * @method removeLink + * @param {Number} link_id + */ + LGraph.prototype.removeLink = function (link_id) { + var link = this.links[link_id]; + if (!link) { + return; + } + var node = this.getNodeById(link.target_id); + if (node) { + node.disconnectInput(link.target_slot); + } + }; + + //save and recover app state *************************************** + /** + * Creates a Object containing all the info about this graph, it can be serialized + * @method serialize + * @return {Object} value of the node + */ + LGraph.prototype.serialize = function () { + var nodes_info = []; + for (var i = 0, l = this._nodes.length; i < l; ++i) { + nodes_info.push(this._nodes[i].serialize()); + } + + //pack link info into a non-verbose format + var links = []; + for (var i in this.links) { + //links is an OBJECT + var link = this.links[i]; + if (!link.serialize) { + //weird bug I havent solved yet + console.warn( + "weird LLink bug, link info is not a LLink but a regular object" + ); + var link2 = new LLink(); + for (var j in link) { + link2[j] = link[j]; } + this.links[i] = link2; + link = link2; + } + + links.push(link.serialize()); + } + + var groups_info = []; + for (var i = 0; i < this._groups.length; ++i) { + groups_info.push(this._groups[i].serialize()); + } + + var data = { + last_node_id: this.last_node_id, + last_link_id: this.last_link_id, + nodes: nodes_info, + links: links, + groups: groups_info, + config: this.config, + extra: this.extra, + version: LiteGraph.VERSION, }; - LLink.prototype.serialize = function() { - return [ - this.id, - this.origin_id, - this.origin_slot, - this.target_id, - this.target_slot, - this.type - ]; + if (this.onSerialize) this.onSerialize(data); + + return data; + }; + + /** + * Configure a graph from a JSON string + * @method configure + * @param {String} str configure a graph from a JSON string + * @param {Boolean} returns if there was any error parsing + */ + LGraph.prototype.configure = function (data, keep_old) { + if (!data) { + return; + } + + if (!keep_old) { + this.clear(); + } + + var nodes = data.nodes; + + //decode links info (they are very verbose) + if (data.links && data.links.constructor === Array) { + var links = []; + for (var i = 0; i < data.links.length; ++i) { + var link_data = data.links[i]; + if (!link_data) { + //weird bug + console.warn("serialized graph link data contains errors, skipping."); + continue; + } + var link = new LLink(); + link.configure(link_data); + links[link.id] = link; + } + data.links = links; + } + + //copy all stored fields + for (var i in data) { + if (i == "nodes" || i == "groups") + //links must be accepted + continue; + this[i] = data[i]; + } + + var error = false; + + //create nodes + this._nodes = []; + if (nodes) { + for (var i = 0, l = nodes.length; i < l; ++i) { + var n_info = nodes[i]; //stored info + var node = LiteGraph.createNode(n_info.type, n_info.title); + if (!node) { + if (LiteGraph.debug) { + console.log("Node not found or has errors: " + n_info.type); + } + + //in case of error we create a replacement node to avoid losing info + node = new LGraphNode(); + node.last_serialization = n_info; + node.has_errors = true; + error = true; + //continue; + } + + node.id = n_info.id; //id it or it will create a new id + this.add(node, true); //add before configure, otherwise configure cannot create links + } + + //configure nodes afterwards so they can reach each other + for (var i = 0, l = nodes.length; i < l; ++i) { + var n_info = nodes[i]; + var node = this.getNodeById(n_info.id); + if (node) { + node.configure(n_info); + } + } + } + + //groups + this._groups.length = 0; + if (data.groups) { + for (var i = 0; i < data.groups.length; ++i) { + var group = new LiteGraph.LGraphGroup(); + group.configure(data.groups[i]); + this.add(group); + } + } + + this.updateExecutionOrder(); + + this.extra = data.extra || {}; + + if (this.onConfigure) this.onConfigure(data); + + this._version++; + this.setDirtyCanvas(true, true); + return error; + }; + + LGraph.prototype.load = function (url, callback) { + var that = this; + + //from file + if (url.constructor === File || url.constructor === Blob) { + var reader = new FileReader(); + reader.addEventListener("load", function (event) { + var data = JSON.parse(event.target.result); + that.configure(data); + if (callback) callback(); + }); + + reader.readAsText(url); + return; + } + + //is a string, then an URL + var req = new XMLHttpRequest(); + req.open("GET", url, true); + req.send(null); + req.onload = function (oEvent) { + if (req.status !== 200) { + console.error("Error loading graph:", req.status, req.response); + return; + } + var data = JSON.parse(req.response); + that.configure(data); + if (callback) callback(); }; + req.onerror = function (err) { + console.error("Error loading graph:", err); + }; + }; - LiteGraph.LLink = LLink; + LGraph.prototype.onNodeTrace = function (node, msg, color) { + //TODO + }; - // ************************************************************* - // Node CLASS ******* - // ************************************************************* + //this is the class in charge of storing link information + function LLink(id, type, origin_id, origin_slot, target_id, target_slot) { + this.id = id; + this.type = type; + this.origin_id = origin_id; + this.origin_slot = origin_slot; + this.target_id = target_id; + this.target_slot = target_slot; - /* + this._data = null; + this._pos = new Float32Array(2); //center + } + + LLink.prototype.configure = function (o) { + if (o.constructor === Array) { + this.id = o[0]; + this.origin_id = o[1]; + this.origin_slot = o[2]; + this.target_id = o[3]; + this.target_slot = o[4]; + this.type = o[5]; + } else { + this.id = o.id; + this.type = o.type; + this.origin_id = o.origin_id; + this.origin_slot = o.origin_slot; + this.target_id = o.target_id; + this.target_slot = o.target_slot; + } + }; + + LLink.prototype.serialize = function () { + return [ + this.id, + this.origin_id, + this.origin_slot, + this.target_id, + this.target_slot, + this.type, + ]; + }; + + LiteGraph.LLink = LLink; + + // ************************************************************* + // Node CLASS ******* + // ************************************************************* + + /* title: string pos: [x,y] size: [x,y] @@ -2404,2410 +2367,2482 @@ + getExtraMenuOptions: to add option to context menu */ - /** - * Base Class for all the node type classes - * @class LGraphNode - * @param {String} name a name for the node - */ + /** + * Base Class for all the node type classes + * @class LGraphNode + * @param {String} name a name for the node + */ - function LGraphNode(title) { - this._ctor(title); + function LGraphNode(title) { + this._ctor(title); + } + + global.LGraphNode = LiteGraph.LGraphNode = LGraphNode; + + LGraphNode.prototype._ctor = function (title) { + this.title = title || "Unnamed"; + this.size = [LiteGraph.NODE_WIDTH, 60]; + this.graph = null; + + this._pos = new Float32Array(10, 10); + + Object.defineProperty(this, "pos", { + set: function (v) { + if (!v || v.length < 2) { + return; + } + this._pos[0] = v[0]; + this._pos[1] = v[1]; + }, + get: function () { + return this._pos; + }, + enumerable: true, + }); + + if (LiteGraph.use_uuids) { + this.id = LiteGraph.uuidv4(); + } else { + this.id = -1; //not know till not added } - - global.LGraphNode = LiteGraph.LGraphNode = LGraphNode; - - LGraphNode.prototype._ctor = function(title) { - this.title = title || "Unnamed"; - this.size = [LiteGraph.NODE_WIDTH, 60]; - this.graph = null; - - this._pos = new Float32Array(10, 10); - - Object.defineProperty(this, "pos", { - set: function(v) { - if (!v || v.length < 2) { - return; - } - this._pos[0] = v[0]; - this._pos[1] = v[1]; - }, - get: function() { - return this._pos; - }, - enumerable: true - }); - - if (LiteGraph.use_uuids) { - this.id = LiteGraph.uuidv4(); - } - else { - this.id = -1; //not know till not added - } - this.type = null; - - //inputs available: array of inputs - this.inputs = []; - this.outputs = []; - this.connections = []; - - //local data - this.properties = {}; //for the values - this.properties_info = []; //for the info - - this.flags = {}; - }; - - /** - * configure a node from an object containing the serialized info - * @method configure - */ - LGraphNode.prototype.configure = function(info) { - if (this.graph) { - this.graph._version++; - } - for (var j in info) { - if (j == "properties") { - //i don't want to clone properties, I want to reuse the old container - for (var k in info.properties) { - this.properties[k] = info.properties[k]; - if (this.onPropertyChanged) { - this.onPropertyChanged( k, info.properties[k] ); - } - } - continue; - } - - if (info[j] == null) { - continue; - } else if (typeof info[j] == "object") { - //object - if (this[j] && this[j].configure) { - this[j].configure(info[j]); - } else { - this[j] = LiteGraph.cloneObject(info[j], this[j]); - } - } //value - else { - this[j] = info[j]; - } - } - - if (!info.title) { - this.title = this.constructor.title; - } - - if (this.inputs) { - for (var i = 0; i < this.inputs.length; ++i) { - var input = this.inputs[i]; - var link_info = this.graph ? this.graph.links[input.link] : null; - if (this.onConnectionsChange) - this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated - - if( this.onInputAdded ) - this.onInputAdded(input); - - } - } - - if (this.outputs) { - for (var i = 0; i < this.outputs.length; ++i) { - var output = this.outputs[i]; - if (!output.links) { - continue; - } - for (var j = 0; j < output.links.length; ++j) { - var link_info = this.graph ? this.graph.links[output.links[j]] : null; - if (this.onConnectionsChange) - this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated - } - - if( this.onOutputAdded ) - this.onOutputAdded(output); - } - } - - if( this.widgets ) - { - for (var i = 0; i < this.widgets.length; ++i) - { - var w = this.widgets[i]; - if(!w) - continue; - if(w.options && w.options.property && (this.properties[ w.options.property ] != undefined)) - w.value = JSON.parse( JSON.stringify( this.properties[ w.options.property ] ) ); - } - if (info.widgets_values) { - for (var i = 0; i < info.widgets_values.length; ++i) { - if (this.widgets[i]) { - this.widgets[i].value = info.widgets_values[i]; - } - } - } - } - - if (this.onConfigure) { - this.onConfigure(info); - } - }; - - /** - * serialize the content - * @method serialize - */ - - LGraphNode.prototype.serialize = function() { - //create serialization object - var o = { - id: this.id, - type: this.type, - pos: this.pos, - size: this.size, - flags: LiteGraph.cloneObject(this.flags), - order: this.order, - mode: this.mode - }; - - //special case for when there were errors - if (this.constructor === LGraphNode && this.last_serialization) { - return this.last_serialization; - } - - if (this.inputs) { - o.inputs = this.inputs; - } - - if (this.outputs) { - //clear outputs last data (because data in connections is never serialized but stored inside the outputs info) - for (var i = 0; i < this.outputs.length; i++) { - delete this.outputs[i]._data; - } - o.outputs = this.outputs; - } - - if (this.title && this.title != this.constructor.title) { - o.title = this.title; - } - - if (this.properties) { - o.properties = LiteGraph.cloneObject(this.properties); - } - - if (this.widgets && this.serialize_widgets) { - o.widgets_values = []; - for (var i = 0; i < this.widgets.length; ++i) { - if(this.widgets[i]) - o.widgets_values[i] = this.widgets[i].value; - else - o.widgets_values[i] = null; - } - } - - if (!o.type) { - o.type = this.constructor.type; - } - - if (this.color) { - o.color = this.color; - } - if (this.bgcolor) { - o.bgcolor = this.bgcolor; - } - if (this.boxcolor) { - o.boxcolor = this.boxcolor; - } - if (this.shape) { - o.shape = this.shape; - } - - if (this.onSerialize) { - if (this.onSerialize(o)) { - console.warn( - "node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter" - ); - } - } - - return o; - }; - - /* Creates a clone of this node */ - LGraphNode.prototype.clone = function() { - var node = LiteGraph.createNode(this.type); - if (!node) { - return null; - } - - //we clone it because serialize returns shared containers - var data = LiteGraph.cloneObject(this.serialize()); - - //remove links - if (data.inputs) { - for (var i = 0; i < data.inputs.length; ++i) { - data.inputs[i].link = null; - } - } - - if (data.outputs) { - for (var i = 0; i < data.outputs.length; ++i) { - if (data.outputs[i].links) { - data.outputs[i].links.length = 0; - } - } - } - - delete data["id"]; - - if (LiteGraph.use_uuids) { - data["id"] = LiteGraph.uuidv4() - } - - //remove links - node.configure(data); - - return node; - }; - - /** - * serialize and stringify - * @method toString - */ - - LGraphNode.prototype.toString = function() { - return JSON.stringify(this.serialize()); - }; - //LGraphNode.prototype.deserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph - - /** - * get the title string - * @method getTitle - */ - - LGraphNode.prototype.getTitle = function() { - return this.title || this.constructor.title; - }; - - /** - * sets the value of a property - * @method setProperty - * @param {String} name - * @param {*} value - */ - LGraphNode.prototype.setProperty = function(name, value) { - if (!this.properties) { - this.properties = {}; - } - if( value === this.properties[name] ) - return; - var prev_value = this.properties[name]; - this.properties[name] = value; - if (this.onPropertyChanged) { - if( this.onPropertyChanged(name, value, prev_value) === false ) //abort change - this.properties[name] = prev_value; - } - if(this.widgets) //widgets could be linked to properties - for(var i = 0; i < this.widgets.length; ++i) - { - var w = this.widgets[i]; - if(!w) - continue; - if(w.options.property == name) - { - w.value = value; - break; - } - } - }; - - // Execution ************************* - /** - * sets the output data - * @method setOutputData - * @param {number} slot - * @param {*} data - */ - LGraphNode.prototype.setOutputData = function(slot, data) { - if (!this.outputs) { - return; - } - - //this maybe slow and a niche case - //if(slot && slot.constructor === String) - // slot = this.findOutputSlot(slot); - - if (slot == -1 || slot >= this.outputs.length) { - return; - } - - var output_info = this.outputs[slot]; - if (!output_info) { - return; - } - - //store data in the output itself in case we want to debug - output_info._data = data; - - //if there are connections, pass the data to the connections - if (this.outputs[slot].links) { - for (var i = 0; i < this.outputs[slot].links.length; i++) { - var link_id = this.outputs[slot].links[i]; - var link = this.graph.links[link_id]; - if(link) - link.data = data; - } - } - }; - - /** - * sets the output data type, useful when you want to be able to overwrite the data type - * @method setOutputDataType - * @param {number} slot - * @param {String} datatype - */ - LGraphNode.prototype.setOutputDataType = function(slot, type) { - if (!this.outputs) { - return; - } - if (slot == -1 || slot >= this.outputs.length) { - return; - } - var output_info = this.outputs[slot]; - if (!output_info) { - return; - } - //store data in the output itself in case we want to debug - output_info.type = type; - - //if there are connections, pass the data to the connections - if (this.outputs[slot].links) { - for (var i = 0; i < this.outputs[slot].links.length; i++) { - var link_id = this.outputs[slot].links[i]; - this.graph.links[link_id].type = type; - } - } - }; - - /** - * Retrieves the input data (data traveling through the connection) from one slot - * @method getInputData - * @param {number} slot - * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link - * @return {*} data or if it is not connected returns undefined - */ - LGraphNode.prototype.getInputData = function(slot, force_update) { - if (!this.inputs) { - return; - } //undefined; - - if (slot >= this.inputs.length || this.inputs[slot].link == null) { - return; - } - - var link_id = this.inputs[slot].link; - var link = this.graph.links[link_id]; - if (!link) { - //bug: weird case but it happens sometimes - return null; - } - - if (!force_update) { - return link.data; - } - - //special case: used to extract data from the incoming connection before the graph has been executed - var node = this.graph.getNodeById(link.origin_id); - if (!node) { - return link.data; - } - - if (node.updateOutputData) { - node.updateOutputData(link.origin_slot); - } else if (node.onExecute) { - node.onExecute(); - } - - return link.data; - }; - - /** - * Retrieves the input data type (in case this supports multiple input types) - * @method getInputDataType - * @param {number} slot - * @return {String} datatype in string format - */ - LGraphNode.prototype.getInputDataType = function(slot) { - if (!this.inputs) { - return null; - } //undefined; - - if (slot >= this.inputs.length || this.inputs[slot].link == null) { - return null; - } - var link_id = this.inputs[slot].link; - var link = this.graph.links[link_id]; - if (!link) { - //bug: weird case but it happens sometimes - return null; - } - var node = this.graph.getNodeById(link.origin_id); - if (!node) { - return link.type; - } - var output_info = node.outputs[link.origin_slot]; - if (output_info) { - return output_info.type; - } - return null; - }; - - /** - * Retrieves the input data from one slot using its name instead of slot number - * @method getInputDataByName - * @param {String} slot_name - * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link - * @return {*} data or if it is not connected returns null - */ - LGraphNode.prototype.getInputDataByName = function( - slot_name, - force_update - ) { - var slot = this.findInputSlot(slot_name); - if (slot == -1) { - return null; - } - return this.getInputData(slot, force_update); - }; - - /** - * tells you if there is a connection in one input slot - * @method isInputConnected - * @param {number} slot - * @return {boolean} - */ - LGraphNode.prototype.isInputConnected = function(slot) { - if (!this.inputs) { - return false; - } - return slot < this.inputs.length && this.inputs[slot].link != null; - }; - - /** - * tells you info about an input connection (which node, type, etc) - * @method getInputInfo - * @param {number} slot - * @return {Object} object or null { link: id, name: string, type: string or 0 } - */ - LGraphNode.prototype.getInputInfo = function(slot) { - if (!this.inputs) { - return null; - } - if (slot < this.inputs.length) { - return this.inputs[slot]; - } - return null; - }; - - /** - * Returns the link info in the connection of an input slot - * @method getInputLink - * @param {number} slot - * @return {LLink} object or null - */ - LGraphNode.prototype.getInputLink = function(slot) { - if (!this.inputs) { - return null; - } - if (slot < this.inputs.length) { - var slot_info = this.inputs[slot]; - return this.graph.links[ slot_info.link ]; - } - return null; - }; - - /** - * returns the node connected in the input slot - * @method getInputNode - * @param {number} slot - * @return {LGraphNode} node or null - */ - LGraphNode.prototype.getInputNode = function(slot) { - if (!this.inputs) { - return null; - } - if (slot >= this.inputs.length) { - return null; - } - var input = this.inputs[slot]; - if (!input || input.link === null) { - return null; - } - var link_info = this.graph.links[input.link]; - if (!link_info) { - return null; - } - return this.graph.getNodeById(link_info.origin_id); - }; - - /** - * returns the value of an input with this name, otherwise checks if there is a property with that name - * @method getInputOrProperty - * @param {string} name - * @return {*} value - */ - LGraphNode.prototype.getInputOrProperty = function(name) { - if (!this.inputs || !this.inputs.length) { - return this.properties ? this.properties[name] : null; - } - - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input_info = this.inputs[i]; - if (name == input_info.name && input_info.link != null) { - var link = this.graph.links[input_info.link]; - if (link) { - return link.data; - } - } - } - return this.properties[name]; - }; - - /** - * tells you the last output data that went in that slot - * @method getOutputData - * @param {number} slot - * @return {Object} object or null - */ - LGraphNode.prototype.getOutputData = function(slot) { - if (!this.outputs) { - return null; - } - if (slot >= this.outputs.length) { - return null; - } - - var info = this.outputs[slot]; - return info._data; - }; - - /** - * tells you info about an output connection (which node, type, etc) - * @method getOutputInfo - * @param {number} slot - * @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] } - */ - LGraphNode.prototype.getOutputInfo = function(slot) { - if (!this.outputs) { - return null; - } - if (slot < this.outputs.length) { - return this.outputs[slot]; - } - return null; - }; - - /** - * tells you if there is a connection in one output slot - * @method isOutputConnected - * @param {number} slot - * @return {boolean} - */ - LGraphNode.prototype.isOutputConnected = function(slot) { - if (!this.outputs) { - return false; - } - return ( - slot < this.outputs.length && - this.outputs[slot].links && - this.outputs[slot].links.length - ); - }; - - /** - * tells you if there is any connection in the output slots - * @method isAnyOutputConnected - * @return {boolean} - */ - LGraphNode.prototype.isAnyOutputConnected = function() { - if (!this.outputs) { - return false; - } - for (var i = 0; i < this.outputs.length; ++i) { - if (this.outputs[i].links && this.outputs[i].links.length) { - return true; - } - } - return false; - }; - - /** - * retrieves all the nodes connected to this output slot - * @method getOutputNodes - * @param {number} slot - * @return {array} - */ - LGraphNode.prototype.getOutputNodes = function(slot) { - if (!this.outputs || this.outputs.length == 0) { - return null; - } - - if (slot >= this.outputs.length) { - return null; - } - - var output = this.outputs[slot]; - if (!output.links || output.links.length == 0) { - return null; - } - - var r = []; - for (var i = 0; i < output.links.length; i++) { - var link_id = output.links[i]; - var link = this.graph.links[link_id]; - if (link) { - var target_node = this.graph.getNodeById(link.target_id); - if (target_node) { - r.push(target_node); - } - } - } - return r; - }; - - LGraphNode.prototype.addOnTriggerInput = function(){ - var trigS = this.findInputSlot("onTrigger"); - if (trigS == -1){ //!trigS || - var input = this.addInput("onTrigger", LiteGraph.EVENT, {optional: true, nameLocked: true}); - return this.findInputSlot("onTrigger"); - } - return trigS; + this.type = null; + + //inputs available: array of inputs + this.inputs = []; + this.outputs = []; + this.connections = []; + + //local data + this.properties = {}; //for the values + this.properties_info = []; //for the info + + this.flags = {}; + }; + + /** + * configure a node from an object containing the serialized info + * @method configure + */ + LGraphNode.prototype.configure = function (info) { + if (this.graph) { + this.graph._version++; } - - LGraphNode.prototype.addOnExecutedOutput = function(){ - var trigS = this.findOutputSlot("onExecuted"); - if (trigS == -1){ //!trigS || - var output = this.addOutput("onExecuted", LiteGraph.ACTION, {optional: true, nameLocked: true}); - return this.findOutputSlot("onExecuted"); - } - return trigS; - } - - LGraphNode.prototype.onAfterExecuteNode = function(param, options){ - var trigS = this.findOutputSlot("onExecuted"); - if (trigS != -1){ - - //console.debug(this.id+":"+this.order+" triggering slot onAfterExecute"); - //console.debug(param); - //console.debug(options); - this.triggerSlot(trigS, param, null, options); - - } - } - - LGraphNode.prototype.changeMode = function(modeTo){ - switch(modeTo){ - case LiteGraph.ON_EVENT: - // this.addOnExecutedOutput(); - break; - - case LiteGraph.ON_TRIGGER: - this.addOnTriggerInput(); - this.addOnExecutedOutput(); - break; - - case LiteGraph.NEVER: - break; - - case LiteGraph.ALWAYS: - break; - - case LiteGraph.ON_REQUEST: - break; - - default: - return false; - break; - } - this.mode = modeTo; - return true; - }; - - /** - * Triggers the node code execution, place a boolean/counter to mark the node as being executed - * @method execute - * @param {*} param - * @param {*} options - */ - LGraphNode.prototype.doExecute = function(param, options) { - options = options || {}; - if (this.onExecute){ - - // enable this to give the event an ID - if (!options.action_call) options.action_call = this.id+"_exec_"+Math.floor(Math.random()*9999); - - this.graph.nodes_executing[this.id] = true; //.push(this.id); - - this.onExecute(param, options); - - this.graph.nodes_executing[this.id] = false; //.pop(); - - // save execution/action ref - this.exec_version = this.graph.iteration; - if(options && options.action_call){ - this.action_call = options.action_call; // if (param) - this.graph.nodes_executedAction[this.id] = options.action_call; - } - } - this.execute_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event - if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); // callback - }; - - /** - * Triggers an action, wrapped by logics to control execution flow - * @method actionDo - * @param {String} action name - * @param {*} param - */ - LGraphNode.prototype.actionDo = function(action, param, options) { - options = options || {}; - if (this.onAction){ - - // enable this to give the event an ID - if (!options.action_call) options.action_call = this.id+"_"+(action?action:"action")+"_"+Math.floor(Math.random()*9999); - - this.graph.nodes_actioning[this.id] = (action?action:"actioning"); //.push(this.id); - - this.onAction(action, param, options); - - this.graph.nodes_actioning[this.id] = false; //.pop(); - - // save execution/action ref - if(options && options.action_call){ - this.action_call = options.action_call; // if (param) - this.graph.nodes_executedAction[this.id] = options.action_call; - } - } - this.action_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event - if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); - }; - - /** - * Triggers an event in this node, this will trigger any output with the same name - * @method trigger - * @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all - * @param {*} param - */ - LGraphNode.prototype.trigger = function(action, param, options) { - if (!this.outputs || !this.outputs.length) { - return; - } - - if (this.graph) - this.graph._last_trigger_time = LiteGraph.getTime(); - - for (var i = 0; i < this.outputs.length; ++i) { - var output = this.outputs[i]; - if ( !output || output.type !== LiteGraph.EVENT || (action && output.name != action) ) - continue; - this.triggerSlot(i, param, null, options); - } - }; - - /** - * Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes - * @method triggerSlot - * @param {Number} slot the index of the output slot - * @param {*} param - * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot - */ - LGraphNode.prototype.triggerSlot = function(slot, param, link_id, options) { - options = options || {}; - if (!this.outputs) { - return; - } - - if(slot == null) - { - console.error("slot must be a number"); - return; - } - - if(slot.constructor !== Number) - console.warn("slot must be a number, use node.trigger('name') if you want to use a string"); - - var output = this.outputs[slot]; - if (!output) { - return; - } - - var links = output.links; - if (!links || !links.length) { - return; - } - - if (this.graph) { - this.graph._last_trigger_time = LiteGraph.getTime(); - } - - //for every link attached here - for (var k = 0; k < links.length; ++k) { - var id = links[k]; - if (link_id != null && link_id != id) { - //to skip links - continue; - } - var link_info = this.graph.links[links[k]]; - if (!link_info) { - //not connected - continue; - } - link_info._last_time = LiteGraph.getTime(); - var node = this.graph.getNodeById(link_info.target_id); - if (!node) { - //node not found? - continue; - } - - //used to mark events in graph - var target_connection = node.inputs[link_info.target_slot]; - - if (node.mode === LiteGraph.ON_TRIGGER) - { - // generate unique trigger ID if not present - if (!options.action_call) options.action_call = this.id+"_trigg_"+Math.floor(Math.random()*9999); - if (node.onExecute) { - // -- wrapping node.onExecute(param); -- - node.doExecute(param, options); - } - } - else if (node.onAction) { - // generate unique action ID if not present - if (!options.action_call) options.action_call = this.id+"_act_"+Math.floor(Math.random()*9999); - //pass the action name - var target_connection = node.inputs[link_info.target_slot]; - // wrap node.onAction(target_connection.name, param); - node.actionDo(target_connection.name, param, options); - } - } - }; - - /** - * clears the trigger slot animation - * @method clearTriggeredSlot - * @param {Number} slot the index of the output slot - * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot - */ - LGraphNode.prototype.clearTriggeredSlot = function(slot, link_id) { - if (!this.outputs) { - return; - } - - var output = this.outputs[slot]; - if (!output) { - return; - } - - var links = output.links; - if (!links || !links.length) { - return; - } - - //for every link attached here - for (var k = 0; k < links.length; ++k) { - var id = links[k]; - if (link_id != null && link_id != id) { - //to skip links - continue; - } - var link_info = this.graph.links[links[k]]; - if (!link_info) { - //not connected - continue; - } - link_info._last_time = 0; - } - }; - - /** - * changes node size and triggers callback - * @method setSize - * @param {vec2} size - */ - LGraphNode.prototype.setSize = function(size) - { - this.size = size; - if(this.onResize) - this.onResize(this.size); - } - - /** - * add a new property to this node - * @method addProperty - * @param {string} name - * @param {*} default_value - * @param {string} type string defining the output type ("vec3","number",...) - * @param {Object} extra_info this can be used to have special properties of the property (like values, etc) - */ - LGraphNode.prototype.addProperty = function( - name, - default_value, - type, - extra_info - ) { - var o = { name: name, type: type, default_value: default_value }; - if (extra_info) { - for (var i in extra_info) { - o[i] = extra_info[i]; - } - } - if (!this.properties_info) { - this.properties_info = []; - } - this.properties_info.push(o); - if (!this.properties) { - this.properties = {}; - } - this.properties[name] = default_value; - return o; - }; - - //connections - - /** - * add a new output slot to use in this node - * @method addOutput - * @param {string} name - * @param {string} type string defining the output type ("vec3","number",...) - * @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc) - */ - LGraphNode.prototype.addOutput = function(name, type, extra_info) { - var output = { name: name, type: type, links: null }; - if (extra_info) { - for (var i in extra_info) { - output[i] = extra_info[i]; - } - } - - if (!this.outputs) { - this.outputs = []; - } - this.outputs.push(output); - if (this.onOutputAdded) { - this.onOutputAdded(output); - } - - if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,type,true); - - this.setSize( this.computeSize() ); - this.setDirtyCanvas(true, true); - return output; - }; - - /** - * add a new output slot to use in this node - * @method addOutputs - * @param {Array} array of triplets like [[name,type,extra_info],[...]] - */ - LGraphNode.prototype.addOutputs = function(array) { - for (var i = 0; i < array.length; ++i) { - var info = array[i]; - var o = { name: info[0], type: info[1], link: null }; - if (array[2]) { - for (var j in info[2]) { - o[j] = info[2][j]; - } - } - - if (!this.outputs) { - this.outputs = []; - } - this.outputs.push(o); - if (this.onOutputAdded) { - this.onOutputAdded(o); - } - - if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,info[1],true); - - } - - this.setSize( this.computeSize() ); - this.setDirtyCanvas(true, true); - }; - - /** - * remove an existing output slot - * @method removeOutput - * @param {number} slot - */ - LGraphNode.prototype.removeOutput = function(slot) { - this.disconnectOutput(slot); - this.outputs.splice(slot, 1); - for (var i = slot; i < this.outputs.length; ++i) { - if (!this.outputs[i] || !this.outputs[i].links) { - continue; - } - var links = this.outputs[i].links; - for (var j = 0; j < links.length; ++j) { - var link = this.graph.links[links[j]]; - if (!link) { - continue; - } - link.origin_slot -= 1; - } - } - - this.setSize( this.computeSize() ); - if (this.onOutputRemoved) { - this.onOutputRemoved(slot); - } - this.setDirtyCanvas(true, true); - }; - - /** - * add a new input slot to use in this node - * @method addInput - * @param {string} name - * @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0 - * @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc) - */ - LGraphNode.prototype.addInput = function(name, type, extra_info) { - type = type || 0; - var input = { name: name, type: type, link: null }; - if (extra_info) { - for (var i in extra_info) { - input[i] = extra_info[i]; - } - } - - if (!this.inputs) { - this.inputs = []; - } - - this.inputs.push(input); - this.setSize( this.computeSize() ); - - if (this.onInputAdded) { - this.onInputAdded(input); - } - - LiteGraph.registerNodeAndSlotType(this,type); - - this.setDirtyCanvas(true, true); - return input; - }; - - /** - * add several new input slots in this node - * @method addInputs - * @param {Array} array of triplets like [[name,type,extra_info],[...]] - */ - LGraphNode.prototype.addInputs = function(array) { - for (var i = 0; i < array.length; ++i) { - var info = array[i]; - var o = { name: info[0], type: info[1], link: null }; - if (array[2]) { - for (var j in info[2]) { - o[j] = info[2][j]; - } - } - - if (!this.inputs) { - this.inputs = []; - } - this.inputs.push(o); - if (this.onInputAdded) { - this.onInputAdded(o); - } - - LiteGraph.registerNodeAndSlotType(this,info[1]); - } - - this.setSize( this.computeSize() ); - this.setDirtyCanvas(true, true); - }; - - /** - * remove an existing input slot - * @method removeInput - * @param {number} slot - */ - LGraphNode.prototype.removeInput = function(slot) { - this.disconnectInput(slot); - var slot_info = this.inputs.splice(slot, 1); - for (var i = slot; i < this.inputs.length; ++i) { - if (!this.inputs[i]) { - continue; - } - var link = this.graph.links[this.inputs[i].link]; - if (!link) { - continue; - } - link.target_slot -= 1; - } - this.setSize( this.computeSize() ); - if (this.onInputRemoved) { - this.onInputRemoved(slot, slot_info[0] ); - } - this.setDirtyCanvas(true, true); - }; - - /** - * add an special connection to this node (used for special kinds of graphs) - * @method addConnection - * @param {string} name - * @param {string} type string defining the input type ("vec3","number",...) - * @param {[x,y]} pos position of the connection inside the node - * @param {string} direction if is input or output - */ - LGraphNode.prototype.addConnection = function(name, type, pos, direction) { - var o = { - name: name, - type: type, - pos: pos, - direction: direction, - links: null - }; - this.connections.push(o); - return o; - }; - - /** - * computes the minimum size of a node according to its inputs and output slots - * @method computeSize - * @param {vec2} minHeight - * @return {vec2} the total size - */ - LGraphNode.prototype.computeSize = function(out) { - if (this.constructor.size) { - return this.constructor.size.concat(); - } - - var rows = Math.max( - this.inputs ? this.inputs.length : 1, - this.outputs ? this.outputs.length : 1 - ); - var size = out || new Float32Array([0, 0]); - rows = Math.max(rows, 1); - var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size - - var title_width = compute_text_size(this.title); - var input_width = 0; - var output_width = 0; - - if (this.inputs) { - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input = this.inputs[i]; - var text = input.label || input.name || ""; - var text_width = compute_text_size(text); - if (input_width < text_width) { - input_width = text_width; - } - } - } - - if (this.outputs) { - for (var i = 0, l = this.outputs.length; i < l; ++i) { - var output = this.outputs[i]; - var text = output.label || output.name || ""; - var text_width = compute_text_size(text); - if (output_width < text_width) { - output_width = text_width; - } - } - } - - size[0] = Math.max(input_width + output_width + 10, title_width); - size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH); - if (this.widgets && this.widgets.length) { - size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5); - } - - size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; - - var widgets_height = 0; - if (this.widgets && this.widgets.length) { - for (var i = 0, l = this.widgets.length; i < l; ++i) { - if (this.widgets[i].computeSize) - widgets_height += this.widgets[i].computeSize(size[0])[1] + 4; - else - widgets_height += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - widgets_height += 8; - } - - //compute height using widgets height - if( this.widgets_up ) - size[1] = Math.max( size[1], widgets_height ); - else if( this.widgets_start_y != null ) - size[1] = Math.max( size[1], widgets_height + this.widgets_start_y ); - else - size[1] += widgets_height; - - function compute_text_size(text) { - if (!text) { - return 0; - } - return font_size * text.length * 0.6; - } - - if ( - this.constructor.min_height && - size[1] < this.constructor.min_height - ) { - size[1] = this.constructor.min_height; - } - - size[1] += 6; //margin - - return size; - }; - - LGraphNode.prototype.inResizeCorner = function(canvasX, canvasY) { - var rows = this.outputs ? this.outputs.length : 1; - var outputs_offset = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; - return isInsideRectangle(canvasX, - canvasY, - this.pos[0] + this.size[0] - 15, - this.pos[1] + Math.max(this.size[1] - 15, outputs_offset), - 20, - 20 - ); - } - - /** - * returns all the info available about a property of this node. - * - * @method getPropertyInfo - * @param {String} property name of the property - * @return {Object} the object with all the available info - */ - LGraphNode.prototype.getPropertyInfo = function( property ) - { - var info = null; - - //there are several ways to define info about a property - //legacy mode - if (this.properties_info) { - for (var i = 0; i < this.properties_info.length; ++i) { - if (this.properties_info[i].name == property) { - info = this.properties_info[i]; - break; - } - } - } - //litescene mode using the constructor - if(this.constructor["@" + property]) - info = this.constructor["@" + property]; - - if(this.constructor.widgets_info && this.constructor.widgets_info[property]) - info = this.constructor.widgets_info[property]; - - //litescene mode using the constructor - if (!info && this.onGetPropertyInfo) { - info = this.onGetPropertyInfo(property); - } - - if (!info) - info = {}; - if(!info.type) - info.type = typeof this.properties[property]; - if(info.widget == "combo") - info.type = "enum"; - - return info; - } - - /** - * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties - * - * @method addWidget - * @param {String} type the widget type (could be "number","string","combo" - * @param {String} name the text to show on the widget - * @param {String} value the default value - * @param {Function|String} callback function to call when it changes (optionally, it can be the name of the property to modify) - * @param {Object} options the object that contains special properties of this widget - * @return {Object} the created widget object - */ - LGraphNode.prototype.addWidget = function( type, name, value, callback, options ) - { - if (!this.widgets) { - this.widgets = []; - } - - if(!options && callback && callback.constructor === Object) - { - options = callback; - callback = null; - } - - if(options && options.constructor === String) //options can be the property name - options = { property: options }; - - if(callback && callback.constructor === String) //callback can be the property name - { - if(!options) - options = {}; - options.property = callback; - callback = null; - } - - if(callback && callback.constructor !== Function) - { - console.warn("addWidget: callback must be a function"); - callback = null; - } - - var w = { - type: type.toLowerCase(), - name: name, - value: value, - callback: callback, - options: options || {} - }; - - if (w.options.y !== undefined) { - w.y = w.options.y; - } - - if (!callback && !w.options.callback && !w.options.property) { - console.warn("LiteGraph addWidget(...) without a callback or property assigned"); - } - if (type == "combo" && !w.options.values) { - throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }"; - } - this.widgets.push(w); - this.setSize( this.computeSize() ); - return w; - }; - - LGraphNode.prototype.addCustomWidget = function(custom_widget) { - if (!this.widgets) { - this.widgets = []; - } - this.widgets.push(custom_widget); - return custom_widget; - }; - - /** - * returns the bounding of the object, used for rendering purposes - * @method getBounding - * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage - * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation - * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] - */ - LGraphNode.prototype.getBounding = function(out, compute_outer) { - out = out || new Float32Array(4); - const nodePos = this.pos; - const isCollapsed = this.flags.collapsed; - const nodeSize = this.size; - - let left_offset = 0; - // 1 offset due to how nodes are rendered - let right_offset = 1 ; - let top_offset = 0; - let bottom_offset = 0; - - if (compute_outer) { - // 4 offset for collapsed node connection points - left_offset = 4; - // 6 offset for right shadow and collapsed node connection points - right_offset = 6 + left_offset; - // 4 offset for collapsed nodes top connection points - top_offset = 4; - // 5 offset for bottom shadow and collapsed node connection points - bottom_offset = 5 + top_offset; - } - - out[0] = nodePos[0] - left_offset; - out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset; - out[2] = isCollapsed ? - (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset : - nodeSize[0] + right_offset; - out[3] = isCollapsed ? - LiteGraph.NODE_TITLE_HEIGHT + bottom_offset : - nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset; - - if (this.onBounding) { - this.onBounding(out); - } - return out; - }; - - /** - * checks if a point is inside the shape of a node - * @method isPointInside - * @param {number} x - * @param {number} y - * @return {boolean} - */ - LGraphNode.prototype.isPointInside = function(x, y, margin, skip_title) { - margin = margin || 0; - - var margin_top = this.graph && this.graph.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT; - if (skip_title) { - margin_top = 0; - } - if (this.flags && this.flags.collapsed) { - //if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS) - if ( - isInsideRectangle( - x, - y, - this.pos[0] - margin, - this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin, - (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + - 2 * margin, - LiteGraph.NODE_TITLE_HEIGHT + 2 * margin - ) - ) { - return true; - } - } else if ( - this.pos[0] - 4 - margin < x && - this.pos[0] + this.size[0] + 4 + margin > x && - this.pos[1] - margin_top - margin < y && - this.pos[1] + this.size[1] + margin > y - ) { - return true; - } - return false; - }; - - /** - * checks if a point is inside a node slot, and returns info about which slot - * @method getSlotInPosition - * @param {number} x - * @param {number} y - * @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] } - */ - LGraphNode.prototype.getSlotInPosition = function(x, y) { - //search for inputs - var link_pos = new Float32Array(2); - if (this.inputs) { - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input = this.inputs[i]; - this.getConnectionPos(true, i, link_pos); - if ( - isInsideRectangle( - x, - y, - link_pos[0] - 10, - link_pos[1] - 5, - 20, - 10 - ) - ) { - return { input: input, slot: i, link_pos: link_pos }; - } - } - } - - if (this.outputs) { - for (var i = 0, l = this.outputs.length; i < l; ++i) { - var output = this.outputs[i]; - this.getConnectionPos(false, i, link_pos); - if ( - isInsideRectangle( - x, - y, - link_pos[0] - 10, - link_pos[1] - 5, - 20, - 10 - ) - ) { - return { output: output, slot: i, link_pos: link_pos }; - } - } - } - - return null; - }; - - /** - * returns the input slot with a given name (used for dynamic slots), -1 if not found - * @method findInputSlot - * @param {string} name the name of the slot - * @param {boolean} returnObj if the obj itself wanted - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findInputSlot = function(name, returnObj) { - if (!this.inputs) { - return -1; - } - for (var i = 0, l = this.inputs.length; i < l; ++i) { - if (name == this.inputs[i].name) { - return !returnObj ? i : this.inputs[i]; - } - } - return -1; - }; - - /** - * returns the output slot with a given name (used for dynamic slots), -1 if not found - * @method findOutputSlot - * @param {string} name the name of the slot - * @param {boolean} returnObj if the obj itself wanted - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findOutputSlot = function(name, returnObj) { - returnObj = returnObj || false; - if (!this.outputs) { - return -1; - } - for (var i = 0, l = this.outputs.length; i < l; ++i) { - if (name == this.outputs[i].name) { - return !returnObj ? i : this.outputs[i]; - } - } - return -1; - }; - - // TODO refactor: USE SINGLE findInput/findOutput functions! :: merge options - - /** - * returns the first free input slot - * @method findInputSlotFree - * @param {object} options - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findInputSlotFree = function(optsIn) { - var optsIn = optsIn || {}; - var optsDef = {returnObj: false - ,typesNotAccepted: [] - }; - var opts = Object.assign(optsDef,optsIn); - if (!this.inputs) { - return -1; - } - for (var i = 0, l = this.inputs.length; i < l; ++i) { - if (this.inputs[i].link && this.inputs[i].link != null) { - continue; - } - if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.inputs[i].type)){ - continue; - } - return !opts.returnObj ? i : this.inputs[i]; - } - return -1; - }; - - /** - * returns the first output slot free - * @method findOutputSlotFree - * @param {object} options - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findOutputSlotFree = function(optsIn) { - var optsIn = optsIn || {}; - var optsDef = { returnObj: false - ,typesNotAccepted: [] - }; - var opts = Object.assign(optsDef,optsIn); - if (!this.outputs) { - return -1; - } - for (var i = 0, l = this.outputs.length; i < l; ++i) { - if (this.outputs[i].links && this.outputs[i].links != null) { - continue; - } - if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.outputs[i].type)){ - continue; - } - return !opts.returnObj ? i : this.outputs[i]; - } - return -1; - }; - - /** - * findSlotByType for INPUTS - */ - LGraphNode.prototype.findInputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) { - return this.findSlotByType(true, type, returnObj, preferFreeSlot, doNotUseOccupied); - }; - - /** - * findSlotByType for OUTPUTS - */ - LGraphNode.prototype.findOutputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) { - return this.findSlotByType(false, type, returnObj, preferFreeSlot, doNotUseOccupied); - }; - - /** - * returns the output (or input) slot with a given type, -1 if not found - * @method findSlotByType - * @param {boolean} input uise inputs instead of outputs - * @param {string} type the type of the slot - * @param {boolean} returnObj if the obj itself wanted - * @param {boolean} preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway) - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findSlotByType = function(input, type, returnObj, preferFreeSlot, doNotUseOccupied) { - input = input || false; - returnObj = returnObj || false; - preferFreeSlot = preferFreeSlot || false; - doNotUseOccupied = doNotUseOccupied || false; - var aSlots = input ? this.inputs : this.outputs; - if (!aSlots) { - return -1; - } - // !! empty string type is considered 0, * !! - if (type == "" || type == "*") type = 0; - for (var i = 0, l = aSlots.length; i < l; ++i) { - var tFound = false; - var aSource = (type+"").toLowerCase().split(","); - var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type; - aDest = (aDest+"").toLowerCase().split(","); - for(var sI=0;sI= 0 && target_slot !== null){ - //console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot) - return this.connect(slot, target_node, target_slot); - }else{ - //console.log("type "+target_slotType+" not found or not free?") - if (opts.createEventInCase && target_slotType == LiteGraph.EVENT){ - // WILL CREATE THE onTrigger IN SLOT - //console.debug("connect WILL CREATE THE onTrigger "+target_slotType+" to "+target_node); - return this.connect(slot, target_node, -1); - } - // connect to the first general output slot if not found a specific type and - if (opts.generalTypeInCase){ - var target_slot = target_node.findInputSlotByType(0, false, true, true); - //console.debug("connect TO a general type (*, 0), if not found the specific type ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); - if (target_slot >= 0){ - return this.connect(slot, target_node, target_slot); - } - } - // connect to the first free input slot if not found a specific type and this output is general - if (opts.firstFreeIfOutputGeneralInCase && (target_slotType == 0 || target_slotType == "*" || target_slotType == "")){ - var target_slot = target_node.findInputSlotFree({typesNotAccepted: [LiteGraph.EVENT] }); - //console.debug("connect TO TheFirstFREE ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); - if (target_slot >= 0){ - return this.connect(slot, target_node, target_slot); - } - } - - console.debug("no way to connect type: ",target_slotType," to targetNODE ",target_node); - //TODO filter - - return null; - } - } - - /** - * connect this node input to the output of another node BY TYPE - * @method connectByType - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} node the target node - * @param {string} target_type the output slot type of the target node - * @return {Object} the link_info is created, otherwise null - */ - LGraphNode.prototype.connectByTypeOutput = function(slot, source_node, source_slotType, optsIn) { - var optsIn = optsIn || {}; - var optsDef = { createEventInCase: true - ,firstFreeIfInputGeneralInCase: true - ,generalTypeInCase: true - }; - var opts = Object.assign(optsDef,optsIn); - if (source_node && source_node.constructor === Number) { - source_node = this.graph.getNodeById(source_node); - } - var source_slot = source_node.findOutputSlotByType(source_slotType, false, true); - if (source_slot >= 0 && source_slot !== null){ - //console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot) - return source_node.connect(source_slot, this, slot); - }else{ - - // connect to the first general output slot if not found a specific type and - if (opts.generalTypeInCase){ - var source_slot = source_node.findOutputSlotByType(0, false, true, true); - if (source_slot >= 0){ - return source_node.connect(source_slot, this, slot); - } - } - - if (opts.createEventInCase && source_slotType == LiteGraph.EVENT){ - // WILL CREATE THE onExecuted OUT SLOT - if (LiteGraph.do_add_triggers_slots){ - var source_slot = source_node.addOnExecutedOutput(); - return source_node.connect(source_slot, this, slot); - } - } - // connect to the first free output slot if not found a specific type and this input is general - if (opts.firstFreeIfInputGeneralInCase && (source_slotType == 0 || source_slotType == "*" || source_slotType == "")){ - var source_slot = source_node.findOutputSlotFree({typesNotAccepted: [LiteGraph.EVENT] }); - if (source_slot >= 0){ - return source_node.connect(source_slot, this, slot); - } - } - - console.debug("no way to connect byOUT type: ",source_slotType," to sourceNODE ",source_node); - //TODO filter - - //console.log("type OUT! "+source_slotType+" not found or not free?") - return null; - } - } - - /** - * connect this node output to the input of another node - * @method connect - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} node the target node - * @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) - * @return {Object} the link_info is created, otherwise null - */ - LGraphNode.prototype.connect = function(slot, target_node, target_slot) { - target_slot = target_slot || 0; - - if (!this.graph) { - //could be connected before adding it to a graph - console.log( - "Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them." - ); //due to link ids being associated with graphs - return null; - } - - //seek for the output slot - if (slot.constructor === String) { - slot = this.findOutputSlot(slot); - if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return null; - } - } else if (!this.outputs || slot >= this.outputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return null; - } - - if (target_node && target_node.constructor === Number) { - target_node = this.graph.getNodeById(target_node); - } - if (!target_node) { - throw "target node is null"; - } - - //avoid loopback - if (target_node == this) { - return null; - } - - //you can specify the slot by name - if (target_slot.constructor === String) { - target_slot = target_node.findInputSlot(target_slot); - if (target_slot == -1) { - if (LiteGraph.debug) { - console.log( - "Connect: Error, no slot of name " + target_slot - ); - } - return null; - } - } else if (target_slot === LiteGraph.EVENT) { - - if (LiteGraph.do_add_triggers_slots){ - //search for first slot with event? :: NO this is done outside - //console.log("Connect: Creating triggerEvent"); - // force mode - target_node.changeMode(LiteGraph.ON_TRIGGER); - target_slot = target_node.findInputSlot("onTrigger"); - }else{ - return null; // -- break -- - } - } else if ( - !target_node.inputs || - target_slot >= target_node.inputs.length - ) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return null; - } - - var changed = false; - - var input = target_node.inputs[target_slot]; - var link_info = null; - var output = this.outputs[slot]; - - if (!this.outputs[slot]){ - /*console.debug("Invalid slot passed: "+slot); - console.debug(this.outputs);*/ - return null; - } - - // allow target node to change slot - if (target_node.onBeforeConnectInput) { - // This way node can choose another slot (or make a new one?) - target_slot = target_node.onBeforeConnectInput(target_slot); //callback - } - - //check target_slot and check connection types - if (target_slot===false || target_slot===null || !LiteGraph.isValidConnection(output.type, input.type)) - { - this.setDirtyCanvas(false, true); - if(changed) - this.graph.connectionChange(this, link_info); - return null; - }else{ - //console.debug("valid connection",output.type, input.type); - } - - //allows nodes to block connection, callback - if (target_node.onConnectInput) { - if ( target_node.onConnectInput(target_slot, output.type, output, this, slot) === false ) { - return null; - } - } - if (this.onConnectOutput) { // callback - if ( this.onConnectOutput(slot, input.type, input, target_node, target_slot) === false ) { - return null; - } - } - - //if there is something already plugged there, disconnect - if (target_node.inputs[target_slot] && target_node.inputs[target_slot].link != null) { - this.graph.beforeChange(); - target_node.disconnectInput(target_slot, {doProcessChange: false}); - changed = true; - } - if (output.links !== null && output.links.length){ - switch(output.type){ - case LiteGraph.EVENT: - if (!LiteGraph.allow_multi_output_for_events){ - this.graph.beforeChange(); - this.disconnectOutput(slot, false, {doProcessChange: false}); // Input(target_slot, {doProcessChange: false}); - changed = true; - } - break; - default: - break; - } - } - - var nextId - if (LiteGraph.use_uuids) - nextId = LiteGraph.uuidv4(); - else - nextId = ++this.graph.last_link_id; - - //create link class - link_info = new LLink( - nextId, - input.type || output.type, - this.id, - slot, - target_node.id, - target_slot - ); - - //add to graph links list - this.graph.links[link_info.id] = link_info; - - //connect in output - if (output.links == null) { - output.links = []; - } - output.links.push(link_info.id); - //connect in input - target_node.inputs[target_slot].link = link_info.id; - if (this.graph) { - this.graph._version++; - } - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - true, - link_info, - output - ); - } //link_info has been created now, so its updated - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - target_slot, - true, - link_info, - input - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - target_slot, - this, - slot - ); - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot, - target_node, - target_slot - ); - } - - this.setDirtyCanvas(false, true); - this.graph.afterChange(); - this.graph.connectionChange(this, link_info); - - return link_info; - }; - - /** - * disconnect one output to an specific node - * @method disconnectOutput - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] - * @return {boolean} if it was disconnected successfully - */ - LGraphNode.prototype.disconnectOutput = function(slot, target_node) { - if (slot.constructor === String) { - slot = this.findOutputSlot(slot); - if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return false; - } - } else if (!this.outputs || slot >= this.outputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return false; - } - - //get output slot - var output = this.outputs[slot]; - if (!output || !output.links || output.links.length == 0) { - return false; - } - - //one of the output links in this slot - if (target_node) { - if (target_node.constructor === Number) { - target_node = this.graph.getNodeById(target_node); - } - if (!target_node) { - throw "Target Node not found"; - } - - for (var i = 0, l = output.links.length; i < l; i++) { - var link_id = output.links[i]; - var link_info = this.graph.links[link_id]; - - //is the link we are searching for... - if (link_info.target_id == target_node.id) { - output.links.splice(i, 1); //remove here - var input = target_node.inputs[link_info.target_slot]; - input.link = null; //remove there - delete this.graph.links[link_id]; //remove the link from the links pool - if (this.graph) { - this.graph._version++; - } - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - link_info.target_slot, - false, - link_info, - input - ); - } //link_info hasn't been modified so its ok - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - break; - } - } - } //all the links in this output slot - else { - for (var i = 0, l = output.links.length; i < l; i++) { - var link_id = output.links[i]; - var link_info = this.graph.links[link_id]; - if (!link_info) { - //bug: it happens sometimes - continue; - } - - var target_node = this.graph.getNodeById(link_info.target_id); - var input = null; - if (this.graph) { - this.graph._version++; - } - if (target_node) { - input = target_node.inputs[link_info.target_slot]; - input.link = null; //remove other side link - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - link_info.target_slot, - false, - link_info, - input - ); - } //link_info hasn't been modified so its ok - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - } - delete this.graph.links[link_id]; //remove the link from the links pool - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - } - output.links = null; - } - - this.setDirtyCanvas(false, true); - this.graph.connectionChange(this); - return true; - }; - - /** - * disconnect one input - * @method disconnectInput - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @return {boolean} if it was disconnected successfully - */ - LGraphNode.prototype.disconnectInput = function(slot) { - //seek for the output slot - if (slot.constructor === String) { - slot = this.findInputSlot(slot); - if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return false; - } - } else if (!this.inputs || slot >= this.inputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return false; - } - - var input = this.inputs[slot]; - if (!input) { - return false; - } - - var link_id = this.inputs[slot].link; - if(link_id != null) - { - this.inputs[slot].link = null; - - //remove other side - var link_info = this.graph.links[link_id]; - if (link_info) { - var target_node = this.graph.getNodeById(link_info.origin_id); - if (!target_node) { - return false; - } - - var output = target_node.outputs[link_info.origin_slot]; - if (!output || !output.links || output.links.length == 0) { - return false; - } - - //search in the inputs list for this link - for (var i = 0, l = output.links.length; i < l; i++) { - if (output.links[i] == link_id) { - output.links.splice(i, 1); - break; - } - } - - delete this.graph.links[link_id]; //remove from the pool - if (this.graph) { - this.graph._version++; - } - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.INPUT, - slot, - false, - link_info, - input - ); - } - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.OUTPUT, - i, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - target_node, - i - ); - this.graph.onNodeConnectionChange(LiteGraph.INPUT, this, slot); - } - } - } //link != null - - this.setDirtyCanvas(false, true); - if(this.graph) - this.graph.connectionChange(this); - return true; - }; - - /** - * returns the center of a connection point in canvas coords - * @method getConnectionPos - * @param {boolean} is_input true if if a input slot, false if it is an output - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {vec2} out [optional] a place to store the output, to free garbage - * @return {[x,y]} the position - **/ - LGraphNode.prototype.getConnectionPos = function( - is_input, - slot_number, - out - ) { - out = out || new Float32Array(2); - var num_slots = 0; - if (is_input && this.inputs) { - num_slots = this.inputs.length; - } - if (!is_input && this.outputs) { - num_slots = this.outputs.length; - } - - var offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5; - - if (this.flags.collapsed) { - var w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH; - if (this.horizontal) { - out[0] = this.pos[0] + w * 0.5; - if (is_input) { - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; - } else { - out[1] = this.pos[1]; - } - } else { - if (is_input) { - out[0] = this.pos[0]; - } else { - out[0] = this.pos[0] + w; - } - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5; - } - return out; - } - - //weird feature that never got finished - if (is_input && slot_number == -1) { - out[0] = this.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; - out[1] = this.pos[1] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; - return out; - } - - //hard-coded pos - if ( - is_input && - num_slots > slot_number && - this.inputs[slot_number].pos - ) { - out[0] = this.pos[0] + this.inputs[slot_number].pos[0]; - out[1] = this.pos[1] + this.inputs[slot_number].pos[1]; - return out; - } else if ( - !is_input && - num_slots > slot_number && - this.outputs[slot_number].pos - ) { - out[0] = this.pos[0] + this.outputs[slot_number].pos[0]; - out[1] = this.pos[1] + this.outputs[slot_number].pos[1]; - return out; - } - - //horizontal distributed slots - if (this.horizontal) { - out[0] = - this.pos[0] + (slot_number + 0.5) * (this.size[0] / num_slots); - if (is_input) { - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; - } else { - out[1] = this.pos[1] + this.size[1]; - } - return out; - } - - //default vertical slots - if (is_input) { - out[0] = this.pos[0] + offset; + for (var j in info) { + if (j == "properties") { + //i don't want to clone properties, I want to reuse the old container + for (var k in info.properties) { + this.properties[k] = info.properties[k]; + if (this.onPropertyChanged) { + this.onPropertyChanged(k, info.properties[k]); + } + } + continue; + } + + if (info[j] == null) { + continue; + } else if (typeof info[j] == "object") { + //object + if (this[j] && this[j].configure) { + this[j].configure(info[j]); } else { - out[0] = this.pos[0] + this.size[0] + 1 - offset; + this[j] = LiteGraph.cloneObject(info[j], this[j]); } - out[1] = - this.pos[1] + - (slot_number + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + - (this.constructor.slot_start_y || 0); - return out; - }; + } //value + else { + this[j] = info[j]; + } + } - /* Force align to grid */ - LGraphNode.prototype.alignToGrid = function() { - this.pos[0] = - LiteGraph.CANVAS_GRID_SIZE * - Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE); - this.pos[1] = - LiteGraph.CANVAS_GRID_SIZE * - Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE); - }; + if (!info.title) { + this.title = this.constructor.title; + } - /* Console output */ - LGraphNode.prototype.trace = function(msg) { - if (!this.console) { - this.console = []; + if (this.inputs) { + for (var i = 0; i < this.inputs.length; ++i) { + var input = this.inputs[i]; + var link_info = this.graph ? this.graph.links[input.link] : null; + if (this.onConnectionsChange) + this.onConnectionsChange(LiteGraph.INPUT, i, true, link_info, input); //link_info has been created now, so its updated + + if (this.onInputAdded) this.onInputAdded(input); + } + } + + if (this.outputs) { + for (var i = 0; i < this.outputs.length; ++i) { + var output = this.outputs[i]; + if (!output.links) { + continue; + } + for (var j = 0; j < output.links.length; ++j) { + var link_info = this.graph ? this.graph.links[output.links[j]] : null; + if (this.onConnectionsChange) + this.onConnectionsChange( + LiteGraph.OUTPUT, + i, + true, + link_info, + output + ); //link_info has been created now, so its updated } - this.console.push(msg); - if (this.console.length > LGraphNode.MAX_CONSOLE) { - this.console.shift(); - } + if (this.onOutputAdded) this.onOutputAdded(output); + } + } - if(this.graph.onNodeTrace) - this.graph.onNodeTrace(this, msg); + if (this.widgets) { + for (var i = 0; i < this.widgets.length; ++i) { + var w = this.widgets[i]; + if (!w) continue; + if ( + w.options && + w.options.property && + this.properties[w.options.property] != undefined + ) + w.value = JSON.parse( + JSON.stringify(this.properties[w.options.property]) + ); + } + if (info.widgets_values) { + for (var i = 0; i < info.widgets_values.length; ++i) { + if (this.widgets[i]) { + this.widgets[i].value = info.widgets_values[i]; + } + } + } + } + + if (this.onConfigure) { + this.onConfigure(info); + } + }; + + /** + * serialize the content + * @method serialize + */ + + LGraphNode.prototype.serialize = function () { + //create serialization object + var o = { + id: this.id, + type: this.type, + pos: this.pos, + size: this.size, + flags: LiteGraph.cloneObject(this.flags), + order: this.order, + mode: this.mode, }; - /* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ - LGraphNode.prototype.setDirtyCanvas = function( - dirty_foreground, - dirty_background + //special case for when there were errors + if (this.constructor === LGraphNode && this.last_serialization) { + return this.last_serialization; + } + + if (this.inputs) { + o.inputs = this.inputs; + } + + if (this.outputs) { + //clear outputs last data (because data in connections is never serialized but stored inside the outputs info) + for (var i = 0; i < this.outputs.length; i++) { + delete this.outputs[i]._data; + } + o.outputs = this.outputs; + } + + if (this.title && this.title != this.constructor.title) { + o.title = this.title; + } + + if (this.properties) { + o.properties = LiteGraph.cloneObject(this.properties); + } + + if (this.widgets && this.serialize_widgets) { + o.widgets_values = []; + for (var i = 0; i < this.widgets.length; ++i) { + if (this.widgets[i]) o.widgets_values[i] = this.widgets[i].value; + else o.widgets_values[i] = null; + } + } + + if (!o.type) { + o.type = this.constructor.type; + } + + if (this.color) { + o.color = this.color; + } + if (this.bgcolor) { + o.bgcolor = this.bgcolor; + } + if (this.boxcolor) { + o.boxcolor = this.boxcolor; + } + if (this.shape) { + o.shape = this.shape; + } + + if (this.onSerialize) { + if (this.onSerialize(o)) { + console.warn( + "node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter" + ); + } + } + + return o; + }; + + /* Creates a clone of this node */ + LGraphNode.prototype.clone = function () { + var node = LiteGraph.createNode(this.type); + if (!node) { + return null; + } + + //we clone it because serialize returns shared containers + var data = LiteGraph.cloneObject(this.serialize()); + + //remove links + if (data.inputs) { + for (var i = 0; i < data.inputs.length; ++i) { + data.inputs[i].link = null; + } + } + + if (data.outputs) { + for (var i = 0; i < data.outputs.length; ++i) { + if (data.outputs[i].links) { + data.outputs[i].links.length = 0; + } + } + } + + delete data["id"]; + + if (LiteGraph.use_uuids) { + data["id"] = LiteGraph.uuidv4(); + } + + //remove links + node.configure(data); + + return node; + }; + + /** + * serialize and stringify + * @method toString + */ + + LGraphNode.prototype.toString = function () { + return JSON.stringify(this.serialize()); + }; + //LGraphNode.prototype.deserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph + + /** + * get the title string + * @method getTitle + */ + + LGraphNode.prototype.getTitle = function () { + return this.title || this.constructor.title; + }; + + /** + * sets the value of a property + * @method setProperty + * @param {String} name + * @param {*} value + */ + LGraphNode.prototype.setProperty = function (name, value) { + if (!this.properties) { + this.properties = {}; + } + if (value === this.properties[name]) return; + var prev_value = this.properties[name]; + this.properties[name] = value; + if (this.onPropertyChanged) { + if (this.onPropertyChanged(name, value, prev_value) === false) + //abort change + this.properties[name] = prev_value; + } + if (this.widgets) + //widgets could be linked to properties + for (var i = 0; i < this.widgets.length; ++i) { + var w = this.widgets[i]; + if (!w) continue; + if (w.options.property == name) { + w.value = value; + break; + } + } + }; + + // Execution ************************* + /** + * sets the output data + * @method setOutputData + * @param {number} slot + * @param {*} data + */ + LGraphNode.prototype.setOutputData = function (slot, data) { + if (!this.outputs) { + return; + } + + //this maybe slow and a niche case + //if(slot && slot.constructor === String) + // slot = this.findOutputSlot(slot); + + if (slot == -1 || slot >= this.outputs.length) { + return; + } + + var output_info = this.outputs[slot]; + if (!output_info) { + return; + } + + //store data in the output itself in case we want to debug + output_info._data = data; + + //if there are connections, pass the data to the connections + if (this.outputs[slot].links) { + for (var i = 0; i < this.outputs[slot].links.length; i++) { + var link_id = this.outputs[slot].links[i]; + var link = this.graph.links[link_id]; + if (link) link.data = data; + } + } + }; + + /** + * sets the output data type, useful when you want to be able to overwrite the data type + * @method setOutputDataType + * @param {number} slot + * @param {String} datatype + */ + LGraphNode.prototype.setOutputDataType = function (slot, type) { + if (!this.outputs) { + return; + } + if (slot == -1 || slot >= this.outputs.length) { + return; + } + var output_info = this.outputs[slot]; + if (!output_info) { + return; + } + //store data in the output itself in case we want to debug + output_info.type = type; + + //if there are connections, pass the data to the connections + if (this.outputs[slot].links) { + for (var i = 0; i < this.outputs[slot].links.length; i++) { + var link_id = this.outputs[slot].links[i]; + this.graph.links[link_id].type = type; + } + } + }; + + /** + * Retrieves the input data (data traveling through the connection) from one slot + * @method getInputData + * @param {number} slot + * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link + * @return {*} data or if it is not connected returns undefined + */ + LGraphNode.prototype.getInputData = function (slot, force_update) { + if (!this.inputs) { + return; + } //undefined; + + if (slot >= this.inputs.length || this.inputs[slot].link == null) { + return; + } + + var link_id = this.inputs[slot].link; + var link = this.graph.links[link_id]; + if (!link) { + //bug: weird case but it happens sometimes + return null; + } + + if (!force_update) { + return link.data; + } + + //special case: used to extract data from the incoming connection before the graph has been executed + var node = this.graph.getNodeById(link.origin_id); + if (!node) { + return link.data; + } + + if (node.updateOutputData) { + node.updateOutputData(link.origin_slot); + } else if (node.onExecute) { + node.onExecute(); + } + + return link.data; + }; + + /** + * Retrieves the input data type (in case this supports multiple input types) + * @method getInputDataType + * @param {number} slot + * @return {String} datatype in string format + */ + LGraphNode.prototype.getInputDataType = function (slot) { + if (!this.inputs) { + return null; + } //undefined; + + if (slot >= this.inputs.length || this.inputs[slot].link == null) { + return null; + } + var link_id = this.inputs[slot].link; + var link = this.graph.links[link_id]; + if (!link) { + //bug: weird case but it happens sometimes + return null; + } + var node = this.graph.getNodeById(link.origin_id); + if (!node) { + return link.type; + } + var output_info = node.outputs[link.origin_slot]; + if (output_info) { + return output_info.type; + } + return null; + }; + + /** + * Retrieves the input data from one slot using its name instead of slot number + * @method getInputDataByName + * @param {String} slot_name + * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link + * @return {*} data or if it is not connected returns null + */ + LGraphNode.prototype.getInputDataByName = function (slot_name, force_update) { + var slot = this.findInputSlot(slot_name); + if (slot == -1) { + return null; + } + return this.getInputData(slot, force_update); + }; + + /** + * tells you if there is a connection in one input slot + * @method isInputConnected + * @param {number} slot + * @return {boolean} + */ + LGraphNode.prototype.isInputConnected = function (slot) { + if (!this.inputs) { + return false; + } + return slot < this.inputs.length && this.inputs[slot].link != null; + }; + + /** + * tells you info about an input connection (which node, type, etc) + * @method getInputInfo + * @param {number} slot + * @return {Object} object or null { link: id, name: string, type: string or 0 } + */ + LGraphNode.prototype.getInputInfo = function (slot) { + if (!this.inputs) { + return null; + } + if (slot < this.inputs.length) { + return this.inputs[slot]; + } + return null; + }; + + /** + * Returns the link info in the connection of an input slot + * @method getInputLink + * @param {number} slot + * @return {LLink} object or null + */ + LGraphNode.prototype.getInputLink = function (slot) { + if (!this.inputs) { + return null; + } + if (slot < this.inputs.length) { + var slot_info = this.inputs[slot]; + return this.graph.links[slot_info.link]; + } + return null; + }; + + /** + * returns the node connected in the input slot + * @method getInputNode + * @param {number} slot + * @return {LGraphNode} node or null + */ + LGraphNode.prototype.getInputNode = function (slot) { + if (!this.inputs) { + return null; + } + if (slot >= this.inputs.length) { + return null; + } + var input = this.inputs[slot]; + if (!input || input.link === null) { + return null; + } + var link_info = this.graph.links[input.link]; + if (!link_info) { + return null; + } + return this.graph.getNodeById(link_info.origin_id); + }; + + /** + * returns the value of an input with this name, otherwise checks if there is a property with that name + * @method getInputOrProperty + * @param {string} name + * @return {*} value + */ + LGraphNode.prototype.getInputOrProperty = function (name) { + if (!this.inputs || !this.inputs.length) { + return this.properties ? this.properties[name] : null; + } + + for (var i = 0, l = this.inputs.length; i < l; ++i) { + var input_info = this.inputs[i]; + if (name == input_info.name && input_info.link != null) { + var link = this.graph.links[input_info.link]; + if (link) { + return link.data; + } + } + } + return this.properties[name]; + }; + + /** + * tells you the last output data that went in that slot + * @method getOutputData + * @param {number} slot + * @return {Object} object or null + */ + LGraphNode.prototype.getOutputData = function (slot) { + if (!this.outputs) { + return null; + } + if (slot >= this.outputs.length) { + return null; + } + + var info = this.outputs[slot]; + return info._data; + }; + + /** + * tells you info about an output connection (which node, type, etc) + * @method getOutputInfo + * @param {number} slot + * @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] } + */ + LGraphNode.prototype.getOutputInfo = function (slot) { + if (!this.outputs) { + return null; + } + if (slot < this.outputs.length) { + return this.outputs[slot]; + } + return null; + }; + + /** + * tells you if there is a connection in one output slot + * @method isOutputConnected + * @param {number} slot + * @return {boolean} + */ + LGraphNode.prototype.isOutputConnected = function (slot) { + if (!this.outputs) { + return false; + } + return ( + slot < this.outputs.length && + this.outputs[slot].links && + this.outputs[slot].links.length + ); + }; + + /** + * tells you if there is any connection in the output slots + * @method isAnyOutputConnected + * @return {boolean} + */ + LGraphNode.prototype.isAnyOutputConnected = function () { + if (!this.outputs) { + return false; + } + for (var i = 0; i < this.outputs.length; ++i) { + if (this.outputs[i].links && this.outputs[i].links.length) { + return true; + } + } + return false; + }; + + /** + * retrieves all the nodes connected to this output slot + * @method getOutputNodes + * @param {number} slot + * @return {array} + */ + LGraphNode.prototype.getOutputNodes = function (slot) { + if (!this.outputs || this.outputs.length == 0) { + return null; + } + + if (slot >= this.outputs.length) { + return null; + } + + var output = this.outputs[slot]; + if (!output.links || output.links.length == 0) { + return null; + } + + var r = []; + for (var i = 0; i < output.links.length; i++) { + var link_id = output.links[i]; + var link = this.graph.links[link_id]; + if (link) { + var target_node = this.graph.getNodeById(link.target_id); + if (target_node) { + r.push(target_node); + } + } + } + return r; + }; + + LGraphNode.prototype.addOnTriggerInput = function () { + var trigS = this.findInputSlot("onTrigger"); + if (trigS == -1) { + //!trigS || + var input = this.addInput("onTrigger", LiteGraph.EVENT, { + optional: true, + nameLocked: true, + }); + return this.findInputSlot("onTrigger"); + } + return trigS; + }; + + LGraphNode.prototype.addOnExecutedOutput = function () { + var trigS = this.findOutputSlot("onExecuted"); + if (trigS == -1) { + //!trigS || + var output = this.addOutput("onExecuted", LiteGraph.ACTION, { + optional: true, + nameLocked: true, + }); + return this.findOutputSlot("onExecuted"); + } + return trigS; + }; + + LGraphNode.prototype.onAfterExecuteNode = function (param, options) { + var trigS = this.findOutputSlot("onExecuted"); + if (trigS != -1) { + //console.debug(this.id+":"+this.order+" triggering slot onAfterExecute"); + //console.debug(param); + //console.debug(options); + this.triggerSlot(trigS, param, null, options); + } + }; + + LGraphNode.prototype.changeMode = function (modeTo) { + switch (modeTo) { + case LiteGraph.ON_EVENT: + // this.addOnExecutedOutput(); + break; + + case LiteGraph.ON_TRIGGER: + this.addOnTriggerInput(); + this.addOnExecutedOutput(); + break; + + case LiteGraph.NEVER: + break; + + case LiteGraph.ALWAYS: + break; + + case LiteGraph.ON_REQUEST: + break; + + default: + return false; + break; + } + this.mode = modeTo; + return true; + }; + + /** + * Triggers the node code execution, place a boolean/counter to mark the node as being executed + * @method execute + * @param {*} param + * @param {*} options + */ + LGraphNode.prototype.doExecute = function (param, options) { + options = options || {}; + if (this.onExecute) { + // enable this to give the event an ID + if (!options.action_call) + options.action_call = + this.id + "_exec_" + Math.floor(Math.random() * 9999); + + this.graph.nodes_executing[this.id] = true; //.push(this.id); + + this.onExecute(param, options); + + this.graph.nodes_executing[this.id] = false; //.pop(); + + // save execution/action ref + this.exec_version = this.graph.iteration; + if (options && options.action_call) { + this.action_call = options.action_call; // if (param) + this.graph.nodes_executedAction[this.id] = options.action_call; + } + } + this.execute_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event + if (this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); // callback + }; + + /** + * Triggers an action, wrapped by logics to control execution flow + * @method actionDo + * @param {String} action name + * @param {*} param + */ + LGraphNode.prototype.actionDo = function (action, param, options) { + options = options || {}; + if (this.onAction) { + // enable this to give the event an ID + if (!options.action_call) + options.action_call = + this.id + + "_" + + (action ? action : "action") + + "_" + + Math.floor(Math.random() * 9999); + + this.graph.nodes_actioning[this.id] = action ? action : "actioning"; //.push(this.id); + + this.onAction(action, param, options); + + this.graph.nodes_actioning[this.id] = false; //.pop(); + + // save execution/action ref + if (options && options.action_call) { + this.action_call = options.action_call; // if (param) + this.graph.nodes_executedAction[this.id] = options.action_call; + } + } + this.action_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event + if (this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); + }; + + /** + * Triggers an event in this node, this will trigger any output with the same name + * @method trigger + * @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all + * @param {*} param + */ + LGraphNode.prototype.trigger = function (action, param, options) { + if (!this.outputs || !this.outputs.length) { + return; + } + + if (this.graph) this.graph._last_trigger_time = LiteGraph.getTime(); + + for (var i = 0; i < this.outputs.length; ++i) { + var output = this.outputs[i]; + if ( + !output || + output.type !== LiteGraph.EVENT || + (action && output.name != action) + ) + continue; + this.triggerSlot(i, param, null, options); + } + }; + + /** + * Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes + * @method triggerSlot + * @param {Number} slot the index of the output slot + * @param {*} param + * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot + */ + LGraphNode.prototype.triggerSlot = function (slot, param, link_id, options) { + options = options || {}; + if (!this.outputs) { + return; + } + + if (slot == null) { + console.error("slot must be a number"); + return; + } + + if (slot.constructor !== Number) + console.warn( + "slot must be a number, use node.trigger('name') if you want to use a string" + ); + + var output = this.outputs[slot]; + if (!output) { + return; + } + + var links = output.links; + if (!links || !links.length) { + return; + } + + if (this.graph) { + this.graph._last_trigger_time = LiteGraph.getTime(); + } + + //for every link attached here + for (var k = 0; k < links.length; ++k) { + var id = links[k]; + if (link_id != null && link_id != id) { + //to skip links + continue; + } + var link_info = this.graph.links[links[k]]; + if (!link_info) { + //not connected + continue; + } + link_info._last_time = LiteGraph.getTime(); + var node = this.graph.getNodeById(link_info.target_id); + if (!node) { + //node not found? + continue; + } + + //used to mark events in graph + var target_connection = node.inputs[link_info.target_slot]; + + if (node.mode === LiteGraph.ON_TRIGGER) { + // generate unique trigger ID if not present + if (!options.action_call) + options.action_call = + this.id + "_trigg_" + Math.floor(Math.random() * 9999); + if (node.onExecute) { + // -- wrapping node.onExecute(param); -- + node.doExecute(param, options); + } + } else if (node.onAction) { + // generate unique action ID if not present + if (!options.action_call) + options.action_call = + this.id + "_act_" + Math.floor(Math.random() * 9999); + //pass the action name + var target_connection = node.inputs[link_info.target_slot]; + // wrap node.onAction(target_connection.name, param); + node.actionDo(target_connection.name, param, options); + } + } + }; + + /** + * clears the trigger slot animation + * @method clearTriggeredSlot + * @param {Number} slot the index of the output slot + * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot + */ + LGraphNode.prototype.clearTriggeredSlot = function (slot, link_id) { + if (!this.outputs) { + return; + } + + var output = this.outputs[slot]; + if (!output) { + return; + } + + var links = output.links; + if (!links || !links.length) { + return; + } + + //for every link attached here + for (var k = 0; k < links.length; ++k) { + var id = links[k]; + if (link_id != null && link_id != id) { + //to skip links + continue; + } + var link_info = this.graph.links[links[k]]; + if (!link_info) { + //not connected + continue; + } + link_info._last_time = 0; + } + }; + + /** + * changes node size and triggers callback + * @method setSize + * @param {vec2} size + */ + LGraphNode.prototype.setSize = function (size) { + this.size = size; + if (this.onResize) this.onResize(this.size); + }; + + /** + * add a new property to this node + * @method addProperty + * @param {string} name + * @param {*} default_value + * @param {string} type string defining the output type ("vec3","number",...) + * @param {Object} extra_info this can be used to have special properties of the property (like values, etc) + */ + LGraphNode.prototype.addProperty = function ( + name, + default_value, + type, + extra_info + ) { + var o = { name: name, type: type, default_value: default_value }; + if (extra_info) { + for (var i in extra_info) { + o[i] = extra_info[i]; + } + } + if (!this.properties_info) { + this.properties_info = []; + } + this.properties_info.push(o); + if (!this.properties) { + this.properties = {}; + } + this.properties[name] = default_value; + return o; + }; + + //connections + + /** + * add a new output slot to use in this node + * @method addOutput + * @param {string} name + * @param {string} type string defining the output type ("vec3","number",...) + * @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc) + */ + LGraphNode.prototype.addOutput = function (name, type, extra_info) { + var output = { name: name, type: type, links: null }; + if (extra_info) { + for (var i in extra_info) { + output[i] = extra_info[i]; + } + } + + if (!this.outputs) { + this.outputs = []; + } + this.outputs.push(output); + if (this.onOutputAdded) { + this.onOutputAdded(output); + } + + if (LiteGraph.auto_load_slot_types) + LiteGraph.registerNodeAndSlotType(this, type, true); + + this.setSize(this.computeSize()); + this.setDirtyCanvas(true, true); + return output; + }; + + /** + * add a new output slot to use in this node + * @method addOutputs + * @param {Array} array of triplets like [[name,type,extra_info],[...]] + */ + LGraphNode.prototype.addOutputs = function (array) { + for (var i = 0; i < array.length; ++i) { + var info = array[i]; + var o = { name: info[0], type: info[1], link: null }; + if (array[2]) { + for (var j in info[2]) { + o[j] = info[2][j]; + } + } + + if (!this.outputs) { + this.outputs = []; + } + this.outputs.push(o); + if (this.onOutputAdded) { + this.onOutputAdded(o); + } + + if (LiteGraph.auto_load_slot_types) + LiteGraph.registerNodeAndSlotType(this, info[1], true); + } + + this.setSize(this.computeSize()); + this.setDirtyCanvas(true, true); + }; + + /** + * remove an existing output slot + * @method removeOutput + * @param {number} slot + */ + LGraphNode.prototype.removeOutput = function (slot) { + this.disconnectOutput(slot); + this.outputs.splice(slot, 1); + for (var i = slot; i < this.outputs.length; ++i) { + if (!this.outputs[i] || !this.outputs[i].links) { + continue; + } + var links = this.outputs[i].links; + for (var j = 0; j < links.length; ++j) { + var link = this.graph.links[links[j]]; + if (!link) { + continue; + } + link.origin_slot -= 1; + } + } + + this.setSize(this.computeSize()); + if (this.onOutputRemoved) { + this.onOutputRemoved(slot); + } + this.setDirtyCanvas(true, true); + }; + + /** + * add a new input slot to use in this node + * @method addInput + * @param {string} name + * @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0 + * @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc) + */ + LGraphNode.prototype.addInput = function (name, type, extra_info) { + type = type || 0; + var input = { name: name, type: type, link: null }; + if (extra_info) { + for (var i in extra_info) { + input[i] = extra_info[i]; + } + } + + if (!this.inputs) { + this.inputs = []; + } + + this.inputs.push(input); + this.setSize(this.computeSize()); + + if (this.onInputAdded) { + this.onInputAdded(input); + } + + LiteGraph.registerNodeAndSlotType(this, type); + + this.setDirtyCanvas(true, true); + return input; + }; + + /** + * add several new input slots in this node + * @method addInputs + * @param {Array} array of triplets like [[name,type,extra_info],[...]] + */ + LGraphNode.prototype.addInputs = function (array) { + for (var i = 0; i < array.length; ++i) { + var info = array[i]; + var o = { name: info[0], type: info[1], link: null }; + if (array[2]) { + for (var j in info[2]) { + o[j] = info[2][j]; + } + } + + if (!this.inputs) { + this.inputs = []; + } + this.inputs.push(o); + if (this.onInputAdded) { + this.onInputAdded(o); + } + + LiteGraph.registerNodeAndSlotType(this, info[1]); + } + + this.setSize(this.computeSize()); + this.setDirtyCanvas(true, true); + }; + + /** + * remove an existing input slot + * @method removeInput + * @param {number} slot + */ + LGraphNode.prototype.removeInput = function (slot) { + this.disconnectInput(slot); + var slot_info = this.inputs.splice(slot, 1); + for (var i = slot; i < this.inputs.length; ++i) { + if (!this.inputs[i]) { + continue; + } + var link = this.graph.links[this.inputs[i].link]; + if (!link) { + continue; + } + link.target_slot -= 1; + } + this.setSize(this.computeSize()); + if (this.onInputRemoved) { + this.onInputRemoved(slot, slot_info[0]); + } + this.setDirtyCanvas(true, true); + }; + + /** + * add an special connection to this node (used for special kinds of graphs) + * @method addConnection + * @param {string} name + * @param {string} type string defining the input type ("vec3","number",...) + * @param {[x,y]} pos position of the connection inside the node + * @param {string} direction if is input or output + */ + LGraphNode.prototype.addConnection = function (name, type, pos, direction) { + var o = { + name: name, + type: type, + pos: pos, + direction: direction, + links: null, + }; + this.connections.push(o); + return o; + }; + + /** + * computes the minimum size of a node according to its inputs and output slots + * @method computeSize + * @param {vec2} minHeight + * @return {vec2} the total size + */ + LGraphNode.prototype.computeSize = function (out) { + if (this.constructor.size) { + return this.constructor.size.concat(); + } + + var rows = Math.max( + this.inputs ? this.inputs.length : 1, + this.outputs ? this.outputs.length : 1 + ); + var size = out || new Float32Array([0, 0]); + rows = Math.max(rows, 1); + var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size + + var title_width = compute_text_size(this.title); + var input_width = 0; + var output_width = 0; + + if (this.inputs) { + for (var i = 0, l = this.inputs.length; i < l; ++i) { + var input = this.inputs[i]; + var text = input.label || input.name || ""; + var text_width = compute_text_size(text); + if (input_width < text_width) { + input_width = text_width; + } + } + } + + if (this.outputs) { + for (var i = 0, l = this.outputs.length; i < l; ++i) { + var output = this.outputs[i]; + var text = output.label || output.name || ""; + var text_width = compute_text_size(text); + if (output_width < text_width) { + output_width = text_width; + } + } + } + + size[0] = Math.max(input_width + output_width + 10, title_width); + size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH); + if (this.widgets && this.widgets.length) { + size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5); + } + + size[1] = + (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; + + var widgets_height = 0; + if (this.widgets && this.widgets.length) { + for (var i = 0, l = this.widgets.length; i < l; ++i) { + if (this.widgets[i].computeSize) + widgets_height += this.widgets[i].computeSize(size[0])[1] + 4; + else widgets_height += LiteGraph.NODE_WIDGET_HEIGHT + 4; + } + widgets_height += 8; + } + + //compute height using widgets height + if (this.widgets_up) size[1] = Math.max(size[1], widgets_height); + else if (this.widgets_start_y != null) + size[1] = Math.max(size[1], widgets_height + this.widgets_start_y); + else size[1] += widgets_height; + + function compute_text_size(text) { + if (!text) { + return 0; + } + return font_size * text.length * 0.6; + } + + if (this.constructor.min_height && size[1] < this.constructor.min_height) { + size[1] = this.constructor.min_height; + } + + size[1] += 6; //margin + + return size; + }; + + LGraphNode.prototype.inResizeCorner = function (canvasX, canvasY) { + var rows = this.outputs ? this.outputs.length : 1; + var outputs_offset = + (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; + return isInsideRectangle( + canvasX, + canvasY, + this.pos[0] + this.size[0] - 15, + this.pos[1] + Math.max(this.size[1] - 15, outputs_offset), + 20, + 20 + ); + }; + + /** + * returns all the info available about a property of this node. + * + * @method getPropertyInfo + * @param {String} property name of the property + * @return {Object} the object with all the available info + */ + LGraphNode.prototype.getPropertyInfo = function (property) { + var info = null; + + //there are several ways to define info about a property + //legacy mode + if (this.properties_info) { + for (var i = 0; i < this.properties_info.length; ++i) { + if (this.properties_info[i].name == property) { + info = this.properties_info[i]; + break; + } + } + } + //litescene mode using the constructor + if (this.constructor["@" + property]) + info = this.constructor["@" + property]; + + if ( + this.constructor.widgets_info && + this.constructor.widgets_info[property] + ) + info = this.constructor.widgets_info[property]; + + //litescene mode using the constructor + if (!info && this.onGetPropertyInfo) { + info = this.onGetPropertyInfo(property); + } + + if (!info) info = {}; + if (!info.type) info.type = typeof this.properties[property]; + if (info.widget == "combo") info.type = "enum"; + + return info; + }; + + /** + * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties + * + * @method addWidget + * @param {String} type the widget type (could be "number","string","combo" + * @param {String} name the text to show on the widget + * @param {String} value the default value + * @param {Function|String} callback function to call when it changes (optionally, it can be the name of the property to modify) + * @param {Object} options the object that contains special properties of this widget + * @return {Object} the created widget object + */ + LGraphNode.prototype.addWidget = function ( + type, + name, + value, + callback, + options + ) { + if (!this.widgets) { + this.widgets = []; + } + + if (!options && callback && callback.constructor === Object) { + options = callback; + callback = null; + } + + if (options && options.constructor === String) + //options can be the property name + options = { property: options }; + + if (callback && callback.constructor === String) { + //callback can be the property name + if (!options) options = {}; + options.property = callback; + callback = null; + } + + if (callback && callback.constructor !== Function) { + console.warn("addWidget: callback must be a function"); + callback = null; + } + + var w = { + type: type.toLowerCase(), + name: name, + value: value, + callback: callback, + options: options || {}, + }; + + if (w.options.y !== undefined) { + w.y = w.options.y; + } + + if (!callback && !w.options.callback && !w.options.property) { + console.warn( + "LiteGraph addWidget(...) without a callback or property assigned" + ); + } + if (type == "combo" && !w.options.values) { + throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }"; + } + this.widgets.push(w); + this.setSize(this.computeSize()); + return w; + }; + + LGraphNode.prototype.addCustomWidget = function (custom_widget) { + if (!this.widgets) { + this.widgets = []; + } + this.widgets.push(custom_widget); + return custom_widget; + }; + + /** + * returns the bounding of the object, used for rendering purposes + * @method getBounding + * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage + * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation + * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] + */ + LGraphNode.prototype.getBounding = function (out, compute_outer) { + out = out || new Float32Array(4); + const nodePos = this.pos; + const isCollapsed = this.flags.collapsed; + const nodeSize = this.size; + + let left_offset = 0; + // 1 offset due to how nodes are rendered + let right_offset = 1; + let top_offset = 0; + let bottom_offset = 0; + + if (compute_outer) { + // 4 offset for collapsed node connection points + left_offset = 4; + // 6 offset for right shadow and collapsed node connection points + right_offset = 6 + left_offset; + // 4 offset for collapsed nodes top connection points + top_offset = 4; + // 5 offset for bottom shadow and collapsed node connection points + bottom_offset = 5 + top_offset; + } + + out[0] = nodePos[0] - left_offset; + out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset; + out[2] = isCollapsed + ? (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset + : nodeSize[0] + right_offset; + out[3] = isCollapsed + ? LiteGraph.NODE_TITLE_HEIGHT + bottom_offset + : nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset; + + if (this.onBounding) { + this.onBounding(out); + } + return out; + }; + + /** + * checks if a point is inside the shape of a node + * @method isPointInside + * @param {number} x + * @param {number} y + * @return {boolean} + */ + LGraphNode.prototype.isPointInside = function (x, y, margin, skip_title) { + margin = margin || 0; + + var margin_top = + this.graph && this.graph.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT; + if (skip_title) { + margin_top = 0; + } + if (this.flags && this.flags.collapsed) { + //if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS) + if ( + isInsideRectangle( + x, + y, + this.pos[0] - margin, + this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin, + (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + + 2 * margin, + LiteGraph.NODE_TITLE_HEIGHT + 2 * margin + ) + ) { + return true; + } + } else if ( + this.pos[0] - 4 - margin < x && + this.pos[0] + this.size[0] + 4 + margin > x && + this.pos[1] - margin_top - margin < y && + this.pos[1] + this.size[1] + margin > y ) { - if (!this.graph) { - return; + return true; + } + return false; + }; + + /** + * checks if a point is inside a node slot, and returns info about which slot + * @method getSlotInPosition + * @param {number} x + * @param {number} y + * @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] } + */ + LGraphNode.prototype.getSlotInPosition = function (x, y) { + //search for inputs + var link_pos = new Float32Array(2); + if (this.inputs) { + for (var i = 0, l = this.inputs.length; i < l; ++i) { + var input = this.inputs[i]; + this.getConnectionPos(true, i, link_pos); + if ( + isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20, 10) + ) { + return { input: input, slot: i, link_pos: link_pos }; } - this.graph.sendActionToCanvas("setDirty", [ - dirty_foreground, - dirty_background - ]); + } + } + + if (this.outputs) { + for (var i = 0, l = this.outputs.length; i < l; ++i) { + var output = this.outputs[i]; + this.getConnectionPos(false, i, link_pos); + if ( + isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20, 10) + ) { + return { output: output, slot: i, link_pos: link_pos }; + } + } + } + + return null; + }; + + /** + * returns the input slot with a given name (used for dynamic slots), -1 if not found + * @method findInputSlot + * @param {string} name the name of the slot + * @param {boolean} returnObj if the obj itself wanted + * @return {number_or_object} the slot (-1 if not found) + */ + LGraphNode.prototype.findInputSlot = function (name, returnObj) { + if (!this.inputs) { + return -1; + } + for (var i = 0, l = this.inputs.length; i < l; ++i) { + if (name == this.inputs[i].name) { + return !returnObj ? i : this.inputs[i]; + } + } + return -1; + }; + + /** + * returns the output slot with a given name (used for dynamic slots), -1 if not found + * @method findOutputSlot + * @param {string} name the name of the slot + * @param {boolean} returnObj if the obj itself wanted + * @return {number_or_object} the slot (-1 if not found) + */ + LGraphNode.prototype.findOutputSlot = function (name, returnObj) { + returnObj = returnObj || false; + if (!this.outputs) { + return -1; + } + for (var i = 0, l = this.outputs.length; i < l; ++i) { + if (name == this.outputs[i].name) { + return !returnObj ? i : this.outputs[i]; + } + } + return -1; + }; + + // TODO refactor: USE SINGLE findInput/findOutput functions! :: merge options + + /** + * returns the first free input slot + * @method findInputSlotFree + * @param {object} options + * @return {number_or_object} the slot (-1 if not found) + */ + LGraphNode.prototype.findInputSlotFree = function (optsIn) { + var optsIn = optsIn || {}; + var optsDef = { returnObj: false, typesNotAccepted: [] }; + var opts = Object.assign(optsDef, optsIn); + if (!this.inputs) { + return -1; + } + for (var i = 0, l = this.inputs.length; i < l; ++i) { + if (this.inputs[i].link && this.inputs[i].link != null) { + continue; + } + if ( + opts.typesNotAccepted && + opts.typesNotAccepted.includes && + opts.typesNotAccepted.includes(this.inputs[i].type) + ) { + continue; + } + return !opts.returnObj ? i : this.inputs[i]; + } + return -1; + }; + + /** + * returns the first output slot free + * @method findOutputSlotFree + * @param {object} options + * @return {number_or_object} the slot (-1 if not found) + */ + LGraphNode.prototype.findOutputSlotFree = function (optsIn) { + var optsIn = optsIn || {}; + var optsDef = { returnObj: false, typesNotAccepted: [] }; + var opts = Object.assign(optsDef, optsIn); + if (!this.outputs) { + return -1; + } + for (var i = 0, l = this.outputs.length; i < l; ++i) { + if (this.outputs[i].links && this.outputs[i].links != null) { + continue; + } + if ( + opts.typesNotAccepted && + opts.typesNotAccepted.includes && + opts.typesNotAccepted.includes(this.outputs[i].type) + ) { + continue; + } + return !opts.returnObj ? i : this.outputs[i]; + } + return -1; + }; + + /** + * findSlotByType for INPUTS + */ + LGraphNode.prototype.findInputSlotByType = function ( + type, + returnObj, + preferFreeSlot, + doNotUseOccupied + ) { + return this.findSlotByType( + true, + type, + returnObj, + preferFreeSlot, + doNotUseOccupied + ); + }; + + /** + * findSlotByType for OUTPUTS + */ + LGraphNode.prototype.findOutputSlotByType = function ( + type, + returnObj, + preferFreeSlot, + doNotUseOccupied + ) { + return this.findSlotByType( + false, + type, + returnObj, + preferFreeSlot, + doNotUseOccupied + ); + }; + + /** + * returns the output (or input) slot with a given type, -1 if not found + * @method findSlotByType + * @param {boolean} input uise inputs instead of outputs + * @param {string} type the type of the slot + * @param {boolean} returnObj if the obj itself wanted + * @param {boolean} preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway) + * @return {number_or_object} the slot (-1 if not found) + */ + LGraphNode.prototype.findSlotByType = function ( + input, + type, + returnObj, + preferFreeSlot, + doNotUseOccupied + ) { + input = input || false; + returnObj = returnObj || false; + preferFreeSlot = preferFreeSlot || false; + doNotUseOccupied = doNotUseOccupied || false; + var aSlots = input ? this.inputs : this.outputs; + if (!aSlots) { + return -1; + } + // !! empty string type is considered 0, * !! + if (type == "" || type == "*") type = 0; + for (var i = 0, l = aSlots.length; i < l; ++i) { + var tFound = false; + var aSource = (type + "").toLowerCase().split(","); + var aDest = + aSlots[i].type == "0" || aSlots[i].type == "*" ? "0" : aSlots[i].type; + aDest = (aDest + "").toLowerCase().split(","); + for (var sI = 0; sI < aSource.length; sI++) { + for (var dI = 0; dI < aDest.length; dI++) { + if (aSource[sI] == "_event_") aSource[sI] = LiteGraph.EVENT; + if (aDest[sI] == "_event_") aDest[sI] = LiteGraph.EVENT; + if (aSource[sI] == "*") aSource[sI] = 0; + if (aDest[sI] == "*") aDest[sI] = 0; + if (aSource[sI] == aDest[dI]) { + if (preferFreeSlot && aSlots[i].links && aSlots[i].links !== null) + continue; + return !returnObj ? i : aSlots[i]; + } + } + } + } + // if didnt find some, stop checking for free slots + if (preferFreeSlot && !doNotUseOccupied) { + for (var i = 0, l = aSlots.length; i < l; ++i) { + var tFound = false; + var aSource = (type + "").toLowerCase().split(","); + var aDest = + aSlots[i].type == "0" || aSlots[i].type == "*" ? "0" : aSlots[i].type; + aDest = (aDest + "").toLowerCase().split(","); + for (var sI = 0; sI < aSource.length; sI++) { + for (var dI = 0; dI < aDest.length; dI++) { + if (aSource[sI] == "*") aSource[sI] = 0; + if (aDest[sI] == "*") aDest[sI] = 0; + if (aSource[sI] == aDest[dI]) { + return !returnObj ? i : aSlots[i]; + } + } + } + } + } + return -1; + }; + + /** + * connect this node output to the input of another node BY TYPE + * @method connectByType + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @param {LGraphNode} node the target node + * @param {string} target_type the input slot type of the target node + * @return {Object} the link_info is created, otherwise null + */ + LGraphNode.prototype.connectByType = function ( + slot, + target_node, + target_slotType, + optsIn + ) { + var optsIn = optsIn || {}; + var optsDef = { + createEventInCase: true, + firstFreeIfOutputGeneralInCase: true, + generalTypeInCase: true, }; + var opts = Object.assign(optsDef, optsIn); + if (target_node && target_node.constructor === Number) { + target_node = this.graph.getNodeById(target_node); + } + var target_slot = target_node.findInputSlotByType( + target_slotType, + false, + true + ); + if (target_slot >= 0 && target_slot !== null) { + //console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot) + return this.connect(slot, target_node, target_slot); + } else { + //console.log("type "+target_slotType+" not found or not free?") + if (opts.createEventInCase && target_slotType == LiteGraph.EVENT) { + // WILL CREATE THE onTrigger IN SLOT + //console.debug("connect WILL CREATE THE onTrigger "+target_slotType+" to "+target_node); + return this.connect(slot, target_node, -1); + } + // connect to the first general output slot if not found a specific type and + if (opts.generalTypeInCase) { + var target_slot = target_node.findInputSlotByType(0, false, true, true); + //console.debug("connect TO a general type (*, 0), if not found the specific type ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); + if (target_slot >= 0) { + return this.connect(slot, target_node, target_slot); + } + } + // connect to the first free input slot if not found a specific type and this output is general + if ( + opts.firstFreeIfOutputGeneralInCase && + (target_slotType == 0 || + target_slotType == "*" || + target_slotType == "") + ) { + var target_slot = target_node.findInputSlotFree({ + typesNotAccepted: [LiteGraph.EVENT], + }); + //console.debug("connect TO TheFirstFREE ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); + if (target_slot >= 0) { + return this.connect(slot, target_node, target_slot); + } + } - LGraphNode.prototype.loadImage = function(url) { - var img = new Image(); - img.src = LiteGraph.node_images_path + url; - img.ready = false; + console.debug( + "no way to connect type: ", + target_slotType, + " to targetNODE ", + target_node + ); + //TODO filter - var that = this; - img.onload = function() { - this.ready = true; - that.setDirtyCanvas(true); - }; - return img; + return null; + } + }; + + /** + * connect this node input to the output of another node BY TYPE + * @method connectByType + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @param {LGraphNode} node the target node + * @param {string} target_type the output slot type of the target node + * @return {Object} the link_info is created, otherwise null + */ + LGraphNode.prototype.connectByTypeOutput = function ( + slot, + source_node, + source_slotType, + optsIn + ) { + var optsIn = optsIn || {}; + var optsDef = { + createEventInCase: true, + firstFreeIfInputGeneralInCase: true, + generalTypeInCase: true, }; + var opts = Object.assign(optsDef, optsIn); + if (source_node && source_node.constructor === Number) { + source_node = this.graph.getNodeById(source_node); + } + var source_slot = source_node.findOutputSlotByType( + source_slotType, + false, + true + ); + if (source_slot >= 0 && source_slot !== null) { + //console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot) + return source_node.connect(source_slot, this, slot); + } else { + // connect to the first general output slot if not found a specific type and + if (opts.generalTypeInCase) { + var source_slot = source_node.findOutputSlotByType( + 0, + false, + true, + true + ); + if (source_slot >= 0) { + return source_node.connect(source_slot, this, slot); + } + } - //safe LGraphNode action execution (not sure if safe) - /* + if (opts.createEventInCase && source_slotType == LiteGraph.EVENT) { + // WILL CREATE THE onExecuted OUT SLOT + if (LiteGraph.do_add_triggers_slots) { + var source_slot = source_node.addOnExecutedOutput(); + return source_node.connect(source_slot, this, slot); + } + } + // connect to the first free output slot if not found a specific type and this input is general + if ( + opts.firstFreeIfInputGeneralInCase && + (source_slotType == 0 || + source_slotType == "*" || + source_slotType == "") + ) { + var source_slot = source_node.findOutputSlotFree({ + typesNotAccepted: [LiteGraph.EVENT], + }); + if (source_slot >= 0) { + return source_node.connect(source_slot, this, slot); + } + } + + console.debug( + "no way to connect byOUT type: ", + source_slotType, + " to sourceNODE ", + source_node + ); + //TODO filter + + //console.log("type OUT! "+source_slotType+" not found or not free?") + return null; + } + }; + + /** + * connect this node output to the input of another node + * @method connect + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @param {LGraphNode} node the target node + * @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) + * @return {Object} the link_info is created, otherwise null + */ + LGraphNode.prototype.connect = function (slot, target_node, target_slot) { + target_slot = target_slot || 0; + + if (!this.graph) { + //could be connected before adding it to a graph + console.log( + "Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them." + ); //due to link ids being associated with graphs + return null; + } + + //seek for the output slot + if (slot.constructor === String) { + slot = this.findOutputSlot(slot); + if (slot == -1) { + if (LiteGraph.debug) { + console.log("Connect: Error, no slot of name " + slot); + } + return null; + } + } else if (!this.outputs || slot >= this.outputs.length) { + if (LiteGraph.debug) { + console.log("Connect: Error, slot number not found"); + } + return null; + } + + if (target_node && target_node.constructor === Number) { + target_node = this.graph.getNodeById(target_node); + } + if (!target_node) { + throw "target node is null"; + } + + //avoid loopback + if (target_node == this) { + return null; + } + + //you can specify the slot by name + if (target_slot.constructor === String) { + target_slot = target_node.findInputSlot(target_slot); + if (target_slot == -1) { + if (LiteGraph.debug) { + console.log("Connect: Error, no slot of name " + target_slot); + } + return null; + } + } else if (target_slot === LiteGraph.EVENT) { + if (LiteGraph.do_add_triggers_slots) { + //search for first slot with event? :: NO this is done outside + //console.log("Connect: Creating triggerEvent"); + // force mode + target_node.changeMode(LiteGraph.ON_TRIGGER); + target_slot = target_node.findInputSlot("onTrigger"); + } else { + return null; // -- break -- + } + } else if ( + !target_node.inputs || + target_slot >= target_node.inputs.length + ) { + if (LiteGraph.debug) { + console.log("Connect: Error, slot number not found"); + } + return null; + } + + var changed = false; + + var input = target_node.inputs[target_slot]; + var link_info = null; + var output = this.outputs[slot]; + + if (!this.outputs[slot]) { + /*console.debug("Invalid slot passed: "+slot); + console.debug(this.outputs);*/ + return null; + } + + // allow target node to change slot + if (target_node.onBeforeConnectInput) { + // This way node can choose another slot (or make a new one?) + target_slot = target_node.onBeforeConnectInput(target_slot); //callback + } + + //check target_slot and check connection types + if ( + target_slot === false || + target_slot === null || + !LiteGraph.isValidConnection(output.type, input.type) + ) { + this.setDirtyCanvas(false, true); + if (changed) this.graph.connectionChange(this, link_info); + return null; + } else { + //console.debug("valid connection",output.type, input.type); + } + + //allows nodes to block connection, callback + if (target_node.onConnectInput) { + if ( + target_node.onConnectInput( + target_slot, + output.type, + output, + this, + slot + ) === false + ) { + return null; + } + } + if (this.onConnectOutput) { + // callback + if ( + this.onConnectOutput( + slot, + input.type, + input, + target_node, + target_slot + ) === false + ) { + return null; + } + } + + //if there is something already plugged there, disconnect + if ( + target_node.inputs[target_slot] && + target_node.inputs[target_slot].link != null + ) { + this.graph.beforeChange(); + target_node.disconnectInput(target_slot, { doProcessChange: false }); + changed = true; + } + if (output.links !== null && output.links.length) { + switch (output.type) { + case LiteGraph.EVENT: + if (!LiteGraph.allow_multi_output_for_events) { + this.graph.beforeChange(); + this.disconnectOutput(slot, false, { doProcessChange: false }); // Input(target_slot, {doProcessChange: false}); + changed = true; + } + break; + default: + break; + } + } + + var nextId; + if (LiteGraph.use_uuids) nextId = LiteGraph.uuidv4(); + else nextId = ++this.graph.last_link_id; + + //create link class + link_info = new LLink( + nextId, + input.type || output.type, + this.id, + slot, + target_node.id, + target_slot + ); + + //add to graph links list + this.graph.links[link_info.id] = link_info; + + //connect in output + if (output.links == null) { + output.links = []; + } + output.links.push(link_info.id); + //connect in input + target_node.inputs[target_slot].link = link_info.id; + if (this.graph) { + this.graph._version++; + } + if (this.onConnectionsChange) { + this.onConnectionsChange(LiteGraph.OUTPUT, slot, true, link_info, output); + } //link_info has been created now, so its updated + if (target_node.onConnectionsChange) { + target_node.onConnectionsChange( + LiteGraph.INPUT, + target_slot, + true, + link_info, + input + ); + } + if (this.graph && this.graph.onNodeConnectionChange) { + this.graph.onNodeConnectionChange( + LiteGraph.INPUT, + target_node, + target_slot, + this, + slot + ); + this.graph.onNodeConnectionChange( + LiteGraph.OUTPUT, + this, + slot, + target_node, + target_slot + ); + } + + this.setDirtyCanvas(false, true); + this.graph.afterChange(); + this.graph.connectionChange(this, link_info); + + return link_info; + }; + + /** + * disconnect one output to an specific node + * @method disconnectOutput + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] + * @return {boolean} if it was disconnected successfully + */ + LGraphNode.prototype.disconnectOutput = function (slot, target_node) { + if (slot.constructor === String) { + slot = this.findOutputSlot(slot); + if (slot == -1) { + if (LiteGraph.debug) { + console.log("Connect: Error, no slot of name " + slot); + } + return false; + } + } else if (!this.outputs || slot >= this.outputs.length) { + if (LiteGraph.debug) { + console.log("Connect: Error, slot number not found"); + } + return false; + } + + //get output slot + var output = this.outputs[slot]; + if (!output || !output.links || output.links.length == 0) { + return false; + } + + //one of the output links in this slot + if (target_node) { + if (target_node.constructor === Number) { + target_node = this.graph.getNodeById(target_node); + } + if (!target_node) { + throw "Target Node not found"; + } + + for (var i = 0, l = output.links.length; i < l; i++) { + var link_id = output.links[i]; + var link_info = this.graph.links[link_id]; + + //is the link we are searching for... + if (link_info.target_id == target_node.id) { + output.links.splice(i, 1); //remove here + var input = target_node.inputs[link_info.target_slot]; + input.link = null; //remove there + delete this.graph.links[link_id]; //remove the link from the links pool + if (this.graph) { + this.graph._version++; + } + if (target_node.onConnectionsChange) { + target_node.onConnectionsChange( + LiteGraph.INPUT, + link_info.target_slot, + false, + link_info, + input + ); + } //link_info hasn't been modified so its ok + if (this.onConnectionsChange) { + this.onConnectionsChange( + LiteGraph.OUTPUT, + slot, + false, + link_info, + output + ); + } + if (this.graph && this.graph.onNodeConnectionChange) { + this.graph.onNodeConnectionChange(LiteGraph.OUTPUT, this, slot); + } + if (this.graph && this.graph.onNodeConnectionChange) { + this.graph.onNodeConnectionChange(LiteGraph.OUTPUT, this, slot); + this.graph.onNodeConnectionChange( + LiteGraph.INPUT, + target_node, + link_info.target_slot + ); + } + break; + } + } + } //all the links in this output slot + else { + for (var i = 0, l = output.links.length; i < l; i++) { + var link_id = output.links[i]; + var link_info = this.graph.links[link_id]; + if (!link_info) { + //bug: it happens sometimes + continue; + } + + var target_node = this.graph.getNodeById(link_info.target_id); + var input = null; + if (this.graph) { + this.graph._version++; + } + if (target_node) { + input = target_node.inputs[link_info.target_slot]; + input.link = null; //remove other side link + if (target_node.onConnectionsChange) { + target_node.onConnectionsChange( + LiteGraph.INPUT, + link_info.target_slot, + false, + link_info, + input + ); + } //link_info hasn't been modified so its ok + if (this.graph && this.graph.onNodeConnectionChange) { + this.graph.onNodeConnectionChange( + LiteGraph.INPUT, + target_node, + link_info.target_slot + ); + } + } + delete this.graph.links[link_id]; //remove the link from the links pool + if (this.onConnectionsChange) { + this.onConnectionsChange( + LiteGraph.OUTPUT, + slot, + false, + link_info, + output + ); + } + if (this.graph && this.graph.onNodeConnectionChange) { + this.graph.onNodeConnectionChange(LiteGraph.OUTPUT, this, slot); + this.graph.onNodeConnectionChange( + LiteGraph.INPUT, + target_node, + link_info.target_slot + ); + } + } + output.links = null; + } + + this.setDirtyCanvas(false, true); + this.graph.connectionChange(this); + return true; + }; + + /** + * disconnect one input + * @method disconnectInput + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @return {boolean} if it was disconnected successfully + */ + LGraphNode.prototype.disconnectInput = function (slot) { + //seek for the output slot + if (slot.constructor === String) { + slot = this.findInputSlot(slot); + if (slot == -1) { + if (LiteGraph.debug) { + console.log("Connect: Error, no slot of name " + slot); + } + return false; + } + } else if (!this.inputs || slot >= this.inputs.length) { + if (LiteGraph.debug) { + console.log("Connect: Error, slot number not found"); + } + return false; + } + + var input = this.inputs[slot]; + if (!input) { + return false; + } + + var link_id = this.inputs[slot].link; + if (link_id != null) { + this.inputs[slot].link = null; + + //remove other side + var link_info = this.graph.links[link_id]; + if (link_info) { + var target_node = this.graph.getNodeById(link_info.origin_id); + if (!target_node) { + return false; + } + + var output = target_node.outputs[link_info.origin_slot]; + if (!output || !output.links || output.links.length == 0) { + return false; + } + + //search in the inputs list for this link + for (var i = 0, l = output.links.length; i < l; i++) { + if (output.links[i] == link_id) { + output.links.splice(i, 1); + break; + } + } + + delete this.graph.links[link_id]; //remove from the pool + if (this.graph) { + this.graph._version++; + } + if (this.onConnectionsChange) { + this.onConnectionsChange( + LiteGraph.INPUT, + slot, + false, + link_info, + input + ); + } + if (target_node.onConnectionsChange) { + target_node.onConnectionsChange( + LiteGraph.OUTPUT, + i, + false, + link_info, + output + ); + } + if (this.graph && this.graph.onNodeConnectionChange) { + this.graph.onNodeConnectionChange(LiteGraph.OUTPUT, target_node, i); + this.graph.onNodeConnectionChange(LiteGraph.INPUT, this, slot); + } + } + } //link != null + + this.setDirtyCanvas(false, true); + if (this.graph) this.graph.connectionChange(this); + return true; + }; + + /** + * returns the center of a connection point in canvas coords + * @method getConnectionPos + * @param {boolean} is_input true if if a input slot, false if it is an output + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @param {vec2} out [optional] a place to store the output, to free garbage + * @return {[x,y]} the position + **/ + LGraphNode.prototype.getConnectionPos = function ( + is_input, + slot_number, + out + ) { + out = out || new Float32Array(2); + var num_slots = 0; + if (is_input && this.inputs) { + num_slots = this.inputs.length; + } + if (!is_input && this.outputs) { + num_slots = this.outputs.length; + } + + var offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5; + + if (this.flags.collapsed) { + var w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH; + if (this.horizontal) { + out[0] = this.pos[0] + w * 0.5; + if (is_input) { + out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; + } else { + out[1] = this.pos[1]; + } + } else { + if (is_input) { + out[0] = this.pos[0]; + } else { + out[0] = this.pos[0] + w; + } + out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5; + } + return out; + } + + //weird feature that never got finished + if (is_input && slot_number == -1) { + out[0] = this.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; + out[1] = this.pos[1] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; + return out; + } + + //hard-coded pos + if (is_input && num_slots > slot_number && this.inputs[slot_number].pos) { + out[0] = this.pos[0] + this.inputs[slot_number].pos[0]; + out[1] = this.pos[1] + this.inputs[slot_number].pos[1]; + return out; + } else if ( + !is_input && + num_slots > slot_number && + this.outputs[slot_number].pos + ) { + out[0] = this.pos[0] + this.outputs[slot_number].pos[0]; + out[1] = this.pos[1] + this.outputs[slot_number].pos[1]; + return out; + } + + //horizontal distributed slots + if (this.horizontal) { + out[0] = this.pos[0] + (slot_number + 0.5) * (this.size[0] / num_slots); + if (is_input) { + out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; + } else { + out[1] = this.pos[1] + this.size[1]; + } + return out; + } + + //default vertical slots + if (is_input) { + out[0] = this.pos[0] + offset; + } else { + out[0] = this.pos[0] + this.size[0] + 1 - offset; + } + out[1] = + this.pos[1] + + (slot_number + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + + (this.constructor.slot_start_y || 0); + return out; + }; + + /* Force align to grid */ + LGraphNode.prototype.alignToGrid = function () { + this.pos[0] = + LiteGraph.CANVAS_GRID_SIZE * + Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE); + this.pos[1] = + LiteGraph.CANVAS_GRID_SIZE * + Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE); + }; + + /* Console output */ + LGraphNode.prototype.trace = function (msg) { + if (!this.console) { + this.console = []; + } + + this.console.push(msg); + if (this.console.length > LGraphNode.MAX_CONSOLE) { + this.console.shift(); + } + + if (this.graph.onNodeTrace) this.graph.onNodeTrace(this, msg); + }; + + /* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ + LGraphNode.prototype.setDirtyCanvas = function ( + dirty_foreground, + dirty_background + ) { + if (!this.graph) { + return; + } + this.graph.sendActionToCanvas("setDirty", [ + dirty_foreground, + dirty_background, + ]); + }; + + LGraphNode.prototype.loadImage = function (url) { + var img = new Image(); + img.src = LiteGraph.node_images_path + url; + img.ready = false; + + var that = this; + img.onload = function () { + this.ready = true; + that.setDirtyCanvas(true); + }; + return img; + }; + + //safe LGraphNode action execution (not sure if safe) + /* LGraphNode.prototype.executeAction = function(action) { if(action == "") return false; @@ -4845,813 +4880,821 @@ LGraphNode.prototype.executeAction = function(action) } */ - /* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ - LGraphNode.prototype.captureInput = function(v) { - if (!this.graph || !this.graph.list_of_graphcanvas) { - return; - } - - var list = this.graph.list_of_graphcanvas; - - for (var i = 0; i < list.length; ++i) { - var c = list[i]; - //releasing somebody elses capture?! - if (!v && c.node_capturing_input != this) { - continue; - } - - //change - c.node_capturing_input = v ? this : null; - } - }; - - /** - * Collapse the node to make it smaller on the canvas - * @method collapse - **/ - LGraphNode.prototype.collapse = function(force) { - this.graph._version++; - if (this.constructor.collapsable === false && !force) { - return; - } - if (!this.flags.collapsed) { - this.flags.collapsed = true; - } else { - this.flags.collapsed = false; - } - this.setDirtyCanvas(true, true); - }; - - /** - * Forces the node to do not move or realign on Z - * @method pin - **/ - - LGraphNode.prototype.pin = function(v) { - this.graph._version++; - if (v === undefined) { - this.flags.pinned = !this.flags.pinned; - } else { - this.flags.pinned = v; - } - }; - - LGraphNode.prototype.localToScreen = function(x, y, graphcanvas) { - return [ - (x + this.pos[0]) * graphcanvas.scale + graphcanvas.offset[0], - (y + this.pos[1]) * graphcanvas.scale + graphcanvas.offset[1] - ]; - }; - - function LGraphGroup(title) { - this._ctor(title); + /* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ + LGraphNode.prototype.captureInput = function (v) { + if (!this.graph || !this.graph.list_of_graphcanvas) { + return; } - global.LGraphGroup = LiteGraph.LGraphGroup = LGraphGroup; + var list = this.graph.list_of_graphcanvas; - LGraphGroup.prototype._ctor = function(title) { - this.title = title || "Group"; - this.font_size = 24; - this.color = LGraphCanvas.node_colors.pale_blue - ? LGraphCanvas.node_colors.pale_blue.groupcolor - : "#AAA"; - this._bounding = new Float32Array([10, 10, 140, 80]); - this._pos = this._bounding.subarray(0, 2); - this._size = this._bounding.subarray(2, 4); - this._nodes = []; - this.graph = null; + for (var i = 0; i < list.length; ++i) { + var c = list[i]; + //releasing somebody elses capture?! + if (!v && c.node_capturing_input != this) { + continue; + } - Object.defineProperty(this, "pos", { - set: function(v) { - if (!v || v.length < 2) { - return; - } - this._pos[0] = v[0]; - this._pos[1] = v[1]; - }, - get: function() { - return this._pos; - }, - enumerable: true - }); + //change + c.node_capturing_input = v ? this : null; + } + }; - Object.defineProperty(this, "size", { - set: function(v) { - if (!v || v.length < 2) { - return; - } - this._size[0] = Math.max(140, v[0]); - this._size[1] = Math.max(80, v[1]); - }, - get: function() { - return this._size; - }, - enumerable: true - }); - }; + /** + * Collapse the node to make it smaller on the canvas + * @method collapse + **/ + LGraphNode.prototype.collapse = function (force) { + this.graph._version++; + if (this.constructor.collapsable === false && !force) { + return; + } + if (!this.flags.collapsed) { + this.flags.collapsed = true; + } else { + this.flags.collapsed = false; + } + this.setDirtyCanvas(true, true); + }; - LGraphGroup.prototype.configure = function(o) { - this.title = o.title; - this._bounding.set(o.bounding); - this.color = o.color; - if (o.font_size) { - this.font_size = o.font_size; + /** + * Forces the node to do not move or realign on Z + * @method pin + **/ + + LGraphNode.prototype.pin = function (v) { + this.graph._version++; + if (v === undefined) { + this.flags.pinned = !this.flags.pinned; + } else { + this.flags.pinned = v; + } + }; + + LGraphNode.prototype.localToScreen = function (x, y, graphcanvas) { + return [ + (x + this.pos[0]) * graphcanvas.scale + graphcanvas.offset[0], + (y + this.pos[1]) * graphcanvas.scale + graphcanvas.offset[1], + ]; + }; + + function LGraphGroup(title) { + this._ctor(title); + } + + global.LGraphGroup = LiteGraph.LGraphGroup = LGraphGroup; + + LGraphGroup.prototype._ctor = function (title) { + this.title = title || "Group"; + this.font_size = 24; + this.color = LGraphCanvas.node_colors.pale_blue + ? LGraphCanvas.node_colors.pale_blue.groupcolor + : "#AAA"; + this._bounding = new Float32Array([10, 10, 140, 80]); + this._pos = this._bounding.subarray(0, 2); + this._size = this._bounding.subarray(2, 4); + this._nodes = []; + this.graph = null; + + Object.defineProperty(this, "pos", { + set: function (v) { + if (!v || v.length < 2) { + return; } - }; + this._pos[0] = v[0]; + this._pos[1] = v[1]; + }, + get: function () { + return this._pos; + }, + enumerable: true, + }); - LGraphGroup.prototype.serialize = function() { - var b = this._bounding; - return { - title: this.title, - bounding: [ - Math.round(b[0]), - Math.round(b[1]), - Math.round(b[2]), - Math.round(b[3]) - ], - color: this.color, - font_size: this.font_size - }; - }; - - LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) { - this._pos[0] += deltax; - this._pos[1] += deltay; - if (ignore_nodes) { - return; - } - for (var i = 0; i < this._nodes.length; ++i) { - var node = this._nodes[i]; - node.pos[0] += deltax; - node.pos[1] += deltay; + Object.defineProperty(this, "size", { + set: function (v) { + if (!v || v.length < 2) { + return; } + this._size[0] = Math.max(140, v[0]); + this._size[1] = Math.max(80, v[1]); + }, + get: function () { + return this._size; + }, + enumerable: true, + }); + }; + + LGraphGroup.prototype.configure = function (o) { + this.title = o.title; + this._bounding.set(o.bounding); + this.color = o.color; + if (o.font_size) { + this.font_size = o.font_size; + } + }; + + LGraphGroup.prototype.serialize = function () { + var b = this._bounding; + return { + title: this.title, + bounding: [ + Math.round(b[0]), + Math.round(b[1]), + Math.round(b[2]), + Math.round(b[3]), + ], + color: this.color, + font_size: this.font_size, }; + }; - LGraphGroup.prototype.recomputeInsideNodes = function() { - this._nodes.length = 0; - var nodes = this.graph._nodes; - var node_bounding = new Float32Array(4); + LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) { + this._pos[0] += deltax; + this._pos[1] += deltay; + if (ignore_nodes) { + return; + } + for (var i = 0; i < this._nodes.length; ++i) { + var node = this._nodes[i]; + node.pos[0] += deltax; + node.pos[1] += deltay; + } + }; - for (var i = 0; i < nodes.length; ++i) { - var node = nodes[i]; - node.getBounding(node_bounding); - if (!overlapBounding(this._bounding, node_bounding)) { - continue; - } //out of the visible area - this._nodes.push(node); - } - }; + LGraphGroup.prototype.recomputeInsideNodes = function () { + this._nodes.length = 0; + var nodes = this.graph._nodes; + var node_bounding = new Float32Array(4); - LGraphGroup.prototype.isPointInside = LGraphNode.prototype.isPointInside; - LGraphGroup.prototype.setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas; + for (var i = 0; i < nodes.length; ++i) { + var node = nodes[i]; + node.getBounding(node_bounding); + if (!overlapBounding(this._bounding, node_bounding)) { + continue; + } //out of the visible area + this._nodes.push(node); + } + }; - //**************************************** + LGraphGroup.prototype.isPointInside = LGraphNode.prototype.isPointInside; + LGraphGroup.prototype.setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas; - //Scale and Offset - function DragAndScale(element, skip_events) { - this.offset = new Float32Array([0, 0]); - this.scale = 1; - this.max_scale = 10; - this.min_scale = 0.1; - this.onredraw = null; - this.enabled = true; - this.last_mouse = [0, 0]; - this.element = null; - this.visible_area = new Float32Array(4); + //**************************************** - if (element) { - this.element = element; - if (!skip_events) { - this.bindEvents(element); - } - } + //Scale and Offset + function DragAndScale(element, skip_events) { + this.offset = new Float32Array([0, 0]); + this.scale = 1; + this.max_scale = 10; + this.min_scale = 0.1; + this.onredraw = null; + this.enabled = true; + this.last_mouse = [0, 0]; + this.element = null; + this.visible_area = new Float32Array(4); + + if (element) { + this.element = element; + if (!skip_events) { + this.bindEvents(element); + } + } + } + + LiteGraph.DragAndScale = DragAndScale; + + DragAndScale.prototype.bindEvents = function (element) { + this.last_mouse = new Float32Array(2); + + this._binded_mouse_callback = this.onMouse.bind(this); + + LiteGraph.pointerListenerAdd(element, "down", this._binded_mouse_callback); + LiteGraph.pointerListenerAdd(element, "move", this._binded_mouse_callback); + LiteGraph.pointerListenerAdd(element, "up", this._binded_mouse_callback); + + element.addEventListener("mousewheel", this._binded_mouse_callback, false); + element.addEventListener("wheel", this._binded_mouse_callback, false); + }; + + DragAndScale.prototype.computeVisibleArea = function (viewport) { + if (!this.element) { + this.visible_area[0] = + this.visible_area[1] = + this.visible_area[2] = + this.visible_area[3] = + 0; + return; + } + var width = this.element.width; + var height = this.element.height; + var startx = -this.offset[0]; + var starty = -this.offset[1]; + if (viewport) { + startx += viewport[0] / this.scale; + starty += viewport[1] / this.scale; + width = viewport[2]; + height = viewport[3]; + } + var endx = startx + width / this.scale; + var endy = starty + height / this.scale; + this.visible_area[0] = startx; + this.visible_area[1] = starty; + this.visible_area[2] = endx - startx; + this.visible_area[3] = endy - starty; + }; + + DragAndScale.prototype.onMouse = function (e) { + if (!this.enabled) { + return; } - LiteGraph.DragAndScale = DragAndScale; + var canvas = this.element; + var rect = canvas.getBoundingClientRect(); + var x = e.clientX - rect.left; + var y = e.clientY - rect.top; + e.canvasx = x; + e.canvasy = y; + e.dragging = this.dragging; - DragAndScale.prototype.bindEvents = function(element) { - this.last_mouse = new Float32Array(2); + var is_inside = + !this.viewport || + (this.viewport && + x >= this.viewport[0] && + x < this.viewport[0] + this.viewport[2] && + y >= this.viewport[1] && + y < this.viewport[1] + this.viewport[3]); - this._binded_mouse_callback = this.onMouse.bind(this); + //console.log("pointerevents: DragAndScale onMouse "+e.type+" "+is_inside); - LiteGraph.pointerListenerAdd(element,"down", this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(element,"move", this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(element,"up", this._binded_mouse_callback); + var ignore = false; + if (this.onmouse) { + ignore = this.onmouse(e); + } - element.addEventListener( - "mousewheel", - this._binded_mouse_callback, - false - ); - element.addEventListener("wheel", this._binded_mouse_callback, false); + if (e.type == LiteGraph.pointerevents_method + "down" && is_inside) { + this.dragging = true; + LiteGraph.pointerListenerRemove( + canvas, + "move", + this._binded_mouse_callback + ); + LiteGraph.pointerListenerAdd( + document, + "move", + this._binded_mouse_callback + ); + LiteGraph.pointerListenerAdd(document, "up", this._binded_mouse_callback); + } else if (e.type == LiteGraph.pointerevents_method + "move") { + if (!ignore) { + var deltax = x - this.last_mouse[0]; + var deltay = y - this.last_mouse[1]; + if (this.dragging) { + this.mouseDrag(deltax, deltay); + } + } + } else if (e.type == LiteGraph.pointerevents_method + "up") { + this.dragging = false; + LiteGraph.pointerListenerRemove( + document, + "move", + this._binded_mouse_callback + ); + LiteGraph.pointerListenerRemove( + document, + "up", + this._binded_mouse_callback + ); + LiteGraph.pointerListenerAdd(canvas, "move", this._binded_mouse_callback); + } else if ( + is_inside && + (e.type == "mousewheel" || + e.type == "wheel" || + e.type == "DOMMouseScroll") + ) { + e.eventType = "mousewheel"; + if (e.type == "wheel") { + e.wheel = -e.deltaY; + } else { + e.wheel = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60; + } + + //from stack overflow + e.delta = e.wheelDelta ? e.wheelDelta / 40 : e.deltaY ? -e.deltaY / 3 : 0; + this.changeDeltaScale(1.0 + e.delta * 0.05); + } + + this.last_mouse[0] = x; + this.last_mouse[1] = y; + + if (is_inside) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + }; + + DragAndScale.prototype.toCanvasContext = function (ctx) { + ctx.scale(this.scale, this.scale); + ctx.translate(this.offset[0], this.offset[1]); + }; + + DragAndScale.prototype.convertOffsetToCanvas = function (pos) { + //return [pos[0] / this.scale - this.offset[0], pos[1] / this.scale - this.offset[1]]; + return [ + (pos[0] + this.offset[0]) * this.scale, + (pos[1] + this.offset[1]) * this.scale, + ]; + }; + + DragAndScale.prototype.convertCanvasToOffset = function (pos, out) { + out = out || [0, 0]; + out[0] = pos[0] / this.scale - this.offset[0]; + out[1] = pos[1] / this.scale - this.offset[1]; + return out; + }; + + DragAndScale.prototype.mouseDrag = function (x, y) { + this.offset[0] += x / this.scale; + this.offset[1] += y / this.scale; + + if (this.onredraw) { + this.onredraw(this); + } + }; + + DragAndScale.prototype.changeScale = function (value, zooming_center) { + if (value < this.min_scale) { + value = this.min_scale; + } else if (value > this.max_scale) { + value = this.max_scale; + } + + if (value == this.scale) { + return; + } + + if (!this.element) { + return; + } + + var rect = this.element.getBoundingClientRect(); + if (!rect) { + return; + } + + zooming_center = zooming_center || [rect.width * 0.5, rect.height * 0.5]; + var center = this.convertCanvasToOffset(zooming_center); + this.scale = value; + if (Math.abs(this.scale - 1) < 0.01) { + this.scale = 1; + } + + var new_center = this.convertCanvasToOffset(zooming_center); + var delta_offset = [new_center[0] - center[0], new_center[1] - center[1]]; + + this.offset[0] += delta_offset[0]; + this.offset[1] += delta_offset[1]; + + if (this.onredraw) { + this.onredraw(this); + } + }; + + DragAndScale.prototype.changeDeltaScale = function (value, zooming_center) { + this.changeScale(this.scale * value, zooming_center); + }; + + DragAndScale.prototype.reset = function () { + this.scale = 1; + this.offset[0] = 0; + this.offset[1] = 0; + }; + + //********************************************************************************* + // LGraphCanvas: LGraph renderer CLASS + //********************************************************************************* + + /** + * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required. + * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked + * + * @class LGraphCanvas + * @constructor + * @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself) + * @param {LGraph} graph [optional] + * @param {Object} options [optional] { skip_rendering, autoresize, viewport } + */ + function LGraphCanvas(canvas, graph, options) { + this.options = options = options || {}; + + //if(graph === undefined) + // throw ("No graph assigned"); + this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE; + + if (canvas && canvas.constructor === String) { + canvas = document.querySelector(canvas); + } + + this.ds = new DragAndScale(); + this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much + + this.title_text_font = "" + LiteGraph.NODE_TEXT_SIZE + "px Arial"; + this.inner_text_font = "normal " + LiteGraph.NODE_SUBTEXT_SIZE + "px Arial"; + this.node_title_color = LiteGraph.NODE_TITLE_COLOR; + this.default_link_color = LiteGraph.LINK_COLOR; + this.default_connection_color = { + input_off: "#778", + input_on: "#7F7", //"#BBD" + output_off: "#778", + output_on: "#7F7", //"#BBD" }; - - DragAndScale.prototype.computeVisibleArea = function( viewport ) { - if (!this.element) { - this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0; - return; - } - var width = this.element.width; - var height = this.element.height; - var startx = -this.offset[0]; - var starty = -this.offset[1]; - if( viewport ) - { - startx += viewport[0] / this.scale; - starty += viewport[1] / this.scale; - width = viewport[2]; - height = viewport[3]; - } - var endx = startx + width / this.scale; - var endy = starty + height / this.scale; - this.visible_area[0] = startx; - this.visible_area[1] = starty; - this.visible_area[2] = endx - startx; - this.visible_area[3] = endy - starty; - }; - - DragAndScale.prototype.onMouse = function(e) { - if (!this.enabled) { - return; - } - - var canvas = this.element; - var rect = canvas.getBoundingClientRect(); - var x = e.clientX - rect.left; - var y = e.clientY - rect.top; - e.canvasx = x; - e.canvasy = y; - e.dragging = this.dragging; - - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - - //console.log("pointerevents: DragAndScale onMouse "+e.type+" "+is_inside); - - var ignore = false; - if (this.onmouse) { - ignore = this.onmouse(e); - } - - if (e.type == LiteGraph.pointerevents_method+"down" && is_inside) { - this.dragging = true; - LiteGraph.pointerListenerRemove(canvas,"move",this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(document,"move",this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(document,"up",this._binded_mouse_callback); - } else if (e.type == LiteGraph.pointerevents_method+"move") { - if (!ignore) { - var deltax = x - this.last_mouse[0]; - var deltay = y - this.last_mouse[1]; - if (this.dragging) { - this.mouseDrag(deltax, deltay); - } - } - } else if (e.type == LiteGraph.pointerevents_method+"up") { - this.dragging = false; - LiteGraph.pointerListenerRemove(document,"move",this._binded_mouse_callback); - LiteGraph.pointerListenerRemove(document,"up",this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(canvas,"move",this._binded_mouse_callback); - } else if ( is_inside && - (e.type == "mousewheel" || - e.type == "wheel" || - e.type == "DOMMouseScroll") - ) { - e.eventType = "mousewheel"; - if (e.type == "wheel") { - e.wheel = -e.deltaY; - } else { - e.wheel = - e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60; - } - - //from stack overflow - e.delta = e.wheelDelta - ? e.wheelDelta / 40 - : e.deltaY - ? -e.deltaY / 3 - : 0; - this.changeDeltaScale(1.0 + e.delta * 0.05); - } - - this.last_mouse[0] = x; - this.last_mouse[1] = y; - - if(is_inside) - { - e.preventDefault(); - e.stopPropagation(); - return false; - } - }; - - DragAndScale.prototype.toCanvasContext = function(ctx) { - ctx.scale(this.scale, this.scale); - ctx.translate(this.offset[0], this.offset[1]); - }; - - DragAndScale.prototype.convertOffsetToCanvas = function(pos) { - //return [pos[0] / this.scale - this.offset[0], pos[1] / this.scale - this.offset[1]]; - return [ - (pos[0] + this.offset[0]) * this.scale, - (pos[1] + this.offset[1]) * this.scale - ]; - }; - - DragAndScale.prototype.convertCanvasToOffset = function(pos, out) { - out = out || [0, 0]; - out[0] = pos[0] / this.scale - this.offset[0]; - out[1] = pos[1] / this.scale - this.offset[1]; - return out; - }; - - DragAndScale.prototype.mouseDrag = function(x, y) { - this.offset[0] += x / this.scale; - this.offset[1] += y / this.scale; - - if (this.onredraw) { - this.onredraw(this); - } - }; - - DragAndScale.prototype.changeScale = function(value, zooming_center) { - if (value < this.min_scale) { - value = this.min_scale; - } else if (value > this.max_scale) { - value = this.max_scale; - } - - if (value == this.scale) { - return; - } - - if (!this.element) { - return; - } - - var rect = this.element.getBoundingClientRect(); - if (!rect) { - return; - } - - zooming_center = zooming_center || [ - rect.width * 0.5, - rect.height * 0.5 - ]; - var center = this.convertCanvasToOffset(zooming_center); - this.scale = value; - if (Math.abs(this.scale - 1) < 0.01) { - this.scale = 1; - } - - var new_center = this.convertCanvasToOffset(zooming_center); - var delta_offset = [ - new_center[0] - center[0], - new_center[1] - center[1] - ]; - - this.offset[0] += delta_offset[0]; - this.offset[1] += delta_offset[1]; - - if (this.onredraw) { - this.onredraw(this); - } - }; - - DragAndScale.prototype.changeDeltaScale = function(value, zooming_center) { - this.changeScale(this.scale * value, zooming_center); - }; - - DragAndScale.prototype.reset = function() { - this.scale = 1; - this.offset[0] = 0; - this.offset[1] = 0; - }; - - //********************************************************************************* - // LGraphCanvas: LGraph renderer CLASS - //********************************************************************************* - - /** - * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required. - * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked - * - * @class LGraphCanvas - * @constructor - * @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself) - * @param {LGraph} graph [optional] - * @param {Object} options [optional] { skip_rendering, autoresize, viewport } - */ - function LGraphCanvas(canvas, graph, options) { - this.options = options = options || {}; - - //if(graph === undefined) - // throw ("No graph assigned"); - this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE; - - if (canvas && canvas.constructor === String) { - canvas = document.querySelector(canvas); - } - - this.ds = new DragAndScale(); - this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much - - this.title_text_font = "" + LiteGraph.NODE_TEXT_SIZE + "px Arial"; - this.inner_text_font = - "normal " + LiteGraph.NODE_SUBTEXT_SIZE + "px Arial"; - this.node_title_color = LiteGraph.NODE_TITLE_COLOR; - this.default_link_color = LiteGraph.LINK_COLOR; - this.default_connection_color = { - input_off: "#778", - input_on: "#7F7", //"#BBD" - output_off: "#778", - output_on: "#7F7" //"#BBD" - }; - this.default_connection_color_byType = { - /*number: "#7F7", + this.default_connection_color_byType = { + /*number: "#7F7", string: "#77F", boolean: "#F77",*/ - } - this.default_connection_color_byTypeOff = { - /*number: "#474", + }; + this.default_connection_color_byTypeOff = { + /*number: "#474", string: "#447", boolean: "#744",*/ - }; + }; - this.highquality_render = true; - this.use_gradients = false; //set to true to render titlebar with gradients - this.editor_alpha = 1; //used for transition - this.pause_rendering = false; - this.clear_background = true; - this.clear_background_color = "#222"; + this.highquality_render = true; + this.use_gradients = false; //set to true to render titlebar with gradients + this.editor_alpha = 1; //used for transition + this.pause_rendering = false; + this.clear_background = true; + this.clear_background_color = "#222"; - this.read_only = false; //if set to true users cannot modify the graph - this.render_only_selected = true; - this.live_mode = false; - this.show_info = true; - this.allow_dragcanvas = true; - this.allow_dragnodes = true; - this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc - this.multi_select = false; //allow selecting multi nodes without pressing extra keys - this.allow_searchbox = true; - this.allow_reconnect_links = true; //allows to change a connection with having to redo it again - this.align_to_grid = false; //snap to grid + this.read_only = false; //if set to true users cannot modify the graph + this.render_only_selected = true; + this.live_mode = false; + this.show_info = true; + this.allow_dragcanvas = true; + this.allow_dragnodes = true; + this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc + this.multi_select = false; //allow selecting multi nodes without pressing extra keys + this.allow_searchbox = true; + this.allow_reconnect_links = true; //allows to change a connection with having to redo it again + this.align_to_grid = false; //snap to grid - this.drag_mode = false; - this.dragging_rectangle = null; + this.drag_mode = false; + this.dragging_rectangle = null; - this.filter = null; //allows to filter to only accept some type of nodes in a graph + this.filter = null; //allows to filter to only accept some type of nodes in a graph - this.set_canvas_dirty_on_mouse_event = true; //forces to redraw the canvas if the mouse does anything - this.always_render_background = false; - this.render_shadows = true; - this.render_canvas_border = true; - this.render_connections_shadows = false; //too much cpu - this.render_connections_border = true; - this.render_curved_connections = false; - this.render_connection_arrows = false; - this.render_collapsed_slots = true; - this.render_execution_order = false; - this.render_title_colored = true; - this.render_link_tooltip = true; + this.set_canvas_dirty_on_mouse_event = true; //forces to redraw the canvas if the mouse does anything + this.always_render_background = false; + this.render_shadows = true; + this.render_canvas_border = true; + this.render_connections_shadows = false; //too much cpu + this.render_connections_border = true; + this.render_curved_connections = false; + this.render_connection_arrows = false; + this.render_collapsed_slots = true; + this.render_execution_order = false; + this.render_title_colored = true; + this.render_link_tooltip = true; - this.links_render_mode = LiteGraph.SPLINE_LINK; + this.links_render_mode = LiteGraph.SPLINE_LINK; - this.mouse = [0, 0]; //mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle - this.graph_mouse = [0, 0]; //mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle - this.canvas_mouse = this.graph_mouse; //LEGACY: REMOVE THIS, USE GRAPH_MOUSE INSTEAD + this.mouse = [0, 0]; //mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle + this.graph_mouse = [0, 0]; //mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle + this.canvas_mouse = this.graph_mouse; //LEGACY: REMOVE THIS, USE GRAPH_MOUSE INSTEAD - //to personalize the search box - this.onSearchBox = null; - this.onSearchBoxSelection = null; + //to personalize the search box + this.onSearchBox = null; + this.onSearchBoxSelection = null; - //callbacks - this.onMouse = null; - this.onDrawBackground = null; //to render background objects (behind nodes and connections) in the canvas affected by transform - this.onDrawForeground = null; //to render foreground objects (above nodes and connections) in the canvas affected by transform - this.onDrawOverlay = null; //to render foreground objects not affected by transform (for GUIs) - this.onDrawLinkTooltip = null; //called when rendering a tooltip - this.onNodeMoved = null; //called after moving a node - this.onSelectionChange = null; //called if the selection changes - this.onConnectingChange = null; //called before any link changes - this.onBeforeChange = null; //called before modifying the graph - this.onAfterChange = null; //called after modifying the graph + //callbacks + this.onMouse = null; + this.onDrawBackground = null; //to render background objects (behind nodes and connections) in the canvas affected by transform + this.onDrawForeground = null; //to render foreground objects (above nodes and connections) in the canvas affected by transform + this.onDrawOverlay = null; //to render foreground objects not affected by transform (for GUIs) + this.onDrawLinkTooltip = null; //called when rendering a tooltip + this.onNodeMoved = null; //called after moving a node + this.onSelectionChange = null; //called if the selection changes + this.onConnectingChange = null; //called before any link changes + this.onBeforeChange = null; //called before modifying the graph + this.onAfterChange = null; //called after modifying the graph - this.connections_width = 3; - this.round_radius = 8; + this.connections_width = 3; + this.round_radius = 8; - this.current_node = null; - this.node_widget = null; //used for widgets - this.over_link_center = null; - this.last_mouse_position = [0, 0]; - this.visible_area = this.ds.visible_area; - this.visible_links = []; + this.current_node = null; + this.node_widget = null; //used for widgets + this.over_link_center = null; + this.last_mouse_position = [0, 0]; + this.visible_area = this.ds.visible_area; + this.visible_links = []; - this.viewport = options.viewport || null; //to constraint render area to a portion of the canvas + this.viewport = options.viewport || null; //to constraint render area to a portion of the canvas - //link canvas and graph - if (graph) { - graph.attachCanvas(this); - } - - this.setCanvas(canvas,options.skip_events); - this.clear(); - - if (!options.skip_render) { - this.startRendering(); - } - - this.autoresize = options.autoresize; + //link canvas and graph + if (graph) { + graph.attachCanvas(this); } - global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas; + this.setCanvas(canvas, options.skip_events); + this.clear(); - LGraphCanvas.DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII="; + if (!options.skip_render) { + this.startRendering(); + } - LGraphCanvas.link_type_colors = { - "-1": LiteGraph.EVENT_LINK_COLOR, - number: "#AAA", - node: "#DCA" - }; - LGraphCanvas.gradients = {}; //cache of gradients + this.autoresize = options.autoresize; + } - /** - * clears all the data inside - * - * @method clear - */ - LGraphCanvas.prototype.clear = function() { - this.frame = 0; - this.last_draw_time = 0; - this.render_time = 0; - this.fps = 0; + global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas; - //this.scale = 1; - //this.offset = [0,0]; + LGraphCanvas.DEFAULT_BACKGROUND_IMAGE = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII="; - this.dragging_rectangle = null; + LGraphCanvas.link_type_colors = { + "-1": LiteGraph.EVENT_LINK_COLOR, + number: "#AAA", + node: "#DCA", + }; + LGraphCanvas.gradients = {}; //cache of gradients - this.selected_nodes = {}; - this.selected_group = null; + /** + * clears all the data inside + * + * @method clear + */ + LGraphCanvas.prototype.clear = function () { + this.frame = 0; + this.last_draw_time = 0; + this.render_time = 0; + this.fps = 0; - this.visible_nodes = []; - this.node_dragged = null; - this.node_over = null; - this.node_capturing_input = null; - this.connecting_node = null; - this.highlighted_links = {}; + //this.scale = 1; + //this.offset = [0,0]; - this.dragging_canvas = false; + this.dragging_rectangle = null; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.dirty_area = null; + this.selected_nodes = {}; + this.selected_group = null; - this.node_in_panel = null; - this.node_widget = null; + this.visible_nodes = []; + this.node_dragged = null; + this.node_over = null; + this.node_capturing_input = null; + this.connecting_node = null; + this.highlighted_links = {}; - this.last_mouse = [0, 0]; - this.last_mouseclick = 0; - this.pointer_is_down = false; - this.pointer_is_double = false; - this.visible_area.set([0, 0, 0, 0]); + this.dragging_canvas = false; - if (this.onClear) { - this.onClear(); - } - }; + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + this.dirty_area = null; - /** - * assigns a graph, you can reassign graphs to the same canvas - * - * @method setGraph - * @param {LGraph} graph - */ - LGraphCanvas.prototype.setGraph = function(graph, skip_clear) { - if (this.graph == graph) { - return; - } + this.node_in_panel = null; + this.node_widget = null; - if (!skip_clear) { - this.clear(); - } + this.last_mouse = [0, 0]; + this.last_mouseclick = 0; + this.pointer_is_down = false; + this.pointer_is_double = false; + this.visible_area.set([0, 0, 0, 0]); - if (!graph && this.graph) { - this.graph.detachCanvas(this); - return; - } + if (this.onClear) { + this.onClear(); + } + }; - graph.attachCanvas(this); + /** + * assigns a graph, you can reassign graphs to the same canvas + * + * @method setGraph + * @param {LGraph} graph + */ + LGraphCanvas.prototype.setGraph = function (graph, skip_clear) { + if (this.graph == graph) { + return; + } - //remove the graph stack in case a subgraph was open - if (this._graph_stack) - this._graph_stack = null; + if (!skip_clear) { + this.clear(); + } - this.setDirty(true, true); - }; + if (!graph && this.graph) { + this.graph.detachCanvas(this); + return; + } - /** - * returns the top level graph (in case there are subgraphs open on the canvas) - * - * @method getTopGraph - * @return {LGraph} graph - */ - LGraphCanvas.prototype.getTopGraph = function() - { - if(this._graph_stack.length) - return this._graph_stack[0]; - return this.graph; - } + graph.attachCanvas(this); - /** - * opens a graph contained inside a node in the current graph - * - * @method openSubgraph - * @param {LGraph} graph - */ - LGraphCanvas.prototype.openSubgraph = function(graph) { - if (!graph) { - throw "graph cannot be null"; - } + //remove the graph stack in case a subgraph was open + if (this._graph_stack) this._graph_stack = null; - if (this.graph == graph) { - throw "graph cannot be the same"; - } + this.setDirty(true, true); + }; - this.clear(); + /** + * returns the top level graph (in case there are subgraphs open on the canvas) + * + * @method getTopGraph + * @return {LGraph} graph + */ + LGraphCanvas.prototype.getTopGraph = function () { + if (this._graph_stack.length) return this._graph_stack[0]; + return this.graph; + }; - if (this.graph) { - if (!this._graph_stack) { - this._graph_stack = []; - } - this._graph_stack.push(this.graph); - } + /** + * opens a graph contained inside a node in the current graph + * + * @method openSubgraph + * @param {LGraph} graph + */ + LGraphCanvas.prototype.openSubgraph = function (graph) { + if (!graph) { + throw "graph cannot be null"; + } - graph.attachCanvas(this); - this.checkPanels(); - this.setDirty(true, true); - }; + if (this.graph == graph) { + throw "graph cannot be the same"; + } - /** - * closes a subgraph contained inside a node - * - * @method closeSubgraph - * @param {LGraph} assigns a graph - */ - LGraphCanvas.prototype.closeSubgraph = function() { - if (!this._graph_stack || this._graph_stack.length == 0) { - return; - } - var subgraph_node = this.graph._subgraph_node; - var graph = this._graph_stack.pop(); - this.selected_nodes = {}; - this.highlighted_links = {}; - graph.attachCanvas(this); - this.setDirty(true, true); - if (subgraph_node) { - this.centerOnNode(subgraph_node); - this.selectNodes([subgraph_node]); - } - // when close sub graph back to offset [0, 0] scale 1 - this.ds.offset = [0, 0] - this.ds.scale = 1 - }; + this.clear(); - /** - * returns the visually active graph (in case there are more in the stack) - * @method getCurrentGraph - * @return {LGraph} the active graph - */ - LGraphCanvas.prototype.getCurrentGraph = function() { - return this.graph; - }; + if (this.graph) { + if (!this._graph_stack) { + this._graph_stack = []; + } + this._graph_stack.push(this.graph); + } - /** - * assigns a canvas - * - * @method setCanvas - * @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector) - */ - LGraphCanvas.prototype.setCanvas = function(canvas, skip_events) { - var that = this; + graph.attachCanvas(this); + this.checkPanels(); + this.setDirty(true, true); + }; - if (canvas) { - if (canvas.constructor === String) { - canvas = document.getElementById(canvas); - if (!canvas) { - throw "Error creating LiteGraph canvas: Canvas not found"; - } - } - } + /** + * closes a subgraph contained inside a node + * + * @method closeSubgraph + * @param {LGraph} assigns a graph + */ + LGraphCanvas.prototype.closeSubgraph = function () { + if (!this._graph_stack || this._graph_stack.length == 0) { + return; + } + var subgraph_node = this.graph._subgraph_node; + var graph = this._graph_stack.pop(); + this.selected_nodes = {}; + this.highlighted_links = {}; + graph.attachCanvas(this); + this.setDirty(true, true); + if (subgraph_node) { + this.centerOnNode(subgraph_node); + this.selectNodes([subgraph_node]); + } + // when close sub graph back to offset [0, 0] scale 1 + this.ds.offset = [0, 0]; + this.ds.scale = 1; + }; - if (canvas === this.canvas) { - return; - } + /** + * returns the visually active graph (in case there are more in the stack) + * @method getCurrentGraph + * @return {LGraph} the active graph + */ + LGraphCanvas.prototype.getCurrentGraph = function () { + return this.graph; + }; - if (!canvas && this.canvas) { - //maybe detach events from old_canvas - if (!skip_events) { - this.unbindEvents(); - } - } - - this.canvas = canvas; - this.ds.element = canvas; + /** + * assigns a canvas + * + * @method setCanvas + * @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector) + */ + LGraphCanvas.prototype.setCanvas = function (canvas, skip_events) { + var that = this; + if (canvas) { + if (canvas.constructor === String) { + canvas = document.getElementById(canvas); if (!canvas) { - return; + throw "Error creating LiteGraph canvas: Canvas not found"; } + } + } - //this.canvas.tabindex = "1000"; - canvas.className += " lgraphcanvas"; - canvas.data = this; - canvas.tabindex = "1"; //to allow key events + if (canvas === this.canvas) { + return; + } - //bg canvas: used for non changing stuff - this.bgcanvas = null; - if (!this.bgcanvas) { - this.bgcanvas = document.createElement("canvas"); - this.bgcanvas.width = this.canvas.width; - this.bgcanvas.height = this.canvas.height; - } + if (!canvas && this.canvas) { + //maybe detach events from old_canvas + if (!skip_events) { + this.unbindEvents(); + } + } - if (canvas.getContext == null) { - if (canvas.localName != "canvas") { - throw "Element supplied for LGraphCanvas must be a element, you passed a " + - canvas.localName; - } - throw "This browser doesn't support Canvas"; - } + this.canvas = canvas; + this.ds.element = canvas; - var ctx = (this.ctx = canvas.getContext("2d")); - if (ctx == null) { - if (!canvas.webgl_enabled) { - console.warn( - "This canvas seems to be WebGL, enabling WebGL renderer" - ); - } - this.enableWebGL(); - } + if (!canvas) { + return; + } - //input: (move and up could be unbinded) - // why here? this._mousemove_callback = this.processMouseMove.bind(this); - // why here? this._mouseup_callback = this.processMouseUp.bind(this); + //this.canvas.tabindex = "1000"; + canvas.className += " lgraphcanvas"; + canvas.data = this; + canvas.tabindex = "1"; //to allow key events - if (!skip_events) { - this.bindEvents(); - } - }; + //bg canvas: used for non changing stuff + this.bgcanvas = null; + if (!this.bgcanvas) { + this.bgcanvas = document.createElement("canvas"); + this.bgcanvas.width = this.canvas.width; + this.bgcanvas.height = this.canvas.height; + } - //used in some events to capture them - LGraphCanvas.prototype._doNothing = function doNothing(e) { - //console.log("pointerevents: _doNothing "+e.type); - e.preventDefault(); - return false; - }; - LGraphCanvas.prototype._doReturnTrue = function doNothing(e) { - e.preventDefault(); - return true; - }; - - /** - * binds mouse, keyboard, touch and drag events to the canvas - * @method bindEvents - **/ - LGraphCanvas.prototype.bindEvents = function() { - if (this._events_binded) { - console.warn("LGraphCanvas: events already binded"); - return; - } - - //console.log("pointerevents: bindEvents"); - - var canvas = this.canvas; - - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; //hack used when moving canvas between windows - - this._mousedown_callback = this.processMouseDown.bind(this); - this._mousewheel_callback = this.processMouseWheel.bind(this); - // why mousemove and mouseup were not binded here? - this._mousemove_callback = this.processMouseMove.bind(this); - this._mouseup_callback = this.processMouseUp.bind(this); - - //touch events -- TODO IMPLEMENT - //this._touch_callback = this.touchHandler.bind(this); - - LiteGraph.pointerListenerAdd(canvas,"down", this._mousedown_callback, true); //down do not need to store the binded - canvas.addEventListener("mousewheel", this._mousewheel_callback, false); - - LiteGraph.pointerListenerAdd(canvas,"up", this._mouseup_callback, true); // CHECK: ??? binded or not - LiteGraph.pointerListenerAdd(canvas,"move", this._mousemove_callback); - - canvas.addEventListener("contextmenu", this._doNothing); - canvas.addEventListener( - "DOMMouseScroll", - this._mousewheel_callback, - false + if (canvas.getContext == null) { + if (canvas.localName != "canvas") { + throw ( + "Element supplied for LGraphCanvas must be a element, you passed a " + + canvas.localName ); + } + throw "This browser doesn't support Canvas"; + } - //touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents - /*if( 'touchstart' in document.documentElement ) + var ctx = (this.ctx = canvas.getContext("2d")); + if (ctx == null) { + if (!canvas.webgl_enabled) { + console.warn("This canvas seems to be WebGL, enabling WebGL renderer"); + } + this.enableWebGL(); + } + + //input: (move and up could be unbinded) + // why here? this._mousemove_callback = this.processMouseMove.bind(this); + // why here? this._mouseup_callback = this.processMouseUp.bind(this); + + if (!skip_events) { + this.bindEvents(); + } + }; + + //used in some events to capture them + LGraphCanvas.prototype._doNothing = function doNothing(e) { + //console.log("pointerevents: _doNothing "+e.type); + e.preventDefault(); + return false; + }; + LGraphCanvas.prototype._doReturnTrue = function doNothing(e) { + e.preventDefault(); + return true; + }; + + /** + * binds mouse, keyboard, touch and drag events to the canvas + * @method bindEvents + **/ + LGraphCanvas.prototype.bindEvents = function () { + if (this._events_binded) { + console.warn("LGraphCanvas: events already binded"); + return; + } + + //console.log("pointerevents: bindEvents"); + + var canvas = this.canvas; + + var ref_window = this.getCanvasWindow(); + var document = ref_window.document; //hack used when moving canvas between windows + + this._mousedown_callback = this.processMouseDown.bind(this); + this._mousewheel_callback = this.processMouseWheel.bind(this); + // why mousemove and mouseup were not binded here? + this._mousemove_callback = this.processMouseMove.bind(this); + this._mouseup_callback = this.processMouseUp.bind(this); + + //touch events -- TODO IMPLEMENT + //this._touch_callback = this.touchHandler.bind(this); + + LiteGraph.pointerListenerAdd( + canvas, + "down", + this._mousedown_callback, + true + ); //down do not need to store the binded + canvas.addEventListener("mousewheel", this._mousewheel_callback, false); + + LiteGraph.pointerListenerAdd(canvas, "up", this._mouseup_callback, true); // CHECK: ??? binded or not + LiteGraph.pointerListenerAdd(canvas, "move", this._mousemove_callback); + + canvas.addEventListener("contextmenu", this._doNothing); + canvas.addEventListener("DOMMouseScroll", this._mousewheel_callback, false); + + //touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents + /*if( 'touchstart' in document.documentElement ) { canvas.addEventListener("touchstart", this._touch_callback, true); canvas.addEventListener("touchmove", this._touch_callback, true); @@ -5659,1075 +5702,1165 @@ LGraphNode.prototype.executeAction = function(action) canvas.addEventListener("touchcancel", this._touch_callback, true); }*/ - //Keyboard ****************** - this._key_callback = this.processKey.bind(this); + //Keyboard ****************** + this._key_callback = this.processKey.bind(this); - canvas.addEventListener("keydown", this._key_callback, true); - document.addEventListener("keyup", this._key_callback, true); //in document, otherwise it doesn't fire keyup + canvas.addEventListener("keydown", this._key_callback, true); + document.addEventListener("keyup", this._key_callback, true); //in document, otherwise it doesn't fire keyup - //Dropping Stuff over nodes ************************************ - this._ondrop_callback = this.processDrop.bind(this); + //Dropping Stuff over nodes ************************************ + this._ondrop_callback = this.processDrop.bind(this); - canvas.addEventListener("dragover", this._doNothing, false); - canvas.addEventListener("dragend", this._doNothing, false); - canvas.addEventListener("drop", this._ondrop_callback, false); - canvas.addEventListener("dragenter", this._doReturnTrue, false); + canvas.addEventListener("dragover", this._doNothing, false); + canvas.addEventListener("dragend", this._doNothing, false); + canvas.addEventListener("drop", this._ondrop_callback, false); + canvas.addEventListener("dragenter", this._doReturnTrue, false); - this._events_binded = true; - }; + this._events_binded = true; + }; - /** - * unbinds mouse events from the canvas - * @method unbindEvents - **/ - LGraphCanvas.prototype.unbindEvents = function() { - if (!this._events_binded) { - console.warn("LGraphCanvas: no events binded"); - return; - } + /** + * unbinds mouse events from the canvas + * @method unbindEvents + **/ + LGraphCanvas.prototype.unbindEvents = function () { + if (!this._events_binded) { + console.warn("LGraphCanvas: no events binded"); + return; + } - //console.log("pointerevents: unbindEvents"); - - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; + //console.log("pointerevents: unbindEvents"); - LiteGraph.pointerListenerRemove(this.canvas,"move", this._mousedown_callback); - LiteGraph.pointerListenerRemove(this.canvas,"up", this._mousedown_callback); - LiteGraph.pointerListenerRemove(this.canvas,"down", this._mousedown_callback); - this.canvas.removeEventListener( - "mousewheel", - this._mousewheel_callback - ); - this.canvas.removeEventListener( - "DOMMouseScroll", - this._mousewheel_callback - ); - this.canvas.removeEventListener("keydown", this._key_callback); - document.removeEventListener("keyup", this._key_callback); - this.canvas.removeEventListener("contextmenu", this._doNothing); - this.canvas.removeEventListener("drop", this._ondrop_callback); - this.canvas.removeEventListener("dragenter", this._doReturnTrue); + var ref_window = this.getCanvasWindow(); + var document = ref_window.document; - //touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents - /*this.canvas.removeEventListener("touchstart", this._touch_callback ); + LiteGraph.pointerListenerRemove( + this.canvas, + "move", + this._mousedown_callback + ); + LiteGraph.pointerListenerRemove( + this.canvas, + "up", + this._mousedown_callback + ); + LiteGraph.pointerListenerRemove( + this.canvas, + "down", + this._mousedown_callback + ); + this.canvas.removeEventListener("mousewheel", this._mousewheel_callback); + this.canvas.removeEventListener( + "DOMMouseScroll", + this._mousewheel_callback + ); + this.canvas.removeEventListener("keydown", this._key_callback); + document.removeEventListener("keyup", this._key_callback); + this.canvas.removeEventListener("contextmenu", this._doNothing); + this.canvas.removeEventListener("drop", this._ondrop_callback); + this.canvas.removeEventListener("dragenter", this._doReturnTrue); + + //touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents + /*this.canvas.removeEventListener("touchstart", this._touch_callback ); this.canvas.removeEventListener("touchmove", this._touch_callback ); this.canvas.removeEventListener("touchend", this._touch_callback ); this.canvas.removeEventListener("touchcancel", this._touch_callback );*/ - this._mousedown_callback = null; - this._mousewheel_callback = null; - this._key_callback = null; - this._ondrop_callback = null; + this._mousedown_callback = null; + this._mousewheel_callback = null; + this._key_callback = null; + this._ondrop_callback = null; - this._events_binded = false; - }; + this._events_binded = false; + }; - LGraphCanvas.getFileExtension = function(url) { - var question = url.indexOf("?"); - if (question != -1) { - url = url.substr(0, question); - } - var point = url.lastIndexOf("."); - if (point == -1) { - return ""; - } - return url.substr(point + 1).toLowerCase(); - }; + LGraphCanvas.getFileExtension = function (url) { + var question = url.indexOf("?"); + if (question != -1) { + url = url.substr(0, question); + } + var point = url.lastIndexOf("."); + if (point == -1) { + return ""; + } + return url.substr(point + 1).toLowerCase(); + }; - /** - * this function allows to render the canvas using WebGL instead of Canvas2D - * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL - * @method enableWebGL - **/ - LGraphCanvas.prototype.enableWebGL = function() { - if (typeof GL === "undefined") { - throw "litegl.js must be included to use a WebGL canvas"; - } - if (typeof enableWebGLCanvas === "undefined") { - throw "webglCanvas.js must be included to use this feature"; - } + /** + * this function allows to render the canvas using WebGL instead of Canvas2D + * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL + * @method enableWebGL + **/ + LGraphCanvas.prototype.enableWebGL = function () { + if (typeof GL === "undefined") { + throw "litegl.js must be included to use a WebGL canvas"; + } + if (typeof enableWebGLCanvas === "undefined") { + throw "webglCanvas.js must be included to use this feature"; + } - this.gl = this.ctx = enableWebGLCanvas(this.canvas); - this.ctx.webgl = true; - this.bgcanvas = this.canvas; - this.bgctx = this.gl; - this.canvas.webgl_enabled = true; + this.gl = this.ctx = enableWebGLCanvas(this.canvas); + this.ctx.webgl = true; + this.bgcanvas = this.canvas; + this.bgctx = this.gl; + this.canvas.webgl_enabled = true; - /* + /* GL.create({ canvas: this.bgcanvas }); this.bgctx = enableWebGLCanvas( this.bgcanvas ); window.gl = this.gl; */ - }; + }; - /** - * marks as dirty the canvas, this way it will be rendered again - * - * @class LGraphCanvas - * @method setDirty - * @param {bool} fgcanvas if the foreground canvas is dirty (the one containing the nodes) - * @param {bool} bgcanvas if the background canvas is dirty (the one containing the wires) - */ - LGraphCanvas.prototype.setDirty = function(fgcanvas, bgcanvas) { - if (fgcanvas) { - this.dirty_canvas = true; - } - if (bgcanvas) { - this.dirty_bgcanvas = true; - } - }; + /** + * marks as dirty the canvas, this way it will be rendered again + * + * @class LGraphCanvas + * @method setDirty + * @param {bool} fgcanvas if the foreground canvas is dirty (the one containing the nodes) + * @param {bool} bgcanvas if the background canvas is dirty (the one containing the wires) + */ + LGraphCanvas.prototype.setDirty = function (fgcanvas, bgcanvas) { + if (fgcanvas) { + this.dirty_canvas = true; + } + if (bgcanvas) { + this.dirty_bgcanvas = true; + } + }; - /** - * Used to attach the canvas in a popup - * - * @method getCanvasWindow - * @return {window} returns the window where the canvas is attached (the DOM root node) - */ - LGraphCanvas.prototype.getCanvasWindow = function() { - if (!this.canvas) { - return window; - } - var doc = this.canvas.ownerDocument; - return doc.defaultView || doc.parentWindow; - }; + /** + * Used to attach the canvas in a popup + * + * @method getCanvasWindow + * @return {window} returns the window where the canvas is attached (the DOM root node) + */ + LGraphCanvas.prototype.getCanvasWindow = function () { + if (!this.canvas) { + return window; + } + var doc = this.canvas.ownerDocument; + return doc.defaultView || doc.parentWindow; + }; - /** - * starts rendering the content of the canvas when needed - * - * @method startRendering - */ - LGraphCanvas.prototype.startRendering = function() { - if (this.is_rendering) { - return; - } //already rendering + /** + * starts rendering the content of the canvas when needed + * + * @method startRendering + */ + LGraphCanvas.prototype.startRendering = function () { + if (this.is_rendering) { + return; + } //already rendering - this.is_rendering = true; - renderFrame.call(this); + this.is_rendering = true; + renderFrame.call(this); - function renderFrame() { - if (!this.pause_rendering) { - this.draw(); - } + function renderFrame() { + if (!this.pause_rendering) { + this.draw(); + } - var window = this.getCanvasWindow(); - if (this.is_rendering) { - window.requestAnimationFrame(renderFrame.bind(this)); - } - } - }; + var window = this.getCanvasWindow(); + if (this.is_rendering) { + window.requestAnimationFrame(renderFrame.bind(this)); + } + } + }; - /** - * stops rendering the content of the canvas (to save resources) - * - * @method stopRendering - */ - LGraphCanvas.prototype.stopRendering = function() { - this.is_rendering = false; - /* + /** + * stops rendering the content of the canvas (to save resources) + * + * @method stopRendering + */ + LGraphCanvas.prototype.stopRendering = function () { + this.is_rendering = false; + /* if(this.rendering_timer_id) { clearInterval(this.rendering_timer_id); this.rendering_timer_id = null; } */ - }; + }; - /* LiteGraphCanvas input */ + /* LiteGraphCanvas input */ - //used to block future mouse events (because of im gui) - LGraphCanvas.prototype.blockClick = function() - { - this.block_click = true; - this.last_mouseclick = 0; - } - - LGraphCanvas.prototype.processMouseDown = function(e) { - - if( this.set_canvas_dirty_on_mouse_event ) - this.dirty_canvas = true; - - if (!this.graph) { - return; - } + //used to block future mouse events (because of im gui) + LGraphCanvas.prototype.blockClick = function () { + this.block_click = true; + this.last_mouseclick = 0; + }; - this.adjustMouseEvent(e); + LGraphCanvas.prototype.processMouseDown = function (e) { + if (this.set_canvas_dirty_on_mouse_event) this.dirty_canvas = true; - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; - LGraphCanvas.active_canvas = this; - var that = this; + if (!this.graph) { + return; + } - var x = e.clientX; - var y = e.clientY; - //console.log(y,this.viewport); - //console.log("pointerevents: processMouseDown pointerId:"+e.pointerId+" which:"+e.which+" isPrimary:"+e.isPrimary+" :: x y "+x+" "+y); + this.adjustMouseEvent(e); - this.ds.viewport = this.viewport; - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); + var ref_window = this.getCanvasWindow(); + var document = ref_window.document; + LGraphCanvas.active_canvas = this; + var that = this; - //move mouse move event to the window in case it drags outside of the canvas - if(!this.options.skip_events) - { - LiteGraph.pointerListenerRemove(this.canvas,"move", this._mousemove_callback); - LiteGraph.pointerListenerAdd(ref_window.document,"move", this._mousemove_callback,true); //catch for the entire window - LiteGraph.pointerListenerAdd(ref_window.document,"up", this._mouseup_callback,true); - } + var x = e.clientX; + var y = e.clientY; + //console.log(y,this.viewport); + //console.log("pointerevents: processMouseDown pointerId:"+e.pointerId+" which:"+e.which+" isPrimary:"+e.isPrimary+" :: x y "+x+" "+y); - if(!is_inside){ - return; - } + this.ds.viewport = this.viewport; + var is_inside = + !this.viewport || + (this.viewport && + x >= this.viewport[0] && + x < this.viewport[0] + this.viewport[2] && + y >= this.viewport[1] && + y < this.viewport[1] + this.viewport[3]); - var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes, 5 ); - var skip_dragging = false; - var skip_action = false; - var now = LiteGraph.getTime(); - var is_primary = (e.isPrimary === undefined || !e.isPrimary); - var is_double_click = (now - this.last_mouseclick < 300); - this.mouse[0] = e.clientX; - this.mouse[1] = e.clientY; - this.graph_mouse[0] = e.canvasX; - this.graph_mouse[1] = e.canvasY; - this.last_click_position = [this.mouse[0],this.mouse[1]]; - - if (this.pointer_is_down && is_primary ){ - this.pointer_is_double = true; - //console.log("pointerevents: pointer_is_double start"); - }else{ - this.pointer_is_double = false; - } - this.pointer_is_down = true; - - - this.canvas.focus(); + //move mouse move event to the window in case it drags outside of the canvas + if (!this.options.skip_events) { + LiteGraph.pointerListenerRemove( + this.canvas, + "move", + this._mousemove_callback + ); + LiteGraph.pointerListenerAdd( + ref_window.document, + "move", + this._mousemove_callback, + true + ); //catch for the entire window + LiteGraph.pointerListenerAdd( + ref_window.document, + "up", + this._mouseup_callback, + true + ); + } - LiteGraph.closeAllContextMenus(ref_window); + if (!is_inside) { + return; + } - if (this.onMouse) - { - if (this.onMouse(e) == true) - return; - } + var node = this.graph.getNodeOnPos( + e.canvasX, + e.canvasY, + this.visible_nodes, + 5 + ); + var skip_dragging = false; + var skip_action = false; + var now = LiteGraph.getTime(); + var is_primary = e.isPrimary === undefined || !e.isPrimary; + var is_double_click = now - this.last_mouseclick < 300; + this.mouse[0] = e.clientX; + this.mouse[1] = e.clientY; + this.graph_mouse[0] = e.canvasX; + this.graph_mouse[1] = e.canvasY; + this.last_click_position = [this.mouse[0], this.mouse[1]]; - //left button mouse / single finger - if (e.which == 1 && !this.pointer_is_double) - { - if (e.ctrlKey) - { - this.dragging_rectangle = new Float32Array(4); - this.dragging_rectangle[0] = e.canvasX; - this.dragging_rectangle[1] = e.canvasY; - this.dragging_rectangle[2] = 1; - this.dragging_rectangle[3] = 1; - skip_action = true; + if (this.pointer_is_down && is_primary) { + this.pointer_is_double = true; + //console.log("pointerevents: pointer_is_double start"); + } else { + this.pointer_is_double = false; + } + this.pointer_is_down = true; + + this.canvas.focus(); + + LiteGraph.closeAllContextMenus(ref_window); + + if (this.onMouse) { + if (this.onMouse(e) == true) return; + } + + //left button mouse / single finger + if (e.which == 1 && !this.pointer_is_double) { + if (e.ctrlKey) { + this.dragging_rectangle = new Float32Array(4); + this.dragging_rectangle[0] = e.canvasX; + this.dragging_rectangle[1] = e.canvasY; + this.dragging_rectangle[2] = 1; + this.dragging_rectangle[3] = 1; + skip_action = true; + } + + // clone node ALT dragging + if ( + LiteGraph.alt_drag_do_clone_nodes && + e.altKey && + node && + this.allow_interaction && + !skip_action && + !this.read_only + ) { + if ((cloned = node.clone())) { + cloned.pos[0] += 5; + cloned.pos[1] += 5; + this.graph.add(cloned, false, { doCalcSize: false }); + node = cloned; + skip_action = true; + if (!block_drag_node) { + if (this.allow_dragnodes) { + this.graph.beforeChange(); + this.node_dragged = node; } + if (!this.selected_nodes[node.id]) { + this.processNodeSelected(node, e); + } + } + } + } - // clone node ALT dragging - if (LiteGraph.alt_drag_do_clone_nodes && e.altKey && node && this.allow_interaction && !skip_action && !this.read_only) - { - if (cloned = node.clone()){ - cloned.pos[0] += 5; - cloned.pos[1] += 5; - this.graph.add(cloned,false,{doCalcSize: false}); - node = cloned; - skip_action = true; - if (!block_drag_node) { - if (this.allow_dragnodes) { - this.graph.beforeChange(); - this.node_dragged = node; - } - if (!this.selected_nodes[node.id]) { - this.processNodeSelected(node, e); - } + var clicking_canvas_bg = false; + + //when clicked on top of a node + //and it is not interactive + if ( + node && + (this.allow_interaction || node.flags.allow_interaction) && + !skip_action && + !this.read_only + ) { + if (!this.live_mode && !node.flags.pinned) { + this.bringToFront(node); + } //if it wasn't selected? + + //not dragging mouse to connect two slots + if ( + this.allow_interaction && + !this.connecting_node && + !node.flags.collapsed && + !this.live_mode + ) { + //Search for corner for resize + if ( + !skip_action && + node.resizable !== false && + node.inResizeCorner(e.canvasX, e.canvasY) + ) { + this.graph.beforeChange(); + this.resizing_node = node; + this.canvas.style.cursor = "se-resize"; + skip_action = true; + } else { + //search for outputs + if (node.outputs) { + for (var i = 0, l = node.outputs.length; i < l; ++i) { + var output = node.outputs[i]; + var link_pos = node.getConnectionPos(false, i); + if ( + isInsideRectangle( + e.canvasX, + e.canvasY, + link_pos[0] - 15, + link_pos[1] - 10, + 30, + 20 + ) + ) { + this.connecting_node = node; + this.connecting_output = output; + this.connecting_output.slot_index = i; + this.connecting_pos = node.getConnectionPos(false, i); + this.connecting_slot = i; + + if (LiteGraph.shift_click_do_break_link_from) { + if (e.shiftKey) { + node.disconnectOutput(i); } + } + + if (is_double_click) { + if (node.onOutputDblClick) { + node.onOutputDblClick(i, e); + } + } else { + if (node.onOutputClick) { + node.onOutputClick(i, e); + } + } + + skip_action = true; + break; } + } } - - var clicking_canvas_bg = false; - //when clicked on top of a node - //and it is not interactive - if (node && (this.allow_interaction || node.flags.allow_interaction) && !skip_action && !this.read_only) { - if (!this.live_mode && !node.flags.pinned) { - this.bringToFront(node); - } //if it wasn't selected? + //search for inputs + if (node.inputs) { + for (var i = 0, l = node.inputs.length; i < l; ++i) { + var input = node.inputs[i]; + var link_pos = node.getConnectionPos(true, i); + if ( + isInsideRectangle( + e.canvasX, + e.canvasY, + link_pos[0] - 15, + link_pos[1] - 10, + 30, + 20 + ) + ) { + if (is_double_click) { + if (node.onInputDblClick) { + node.onInputDblClick(i, e); + } + } else { + if (node.onInputClick) { + node.onInputClick(i, e); + } + } - //not dragging mouse to connect two slots - if ( this.allow_interaction && !this.connecting_node && !node.flags.collapsed && !this.live_mode ) { - //Search for corner for resize - if ( !skip_action && - node.resizable !== false && node.inResizeCorner(e.canvasX, e.canvasY) + if (input.link !== null) { + var link_info = this.graph.links[input.link]; //before disconnecting + if (LiteGraph.click_do_break_link_to) { + node.disconnectInput(i); + this.dirty_bgcanvas = true; + skip_action = true; + } else { + // do same action as has not node ? + } + + if ( + this.allow_reconnect_links || + //this.move_destination_link_without_shift || + e.shiftKey ) { - this.graph.beforeChange(); - this.resizing_node = node; - this.canvas.style.cursor = "se-resize"; - skip_action = true; - } else { - //search for outputs - if (node.outputs) { - for ( var i = 0, l = node.outputs.length; i < l; ++i ) { - var output = node.outputs[i]; - var link_pos = node.getConnectionPos(false, i); - if ( - isInsideRectangle( - e.canvasX, - e.canvasY, - link_pos[0] - 15, - link_pos[1] - 10, - 30, - 20 - ) - ) { - this.connecting_node = node; - this.connecting_output = output; - this.connecting_output.slot_index = i; - this.connecting_pos = node.getConnectionPos( false, i ); - this.connecting_slot = i; + if (!LiteGraph.click_do_break_link_to) { + node.disconnectInput(i); + } + this.connecting_node = + this.graph._nodes_by_id[link_info.origin_id]; + this.connecting_slot = link_info.origin_slot; + this.connecting_output = + this.connecting_node.outputs[this.connecting_slot]; + this.connecting_pos = + this.connecting_node.getConnectionPos( + false, + this.connecting_slot + ); - if (LiteGraph.shift_click_do_break_link_from){ - if (e.shiftKey) { - node.disconnectOutput(i); - } - } + this.dirty_bgcanvas = true; + skip_action = true; + } + } else { + // has not node + } - if (is_double_click) { - if (node.onOutputDblClick) { - node.onOutputDblClick(i, e); - } - } else { - if (node.onOutputClick) { - node.onOutputClick(i, e); - } - } + if (!skip_action) { + // connect from in to out, from to to from + this.connecting_node = node; + this.connecting_input = input; + this.connecting_input.slot_index = i; + this.connecting_pos = node.getConnectionPos(true, i); + this.connecting_slot = i; - skip_action = true; - break; - } - } - } - - //search for inputs - if (node.inputs) { - for ( var i = 0, l = node.inputs.length; i < l; ++i ) { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos(true, i); - if ( - isInsideRectangle( - e.canvasX, - e.canvasY, - link_pos[0] - 15, - link_pos[1] - 10, - 30, - 20 - ) - ) { - if (is_double_click) { - if (node.onInputDblClick) { - node.onInputDblClick(i, e); - } - } else { - if (node.onInputClick) { - node.onInputClick(i, e); - } - } - - if (input.link !== null) { - var link_info = this.graph.links[ - input.link - ]; //before disconnecting - if (LiteGraph.click_do_break_link_to){ - node.disconnectInput(i); - this.dirty_bgcanvas = true; - skip_action = true; - }else{ - // do same action as has not node ? - } - - if ( - this.allow_reconnect_links || - //this.move_destination_link_without_shift || - e.shiftKey - ) { - if (!LiteGraph.click_do_break_link_to){ - node.disconnectInput(i); - } - this.connecting_node = this.graph._nodes_by_id[ - link_info.origin_id - ]; - this.connecting_slot = - link_info.origin_slot; - this.connecting_output = this.connecting_node.outputs[ - this.connecting_slot - ]; - this.connecting_pos = this.connecting_node.getConnectionPos( false, this.connecting_slot ); - - this.dirty_bgcanvas = true; - skip_action = true; - } - - - }else{ - // has not node - } - - if (!skip_action){ - // connect from in to out, from to to from - this.connecting_node = node; - this.connecting_input = input; - this.connecting_input.slot_index = i; - this.connecting_pos = node.getConnectionPos( true, i ); - this.connecting_slot = i; - - this.dirty_bgcanvas = true; - skip_action = true; - } - } - } - } - } //not resizing + this.dirty_bgcanvas = true; + skip_action = true; + } } - - //it wasn't clicked on the links boxes - if (!skip_action) { - var block_drag_node = false; - if(node && node.flags && node.flags.pinned) { - block_drag_node = true; - } - var pos = [e.canvasX - node.pos[0], e.canvasY - node.pos[1]]; - - //widgets - var widget = this.processNodeWidgets( node, this.graph_mouse, e ); - if (widget) { - block_drag_node = true; - this.node_widget = [node, widget]; - } - - //double clicking - if (this.allow_interaction && is_double_click && this.selected_nodes[node.id]) { - //double click node - if (node.onDblClick) { - node.onDblClick( e, pos, this ); - } - this.processNodeDblClicked(node); - block_drag_node = true; - } - - //if do not capture mouse - if ( node.onMouseDown && node.onMouseDown( e, pos, this ) ) { - block_drag_node = true; - } else { - //open subgraph button - if(node.subgraph && !node.skip_subgraph_button) - { - if ( !node.flags.collapsed && pos[0] > node.size[0] - LiteGraph.NODE_TITLE_HEIGHT && pos[1] < 0 ) { - var that = this; - setTimeout(function() { - that.openSubgraph(node.subgraph); - }, 10); - } - } - - if (this.live_mode) { - clicking_canvas_bg = true; - block_drag_node = true; - } - } - - if (!block_drag_node) { - if (this.allow_dragnodes) { - this.graph.beforeChange(); - this.node_dragged = node; - } - this.processNodeSelected(node, e); - } else { // double-click - /** - * Don't call the function if the block is already selected. - * Otherwise, it could cause the block to be unselected while its panel is open. - */ - if (!node.is_selected) this.processNodeSelected(node, e); - } - - this.dirty_canvas = true; - } - } //clicked outside of nodes - else { - if (!skip_action){ - //search for link connector - if(!this.read_only) { - for (var i = 0; i < this.visible_links.length; ++i) { - var link = this.visible_links[i]; - var center = link._pos; - if ( - !center || - e.canvasX < center[0] - 4 || - e.canvasX > center[0] + 4 || - e.canvasY < center[1] - 4 || - e.canvasY > center[1] + 4 - ) { - continue; - } - //link clicked - this.showLinkMenu(link, e); - this.over_link_center = null; //clear tooltip - break; - } - } - - this.selected_group = this.graph.getGroupOnPos( e.canvasX, e.canvasY ); - this.selected_group_resizing = false; - if (this.selected_group && !this.read_only ) { - if (e.ctrlKey) { - this.dragging_rectangle = null; - } - - var dist = distance( [e.canvasX, e.canvasY], [ this.selected_group.pos[0] + this.selected_group.size[0], this.selected_group.pos[1] + this.selected_group.size[1] ] ); - if (dist * this.ds.scale < 10) { - this.selected_group_resizing = true; - } else { - this.selected_group.recomputeInsideNodes(); - } - } - - if (is_double_click && !this.read_only && this.allow_searchbox) { - this.showSearchBox(e); - e.preventDefault(); - e.stopPropagation(); - } - - clicking_canvas_bg = true; - } + } } - - if (!skip_action && clicking_canvas_bg && this.allow_dragcanvas) { - //console.log("pointerevents: dragging_canvas start"); - this.dragging_canvas = true; - } - - } else if (e.which == 2) { - //middle button - - if (LiteGraph.middle_click_slot_add_default_node){ - if (node && this.allow_interaction && !skip_action && !this.read_only){ - //not dragging mouse to connect two slots - if ( - !this.connecting_node && - !node.flags.collapsed && - !this.live_mode - ) { - var mClikSlot = false; - var mClikSlot_index = false; - var mClikSlot_isOut = false; - //search for outputs - if (node.outputs) { - for ( var i = 0, l = node.outputs.length; i < l; ++i ) { - var output = node.outputs[i]; - var link_pos = node.getConnectionPos(false, i); - if (isInsideRectangle(e.canvasX,e.canvasY,link_pos[0] - 15,link_pos[1] - 10,30,20)) { - mClikSlot = output; - mClikSlot_index = i; - mClikSlot_isOut = true; - break; - } - } - } - - //search for inputs - if (node.inputs) { - for ( var i = 0, l = node.inputs.length; i < l; ++i ) { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos(true, i); - if (isInsideRectangle(e.canvasX,e.canvasY,link_pos[0] - 15,link_pos[1] - 10,30,20)) { - mClikSlot = input; - mClikSlot_index = i; - mClikSlot_isOut = false; - break; - } - } - } - //console.log("middleClickSlots? "+mClikSlot+" & "+(mClikSlot_index!==false)); - if (mClikSlot && mClikSlot_index!==false){ - - var alphaPosY = 0.5-((mClikSlot_index+1)/((mClikSlot_isOut?node.outputs.length:node.inputs.length))); - var node_bounding = node.getBounding(); - // estimate a position: this is a bad semi-bad-working mess .. REFACTOR with a correct autoplacement that knows about the others slots and nodes - var posRef = [ (!mClikSlot_isOut?node_bounding[0]:node_bounding[0]+node_bounding[2])// + node_bounding[0]/this.canvas.width*150 - ,e.canvasY-80// + node_bounding[0]/this.canvas.width*66 // vertical "derive" - ]; - var nodeCreated = this.createDefaultNodeForSlot({ nodeFrom: !mClikSlot_isOut?null:node - ,slotFrom: !mClikSlot_isOut?null:mClikSlot_index - ,nodeTo: !mClikSlot_isOut?node:null - ,slotTo: !mClikSlot_isOut?mClikSlot_index:null - ,position: posRef //,e: e - ,nodeType: "AUTO" //nodeNewType - ,posAdd:[!mClikSlot_isOut?-30:30, -alphaPosY*130] //-alphaPosY*30] - ,posSizeFix:[!mClikSlot_isOut?-1:0, 0] //-alphaPosY*2*/ - }); - skip_action = true; - } - } - } - } - - if (!skip_action && this.allow_dragcanvas) { - //console.log("pointerevents: dragging_canvas start from middle button"); - this.dragging_canvas = true; - } - - - } else if (e.which == 3 || this.pointer_is_double) { - - //right button - if (this.allow_interaction && !skip_action && !this.read_only){ - - // is it hover a node ? - if (node){ - if(Object.keys(this.selected_nodes).length - && (this.selected_nodes[node.id] || e.shiftKey || e.ctrlKey || e.metaKey) - ){ - // is multiselected or using shift to include the now node - if (!this.selected_nodes[node.id]) this.selectNodes([node],true); // add this if not present - }else{ - // update selection - this.selectNodes([node]); - } - } - - // show menu on this node - this.processContextMenu(node, e); - } - + } //not resizing } - //TODO - //if(this.node_selected != prev_selected) - // this.onNodeSelectionChange(this.node_selected); + //it wasn't clicked on the links boxes + if (!skip_action) { + var block_drag_node = false; + if (node && node.flags && node.flags.pinned) { + block_drag_node = true; + } + var pos = [e.canvasX - node.pos[0], e.canvasY - node.pos[1]]; - this.last_mouse[0] = e.clientX; - this.last_mouse[1] = e.clientY; - this.last_mouseclick = LiteGraph.getTime(); - this.last_mouse_dragging = true; + //widgets + var widget = this.processNodeWidgets(node, this.graph_mouse, e); + if (widget) { + block_drag_node = true; + this.node_widget = [node, widget]; + } - /* + //double clicking + if ( + this.allow_interaction && + is_double_click && + this.selected_nodes[node.id] + ) { + //double click node + if (node.onDblClick) { + node.onDblClick(e, pos, this); + } + this.processNodeDblClicked(node); + block_drag_node = true; + } + + //if do not capture mouse + if (node.onMouseDown && node.onMouseDown(e, pos, this)) { + block_drag_node = true; + } else { + //open subgraph button + if (node.subgraph && !node.skip_subgraph_button) { + if ( + !node.flags.collapsed && + pos[0] > node.size[0] - LiteGraph.NODE_TITLE_HEIGHT && + pos[1] < 0 + ) { + var that = this; + setTimeout(function () { + that.openSubgraph(node.subgraph); + }, 10); + } + } + + if (this.live_mode) { + clicking_canvas_bg = true; + block_drag_node = true; + } + } + + if (!block_drag_node) { + if (this.allow_dragnodes) { + this.graph.beforeChange(); + this.node_dragged = node; + } + this.processNodeSelected(node, e); + } else { + // double-click + /** + * Don't call the function if the block is already selected. + * Otherwise, it could cause the block to be unselected while its panel is open. + */ + if (!node.is_selected) this.processNodeSelected(node, e); + } + + this.dirty_canvas = true; + } + } //clicked outside of nodes + else { + if (!skip_action) { + //search for link connector + if (!this.read_only) { + for (var i = 0; i < this.visible_links.length; ++i) { + var link = this.visible_links[i]; + var center = link._pos; + if ( + !center || + e.canvasX < center[0] - 4 || + e.canvasX > center[0] + 4 || + e.canvasY < center[1] - 4 || + e.canvasY > center[1] + 4 + ) { + continue; + } + //link clicked + this.showLinkMenu(link, e); + this.over_link_center = null; //clear tooltip + break; + } + } + + this.selected_group = this.graph.getGroupOnPos(e.canvasX, e.canvasY); + this.selected_group_resizing = false; + if (this.selected_group && !this.read_only) { + if (e.ctrlKey) { + this.dragging_rectangle = null; + } + + var dist = distance( + [e.canvasX, e.canvasY], + [ + this.selected_group.pos[0] + this.selected_group.size[0], + this.selected_group.pos[1] + this.selected_group.size[1], + ] + ); + if (dist * this.ds.scale < 10) { + this.selected_group_resizing = true; + } else { + this.selected_group.recomputeInsideNodes(); + } + } + + if (is_double_click && !this.read_only && this.allow_searchbox) { + this.showSearchBox(e); + e.preventDefault(); + e.stopPropagation(); + } + + clicking_canvas_bg = true; + } + } + + if (!skip_action && clicking_canvas_bg && this.allow_dragcanvas) { + //console.log("pointerevents: dragging_canvas start"); + this.dragging_canvas = true; + } + } else if (e.which == 2) { + //middle button + + if (LiteGraph.middle_click_slot_add_default_node) { + if (node && this.allow_interaction && !skip_action && !this.read_only) { + //not dragging mouse to connect two slots + if ( + !this.connecting_node && + !node.flags.collapsed && + !this.live_mode + ) { + var mClikSlot = false; + var mClikSlot_index = false; + var mClikSlot_isOut = false; + //search for outputs + if (node.outputs) { + for (var i = 0, l = node.outputs.length; i < l; ++i) { + var output = node.outputs[i]; + var link_pos = node.getConnectionPos(false, i); + if ( + isInsideRectangle( + e.canvasX, + e.canvasY, + link_pos[0] - 15, + link_pos[1] - 10, + 30, + 20 + ) + ) { + mClikSlot = output; + mClikSlot_index = i; + mClikSlot_isOut = true; + break; + } + } + } + + //search for inputs + if (node.inputs) { + for (var i = 0, l = node.inputs.length; i < l; ++i) { + var input = node.inputs[i]; + var link_pos = node.getConnectionPos(true, i); + if ( + isInsideRectangle( + e.canvasX, + e.canvasY, + link_pos[0] - 15, + link_pos[1] - 10, + 30, + 20 + ) + ) { + mClikSlot = input; + mClikSlot_index = i; + mClikSlot_isOut = false; + break; + } + } + } + //console.log("middleClickSlots? "+mClikSlot+" & "+(mClikSlot_index!==false)); + if (mClikSlot && mClikSlot_index !== false) { + var alphaPosY = + 0.5 - + (mClikSlot_index + 1) / + (mClikSlot_isOut ? node.outputs.length : node.inputs.length); + var node_bounding = node.getBounding(); + // estimate a position: this is a bad semi-bad-working mess .. REFACTOR with a correct autoplacement that knows about the others slots and nodes + var posRef = [ + !mClikSlot_isOut + ? node_bounding[0] + : node_bounding[0] + node_bounding[2], // + node_bounding[0]/this.canvas.width*150 + e.canvasY - 80, // + node_bounding[0]/this.canvas.width*66 // vertical "derive" + ]; + var nodeCreated = this.createDefaultNodeForSlot({ + nodeFrom: !mClikSlot_isOut ? null : node, + slotFrom: !mClikSlot_isOut ? null : mClikSlot_index, + nodeTo: !mClikSlot_isOut ? node : null, + slotTo: !mClikSlot_isOut ? mClikSlot_index : null, + position: posRef, //,e: e + nodeType: "AUTO", //nodeNewType + posAdd: [!mClikSlot_isOut ? -30 : 30, -alphaPosY * 130], //-alphaPosY*30] + posSizeFix: [!mClikSlot_isOut ? -1 : 0, 0], //-alphaPosY*2*/ + }); + skip_action = true; + } + } + } + } + + if (!skip_action && this.allow_dragcanvas) { + //console.log("pointerevents: dragging_canvas start from middle button"); + this.dragging_canvas = true; + } + } else if (e.which == 3 || this.pointer_is_double) { + //right button + if (this.allow_interaction && !skip_action && !this.read_only) { + // is it hover a node ? + if (node) { + if ( + Object.keys(this.selected_nodes).length && + (this.selected_nodes[node.id] || + e.shiftKey || + e.ctrlKey || + e.metaKey) + ) { + // is multiselected or using shift to include the now node + if (!this.selected_nodes[node.id]) this.selectNodes([node], true); // add this if not present + } else { + // update selection + this.selectNodes([node]); + } + } + + // show menu on this node + this.processContextMenu(node, e); + } + } + + //TODO + //if(this.node_selected != prev_selected) + // this.onNodeSelectionChange(this.node_selected); + + this.last_mouse[0] = e.clientX; + this.last_mouse[1] = e.clientY; + this.last_mouseclick = LiteGraph.getTime(); + this.last_mouse_dragging = true; + + /* if( (this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) this.draw(); */ - this.graph.change(); + this.graph.change(); - //this is to ensure to defocus(blur) if a text input element is on focus - if ( - !ref_window.document.activeElement || - (ref_window.document.activeElement.nodeName.toLowerCase() != - "input" && - ref_window.document.activeElement.nodeName.toLowerCase() != - "textarea") - ) { - e.preventDefault(); - } - e.stopPropagation(); + //this is to ensure to defocus(blur) if a text input element is on focus + if ( + !ref_window.document.activeElement || + (ref_window.document.activeElement.nodeName.toLowerCase() != "input" && + ref_window.document.activeElement.nodeName.toLowerCase() != "textarea") + ) { + e.preventDefault(); + } + e.stopPropagation(); - if (this.onMouseDown) { - this.onMouseDown(e); - } + if (this.onMouseDown) { + this.onMouseDown(e); + } - return false; - }; + return false; + }; - /** - * Called when a mouse move event has to be processed - * @method processMouseMove - **/ - LGraphCanvas.prototype.processMouseMove = function(e) { - if (this.autoresize) { - this.resize(); - } + /** + * Called when a mouse move event has to be processed + * @method processMouseMove + **/ + LGraphCanvas.prototype.processMouseMove = function (e) { + if (this.autoresize) { + this.resize(); + } - if( this.set_canvas_dirty_on_mouse_event ) - this.dirty_canvas = true; + if (this.set_canvas_dirty_on_mouse_event) this.dirty_canvas = true; - if (!this.graph) { - return; - } + if (!this.graph) { + return; + } - LGraphCanvas.active_canvas = this; - this.adjustMouseEvent(e); - var mouse = [e.clientX, e.clientY]; - this.mouse[0] = mouse[0]; - this.mouse[1] = mouse[1]; - var delta = [ - mouse[0] - this.last_mouse[0], - mouse[1] - this.last_mouse[1] + LGraphCanvas.active_canvas = this; + this.adjustMouseEvent(e); + var mouse = [e.clientX, e.clientY]; + this.mouse[0] = mouse[0]; + this.mouse[1] = mouse[1]; + var delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]]; + this.last_mouse = mouse; + this.graph_mouse[0] = e.canvasX; + this.graph_mouse[1] = e.canvasY; + + //console.log("pointerevents: processMouseMove "+e.pointerId+" "+e.isPrimary); + + if (this.block_click) { + //console.log("pointerevents: processMouseMove block_click"); + e.preventDefault(); + return false; + } + + e.dragging = this.last_mouse_dragging; + + if (this.node_widget) { + this.processNodeWidgets( + this.node_widget[0], + this.graph_mouse, + e, + this.node_widget[1] + ); + this.dirty_canvas = true; + } + + //get node over + var node = this.graph.getNodeOnPos( + e.canvasX, + e.canvasY, + this.visible_nodes + ); + + if (this.dragging_rectangle) { + this.dragging_rectangle[2] = e.canvasX - this.dragging_rectangle[0]; + this.dragging_rectangle[3] = e.canvasY - this.dragging_rectangle[1]; + this.dirty_canvas = true; + } else if (this.selected_group && !this.read_only) { + //moving/resizing a group + if (this.selected_group_resizing) { + this.selected_group.size = [ + e.canvasX - this.selected_group.pos[0], + e.canvasY - this.selected_group.pos[1], ]; - this.last_mouse = mouse; - this.graph_mouse[0] = e.canvasX; - this.graph_mouse[1] = e.canvasY; + } else { + var deltax = delta[0] / this.ds.scale; + var deltay = delta[1] / this.ds.scale; + this.selected_group.move(deltax, deltay, e.ctrlKey); + if (this.selected_group._nodes.length) { + this.dirty_canvas = true; + } + } + this.dirty_bgcanvas = true; + } else if (this.dragging_canvas) { + ////console.log("pointerevents: processMouseMove is dragging_canvas"); + this.ds.offset[0] += delta[0] / this.ds.scale; + this.ds.offset[1] += delta[1] / this.ds.scale; + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + } else if ( + (this.allow_interaction || (node && node.flags.allow_interaction)) && + !this.read_only + ) { + if (this.connecting_node) { + this.dirty_canvas = true; + } - //console.log("pointerevents: processMouseMove "+e.pointerId+" "+e.isPrimary); - - if(this.block_click) - { - //console.log("pointerevents: processMouseMove block_click"); - e.preventDefault(); - return false; - } + //remove mouseover flag + for (var i = 0, l = this.graph._nodes.length; i < l; ++i) { + if (this.graph._nodes[i].mouseOver && node != this.graph._nodes[i]) { + //mouse leave + this.graph._nodes[i].mouseOver = false; + if (this.node_over && this.node_over.onMouseLeave) { + this.node_over.onMouseLeave(e); + } + this.node_over = null; + this.dirty_canvas = true; + } + } - e.dragging = this.last_mouse_dragging; + //mouse over a node + if (node) { + if (node.redraw_on_mouse) this.dirty_canvas = true; - if (this.node_widget) { - this.processNodeWidgets( - this.node_widget[0], - this.graph_mouse, - e, - this.node_widget[1] - ); - this.dirty_canvas = true; + //this.canvas.style.cursor = "move"; + if (!node.mouseOver) { + //mouse enter + node.mouseOver = true; + this.node_over = node; + this.dirty_canvas = true; + + if (node.onMouseEnter) { + node.onMouseEnter(e); + } } - //get node over - var node = this.graph.getNodeOnPos(e.canvasX,e.canvasY,this.visible_nodes); + //in case the node wants to do something + if (node.onMouseMove) { + node.onMouseMove( + e, + [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], + this + ); + } - if (this.dragging_rectangle) - { - this.dragging_rectangle[2] = e.canvasX - this.dragging_rectangle[0]; - this.dragging_rectangle[3] = e.canvasY - this.dragging_rectangle[1]; - this.dirty_canvas = true; - } - else if (this.selected_group && !this.read_only) - { - //moving/resizing a group - if (this.selected_group_resizing) { - this.selected_group.size = [ - e.canvasX - this.selected_group.pos[0], - e.canvasY - this.selected_group.pos[1] - ]; + //if dragging a link + if (this.connecting_node) { + if (this.connecting_output) { + var pos = this._highlight_input || [0, 0]; //to store the output of isOverNodeInput + + //on top of input + if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) { + //mouse on top of the corner box, don't know what to do } else { - var deltax = delta[0] / this.ds.scale; - var deltay = delta[1] / this.ds.scale; - this.selected_group.move(deltax, deltay, e.ctrlKey); - if (this.selected_group._nodes.length) { - this.dirty_canvas = true; + //check if I have a slot below de mouse + var slot = this.isOverNodeInput(node, e.canvasX, e.canvasY, pos); + if (slot != -1 && node.inputs[slot]) { + var slot_type = node.inputs[slot].type; + if ( + LiteGraph.isValidConnection( + this.connecting_output.type, + slot_type + ) + ) { + this._highlight_input = pos; + this._highlight_input_slot = node.inputs[slot]; // XXX CHECK THIS } + } else { + this._highlight_input = null; + this._highlight_input_slot = null; // XXX CHECK THIS + } } - this.dirty_bgcanvas = true; - } else if (this.dragging_canvas) { - ////console.log("pointerevents: processMouseMove is dragging_canvas"); - this.ds.offset[0] += delta[0] / this.ds.scale; - this.ds.offset[1] += delta[1] / this.ds.scale; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } else if ((this.allow_interaction || (node && node.flags.allow_interaction)) && !this.read_only) { - if (this.connecting_node) { - this.dirty_canvas = true; - } - - //remove mouseover flag - for (var i = 0, l = this.graph._nodes.length; i < l; ++i) { - if (this.graph._nodes[i].mouseOver && node != this.graph._nodes[i] ) { - //mouse leave - this.graph._nodes[i].mouseOver = false; - if (this.node_over && this.node_over.onMouseLeave) { - this.node_over.onMouseLeave(e); - } - this.node_over = null; - this.dirty_canvas = true; - } - } - - //mouse over a node - if (node) { - - if(node.redraw_on_mouse) - this.dirty_canvas = true; - - //this.canvas.style.cursor = "move"; - if (!node.mouseOver) { - //mouse enter - node.mouseOver = true; - this.node_over = node; - this.dirty_canvas = true; - - if (node.onMouseEnter) { - node.onMouseEnter(e); - } - } - - //in case the node wants to do something - if (node.onMouseMove) { - node.onMouseMove( e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this ); - } - - //if dragging a link - if (this.connecting_node) { - - if (this.connecting_output){ - - var pos = this._highlight_input || [0, 0]; //to store the output of isOverNodeInput - - //on top of input - if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) { - //mouse on top of the corner box, don't know what to do - } else { - //check if I have a slot below de mouse - var slot = this.isOverNodeInput( node, e.canvasX, e.canvasY, pos ); - if (slot != -1 && node.inputs[slot]) { - var slot_type = node.inputs[slot].type; - if ( LiteGraph.isValidConnection( this.connecting_output.type, slot_type ) ) { - this._highlight_input = pos; - this._highlight_input_slot = node.inputs[slot]; // XXX CHECK THIS - } - } else { - this._highlight_input = null; - this._highlight_input_slot = null; // XXX CHECK THIS - } - } - - }else if(this.connecting_input){ - - var pos = this._highlight_output || [0, 0]; //to store the output of isOverNodeOutput - - //on top of output - if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) { - //mouse on top of the corner box, don't know what to do - } else { - //check if I have a slot below de mouse - var slot = this.isOverNodeOutput( node, e.canvasX, e.canvasY, pos ); - if (slot != -1 && node.outputs[slot]) { - var slot_type = node.outputs[slot].type; - if ( LiteGraph.isValidConnection( this.connecting_input.type, slot_type ) ) { - this._highlight_output = pos; - } - } else { - this._highlight_output = null; - } - } - } - } - - //Search for corner - if (this.canvas) { - if (node.inResizeCorner(e.canvasX, e.canvasY)) { - this.canvas.style.cursor = "se-resize"; - } else { - this.canvas.style.cursor = "crosshair"; - } - } - } else { //not over a node - - //search for link connector - var over_link = null; - for (var i = 0; i < this.visible_links.length; ++i) { - var link = this.visible_links[i]; - var center = link._pos; - if ( - !center || - e.canvasX < center[0] - 4 || - e.canvasX > center[0] + 4 || - e.canvasY < center[1] - 4 || - e.canvasY > center[1] + 4 - ) { - continue; - } - over_link = link; - break; - } - if( over_link != this.over_link_center ) - { - this.over_link_center = over_link; - this.dirty_canvas = true; - } - - if (this.canvas) { - this.canvas.style.cursor = ""; - } - } //end - - //send event to node if capturing input (used with widgets that allow drag outside of the area of the node) - if ( this.node_capturing_input && this.node_capturing_input != node && this.node_capturing_input.onMouseMove ) { - this.node_capturing_input.onMouseMove(e,[e.canvasX - this.node_capturing_input.pos[0],e.canvasY - this.node_capturing_input.pos[1]], this); - } - - //node being dragged - if (this.node_dragged && !this.live_mode) { - //console.log("draggin!",this.selected_nodes); - for (var i in this.selected_nodes) { - var n = this.selected_nodes[i]; - n.pos[0] += delta[0] / this.ds.scale; - n.pos[1] += delta[1] / this.ds.scale; - if (!n.is_selected) this.processNodeSelected(n, e); /* - * Don't call the function if the block is already selected. - * Otherwise, it could cause the block to be unselected while dragging. - */ - } - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } - - if (this.resizing_node && !this.live_mode) { - //convert mouse to node space - var desired_size = [ e.canvasX - this.resizing_node.pos[0], e.canvasY - this.resizing_node.pos[1] ]; - var min_size = this.resizing_node.computeSize(); - desired_size[0] = Math.max( min_size[0], desired_size[0] ); - desired_size[1] = Math.max( min_size[1], desired_size[1] ); - this.resizing_node.setSize( desired_size ); - - this.canvas.style.cursor = "se-resize"; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; + } else if (this.connecting_input) { + var pos = this._highlight_output || [0, 0]; //to store the output of isOverNodeOutput + + //on top of output + if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) { + //mouse on top of the corner box, don't know what to do + } else { + //check if I have a slot below de mouse + var slot = this.isOverNodeOutput(node, e.canvasX, e.canvasY, pos); + if (slot != -1 && node.outputs[slot]) { + var slot_type = node.outputs[slot].type; + if ( + LiteGraph.isValidConnection( + this.connecting_input.type, + slot_type + ) + ) { + this._highlight_output = pos; + } + } else { + this._highlight_output = null; + } } + } } - e.preventDefault(); - return false; - }; + //Search for corner + if (this.canvas) { + if (node.inResizeCorner(e.canvasX, e.canvasY)) { + this.canvas.style.cursor = "se-resize"; + } else { + this.canvas.style.cursor = "crosshair"; + } + } + } else { + //not over a node - /** - * Called when a mouse up event has to be processed - * @method processMouseUp - **/ - LGraphCanvas.prototype.processMouseUp = function(e) { + //search for link connector + var over_link = null; + for (var i = 0; i < this.visible_links.length; ++i) { + var link = this.visible_links[i]; + var center = link._pos; + if ( + !center || + e.canvasX < center[0] - 4 || + e.canvasX > center[0] + 4 || + e.canvasY < center[1] - 4 || + e.canvasY > center[1] + 4 + ) { + continue; + } + over_link = link; + break; + } + if (over_link != this.over_link_center) { + this.over_link_center = over_link; + this.dirty_canvas = true; + } - var is_primary = ( e.isPrimary === undefined || e.isPrimary ); + if (this.canvas) { + this.canvas.style.cursor = ""; + } + } //end - //early exit for extra pointer - if(!is_primary){ - /*e.stopPropagation(); + //send event to node if capturing input (used with widgets that allow drag outside of the area of the node) + if ( + this.node_capturing_input && + this.node_capturing_input != node && + this.node_capturing_input.onMouseMove + ) { + this.node_capturing_input.onMouseMove( + e, + [ + e.canvasX - this.node_capturing_input.pos[0], + e.canvasY - this.node_capturing_input.pos[1], + ], + this + ); + } + + //node being dragged + if (this.node_dragged && !this.live_mode) { + //console.log("draggin!",this.selected_nodes); + for (var i in this.selected_nodes) { + var n = this.selected_nodes[i]; + n.pos[0] += delta[0] / this.ds.scale; + n.pos[1] += delta[1] / this.ds.scale; + if (!n.is_selected) this.processNodeSelected(n, e); /* + * Don't call the function if the block is already selected. + * Otherwise, it could cause the block to be unselected while dragging. + */ + } + + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + } + + if (this.resizing_node && !this.live_mode) { + //convert mouse to node space + var desired_size = [ + e.canvasX - this.resizing_node.pos[0], + e.canvasY - this.resizing_node.pos[1], + ]; + var min_size = this.resizing_node.computeSize(); + desired_size[0] = Math.max(min_size[0], desired_size[0]); + desired_size[1] = Math.max(min_size[1], desired_size[1]); + this.resizing_node.setSize(desired_size); + + this.canvas.style.cursor = "se-resize"; + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + } + } + + e.preventDefault(); + return false; + }; + + /** + * Called when a mouse up event has to be processed + * @method processMouseUp + **/ + LGraphCanvas.prototype.processMouseUp = function (e) { + var is_primary = e.isPrimary === undefined || e.isPrimary; + + //early exit for extra pointer + if (!is_primary) { + /*e.stopPropagation(); e.preventDefault();*/ - //console.log("pointerevents: processMouseUp pointerN_stop "+e.pointerId+" "+e.isPrimary); - return false; - } - - //console.log("pointerevents: processMouseUp "+e.pointerId+" "+e.isPrimary+" :: "+e.clientX+" "+e.clientY); - - if( this.set_canvas_dirty_on_mouse_event ) - this.dirty_canvas = true; + //console.log("pointerevents: processMouseUp pointerN_stop "+e.pointerId+" "+e.isPrimary); + return false; + } - if (!this.graph) - return; + //console.log("pointerevents: processMouseUp "+e.pointerId+" "+e.isPrimary+" :: "+e.clientX+" "+e.clientY); - var window = this.getCanvasWindow(); - var document = window.document; - LGraphCanvas.active_canvas = this; + if (this.set_canvas_dirty_on_mouse_event) this.dirty_canvas = true; - //restore the mousemove event back to the canvas - if(!this.options.skip_events) - { - //console.log("pointerevents: processMouseUp adjustEventListener"); - LiteGraph.pointerListenerRemove(document,"move", this._mousemove_callback,true); - LiteGraph.pointerListenerAdd(this.canvas,"move", this._mousemove_callback,true); - LiteGraph.pointerListenerRemove(document,"up", this._mouseup_callback,true); - } + if (!this.graph) return; - this.adjustMouseEvent(e); - var now = LiteGraph.getTime(); - e.click_time = now - this.last_mouseclick; - this.last_mouse_dragging = false; - this.last_click_position = null; + var window = this.getCanvasWindow(); + var document = window.document; + LGraphCanvas.active_canvas = this; - if(this.block_click) - { - //console.log("pointerevents: processMouseUp block_clicks"); - this.block_click = false; //used to avoid sending twice a click in a immediate button - } + //restore the mousemove event back to the canvas + if (!this.options.skip_events) { + //console.log("pointerevents: processMouseUp adjustEventListener"); + LiteGraph.pointerListenerRemove( + document, + "move", + this._mousemove_callback, + true + ); + LiteGraph.pointerListenerAdd( + this.canvas, + "move", + this._mousemove_callback, + true + ); + LiteGraph.pointerListenerRemove( + document, + "up", + this._mouseup_callback, + true + ); + } - //console.log("pointerevents: processMouseUp which: "+e.which); - - if (e.which == 1) { + this.adjustMouseEvent(e); + var now = LiteGraph.getTime(); + e.click_time = now - this.last_mouseclick; + this.last_mouse_dragging = false; + this.last_click_position = null; - if( this.node_widget ) - { - this.processNodeWidgets( this.node_widget[0], this.graph_mouse, e ); - } + if (this.block_click) { + //console.log("pointerevents: processMouseUp block_clicks"); + this.block_click = false; //used to avoid sending twice a click in a immediate button + } - //left button - this.node_widget = null; + //console.log("pointerevents: processMouseUp which: "+e.which); - if (this.selected_group) { - var diffx = - this.selected_group.pos[0] - - Math.round(this.selected_group.pos[0]); - var diffy = - this.selected_group.pos[1] - - Math.round(this.selected_group.pos[1]); - this.selected_group.move(diffx, diffy, e.ctrlKey); - this.selected_group.pos[0] = Math.round( - this.selected_group.pos[0] - ); - this.selected_group.pos[1] = Math.round( - this.selected_group.pos[1] - ); - if (this.selected_group._nodes.length) { - this.dirty_canvas = true; - } - this.selected_group = null; + if (e.which == 1) { + if (this.node_widget) { + this.processNodeWidgets(this.node_widget[0], this.graph_mouse, e); + } + + //left button + this.node_widget = null; + + if (this.selected_group) { + var diffx = + this.selected_group.pos[0] - Math.round(this.selected_group.pos[0]); + var diffy = + this.selected_group.pos[1] - Math.round(this.selected_group.pos[1]); + this.selected_group.move(diffx, diffy, e.ctrlKey); + this.selected_group.pos[0] = Math.round(this.selected_group.pos[0]); + this.selected_group.pos[1] = Math.round(this.selected_group.pos[1]); + if (this.selected_group._nodes.length) { + this.dirty_canvas = true; + } + this.selected_group = null; + } + this.selected_group_resizing = false; + + var node = this.graph.getNodeOnPos( + e.canvasX, + e.canvasY, + this.visible_nodes + ); + + if (this.dragging_rectangle) { + if (this.graph) { + var nodes = this.graph._nodes; + var node_bounding = new Float32Array(4); + + //compute bounding and flip if left to right + var w = Math.abs(this.dragging_rectangle[2]); + var h = Math.abs(this.dragging_rectangle[3]); + var startx = + this.dragging_rectangle[2] < 0 + ? this.dragging_rectangle[0] - w + : this.dragging_rectangle[0]; + var starty = + this.dragging_rectangle[3] < 0 + ? this.dragging_rectangle[1] - h + : this.dragging_rectangle[1]; + this.dragging_rectangle[0] = startx; + this.dragging_rectangle[1] = starty; + this.dragging_rectangle[2] = w; + this.dragging_rectangle[3] = h; + + // test dragging rect size, if minimun simulate a click + if (!node || (w > 10 && h > 10)) { + //test against all nodes (not visible because the rectangle maybe start outside + var to_select = []; + for (var i = 0; i < nodes.length; ++i) { + var nodeX = nodes[i]; + nodeX.getBounding(node_bounding); + if (!overlapBounding(this.dragging_rectangle, node_bounding)) { + continue; + } //out of the visible area + to_select.push(nodeX); } - this.selected_group_resizing = false; + if (to_select.length) { + this.selectNodes(to_select, e.shiftKey); // add to selection with shift + } + } else { + // will select of update selection + this.selectNodes([node], e.shiftKey || e.ctrlKey); // add to selection add to selection with ctrlKey or shiftKey + } + } + this.dragging_rectangle = null; + } else if (this.connecting_node) { + //dragging a connection + this.dirty_canvas = true; + this.dirty_bgcanvas = true; - var node = this.graph.getNodeOnPos( - e.canvasX, - e.canvasY, - this.visible_nodes - ); - - if (this.dragging_rectangle) { - if (this.graph) { - var nodes = this.graph._nodes; - var node_bounding = new Float32Array(4); - - //compute bounding and flip if left to right - var w = Math.abs(this.dragging_rectangle[2]); - var h = Math.abs(this.dragging_rectangle[3]); - var startx = - this.dragging_rectangle[2] < 0 - ? this.dragging_rectangle[0] - w - : this.dragging_rectangle[0]; - var starty = - this.dragging_rectangle[3] < 0 - ? this.dragging_rectangle[1] - h - : this.dragging_rectangle[1]; - this.dragging_rectangle[0] = startx; - this.dragging_rectangle[1] = starty; - this.dragging_rectangle[2] = w; - this.dragging_rectangle[3] = h; + var connInOrOut = this.connecting_output || this.connecting_input; + var connType = connInOrOut.type; - // test dragging rect size, if minimun simulate a click - if (!node || (w > 10 && h > 10 )){ - //test against all nodes (not visible because the rectangle maybe start outside - var to_select = []; - for (var i = 0; i < nodes.length; ++i) { - var nodeX = nodes[i]; - nodeX.getBounding(node_bounding); - if ( - !overlapBounding( - this.dragging_rectangle, - node_bounding - ) - ) { - continue; - } //out of the visible area - to_select.push(nodeX); - } - if (to_select.length) { - this.selectNodes(to_select,e.shiftKey); // add to selection with shift - } - }else{ - // will select of update selection - this.selectNodes([node],e.shiftKey||e.ctrlKey); // add to selection add to selection with ctrlKey or shiftKey - } - - } - this.dragging_rectangle = null; - } else if (this.connecting_node) { - //dragging a connection - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - - var connInOrOut = this.connecting_output || this.connecting_input; - var connType = connInOrOut.type; - - //node below mouse - if (node) { - - /* no need to condition on event type.. just another type + //node below mouse + if (node) { + /* no need to condition on event type.. just another type if ( connType == LiteGraph.EVENT && this.isOverNodeBox(node, e.canvasX, e.canvasY) @@ -6740,869 +6873,912 @@ LGraphNode.prototype.executeAction = function(action) ); } else {*/ - - //slot below mouse? connect - - if (this.connecting_output){ - - var slot = this.isOverNodeInput( - node, - e.canvasX, - e.canvasY - ); - if (slot != -1) { - this.connecting_node.connect(this.connecting_slot, node, slot); - } else { - //not on top of an input - // look for a good slot - this.connecting_node.connectByType(this.connecting_slot,node,connType); - } - - }else if (this.connecting_input){ - - var slot = this.isOverNodeOutput( - node, - e.canvasX, - e.canvasY - ); - if (slot != -1) { - node.connect(slot, this.connecting_node, this.connecting_slot); // this is inverted has output-input nature like - } else { - //not on top of an input - // look for a good slot - this.connecting_node.connectByTypeOutput(this.connecting_slot,node,connType); - } - - } - - - //} - - }else{ - - // add menu when releasing link in empty space - if (LiteGraph.release_link_on_empty_shows_menu){ - if (e.shiftKey && this.allow_searchbox){ - if(this.connecting_output){ - this.showSearchBox(e,{node_from: this.connecting_node, slot_from: this.connecting_output, type_filter_in: this.connecting_output.type}); - }else if(this.connecting_input){ - this.showSearchBox(e,{node_to: this.connecting_node, slot_from: this.connecting_input, type_filter_out: this.connecting_input.type}); - } - }else{ - if(this.connecting_output){ - this.showConnectionMenu({nodeFrom: this.connecting_node, slotFrom: this.connecting_output, e: e}); - }else if(this.connecting_input){ - this.showConnectionMenu({nodeTo: this.connecting_node, slotTo: this.connecting_input, e: e}); - } - } - } - } + //slot below mouse? connect - this.connecting_output = null; - this.connecting_input = null; - this.connecting_pos = null; - this.connecting_node = null; - this.connecting_slot = -1; - } //not dragging connection - else if (this.resizing_node) { - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.graph.afterChange(this.resizing_node); - this.resizing_node = null; - } else if (this.node_dragged) { - //node being dragged? - var node = this.node_dragged; - if ( - node && - e.click_time < 300 && - isInsideRectangle( e.canvasX, e.canvasY, node.pos[0], node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT ) - ) { - node.collapse(); - } - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]); - this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]); - if (this.graph.config.align_to_grid || this.align_to_grid ) { - this.node_dragged.alignToGrid(); - } - if( this.onNodeMoved ) - this.onNodeMoved( this.node_dragged ); - this.graph.afterChange(this.node_dragged); - this.node_dragged = null; - } //no node being dragged - else { - //get node over - var node = this.graph.getNodeOnPos( - e.canvasX, - e.canvasY, - this.visible_nodes - ); - - if (!node && e.click_time < 300) { - this.deselectAllNodes(); - } - - this.dirty_canvas = true; - this.dragging_canvas = false; - - if (this.node_over && this.node_over.onMouseUp) { - this.node_over.onMouseUp( e, [ e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1] ], this ); - } - if ( - this.node_capturing_input && - this.node_capturing_input.onMouseUp - ) { - this.node_capturing_input.onMouseUp(e, [ - e.canvasX - this.node_capturing_input.pos[0], - e.canvasY - this.node_capturing_input.pos[1] - ]); - } + if (this.connecting_output) { + var slot = this.isOverNodeInput(node, e.canvasX, e.canvasY); + if (slot != -1) { + this.connecting_node.connect(this.connecting_slot, node, slot); + } else { + //not on top of an input + // look for a good slot + this.connecting_node.connectByType( + this.connecting_slot, + node, + connType + ); } - } else if (e.which == 2) { - //middle button - //trace("middle"); - this.dirty_canvas = true; - this.dragging_canvas = false; - } else if (e.which == 3) { - //right button - //trace("right"); - this.dirty_canvas = true; - this.dragging_canvas = false; + } else if (this.connecting_input) { + var slot = this.isOverNodeOutput(node, e.canvasX, e.canvasY); + + if (slot != -1) { + node.connect(slot, this.connecting_node, this.connecting_slot); // this is inverted has output-input nature like + } else { + //not on top of an input + // look for a good slot + this.connecting_node.connectByTypeOutput( + this.connecting_slot, + node, + connType + ); + } + } + + //} + } else { + // add menu when releasing link in empty space + if (LiteGraph.release_link_on_empty_shows_menu) { + if (e.shiftKey && this.allow_searchbox) { + if (this.connecting_output) { + this.showSearchBox(e, { + node_from: this.connecting_node, + slot_from: this.connecting_output, + type_filter_in: this.connecting_output.type, + }); + } else if (this.connecting_input) { + this.showSearchBox(e, { + node_to: this.connecting_node, + slot_from: this.connecting_input, + type_filter_out: this.connecting_input.type, + }); + } + } else { + if (this.connecting_output) { + this.showConnectionMenu({ + nodeFrom: this.connecting_node, + slotFrom: this.connecting_output, + e: e, + }); + } else if (this.connecting_input) { + this.showConnectionMenu({ + nodeTo: this.connecting_node, + slotTo: this.connecting_input, + e: e, + }); + } + } + } } - /* + this.connecting_output = null; + this.connecting_input = null; + this.connecting_pos = null; + this.connecting_node = null; + this.connecting_slot = -1; + } //not dragging connection + else if (this.resizing_node) { + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + this.graph.afterChange(this.resizing_node); + this.resizing_node = null; + } else if (this.node_dragged) { + //node being dragged? + var node = this.node_dragged; + if ( + node && + e.click_time < 300 && + isInsideRectangle( + e.canvasX, + e.canvasY, + node.pos[0], + node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, + LiteGraph.NODE_TITLE_HEIGHT, + LiteGraph.NODE_TITLE_HEIGHT + ) + ) { + node.collapse(); + } + + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]); + this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]); + if (this.graph.config.align_to_grid || this.align_to_grid) { + this.node_dragged.alignToGrid(); + } + if (this.onNodeMoved) this.onNodeMoved(this.node_dragged); + this.graph.afterChange(this.node_dragged); + this.node_dragged = null; + } //no node being dragged + else { + //get node over + var node = this.graph.getNodeOnPos( + e.canvasX, + e.canvasY, + this.visible_nodes + ); + + if (!node && e.click_time < 300) { + this.deselectAllNodes(); + } + + this.dirty_canvas = true; + this.dragging_canvas = false; + + if (this.node_over && this.node_over.onMouseUp) { + this.node_over.onMouseUp( + e, + [ + e.canvasX - this.node_over.pos[0], + e.canvasY - this.node_over.pos[1], + ], + this + ); + } + if (this.node_capturing_input && this.node_capturing_input.onMouseUp) { + this.node_capturing_input.onMouseUp(e, [ + e.canvasX - this.node_capturing_input.pos[0], + e.canvasY - this.node_capturing_input.pos[1], + ]); + } + } + } else if (e.which == 2) { + //middle button + //trace("middle"); + this.dirty_canvas = true; + this.dragging_canvas = false; + } else if (e.which == 3) { + //right button + //trace("right"); + this.dirty_canvas = true; + this.dragging_canvas = false; + } + + /* if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) this.draw(); */ - if (is_primary) - { - this.pointer_is_down = false; - this.pointer_is_double = false; - } - - this.graph.change(); + if (is_primary) { + this.pointer_is_down = false; + this.pointer_is_double = false; + } - //console.log("pointerevents: processMouseUp stopPropagation"); - e.stopPropagation(); - e.preventDefault(); - return false; - }; + this.graph.change(); - /** - * Called when a mouse wheel event has to be processed - * @method processMouseWheel - **/ - LGraphCanvas.prototype.processMouseWheel = function(e) { - if (!this.graph || !this.allow_dragcanvas) { - return; - } + //console.log("pointerevents: processMouseUp stopPropagation"); + e.stopPropagation(); + e.preventDefault(); + return false; + }; - var delta = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60; + /** + * Called when a mouse wheel event has to be processed + * @method processMouseWheel + **/ + LGraphCanvas.prototype.processMouseWheel = function (e) { + if (!this.graph || !this.allow_dragcanvas) { + return; + } - this.adjustMouseEvent(e); + var delta = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60; - var x = e.clientX; - var y = e.clientY; - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - if(!is_inside) - return; + this.adjustMouseEvent(e); - var scale = this.ds.scale; + var x = e.clientX; + var y = e.clientY; + var is_inside = + !this.viewport || + (this.viewport && + x >= this.viewport[0] && + x < this.viewport[0] + this.viewport[2] && + y >= this.viewport[1] && + y < this.viewport[1] + this.viewport[3]); + if (!is_inside) return; - if (delta > 0) { - scale *= 1.1; - } else if (delta < 0) { - scale *= 1 / 1.1; - } + var scale = this.ds.scale; - //this.setZoom( scale, [ e.clientX, e.clientY ] ); - this.ds.changeScale(scale, [e.clientX, e.clientY]); + if (delta > 0) { + scale *= 1.1; + } else if (delta < 0) { + scale *= 1 / 1.1; + } - this.graph.change(); + //this.setZoom( scale, [ e.clientX, e.clientY ] ); + this.ds.changeScale(scale, [e.clientX, e.clientY]); - e.preventDefault(); - return false; // prevent default - }; + this.graph.change(); - /** - * returns true if a position (in graph space) is on top of a node little corner box - * @method isOverNodeBox - **/ - LGraphCanvas.prototype.isOverNodeBox = function(node, canvasx, canvasy) { - var title_height = LiteGraph.NODE_TITLE_HEIGHT; - if ( - isInsideRectangle( - canvasx, - canvasy, - node.pos[0] + 2, - node.pos[1] + 2 - title_height, - title_height - 4, - title_height - 4 - ) - ) { - return true; - } - return false; - }; + e.preventDefault(); + return false; // prevent default + }; - /** - * returns the INDEX if a position (in graph space) is on top of a node input slot - * @method isOverNodeInput - **/ - LGraphCanvas.prototype.isOverNodeInput = function( - node, + /** + * returns true if a position (in graph space) is on top of a node little corner box + * @method isOverNodeBox + **/ + LGraphCanvas.prototype.isOverNodeBox = function (node, canvasx, canvasy) { + var title_height = LiteGraph.NODE_TITLE_HEIGHT; + if ( + isInsideRectangle( canvasx, canvasy, - slot_pos + node.pos[0] + 2, + node.pos[1] + 2 - title_height, + title_height - 4, + title_height - 4 + ) ) { - if (node.inputs) { - for (var i = 0, l = node.inputs.length; i < l; ++i) { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos(true, i); - var is_inside = false; - if (node.horizontal) { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 5, - link_pos[1] - 10, - 10, - 20 - ); - } else { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 10, - link_pos[1] - 5, - 40, - 10 - ); - } - if (is_inside) { - if (slot_pos) { - slot_pos[0] = link_pos[0]; - slot_pos[1] = link_pos[1]; - } - return i; - } - } - } - return -1; - }; - - /** - * returns the INDEX if a position (in graph space) is on top of a node output slot - * @method isOverNodeOuput - **/ - LGraphCanvas.prototype.isOverNodeOutput = function( - node, - canvasx, - canvasy, - slot_pos - ) { - if (node.outputs) { - for (var i = 0, l = node.outputs.length; i < l; ++i) { - var output = node.outputs[i]; - var link_pos = node.getConnectionPos(false, i); - var is_inside = false; - if (node.horizontal) { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 5, - link_pos[1] - 10, - 10, - 20 - ); - } else { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 10, - link_pos[1] - 5, - 40, - 10 - ); - } - if (is_inside) { - if (slot_pos) { - slot_pos[0] = link_pos[0]; - slot_pos[1] = link_pos[1]; - } - return i; - } - } - } - return -1; - }; + return true; + } + return false; + }; - /** - * process a key event - * @method processKey - **/ - LGraphCanvas.prototype.processKey = function(e) { - if (!this.graph) { - return; - } - - var block_default = false; - //console.log(e); //debug - - if (e.target.localName == "input") { - return; - } - - if (e.type == "keydown") { - if (e.keyCode == 32) { - //space - this.dragging_canvas = true; - block_default = true; - } - - if (e.keyCode == 27) { - //esc - if(this.node_panel) this.node_panel.close(); - if(this.options_panel) this.options_panel.close(); - block_default = true; - } - - //select all Control A - if (e.keyCode == 65 && e.ctrlKey) { - this.selectNodes(); - block_default = true; - } - - if ((e.keyCode === 67) && (e.metaKey || e.ctrlKey) && !e.shiftKey) { - //copy - if (this.selected_nodes) { - this.copyToClipboard(); - block_default = true; - } - } - - if ((e.keyCode === 86) && (e.metaKey || e.ctrlKey)) { - //paste - this.pasteFromClipboard(e.shiftKey); - } - - //delete or backspace - if (e.keyCode == 46 || e.keyCode == 8) { - if ( - e.target.localName != "input" && - e.target.localName != "textarea" - ) { - this.deleteSelectedNodes(); - block_default = true; - } - } - - //collapse - //... - - //TODO - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].onKeyDown) { - this.selected_nodes[i].onKeyDown(e); - } - } - } - } else if (e.type == "keyup") { - if (e.keyCode == 32) { - // space - this.dragging_canvas = false; - } - - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].onKeyUp) { - this.selected_nodes[i].onKeyUp(e); - } - } - } - } - - this.graph.change(); - - if (block_default) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }; - - LGraphCanvas.prototype.copyToClipboard = function(nodes) { - var clipboard_info = { - nodes: [], - links: [] - }; - var index = 0; - var selected_nodes_array = []; - if (!nodes) nodes = this.selected_nodes; - for (var i in nodes) { - var node = nodes[i]; - if (node.clonable === false) - continue; - node._relative_id = index; - selected_nodes_array.push(node); - index += 1; - } - - for (var i = 0; i < selected_nodes_array.length; ++i) { - var node = selected_nodes_array[i]; - var cloned = node.clone(); - if(!cloned) - { - console.warn("node type not found: " + node.type ); - continue; - } - clipboard_info.nodes.push(cloned.serialize()); - if (node.inputs && node.inputs.length) { - for (var j = 0; j < node.inputs.length; ++j) { - var input = node.inputs[j]; - if (!input || input.link == null) { - continue; - } - var link_info = this.graph.links[input.link]; - if (!link_info) { - continue; - } - var target_node = this.graph.getNodeById( - link_info.origin_id - ); - if (!target_node) { - continue; - } - clipboard_info.links.push([ - target_node._relative_id, - link_info.origin_slot, //j, - node._relative_id, - link_info.target_slot, - target_node.id - ]); - } - } - } - localStorage.setItem( - "litegrapheditor_clipboard", - JSON.stringify(clipboard_info) - ); - }; - - LGraphCanvas.prototype.pasteFromClipboard = function(isConnectUnselected = false) { - // if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior - if (!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) { - return; - } - var data = localStorage.getItem("litegrapheditor_clipboard"); - if (!data) { - return; - } - - this.graph.beforeChange(); - - //create nodes - var clipboard_info = JSON.parse(data); - // calculate top-left node, could work without this processing but using diff with last node pos :: clipboard_info.nodes[clipboard_info.nodes.length-1].pos - var posMin = false; - var posMinIndexes = false; - for (var i = 0; i < clipboard_info.nodes.length; ++i) { - if (posMin){ - if(posMin[0]>clipboard_info.nodes[i].pos[0]){ - posMin[0] = clipboard_info.nodes[i].pos[0]; - posMinIndexes[0] = i; - } - if(posMin[1]>clipboard_info.nodes[i].pos[1]){ - posMin[1] = clipboard_info.nodes[i].pos[1]; - posMinIndexes[1] = i; - } - } - else{ - posMin = [clipboard_info.nodes[i].pos[0], clipboard_info.nodes[i].pos[1]]; - posMinIndexes = [i, i]; - } - } - var nodes = []; - for (var i = 0; i < clipboard_info.nodes.length; ++i) { - var node_data = clipboard_info.nodes[i]; - var node = LiteGraph.createNode(node_data.type); - if (node) { - node.configure(node_data); - - //paste in last known mouse position - node.pos[0] += this.graph_mouse[0] - posMin[0]; //+= 5; - node.pos[1] += this.graph_mouse[1] - posMin[1]; //+= 5; - - this.graph.add(node,{doProcessChange:false}); - - nodes.push(node); - } - } - - //create links - for (var i = 0; i < clipboard_info.links.length; ++i) { - var link_info = clipboard_info.links[i]; - var origin_node = undefined; - var origin_node_relative_id = link_info[0]; - if (origin_node_relative_id != null) { - origin_node = nodes[origin_node_relative_id]; - } else if (LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) { - var origin_node_id = link_info[4]; - if (origin_node_id) { - origin_node = this.graph.getNodeById(origin_node_id); - } - } - var target_node = nodes[link_info[2]]; - if( origin_node && target_node ) - origin_node.connect(link_info[1], target_node, link_info[3]); - else - console.warn("Warning, nodes missing on pasting"); - } - - this.selectNodes(nodes); - - this.graph.afterChange(); - }; - - /** - * process a item drop event on top the canvas - * @method processDrop - **/ - LGraphCanvas.prototype.processDrop = function(e) { - e.preventDefault(); - this.adjustMouseEvent(e); - var x = e.clientX; - var y = e.clientY; - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - if(!is_inside){ - return; - // --- BREAK --- - } - - var pos = [e.canvasX, e.canvasY]; - - - var node = this.graph ? this.graph.getNodeOnPos(pos[0], pos[1]) : null; - - if (!node) { - var r = null; - if (this.onDropItem) { - r = this.onDropItem(event); - } - if (!r) { - this.checkDropItem(e); - } - return; - } - - if (node.onDropFile || node.onDropData) { - var files = e.dataTransfer.files; - if (files && files.length) { - for (var i = 0; i < files.length; i++) { - var file = e.dataTransfer.files[0]; - var filename = file.name; - var ext = LGraphCanvas.getFileExtension(filename); - //console.log(file); - - if (node.onDropFile) { - node.onDropFile(file); - } - - if (node.onDropData) { - //prepare reader - var reader = new FileReader(); - reader.onload = function(event) { - //console.log(event.target); - var data = event.target.result; - node.onDropData(data, filename, file); - }; - - //read data - var type = file.type.split("/")[0]; - if (type == "text" || type == "") { - reader.readAsText(file); - } else if (type == "image") { - reader.readAsDataURL(file); - } else { - reader.readAsArrayBuffer(file); - } - } - } - } - } - - if (node.onDropItem) { - if (node.onDropItem(event)) { - return true; - } - } - - if (this.onDropItem) { - return this.onDropItem(event); - } - - return false; - }; - - //called if the graph doesn't have a default drop item behaviour - LGraphCanvas.prototype.checkDropItem = function(e) { - if (e.dataTransfer.files.length) { - var file = e.dataTransfer.files[0]; - var ext = LGraphCanvas.getFileExtension(file.name).toLowerCase(); - var nodetype = LiteGraph.node_types_by_file_extension[ext]; - if (nodetype) { - this.graph.beforeChange(); - var node = LiteGraph.createNode(nodetype.type); - node.pos = [e.canvasX, e.canvasY]; - this.graph.add(node); - if (node.onDropFile) { - node.onDropFile(file); - } - this.graph.afterChange(); - } - } - }; - - LGraphCanvas.prototype.processNodeDblClicked = function(n) { - if (this.onShowNodePanel) { - this.onShowNodePanel(n); - } - - if (this.onNodeDblClicked) { - this.onNodeDblClicked(n); - } - - this.setDirty(true); - }; - - LGraphCanvas.prototype.processNodeSelected = function(node, e) { - this.selectNode(node, e && (e.shiftKey || e.ctrlKey || this.multi_select)); - if (this.onNodeSelected) { - this.onNodeSelected(node); - } - }; - - /** - * selects a given node (or adds it to the current selection) - * @method selectNode - **/ - LGraphCanvas.prototype.selectNode = function( - node, - add_to_current_selection - ) { - if (node == null) { - this.deselectAllNodes(); + /** + * returns the INDEX if a position (in graph space) is on top of a node input slot + * @method isOverNodeInput + **/ + LGraphCanvas.prototype.isOverNodeInput = function ( + node, + canvasx, + canvasy, + slot_pos + ) { + if (node.inputs) { + for (var i = 0, l = node.inputs.length; i < l; ++i) { + var input = node.inputs[i]; + var link_pos = node.getConnectionPos(true, i); + var is_inside = false; + if (node.horizontal) { + is_inside = isInsideRectangle( + canvasx, + canvasy, + link_pos[0] - 5, + link_pos[1] - 10, + 10, + 20 + ); } else { - this.selectNodes([node], add_to_current_selection); + is_inside = isInsideRectangle( + canvasx, + canvasy, + link_pos[0] - 10, + link_pos[1] - 5, + 40, + 10 + ); } - }; - - /** - * selects several nodes (or adds them to the current selection) - * @method selectNodes - **/ - LGraphCanvas.prototype.selectNodes = function( nodes, add_to_current_selection ) - { - if (!add_to_current_selection) { - this.deselectAllNodes(); + if (is_inside) { + if (slot_pos) { + slot_pos[0] = link_pos[0]; + slot_pos[1] = link_pos[1]; + } + return i; } + } + } + return -1; + }; - nodes = nodes || this.graph._nodes; - if (typeof nodes == "string") nodes = [nodes]; - for (var i in nodes) { - var node = nodes[i]; - if (node.is_selected) { - this.deselectNode(node); - continue; - } - - if (!node.is_selected && node.onSelected) { - node.onSelected(); - } - node.is_selected = true; - this.selected_nodes[node.id] = node; - - if (node.inputs) { - for (var j = 0; j < node.inputs.length; ++j) { - this.highlighted_links[node.inputs[j].link] = true; - } - } - if (node.outputs) { - for (var j = 0; j < node.outputs.length; ++j) { - var out = node.outputs[j]; - if (out.links) { - for (var k = 0; k < out.links.length; ++k) { - this.highlighted_links[out.links[k]] = true; - } - } - } - } + /** + * returns the INDEX if a position (in graph space) is on top of a node output slot + * @method isOverNodeOuput + **/ + LGraphCanvas.prototype.isOverNodeOutput = function ( + node, + canvasx, + canvasy, + slot_pos + ) { + if (node.outputs) { + for (var i = 0, l = node.outputs.length; i < l; ++i) { + var output = node.outputs[i]; + var link_pos = node.getConnectionPos(false, i); + var is_inside = false; + if (node.horizontal) { + is_inside = isInsideRectangle( + canvasx, + canvasy, + link_pos[0] - 5, + link_pos[1] - 10, + 10, + 20 + ); + } else { + is_inside = isInsideRectangle( + canvasx, + canvasy, + link_pos[0] - 10, + link_pos[1] - 5, + 40, + 10 + ); } - - if( this.onSelectionChange ) - this.onSelectionChange( this.selected_nodes ); - - this.setDirty(true); - }; - - /** - * removes a node from the current selection - * @method deselectNode - **/ - LGraphCanvas.prototype.deselectNode = function(node) { - if (!node.is_selected) { - return; + if (is_inside) { + if (slot_pos) { + slot_pos[0] = link_pos[0]; + slot_pos[1] = link_pos[1]; + } + return i; } - if (node.onDeselected) { - node.onDeselected(); - } - node.is_selected = false; + } + } + return -1; + }; - if (this.onNodeDeselected) { - this.onNodeDeselected(node); - } + /** + * process a key event + * @method processKey + **/ + LGraphCanvas.prototype.processKey = function (e) { + if (!this.graph) { + return; + } - //remove highlighted - if (node.inputs) { - for (var i = 0; i < node.inputs.length; ++i) { - delete this.highlighted_links[node.inputs[i].link]; - } - } - if (node.outputs) { - for (var i = 0; i < node.outputs.length; ++i) { - var out = node.outputs[i]; - if (out.links) { - for (var j = 0; j < out.links.length; ++j) { - delete this.highlighted_links[out.links[j]]; - } - } - } - } - }; + var block_default = false; + //console.log(e); //debug - /** - * removes all nodes from the current selection - * @method deselectAllNodes - **/ - LGraphCanvas.prototype.deselectAllNodes = function() { - if (!this.graph) { - return; + if (e.target.localName == "input") { + return; + } + + if (e.type == "keydown") { + if (e.keyCode == 32) { + //space + this.dragging_canvas = true; + block_default = true; + } + + if (e.keyCode == 27) { + //esc + if (this.node_panel) this.node_panel.close(); + if (this.options_panel) this.options_panel.close(); + block_default = true; + } + + //select all Control A + if (e.keyCode == 65 && e.ctrlKey) { + this.selectNodes(); + block_default = true; + } + + if (e.keyCode === 67 && (e.metaKey || e.ctrlKey) && !e.shiftKey) { + //copy + if (this.selected_nodes) { + this.copyToClipboard(); + block_default = true; } - var nodes = this.graph._nodes; - for (var i = 0, l = nodes.length; i < l; ++i) { - var node = nodes[i]; - if (!node.is_selected) { - continue; - } - if (node.onDeselected) { - node.onDeselected(); - } - node.is_selected = false; - if (this.onNodeDeselected) { - this.onNodeDeselected(node); - } + } + + if (e.keyCode === 86 && (e.metaKey || e.ctrlKey)) { + //paste + this.pasteFromClipboard(e.shiftKey); + } + + //delete or backspace + if (e.keyCode == 46 || e.keyCode == 8) { + if (e.target.localName != "input" && e.target.localName != "textarea") { + this.deleteSelectedNodes(); + block_default = true; } - this.selected_nodes = {}; - this.current_node = null; - this.highlighted_links = {}; - if( this.onSelectionChange ) - this.onSelectionChange( this.selected_nodes ); - this.setDirty(true); - }; + } - /** - * deletes all nodes in the current selection from the graph - * @method deleteSelectedNodes - **/ - LGraphCanvas.prototype.deleteSelectedNodes = function() { - - this.graph.beforeChange(); + //collapse + //... + //TODO + if (this.selected_nodes) { for (var i in this.selected_nodes) { - var node = this.selected_nodes[i]; - - if(node.block_delete) - continue; - - //autoconnect when possible (very basic, only takes into account first input-output) - if(node.inputs && node.inputs.length && node.outputs && node.outputs.length && LiteGraph.isValidConnection( node.inputs[0].type, node.outputs[0].type ) && node.inputs[0].link && node.outputs[0].links && node.outputs[0].links.length ) - { - var input_link = node.graph.links[ node.inputs[0].link ]; - var output_link = node.graph.links[ node.outputs[0].links[0] ]; - var input_node = node.getInputNode(0); - var output_node = node.getOutputNodes(0)[0]; - if(input_node && output_node) - input_node.connect( input_link.origin_slot, output_node, output_link.target_slot ); - } - this.graph.remove(node); - if (this.onNodeDeselected) { - this.onNodeDeselected(node); - } + if (this.selected_nodes[i].onKeyDown) { + this.selected_nodes[i].onKeyDown(e); + } } - this.selected_nodes = {}; - this.current_node = null; - this.highlighted_links = {}; - this.setDirty(true); - this.graph.afterChange(); - }; - - /** - * centers the camera on a given node - * @method centerOnNode - **/ - LGraphCanvas.prototype.centerOnNode = function(node) { - this.ds.offset[0] = - -node.pos[0] - - node.size[0] * 0.5 + - (this.canvas.width * 0.5) / this.ds.scale; - this.ds.offset[1] = - -node.pos[1] - - node.size[1] * 0.5 + - (this.canvas.height * 0.5) / this.ds.scale; - this.setDirty(true, true); - }; + } + } else if (e.type == "keyup") { + if (e.keyCode == 32) { + // space + this.dragging_canvas = false; + } - /** - * adds some useful properties to a mouse event, like the position in graph coordinates - * @method adjustMouseEvent - **/ - LGraphCanvas.prototype.adjustMouseEvent = function(e) { - var clientX_rel = 0; - var clientY_rel = 0; - - if (this.canvas) { - var b = this.canvas.getBoundingClientRect(); - clientX_rel = e.clientX - b.left; - clientY_rel = e.clientY - b.top; - } else { - clientX_rel = e.clientX; - clientY_rel = e.clientY; + if (this.selected_nodes) { + for (var i in this.selected_nodes) { + if (this.selected_nodes[i].onKeyUp) { + this.selected_nodes[i].onKeyUp(e); + } } - - e.deltaX = clientX_rel - this.last_mouse_position[0]; - e.deltaY = clientY_rel- this.last_mouse_position[1]; + } + } - this.last_mouse_position[0] = clientX_rel; - this.last_mouse_position[1] = clientY_rel; + this.graph.change(); - e.canvasX = clientX_rel / this.ds.scale - this.ds.offset[0]; - e.canvasY = clientY_rel / this.ds.scale - this.ds.offset[1]; - - //console.log("pointerevents: adjustMouseEvent "+e.clientX+":"+e.clientY+" "+clientX_rel+":"+clientY_rel+" "+e.canvasX+":"+e.canvasY); + if (block_default) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + }; + + LGraphCanvas.prototype.copyToClipboard = function (nodes) { + var clipboard_info = { + nodes: [], + links: [], }; + var index = 0; + var selected_nodes_array = []; + if (!nodes) nodes = this.selected_nodes; + for (var i in nodes) { + var node = nodes[i]; + if (node.clonable === false) continue; + node._relative_id = index; + selected_nodes_array.push(node); + index += 1; + } - /** - * changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom - * @method setZoom - **/ - LGraphCanvas.prototype.setZoom = function(value, zooming_center) { - this.ds.changeScale(value, zooming_center); - /* + for (var i = 0; i < selected_nodes_array.length; ++i) { + var node = selected_nodes_array[i]; + var cloned = node.clone(); + if (!cloned) { + console.warn("node type not found: " + node.type); + continue; + } + clipboard_info.nodes.push(cloned.serialize()); + if (node.inputs && node.inputs.length) { + for (var j = 0; j < node.inputs.length; ++j) { + var input = node.inputs[j]; + if (!input || input.link == null) { + continue; + } + var link_info = this.graph.links[input.link]; + if (!link_info) { + continue; + } + var target_node = this.graph.getNodeById(link_info.origin_id); + if (!target_node) { + continue; + } + clipboard_info.links.push([ + target_node._relative_id, + link_info.origin_slot, //j, + node._relative_id, + link_info.target_slot, + target_node.id, + ]); + } + } + } + localStorage.setItem( + "litegrapheditor_clipboard", + JSON.stringify(clipboard_info) + ); + }; + + LGraphCanvas.prototype.pasteFromClipboard = function ( + isConnectUnselected = false + ) { + // if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior + if ( + !LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && + isConnectUnselected + ) { + return; + } + var data = localStorage.getItem("litegrapheditor_clipboard"); + if (!data) { + return; + } + + this.graph.beforeChange(); + + //create nodes + var clipboard_info = JSON.parse(data); + // calculate top-left node, could work without this processing but using diff with last node pos :: clipboard_info.nodes[clipboard_info.nodes.length-1].pos + var posMin = false; + var posMinIndexes = false; + for (var i = 0; i < clipboard_info.nodes.length; ++i) { + if (posMin) { + if (posMin[0] > clipboard_info.nodes[i].pos[0]) { + posMin[0] = clipboard_info.nodes[i].pos[0]; + posMinIndexes[0] = i; + } + if (posMin[1] > clipboard_info.nodes[i].pos[1]) { + posMin[1] = clipboard_info.nodes[i].pos[1]; + posMinIndexes[1] = i; + } + } else { + posMin = [ + clipboard_info.nodes[i].pos[0], + clipboard_info.nodes[i].pos[1], + ]; + posMinIndexes = [i, i]; + } + } + var nodes = []; + for (var i = 0; i < clipboard_info.nodes.length; ++i) { + var node_data = clipboard_info.nodes[i]; + var node = LiteGraph.createNode(node_data.type); + if (node) { + node.configure(node_data); + + //paste in last known mouse position + node.pos[0] += this.graph_mouse[0] - posMin[0]; //+= 5; + node.pos[1] += this.graph_mouse[1] - posMin[1]; //+= 5; + + this.graph.add(node, { doProcessChange: false }); + + nodes.push(node); + } + } + + //create links + for (var i = 0; i < clipboard_info.links.length; ++i) { + var link_info = clipboard_info.links[i]; + var origin_node = undefined; + var origin_node_relative_id = link_info[0]; + if (origin_node_relative_id != null) { + origin_node = nodes[origin_node_relative_id]; + } else if ( + LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && + isConnectUnselected + ) { + var origin_node_id = link_info[4]; + if (origin_node_id) { + origin_node = this.graph.getNodeById(origin_node_id); + } + } + var target_node = nodes[link_info[2]]; + if (origin_node && target_node) + origin_node.connect(link_info[1], target_node, link_info[3]); + else console.warn("Warning, nodes missing on pasting"); + } + + this.selectNodes(nodes); + + this.graph.afterChange(); + }; + + /** + * process a item drop event on top the canvas + * @method processDrop + **/ + LGraphCanvas.prototype.processDrop = function (e) { + e.preventDefault(); + this.adjustMouseEvent(e); + var x = e.clientX; + var y = e.clientY; + var is_inside = + !this.viewport || + (this.viewport && + x >= this.viewport[0] && + x < this.viewport[0] + this.viewport[2] && + y >= this.viewport[1] && + y < this.viewport[1] + this.viewport[3]); + if (!is_inside) { + return; + // --- BREAK --- + } + + var pos = [e.canvasX, e.canvasY]; + + var node = this.graph ? this.graph.getNodeOnPos(pos[0], pos[1]) : null; + + if (!node) { + var r = null; + if (this.onDropItem) { + r = this.onDropItem(event); + } + if (!r) { + this.checkDropItem(e); + } + return; + } + + if (node.onDropFile || node.onDropData) { + var files = e.dataTransfer.files; + if (files && files.length) { + for (var i = 0; i < files.length; i++) { + var file = e.dataTransfer.files[0]; + var filename = file.name; + var ext = LGraphCanvas.getFileExtension(filename); + //console.log(file); + + if (node.onDropFile) { + node.onDropFile(file); + } + + if (node.onDropData) { + //prepare reader + var reader = new FileReader(); + reader.onload = function (event) { + //console.log(event.target); + var data = event.target.result; + node.onDropData(data, filename, file); + }; + + //read data + var type = file.type.split("/")[0]; + if (type == "text" || type == "") { + reader.readAsText(file); + } else if (type == "image") { + reader.readAsDataURL(file); + } else { + reader.readAsArrayBuffer(file); + } + } + } + } + } + + if (node.onDropItem) { + if (node.onDropItem(event)) { + return true; + } + } + + if (this.onDropItem) { + return this.onDropItem(event); + } + + return false; + }; + + //called if the graph doesn't have a default drop item behaviour + LGraphCanvas.prototype.checkDropItem = function (e) { + if (e.dataTransfer.files.length) { + var file = e.dataTransfer.files[0]; + var ext = LGraphCanvas.getFileExtension(file.name).toLowerCase(); + var nodetype = LiteGraph.node_types_by_file_extension[ext]; + if (nodetype) { + this.graph.beforeChange(); + var node = LiteGraph.createNode(nodetype.type); + node.pos = [e.canvasX, e.canvasY]; + this.graph.add(node); + if (node.onDropFile) { + node.onDropFile(file); + } + this.graph.afterChange(); + } + } + }; + + LGraphCanvas.prototype.processNodeDblClicked = function (n) { + if (this.onShowNodePanel) { + this.onShowNodePanel(n); + } + + if (this.onNodeDblClicked) { + this.onNodeDblClicked(n); + } + + this.setDirty(true); + }; + + LGraphCanvas.prototype.processNodeSelected = function (node, e) { + this.selectNode(node, e && (e.shiftKey || e.ctrlKey || this.multi_select)); + if (this.onNodeSelected) { + this.onNodeSelected(node); + } + }; + + /** + * selects a given node (or adds it to the current selection) + * @method selectNode + **/ + LGraphCanvas.prototype.selectNode = function ( + node, + add_to_current_selection + ) { + if (node == null) { + this.deselectAllNodes(); + } else { + this.selectNodes([node], add_to_current_selection); + } + }; + + /** + * selects several nodes (or adds them to the current selection) + * @method selectNodes + **/ + LGraphCanvas.prototype.selectNodes = function ( + nodes, + add_to_current_selection + ) { + if (!add_to_current_selection) { + this.deselectAllNodes(); + } + + nodes = nodes || this.graph._nodes; + if (typeof nodes == "string") nodes = [nodes]; + for (var i in nodes) { + var node = nodes[i]; + if (node.is_selected) { + this.deselectNode(node); + continue; + } + + if (!node.is_selected && node.onSelected) { + node.onSelected(); + } + node.is_selected = true; + this.selected_nodes[node.id] = node; + + if (node.inputs) { + for (var j = 0; j < node.inputs.length; ++j) { + this.highlighted_links[node.inputs[j].link] = true; + } + } + if (node.outputs) { + for (var j = 0; j < node.outputs.length; ++j) { + var out = node.outputs[j]; + if (out.links) { + for (var k = 0; k < out.links.length; ++k) { + this.highlighted_links[out.links[k]] = true; + } + } + } + } + } + + if (this.onSelectionChange) this.onSelectionChange(this.selected_nodes); + + this.setDirty(true); + }; + + /** + * removes a node from the current selection + * @method deselectNode + **/ + LGraphCanvas.prototype.deselectNode = function (node) { + if (!node.is_selected) { + return; + } + if (node.onDeselected) { + node.onDeselected(); + } + node.is_selected = false; + + if (this.onNodeDeselected) { + this.onNodeDeselected(node); + } + + //remove highlighted + if (node.inputs) { + for (var i = 0; i < node.inputs.length; ++i) { + delete this.highlighted_links[node.inputs[i].link]; + } + } + if (node.outputs) { + for (var i = 0; i < node.outputs.length; ++i) { + var out = node.outputs[i]; + if (out.links) { + for (var j = 0; j < out.links.length; ++j) { + delete this.highlighted_links[out.links[j]]; + } + } + } + } + }; + + /** + * removes all nodes from the current selection + * @method deselectAllNodes + **/ + LGraphCanvas.prototype.deselectAllNodes = function () { + if (!this.graph) { + return; + } + var nodes = this.graph._nodes; + for (var i = 0, l = nodes.length; i < l; ++i) { + var node = nodes[i]; + if (!node.is_selected) { + continue; + } + if (node.onDeselected) { + node.onDeselected(); + } + node.is_selected = false; + if (this.onNodeDeselected) { + this.onNodeDeselected(node); + } + } + this.selected_nodes = {}; + this.current_node = null; + this.highlighted_links = {}; + if (this.onSelectionChange) this.onSelectionChange(this.selected_nodes); + this.setDirty(true); + }; + + /** + * deletes all nodes in the current selection from the graph + * @method deleteSelectedNodes + **/ + LGraphCanvas.prototype.deleteSelectedNodes = function () { + this.graph.beforeChange(); + + for (var i in this.selected_nodes) { + var node = this.selected_nodes[i]; + + if (node.block_delete) continue; + + //autoconnect when possible (very basic, only takes into account first input-output) + if ( + node.inputs && + node.inputs.length && + node.outputs && + node.outputs.length && + LiteGraph.isValidConnection( + node.inputs[0].type, + node.outputs[0].type + ) && + node.inputs[0].link && + node.outputs[0].links && + node.outputs[0].links.length + ) { + var input_link = node.graph.links[node.inputs[0].link]; + var output_link = node.graph.links[node.outputs[0].links[0]]; + var input_node = node.getInputNode(0); + var output_node = node.getOutputNodes(0)[0]; + if (input_node && output_node) + input_node.connect( + input_link.origin_slot, + output_node, + output_link.target_slot + ); + } + this.graph.remove(node); + if (this.onNodeDeselected) { + this.onNodeDeselected(node); + } + } + this.selected_nodes = {}; + this.current_node = null; + this.highlighted_links = {}; + this.setDirty(true); + this.graph.afterChange(); + }; + + /** + * centers the camera on a given node + * @method centerOnNode + **/ + LGraphCanvas.prototype.centerOnNode = function (node) { + this.ds.offset[0] = + -node.pos[0] - + node.size[0] * 0.5 + + (this.canvas.width * 0.5) / this.ds.scale; + this.ds.offset[1] = + -node.pos[1] - + node.size[1] * 0.5 + + (this.canvas.height * 0.5) / this.ds.scale; + this.setDirty(true, true); + }; + + /** + * adds some useful properties to a mouse event, like the position in graph coordinates + * @method adjustMouseEvent + **/ + LGraphCanvas.prototype.adjustMouseEvent = function (e) { + var clientX_rel = 0; + var clientY_rel = 0; + + if (this.canvas) { + var b = this.canvas.getBoundingClientRect(); + clientX_rel = e.clientX - b.left; + clientY_rel = e.clientY - b.top; + } else { + clientX_rel = e.clientX; + clientY_rel = e.clientY; + } + + e.deltaX = clientX_rel - this.last_mouse_position[0]; + e.deltaY = clientY_rel - this.last_mouse_position[1]; + + this.last_mouse_position[0] = clientX_rel; + this.last_mouse_position[1] = clientY_rel; + + e.canvasX = clientX_rel / this.ds.scale - this.ds.offset[0]; + e.canvasY = clientY_rel / this.ds.scale - this.ds.offset[1]; + + //console.log("pointerevents: adjustMouseEvent "+e.clientX+":"+e.clientY+" "+clientX_rel+":"+clientY_rel+" "+e.canvasX+":"+e.canvasY); + }; + + /** + * changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom + * @method setZoom + **/ + LGraphCanvas.prototype.setZoom = function (value, zooming_center) { + this.ds.changeScale(value, zooming_center); + /* if(!zooming_center && this.canvas) zooming_center = [this.canvas.width * 0.5,this.canvas.height * 0.5]; @@ -7622,2704 +7798,2704 @@ LGraphNode.prototype.executeAction = function(action) this.offset[1] += delta_offset[1]; */ - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - }; + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + }; - /** - * converts a coordinate from graph coordinates to canvas2D coordinates - * @method convertOffsetToCanvas - **/ - LGraphCanvas.prototype.convertOffsetToCanvas = function(pos, out) { - return this.ds.convertOffsetToCanvas(pos, out); - }; + /** + * converts a coordinate from graph coordinates to canvas2D coordinates + * @method convertOffsetToCanvas + **/ + LGraphCanvas.prototype.convertOffsetToCanvas = function (pos, out) { + return this.ds.convertOffsetToCanvas(pos, out); + }; - /** - * converts a coordinate from Canvas2D coordinates to graph space - * @method convertCanvasToOffset - **/ - LGraphCanvas.prototype.convertCanvasToOffset = function(pos, out) { - return this.ds.convertCanvasToOffset(pos, out); - }; + /** + * converts a coordinate from Canvas2D coordinates to graph space + * @method convertCanvasToOffset + **/ + LGraphCanvas.prototype.convertCanvasToOffset = function (pos, out) { + return this.ds.convertCanvasToOffset(pos, out); + }; - //converts event coordinates from canvas2D to graph coordinates - LGraphCanvas.prototype.convertEventToCanvasOffset = function(e) { - var rect = this.canvas.getBoundingClientRect(); - return this.convertCanvasToOffset([ - e.clientX - rect.left, - e.clientY - rect.top - ]); - }; + //converts event coordinates from canvas2D to graph coordinates + LGraphCanvas.prototype.convertEventToCanvasOffset = function (e) { + var rect = this.canvas.getBoundingClientRect(); + return this.convertCanvasToOffset([ + e.clientX - rect.left, + e.clientY - rect.top, + ]); + }; - /** - * brings a node to front (above all other nodes) - * @method bringToFront - **/ - LGraphCanvas.prototype.bringToFront = function(node) { - var i = this.graph._nodes.indexOf(node); - if (i == -1) { - return; - } - - this.graph._nodes.splice(i, 1); - this.graph._nodes.push(node); - }; - - /** - * sends a node to the back (below all other nodes) - * @method sendToBack - **/ - LGraphCanvas.prototype.sendToBack = function(node) { - var i = this.graph._nodes.indexOf(node); - if (i == -1) { - return; - } - - this.graph._nodes.splice(i, 1); - this.graph._nodes.unshift(node); - }; - - /* Interaction */ - - /* LGraphCanvas render */ - var temp = new Float32Array(4); - - /** - * checks which nodes are visible (inside the camera area) - * @method computeVisibleNodes - **/ - LGraphCanvas.prototype.computeVisibleNodes = function(nodes, out) { - var visible_nodes = out || []; - visible_nodes.length = 0; - nodes = nodes || this.graph._nodes; - for (var i = 0, l = nodes.length; i < l; ++i) { - var n = nodes[i]; - - //skip rendering nodes in live mode - if (this.live_mode && !n.onDrawBackground && !n.onDrawForeground) { - continue; - } - - if (!overlapBounding(this.visible_area, n.getBounding(temp, true))) { - continue; - } //out of the visible area - - visible_nodes.push(n); - } - return visible_nodes; - }; - - /** - * renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) - * @method draw - **/ - LGraphCanvas.prototype.draw = function(force_canvas, force_bgcanvas) { - if (!this.canvas || this.canvas.width == 0 || this.canvas.height == 0) { - return; - } - - //fps counting - var now = LiteGraph.getTime(); - this.render_time = (now - this.last_draw_time) * 0.001; - this.last_draw_time = now; - - if (this.graph) { - this.ds.computeVisibleArea(this.viewport); - } - - if ( - this.dirty_bgcanvas || - force_bgcanvas || - this.always_render_background || - (this.graph && - this.graph._last_trigger_time && - now - this.graph._last_trigger_time < 1000) - ) { - this.drawBackCanvas(); - } - - if (this.dirty_canvas || force_canvas) { - this.drawFrontCanvas(); - } - - this.fps = this.render_time ? 1.0 / this.render_time : 0; - this.frame += 1; - }; - - /** - * draws the front canvas (the one containing all the nodes) - * @method drawFrontCanvas - **/ - LGraphCanvas.prototype.drawFrontCanvas = function() { - this.dirty_canvas = false; - - if (!this.ctx) { - this.ctx = this.bgcanvas.getContext("2d"); - } - var ctx = this.ctx; - if (!ctx) { - //maybe is using webgl... - return; - } - - var canvas = this.canvas; - if ( ctx.start2D && !this.viewport ) { - ctx.start2D(); - ctx.restore(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - } - - //clip dirty area if there is one, otherwise work in full canvas - var area = this.viewport || this.dirty_area; - if (area) { - ctx.save(); - ctx.beginPath(); - ctx.rect( area[0],area[1],area[2],area[3] ); - ctx.clip(); - } - - //clear - //canvas.width = canvas.width; - if (this.clear_background) { - if(area) - ctx.clearRect( area[0],area[1],area[2],area[3] ); - else - ctx.clearRect(0, 0, canvas.width, canvas.height); - } - - //draw bg canvas - if (this.bgcanvas == this.canvas) { - this.drawBackCanvas(); - } else { - ctx.drawImage( this.bgcanvas, 0, 0 ); - } - - //rendering - if (this.onRender) { - this.onRender(canvas, ctx); - } - - //info widget - if (this.show_info) { - this.renderInfo(ctx, area ? area[0] : 0, area ? area[1] : 0 ); - } - - if (this.graph) { - //apply transformations - ctx.save(); - this.ds.toCanvasContext(ctx); - - //draw nodes - var drawn_nodes = 0; - var visible_nodes = this.computeVisibleNodes( - null, - this.visible_nodes - ); - - for (var i = 0; i < visible_nodes.length; ++i) { - var node = visible_nodes[i]; - - //transform coords system - ctx.save(); - ctx.translate(node.pos[0], node.pos[1]); - - //Draw - this.drawNode(node, ctx); - drawn_nodes += 1; - - //Restore - ctx.restore(); - } - - //on top (debug) - if (this.render_execution_order) { - this.drawExecutionOrder(ctx); - } - - //connections ontop? - if (this.graph.config.links_ontop) { - if (!this.live_mode) { - this.drawConnections(ctx); - } - } - - //current connection (the one being dragged by the mouse) - if (this.connecting_pos != null) { - ctx.lineWidth = this.connections_width; - var link_color = null; - - var connInOrOut = this.connecting_output || this.connecting_input; - - var connType = connInOrOut.type; - var connDir = connInOrOut.dir; - if(connDir == null) - { - if (this.connecting_output) - connDir = this.connecting_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT; - else - connDir = this.connecting_node.horizontal ? LiteGraph.UP : LiteGraph.LEFT; - } - var connShape = connInOrOut.shape; - - switch (connType) { - case LiteGraph.EVENT: - link_color = LiteGraph.EVENT_LINK_COLOR; - break; - default: - link_color = LiteGraph.CONNECTING_LINK_COLOR; - } - - //the connection being dragged by the mouse - this.renderLink( - ctx, - this.connecting_pos, - [this.graph_mouse[0], this.graph_mouse[1]], - null, - false, - null, - link_color, - connDir, - LiteGraph.CENTER - ); - - ctx.beginPath(); - if ( - connType === LiteGraph.EVENT || - connShape === LiteGraph.BOX_SHAPE - ) { - ctx.rect( - this.connecting_pos[0] - 6 + 0.5, - this.connecting_pos[1] - 5 + 0.5, - 14, - 10 - ); - ctx.fill(); - ctx.beginPath(); - ctx.rect( - this.graph_mouse[0] - 6 + 0.5, - this.graph_mouse[1] - 5 + 0.5, - 14, - 10 - ); - } else if (connShape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this.connecting_pos[0] + 8, this.connecting_pos[1] + 0.5); - ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] + 6 + 0.5); - ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] - 6 + 0.5); - ctx.closePath(); - } - else { - ctx.arc( - this.connecting_pos[0], - this.connecting_pos[1], - 4, - 0, - Math.PI * 2 - ); - ctx.fill(); - ctx.beginPath(); - ctx.arc( - this.graph_mouse[0], - this.graph_mouse[1], - 4, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - - ctx.fillStyle = "#ffcc00"; - if (this._highlight_input) { - ctx.beginPath(); - var shape = this._highlight_input_slot.shape; - if (shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this._highlight_input[0] + 8, this._highlight_input[1] + 0.5); - ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] + 6 + 0.5); - ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] - 6 + 0.5); - ctx.closePath(); - } else { - ctx.arc( - this._highlight_input[0], - this._highlight_input[1], - 6, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - } - if (this._highlight_output) { - ctx.beginPath(); - if (shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this._highlight_output[0] + 8, this._highlight_output[1] + 0.5); - ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] + 6 + 0.5); - ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] - 6 + 0.5); - ctx.closePath(); - } else { - ctx.arc( - this._highlight_output[0], - this._highlight_output[1], - 6, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - } - } - - //the selection rectangle - if (this.dragging_rectangle) { - ctx.strokeStyle = "#FFF"; - ctx.strokeRect( - this.dragging_rectangle[0], - this.dragging_rectangle[1], - this.dragging_rectangle[2], - this.dragging_rectangle[3] - ); - } - - //on top of link center - if(this.over_link_center && this.render_link_tooltip) - this.drawLinkTooltip( ctx, this.over_link_center ); - else - if(this.onDrawLinkTooltip) //to remove - this.onDrawLinkTooltip(ctx,null); - - //custom info - if (this.onDrawForeground) { - this.onDrawForeground(ctx, this.visible_rect); - } - - ctx.restore(); - } - - //draws panel in the corner - if (this._graph_stack && this._graph_stack.length) { - this.drawSubgraphPanel( ctx ); - } - - - if (this.onDrawOverlay) { - this.onDrawOverlay(ctx); - } - - if (area){ - ctx.restore(); - } - - if (ctx.finish2D) { - //this is a function I use in webgl renderer - ctx.finish2D(); - } - }; - - /** - * draws the panel in the corner that shows subgraph properties - * @method drawSubgraphPanel - **/ - LGraphCanvas.prototype.drawSubgraphPanel = function (ctx) { - var subgraph = this.graph; - var subnode = subgraph._subgraph_node; - if (!subnode) { - console.warn("subgraph without subnode"); - return; - } - this.drawSubgraphPanelLeft(subgraph, subnode, ctx) - this.drawSubgraphPanelRight(subgraph, subnode, ctx) + /** + * brings a node to front (above all other nodes) + * @method bringToFront + **/ + LGraphCanvas.prototype.bringToFront = function (node) { + var i = this.graph._nodes.indexOf(node); + if (i == -1) { + return; } - LGraphCanvas.prototype.drawSubgraphPanelLeft = function (subgraph, subnode, ctx) { - var num = subnode.inputs ? subnode.inputs.length : 0; - var w = 200; - var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6); + this.graph._nodes.splice(i, 1); + this.graph._nodes.push(node); + }; - ctx.fillStyle = "#111"; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - ctx.roundRect(10, 10, w, (num + 1) * h + 50, [8]); - ctx.fill(); - ctx.globalAlpha = 1; - - ctx.fillStyle = "#888"; - ctx.font = "14px Arial"; - ctx.textAlign = "left"; - ctx.fillText("Graph Inputs", 20, 34); - // var pos = this.mouse; - - if (this.drawButton(w - 20, 20, 20, 20, "X", "#151515")) { - this.closeSubgraph(); - return; - } - - var y = 50; - ctx.font = "14px Arial"; - if (subnode.inputs) - for (var i = 0; i < subnode.inputs.length; ++i) { - var input = subnode.inputs[i]; - if (input.not_subgraph_input) - continue; - - //input button clicked - if (this.drawButton(20, y + 2, w - 20, h - 2)) { - var type = subnode.constructor.input_node_type || "graph/input"; - this.graph.beforeChange(); - var newnode = LiteGraph.createNode(type); - if (newnode) { - subgraph.add(newnode); - this.block_click = false; - this.last_click_position = null; - this.selectNodes([newnode]); - this.node_dragged = newnode; - this.dragging_canvas = false; - newnode.setProperty("name", input.name); - newnode.setProperty("type", input.type); - this.node_dragged.pos[0] = this.graph_mouse[0] - 5; - this.node_dragged.pos[1] = this.graph_mouse[1] - 5; - this.graph.afterChange(); - } - else - console.error("graph input node not found:", type); - } - ctx.fillStyle = "#9C9"; - ctx.beginPath(); - ctx.arc(w - 16, y + h * 0.5, 5, 0, 2 * Math.PI); - ctx.fill(); - ctx.fillStyle = "#AAA"; - ctx.fillText(input.name, 30, y + h * 0.75); - // var tw = ctx.measureText(input.name); - ctx.fillStyle = "#777"; - ctx.fillText(input.type, 130, y + h * 0.75); - y += h; - } - //add + button - if (this.drawButton(20, y + 2, w - 20, h - 2, "+", "#151515", "#222")) { - this.showSubgraphPropertiesDialog(subnode); - } + /** + * sends a node to the back (below all other nodes) + * @method sendToBack + **/ + LGraphCanvas.prototype.sendToBack = function (node) { + var i = this.graph._nodes.indexOf(node); + if (i == -1) { + return; } - LGraphCanvas.prototype.drawSubgraphPanelRight = function (subgraph, subnode, ctx) { - var num = subnode.outputs ? subnode.outputs.length : 0; - var canvas_w = this.bgcanvas.width - var w = 200; - var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6); - ctx.fillStyle = "#111"; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - ctx.roundRect(canvas_w - w - 10, 10, w, (num + 1) * h + 50, [8]); - ctx.fill(); - ctx.globalAlpha = 1; + this.graph._nodes.splice(i, 1); + this.graph._nodes.unshift(node); + }; - ctx.fillStyle = "#888"; - ctx.font = "14px Arial"; - ctx.textAlign = "left"; - var title_text = "Graph Outputs" - var tw = ctx.measureText(title_text).width - ctx.fillText(title_text, (canvas_w - tw) - 20, 34); - // var pos = this.mouse; - if (this.drawButton(canvas_w - w, 20, 20, 20, "X", "#151515")) { - this.closeSubgraph(); - return; - } + /* Interaction */ - var y = 50; - ctx.font = "14px Arial"; - if (subnode.outputs) - for (var i = 0; i < subnode.outputs.length; ++i) { - var output = subnode.outputs[i]; - if (output.not_subgraph_input) - continue; + /* LGraphCanvas render */ + var temp = new Float32Array(4); - //output button clicked - if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2)) { - var type = subnode.constructor.output_node_type || "graph/output"; - this.graph.beforeChange(); - var newnode = LiteGraph.createNode(type); - if (newnode) { - subgraph.add(newnode); - this.block_click = false; - this.last_click_position = null; - this.selectNodes([newnode]); - this.node_dragged = newnode; - this.dragging_canvas = false; - newnode.setProperty("name", output.name); - newnode.setProperty("type", output.type); - this.node_dragged.pos[0] = this.graph_mouse[0] - 5; - this.node_dragged.pos[1] = this.graph_mouse[1] - 5; - this.graph.afterChange(); - } - else - console.error("graph input node not found:", type); - } - ctx.fillStyle = "#9C9"; - ctx.beginPath(); - ctx.arc(canvas_w - w + 16, y + h * 0.5, 5, 0, 2 * Math.PI); - ctx.fill(); - ctx.fillStyle = "#AAA"; - ctx.fillText(output.name, canvas_w - w + 30, y + h * 0.75); - // var tw = ctx.measureText(input.name); - ctx.fillStyle = "#777"; - ctx.fillText(output.type, canvas_w - w + 130, y + h * 0.75); - y += h; - } - //add + button - if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2, "+", "#151515", "#222")) { - this.showSubgraphPropertiesDialogRight(subnode); - } + /** + * checks which nodes are visible (inside the camera area) + * @method computeVisibleNodes + **/ + LGraphCanvas.prototype.computeVisibleNodes = function (nodes, out) { + var visible_nodes = out || []; + visible_nodes.length = 0; + nodes = nodes || this.graph._nodes; + for (var i = 0, l = nodes.length; i < l; ++i) { + var n = nodes[i]; + + //skip rendering nodes in live mode + if (this.live_mode && !n.onDrawBackground && !n.onDrawForeground) { + continue; + } + + if (!overlapBounding(this.visible_area, n.getBounding(temp, true))) { + continue; + } //out of the visible area + + visible_nodes.push(n); } - //Draws a button into the canvas overlay and computes if it was clicked using the immediate gui paradigm - LGraphCanvas.prototype.drawButton = function( x,y,w,h, text, bgcolor, hovercolor, textcolor ) - { - var ctx = this.ctx; - bgcolor = bgcolor || LiteGraph.NODE_DEFAULT_COLOR; - hovercolor = hovercolor || "#555"; - textcolor = textcolor || LiteGraph.NODE_TEXT_COLOR; - var pos = this.ds.convertOffsetToCanvas(this.graph_mouse); - var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - pos = this.last_click_position ? [this.last_click_position[0], this.last_click_position[1]] : null; - if(pos) { - var rect = this.canvas.getBoundingClientRect(); - pos[0] -= rect.left; - pos[1] -= rect.top; - } - var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); + return visible_nodes; + }; - ctx.fillStyle = hover ? hovercolor : bgcolor; - if(clicked) - ctx.fillStyle = "#AAA"; - ctx.beginPath(); - ctx.roundRect(x,y,w,h,[4] ); - ctx.fill(); + /** + * renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) + * @method draw + **/ + LGraphCanvas.prototype.draw = function (force_canvas, force_bgcanvas) { + if (!this.canvas || this.canvas.width == 0 || this.canvas.height == 0) { + return; + } - if(text != null) - { - if(text.constructor == String) - { - ctx.fillStyle = textcolor; - ctx.textAlign = "center"; - ctx.font = ((h * 0.65)|0) + "px Arial"; - ctx.fillText( text, x + w * 0.5,y + h * 0.75 ); - ctx.textAlign = "left"; - } - } + //fps counting + var now = LiteGraph.getTime(); + this.render_time = (now - this.last_draw_time) * 0.001; + this.last_draw_time = now; - var was_clicked = clicked && !this.block_click; - if(clicked) - this.blockClick(); - return was_clicked; - } + if (this.graph) { + this.ds.computeVisibleArea(this.viewport); + } - LGraphCanvas.prototype.isAreaClicked = function( x,y,w,h, hold_click ) - { - var pos = this.mouse; - var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - pos = this.last_click_position; - var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - var was_clicked = clicked && !this.block_click; - if(clicked && hold_click) - this.blockClick(); - return was_clicked; - } + if ( + this.dirty_bgcanvas || + force_bgcanvas || + this.always_render_background || + (this.graph && + this.graph._last_trigger_time && + now - this.graph._last_trigger_time < 1000) + ) { + this.drawBackCanvas(); + } - /** - * draws some useful stats in the corner of the canvas - * @method renderInfo - **/ - LGraphCanvas.prototype.renderInfo = function(ctx, x, y) { - x = x || 10; - y = y || this.canvas.offsetHeight - 80; + if (this.dirty_canvas || force_canvas) { + this.drawFrontCanvas(); + } + this.fps = this.render_time ? 1.0 / this.render_time : 0; + this.frame += 1; + }; + + /** + * draws the front canvas (the one containing all the nodes) + * @method drawFrontCanvas + **/ + LGraphCanvas.prototype.drawFrontCanvas = function () { + this.dirty_canvas = false; + + if (!this.ctx) { + this.ctx = this.bgcanvas.getContext("2d"); + } + var ctx = this.ctx; + if (!ctx) { + //maybe is using webgl... + return; + } + + var canvas = this.canvas; + if (ctx.start2D && !this.viewport) { + ctx.start2D(); + ctx.restore(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + } + + //clip dirty area if there is one, otherwise work in full canvas + var area = this.viewport || this.dirty_area; + if (area) { + ctx.save(); + ctx.beginPath(); + ctx.rect(area[0], area[1], area[2], area[3]); + ctx.clip(); + } + + //clear + //canvas.width = canvas.width; + if (this.clear_background) { + if (area) ctx.clearRect(area[0], area[1], area[2], area[3]); + else ctx.clearRect(0, 0, canvas.width, canvas.height); + } + + //draw bg canvas + if (this.bgcanvas == this.canvas) { + this.drawBackCanvas(); + } else { + ctx.drawImage(this.bgcanvas, 0, 0); + } + + //rendering + if (this.onRender) { + this.onRender(canvas, ctx); + } + + //info widget + if (this.show_info) { + this.renderInfo(ctx, area ? area[0] : 0, area ? area[1] : 0); + } + + if (this.graph) { + //apply transformations + ctx.save(); + this.ds.toCanvasContext(ctx); + + //draw nodes + var drawn_nodes = 0; + var visible_nodes = this.computeVisibleNodes(null, this.visible_nodes); + + for (var i = 0; i < visible_nodes.length; ++i) { + var node = visible_nodes[i]; + + //transform coords system ctx.save(); - ctx.translate(x, y); + ctx.translate(node.pos[0], node.pos[1]); - ctx.font = "10px Arial"; - ctx.fillStyle = "#888"; - ctx.textAlign = "left"; - if (this.graph) { - ctx.fillText( "T: " + this.graph.globaltime.toFixed(2) + "s", 5, 13 * 1 ); - ctx.fillText("I: " + this.graph.iteration, 5, 13 * 2 ); - ctx.fillText("N: " + this.graph._nodes.length + " [" + this.visible_nodes.length + "]", 5, 13 * 3 ); - ctx.fillText("V: " + this.graph._version, 5, 13 * 4); - ctx.fillText("FPS:" + this.fps.toFixed(2), 5, 13 * 5); - } else { - ctx.fillText("No graph selected", 5, 13 * 1); - } + //Draw + this.drawNode(node, ctx); + drawn_nodes += 1; + + //Restore ctx.restore(); - }; + } - /** - * draws the back canvas (the one containing the background and the connections) - * @method drawBackCanvas - **/ - LGraphCanvas.prototype.drawBackCanvas = function() { - var canvas = this.bgcanvas; - if ( - canvas.width != this.canvas.width || - canvas.height != this.canvas.height - ) { - canvas.width = this.canvas.width; - canvas.height = this.canvas.height; + //on top (debug) + if (this.render_execution_order) { + this.drawExecutionOrder(ctx); + } + + //connections ontop? + if (this.graph.config.links_ontop) { + if (!this.live_mode) { + this.drawConnections(ctx); + } + } + + //current connection (the one being dragged by the mouse) + if (this.connecting_pos != null) { + ctx.lineWidth = this.connections_width; + var link_color = null; + + var connInOrOut = this.connecting_output || this.connecting_input; + + var connType = connInOrOut.type; + var connDir = connInOrOut.dir; + if (connDir == null) { + if (this.connecting_output) + connDir = this.connecting_node.horizontal + ? LiteGraph.DOWN + : LiteGraph.RIGHT; + else + connDir = this.connecting_node.horizontal + ? LiteGraph.UP + : LiteGraph.LEFT; + } + var connShape = connInOrOut.shape; + + switch (connType) { + case LiteGraph.EVENT: + link_color = LiteGraph.EVENT_LINK_COLOR; + break; + default: + link_color = LiteGraph.CONNECTING_LINK_COLOR; } - if (!this.bgctx) { - this.bgctx = this.bgcanvas.getContext("2d"); - } - var ctx = this.bgctx; - if (ctx.start) { - ctx.start(); - } - - var viewport = this.viewport || [0,0,ctx.canvas.width,ctx.canvas.height]; - - //clear - if (this.clear_background) { - ctx.clearRect( viewport[0], viewport[1], viewport[2], viewport[3] ); - } - - //show subgraph stack header - if (this._graph_stack && this._graph_stack.length) { - ctx.save(); - var parent_graph = this._graph_stack[this._graph_stack.length - 1]; - var subgraph_node = this.graph._subgraph_node; - ctx.strokeStyle = subgraph_node.bgcolor; - ctx.lineWidth = 10; - ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2); - ctx.lineWidth = 1; - ctx.font = "40px Arial"; - ctx.textAlign = "center"; - ctx.fillStyle = subgraph_node.bgcolor || "#AAA"; - var title = ""; - for (var i = 1; i < this._graph_stack.length; ++i) { - title += - this._graph_stack[i]._subgraph_node.getTitle() + " >> "; - } - ctx.fillText( - title + subgraph_node.getTitle(), - canvas.width * 0.5, - 40 - ); - ctx.restore(); - } - - var bg_already_painted = false; - if (this.onRenderBackground) { - bg_already_painted = this.onRenderBackground(canvas, ctx); - } - - //reset in case of error - if ( !this.viewport ) - { - ctx.restore(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - } - this.visible_links.length = 0; - - if (this.graph) { - //apply transformations - ctx.save(); - this.ds.toCanvasContext(ctx); - - //render BG - if ( this.ds.scale < 1.5 && !bg_already_painted && this.clear_background_color ) - { - ctx.fillStyle = this.clear_background_color; - ctx.fillRect( - this.visible_area[0], - this.visible_area[1], - this.visible_area[2], - this.visible_area[3] - ); - } - - if ( - this.background_image && - this.ds.scale > 0.5 && - !bg_already_painted - ) { - if (this.zoom_modify_alpha) { - ctx.globalAlpha = - (1.0 - 0.5 / this.ds.scale) * this.editor_alpha; - } else { - ctx.globalAlpha = this.editor_alpha; - } - ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = false; // ctx.mozImageSmoothingEnabled = - if ( - !this._bg_img || - this._bg_img.name != this.background_image - ) { - this._bg_img = new Image(); - this._bg_img.name = this.background_image; - this._bg_img.src = this.background_image; - var that = this; - this._bg_img.onload = function() { - that.draw(true, true); - }; - } - - var pattern = null; - if (this._pattern == null && this._bg_img.width > 0) { - pattern = ctx.createPattern(this._bg_img, "repeat"); - this._pattern_img = this._bg_img; - this._pattern = pattern; - } else { - pattern = this._pattern; - } - if (pattern) { - ctx.fillStyle = pattern; - ctx.fillRect( - this.visible_area[0], - this.visible_area[1], - this.visible_area[2], - this.visible_area[3] - ); - ctx.fillStyle = "transparent"; - } - - ctx.globalAlpha = 1.0; - ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = true; //= ctx.mozImageSmoothingEnabled - } - - //groups - if (this.graph._groups.length && !this.live_mode) { - this.drawGroups(canvas, ctx); - } - - if (this.onDrawBackground) { - this.onDrawBackground(ctx, this.visible_area); - } - if (this.onBackgroundRender) { - //LEGACY - console.error( - "WARNING! onBackgroundRender deprecated, now is named onDrawBackground " - ); - this.onBackgroundRender = null; - } - - //DEBUG: show clipping area - //ctx.fillStyle = "red"; - //ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20); - - //bg - if (this.render_canvas_border) { - ctx.strokeStyle = "#235"; - ctx.strokeRect(0, 0, canvas.width, canvas.height); - } - - if (this.render_connections_shadows) { - ctx.shadowColor = "#000"; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - ctx.shadowBlur = 6; - } else { - ctx.shadowColor = "rgba(0,0,0,0)"; - } - - //draw connections - if (!this.live_mode) { - this.drawConnections(ctx); - } - - ctx.shadowColor = "rgba(0,0,0,0)"; - - //restore state - ctx.restore(); - } - - if (ctx.finish) { - ctx.finish(); - } - - this.dirty_bgcanvas = false; - this.dirty_canvas = true; //to force to repaint the front canvas with the bgcanvas - }; - - var temp_vec2 = new Float32Array(2); - - /** - * draws the given node inside the canvas - * @method drawNode - **/ - LGraphCanvas.prototype.drawNode = function(node, ctx) { - var glow = false; - this.current_node = node; - - var color = node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR; - var bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR; - - //shadow and glow - if (node.mouseOver) { - glow = true; - } - - var low_quality = this.ds.scale < 0.6; //zoomed out - - //only render if it forces it to do it - if (this.live_mode) { - if (!node.flags.collapsed) { - ctx.shadowColor = "transparent"; - if (node.onDrawForeground) { - node.onDrawForeground(ctx, this, this.canvas); - } - } - return; - } - - var editor_alpha = this.editor_alpha; - ctx.globalAlpha = editor_alpha; - - if (this.render_shadows && !low_quality) { - ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR; - ctx.shadowOffsetX = 2 * this.ds.scale; - ctx.shadowOffsetY = 2 * this.ds.scale; - ctx.shadowBlur = 3 * this.ds.scale; - } else { - ctx.shadowColor = "transparent"; - } - - //custom draw collapsed method (draw after shadows because they are affected) - if ( - node.flags.collapsed && - node.onDrawCollapsed && - node.onDrawCollapsed(ctx, this) == true - ) { - return; - } - - //clip if required (mask) - var shape = node._shape || LiteGraph.BOX_SHAPE; - var size = temp_vec2; - temp_vec2.set(node.size); - var horizontal = node.horizontal; // || node.flags.horizontal; - - if (node.flags.collapsed) { - ctx.font = this.inner_text_font; - var title = node.getTitle ? node.getTitle() : node.title; - if (title != null) { - node._collapsed_width = Math.min( - node.size[0], - ctx.measureText(title).width + - LiteGraph.NODE_TITLE_HEIGHT * 2 - ); //LiteGraph.NODE_COLLAPSED_WIDTH; - size[0] = node._collapsed_width; - size[1] = 0; - } - } - - if (node.clip_area) { - //Start clipping - ctx.save(); - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE) { - ctx.rect(0, 0, size[0], size[1]); - } else if (shape == LiteGraph.ROUND_SHAPE) { - ctx.roundRect(0, 0, size[0], size[1], [10]); - } else if (shape == LiteGraph.CIRCLE_SHAPE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5, - 0, - Math.PI * 2 - ); - } - ctx.clip(); - } - - //draw shape - if (node.has_errors) { - bgcolor = "red"; - } - this.drawNodeShape( - node, - ctx, - size, - color, - bgcolor, - node.is_selected, - node.mouseOver + //the connection being dragged by the mouse + this.renderLink( + ctx, + this.connecting_pos, + [this.graph_mouse[0], this.graph_mouse[1]], + null, + false, + null, + link_color, + connDir, + LiteGraph.CENTER ); - ctx.shadowColor = "transparent"; - //draw foreground - if (node.onDrawForeground) { - node.onDrawForeground(ctx, this, this.canvas); + ctx.beginPath(); + if (connType === LiteGraph.EVENT || connShape === LiteGraph.BOX_SHAPE) { + ctx.rect( + this.connecting_pos[0] - 6 + 0.5, + this.connecting_pos[1] - 5 + 0.5, + 14, + 10 + ); + ctx.fill(); + ctx.beginPath(); + ctx.rect( + this.graph_mouse[0] - 6 + 0.5, + this.graph_mouse[1] - 5 + 0.5, + 14, + 10 + ); + } else if (connShape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(this.connecting_pos[0] + 8, this.connecting_pos[1] + 0.5); + ctx.lineTo( + this.connecting_pos[0] - 4, + this.connecting_pos[1] + 6 + 0.5 + ); + ctx.lineTo( + this.connecting_pos[0] - 4, + this.connecting_pos[1] - 6 + 0.5 + ); + ctx.closePath(); + } else { + ctx.arc( + this.connecting_pos[0], + this.connecting_pos[1], + 4, + 0, + Math.PI * 2 + ); + ctx.fill(); + ctx.beginPath(); + ctx.arc(this.graph_mouse[0], this.graph_mouse[1], 4, 0, Math.PI * 2); + } + ctx.fill(); + + ctx.fillStyle = "#ffcc00"; + if (this._highlight_input) { + ctx.beginPath(); + var shape = this._highlight_input_slot.shape; + if (shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo( + this._highlight_input[0] + 8, + this._highlight_input[1] + 0.5 + ); + ctx.lineTo( + this._highlight_input[0] - 4, + this._highlight_input[1] + 6 + 0.5 + ); + ctx.lineTo( + this._highlight_input[0] - 4, + this._highlight_input[1] - 6 + 0.5 + ); + ctx.closePath(); + } else { + ctx.arc( + this._highlight_input[0], + this._highlight_input[1], + 6, + 0, + Math.PI * 2 + ); + } + ctx.fill(); + } + if (this._highlight_output) { + ctx.beginPath(); + if (shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo( + this._highlight_output[0] + 8, + this._highlight_output[1] + 0.5 + ); + ctx.lineTo( + this._highlight_output[0] - 4, + this._highlight_output[1] + 6 + 0.5 + ); + ctx.lineTo( + this._highlight_output[0] - 4, + this._highlight_output[1] - 6 + 0.5 + ); + ctx.closePath(); + } else { + ctx.arc( + this._highlight_output[0], + this._highlight_output[1], + 6, + 0, + Math.PI * 2 + ); + } + ctx.fill(); + } + } + + //the selection rectangle + if (this.dragging_rectangle) { + ctx.strokeStyle = "#FFF"; + ctx.strokeRect( + this.dragging_rectangle[0], + this.dragging_rectangle[1], + this.dragging_rectangle[2], + this.dragging_rectangle[3] + ); + } + + //on top of link center + if (this.over_link_center && this.render_link_tooltip) + this.drawLinkTooltip(ctx, this.over_link_center); + else if (this.onDrawLinkTooltip) + //to remove + this.onDrawLinkTooltip(ctx, null); + + //custom info + if (this.onDrawForeground) { + this.onDrawForeground(ctx, this.visible_rect); + } + + ctx.restore(); + } + + //draws panel in the corner + if (this._graph_stack && this._graph_stack.length) { + this.drawSubgraphPanel(ctx); + } + + if (this.onDrawOverlay) { + this.onDrawOverlay(ctx); + } + + if (area) { + ctx.restore(); + } + + if (ctx.finish2D) { + //this is a function I use in webgl renderer + ctx.finish2D(); + } + }; + + /** + * draws the panel in the corner that shows subgraph properties + * @method drawSubgraphPanel + **/ + LGraphCanvas.prototype.drawSubgraphPanel = function (ctx) { + var subgraph = this.graph; + var subnode = subgraph._subgraph_node; + if (!subnode) { + console.warn("subgraph without subnode"); + return; + } + this.drawSubgraphPanelLeft(subgraph, subnode, ctx); + this.drawSubgraphPanelRight(subgraph, subnode, ctx); + }; + + LGraphCanvas.prototype.drawSubgraphPanelLeft = function ( + subgraph, + subnode, + ctx + ) { + var num = subnode.inputs ? subnode.inputs.length : 0; + var w = 200; + var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6); + + ctx.fillStyle = "#111"; + ctx.globalAlpha = 0.8; + ctx.beginPath(); + ctx.roundRect(10, 10, w, (num + 1) * h + 50, [8]); + ctx.fill(); + ctx.globalAlpha = 1; + + ctx.fillStyle = "#888"; + ctx.font = "14px Arial"; + ctx.textAlign = "left"; + ctx.fillText("Graph Inputs", 20, 34); + // var pos = this.mouse; + + if (this.drawButton(w - 20, 20, 20, 20, "X", "#151515")) { + this.closeSubgraph(); + return; + } + + var y = 50; + ctx.font = "14px Arial"; + if (subnode.inputs) + for (var i = 0; i < subnode.inputs.length; ++i) { + var input = subnode.inputs[i]; + if (input.not_subgraph_input) continue; + + //input button clicked + if (this.drawButton(20, y + 2, w - 20, h - 2)) { + var type = subnode.constructor.input_node_type || "graph/input"; + this.graph.beforeChange(); + var newnode = LiteGraph.createNode(type); + if (newnode) { + subgraph.add(newnode); + this.block_click = false; + this.last_click_position = null; + this.selectNodes([newnode]); + this.node_dragged = newnode; + this.dragging_canvas = false; + newnode.setProperty("name", input.name); + newnode.setProperty("type", input.type); + this.node_dragged.pos[0] = this.graph_mouse[0] - 5; + this.node_dragged.pos[1] = this.graph_mouse[1] - 5; + this.graph.afterChange(); + } else console.error("graph input node not found:", type); + } + ctx.fillStyle = "#9C9"; + ctx.beginPath(); + ctx.arc(w - 16, y + h * 0.5, 5, 0, 2 * Math.PI); + ctx.fill(); + ctx.fillStyle = "#AAA"; + ctx.fillText(input.name, 30, y + h * 0.75); + // var tw = ctx.measureText(input.name); + ctx.fillStyle = "#777"; + ctx.fillText(input.type, 130, y + h * 0.75); + y += h; + } + //add + button + if (this.drawButton(20, y + 2, w - 20, h - 2, "+", "#151515", "#222")) { + this.showSubgraphPropertiesDialog(subnode); + } + }; + LGraphCanvas.prototype.drawSubgraphPanelRight = function ( + subgraph, + subnode, + ctx + ) { + var num = subnode.outputs ? subnode.outputs.length : 0; + var canvas_w = this.bgcanvas.width; + var w = 200; + var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6); + + ctx.fillStyle = "#111"; + ctx.globalAlpha = 0.8; + ctx.beginPath(); + ctx.roundRect(canvas_w - w - 10, 10, w, (num + 1) * h + 50, [8]); + ctx.fill(); + ctx.globalAlpha = 1; + + ctx.fillStyle = "#888"; + ctx.font = "14px Arial"; + ctx.textAlign = "left"; + var title_text = "Graph Outputs"; + var tw = ctx.measureText(title_text).width; + ctx.fillText(title_text, canvas_w - tw - 20, 34); + // var pos = this.mouse; + if (this.drawButton(canvas_w - w, 20, 20, 20, "X", "#151515")) { + this.closeSubgraph(); + return; + } + + var y = 50; + ctx.font = "14px Arial"; + if (subnode.outputs) + for (var i = 0; i < subnode.outputs.length; ++i) { + var output = subnode.outputs[i]; + if (output.not_subgraph_input) continue; + + //output button clicked + if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2)) { + var type = subnode.constructor.output_node_type || "graph/output"; + this.graph.beforeChange(); + var newnode = LiteGraph.createNode(type); + if (newnode) { + subgraph.add(newnode); + this.block_click = false; + this.last_click_position = null; + this.selectNodes([newnode]); + this.node_dragged = newnode; + this.dragging_canvas = false; + newnode.setProperty("name", output.name); + newnode.setProperty("type", output.type); + this.node_dragged.pos[0] = this.graph_mouse[0] - 5; + this.node_dragged.pos[1] = this.graph_mouse[1] - 5; + this.graph.afterChange(); + } else console.error("graph input node not found:", type); + } + ctx.fillStyle = "#9C9"; + ctx.beginPath(); + ctx.arc(canvas_w - w + 16, y + h * 0.5, 5, 0, 2 * Math.PI); + ctx.fill(); + ctx.fillStyle = "#AAA"; + ctx.fillText(output.name, canvas_w - w + 30, y + h * 0.75); + // var tw = ctx.measureText(input.name); + ctx.fillStyle = "#777"; + ctx.fillText(output.type, canvas_w - w + 130, y + h * 0.75); + y += h; + } + //add + button + if ( + this.drawButton( + canvas_w - w, + y + 2, + w - 20, + h - 2, + "+", + "#151515", + "#222" + ) + ) { + this.showSubgraphPropertiesDialogRight(subnode); + } + }; + //Draws a button into the canvas overlay and computes if it was clicked using the immediate gui paradigm + LGraphCanvas.prototype.drawButton = function ( + x, + y, + w, + h, + text, + bgcolor, + hovercolor, + textcolor + ) { + var ctx = this.ctx; + bgcolor = bgcolor || LiteGraph.NODE_DEFAULT_COLOR; + hovercolor = hovercolor || "#555"; + textcolor = textcolor || LiteGraph.NODE_TEXT_COLOR; + var pos = this.ds.convertOffsetToCanvas(this.graph_mouse); + var hover = LiteGraph.isInsideRectangle(pos[0], pos[1], x, y, w, h); + pos = this.last_click_position + ? [this.last_click_position[0], this.last_click_position[1]] + : null; + if (pos) { + var rect = this.canvas.getBoundingClientRect(); + pos[0] -= rect.left; + pos[1] -= rect.top; + } + var clicked = + pos && LiteGraph.isInsideRectangle(pos[0], pos[1], x, y, w, h); + + ctx.fillStyle = hover ? hovercolor : bgcolor; + if (clicked) ctx.fillStyle = "#AAA"; + ctx.beginPath(); + ctx.roundRect(x, y, w, h, [4]); + ctx.fill(); + + if (text != null) { + if (text.constructor == String) { + ctx.fillStyle = textcolor; + ctx.textAlign = "center"; + ctx.font = ((h * 0.65) | 0) + "px Arial"; + ctx.fillText(text, x + w * 0.5, y + h * 0.75); + ctx.textAlign = "left"; + } + } + + var was_clicked = clicked && !this.block_click; + if (clicked) this.blockClick(); + return was_clicked; + }; + + LGraphCanvas.prototype.isAreaClicked = function (x, y, w, h, hold_click) { + var pos = this.mouse; + var hover = LiteGraph.isInsideRectangle(pos[0], pos[1], x, y, w, h); + pos = this.last_click_position; + var clicked = + pos && LiteGraph.isInsideRectangle(pos[0], pos[1], x, y, w, h); + var was_clicked = clicked && !this.block_click; + if (clicked && hold_click) this.blockClick(); + return was_clicked; + }; + + /** + * draws some useful stats in the corner of the canvas + * @method renderInfo + **/ + LGraphCanvas.prototype.renderInfo = function (ctx, x, y) { + x = x || 10; + y = y || this.canvas.offsetHeight - 80; + + ctx.save(); + ctx.translate(x, y); + + ctx.font = "10px Arial"; + ctx.fillStyle = "#888"; + ctx.textAlign = "left"; + if (this.graph) { + ctx.fillText("T: " + this.graph.globaltime.toFixed(2) + "s", 5, 13 * 1); + ctx.fillText("I: " + this.graph.iteration, 5, 13 * 2); + ctx.fillText( + "N: " + + this.graph._nodes.length + + " [" + + this.visible_nodes.length + + "]", + 5, + 13 * 3 + ); + ctx.fillText("V: " + this.graph._version, 5, 13 * 4); + ctx.fillText("FPS:" + this.fps.toFixed(2), 5, 13 * 5); + } else { + ctx.fillText("No graph selected", 5, 13 * 1); + } + ctx.restore(); + }; + + /** + * draws the back canvas (the one containing the background and the connections) + * @method drawBackCanvas + **/ + LGraphCanvas.prototype.drawBackCanvas = function () { + var canvas = this.bgcanvas; + if ( + canvas.width != this.canvas.width || + canvas.height != this.canvas.height + ) { + canvas.width = this.canvas.width; + canvas.height = this.canvas.height; + } + + if (!this.bgctx) { + this.bgctx = this.bgcanvas.getContext("2d"); + } + var ctx = this.bgctx; + if (ctx.start) { + ctx.start(); + } + + var viewport = this.viewport || [0, 0, ctx.canvas.width, ctx.canvas.height]; + + //clear + if (this.clear_background) { + ctx.clearRect(viewport[0], viewport[1], viewport[2], viewport[3]); + } + + //show subgraph stack header + if (this._graph_stack && this._graph_stack.length) { + ctx.save(); + var parent_graph = this._graph_stack[this._graph_stack.length - 1]; + var subgraph_node = this.graph._subgraph_node; + ctx.strokeStyle = subgraph_node.bgcolor; + ctx.lineWidth = 10; + ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2); + ctx.lineWidth = 1; + ctx.font = "40px Arial"; + ctx.textAlign = "center"; + ctx.fillStyle = subgraph_node.bgcolor || "#AAA"; + var title = ""; + for (var i = 1; i < this._graph_stack.length; ++i) { + title += this._graph_stack[i]._subgraph_node.getTitle() + " >> "; + } + ctx.fillText(title + subgraph_node.getTitle(), canvas.width * 0.5, 40); + ctx.restore(); + } + + var bg_already_painted = false; + if (this.onRenderBackground) { + bg_already_painted = this.onRenderBackground(canvas, ctx); + } + + //reset in case of error + if (!this.viewport) { + ctx.restore(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + } + this.visible_links.length = 0; + + if (this.graph) { + //apply transformations + ctx.save(); + this.ds.toCanvasContext(ctx); + + //render BG + if ( + this.ds.scale < 1.5 && + !bg_already_painted && + this.clear_background_color + ) { + ctx.fillStyle = this.clear_background_color; + ctx.fillRect( + this.visible_area[0], + this.visible_area[1], + this.visible_area[2], + this.visible_area[3] + ); + } + + if (this.background_image && this.ds.scale > 0.5 && !bg_already_painted) { + if (this.zoom_modify_alpha) { + ctx.globalAlpha = (1.0 - 0.5 / this.ds.scale) * this.editor_alpha; + } else { + ctx.globalAlpha = this.editor_alpha; + } + ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = false; // ctx.mozImageSmoothingEnabled = + if (!this._bg_img || this._bg_img.name != this.background_image) { + this._bg_img = new Image(); + this._bg_img.name = this.background_image; + this._bg_img.src = this.background_image; + var that = this; + this._bg_img.onload = function () { + that.draw(true, true); + }; } - //connection slots - ctx.textAlign = horizontal ? "center" : "left"; - ctx.font = this.inner_text_font; - - var render_text = !low_quality; - - var out_slot = this.connecting_output; - var in_slot = this.connecting_input; - ctx.lineWidth = 1; - - var max_y = 0; - var slot_pos = new Float32Array(2); //to reuse - - //render inputs and outputs - if (!node.flags.collapsed) { - //input connection slots - if (node.inputs) { - for (var i = 0; i < node.inputs.length; i++) { - var slot = node.inputs[i]; - - var slot_type = slot.type; - var slot_shape = slot.shape; - - ctx.globalAlpha = editor_alpha; - //change opacity of incompatible slots when dragging a connection - if ( this.connecting_output && !LiteGraph.isValidConnection( slot.type , out_slot.type) ) { - ctx.globalAlpha = 0.4 * editor_alpha; - } - - ctx.fillStyle = - slot.link != null - ? slot.color_on || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.input_on - : slot.color_off || - this.default_connection_color_byTypeOff[slot_type] || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.input_off; - - var pos = node.getConnectionPos(true, i, slot_pos); - pos[0] -= node.pos[0]; - pos[1] -= node.pos[1]; - if (max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5) { - max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5; - } - - ctx.beginPath(); - - if (slot_type == "array"){ - slot_shape = LiteGraph.GRID_SHAPE; // place in addInput? addOutput instead? - } - - var doStroke = true; - - if ( - slot.type === LiteGraph.EVENT || - slot.shape === LiteGraph.BOX_SHAPE - ) { - if (horizontal) { - ctx.rect( - pos[0] - 5 + 0.5, - pos[1] - 8 + 0.5, - 10, - 14 - ); - } else { - ctx.rect( - pos[0] - 6 + 0.5, - pos[1] - 5 + 0.5, - 14, - 10 - ); - } - } else if (slot_shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(pos[0] + 8, pos[1] + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5); - ctx.closePath(); - } else if (slot_shape === LiteGraph.GRID_SHAPE) { - ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2); - ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2); - ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2); - doStroke = false; - } else { - if(low_quality) - ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8 ); //faster - else - ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2); - } - ctx.fill(); - - //render name - if (render_text) { - var text = slot.label != null ? slot.label : slot.name; - if (text) { - ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; - if (horizontal || slot.dir == LiteGraph.UP) { - ctx.fillText(text, pos[0], pos[1] - 10); - } else { - ctx.fillText(text, pos[0] + 10, pos[1] + 5); - } - } - } - } - } - - //output connection slots - - ctx.textAlign = horizontal ? "center" : "right"; - ctx.strokeStyle = "black"; - if (node.outputs) { - for (var i = 0; i < node.outputs.length; i++) { - var slot = node.outputs[i]; - - var slot_type = slot.type; - var slot_shape = slot.shape; - - //change opacity of incompatible slots when dragging a connection - if (this.connecting_input && !LiteGraph.isValidConnection( slot_type , in_slot.type) ) { - ctx.globalAlpha = 0.4 * editor_alpha; - } - - var pos = node.getConnectionPos(false, i, slot_pos); - pos[0] -= node.pos[0]; - pos[1] -= node.pos[1]; - if (max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5) { - max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5; - } - - ctx.fillStyle = - slot.links && slot.links.length - ? slot.color_on || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.output_on - : slot.color_off || - this.default_connection_color_byTypeOff[slot_type] || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.output_off; - ctx.beginPath(); - //ctx.rect( node.size[0] - 14,i*14,10,10); - - if (slot_type == "array"){ - slot_shape = LiteGraph.GRID_SHAPE; - } - - var doStroke = true; - - if ( - slot_type === LiteGraph.EVENT || - slot_shape === LiteGraph.BOX_SHAPE - ) { - if (horizontal) { - ctx.rect( - pos[0] - 5 + 0.5, - pos[1] - 8 + 0.5, - 10, - 14 - ); - } else { - ctx.rect( - pos[0] - 6 + 0.5, - pos[1] - 5 + 0.5, - 14, - 10 - ); - } - } else if (slot_shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(pos[0] + 8, pos[1] + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5); - ctx.closePath(); - } else if (slot_shape === LiteGraph.GRID_SHAPE) { - ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2); - ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2); - ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2); - doStroke = false; - } else { - if(low_quality) - ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8 ); - else - ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2); - } - - //trigger - //if(slot.node_id != null && slot.slot == -1) - // ctx.fillStyle = "#F85"; - - //if(slot.links != null && slot.links.length) - ctx.fill(); - if(!low_quality && doStroke) - ctx.stroke(); - - //render output name - if (render_text) { - var text = slot.label != null ? slot.label : slot.name; - if (text) { - ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; - if (horizontal || slot.dir == LiteGraph.DOWN) { - ctx.fillText(text, pos[0], pos[1] - 8); - } else { - ctx.fillText(text, pos[0] - 10, pos[1] + 5); - } - } - } - } - } - - ctx.textAlign = "left"; - ctx.globalAlpha = 1; - - if (node.widgets) { - var widgets_y = max_y; - if (horizontal || node.widgets_up) { - widgets_y = 2; - } - if( node.widgets_start_y != null ) - widgets_y = node.widgets_start_y; - this.drawNodeWidgets( - node, - widgets_y, - ctx, - this.node_widget && this.node_widget[0] == node - ? this.node_widget[1] - : null - ); - } - } else if (this.render_collapsed_slots) { - //if collapsed - var input_slot = null; - var output_slot = null; - - //get first connected slot to render - if (node.inputs) { - for (var i = 0; i < node.inputs.length; i++) { - var slot = node.inputs[i]; - if (slot.link == null) { - continue; - } - input_slot = slot; - break; - } - } - if (node.outputs) { - for (var i = 0; i < node.outputs.length; i++) { - var slot = node.outputs[i]; - if (!slot.links || !slot.links.length) { - continue; - } - output_slot = slot; - } - } - - if (input_slot) { - var x = 0; - var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center - if (horizontal) { - x = node._collapsed_width * 0.5; - y = -LiteGraph.NODE_TITLE_HEIGHT; - } - ctx.fillStyle = "#686"; - ctx.beginPath(); - if ( - slot.type === LiteGraph.EVENT || - slot.shape === LiteGraph.BOX_SHAPE - ) { - ctx.rect(x - 7 + 0.5, y - 4, 14, 8); - } else if (slot.shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(x + 8, y); - ctx.lineTo(x + -4, y - 4); - ctx.lineTo(x + -4, y + 4); - ctx.closePath(); - } else { - ctx.arc(x, y, 4, 0, Math.PI * 2); - } - ctx.fill(); - } - - if (output_slot) { - var x = node._collapsed_width; - var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center - if (horizontal) { - x = node._collapsed_width * 0.5; - y = 0; - } - ctx.fillStyle = "#686"; - ctx.strokeStyle = "black"; - ctx.beginPath(); - if ( - slot.type === LiteGraph.EVENT || - slot.shape === LiteGraph.BOX_SHAPE - ) { - ctx.rect(x - 7 + 0.5, y - 4, 14, 8); - } else if (slot.shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(x + 6, y); - ctx.lineTo(x - 6, y - 4); - ctx.lineTo(x - 6, y + 4); - ctx.closePath(); - } else { - ctx.arc(x, y, 4, 0, Math.PI * 2); - } - ctx.fill(); - //ctx.stroke(); - } + var pattern = null; + if (this._pattern == null && this._bg_img.width > 0) { + pattern = ctx.createPattern(this._bg_img, "repeat"); + this._pattern_img = this._bg_img; + this._pattern = pattern; + } else { + pattern = this._pattern; } - - if (node.clip_area) { - ctx.restore(); + if (pattern) { + ctx.fillStyle = pattern; + ctx.fillRect( + this.visible_area[0], + this.visible_area[1], + this.visible_area[2], + this.visible_area[3] + ); + ctx.fillStyle = "transparent"; } ctx.globalAlpha = 1.0; - }; + ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = true; //= ctx.mozImageSmoothingEnabled + } - //used by this.over_link_center - LGraphCanvas.prototype.drawLinkTooltip = function( ctx, link ) - { - var pos = link._pos; - ctx.fillStyle = "black"; - ctx.beginPath(); - ctx.arc( pos[0], pos[1], 3, 0, Math.PI * 2 ); - ctx.fill(); + //groups + if (this.graph._groups.length && !this.live_mode) { + this.drawGroups(canvas, ctx); + } - if(link.data == null) - return; + if (this.onDrawBackground) { + this.onDrawBackground(ctx, this.visible_area); + } + if (this.onBackgroundRender) { + //LEGACY + console.error( + "WARNING! onBackgroundRender deprecated, now is named onDrawBackground " + ); + this.onBackgroundRender = null; + } - if(this.onDrawLinkTooltip) - if( this.onDrawLinkTooltip(ctx,link,this) == true ) - return; + //DEBUG: show clipping area + //ctx.fillStyle = "red"; + //ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20); - var data = link.data; - var text = null; + //bg + if (this.render_canvas_border) { + ctx.strokeStyle = "#235"; + ctx.strokeRect(0, 0, canvas.width, canvas.height); + } - if( data.constructor === Number ) - text = data.toFixed(2); - else if( data.constructor === String ) - text = "\"" + data + "\""; - else if( data.constructor === Boolean ) - text = String(data); - else if (data.toToolTip) - text = data.toToolTip(); - else - text = "[" + data.constructor.name + "]"; + if (this.render_connections_shadows) { + ctx.shadowColor = "#000"; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 6; + } else { + ctx.shadowColor = "rgba(0,0,0,0)"; + } - if(text == null) - return; - text = text.substr(0,30); //avoid weird + //draw connections + if (!this.live_mode) { + this.drawConnections(ctx); + } - ctx.font = "14px Courier New"; - var info = ctx.measureText(text); - var w = info.width + 20; - var h = 24; - ctx.shadowColor = "black"; - ctx.shadowOffsetX = 2; - ctx.shadowOffsetY = 2; - ctx.shadowBlur = 3; - ctx.fillStyle = "#454"; - ctx.beginPath(); - ctx.roundRect( pos[0] - w*0.5, pos[1] - 15 - h, w, h, [3]); - ctx.moveTo( pos[0] - 10, pos[1] - 15 ); - ctx.lineTo( pos[0] + 10, pos[1] - 15 ); - ctx.lineTo( pos[0], pos[1] - 5 ); - ctx.fill(); + ctx.shadowColor = "rgba(0,0,0,0)"; + + //restore state + ctx.restore(); + } + + if (ctx.finish) { + ctx.finish(); + } + + this.dirty_bgcanvas = false; + this.dirty_canvas = true; //to force to repaint the front canvas with the bgcanvas + }; + + var temp_vec2 = new Float32Array(2); + + /** + * draws the given node inside the canvas + * @method drawNode + **/ + LGraphCanvas.prototype.drawNode = function (node, ctx) { + var glow = false; + this.current_node = node; + + var color = + node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR; + var bgcolor = + node.bgcolor || + node.constructor.bgcolor || + LiteGraph.NODE_DEFAULT_BGCOLOR; + + //shadow and glow + if (node.mouseOver) { + glow = true; + } + + var low_quality = this.ds.scale < 0.6; //zoomed out + + //only render if it forces it to do it + if (this.live_mode) { + if (!node.flags.collapsed) { ctx.shadowColor = "transparent"; - ctx.textAlign = "center"; - ctx.fillStyle = "#CEC"; - ctx.fillText(text, pos[0], pos[1] - 15 - h * 0.3); - } + if (node.onDrawForeground) { + node.onDrawForeground(ctx, this, this.canvas); + } + } + return; + } - /** - * draws the shape of the given node in the canvas - * @method drawNodeShape - **/ - var tmp_area = new Float32Array(4); + var editor_alpha = this.editor_alpha; + ctx.globalAlpha = editor_alpha; - LGraphCanvas.prototype.drawNodeShape = function( - node, - ctx, - size, - fgcolor, - bgcolor, - selected, - mouse_over + if (this.render_shadows && !low_quality) { + ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR; + ctx.shadowOffsetX = 2 * this.ds.scale; + ctx.shadowOffsetY = 2 * this.ds.scale; + ctx.shadowBlur = 3 * this.ds.scale; + } else { + ctx.shadowColor = "transparent"; + } + + //custom draw collapsed method (draw after shadows because they are affected) + if ( + node.flags.collapsed && + node.onDrawCollapsed && + node.onDrawCollapsed(ctx, this) == true ) { - //bg rect - ctx.strokeStyle = fgcolor; - ctx.fillStyle = bgcolor; + return; + } - var title_height = LiteGraph.NODE_TITLE_HEIGHT; - var low_quality = this.ds.scale < 0.5; + //clip if required (mask) + var shape = node._shape || LiteGraph.BOX_SHAPE; + var size = temp_vec2; + temp_vec2.set(node.size); + var horizontal = node.horizontal; // || node.flags.horizontal; - //render node area depending on shape - var shape = - node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE; + if (node.flags.collapsed) { + ctx.font = this.inner_text_font; + var title = node.getTitle ? node.getTitle() : node.title; + if (title != null) { + node._collapsed_width = Math.min( + node.size[0], + ctx.measureText(title).width + LiteGraph.NODE_TITLE_HEIGHT * 2 + ); //LiteGraph.NODE_COLLAPSED_WIDTH; + size[0] = node._collapsed_width; + size[1] = 0; + } + } - var title_mode = node.constructor.title_mode; + if (node.clip_area) { + //Start clipping + ctx.save(); + ctx.beginPath(); + if (shape == LiteGraph.BOX_SHAPE) { + ctx.rect(0, 0, size[0], size[1]); + } else if (shape == LiteGraph.ROUND_SHAPE) { + ctx.roundRect(0, 0, size[0], size[1], [10]); + } else if (shape == LiteGraph.CIRCLE_SHAPE) { + ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI * 2); + } + ctx.clip(); + } - var render_title = true; - if (title_mode == LiteGraph.TRANSPARENT_TITLE || title_mode == LiteGraph.NO_TITLE) { - render_title = false; - } else if (title_mode == LiteGraph.AUTOHIDE_TITLE && mouse_over) { - render_title = true; - } + //draw shape + if (node.has_errors) { + bgcolor = "red"; + } + this.drawNodeShape( + node, + ctx, + size, + color, + bgcolor, + node.is_selected, + node.mouseOver + ); + ctx.shadowColor = "transparent"; - var area = tmp_area; - area[0] = 0; //x - area[1] = render_title ? -title_height : 0; //y - area[2] = size[0] + 1; //w - area[3] = render_title ? size[1] + title_height : size[1]; //h + //draw foreground + if (node.onDrawForeground) { + node.onDrawForeground(ctx, this, this.canvas); + } - var old_alpha = ctx.globalAlpha; + //connection slots + ctx.textAlign = horizontal ? "center" : "left"; + ctx.font = this.inner_text_font; - //full node shape - //if(node.flags.collapsed) - { - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE || low_quality) { - ctx.fillRect(area[0], area[1], area[2], area[3]); - } else if ( - shape == LiteGraph.ROUND_SHAPE || - shape == LiteGraph.CARD_SHAPE - ) { - ctx.roundRect( - area[0], - area[1], - area[2], - area[3], - shape == LiteGraph.CARD_SHAPE ? [this.round_radius,this.round_radius,0,0] : [this.round_radius] - ); - } else if (shape == LiteGraph.CIRCLE_SHAPE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5, - 0, - Math.PI * 2 - ); - } - ctx.fill(); + var render_text = !low_quality; - //separator - if(!node.flags.collapsed && render_title) - { - ctx.shadowColor = "transparent"; - ctx.fillStyle = "rgba(0,0,0,0.2)"; - ctx.fillRect(0, -1, area[2], 2); - } - } - ctx.shadowColor = "transparent"; + var out_slot = this.connecting_output; + var in_slot = this.connecting_input; + ctx.lineWidth = 1; - if (node.onDrawBackground) { - node.onDrawBackground(ctx, this, this.canvas, this.graph_mouse ); - } + var max_y = 0; + var slot_pos = new Float32Array(2); //to reuse - //title bg (remember, it is rendered ABOVE the node) - if (render_title || title_mode == LiteGraph.TRANSPARENT_TITLE) { - //title bar - if (node.onDrawTitleBar) { - node.onDrawTitleBar( ctx, title_height, size, this.ds.scale, fgcolor ); - } else if ( - title_mode != LiteGraph.TRANSPARENT_TITLE && - (node.constructor.title_color || this.render_title_colored) - ) { - var title_color = node.constructor.title_color || fgcolor; + //render inputs and outputs + if (!node.flags.collapsed) { + //input connection slots + if (node.inputs) { + for (var i = 0; i < node.inputs.length; i++) { + var slot = node.inputs[i]; - if (node.flags.collapsed) { - ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR; - } + var slot_type = slot.type; + var slot_shape = slot.shape; - //* gradient test - if (this.use_gradients) { - var grad = LGraphCanvas.gradients[title_color]; - if (!grad) { - grad = LGraphCanvas.gradients[ title_color ] = ctx.createLinearGradient(0, 0, 400, 0); - grad.addColorStop(0, title_color); // TODO refactor: validate color !! prevent DOMException - grad.addColorStop(1, "#000"); - } - ctx.fillStyle = grad; - } else { - ctx.fillStyle = title_color; - } + ctx.globalAlpha = editor_alpha; + //change opacity of incompatible slots when dragging a connection + if ( + this.connecting_output && + !LiteGraph.isValidConnection(slot.type, out_slot.type) + ) { + ctx.globalAlpha = 0.4 * editor_alpha; + } - //ctx.globalAlpha = 0.5 * old_alpha; - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE || low_quality) { - ctx.rect(0, -title_height, size[0] + 1, title_height); - } else if ( shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE ) { - ctx.roundRect( - 0, - -title_height, - size[0] + 1, - title_height, - node.flags.collapsed ? [this.round_radius] : [this.round_radius,this.round_radius,0,0] - ); - } - ctx.fill(); - ctx.shadowColor = "transparent"; - } + ctx.fillStyle = + slot.link != null + ? slot.color_on || + this.default_connection_color_byType[slot_type] || + this.default_connection_color.input_on + : slot.color_off || + this.default_connection_color_byTypeOff[slot_type] || + this.default_connection_color_byType[slot_type] || + this.default_connection_color.input_off; - var colState = false; - if (LiteGraph.node_box_coloured_by_mode){ - if(LiteGraph.NODE_MODES_COLORS[node.mode]){ - colState = LiteGraph.NODE_MODES_COLORS[node.mode]; - } - } - if (LiteGraph.node_box_coloured_when_on){ - colState = node.action_triggered ? "#FFF" : (node.execute_triggered ? "#AAA" : colState); - } - - //title box - var box_size = 10; - if (node.onDrawTitleBox) { - node.onDrawTitleBox(ctx, title_height, size, this.ds.scale); - } else if ( - shape == LiteGraph.ROUND_SHAPE || - shape == LiteGraph.CIRCLE_SHAPE || - shape == LiteGraph.CARD_SHAPE - ) { - if (low_quality) { - ctx.fillStyle = "black"; - ctx.beginPath(); - ctx.arc( - title_height * 0.5, - title_height * -0.5, - box_size * 0.5 + 1, - 0, - Math.PI * 2 - ); - ctx.fill(); - } - - ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR; - if(low_quality) - ctx.fillRect( title_height * 0.5 - box_size *0.5, title_height * -0.5 - box_size *0.5, box_size , box_size ); - else - { - ctx.beginPath(); - ctx.arc( - title_height * 0.5, - title_height * -0.5, - box_size * 0.5, - 0, - Math.PI * 2 - ); - ctx.fill(); - } + var pos = node.getConnectionPos(true, i, slot_pos); + pos[0] -= node.pos[0]; + pos[1] -= node.pos[1]; + if (max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5) { + max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5; + } + + ctx.beginPath(); + + if (slot_type == "array") { + slot_shape = LiteGraph.GRID_SHAPE; // place in addInput? addOutput instead? + } + + var doStroke = true; + + if ( + slot.type === LiteGraph.EVENT || + slot.shape === LiteGraph.BOX_SHAPE + ) { + if (horizontal) { + ctx.rect(pos[0] - 5 + 0.5, pos[1] - 8 + 0.5, 10, 14); } else { - if (low_quality) { - ctx.fillStyle = "black"; - ctx.fillRect( - (title_height - box_size) * 0.5 - 1, - (title_height + box_size) * -0.5 - 1, - box_size + 2, - box_size + 2 - ); - } - ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR; - ctx.fillRect( - (title_height - box_size) * 0.5, - (title_height + box_size) * -0.5, - box_size, - box_size - ); + ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10); } - ctx.globalAlpha = old_alpha; + } else if (slot_shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(pos[0] + 8, pos[1] + 0.5); + ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5); + ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5); + ctx.closePath(); + } else if (slot_shape === LiteGraph.GRID_SHAPE) { + ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2); + ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2); + ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2); + ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2); + ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2); + ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2); + ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2); + ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2); + ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2); + doStroke = false; + } else { + if (low_quality) + ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8); //faster + else ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2); + } + ctx.fill(); - //title text - if (node.onDrawTitleText) { - node.onDrawTitleText( - ctx, - title_height, - size, - this.ds.scale, - this.title_text_font, - selected - ); + //render name + if (render_text) { + var text = slot.label != null ? slot.label : slot.name; + if (text) { + ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; + if (horizontal || slot.dir == LiteGraph.UP) { + ctx.fillText(text, pos[0], pos[1] - 10); + } else { + ctx.fillText(text, pos[0] + 10, pos[1] + 5); + } } - if (!low_quality) { - ctx.font = this.title_text_font; - var title = String(node.getTitle()); - if (title) { - if (selected) { - ctx.fillStyle = LiteGraph.NODE_SELECTED_TITLE_COLOR; - } else { - ctx.fillStyle = - node.constructor.title_text_color || - this.node_title_color; - } - if (node.flags.collapsed) { - ctx.textAlign = "left"; - var measure = ctx.measureText(title); - ctx.fillText( - title.substr(0,20), //avoid urls too long - title_height,// + measure.width * 0.5, - LiteGraph.NODE_TITLE_TEXT_Y - title_height - ); - ctx.textAlign = "left"; - } else { - ctx.textAlign = "left"; - ctx.fillText( - title, - title_height, - LiteGraph.NODE_TITLE_TEXT_Y - title_height - ); - } - } + } + } + } + + //output connection slots + + ctx.textAlign = horizontal ? "center" : "right"; + ctx.strokeStyle = "black"; + if (node.outputs) { + for (var i = 0; i < node.outputs.length; i++) { + var slot = node.outputs[i]; + + var slot_type = slot.type; + var slot_shape = slot.shape; + + //change opacity of incompatible slots when dragging a connection + if ( + this.connecting_input && + !LiteGraph.isValidConnection(slot_type, in_slot.type) + ) { + ctx.globalAlpha = 0.4 * editor_alpha; + } + + var pos = node.getConnectionPos(false, i, slot_pos); + pos[0] -= node.pos[0]; + pos[1] -= node.pos[1]; + if (max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5) { + max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5; + } + + ctx.fillStyle = + slot.links && slot.links.length + ? slot.color_on || + this.default_connection_color_byType[slot_type] || + this.default_connection_color.output_on + : slot.color_off || + this.default_connection_color_byTypeOff[slot_type] || + this.default_connection_color_byType[slot_type] || + this.default_connection_color.output_off; + ctx.beginPath(); + //ctx.rect( node.size[0] - 14,i*14,10,10); + + if (slot_type == "array") { + slot_shape = LiteGraph.GRID_SHAPE; + } + + var doStroke = true; + + if ( + slot_type === LiteGraph.EVENT || + slot_shape === LiteGraph.BOX_SHAPE + ) { + if (horizontal) { + ctx.rect(pos[0] - 5 + 0.5, pos[1] - 8 + 0.5, 10, 14); + } else { + ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10); } + } else if (slot_shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(pos[0] + 8, pos[1] + 0.5); + ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5); + ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5); + ctx.closePath(); + } else if (slot_shape === LiteGraph.GRID_SHAPE) { + ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2); + ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2); + ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2); + ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2); + ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2); + ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2); + ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2); + ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2); + ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2); + doStroke = false; + } else { + if (low_quality) ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8); + else ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2); + } - //subgraph box - if (!node.flags.collapsed && node.subgraph && !node.skip_subgraph_button) { - var w = LiteGraph.NODE_TITLE_HEIGHT; - var x = node.size[0] - w; - var over = LiteGraph.isInsideRectangle( this.graph_mouse[0] - node.pos[0], this.graph_mouse[1] - node.pos[1], x+2, -w+2, w-4, w-4 ); - ctx.fillStyle = over ? "#888" : "#555"; - if( shape == LiteGraph.BOX_SHAPE || low_quality) - ctx.fillRect(x+2, -w+2, w-4, w-4); - else - { - ctx.beginPath(); - ctx.roundRect(x+2, -w+2, w-4, w-4,[4]); - ctx.fill(); - } - ctx.fillStyle = "#333"; - ctx.beginPath(); - ctx.moveTo(x + w * 0.2, -w * 0.6); - ctx.lineTo(x + w * 0.8, -w * 0.6); - ctx.lineTo(x + w * 0.5, -w * 0.3); - ctx.fill(); - } + //trigger + //if(slot.node_id != null && slot.slot == -1) + // ctx.fillStyle = "#F85"; - //custom title render - if (node.onDrawTitle) { - node.onDrawTitle(ctx); + //if(slot.links != null && slot.links.length) + ctx.fill(); + if (!low_quality && doStroke) ctx.stroke(); + + //render output name + if (render_text) { + var text = slot.label != null ? slot.label : slot.name; + if (text) { + ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; + if (horizontal || slot.dir == LiteGraph.DOWN) { + ctx.fillText(text, pos[0], pos[1] - 8); + } else { + ctx.fillText(text, pos[0] - 10, pos[1] + 5); + } } + } } + } - //render selection marker - if (selected) { - if (node.onBounding) { - node.onBounding(area); - } + ctx.textAlign = "left"; + ctx.globalAlpha = 1; - if (title_mode == LiteGraph.TRANSPARENT_TITLE) { - area[1] -= title_height; - area[3] += title_height; - } - ctx.lineWidth = 1; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE) { - ctx.rect( - -6 + area[0], - -6 + area[1], - 12 + area[2], - 12 + area[3] - ); - } else if ( - shape == LiteGraph.ROUND_SHAPE || - (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) - ) { - ctx.roundRect( - -6 + area[0], - -6 + area[1], - 12 + area[2], - 12 + area[3], - [this.round_radius * 2] - ); - } else if (shape == LiteGraph.CARD_SHAPE) { - ctx.roundRect( - -6 + area[0], - -6 + area[1], - 12 + area[2], - 12 + area[3], - [this.round_radius * 2,2,this.round_radius * 2,2] - ); - } else if (shape == LiteGraph.CIRCLE_SHAPE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5 + 6, - 0, - Math.PI * 2 - ); - } - ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR; - ctx.stroke(); - ctx.strokeStyle = fgcolor; - ctx.globalAlpha = 1; + if (node.widgets) { + var widgets_y = max_y; + if (horizontal || node.widgets_up) { + widgets_y = 2; } - - // these counter helps in conditioning drawing based on if the node has been executed or an action occurred - if (node.execute_triggered>0) node.execute_triggered--; - if (node.action_triggered>0) node.action_triggered--; - }; + if (node.widgets_start_y != null) widgets_y = node.widgets_start_y; + this.drawNodeWidgets( + node, + widgets_y, + ctx, + this.node_widget && this.node_widget[0] == node + ? this.node_widget[1] + : null + ); + } + } else if (this.render_collapsed_slots) { + //if collapsed + var input_slot = null; + var output_slot = null; - var margin_area = new Float32Array(4); - var link_bounding = new Float32Array(4); - var tempA = new Float32Array(2); - var tempB = new Float32Array(2); - - /** - * draws every connection visible in the canvas - * OPTIMIZE THIS: pre-catch connections position instead of recomputing them every time - * @method drawConnections - **/ - LGraphCanvas.prototype.drawConnections = function(ctx) { - var now = LiteGraph.getTime(); - var visible_area = this.visible_area; - margin_area[0] = visible_area[0] - 20; - margin_area[1] = visible_area[1] - 20; - margin_area[2] = visible_area[2] + 40; - margin_area[3] = visible_area[3] + 40; - - //draw connections - ctx.lineWidth = this.connections_width; - - ctx.fillStyle = "#AAA"; - ctx.strokeStyle = "#AAA"; - ctx.globalAlpha = this.editor_alpha; - //for every node - var nodes = this.graph._nodes; - for (var n = 0, l = nodes.length; n < l; ++n) { - var node = nodes[n]; - //for every input (we render just inputs because it is easier as every slot can only have one input) - if (!node.inputs || !node.inputs.length) { - continue; - } - - for (var i = 0; i < node.inputs.length; ++i) { - var input = node.inputs[i]; - if (!input || input.link == null) { - continue; - } - var link_id = input.link; - var link = this.graph.links[link_id]; - if (!link) { - continue; - } - - //find link info - var start_node = this.graph.getNodeById(link.origin_id); - if (start_node == null) { - continue; - } - var start_node_slot = link.origin_slot; - var start_node_slotpos = null; - if (start_node_slot == -1) { - start_node_slotpos = [ - start_node.pos[0] + 10, - start_node.pos[1] + 10 - ]; - } else { - start_node_slotpos = start_node.getConnectionPos( - false, - start_node_slot, - tempA - ); - } - var end_node_slotpos = node.getConnectionPos(true, i, tempB); - - //compute link bounding - link_bounding[0] = start_node_slotpos[0]; - link_bounding[1] = start_node_slotpos[1]; - link_bounding[2] = end_node_slotpos[0] - start_node_slotpos[0]; - link_bounding[3] = end_node_slotpos[1] - start_node_slotpos[1]; - if (link_bounding[2] < 0) { - link_bounding[0] += link_bounding[2]; - link_bounding[2] = Math.abs(link_bounding[2]); - } - if (link_bounding[3] < 0) { - link_bounding[1] += link_bounding[3]; - link_bounding[3] = Math.abs(link_bounding[3]); - } - - //skip links outside of the visible area of the canvas - if (!overlapBounding(link_bounding, margin_area)) { - continue; - } - - var start_slot = start_node.outputs[start_node_slot]; - var end_slot = node.inputs[i]; - if (!start_slot || !end_slot) { - continue; - } - var start_dir = - start_slot.dir || - (start_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT); - var end_dir = - end_slot.dir || - (node.horizontal ? LiteGraph.UP : LiteGraph.LEFT); - - this.renderLink( - ctx, - start_node_slotpos, - end_node_slotpos, - link, - false, - 0, - null, - start_dir, - end_dir - ); - - //event triggered rendered on top - if (link && link._last_time && now - link._last_time < 1000) { - var f = 2.0 - (now - link._last_time) * 0.002; - var tmp = ctx.globalAlpha; - ctx.globalAlpha = tmp * f; - this.renderLink( - ctx, - start_node_slotpos, - end_node_slotpos, - link, - true, - f, - "white", - start_dir, - end_dir - ); - ctx.globalAlpha = tmp; - } - } + //get first connected slot to render + if (node.inputs) { + for (var i = 0; i < node.inputs.length; i++) { + var slot = node.inputs[i]; + if (slot.link == null) { + continue; + } + input_slot = slot; + break; } - ctx.globalAlpha = 1; - }; - - /** - * draws a link between two points - * @method renderLink - * @param {vec2} a start pos - * @param {vec2} b end pos - * @param {Object} link the link object with all the link info - * @param {boolean} skip_border ignore the shadow of the link - * @param {boolean} flow show flow animation (for events) - * @param {string} color the color for the link - * @param {number} start_dir the direction enum - * @param {number} end_dir the direction enum - * @param {number} num_sublines number of sublines (useful to represent vec3 or rgb) - **/ - LGraphCanvas.prototype.renderLink = function( - ctx, - a, - b, - link, - skip_border, - flow, - color, - start_dir, - end_dir, - num_sublines - ) { - if (link) { - this.visible_links.push(link); + } + if (node.outputs) { + for (var i = 0; i < node.outputs.length; i++) { + var slot = node.outputs[i]; + if (!slot.links || !slot.links.length) { + continue; + } + output_slot = slot; } + } - //choose color - if (!color && link) { - color = link.color || LGraphCanvas.link_type_colors[link.type]; + if (input_slot) { + var x = 0; + var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center + if (horizontal) { + x = node._collapsed_width * 0.5; + y = -LiteGraph.NODE_TITLE_HEIGHT; } - if (!color) { - color = this.default_link_color; - } - if (link != null && this.highlighted_links[link.id]) { - color = "#FFF"; - } - - start_dir = start_dir || LiteGraph.RIGHT; - end_dir = end_dir || LiteGraph.LEFT; - - var dist = distance(a, b); - - if (this.render_connections_border && this.ds.scale > 0.6) { - ctx.lineWidth = this.connections_width + 4; - } - ctx.lineJoin = "round"; - num_sublines = num_sublines || 1; - if (num_sublines > 1) { - ctx.lineWidth = 0.5; - } - - //begin line shape + ctx.fillStyle = "#686"; ctx.beginPath(); - for (var i = 0; i < num_sublines; i += 1) { - var offsety = (i - (num_sublines - 1) * 0.5) * 5; - - if (this.links_render_mode == LiteGraph.SPLINE_LINK) { - ctx.moveTo(a[0], a[1] + offsety); - var start_offset_x = 0; - var start_offset_y = 0; - var end_offset_x = 0; - var end_offset_y = 0; - switch (start_dir) { - case LiteGraph.LEFT: - start_offset_x = dist * -0.25; - break; - case LiteGraph.RIGHT: - start_offset_x = dist * 0.25; - break; - case LiteGraph.UP: - start_offset_y = dist * -0.25; - break; - case LiteGraph.DOWN: - start_offset_y = dist * 0.25; - break; - } - switch (end_dir) { - case LiteGraph.LEFT: - end_offset_x = dist * -0.25; - break; - case LiteGraph.RIGHT: - end_offset_x = dist * 0.25; - break; - case LiteGraph.UP: - end_offset_y = dist * -0.25; - break; - case LiteGraph.DOWN: - end_offset_y = dist * 0.25; - break; - } - ctx.bezierCurveTo( - a[0] + start_offset_x, - a[1] + start_offset_y + offsety, - b[0] + end_offset_x, - b[1] + end_offset_y + offsety, - b[0], - b[1] + offsety - ); - } else if (this.links_render_mode == LiteGraph.LINEAR_LINK) { - ctx.moveTo(a[0], a[1] + offsety); - var start_offset_x = 0; - var start_offset_y = 0; - var end_offset_x = 0; - var end_offset_y = 0; - switch (start_dir) { - case LiteGraph.LEFT: - start_offset_x = -1; - break; - case LiteGraph.RIGHT: - start_offset_x = 1; - break; - case LiteGraph.UP: - start_offset_y = -1; - break; - case LiteGraph.DOWN: - start_offset_y = 1; - break; - } - switch (end_dir) { - case LiteGraph.LEFT: - end_offset_x = -1; - break; - case LiteGraph.RIGHT: - end_offset_x = 1; - break; - case LiteGraph.UP: - end_offset_y = -1; - break; - case LiteGraph.DOWN: - end_offset_y = 1; - break; - } - var l = 15; - ctx.lineTo( - a[0] + start_offset_x * l, - a[1] + start_offset_y * l + offsety - ); - ctx.lineTo( - b[0] + end_offset_x * l, - b[1] + end_offset_y * l + offsety - ); - ctx.lineTo(b[0], b[1] + offsety); - } else if (this.links_render_mode == LiteGraph.STRAIGHT_LINK) { - ctx.moveTo(a[0], a[1]); - var start_x = a[0]; - var start_y = a[1]; - var end_x = b[0]; - var end_y = b[1]; - if (start_dir == LiteGraph.RIGHT) { - start_x += 10; - } else { - start_y += 10; - } - if (end_dir == LiteGraph.LEFT) { - end_x -= 10; - } else { - end_y -= 10; - } - ctx.lineTo(start_x, start_y); - ctx.lineTo((start_x + end_x) * 0.5, start_y); - ctx.lineTo((start_x + end_x) * 0.5, end_y); - ctx.lineTo(end_x, end_y); - ctx.lineTo(b[0], b[1]); - } else { - return; - } //unknown - } - - //rendering the outline of the connection can be a little bit slow if ( - this.render_connections_border && - this.ds.scale > 0.6 && - !skip_border + slot.type === LiteGraph.EVENT || + slot.shape === LiteGraph.BOX_SHAPE ) { - ctx.strokeStyle = "rgba(0,0,0,0.5)"; - ctx.stroke(); + ctx.rect(x - 7 + 0.5, y - 4, 14, 8); + } else if (slot.shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(x + 8, y); + ctx.lineTo(x + -4, y - 4); + ctx.lineTo(x + -4, y + 4); + ctx.closePath(); + } else { + ctx.arc(x, y, 4, 0, Math.PI * 2); } + ctx.fill(); + } - ctx.lineWidth = this.connections_width; - ctx.fillStyle = ctx.strokeStyle = color; - ctx.stroke(); - //end line shape - - var pos = this.computeConnectionPoint(a, b, 0.5, start_dir, end_dir); - if (link && link._pos) { - link._pos[0] = pos[0]; - link._pos[1] = pos[1]; + if (output_slot) { + var x = node._collapsed_width; + var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center + if (horizontal) { + x = node._collapsed_width * 0.5; + y = 0; } - - //render arrow in the middle + ctx.fillStyle = "#686"; + ctx.strokeStyle = "black"; + ctx.beginPath(); if ( - this.ds.scale >= 0.6 && - this.highquality_render && - end_dir != LiteGraph.CENTER + slot.type === LiteGraph.EVENT || + slot.shape === LiteGraph.BOX_SHAPE ) { - //render arrow - if (this.render_connection_arrows) { - //compute two points in the connection - var posA = this.computeConnectionPoint( - a, - b, - 0.25, - start_dir, - end_dir - ); - var posB = this.computeConnectionPoint( - a, - b, - 0.26, - start_dir, - end_dir - ); - var posC = this.computeConnectionPoint( - a, - b, - 0.75, - start_dir, - end_dir - ); - var posD = this.computeConnectionPoint( - a, - b, - 0.76, - start_dir, - end_dir - ); - - //compute the angle between them so the arrow points in the right direction - var angleA = 0; - var angleB = 0; - if (this.render_curved_connections) { - angleA = -Math.atan2(posB[0] - posA[0], posB[1] - posA[1]); - angleB = -Math.atan2(posD[0] - posC[0], posD[1] - posC[1]); - } else { - angleB = angleA = b[1] > a[1] ? 0 : Math.PI; - } - - //render arrow - ctx.save(); - ctx.translate(posA[0], posA[1]); - ctx.rotate(angleA); - ctx.beginPath(); - ctx.moveTo(-5, -3); - ctx.lineTo(0, +7); - ctx.lineTo(+5, -3); - ctx.fill(); - ctx.restore(); - ctx.save(); - ctx.translate(posC[0], posC[1]); - ctx.rotate(angleB); - ctx.beginPath(); - ctx.moveTo(-5, -3); - ctx.lineTo(0, +7); - ctx.lineTo(+5, -3); - ctx.fill(); - ctx.restore(); - } - - //circle - ctx.beginPath(); - ctx.arc(pos[0], pos[1], 5, 0, Math.PI * 2); - ctx.fill(); + ctx.rect(x - 7 + 0.5, y - 4, 14, 8); + } else if (slot.shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(x + 6, y); + ctx.lineTo(x - 6, y - 4); + ctx.lineTo(x - 6, y + 4); + ctx.closePath(); + } else { + ctx.arc(x, y, 4, 0, Math.PI * 2); } + ctx.fill(); + //ctx.stroke(); + } + } - //render flowing points - if (flow) { - ctx.fillStyle = color; - for (var i = 0; i < 5; ++i) { - var f = (LiteGraph.getTime() * 0.001 + i * 0.2) % 1; - var pos = this.computeConnectionPoint( - a, - b, - f, - start_dir, - end_dir - ); - ctx.beginPath(); - ctx.arc(pos[0], pos[1], 5, 0, 2 * Math.PI); - ctx.fill(); - } - } - }; + if (node.clip_area) { + ctx.restore(); + } - //returns the link center point based on curvature - LGraphCanvas.prototype.computeConnectionPoint = function( - a, - b, - t, - start_dir, - end_dir + ctx.globalAlpha = 1.0; + }; + + //used by this.over_link_center + LGraphCanvas.prototype.drawLinkTooltip = function (ctx, link) { + var pos = link._pos; + ctx.fillStyle = "black"; + ctx.beginPath(); + ctx.arc(pos[0], pos[1], 3, 0, Math.PI * 2); + ctx.fill(); + + if (link.data == null) return; + + if (this.onDrawLinkTooltip) + if (this.onDrawLinkTooltip(ctx, link, this) == true) return; + + var data = link.data; + var text = null; + + if (data.constructor === Number) text = data.toFixed(2); + else if (data.constructor === String) text = '"' + data + '"'; + else if (data.constructor === Boolean) text = String(data); + else if (data.toToolTip) text = data.toToolTip(); + else text = "[" + data.constructor.name + "]"; + + if (text == null) return; + text = text.substr(0, 30); //avoid weird + + ctx.font = "14px Courier New"; + var info = ctx.measureText(text); + var w = info.width + 20; + var h = 24; + ctx.shadowColor = "black"; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + ctx.shadowBlur = 3; + ctx.fillStyle = "#454"; + ctx.beginPath(); + ctx.roundRect(pos[0] - w * 0.5, pos[1] - 15 - h, w, h, [3]); + ctx.moveTo(pos[0] - 10, pos[1] - 15); + ctx.lineTo(pos[0] + 10, pos[1] - 15); + ctx.lineTo(pos[0], pos[1] - 5); + ctx.fill(); + ctx.shadowColor = "transparent"; + ctx.textAlign = "center"; + ctx.fillStyle = "#CEC"; + ctx.fillText(text, pos[0], pos[1] - 15 - h * 0.3); + }; + + /** + * draws the shape of the given node in the canvas + * @method drawNodeShape + **/ + var tmp_area = new Float32Array(4); + + LGraphCanvas.prototype.drawNodeShape = function ( + node, + ctx, + size, + fgcolor, + bgcolor, + selected, + mouse_over + ) { + //bg rect + ctx.strokeStyle = fgcolor; + ctx.fillStyle = bgcolor; + + var title_height = LiteGraph.NODE_TITLE_HEIGHT; + var low_quality = this.ds.scale < 0.5; + + //render node area depending on shape + var shape = node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE; + + var title_mode = node.constructor.title_mode; + + var render_title = true; + if ( + title_mode == LiteGraph.TRANSPARENT_TITLE || + title_mode == LiteGraph.NO_TITLE ) { - start_dir = start_dir || LiteGraph.RIGHT; - end_dir = end_dir || LiteGraph.LEFT; + render_title = false; + } else if (title_mode == LiteGraph.AUTOHIDE_TITLE && mouse_over) { + render_title = true; + } - var dist = distance(a, b); - var p0 = a; - var p1 = [a[0], a[1]]; - var p2 = [b[0], b[1]]; - var p3 = b; + var area = tmp_area; + area[0] = 0; //x + area[1] = render_title ? -title_height : 0; //y + area[2] = size[0] + 1; //w + area[3] = render_title ? size[1] + title_height : size[1]; //h + var old_alpha = ctx.globalAlpha; + + //full node shape + //if(node.flags.collapsed) + { + ctx.beginPath(); + if (shape == LiteGraph.BOX_SHAPE || low_quality) { + ctx.fillRect(area[0], area[1], area[2], area[3]); + } else if ( + shape == LiteGraph.ROUND_SHAPE || + shape == LiteGraph.CARD_SHAPE + ) { + ctx.roundRect( + area[0], + area[1], + area[2], + area[3], + shape == LiteGraph.CARD_SHAPE + ? [this.round_radius, this.round_radius, 0, 0] + : [this.round_radius] + ); + } else if (shape == LiteGraph.CIRCLE_SHAPE) { + ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI * 2); + } + ctx.fill(); + + //separator + if (!node.flags.collapsed && render_title) { + ctx.shadowColor = "transparent"; + ctx.fillStyle = "rgba(0,0,0,0.2)"; + ctx.fillRect(0, -1, area[2], 2); + } + } + ctx.shadowColor = "transparent"; + + if (node.onDrawBackground) { + node.onDrawBackground(ctx, this, this.canvas, this.graph_mouse); + } + + //title bg (remember, it is rendered ABOVE the node) + if (render_title || title_mode == LiteGraph.TRANSPARENT_TITLE) { + //title bar + if (node.onDrawTitleBar) { + node.onDrawTitleBar(ctx, title_height, size, this.ds.scale, fgcolor); + } else if ( + title_mode != LiteGraph.TRANSPARENT_TITLE && + (node.constructor.title_color || this.render_title_colored) + ) { + var title_color = node.constructor.title_color || fgcolor; + + if (node.flags.collapsed) { + ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR; + } + + //* gradient test + if (this.use_gradients) { + var grad = LGraphCanvas.gradients[title_color]; + if (!grad) { + grad = LGraphCanvas.gradients[title_color] = + ctx.createLinearGradient(0, 0, 400, 0); + grad.addColorStop(0, title_color); // TODO refactor: validate color !! prevent DOMException + grad.addColorStop(1, "#000"); + } + ctx.fillStyle = grad; + } else { + ctx.fillStyle = title_color; + } + + //ctx.globalAlpha = 0.5 * old_alpha; + ctx.beginPath(); + if (shape == LiteGraph.BOX_SHAPE || low_quality) { + ctx.rect(0, -title_height, size[0] + 1, title_height); + } else if ( + shape == LiteGraph.ROUND_SHAPE || + shape == LiteGraph.CARD_SHAPE + ) { + ctx.roundRect( + 0, + -title_height, + size[0] + 1, + title_height, + node.flags.collapsed + ? [this.round_radius] + : [this.round_radius, this.round_radius, 0, 0] + ); + } + ctx.fill(); + ctx.shadowColor = "transparent"; + } + + var colState = false; + if (LiteGraph.node_box_coloured_by_mode) { + if (LiteGraph.NODE_MODES_COLORS[node.mode]) { + colState = LiteGraph.NODE_MODES_COLORS[node.mode]; + } + } + if (LiteGraph.node_box_coloured_when_on) { + colState = node.action_triggered + ? "#FFF" + : node.execute_triggered + ? "#AAA" + : colState; + } + + //title box + var box_size = 10; + if (node.onDrawTitleBox) { + node.onDrawTitleBox(ctx, title_height, size, this.ds.scale); + } else if ( + shape == LiteGraph.ROUND_SHAPE || + shape == LiteGraph.CIRCLE_SHAPE || + shape == LiteGraph.CARD_SHAPE + ) { + if (low_quality) { + ctx.fillStyle = "black"; + ctx.beginPath(); + ctx.arc( + title_height * 0.5, + title_height * -0.5, + box_size * 0.5 + 1, + 0, + Math.PI * 2 + ); + ctx.fill(); + } + + ctx.fillStyle = + node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR; + if (low_quality) + ctx.fillRect( + title_height * 0.5 - box_size * 0.5, + title_height * -0.5 - box_size * 0.5, + box_size, + box_size + ); + else { + ctx.beginPath(); + ctx.arc( + title_height * 0.5, + title_height * -0.5, + box_size * 0.5, + 0, + Math.PI * 2 + ); + ctx.fill(); + } + } else { + if (low_quality) { + ctx.fillStyle = "black"; + ctx.fillRect( + (title_height - box_size) * 0.5 - 1, + (title_height + box_size) * -0.5 - 1, + box_size + 2, + box_size + 2 + ); + } + ctx.fillStyle = + node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR; + ctx.fillRect( + (title_height - box_size) * 0.5, + (title_height + box_size) * -0.5, + box_size, + box_size + ); + } + ctx.globalAlpha = old_alpha; + + //title text + if (node.onDrawTitleText) { + node.onDrawTitleText( + ctx, + title_height, + size, + this.ds.scale, + this.title_text_font, + selected + ); + } + if (!low_quality) { + ctx.font = this.title_text_font; + var title = String(node.getTitle()); + if (title) { + if (selected) { + ctx.fillStyle = LiteGraph.NODE_SELECTED_TITLE_COLOR; + } else { + ctx.fillStyle = + node.constructor.title_text_color || this.node_title_color; + } + if (node.flags.collapsed) { + ctx.textAlign = "left"; + var measure = ctx.measureText(title); + ctx.fillText( + title.substr(0, 20), //avoid urls too long + title_height, // + measure.width * 0.5, + LiteGraph.NODE_TITLE_TEXT_Y - title_height + ); + ctx.textAlign = "left"; + } else { + ctx.textAlign = "left"; + ctx.fillText( + title, + title_height, + LiteGraph.NODE_TITLE_TEXT_Y - title_height + ); + } + } + } + + //subgraph box + if ( + !node.flags.collapsed && + node.subgraph && + !node.skip_subgraph_button + ) { + var w = LiteGraph.NODE_TITLE_HEIGHT; + var x = node.size[0] - w; + var over = LiteGraph.isInsideRectangle( + this.graph_mouse[0] - node.pos[0], + this.graph_mouse[1] - node.pos[1], + x + 2, + -w + 2, + w - 4, + w - 4 + ); + ctx.fillStyle = over ? "#888" : "#555"; + if (shape == LiteGraph.BOX_SHAPE || low_quality) + ctx.fillRect(x + 2, -w + 2, w - 4, w - 4); + else { + ctx.beginPath(); + ctx.roundRect(x + 2, -w + 2, w - 4, w - 4, [4]); + ctx.fill(); + } + ctx.fillStyle = "#333"; + ctx.beginPath(); + ctx.moveTo(x + w * 0.2, -w * 0.6); + ctx.lineTo(x + w * 0.8, -w * 0.6); + ctx.lineTo(x + w * 0.5, -w * 0.3); + ctx.fill(); + } + + //custom title render + if (node.onDrawTitle) { + node.onDrawTitle(ctx); + } + } + + //render selection marker + if (selected) { + if (node.onBounding) { + node.onBounding(area); + } + + if (title_mode == LiteGraph.TRANSPARENT_TITLE) { + area[1] -= title_height; + area[3] += title_height; + } + ctx.lineWidth = 1; + ctx.globalAlpha = 0.8; + ctx.beginPath(); + if (shape == LiteGraph.BOX_SHAPE) { + ctx.rect(-6 + area[0], -6 + area[1], 12 + area[2], 12 + area[3]); + } else if ( + shape == LiteGraph.ROUND_SHAPE || + (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) + ) { + ctx.roundRect(-6 + area[0], -6 + area[1], 12 + area[2], 12 + area[3], [ + this.round_radius * 2, + ]); + } else if (shape == LiteGraph.CARD_SHAPE) { + ctx.roundRect(-6 + area[0], -6 + area[1], 12 + area[2], 12 + area[3], [ + this.round_radius * 2, + 2, + this.round_radius * 2, + 2, + ]); + } else if (shape == LiteGraph.CIRCLE_SHAPE) { + ctx.arc( + size[0] * 0.5, + size[1] * 0.5, + size[0] * 0.5 + 6, + 0, + Math.PI * 2 + ); + } + ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR; + ctx.stroke(); + ctx.strokeStyle = fgcolor; + ctx.globalAlpha = 1; + } + + // these counter helps in conditioning drawing based on if the node has been executed or an action occurred + if (node.execute_triggered > 0) node.execute_triggered--; + if (node.action_triggered > 0) node.action_triggered--; + }; + + var margin_area = new Float32Array(4); + var link_bounding = new Float32Array(4); + var tempA = new Float32Array(2); + var tempB = new Float32Array(2); + + /** + * draws every connection visible in the canvas + * OPTIMIZE THIS: pre-catch connections position instead of recomputing them every time + * @method drawConnections + **/ + LGraphCanvas.prototype.drawConnections = function (ctx) { + var now = LiteGraph.getTime(); + var visible_area = this.visible_area; + margin_area[0] = visible_area[0] - 20; + margin_area[1] = visible_area[1] - 20; + margin_area[2] = visible_area[2] + 40; + margin_area[3] = visible_area[3] + 40; + + //draw connections + ctx.lineWidth = this.connections_width; + + ctx.fillStyle = "#AAA"; + ctx.strokeStyle = "#AAA"; + ctx.globalAlpha = this.editor_alpha; + //for every node + var nodes = this.graph._nodes; + for (var n = 0, l = nodes.length; n < l; ++n) { + var node = nodes[n]; + //for every input (we render just inputs because it is easier as every slot can only have one input) + if (!node.inputs || !node.inputs.length) { + continue; + } + + for (var i = 0; i < node.inputs.length; ++i) { + var input = node.inputs[i]; + if (!input || input.link == null) { + continue; + } + var link_id = input.link; + var link = this.graph.links[link_id]; + if (!link) { + continue; + } + + //find link info + var start_node = this.graph.getNodeById(link.origin_id); + if (start_node == null) { + continue; + } + var start_node_slot = link.origin_slot; + var start_node_slotpos = null; + if (start_node_slot == -1) { + start_node_slotpos = [start_node.pos[0] + 10, start_node.pos[1] + 10]; + } else { + start_node_slotpos = start_node.getConnectionPos( + false, + start_node_slot, + tempA + ); + } + var end_node_slotpos = node.getConnectionPos(true, i, tempB); + + //compute link bounding + link_bounding[0] = start_node_slotpos[0]; + link_bounding[1] = start_node_slotpos[1]; + link_bounding[2] = end_node_slotpos[0] - start_node_slotpos[0]; + link_bounding[3] = end_node_slotpos[1] - start_node_slotpos[1]; + if (link_bounding[2] < 0) { + link_bounding[0] += link_bounding[2]; + link_bounding[2] = Math.abs(link_bounding[2]); + } + if (link_bounding[3] < 0) { + link_bounding[1] += link_bounding[3]; + link_bounding[3] = Math.abs(link_bounding[3]); + } + + //skip links outside of the visible area of the canvas + if (!overlapBounding(link_bounding, margin_area)) { + continue; + } + + var start_slot = start_node.outputs[start_node_slot]; + var end_slot = node.inputs[i]; + if (!start_slot || !end_slot) { + continue; + } + var start_dir = + start_slot.dir || + (start_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT); + var end_dir = + end_slot.dir || (node.horizontal ? LiteGraph.UP : LiteGraph.LEFT); + + this.renderLink( + ctx, + start_node_slotpos, + end_node_slotpos, + link, + false, + 0, + null, + start_dir, + end_dir + ); + + //event triggered rendered on top + if (link && link._last_time && now - link._last_time < 1000) { + var f = 2.0 - (now - link._last_time) * 0.002; + var tmp = ctx.globalAlpha; + ctx.globalAlpha = tmp * f; + this.renderLink( + ctx, + start_node_slotpos, + end_node_slotpos, + link, + true, + f, + "white", + start_dir, + end_dir + ); + ctx.globalAlpha = tmp; + } + } + } + ctx.globalAlpha = 1; + }; + + /** + * draws a link between two points + * @method renderLink + * @param {vec2} a start pos + * @param {vec2} b end pos + * @param {Object} link the link object with all the link info + * @param {boolean} skip_border ignore the shadow of the link + * @param {boolean} flow show flow animation (for events) + * @param {string} color the color for the link + * @param {number} start_dir the direction enum + * @param {number} end_dir the direction enum + * @param {number} num_sublines number of sublines (useful to represent vec3 or rgb) + **/ + LGraphCanvas.prototype.renderLink = function ( + ctx, + a, + b, + link, + skip_border, + flow, + color, + start_dir, + end_dir, + num_sublines + ) { + if (link) { + this.visible_links.push(link); + } + + //choose color + if (!color && link) { + color = link.color || LGraphCanvas.link_type_colors[link.type]; + } + if (!color) { + color = this.default_link_color; + } + if (link != null && this.highlighted_links[link.id]) { + color = "#FFF"; + } + + start_dir = start_dir || LiteGraph.RIGHT; + end_dir = end_dir || LiteGraph.LEFT; + + var dist = distance(a, b); + + if (this.render_connections_border && this.ds.scale > 0.6) { + ctx.lineWidth = this.connections_width + 4; + } + ctx.lineJoin = "round"; + num_sublines = num_sublines || 1; + if (num_sublines > 1) { + ctx.lineWidth = 0.5; + } + + //begin line shape + ctx.beginPath(); + for (var i = 0; i < num_sublines; i += 1) { + var offsety = (i - (num_sublines - 1) * 0.5) * 5; + + if (this.links_render_mode == LiteGraph.SPLINE_LINK) { + ctx.moveTo(a[0], a[1] + offsety); + var start_offset_x = 0; + var start_offset_y = 0; + var end_offset_x = 0; + var end_offset_y = 0; switch (start_dir) { - case LiteGraph.LEFT: - p1[0] += dist * -0.25; - break; - case LiteGraph.RIGHT: - p1[0] += dist * 0.25; - break; - case LiteGraph.UP: - p1[1] += dist * -0.25; - break; - case LiteGraph.DOWN: - p1[1] += dist * 0.25; - break; + case LiteGraph.LEFT: + start_offset_x = dist * -0.25; + break; + case LiteGraph.RIGHT: + start_offset_x = dist * 0.25; + break; + case LiteGraph.UP: + start_offset_y = dist * -0.25; + break; + case LiteGraph.DOWN: + start_offset_y = dist * 0.25; + break; } switch (end_dir) { - case LiteGraph.LEFT: - p2[0] += dist * -0.25; - break; - case LiteGraph.RIGHT: - p2[0] += dist * 0.25; - break; - case LiteGraph.UP: - p2[1] += dist * -0.25; - break; - case LiteGraph.DOWN: - p2[1] += dist * 0.25; - break; + case LiteGraph.LEFT: + end_offset_x = dist * -0.25; + break; + case LiteGraph.RIGHT: + end_offset_x = dist * 0.25; + break; + case LiteGraph.UP: + end_offset_y = dist * -0.25; + break; + case LiteGraph.DOWN: + end_offset_y = dist * 0.25; + break; } - - var c1 = (1 - t) * (1 - t) * (1 - t); - var c2 = 3 * ((1 - t) * (1 - t)) * t; - var c3 = 3 * (1 - t) * (t * t); - var c4 = t * t * t; - - var x = c1 * p0[0] + c2 * p1[0] + c3 * p2[0] + c4 * p3[0]; - var y = c1 * p0[1] + c2 * p1[1] + c3 * p2[1] + c4 * p3[1]; - return [x, y]; - }; - - LGraphCanvas.prototype.drawExecutionOrder = function(ctx) { - ctx.shadowColor = "transparent"; - ctx.globalAlpha = 0.25; - - ctx.textAlign = "center"; - ctx.strokeStyle = "white"; - ctx.globalAlpha = 0.75; - - var visible_nodes = this.visible_nodes; - for (var i = 0; i < visible_nodes.length; ++i) { - var node = visible_nodes[i]; - ctx.fillStyle = "black"; - ctx.fillRect( - node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT, - node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT - ); - if (node.order == 0) { - ctx.strokeRect( - node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, - node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, - LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT - ); - } - ctx.fillStyle = "#FFF"; - ctx.fillText( - node.order, - node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5, - node.pos[1] - 6 - ); + ctx.bezierCurveTo( + a[0] + start_offset_x, + a[1] + start_offset_y + offsety, + b[0] + end_offset_x, + b[1] + end_offset_y + offsety, + b[0], + b[1] + offsety + ); + } else if (this.links_render_mode == LiteGraph.LINEAR_LINK) { + ctx.moveTo(a[0], a[1] + offsety); + var start_offset_x = 0; + var start_offset_y = 0; + var end_offset_x = 0; + var end_offset_y = 0; + switch (start_dir) { + case LiteGraph.LEFT: + start_offset_x = -1; + break; + case LiteGraph.RIGHT: + start_offset_x = 1; + break; + case LiteGraph.UP: + start_offset_y = -1; + break; + case LiteGraph.DOWN: + start_offset_y = 1; + break; } - ctx.globalAlpha = 1; - }; + switch (end_dir) { + case LiteGraph.LEFT: + end_offset_x = -1; + break; + case LiteGraph.RIGHT: + end_offset_x = 1; + break; + case LiteGraph.UP: + end_offset_y = -1; + break; + case LiteGraph.DOWN: + end_offset_y = 1; + break; + } + var l = 15; + ctx.lineTo( + a[0] + start_offset_x * l, + a[1] + start_offset_y * l + offsety + ); + ctx.lineTo(b[0] + end_offset_x * l, b[1] + end_offset_y * l + offsety); + ctx.lineTo(b[0], b[1] + offsety); + } else if (this.links_render_mode == LiteGraph.STRAIGHT_LINK) { + ctx.moveTo(a[0], a[1]); + var start_x = a[0]; + var start_y = a[1]; + var end_x = b[0]; + var end_y = b[1]; + if (start_dir == LiteGraph.RIGHT) { + start_x += 10; + } else { + start_y += 10; + } + if (end_dir == LiteGraph.LEFT) { + end_x -= 10; + } else { + end_y -= 10; + } + ctx.lineTo(start_x, start_y); + ctx.lineTo((start_x + end_x) * 0.5, start_y); + ctx.lineTo((start_x + end_x) * 0.5, end_y); + ctx.lineTo(end_x, end_y); + ctx.lineTo(b[0], b[1]); + } else { + return; + } //unknown + } - /** - * draws the widgets stored inside a node - * @method drawNodeWidgets - **/ - LGraphCanvas.prototype.drawNodeWidgets = function( - node, - posY, - ctx, - active_widget + //rendering the outline of the connection can be a little bit slow + if (this.render_connections_border && this.ds.scale > 0.6 && !skip_border) { + ctx.strokeStyle = "rgba(0,0,0,0.5)"; + ctx.stroke(); + } + + ctx.lineWidth = this.connections_width; + ctx.fillStyle = ctx.strokeStyle = color; + ctx.stroke(); + //end line shape + + var pos = this.computeConnectionPoint(a, b, 0.5, start_dir, end_dir); + if (link && link._pos) { + link._pos[0] = pos[0]; + link._pos[1] = pos[1]; + } + + //render arrow in the middle + if ( + this.ds.scale >= 0.6 && + this.highquality_render && + end_dir != LiteGraph.CENTER ) { - if (!node.widgets || !node.widgets.length) { - return 0; + //render arrow + if (this.render_connection_arrows) { + //compute two points in the connection + var posA = this.computeConnectionPoint(a, b, 0.25, start_dir, end_dir); + var posB = this.computeConnectionPoint(a, b, 0.26, start_dir, end_dir); + var posC = this.computeConnectionPoint(a, b, 0.75, start_dir, end_dir); + var posD = this.computeConnectionPoint(a, b, 0.76, start_dir, end_dir); + + //compute the angle between them so the arrow points in the right direction + var angleA = 0; + var angleB = 0; + if (this.render_curved_connections) { + angleA = -Math.atan2(posB[0] - posA[0], posB[1] - posA[1]); + angleB = -Math.atan2(posD[0] - posC[0], posD[1] - posC[1]); + } else { + angleB = angleA = b[1] > a[1] ? 0 : Math.PI; } - var width = node.size[0]; - var widgets = node.widgets; - posY += 2; - var H = LiteGraph.NODE_WIDGET_HEIGHT; - var show_text = this.ds.scale > 0.5; + + //render arrow ctx.save(); - ctx.globalAlpha = this.editor_alpha; - var outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; - var background_color = LiteGraph.WIDGET_BGCOLOR; - var text_color = LiteGraph.WIDGET_TEXT_COLOR; - var secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; - var margin = 15; - - for (var i = 0; i < widgets.length; ++i) { - var w = widgets[i]; - var y = posY; - if (w.y) { - y = w.y; - } - w.last_y = y; - ctx.strokeStyle = outline_color; - ctx.fillStyle = "#222"; - ctx.textAlign = "left"; - //ctx.lineWidth = 2; - if(w.disabled) - ctx.globalAlpha *= 0.5; - var widget_width = w.width || width; - - switch (w.type) { - case "button": - ctx.fillStyle = background_color; - if (w.clicked) { - ctx.fillStyle = "#AAA"; - w.clicked = false; - this.dirty_canvas = true; - } - ctx.fillRect(margin, y, widget_width - margin * 2, H); - if(show_text && !w.disabled) - ctx.strokeRect( margin, y, widget_width - margin * 2, H ); - if (show_text) { - ctx.textAlign = "center"; - ctx.fillStyle = text_color; - ctx.fillText(w.label || w.name, widget_width * 0.5, y + H * 0.7); - } - break; - case "toggle": - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if (show_text) - ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); - else - ctx.rect(margin, y, widget_width - margin * 2, H ); - ctx.fill(); - if(show_text && !w.disabled) - ctx.stroke(); - ctx.fillStyle = w.value ? "#89A" : "#333"; - ctx.beginPath(); - ctx.arc( widget_width - margin * 2, y + H * 0.5, H * 0.36, 0, Math.PI * 2 ); - ctx.fill(); - if (show_text) { - ctx.fillStyle = secondary_text_color; - const label = w.label || w.name; - if (label != null) { - ctx.fillText(label, margin * 2, y + H * 0.7); - } - ctx.fillStyle = w.value ? text_color : secondary_text_color; - ctx.textAlign = "right"; - ctx.fillText( - w.value - ? w.options.on || "true" - : w.options.off || "false", - widget_width - 40, - y + H * 0.7 - ); - } - break; - case "slider": - ctx.fillStyle = background_color; - ctx.fillRect(margin, y, widget_width - margin * 2, H); - var range = w.options.max - w.options.min; - var nvalue = (w.value - w.options.min) / range; - if(nvalue < 0.0) nvalue = 0.0; - if(nvalue > 1.0) nvalue = 1.0; - ctx.fillStyle = w.options.hasOwnProperty("slider_color") ? w.options.slider_color : (active_widget == w ? "#89A" : "#678"); - ctx.fillRect(margin, y, nvalue * (widget_width - margin * 2), H); - if(show_text && !w.disabled) - ctx.strokeRect(margin, y, widget_width - margin * 2, H); - if (w.marker) { - var marker_nvalue = (w.marker - w.options.min) / range; - if(marker_nvalue < 0.0) marker_nvalue = 0.0; - if(marker_nvalue > 1.0) marker_nvalue = 1.0; - ctx.fillStyle = w.options.hasOwnProperty("marker_color") ? w.options.marker_color : "#AA9"; - ctx.fillRect( margin + marker_nvalue * (widget_width - margin * 2), y, 2, H ); - } - if (show_text) { - ctx.textAlign = "center"; - ctx.fillStyle = text_color; - ctx.fillText( - w.label || w.name + " " + Number(w.value).toFixed( - w.options.precision != null - ? w.options.precision - : 3 - ), - widget_width * 0.5, - y + H * 0.7 - ); - } - break; - case "number": - case "combo": - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if(show_text) - ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5] ); - else - ctx.rect(margin, y, widget_width - margin * 2, H ); - ctx.fill(); - if (show_text) { - if(!w.disabled) - ctx.stroke(); - ctx.fillStyle = text_color; - if(!w.disabled) - { - ctx.beginPath(); - ctx.moveTo(margin + 16, y + 5); - ctx.lineTo(margin + 6, y + H * 0.5); - ctx.lineTo(margin + 16, y + H - 5); - ctx.fill(); - ctx.beginPath(); - ctx.moveTo(widget_width - margin - 16, y + 5); - ctx.lineTo(widget_width - margin - 6, y + H * 0.5); - ctx.lineTo(widget_width - margin - 16, y + H - 5); - ctx.fill(); - } - ctx.fillStyle = secondary_text_color; - ctx.fillText(w.label || w.name, margin * 2 + 5, y + H * 0.7); - ctx.fillStyle = text_color; - ctx.textAlign = "right"; - if (w.type == "number") { - ctx.fillText( - Number(w.value).toFixed( - w.options.precision !== undefined - ? w.options.precision - : 3 - ), - widget_width - margin * 2 - 20, - y + H * 0.7 - ); - } else { - var v = w.value; - if( w.options.values ) - { - var values = w.options.values; - if( values.constructor === Function ) - values = values(); - if(values && values.constructor !== Array) - v = values[ w.value ]; - } - ctx.fillText( - v, - widget_width - margin * 2 - 20, - y + H * 0.7 - ); - } - } - break; - case "string": - case "text": - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if (show_text) - ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); - else - ctx.rect( margin, y, widget_width - margin * 2, H ); - ctx.fill(); - if (show_text) { - if(!w.disabled) - ctx.stroke(); - ctx.save(); - ctx.beginPath(); - ctx.rect(margin, y, widget_width - margin * 2, H); - ctx.clip(); - - //ctx.stroke(); - ctx.fillStyle = secondary_text_color; - const label = w.label || w.name; - if (label != null) { - ctx.fillText(label, margin * 2, y + H * 0.7); - } - ctx.fillStyle = text_color; - ctx.textAlign = "right"; - ctx.fillText(String(w.value).substr(0,30), widget_width - margin * 2, y + H * 0.7); //30 chars max - ctx.restore(); - } - break; - default: - if (w.draw) { - w.draw(ctx, node, widget_width, y, H); - } - break; - } - posY += (w.computeSize ? w.computeSize(widget_width)[1] : H) + 4; - ctx.globalAlpha = this.editor_alpha; - - } + ctx.translate(posA[0], posA[1]); + ctx.rotate(angleA); + ctx.beginPath(); + ctx.moveTo(-5, -3); + ctx.lineTo(0, +7); + ctx.lineTo(+5, -3); + ctx.fill(); ctx.restore(); - ctx.textAlign = "left"; - }; - - /** - * process an event on widgets - * @method processNodeWidgets - **/ - LGraphCanvas.prototype.processNodeWidgets = function( - node, - pos, - event, - active_widget - ) { - if (!node.widgets || !node.widgets.length || (!this.allow_interaction && !node.flags.allow_interaction)) { - return null; - } - - var x = pos[0] - node.pos[0]; - var y = pos[1] - node.pos[1]; - var width = node.size[0]; - var that = this; - var ref_window = this.getCanvasWindow(); - - for (var i = 0; i < node.widgets.length; ++i) { - var w = node.widgets[i]; - if(!w || w.disabled) - continue; - var widget_height = w.computeSize ? w.computeSize(width)[1] : LiteGraph.NODE_WIDGET_HEIGHT; - var widget_width = w.width || width; - //outside - if ( w != active_widget && - (x < 6 || x > widget_width - 12 || y < w.last_y || y > w.last_y + widget_height || w.last_y === undefined) ) - continue; - - var old_value = w.value; - - //if ( w == active_widget || (x > 6 && x < widget_width - 12 && y > w.last_y && y < w.last_y + widget_height) ) { - //inside widget - switch (w.type) { - case "button": - if (event.type === LiteGraph.pointerevents_method+"down") { - if (w.callback) { - setTimeout(function() { - w.callback(w, that, node, pos, event); - }, 20); - } - w.clicked = true; - this.dirty_canvas = true; - } - break; - case "slider": - var old_value = w.value; - var nvalue = clamp((x - 15) / (widget_width - 30), 0, 1); - if(w.options.read_only) break; - w.value = w.options.min + (w.options.max - w.options.min) * nvalue; - if (old_value != w.value) { - setTimeout(function() { - inner_value_change(w, w.value); - }, 20); - } - this.dirty_canvas = true; - break; - case "number": - case "combo": - var old_value = w.value; - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - var allow_scroll = true; - if (delta) { - if (x > -3 && x < widget_width + 3) { - allow_scroll = false; - } - } - if (allow_scroll && event.type == LiteGraph.pointerevents_method+"move" && w.type == "number") { - if(event.deltaX) - w.value += event.deltaX * 0.1 * (w.options.step || 1); - if ( w.options.min != null && w.value < w.options.min ) { - w.value = w.options.min; - } - if ( w.options.max != null && w.value > w.options.max ) { - w.value = w.options.max; - } - } else if (event.type == LiteGraph.pointerevents_method+"down") { - var values = w.options.values; - if (values && values.constructor === Function) { - values = w.options.values(w, node); - } - var values_list = null; - - if( w.type != "number") - values_list = values.constructor === Array ? values : Object.keys(values); - - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (w.type == "number") { - w.value += delta * 0.1 * (w.options.step || 1); - if ( w.options.min != null && w.value < w.options.min ) { - w.value = w.options.min; - } - if ( w.options.max != null && w.value > w.options.max ) { - w.value = w.options.max; - } - } else if (delta) { //clicked in arrow, used for combos - var index = -1; - this.last_mouseclick = 0; //avoids dobl click event - if(values.constructor === Object) - index = values_list.indexOf( String( w.value ) ) + delta; - else - index = values_list.indexOf( w.value ) + delta; - if (index >= values_list.length) { - index = values_list.length - 1; - } - if (index < 0) { - index = 0; - } - if( values.constructor === Array ) - w.value = values[index]; - else - w.value = index; - } else { //combo clicked - var text_values = values != values_list ? Object.values(values) : values; - var menu = new LiteGraph.ContextMenu(text_values, { - scale: Math.max(1, this.ds.scale), - event: event, - className: "dark", - callback: inner_clicked.bind(w) - }, - ref_window); - function inner_clicked(v, option, event) { - if(values != values_list) - v = text_values.indexOf(v); - this.value = v; - inner_value_change(this, v); - that.dirty_canvas = true; - return false; - } - } - } //end mousedown - else if(event.type == LiteGraph.pointerevents_method+"up" && w.type == "number") - { - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (event.click_time < 200 && delta == 0) { - this.prompt("Value",w.value,function(v) { - // check if v is a valid equation or a number - if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { - try {//solve the equation if possible - v = eval(v); - } catch (e) { } - } - this.value = Number(v); - inner_value_change(this, this.value); - }.bind(w), - event); - } - } - - if( old_value != w.value ) - setTimeout( - function() { - inner_value_change(this, this.value); - }.bind(w), - 20 - ); - this.dirty_canvas = true; - break; - case "toggle": - if (event.type == LiteGraph.pointerevents_method+"down") { - w.value = !w.value; - setTimeout(function() { - inner_value_change(w, w.value); - }, 20); - } - break; - case "string": - case "text": - if (event.type == LiteGraph.pointerevents_method+"down") { - this.prompt("Value",w.value,function(v) { - inner_value_change(this, v); - }.bind(w), - event,w.options ? w.options.multiline : false ); - } - break; - default: - if (w.mouse) { - this.dirty_canvas = w.mouse(event, [x, y], node); - } - break; - } //end switch - - //value changed - if( old_value != w.value ) - { - if(node.onWidgetChanged) - node.onWidgetChanged( w.name,w.value,old_value,w ); - node.graph._version++; - } - - return w; - }//end for - - function inner_value_change(widget, value) { - if(widget.type == "number"){ - value = Number(value); - } - widget.value = value; - if ( widget.options && widget.options.property && node.properties[widget.options.property] !== undefined ) { - node.setProperty( widget.options.property, value ); - } - if (widget.callback) { - widget.callback(widget.value, that, node, pos, event); - } - } - - return null; - }; - - /** - * draws every group area in the background - * @method drawGroups - **/ - LGraphCanvas.prototype.drawGroups = function(canvas, ctx) { - if (!this.graph) { - return; - } - - var groups = this.graph._groups; - ctx.save(); - ctx.globalAlpha = 0.5 * this.editor_alpha; - - for (var i = 0; i < groups.length; ++i) { - var group = groups[i]; - - if (!overlapBounding(this.visible_area, group._bounding)) { - continue; - } //out of the visible area - - ctx.fillStyle = group.color || "#335"; - ctx.strokeStyle = group.color || "#335"; - var pos = group._pos; - var size = group._size; - ctx.globalAlpha = 0.25 * this.editor_alpha; - ctx.beginPath(); - ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], size[1]); - ctx.fill(); - ctx.globalAlpha = this.editor_alpha; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(pos[0] + size[0], pos[1] + size[1]); - ctx.lineTo(pos[0] + size[0] - 10, pos[1] + size[1]); - ctx.lineTo(pos[0] + size[0], pos[1] + size[1] - 10); - ctx.fill(); - - var font_size = - group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - ctx.font = font_size + "px Arial"; - ctx.textAlign = "left"; - ctx.fillText(group.title, pos[0] + 4, pos[1] + font_size); - } - + ctx.translate(posC[0], posC[1]); + ctx.rotate(angleB); + ctx.beginPath(); + ctx.moveTo(-5, -3); + ctx.lineTo(0, +7); + ctx.lineTo(+5, -3); + ctx.fill(); ctx.restore(); - }; + } - LGraphCanvas.prototype.adjustNodesSize = function() { - var nodes = this.graph._nodes; - for (var i = 0; i < nodes.length; ++i) { - nodes[i].size = nodes[i].computeSize(); - } - this.setDirty(true, true); - }; + //circle + ctx.beginPath(); + ctx.arc(pos[0], pos[1], 5, 0, Math.PI * 2); + ctx.fill(); + } - /** - * resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode - * @method resize - **/ - LGraphCanvas.prototype.resize = function(width, height) { - if (!width && !height) { - var parent = this.canvas.parentNode; - width = parent.offsetWidth; - height = parent.offsetHeight; - } + //render flowing points + if (flow) { + ctx.fillStyle = color; + for (var i = 0; i < 5; ++i) { + var f = (LiteGraph.getTime() * 0.001 + i * 0.2) % 1; + var pos = this.computeConnectionPoint(a, b, f, start_dir, end_dir); + ctx.beginPath(); + ctx.arc(pos[0], pos[1], 5, 0, 2 * Math.PI); + ctx.fill(); + } + } + }; - if (this.canvas.width == width && this.canvas.height == height) { - return; - } + //returns the link center point based on curvature + LGraphCanvas.prototype.computeConnectionPoint = function ( + a, + b, + t, + start_dir, + end_dir + ) { + start_dir = start_dir || LiteGraph.RIGHT; + end_dir = end_dir || LiteGraph.LEFT; - this.canvas.width = width; - this.canvas.height = height; - this.bgcanvas.width = this.canvas.width; - this.bgcanvas.height = this.canvas.height; - this.setDirty(true, true); - }; + var dist = distance(a, b); + var p0 = a; + var p1 = [a[0], a[1]]; + var p2 = [b[0], b[1]]; + var p3 = b; - /** - * switches to live mode (node shapes are not rendered, only the content) - * this feature was designed when graphs where meant to create user interfaces - * @method switchLiveMode - **/ - LGraphCanvas.prototype.switchLiveMode = function(transition) { - if (!transition) { - this.live_mode = !this.live_mode; + switch (start_dir) { + case LiteGraph.LEFT: + p1[0] += dist * -0.25; + break; + case LiteGraph.RIGHT: + p1[0] += dist * 0.25; + break; + case LiteGraph.UP: + p1[1] += dist * -0.25; + break; + case LiteGraph.DOWN: + p1[1] += dist * 0.25; + break; + } + switch (end_dir) { + case LiteGraph.LEFT: + p2[0] += dist * -0.25; + break; + case LiteGraph.RIGHT: + p2[0] += dist * 0.25; + break; + case LiteGraph.UP: + p2[1] += dist * -0.25; + break; + case LiteGraph.DOWN: + p2[1] += dist * 0.25; + break; + } + + var c1 = (1 - t) * (1 - t) * (1 - t); + var c2 = 3 * ((1 - t) * (1 - t)) * t; + var c3 = 3 * (1 - t) * (t * t); + var c4 = t * t * t; + + var x = c1 * p0[0] + c2 * p1[0] + c3 * p2[0] + c4 * p3[0]; + var y = c1 * p0[1] + c2 * p1[1] + c3 * p2[1] + c4 * p3[1]; + return [x, y]; + }; + + LGraphCanvas.prototype.drawExecutionOrder = function (ctx) { + ctx.shadowColor = "transparent"; + ctx.globalAlpha = 0.25; + + ctx.textAlign = "center"; + ctx.strokeStyle = "white"; + ctx.globalAlpha = 0.75; + + var visible_nodes = this.visible_nodes; + for (var i = 0; i < visible_nodes.length; ++i) { + var node = visible_nodes[i]; + ctx.fillStyle = "black"; + ctx.fillRect( + node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT, + node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, + LiteGraph.NODE_TITLE_HEIGHT, + LiteGraph.NODE_TITLE_HEIGHT + ); + if (node.order == 0) { + ctx.strokeRect( + node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, + node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, + LiteGraph.NODE_TITLE_HEIGHT, + LiteGraph.NODE_TITLE_HEIGHT + ); + } + ctx.fillStyle = "#FFF"; + ctx.fillText( + node.order, + node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5, + node.pos[1] - 6 + ); + } + ctx.globalAlpha = 1; + }; + + /** + * draws the widgets stored inside a node + * @method drawNodeWidgets + **/ + LGraphCanvas.prototype.drawNodeWidgets = function ( + node, + posY, + ctx, + active_widget + ) { + if (!node.widgets || !node.widgets.length) { + return 0; + } + var width = node.size[0]; + var widgets = node.widgets; + posY += 2; + var H = LiteGraph.NODE_WIDGET_HEIGHT; + var show_text = this.ds.scale > 0.5; + ctx.save(); + ctx.globalAlpha = this.editor_alpha; + var outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; + var background_color = LiteGraph.WIDGET_BGCOLOR; + var text_color = LiteGraph.WIDGET_TEXT_COLOR; + var secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + var margin = 15; + + for (var i = 0; i < widgets.length; ++i) { + var w = widgets[i]; + var y = posY; + if (w.y) { + y = w.y; + } + w.last_y = y; + ctx.strokeStyle = outline_color; + ctx.fillStyle = "#222"; + ctx.textAlign = "left"; + //ctx.lineWidth = 2; + if (w.disabled) ctx.globalAlpha *= 0.5; + var widget_width = w.width || width; + + switch (w.type) { + case "button": + ctx.fillStyle = background_color; + if (w.clicked) { + ctx.fillStyle = "#AAA"; + w.clicked = false; this.dirty_canvas = true; - this.dirty_bgcanvas = true; - return; - } - - var self = this; - var delta = this.live_mode ? 1.1 : 0.9; - if (this.live_mode) { - this.live_mode = false; - this.editor_alpha = 0.1; - } - - var t = setInterval(function() { - self.editor_alpha *= delta; - self.dirty_canvas = true; - self.dirty_bgcanvas = true; - - if (delta < 1 && self.editor_alpha < 0.01) { - clearInterval(t); - if (delta < 1) { - self.live_mode = true; - } + } + ctx.fillRect(margin, y, widget_width - margin * 2, H); + if (show_text && !w.disabled) + ctx.strokeRect(margin, y, widget_width - margin * 2, H); + if (show_text) { + ctx.textAlign = "center"; + ctx.fillStyle = text_color; + ctx.fillText(w.label || w.name, widget_width * 0.5, y + H * 0.7); + } + break; + case "toggle": + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + if (show_text) + ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); + else ctx.rect(margin, y, widget_width - margin * 2, H); + ctx.fill(); + if (show_text && !w.disabled) ctx.stroke(); + ctx.fillStyle = w.value ? "#89A" : "#333"; + ctx.beginPath(); + ctx.arc( + widget_width - margin * 2, + y + H * 0.5, + H * 0.36, + 0, + Math.PI * 2 + ); + ctx.fill(); + if (show_text) { + ctx.fillStyle = secondary_text_color; + const label = w.label || w.name; + if (label != null) { + ctx.fillText(label, margin * 2, y + H * 0.7); } - if (delta > 1 && self.editor_alpha > 0.99) { - clearInterval(t); - self.editor_alpha = 1; + ctx.fillStyle = w.value ? text_color : secondary_text_color; + ctx.textAlign = "right"; + ctx.fillText( + w.value ? w.options.on || "true" : w.options.off || "false", + widget_width - 40, + y + H * 0.7 + ); + } + break; + case "slider": + ctx.fillStyle = background_color; + ctx.fillRect(margin, y, widget_width - margin * 2, H); + var range = w.options.max - w.options.min; + var nvalue = (w.value - w.options.min) / range; + if (nvalue < 0.0) nvalue = 0.0; + if (nvalue > 1.0) nvalue = 1.0; + ctx.fillStyle = w.options.hasOwnProperty("slider_color") + ? w.options.slider_color + : active_widget == w + ? "#89A" + : "#678"; + ctx.fillRect(margin, y, nvalue * (widget_width - margin * 2), H); + if (show_text && !w.disabled) + ctx.strokeRect(margin, y, widget_width - margin * 2, H); + if (w.marker) { + var marker_nvalue = (w.marker - w.options.min) / range; + if (marker_nvalue < 0.0) marker_nvalue = 0.0; + if (marker_nvalue > 1.0) marker_nvalue = 1.0; + ctx.fillStyle = w.options.hasOwnProperty("marker_color") + ? w.options.marker_color + : "#AA9"; + ctx.fillRect( + margin + marker_nvalue * (widget_width - margin * 2), + y, + 2, + H + ); + } + if (show_text) { + ctx.textAlign = "center"; + ctx.fillStyle = text_color; + ctx.fillText( + w.label || + w.name + + " " + + Number(w.value).toFixed( + w.options.precision != null ? w.options.precision : 3 + ), + widget_width * 0.5, + y + H * 0.7 + ); + } + break; + case "number": + case "combo": + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + if (show_text) + ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); + else ctx.rect(margin, y, widget_width - margin * 2, H); + ctx.fill(); + if (show_text) { + if (!w.disabled) ctx.stroke(); + ctx.fillStyle = text_color; + if (!w.disabled) { + ctx.beginPath(); + ctx.moveTo(margin + 16, y + 5); + ctx.lineTo(margin + 6, y + H * 0.5); + ctx.lineTo(margin + 16, y + H - 5); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(widget_width - margin - 16, y + 5); + ctx.lineTo(widget_width - margin - 6, y + H * 0.5); + ctx.lineTo(widget_width - margin - 16, y + H - 5); + ctx.fill(); } - }, 1); - }; + ctx.fillStyle = secondary_text_color; + ctx.fillText(w.label || w.name, margin * 2 + 5, y + H * 0.7); + ctx.fillStyle = text_color; + ctx.textAlign = "right"; + if (w.type == "number") { + ctx.fillText( + Number(w.value).toFixed( + w.options.precision !== undefined ? w.options.precision : 3 + ), + widget_width - margin * 2 - 20, + y + H * 0.7 + ); + } else { + var v = w.value; + if (w.options.values) { + var values = w.options.values; + if (values.constructor === Function) values = values(); + if (values && values.constructor !== Array) v = values[w.value]; + } + ctx.fillText(v, widget_width - margin * 2 - 20, y + H * 0.7); + } + } + break; + case "string": + case "text": + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + if (show_text) + ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); + else ctx.rect(margin, y, widget_width - margin * 2, H); + ctx.fill(); + if (show_text) { + if (!w.disabled) ctx.stroke(); + ctx.save(); + ctx.beginPath(); + ctx.rect(margin, y, widget_width - margin * 2, H); + ctx.clip(); - LGraphCanvas.prototype.onNodeSelectionChange = function(node) { - return; //disabled - }; + //ctx.stroke(); + ctx.fillStyle = secondary_text_color; + const label = w.label || w.name; + if (label != null) { + ctx.fillText(label, margin * 2, y + H * 0.7); + } + ctx.fillStyle = text_color; + ctx.textAlign = "right"; + ctx.fillText( + String(w.value).substr(0, 30), + widget_width - margin * 2, + y + H * 0.7 + ); //30 chars max + ctx.restore(); + } + break; + default: + if (w.draw) { + w.draw(ctx, node, widget_width, y, H); + } + break; + } + posY += (w.computeSize ? w.computeSize(widget_width)[1] : H) + 4; + ctx.globalAlpha = this.editor_alpha; + } + ctx.restore(); + ctx.textAlign = "left"; + }; - /* this is an implementation for touch not in production and not ready - */ - /*LGraphCanvas.prototype.touchHandler = function(event) { + /** + * process an event on widgets + * @method processNodeWidgets + **/ + LGraphCanvas.prototype.processNodeWidgets = function ( + node, + pos, + event, + active_widget + ) { + if ( + !node.widgets || + !node.widgets.length || + (!this.allow_interaction && !node.flags.allow_interaction) + ) { + return null; + } + + var x = pos[0] - node.pos[0]; + var y = pos[1] - node.pos[1]; + var width = node.size[0]; + var that = this; + var ref_window = this.getCanvasWindow(); + + for (var i = 0; i < node.widgets.length; ++i) { + var w = node.widgets[i]; + if (!w || w.disabled) continue; + var widget_height = w.computeSize + ? w.computeSize(width)[1] + : LiteGraph.NODE_WIDGET_HEIGHT; + var widget_width = w.width || width; + //outside + if ( + w != active_widget && + (x < 6 || + x > widget_width - 12 || + y < w.last_y || + y > w.last_y + widget_height || + w.last_y === undefined) + ) + continue; + + var old_value = w.value; + + //if ( w == active_widget || (x > 6 && x < widget_width - 12 && y > w.last_y && y < w.last_y + widget_height) ) { + //inside widget + switch (w.type) { + case "button": + if (event.type === LiteGraph.pointerevents_method + "down") { + if (w.callback) { + setTimeout(function () { + w.callback(w, that, node, pos, event); + }, 20); + } + w.clicked = true; + this.dirty_canvas = true; + } + break; + case "slider": + var old_value = w.value; + var nvalue = clamp((x - 15) / (widget_width - 30), 0, 1); + if (w.options.read_only) break; + w.value = w.options.min + (w.options.max - w.options.min) * nvalue; + if (old_value != w.value) { + setTimeout(function () { + inner_value_change(w, w.value); + }, 20); + } + this.dirty_canvas = true; + break; + case "number": + case "combo": + var old_value = w.value; + var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; + var allow_scroll = true; + if (delta) { + if (x > -3 && x < widget_width + 3) { + allow_scroll = false; + } + } + if ( + allow_scroll && + event.type == LiteGraph.pointerevents_method + "move" && + w.type == "number" + ) { + if (event.deltaX) + w.value += event.deltaX * 0.1 * (w.options.step || 1); + if (w.options.min != null && w.value < w.options.min) { + w.value = w.options.min; + } + if (w.options.max != null && w.value > w.options.max) { + w.value = w.options.max; + } + } else if (event.type == LiteGraph.pointerevents_method + "down") { + var values = w.options.values; + if (values && values.constructor === Function) { + values = w.options.values(w, node); + } + var values_list = null; + + if (w.type != "number") + values_list = + values.constructor === Array ? values : Object.keys(values); + + var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; + if (w.type == "number") { + w.value += delta * 0.1 * (w.options.step || 1); + if (w.options.min != null && w.value < w.options.min) { + w.value = w.options.min; + } + if (w.options.max != null && w.value > w.options.max) { + w.value = w.options.max; + } + } else if (delta) { + //clicked in arrow, used for combos + var index = -1; + this.last_mouseclick = 0; //avoids dobl click event + if (values.constructor === Object) + index = values_list.indexOf(String(w.value)) + delta; + else index = values_list.indexOf(w.value) + delta; + if (index >= values_list.length) { + index = values_list.length - 1; + } + if (index < 0) { + index = 0; + } + if (values.constructor === Array) w.value = values[index]; + else w.value = index; + } else { + //combo clicked + var text_values = + values != values_list ? Object.values(values) : values; + var menu = new LiteGraph.ContextMenu( + text_values, + { + scale: Math.max(1, this.ds.scale), + event: event, + className: "dark", + callback: inner_clicked.bind(w), + }, + ref_window + ); + function inner_clicked(v, option, event) { + if (values != values_list) v = text_values.indexOf(v); + this.value = v; + inner_value_change(this, v); + that.dirty_canvas = true; + return false; + } + } + } //end mousedown + else if ( + event.type == LiteGraph.pointerevents_method + "up" && + w.type == "number" + ) { + var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; + if (event.click_time < 200 && delta == 0) { + this.prompt( + "Value", + w.value, + function (v) { + // check if v is a valid equation or a number + if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { + try { + //solve the equation if possible + v = eval(v); + } catch (e) {} + } + this.value = Number(v); + inner_value_change(this, this.value); + }.bind(w), + event + ); + } + } + + if (old_value != w.value) + setTimeout( + function () { + inner_value_change(this, this.value); + }.bind(w), + 20 + ); + this.dirty_canvas = true; + break; + case "toggle": + if (event.type == LiteGraph.pointerevents_method + "down") { + w.value = !w.value; + setTimeout(function () { + inner_value_change(w, w.value); + }, 20); + } + break; + case "string": + case "text": + if (event.type == LiteGraph.pointerevents_method + "down") { + this.prompt( + "Value", + w.value, + function (v) { + inner_value_change(this, v); + }.bind(w), + event, + w.options ? w.options.multiline : false + ); + } + break; + default: + if (w.mouse) { + this.dirty_canvas = w.mouse(event, [x, y], node); + } + break; + } //end switch + + //value changed + if (old_value != w.value) { + if (node.onWidgetChanged) + node.onWidgetChanged(w.name, w.value, old_value, w); + node.graph._version++; + } + + return w; + } //end for + + function inner_value_change(widget, value) { + if (widget.type == "number") { + value = Number(value); + } + widget.value = value; + if ( + widget.options && + widget.options.property && + node.properties[widget.options.property] !== undefined + ) { + node.setProperty(widget.options.property, value); + } + if (widget.callback) { + widget.callback(widget.value, that, node, pos, event); + } + } + + return null; + }; + + /** + * draws every group area in the background + * @method drawGroups + **/ + LGraphCanvas.prototype.drawGroups = function (canvas, ctx) { + if (!this.graph) { + return; + } + + var groups = this.graph._groups; + + ctx.save(); + ctx.globalAlpha = 0.5 * this.editor_alpha; + + for (var i = 0; i < groups.length; ++i) { + var group = groups[i]; + + if (!overlapBounding(this.visible_area, group._bounding)) { + continue; + } //out of the visible area + + ctx.fillStyle = group.color || "#335"; + ctx.strokeStyle = group.color || "#335"; + var pos = group._pos; + var size = group._size; + ctx.globalAlpha = 0.25 * this.editor_alpha; + ctx.beginPath(); + ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], size[1]); + ctx.fill(); + ctx.globalAlpha = this.editor_alpha; + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(pos[0] + size[0], pos[1] + size[1]); + ctx.lineTo(pos[0] + size[0] - 10, pos[1] + size[1]); + ctx.lineTo(pos[0] + size[0], pos[1] + size[1] - 10); + ctx.fill(); + + var font_size = group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; + ctx.font = font_size + "px Arial"; + ctx.textAlign = "left"; + ctx.fillText(group.title, pos[0] + 4, pos[1] + font_size); + } + + ctx.restore(); + }; + + LGraphCanvas.prototype.adjustNodesSize = function () { + var nodes = this.graph._nodes; + for (var i = 0; i < nodes.length; ++i) { + nodes[i].size = nodes[i].computeSize(); + } + this.setDirty(true, true); + }; + + /** + * resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode + * @method resize + **/ + LGraphCanvas.prototype.resize = function (width, height) { + if (!width && !height) { + var parent = this.canvas.parentNode; + width = parent.offsetWidth; + height = parent.offsetHeight; + } + + if (this.canvas.width == width && this.canvas.height == height) { + return; + } + + this.canvas.width = width; + this.canvas.height = height; + this.bgcanvas.width = this.canvas.width; + this.bgcanvas.height = this.canvas.height; + this.setDirty(true, true); + }; + + /** + * switches to live mode (node shapes are not rendered, only the content) + * this feature was designed when graphs where meant to create user interfaces + * @method switchLiveMode + **/ + LGraphCanvas.prototype.switchLiveMode = function (transition) { + if (!transition) { + this.live_mode = !this.live_mode; + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + return; + } + + var self = this; + var delta = this.live_mode ? 1.1 : 0.9; + if (this.live_mode) { + this.live_mode = false; + this.editor_alpha = 0.1; + } + + var t = setInterval(function () { + self.editor_alpha *= delta; + self.dirty_canvas = true; + self.dirty_bgcanvas = true; + + if (delta < 1 && self.editor_alpha < 0.01) { + clearInterval(t); + if (delta < 1) { + self.live_mode = true; + } + } + if (delta > 1 && self.editor_alpha > 0.99) { + clearInterval(t); + self.editor_alpha = 1; + } + }, 1); + }; + + LGraphCanvas.prototype.onNodeSelectionChange = function (node) { + return; //disabled + }; + + /* this is an implementation for touch not in production and not ready + */ + /*LGraphCanvas.prototype.touchHandler = function(event) { //alert("foo"); var touches = event.changedTouches, first = touches[0], @@ -10374,556 +10550,599 @@ LGraphNode.prototype.executeAction = function(action) event.preventDefault(); };*/ - /* CONTEXT MENU ********************/ + /* CONTEXT MENU ********************/ - LGraphCanvas.onGroupAdd = function(info, entry, mouse_event) { - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); + LGraphCanvas.onGroupAdd = function (info, entry, mouse_event) { + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); - var group = new LiteGraph.LGraphGroup(); - group.pos = canvas.convertEventToCanvasOffset(mouse_event); - canvas.graph.add(group); + var group = new LiteGraph.LGraphGroup(); + group.pos = canvas.convertEventToCanvasOffset(mouse_event); + canvas.graph.add(group); + }; + + /** + * Determines the furthest nodes in each direction + * @param nodes {LGraphNode[]} the nodes to from which boundary nodes will be extracted + * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} + */ + LGraphCanvas.getBoundaryNodes = function (nodes) { + let top = null; + let right = null; + let bottom = null; + let left = null; + for (const nID in nodes) { + const node = nodes[nID]; + const [x, y] = node.pos; + const [width, height] = node.size; + + if (top === null || y < top.pos[1]) { + top = node; + } + if (right === null || x + width > right.pos[0] + right.size[0]) { + right = node; + } + if (bottom === null || y + height > bottom.pos[1] + bottom.size[1]) { + bottom = node; + } + if (left === null || x < left.pos[0]) { + left = node; + } + } + + return { + top: top, + right: right, + bottom: bottom, + left: left, }; + }; + /** + * Determines the furthest nodes in each direction for the currently selected nodes + * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} + */ + LGraphCanvas.prototype.boundaryNodesForSelection = function () { + return LGraphCanvas.getBoundaryNodes(Object.values(this.selected_nodes)); + }; - /** - * Determines the furthest nodes in each direction - * @param nodes {LGraphNode[]} the nodes to from which boundary nodes will be extracted - * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} - */ - LGraphCanvas.getBoundaryNodes = function(nodes) { - let top = null; - let right = null; - let bottom = null; - let left = null; - for (const nID in nodes) { - const node = nodes[nID]; - const [x, y] = node.pos; - const [width, height] = node.size; + /** + * + * @param {LGraphNode[]} nodes a list of nodes + * @param {"top"|"bottom"|"left"|"right"} direction Direction to align the nodes + * @param {LGraphNode?} align_to Node to align to (if null, align to the furthest node in the given direction) + */ + LGraphCanvas.alignNodes = function (nodes, direction, align_to) { + if (!nodes) { + return; + } - if (top === null || y < top.pos[1]) { - top = node; - } - if (right === null || x + width > right.pos[0] + right.size[0]) { - right = node; - } - if (bottom === null || y + height > bottom.pos[1] + bottom.size[1]) { - bottom = node; - } - if (left === null || x < left.pos[0]) { - left = node; - } + const canvas = LGraphCanvas.active_canvas; + let boundaryNodes = []; + if (align_to === undefined) { + boundaryNodes = LGraphCanvas.getBoundaryNodes(nodes); + } else { + boundaryNodes = { + top: align_to, + right: align_to, + bottom: align_to, + left: align_to, + }; + } + + for (const [_, node] of Object.entries(canvas.selected_nodes)) { + switch (direction) { + case "right": + node.pos[0] = + boundaryNodes["right"].pos[0] + + boundaryNodes["right"].size[0] - + node.size[0]; + break; + case "left": + node.pos[0] = boundaryNodes["left"].pos[0]; + break; + case "top": + node.pos[1] = boundaryNodes["top"].pos[1]; + break; + case "bottom": + node.pos[1] = + boundaryNodes["bottom"].pos[1] + + boundaryNodes["bottom"].size[1] - + node.size[1]; + break; + } + } + + canvas.dirty_canvas = true; + canvas.dirty_bgcanvas = true; + }; + + LGraphCanvas.onNodeAlign = function (value, options, event, prev_menu, node) { + new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { + event: event, + callback: inner_clicked, + parentMenu: prev_menu, + }); + + function inner_clicked(value) { + LGraphCanvas.alignNodes( + LGraphCanvas.active_canvas.selected_nodes, + value.toLowerCase(), + node + ); + } + }; + + LGraphCanvas.onGroupAlign = function (value, options, event, prev_menu) { + new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { + event: event, + callback: inner_clicked, + parentMenu: prev_menu, + }); + + function inner_clicked(value) { + LGraphCanvas.alignNodes( + LGraphCanvas.active_canvas.selected_nodes, + value.toLowerCase() + ); + } + }; + + LGraphCanvas.onMenuAdd = function (node, options, e, prev_menu, callback) { + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); + var graph = canvas.graph; + if (!graph) return; + + function inner_onMenuAdded(base_category, prev_menu) { + var categories = LiteGraph.getNodeTypesCategories( + canvas.filter || graph.filter + ).filter(function (category) { + return category.startsWith(base_category); + }); + var entries = []; + + categories.map(function (category) { + if (!category) return; + + var base_category_regex = new RegExp("^(" + base_category + ")"); + var category_name = category + .replace(base_category_regex, "") + .split("/")[0]; + var category_path = + base_category === "" + ? category_name + "/" + : base_category + category_name + "/"; + + var name = category_name; + if (name.indexOf("::") != -1) + //in case it has a namespace like "shader::math/rand" it hides the namespace + name = name.split("::")[1]; + + var index = entries.findIndex(function (entry) { + return entry.value === category_path; + }); + if (index === -1) { + entries.push({ + value: category_path, + content: name, + has_submenu: true, + callback: function (value, event, mouseEvent, contextMenu) { + inner_onMenuAdded(value.value, contextMenu); + }, + }); } + }); - return { - "top": top, - "right": right, - "bottom": bottom, - "left": left + var nodes = LiteGraph.getNodeTypesInCategory( + base_category.slice(0, -1), + canvas.filter || graph.filter + ); + nodes.map(function (node) { + if (node.skip_list) return; + + var entry = { + value: node.type, + content: node.title, + has_submenu: false, + callback: function (value, event, mouseEvent, contextMenu) { + var first_event = contextMenu.getFirstEvent(); + canvas.graph.beforeChange(); + var node = LiteGraph.createNode(value.value); + if (node) { + node.pos = canvas.convertEventToCanvasOffset(first_event); + canvas.graph.add(node); + } + if (callback) callback(node); + canvas.graph.afterChange(); + }, }; - } - /** - * Determines the furthest nodes in each direction for the currently selected nodes - * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} - */ - LGraphCanvas.prototype.boundaryNodesForSelection = function() { - return LGraphCanvas.getBoundaryNodes(Object.values(this.selected_nodes)); + + entries.push(entry); + }); + + new LiteGraph.ContextMenu( + entries, + { event: e, parentMenu: prev_menu }, + ref_window + ); } - /** - * - * @param {LGraphNode[]} nodes a list of nodes - * @param {"top"|"bottom"|"left"|"right"} direction Direction to align the nodes - * @param {LGraphNode?} align_to Node to align to (if null, align to the furthest node in the given direction) - */ - LGraphCanvas.alignNodes = function (nodes, direction, align_to) { - if (!nodes) { - return; - } + inner_onMenuAdded("", prev_menu); + return false; + }; - const canvas = LGraphCanvas.active_canvas; - let boundaryNodes = [] - if (align_to === undefined) { - boundaryNodes = LGraphCanvas.getBoundaryNodes(nodes) - } else { - boundaryNodes = { - "top": align_to, - "right": align_to, - "bottom": align_to, - "left": align_to - } - } + LGraphCanvas.onMenuCollapseAll = function () {}; - for (const [_, node] of Object.entries(canvas.selected_nodes)) { - switch (direction) { - case "right": - node.pos[0] = boundaryNodes["right"].pos[0] + boundaryNodes["right"].size[0] - node.size[0]; - break; - case "left": - node.pos[0] = boundaryNodes["left"].pos[0]; - break; - case "top": - node.pos[1] = boundaryNodes["top"].pos[1]; - break; - case "bottom": - node.pos[1] = boundaryNodes["bottom"].pos[1] + boundaryNodes["bottom"].size[1] - node.size[1]; - break; - } - } + LGraphCanvas.onMenuNodeEdit = function () {}; - canvas.dirty_canvas = true; - canvas.dirty_bgcanvas = true; - }; - - LGraphCanvas.onNodeAlign = function(value, options, event, prev_menu, node) { - new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { - event: event, - callback: inner_clicked, - parentMenu: prev_menu, - }); - - function inner_clicked(value) { - LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase(), node); - } + LGraphCanvas.showMenuNodeOptionalInputs = function ( + v, + options, + e, + prev_menu, + node + ) { + if (!node) { + return; } - LGraphCanvas.onGroupAlign = function(value, options, event, prev_menu) { - new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { - event: event, - callback: inner_clicked, - parentMenu: prev_menu, - }); + var that = this; + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); - function inner_clicked(value) { - LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase()); - } + var options = node.optional_inputs; + if (node.onGetInputs) { + options = node.onGetInputs(); } - LGraphCanvas.onMenuAdd = function (node, options, e, prev_menu, callback) { - - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - var graph = canvas.graph; - if (!graph) - return; - - function inner_onMenuAdded(base_category ,prev_menu){ - - var categories = LiteGraph.getNodeTypesCategories(canvas.filter || graph.filter).filter(function(category){return category.startsWith(base_category)}); - var entries = []; - - categories.map(function(category){ - - if (!category) - return; - - var base_category_regex = new RegExp('^(' + base_category + ')'); - var category_name = category.replace(base_category_regex,"").split('/')[0]; - var category_path = base_category === '' ? category_name + '/' : base_category + category_name + '/'; - - var name = category_name; - if(name.indexOf("::") != -1) //in case it has a namespace like "shader::math/rand" it hides the namespace - name = name.split("::")[1]; - - var index = entries.findIndex(function(entry){return entry.value === category_path}); - if (index === -1) { - entries.push({ value: category_path, content: name, has_submenu: true, callback : function(value, event, mouseEvent, contextMenu){ - inner_onMenuAdded(value.value, contextMenu) - }}); - } - - }); - - var nodes = LiteGraph.getNodeTypesInCategory(base_category.slice(0, -1), canvas.filter || graph.filter ); - nodes.map(function(node){ - - if (node.skip_list) - return; - - var entry = { value: node.type, content: node.title, has_submenu: false , callback : function(value, event, mouseEvent, contextMenu){ - - var first_event = contextMenu.getFirstEvent(); - canvas.graph.beforeChange(); - var node = LiteGraph.createNode(value.value); - if (node) { - node.pos = canvas.convertEventToCanvasOffset(first_event); - canvas.graph.add(node); - } - if(callback) - callback(node); - canvas.graph.afterChange(); - - } - } - - entries.push(entry); - - }); - - new LiteGraph.ContextMenu( entries, { event: e, parentMenu: prev_menu }, ref_window ); - + var entries = []; + if (options) { + for (var i = 0; i < options.length; i++) { + var entry = options[i]; + if (!entry) { + entries.push(null); + continue; } - - inner_onMenuAdded('',prev_menu); - return false; - - }; + var label = entry[0]; + if (!entry[2]) entry[2] = {}; - LGraphCanvas.onMenuCollapseAll = function() {}; - - LGraphCanvas.onMenuNodeEdit = function() {}; - - LGraphCanvas.showMenuNodeOptionalInputs = function( - v, - options, - e, - prev_menu, - node - ) { - if (!node) { - return; + if (entry[2].label) { + label = entry[2].label; } - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var options = node.optional_inputs; - if (node.onGetInputs) { - options = node.onGetInputs(); + entry[2].removable = true; + var data = { content: label, value: entry }; + if (entry[1] == LiteGraph.ACTION) { + data.className = "event"; } + entries.push(data); + } + } - var entries = []; - if (options) { - for (var i=0; i < options.length; i++) { - var entry = options[i]; - if (!entry) { - entries.push(null); - continue; - } - var label = entry[0]; - if(!entry[2]) - entry[2] = {}; + if (node.onMenuNodeInputs) { + var retEntries = node.onMenuNodeInputs(entries); + if (retEntries) entries = retEntries; + } - if (entry[2].label) { - label = entry[2].label; - } + if (!entries.length) { + console.log("no input entries"); + return; + } - entry[2].removable = true; - var data = { content: label, value: entry }; - if (entry[1] == LiteGraph.ACTION) { - data.className = "event"; - } - entries.push(data); - } + var menu = new LiteGraph.ContextMenu( + entries, + { + event: e, + callback: inner_clicked, + parentMenu: prev_menu, + node: node, + }, + ref_window + ); + + function inner_clicked(v, e, prev) { + if (!node) { + return; + } + + if (v.callback) { + v.callback.call(that, node, v, e, prev); + } + + if (v.value) { + node.graph.beforeChange(); + node.addInput(v.value[0], v.value[1], v.value[2]); + + if (node.onNodeInputAdd) { + // callback to the node when adding a slot + node.onNodeInputAdd(v.value); } - - if (node.onMenuNodeInputs) { - var retEntries = node.onMenuNodeInputs(entries); - if(retEntries) entries = retEntries; - } - - if (!entries.length) { - console.log("no input entries"); - return; - } - - var menu = new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node - }, - ref_window - ); - - function inner_clicked(v, e, prev) { - if (!node) { - return; - } - - if (v.callback) { - v.callback.call(that, node, v, e, prev); - } - - if (v.value) { - node.graph.beforeChange(); - node.addInput(v.value[0], v.value[1], v.value[2]); - - if (node.onNodeInputAdd) { // callback to the node when adding a slot - node.onNodeInputAdd(v.value); - } - node.setDirtyCanvas(true, true); - node.graph.afterChange(); - } - } - - return false; - }; - - LGraphCanvas.showMenuNodeOptionalOutputs = function( - v, - options, - e, - prev_menu, - node - ) { - if (!node) { - return; - } - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var options = node.optional_outputs; - if (node.onGetOutputs) { - options = node.onGetOutputs(); - } - - var entries = []; - if (options) { - for (var i=0; i < options.length; i++) { - var entry = options[i]; - if (!entry) { - //separator? - entries.push(null); - continue; - } - - if ( - node.flags && - node.flags.skip_repeated_outputs && - node.findOutputSlot(entry[0]) != -1 - ) { - continue; - } //skip the ones already on - var label = entry[0]; - if(!entry[2]) - entry[2] = {}; - if (entry[2].label) { - label = entry[2].label; - } - entry[2].removable = true; - var data = { content: label, value: entry }; - if (entry[1] == LiteGraph.EVENT) { - data.className = "event"; - } - entries.push(data); - } - } - - if (this.onMenuNodeOutputs) { - entries = this.onMenuNodeOutputs(entries); - } - if (LiteGraph.do_add_triggers_slots){ //canvas.allow_addOutSlot_onExecuted - if (node.findOutputSlot("onExecuted") == -1){ - entries.push({content: "On Executed", value: ["onExecuted", LiteGraph.EVENT, {nameLocked: true}], className: "event"}); //, opts: {} - } - } - // add callback for modifing the menu elements onMenuNodeOutputs - if (node.onMenuNodeOutputs) { - var retEntries = node.onMenuNodeOutputs(entries); - if(retEntries) entries = retEntries; - } - - if (!entries.length) { - return; - } - - var menu = new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node - }, - ref_window - ); - - function inner_clicked(v, e, prev) { - if (!node) { - return; - } - - if (v.callback) { - v.callback.call(that, node, v, e, prev); - } - - if (!v.value) { - return; - } - - var value = v.value[1]; - - if ( - value && - (value.constructor === Object || value.constructor === Array) - ) { - //submenu why? - var entries = []; - for (var i in value) { - entries.push({ content: i, value: value[i] }); - } - new LiteGraph.ContextMenu(entries, { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node - }); - return false; - } else { - node.graph.beforeChange(); - node.addOutput(v.value[0], v.value[1], v.value[2]); - - if (node.onNodeOutputAdd) { // a callback to the node when adding a slot - node.onNodeOutputAdd(v.value); - } - node.setDirtyCanvas(true, true); - node.graph.afterChange(); - } - } - - return false; - }; - - LGraphCanvas.onShowMenuNodeProperties = function( - value, - options, - e, - prev_menu, - node - ) { - if (!node || !node.properties) { - return; - } - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var entries = []; - for (var i in node.properties) { - var value = node.properties[i] !== undefined ? node.properties[i] : " "; - if( typeof value == "object" ) - value = JSON.stringify(value); - var info = node.getPropertyInfo(i); - if(info.type == "enum" || info.type == "combo") - value = LGraphCanvas.getPropertyPrintableValue( value, info.values ); - - //value could contain invalid html characters, clean that - value = LGraphCanvas.decodeHTML(value); - entries.push({ - content: - "" + - (info.label ? info.label : i) + - "" + - "" + - value + - "", - value: i - }); - } - if (!entries.length) { - return; - } - - var menu = new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - allow_html: true, - node: node - }, - ref_window - ); - - function inner_clicked(v, options, e, prev) { - if (!node) { - return; - } - var rect = this.getBoundingClientRect(); - canvas.showEditPropertyValue(node, v.value, { - position: [rect.left, rect.top] - }); - } - - return false; - }; - - LGraphCanvas.decodeHTML = function(str) { - var e = document.createElement("div"); - e.innerText = str; - return e.innerHTML; - }; - - LGraphCanvas.onMenuResizeNode = function(value, options, e, menu, node) { - if (!node) { - return; - } - - var fApplyMultiNode = function(node){ - node.size = node.computeSize(); - if (node.onResize) - node.onResize(node.size); - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - node.setDirtyCanvas(true, true); + node.graph.afterChange(); + } + } + + return false; + }; + + LGraphCanvas.showMenuNodeOptionalOutputs = function ( + v, + options, + e, + prev_menu, + node + ) { + if (!node) { + return; + } + + var that = this; + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); + + var options = node.optional_outputs; + if (node.onGetOutputs) { + options = node.onGetOutputs(); + } + + var entries = []; + if (options) { + for (var i = 0; i < options.length; i++) { + var entry = options[i]; + if (!entry) { + //separator? + entries.push(null); + continue; + } + + if ( + node.flags && + node.flags.skip_repeated_outputs && + node.findOutputSlot(entry[0]) != -1 + ) { + continue; + } //skip the ones already on + var label = entry[0]; + if (!entry[2]) entry[2] = {}; + if (entry[2].label) { + label = entry[2].label; + } + entry[2].removable = true; + var data = { content: label, value: entry }; + if (entry[1] == LiteGraph.EVENT) { + data.className = "event"; + } + entries.push(data); + } + } + + if (this.onMenuNodeOutputs) { + entries = this.onMenuNodeOutputs(entries); + } + if (LiteGraph.do_add_triggers_slots) { + //canvas.allow_addOutSlot_onExecuted + if (node.findOutputSlot("onExecuted") == -1) { + entries.push({ + content: "On Executed", + value: ["onExecuted", LiteGraph.EVENT, { nameLocked: true }], + className: "event", + }); //, opts: {} + } + } + // add callback for modifing the menu elements onMenuNodeOutputs + if (node.onMenuNodeOutputs) { + var retEntries = node.onMenuNodeOutputs(entries); + if (retEntries) entries = retEntries; + } + + if (!entries.length) { + return; + } + + var menu = new LiteGraph.ContextMenu( + entries, + { + event: e, + callback: inner_clicked, + parentMenu: prev_menu, + node: node, + }, + ref_window + ); + + function inner_clicked(v, e, prev) { + if (!node) { + return; + } + + if (v.callback) { + v.callback.call(that, node, v, e, prev); + } + + if (!v.value) { + return; + } + + var value = v.value[1]; + + if ( + value && + (value.constructor === Object || value.constructor === Array) + ) { + //submenu why? + var entries = []; + for (var i in value) { + entries.push({ content: i, value: value[i] }); + } + new LiteGraph.ContextMenu(entries, { + event: e, + callback: inner_clicked, + parentMenu: prev_menu, + node: node, + }); + return false; + } else { + node.graph.beforeChange(); + node.addOutput(v.value[0], v.value[1], v.value[2]); + + if (node.onNodeOutputAdd) { + // a callback to the node when adding a slot + node.onNodeOutputAdd(v.value); + } + node.setDirtyCanvas(true, true); + node.graph.afterChange(); + } + } + + return false; + }; + + LGraphCanvas.onShowMenuNodeProperties = function ( + value, + options, + e, + prev_menu, + node + ) { + if (!node || !node.properties) { + return; + } + + var that = this; + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); + + var entries = []; + for (var i in node.properties) { + var value = node.properties[i] !== undefined ? node.properties[i] : " "; + if (typeof value == "object") value = JSON.stringify(value); + var info = node.getPropertyInfo(i); + if (info.type == "enum" || info.type == "combo") + value = LGraphCanvas.getPropertyPrintableValue(value, info.values); + + //value could contain invalid html characters, clean that + value = LGraphCanvas.decodeHTML(value); + entries.push({ + content: + "" + + (info.label ? info.label : i) + + "" + + "" + + value + + "", + value: i, + }); + } + if (!entries.length) { + return; + } + + var menu = new LiteGraph.ContextMenu( + entries, + { + event: e, + callback: inner_clicked, + parentMenu: prev_menu, + allow_html: true, + node: node, + }, + ref_window + ); + + function inner_clicked(v, options, e, prev) { + if (!node) { + return; + } + var rect = this.getBoundingClientRect(); + canvas.showEditPropertyValue(node, v.value, { + position: [rect.left, rect.top], + }); + } + + return false; + }; + + LGraphCanvas.decodeHTML = function (str) { + var e = document.createElement("div"); + e.innerText = str; + return e.innerHTML; + }; + + LGraphCanvas.onMenuResizeNode = function (value, options, e, menu, node) { + if (!node) { + return; + } + + var fApplyMultiNode = function (node) { + node.size = node.computeSize(); + if (node.onResize) node.onResize(node.size); }; - LGraphCanvas.prototype.showLinkMenu = function(link, e) { - var that = this; - // console.log(link); - var node_left = that.graph.getNodeById( link.origin_id ); - var node_right = that.graph.getNodeById( link.target_id ); - var fromType = false; - if (node_left && node_left.outputs && node_left.outputs[link.origin_slot]) fromType = node_left.outputs[link.origin_slot].type; - var destType = false; - if (node_right && node_right.outputs && node_right.outputs[link.target_slot]) destType = node_right.inputs[link.target_slot].type; - - var options = ["Add Node",null,"Delete",null]; - - - var menu = new LiteGraph.ContextMenu(options, { - event: e, - title: link.data != null ? link.data.constructor.name : null, - callback: inner_clicked - }); + var graphcanvas = LGraphCanvas.active_canvas; + if ( + !graphcanvas.selected_nodes || + Object.keys(graphcanvas.selected_nodes).length <= 1 + ) { + fApplyMultiNode(node); + } else { + for (var i in graphcanvas.selected_nodes) { + fApplyMultiNode(graphcanvas.selected_nodes[i]); + } + } - function inner_clicked(v,options,e) { - switch (v) { - case "Add Node": - LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){ - // console.debug("node autoconnect"); - if(!node.inputs || !node.inputs.length || !node.outputs || !node.outputs.length){ - return; - } - // leave the connection type checking inside connectByType - if (node_left.connectByType( link.origin_slot, node, fromType )){ - node.connectByType( link.target_slot, node_right, destType ); - node.pos[0] -= node.size[0] * 0.5; - } - }); - break; - - case "Delete": - that.graph.removeLink(link.id); - break; - default: - /*var nodeCreated = createDefaultNodeForSlot({ nodeFrom: node_left + node.setDirtyCanvas(true, true); + }; + + LGraphCanvas.prototype.showLinkMenu = function (link, e) { + var that = this; + // console.log(link); + var node_left = that.graph.getNodeById(link.origin_id); + var node_right = that.graph.getNodeById(link.target_id); + var fromType = false; + if (node_left && node_left.outputs && node_left.outputs[link.origin_slot]) + fromType = node_left.outputs[link.origin_slot].type; + var destType = false; + if ( + node_right && + node_right.outputs && + node_right.outputs[link.target_slot] + ) + destType = node_right.inputs[link.target_slot].type; + + var options = ["Add Node", null, "Delete", null]; + + var menu = new LiteGraph.ContextMenu(options, { + event: e, + title: link.data != null ? link.data.constructor.name : null, + callback: inner_clicked, + }); + + function inner_clicked(v, options, e) { + switch (v) { + case "Add Node": + LGraphCanvas.onMenuAdd(null, null, e, menu, function (node) { + // console.debug("node autoconnect"); + if ( + !node.inputs || + !node.inputs.length || + !node.outputs || + !node.outputs.length + ) { + return; + } + // leave the connection type checking inside connectByType + if (node_left.connectByType(link.origin_slot, node, fromType)) { + node.connectByType(link.target_slot, node_right, destType); + node.pos[0] -= node.size[0] * 0.5; + } + }); + break; + + case "Delete": + that.graph.removeLink(link.id); + break; + default: + /*var nodeCreated = createDefaultNodeForSlot({ nodeFrom: node_left ,slotFrom: link.origin_slot ,nodeTo: node ,slotTo: link.target_slot @@ -10931,712 +11150,807 @@ LGraphNode.prototype.executeAction = function(action) ,nodeType: "AUTO" }); if(nodeCreated) console.log("new node in beetween "+v+" created");*/ - } - } + } + } + return false; + }; + + LGraphCanvas.prototype.createDefaultNodeForSlot = function (optPass) { + // addNodeMenu for connection + var optPass = optPass || {}; + var opts = Object.assign( + { + nodeFrom: null, // input + slotFrom: null, // input + nodeTo: null, // output + slotTo: null, // output + position: [], // pass the event coords + nodeType: null, // choose a nodetype to add, AUTO to set at first good + posAdd: [0, 0], // adjust x,y + posSizeFix: [0, 0], // alpha, adjust the position x,y based on the new node size w,h + }, + optPass + ); + var that = this; + + var isFrom = opts.nodeFrom && opts.slotFrom !== null; + var isTo = !isFrom && opts.nodeTo && opts.slotTo !== null; + + if (!isFrom && !isTo) { + console.warn( + "No data passed to createDefaultNodeForSlot " + + opts.nodeFrom + + " " + + opts.slotFrom + + " " + + opts.nodeTo + + " " + + opts.slotTo + ); + return false; + } + if (!opts.nodeType) { + console.warn("No type to createDefaultNodeForSlot"); + return false; + } + + var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; + var slotX = isFrom ? opts.slotFrom : opts.slotTo; + + var iSlotConn = false; + switch (typeof slotX) { + case "string": + iSlotConn = isFrom + ? nodeX.findOutputSlot(slotX, false) + : nodeX.findInputSlot(slotX, false); + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; + break; + case "object": + // ok slotX + iSlotConn = isFrom + ? nodeX.findOutputSlot(slotX.name) + : nodeX.findInputSlot(slotX.name); + break; + case "number": + iSlotConn = slotX; + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; + break; + case "undefined": + default: + // bad ? + //iSlotConn = 0; + console.warn("Cant get slot information " + slotX); return false; - }; - - LGraphCanvas.prototype.createDefaultNodeForSlot = function(optPass) { // addNodeMenu for connection - var optPass = optPass || {}; - var opts = Object.assign({ nodeFrom: null // input - ,slotFrom: null // input - ,nodeTo: null // output - ,slotTo: null // output - ,position: [] // pass the event coords - ,nodeType: null // choose a nodetype to add, AUTO to set at first good - ,posAdd:[0,0] // adjust x,y - ,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h - } - ,optPass - ); - var that = this; - - var isFrom = opts.nodeFrom && opts.slotFrom!==null; - var isTo = !isFrom && opts.nodeTo && opts.slotTo!==null; - - if (!isFrom && !isTo){ - console.warn("No data passed to createDefaultNodeForSlot "+opts.nodeFrom+" "+opts.slotFrom+" "+opts.nodeTo+" "+opts.slotTo); - return false; - } - if (!opts.nodeType){ - console.warn("No type to createDefaultNodeForSlot"); - return false; - } - - var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; - var slotX = isFrom ? opts.slotFrom : opts.slotTo; - - var iSlotConn = false; - switch (typeof slotX){ - case "string": - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false); - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - case "object": - // ok slotX - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name); - break; - case "number": - iSlotConn = slotX; - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - case "undefined": - default: - // bad ? - //iSlotConn = 0; - console.warn("Cant get slot information "+slotX); - return false; - } - - if (slotX===false || iSlotConn===false){ - console.warn("createDefaultNodeForSlot bad slotX "+slotX+" "+iSlotConn); - } - - // check for defaults nodes for this slottype - var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type; - var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in; - if(slotTypesDefault && slotTypesDefault[fromSlotType]){ - if (slotX.link !== null) { - // is connected - }else{ - // is not not connected - } - nodeNewType = false; - if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){ - for(var typeX in slotTypesDefault[fromSlotType]){ - if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == "AUTO"){ - nodeNewType = slotTypesDefault[fromSlotType][typeX]; - // console.log("opts.nodeType == slotTypesDefault[fromSlotType][typeX] :: "+opts.nodeType); - break; // -------- - } - } - }else{ - if (opts.nodeType == slotTypesDefault[fromSlotType] || opts.nodeType == "AUTO") nodeNewType = slotTypesDefault[fromSlotType]; - } - if (nodeNewType) { - var nodeNewOpts = false; - if (typeof nodeNewType == "object" && nodeNewType.node){ - nodeNewOpts = nodeNewType; - nodeNewType = nodeNewType.node; - } - - //that.graph.beforeChange(); - - var newNode = LiteGraph.createNode(nodeNewType); - if(newNode){ - // if is object pass options - if (nodeNewOpts){ - if (nodeNewOpts.properties) { - for (var i in nodeNewOpts.properties) { - newNode.addProperty( i, nodeNewOpts.properties[i] ); - } - } - if (nodeNewOpts.inputs) { - newNode.inputs = []; - for (var i in nodeNewOpts.inputs) { - newNode.addOutput( - nodeNewOpts.inputs[i][0], - nodeNewOpts.inputs[i][1] - ); - } - } - if (nodeNewOpts.outputs) { - newNode.outputs = []; - for (var i in nodeNewOpts.outputs) { - newNode.addOutput( - nodeNewOpts.outputs[i][0], - nodeNewOpts.outputs[i][1] - ); - } - } - if (nodeNewOpts.title) { - newNode.title = nodeNewOpts.title; - } - if (nodeNewOpts.json) { - newNode.configure(nodeNewOpts.json); - } + } - } - - // add the node - that.graph.add(newNode); - newNode.pos = [ opts.position[0]+opts.posAdd[0]+(opts.posSizeFix[0]?opts.posSizeFix[0]*newNode.size[0]:0) - ,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/ - - //that.graph.afterChange(); - - // connect the two! - if (isFrom){ - opts.nodeFrom.connectByType( iSlotConn, newNode, fromSlotType ); - }else{ - opts.nodeTo.connectByTypeOutput( iSlotConn, newNode, fromSlotType ); - } - - // if connecting in between - if (isFrom && isTo){ - // TODO - } - - return true; - - }else{ - console.log("failed creating "+nodeNewType); - } - } - } - return false; - } - - LGraphCanvas.prototype.showConnectionMenu = function(optPass) { // addNodeMenu for connection - var optPass = optPass || {}; - var opts = Object.assign({ nodeFrom: null // input - ,slotFrom: null // input - ,nodeTo: null // output - ,slotTo: null // output - ,e: null - } - ,optPass - ); - var that = this; - - var isFrom = opts.nodeFrom && opts.slotFrom; - var isTo = !isFrom && opts.nodeTo && opts.slotTo; - - if (!isFrom && !isTo){ - console.warn("No data passed to showConnectionMenu"); - return false; + if (slotX === false || iSlotConn === false) { + console.warn( + "createDefaultNodeForSlot bad slotX " + slotX + " " + iSlotConn + ); + } + + // check for defaults nodes for this slottype + var fromSlotType = slotX.type == LiteGraph.EVENT ? "_event_" : slotX.type; + var slotTypesDefault = isFrom + ? LiteGraph.slot_types_default_out + : LiteGraph.slot_types_default_in; + if (slotTypesDefault && slotTypesDefault[fromSlotType]) { + if (slotX.link !== null) { + // is connected + } else { + // is not not connected + } + nodeNewType = false; + if ( + typeof slotTypesDefault[fromSlotType] == "object" || + typeof slotTypesDefault[fromSlotType] == "array" + ) { + for (var typeX in slotTypesDefault[fromSlotType]) { + if ( + opts.nodeType == slotTypesDefault[fromSlotType][typeX] || + opts.nodeType == "AUTO" + ) { + nodeNewType = slotTypesDefault[fromSlotType][typeX]; + // console.log("opts.nodeType == slotTypesDefault[fromSlotType][typeX] :: "+opts.nodeType); + break; // -------- + } } - - var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; - var slotX = isFrom ? opts.slotFrom : opts.slotTo; - - var iSlotConn = false; - switch (typeof slotX){ - case "string": - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false); - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - case "object": - // ok slotX - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name); - break; - case "number": - iSlotConn = slotX; - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - default: - // bad ? - //iSlotConn = 0; - console.warn("Cant get slot information "+slotX); - return false; + } else { + if ( + opts.nodeType == slotTypesDefault[fromSlotType] || + opts.nodeType == "AUTO" + ) + nodeNewType = slotTypesDefault[fromSlotType]; + } + if (nodeNewType) { + var nodeNewOpts = false; + if (typeof nodeNewType == "object" && nodeNewType.node) { + nodeNewOpts = nodeNewType; + nodeNewType = nodeNewType.node; } - - var options = ["Add Node",null]; - - if (that.allow_searchbox){ - options.push("Search"); - options.push(null); - } - - // get defaults nodes for this slottype - var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type; - var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in; - if(slotTypesDefault && slotTypesDefault[fromSlotType]){ - if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){ - for(var typeX in slotTypesDefault[fromSlotType]){ - options.push(slotTypesDefault[fromSlotType][typeX]); - } - }else{ - options.push(slotTypesDefault[fromSlotType]); - } - } - - // build menu - var menu = new LiteGraph.ContextMenu(options, { - event: opts.e, - title: (slotX && slotX.name!="" ? (slotX.name + (fromSlotType?" | ":"")) : "")+(slotX && fromSlotType ? fromSlotType : ""), - callback: inner_clicked - }); - - // callback - function inner_clicked(v,options,e) { - //console.log("Process showConnectionMenu selection"); - switch (v) { - case "Add Node": - LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){ - if (isFrom){ - opts.nodeFrom.connectByType( iSlotConn, node, fromSlotType ); - }else{ - opts.nodeTo.connectByTypeOutput( iSlotConn, node, fromSlotType ); - } - }); - break; - case "Search": - if(isFrom){ - that.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType}); - }else{ - that.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType}); - } - break; - default: - // check for defaults nodes for this slottype - var nodeCreated = that.createDefaultNodeForSlot(Object.assign(opts,{ position: [opts.e.canvasX, opts.e.canvasY] - ,nodeType: v - })); - if (nodeCreated){ - // new node created - //console.log("node "+v+" created") - }else{ - // failed or v is not in defaults - } - break; + + //that.graph.beforeChange(); + + var newNode = LiteGraph.createNode(nodeNewType); + if (newNode) { + // if is object pass options + if (nodeNewOpts) { + if (nodeNewOpts.properties) { + for (var i in nodeNewOpts.properties) { + newNode.addProperty(i, nodeNewOpts.properties[i]); + } } - } - - return false; - }; - - // TODO refactor :: this is used fot title but not for properties! - LGraphCanvas.onShowPropertyEditor = function(item, options, e, menu, node) { - var input_html = ""; - var property = item.property || "title"; - var value = node[property]; - - // TODO refactor :: use createDialog ? - - var dialog = document.createElement("div"); - dialog.is_modified = false; - dialog.className = "graphdialog"; - dialog.innerHTML = - ""; - dialog.close = function() { - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); + if (nodeNewOpts.inputs) { + newNode.inputs = []; + for (var i in nodeNewOpts.inputs) { + newNode.addOutput( + nodeNewOpts.inputs[i][0], + nodeNewOpts.inputs[i][1] + ); + } } - }; - var title = dialog.querySelector(".name"); - title.innerText = property; - var input = dialog.querySelector(".value"); - if (input) { - input.value = value; - input.addEventListener("blur", function(e) { - this.focus(); - }); - input.addEventListener("keydown", function(e) { - dialog.is_modified = true; - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - inner(); // save - } else if (e.keyCode != 13 && e.target.localName != "textarea") { - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - } + if (nodeNewOpts.outputs) { + newNode.outputs = []; + for (var i in nodeNewOpts.outputs) { + newNode.addOutput( + nodeNewOpts.outputs[i][0], + nodeNewOpts.outputs[i][1] + ); + } + } + if (nodeNewOpts.title) { + newNode.title = nodeNewOpts.title; + } + if (nodeNewOpts.json) { + newNode.configure(nodeNewOpts.json); + } + } - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; + // add the node + that.graph.add(newNode); + newNode.pos = [ + opts.position[0] + + opts.posAdd[0] + + (opts.posSizeFix[0] ? opts.posSizeFix[0] * newNode.size[0] : 0), + opts.position[1] + + opts.posAdd[1] + + (opts.posSizeFix[1] ? opts.posSizeFix[1] * newNode.size[1] : 0), + ]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/ - var rect = canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; - } + //that.graph.afterChange(); - if (event) { - dialog.style.left = event.clientX + offsetx + "px"; - dialog.style.top = event.clientY + offsety + "px"; + // connect the two! + if (isFrom) { + opts.nodeFrom.connectByType(iSlotConn, newNode, fromSlotType); + } else { + opts.nodeTo.connectByTypeOutput(iSlotConn, newNode, fromSlotType); + } + + // if connecting in between + if (isFrom && isTo) { + // TODO + } + + return true; } else { - dialog.style.left = canvas.width * 0.5 + offsetx + "px"; - dialog.style.top = canvas.height * 0.5 + offsety + "px"; + console.log("failed creating " + nodeNewType); } + } + } + return false; + }; - var button = dialog.querySelector("button"); - button.addEventListener("click", inner); - canvas.parentNode.appendChild(dialog); + LGraphCanvas.prototype.showConnectionMenu = function (optPass) { + // addNodeMenu for connection + var optPass = optPass || {}; + var opts = Object.assign( + { + nodeFrom: null, // input + slotFrom: null, // input + nodeTo: null, // output + slotTo: null, // output + e: null, + }, + optPass + ); + var that = this; - if(input) input.focus(); - - var dialogCloseTimer = null; - dialog.addEventListener("mouseleave", function(e) { - if(LiteGraph.dialog_close_on_mouse_leave) - if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) - dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); - }); - dialog.addEventListener("mouseenter", function(e) { - if(LiteGraph.dialog_close_on_mouse_leave) - if(dialogCloseTimer) clearTimeout(dialogCloseTimer); - }); - - function inner() { - if(input) setValue(input.value); + var isFrom = opts.nodeFrom && opts.slotFrom; + var isTo = !isFrom && opts.nodeTo && opts.slotTo; + + if (!isFrom && !isTo) { + console.warn("No data passed to showConnectionMenu"); + return false; + } + + var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; + var slotX = isFrom ? opts.slotFrom : opts.slotTo; + + var iSlotConn = false; + switch (typeof slotX) { + case "string": + iSlotConn = isFrom + ? nodeX.findOutputSlot(slotX, false) + : nodeX.findInputSlot(slotX, false); + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; + break; + case "object": + // ok slotX + iSlotConn = isFrom + ? nodeX.findOutputSlot(slotX.name) + : nodeX.findInputSlot(slotX.name); + break; + case "number": + iSlotConn = slotX; + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; + break; + default: + // bad ? + //iSlotConn = 0; + console.warn("Cant get slot information " + slotX); + return false; + } + + var options = ["Add Node", null]; + + if (that.allow_searchbox) { + options.push("Search"); + options.push(null); + } + + // get defaults nodes for this slottype + var fromSlotType = slotX.type == LiteGraph.EVENT ? "_event_" : slotX.type; + var slotTypesDefault = isFrom + ? LiteGraph.slot_types_default_out + : LiteGraph.slot_types_default_in; + if (slotTypesDefault && slotTypesDefault[fromSlotType]) { + if ( + typeof slotTypesDefault[fromSlotType] == "object" || + typeof slotTypesDefault[fromSlotType] == "array" + ) { + for (var typeX in slotTypesDefault[fromSlotType]) { + options.push(slotTypesDefault[fromSlotType][typeX]); } + } else { + options.push(slotTypesDefault[fromSlotType]); + } + } - function setValue(value) { - if (item.type == "Number") { - value = Number(value); - } else if (item.type == "Boolean") { - value = Boolean(value); - } - node[property] = value; - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - node.setDirtyCanvas(true, true); - } - }; + // build menu + var menu = new LiteGraph.ContextMenu(options, { + event: opts.e, + title: + (slotX && slotX.name != "" + ? slotX.name + (fromSlotType ? " | " : "") + : "") + (slotX && fromSlotType ? fromSlotType : ""), + callback: inner_clicked, + }); - // refactor: there are different dialogs, some uses createDialog some dont - LGraphCanvas.prototype.prompt = function(title, value, callback, event, multiline) { - var that = this; - var input_html = ""; - title = title || ""; - - var dialog = document.createElement("div"); - dialog.is_modified = false; - dialog.className = "graphdialog rounded"; - if(multiline) - dialog.innerHTML = " "; - else - dialog.innerHTML = " "; - dialog.close = function() { - that.prompt_box = null; - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; - canvas.parentNode.appendChild(dialog); - - if (this.ds.scale > 1) { - dialog.style.transform = "scale(" + this.ds.scale + ")"; - } - - var dialogCloseTimer = null; - var prevent_timeout = false; - LiteGraph.pointerListenerAdd(dialog,"leave", function(e) { - if (prevent_timeout) - return; - if(LiteGraph.dialog_close_on_mouse_leave) - if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) - dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); - }); - LiteGraph.pointerListenerAdd(dialog,"enter", function(e) { - if(LiteGraph.dialog_close_on_mouse_leave) - if(dialogCloseTimer) clearTimeout(dialogCloseTimer); - }); - var selInDia = dialog.querySelectorAll("select"); - if (selInDia){ - // if filtering, check focus changed to comboboxes and prevent closing - selInDia.forEach(function(selIn) { - selIn.addEventListener("click", function(e) { - prevent_timeout++; - }); - selIn.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selIn.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - }); - } - - if (that.prompt_box) { - that.prompt_box.close(); - } - that.prompt_box = dialog; - - var first = null; - var timeout = null; - var selected = null; - - var name_element = dialog.querySelector(".name"); - name_element.innerText = title; - var value_element = dialog.querySelector(".value"); - value_element.value = value; - value_element.select(); - - var input = value_element; - input.addEventListener("keydown", function(e) { - dialog.is_modified = true; - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13 && e.target.localName != "textarea") { - if (callback) { - callback(this.value); - } - dialog.close(); + // callback + function inner_clicked(v, options, e) { + //console.log("Process showConnectionMenu selection"); + switch (v) { + case "Add Node": + LGraphCanvas.onMenuAdd(null, null, e, menu, function (node) { + if (isFrom) { + opts.nodeFrom.connectByType(iSlotConn, node, fromSlotType); } else { - return; + opts.nodeTo.connectByTypeOutput(iSlotConn, node, fromSlotType); } - e.preventDefault(); - e.stopPropagation(); - }); + }); + break; + case "Search": + if (isFrom) { + that.showSearchBox(e, { + node_from: opts.nodeFrom, + slot_from: slotX, + type_filter_in: fromSlotType, + }); + } else { + that.showSearchBox(e, { + node_to: opts.nodeTo, + slot_from: slotX, + type_filter_out: fromSlotType, + }); + } + break; + default: + // check for defaults nodes for this slottype + var nodeCreated = that.createDefaultNodeForSlot( + Object.assign(opts, { + position: [opts.e.canvasX, opts.e.canvasY], + nodeType: v, + }) + ); + if (nodeCreated) { + // new node created + //console.log("node "+v+" created") + } else { + // failed or v is not in defaults + } + break; + } + } - var button = dialog.querySelector("button"); - button.addEventListener("click", function(e) { - if (callback) { - callback(input.value); - } - that.setDirty(true); - dialog.close(); - }); + return false; + }; - var rect = canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; + // TODO refactor :: this is used fot title but not for properties! + LGraphCanvas.onShowPropertyEditor = function (item, options, e, menu, node) { + var input_html = ""; + var property = item.property || "title"; + var value = node[property]; + + // TODO refactor :: use createDialog ? + + var dialog = document.createElement("div"); + dialog.is_modified = false; + dialog.className = "graphdialog"; + dialog.innerHTML = + ""; + dialog.close = function () { + if (dialog.parentNode) { + dialog.parentNode.removeChild(dialog); + } + }; + var title = dialog.querySelector(".name"); + title.innerText = property; + var input = dialog.querySelector(".value"); + if (input) { + input.value = value; + input.addEventListener("blur", function (e) { + this.focus(); + }); + input.addEventListener("keydown", function (e) { + dialog.is_modified = true; + if (e.keyCode == 27) { + //ESC + dialog.close(); + } else if (e.keyCode == 13) { + inner(); // save + } else if (e.keyCode != 13 && e.target.localName != "textarea") { + return; } + e.preventDefault(); + e.stopPropagation(); + }); + } - if (event) { - dialog.style.left = event.clientX + offsetx + "px"; - dialog.style.top = event.clientY + offsety + "px"; - } else { - dialog.style.left = canvas.width * 0.5 + offsetx + "px"; - dialog.style.top = canvas.height * 0.5 + offsety + "px"; - } + var graphcanvas = LGraphCanvas.active_canvas; + var canvas = graphcanvas.canvas; - setTimeout(function() { - input.focus(); - }, 10); + var rect = canvas.getBoundingClientRect(); + var offsetx = -20; + var offsety = -20; + if (rect) { + offsetx -= rect.left; + offsety -= rect.top; + } - return dialog; + if (event) { + dialog.style.left = event.clientX + offsetx + "px"; + dialog.style.top = event.clientY + offsety + "px"; + } else { + dialog.style.left = canvas.width * 0.5 + offsetx + "px"; + dialog.style.top = canvas.height * 0.5 + offsety + "px"; + } + + var button = dialog.querySelector("button"); + button.addEventListener("click", inner); + canvas.parentNode.appendChild(dialog); + + if (input) input.focus(); + + var dialogCloseTimer = null; + dialog.addEventListener("mouseleave", function (e) { + if (LiteGraph.dialog_close_on_mouse_leave) + if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) + dialogCloseTimer = setTimeout( + dialog.close, + LiteGraph.dialog_close_on_mouse_leave_delay + ); //dialog.close(); + }); + dialog.addEventListener("mouseenter", function (e) { + if (LiteGraph.dialog_close_on_mouse_leave) + if (dialogCloseTimer) clearTimeout(dialogCloseTimer); + }); + + function inner() { + if (input) setValue(input.value); + } + + function setValue(value) { + if (item.type == "Number") { + value = Number(value); + } else if (item.type == "Boolean") { + value = Boolean(value); + } + node[property] = value; + if (dialog.parentNode) { + dialog.parentNode.removeChild(dialog); + } + node.setDirtyCanvas(true, true); + } + }; + + // refactor: there are different dialogs, some uses createDialog some dont + LGraphCanvas.prototype.prompt = function ( + title, + value, + callback, + event, + multiline + ) { + var that = this; + var input_html = ""; + title = title || ""; + + var dialog = document.createElement("div"); + dialog.is_modified = false; + dialog.className = "graphdialog rounded"; + if (multiline) + dialog.innerHTML = + " "; + else + dialog.innerHTML = + " "; + dialog.close = function () { + that.prompt_box = null; + if (dialog.parentNode) { + dialog.parentNode.removeChild(dialog); + } }; - LGraphCanvas.search_limit = -1; - LGraphCanvas.prototype.showSearchBox = function(event, options) { - // proposed defaults - var def_options = { slot_from: null - ,node_from: null - ,node_to: null - ,do_type_filter: LiteGraph.search_filter_enabled // TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out - ,type_filter_in: false // these are default: pass to set initially set values - ,type_filter_out: false - ,show_general_if_none_on_typefilter: true - ,show_general_after_typefiltered: true - ,hide_on_mouse_leave: LiteGraph.search_hide_on_mouse_leave - ,show_all_if_empty: true - ,show_all_on_open: LiteGraph.search_show_all_on_open - }; - options = Object.assign(def_options, options || {}); - - //console.log(options); - - var that = this; - var input_html = ""; - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; - var root_document = canvas.ownerDocument || document; + var graphcanvas = LGraphCanvas.active_canvas; + var canvas = graphcanvas.canvas; + canvas.parentNode.appendChild(dialog); - var dialog = document.createElement("div"); - dialog.className = "litegraph litesearchbox graphdialog rounded"; - dialog.innerHTML = "Search "; - if (options.do_type_filter){ - dialog.innerHTML += ""; - dialog.innerHTML += ""; + if (this.ds.scale > 1) { + dialog.style.transform = "scale(" + this.ds.scale + ")"; + } + + var dialogCloseTimer = null; + var prevent_timeout = false; + LiteGraph.pointerListenerAdd(dialog, "leave", function (e) { + if (prevent_timeout) return; + if (LiteGraph.dialog_close_on_mouse_leave) + if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) + dialogCloseTimer = setTimeout( + dialog.close, + LiteGraph.dialog_close_on_mouse_leave_delay + ); //dialog.close(); + }); + LiteGraph.pointerListenerAdd(dialog, "enter", function (e) { + if (LiteGraph.dialog_close_on_mouse_leave) + if (dialogCloseTimer) clearTimeout(dialogCloseTimer); + }); + var selInDia = dialog.querySelectorAll("select"); + if (selInDia) { + // if filtering, check focus changed to comboboxes and prevent closing + selInDia.forEach(function (selIn) { + selIn.addEventListener("click", function (e) { + prevent_timeout++; + }); + selIn.addEventListener("blur", function (e) { + prevent_timeout = 0; + }); + selIn.addEventListener("change", function (e) { + prevent_timeout = -1; + }); + }); + } + + if (that.prompt_box) { + that.prompt_box.close(); + } + that.prompt_box = dialog; + + var first = null; + var timeout = null; + var selected = null; + + var name_element = dialog.querySelector(".name"); + name_element.innerText = title; + var value_element = dialog.querySelector(".value"); + value_element.value = value; + value_element.select(); + + var input = value_element; + input.addEventListener("keydown", function (e) { + dialog.is_modified = true; + if (e.keyCode == 27) { + //ESC + dialog.close(); + } else if (e.keyCode == 13 && e.target.localName != "textarea") { + if (callback) { + callback(this.value); } - dialog.innerHTML += "
"; - - if( root_document.fullscreenElement ) - root_document.fullscreenElement.appendChild(dialog); - else - { - root_document.body.appendChild(dialog); - root_document.body.style.overflow = "hidden"; - } - // dialog element has been appended - - if (options.do_type_filter){ - var selIn = dialog.querySelector(".slot_in_type_filter"); - var selOut = dialog.querySelector(".slot_out_type_filter"); + dialog.close(); + } else { + return; + } + e.preventDefault(); + e.stopPropagation(); + }); + + var button = dialog.querySelector("button"); + button.addEventListener("click", function (e) { + if (callback) { + callback(input.value); + } + that.setDirty(true); + dialog.close(); + }); + + var rect = canvas.getBoundingClientRect(); + var offsetx = -20; + var offsety = -20; + if (rect) { + offsetx -= rect.left; + offsety -= rect.top; + } + + if (event) { + dialog.style.left = event.clientX + offsetx + "px"; + dialog.style.top = event.clientY + offsety + "px"; + } else { + dialog.style.left = canvas.width * 0.5 + offsetx + "px"; + dialog.style.top = canvas.height * 0.5 + offsety + "px"; + } + + setTimeout(function () { + input.focus(); + }, 10); + + return dialog; + }; + + LGraphCanvas.search_limit = -1; + LGraphCanvas.prototype.showSearchBox = function (event, options) { + // proposed defaults + var def_options = { + slot_from: null, + node_from: null, + node_to: null, + do_type_filter: LiteGraph.search_filter_enabled, // TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out + type_filter_in: false, // these are default: pass to set initially set values + type_filter_out: false, + show_general_if_none_on_typefilter: true, + show_general_after_typefiltered: true, + hide_on_mouse_leave: LiteGraph.search_hide_on_mouse_leave, + show_all_if_empty: true, + show_all_on_open: LiteGraph.search_show_all_on_open, + }; + options = Object.assign(def_options, options || {}); + + //console.log(options); + + var that = this; + var input_html = ""; + var graphcanvas = LGraphCanvas.active_canvas; + var canvas = graphcanvas.canvas; + var root_document = canvas.ownerDocument || document; + + var dialog = document.createElement("div"); + dialog.className = "litegraph litesearchbox graphdialog rounded"; + dialog.innerHTML = + "Search "; + if (options.do_type_filter) { + dialog.innerHTML += + ""; + dialog.innerHTML += + ""; + } + dialog.innerHTML += "
"; + + if (root_document.fullscreenElement) + root_document.fullscreenElement.appendChild(dialog); + else { + root_document.body.appendChild(dialog); + root_document.body.style.overflow = "hidden"; + } + // dialog element has been appended + + if (options.do_type_filter) { + var selIn = dialog.querySelector(".slot_in_type_filter"); + var selOut = dialog.querySelector(".slot_out_type_filter"); + } + + dialog.close = function () { + that.search_box = null; + this.blur(); + canvas.focus(); + root_document.body.style.overflow = ""; + + setTimeout(function () { + that.canvas.focus(); + }, 20); //important, if canvas loses focus keys wont be captured + if (dialog.parentNode) { + dialog.parentNode.removeChild(dialog); + } + }; + + if (this.ds.scale > 1) { + dialog.style.transform = "scale(" + this.ds.scale + ")"; + } + + // hide on mouse leave + if (options.hide_on_mouse_leave) { + var prevent_timeout = false; + var timeout_close = null; + LiteGraph.pointerListenerAdd(dialog, "enter", function (e) { + if (timeout_close) { + clearTimeout(timeout_close); + timeout_close = null; } - - dialog.close = function() { - that.search_box = null; - this.blur(); - canvas.focus(); - root_document.body.style.overflow = ""; - - setTimeout(function() { - that.canvas.focus(); - }, 20); //important, if canvas loses focus keys wont be captured - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - - if (this.ds.scale > 1) { - dialog.style.transform = "scale(" + this.ds.scale + ")"; + }); + LiteGraph.pointerListenerAdd(dialog, "leave", function (e) { + if (prevent_timeout) { + return; } + timeout_close = setTimeout( + function () { + dialog.close(); + }, + typeof options.hide_on_mouse_leave === "number" + ? options.hide_on_mouse_leave + : 500 + ); + }); + // if filtering, check focus changed to comboboxes and prevent closing + if (options.do_type_filter) { + selIn.addEventListener("click", function (e) { + prevent_timeout++; + }); + selIn.addEventListener("blur", function (e) { + prevent_timeout = 0; + }); + selIn.addEventListener("change", function (e) { + prevent_timeout = -1; + }); + selOut.addEventListener("click", function (e) { + prevent_timeout++; + }); + selOut.addEventListener("blur", function (e) { + prevent_timeout = 0; + }); + selOut.addEventListener("change", function (e) { + prevent_timeout = -1; + }); + } + } - // hide on mouse leave - if(options.hide_on_mouse_leave){ - var prevent_timeout = false; - var timeout_close = null; - LiteGraph.pointerListenerAdd(dialog,"enter", function(e) { - if (timeout_close) { - clearTimeout(timeout_close); - timeout_close = null; - } - }); - LiteGraph.pointerListenerAdd(dialog,"leave", function(e) { - if (prevent_timeout){ - return; - } - timeout_close = setTimeout(function() { - dialog.close(); - }, typeof options.hide_on_mouse_leave === "number" ? options.hide_on_mouse_leave : 500); - }); - // if filtering, check focus changed to comboboxes and prevent closing - if (options.do_type_filter){ - selIn.addEventListener("click", function(e) { - prevent_timeout++; - }); - selIn.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selIn.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - selOut.addEventListener("click", function(e) { - prevent_timeout++; - }); - selOut.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selOut.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - } + if (that.search_box) { + that.search_box.close(); + } + that.search_box = dialog; + + var helper = dialog.querySelector(".helper"); + + var first = null; + var timeout = null; + var selected = null; + + var input = dialog.querySelector("input"); + if (input) { + input.addEventListener("blur", function (e) { + this.focus(); + }); + input.addEventListener("keydown", function (e) { + if (e.keyCode == 38) { + //UP + changeSelection(false); + } else if (e.keyCode == 40) { + //DOWN + changeSelection(true); + } else if (e.keyCode == 27) { + //ESC + dialog.close(); + } else if (e.keyCode == 13) { + if (selected) { + select(unescape(selected.dataset["type"])); + } else if (first) { + select(first); + } else { + dialog.close(); + } + } else { + if (timeout) { + clearInterval(timeout); + } + timeout = setTimeout(refreshHelper, 10); + return; } + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + return true; + }); + } - if (that.search_box) { - that.search_box.close(); - } - that.search_box = dialog; + // if should filter on type, load and fill selected and choose elements if passed + if (options.do_type_filter) { + if (selIn) { + var aSlots = LiteGraph.slot_types_in; + var nSlots = aSlots.length; // this for object :: Object.keys(aSlots).length; - var helper = dialog.querySelector(".helper"); - - var first = null; - var timeout = null; - var selected = null; - - var input = dialog.querySelector("input"); - if (input) { - input.addEventListener("blur", function(e) { - this.focus(); - }); - input.addEventListener("keydown", function(e) { - if (e.keyCode == 38) { - //UP - changeSelection(false); - } else if (e.keyCode == 40) { - //DOWN - changeSelection(true); - } else if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - if (selected) { - select(unescape(selected.dataset["type"])); - } else if (first) { - select(first); - } else { - dialog.close(); - } - } else { - if (timeout) { - clearInterval(timeout); - } - timeout = setTimeout(refreshHelper, 10); - return; - } - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - return true; - }); - } - - // if should filter on type, load and fill selected and choose elements if passed - if (options.do_type_filter){ - if (selIn){ - var aSlots = LiteGraph.slot_types_in; - var nSlots = aSlots.length; // this for object :: Object.keys(aSlots).length; - - if (options.type_filter_in == LiteGraph.EVENT || options.type_filter_in == LiteGraph.ACTION) - options.type_filter_in = "_event_"; - /* this will filter on * .. but better do it manually in case + if ( + options.type_filter_in == LiteGraph.EVENT || + options.type_filter_in == LiteGraph.ACTION + ) + options.type_filter_in = "_event_"; + /* this will filter on * .. but better do it manually in case else if(options.type_filter_in === "" || options.type_filter_in === 0) options.type_filter_in = "*";*/ - - for (var iK=0; iK (rect.height - 200)) - helper.style.maxHeight = (rect.height - event.layerY - 20) + "px"; + var left = (event ? event.clientX : rect.left + rect.width * 0.5) - 80; + var top = (event ? event.clientY : rect.top + rect.height * 0.5) - 20; + dialog.style.left = left + "px"; + dialog.style.top = top + "px"; - /* + //To avoid out of screen problems + if (event.layerY > rect.height - 200) + helper.style.maxHeight = rect.height - event.layerY - 20 + "px"; + + /* var offsetx = -20; var offsety = -20; if (rect) { @@ -11654,845 +11968,904 @@ LGraphNode.prototype.executeAction = function(action) canvas.parentNode.appendChild(dialog); */ - input.focus(); - if (options.show_all_on_open) refreshHelper(); + input.focus(); + if (options.show_all_on_open) refreshHelper(); - function select(name) { - if (name) { - if (that.onSearchBoxSelection) { - that.onSearchBoxSelection(name, event, graphcanvas); - } else { - var extra = LiteGraph.searchbox_extras[name.toLowerCase()]; - if (extra) { - name = extra.type; - } - - graphcanvas.graph.beforeChange(); - var node = LiteGraph.createNode(name); - if (node) { - node.pos = graphcanvas.convertEventToCanvasOffset( - event - ); - graphcanvas.graph.add(node, false); - } - - if (extra && extra.data) { - if (extra.data.properties) { - for (var i in extra.data.properties) { - node.addProperty( i, extra.data.properties[i] ); - } - } - if (extra.data.inputs) { - node.inputs = []; - for (var i in extra.data.inputs) { - node.addOutput( - extra.data.inputs[i][0], - extra.data.inputs[i][1] - ); - } - } - if (extra.data.outputs) { - node.outputs = []; - for (var i in extra.data.outputs) { - node.addOutput( - extra.data.outputs[i][0], - extra.data.outputs[i][1] - ); - } - } - if (extra.data.title) { - node.title = extra.data.title; - } - if (extra.data.json) { - node.configure(extra.data.json); - } - - } - - // join node after inserting - if (options.node_from){ - var iS = false; - switch (typeof options.slot_from){ - case "string": - iS = options.node_from.findOutputSlot(options.slot_from); - break; - case "object": - if (options.slot_from.name){ - iS = options.node_from.findOutputSlot(options.slot_from.name); - }else{ - iS = -1; - } - if (iS==-1 && typeof options.slot_from.slot_index !== "undefined") iS = options.slot_from.slot_index; - break; - case "number": - iS = options.slot_from; - break; - default: - iS = 0; // try with first if no name set - } - if (typeof options.node_from.outputs[iS] !== "undefined"){ - if (iS!==false && iS>-1){ - options.node_from.connectByType( iS, node, options.node_from.outputs[iS].type ); - } - }else{ - // console.warn("cant find slot " + options.slot_from); - } - } - if (options.node_to){ - var iS = false; - switch (typeof options.slot_from){ - case "string": - iS = options.node_to.findInputSlot(options.slot_from); - break; - case "object": - if (options.slot_from.name){ - iS = options.node_to.findInputSlot(options.slot_from.name); - }else{ - iS = -1; - } - if (iS==-1 && typeof options.slot_from.slot_index !== "undefined") iS = options.slot_from.slot_index; - break; - case "number": - iS = options.slot_from; - break; - default: - iS = 0; // try with first if no name set - } - if (typeof options.node_to.inputs[iS] !== "undefined"){ - if (iS!==false && iS>-1){ - // try connection - options.node_to.connectByTypeOutput(iS,node,options.node_to.inputs[iS].type); - } - }else{ - // console.warn("cant find slot_nodeTO " + options.slot_from); - } - } - - graphcanvas.graph.afterChange(); - } - } - - dialog.close(); - } - - function changeSelection(forward) { - var prev = selected; - if (selected) { - selected.classList.remove("selected"); - } - if (!selected) { - selected = forward - ? helper.childNodes[0] - : helper.childNodes[helper.childNodes.length]; - } else { - selected = forward - ? selected.nextSibling - : selected.previousSibling; - if (!selected) { - selected = prev; - } - } - if (!selected) { - return; - } - selected.classList.add("selected"); - selected.scrollIntoView({block: "end", behavior: "smooth"}); - } - - function refreshHelper() { - timeout = null; - var str = input.value; - first = null; - helper.innerHTML = ""; - if (!str && !options.show_all_if_empty) { - return; - } - - if (that.onSearchBox) { - var list = that.onSearchBox(helper, str, graphcanvas); - if (list) { - for (var i = 0; i < list.length; ++i) { - addResult(list[i]); - } - } - } else { - var c = 0; - str = str.toLowerCase(); - var filter = graphcanvas.filter || graphcanvas.graph.filter; - - // filter by type preprocess - if(options.do_type_filter && that.search_box){ - var sIn = that.search_box.querySelector(".slot_in_type_filter"); - var sOut = that.search_box.querySelector(".slot_out_type_filter"); - }else{ - var sIn = false; - var sOut = false; - } - - //extras - for (var i in LiteGraph.searchbox_extras) { - var extra = LiteGraph.searchbox_extras[i]; - if ((!options.show_all_if_empty || str) && extra.desc.toLowerCase().indexOf(str) === -1) { - continue; - } - var ctor = LiteGraph.registered_node_types[ extra.type ]; - if( ctor && ctor.filter != filter ) - continue; - if( ! inner_test_filter(extra.type) ) - continue; - addResult( extra.desc, "searchbox_extra" ); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - - var filtered = null; - if (Array.prototype.filter) { //filter supported - var keys = Object.keys( LiteGraph.registered_node_types ); //types - var filtered = keys.filter( inner_test_filter ); - } else { - filtered = []; - for (var i in LiteGraph.registered_node_types) { - if( inner_test_filter(i) ) - filtered.push(i); - } - } - - for (var i = 0; i < filtered.length; i++) { - addResult(filtered[i]); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - - // add general type if filtering - if (options.show_general_after_typefiltered - && (sIn.value || sOut.value) - ){ - filtered_extra = []; - for (var i in LiteGraph.registered_node_types) { - if( inner_test_filter(i, {inTypeOverride: sIn&&sIn.value?"*":false, outTypeOverride: sOut&&sOut.value?"*":false}) ) - filtered_extra.push(i); - } - for (var i = 0; i < filtered_extra.length; i++) { - addResult(filtered_extra[i], "generic_type"); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - } - - // check il filtering gave no results - if ((sIn.value || sOut.value) && - ( (helper.childNodes.length == 0 && options.show_general_if_none_on_typefilter) ) - ){ - filtered_extra = []; - for (var i in LiteGraph.registered_node_types) { - if( inner_test_filter(i, {skipFilter: true}) ) - filtered_extra.push(i); - } - for (var i = 0; i < filtered_extra.length; i++) { - addResult(filtered_extra[i], "not_in_filter"); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - } - - function inner_test_filter( type, optsIn ) - { - var optsIn = optsIn || {}; - var optsDef = { skipFilter: false - ,inTypeOverride: false - ,outTypeOverride: false - }; - var opts = Object.assign(optsDef,optsIn); - var ctor = LiteGraph.registered_node_types[ type ]; - if(filter && ctor.filter != filter ) - return false; - if ((!options.show_all_if_empty || str) && type.toLowerCase().indexOf(str) === -1 && (!ctor.title || ctor.title.toLowerCase().indexOf(str) === -1)) - return false; - - // filter by slot IN, OUT types - if(options.do_type_filter && !opts.skipFilter){ - var sType = type; - - var sV = sIn.value; - if (opts.inTypeOverride!==false) sV = opts.inTypeOverride; - //if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 - - if(sIn && sV){ - //console.log("will check filter against "+sV); - if (LiteGraph.registered_slot_in_types[sV] && LiteGraph.registered_slot_in_types[sV].nodes){ // type is stored - //console.debug("check "+sType+" in "+LiteGraph.registered_slot_in_types[sV].nodes); - var doesInc = LiteGraph.registered_slot_in_types[sV].nodes.includes(sType); - if (doesInc!==false){ - //console.log(sType+" HAS "+sV); - }else{ - /*console.debug(LiteGraph.registered_slot_in_types[sV]); - console.log(+" DONT includes "+type);*/ - return false; - } - } - } - - var sV = sOut.value; - if (opts.outTypeOverride!==false) sV = opts.outTypeOverride; - //if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 - - if(sOut && sV){ - //console.log("search will check filter against "+sV); - if (LiteGraph.registered_slot_out_types[sV] && LiteGraph.registered_slot_out_types[sV].nodes){ // type is stored - //console.debug("check "+sType+" in "+LiteGraph.registered_slot_out_types[sV].nodes); - var doesInc = LiteGraph.registered_slot_out_types[sV].nodes.includes(sType); - if (doesInc!==false){ - //console.log(sType+" HAS "+sV); - }else{ - /*console.debug(LiteGraph.registered_slot_out_types[sV]); - console.log(+" DONT includes "+type);*/ - return false; - } - } - } - } - return true; - } - } - - function addResult(type, className) { - var help = document.createElement("div"); - if (!first) { - first = type; - } - - const nodeType = LiteGraph.registered_node_types[type]; - if (nodeType?.title) { - help.innerText = nodeType?.title; - const typeEl = document.createElement("span"); - typeEl.className = "litegraph lite-search-item-type"; - typeEl.textContent = type; - help.append(typeEl); - } else { - help.innerText = type; - } - - help.dataset["type"] = escape(type); - help.className = "litegraph lite-search-item"; - if (className) { - help.className += " " + className; - } - help.addEventListener("click", function(e) { - select(unescape(this.dataset["type"])); - }); - helper.appendChild(help); - } - } - - return dialog; - }; - - LGraphCanvas.prototype.showEditPropertyValue = function( node, property, options ) { - if (!node || node.properties[property] === undefined) { - return; - } - - options = options || {}; - var that = this; - - var info = node.getPropertyInfo(property); - var type = info.type; - - var input_html = ""; - - if (type == "string" || type == "number" || type == "array" || type == "object") { - input_html = ""; - } else if ( (type == "enum" || type == "combo") && info.values) { - input_html = ""; - } else if (type == "boolean" || type == "toggle") { - input_html = - ""; + function select(name) { + if (name) { + if (that.onSearchBoxSelection) { + that.onSearchBoxSelection(name, event, graphcanvas); } else { - console.warn("unknown type: " + type); - return; - } + var extra = LiteGraph.searchbox_extras[name.toLowerCase()]; + if (extra) { + name = extra.type; + } - var dialog = this.createDialog( - "" + - (info.label ? info.label : property) + - "" + - input_html + - "", - options - ); + graphcanvas.graph.beforeChange(); + var node = LiteGraph.createNode(name); + if (node) { + node.pos = graphcanvas.convertEventToCanvasOffset(event); + graphcanvas.graph.add(node, false); + } - var input = false; - if ((type == "enum" || type == "combo") && info.values) { - input = dialog.querySelector("select"); - input.addEventListener("change", function(e) { - dialog.modified(); - setValue(e.target.value); - //var index = e.target.value; - //setValue( e.options[e.selectedIndex].value ); - }); - } else if (type == "boolean" || type == "toggle") { - input = dialog.querySelector("input"); - if (input) { - input.addEventListener("click", function(e) { - dialog.modified(); - setValue(!!input.checked); - }); + if (extra && extra.data) { + if (extra.data.properties) { + for (var i in extra.data.properties) { + node.addProperty(i, extra.data.properties[i]); + } } - } else { - input = dialog.querySelector("input"); - if (input) { - input.addEventListener("blur", function(e) { - this.focus(); - }); + if (extra.data.inputs) { + node.inputs = []; + for (var i in extra.data.inputs) { + node.addOutput( + extra.data.inputs[i][0], + extra.data.inputs[i][1] + ); + } + } + if (extra.data.outputs) { + node.outputs = []; + for (var i in extra.data.outputs) { + node.addOutput( + extra.data.outputs[i][0], + extra.data.outputs[i][1] + ); + } + } + if (extra.data.title) { + node.title = extra.data.title; + } + if (extra.data.json) { + node.configure(extra.data.json); + } + } - var v = node.properties[property] !== undefined ? node.properties[property] : ""; - if (type !== 'string') { - v = JSON.stringify(v); + // join node after inserting + if (options.node_from) { + var iS = false; + switch (typeof options.slot_from) { + case "string": + iS = options.node_from.findOutputSlot(options.slot_from); + break; + case "object": + if (options.slot_from.name) { + iS = options.node_from.findOutputSlot(options.slot_from.name); + } else { + iS = -1; } - - input.value = v; - input.addEventListener("keydown", function(e) { - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - // ENTER - inner(); // save - } else if (e.keyCode != 13) { - dialog.modified(); - return; - } - e.preventDefault(); - e.stopPropagation(); - }); + if ( + iS == -1 && + typeof options.slot_from.slot_index !== "undefined" + ) + iS = options.slot_from.slot_index; + break; + case "number": + iS = options.slot_from; + break; + default: + iS = 0; // try with first if no name set } - } - if (input) input.focus(); - - var button = dialog.querySelector("button"); - button.addEventListener("click", inner); - - function inner() { - setValue(input.value); - } - - function setValue(value) { - - if(info && info.values && info.values.constructor === Object && info.values[value] != undefined ) - value = info.values[value]; - - if (typeof node.properties[property] == "number") { - value = Number(value); + if (typeof options.node_from.outputs[iS] !== "undefined") { + if (iS !== false && iS > -1) { + options.node_from.connectByType( + iS, + node, + options.node_from.outputs[iS].type + ); + } + } else { + // console.warn("cant find slot " + options.slot_from); } - if (type == "array" || type == "object") { - value = JSON.parse(value); + } + if (options.node_to) { + var iS = false; + switch (typeof options.slot_from) { + case "string": + iS = options.node_to.findInputSlot(options.slot_from); + break; + case "object": + if (options.slot_from.name) { + iS = options.node_to.findInputSlot(options.slot_from.name); + } else { + iS = -1; + } + if ( + iS == -1 && + typeof options.slot_from.slot_index !== "undefined" + ) + iS = options.slot_from.slot_index; + break; + case "number": + iS = options.slot_from; + break; + default: + iS = 0; // try with first if no name set } - node.properties[property] = value; - if (node.graph) { - node.graph._version++; + if (typeof options.node_to.inputs[iS] !== "undefined") { + if (iS !== false && iS > -1) { + // try connection + options.node_to.connectByTypeOutput( + iS, + node, + options.node_to.inputs[iS].type + ); + } + } else { + // console.warn("cant find slot_nodeTO " + options.slot_from); } - if (node.onPropertyChanged) { - node.onPropertyChanged(property, value); - } - if(options.onclose) - options.onclose(); - dialog.close(); - node.setDirtyCanvas(true, true); + } + + graphcanvas.graph.afterChange(); } + } - return dialog; - }; - - // TODO refactor, theer are different dialog, some uses createDialog, some dont - LGraphCanvas.prototype.createDialog = function(html, options) { - var def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true }; - options = Object.assign(def_options, options || {}); - - var dialog = document.createElement("div"); - dialog.className = "graphdialog"; - dialog.innerHTML = html; - dialog.is_modified = false; - - var rect = this.canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; - } - - if (options.position) { - offsetx += options.position[0]; - offsety += options.position[1]; - } else if (options.event) { - offsetx += options.event.clientX; - offsety += options.event.clientY; - } //centered - else { - offsetx += this.canvas.width * 0.5; - offsety += this.canvas.height * 0.5; - } - - dialog.style.left = offsetx + "px"; - dialog.style.top = offsety + "px"; - - this.canvas.parentNode.appendChild(dialog); - - // acheck for input and use default behaviour: save on enter, close on esc - if (options.checkForInput){ - var aI = []; - var focused = false; - if (aI = dialog.querySelectorAll("input")){ - aI.forEach(function(iX) { - iX.addEventListener("keydown",function(e){ - dialog.modified(); - if (e.keyCode == 27) { - dialog.close(); - } else if (e.keyCode != 13) { - return; - } - // set value ? - e.preventDefault(); - e.stopPropagation(); - }); - if (!focused) iX.focus(); - }); - } - } - - dialog.modified = function(){ - dialog.is_modified = true; - } - dialog.close = function() { - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - - var dialogCloseTimer = null; - var prevent_timeout = false; - dialog.addEventListener("mouseleave", function(e) { - if (prevent_timeout) - return; - if(options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) - if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) - dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); - }); - dialog.addEventListener("mouseenter", function(e) { - if(options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) - if(dialogCloseTimer) clearTimeout(dialogCloseTimer); - }); - var selInDia = dialog.querySelectorAll("select"); - if (selInDia){ - // if filtering, check focus changed to comboboxes and prevent closing - selInDia.forEach(function(selIn) { - selIn.addEventListener("click", function(e) { - prevent_timeout++; - }); - selIn.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selIn.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - }); - } - - return dialog; - }; - - LGraphCanvas.prototype.createPanel = function(title, options) { - options = options || {}; - - var ref_window = options.window || window; - var root = document.createElement("div"); - root.className = "litegraph dialog"; - root.innerHTML = "
"; - root.header = root.querySelector(".dialog-header"); - - if(options.width) - root.style.width = options.width + (options.width.constructor === Number ? "px" : ""); - if(options.height) - root.style.height = options.height + (options.height.constructor === Number ? "px" : ""); - if(options.closable) - { - var close = document.createElement("span"); - close.innerHTML = "✕"; - close.classList.add("close"); - close.addEventListener("click",function(){ - root.close(); - }); - root.header.appendChild(close); - } - root.title_element = root.querySelector(".dialog-title"); - root.title_element.innerText = title; - root.content = root.querySelector(".dialog-content"); - root.alt_content = root.querySelector(".dialog-alt-content"); - root.footer = root.querySelector(".dialog-footer"); - - root.close = function() - { - if (root.onClose && typeof root.onClose == "function"){ - root.onClose(); - } - if(root.parentNode) - root.parentNode.removeChild(root); - /* XXX CHECK THIS */ - if(this.parentNode){ - this.parentNode.removeChild(this); - } - /* XXX this was not working, was fixed with an IF, check this */ - } - - // function to swap panel content - root.toggleAltContent = function(force){ - if (typeof force != "undefined"){ - var vTo = force ? "block" : "none"; - var vAlt = force ? "none" : "block"; - }else{ - var vTo = root.alt_content.style.display != "block" ? "block" : "none"; - var vAlt = root.alt_content.style.display != "block" ? "none" : "block"; - } - root.alt_content.style.display = vTo; - root.content.style.display = vAlt; - } - - root.toggleFooterVisibility = function(force){ - if (typeof force != "undefined"){ - var vTo = force ? "block" : "none"; - }else{ - var vTo = root.footer.style.display != "block" ? "block" : "none"; - } - root.footer.style.display = vTo; - } - - root.clear = function() - { - this.content.innerHTML = ""; - } - - root.addHTML = function(code, classname, on_footer) - { - var elem = document.createElement("div"); - if(classname) - elem.className = classname; - elem.innerHTML = code; - if(on_footer) - root.footer.appendChild(elem); - else - root.content.appendChild(elem); - return elem; - } - - root.addButton = function( name, callback, options ) - { - var elem = document.createElement("button"); - elem.innerText = name; - elem.options = options; - elem.classList.add("btn"); - elem.addEventListener("click",callback); - root.footer.appendChild(elem); - return elem; - } - - root.addSeparator = function() - { - var elem = document.createElement("div"); - elem.className = "separator"; - root.content.appendChild(elem); - } - - root.addWidget = function( type, name, value, options, callback ) - { - options = options || {}; - var str_value = String(value); - type = type.toLowerCase(); - if(type == "number") - str_value = value.toFixed(3); - - var elem = document.createElement("div"); - elem.className = "property"; - elem.innerHTML = ""; - elem.querySelector(".property_name").innerText = options.label || name; - var value_element = elem.querySelector(".property_value"); - value_element.innerText = str_value; - elem.dataset["property"] = name; - elem.dataset["type"] = options.type || type; - elem.options = options; - elem.value = value; - - if( type == "code" ) - elem.addEventListener("click", function(e){ root.inner_showCodePad( this.dataset["property"] ); }); - else if (type == "boolean") - { - elem.classList.add("boolean"); - if(value) - elem.classList.add("bool-on"); - elem.addEventListener("click", function(){ - //var v = node.properties[this.dataset["property"]]; - //node.setProperty(this.dataset["property"],!v); this.innerText = v ? "true" : "false"; - var propname = this.dataset["property"]; - this.value = !this.value; - this.classList.toggle("bool-on"); - this.querySelector(".property_value").innerText = this.value ? "true" : "false"; - innerChange(propname, this.value ); - }); - } - else if (type == "string" || type == "number") - { - value_element.setAttribute("contenteditable",true); - value_element.addEventListener("keydown", function(e){ - if(e.code == "Enter" && (type != "string" || !e.shiftKey)) // allow for multiline - { - e.preventDefault(); - this.blur(); - } - }); - value_element.addEventListener("blur", function(){ - var v = this.innerText; - var propname = this.parentNode.dataset["property"]; - var proptype = this.parentNode.dataset["type"]; - if( proptype == "number") - v = Number(v); - innerChange(propname, v); - }); - } - else if (type == "enum" || type == "combo") { - var str_value = LGraphCanvas.getPropertyPrintableValue( value, options.values ); - value_element.innerText = str_value; - - value_element.addEventListener("click", function(event){ - var values = options.values || []; - var propname = this.parentNode.dataset["property"]; - var elem_that = this; - var menu = new LiteGraph.ContextMenu(values,{ - event: event, - className: "dark", - callback: inner_clicked - }, - ref_window); - function inner_clicked(v, option, event) { - //node.setProperty(propname,v); - //graphcanvas.dirty_canvas = true; - elem_that.innerText = v; - innerChange(propname,v); - return false; - } - }); - } - - root.content.appendChild(elem); - - function innerChange(name, value) - { - //console.log("change",name,value); - //that.dirty_canvas = true; - if(options.callback) - options.callback(name,value,options); - if(callback) - callback(name,value,options); - } - - return elem; - } - - if (root.onOpen && typeof root.onOpen == "function") root.onOpen(); - - return root; - }; - - LGraphCanvas.getPropertyPrintableValue = function(value, values) - { - if(!values) - return String(value); - - if(values.constructor === Array) - { - return String(value); - } - - if(values.constructor === Object) - { - var desc_value = ""; - for(var k in values) - { - if(values[k] != value) - continue; - desc_value = k; - break; - } - return String(value) + " ("+desc_value+")"; - } - } - - LGraphCanvas.prototype.closePanels = function(){ - var panel = document.querySelector("#node-panel"); - if(panel) - panel.close(); - var panel = document.querySelector("#option-panel"); - if(panel) - panel.close(); + dialog.close(); } - - LGraphCanvas.prototype.showShowGraphOptionsPanel = function(refOpts, obEv, refMenu, refMenu2){ - if(this.constructor && this.constructor.name == "HTMLDivElement"){ - // assume coming from the menu event click - if (!obEv || !obEv.event || !obEv.event.target || !obEv.event.target.lgraphcanvas){ - console.warn("Canvas not found"); // need a ref to canvas obj - /*console.debug(event); - console.debug(event.target);*/ - return; - } - var graphcanvas = obEv.event.target.lgraphcanvas; - }else{ - // assume called internally - var graphcanvas = this; - } - graphcanvas.closePanels(); - var ref_window = graphcanvas.getCanvasWindow(); - panel = graphcanvas.createPanel("Options",{ - closable: true - ,window: ref_window - ,onOpen: function(){ - graphcanvas.OPTIONPANEL_IS_OPEN = true; - } - ,onClose: function(){ - graphcanvas.OPTIONPANEL_IS_OPEN = false; - graphcanvas.options_panel = null; - } - }); - graphcanvas.options_panel = panel; - panel.id = "option-panel"; - panel.classList.add("settings"); - - function inner_refresh(){ - - panel.content.innerHTML = ""; //clear - var fUpdate = function(name, value, options){ - switch(name){ - /*case "Render mode": + function changeSelection(forward) { + var prev = selected; + if (selected) { + selected.classList.remove("selected"); + } + if (!selected) { + selected = forward + ? helper.childNodes[0] + : helper.childNodes[helper.childNodes.length]; + } else { + selected = forward ? selected.nextSibling : selected.previousSibling; + if (!selected) { + selected = prev; + } + } + if (!selected) { + return; + } + selected.classList.add("selected"); + selected.scrollIntoView({ block: "end", behavior: "smooth" }); + } + + function refreshHelper() { + timeout = null; + var str = input.value; + first = null; + helper.innerHTML = ""; + if (!str && !options.show_all_if_empty) { + return; + } + + if (that.onSearchBox) { + var list = that.onSearchBox(helper, str, graphcanvas); + if (list) { + for (var i = 0; i < list.length; ++i) { + addResult(list[i]); + } + } + } else { + var c = 0; + str = str.toLowerCase(); + var filter = graphcanvas.filter || graphcanvas.graph.filter; + + // filter by type preprocess + if (options.do_type_filter && that.search_box) { + var sIn = that.search_box.querySelector(".slot_in_type_filter"); + var sOut = that.search_box.querySelector(".slot_out_type_filter"); + } else { + var sIn = false; + var sOut = false; + } + + //extras + for (var i in LiteGraph.searchbox_extras) { + var extra = LiteGraph.searchbox_extras[i]; + if ( + (!options.show_all_if_empty || str) && + extra.desc.toLowerCase().indexOf(str) === -1 + ) { + continue; + } + var ctor = LiteGraph.registered_node_types[extra.type]; + if (ctor && ctor.filter != filter) continue; + if (!inner_test_filter(extra.type)) continue; + addResult(extra.desc, "searchbox_extra"); + if ( + LGraphCanvas.search_limit !== -1 && + c++ > LGraphCanvas.search_limit + ) { + break; + } + } + + var filtered = null; + if (Array.prototype.filter) { + //filter supported + var keys = Object.keys(LiteGraph.registered_node_types); //types + var filtered = keys.filter(inner_test_filter); + } else { + filtered = []; + for (var i in LiteGraph.registered_node_types) { + if (inner_test_filter(i)) filtered.push(i); + } + } + + for (var i = 0; i < filtered.length; i++) { + addResult(filtered[i]); + if ( + LGraphCanvas.search_limit !== -1 && + c++ > LGraphCanvas.search_limit + ) { + break; + } + } + + // add general type if filtering + if ( + options.show_general_after_typefiltered && + (sIn.value || sOut.value) + ) { + filtered_extra = []; + for (var i in LiteGraph.registered_node_types) { + if ( + inner_test_filter(i, { + inTypeOverride: sIn && sIn.value ? "*" : false, + outTypeOverride: sOut && sOut.value ? "*" : false, + }) + ) + filtered_extra.push(i); + } + for (var i = 0; i < filtered_extra.length; i++) { + addResult(filtered_extra[i], "generic_type"); + if ( + LGraphCanvas.search_limit !== -1 && + c++ > LGraphCanvas.search_limit + ) { + break; + } + } + } + + // check il filtering gave no results + if ( + (sIn.value || sOut.value) && + helper.childNodes.length == 0 && + options.show_general_if_none_on_typefilter + ) { + filtered_extra = []; + for (var i in LiteGraph.registered_node_types) { + if (inner_test_filter(i, { skipFilter: true })) + filtered_extra.push(i); + } + for (var i = 0; i < filtered_extra.length; i++) { + addResult(filtered_extra[i], "not_in_filter"); + if ( + LGraphCanvas.search_limit !== -1 && + c++ > LGraphCanvas.search_limit + ) { + break; + } + } + } + + function inner_test_filter(type, optsIn) { + var optsIn = optsIn || {}; + var optsDef = { + skipFilter: false, + inTypeOverride: false, + outTypeOverride: false, + }; + var opts = Object.assign(optsDef, optsIn); + var ctor = LiteGraph.registered_node_types[type]; + if (filter && ctor.filter != filter) return false; + if ( + (!options.show_all_if_empty || str) && + type.toLowerCase().indexOf(str) === -1 && + (!ctor.title || ctor.title.toLowerCase().indexOf(str) === -1) + ) + return false; + + // filter by slot IN, OUT types + if (options.do_type_filter && !opts.skipFilter) { + var sType = type; + + var sV = sIn.value; + if (opts.inTypeOverride !== false) sV = opts.inTypeOverride; + //if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 + + if (sIn && sV) { + //console.log("will check filter against "+sV); + if ( + LiteGraph.registered_slot_in_types[sV] && + LiteGraph.registered_slot_in_types[sV].nodes + ) { + // type is stored + //console.debug("check "+sType+" in "+LiteGraph.registered_slot_in_types[sV].nodes); + var doesInc = + LiteGraph.registered_slot_in_types[sV].nodes.includes(sType); + if (doesInc !== false) { + //console.log(sType+" HAS "+sV); + } else { + /*console.debug(LiteGraph.registered_slot_in_types[sV]); + console.log(+" DONT includes "+type);*/ + return false; + } + } + } + + var sV = sOut.value; + if (opts.outTypeOverride !== false) sV = opts.outTypeOverride; + //if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 + + if (sOut && sV) { + //console.log("search will check filter against "+sV); + if ( + LiteGraph.registered_slot_out_types[sV] && + LiteGraph.registered_slot_out_types[sV].nodes + ) { + // type is stored + //console.debug("check "+sType+" in "+LiteGraph.registered_slot_out_types[sV].nodes); + var doesInc = + LiteGraph.registered_slot_out_types[sV].nodes.includes(sType); + if (doesInc !== false) { + //console.log(sType+" HAS "+sV); + } else { + /*console.debug(LiteGraph.registered_slot_out_types[sV]); + console.log(+" DONT includes "+type);*/ + return false; + } + } + } + } + return true; + } + } + + function addResult(type, className) { + var help = document.createElement("div"); + if (!first) { + first = type; + } + + const nodeType = LiteGraph.registered_node_types[type]; + if (nodeType?.title) { + help.innerText = nodeType?.title; + const typeEl = document.createElement("span"); + typeEl.className = "litegraph lite-search-item-type"; + typeEl.textContent = type; + help.append(typeEl); + } else { + help.innerText = type; + } + + help.dataset["type"] = escape(type); + help.className = "litegraph lite-search-item"; + if (className) { + help.className += " " + className; + } + help.addEventListener("click", function (e) { + select(unescape(this.dataset["type"])); + }); + helper.appendChild(help); + } + } + + return dialog; + }; + + LGraphCanvas.prototype.showEditPropertyValue = function ( + node, + property, + options + ) { + if (!node || node.properties[property] === undefined) { + return; + } + + options = options || {}; + var that = this; + + var info = node.getPropertyInfo(property); + var type = info.type; + + var input_html = ""; + + if ( + type == "string" || + type == "number" || + type == "array" || + type == "object" + ) { + input_html = ""; + } else if ((type == "enum" || type == "combo") && info.values) { + input_html = ""; + } else if (type == "boolean" || type == "toggle") { + input_html = + ""; + } else { + console.warn("unknown type: " + type); + return; + } + + var dialog = this.createDialog( + "" + + (info.label ? info.label : property) + + "" + + input_html + + "", + options + ); + + var input = false; + if ((type == "enum" || type == "combo") && info.values) { + input = dialog.querySelector("select"); + input.addEventListener("change", function (e) { + dialog.modified(); + setValue(e.target.value); + //var index = e.target.value; + //setValue( e.options[e.selectedIndex].value ); + }); + } else if (type == "boolean" || type == "toggle") { + input = dialog.querySelector("input"); + if (input) { + input.addEventListener("click", function (e) { + dialog.modified(); + setValue(!!input.checked); + }); + } + } else { + input = dialog.querySelector("input"); + if (input) { + input.addEventListener("blur", function (e) { + this.focus(); + }); + + var v = + node.properties[property] !== undefined + ? node.properties[property] + : ""; + if (type !== "string") { + v = JSON.stringify(v); + } + + input.value = v; + input.addEventListener("keydown", function (e) { + if (e.keyCode == 27) { + //ESC + dialog.close(); + } else if (e.keyCode == 13) { + // ENTER + inner(); // save + } else if (e.keyCode != 13) { + dialog.modified(); + return; + } + e.preventDefault(); + e.stopPropagation(); + }); + } + } + if (input) input.focus(); + + var button = dialog.querySelector("button"); + button.addEventListener("click", inner); + + function inner() { + setValue(input.value); + } + + function setValue(value) { + if ( + info && + info.values && + info.values.constructor === Object && + info.values[value] != undefined + ) + value = info.values[value]; + + if (typeof node.properties[property] == "number") { + value = Number(value); + } + if (type == "array" || type == "object") { + value = JSON.parse(value); + } + node.properties[property] = value; + if (node.graph) { + node.graph._version++; + } + if (node.onPropertyChanged) { + node.onPropertyChanged(property, value); + } + if (options.onclose) options.onclose(); + dialog.close(); + node.setDirtyCanvas(true, true); + } + + return dialog; + }; + + // TODO refactor, theer are different dialog, some uses createDialog, some dont + LGraphCanvas.prototype.createDialog = function (html, options) { + var def_options = { + checkForInput: false, + closeOnLeave: true, + closeOnLeave_checkModified: true, + }; + options = Object.assign(def_options, options || {}); + + var dialog = document.createElement("div"); + dialog.className = "graphdialog"; + dialog.innerHTML = html; + dialog.is_modified = false; + + var rect = this.canvas.getBoundingClientRect(); + var offsetx = -20; + var offsety = -20; + if (rect) { + offsetx -= rect.left; + offsety -= rect.top; + } + + if (options.position) { + offsetx += options.position[0]; + offsety += options.position[1]; + } else if (options.event) { + offsetx += options.event.clientX; + offsety += options.event.clientY; + } //centered + else { + offsetx += this.canvas.width * 0.5; + offsety += this.canvas.height * 0.5; + } + + dialog.style.left = offsetx + "px"; + dialog.style.top = offsety + "px"; + + this.canvas.parentNode.appendChild(dialog); + + // acheck for input and use default behaviour: save on enter, close on esc + if (options.checkForInput) { + var aI = []; + var focused = false; + if ((aI = dialog.querySelectorAll("input"))) { + aI.forEach(function (iX) { + iX.addEventListener("keydown", function (e) { + dialog.modified(); + if (e.keyCode == 27) { + dialog.close(); + } else if (e.keyCode != 13) { + return; + } + // set value ? + e.preventDefault(); + e.stopPropagation(); + }); + if (!focused) iX.focus(); + }); + } + } + + dialog.modified = function () { + dialog.is_modified = true; + }; + dialog.close = function () { + if (dialog.parentNode) { + dialog.parentNode.removeChild(dialog); + } + }; + + var dialogCloseTimer = null; + var prevent_timeout = false; + dialog.addEventListener("mouseleave", function (e) { + if (prevent_timeout) return; + if (options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) + if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) + dialogCloseTimer = setTimeout( + dialog.close, + LiteGraph.dialog_close_on_mouse_leave_delay + ); //dialog.close(); + }); + dialog.addEventListener("mouseenter", function (e) { + if (options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) + if (dialogCloseTimer) clearTimeout(dialogCloseTimer); + }); + var selInDia = dialog.querySelectorAll("select"); + if (selInDia) { + // if filtering, check focus changed to comboboxes and prevent closing + selInDia.forEach(function (selIn) { + selIn.addEventListener("click", function (e) { + prevent_timeout++; + }); + selIn.addEventListener("blur", function (e) { + prevent_timeout = 0; + }); + selIn.addEventListener("change", function (e) { + prevent_timeout = -1; + }); + }); + } + + return dialog; + }; + + LGraphCanvas.prototype.createPanel = function (title, options) { + options = options || {}; + + var ref_window = options.window || window; + var root = document.createElement("div"); + root.className = "litegraph dialog"; + root.innerHTML = + "
"; + root.header = root.querySelector(".dialog-header"); + + if (options.width) + root.style.width = + options.width + (options.width.constructor === Number ? "px" : ""); + if (options.height) + root.style.height = + options.height + (options.height.constructor === Number ? "px" : ""); + if (options.closable) { + var close = document.createElement("span"); + close.innerHTML = "✕"; + close.classList.add("close"); + close.addEventListener("click", function () { + root.close(); + }); + root.header.appendChild(close); + } + root.title_element = root.querySelector(".dialog-title"); + root.title_element.innerText = title; + root.content = root.querySelector(".dialog-content"); + root.alt_content = root.querySelector(".dialog-alt-content"); + root.footer = root.querySelector(".dialog-footer"); + + root.close = function () { + if (root.onClose && typeof root.onClose == "function") { + root.onClose(); + } + if (root.parentNode) root.parentNode.removeChild(root); + /* XXX CHECK THIS */ + if (this.parentNode) { + this.parentNode.removeChild(this); + } + /* XXX this was not working, was fixed with an IF, check this */ + }; + + // function to swap panel content + root.toggleAltContent = function (force) { + if (typeof force != "undefined") { + var vTo = force ? "block" : "none"; + var vAlt = force ? "none" : "block"; + } else { + var vTo = root.alt_content.style.display != "block" ? "block" : "none"; + var vAlt = root.alt_content.style.display != "block" ? "none" : "block"; + } + root.alt_content.style.display = vTo; + root.content.style.display = vAlt; + }; + + root.toggleFooterVisibility = function (force) { + if (typeof force != "undefined") { + var vTo = force ? "block" : "none"; + } else { + var vTo = root.footer.style.display != "block" ? "block" : "none"; + } + root.footer.style.display = vTo; + }; + + root.clear = function () { + this.content.innerHTML = ""; + }; + + root.addHTML = function (code, classname, on_footer) { + var elem = document.createElement("div"); + if (classname) elem.className = classname; + elem.innerHTML = code; + if (on_footer) root.footer.appendChild(elem); + else root.content.appendChild(elem); + return elem; + }; + + root.addButton = function (name, callback, options) { + var elem = document.createElement("button"); + elem.innerText = name; + elem.options = options; + elem.classList.add("btn"); + elem.addEventListener("click", callback); + root.footer.appendChild(elem); + return elem; + }; + + root.addSeparator = function () { + var elem = document.createElement("div"); + elem.className = "separator"; + root.content.appendChild(elem); + }; + + root.addWidget = function (type, name, value, options, callback) { + options = options || {}; + var str_value = String(value); + type = type.toLowerCase(); + if (type == "number") str_value = value.toFixed(3); + + var elem = document.createElement("div"); + elem.className = "property"; + elem.innerHTML = + ""; + elem.querySelector(".property_name").innerText = options.label || name; + var value_element = elem.querySelector(".property_value"); + value_element.innerText = str_value; + elem.dataset["property"] = name; + elem.dataset["type"] = options.type || type; + elem.options = options; + elem.value = value; + + if (type == "code") + elem.addEventListener("click", function (e) { + root.inner_showCodePad(this.dataset["property"]); + }); + else if (type == "boolean") { + elem.classList.add("boolean"); + if (value) elem.classList.add("bool-on"); + elem.addEventListener("click", function () { + //var v = node.properties[this.dataset["property"]]; + //node.setProperty(this.dataset["property"],!v); this.innerText = v ? "true" : "false"; + var propname = this.dataset["property"]; + this.value = !this.value; + this.classList.toggle("bool-on"); + this.querySelector(".property_value").innerText = this.value + ? "true" + : "false"; + innerChange(propname, this.value); + }); + } else if (type == "string" || type == "number") { + value_element.setAttribute("contenteditable", true); + value_element.addEventListener("keydown", function (e) { + if (e.code == "Enter" && (type != "string" || !e.shiftKey)) { + // allow for multiline + e.preventDefault(); + this.blur(); + } + }); + value_element.addEventListener("blur", function () { + var v = this.innerText; + var propname = this.parentNode.dataset["property"]; + var proptype = this.parentNode.dataset["type"]; + if (proptype == "number") v = Number(v); + innerChange(propname, v); + }); + } else if (type == "enum" || type == "combo") { + var str_value = LGraphCanvas.getPropertyPrintableValue( + value, + options.values + ); + value_element.innerText = str_value; + + value_element.addEventListener("click", function (event) { + var values = options.values || []; + var propname = this.parentNode.dataset["property"]; + var elem_that = this; + var menu = new LiteGraph.ContextMenu( + values, + { + event: event, + className: "dark", + callback: inner_clicked, + }, + ref_window + ); + function inner_clicked(v, option, event) { + //node.setProperty(propname,v); + //graphcanvas.dirty_canvas = true; + elem_that.innerText = v; + innerChange(propname, v); + return false; + } + }); + } + + root.content.appendChild(elem); + + function innerChange(name, value) { + //console.log("change",name,value); + //that.dirty_canvas = true; + if (options.callback) options.callback(name, value, options); + if (callback) callback(name, value, options); + } + + return elem; + }; + + if (root.onOpen && typeof root.onOpen == "function") root.onOpen(); + + return root; + }; + + LGraphCanvas.getPropertyPrintableValue = function (value, values) { + if (!values) return String(value); + + if (values.constructor === Array) { + return String(value); + } + + if (values.constructor === Object) { + var desc_value = ""; + for (var k in values) { + if (values[k] != value) continue; + desc_value = k; + break; + } + return String(value) + " (" + desc_value + ")"; + } + }; + + LGraphCanvas.prototype.closePanels = function () { + var panel = document.querySelector("#node-panel"); + if (panel) panel.close(); + var panel = document.querySelector("#option-panel"); + if (panel) panel.close(); + }; + + LGraphCanvas.prototype.showShowGraphOptionsPanel = function ( + refOpts, + obEv, + refMenu, + refMenu2 + ) { + if (this.constructor && this.constructor.name == "HTMLDivElement") { + // assume coming from the menu event click + if ( + !obEv || + !obEv.event || + !obEv.event.target || + !obEv.event.target.lgraphcanvas + ) { + console.warn("Canvas not found"); // need a ref to canvas obj + /*console.debug(event); + console.debug(event.target);*/ + return; + } + var graphcanvas = obEv.event.target.lgraphcanvas; + } else { + // assume called internally + var graphcanvas = this; + } + graphcanvas.closePanels(); + var ref_window = graphcanvas.getCanvasWindow(); + panel = graphcanvas.createPanel("Options", { + closable: true, + window: ref_window, + onOpen: function () { + graphcanvas.OPTIONPANEL_IS_OPEN = true; + }, + onClose: function () { + graphcanvas.OPTIONPANEL_IS_OPEN = false; + graphcanvas.options_panel = null; + }, + }); + graphcanvas.options_panel = panel; + panel.id = "option-panel"; + panel.classList.add("settings"); + + function inner_refresh() { + panel.content.innerHTML = ""; //clear + + var fUpdate = function (name, value, options) { + switch (name) { + /*case "Render mode": // Case "".. if (options.values && options.key){ var kV = Object.values(options.values).indexOf(value); @@ -12506,147 +12879,176 @@ LGraphNode.prototype.executeAction = function(action) console.warn("unexpected options"); console.debug(options); break;*/ - default: - //console.debug("want to update graph options: "+name+": "+value); - if (options && options.key){ - name = options.key; - } - if (options.values){ - value = Object.values(options.values).indexOf(value); - } - //console.debug("update graph option: "+name+": "+value); - graphcanvas[name] = value; - break; - } - }; - - // panel.addWidget( "string", "Graph name", "", {}, fUpdate); // implement - - var aProps = LiteGraph.availableCanvasOptions; - aProps.sort(); - for(var pI in aProps){ - var pX = aProps[pI]; - panel.addWidget( "boolean", pX, graphcanvas[pX], {key: pX, on: "True", off: "False"}, fUpdate); + default: + //console.debug("want to update graph options: "+name+": "+value); + if (options && options.key) { + name = options.key; } - - var aLinks = [ graphcanvas.links_render_mode ]; - panel.addWidget( "combo", "Render mode", LiteGraph.LINK_RENDER_MODES[graphcanvas.links_render_mode], {key: "links_render_mode", values: LiteGraph.LINK_RENDER_MODES}, fUpdate); - - panel.addSeparator(); - - panel.footer.innerHTML = ""; // clear + if (options.values) { + value = Object.values(options.values).indexOf(value); + } + //console.debug("update graph option: "+name+": "+value); + graphcanvas[name] = value; + break; + } + }; - } - inner_refresh(); + // panel.addWidget( "string", "Graph name", "", {}, fUpdate); // implement - graphcanvas.canvas.parentNode.appendChild( panel ); + var aProps = LiteGraph.availableCanvasOptions; + aProps.sort(); + for (var pI in aProps) { + var pX = aProps[pI]; + panel.addWidget( + "boolean", + pX, + graphcanvas[pX], + { key: pX, on: "True", off: "False" }, + fUpdate + ); + } + + var aLinks = [graphcanvas.links_render_mode]; + panel.addWidget( + "combo", + "Render mode", + LiteGraph.LINK_RENDER_MODES[graphcanvas.links_render_mode], + { key: "links_render_mode", values: LiteGraph.LINK_RENDER_MODES }, + fUpdate + ); + + panel.addSeparator(); + + panel.footer.innerHTML = ""; // clear } - - LGraphCanvas.prototype.showShowNodePanel = function( node ) - { - this.SELECTED_NODE = node; - this.closePanels(); - var ref_window = this.getCanvasWindow(); - var that = this; - var graphcanvas = this; - var panel = this.createPanel(node.title || "",{ - closable: true - ,window: ref_window - ,onOpen: function(){ - graphcanvas.NODEPANEL_IS_OPEN = true; - } - ,onClose: function(){ - graphcanvas.NODEPANEL_IS_OPEN = false; - graphcanvas.node_panel = null; - } - }); - graphcanvas.node_panel = panel; - panel.id = "node-panel"; - panel.node = node; - panel.classList.add("settings"); + inner_refresh(); - function inner_refresh() - { - panel.content.innerHTML = ""; //clear - panel.addHTML(""+node.type+""+(node.constructor.desc || "")+""); + graphcanvas.canvas.parentNode.appendChild(panel); + }; - panel.addHTML("

Properties

"); + LGraphCanvas.prototype.showShowNodePanel = function (node) { + this.SELECTED_NODE = node; + this.closePanels(); + var ref_window = this.getCanvasWindow(); + var that = this; + var graphcanvas = this; + var panel = this.createPanel(node.title || "", { + closable: true, + window: ref_window, + onOpen: function () { + graphcanvas.NODEPANEL_IS_OPEN = true; + }, + onClose: function () { + graphcanvas.NODEPANEL_IS_OPEN = false; + graphcanvas.node_panel = null; + }, + }); + graphcanvas.node_panel = panel; + panel.id = "node-panel"; + panel.node = node; + panel.classList.add("settings"); - var fUpdate = function(name,value){ - graphcanvas.graph.beforeChange(node); - switch(name){ - case "Title": - node.title = value; - break; - case "Mode": - var kV = Object.values(LiteGraph.NODE_MODES).indexOf(value); - if (kV>=0 && LiteGraph.NODE_MODES[kV]){ - node.changeMode(kV); - }else{ - console.warn("unexpected mode: "+value); - } - break; - case "Color": - if (LGraphCanvas.node_colors[value]){ - node.color = LGraphCanvas.node_colors[value].color; - node.bgcolor = LGraphCanvas.node_colors[value].bgcolor; - }else{ - console.warn("unexpected color: "+value); - } - break; - default: - node.setProperty(name,value); - break; - } - graphcanvas.graph.afterChange(); - graphcanvas.dirty_canvas = true; - }; - - panel.addWidget( "string", "Title", node.title, {}, fUpdate); - - panel.addWidget( "combo", "Mode", LiteGraph.NODE_MODES[node.mode], {values: LiteGraph.NODE_MODES}, fUpdate); - - var nodeCol = ""; - if (node.color !== undefined){ - nodeCol = Object.keys(LGraphCanvas.node_colors).filter(function(nK){ return LGraphCanvas.node_colors[nK].color == node.color; }); + function inner_refresh() { + panel.content.innerHTML = ""; //clear + panel.addHTML( + "" + + node.type + + "" + + (node.constructor.desc || "") + + "" + ); + + panel.addHTML("

Properties

"); + + var fUpdate = function (name, value) { + graphcanvas.graph.beforeChange(node); + switch (name) { + case "Title": + node.title = value; + break; + case "Mode": + var kV = Object.values(LiteGraph.NODE_MODES).indexOf(value); + if (kV >= 0 && LiteGraph.NODE_MODES[kV]) { + node.changeMode(kV); + } else { + console.warn("unexpected mode: " + value); } - - panel.addWidget( "combo", "Color", nodeCol, {values: Object.keys(LGraphCanvas.node_colors)}, fUpdate); - - for(var pName in node.properties) - { - var value = node.properties[pName]; - var info = node.getPropertyInfo(pName); - var type = info.type || "string"; + break; + case "Color": + if (LGraphCanvas.node_colors[value]) { + node.color = LGraphCanvas.node_colors[value].color; + node.bgcolor = LGraphCanvas.node_colors[value].bgcolor; + } else { + console.warn("unexpected color: " + value); + } + break; + default: + node.setProperty(name, value); + break; + } + graphcanvas.graph.afterChange(); + graphcanvas.dirty_canvas = true; + }; - //in case the user wants control over the side panel widget - if( node.onAddPropertyToPanel && node.onAddPropertyToPanel(pName,panel) ) - continue; + panel.addWidget("string", "Title", node.title, {}, fUpdate); - panel.addWidget( info.widget || info.type, pName, value, info, fUpdate); - } + panel.addWidget( + "combo", + "Mode", + LiteGraph.NODE_MODES[node.mode], + { values: LiteGraph.NODE_MODES }, + fUpdate + ); - panel.addSeparator(); + var nodeCol = ""; + if (node.color !== undefined) { + nodeCol = Object.keys(LGraphCanvas.node_colors).filter(function (nK) { + return LGraphCanvas.node_colors[nK].color == node.color; + }); + } - if(node.onShowCustomPanelInfo) - node.onShowCustomPanelInfo(panel); + panel.addWidget( + "combo", + "Color", + nodeCol, + { values: Object.keys(LGraphCanvas.node_colors) }, + fUpdate + ); - panel.footer.innerHTML = ""; // clear - panel.addButton("Delete",function(){ - if(node.block_delete) - return; - node.graph.remove(node); - panel.close(); - }).classList.add("delete"); - } + for (var pName in node.properties) { + var value = node.properties[pName]; + var info = node.getPropertyInfo(pName); + var type = info.type || "string"; - panel.inner_showCodePad = function( propname ) - { - panel.classList.remove("settings"); - panel.classList.add("centered"); + //in case the user wants control over the side panel widget + if ( + node.onAddPropertyToPanel && + node.onAddPropertyToPanel(pName, panel) + ) + continue; - - /*if(window.CodeFlask) //disabled for now + panel.addWidget(info.widget || info.type, pName, value, info, fUpdate); + } + + panel.addSeparator(); + + if (node.onShowCustomPanelInfo) node.onShowCustomPanelInfo(panel); + + panel.footer.innerHTML = ""; // clear + panel + .addButton("Delete", function () { + if (node.block_delete) return; + node.graph.remove(node); + panel.close(); + }) + .classList.add("delete"); + } + + panel.inner_showCodePad = function (propname) { + panel.classList.remove("settings"); + panel.classList.add("centered"); + + /*if(window.CodeFlask) //disabled for now { panel.content.innerHTML = "
"; var flask = new CodeFlask( "div.code", { language: 'js' }); @@ -12657,1124 +13059,1128 @@ LGraphNode.prototype.executeAction = function(action) } else {*/ - panel.alt_content.innerHTML = ""; - var textarea = panel.alt_content.querySelector("textarea"); - var fDoneWith = function(){ - panel.toggleAltContent(false); //if(node_prop_div) node_prop_div.style.display = "block"; // panel.close(); - panel.toggleFooterVisibility(true); - textarea.parentNode.removeChild(textarea); - panel.classList.add("settings"); - panel.classList.remove("centered"); - inner_refresh(); - } - textarea.value = node.properties[propname]; - textarea.addEventListener("keydown", function(e){ - if(e.code == "Enter" && e.ctrlKey ) - { - node.setProperty(propname, textarea.value); - fDoneWith(); - } - }); - panel.toggleAltContent(true); - panel.toggleFooterVisibility(false); - textarea.style.height = "calc(100% - 40px)"; - /*}*/ - var assign = panel.addButton( "Assign", function(){ - node.setProperty(propname, textarea.value); - fDoneWith(); - }); - panel.alt_content.appendChild(assign); //panel.content.appendChild(assign); - var button = panel.addButton( "Close", fDoneWith); - button.style.float = "right"; - panel.alt_content.appendChild(button); // panel.content.appendChild(button); - } - - inner_refresh(); - - this.canvas.parentNode.appendChild( panel ); - } - - LGraphCanvas.prototype.showSubgraphPropertiesDialog = function(node) - { - console.log("showing subgraph properties dialog"); - - var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog"); - if(old_panel) - old_panel.close(); - - var panel = this.createPanel("Subgraph Inputs",{closable:true, width: 500}); - panel.node = node; - panel.classList.add("subgraph_dialog"); - - function inner_refresh() - { - panel.clear(); - - //show currents - if(node.inputs) - for(var i = 0; i < node.inputs.length; ++i) - { - var input = node.inputs[i]; - if(input.not_subgraph_input) - continue; - var html = " "; - var elem = panel.addHTML(html,"subgraph_property"); - elem.dataset["name"] = input.name; - elem.dataset["slot"] = i; - elem.querySelector(".name").innerText = input.name; - elem.querySelector(".type").innerText = input.type; - elem.querySelector("button").addEventListener("click",function(e){ - node.removeInput( Number( this.parentNode.dataset["slot"] ) ); - inner_refresh(); - }); - } - } - - //add extra - var html = " + NameType"; - var elem = panel.addHTML(html,"subgraph_property extra", true); - elem.querySelector("button").addEventListener("click", function(e){ - var elem = this.parentNode; - var name = elem.querySelector(".name").value; - var type = elem.querySelector(".type").value; - if(!name || node.findInputSlot(name) != -1) - return; - node.addInput(name,type); - elem.querySelector(".name").value = ""; - elem.querySelector(".type").value = ""; - inner_refresh(); - }); - - inner_refresh(); - this.canvas.parentNode.appendChild(panel); - return panel; - } - LGraphCanvas.prototype.showSubgraphPropertiesDialogRight = function (node) { - - // console.log("showing subgraph properties dialog"); - var that = this; - // old_panel if old_panel is exist close it - var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog"); - if (old_panel) - old_panel.close(); - // new panel - var panel = this.createPanel("Subgraph Outputs", { closable: true, width: 500 }); - panel.node = node; - panel.classList.add("subgraph_dialog"); - - function inner_refresh() { - panel.clear(); - //show currents - if (node.outputs) - for (var i = 0; i < node.outputs.length; ++i) { - var input = node.outputs[i]; - if (input.not_subgraph_output) - continue; - var html = " "; - var elem = panel.addHTML(html, "subgraph_property"); - elem.dataset["name"] = input.name; - elem.dataset["slot"] = i; - elem.querySelector(".name").innerText = input.name; - elem.querySelector(".type").innerText = input.type; - elem.querySelector("button").addEventListener("click", function (e) { - node.removeOutput(Number(this.parentNode.dataset["slot"])); - inner_refresh(); - }); - } - } - - //add extra - var html = " + NameType"; - var elem = panel.addHTML(html, "subgraph_property extra", true); - elem.querySelector(".name").addEventListener("keydown", function (e) { - if (e.keyCode == 13) { - addOutput.apply(this) - } - }) - elem.querySelector("button").addEventListener("click", function (e) { - addOutput.apply(this) - }); - function addOutput() { - var elem = this.parentNode; - var name = elem.querySelector(".name").value; - var type = elem.querySelector(".type").value; - if (!name || node.findOutputSlot(name) != -1) - return; - node.addOutput(name, type); - elem.querySelector(".name").value = ""; - elem.querySelector(".type").value = ""; - inner_refresh(); - } - + panel.alt_content.innerHTML = ""; + var textarea = panel.alt_content.querySelector("textarea"); + var fDoneWith = function () { + panel.toggleAltContent(false); //if(node_prop_div) node_prop_div.style.display = "block"; // panel.close(); + panel.toggleFooterVisibility(true); + textarea.parentNode.removeChild(textarea); + panel.classList.add("settings"); + panel.classList.remove("centered"); inner_refresh(); - this.canvas.parentNode.appendChild(panel); - return panel; + }; + textarea.value = node.properties[propname]; + textarea.addEventListener("keydown", function (e) { + if (e.code == "Enter" && e.ctrlKey) { + node.setProperty(propname, textarea.value); + fDoneWith(); + } + }); + panel.toggleAltContent(true); + panel.toggleFooterVisibility(false); + textarea.style.height = "calc(100% - 40px)"; + /*}*/ + var assign = panel.addButton("Assign", function () { + node.setProperty(propname, textarea.value); + fDoneWith(); + }); + panel.alt_content.appendChild(assign); //panel.content.appendChild(assign); + var button = panel.addButton("Close", fDoneWith); + button.style.float = "right"; + panel.alt_content.appendChild(button); // panel.content.appendChild(button); + }; + + inner_refresh(); + + this.canvas.parentNode.appendChild(panel); + }; + + LGraphCanvas.prototype.showSubgraphPropertiesDialog = function (node) { + console.log("showing subgraph properties dialog"); + + var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog"); + if (old_panel) old_panel.close(); + + var panel = this.createPanel("Subgraph Inputs", { + closable: true, + width: 500, + }); + panel.node = node; + panel.classList.add("subgraph_dialog"); + + function inner_refresh() { + panel.clear(); + + //show currents + if (node.inputs) + for (var i = 0; i < node.inputs.length; ++i) { + var input = node.inputs[i]; + if (input.not_subgraph_input) continue; + var html = + " "; + var elem = panel.addHTML(html, "subgraph_property"); + elem.dataset["name"] = input.name; + elem.dataset["slot"] = i; + elem.querySelector(".name").innerText = input.name; + elem.querySelector(".type").innerText = input.type; + elem.querySelector("button").addEventListener("click", function (e) { + node.removeInput(Number(this.parentNode.dataset["slot"])); + inner_refresh(); + }); + } } - LGraphCanvas.prototype.checkPanels = function() - { - if(!this.canvas) - return; - var panels = this.canvas.parentNode.querySelectorAll(".litegraph.dialog"); - for(var i = 0; i < panels.length; ++i) - { - var panel = panels[i]; - if( !panel.node ) - continue; - if( !panel.node.graph || panel.graph != this.graph ) - panel.close(); - } - } - LGraphCanvas.onMenuNodeCollapse = function(value, options, e, menu, node) { - node.graph.beforeChange(/*?*/); - - var fApplyMultiNode = function(node){ - node.collapse(); - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - node.graph.afterChange(/*?*/); - }; + //add extra + var html = + " + NameType"; + var elem = panel.addHTML(html, "subgraph_property extra", true); + elem.querySelector("button").addEventListener("click", function (e) { + var elem = this.parentNode; + var name = elem.querySelector(".name").value; + var type = elem.querySelector(".type").value; + if (!name || node.findInputSlot(name) != -1) return; + node.addInput(name, type); + elem.querySelector(".name").value = ""; + elem.querySelector(".type").value = ""; + inner_refresh(); + }); - LGraphCanvas.onMenuNodePin = function(value, options, e, menu, node) { - node.pin(); - }; + inner_refresh(); + this.canvas.parentNode.appendChild(panel); + return panel; + }; + LGraphCanvas.prototype.showSubgraphPropertiesDialogRight = function (node) { + // console.log("showing subgraph properties dialog"); + var that = this; + // old_panel if old_panel is exist close it + var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog"); + if (old_panel) old_panel.close(); + // new panel + var panel = this.createPanel("Subgraph Outputs", { + closable: true, + width: 500, + }); + panel.node = node; + panel.classList.add("subgraph_dialog"); - LGraphCanvas.onMenuNodeMode = function(value, options, e, menu, node) { - new LiteGraph.ContextMenu( - LiteGraph.NODE_MODES, - { event: e, callback: inner_clicked, parentMenu: menu, node: node } - ); - - function inner_clicked(v) { - if (!node) { - return; - } - var kV = Object.values(LiteGraph.NODE_MODES).indexOf(v); - var fApplyMultiNode = function(node){ - if (kV>=0 && LiteGraph.NODE_MODES[kV]) - node.changeMode(kV); - else{ - console.warn("unexpected mode: "+v); - node.changeMode(LiteGraph.ALWAYS); - } - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } + function inner_refresh() { + panel.clear(); + //show currents + if (node.outputs) + for (var i = 0; i < node.outputs.length; ++i) { + var input = node.outputs[i]; + if (input.not_subgraph_output) continue; + var html = + " "; + var elem = panel.addHTML(html, "subgraph_property"); + elem.dataset["name"] = input.name; + elem.dataset["slot"] = i; + elem.querySelector(".name").innerText = input.name; + elem.querySelector(".type").innerText = input.type; + elem.querySelector("button").addEventListener("click", function (e) { + node.removeOutput(Number(this.parentNode.dataset["slot"])); + inner_refresh(); + }); } + } - return false; + //add extra + var html = + " + NameType"; + var elem = panel.addHTML(html, "subgraph_property extra", true); + elem.querySelector(".name").addEventListener("keydown", function (e) { + if (e.keyCode == 13) { + addOutput.apply(this); + } + }); + elem.querySelector("button").addEventListener("click", function (e) { + addOutput.apply(this); + }); + function addOutput() { + var elem = this.parentNode; + var name = elem.querySelector(".name").value; + var type = elem.querySelector(".type").value; + if (!name || node.findOutputSlot(name) != -1) return; + node.addOutput(name, type); + elem.querySelector(".name").value = ""; + elem.querySelector(".type").value = ""; + inner_refresh(); + } + + inner_refresh(); + this.canvas.parentNode.appendChild(panel); + return panel; + }; + LGraphCanvas.prototype.checkPanels = function () { + if (!this.canvas) return; + var panels = this.canvas.parentNode.querySelectorAll(".litegraph.dialog"); + for (var i = 0; i < panels.length; ++i) { + var panel = panels[i]; + if (!panel.node) continue; + if (!panel.node.graph || panel.graph != this.graph) panel.close(); + } + }; + + LGraphCanvas.onMenuNodeCollapse = function (value, options, e, menu, node) { + node.graph.beforeChange(/*?*/); + + var fApplyMultiNode = function (node) { + node.collapse(); }; - LGraphCanvas.onMenuNodeColors = function(value, options, e, menu, node) { - if (!node) { - throw "no node for color"; + var graphcanvas = LGraphCanvas.active_canvas; + if ( + !graphcanvas.selected_nodes || + Object.keys(graphcanvas.selected_nodes).length <= 1 + ) { + fApplyMultiNode(node); + } else { + for (var i in graphcanvas.selected_nodes) { + fApplyMultiNode(graphcanvas.selected_nodes[i]); + } + } + + node.graph.afterChange(/*?*/); + }; + + LGraphCanvas.onMenuNodePin = function (value, options, e, menu, node) { + node.pin(); + }; + + LGraphCanvas.onMenuNodeMode = function (value, options, e, menu, node) { + new LiteGraph.ContextMenu(LiteGraph.NODE_MODES, { + event: e, + callback: inner_clicked, + parentMenu: menu, + node: node, + }); + + function inner_clicked(v) { + if (!node) { + return; + } + var kV = Object.values(LiteGraph.NODE_MODES).indexOf(v); + var fApplyMultiNode = function (node) { + if (kV >= 0 && LiteGraph.NODE_MODES[kV]) node.changeMode(kV); + else { + console.warn("unexpected mode: " + v); + node.changeMode(LiteGraph.ALWAYS); } + }; - var values = []; - values.push({ - value: null, - content: - "No color" - }); - - for (var i in LGraphCanvas.node_colors) { - var color = LGraphCanvas.node_colors[i]; - var value = { - value: i, - content: - "" + - i + - "" - }; - values.push(value); + var graphcanvas = LGraphCanvas.active_canvas; + if ( + !graphcanvas.selected_nodes || + Object.keys(graphcanvas.selected_nodes).length <= 1 + ) { + fApplyMultiNode(node); + } else { + for (var i in graphcanvas.selected_nodes) { + fApplyMultiNode(graphcanvas.selected_nodes[i]); } - new LiteGraph.ContextMenu(values, { - event: e, - callback: inner_clicked, - parentMenu: menu, - node: node - }); + } + } - function inner_clicked(v) { - if (!node) { - return; - } + return false; + }; - var color = v.value ? LGraphCanvas.node_colors[v.value] : null; - - var fApplyColor = function(node){ - if (color) { - if (node.constructor === LiteGraph.LGraphGroup) { - node.color = color.groupcolor; - } else { - node.color = color.color; - node.bgcolor = color.bgcolor; - } - } else { - delete node.color; - delete node.bgcolor; - } - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyColor(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyColor(graphcanvas.selected_nodes[i]); - } - } - node.setDirtyCanvas(true, true); - } + LGraphCanvas.onMenuNodeColors = function (value, options, e, menu, node) { + if (!node) { + throw "no node for color"; + } - return false; - }; + var values = []; + values.push({ + value: null, + content: + "No color", + }); - LGraphCanvas.onMenuNodeShapes = function(value, options, e, menu, node) { - if (!node) { - throw "no node passed"; - } + for (var i in LGraphCanvas.node_colors) { + var color = LGraphCanvas.node_colors[i]; + var value = { + value: i, + content: + "" + + i + + "", + }; + values.push(value); + } + new LiteGraph.ContextMenu(values, { + event: e, + callback: inner_clicked, + parentMenu: menu, + node: node, + }); - new LiteGraph.ContextMenu(LiteGraph.VALID_SHAPES, { - event: e, - callback: inner_clicked, - parentMenu: menu, - node: node - }); + function inner_clicked(v) { + if (!node) { + return; + } - function inner_clicked(v) { - if (!node) { - return; - } - node.graph.beforeChange(/*?*/); //node - - var fApplyMultiNode = function(node){ - node.shape = v; - } + var color = v.value ? LGraphCanvas.node_colors[v.value] : null; - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - node.graph.afterChange(/*?*/); //node - node.setDirtyCanvas(true); - } - - return false; - }; - - LGraphCanvas.onMenuNodeRemove = function(value, options, e, menu, node) { - if (!node) { - throw "no node passed"; - } - - var graph = node.graph; - graph.beforeChange(); - - - var fApplyMultiNode = function(node){ - if (node.removable === false) { - return; - } - graph.remove(node); - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - graph.afterChange(); - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.onMenuNodeToSubgraph = function(value, options, e, menu, node) { - var graph = node.graph; - var graphcanvas = LGraphCanvas.active_canvas; - if(!graphcanvas) //?? - return; - - var nodes_list = Object.values( graphcanvas.selected_nodes || {} ); - if( !nodes_list.length ) - nodes_list = [ node ]; - - var subgraph_node = LiteGraph.createNode("graph/subgraph"); - subgraph_node.pos = node.pos.concat(); - graph.add(subgraph_node); - - subgraph_node.buildFromNodes( nodes_list ); - - graphcanvas.deselectAllNodes(); - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.onMenuNodeClone = function(value, options, e, menu, node) { - - node.graph.beforeChange(); - - var newSelected = {}; - - var fApplyMultiNode = function(node){ - if (node.clonable === false) { - return; - } - var newnode = node.clone(); - if (!newnode) { - return; - } - newnode.pos = [node.pos[0] + 5, node.pos[1] + 5]; - node.graph.add(newnode); - newSelected[newnode.id] = newnode; - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - if(Object.keys(newSelected).length){ - graphcanvas.selectNodes(newSelected); - } - - node.graph.afterChange(); - - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.node_colors = { - red: { color: "#322", bgcolor: "#533", groupcolor: "#A88" }, - brown: { color: "#332922", bgcolor: "#593930", groupcolor: "#b06634" }, - green: { color: "#232", bgcolor: "#353", groupcolor: "#8A8" }, - blue: { color: "#223", bgcolor: "#335", groupcolor: "#88A" }, - pale_blue: { - color: "#2a363b", - bgcolor: "#3f5159", - groupcolor: "#3f789e" - }, - cyan: { color: "#233", bgcolor: "#355", groupcolor: "#8AA" }, - purple: { color: "#323", bgcolor: "#535", groupcolor: "#a1309b" }, - yellow: { color: "#432", bgcolor: "#653", groupcolor: "#b58b2a" }, - black: { color: "#222", bgcolor: "#000", groupcolor: "#444" } - }; - - LGraphCanvas.prototype.getCanvasMenuOptions = function() { - var options = null; - var that = this; - if (this.getMenuOptions) { - options = this.getMenuOptions(); + var fApplyColor = function (node) { + if (color) { + if (node.constructor === LiteGraph.LGraphGroup) { + node.color = color.groupcolor; + } else { + node.color = color.color; + node.bgcolor = color.bgcolor; + } } else { - options = [ - { - content: "Add Node", - has_submenu: true, - callback: LGraphCanvas.onMenuAdd - }, - { content: "Add Group", callback: LGraphCanvas.onGroupAdd }, - //{ content: "Arrange", callback: that.graph.arrange }, - //{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll } - ]; - /*if (LiteGraph.showCanvasOptions){ + delete node.color; + delete node.bgcolor; + } + }; + + var graphcanvas = LGraphCanvas.active_canvas; + if ( + !graphcanvas.selected_nodes || + Object.keys(graphcanvas.selected_nodes).length <= 1 + ) { + fApplyColor(node); + } else { + for (var i in graphcanvas.selected_nodes) { + fApplyColor(graphcanvas.selected_nodes[i]); + } + } + node.setDirtyCanvas(true, true); + } + + return false; + }; + + LGraphCanvas.onMenuNodeShapes = function (value, options, e, menu, node) { + if (!node) { + throw "no node passed"; + } + + new LiteGraph.ContextMenu(LiteGraph.VALID_SHAPES, { + event: e, + callback: inner_clicked, + parentMenu: menu, + node: node, + }); + + function inner_clicked(v) { + if (!node) { + return; + } + node.graph.beforeChange(/*?*/); //node + + var fApplyMultiNode = function (node) { + node.shape = v; + }; + + var graphcanvas = LGraphCanvas.active_canvas; + if ( + !graphcanvas.selected_nodes || + Object.keys(graphcanvas.selected_nodes).length <= 1 + ) { + fApplyMultiNode(node); + } else { + for (var i in graphcanvas.selected_nodes) { + fApplyMultiNode(graphcanvas.selected_nodes[i]); + } + } + + node.graph.afterChange(/*?*/); //node + node.setDirtyCanvas(true); + } + + return false; + }; + + LGraphCanvas.onMenuNodeRemove = function (value, options, e, menu, node) { + if (!node) { + throw "no node passed"; + } + + var graph = node.graph; + graph.beforeChange(); + + var fApplyMultiNode = function (node) { + if (node.removable === false) { + return; + } + graph.remove(node); + }; + + var graphcanvas = LGraphCanvas.active_canvas; + if ( + !graphcanvas.selected_nodes || + Object.keys(graphcanvas.selected_nodes).length <= 1 + ) { + fApplyMultiNode(node); + } else { + for (var i in graphcanvas.selected_nodes) { + fApplyMultiNode(graphcanvas.selected_nodes[i]); + } + } + + graph.afterChange(); + node.setDirtyCanvas(true, true); + }; + + LGraphCanvas.onMenuNodeToSubgraph = function (value, options, e, menu, node) { + var graph = node.graph; + var graphcanvas = LGraphCanvas.active_canvas; + if (!graphcanvas) + //?? + return; + + var nodes_list = Object.values(graphcanvas.selected_nodes || {}); + if (!nodes_list.length) nodes_list = [node]; + + var subgraph_node = LiteGraph.createNode("graph/subgraph"); + subgraph_node.pos = node.pos.concat(); + graph.add(subgraph_node); + + subgraph_node.buildFromNodes(nodes_list); + + graphcanvas.deselectAllNodes(); + node.setDirtyCanvas(true, true); + }; + + LGraphCanvas.onMenuNodeClone = function (value, options, e, menu, node) { + node.graph.beforeChange(); + + var newSelected = {}; + + var fApplyMultiNode = function (node) { + if (node.clonable === false) { + return; + } + var newnode = node.clone(); + if (!newnode) { + return; + } + newnode.pos = [node.pos[0] + 5, node.pos[1] + 5]; + node.graph.add(newnode); + newSelected[newnode.id] = newnode; + }; + + var graphcanvas = LGraphCanvas.active_canvas; + if ( + !graphcanvas.selected_nodes || + Object.keys(graphcanvas.selected_nodes).length <= 1 + ) { + fApplyMultiNode(node); + } else { + for (var i in graphcanvas.selected_nodes) { + fApplyMultiNode(graphcanvas.selected_nodes[i]); + } + } + + if (Object.keys(newSelected).length) { + graphcanvas.selectNodes(newSelected); + } + + node.graph.afterChange(); + + node.setDirtyCanvas(true, true); + }; + + LGraphCanvas.node_colors = { + red: { color: "#322", bgcolor: "#533", groupcolor: "#A88" }, + brown: { color: "#332922", bgcolor: "#593930", groupcolor: "#b06634" }, + green: { color: "#232", bgcolor: "#353", groupcolor: "#8A8" }, + blue: { color: "#223", bgcolor: "#335", groupcolor: "#88A" }, + pale_blue: { + color: "#2a363b", + bgcolor: "#3f5159", + groupcolor: "#3f789e", + }, + cyan: { color: "#233", bgcolor: "#355", groupcolor: "#8AA" }, + purple: { color: "#323", bgcolor: "#535", groupcolor: "#a1309b" }, + yellow: { color: "#432", bgcolor: "#653", groupcolor: "#b58b2a" }, + black: { color: "#222", bgcolor: "#000", groupcolor: "#444" }, + }; + + LGraphCanvas.prototype.getCanvasMenuOptions = function () { + var options = null; + var that = this; + if (this.getMenuOptions) { + options = this.getMenuOptions(); + } else { + options = [ + { + content: "Add Node", + has_submenu: true, + callback: LGraphCanvas.onMenuAdd, + }, + { content: "Add Group", callback: LGraphCanvas.onGroupAdd }, + //{ content: "Arrange", callback: that.graph.arrange }, + //{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll } + ]; + /*if (LiteGraph.showCanvasOptions){ options.push({ content: "Options", callback: that.showShowGraphOptionsPanel }); }*/ - if (Object.keys(this.selected_nodes).length > 1) { - options.push({ - content: "Align", - has_submenu: true, - callback: LGraphCanvas.onGroupAlign, - }) - } + if (Object.keys(this.selected_nodes).length > 1) { + options.push({ + content: "Align", + has_submenu: true, + callback: LGraphCanvas.onGroupAlign, + }); + } - if (this._graph_stack && this._graph_stack.length > 0) { - options.push(null, { - content: "Close subgraph", - callback: this.closeSubgraph.bind(this) - }); - } - } + if (this._graph_stack && this._graph_stack.length > 0) { + options.push(null, { + content: "Close subgraph", + callback: this.closeSubgraph.bind(this), + }); + } + } - if (this.getExtraMenuOptions) { - var extra = this.getExtraMenuOptions(this, options); - if (extra) { - options = options.concat(extra); - } - } + if (this.getExtraMenuOptions) { + var extra = this.getExtraMenuOptions(this, options); + if (extra) { + options = options.concat(extra); + } + } - return options; + return options; + }; + + //called by processContextMenu to extract the menu list + LGraphCanvas.prototype.getNodeMenuOptions = function (node) { + var options = null; + + if (node.getMenuOptions) { + options = node.getMenuOptions(this); + } else { + options = [ + { + content: "Inputs", + has_submenu: true, + disabled: true, + callback: LGraphCanvas.showMenuNodeOptionalInputs, + }, + { + content: "Outputs", + has_submenu: true, + disabled: true, + callback: LGraphCanvas.showMenuNodeOptionalOutputs, + }, + null, + { + content: "Properties", + has_submenu: true, + callback: LGraphCanvas.onShowMenuNodeProperties, + }, + { + content: "Properties Panel", + callback: function (item, options, e, menu, node) { + LGraphCanvas.active_canvas.showShowNodePanel(node); + }, + }, + null, + { + content: "Title", + callback: LGraphCanvas.onShowPropertyEditor, + }, + { + content: "Mode", + has_submenu: true, + callback: LGraphCanvas.onMenuNodeMode, + }, + ]; + if (node.resizable !== false) { + options.push({ + content: "Resize", + callback: LGraphCanvas.onMenuResizeNode, + }); + } + options.push( + { + content: "Collapse", + callback: LGraphCanvas.onMenuNodeCollapse, + }, + { content: "Pin", callback: LGraphCanvas.onMenuNodePin }, + { + content: "Colors", + has_submenu: true, + callback: LGraphCanvas.onMenuNodeColors, + }, + { + content: "Shapes", + has_submenu: true, + callback: LGraphCanvas.onMenuNodeShapes, + }, + null + ); + } + + if (node.onGetInputs) { + var inputs = node.onGetInputs(); + if (inputs && inputs.length) { + options[0].disabled = false; + } + } + + if (node.onGetOutputs) { + var outputs = node.onGetOutputs(); + if (outputs && outputs.length) { + options[1].disabled = false; + } + } + + if (node.getExtraMenuOptions) { + var extra = node.getExtraMenuOptions(this, options); + if (extra) { + extra.push(null); + options = extra.concat(options); + } + } + + if (node.clonable !== false) { + options.push({ + content: "Clone", + callback: LGraphCanvas.onMenuNodeClone, + }); + } + + if (0) + //TODO + options.push({ + content: "To Subgraph", + callback: LGraphCanvas.onMenuNodeToSubgraph, + }); + + if (Object.keys(this.selected_nodes).length > 1) { + options.push({ + content: "Align Selected To", + has_submenu: true, + callback: LGraphCanvas.onNodeAlign, + }); + } + + options.push(null, { + content: "Remove", + disabled: !(node.removable !== false && !node.block_delete), + callback: LGraphCanvas.onMenuNodeRemove, + }); + + if (node.graph && node.graph.onGetNodeMenuOptions) { + node.graph.onGetNodeMenuOptions(options, node); + } + + return options; + }; + + LGraphCanvas.prototype.getGroupMenuOptions = function (node) { + var o = [ + { content: "Title", callback: LGraphCanvas.onShowPropertyEditor }, + { + content: "Color", + has_submenu: true, + callback: LGraphCanvas.onMenuNodeColors, + }, + { + content: "Font size", + property: "font_size", + type: "Number", + callback: LGraphCanvas.onShowPropertyEditor, + }, + null, + { content: "Remove", callback: LGraphCanvas.onMenuNodeRemove }, + ]; + + return o; + }; + + LGraphCanvas.prototype.processContextMenu = function (node, event) { + var that = this; + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); + + var menu_info = null; + var options = { + event: event, + callback: inner_option_clicked, + extra: node, }; - //called by processContextMenu to extract the menu list - LGraphCanvas.prototype.getNodeMenuOptions = function(node) { - var options = null; + if (node) options.title = node.type; - if (node.getMenuOptions) { - options = node.getMenuOptions(this); - } else { - options = [ - { - content: "Inputs", - has_submenu: true, - disabled: true, - callback: LGraphCanvas.showMenuNodeOptionalInputs - }, - { - content: "Outputs", - has_submenu: true, - disabled: true, - callback: LGraphCanvas.showMenuNodeOptionalOutputs - }, - null, - { - content: "Properties", - has_submenu: true, - callback: LGraphCanvas.onShowMenuNodeProperties - }, - { - content: "Properties Panel", - callback: function(item, options, e, menu, node) { LGraphCanvas.active_canvas.showShowNodePanel(node) } - }, - null, - { - content: "Title", - callback: LGraphCanvas.onShowPropertyEditor - }, - { - content: "Mode", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeMode - }]; - if(node.resizable !== false){ - options.push({ - content: "Resize", callback: LGraphCanvas.onMenuResizeNode - }); - } - options.push( - { - content: "Collapse", - callback: LGraphCanvas.onMenuNodeCollapse - }, - { content: "Pin", callback: LGraphCanvas.onMenuNodePin }, - { - content: "Colors", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeColors - }, - { - content: "Shapes", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeShapes - }, - null - ); + //check if mouse is in input + var slot = null; + if (node) { + slot = node.getSlotInPosition(event.canvasX, event.canvasY); + LGraphCanvas.active_node = node; + } + + if (slot) { + //on slot + menu_info = []; + if (node.getSlotMenuOptions) { + menu_info = node.getSlotMenuOptions(slot); + } else { + if ( + slot && + slot.output && + slot.output.links && + slot.output.links.length + ) { + menu_info.push({ content: "Disconnect Links", slot: slot }); } - - if (node.onGetInputs) { - var inputs = node.onGetInputs(); - if (inputs && inputs.length) { - options[0].disabled = false; - } + var _slot = slot.input || slot.output; + if (_slot.removable) { + menu_info.push( + _slot.locked + ? "Cannot remove" + : { content: "Remove Slot", slot: slot } + ); } - - if (node.onGetOutputs) { - var outputs = node.onGetOutputs(); - if (outputs && outputs.length) { - options[1].disabled = false; - } + if (!_slot.nameLocked) { + menu_info.push({ content: "Rename Slot", slot: slot }); } - - if (node.getExtraMenuOptions) { - var extra = node.getExtraMenuOptions(this, options); - if (extra) { - extra.push(null); - options = extra.concat(options); - } - } - - if (node.clonable !== false) { - options.push({ - content: "Clone", - callback: LGraphCanvas.onMenuNodeClone - }); - } - - if(0) //TODO - options.push({ - content: "To Subgraph", - callback: LGraphCanvas.onMenuNodeToSubgraph - }); - - if (Object.keys(this.selected_nodes).length > 1) { - options.push({ - content: "Align Selected To", - has_submenu: true, - callback: LGraphCanvas.onNodeAlign, - }) - } - - options.push(null, { - content: "Remove", - disabled: !(node.removable !== false && !node.block_delete ), - callback: LGraphCanvas.onMenuNodeRemove - }); - - if (node.graph && node.graph.onGetNodeMenuOptions) { - node.graph.onGetNodeMenuOptions(options, node); - } - - return options; - }; - - LGraphCanvas.prototype.getGroupMenuOptions = function(node) { - var o = [ - { content: "Title", callback: LGraphCanvas.onShowPropertyEditor }, - { - content: "Color", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeColors + } + options.title = (slot.input ? slot.input.type : slot.output.type) || "*"; + if (slot.input && slot.input.type == LiteGraph.ACTION) { + options.title = "Action"; + } + if (slot.output && slot.output.type == LiteGraph.EVENT) { + options.title = "Event"; + } + } else { + if (node) { + //on node + menu_info = this.getNodeMenuOptions(node); + } else { + menu_info = this.getCanvasMenuOptions(); + var group = this.graph.getGroupOnPos(event.canvasX, event.canvasY); + if (group) { + //on group + menu_info.push(null, { + content: "Edit Group", + has_submenu: true, + submenu: { + title: "Group", + extra: group, + options: this.getGroupMenuOptions(group), }, - { - content: "Font size", - property: "font_size", - type: "Number", - callback: LGraphCanvas.onShowPropertyEditor - }, - null, - { content: "Remove", callback: LGraphCanvas.onMenuNodeRemove } - ]; + }); + } + } + } - return o; - }; + //show menu + if (!menu_info) { + return; + } - LGraphCanvas.prototype.processContextMenu = function(node, event) { - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); + var menu = new LiteGraph.ContextMenu(menu_info, options, ref_window); - var menu_info = null; - var options = { - event: event, - callback: inner_option_clicked, - extra: node + function inner_option_clicked(v, options, e) { + if (!v) { + return; + } + + if (v.content == "Remove Slot") { + var info = v.slot; + node.graph.beforeChange(); + if (info.input) { + node.removeInput(info.slot); + } else if (info.output) { + node.removeOutput(info.slot); + } + node.graph.afterChange(); + return; + } else if (v.content == "Disconnect Links") { + var info = v.slot; + node.graph.beforeChange(); + if (info.output) { + node.disconnectOutput(info.slot); + } else if (info.input) { + node.disconnectInput(info.slot); + } + node.graph.afterChange(); + return; + } else if (v.content == "Rename Slot") { + var info = v.slot; + var slot_info = info.input + ? node.getInputInfo(info.slot) + : node.getOutputInfo(info.slot); + var dialog = that.createDialog( + "Name", + options + ); + var input = dialog.querySelector("input"); + if (input && slot_info) { + input.value = slot_info.label || ""; + } + var inner = function () { + node.graph.beforeChange(); + if (input.value) { + if (slot_info) { + slot_info.label = input.value; + } + that.setDirty(true); + } + dialog.close(); + node.graph.afterChange(); }; - - if(node) - options.title = node.type; - - //check if mouse is in input - var slot = null; - if (node) { - slot = node.getSlotInPosition(event.canvasX, event.canvasY); - LGraphCanvas.active_node = node; - } - - if (slot) { - //on slot - menu_info = []; - if (node.getSlotMenuOptions) { - menu_info = node.getSlotMenuOptions(slot); - } else { - if ( - slot && - slot.output && - slot.output.links && - slot.output.links.length - ) { - menu_info.push({ content: "Disconnect Links", slot: slot }); - } - var _slot = slot.input || slot.output; - if (_slot.removable){ - menu_info.push( - _slot.locked - ? "Cannot remove" - : { content: "Remove Slot", slot: slot } - ); - } - if (!_slot.nameLocked){ - menu_info.push({ content: "Rename Slot", slot: slot }); - } - - } - options.title = - (slot.input ? slot.input.type : slot.output.type) || "*"; - if (slot.input && slot.input.type == LiteGraph.ACTION) { - options.title = "Action"; - } - if (slot.output && slot.output.type == LiteGraph.EVENT) { - options.title = "Event"; - } - } else { - if (node) { - //on node - menu_info = this.getNodeMenuOptions(node); - } else { - menu_info = this.getCanvasMenuOptions(); - var group = this.graph.getGroupOnPos( - event.canvasX, - event.canvasY - ); - if (group) { - //on group - menu_info.push(null, { - content: "Edit Group", - has_submenu: true, - submenu: { - title: "Group", - extra: group, - options: this.getGroupMenuOptions(group) - } - }); - } - } - } - - //show menu - if (!menu_info) { + dialog.querySelector("button").addEventListener("click", inner); + input.addEventListener("keydown", function (e) { + dialog.is_modified = true; + if (e.keyCode == 27) { + //ESC + dialog.close(); + } else if (e.keyCode == 13) { + inner(); // save + } else if (e.keyCode != 13 && e.target.localName != "textarea") { return; - } + } + e.preventDefault(); + e.stopPropagation(); + }); + input.focus(); + } - var menu = new LiteGraph.ContextMenu(menu_info, options, ref_window); + //if(v.callback) + // return v.callback.call(that, node, options, e, menu, that, event ); + } + }; - function inner_option_clicked(v, options, e) { - if (!v) { - return; - } + //API ************************************************* + //like rect but rounded corners + if ( + typeof window != "undefined" && + window.CanvasRenderingContext2D && + !window.CanvasRenderingContext2D.prototype.roundRect + ) { + window.CanvasRenderingContext2D.prototype.roundRect = function ( + x, + y, + w, + h, + radius, + radius_low + ) { + var top_left_radius = 0; + var top_right_radius = 0; + var bottom_left_radius = 0; + var bottom_right_radius = 0; - if (v.content == "Remove Slot") { - var info = v.slot; - node.graph.beforeChange(); - if (info.input) { - node.removeInput(info.slot); - } else if (info.output) { - node.removeOutput(info.slot); - } - node.graph.afterChange(); - return; - } else if (v.content == "Disconnect Links") { - var info = v.slot; - node.graph.beforeChange(); - if (info.output) { - node.disconnectOutput(info.slot); - } else if (info.input) { - node.disconnectInput(info.slot); - } - node.graph.afterChange(); - return; - } else if (v.content == "Rename Slot") { - var info = v.slot; - var slot_info = info.input - ? node.getInputInfo(info.slot) - : node.getOutputInfo(info.slot); - var dialog = that.createDialog( - "Name", - options - ); - var input = dialog.querySelector("input"); - if (input && slot_info) { - input.value = slot_info.label || ""; - } - var inner = function(){ - node.graph.beforeChange(); - if (input.value) { - if (slot_info) { - slot_info.label = input.value; - } - that.setDirty(true); - } - dialog.close(); - node.graph.afterChange(); - } - dialog.querySelector("button").addEventListener("click", inner); - input.addEventListener("keydown", function(e) { - dialog.is_modified = true; - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - inner(); // save - } else if (e.keyCode != 13 && e.target.localName != "textarea") { - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - input.focus(); - } + if (radius === 0) { + this.rect(x, y, w, h); + return; + } - //if(v.callback) - // return v.callback.call(that, node, options, e, menu, that, event ); - } + if (radius_low === undefined) radius_low = radius; + + //make it compatible with official one + if (radius != null && radius.constructor === Array) { + if (radius.length == 1) + top_left_radius = + top_right_radius = + bottom_left_radius = + bottom_right_radius = + radius[0]; + else if (radius.length == 2) { + top_left_radius = bottom_right_radius = radius[0]; + top_right_radius = bottom_left_radius = radius[1]; + } else if (radius.length == 4) { + top_left_radius = radius[0]; + top_right_radius = radius[1]; + bottom_left_radius = radius[2]; + bottom_right_radius = radius[3]; + } else return; + } //old using numbers + else { + top_left_radius = radius || 0; + top_right_radius = radius || 0; + bottom_left_radius = radius_low || 0; + bottom_right_radius = radius_low || 0; + } + + //top right + this.moveTo(x + top_left_radius, y); + this.lineTo(x + w - top_right_radius, y); + this.quadraticCurveTo(x + w, y, x + w, y + top_right_radius); + + //bottom right + this.lineTo(x + w, y + h - bottom_right_radius); + this.quadraticCurveTo(x + w, y + h, x + w - bottom_right_radius, y + h); + + //bottom left + this.lineTo(x + bottom_right_radius, y + h); + this.quadraticCurveTo(x, y + h, x, y + h - bottom_left_radius); + + //top left + this.lineTo(x, y + bottom_left_radius); + this.quadraticCurveTo(x, y, x + top_left_radius, y); }; + } //if - //API ************************************************* - //like rect but rounded corners - if (typeof(window) != "undefined" && window.CanvasRenderingContext2D && !window.CanvasRenderingContext2D.prototype.roundRect) { - window.CanvasRenderingContext2D.prototype.roundRect = function( - x, - y, - w, - h, - radius, - radius_low - ) { - var top_left_radius = 0; - var top_right_radius = 0; - var bottom_left_radius = 0; - var bottom_right_radius = 0; - - if ( radius === 0 ) - { - this.rect(x,y,w,h); - return; - } - - if(radius_low === undefined) - radius_low = radius; - - //make it compatible with official one - if(radius != null && radius.constructor === Array) - { - if(radius.length == 1) - top_left_radius = top_right_radius = bottom_left_radius = bottom_right_radius = radius[0]; - else if(radius.length == 2) - { - top_left_radius = bottom_right_radius = radius[0]; - top_right_radius = bottom_left_radius = radius[1]; - } - else if(radius.length == 4) - { - top_left_radius = radius[0]; - top_right_radius = radius[1]; - bottom_left_radius = radius[2]; - bottom_right_radius = radius[3]; - } - else - return; - } - else //old using numbers - { - top_left_radius = radius || 0; - top_right_radius = radius || 0; - bottom_left_radius = radius_low || 0; - bottom_right_radius = radius_low || 0; - } - - //top right - this.moveTo(x + top_left_radius, y); - this.lineTo(x + w - top_right_radius, y); - this.quadraticCurveTo(x + w, y, x + w, y + top_right_radius); - - //bottom right - this.lineTo(x + w, y + h - bottom_right_radius); - this.quadraticCurveTo( - x + w, - y + h, - x + w - bottom_right_radius, - y + h - ); - - //bottom left - this.lineTo(x + bottom_right_radius, y + h); - this.quadraticCurveTo(x, y + h, x, y + h - bottom_left_radius); - - //top left - this.lineTo(x, y + bottom_left_radius); - this.quadraticCurveTo(x, y, x + top_left_radius, y); - }; - }//if - - function compareObjects(a, b) { - for (var i in a) { - if (a[i] != b[i]) { - return false; - } - } - return true; - } - LiteGraph.compareObjects = compareObjects; - - function distance(a, b) { - return Math.sqrt( - (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) - ); - } - LiteGraph.distance = distance; - - function colorToString(c) { - return ( - "rgba(" + - Math.round(c[0] * 255).toFixed() + - "," + - Math.round(c[1] * 255).toFixed() + - "," + - Math.round(c[2] * 255).toFixed() + - "," + - (c.length == 4 ? c[3].toFixed(2) : "1.0") + - ")" - ); - } - LiteGraph.colorToString = colorToString; - - function isInsideRectangle(x, y, left, top, width, height) { - if (left < x && left + width > x && top < y && top + height > y) { - return true; - } + function compareObjects(a, b) { + for (var i in a) { + if (a[i] != b[i]) { return false; + } } - LiteGraph.isInsideRectangle = isInsideRectangle; + return true; + } + LiteGraph.compareObjects = compareObjects; - //[minx,miny,maxx,maxy] - function growBounding(bounding, x, y) { - if (x < bounding[0]) { - bounding[0] = x; - } else if (x > bounding[2]) { - bounding[2] = x; - } + function distance(a, b) { + return Math.sqrt( + (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) + ); + } + LiteGraph.distance = distance; - if (y < bounding[1]) { - bounding[1] = y; - } else if (y > bounding[3]) { - bounding[3] = y; - } + function colorToString(c) { + return ( + "rgba(" + + Math.round(c[0] * 255).toFixed() + + "," + + Math.round(c[1] * 255).toFixed() + + "," + + Math.round(c[2] * 255).toFixed() + + "," + + (c.length == 4 ? c[3].toFixed(2) : "1.0") + + ")" + ); + } + LiteGraph.colorToString = colorToString; + + function isInsideRectangle(x, y, left, top, width, height) { + if (left < x && left + width > x && top < y && top + height > y) { + return true; } - LiteGraph.growBounding = growBounding; + return false; + } + LiteGraph.isInsideRectangle = isInsideRectangle; - //point inside bounding box - function isInsideBounding(p, bb) { - if ( - p[0] < bb[0][0] || - p[1] < bb[0][1] || - p[0] > bb[1][0] || - p[1] > bb[1][1] - ) { - return false; - } + //[minx,miny,maxx,maxy] + function growBounding(bounding, x, y) { + if (x < bounding[0]) { + bounding[0] = x; + } else if (x > bounding[2]) { + bounding[2] = x; + } + + if (y < bounding[1]) { + bounding[1] = y; + } else if (y > bounding[3]) { + bounding[3] = y; + } + } + LiteGraph.growBounding = growBounding; + + //point inside bounding box + function isInsideBounding(p, bb) { + if ( + p[0] < bb[0][0] || + p[1] < bb[0][1] || + p[0] > bb[1][0] || + p[1] > bb[1][1] + ) { + return false; + } + return true; + } + LiteGraph.isInsideBounding = isInsideBounding; + + //bounding overlap, format: [ startx, starty, width, height ] + function overlapBounding(a, b) { + var A_end_x = a[0] + a[2]; + var A_end_y = a[1] + a[3]; + var B_end_x = b[0] + b[2]; + var B_end_y = b[1] + b[3]; + + if (a[0] > B_end_x || a[1] > B_end_y || A_end_x < b[0] || A_end_y < b[1]) { + return false; + } + return true; + } + LiteGraph.overlapBounding = overlapBounding; + + //Convert a hex value to its decimal value - the inputted hex must be in the + // format of a hex triplet - the kind we use for HTML colours. The function + // will return an array with three values. + function hex2num(hex) { + if (hex.charAt(0) == "#") { + hex = hex.slice(1); + } //Remove the '#' char - if there is one. + hex = hex.toUpperCase(); + var hex_alphabets = "0123456789ABCDEF"; + var value = new Array(3); + var k = 0; + var int1, int2; + for (var i = 0; i < 6; i += 2) { + int1 = hex_alphabets.indexOf(hex.charAt(i)); + int2 = hex_alphabets.indexOf(hex.charAt(i + 1)); + value[k] = int1 * 16 + int2; + k++; + } + return value; + } + + LiteGraph.hex2num = hex2num; + + //Give a array with three values as the argument and the function will return + // the corresponding hex triplet. + function num2hex(triplet) { + var hex_alphabets = "0123456789ABCDEF"; + var hex = "#"; + var int1, int2; + for (var i = 0; i < 3; i++) { + int1 = triplet[i] / 16; + int2 = triplet[i] % 16; + + hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2); + } + return hex; + } + + LiteGraph.num2hex = num2hex; + + /* LiteGraph GUI elements used for canvas editing *************************************/ + + /** + * ContextMenu from LiteGUI + * + * @class ContextMenu + * @constructor + * @param {Array} values (allows object { title: "Nice text", callback: function ... }) + * @param {Object} options [optional] Some options:\ + * - title: title to show on top of the menu + * - callback: function to call when an option is clicked, it receives the item information + * - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback + * - event: you can pass a MouseEvent, this way the ContextMenu appears in that position + */ + function ContextMenu(values, options) { + options = options || {}; + this.options = options; + var that = this; + + //to link a menu with its parent + if (options.parentMenu) { + if (options.parentMenu.constructor !== this.constructor) { + console.error("parentMenu must be of class ContextMenu, ignoring it"); + options.parentMenu = null; + } else { + this.parentMenu = options.parentMenu; + this.parentMenu.lock = true; + this.parentMenu.current_submenu = this; + } + } + + var eventClass = null; + if (options.event) + //use strings because comparing classes between windows doesnt work + eventClass = options.event.constructor.name; + if ( + eventClass !== "MouseEvent" && + eventClass !== "CustomEvent" && + eventClass !== "PointerEvent" + ) { + console.error( + "Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. (" + + eventClass + + ")" + ); + options.event = null; + } + + var root = document.createElement("div"); + root.className = "litegraph litecontextmenu litemenubar-panel"; + if (options.className) { + root.className += " " + options.className; + } + root.style.minWidth = 100; + root.style.minHeight = 100; + root.style.pointerEvents = "none"; + setTimeout(function () { + root.style.pointerEvents = "auto"; + }, 100); //delay so the mouse up event is not caught by this element + + //this prevents the default context browser menu to open in case this menu was created when pressing right button + LiteGraph.pointerListenerAdd( + root, + "up", + function (e) { + //console.log("pointerevents: ContextMenu up root prevent"); + e.preventDefault(); return true; - } - LiteGraph.isInsideBounding = isInsideBounding; - - //bounding overlap, format: [ startx, starty, width, height ] - function overlapBounding(a, b) { - var A_end_x = a[0] + a[2]; - var A_end_y = a[1] + a[3]; - var B_end_x = b[0] + b[2]; - var B_end_y = b[1] + b[3]; - - if ( - a[0] > B_end_x || - a[1] > B_end_y || - A_end_x < b[0] || - A_end_y < b[1] - ) { - return false; + }, + true + ); + root.addEventListener( + "contextmenu", + function (e) { + if (e.button != 2) { + //right button + return false; } - return true; - } - LiteGraph.overlapBounding = overlapBounding; + e.preventDefault(); + return false; + }, + true + ); - //Convert a hex value to its decimal value - the inputted hex must be in the - // format of a hex triplet - the kind we use for HTML colours. The function - // will return an array with three values. - function hex2num(hex) { - if (hex.charAt(0) == "#") { - hex = hex.slice(1); - } //Remove the '#' char - if there is one. - hex = hex.toUpperCase(); - var hex_alphabets = "0123456789ABCDEF"; - var value = new Array(3); - var k = 0; - var int1, int2; - for (var i = 0; i < 6; i += 2) { - int1 = hex_alphabets.indexOf(hex.charAt(i)); - int2 = hex_alphabets.indexOf(hex.charAt(i + 1)); - value[k] = int1 * 16 + int2; - k++; + LiteGraph.pointerListenerAdd( + root, + "down", + function (e) { + //console.log("pointerevents: ContextMenu down"); + if (e.button == 2) { + that.close(); + e.preventDefault(); + return true; } - return value; + }, + true + ); + + function on_mouse_wheel(e) { + var pos = parseInt(root.style.top); + root.style.top = (pos + e.deltaY * options.scroll_speed).toFixed() + "px"; + e.preventDefault(); + return true; } - LiteGraph.hex2num = hex2num; - - //Give a array with three values as the argument and the function will return - // the corresponding hex triplet. - function num2hex(triplet) { - var hex_alphabets = "0123456789ABCDEF"; - var hex = "#"; - var int1, int2; - for (var i = 0; i < 3; i++) { - int1 = triplet[i] / 16; - int2 = triplet[i] % 16; - - hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2); - } - return hex; + if (!options.scroll_speed) { + options.scroll_speed = 0.1; } - LiteGraph.num2hex = num2hex; + root.addEventListener("wheel", on_mouse_wheel, true); + root.addEventListener("mousewheel", on_mouse_wheel, true); - /* LiteGraph GUI elements used for canvas editing *************************************/ + this.root = root; - /** - * ContextMenu from LiteGUI - * - * @class ContextMenu - * @constructor - * @param {Array} values (allows object { title: "Nice text", callback: function ... }) - * @param {Object} options [optional] Some options:\ - * - title: title to show on top of the menu - * - callback: function to call when an option is clicked, it receives the item information - * - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback - * - event: you can pass a MouseEvent, this way the ContextMenu appears in that position - */ - function ContextMenu(values, options) { - options = options || {}; - this.options = options; - var that = this; + //title + if (options.title) { + var element = document.createElement("div"); + element.className = "litemenu-title"; + element.innerHTML = options.title; + root.appendChild(element); + } - //to link a menu with its parent - if (options.parentMenu) { - if (options.parentMenu.constructor !== this.constructor) { - console.error( - "parentMenu must be of class ContextMenu, ignoring it" - ); - options.parentMenu = null; - } else { - this.parentMenu = options.parentMenu; - this.parentMenu.lock = true; - this.parentMenu.current_submenu = this; - } - } + //entries + var num = 0; + for (var i = 0; i < values.length; i++) { + var name = values.constructor == Array ? values[i] : i; + if (name != null && name.constructor !== String) { + name = name.content === undefined ? String(name) : name.content; + } + var value = values[i]; + this.addItem(name, value, options); + num++; + } - var eventClass = null; - if(options.event) //use strings because comparing classes between windows doesnt work - eventClass = options.event.constructor.name; - if ( eventClass !== "MouseEvent" && - eventClass !== "CustomEvent" && - eventClass !== "PointerEvent" - ) { - console.error( - "Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. ("+eventClass+")" - ); - options.event = null; - } - - var root = document.createElement("div"); - root.className = "litegraph litecontextmenu litemenubar-panel"; - if (options.className) { - root.className += " " + options.className; - } - root.style.minWidth = 100; - root.style.minHeight = 100; - root.style.pointerEvents = "none"; - setTimeout(function() { - root.style.pointerEvents = "auto"; - }, 100); //delay so the mouse up event is not caught by this element - - //this prevents the default context browser menu to open in case this menu was created when pressing right button - LiteGraph.pointerListenerAdd(root,"up", - function(e) { - //console.log("pointerevents: ContextMenu up root prevent"); - e.preventDefault(); - return true; - }, - true - ); - root.addEventListener( - "contextmenu", - function(e) { - if (e.button != 2) { - //right button - return false; - } - e.preventDefault(); - return false; - }, - true - ); - - LiteGraph.pointerListenerAdd(root,"down", - function(e) { - //console.log("pointerevents: ContextMenu down"); - if (e.button == 2) { - that.close(); - e.preventDefault(); - return true; - } - }, - true - ); - - function on_mouse_wheel(e) { - var pos = parseInt(root.style.top); - root.style.top = - (pos + e.deltaY * options.scroll_speed).toFixed() + "px"; - e.preventDefault(); - return true; - } - - if (!options.scroll_speed) { - options.scroll_speed = 0.1; - } - - root.addEventListener("wheel", on_mouse_wheel, true); - root.addEventListener("mousewheel", on_mouse_wheel, true); - - this.root = root; - - //title - if (options.title) { - var element = document.createElement("div"); - element.className = "litemenu-title"; - element.innerHTML = options.title; - root.appendChild(element); - } - - //entries - var num = 0; - for (var i=0; i < values.length; i++) { - var name = values.constructor == Array ? values[i] : i; - if (name != null && name.constructor !== String) { - name = name.content === undefined ? String(name) : name.content; - } - var value = values[i]; - this.addItem(name, value, options); - num++; - } - - //close on leave? touch enabled devices won't work TODO use a global device detector and condition on that - /*LiteGraph.pointerListenerAdd(root,"leave", function(e) { + //close on leave? touch enabled devices won't work TODO use a global device detector and condition on that + /*LiteGraph.pointerListenerAdd(root,"leave", function(e) { console.log("pointerevents: ContextMenu leave"); if (that.lock) { return; @@ -13786,639 +14192,682 @@ LGraphNode.prototype.executeAction = function(action) //that.close(e); });*/ - LiteGraph.pointerListenerAdd(root,"enter", function(e) { - //console.log("pointerevents: ContextMenu enter"); - if (root.closing_timer) { - clearTimeout(root.closing_timer); - } - }); + LiteGraph.pointerListenerAdd(root, "enter", function (e) { + //console.log("pointerevents: ContextMenu enter"); + if (root.closing_timer) { + clearTimeout(root.closing_timer); + } + }); - //insert before checking position - var root_document = document; - if (options.event) { - root_document = options.event.target.ownerDocument; - } - - if (!root_document) { - root_document = document; - } - - if( root_document.fullscreenElement ) - root_document.fullscreenElement.appendChild(root); - else - root_document.body.appendChild(root); - - //compute best position - var left = options.left || 0; - var top = options.top || 0; - if (options.event) { - left = options.event.clientX - 10; - top = options.event.clientY - 10; - if (options.title) { - top -= 20; - } - - if (options.parentMenu) { - var rect = options.parentMenu.root.getBoundingClientRect(); - left = rect.left + rect.width; - } - - var body_rect = document.body.getBoundingClientRect(); - var root_rect = root.getBoundingClientRect(); - if(body_rect.height == 0) - console.error("document.body height is 0. That is dangerous, set html,body { height: 100%; }"); - - if (body_rect.width && left > body_rect.width - root_rect.width - 10) { - left = body_rect.width - root_rect.width - 10; - } - if (body_rect.height && top > body_rect.height - root_rect.height - 10) { - top = body_rect.height - root_rect.height - 10; - } - } - - root.style.left = left + "px"; - root.style.top = top + "px"; - - if (options.scale) { - root.style.transform = "scale(" + options.scale + ")"; - } + //insert before checking position + var root_document = document; + if (options.event) { + root_document = options.event.target.ownerDocument; } - ContextMenu.prototype.addItem = function(name, value, options) { - var that = this; - options = options || {}; + if (!root_document) { + root_document = document; + } - var element = document.createElement("div"); - element.className = "litemenu-entry submenu"; + if (root_document.fullscreenElement) + root_document.fullscreenElement.appendChild(root); + else root_document.body.appendChild(root); - var disabled = false; + //compute best position + var left = options.left || 0; + var top = options.top || 0; + if (options.event) { + left = options.event.clientX - 10; + top = options.event.clientY - 10; + if (options.title) { + top -= 20; + } - if (value === null) { - element.classList.add("separator"); - //element.innerHTML = "
" - //continue; - } else { - element.innerHTML = value && value.title ? value.title : name; - element.value = value; + if (options.parentMenu) { + var rect = options.parentMenu.root.getBoundingClientRect(); + left = rect.left + rect.width; + } - if (value) { - if (value.disabled) { - disabled = true; - element.classList.add("disabled"); - } - if (value.submenu || value.has_submenu) { - element.classList.add("has_submenu"); - } - } + var body_rect = document.body.getBoundingClientRect(); + var root_rect = root.getBoundingClientRect(); + if (body_rect.height == 0) + console.error( + "document.body height is 0. That is dangerous, set html,body { height: 100%; }" + ); - if (typeof value == "function") { - element.dataset["value"] = name; - element.onclick_callback = value; - } else { - element.dataset["value"] = value; - } + if (body_rect.width && left > body_rect.width - root_rect.width - 10) { + left = body_rect.width - root_rect.width - 10; + } + if (body_rect.height && top > body_rect.height - root_rect.height - 10) { + top = body_rect.height - root_rect.height - 10; + } + } - if (value.className) { - element.className += " " + value.className; - } + root.style.left = left + "px"; + root.style.top = top + "px"; + + if (options.scale) { + root.style.transform = "scale(" + options.scale + ")"; + } + } + + ContextMenu.prototype.addItem = function (name, value, options) { + var that = this; + options = options || {}; + + var element = document.createElement("div"); + element.className = "litemenu-entry submenu"; + + var disabled = false; + + if (value === null) { + element.classList.add("separator"); + //element.innerHTML = "
" + //continue; + } else { + element.innerHTML = value && value.title ? value.title : name; + element.value = value; + + if (value) { + if (value.disabled) { + disabled = true; + element.classList.add("disabled"); } - - this.root.appendChild(element); - if (!disabled) { - element.addEventListener("click", inner_onclick); + if (value.submenu || value.has_submenu) { + element.classList.add("has_submenu"); } - if (!disabled && options.autoopen) { - LiteGraph.pointerListenerAdd(element,"enter",inner_over); + } + + if (typeof value == "function") { + element.dataset["value"] = name; + element.onclick_callback = value; + } else { + element.dataset["value"] = value; + } + + if (value.className) { + element.className += " " + value.className; + } + } + + this.root.appendChild(element); + if (!disabled) { + element.addEventListener("click", inner_onclick); + } + if (!disabled && options.autoopen) { + LiteGraph.pointerListenerAdd(element, "enter", inner_over); + } + + function inner_over(e) { + var value = this.value; + if (!value || !value.has_submenu) { + return; + } + //if it is a submenu, autoopen like the item was clicked + inner_onclick.call(this, e); + } + + //menu option clicked + function inner_onclick(e) { + var value = this.value; + var close_parent = true; + + if (that.current_submenu) { + that.current_submenu.close(e); + } + + //global callback + if (options.callback) { + var r = options.callback.call( + this, + value, + options, + e, + that, + options.node + ); + if (r === true) { + close_parent = false; } + } - function inner_over(e) { - var value = this.value; - if (!value || !value.has_submenu) { - return; - } - //if it is a submenu, autoopen like the item was clicked - inner_onclick.call(this, e); - } - - //menu option clicked - function inner_onclick(e) { - var value = this.value; - var close_parent = true; - - if (that.current_submenu) { - that.current_submenu.close(e); - } - - //global callback - if (options.callback) { - var r = options.callback.call( - this, - value, - options, - e, - that, - options.node - ); - if (r === true) { - close_parent = false; - } - } - - //special cases - if (value) { - if ( - value.callback && - !options.ignore_item_callbacks && - value.disabled !== true - ) { - //item callback - var r = value.callback.call( - this, - value, - options, - e, - that, - options.extra - ); - if (r === true) { - close_parent = false; - } - } - if (value.submenu) { - if (!value.submenu.options) { - throw "ContextMenu submenu needs options"; - } - var submenu = new that.constructor(value.submenu.options, { - callback: value.submenu.callback, - event: e, - parentMenu: that, - ignore_item_callbacks: - value.submenu.ignore_item_callbacks, - title: value.submenu.title, - extra: value.submenu.extra, - autoopen: options.autoopen - }); - close_parent = false; - } - } - - if (close_parent && !that.lock) { - that.close(); - } - } - - return element; - }; - - ContextMenu.prototype.close = function(e, ignore_parent_menu) { - if (this.root.parentNode) { - this.root.parentNode.removeChild(this.root); - } - if (this.parentMenu && !ignore_parent_menu) { - this.parentMenu.lock = false; - this.parentMenu.current_submenu = null; - if (e === undefined) { - this.parentMenu.close(); - } else if ( - e && - !ContextMenu.isCursorOverElement(e, this.parentMenu.root) - ) { - ContextMenu.trigger(this.parentMenu.root, LiteGraph.pointerevents_method+"leave", e); - } - } - if (this.current_submenu) { - this.current_submenu.close(e, true); - } - - if (this.root.closing_timer) { - clearTimeout(this.root.closing_timer); - } - - // TODO implement : LiteGraph.contextMenuClosed(); :: keep track of opened / closed / current ContextMenu - // on key press, allow filtering/selecting the context menu elements - }; - - //this code is used to trigger events easily (used in the context menu mouseleave - ContextMenu.trigger = function(element, event_name, params, origin) { - var evt = document.createEvent("CustomEvent"); - evt.initCustomEvent(event_name, true, true, params); //canBubble, cancelable, detail - evt.srcElement = origin; - if (element.dispatchEvent) { - element.dispatchEvent(evt); - } else if (element.__events) { - element.__events.dispatchEvent(evt); - } - //else nothing seems binded here so nothing to do - return evt; - }; - - //returns the top most menu - ContextMenu.prototype.getTopMenu = function() { - if (this.options.parentMenu) { - return this.options.parentMenu.getTopMenu(); - } - return this; - }; - - ContextMenu.prototype.getFirstEvent = function() { - if (this.options.parentMenu) { - return this.options.parentMenu.getFirstEvent(); - } - return this.options.event; - }; - - ContextMenu.isCursorOverElement = function(event, element) { - var left = event.clientX; - var top = event.clientY; - var rect = element.getBoundingClientRect(); - if (!rect) { - return false; - } + //special cases + if (value) { if ( - top > rect.top && - top < rect.top + rect.height && - left > rect.left && - left < rect.left + rect.width + value.callback && + !options.ignore_item_callbacks && + value.disabled !== true ) { - return true; + //item callback + var r = value.callback.call( + this, + value, + options, + e, + that, + options.extra + ); + if (r === true) { + close_parent = false; + } } - return false; - }; - - LiteGraph.ContextMenu = ContextMenu; - - LiteGraph.closeAllContextMenus = function(ref_window) { - ref_window = ref_window || window; - - var elements = ref_window.document.querySelectorAll(".litecontextmenu"); - if (!elements.length) { - return; + if (value.submenu) { + if (!value.submenu.options) { + throw "ContextMenu submenu needs options"; + } + var submenu = new that.constructor(value.submenu.options, { + callback: value.submenu.callback, + event: e, + parentMenu: that, + ignore_item_callbacks: value.submenu.ignore_item_callbacks, + title: value.submenu.title, + extra: value.submenu.extra, + autoopen: options.autoopen, + }); + close_parent = false; } + } - var result = []; - for (var i = 0; i < elements.length; i++) { - result.push(elements[i]); - } - - for (var i=0; i < result.length; i++) { - if (result[i].close) { - result[i].close(); - } else if (result[i].parentNode) { - result[i].parentNode.removeChild(result[i]); - } - } - }; - - LiteGraph.extendClass = function(target, origin) { - for (var i in origin) { - //copy class properties - if (target.hasOwnProperty(i)) { - continue; - } - target[i] = origin[i]; - } - - if (origin.prototype) { - //copy prototype properties - for (var i in origin.prototype) { - //only enumerable - if (!origin.prototype.hasOwnProperty(i)) { - continue; - } - - if (target.prototype.hasOwnProperty(i)) { - //avoid overwriting existing ones - continue; - } - - //copy getters - if (origin.prototype.__lookupGetter__(i)) { - target.prototype.__defineGetter__( - i, - origin.prototype.__lookupGetter__(i) - ); - } else { - target.prototype[i] = origin.prototype[i]; - } - - //and setters - if (origin.prototype.__lookupSetter__(i)) { - target.prototype.__defineSetter__( - i, - origin.prototype.__lookupSetter__(i) - ); - } - } - } - }; - - //used by some widgets to render a curve editor - function CurveEditor( points ) - { - this.points = points; - this.selected = -1; - this.nearest = -1; - this.size = null; //stores last size used - this.must_update = true; - this.margin = 5; - } - - CurveEditor.sampleCurve = function(f,points) - { - if(!points) - return; - for(var i = 0; i < points.length - 1; ++i) - { - var p = points[i]; - var pn = points[i+1]; - if(pn[0] < f) - continue; - var r = (pn[0] - p[0]); - if( Math.abs(r) < 0.00001 ) - return p[1]; - var local_f = (f - p[0]) / r; - return p[1] * (1.0 - local_f) + pn[1] * local_f; - } - return 0; - } - - CurveEditor.prototype.draw = function( ctx, size, graphcanvas, background_color, line_color, inactive ) - { - var points = this.points; - if(!points) - return; - this.size = size; - var w = size[0] - this.margin * 2; - var h = size[1] - this.margin * 2; - - line_color = line_color || "#666"; - - ctx.save(); - ctx.translate(this.margin,this.margin); - - if(background_color) - { - ctx.fillStyle = "#111"; - ctx.fillRect(0,0,w,h); - ctx.fillStyle = "#222"; - ctx.fillRect(w*0.5,0,1,h); - ctx.strokeStyle = "#333"; - ctx.strokeRect(0,0,w,h); - } - ctx.strokeStyle = line_color; - if(inactive) - ctx.globalAlpha = 0.5; - ctx.beginPath(); - for(var i = 0; i < points.length; ++i) - { - var p = points[i]; - ctx.lineTo( p[0] * w, (1.0 - p[1]) * h ); - } - ctx.stroke(); - ctx.globalAlpha = 1; - if(!inactive) - for(var i = 0; i < points.length; ++i) - { - var p = points[i]; - ctx.fillStyle = this.selected == i ? "#FFF" : (this.nearest == i ? "#DDD" : "#AAA"); - ctx.beginPath(); - ctx.arc( p[0] * w, (1.0 - p[1]) * h, 2, 0, Math.PI * 2 ); - ctx.fill(); - } - ctx.restore(); - } - - //localpos is mouse in curve editor space - CurveEditor.prototype.onMouseDown = function( localpos, graphcanvas ) - { - var points = this.points; - if(!points) - return; - if( localpos[1] < 0 ) - return; - - //this.captureInput(true); - var w = this.size[0] - this.margin * 2; - var h = this.size[1] - this.margin * 2; - var x = localpos[0] - this.margin; - var y = localpos[1] - this.margin; - var pos = [x,y]; - var max_dist = 30 / graphcanvas.ds.scale; - //search closer one - this.selected = this.getCloserPoint(pos, max_dist); - //create one - if(this.selected == -1) - { - var point = [x / w, 1 - y / h]; - points.push(point); - points.sort(function(a,b){ return a[0] - b[0]; }); - this.selected = points.indexOf(point); - this.must_update = true; - } - if(this.selected != -1) - return true; - } - - CurveEditor.prototype.onMouseMove = function( localpos, graphcanvas ) - { - var points = this.points; - if(!points) - return; - var s = this.selected; - if(s < 0) - return; - var x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2 ); - var y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2 ); - var curvepos = [(localpos[0] - this.margin),(localpos[1] - this.margin)]; - var max_dist = 30 / graphcanvas.ds.scale; - this._nearest = this.getCloserPoint(curvepos, max_dist); - var point = points[s]; - if(point) - { - var is_edge_point = s == 0 || s == points.length - 1; - if( !is_edge_point && (localpos[0] < -10 || localpos[0] > this.size[0] + 10 || localpos[1] < -10 || localpos[1] > this.size[1] + 10) ) - { - points.splice(s,1); - this.selected = -1; - return; - } - if( !is_edge_point ) //not edges - point[0] = clamp(x, 0, 1); - else - point[0] = s == 0 ? 0 : 1; - point[1] = 1.0 - clamp(y, 0, 1); - points.sort(function(a,b){ return a[0] - b[0]; }); - this.selected = points.indexOf(point); - this.must_update = true; - } - } - - CurveEditor.prototype.onMouseUp = function( localpos, graphcanvas ) - { - this.selected = -1; - return false; - } - - CurveEditor.prototype.getCloserPoint = function(pos, max_dist) - { - var points = this.points; - if(!points) - return -1; - max_dist = max_dist || 30; - var w = (this.size[0] - this.margin * 2); - var h = (this.size[1] - this.margin * 2); - var num = points.length; - var p2 = [0,0]; - var min_dist = 1000000; - var closest = -1; - var last_valid = -1; - for(var i = 0; i < num; ++i) - { - var p = points[i]; - p2[0] = p[0] * w; - p2[1] = (1.0 - p[1]) * h; - if(p2[0] < pos[0]) - last_valid = i; - var dist = vec2.distance(pos,p2); - if(dist > min_dist || dist > max_dist) - continue; - closest = i; - min_dist = dist; - } - return closest; - } - - LiteGraph.CurveEditor = CurveEditor; - - //used to create nodes from wrapping functions - LiteGraph.getParameterNames = function(func) { - return (func + "") - .replace(/[/][/].*$/gm, "") // strip single-line comments - .replace(/\s+/g, "") // strip white space - .replace(/[/][*][^/*]*[*][/]/g, "") // strip multi-line comments /**/ - .split("){", 1)[0] - .replace(/^[^(]*[(]/, "") // extract the parameters - .replace(/=[^,]+/g, "") // strip any ES6 defaults - .split(",") - .filter(Boolean); // split & filter [""] - }; - - /* helper for interaction: pointer, touch, mouse Listeners - used by LGraphCanvas DragAndScale ContextMenu*/ - LiteGraph.pointerListenerAdd = function(oDOM, sEvIn, fCall, capture=false) { - if (!oDOM || !oDOM.addEventListener || !sEvIn || typeof fCall!=="function"){ - //console.log("cant pointerListenerAdd "+oDOM+", "+sEvent+", "+fCall); - return; // -- break -- - } - - var sMethod = LiteGraph.pointerevents_method; - var sEvent = sEvIn; - - // UNDER CONSTRUCTION - // convert pointerevents to touch event when not available - if (sMethod=="pointer" && !window.PointerEvent){ - console.warn("sMethod=='pointer' && !window.PointerEvent"); - console.log("Converting pointer["+sEvent+"] : down move up cancel enter TO touchstart touchmove touchend, etc .."); - switch(sEvent){ - case "down":{ - sMethod = "touch"; - sEvent = "start"; - break; - } - case "move":{ - sMethod = "touch"; - //sEvent = "move"; - break; - } - case "up":{ - sMethod = "touch"; - sEvent = "end"; - break; - } - case "cancel":{ - sMethod = "touch"; - //sEvent = "cancel"; - break; - } - case "enter":{ - console.log("debug: Should I send a move event?"); // ??? - break; - } - // case "over": case "out": not used at now - default:{ - console.warn("PointerEvent not available in this browser ? The event "+sEvent+" would not be called"); - } - } - } - - switch(sEvent){ - //both pointer and move events - case "down": case "up": case "move": case "over": case "out": case "enter": - { - oDOM.addEventListener(sMethod+sEvent, fCall, capture); - } - // only pointerevents - case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": - { - if (sMethod!="mouse"){ - return oDOM.addEventListener(sMethod+sEvent, fCall, capture); - } - } - // not "pointer" || "mouse" - default: - return oDOM.addEventListener(sEvent, fCall, capture); - } - } - LiteGraph.pointerListenerRemove = function(oDOM, sEvent, fCall, capture=false) { - if (!oDOM || !oDOM.removeEventListener || !sEvent || typeof fCall!=="function"){ - //console.log("cant pointerListenerRemove "+oDOM+", "+sEvent+", "+fCall); - return; // -- break -- - } - switch(sEvent){ - //both pointer and move events - case "down": case "up": case "move": case "over": case "out": case "enter": - { - if (LiteGraph.pointerevents_method=="pointer" || LiteGraph.pointerevents_method=="mouse"){ - oDOM.removeEventListener(LiteGraph.pointerevents_method+sEvent, fCall, capture); - } - } - // only pointerevents - case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": - { - if (LiteGraph.pointerevents_method=="pointer"){ - return oDOM.removeEventListener(LiteGraph.pointerevents_method+sEvent, fCall, capture); - } - } - // not "pointer" || "mouse" - default: - return oDOM.removeEventListener(sEvent, fCall, capture); - } - } - - function clamp(v, a, b) { - return a > v ? a : b < v ? b : v; - }; - global.clamp = clamp; - - if (typeof window != "undefined" && !window["requestAnimationFrame"]) { - window.requestAnimationFrame = - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - function(callback) { - window.setTimeout(callback, 1000 / 60); - }; + if (close_parent && !that.lock) { + that.close(); + } } + + return element; + }; + + ContextMenu.prototype.close = function (e, ignore_parent_menu) { + if (this.root.parentNode) { + this.root.parentNode.removeChild(this.root); + } + if (this.parentMenu && !ignore_parent_menu) { + this.parentMenu.lock = false; + this.parentMenu.current_submenu = null; + if (e === undefined) { + this.parentMenu.close(); + } else if ( + e && + !ContextMenu.isCursorOverElement(e, this.parentMenu.root) + ) { + ContextMenu.trigger( + this.parentMenu.root, + LiteGraph.pointerevents_method + "leave", + e + ); + } + } + if (this.current_submenu) { + this.current_submenu.close(e, true); + } + + if (this.root.closing_timer) { + clearTimeout(this.root.closing_timer); + } + + // TODO implement : LiteGraph.contextMenuClosed(); :: keep track of opened / closed / current ContextMenu + // on key press, allow filtering/selecting the context menu elements + }; + + //this code is used to trigger events easily (used in the context menu mouseleave + ContextMenu.trigger = function (element, event_name, params, origin) { + var evt = document.createEvent("CustomEvent"); + evt.initCustomEvent(event_name, true, true, params); //canBubble, cancelable, detail + evt.srcElement = origin; + if (element.dispatchEvent) { + element.dispatchEvent(evt); + } else if (element.__events) { + element.__events.dispatchEvent(evt); + } + //else nothing seems binded here so nothing to do + return evt; + }; + + //returns the top most menu + ContextMenu.prototype.getTopMenu = function () { + if (this.options.parentMenu) { + return this.options.parentMenu.getTopMenu(); + } + return this; + }; + + ContextMenu.prototype.getFirstEvent = function () { + if (this.options.parentMenu) { + return this.options.parentMenu.getFirstEvent(); + } + return this.options.event; + }; + + ContextMenu.isCursorOverElement = function (event, element) { + var left = event.clientX; + var top = event.clientY; + var rect = element.getBoundingClientRect(); + if (!rect) { + return false; + } + if ( + top > rect.top && + top < rect.top + rect.height && + left > rect.left && + left < rect.left + rect.width + ) { + return true; + } + return false; + }; + + LiteGraph.ContextMenu = ContextMenu; + + LiteGraph.closeAllContextMenus = function (ref_window) { + ref_window = ref_window || window; + + var elements = ref_window.document.querySelectorAll(".litecontextmenu"); + if (!elements.length) { + return; + } + + var result = []; + for (var i = 0; i < elements.length; i++) { + result.push(elements[i]); + } + + for (var i = 0; i < result.length; i++) { + if (result[i].close) { + result[i].close(); + } else if (result[i].parentNode) { + result[i].parentNode.removeChild(result[i]); + } + } + }; + + LiteGraph.extendClass = function (target, origin) { + for (var i in origin) { + //copy class properties + if (target.hasOwnProperty(i)) { + continue; + } + target[i] = origin[i]; + } + + if (origin.prototype) { + //copy prototype properties + for (var i in origin.prototype) { + //only enumerable + if (!origin.prototype.hasOwnProperty(i)) { + continue; + } + + if (target.prototype.hasOwnProperty(i)) { + //avoid overwriting existing ones + continue; + } + + //copy getters + if (origin.prototype.__lookupGetter__(i)) { + target.prototype.__defineGetter__( + i, + origin.prototype.__lookupGetter__(i) + ); + } else { + target.prototype[i] = origin.prototype[i]; + } + + //and setters + if (origin.prototype.__lookupSetter__(i)) { + target.prototype.__defineSetter__( + i, + origin.prototype.__lookupSetter__(i) + ); + } + } + } + }; + + //used by some widgets to render a curve editor + function CurveEditor(points) { + this.points = points; + this.selected = -1; + this.nearest = -1; + this.size = null; //stores last size used + this.must_update = true; + this.margin = 5; + } + + CurveEditor.sampleCurve = function (f, points) { + if (!points) return; + for (var i = 0; i < points.length - 1; ++i) { + var p = points[i]; + var pn = points[i + 1]; + if (pn[0] < f) continue; + var r = pn[0] - p[0]; + if (Math.abs(r) < 0.00001) return p[1]; + var local_f = (f - p[0]) / r; + return p[1] * (1.0 - local_f) + pn[1] * local_f; + } + return 0; + }; + + CurveEditor.prototype.draw = function ( + ctx, + size, + graphcanvas, + background_color, + line_color, + inactive + ) { + var points = this.points; + if (!points) return; + this.size = size; + var w = size[0] - this.margin * 2; + var h = size[1] - this.margin * 2; + + line_color = line_color || "#666"; + + ctx.save(); + ctx.translate(this.margin, this.margin); + + if (background_color) { + ctx.fillStyle = "#111"; + ctx.fillRect(0, 0, w, h); + ctx.fillStyle = "#222"; + ctx.fillRect(w * 0.5, 0, 1, h); + ctx.strokeStyle = "#333"; + ctx.strokeRect(0, 0, w, h); + } + ctx.strokeStyle = line_color; + if (inactive) ctx.globalAlpha = 0.5; + ctx.beginPath(); + for (var i = 0; i < points.length; ++i) { + var p = points[i]; + ctx.lineTo(p[0] * w, (1.0 - p[1]) * h); + } + ctx.stroke(); + ctx.globalAlpha = 1; + if (!inactive) + for (var i = 0; i < points.length; ++i) { + var p = points[i]; + ctx.fillStyle = + this.selected == i ? "#FFF" : this.nearest == i ? "#DDD" : "#AAA"; + ctx.beginPath(); + ctx.arc(p[0] * w, (1.0 - p[1]) * h, 2, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); + }; + + //localpos is mouse in curve editor space + CurveEditor.prototype.onMouseDown = function (localpos, graphcanvas) { + var points = this.points; + if (!points) return; + if (localpos[1] < 0) return; + + //this.captureInput(true); + var w = this.size[0] - this.margin * 2; + var h = this.size[1] - this.margin * 2; + var x = localpos[0] - this.margin; + var y = localpos[1] - this.margin; + var pos = [x, y]; + var max_dist = 30 / graphcanvas.ds.scale; + //search closer one + this.selected = this.getCloserPoint(pos, max_dist); + //create one + if (this.selected == -1) { + var point = [x / w, 1 - y / h]; + points.push(point); + points.sort(function (a, b) { + return a[0] - b[0]; + }); + this.selected = points.indexOf(point); + this.must_update = true; + } + if (this.selected != -1) return true; + }; + + CurveEditor.prototype.onMouseMove = function (localpos, graphcanvas) { + var points = this.points; + if (!points) return; + var s = this.selected; + if (s < 0) return; + var x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2); + var y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2); + var curvepos = [localpos[0] - this.margin, localpos[1] - this.margin]; + var max_dist = 30 / graphcanvas.ds.scale; + this._nearest = this.getCloserPoint(curvepos, max_dist); + var point = points[s]; + if (point) { + var is_edge_point = s == 0 || s == points.length - 1; + if ( + !is_edge_point && + (localpos[0] < -10 || + localpos[0] > this.size[0] + 10 || + localpos[1] < -10 || + localpos[1] > this.size[1] + 10) + ) { + points.splice(s, 1); + this.selected = -1; + return; + } + if (!is_edge_point) + //not edges + point[0] = clamp(x, 0, 1); + else point[0] = s == 0 ? 0 : 1; + point[1] = 1.0 - clamp(y, 0, 1); + points.sort(function (a, b) { + return a[0] - b[0]; + }); + this.selected = points.indexOf(point); + this.must_update = true; + } + }; + + CurveEditor.prototype.onMouseUp = function (localpos, graphcanvas) { + this.selected = -1; + return false; + }; + + CurveEditor.prototype.getCloserPoint = function (pos, max_dist) { + var points = this.points; + if (!points) return -1; + max_dist = max_dist || 30; + var w = this.size[0] - this.margin * 2; + var h = this.size[1] - this.margin * 2; + var num = points.length; + var p2 = [0, 0]; + var min_dist = 1000000; + var closest = -1; + var last_valid = -1; + for (var i = 0; i < num; ++i) { + var p = points[i]; + p2[0] = p[0] * w; + p2[1] = (1.0 - p[1]) * h; + if (p2[0] < pos[0]) last_valid = i; + var dist = vec2.distance(pos, p2); + if (dist > min_dist || dist > max_dist) continue; + closest = i; + min_dist = dist; + } + return closest; + }; + + LiteGraph.CurveEditor = CurveEditor; + + //used to create nodes from wrapping functions + LiteGraph.getParameterNames = function (func) { + return (func + "") + .replace(/[/][/].*$/gm, "") // strip single-line comments + .replace(/\s+/g, "") // strip white space + .replace(/[/][*][^/*]*[*][/]/g, "") // strip multi-line comments /**/ + .split("){", 1)[0] + .replace(/^[^(]*[(]/, "") // extract the parameters + .replace(/=[^,]+/g, "") // strip any ES6 defaults + .split(",") + .filter(Boolean); // split & filter [""] + }; + + /* helper for interaction: pointer, touch, mouse Listeners + used by LGraphCanvas DragAndScale ContextMenu*/ + LiteGraph.pointerListenerAdd = function ( + oDOM, + sEvIn, + fCall, + capture = false + ) { + if ( + !oDOM || + !oDOM.addEventListener || + !sEvIn || + typeof fCall !== "function" + ) { + //console.log("cant pointerListenerAdd "+oDOM+", "+sEvent+", "+fCall); + return; // -- break -- + } + + var sMethod = LiteGraph.pointerevents_method; + var sEvent = sEvIn; + + // UNDER CONSTRUCTION + // convert pointerevents to touch event when not available + if (sMethod == "pointer" && !window.PointerEvent) { + console.warn("sMethod=='pointer' && !window.PointerEvent"); + console.log( + "Converting pointer[" + + sEvent + + "] : down move up cancel enter TO touchstart touchmove touchend, etc .." + ); + switch (sEvent) { + case "down": { + sMethod = "touch"; + sEvent = "start"; + break; + } + case "move": { + sMethod = "touch"; + //sEvent = "move"; + break; + } + case "up": { + sMethod = "touch"; + sEvent = "end"; + break; + } + case "cancel": { + sMethod = "touch"; + //sEvent = "cancel"; + break; + } + case "enter": { + console.log("debug: Should I send a move event?"); // ??? + break; + } + // case "over": case "out": not used at now + default: { + console.warn( + "PointerEvent not available in this browser ? The event " + + sEvent + + " would not be called" + ); + } + } + } + + switch (sEvent) { + //both pointer and move events + case "down": + case "up": + case "move": + case "over": + case "out": + case "enter": { + oDOM.addEventListener(sMethod + sEvent, fCall, capture); + } + // only pointerevents + case "leave": + case "cancel": + case "gotpointercapture": + case "lostpointercapture": { + if (sMethod != "mouse") { + return oDOM.addEventListener(sMethod + sEvent, fCall, capture); + } + } + // not "pointer" || "mouse" + default: + return oDOM.addEventListener(sEvent, fCall, capture); + } + }; + LiteGraph.pointerListenerRemove = function ( + oDOM, + sEvent, + fCall, + capture = false + ) { + if ( + !oDOM || + !oDOM.removeEventListener || + !sEvent || + typeof fCall !== "function" + ) { + //console.log("cant pointerListenerRemove "+oDOM+", "+sEvent+", "+fCall); + return; // -- break -- + } + switch (sEvent) { + //both pointer and move events + case "down": + case "up": + case "move": + case "over": + case "out": + case "enter": { + if ( + LiteGraph.pointerevents_method == "pointer" || + LiteGraph.pointerevents_method == "mouse" + ) { + oDOM.removeEventListener( + LiteGraph.pointerevents_method + sEvent, + fCall, + capture + ); + } + } + // only pointerevents + case "leave": + case "cancel": + case "gotpointercapture": + case "lostpointercapture": { + if (LiteGraph.pointerevents_method == "pointer") { + return oDOM.removeEventListener( + LiteGraph.pointerevents_method + sEvent, + fCall, + capture + ); + } + } + // not "pointer" || "mouse" + default: + return oDOM.removeEventListener(sEvent, fCall, capture); + } + }; + + function clamp(v, a, b) { + return a > v ? a : b < v ? b : v; + } + global.clamp = clamp; + + if (typeof window != "undefined" && !window["requestAnimationFrame"]) { + window.requestAnimationFrame = + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60); + }; + } })(this); if (typeof exports != "undefined") { - exports.LiteGraph = this.LiteGraph; - exports.LGraph = this.LGraph; - exports.LLink = this.LLink; - exports.LGraphNode = this.LGraphNode; - exports.LGraphGroup = this.LGraphGroup; - exports.DragAndScale = this.DragAndScale; - exports.LGraphCanvas = this.LGraphCanvas; - exports.ContextMenu = this.ContextMenu; + exports.LiteGraph = this.LiteGraph; + exports.LGraph = this.LGraph; + exports.LLink = this.LLink; + exports.LGraphNode = this.LGraphNode; + exports.LGraphGroup = this.LGraphGroup; + exports.DragAndScale = this.DragAndScale; + exports.LGraphCanvas = this.LGraphCanvas; + exports.ContextMenu = this.ContextMenu; } - - diff --git a/src/lib/litegraph.extensions.js b/src/lib/litegraph.extensions.js index 32853fe49..5538066fe 100644 --- a/src/lib/litegraph.extensions.js +++ b/src/lib/litegraph.extensions.js @@ -6,16 +6,19 @@ * @param {clearBackgroundColor} String * @ */ -LGraphCanvas.prototype.updateBackground = function (image, clearBackgroundColor) { - this._bg_img = new Image(); - this._bg_img.name = image; - this._bg_img.src = image; - this._bg_img.onload = () => { - this.draw(true, true); - }; - this.background_image = image; +LGraphCanvas.prototype.updateBackground = function ( + image, + clearBackgroundColor +) { + this._bg_img = new Image(); + this._bg_img.name = image; + this._bg_img.src = image; + this._bg_img.onload = () => { + this.draw(true, true); + }; + this.background_image = image; - this.clear_background = true; - this.clear_background_color = clearBackgroundColor; - this._pattern = null -} + this.clear_background = true; + this.clear_background_color = clearBackgroundColor; + this._pattern = null; +}; diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 5af030afa..fd96f9a56 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1,514 +1,558 @@ import { ComfyWorkflow } from "types/comfyWorkflow"; -import { HistoryTaskItem, PendingTaskItem, RunningTaskItem, ComfyNodeDef } from "/types/apiTypes"; - +import { + HistoryTaskItem, + PendingTaskItem, + RunningTaskItem, + ComfyNodeDef, +} from "/types/apiTypes"; interface QueuePromptRequestBody { - client_id: string; - // Mapping from node id to node info + input values - // TODO: Type this. - prompt: Record; - extra_data: { - extra_pnginfo: { - workflow: ComfyWorkflow; - }; - }; - front?: boolean; - number?: number; + client_id: string; + // Mapping from node id to node info + input values + // TODO: Type this. + prompt: Record; + extra_data: { + extra_pnginfo: { + workflow: ComfyWorkflow; + }; + }; + front?: boolean; + number?: number; } - class ComfyApi extends EventTarget { - #registered = new Set(); - api_host: string; - api_base: string; - initialClientId: string; - user: string; - socket?: WebSocket; - clientId?: string; + #registered = new Set(); + api_host: string; + api_base: string; + initialClientId: string; + user: string; + socket?: WebSocket; + clientId?: string; - constructor() { - super(); - this.api_host = location.host; - this.api_base = location.pathname.split('/').slice(0, -1).join('/'); - this.initialClientId = sessionStorage.getItem("clientId"); - } + constructor() { + super(); + this.api_host = location.host; + this.api_base = location.pathname.split("/").slice(0, -1).join("/"); + this.initialClientId = sessionStorage.getItem("clientId"); + } - apiURL(route: string): string { - return this.api_base + "/api" + route; - } + apiURL(route: string): string { + return this.api_base + "/api" + route; + } - fileURL(route: string): string { - return this.api_base + route; - } + fileURL(route: string): string { + return this.api_base + route; + } - fetchApi(route, options?) { - if (!options) { - options = {}; - } - if (!options.headers) { - options.headers = {}; - } - options.headers["Comfy-User"] = this.user; - return fetch(this.apiURL(route), options); - } + fetchApi(route, options?) { + if (!options) { + options = {}; + } + if (!options.headers) { + options.headers = {}; + } + options.headers["Comfy-User"] = this.user; + return fetch(this.apiURL(route), options); + } - addEventListener(type, callback, options?) { - super.addEventListener(type, callback, options); - this.#registered.add(type); - } + addEventListener(type, callback, options?) { + super.addEventListener(type, callback, options); + this.#registered.add(type); + } - /** - * Poll status for colab and other things that don't support websockets. - */ - #pollQueue() { - setInterval(async () => { - try { - const resp = await this.fetchApi("/prompt"); - const status = await resp.json(); - this.dispatchEvent(new CustomEvent("status", { detail: status })); - } catch (error) { - this.dispatchEvent(new CustomEvent("status", { detail: null })); - } - }, 1000); - } + /** + * Poll status for colab and other things that don't support websockets. + */ + #pollQueue() { + setInterval(async () => { + try { + const resp = await this.fetchApi("/prompt"); + const status = await resp.json(); + this.dispatchEvent(new CustomEvent("status", { detail: status })); + } catch (error) { + this.dispatchEvent(new CustomEvent("status", { detail: null })); + } + }, 1000); + } - /** - * Creates and connects a WebSocket for realtime updates - * @param {boolean} isReconnect If the socket is connection is a reconnect attempt - */ - #createSocket(isReconnect?) { - if (this.socket) { - return; - } + /** + * Creates and connects a WebSocket for realtime updates + * @param {boolean} isReconnect If the socket is connection is a reconnect attempt + */ + #createSocket(isReconnect?) { + if (this.socket) { + return; + } - let opened = false; - let existingSession = window.name; - if (existingSession) { - existingSession = "?clientId=" + existingSession; - } - this.socket = new WebSocket( - `ws${window.location.protocol === "https:" ? "s" : ""}://${this.api_host}${this.api_base}/ws${existingSession}` - ); - this.socket.binaryType = "arraybuffer"; + let opened = false; + let existingSession = window.name; + if (existingSession) { + existingSession = "?clientId=" + existingSession; + } + this.socket = new WebSocket( + `ws${window.location.protocol === "https:" ? "s" : ""}://${this.api_host}${this.api_base}/ws${existingSession}` + ); + this.socket.binaryType = "arraybuffer"; - this.socket.addEventListener("open", () => { - opened = true; - if (isReconnect) { - this.dispatchEvent(new CustomEvent("reconnected")); - } - }); + this.socket.addEventListener("open", () => { + opened = true; + if (isReconnect) { + this.dispatchEvent(new CustomEvent("reconnected")); + } + }); - this.socket.addEventListener("error", () => { - if (this.socket) this.socket.close(); - if (!isReconnect && !opened) { - this.#pollQueue(); - } - }); + this.socket.addEventListener("error", () => { + if (this.socket) this.socket.close(); + if (!isReconnect && !opened) { + this.#pollQueue(); + } + }); - this.socket.addEventListener("close", () => { - setTimeout(() => { - this.socket = null; - this.#createSocket(true); - }, 300); - if (opened) { - this.dispatchEvent(new CustomEvent("status", { detail: null })); - this.dispatchEvent(new CustomEvent("reconnecting")); - } - }); + this.socket.addEventListener("close", () => { + setTimeout(() => { + this.socket = null; + this.#createSocket(true); + }, 300); + if (opened) { + this.dispatchEvent(new CustomEvent("status", { detail: null })); + this.dispatchEvent(new CustomEvent("reconnecting")); + } + }); - this.socket.addEventListener("message", (event) => { - try { - if (event.data instanceof ArrayBuffer) { - const view = new DataView(event.data); - const eventType = view.getUint32(0); - const buffer = event.data.slice(4); - switch (eventType) { - case 1: - const view2 = new DataView(event.data); - const imageType = view2.getUint32(0) - let imageMime - switch (imageType) { - case 1: - default: - imageMime = "image/jpeg"; - break; - case 2: - imageMime = "image/png" - } - const imageBlob = new Blob([buffer.slice(4)], { type: imageMime }); - this.dispatchEvent(new CustomEvent("b_preview", { detail: imageBlob })); - break; - default: - throw new Error(`Unknown binary websocket message of type ${eventType}`); - } - } - else { - const msg = JSON.parse(event.data); - switch (msg.type) { - case "status": - if (msg.data.sid) { - this.clientId = msg.data.sid; - window.name = this.clientId; // use window name so it isnt reused when duplicating tabs - sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow - } - this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status })); - break; - case "progress": - this.dispatchEvent(new CustomEvent("progress", { detail: msg.data })); - break; - case "executing": - this.dispatchEvent(new CustomEvent("executing", { detail: msg.data.node })); - break; - case "executed": - this.dispatchEvent(new CustomEvent("executed", { detail: msg.data })); - break; - case "execution_start": - this.dispatchEvent(new CustomEvent("execution_start", { detail: msg.data })); - break; - case "execution_error": - this.dispatchEvent(new CustomEvent("execution_error", { detail: msg.data })); - break; - case "execution_cached": - this.dispatchEvent(new CustomEvent("execution_cached", { detail: msg.data })); - break; - default: - if (this.#registered.has(msg.type)) { - this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data })); - } else { - throw new Error(`Unknown message type ${msg.type}`); - } - } - } - } catch (error) { - console.warn("Unhandled message:", event.data, error); - } - }); - } + this.socket.addEventListener("message", (event) => { + try { + if (event.data instanceof ArrayBuffer) { + const view = new DataView(event.data); + const eventType = view.getUint32(0); + const buffer = event.data.slice(4); + switch (eventType) { + case 1: + const view2 = new DataView(event.data); + const imageType = view2.getUint32(0); + let imageMime; + switch (imageType) { + case 1: + default: + imageMime = "image/jpeg"; + break; + case 2: + imageMime = "image/png"; + } + const imageBlob = new Blob([buffer.slice(4)], { + type: imageMime, + }); + this.dispatchEvent( + new CustomEvent("b_preview", { detail: imageBlob }) + ); + break; + default: + throw new Error( + `Unknown binary websocket message of type ${eventType}` + ); + } + } else { + const msg = JSON.parse(event.data); + switch (msg.type) { + case "status": + if (msg.data.sid) { + this.clientId = msg.data.sid; + window.name = this.clientId; // use window name so it isnt reused when duplicating tabs + sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow + } + this.dispatchEvent( + new CustomEvent("status", { detail: msg.data.status }) + ); + break; + case "progress": + this.dispatchEvent( + new CustomEvent("progress", { detail: msg.data }) + ); + break; + case "executing": + this.dispatchEvent( + new CustomEvent("executing", { detail: msg.data.node }) + ); + break; + case "executed": + this.dispatchEvent( + new CustomEvent("executed", { detail: msg.data }) + ); + break; + case "execution_start": + this.dispatchEvent( + new CustomEvent("execution_start", { detail: msg.data }) + ); + break; + case "execution_error": + this.dispatchEvent( + new CustomEvent("execution_error", { detail: msg.data }) + ); + break; + case "execution_cached": + this.dispatchEvent( + new CustomEvent("execution_cached", { detail: msg.data }) + ); + break; + default: + if (this.#registered.has(msg.type)) { + this.dispatchEvent( + new CustomEvent(msg.type, { detail: msg.data }) + ); + } else { + throw new Error(`Unknown message type ${msg.type}`); + } + } + } + } catch (error) { + console.warn("Unhandled message:", event.data, error); + } + }); + } - /** - * Initialises sockets and realtime updates - */ - init() { - this.#createSocket(); - } + /** + * Initialises sockets and realtime updates + */ + init() { + this.#createSocket(); + } - /** - * Gets a list of extension urls - * @returns An array of script urls to import - */ - async getExtensions() { - const resp = await this.fetchApi("/extensions", { cache: "no-store" }); - return await resp.json(); - } + /** + * Gets a list of extension urls + * @returns An array of script urls to import + */ + async getExtensions() { + const resp = await this.fetchApi("/extensions", { cache: "no-store" }); + return await resp.json(); + } - /** - * Gets a list of embedding names - * @returns An array of script urls to import - */ - async getEmbeddings() { - const resp = await this.fetchApi("/embeddings", { cache: "no-store" }); - return await resp.json(); - } + /** + * Gets a list of embedding names + * @returns An array of script urls to import + */ + async getEmbeddings() { + const resp = await this.fetchApi("/embeddings", { cache: "no-store" }); + return await resp.json(); + } - /** - * Loads node object definitions for the graph - * @returns The node definitions - */ - async getNodeDefs(): Promise> { - const resp = await this.fetchApi("/object_info", { cache: "no-store" }); - return await resp.json(); - } + /** + * Loads node object definitions for the graph + * @returns The node definitions + */ + async getNodeDefs(): Promise> { + const resp = await this.fetchApi("/object_info", { cache: "no-store" }); + return await resp.json(); + } - /** - * - * @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue - * @param {object} prompt The prompt data to queue - */ - async queuePrompt(number: number, { output, workflow }) { - const body: QueuePromptRequestBody = { - client_id: this.clientId, - prompt: output, - extra_data: { extra_pnginfo: { workflow } }, - }; + /** + * + * @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue + * @param {object} prompt The prompt data to queue + */ + async queuePrompt(number: number, { output, workflow }) { + const body: QueuePromptRequestBody = { + client_id: this.clientId, + prompt: output, + extra_data: { extra_pnginfo: { workflow } }, + }; - if (number === -1) { - body.front = true; - } else if (number != 0) { - body.number = number; - } + if (number === -1) { + body.front = true; + } else if (number != 0) { + body.number = number; + } - const res = await this.fetchApi("/prompt", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); + const res = await this.fetchApi("/prompt", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); - if (res.status !== 200) { - throw { - response: await res.json(), - }; - } + if (res.status !== 200) { + throw { + response: await res.json(), + }; + } - return await res.json(); - } + return await res.json(); + } - /** - * Loads a list of items (queue or history) - * @param {string} type The type of items to load, queue or history - * @returns The items of the specified type grouped by their status - */ - async getItems(type) { - if (type === "queue") { - return this.getQueue(); - } - return this.getHistory(); - } + /** + * Loads a list of items (queue or history) + * @param {string} type The type of items to load, queue or history + * @returns The items of the specified type grouped by their status + */ + async getItems(type) { + if (type === "queue") { + return this.getQueue(); + } + return this.getHistory(); + } - /** - * Gets the current state of the queue - * @returns The currently running and queued items - */ - async getQueue(): Promise<{ Running: RunningTaskItem[], Pending: PendingTaskItem[] }>{ - try { - const res = await this.fetchApi("/queue"); - const data = await res.json(); - return { - // Running action uses a different endpoint for cancelling - Running: data.queue_running.map((prompt) => ({ - prompt, - remove: { name: "Cancel", cb: () => api.interrupt() }, - })), - Pending: data.queue_pending.map((prompt) => ({ prompt })), - }; - } catch (error) { - console.error(error); - return { Running: [], Pending: [] }; - } - } + /** + * Gets the current state of the queue + * @returns The currently running and queued items + */ + async getQueue(): Promise<{ + Running: RunningTaskItem[]; + Pending: PendingTaskItem[]; + }> { + try { + const res = await this.fetchApi("/queue"); + const data = await res.json(); + return { + // Running action uses a different endpoint for cancelling + Running: data.queue_running.map((prompt) => ({ + prompt, + remove: { name: "Cancel", cb: () => api.interrupt() }, + })), + Pending: data.queue_pending.map((prompt) => ({ prompt })), + }; + } catch (error) { + console.error(error); + return { Running: [], Pending: [] }; + } + } - /** - * Gets the prompt execution history - * @returns Prompt history including node outputs - */ - async getHistory(max_items: number = 200): Promise<{History: HistoryTaskItem[]}> { - try { - const res = await this.fetchApi(`/history?max_items=${max_items}`); - return { History: Object.values(await res.json()) }; - } catch (error) { - console.error(error); - return { History: [] }; - } - } + /** + * Gets the prompt execution history + * @returns Prompt history including node outputs + */ + async getHistory( + max_items: number = 200 + ): Promise<{ History: HistoryTaskItem[] }> { + try { + const res = await this.fetchApi(`/history?max_items=${max_items}`); + return { History: Object.values(await res.json()) }; + } catch (error) { + console.error(error); + return { History: [] }; + } + } - /** - * Gets system & device stats - * @returns System stats such as python version, OS, per device info - */ - async getSystemStats() { - const res = await this.fetchApi("/system_stats"); - return await res.json(); - } + /** + * Gets system & device stats + * @returns System stats such as python version, OS, per device info + */ + async getSystemStats() { + const res = await this.fetchApi("/system_stats"); + return await res.json(); + } - /** - * Sends a POST request to the API - * @param {*} type The endpoint to post to - * @param {*} body Optional POST data - */ - async #postItem(type, body) { - try { - await this.fetchApi("/" + type, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: body ? JSON.stringify(body) : undefined, - }); - } catch (error) { - console.error(error); - } - } + /** + * Sends a POST request to the API + * @param {*} type The endpoint to post to + * @param {*} body Optional POST data + */ + async #postItem(type, body) { + try { + await this.fetchApi("/" + type, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: body ? JSON.stringify(body) : undefined, + }); + } catch (error) { + console.error(error); + } + } - /** - * Deletes an item from the specified list - * @param {string} type The type of item to delete, queue or history - * @param {number} id The id of the item to delete - */ - async deleteItem(type, id) { - await this.#postItem(type, { delete: [id] }); - } + /** + * Deletes an item from the specified list + * @param {string} type The type of item to delete, queue or history + * @param {number} id The id of the item to delete + */ + async deleteItem(type, id) { + await this.#postItem(type, { delete: [id] }); + } - /** - * Clears the specified list - * @param {string} type The type of list to clear, queue or history - */ - async clearItems(type) { - await this.#postItem(type, { clear: true }); - } + /** + * Clears the specified list + * @param {string} type The type of list to clear, queue or history + */ + async clearItems(type) { + await this.#postItem(type, { clear: true }); + } - /** - * Interrupts the execution of the running prompt - */ - async interrupt() { - await this.#postItem("interrupt", null); - } + /** + * Interrupts the execution of the running prompt + */ + async interrupt() { + await this.#postItem("interrupt", null); + } - /** - * Gets user configuration data and where data should be stored - * @returns { Promise<{ storage: "server" | "browser", users?: Promise, migrated?: boolean }> } - */ - async getUserConfig() { - return (await this.fetchApi("/users")).json(); - } + /** + * Gets user configuration data and where data should be stored + * @returns { Promise<{ storage: "server" | "browser", users?: Promise, migrated?: boolean }> } + */ + async getUserConfig() { + return (await this.fetchApi("/users")).json(); + } - /** - * Creates a new user - * @param { string } username - * @returns The fetch response - */ - createUser(username) { - return this.fetchApi("/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ username }), - }); - } + /** + * Creates a new user + * @param { string } username + * @returns The fetch response + */ + createUser(username) { + return this.fetchApi("/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username }), + }); + } - /** - * Gets all setting values for the current user - * @returns { Promise } A dictionary of id -> value - */ - async getSettings() { - return (await this.fetchApi("/settings")).json(); - } + /** + * Gets all setting values for the current user + * @returns { Promise } A dictionary of id -> value + */ + async getSettings() { + return (await this.fetchApi("/settings")).json(); + } - /** - * Gets a setting for the current user - * @param { string } id The id of the setting to fetch - * @returns { Promise } The setting value - */ - async getSetting(id) { - return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json(); - } + /** + * Gets a setting for the current user + * @param { string } id The id of the setting to fetch + * @returns { Promise } The setting value + */ + async getSetting(id) { + return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json(); + } - /** - * Stores a dictionary of settings for the current user - * @param { Record } settings Dictionary of setting id -> value to save - * @returns { Promise } - */ - async storeSettings(settings) { - return this.fetchApi(`/settings`, { - method: "POST", - body: JSON.stringify(settings) - }); - } + /** + * Stores a dictionary of settings for the current user + * @param { Record } settings Dictionary of setting id -> value to save + * @returns { Promise } + */ + async storeSettings(settings) { + return this.fetchApi(`/settings`, { + method: "POST", + body: JSON.stringify(settings), + }); + } - /** - * Stores a setting for the current user - * @param { string } id The id of the setting to update - * @param { unknown } value The value of the setting - * @returns { Promise } - */ - async storeSetting(id, value) { - return this.fetchApi(`/settings/${encodeURIComponent(id)}`, { - method: "POST", - body: JSON.stringify(value) - }); - } + /** + * Stores a setting for the current user + * @param { string } id The id of the setting to update + * @param { unknown } value The value of the setting + * @returns { Promise } + */ + async storeSetting(id, value) { + return this.fetchApi(`/settings/${encodeURIComponent(id)}`, { + method: "POST", + body: JSON.stringify(value), + }); + } - /** - * Gets a user data file for the current user - * @param { string } file The name of the userdata file to load - * @param { RequestInit } [options] - * @returns { Promise } The fetch response object - */ - async getUserData(file, options?) { - return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options); - } + /** + * Gets a user data file for the current user + * @param { string } file The name of the userdata file to load + * @param { RequestInit } [options] + * @returns { Promise } The fetch response object + */ + async getUserData(file, options?) { + return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options); + } - /** - * Stores a user data file for the current user - * @param { string } file The name of the userdata file to save - * @param { unknown } data The data to save to the file - * @param { RequestInit & { stringify?: boolean, throwOnError?: boolean } } [options] - * @returns { Promise } - */ - async storeUserData( - file: string, - data: unknown, - options: RequestInit & { overwrite?: boolean, stringify?: boolean, throwOnError?: boolean } - = { overwrite: true, stringify: true, throwOnError: true } - ): Promise { - const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}?overwrite=${options.overwrite}`, { - method: "POST", - body: options?.stringify ? JSON.stringify(data) : data, - ...options, - }); - if (resp.status !== 200 && options.throwOnError !== false) { - throw new Error(`Error storing user data file '${file}': ${resp.status} ${(await resp).statusText}`); - } + /** + * Stores a user data file for the current user + * @param { string } file The name of the userdata file to save + * @param { unknown } data The data to save to the file + * @param { RequestInit & { stringify?: boolean, throwOnError?: boolean } } [options] + * @returns { Promise } + */ + async storeUserData( + file: string, + data: unknown, + options: RequestInit & { + overwrite?: boolean; + stringify?: boolean; + throwOnError?: boolean; + } = { overwrite: true, stringify: true, throwOnError: true } + ): Promise { + const resp = await this.fetchApi( + `/userdata/${encodeURIComponent(file)}?overwrite=${options.overwrite}`, + { + method: "POST", + body: options?.stringify ? JSON.stringify(data) : data, + ...options, + } + ); + if (resp.status !== 200 && options.throwOnError !== false) { + throw new Error( + `Error storing user data file '${file}': ${resp.status} ${(await resp).statusText}` + ); + } - return resp; - } + return resp; + } - /** - * Deletes a user data file for the current user - * @param { string } file The name of the userdata file to delete - */ - async deleteUserData(file) { - const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, { - method: "DELETE", - }); - if (resp.status !== 204) { - throw new Error(`Error removing user data file '${file}': ${resp.status} ${(resp).statusText}`); - } - } + /** + * Deletes a user data file for the current user + * @param { string } file The name of the userdata file to delete + */ + async deleteUserData(file) { + const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, { + method: "DELETE", + }); + if (resp.status !== 204) { + throw new Error( + `Error removing user data file '${file}': ${resp.status} ${resp.statusText}` + ); + } + } - /** - * Move a user data file for the current user - * @param { string } source The userdata file to move - * @param { string } dest The destination for the file - */ - async moveUserData(source, dest, options = { overwrite: false }) { - const resp = await this.fetchApi(`/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`, { - method: "POST", - }); - return resp; - } + /** + * Move a user data file for the current user + * @param { string } source The userdata file to move + * @param { string } dest The destination for the file + */ + async moveUserData(source, dest, options = { overwrite: false }) { + const resp = await this.fetchApi( + `/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`, + { + method: "POST", + } + ); + return resp; + } - /** - * @overload - * Lists user data files for the current user - * @param { string } dir The directory in which to list files - * @param { boolean } [recurse] If the listing should be recursive - * @param { true } [split] If the paths should be split based on the os path separator - * @returns { Promise> } The list of split file paths in the format [fullPath, ...splitPath] - */ - /** - * @overload - * Lists user data files for the current user - * @param { string } dir The directory in which to list files - * @param { boolean } [recurse] If the listing should be recursive - * @param { false | undefined } [split] If the paths should be split based on the os path separator - * @returns { Promise } The list of files - */ - async listUserData(dir, recurse, split) { - const resp = await this.fetchApi( - `/userdata?${new URLSearchParams({ - recurse, - dir, - split, - })}` - ); - if (resp.status === 404) return []; - if (resp.status !== 200) { - throw new Error(`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`); - } - return resp.json(); - } + /** + * @overload + * Lists user data files for the current user + * @param { string } dir The directory in which to list files + * @param { boolean } [recurse] If the listing should be recursive + * @param { true } [split] If the paths should be split based on the os path separator + * @returns { Promise> } The list of split file paths in the format [fullPath, ...splitPath] + */ + /** + * @overload + * Lists user data files for the current user + * @param { string } dir The directory in which to list files + * @param { boolean } [recurse] If the listing should be recursive + * @param { false | undefined } [split] If the paths should be split based on the os path separator + * @returns { Promise } The list of files + */ + async listUserData(dir, recurse, split) { + const resp = await this.fetchApi( + `/userdata?${new URLSearchParams({ + recurse, + dir, + split, + })}` + ); + if (resp.status === 404) return []; + if (resp.status !== 200) { + throw new Error( + `Error getting user data list '${dir}': ${resp.status} ${resp.statusText}` + ); + } + return resp.json(); + } } export const api = new ComfyApi(); diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 6b98f3c8f..bb614b555 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -3,9 +3,15 @@ import { ComfyWidgetConstructor, ComfyWidgets, initWidgets } from "./widgets"; import { ComfyUI, $el } from "./ui"; import { api } from "./api"; import { defaultGraph } from "./defaultGraph"; -import { getPngMetadata, getWebpMetadata, getFlacMetadata, importA1111, getLatentMetadata } from "./pnginfo"; +import { + getPngMetadata, + getWebpMetadata, + getFlacMetadata, + importA1111, + getLatentMetadata, +} from "./pnginfo"; import { addDomClippingSetting } from "./domWidget"; -import { createImageHost, calculateImageGrid } from "./ui/imagePreview" +import { createImageHost, calculateImageGrid } from "./ui/imagePreview"; import { DraggableList } from "./ui/draggableList"; import { applyTextReplacements, addStylesheet } from "./utils"; import type { ComfyExtension } from "/types/comfy"; @@ -16,21 +22,21 @@ import { ComfyAppMenu } from "./ui/menu/index.js"; import { getStorageValue, setStorageValue } from "./utils.js"; import { ComfyWorkflowManager } from "./workflows.js"; -export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview" +export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview"; function sanitizeNodeName(string) { - let entityMap = { - '&': '', - '<': '', - '>': '', - '"': '', - "'": '', - '`': '', - '=': '' - }; - return String(string).replace(/[&<>"'`=]/g, function fromEntityMap (s) { - return entityMap[s]; - }); + let entityMap = { + "&": "", + "<": "", + ">": "", + '"': "", + "'": "", + "`": "", + "=": "", + }; + return String(string).replace(/[&<>"'`=]/g, function fromEntityMap(s) { + return entityMap[s]; + }); } /** @@ -38,2583 +44,2864 @@ function sanitizeNodeName(string) { */ export class ComfyApp { - /** - * List of entries to queue - * @type {{number: number, batchCount: number}[]} - */ - #queueItems = []; - /** - * If the queue is currently being processed - * @type {boolean} - */ - #processingQueue = false; - - /** - * Content Clipboard - * @type {serialized node object} - */ - static clipspace = null; - static clipspace_invalidate_handler = null; - static open_maskeditor = null; - static clipspace_return_node = null; - - // Force vite to import utils.ts as part of index. - // Force import of DraggableList. - static utils = { - applyTextReplacements, - addStylesheet, - DraggableList, - }; - - ui: ComfyUI; - logging: ComfyLogging; - extensions: ComfyExtension[]; - _nodeOutputs: Record; - nodePreviewImages: Record; - shiftDown: boolean; - graph: LGraph; - enableWorkflowViewRestore: any; - canvas: LGraphCanvas; - dragOverNode: LGraphNode | null; - canvasEl: HTMLCanvasElement; - // x, y, scale - zoom_drag_start: [number, number, number] | null; - lastNodeErrors: any[] | null; - runningNodeId: number | null; - lastExecutionError: { node_id: number } | null; - progress: { value: number, max: number } | null; - configuringGraph: boolean; - isNewUserSession: boolean; - // Are there any other options than "server"? - storageLocation: string; - multiUserServer: boolean; - ctx: CanvasRenderingContext2D; - widgets: Record; - workflowManager: ComfyWorkflowManager; - bodyTop: HTMLElement; - bodyLeft: HTMLElement; - bodyRight: HTMLElement; - bodyBottom: HTMLElement; - menu: ComfyAppMenu; - - constructor() { - this.ui = new ComfyUI(this); - this.logging = new ComfyLogging(this); - this.workflowManager = new ComfyWorkflowManager(this); - this.bodyTop = $el("div.comfyui-body-top", { parent: document.body }); - this.bodyLeft = $el("div.comfyui-body-left", { parent: document.body }); - this.bodyRight = $el("div.comfyui-body-right", { parent: document.body }); - this.bodyBottom = $el("div.comfyui-body-bottom", { parent: document.body }); - this.menu = new ComfyAppMenu(this); - - /** - * List of extensions that are registered with the app - * @type {ComfyExtension[]} - */ - this.extensions = []; - - /** - * Stores the execution output data for each node - * @type {Record} - */ - this.nodeOutputs = {}; - - /** - * Stores the preview image data for each node - * @type {Record} - */ - this.nodePreviewImages = {}; - - /** - * If the shift key on the keyboard is pressed - * @type {boolean} - */ - this.shiftDown = false; - } - - get nodeOutputs() { - return this._nodeOutputs; - } - - set nodeOutputs(value) { - this._nodeOutputs = value; - this.#invokeExtensions("onNodeOutputsUpdated", value); - } - - getPreviewFormatParam() { - let preview_format = this.ui.settings.getSettingValue("Comfy.PreviewFormat"); - if(preview_format) - return `&preview=${preview_format}`; - else - return ""; - } - - getRandParam() { - return "&rand=" + Math.random(); - } - - static isImageNode(node) { - return node.imgs || (node && node.widgets && node.widgets.findIndex(obj => obj.name === 'image') >= 0); - } - - static onClipspaceEditorSave() { - if(ComfyApp.clipspace_return_node) { - ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node); - } - } - - static onClipspaceEditorClosed() { - ComfyApp.clipspace_return_node = null; - } - - static copyToClipspace(node) { - var widgets = null; - if(node.widgets) { - widgets = node.widgets.map(({ type, name, value }) => ({ type, name, value })); - } - - var imgs = undefined; - var orig_imgs = undefined; - if(node.imgs != undefined) { - imgs = []; - orig_imgs = []; - - for (let i = 0; i < node.imgs.length; i++) { - imgs[i] = new Image(); - imgs[i].src = node.imgs[i].src; - orig_imgs[i] = imgs[i]; - } - } - - var selectedIndex = 0; - if(node.imageIndex) { - selectedIndex = node.imageIndex; - } - - ComfyApp.clipspace = { - 'widgets': widgets, - 'imgs': imgs, - 'original_imgs': orig_imgs, - 'images': node.images, - 'selectedIndex': selectedIndex, - 'img_paste_mode': 'selected' // reset to default im_paste_mode state on copy action - }; - - ComfyApp.clipspace_return_node = null; - - if(ComfyApp.clipspace_invalidate_handler) { - ComfyApp.clipspace_invalidate_handler(); - } - } - - static pasteFromClipspace(node) { - if(ComfyApp.clipspace) { - // image paste - if(ComfyApp.clipspace.imgs && node.imgs) { - if(node.images && ComfyApp.clipspace.images) { - if(ComfyApp.clipspace['img_paste_mode'] == 'selected') { - node.images = [ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']]]; - } - else { - node.images = ComfyApp.clipspace.images; - } - - if(app.nodeOutputs[node.id + ""]) - app.nodeOutputs[node.id + ""].images = node.images; - } - - if(ComfyApp.clipspace.imgs) { - // deep-copy to cut link with clipspace - if(ComfyApp.clipspace['img_paste_mode'] == 'selected') { - const img = new Image(); - img.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src; - node.imgs = [img]; - node.imageIndex = 0; - } - else { - const imgs = []; - for(let i=0; i obj.name === 'image'); - if(index >= 0) { - if(node.widgets[index].type != 'image' && typeof node.widgets[index].value == "string" && clip_image.filename) { - node.widgets[index].value = (clip_image.subfolder?clip_image.subfolder+'/':'') + clip_image.filename + (clip_image.type?` [${clip_image.type}]`:''); - } - else { - node.widgets[index].value = clip_image; - } - } - } - if(ComfyApp.clipspace.widgets) { - ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => { - // @ts-ignore - const prop = Object.values(node.widgets).find(obj => obj.type === type && obj.name === name); - // @ts-ignore - if (prop && prop.type != 'button') { - // @ts-ignore - if(prop.type != 'image' && typeof prop.value == "string" && value.filename) { - // @ts-ignore - prop.value = (value.subfolder?value.subfolder+'/':'') + value.filename + (value.type?` [${value.type}]`:''); - } - else { - // @ts-ignore - prop.value = value; - // @ts-ignore - prop.callback(value); - } - } - }); - } - } - - app.graph.setDirtyCanvas(true); - } - } - - /** - * Invoke an extension callback - * @param {keyof ComfyExtension} method The extension callback to execute - * @param {any[]} args Any arguments to pass to the callback - * @returns - */ - #invokeExtensions(method, ...args) { - let results = []; - for (const ext of this.extensions) { - if (method in ext) { - try { - results.push(ext[method](...args, this)); - } catch (error) { - console.error( - `Error calling extension '${ext.name}' method '${method}'`, - { error }, - { extension: ext }, - { args } - ); - } - } - } - return results; - } - - /** - * Invoke an async extension callback - * Each callback will be invoked concurrently - * @param {string} method The extension callback to execute - * @param {...any} args Any arguments to pass to the callback - * @returns - */ - async #invokeExtensionsAsync(method, ...args) { - return await Promise.all( - this.extensions.map(async (ext) => { - if (method in ext) { - try { - return await ext[method](...args, this); - } catch (error) { - console.error( - `Error calling extension '${ext.name}' method '${method}'`, - { error }, - { extension: ext }, - { args } - ); - } - } - }) - ); - } - - #addRestoreWorkflowView() { - // @ts-ignore - const serialize = LGraph.prototype.serialize; - const self = this; - // @ts-ignore - LGraph.prototype.serialize = function() { - const workflow = serialize.apply(this, arguments); - - // Store the drag & scale info in the serialized workflow if the setting is enabled - if (self.enableWorkflowViewRestore.value) { - if (!workflow.extra) { - workflow.extra = {}; - } - workflow.extra.ds = { - scale: self.canvas.ds.scale, - offset: self.canvas.ds.offset, - }; - } else if (workflow.extra?.ds) { - // Clear any old view data - delete workflow.extra.ds; - } - - return workflow; - } - this.enableWorkflowViewRestore = this.ui.settings.addSetting({ - id: "Comfy.EnableWorkflowViewRestore", - name: "Save and restore canvas position and zoom level in workflows", - type: "boolean", - defaultValue: true - }); - } - - /** - * Adds special context menu handling for nodes - * e.g. this adds Open Image functionality for nodes that show images - * @param {*} node The node to add the menu handler - */ - #addNodeContextMenuHandler(node) { - function getCopyImageOption(img) { - if (typeof window.ClipboardItem === "undefined") return []; - return [ - { - content: "Copy Image", - callback: async () => { - const url = new URL(img.src); - url.searchParams.delete("preview"); - - const writeImage = async (blob) => { - await navigator.clipboard.write([ - new ClipboardItem({ - [blob.type]: blob, - }), - ]); - }; - - try { - const data = await fetch(url); - const blob = await data.blob(); - try { - await writeImage(blob); - } catch (error) { - // Chrome seems to only support PNG on write, convert and try again - if (blob.type !== "image/png") { - const canvas = $el("canvas", { - width: img.naturalWidth, - height: img.naturalHeight, - }) as HTMLCanvasElement; - const ctx = canvas.getContext("2d"); - let image; - if (typeof window.createImageBitmap === "undefined") { - image = new Image(); - const p = new Promise((resolve, reject) => { - image.onload = resolve; - image.onerror = reject; - }).finally(() => { - URL.revokeObjectURL(image.src); - }); - image.src = URL.createObjectURL(blob); - await p; - } else { - image = await createImageBitmap(blob); - } - try { - ctx.drawImage(image, 0, 0); - canvas.toBlob(writeImage, "image/png"); - } finally { - if (typeof image.close === "function") { - image.close(); - } - } - - return; - } - throw error; - } - } catch (error) { - alert("Error copying image: " + (error.message ?? error)); - } - }, - }, - ]; - } - - node.prototype.getExtraMenuOptions = function (_, options) { - if (this.imgs) { - // If this node has images then we add an open in new tab item - let img; - if (this.imageIndex != null) { - // An image is selected so select that - img = this.imgs[this.imageIndex]; - } else if (this.overIndex != null) { - // No image is selected but one is hovered - img = this.imgs[this.overIndex]; - } - if (img) { - options.unshift( - { - content: "Open Image", - callback: () => { - let url = new URL(img.src); - url.searchParams.delete("preview"); - window.open(url, "_blank"); - }, - }, - ...getCopyImageOption(img), - { - content: "Save Image", - callback: () => { - const a = document.createElement("a"); - let url = new URL(img.src); - url.searchParams.delete("preview"); - a.href = url.toString(); - a.setAttribute("download", new URLSearchParams(url.search).get("filename")); - document.body.append(a); - a.click(); - requestAnimationFrame(() => a.remove()); - }, - } - ); - } - } - - options.push({ - content: "Bypass", - callback: (obj) => { - if (this.mode === 4) this.mode = 0; - else this.mode = 4; - this.graph.change(); - }, - }); - - // prevent conflict of clipspace content - if (!ComfyApp.clipspace_return_node) { - options.push({ - content: "Copy (Clipspace)", - callback: (obj) => { - ComfyApp.copyToClipspace(this); - }, - }); - - if (ComfyApp.clipspace != null) { - options.push({ - content: "Paste (Clipspace)", - callback: () => { - ComfyApp.pasteFromClipspace(this); - }, - }); - } - - if (ComfyApp.isImageNode(this)) { - options.push({ - content: "Open in MaskEditor", - callback: (obj) => { - ComfyApp.copyToClipspace(this); - ComfyApp.clipspace_return_node = this; - ComfyApp.open_maskeditor(); - }, - }); - } - } - }; - } - - #addNodeKeyHandler(node) { - const app = this; - const origNodeOnKeyDown = node.prototype.onKeyDown; - - node.prototype.onKeyDown = function(e) { - if (origNodeOnKeyDown && origNodeOnKeyDown.apply(this, e) === false) { - return false; - } - - if (this.flags.collapsed || !this.imgs || this.imageIndex === null) { - return; - } - - let handled = false; - - if (e.key === "ArrowLeft" || e.key === "ArrowRight") { - if (e.key === "ArrowLeft") { - this.imageIndex -= 1; - } else if (e.key === "ArrowRight") { - this.imageIndex += 1; - } - this.imageIndex %= this.imgs.length; - - if (this.imageIndex < 0) { - this.imageIndex = this.imgs.length + this.imageIndex; - } - handled = true; - } else if (e.key === "Escape") { - this.imageIndex = null; - handled = true; - } - - if (handled === true) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - } - } - - /** - * Adds Custom drawing logic for nodes - * e.g. Draws images and handles thumbnail navigation on nodes that output images - * @param {*} node The node to add the draw handler - */ - #addDrawBackgroundHandler(node) { - const app = this; - - function getImageTop(node) { - let shiftY; - if (node.imageOffset != null) { - shiftY = node.imageOffset; - } else { - if (node.widgets?.length) { - const w = node.widgets[node.widgets.length - 1]; - shiftY = w.last_y; - if (w.computeSize) { - shiftY += w.computeSize()[1] + 4; - } - else if(w.computedHeight) { - shiftY += w.computedHeight; - } - else { - shiftY += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } else { - shiftY = node.computeSize()[1]; - } - } - return shiftY; - } - - node.prototype.setSizeForImage = function (force) { - if(!force && this.animatedImages) return; - - if (this.inputHeight || this.freeWidgetSpace > 210) { - this.setSize(this.size); - return; - } - const minHeight = getImageTop(this) + 220; - if (this.size[1] < minHeight) { - this.setSize([this.size[0], minHeight]); - } - }; - - node.prototype.onDrawBackground = function (ctx) { - if (!this.flags.collapsed) { - let imgURLs = [] - let imagesChanged = false - - const output = app.nodeOutputs[this.id + ""]; - if (output?.images) { - this.animatedImages = output?.animated?.find(Boolean); - if (this.images !== output.images) { - this.images = output.images; - imagesChanged = true; - imgURLs = imgURLs.concat( - output.images.map((params) => { - return api.apiURL( - "/view?" + - new URLSearchParams(params).toString() + - (this.animatedImages ? "" : app.getPreviewFormatParam()) + app.getRandParam() - ); - }) - ); - } - } - - const preview = app.nodePreviewImages[this.id + ""] - if (this.preview !== preview) { - this.preview = preview - imagesChanged = true; - if (preview != null) { - imgURLs.push(preview); - } - } - - if (imagesChanged) { - this.imageIndex = null; - if (imgURLs.length > 0) { - Promise.all( - imgURLs.map((src) => { - return new Promise((r) => { - const img = new Image(); - img.onload = () => r(img); - img.onerror = () => r(null); - img.src = src - }); - }) - ).then((imgs) => { - if ((!output || this.images === output.images) && (!preview || this.preview === preview)) { - this.imgs = imgs.filter(Boolean); - this.setSizeForImage?.(); - app.graph.setDirtyCanvas(true); - } - }); - } - else { - this.imgs = null; - } - } - - const calculateGrid = (w, h, n) => { - let columns, rows, cellsize; - - if (w > h) { - cellsize = h; - columns = Math.ceil(w / cellsize); - rows = Math.ceil(n / columns); - } else { - cellsize = w; - rows = Math.ceil(h / cellsize); - columns = Math.ceil(n / rows); - } - - while (columns * rows < n) { - cellsize++; - if (w >= h) { - columns = Math.ceil(w / cellsize); - rows = Math.ceil(n / columns); - } else { - rows = Math.ceil(h / cellsize); - columns = Math.ceil(n / rows); - } - } - - const cell_size = Math.min(w/columns, h/rows); - return {cell_size, columns, rows}; - } - - const is_all_same_aspect_ratio = (imgs) => { - // assume: imgs.length >= 2 - let ratio = imgs[0].naturalWidth/imgs[0].naturalHeight; - - for(let i=1; i w.name === ANIM_PREVIEW_WIDGET); - - if(this.animatedImages) { - // Instead of using the canvas we'll use a IMG - if(widgetIdx > -1) { - // Replace content - const widget = this.widgets[widgetIdx]; - widget.options.host.updateImages(this.imgs); - } else { - const host = createImageHost(this); - this.setSizeForImage(true); - const widget = this.addDOMWidget(ANIM_PREVIEW_WIDGET, "img", host.el, { - host, - getHeight: host.getHeight, - onDraw: host.onDraw, - hideOnZoom: false - }); - widget.serializeValue = () => undefined; - widget.options.host.updateImages(this.imgs); - } - return; - } - - if (widgetIdx > -1) { - this.widgets[widgetIdx].onRemove?.(); - this.widgets.splice(widgetIdx, 1); - } - - const canvas = app.graph.list_of_graphcanvas[0]; - const mouse = canvas.graph_mouse; - if (!canvas.pointer_is_down && this.pointerDown) { - if (mouse[0] === this.pointerDown.pos[0] && mouse[1] === this.pointerDown.pos[1]) { - this.imageIndex = this.pointerDown.index; - } - this.pointerDown = null; - } - - let imageIndex = this.imageIndex; - const numImages = this.imgs.length; - if (numImages === 1 && !imageIndex) { - this.imageIndex = imageIndex = 0; - } - - const top = getImageTop(this); - var shiftY = top; - - let dw = this.size[0]; - let dh = this.size[1]; - dh -= shiftY; - - if (imageIndex == null) { - var cellWidth, cellHeight, shiftX, cell_padding, cols; - - const compact_mode = is_all_same_aspect_ratio(this.imgs); - if(!compact_mode) { - // use rectangle cell style and border line - cell_padding = 2; - const { cell_size, columns, rows } = calculateGrid(dw, dh, numImages); - cols = columns; - - cellWidth = cell_size; - cellHeight = cell_size; - shiftX = (dw-cell_size*cols)/2; - shiftY = (dh-cell_size*rows)/2 + top; - } - else { - cell_padding = 0; - ({ cellWidth, cellHeight, cols, shiftX } = calculateImageGrid(this.imgs, dw, dh)); - } - - let anyHovered = false; - this.imageRects = []; - for (let i = 0; i < numImages; i++) { - const img = this.imgs[i]; - const row = Math.floor(i / cols); - const col = i % cols; - const x = col * cellWidth + shiftX; - const y = row * cellHeight + shiftY; - if (!anyHovered) { - anyHovered = LiteGraph.isInsideRectangle( - mouse[0], - mouse[1], - x + this.pos[0], - y + this.pos[1], - cellWidth, - cellHeight - ); - if (anyHovered) { - this.overIndex = i; - let value = 110; - if (canvas.pointer_is_down) { - if (!this.pointerDown || this.pointerDown.index !== i) { - this.pointerDown = { index: i, pos: [...mouse] }; - } - value = 125; - } - ctx.filter = `contrast(${value}%) brightness(${value}%)`; - canvas.canvas.style.cursor = "pointer"; - } - } - this.imageRects.push([x, y, cellWidth, cellHeight]); - - let wratio = cellWidth/img.width; - let hratio = cellHeight/img.height; - var ratio = Math.min(wratio, hratio); - - let imgHeight = ratio * img.height; - let imgY = row * cellHeight + shiftY + (cellHeight - imgHeight)/2; - let imgWidth = ratio * img.width; - let imgX = col * cellWidth + shiftX + (cellWidth - imgWidth)/2; - - ctx.drawImage(img, imgX+cell_padding, imgY+cell_padding, imgWidth-cell_padding*2, imgHeight-cell_padding*2); - if(!compact_mode) { - // rectangle cell and border line style - ctx.strokeStyle = "#8F8F8F"; - ctx.lineWidth = 1; - ctx.strokeRect(x+cell_padding, y+cell_padding, cellWidth-cell_padding*2, cellHeight-cell_padding*2); - } - - ctx.filter = "none"; - } - - if (!anyHovered) { - this.pointerDown = null; - this.overIndex = null; - } - } else { - // Draw individual - let w = this.imgs[imageIndex].naturalWidth; - let h = this.imgs[imageIndex].naturalHeight; - - const scaleX = dw / w; - const scaleY = dh / h; - const scale = Math.min(scaleX, scaleY, 1); - - w *= scale; - h *= scale; - - let x = (dw - w) / 2; - let y = (dh - h) / 2 + shiftY; - ctx.drawImage(this.imgs[imageIndex], x, y, w, h); - - const drawButton = (x, y, sz, text) => { - const hovered = LiteGraph.isInsideRectangle(mouse[0], mouse[1], x + this.pos[0], y + this.pos[1], sz, sz); - let fill = "#333"; - let textFill = "#fff"; - let isClicking = false; - if (hovered) { - canvas.canvas.style.cursor = "pointer"; - if (canvas.pointer_is_down) { - fill = "#1e90ff"; - isClicking = true; - } else { - fill = "#eee"; - textFill = "#000"; - } - } else { - this.pointerWasDown = null; - } - - ctx.fillStyle = fill; - ctx.beginPath(); - ctx.roundRect(x, y, sz, sz, [4]); - ctx.fill(); - ctx.fillStyle = textFill; - ctx.font = "12px Arial"; - ctx.textAlign = "center"; - ctx.fillText(text, x + 15, y + 20); - - return isClicking; - }; - - if (numImages > 1) { - if (drawButton(dw - 40, dh + top - 40, 30, `${this.imageIndex + 1}/${numImages}`)) { - let i = this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1; - if (!this.pointerDown || !this.pointerDown.index === i) { - this.pointerDown = { index: i, pos: [...mouse] }; - } - } - - if (drawButton(dw - 40, top + 10, 30, `x`)) { - if (!this.pointerDown || !this.pointerDown.index === null) { - this.pointerDown = { index: null, pos: [...mouse] }; - } - } - } - } - } - } - }; - } - - /** - * Adds a handler allowing drag+drop of files onto the window to load workflows - */ - #addDropHandler() { - // Get prompt from dropped PNG or json - document.addEventListener("drop", async (event) => { - event.preventDefault(); - event.stopPropagation(); - - const n = this.dragOverNode; - this.dragOverNode = null; - // Node handles file drop, we dont use the built in onDropFile handler as its buggy - // If you drag multiple files it will call it multiple times with the same file - // @ts-ignore This is not a standard event. TODO fix it. - if (n && n.onDragDrop && (await n.onDragDrop(event))) { - return; - } - // Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that - if (event.dataTransfer.files.length && event.dataTransfer.files[0].type !== "image/bmp") { - await this.handleFile(event.dataTransfer.files[0]); - } else { - // Try loading the first URI in the transfer list - const validTypes = ["text/uri-list", "text/x-moz-url"]; - const match = [...event.dataTransfer.types].find((t) => validTypes.find(v => t === v)); - if (match) { - const uri = event.dataTransfer.getData(match)?.split("\n")?.[0]; - if (uri) { - await this.handleFile(await (await fetch(uri)).blob()); - } - } - } - }); - - // Always clear over node on drag leave - this.canvasEl.addEventListener("dragleave", async () => { - if (this.dragOverNode) { - this.dragOverNode = null; - this.graph.setDirtyCanvas(false, true); - } - }); - - // Add handler for dropping onto a specific node - this.canvasEl.addEventListener( - "dragover", - (e) => { - this.canvas.adjustMouseEvent(e); - // @ts-ignore: canvasX and canvasY are added by adjustMouseEvent in litegraph - const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY); - if (node) { - // @ts-ignore This is not a standard event. TODO fix it. - if (node.onDragOver && node.onDragOver(e)) { - this.dragOverNode = node; - - // dragover event is fired very frequently, run this on an animation frame - requestAnimationFrame(() => { - this.graph.setDirtyCanvas(false, true); - }); - return; - } - } - this.dragOverNode = null; - }, - false - ); - } - - /** - * Adds a handler on paste that extracts and loads images or workflows from pasted JSON data - */ - #addPasteHandler() { - document.addEventListener("paste", async (e: ClipboardEvent) => { - // ctrl+shift+v is used to paste nodes with connections - // this is handled by litegraph - if(this.shiftDown) return; - - // @ts-ignore: Property 'clipboardData' does not exist on type 'Window & typeof globalThis'. - // Did you mean 'Clipboard'?ts(2551) - // TODO: Not sure what the code wants to do. - let data = (e.clipboardData || window.clipboardData); - const items = data.items; - - // Look for image paste data - for (const item of items) { - if (item.type.startsWith('image/')) { - var imageNode = null; - - // If an image node is selected, paste into it - if (this.canvas.current_node && - this.canvas.current_node.is_selected && - ComfyApp.isImageNode(this.canvas.current_node)) { - imageNode = this.canvas.current_node; - } - - // No image node selected: add a new one - if (!imageNode) { - const newNode = LiteGraph.createNode("LoadImage"); - newNode.pos = [...this.canvas.graph_mouse]; - imageNode = this.graph.add(newNode); - this.graph.change(); - } - const blob = item.getAsFile(); - imageNode.pasteFile(blob); - return; - } - } - - // No image found. Look for node data - data = data.getData("text/plain"); - let workflow: ComfyWorkflow; - try { - data = data.slice(data.indexOf("{")); - workflow = await parseComfyWorkflow(data); - } catch (err) { - try { - data = data.slice(data.indexOf("workflow\n")); - data = data.slice(data.indexOf("{")); - workflow = await parseComfyWorkflow(data); - } catch (error) { - console.error(error); - } - } - - if (workflow && workflow.version && workflow.nodes && workflow.extra) { - await this.loadGraphData(workflow); - } else { - if ((e.target instanceof HTMLInputElement) && (e.target.type === "text" || e.target.type === "textarea")) { - return; - } - - // Litegraph default paste - this.canvas.pasteFromClipboard(); - } - - - }); - } - - - /** - * Adds a handler on copy that serializes selected nodes to JSON - */ - #addCopyHandler() { - document.addEventListener("copy", (e) => { - if ((e.target instanceof HTMLInputElement) && (e.target.type === "text" || e.target.type === "textarea")) { - // Default system copy - return; - } - - // copy nodes and clear clipboard - if ((e.target instanceof HTMLElement) && e.target.className === "litegraph" && this.canvas.selected_nodes) { - this.canvas.copyToClipboard(); - e.clipboardData.setData('text', ' '); //clearData doesn't remove images from clipboard - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }); - } - - - /** - * Handle mouse - * - * Move group by header - */ - #addProcessMouseHandler() { - const self = this; - - // @ts-ignore - const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown; - // @ts-ignore - LGraphCanvas.prototype.processMouseDown = function(e) { - // prepare for ctrl+shift drag: zoom start - if(e.ctrlKey && e.shiftKey && e.buttons) { - self.zoom_drag_start = [e.x, e.y, this.ds.scale]; - return; - } - - const res = origProcessMouseDown.apply(this, arguments); - - this.selected_group_moving = false; - - if (this.selected_group && !this.selected_group_resizing) { - var font_size = - this.selected_group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - var height = font_size * 1.4; - - // Move group by header - // @ts-ignore - if (LiteGraph.isInsideRectangle(e.canvasX, e.canvasY, this.selected_group.pos[0], this.selected_group.pos[1], this.selected_group.size[0], height)) { - this.selected_group_moving = true; - } - } - - return res; - } - // @ts-ignore - const origProcessMouseMove = LGraphCanvas.prototype.processMouseMove; - // @ts-ignore - LGraphCanvas.prototype.processMouseMove = function(e) { - // handle ctrl+shift drag - if(e.ctrlKey && e.shiftKey && self.zoom_drag_start) { - // stop canvas zoom action - if(!e.buttons) { - self.zoom_drag_start = null; - return; - } - - // calculate delta - let deltaY = e.y - self.zoom_drag_start[1]; - let startScale = self.zoom_drag_start[2]; - - let scale = startScale - deltaY/100; - - this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]); - this.graph.change(); - - return; - } - - const orig_selected_group = this.selected_group; - - if (this.selected_group && !this.selected_group_resizing && !this.selected_group_moving) { - this.selected_group = null; - } - - const res = origProcessMouseMove.apply(this, arguments); - - if (orig_selected_group && !this.selected_group_resizing && !this.selected_group_moving) { - this.selected_group = orig_selected_group; - } - - return res; - }; - } - - /** - * Handle keypress - * - * Ctrl + M mute/unmute selected nodes - */ - #addProcessKeyHandler() { - const self = this; - // @ts-ignore - const origProcessKey = LGraphCanvas.prototype.processKey; - // @ts-ignore - LGraphCanvas.prototype.processKey = function(e) { - if (!this.graph) { - return; - } - - var block_default = false; - - if ((e.target instanceof HTMLElement) && e.target.localName == "input") { - return; - } - - if (e.type == "keydown" && !e.repeat) { - - // Ctrl + M mute/unmute - if (e.key === 'm' && e.ctrlKey) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].mode === 2) { // never - this.selected_nodes[i].mode = 0; // always - } else { - this.selected_nodes[i].mode = 2; // never - } - } - } - block_default = true; - } - - // Ctrl + B bypass - if (e.key === 'b' && e.ctrlKey) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].mode === 4) { // never - this.selected_nodes[i].mode = 0; // always - } else { - this.selected_nodes[i].mode = 4; // never - } - } - } - block_default = true; - } - - // Alt + C collapse/uncollapse - if (e.key === 'c' && e.altKey) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - this.selected_nodes[i].collapse() - } - } - block_default = true; - } - - // Ctrl+C Copy - if ((e.key === 'c') && (e.metaKey || e.ctrlKey)) { - // Trigger onCopy - return true; - } - - // Ctrl+V Paste - if ((e.key === 'v' || e.key == 'V') && (e.metaKey || e.ctrlKey) && !e.shiftKey) { - // Trigger onPaste - return true; - } - - if((e.key === '+') && e.altKey) { - block_default = true; - let scale = this.ds.scale * 1.1; - this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]); - this.graph.change(); - } - - if((e.key === '-') && e.altKey) { - block_default = true; - let scale = this.ds.scale * 1 / 1.1; - this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]); - this.graph.change(); - } - } - - this.graph.change(); - - if (block_default) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - - // Fall through to Litegraph defaults - return origProcessKey.apply(this, arguments); - }; - } - - /** - * Draws group header bar - */ - #addDrawGroupsHandler() { - const self = this; - // @ts-ignore - const origDrawGroups = LGraphCanvas.prototype.drawGroups; - // @ts-ignore - LGraphCanvas.prototype.drawGroups = function(canvas, ctx) { - if (!this.graph) { - return; - } - - var groups = this.graph._groups; - - ctx.save(); - ctx.globalAlpha = 0.7 * this.editor_alpha; - - for (var i = 0; i < groups.length; ++i) { - var group = groups[i]; - - if (!LiteGraph.overlapBounding(this.visible_area, group._bounding)) { - continue; - } //out of the visible area - - ctx.fillStyle = group.color || "#335"; - ctx.strokeStyle = group.color || "#335"; - var pos = group._pos; - var size = group._size; - ctx.globalAlpha = 0.25 * this.editor_alpha; - ctx.beginPath(); - var font_size = - group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], font_size * 1.4); - ctx.fill(); - ctx.globalAlpha = this.editor_alpha; - } - - ctx.restore(); - - const res = origDrawGroups.apply(this, arguments); - return res; - } - } - - /** - * Draws node highlights (executing, drag drop) and progress bar - */ - #addDrawNodeHandler() { - // @ts-ignore - const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape; - const self = this; - // @ts-ignore - LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) { - const res = origDrawNodeShape.apply(this, arguments); - - const nodeErrors = self.lastNodeErrors?.[node.id]; - - let color = null; - let lineWidth = 1; - if (node.id === +self.runningNodeId) { - color = "#0f0"; - } else if (self.dragOverNode && node.id === self.dragOverNode.id) { - color = "dodgerblue"; - } - else if (nodeErrors?.errors) { - color = "red"; - lineWidth = 2; - } - else if (self.lastExecutionError && +self.lastExecutionError.node_id === node.id) { - color = "#f0f"; - lineWidth = 2; - } - - if (color) { - // @ts-ignore - const shape = node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE; - ctx.lineWidth = lineWidth; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE) - ctx.rect(-6, -6 - LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT); - else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed)) - ctx.roundRect( - -6, - -6 - LiteGraph.NODE_TITLE_HEIGHT, - 12 + size[0] + 1, - 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, - this.round_radius * 2 - ); - else if (shape == LiteGraph.CARD_SHAPE) - ctx.roundRect( - -6, - -6 - LiteGraph.NODE_TITLE_HEIGHT, - 12 + size[0] + 1, - 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, - [this.round_radius * 2, this.round_radius * 2, 2, 2] - ); - else if (shape == LiteGraph.CIRCLE_SHAPE) - ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2); - ctx.strokeStyle = color; - ctx.stroke(); - ctx.strokeStyle = fgcolor; - ctx.globalAlpha = 1; - } - - if (self.progress && node.id === +self.runningNodeId) { - ctx.fillStyle = "green"; - ctx.fillRect(0, 0, size[0] * (self.progress.value / self.progress.max), 6); - ctx.fillStyle = bgcolor; - } - - // Highlight inputs that failed validation - if (nodeErrors) { - ctx.lineWidth = 2; - ctx.strokeStyle = "red"; - for (const error of nodeErrors.errors) { - if (error.extra_info && error.extra_info.input_name) { - const inputIndex = node.findInputSlot(error.extra_info.input_name) - if (inputIndex !== -1) { - let pos = node.getConnectionPos(true, inputIndex); - ctx.beginPath(); - ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false) - ctx.stroke(); - } - } - } - } - - return res; - }; - - // @ts-ignore - const origDrawNode = LGraphCanvas.prototype.drawNode; - // @ts-ignore - LGraphCanvas.prototype.drawNode = function (node, ctx) { - var editor_alpha = this.editor_alpha; - var old_color = node.bgcolor; - - if (node.mode === 2) { // never - this.editor_alpha = 0.4; - } - - // Mode never equals 4 by ts check. - // if (node.mode === 4) { // never - // node.bgcolor = "#FF00FF"; - // this.editor_alpha = 0.2; - // } - - const res = origDrawNode.apply(this, arguments); - - this.editor_alpha = editor_alpha; - node.bgcolor = old_color; - - return res; - }; - } - - /** - * Handles updates from the API socket - */ - #addApiUpdateHandlers() { - api.addEventListener("status", ({ detail }) => { - this.ui.setStatus(detail); - }); - - api.addEventListener("reconnecting", () => { - this.ui.dialog.show("Reconnecting..."); - }); - - api.addEventListener("reconnected", () => { - this.ui.dialog.close(); - }); - - api.addEventListener("progress", ({ detail }) => { - if (this.workflowManager.activePrompt?.workflow - && this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return; - this.progress = detail; - this.graph.setDirtyCanvas(true, false); - }); - - api.addEventListener("executing", ({ detail }) => { - if (this.workflowManager.activePrompt ?.workflow - && this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return; - this.progress = null; - this.runningNodeId = detail; - this.graph.setDirtyCanvas(true, false); - delete this.nodePreviewImages[this.runningNodeId] - }); - - api.addEventListener("executed", ({ detail }) => { - if (this.workflowManager.activePrompt ?.workflow - && this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return; - const output = this.nodeOutputs[detail.node]; - if (detail.merge && output) { - for (const k in detail.output ?? {}) { - const v = output[k]; - if (v instanceof Array) { - output[k] = v.concat(detail.output[k]); - } else { - output[k] = detail.output[k]; - } - } - } else { - this.nodeOutputs[detail.node] = detail.output; - } - const node = this.graph.getNodeById(detail.node); - if (node) { - // @ts-ignore - if (node.onExecuted) - // @ts-ignore - node.onExecuted(detail.output); - } - }); - - api.addEventListener("execution_start", ({ detail }) => { - this.runningNodeId = null; - this.lastExecutionError = null - // @ts-ignore - this.graph._nodes.forEach((node) => { - // @ts-ignore - if (node.onExecutionStart) - // @ts-ignore - node.onExecutionStart() - }) - }); - - api.addEventListener("execution_error", ({ detail }) => { - this.lastExecutionError = detail; - const formattedError = this.#formatExecutionError(detail); - this.ui.dialog.show(formattedError); - this.canvas.draw(true, true); - }); - - api.addEventListener("b_preview", ({ detail }) => { - const id = this.runningNodeId - if (id == null) - return; - - const blob = detail - const blobUrl = URL.createObjectURL(blob) - // @ts-ignore - this.nodePreviewImages[id] = [blobUrl] - }); - - api.init(); - } - - #addKeyboardHandler() { - window.addEventListener("keydown", (e) => { - this.shiftDown = e.shiftKey; - }); - window.addEventListener("keyup", (e) => { - this.shiftDown = e.shiftKey; - }); - } - - #addConfigureHandler() { - const app = this; - // @ts-ignore - const configure = LGraph.prototype.configure; - // Flag that the graph is configuring to prevent nodes from running checks while its still loading - // @ts-ignore - LGraph.prototype.configure = function () { - app.configuringGraph = true; - try { - return configure.apply(this, arguments); - } finally { - app.configuringGraph = false; - } - }; - } - - #addAfterConfigureHandler() { - const app = this; - // @ts-ignore - const onConfigure = app.graph.onConfigure; - // @ts-ignore - app.graph.onConfigure = function () { - // Fire callbacks before the onConfigure, this is used by widget inputs to setup the config - // @ts-ignore - for (const node of app.graph._nodes) { - // @ts-ignore - node.onGraphConfigured?.(); - } - - const r = onConfigure?.apply(this, arguments); - - // Fire after onConfigure, used by primitves to generate widget using input nodes config - // @ts-ignore _nodes is private. - for (const node of app.graph._nodes) { - // @ts-ignore - node.onAfterGraphConfigured?.(); - } - - return r; - }; - } - - /** - * Loads all extensions from the API into the window in parallel - */ - async #loadExtensions() { - const extensions = await api.getExtensions(); - this.logging.addEntry("Comfy.App", "debug", { Extensions: extensions }); - - // Need to load core extensions first as some custom extensions - // may depend on them. - await import("../extensions/core/index.js"); - await Promise.all(extensions - .filter(extension => !extension.includes("extensions/core")) - .map(async ext => { - try { - await import(/* @vite-ignore */api.fileURL(ext)); - } catch (error) { - console.error("Error loading extension", ext, error); - } - })); - - try { - this.menu.workflows.registerExtension(this); - } catch (error) { - console.error(error); - } - } - - async #migrateSettings() { - this.isNewUserSession = true; - // Store all current settings - const settings = Object.keys(this.ui.settings).reduce((p, n) => { - const v = localStorage[`Comfy.Settings.${n}`]; - if (v) { - try { - p[n] = JSON.parse(v); - } catch (error) {} - } - return p; - }, {}); - - await api.storeSettings(settings); - } - - async #setUser() { - const userConfig = await api.getUserConfig(); - this.storageLocation = userConfig.storage; - if (typeof userConfig.migrated == "boolean") { - // Single user mode migrated true/false for if the default user is created - if (!userConfig.migrated && this.storageLocation === "server") { - // Default user not created yet - await this.#migrateSettings(); - } - return; - } - - this.multiUserServer = true; - let user = localStorage["Comfy.userId"]; - const users = userConfig.users ?? {}; - if (!user || !users[user]) { - // This will rarely be hit so move the loading to on demand - const { UserSelectionScreen } = await import("./ui/userSelection"); - - this.ui.menuContainer.style.display = "none"; - const { userId, username, created } = await new UserSelectionScreen().show(users, user); - this.ui.menuContainer.style.display = ""; - - user = userId; - localStorage["Comfy.userName"] = username; - localStorage["Comfy.userId"] = user; - - if (created) { - api.user = user; - await this.#migrateSettings(); - } - } - - api.user = user; - - this.ui.settings.addSetting({ - id: "Comfy.SwitchUser", - name: "Switch User", - type: (name) => { - let currentUser = localStorage["Comfy.userName"]; - if (currentUser) { - currentUser = ` (${currentUser})`; - } - return $el("tr", [ - $el("td", [ - $el("label", { - textContent: name, - }), - ]), - $el("td", [ - $el("button", { - textContent: name + (currentUser ?? ""), - onclick: () => { - delete localStorage["Comfy.userId"]; - delete localStorage["Comfy.userName"]; - window.location.reload(); - }, - }), - ]), - ]); - }, - // TODO: Is that the correct default value? - defaultValue: undefined, - }); - } - - /** - * Set up the app on the page - */ - async setup() { - await this.#setUser(); - - // Create and mount the LiteGraph in the DOM - const mainCanvas = document.createElement("canvas") - mainCanvas.style.touchAction = "none" - const canvasEl = (this.canvasEl = Object.assign(mainCanvas, { id: "graph-canvas" })); - canvasEl.tabIndex = 1; - document.body.prepend(canvasEl); - this.resizeCanvas(); - - await Promise.all([this.workflowManager.loadWorkflows(), this.ui.settings.load()]); - await this.#loadExtensions(); - - addDomClippingSetting(); - this.#addProcessMouseHandler(); - this.#addProcessKeyHandler(); - this.#addConfigureHandler(); - this.#addApiUpdateHandlers(); - this.#addRestoreWorkflowView(); - - // @ts-ignore - this.graph = new LGraph(); - - this.#addAfterConfigureHandler(); - - // @ts-ignore - this.canvas = new LGraphCanvas(canvasEl, this.graph); - this.ctx = canvasEl.getContext("2d"); - - LiteGraph.release_link_on_empty_shows_menu = true; - LiteGraph.alt_drag_do_clone_nodes = true; - - this.graph.start(); - - // Ensure the canvas fills the window - this.resizeCanvas(); - window.addEventListener("resize", () => this.resizeCanvas()); - const ro = new ResizeObserver(() => this.resizeCanvas()); - ro.observe(this.bodyTop); - ro.observe(this.bodyLeft); - ro.observe(this.bodyRight); - ro.observe(this.bodyBottom); - - await this.#invokeExtensionsAsync("init"); - await this.registerNodes(); - initWidgets(this); - - // Load previous workflow - let restored = false; - try { - const loadWorkflow = async (json) => { - if (json) { - const workflow = await parseComfyWorkflow(json); - const workflowName = getStorageValue("Comfy.PreviousWorkflow"); - await this.loadGraphData(workflow, true, true, workflowName); - return true; - } - }; - const clientId = api.initialClientId ?? api.clientId; - restored = - (clientId && (await loadWorkflow(sessionStorage.getItem(`workflow:${clientId}`)))) || - (await loadWorkflow(localStorage.getItem("workflow"))); - } catch (err) { - console.error("Error loading previous workflow", err); - } - - // We failed to restore a workflow so load the default - if (!restored) { - await this.loadGraphData(); - } - - // Save current workflow automatically - setInterval(() => { - const workflow = JSON.stringify(this.graph.serialize()); - localStorage.setItem("workflow", workflow); - if (api.clientId) { - sessionStorage.setItem(`workflow:${api.clientId}`, workflow); - } - }, 1000); - - this.#addDrawNodeHandler(); - this.#addDrawGroupsHandler(); - this.#addDropHandler(); - this.#addCopyHandler(); - this.#addPasteHandler(); - this.#addKeyboardHandler(); - - await this.#invokeExtensionsAsync("setup"); - } - - resizeCanvas() { - // Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845 - const scale = Math.max(window.devicePixelRatio, 1); - - // Clear fixed width and height while calculating rect so it uses 100% instead - this.canvasEl.height = this.canvasEl.width = NaN; - const { width, height } = this.canvasEl.getBoundingClientRect(); - this.canvasEl.width = Math.round(width * scale); - this.canvasEl.height = Math.round(height * scale); - this.canvasEl.getContext("2d").scale(scale, scale); - this.canvas?.draw(true, true); - } - - /** - * Registers nodes with the graph - */ - async registerNodes() { - const app = this; - // Load node definitions from the backend - const defs = await api.getNodeDefs(); - await this.registerNodesFromDefs(defs); - await this.#invokeExtensionsAsync("registerCustomNodes"); - } - - getWidgetType(inputData, inputName) { - const type = inputData[0]; - - if (Array.isArray(type)) { - return "COMBO"; - } else if (`${type}:${inputName}` in this.widgets) { - return `${type}:${inputName}`; - } else if (type in this.widgets) { - return type; - } else { - return null; - } - } - - async registerNodeDef(nodeId: string, nodeData: ComfyNodeDef) { - const self = this; - const node = Object.assign( - function ComfyNode() { - var inputs = nodeData["input"]["required"]; - if (nodeData["input"]["optional"] != undefined) { - inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]); - } - const config = { minWidth: 1, minHeight: 1 }; - for (const inputName in inputs) { - const inputData = inputs[inputName]; - const type = inputData[0]; - - let widgetCreated = true; - const widgetType = self.getWidgetType(inputData, inputName); - if(widgetType) { - if(widgetType === "COMBO") { - Object.assign(config, self.widgets.COMBO(this, inputName, inputData, app) || {}); - } else { - Object.assign(config, self.widgets[widgetType](this, inputName, inputData, app) || {}); - } - } else { - // Node connection inputs - this.addInput(inputName, type); - widgetCreated = false; - } - // @ts-ignore - if(widgetCreated && inputData[1]?.forceInput && config?.widget) { - // @ts-ignore - if (!config.widget.options) config.widget.options = {}; - // @ts-ignore - config.widget.options.forceInput = inputData[1].forceInput; - } - // @ts-ignore - if(widgetCreated && inputData[1]?.defaultInput && config?.widget) { - // @ts-ignore - if (!config.widget.options) config.widget.options = {}; - // @ts-ignore - config.widget.options.defaultInput = inputData[1].defaultInput; - } - } - - for (const o in nodeData["output"]) { - let output = nodeData["output"][o]; - if(output instanceof Array) output = "COMBO"; - const outputName = nodeData["output_name"][o] || output; - const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ; - this.addOutput(outputName, output, { shape: outputShape }); - } - - const s = this.computeSize(); - s[0] = Math.max(config.minWidth, s[0] * 1.5); - s[1] = Math.max(config.minHeight, s[1]); - this.size = s; - this.serialize_widgets = true; - - app.#invokeExtensionsAsync("nodeCreated", this); - }, - { - title: nodeData.display_name || nodeData.name, - comfyClass: nodeData.name, - nodeData - } - ); - node.prototype.comfyClass = nodeData.name; - - this.#addNodeContextMenuHandler(node); - this.#addDrawBackgroundHandler(node); - this.#addNodeKeyHandler(node); - - await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData); - // @ts-ignore - LiteGraph.registerNodeType(nodeId, node); - // @ts-ignore - node.category = nodeData.category; - } - - async registerNodesFromDefs(defs: Record) { - await this.#invokeExtensionsAsync("addCustomNodeDefs", defs); - - // Generate list of known widgets - this.widgets = Object.assign( - {}, - ComfyWidgets, - ...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean) - ); - - // Register a node for each definition - for (const nodeId in defs) { - this.registerNodeDef(nodeId, defs[nodeId]); - } - } - - loadTemplateData(templateData) { - if (!templateData?.templates) { - return; - } - - const old = localStorage.getItem("litegrapheditor_clipboard"); - - var maxY, nodeBottom, node; - - for (const template of templateData.templates) { - if (!template?.data) { - continue; - } - - localStorage.setItem("litegrapheditor_clipboard", template.data); - app.canvas.pasteFromClipboard(); - - // Move mouse position down to paste the next template below - - maxY = false; - - for (const i in app.canvas.selected_nodes) { - node = app.canvas.selected_nodes[i]; - - nodeBottom = node.pos[1] + node.size[1]; - - if (maxY === false || nodeBottom > maxY) { - maxY = nodeBottom; - } - } - - app.canvas.graph_mouse[1] = maxY + 50; - } - - localStorage.setItem("litegrapheditor_clipboard", old); - } - - showMissingNodesError(missingNodeTypes, hasAddedNodes = true) { - let seenTypes = new Set(); - - this.ui.dialog.show( - $el("div.comfy-missing-nodes", [ - $el("span", { textContent: "When loading the graph, the following node types were not found: " }), - $el( - "ul", - Array.from(new Set(missingNodeTypes)).map((t) => { - let children = []; - if (typeof t === "object") { - // @ts-ignore - if(seenTypes.has(t.type)) return null; - // @ts-ignore - seenTypes.add(t.type); - // @ts-ignore - children.push($el("span", { textContent: t.type })); - // @ts-ignore - if (t.hint) { - // @ts-ignore - children.push($el("span", { textContent: t.hint })); - } - // @ts-ignore - if (t.action) { - // @ts-ignore - children.push($el("button", { onclick: t.action.callback, textContent: t.action.text })); - } - } else { - if(seenTypes.has(t)) return null; - seenTypes.add(t); - // @ts-ignore - children.push($el("span", { textContent: t })); - } - return $el("li", children); - }).filter(Boolean) - ), - ...(hasAddedNodes - ? [$el("span", { textContent: "Nodes that have failed to load will show as red on the graph." })] - : []), - ]) - ); - this.logging.addEntry("Comfy.App", "warn", { - MissingNodes: missingNodeTypes, - }); - } - - async changeWorkflow(callback, workflow = null) { - try { - this.workflowManager.activeWorkflow?.changeTracker?.store() - } catch (error) { - console.error(error); - } - await callback(); - try { - this.workflowManager.setWorkflow(workflow); - this.workflowManager.activeWorkflow?.track() - } catch (error) { - console.error(error); - } - } - - /** - * Populates the graph with the specified workflow data - * @param {*} graphData A serialized graph object - * @param { boolean } clean If the graph state, e.g. images, should be cleared - */ - async loadGraphData( - graphData?: ComfyWorkflow, - clean: boolean = true, - restore_view: boolean = true, - workflow: string | null = null - ) { - if (clean !== false) { - this.clean(); - } - - let reset_invalid_values = false; - if (!graphData) { - graphData = defaultGraph; - reset_invalid_values = true; - } - - if (typeof structuredClone === "undefined") - { - graphData = JSON.parse(JSON.stringify(graphData)); - }else - { - graphData = structuredClone(graphData); - } - - try { - this.workflowManager.setWorkflow(workflow); - } catch (error) { - console.error(error); - } - - const missingNodeTypes = []; - await this.#invokeExtensionsAsync("beforeConfigureGraph", graphData, missingNodeTypes); - for (let n of graphData.nodes) { - // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now - if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader"; - if (n.type == "ConditioningAverage ") n.type = "ConditioningAverage"; //typo fix - if (n.type == "SDV_img2vid_Conditioning") n.type = "SVD_img2vid_Conditioning"; //typo fix - - // Find missing node types - if (!(n.type in LiteGraph.registered_node_types)) { - missingNodeTypes.push(n.type); - n.type = sanitizeNodeName(n.type); - } - } - - try { - this.graph.configure(graphData); - if (restore_view && this.enableWorkflowViewRestore.value && graphData.extra?.ds) { - // @ts-ignore - // Need to set strict: true for zod to match the type [number, number] - // https://github.com/colinhacks/zod/issues/3056 - this.canvas.ds.offset = graphData.extra.ds.offset; - this.canvas.ds.scale = graphData.extra.ds.scale; - } - - try { - this.workflowManager.activeWorkflow?.track() - } catch (error) { - // TODO: Do we want silently fail here? - } - } catch (error) { - let errorHint = []; - // Try extracting filename to see if it was caused by an extension script - const filename = error.fileName || (error.stack || "").match(/(\/extensions\/.*\.js)/)?.[1]; - const pos = (filename || "").indexOf("/extensions/"); - if (pos > -1) { - errorHint.push( - $el("span", { textContent: "This may be due to the following script:" }), - $el("br"), - $el("span", { - style: { - fontWeight: "bold", - }, - textContent: filename.substring(pos), - }) - ); - } - - // Show dialog to let the user know something went wrong loading the data - this.ui.dialog.show( - $el("div", [ - $el("p", { textContent: "Loading aborted due to error reloading workflow data" }), - $el("pre", { - style: { padding: "5px", backgroundColor: "rgba(255,0,0,0.2)" }, - textContent: error.toString(), - }), - $el("pre", { - style: { - padding: "5px", - color: "#ccc", - fontSize: "10px", - maxHeight: "50vh", - overflow: "auto", - backgroundColor: "rgba(0,0,0,0.2)", - }, - textContent: error.stack || "No stacktrace available", - }), - ...errorHint, - ]).outerHTML - ); - - return; - } - // @ts-ignore - for (const node of this.graph._nodes) { - const size = node.computeSize(); - size[0] = Math.max(node.size[0], size[0]); - size[1] = Math.max(node.size[1], size[1]); - node.size = size; - // @ts-ignore - if (node.widgets) { - // If you break something in the backend and want to patch workflows in the frontend - // This is the place to do this - // @ts-ignore - for (let widget of node.widgets) { - if (node.type == "KSampler" || node.type == "KSamplerAdvanced") { - if (widget.name == "sampler_name") { - if (widget.value.startsWith("sample_")) { - widget.value = widget.value.slice(7); - } - } - } - if (node.type == "KSampler" || node.type == "KSamplerAdvanced" || node.type == "PrimitiveNode") { - if (widget.name == "control_after_generate") { - if (widget.value === true) { - widget.value = "randomize"; - } else if (widget.value === false) { - widget.value = "fixed"; - } - } - } - if (reset_invalid_values) { - if (widget.type == "combo") { - if (!widget.options.values.includes(widget.value) && widget.options.values.length > 0) { - widget.value = widget.options.values[0]; - } - } - } - } - } - - this.#invokeExtensions("loadedGraphNode", node); - } - - if (missingNodeTypes.length) { - this.showMissingNodesError(missingNodeTypes); - } - await this.#invokeExtensionsAsync("afterConfigureGraph", missingNodeTypes); - requestAnimationFrame(() => { - this.graph.setDirtyCanvas(true, true); - }); - } - - /** - * Converts the current graph workflow for sending to the API - * @returns The workflow and node links - */ - async graphToPrompt(graph = this.graph, clean = true) { - for (const outerNode of this.graph.computeExecutionOrder(false)) { - if (outerNode.widgets) { - for (const widget of outerNode.widgets) { - // Allow widgets to run callbacks before a prompt has been queued - // e.g. random seed before every gen - widget.beforeQueued?.(); - } - } - - const innerNodes = outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode]; - for (const node of innerNodes) { - if (node.isVirtualNode) { - // Don't serialize frontend only nodes but let them make changes - if (node.applyToGraph) { - node.applyToGraph(); - } - } - } - } - - const workflow = graph.serialize(); - const output = {}; - // Process nodes in order of execution - for (const outerNode of graph.computeExecutionOrder(false)) { - const skipNode = outerNode.mode === 2 || outerNode.mode === 4; - const innerNodes = (!skipNode && outerNode.getInnerNodes) ? outerNode.getInnerNodes() : [outerNode]; - for (const node of innerNodes) { - if (node.isVirtualNode) { - continue; - } - - if (node.mode === 2 || node.mode === 4) { - // Don't serialize muted nodes - continue; - } - - const inputs = {}; - const widgets = node.widgets; - - // Store all widget values - if (widgets) { - for (const i in widgets) { - const widget = widgets[i]; - if (!widget.options || widget.options.serialize !== false) { - inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(node, i) : widget.value; - } - } - } - - // Store all node links - for (let i in node.inputs) { - let parent = node.getInputNode(i); - if (parent) { - let link = node.getInputLink(i); - while (parent.mode === 4 || parent.isVirtualNode) { - let found = false; - if (parent.isVirtualNode) { - link = parent.getInputLink(link.origin_slot); - if (link) { - parent = parent.getInputNode(link.target_slot); - if (parent) { - found = true; - } - } - } else if (link && parent.mode === 4) { - let all_inputs = [link.origin_slot]; - if (parent.inputs) { - all_inputs = all_inputs.concat(Object.keys(parent.inputs)) - for (let parent_input in all_inputs) { - parent_input = all_inputs[parent_input]; - if (parent.inputs[parent_input]?.type === node.inputs[i].type) { - link = parent.getInputLink(parent_input); - if (link) { - parent = parent.getInputNode(parent_input); - } - found = true; - break; - } - } - } - } - - if (!found) { - break; - } - } - - if (link) { - if (parent?.updateLink) { - link = parent.updateLink(link); - } - if (link) { - inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)]; - } - } - } - } - - let node_data = { - inputs, - class_type: node.comfyClass, - }; - - if (this.ui.settings.getSettingValue("Comfy.DevMode")) { - // Ignored by the backend. - node_data["_meta"] = { - title: node.title, - } - } - - output[String(node.id)] = node_data; - } - } - - // Remove inputs connected to removed nodes - if(clean) { - for (const o in output) { - for (const i in output[o].inputs) { - if (Array.isArray(output[o].inputs[i]) - && output[o].inputs[i].length === 2 - && !output[output[o].inputs[i][0]]) { - delete output[o].inputs[i]; - } - } - } - } - - return { workflow, output }; - } - - #formatPromptError(error) { - if (error == null) { - return "(unknown error)" - } - else if (typeof error === "string") { - return error; - } - else if (error.stack && error.message) { - return error.toString() - } - else if (error.response) { - let message = error.response.error.message; - if (error.response.error.details) - message += ": " + error.response.error.details; - for (const [nodeID, nodeError] of Object.entries(error.response.node_errors)) { - // @ts-ignore - message += "\n" + nodeError.class_type + ":" - // @ts-ignore - for (const errorReason of nodeError.errors) { - message += "\n - " + errorReason.message + ": " + errorReason.details - } - } - return message - } - return "(unknown error)" - } - - #formatExecutionError(error) { - if (error == null) { - return "(unknown error)" - } - - const traceback = error.traceback.join("") - const nodeId = error.node_id - const nodeType = error.node_type - - return `Error occurred when executing ${nodeType}:\n\n${error.exception_message}\n\n${traceback}` - } - - async queuePrompt(number, batchCount = 1) { - this.#queueItems.push({ number, batchCount }); - - // Only have one action process the items so each one gets a unique seed correctly - if (this.#processingQueue) { - return; - } - - this.#processingQueue = true; - this.lastNodeErrors = null; - - try { - while (this.#queueItems.length) { - ({ number, batchCount } = this.#queueItems.pop()); - - for (let i = 0; i < batchCount; i++) { - const p = await this.graphToPrompt(); - - try { - const res = await api.queuePrompt(number, p); - this.lastNodeErrors = res.node_errors; - if (this.lastNodeErrors.length > 0) { - this.canvas.draw(true, true); - } else { - try { - this.workflowManager.storePrompt({ - id: res.prompt_id, - nodes: Object.keys(p.output) - }); - } catch (error) { - } - } - } catch (error) { - const formattedError = this.#formatPromptError(error) - this.ui.dialog.show(formattedError); - if (error.response) { - this.lastNodeErrors = error.response.node_errors; - this.canvas.draw(true, true); - } - break; - } - - for (const n of p.workflow.nodes) { - const node = this.graph.getNodeById(n.id); - if (node.widgets) { - for (const widget of node.widgets) { - // Allow widgets to run callbacks after a prompt has been queued - // e.g. random seed after every gen - // @ts-ignore - if (widget.afterQueued) { - // @ts-ignore - widget.afterQueued(); - } - } - } - } - - this.canvas.draw(true, true); - await this.ui.queue.update(); - } - } - } finally { - this.#processingQueue = false; - } - api.dispatchEvent(new CustomEvent("promptQueued", { detail: { number, batchCount } })); - return !this.lastNodeErrors; - } - - showErrorOnFileLoad(file) { - this.ui.dialog.show( - $el("div", [ - $el("p", {textContent: `Unable to find workflow in ${file.name}`}) - ]).outerHTML - ); - } - - /** - * Loads workflow data from the specified file - * @param {File} file - */ - async handleFile(file) { - const removeExt = f => { - if(!f) return f; - const p = f.lastIndexOf("."); - if(p === -1) return f; - return f.substring(0, p); - }; - const fileName = removeExt(file.name); - if (file.type === "image/png") { - const pngInfo = await getPngMetadata(file); - if (pngInfo?.workflow) { - await this.loadGraphData(await parseComfyWorkflow(pngInfo.workflow), true, true, fileName); - } else if (pngInfo?.prompt) { - this.loadApiJson(JSON.parse(pngInfo.prompt), fileName); - } else if (pngInfo?.parameters) { - this.changeWorkflow(() => { - importA1111(this.graph, pngInfo.parameters); - }, fileName) - } else { - this.showErrorOnFileLoad(file); - } - } else if (file.type === "image/webp") { - const pngInfo = await getWebpMetadata(file); - // Support loading workflows from that webp custom node. - const workflow = pngInfo?.workflow || pngInfo?.Workflow; - const prompt = pngInfo?.prompt || pngInfo?.Prompt; - - if (workflow) { - this.loadGraphData(await parseComfyWorkflow(workflow), true, true, fileName); - } else if (prompt) { - this.loadApiJson(JSON.parse(prompt), fileName); - } else { - this.showErrorOnFileLoad(file); - } - } else if (file.type === "audio/flac" || file.type === "audio/x-flac") { - const pngInfo = await getFlacMetadata(file); - const workflow = pngInfo?.workflow || pngInfo?.Workflow; - const prompt = pngInfo?.prompt || pngInfo?.Prompt; - - if (workflow) { - this.loadGraphData(await parseComfyWorkflow(workflow), true, true, fileName); - } else if (prompt) { - this.loadApiJson(JSON.parse(prompt), fileName); - } else { - this.showErrorOnFileLoad(file); - } - } else if (file.type === "application/json" || file.name?.endsWith(".json")) { - const reader = new FileReader(); - reader.onload = async () => { - const readerResult = reader.result as string; - const jsonContent = JSON.parse(readerResult); - if (jsonContent?.templates) { - this.loadTemplateData(jsonContent); - } else if(this.isApiJson(jsonContent)) { - this.loadApiJson(jsonContent, fileName); - } else { - await this.loadGraphData(await parseComfyWorkflow(readerResult), true, fileName); - } - }; - reader.readAsText(file); - } else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) { - const info = await getLatentMetadata(file); - // TODO define schema to LatentMetadata - // @ts-ignore - if (info.workflow) { - // @ts-ignore - await this.loadGraphData(await parseComfyWorkflow(info.workflow), true, true, fileName); - // @ts-ignore - } else if (info.prompt) { - // @ts-ignore - this.loadApiJson(JSON.parse(info.prompt)); - } else { - this.showErrorOnFileLoad(file); - } - } else { - this.showErrorOnFileLoad(file); - } - } - - isApiJson(data) { - // @ts-ignore - return Object.values(data).every((v) => v.class_type); - } - - loadApiJson(apiData, fileName: string) { - // @ts-ignore - const missingNodeTypes = Object.values(apiData).filter((n) => !LiteGraph.registered_node_types[n.class_type]); - if (missingNodeTypes.length) { - // @ts-ignore - this.showMissingNodesError(missingNodeTypes.map(t => t.class_type), false); - return; - } - - const ids = Object.keys(apiData); - app.graph.clear(); - for (const id of ids) { - const data = apiData[id]; - const node = LiteGraph.createNode(data.class_type); - // @ts-ignore - node.id = isNaN(+id) ? id : +id; - node.title = data._meta?.title ?? node.title - app.graph.add(node); - } - - this.changeWorkflow(() => { - for (const id of ids) { - const data = apiData[id]; - const node = app.graph.getNodeById(Number.parseInt(id)); - for (const input in data.inputs ?? {}) { - const value = data.inputs[input]; - if (value instanceof Array) { - const [fromId, fromSlot] = value; - const fromNode = app.graph.getNodeById(fromId); - let toSlot = node.inputs?.findIndex((inp) => inp.name === input); - if (toSlot == null || toSlot === -1) { - try { - // Target has no matching input, most likely a converted widget - const widget = node.widgets?.find((w) => w.name === input); - // @ts-ignore - if (widget && node.convertWidgetToInput?.(widget)) { - toSlot = node.inputs?.length - 1; - } - } catch (error) {} - } - if (toSlot != null || toSlot !== -1) { - fromNode.connect(fromSlot, node, toSlot); - } - } else { - const widget = node.widgets?.find((w) => w.name === input); - if (widget) { - widget.value = value; - // @ts-ignore - widget.callback?.(value); - } - } - } - } - app.graph.arrange(); - }, fileName); - - for (const id of ids) { - const data = apiData[id]; - const node = app.graph.getNodeById(Number.parseInt(id)); - for (const input in data.inputs ?? {}) { - const value = data.inputs[input]; - if (value instanceof Array) { - const [fromId, fromSlot] = value; - const fromNode = app.graph.getNodeById(fromId); - let toSlot = node.inputs?.findIndex((inp) => inp.name === input); - if (toSlot == null || toSlot === -1) { - try { - // Target has no matching input, most likely a converted widget - const widget = node.widgets?.find((w) => w.name === input); - // @ts-ignore - if (widget && node.convertWidgetToInput?.(widget)) { - toSlot = node.inputs?.length - 1; - } - } catch (error) {} - } - if (toSlot != null || toSlot !== -1) { - fromNode.connect(fromSlot, node, toSlot); - } - } else { - const widget = node.widgets?.find((w) => w.name === input); - if (widget) { - widget.value = value; - // @ts-ignore - widget.callback?.(value); - } - } - } - } - - app.graph.arrange(); - } - - /** - * Registers a Comfy web extension with the app - * @param {ComfyExtension} extension - */ - registerExtension(extension) { - if (!extension.name) { - throw new Error("Extensions must have a 'name' property."); - } - if (this.extensions.find((ext) => ext.name === extension.name)) { - throw new Error(`Extension named '${extension.name}' already registered.`); - } - this.extensions.push(extension); - } - - /** - * Refresh combo list on whole nodes - */ - async refreshComboInNodes() { - const defs = await api.getNodeDefs(); - - for (const nodeId in defs) { - this.registerNodeDef(nodeId, defs[nodeId]); - } - // @ts-ignore - for(let nodeNum in this.graph._nodes) { - // @ts-ignore - const node = this.graph._nodes[nodeNum]; - const def = defs[node.type]; - // @ts-ignore - // Allow primitive nodes to handle refresh - node.refreshComboInNode?.(defs); - - if(!def) - continue; - - for(const widgetNum in node.widgets) { - const widget = node.widgets[widgetNum] - if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) { - widget.options.values = def["input"]["required"][widget.name][0]; - - if(widget.name != 'image' && !widget.options.values.includes(widget.value)) { - widget.value = widget.options.values[0]; - // @ts-ignore - widget.callback(widget.value); - } - } - } - } - - await this.#invokeExtensionsAsync("refreshComboInNodes", defs); - } - - resetView() { - app.canvas.ds.scale = 1; - app.canvas.ds.offset = [0, 0] - app.graph.setDirtyCanvas(true, true); - } - - /** - * Clean current state - */ - clean() { - this.nodeOutputs = {}; - this.nodePreviewImages = {} - this.lastNodeErrors = null; - this.lastExecutionError = null; - this.runningNodeId = null; - } + /** + * List of entries to queue + * @type {{number: number, batchCount: number}[]} + */ + #queueItems = []; + /** + * If the queue is currently being processed + * @type {boolean} + */ + #processingQueue = false; + + /** + * Content Clipboard + * @type {serialized node object} + */ + static clipspace = null; + static clipspace_invalidate_handler = null; + static open_maskeditor = null; + static clipspace_return_node = null; + + // Force vite to import utils.ts as part of index. + // Force import of DraggableList. + static utils = { + applyTextReplacements, + addStylesheet, + DraggableList, + }; + + ui: ComfyUI; + logging: ComfyLogging; + extensions: ComfyExtension[]; + _nodeOutputs: Record; + nodePreviewImages: Record; + shiftDown: boolean; + graph: LGraph; + enableWorkflowViewRestore: any; + canvas: LGraphCanvas; + dragOverNode: LGraphNode | null; + canvasEl: HTMLCanvasElement; + // x, y, scale + zoom_drag_start: [number, number, number] | null; + lastNodeErrors: any[] | null; + runningNodeId: number | null; + lastExecutionError: { node_id: number } | null; + progress: { value: number; max: number } | null; + configuringGraph: boolean; + isNewUserSession: boolean; + // Are there any other options than "server"? + storageLocation: string; + multiUserServer: boolean; + ctx: CanvasRenderingContext2D; + widgets: Record; + workflowManager: ComfyWorkflowManager; + bodyTop: HTMLElement; + bodyLeft: HTMLElement; + bodyRight: HTMLElement; + bodyBottom: HTMLElement; + menu: ComfyAppMenu; + + constructor() { + this.ui = new ComfyUI(this); + this.logging = new ComfyLogging(this); + this.workflowManager = new ComfyWorkflowManager(this); + this.bodyTop = $el("div.comfyui-body-top", { parent: document.body }); + this.bodyLeft = $el("div.comfyui-body-left", { parent: document.body }); + this.bodyRight = $el("div.comfyui-body-right", { parent: document.body }); + this.bodyBottom = $el("div.comfyui-body-bottom", { parent: document.body }); + this.menu = new ComfyAppMenu(this); + + /** + * List of extensions that are registered with the app + * @type {ComfyExtension[]} + */ + this.extensions = []; + + /** + * Stores the execution output data for each node + * @type {Record} + */ + this.nodeOutputs = {}; + + /** + * Stores the preview image data for each node + * @type {Record} + */ + this.nodePreviewImages = {}; + + /** + * If the shift key on the keyboard is pressed + * @type {boolean} + */ + this.shiftDown = false; + } + + get nodeOutputs() { + return this._nodeOutputs; + } + + set nodeOutputs(value) { + this._nodeOutputs = value; + this.#invokeExtensions("onNodeOutputsUpdated", value); + } + + getPreviewFormatParam() { + let preview_format = this.ui.settings.getSettingValue( + "Comfy.PreviewFormat" + ); + if (preview_format) return `&preview=${preview_format}`; + else return ""; + } + + getRandParam() { + return "&rand=" + Math.random(); + } + + static isImageNode(node) { + return ( + node.imgs || + (node && + node.widgets && + node.widgets.findIndex((obj) => obj.name === "image") >= 0) + ); + } + + static onClipspaceEditorSave() { + if (ComfyApp.clipspace_return_node) { + ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node); + } + } + + static onClipspaceEditorClosed() { + ComfyApp.clipspace_return_node = null; + } + + static copyToClipspace(node) { + var widgets = null; + if (node.widgets) { + widgets = node.widgets.map(({ type, name, value }) => ({ + type, + name, + value, + })); + } + + var imgs = undefined; + var orig_imgs = undefined; + if (node.imgs != undefined) { + imgs = []; + orig_imgs = []; + + for (let i = 0; i < node.imgs.length; i++) { + imgs[i] = new Image(); + imgs[i].src = node.imgs[i].src; + orig_imgs[i] = imgs[i]; + } + } + + var selectedIndex = 0; + if (node.imageIndex) { + selectedIndex = node.imageIndex; + } + + ComfyApp.clipspace = { + widgets: widgets, + imgs: imgs, + original_imgs: orig_imgs, + images: node.images, + selectedIndex: selectedIndex, + img_paste_mode: "selected", // reset to default im_paste_mode state on copy action + }; + + ComfyApp.clipspace_return_node = null; + + if (ComfyApp.clipspace_invalidate_handler) { + ComfyApp.clipspace_invalidate_handler(); + } + } + + static pasteFromClipspace(node) { + if (ComfyApp.clipspace) { + // image paste + if (ComfyApp.clipspace.imgs && node.imgs) { + if (node.images && ComfyApp.clipspace.images) { + if (ComfyApp.clipspace["img_paste_mode"] == "selected") { + node.images = [ + ComfyApp.clipspace.images[ComfyApp.clipspace["selectedIndex"]], + ]; + } else { + node.images = ComfyApp.clipspace.images; + } + + if (app.nodeOutputs[node.id + ""]) + app.nodeOutputs[node.id + ""].images = node.images; + } + + if (ComfyApp.clipspace.imgs) { + // deep-copy to cut link with clipspace + if (ComfyApp.clipspace["img_paste_mode"] == "selected") { + const img = new Image(); + img.src = + ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src; + node.imgs = [img]; + node.imageIndex = 0; + } else { + const imgs = []; + for (let i = 0; i < ComfyApp.clipspace.imgs.length; i++) { + imgs[i] = new Image(); + imgs[i].src = ComfyApp.clipspace.imgs[i].src; + node.imgs = imgs; + } + } + } + } + + if (node.widgets) { + if (ComfyApp.clipspace.images) { + const clip_image = + ComfyApp.clipspace.images[ComfyApp.clipspace["selectedIndex"]]; + const index = node.widgets.findIndex((obj) => obj.name === "image"); + if (index >= 0) { + if ( + node.widgets[index].type != "image" && + typeof node.widgets[index].value == "string" && + clip_image.filename + ) { + node.widgets[index].value = + (clip_image.subfolder ? clip_image.subfolder + "/" : "") + + clip_image.filename + + (clip_image.type ? ` [${clip_image.type}]` : ""); + } else { + node.widgets[index].value = clip_image; + } + } + } + if (ComfyApp.clipspace.widgets) { + ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => { + const prop = Object.values(node.widgets).find( + // @ts-ignoreg + (obj) => obj.type === type && obj.name === name + ); + // @ts-ignore + if (prop && prop.type != "button") { + if ( + // @ts-ignore + prop.type != "image" && + // @ts-ignore + typeof prop.value == "string" && + value.filename + ) { + // @ts-ignore + prop.value = + (value.subfolder ? value.subfolder + "/" : "") + + value.filename + + (value.type ? ` [${value.type}]` : ""); + } else { + // @ts-ignore + prop.value = value; + // @ts-ignore + prop.callback(value); + } + } + }); + } + } + + app.graph.setDirtyCanvas(true); + } + } + + /** + * Invoke an extension callback + * @param {keyof ComfyExtension} method The extension callback to execute + * @param {any[]} args Any arguments to pass to the callback + * @returns + */ + #invokeExtensions(method, ...args) { + let results = []; + for (const ext of this.extensions) { + if (method in ext) { + try { + results.push(ext[method](...args, this)); + } catch (error) { + console.error( + `Error calling extension '${ext.name}' method '${method}'`, + { error }, + { extension: ext }, + { args } + ); + } + } + } + return results; + } + + /** + * Invoke an async extension callback + * Each callback will be invoked concurrently + * @param {string} method The extension callback to execute + * @param {...any} args Any arguments to pass to the callback + * @returns + */ + async #invokeExtensionsAsync(method, ...args) { + return await Promise.all( + this.extensions.map(async (ext) => { + if (method in ext) { + try { + return await ext[method](...args, this); + } catch (error) { + console.error( + `Error calling extension '${ext.name}' method '${method}'`, + { error }, + { extension: ext }, + { args } + ); + } + } + }) + ); + } + + #addRestoreWorkflowView() { + // @ts-ignore + const serialize = LGraph.prototype.serialize; + const self = this; + // @ts-ignore + LGraph.prototype.serialize = function () { + const workflow = serialize.apply(this, arguments); + + // Store the drag & scale info in the serialized workflow if the setting is enabled + if (self.enableWorkflowViewRestore.value) { + if (!workflow.extra) { + workflow.extra = {}; + } + workflow.extra.ds = { + scale: self.canvas.ds.scale, + offset: self.canvas.ds.offset, + }; + } else if (workflow.extra?.ds) { + // Clear any old view data + delete workflow.extra.ds; + } + + return workflow; + }; + this.enableWorkflowViewRestore = this.ui.settings.addSetting({ + id: "Comfy.EnableWorkflowViewRestore", + name: "Save and restore canvas position and zoom level in workflows", + type: "boolean", + defaultValue: true, + }); + } + + /** + * Adds special context menu handling for nodes + * e.g. this adds Open Image functionality for nodes that show images + * @param {*} node The node to add the menu handler + */ + #addNodeContextMenuHandler(node) { + function getCopyImageOption(img) { + if (typeof window.ClipboardItem === "undefined") return []; + return [ + { + content: "Copy Image", + callback: async () => { + const url = new URL(img.src); + url.searchParams.delete("preview"); + + const writeImage = async (blob) => { + await navigator.clipboard.write([ + new ClipboardItem({ + [blob.type]: blob, + }), + ]); + }; + + try { + const data = await fetch(url); + const blob = await data.blob(); + try { + await writeImage(blob); + } catch (error) { + // Chrome seems to only support PNG on write, convert and try again + if (blob.type !== "image/png") { + const canvas = $el("canvas", { + width: img.naturalWidth, + height: img.naturalHeight, + }) as HTMLCanvasElement; + const ctx = canvas.getContext("2d"); + let image; + if (typeof window.createImageBitmap === "undefined") { + image = new Image(); + const p = new Promise((resolve, reject) => { + image.onload = resolve; + image.onerror = reject; + }).finally(() => { + URL.revokeObjectURL(image.src); + }); + image.src = URL.createObjectURL(blob); + await p; + } else { + image = await createImageBitmap(blob); + } + try { + ctx.drawImage(image, 0, 0); + canvas.toBlob(writeImage, "image/png"); + } finally { + if (typeof image.close === "function") { + image.close(); + } + } + + return; + } + throw error; + } + } catch (error) { + alert("Error copying image: " + (error.message ?? error)); + } + }, + }, + ]; + } + + node.prototype.getExtraMenuOptions = function (_, options) { + if (this.imgs) { + // If this node has images then we add an open in new tab item + let img; + if (this.imageIndex != null) { + // An image is selected so select that + img = this.imgs[this.imageIndex]; + } else if (this.overIndex != null) { + // No image is selected but one is hovered + img = this.imgs[this.overIndex]; + } + if (img) { + options.unshift( + { + content: "Open Image", + callback: () => { + let url = new URL(img.src); + url.searchParams.delete("preview"); + window.open(url, "_blank"); + }, + }, + ...getCopyImageOption(img), + { + content: "Save Image", + callback: () => { + const a = document.createElement("a"); + let url = new URL(img.src); + url.searchParams.delete("preview"); + a.href = url.toString(); + a.setAttribute( + "download", + new URLSearchParams(url.search).get("filename") + ); + document.body.append(a); + a.click(); + requestAnimationFrame(() => a.remove()); + }, + } + ); + } + } + + options.push({ + content: "Bypass", + callback: (obj) => { + if (this.mode === 4) this.mode = 0; + else this.mode = 4; + this.graph.change(); + }, + }); + + // prevent conflict of clipspace content + if (!ComfyApp.clipspace_return_node) { + options.push({ + content: "Copy (Clipspace)", + callback: (obj) => { + ComfyApp.copyToClipspace(this); + }, + }); + + if (ComfyApp.clipspace != null) { + options.push({ + content: "Paste (Clipspace)", + callback: () => { + ComfyApp.pasteFromClipspace(this); + }, + }); + } + + if (ComfyApp.isImageNode(this)) { + options.push({ + content: "Open in MaskEditor", + callback: (obj) => { + ComfyApp.copyToClipspace(this); + ComfyApp.clipspace_return_node = this; + ComfyApp.open_maskeditor(); + }, + }); + } + } + }; + } + + #addNodeKeyHandler(node) { + const app = this; + const origNodeOnKeyDown = node.prototype.onKeyDown; + + node.prototype.onKeyDown = function (e) { + if (origNodeOnKeyDown && origNodeOnKeyDown.apply(this, e) === false) { + return false; + } + + if (this.flags.collapsed || !this.imgs || this.imageIndex === null) { + return; + } + + let handled = false; + + if (e.key === "ArrowLeft" || e.key === "ArrowRight") { + if (e.key === "ArrowLeft") { + this.imageIndex -= 1; + } else if (e.key === "ArrowRight") { + this.imageIndex += 1; + } + this.imageIndex %= this.imgs.length; + + if (this.imageIndex < 0) { + this.imageIndex = this.imgs.length + this.imageIndex; + } + handled = true; + } else if (e.key === "Escape") { + this.imageIndex = null; + handled = true; + } + + if (handled === true) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + }; + } + + /** + * Adds Custom drawing logic for nodes + * e.g. Draws images and handles thumbnail navigation on nodes that output images + * @param {*} node The node to add the draw handler + */ + #addDrawBackgroundHandler(node) { + const app = this; + + function getImageTop(node) { + let shiftY; + if (node.imageOffset != null) { + shiftY = node.imageOffset; + } else { + if (node.widgets?.length) { + const w = node.widgets[node.widgets.length - 1]; + shiftY = w.last_y; + if (w.computeSize) { + shiftY += w.computeSize()[1] + 4; + } else if (w.computedHeight) { + shiftY += w.computedHeight; + } else { + shiftY += LiteGraph.NODE_WIDGET_HEIGHT + 4; + } + } else { + shiftY = node.computeSize()[1]; + } + } + return shiftY; + } + + node.prototype.setSizeForImage = function (force) { + if (!force && this.animatedImages) return; + + if (this.inputHeight || this.freeWidgetSpace > 210) { + this.setSize(this.size); + return; + } + const minHeight = getImageTop(this) + 220; + if (this.size[1] < minHeight) { + this.setSize([this.size[0], minHeight]); + } + }; + + node.prototype.onDrawBackground = function (ctx) { + if (!this.flags.collapsed) { + let imgURLs = []; + let imagesChanged = false; + + const output = app.nodeOutputs[this.id + ""]; + if (output?.images) { + this.animatedImages = output?.animated?.find(Boolean); + if (this.images !== output.images) { + this.images = output.images; + imagesChanged = true; + imgURLs = imgURLs.concat( + output.images.map((params) => { + return api.apiURL( + "/view?" + + new URLSearchParams(params).toString() + + (this.animatedImages ? "" : app.getPreviewFormatParam()) + + app.getRandParam() + ); + }) + ); + } + } + + const preview = app.nodePreviewImages[this.id + ""]; + if (this.preview !== preview) { + this.preview = preview; + imagesChanged = true; + if (preview != null) { + imgURLs.push(preview); + } + } + + if (imagesChanged) { + this.imageIndex = null; + if (imgURLs.length > 0) { + Promise.all( + imgURLs.map((src) => { + return new Promise((r) => { + const img = new Image(); + img.onload = () => r(img); + img.onerror = () => r(null); + img.src = src; + }); + }) + ).then((imgs) => { + if ( + (!output || this.images === output.images) && + (!preview || this.preview === preview) + ) { + this.imgs = imgs.filter(Boolean); + this.setSizeForImage?.(); + app.graph.setDirtyCanvas(true); + } + }); + } else { + this.imgs = null; + } + } + + const calculateGrid = (w, h, n) => { + let columns, rows, cellsize; + + if (w > h) { + cellsize = h; + columns = Math.ceil(w / cellsize); + rows = Math.ceil(n / columns); + } else { + cellsize = w; + rows = Math.ceil(h / cellsize); + columns = Math.ceil(n / rows); + } + + while (columns * rows < n) { + cellsize++; + if (w >= h) { + columns = Math.ceil(w / cellsize); + rows = Math.ceil(n / columns); + } else { + rows = Math.ceil(h / cellsize); + columns = Math.ceil(n / rows); + } + } + + const cell_size = Math.min(w / columns, h / rows); + return { cell_size, columns, rows }; + }; + + const is_all_same_aspect_ratio = (imgs) => { + // assume: imgs.length >= 2 + let ratio = imgs[0].naturalWidth / imgs[0].naturalHeight; + + for (let i = 1; i < imgs.length; i++) { + let this_ratio = imgs[i].naturalWidth / imgs[i].naturalHeight; + if (ratio != this_ratio) return false; + } + + return true; + }; + + if (this.imgs?.length) { + const widgetIdx = this.widgets?.findIndex( + (w) => w.name === ANIM_PREVIEW_WIDGET + ); + + if (this.animatedImages) { + // Instead of using the canvas we'll use a IMG + if (widgetIdx > -1) { + // Replace content + const widget = this.widgets[widgetIdx]; + widget.options.host.updateImages(this.imgs); + } else { + const host = createImageHost(this); + this.setSizeForImage(true); + const widget = this.addDOMWidget( + ANIM_PREVIEW_WIDGET, + "img", + host.el, + { + host, + getHeight: host.getHeight, + onDraw: host.onDraw, + hideOnZoom: false, + } + ); + widget.serializeValue = () => undefined; + widget.options.host.updateImages(this.imgs); + } + return; + } + + if (widgetIdx > -1) { + this.widgets[widgetIdx].onRemove?.(); + this.widgets.splice(widgetIdx, 1); + } + + const canvas = app.graph.list_of_graphcanvas[0]; + const mouse = canvas.graph_mouse; + if (!canvas.pointer_is_down && this.pointerDown) { + if ( + mouse[0] === this.pointerDown.pos[0] && + mouse[1] === this.pointerDown.pos[1] + ) { + this.imageIndex = this.pointerDown.index; + } + this.pointerDown = null; + } + + let imageIndex = this.imageIndex; + const numImages = this.imgs.length; + if (numImages === 1 && !imageIndex) { + this.imageIndex = imageIndex = 0; + } + + const top = getImageTop(this); + var shiftY = top; + + let dw = this.size[0]; + let dh = this.size[1]; + dh -= shiftY; + + if (imageIndex == null) { + var cellWidth, cellHeight, shiftX, cell_padding, cols; + + const compact_mode = is_all_same_aspect_ratio(this.imgs); + if (!compact_mode) { + // use rectangle cell style and border line + cell_padding = 2; + const { cell_size, columns, rows } = calculateGrid( + dw, + dh, + numImages + ); + cols = columns; + + cellWidth = cell_size; + cellHeight = cell_size; + shiftX = (dw - cell_size * cols) / 2; + shiftY = (dh - cell_size * rows) / 2 + top; + } else { + cell_padding = 0; + ({ cellWidth, cellHeight, cols, shiftX } = calculateImageGrid( + this.imgs, + dw, + dh + )); + } + + let anyHovered = false; + this.imageRects = []; + for (let i = 0; i < numImages; i++) { + const img = this.imgs[i]; + const row = Math.floor(i / cols); + const col = i % cols; + const x = col * cellWidth + shiftX; + const y = row * cellHeight + shiftY; + if (!anyHovered) { + anyHovered = LiteGraph.isInsideRectangle( + mouse[0], + mouse[1], + x + this.pos[0], + y + this.pos[1], + cellWidth, + cellHeight + ); + if (anyHovered) { + this.overIndex = i; + let value = 110; + if (canvas.pointer_is_down) { + if (!this.pointerDown || this.pointerDown.index !== i) { + this.pointerDown = { index: i, pos: [...mouse] }; + } + value = 125; + } + ctx.filter = `contrast(${value}%) brightness(${value}%)`; + canvas.canvas.style.cursor = "pointer"; + } + } + this.imageRects.push([x, y, cellWidth, cellHeight]); + + let wratio = cellWidth / img.width; + let hratio = cellHeight / img.height; + var ratio = Math.min(wratio, hratio); + + let imgHeight = ratio * img.height; + let imgY = + row * cellHeight + shiftY + (cellHeight - imgHeight) / 2; + let imgWidth = ratio * img.width; + let imgX = col * cellWidth + shiftX + (cellWidth - imgWidth) / 2; + + ctx.drawImage( + img, + imgX + cell_padding, + imgY + cell_padding, + imgWidth - cell_padding * 2, + imgHeight - cell_padding * 2 + ); + if (!compact_mode) { + // rectangle cell and border line style + ctx.strokeStyle = "#8F8F8F"; + ctx.lineWidth = 1; + ctx.strokeRect( + x + cell_padding, + y + cell_padding, + cellWidth - cell_padding * 2, + cellHeight - cell_padding * 2 + ); + } + + ctx.filter = "none"; + } + + if (!anyHovered) { + this.pointerDown = null; + this.overIndex = null; + } + } else { + // Draw individual + let w = this.imgs[imageIndex].naturalWidth; + let h = this.imgs[imageIndex].naturalHeight; + + const scaleX = dw / w; + const scaleY = dh / h; + const scale = Math.min(scaleX, scaleY, 1); + + w *= scale; + h *= scale; + + let x = (dw - w) / 2; + let y = (dh - h) / 2 + shiftY; + ctx.drawImage(this.imgs[imageIndex], x, y, w, h); + + const drawButton = (x, y, sz, text) => { + const hovered = LiteGraph.isInsideRectangle( + mouse[0], + mouse[1], + x + this.pos[0], + y + this.pos[1], + sz, + sz + ); + let fill = "#333"; + let textFill = "#fff"; + let isClicking = false; + if (hovered) { + canvas.canvas.style.cursor = "pointer"; + if (canvas.pointer_is_down) { + fill = "#1e90ff"; + isClicking = true; + } else { + fill = "#eee"; + textFill = "#000"; + } + } else { + this.pointerWasDown = null; + } + + ctx.fillStyle = fill; + ctx.beginPath(); + ctx.roundRect(x, y, sz, sz, [4]); + ctx.fill(); + ctx.fillStyle = textFill; + ctx.font = "12px Arial"; + ctx.textAlign = "center"; + ctx.fillText(text, x + 15, y + 20); + + return isClicking; + }; + + if (numImages > 1) { + if ( + drawButton( + dw - 40, + dh + top - 40, + 30, + `${this.imageIndex + 1}/${numImages}` + ) + ) { + let i = + this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1; + if (!this.pointerDown || !this.pointerDown.index === i) { + this.pointerDown = { index: i, pos: [...mouse] }; + } + } + + if (drawButton(dw - 40, top + 10, 30, `x`)) { + if (!this.pointerDown || !this.pointerDown.index === null) { + this.pointerDown = { index: null, pos: [...mouse] }; + } + } + } + } + } + } + }; + } + + /** + * Adds a handler allowing drag+drop of files onto the window to load workflows + */ + #addDropHandler() { + // Get prompt from dropped PNG or json + document.addEventListener("drop", async (event) => { + event.preventDefault(); + event.stopPropagation(); + + const n = this.dragOverNode; + this.dragOverNode = null; + // Node handles file drop, we dont use the built in onDropFile handler as its buggy + // If you drag multiple files it will call it multiple times with the same file + // @ts-ignore This is not a standard event. TODO fix it. + if (n && n.onDragDrop && (await n.onDragDrop(event))) { + return; + } + // Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that + if ( + event.dataTransfer.files.length && + event.dataTransfer.files[0].type !== "image/bmp" + ) { + await this.handleFile(event.dataTransfer.files[0]); + } else { + // Try loading the first URI in the transfer list + const validTypes = ["text/uri-list", "text/x-moz-url"]; + const match = [...event.dataTransfer.types].find((t) => + validTypes.find((v) => t === v) + ); + if (match) { + const uri = event.dataTransfer.getData(match)?.split("\n")?.[0]; + if (uri) { + await this.handleFile(await (await fetch(uri)).blob()); + } + } + } + }); + + // Always clear over node on drag leave + this.canvasEl.addEventListener("dragleave", async () => { + if (this.dragOverNode) { + this.dragOverNode = null; + this.graph.setDirtyCanvas(false, true); + } + }); + + // Add handler for dropping onto a specific node + this.canvasEl.addEventListener( + "dragover", + (e) => { + this.canvas.adjustMouseEvent(e); + // @ts-ignore: canvasX and canvasY are added by adjustMouseEvent in litegraph + const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY); + if (node) { + // @ts-ignore This is not a standard event. TODO fix it. + if (node.onDragOver && node.onDragOver(e)) { + this.dragOverNode = node; + + // dragover event is fired very frequently, run this on an animation frame + requestAnimationFrame(() => { + this.graph.setDirtyCanvas(false, true); + }); + return; + } + } + this.dragOverNode = null; + }, + false + ); + } + + /** + * Adds a handler on paste that extracts and loads images or workflows from pasted JSON data + */ + #addPasteHandler() { + document.addEventListener("paste", async (e: ClipboardEvent) => { + // ctrl+shift+v is used to paste nodes with connections + // this is handled by litegraph + if (this.shiftDown) return; + + // @ts-ignore: Property 'clipboardData' does not exist on type 'Window & typeof globalThis'. + // Did you mean 'Clipboard'?ts(2551) + // TODO: Not sure what the code wants to do. + let data = e.clipboardData || window.clipboardData; + const items = data.items; + + // Look for image paste data + for (const item of items) { + if (item.type.startsWith("image/")) { + var imageNode = null; + + // If an image node is selected, paste into it + if ( + this.canvas.current_node && + this.canvas.current_node.is_selected && + ComfyApp.isImageNode(this.canvas.current_node) + ) { + imageNode = this.canvas.current_node; + } + + // No image node selected: add a new one + if (!imageNode) { + const newNode = LiteGraph.createNode("LoadImage"); + newNode.pos = [...this.canvas.graph_mouse]; + imageNode = this.graph.add(newNode); + this.graph.change(); + } + const blob = item.getAsFile(); + imageNode.pasteFile(blob); + return; + } + } + + // No image found. Look for node data + data = data.getData("text/plain"); + let workflow: ComfyWorkflow; + try { + data = data.slice(data.indexOf("{")); + workflow = await parseComfyWorkflow(data); + } catch (err) { + try { + data = data.slice(data.indexOf("workflow\n")); + data = data.slice(data.indexOf("{")); + workflow = await parseComfyWorkflow(data); + } catch (error) { + console.error(error); + } + } + + if (workflow && workflow.version && workflow.nodes && workflow.extra) { + await this.loadGraphData(workflow); + } else { + if ( + e.target instanceof HTMLInputElement && + (e.target.type === "text" || e.target.type === "textarea") + ) { + return; + } + + // Litegraph default paste + this.canvas.pasteFromClipboard(); + } + }); + } + + /** + * Adds a handler on copy that serializes selected nodes to JSON + */ + #addCopyHandler() { + document.addEventListener("copy", (e) => { + if ( + e.target instanceof HTMLInputElement && + (e.target.type === "text" || e.target.type === "textarea") + ) { + // Default system copy + return; + } + + // copy nodes and clear clipboard + if ( + e.target instanceof HTMLElement && + e.target.className === "litegraph" && + this.canvas.selected_nodes + ) { + this.canvas.copyToClipboard(); + e.clipboardData.setData("text", " "); //clearData doesn't remove images from clipboard + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + }); + } + + /** + * Handle mouse + * + * Move group by header + */ + #addProcessMouseHandler() { + const self = this; + + // @ts-ignore + const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown; + // @ts-ignore + LGraphCanvas.prototype.processMouseDown = function (e) { + // prepare for ctrl+shift drag: zoom start + if (e.ctrlKey && e.shiftKey && e.buttons) { + self.zoom_drag_start = [e.x, e.y, this.ds.scale]; + return; + } + + const res = origProcessMouseDown.apply(this, arguments); + + this.selected_group_moving = false; + + if (this.selected_group && !this.selected_group_resizing) { + var font_size = + this.selected_group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; + var height = font_size * 1.4; + + // Move group by header + if ( + LiteGraph.isInsideRectangle( + // @ts-ignore + e.canvasX, + // @ts-ignore + e.canvasY, + this.selected_group.pos[0], + this.selected_group.pos[1], + this.selected_group.size[0], + height + ) + ) { + this.selected_group_moving = true; + } + } + + return res; + }; + // @ts-ignore + const origProcessMouseMove = LGraphCanvas.prototype.processMouseMove; + // @ts-ignore + LGraphCanvas.prototype.processMouseMove = function (e) { + // handle ctrl+shift drag + if (e.ctrlKey && e.shiftKey && self.zoom_drag_start) { + // stop canvas zoom action + if (!e.buttons) { + self.zoom_drag_start = null; + return; + } + + // calculate delta + let deltaY = e.y - self.zoom_drag_start[1]; + let startScale = self.zoom_drag_start[2]; + + let scale = startScale - deltaY / 100; + + this.ds.changeScale(scale, [ + this.ds.element.width / 2, + this.ds.element.height / 2, + ]); + this.graph.change(); + + return; + } + + const orig_selected_group = this.selected_group; + + if ( + this.selected_group && + !this.selected_group_resizing && + !this.selected_group_moving + ) { + this.selected_group = null; + } + + const res = origProcessMouseMove.apply(this, arguments); + + if ( + orig_selected_group && + !this.selected_group_resizing && + !this.selected_group_moving + ) { + this.selected_group = orig_selected_group; + } + + return res; + }; + } + + /** + * Handle keypress + * + * Ctrl + M mute/unmute selected nodes + */ + #addProcessKeyHandler() { + const self = this; + // @ts-ignore + const origProcessKey = LGraphCanvas.prototype.processKey; + // @ts-ignore + LGraphCanvas.prototype.processKey = function (e) { + if (!this.graph) { + return; + } + + var block_default = false; + + if (e.target instanceof HTMLElement && e.target.localName == "input") { + return; + } + + if (e.type == "keydown" && !e.repeat) { + // Ctrl + M mute/unmute + if (e.key === "m" && e.ctrlKey) { + if (this.selected_nodes) { + for (var i in this.selected_nodes) { + if (this.selected_nodes[i].mode === 2) { + // never + this.selected_nodes[i].mode = 0; // always + } else { + this.selected_nodes[i].mode = 2; // never + } + } + } + block_default = true; + } + + // Ctrl + B bypass + if (e.key === "b" && e.ctrlKey) { + if (this.selected_nodes) { + for (var i in this.selected_nodes) { + if (this.selected_nodes[i].mode === 4) { + // never + this.selected_nodes[i].mode = 0; // always + } else { + this.selected_nodes[i].mode = 4; // never + } + } + } + block_default = true; + } + + // Alt + C collapse/uncollapse + if (e.key === "c" && e.altKey) { + if (this.selected_nodes) { + for (var i in this.selected_nodes) { + this.selected_nodes[i].collapse(); + } + } + block_default = true; + } + + // Ctrl+C Copy + if (e.key === "c" && (e.metaKey || e.ctrlKey)) { + // Trigger onCopy + return true; + } + + // Ctrl+V Paste + if ( + (e.key === "v" || e.key == "V") && + (e.metaKey || e.ctrlKey) && + !e.shiftKey + ) { + // Trigger onPaste + return true; + } + + if (e.key === "+" && e.altKey) { + block_default = true; + let scale = this.ds.scale * 1.1; + this.ds.changeScale(scale, [ + this.ds.element.width / 2, + this.ds.element.height / 2, + ]); + this.graph.change(); + } + + if (e.key === "-" && e.altKey) { + block_default = true; + let scale = (this.ds.scale * 1) / 1.1; + this.ds.changeScale(scale, [ + this.ds.element.width / 2, + this.ds.element.height / 2, + ]); + this.graph.change(); + } + } + + this.graph.change(); + + if (block_default) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + + // Fall through to Litegraph defaults + return origProcessKey.apply(this, arguments); + }; + } + + /** + * Draws group header bar + */ + #addDrawGroupsHandler() { + const self = this; + // @ts-ignore + const origDrawGroups = LGraphCanvas.prototype.drawGroups; + // @ts-ignore + LGraphCanvas.prototype.drawGroups = function (canvas, ctx) { + if (!this.graph) { + return; + } + + var groups = this.graph._groups; + + ctx.save(); + ctx.globalAlpha = 0.7 * this.editor_alpha; + + for (var i = 0; i < groups.length; ++i) { + var group = groups[i]; + + if (!LiteGraph.overlapBounding(this.visible_area, group._bounding)) { + continue; + } //out of the visible area + + ctx.fillStyle = group.color || "#335"; + ctx.strokeStyle = group.color || "#335"; + var pos = group._pos; + var size = group._size; + ctx.globalAlpha = 0.25 * this.editor_alpha; + ctx.beginPath(); + var font_size = group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; + ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], font_size * 1.4); + ctx.fill(); + ctx.globalAlpha = this.editor_alpha; + } + + ctx.restore(); + + const res = origDrawGroups.apply(this, arguments); + return res; + }; + } + + /** + * Draws node highlights (executing, drag drop) and progress bar + */ + #addDrawNodeHandler() { + // @ts-ignore + const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape; + const self = this; + // @ts-ignore + LGraphCanvas.prototype.drawNodeShape = function ( + node, + ctx, + size, + fgcolor, + bgcolor, + selected, + mouse_over + ) { + const res = origDrawNodeShape.apply(this, arguments); + + const nodeErrors = self.lastNodeErrors?.[node.id]; + + let color = null; + let lineWidth = 1; + if (node.id === +self.runningNodeId) { + color = "#0f0"; + } else if (self.dragOverNode && node.id === self.dragOverNode.id) { + color = "dodgerblue"; + } else if (nodeErrors?.errors) { + color = "red"; + lineWidth = 2; + } else if ( + self.lastExecutionError && + +self.lastExecutionError.node_id === node.id + ) { + color = "#f0f"; + lineWidth = 2; + } + + if (color) { + const shape = + // @ts-ignore + node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE; + ctx.lineWidth = lineWidth; + ctx.globalAlpha = 0.8; + ctx.beginPath(); + if (shape == LiteGraph.BOX_SHAPE) + ctx.rect( + -6, + -6 - LiteGraph.NODE_TITLE_HEIGHT, + 12 + size[0] + 1, + 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT + ); + else if ( + shape == LiteGraph.ROUND_SHAPE || + (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) + ) + ctx.roundRect( + -6, + -6 - LiteGraph.NODE_TITLE_HEIGHT, + 12 + size[0] + 1, + 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, + this.round_radius * 2 + ); + else if (shape == LiteGraph.CARD_SHAPE) + ctx.roundRect( + -6, + -6 - LiteGraph.NODE_TITLE_HEIGHT, + 12 + size[0] + 1, + 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, + [this.round_radius * 2, this.round_radius * 2, 2, 2] + ); + else if (shape == LiteGraph.CIRCLE_SHAPE) + ctx.arc( + size[0] * 0.5, + size[1] * 0.5, + size[0] * 0.5 + 6, + 0, + Math.PI * 2 + ); + ctx.strokeStyle = color; + ctx.stroke(); + ctx.strokeStyle = fgcolor; + ctx.globalAlpha = 1; + } + + if (self.progress && node.id === +self.runningNodeId) { + ctx.fillStyle = "green"; + ctx.fillRect( + 0, + 0, + size[0] * (self.progress.value / self.progress.max), + 6 + ); + ctx.fillStyle = bgcolor; + } + + // Highlight inputs that failed validation + if (nodeErrors) { + ctx.lineWidth = 2; + ctx.strokeStyle = "red"; + for (const error of nodeErrors.errors) { + if (error.extra_info && error.extra_info.input_name) { + const inputIndex = node.findInputSlot(error.extra_info.input_name); + if (inputIndex !== -1) { + let pos = node.getConnectionPos(true, inputIndex); + ctx.beginPath(); + ctx.arc( + pos[0] - node.pos[0], + pos[1] - node.pos[1], + 12, + 0, + 2 * Math.PI, + false + ); + ctx.stroke(); + } + } + } + } + + return res; + }; + + // @ts-ignore + const origDrawNode = LGraphCanvas.prototype.drawNode; + // @ts-ignore + LGraphCanvas.prototype.drawNode = function (node, ctx) { + var editor_alpha = this.editor_alpha; + var old_color = node.bgcolor; + + if (node.mode === 2) { + // never + this.editor_alpha = 0.4; + } + + // Mode never equals 4 by ts check. + // if (node.mode === 4) { // never + // node.bgcolor = "#FF00FF"; + // this.editor_alpha = 0.2; + // } + + const res = origDrawNode.apply(this, arguments); + + this.editor_alpha = editor_alpha; + node.bgcolor = old_color; + + return res; + }; + } + + /** + * Handles updates from the API socket + */ + #addApiUpdateHandlers() { + api.addEventListener("status", ({ detail }) => { + this.ui.setStatus(detail); + }); + + api.addEventListener("reconnecting", () => { + this.ui.dialog.show("Reconnecting..."); + }); + + api.addEventListener("reconnected", () => { + this.ui.dialog.close(); + }); + + api.addEventListener("progress", ({ detail }) => { + if ( + this.workflowManager.activePrompt?.workflow && + this.workflowManager.activePrompt.workflow !== + this.workflowManager.activeWorkflow + ) + return; + this.progress = detail; + this.graph.setDirtyCanvas(true, false); + }); + + api.addEventListener("executing", ({ detail }) => { + if ( + this.workflowManager.activePrompt?.workflow && + this.workflowManager.activePrompt.workflow !== + this.workflowManager.activeWorkflow + ) + return; + this.progress = null; + this.runningNodeId = detail; + this.graph.setDirtyCanvas(true, false); + delete this.nodePreviewImages[this.runningNodeId]; + }); + + api.addEventListener("executed", ({ detail }) => { + if ( + this.workflowManager.activePrompt?.workflow && + this.workflowManager.activePrompt.workflow !== + this.workflowManager.activeWorkflow + ) + return; + const output = this.nodeOutputs[detail.node]; + if (detail.merge && output) { + for (const k in detail.output ?? {}) { + const v = output[k]; + if (v instanceof Array) { + output[k] = v.concat(detail.output[k]); + } else { + output[k] = detail.output[k]; + } + } + } else { + this.nodeOutputs[detail.node] = detail.output; + } + const node = this.graph.getNodeById(detail.node); + if (node) { + // @ts-ignore + if (node.onExecuted) + // @ts-ignore + node.onExecuted(detail.output); + } + }); + + api.addEventListener("execution_start", ({ detail }) => { + this.runningNodeId = null; + this.lastExecutionError = null; + // @ts-ignore + this.graph._nodes.forEach((node) => { + // @ts-ignore + if (node.onExecutionStart) + // @ts-ignore + node.onExecutionStart(); + }); + }); + + api.addEventListener("execution_error", ({ detail }) => { + this.lastExecutionError = detail; + const formattedError = this.#formatExecutionError(detail); + this.ui.dialog.show(formattedError); + this.canvas.draw(true, true); + }); + + api.addEventListener("b_preview", ({ detail }) => { + const id = this.runningNodeId; + if (id == null) return; + + const blob = detail; + const blobUrl = URL.createObjectURL(blob); + // @ts-ignore + this.nodePreviewImages[id] = [blobUrl]; + }); + + api.init(); + } + + #addKeyboardHandler() { + window.addEventListener("keydown", (e) => { + this.shiftDown = e.shiftKey; + }); + window.addEventListener("keyup", (e) => { + this.shiftDown = e.shiftKey; + }); + } + + #addConfigureHandler() { + const app = this; + // @ts-ignore + const configure = LGraph.prototype.configure; + // Flag that the graph is configuring to prevent nodes from running checks while its still loading + // @ts-ignore + LGraph.prototype.configure = function () { + app.configuringGraph = true; + try { + return configure.apply(this, arguments); + } finally { + app.configuringGraph = false; + } + }; + } + + #addAfterConfigureHandler() { + const app = this; + // @ts-ignore + const onConfigure = app.graph.onConfigure; + // @ts-ignore + app.graph.onConfigure = function () { + // Fire callbacks before the onConfigure, this is used by widget inputs to setup the config + // @ts-ignore + for (const node of app.graph._nodes) { + // @ts-ignore + node.onGraphConfigured?.(); + } + + const r = onConfigure?.apply(this, arguments); + + // Fire after onConfigure, used by primitves to generate widget using input nodes config + // @ts-ignore _nodes is private. + for (const node of app.graph._nodes) { + // @ts-ignore + node.onAfterGraphConfigured?.(); + } + + return r; + }; + } + + /** + * Loads all extensions from the API into the window in parallel + */ + async #loadExtensions() { + const extensions = await api.getExtensions(); + this.logging.addEntry("Comfy.App", "debug", { Extensions: extensions }); + + // Need to load core extensions first as some custom extensions + // may depend on them. + await import("../extensions/core/index.js"); + await Promise.all( + extensions + .filter((extension) => !extension.includes("extensions/core")) + .map(async (ext) => { + try { + await import(/* @vite-ignore */ api.fileURL(ext)); + } catch (error) { + console.error("Error loading extension", ext, error); + } + }) + ); + + try { + this.menu.workflows.registerExtension(this); + } catch (error) { + console.error(error); + } + } + + async #migrateSettings() { + this.isNewUserSession = true; + // Store all current settings + const settings = Object.keys(this.ui.settings).reduce((p, n) => { + const v = localStorage[`Comfy.Settings.${n}`]; + if (v) { + try { + p[n] = JSON.parse(v); + } catch (error) {} + } + return p; + }, {}); + + await api.storeSettings(settings); + } + + async #setUser() { + const userConfig = await api.getUserConfig(); + this.storageLocation = userConfig.storage; + if (typeof userConfig.migrated == "boolean") { + // Single user mode migrated true/false for if the default user is created + if (!userConfig.migrated && this.storageLocation === "server") { + // Default user not created yet + await this.#migrateSettings(); + } + return; + } + + this.multiUserServer = true; + let user = localStorage["Comfy.userId"]; + const users = userConfig.users ?? {}; + if (!user || !users[user]) { + // This will rarely be hit so move the loading to on demand + const { UserSelectionScreen } = await import("./ui/userSelection"); + + this.ui.menuContainer.style.display = "none"; + const { userId, username, created } = + await new UserSelectionScreen().show(users, user); + this.ui.menuContainer.style.display = ""; + + user = userId; + localStorage["Comfy.userName"] = username; + localStorage["Comfy.userId"] = user; + + if (created) { + api.user = user; + await this.#migrateSettings(); + } + } + + api.user = user; + + this.ui.settings.addSetting({ + id: "Comfy.SwitchUser", + name: "Switch User", + type: (name) => { + let currentUser = localStorage["Comfy.userName"]; + if (currentUser) { + currentUser = ` (${currentUser})`; + } + return $el("tr", [ + $el("td", [ + $el("label", { + textContent: name, + }), + ]), + $el("td", [ + $el("button", { + textContent: name + (currentUser ?? ""), + onclick: () => { + delete localStorage["Comfy.userId"]; + delete localStorage["Comfy.userName"]; + window.location.reload(); + }, + }), + ]), + ]); + }, + // TODO: Is that the correct default value? + defaultValue: undefined, + }); + } + + /** + * Set up the app on the page + */ + async setup() { + await this.#setUser(); + + // Create and mount the LiteGraph in the DOM + const mainCanvas = document.createElement("canvas"); + mainCanvas.style.touchAction = "none"; + const canvasEl = (this.canvasEl = Object.assign(mainCanvas, { + id: "graph-canvas", + })); + canvasEl.tabIndex = 1; + document.body.prepend(canvasEl); + this.resizeCanvas(); + + await Promise.all([ + this.workflowManager.loadWorkflows(), + this.ui.settings.load(), + ]); + await this.#loadExtensions(); + + addDomClippingSetting(); + this.#addProcessMouseHandler(); + this.#addProcessKeyHandler(); + this.#addConfigureHandler(); + this.#addApiUpdateHandlers(); + this.#addRestoreWorkflowView(); + + // @ts-ignore + this.graph = new LGraph(); + + this.#addAfterConfigureHandler(); + + // @ts-ignore + this.canvas = new LGraphCanvas(canvasEl, this.graph); + this.ctx = canvasEl.getContext("2d"); + + LiteGraph.release_link_on_empty_shows_menu = true; + LiteGraph.alt_drag_do_clone_nodes = true; + + this.graph.start(); + + // Ensure the canvas fills the window + this.resizeCanvas(); + window.addEventListener("resize", () => this.resizeCanvas()); + const ro = new ResizeObserver(() => this.resizeCanvas()); + ro.observe(this.bodyTop); + ro.observe(this.bodyLeft); + ro.observe(this.bodyRight); + ro.observe(this.bodyBottom); + + await this.#invokeExtensionsAsync("init"); + await this.registerNodes(); + initWidgets(this); + + // Load previous workflow + let restored = false; + try { + const loadWorkflow = async (json) => { + if (json) { + const workflow = await parseComfyWorkflow(json); + const workflowName = getStorageValue("Comfy.PreviousWorkflow"); + await this.loadGraphData(workflow, true, true, workflowName); + return true; + } + }; + const clientId = api.initialClientId ?? api.clientId; + restored = + (clientId && + (await loadWorkflow( + sessionStorage.getItem(`workflow:${clientId}`) + ))) || + (await loadWorkflow(localStorage.getItem("workflow"))); + } catch (err) { + console.error("Error loading previous workflow", err); + } + + // We failed to restore a workflow so load the default + if (!restored) { + await this.loadGraphData(); + } + + // Save current workflow automatically + setInterval(() => { + const workflow = JSON.stringify(this.graph.serialize()); + localStorage.setItem("workflow", workflow); + if (api.clientId) { + sessionStorage.setItem(`workflow:${api.clientId}`, workflow); + } + }, 1000); + + this.#addDrawNodeHandler(); + this.#addDrawGroupsHandler(); + this.#addDropHandler(); + this.#addCopyHandler(); + this.#addPasteHandler(); + this.#addKeyboardHandler(); + + await this.#invokeExtensionsAsync("setup"); + } + + resizeCanvas() { + // Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845 + const scale = Math.max(window.devicePixelRatio, 1); + + // Clear fixed width and height while calculating rect so it uses 100% instead + this.canvasEl.height = this.canvasEl.width = NaN; + const { width, height } = this.canvasEl.getBoundingClientRect(); + this.canvasEl.width = Math.round(width * scale); + this.canvasEl.height = Math.round(height * scale); + this.canvasEl.getContext("2d").scale(scale, scale); + this.canvas?.draw(true, true); + } + + /** + * Registers nodes with the graph + */ + async registerNodes() { + const app = this; + // Load node definitions from the backend + const defs = await api.getNodeDefs(); + await this.registerNodesFromDefs(defs); + await this.#invokeExtensionsAsync("registerCustomNodes"); + } + + getWidgetType(inputData, inputName) { + const type = inputData[0]; + + if (Array.isArray(type)) { + return "COMBO"; + } else if (`${type}:${inputName}` in this.widgets) { + return `${type}:${inputName}`; + } else if (type in this.widgets) { + return type; + } else { + return null; + } + } + + async registerNodeDef(nodeId: string, nodeData: ComfyNodeDef) { + const self = this; + const node = Object.assign( + function ComfyNode() { + var inputs = nodeData["input"]["required"]; + if (nodeData["input"]["optional"] != undefined) { + inputs = Object.assign( + {}, + nodeData["input"]["required"], + nodeData["input"]["optional"] + ); + } + const config = { minWidth: 1, minHeight: 1 }; + for (const inputName in inputs) { + const inputData = inputs[inputName]; + const type = inputData[0]; + + let widgetCreated = true; + const widgetType = self.getWidgetType(inputData, inputName); + if (widgetType) { + if (widgetType === "COMBO") { + Object.assign( + config, + self.widgets.COMBO(this, inputName, inputData, app) || {} + ); + } else { + Object.assign( + config, + self.widgets[widgetType](this, inputName, inputData, app) || {} + ); + } + } else { + // Node connection inputs + this.addInput(inputName, type); + widgetCreated = false; + } + // @ts-ignore + if (widgetCreated && inputData[1]?.forceInput && config?.widget) { + // @ts-ignore + if (!config.widget.options) config.widget.options = {}; + // @ts-ignore + config.widget.options.forceInput = inputData[1].forceInput; + } + // @ts-ignore + if (widgetCreated && inputData[1]?.defaultInput && config?.widget) { + // @ts-ignore + if (!config.widget.options) config.widget.options = {}; + // @ts-ignore + config.widget.options.defaultInput = inputData[1].defaultInput; + } + } + + for (const o in nodeData["output"]) { + let output = nodeData["output"][o]; + if (output instanceof Array) output = "COMBO"; + const outputName = nodeData["output_name"][o] || output; + const outputShape = nodeData["output_is_list"][o] + ? LiteGraph.GRID_SHAPE + : LiteGraph.CIRCLE_SHAPE; + this.addOutput(outputName, output, { shape: outputShape }); + } + + const s = this.computeSize(); + s[0] = Math.max(config.minWidth, s[0] * 1.5); + s[1] = Math.max(config.minHeight, s[1]); + this.size = s; + this.serialize_widgets = true; + + app.#invokeExtensionsAsync("nodeCreated", this); + }, + { + title: nodeData.display_name || nodeData.name, + comfyClass: nodeData.name, + nodeData, + } + ); + node.prototype.comfyClass = nodeData.name; + + this.#addNodeContextMenuHandler(node); + this.#addDrawBackgroundHandler(node); + this.#addNodeKeyHandler(node); + + await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData); + // @ts-ignore + LiteGraph.registerNodeType(nodeId, node); + // @ts-ignore + node.category = nodeData.category; + } + + async registerNodesFromDefs(defs: Record) { + await this.#invokeExtensionsAsync("addCustomNodeDefs", defs); + + // Generate list of known widgets + this.widgets = Object.assign( + {}, + ComfyWidgets, + ...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean) + ); + + // Register a node for each definition + for (const nodeId in defs) { + this.registerNodeDef(nodeId, defs[nodeId]); + } + } + + loadTemplateData(templateData) { + if (!templateData?.templates) { + return; + } + + const old = localStorage.getItem("litegrapheditor_clipboard"); + + var maxY, nodeBottom, node; + + for (const template of templateData.templates) { + if (!template?.data) { + continue; + } + + localStorage.setItem("litegrapheditor_clipboard", template.data); + app.canvas.pasteFromClipboard(); + + // Move mouse position down to paste the next template below + + maxY = false; + + for (const i in app.canvas.selected_nodes) { + node = app.canvas.selected_nodes[i]; + + nodeBottom = node.pos[1] + node.size[1]; + + if (maxY === false || nodeBottom > maxY) { + maxY = nodeBottom; + } + } + + app.canvas.graph_mouse[1] = maxY + 50; + } + + localStorage.setItem("litegrapheditor_clipboard", old); + } + + showMissingNodesError(missingNodeTypes, hasAddedNodes = true) { + let seenTypes = new Set(); + + this.ui.dialog.show( + $el("div.comfy-missing-nodes", [ + $el("span", { + textContent: + "When loading the graph, the following node types were not found: ", + }), + $el( + "ul", + Array.from(new Set(missingNodeTypes)) + .map((t) => { + let children = []; + if (typeof t === "object") { + // @ts-ignore + if (seenTypes.has(t.type)) return null; + // @ts-ignore + seenTypes.add(t.type); + // @ts-ignore + children.push($el("span", { textContent: t.type })); + // @ts-ignore + if (t.hint) { + // @ts-ignore + children.push($el("span", { textContent: t.hint })); + } + // @ts-ignore + if (t.action) { + children.push( + $el("button", { + // @ts-ignore + onclick: t.action.callback, + // @ts-ignore + textContent: t.action.text, + }) + ); + } + } else { + if (seenTypes.has(t)) return null; + seenTypes.add(t); + // @ts-ignore + children.push($el("span", { textContent: t })); + } + return $el("li", children); + }) + .filter(Boolean) + ), + ...(hasAddedNodes + ? [ + $el("span", { + textContent: + "Nodes that have failed to load will show as red on the graph.", + }), + ] + : []), + ]) + ); + this.logging.addEntry("Comfy.App", "warn", { + MissingNodes: missingNodeTypes, + }); + } + + async changeWorkflow(callback, workflow = null) { + try { + this.workflowManager.activeWorkflow?.changeTracker?.store(); + } catch (error) { + console.error(error); + } + await callback(); + try { + this.workflowManager.setWorkflow(workflow); + this.workflowManager.activeWorkflow?.track(); + } catch (error) { + console.error(error); + } + } + + /** + * Populates the graph with the specified workflow data + * @param {*} graphData A serialized graph object + * @param { boolean } clean If the graph state, e.g. images, should be cleared + */ + async loadGraphData( + graphData?: ComfyWorkflow, + clean: boolean = true, + restore_view: boolean = true, + workflow: string | null = null + ) { + if (clean !== false) { + this.clean(); + } + + let reset_invalid_values = false; + if (!graphData) { + graphData = defaultGraph; + reset_invalid_values = true; + } + + if (typeof structuredClone === "undefined") { + graphData = JSON.parse(JSON.stringify(graphData)); + } else { + graphData = structuredClone(graphData); + } + + try { + this.workflowManager.setWorkflow(workflow); + } catch (error) { + console.error(error); + } + + const missingNodeTypes = []; + await this.#invokeExtensionsAsync( + "beforeConfigureGraph", + graphData, + missingNodeTypes + ); + for (let n of graphData.nodes) { + // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now + if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader"; + if (n.type == "ConditioningAverage ") n.type = "ConditioningAverage"; //typo fix + if (n.type == "SDV_img2vid_Conditioning") + n.type = "SVD_img2vid_Conditioning"; //typo fix + + // Find missing node types + if (!(n.type in LiteGraph.registered_node_types)) { + missingNodeTypes.push(n.type); + n.type = sanitizeNodeName(n.type); + } + } + + try { + this.graph.configure(graphData); + if ( + restore_view && + this.enableWorkflowViewRestore.value && + graphData.extra?.ds + ) { + // @ts-ignore + // Need to set strict: true for zod to match the type [number, number] + // https://github.com/colinhacks/zod/issues/3056 + this.canvas.ds.offset = graphData.extra.ds.offset; + this.canvas.ds.scale = graphData.extra.ds.scale; + } + + try { + this.workflowManager.activeWorkflow?.track(); + } catch (error) { + // TODO: Do we want silently fail here? + } + } catch (error) { + let errorHint = []; + // Try extracting filename to see if it was caused by an extension script + const filename = + error.fileName || + (error.stack || "").match(/(\/extensions\/.*\.js)/)?.[1]; + const pos = (filename || "").indexOf("/extensions/"); + if (pos > -1) { + errorHint.push( + $el("span", { + textContent: "This may be due to the following script:", + }), + $el("br"), + $el("span", { + style: { + fontWeight: "bold", + }, + textContent: filename.substring(pos), + }) + ); + } + + // Show dialog to let the user know something went wrong loading the data + this.ui.dialog.show( + $el("div", [ + $el("p", { + textContent: "Loading aborted due to error reloading workflow data", + }), + $el("pre", { + style: { padding: "5px", backgroundColor: "rgba(255,0,0,0.2)" }, + textContent: error.toString(), + }), + $el("pre", { + style: { + padding: "5px", + color: "#ccc", + fontSize: "10px", + maxHeight: "50vh", + overflow: "auto", + backgroundColor: "rgba(0,0,0,0.2)", + }, + textContent: error.stack || "No stacktrace available", + }), + ...errorHint, + ]).outerHTML + ); + + return; + } + // @ts-ignore + for (const node of this.graph._nodes) { + const size = node.computeSize(); + size[0] = Math.max(node.size[0], size[0]); + size[1] = Math.max(node.size[1], size[1]); + node.size = size; + // @ts-ignore + if (node.widgets) { + // If you break something in the backend and want to patch workflows in the frontend + // This is the place to do this + // @ts-ignore + for (let widget of node.widgets) { + if (node.type == "KSampler" || node.type == "KSamplerAdvanced") { + if (widget.name == "sampler_name") { + if (widget.value.startsWith("sample_")) { + widget.value = widget.value.slice(7); + } + } + } + if ( + node.type == "KSampler" || + node.type == "KSamplerAdvanced" || + node.type == "PrimitiveNode" + ) { + if (widget.name == "control_after_generate") { + if (widget.value === true) { + widget.value = "randomize"; + } else if (widget.value === false) { + widget.value = "fixed"; + } + } + } + if (reset_invalid_values) { + if (widget.type == "combo") { + if ( + !widget.options.values.includes(widget.value) && + widget.options.values.length > 0 + ) { + widget.value = widget.options.values[0]; + } + } + } + } + } + + this.#invokeExtensions("loadedGraphNode", node); + } + + if (missingNodeTypes.length) { + this.showMissingNodesError(missingNodeTypes); + } + await this.#invokeExtensionsAsync("afterConfigureGraph", missingNodeTypes); + requestAnimationFrame(() => { + this.graph.setDirtyCanvas(true, true); + }); + } + + /** + * Converts the current graph workflow for sending to the API + * @returns The workflow and node links + */ + async graphToPrompt(graph = this.graph, clean = true) { + for (const outerNode of this.graph.computeExecutionOrder(false)) { + if (outerNode.widgets) { + for (const widget of outerNode.widgets) { + // Allow widgets to run callbacks before a prompt has been queued + // e.g. random seed before every gen + widget.beforeQueued?.(); + } + } + + const innerNodes = outerNode.getInnerNodes + ? outerNode.getInnerNodes() + : [outerNode]; + for (const node of innerNodes) { + if (node.isVirtualNode) { + // Don't serialize frontend only nodes but let them make changes + if (node.applyToGraph) { + node.applyToGraph(); + } + } + } + } + + const workflow = graph.serialize(); + const output = {}; + // Process nodes in order of execution + for (const outerNode of graph.computeExecutionOrder(false)) { + const skipNode = outerNode.mode === 2 || outerNode.mode === 4; + const innerNodes = + !skipNode && outerNode.getInnerNodes + ? outerNode.getInnerNodes() + : [outerNode]; + for (const node of innerNodes) { + if (node.isVirtualNode) { + continue; + } + + if (node.mode === 2 || node.mode === 4) { + // Don't serialize muted nodes + continue; + } + + const inputs = {}; + const widgets = node.widgets; + + // Store all widget values + if (widgets) { + for (const i in widgets) { + const widget = widgets[i]; + if (!widget.options || widget.options.serialize !== false) { + inputs[widget.name] = widget.serializeValue + ? await widget.serializeValue(node, i) + : widget.value; + } + } + } + + // Store all node links + for (let i in node.inputs) { + let parent = node.getInputNode(i); + if (parent) { + let link = node.getInputLink(i); + while (parent.mode === 4 || parent.isVirtualNode) { + let found = false; + if (parent.isVirtualNode) { + link = parent.getInputLink(link.origin_slot); + if (link) { + parent = parent.getInputNode(link.target_slot); + if (parent) { + found = true; + } + } + } else if (link && parent.mode === 4) { + let all_inputs = [link.origin_slot]; + if (parent.inputs) { + all_inputs = all_inputs.concat(Object.keys(parent.inputs)); + for (let parent_input in all_inputs) { + parent_input = all_inputs[parent_input]; + if ( + parent.inputs[parent_input]?.type === node.inputs[i].type + ) { + link = parent.getInputLink(parent_input); + if (link) { + parent = parent.getInputNode(parent_input); + } + found = true; + break; + } + } + } + } + + if (!found) { + break; + } + } + + if (link) { + if (parent?.updateLink) { + link = parent.updateLink(link); + } + if (link) { + inputs[node.inputs[i].name] = [ + String(link.origin_id), + parseInt(link.origin_slot), + ]; + } + } + } + } + + let node_data = { + inputs, + class_type: node.comfyClass, + }; + + if (this.ui.settings.getSettingValue("Comfy.DevMode")) { + // Ignored by the backend. + node_data["_meta"] = { + title: node.title, + }; + } + + output[String(node.id)] = node_data; + } + } + + // Remove inputs connected to removed nodes + if (clean) { + for (const o in output) { + for (const i in output[o].inputs) { + if ( + Array.isArray(output[o].inputs[i]) && + output[o].inputs[i].length === 2 && + !output[output[o].inputs[i][0]] + ) { + delete output[o].inputs[i]; + } + } + } + } + + return { workflow, output }; + } + + #formatPromptError(error) { + if (error == null) { + return "(unknown error)"; + } else if (typeof error === "string") { + return error; + } else if (error.stack && error.message) { + return error.toString(); + } else if (error.response) { + let message = error.response.error.message; + if (error.response.error.details) + message += ": " + error.response.error.details; + for (const [nodeID, nodeError] of Object.entries( + error.response.node_errors + )) { + // @ts-ignore + message += "\n" + nodeError.class_type + ":"; + // @ts-ignore + for (const errorReason of nodeError.errors) { + message += + "\n - " + errorReason.message + ": " + errorReason.details; + } + } + return message; + } + return "(unknown error)"; + } + + #formatExecutionError(error) { + if (error == null) { + return "(unknown error)"; + } + + const traceback = error.traceback.join(""); + const nodeId = error.node_id; + const nodeType = error.node_type; + + return `Error occurred when executing ${nodeType}:\n\n${error.exception_message}\n\n${traceback}`; + } + + async queuePrompt(number, batchCount = 1) { + this.#queueItems.push({ number, batchCount }); + + // Only have one action process the items so each one gets a unique seed correctly + if (this.#processingQueue) { + return; + } + + this.#processingQueue = true; + this.lastNodeErrors = null; + + try { + while (this.#queueItems.length) { + ({ number, batchCount } = this.#queueItems.pop()); + + for (let i = 0; i < batchCount; i++) { + const p = await this.graphToPrompt(); + + try { + const res = await api.queuePrompt(number, p); + this.lastNodeErrors = res.node_errors; + if (this.lastNodeErrors.length > 0) { + this.canvas.draw(true, true); + } else { + try { + this.workflowManager.storePrompt({ + id: res.prompt_id, + nodes: Object.keys(p.output), + }); + } catch (error) {} + } + } catch (error) { + const formattedError = this.#formatPromptError(error); + this.ui.dialog.show(formattedError); + if (error.response) { + this.lastNodeErrors = error.response.node_errors; + this.canvas.draw(true, true); + } + break; + } + + for (const n of p.workflow.nodes) { + const node = this.graph.getNodeById(n.id); + if (node.widgets) { + for (const widget of node.widgets) { + // Allow widgets to run callbacks after a prompt has been queued + // e.g. random seed after every gen + // @ts-ignore + if (widget.afterQueued) { + // @ts-ignore + widget.afterQueued(); + } + } + } + } + + this.canvas.draw(true, true); + await this.ui.queue.update(); + } + } + } finally { + this.#processingQueue = false; + } + api.dispatchEvent( + new CustomEvent("promptQueued", { detail: { number, batchCount } }) + ); + return !this.lastNodeErrors; + } + + showErrorOnFileLoad(file) { + this.ui.dialog.show( + $el("div", [ + $el("p", { textContent: `Unable to find workflow in ${file.name}` }), + ]).outerHTML + ); + } + + /** + * Loads workflow data from the specified file + * @param {File} file + */ + async handleFile(file) { + const removeExt = (f) => { + if (!f) return f; + const p = f.lastIndexOf("."); + if (p === -1) return f; + return f.substring(0, p); + }; + const fileName = removeExt(file.name); + if (file.type === "image/png") { + const pngInfo = await getPngMetadata(file); + if (pngInfo?.workflow) { + await this.loadGraphData( + await parseComfyWorkflow(pngInfo.workflow), + true, + true, + fileName + ); + } else if (pngInfo?.prompt) { + this.loadApiJson(JSON.parse(pngInfo.prompt), fileName); + } else if (pngInfo?.parameters) { + this.changeWorkflow(() => { + importA1111(this.graph, pngInfo.parameters); + }, fileName); + } else { + this.showErrorOnFileLoad(file); + } + } else if (file.type === "image/webp") { + const pngInfo = await getWebpMetadata(file); + // Support loading workflows from that webp custom node. + const workflow = pngInfo?.workflow || pngInfo?.Workflow; + const prompt = pngInfo?.prompt || pngInfo?.Prompt; + + if (workflow) { + this.loadGraphData( + await parseComfyWorkflow(workflow), + true, + true, + fileName + ); + } else if (prompt) { + this.loadApiJson(JSON.parse(prompt), fileName); + } else { + this.showErrorOnFileLoad(file); + } + } else if (file.type === "audio/flac" || file.type === "audio/x-flac") { + const pngInfo = await getFlacMetadata(file); + const workflow = pngInfo?.workflow || pngInfo?.Workflow; + const prompt = pngInfo?.prompt || pngInfo?.Prompt; + + if (workflow) { + this.loadGraphData( + await parseComfyWorkflow(workflow), + true, + true, + fileName + ); + } else if (prompt) { + this.loadApiJson(JSON.parse(prompt), fileName); + } else { + this.showErrorOnFileLoad(file); + } + } else if ( + file.type === "application/json" || + file.name?.endsWith(".json") + ) { + const reader = new FileReader(); + reader.onload = async () => { + const readerResult = reader.result as string; + const jsonContent = JSON.parse(readerResult); + if (jsonContent?.templates) { + this.loadTemplateData(jsonContent); + } else if (this.isApiJson(jsonContent)) { + this.loadApiJson(jsonContent, fileName); + } else { + await this.loadGraphData( + await parseComfyWorkflow(readerResult), + true, + fileName + ); + } + }; + reader.readAsText(file); + } else if ( + file.name?.endsWith(".latent") || + file.name?.endsWith(".safetensors") + ) { + const info = await getLatentMetadata(file); + // TODO define schema to LatentMetadata + // @ts-ignore + if (info.workflow) { + await this.loadGraphData( + // @ts-ignore + await parseComfyWorkflow(info.workflow), + true, + true, + fileName + ); + // @ts-ignore + } else if (info.prompt) { + // @ts-ignore + this.loadApiJson(JSON.parse(info.prompt)); + } else { + this.showErrorOnFileLoad(file); + } + } else { + this.showErrorOnFileLoad(file); + } + } + + isApiJson(data) { + // @ts-ignore + return Object.values(data).every((v) => v.class_type); + } + + loadApiJson(apiData, fileName: string) { + const missingNodeTypes = Object.values(apiData).filter( + // @ts-ignore + (n) => !LiteGraph.registered_node_types[n.class_type] + ); + if (missingNodeTypes.length) { + this.showMissingNodesError( + // @ts-ignore + missingNodeTypes.map((t) => t.class_type), + false + ); + return; + } + + const ids = Object.keys(apiData); + app.graph.clear(); + for (const id of ids) { + const data = apiData[id]; + const node = LiteGraph.createNode(data.class_type); + // @ts-ignore + node.id = isNaN(+id) ? id : +id; + node.title = data._meta?.title ?? node.title; + app.graph.add(node); + } + + this.changeWorkflow(() => { + for (const id of ids) { + const data = apiData[id]; + const node = app.graph.getNodeById(Number.parseInt(id)); + for (const input in data.inputs ?? {}) { + const value = data.inputs[input]; + if (value instanceof Array) { + const [fromId, fromSlot] = value; + const fromNode = app.graph.getNodeById(fromId); + let toSlot = node.inputs?.findIndex((inp) => inp.name === input); + if (toSlot == null || toSlot === -1) { + try { + // Target has no matching input, most likely a converted widget + const widget = node.widgets?.find((w) => w.name === input); + // @ts-ignore + if (widget && node.convertWidgetToInput?.(widget)) { + toSlot = node.inputs?.length - 1; + } + } catch (error) {} + } + if (toSlot != null || toSlot !== -1) { + fromNode.connect(fromSlot, node, toSlot); + } + } else { + const widget = node.widgets?.find((w) => w.name === input); + if (widget) { + widget.value = value; + // @ts-ignore + widget.callback?.(value); + } + } + } + } + app.graph.arrange(); + }, fileName); + + for (const id of ids) { + const data = apiData[id]; + const node = app.graph.getNodeById(Number.parseInt(id)); + for (const input in data.inputs ?? {}) { + const value = data.inputs[input]; + if (value instanceof Array) { + const [fromId, fromSlot] = value; + const fromNode = app.graph.getNodeById(fromId); + let toSlot = node.inputs?.findIndex((inp) => inp.name === input); + if (toSlot == null || toSlot === -1) { + try { + // Target has no matching input, most likely a converted widget + const widget = node.widgets?.find((w) => w.name === input); + // @ts-ignore + if (widget && node.convertWidgetToInput?.(widget)) { + toSlot = node.inputs?.length - 1; + } + } catch (error) {} + } + if (toSlot != null || toSlot !== -1) { + fromNode.connect(fromSlot, node, toSlot); + } + } else { + const widget = node.widgets?.find((w) => w.name === input); + if (widget) { + widget.value = value; + // @ts-ignore + widget.callback?.(value); + } + } + } + } + + app.graph.arrange(); + } + + /** + * Registers a Comfy web extension with the app + * @param {ComfyExtension} extension + */ + registerExtension(extension) { + if (!extension.name) { + throw new Error("Extensions must have a 'name' property."); + } + if (this.extensions.find((ext) => ext.name === extension.name)) { + throw new Error( + `Extension named '${extension.name}' already registered.` + ); + } + this.extensions.push(extension); + } + + /** + * Refresh combo list on whole nodes + */ + async refreshComboInNodes() { + const defs = await api.getNodeDefs(); + + for (const nodeId in defs) { + this.registerNodeDef(nodeId, defs[nodeId]); + } + // @ts-ignore + for (let nodeNum in this.graph._nodes) { + // @ts-ignore + const node = this.graph._nodes[nodeNum]; + const def = defs[node.type]; + // @ts-ignore + // Allow primitive nodes to handle refresh + node.refreshComboInNode?.(defs); + + if (!def) continue; + + for (const widgetNum in node.widgets) { + const widget = node.widgets[widgetNum]; + if ( + widget.type == "combo" && + def["input"]["required"][widget.name] !== undefined + ) { + widget.options.values = def["input"]["required"][widget.name][0]; + + if ( + widget.name != "image" && + !widget.options.values.includes(widget.value) + ) { + widget.value = widget.options.values[0]; + // @ts-ignore + widget.callback(widget.value); + } + } + } + } + + await this.#invokeExtensionsAsync("refreshComboInNodes", defs); + } + + resetView() { + app.canvas.ds.scale = 1; + app.canvas.ds.offset = [0, 0]; + app.graph.setDirtyCanvas(true, true); + } + + /** + * Clean current state + */ + clean() { + this.nodeOutputs = {}; + this.nodePreviewImages = {}; + this.lastNodeErrors = null; + this.lastExecutionError = null; + this.runningNodeId = null; + } } export const app = new ComfyApp(); diff --git a/src/scripts/changeTracker.js b/src/scripts/changeTracker.js index 7d64fecb2..fe63abcc3 100644 --- a/src/scripts/changeTracker.js +++ b/src/scripts/changeTracker.js @@ -3,255 +3,271 @@ import { api } from "./api"; import { clone } from "./utils"; - export class ChangeTracker { - static MAX_HISTORY = 50; - #app; - undo = []; - redo = []; - activeState = null; - isOurLoad = false; - /** @type { import("./workflows").ComfyWorkflow | null } */ - workflow; + static MAX_HISTORY = 50; + #app; + undo = []; + redo = []; + activeState = null; + isOurLoad = false; + /** @type { import("./workflows").ComfyWorkflow | null } */ + workflow; - ds; - nodeOutputs; + ds; + nodeOutputs; - get app() { - return this.#app ?? this.workflow.manager.app; - } + get app() { + return this.#app ?? this.workflow.manager.app; + } - constructor(workflow) { - this.workflow = workflow; - } + constructor(workflow) { + this.workflow = workflow; + } - #setApp(app) { - this.#app = app; - } + #setApp(app) { + this.#app = app; + } - store() { - this.ds = { scale: this.app.canvas.ds.scale, offset: [...this.app.canvas.ds.offset] }; - } + store() { + this.ds = { + scale: this.app.canvas.ds.scale, + offset: [...this.app.canvas.ds.offset], + }; + } - restore() { - if (this.ds) { - this.app.canvas.ds.scale = this.ds.scale; - this.app.canvas.ds.offset = this.ds.offset; - } - if (this.nodeOutputs) { - this.app.nodeOutputs = this.nodeOutputs; - } - } + restore() { + if (this.ds) { + this.app.canvas.ds.scale = this.ds.scale; + this.app.canvas.ds.offset = this.ds.offset; + } + if (this.nodeOutputs) { + this.app.nodeOutputs = this.nodeOutputs; + } + } - checkState() { - if (!this.app.graph) return; + checkState() { + if (!this.app.graph) return; - const currentState = this.app.graph.serialize(); - if (!this.activeState) { - this.activeState = clone(currentState); - return; - } - if (!ChangeTracker.graphEqual(this.activeState, currentState)) { - this.undo.push(this.activeState); - if (this.undo.length > ChangeTracker.MAX_HISTORY) { - this.undo.shift(); - } - this.activeState = clone(currentState); - this.redo.length = 0; - this.workflow.unsaved = true; - api.dispatchEvent(new CustomEvent("graphChanged", { detail: this.activeState })); - } - } + const currentState = this.app.graph.serialize(); + if (!this.activeState) { + this.activeState = clone(currentState); + return; + } + if (!ChangeTracker.graphEqual(this.activeState, currentState)) { + this.undo.push(this.activeState); + if (this.undo.length > ChangeTracker.MAX_HISTORY) { + this.undo.shift(); + } + this.activeState = clone(currentState); + this.redo.length = 0; + this.workflow.unsaved = true; + api.dispatchEvent( + new CustomEvent("graphChanged", { detail: this.activeState }) + ); + } + } - async updateState(source, target) { - const prevState = source.pop(); - if (prevState) { - target.push(this.activeState); - this.isOurLoad = true; - await this.app.loadGraphData(prevState, false, false, this.workflow); - this.activeState = prevState; - } - } + async updateState(source, target) { + const prevState = source.pop(); + if (prevState) { + target.push(this.activeState); + this.isOurLoad = true; + await this.app.loadGraphData(prevState, false, false, this.workflow); + this.activeState = prevState; + } + } - async undoRedo(e) { - if (e.ctrlKey || e.metaKey) { - if (e.key === "y") { - this.updateState(this.redo, this.undo); - return true; - } else if (e.key === "z") { - this.updateState(this.undo, this.redo); - return true; - } - } - } + async undoRedo(e) { + if (e.ctrlKey || e.metaKey) { + if (e.key === "y") { + this.updateState(this.redo, this.undo); + return true; + } else if (e.key === "z") { + this.updateState(this.undo, this.redo); + return true; + } + } + } - /** @param { import("./app").ComfyApp } app */ - static init(app) { - const changeTracker = () => app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker; - globalTracker.#setApp(app); + /** @param { import("./app").ComfyApp } app */ + static init(app) { + const changeTracker = () => + app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker; + globalTracker.#setApp(app); - const loadGraphData = app.loadGraphData; - app.loadGraphData = async function () { - const v = await loadGraphData.apply(this, arguments); - const ct = changeTracker(); - if (ct.isOurLoad) { - ct.isOurLoad = false; - } else { - ct.checkState(); - } - return v; - }; + const loadGraphData = app.loadGraphData; + app.loadGraphData = async function () { + const v = await loadGraphData.apply(this, arguments); + const ct = changeTracker(); + if (ct.isOurLoad) { + ct.isOurLoad = false; + } else { + ct.checkState(); + } + return v; + }; - let keyIgnored = false; - window.addEventListener( - "keydown", - (e) => { - requestAnimationFrame(async () => { - let activeEl; - // If we are auto queue in change mode then we do want to trigger on inputs - if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") { - activeEl = document.activeElement; - if (activeEl?.tagName === "INPUT" || activeEl?.["type"] === "textarea") { - // Ignore events on inputs, they have their native history - return; - } - } + let keyIgnored = false; + window.addEventListener( + "keydown", + (e) => { + requestAnimationFrame(async () => { + let activeEl; + // If we are auto queue in change mode then we do want to trigger on inputs + if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") { + activeEl = document.activeElement; + if ( + activeEl?.tagName === "INPUT" || + activeEl?.["type"] === "textarea" + ) { + // Ignore events on inputs, they have their native history + return; + } + } - keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta"; - if (keyIgnored) return; + keyIgnored = + e.key === "Control" || + e.key === "Shift" || + e.key === "Alt" || + e.key === "Meta"; + if (keyIgnored) return; - // Check if this is a ctrl+z ctrl+y - if (await changeTracker().undoRedo(e)) return; + // Check if this is a ctrl+z ctrl+y + if (await changeTracker().undoRedo(e)) return; - // If our active element is some type of input then handle changes after they're done - if (ChangeTracker.bindInput(activeEl)) return; - changeTracker().checkState(); - }); - }, - true - ); + // If our active element is some type of input then handle changes after they're done + if (ChangeTracker.bindInput(activeEl)) return; + changeTracker().checkState(); + }); + }, + true + ); - window.addEventListener("keyup", (e) => { - if (keyIgnored) { - keyIgnored = false; - changeTracker().checkState(); - } - }); + window.addEventListener("keyup", (e) => { + if (keyIgnored) { + keyIgnored = false; + changeTracker().checkState(); + } + }); - // Handle clicking DOM elements (e.g. widgets) - window.addEventListener("mouseup", () => { - changeTracker().checkState(); - }); + // Handle clicking DOM elements (e.g. widgets) + window.addEventListener("mouseup", () => { + changeTracker().checkState(); + }); - // Handle prompt queue event for dynamic widget changes - api.addEventListener("promptQueued", () => { - changeTracker().checkState(); - }); + // Handle prompt queue event for dynamic widget changes + api.addEventListener("promptQueued", () => { + changeTracker().checkState(); + }); - // Handle litegraph clicks - // @ts-ignore - const processMouseUp = LGraphCanvas.prototype.processMouseUp; - // @ts-ignore - LGraphCanvas.prototype.processMouseUp = function (e) { - const v = processMouseUp.apply(this, arguments); - changeTracker().checkState(); - return v; - }; - // @ts-ignore - const processMouseDown = LGraphCanvas.prototype.processMouseDown; - // @ts-ignore - LGraphCanvas.prototype.processMouseDown = function (e) { - const v = processMouseDown.apply(this, arguments); - changeTracker().checkState(); - return v; - }; + // Handle litegraph clicks + // @ts-ignore + const processMouseUp = LGraphCanvas.prototype.processMouseUp; + // @ts-ignore + LGraphCanvas.prototype.processMouseUp = function (e) { + const v = processMouseUp.apply(this, arguments); + changeTracker().checkState(); + return v; + }; + // @ts-ignore + const processMouseDown = LGraphCanvas.prototype.processMouseDown; + // @ts-ignore + LGraphCanvas.prototype.processMouseDown = function (e) { + const v = processMouseDown.apply(this, arguments); + changeTracker().checkState(); + return v; + }; - // Handle litegraph context menu for COMBO widgets - const close = LiteGraph.ContextMenu.prototype.close; - LiteGraph.ContextMenu.prototype.close = function (e) { - const v = close.apply(this, arguments); - changeTracker().checkState(); - return v; - }; + // Handle litegraph context menu for COMBO widgets + const close = LiteGraph.ContextMenu.prototype.close; + LiteGraph.ContextMenu.prototype.close = function (e) { + const v = close.apply(this, arguments); + changeTracker().checkState(); + return v; + }; - // Detects nodes being added via the node search dialog - const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded; - LiteGraph.LGraph.prototype.onNodeAdded = function () { - const v = onNodeAdded?.apply(this, arguments); - const ct = changeTracker(); - if (!ct.isOurLoad) { - ct.checkState(); - } - return v; - }; + // Detects nodes being added via the node search dialog + const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded; + LiteGraph.LGraph.prototype.onNodeAdded = function () { + const v = onNodeAdded?.apply(this, arguments); + const ct = changeTracker(); + if (!ct.isOurLoad) { + ct.checkState(); + } + return v; + }; - // Store node outputs - api.addEventListener("executed", ({ detail }) => { - const prompt = app.workflowManager.queuedPrompts[detail.prompt_id]; - if (!prompt?.workflow) return; - const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {}); - const output = nodeOutputs[detail.node]; - if (detail.merge && output) { - for (const k in detail.output ?? {}) { - const v = output[k]; - if (v instanceof Array) { - output[k] = v.concat(detail.output[k]); - } else { - output[k] = detail.output[k]; - } - } - } else { - nodeOutputs[detail.node] = detail.output; - } - }); - } + // Store node outputs + api.addEventListener("executed", ({ detail }) => { + const prompt = app.workflowManager.queuedPrompts[detail.prompt_id]; + if (!prompt?.workflow) return; + const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {}); + const output = nodeOutputs[detail.node]; + if (detail.merge && output) { + for (const k in detail.output ?? {}) { + const v = output[k]; + if (v instanceof Array) { + output[k] = v.concat(detail.output[k]); + } else { + output[k] = detail.output[k]; + } + } + } else { + nodeOutputs[detail.node] = detail.output; + } + }); + } - static bindInput(app, activeEl) { - if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") { - for (const evt of ["change", "input", "blur"]) { - if (`on${evt}` in activeEl) { - const listener = () => { - app.workflowManager.activeWorkflow.changeTracker.checkState(); - activeEl.removeEventListener(evt, listener); - }; - activeEl.addEventListener(evt, listener); - return true; - } - } - } - } + static bindInput(app, activeEl) { + if ( + activeEl && + activeEl.tagName !== "CANVAS" && + activeEl.tagName !== "BODY" + ) { + for (const evt of ["change", "input", "blur"]) { + if (`on${evt}` in activeEl) { + const listener = () => { + app.workflowManager.activeWorkflow.changeTracker.checkState(); + activeEl.removeEventListener(evt, listener); + }; + activeEl.addEventListener(evt, listener); + return true; + } + } + } + } - static graphEqual(a, b, path = "") { - if (a === b) return true; + static graphEqual(a, b, path = "") { + if (a === b) return true; - if (typeof a == "object" && a && typeof b == "object" && b) { - const keys = Object.getOwnPropertyNames(a); + if (typeof a == "object" && a && typeof b == "object" && b) { + const keys = Object.getOwnPropertyNames(a); - if (keys.length != Object.getOwnPropertyNames(b).length) { - return false; - } + if (keys.length != Object.getOwnPropertyNames(b).length) { + return false; + } - for (const key of keys) { - let av = a[key]; - let bv = b[key]; - if (!path && key === "nodes") { - // Nodes need to be sorted as the order changes when selecting nodes - av = [...av].sort((a, b) => a.id - b.id); - bv = [...bv].sort((a, b) => a.id - b.id); - } else if (path === "extra.ds") { - // Ignore view changes - continue; - } - if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) { - return false; - } - } + for (const key of keys) { + let av = a[key]; + let bv = b[key]; + if (!path && key === "nodes") { + // Nodes need to be sorted as the order changes when selecting nodes + av = [...av].sort((a, b) => a.id - b.id); + bv = [...bv].sort((a, b) => a.id - b.id); + } else if (path === "extra.ds") { + // Ignore view changes + continue; + } + if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) { + return false; + } + } - return true; - } + return true; + } - return false; - } + return false; + } } -const globalTracker = new ChangeTracker({}); \ No newline at end of file +const globalTracker = new ChangeTracker({}); diff --git a/src/scripts/defaultGraph.ts b/src/scripts/defaultGraph.ts index 7f2422b5b..d3bb0e8b2 100644 --- a/src/scripts/defaultGraph.ts +++ b/src/scripts/defaultGraph.ts @@ -1,121 +1,137 @@ import type { ComfyWorkflow } from "/types/comfyWorkflow"; export const defaultGraph: ComfyWorkflow = { - last_node_id: 9, - last_link_id: 9, - nodes: [ - { - id: 7, - type: "CLIPTextEncode", - pos: [413, 389], - size: { 0: 425.27801513671875, 1: 180.6060791015625 }, - flags: {}, - order: 3, - mode: 0, - inputs: [{ name: "clip", type: "CLIP", link: 5 }], - outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }], - properties: {}, - widgets_values: ["text, watermark"], - }, - { - id: 6, - type: "CLIPTextEncode", - pos: [415, 186], - size: { 0: 422.84503173828125, 1: 164.31304931640625 }, - flags: {}, - order: 2, - mode: 0, - inputs: [{ name: "clip", type: "CLIP", link: 3 }], - outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }], - properties: {}, - widgets_values: ["beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"], - }, - { - id: 5, - type: "EmptyLatentImage", - pos: [473, 609], - size: { 0: 315, 1: 106 }, - flags: {}, - order: 1, - mode: 0, - outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }], - properties: {}, - widgets_values: [512, 512, 1], - }, - { - id: 3, - type: "KSampler", - pos: [863, 186], - size: { 0: 315, 1: 262 }, - flags: {}, - order: 4, - mode: 0, - inputs: [ - { name: "model", type: "MODEL", link: 1 }, - { name: "positive", type: "CONDITIONING", link: 4 }, - { name: "negative", type: "CONDITIONING", link: 6 }, - { name: "latent_image", type: "LATENT", link: 2 }, - ], - outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }], - properties: {}, - widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1], - }, - { - id: 8, - type: "VAEDecode", - pos: [1209, 188], - size: { 0: 210, 1: 46 }, - flags: {}, - order: 5, - mode: 0, - inputs: [ - { name: "samples", type: "LATENT", link: 7 }, - { name: "vae", type: "VAE", link: 8 }, - ], - outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }], - properties: {}, - }, - { - id: 9, - type: "SaveImage", - pos: [1451, 189], - size: { 0: 210, 1: 26 }, - flags: {}, - order: 6, - mode: 0, - inputs: [{ name: "images", type: "IMAGE", link: 9 }], - properties: {}, - }, - { - id: 4, - type: "CheckpointLoaderSimple", - pos: [26, 474], - size: { 0: 315, 1: 98 }, - flags: {}, - order: 0, - mode: 0, - outputs: [ - { name: "MODEL", type: "MODEL", links: [1], slot_index: 0 }, - { name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 }, - { name: "VAE", type: "VAE", links: [8], slot_index: 2 }, - ], - properties: {}, - widgets_values: ["v1-5-pruned-emaonly.ckpt"], - }, - ], - links: [ - [1, 4, 0, 3, 0, "MODEL"], - [2, 5, 0, 3, 3, "LATENT"], - [3, 4, 1, 6, 0, "CLIP"], - [4, 6, 0, 3, 1, "CONDITIONING"], - [5, 4, 1, 7, 0, "CLIP"], - [6, 7, 0, 3, 2, "CONDITIONING"], - [7, 3, 0, 8, 0, "LATENT"], - [8, 4, 2, 8, 1, "VAE"], - [9, 8, 0, 9, 0, "IMAGE"], - ], - groups: [], - config: {}, - extra: {}, - version: 0.4, + last_node_id: 9, + last_link_id: 9, + nodes: [ + { + id: 7, + type: "CLIPTextEncode", + pos: [413, 389], + size: { 0: 425.27801513671875, 1: 180.6060791015625 }, + flags: {}, + order: 3, + mode: 0, + inputs: [{ name: "clip", type: "CLIP", link: 5 }], + outputs: [ + { + name: "CONDITIONING", + type: "CONDITIONING", + links: [6], + slot_index: 0, + }, + ], + properties: {}, + widgets_values: ["text, watermark"], + }, + { + id: 6, + type: "CLIPTextEncode", + pos: [415, 186], + size: { 0: 422.84503173828125, 1: 164.31304931640625 }, + flags: {}, + order: 2, + mode: 0, + inputs: [{ name: "clip", type: "CLIP", link: 3 }], + outputs: [ + { + name: "CONDITIONING", + type: "CONDITIONING", + links: [4], + slot_index: 0, + }, + ], + properties: {}, + widgets_values: [ + "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,", + ], + }, + { + id: 5, + type: "EmptyLatentImage", + pos: [473, 609], + size: { 0: 315, 1: 106 }, + flags: {}, + order: 1, + mode: 0, + outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }], + properties: {}, + widgets_values: [512, 512, 1], + }, + { + id: 3, + type: "KSampler", + pos: [863, 186], + size: { 0: 315, 1: 262 }, + flags: {}, + order: 4, + mode: 0, + inputs: [ + { name: "model", type: "MODEL", link: 1 }, + { name: "positive", type: "CONDITIONING", link: 4 }, + { name: "negative", type: "CONDITIONING", link: 6 }, + { name: "latent_image", type: "LATENT", link: 2 }, + ], + outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }], + properties: {}, + widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1], + }, + { + id: 8, + type: "VAEDecode", + pos: [1209, 188], + size: { 0: 210, 1: 46 }, + flags: {}, + order: 5, + mode: 0, + inputs: [ + { name: "samples", type: "LATENT", link: 7 }, + { name: "vae", type: "VAE", link: 8 }, + ], + outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }], + properties: {}, + }, + { + id: 9, + type: "SaveImage", + pos: [1451, 189], + size: { 0: 210, 1: 26 }, + flags: {}, + order: 6, + mode: 0, + inputs: [{ name: "images", type: "IMAGE", link: 9 }], + properties: {}, + }, + { + id: 4, + type: "CheckpointLoaderSimple", + pos: [26, 474], + size: { 0: 315, 1: 98 }, + flags: {}, + order: 0, + mode: 0, + outputs: [ + { name: "MODEL", type: "MODEL", links: [1], slot_index: 0 }, + { name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 }, + { name: "VAE", type: "VAE", links: [8], slot_index: 2 }, + ], + properties: {}, + widgets_values: ["v1-5-pruned-emaonly.ckpt"], + }, + ], + links: [ + [1, 4, 0, 3, 0, "MODEL"], + [2, 5, 0, 3, 3, "LATENT"], + [3, 4, 1, 6, 0, "CLIP"], + [4, 6, 0, 3, 1, "CONDITIONING"], + [5, 4, 1, 7, 0, "CLIP"], + [6, 7, 0, 3, 2, "CONDITIONING"], + [7, 3, 0, 8, 0, "LATENT"], + [8, 4, 2, 8, 1, "VAE"], + [9, 8, 0, 9, 0, "IMAGE"], + ], + groups: [], + config: {}, + extra: {}, + version: 0.4, }; diff --git a/src/scripts/domWidget.ts b/src/scripts/domWidget.ts index e6981255a..b0a1fcb9a 100644 --- a/src/scripts/domWidget.ts +++ b/src/scripts/domWidget.ts @@ -1,194 +1,216 @@ import { app, ANIM_PREVIEW_WIDGET } from "./app"; import type { LGraphNode, Vector4 } from "/types/litegraph"; - const SIZE = Symbol(); - interface Rect { - height: number; - width: number; - x: number; - y: number; + height: number; + width: number; + x: number; + y: number; } export interface DOMWidget { - type: string; - name: string; - computedHeight?: number; - element?: T; - options: any; - value?: any; - y?: number; - callback?: (value: any) => void; - draw?: (ctx: CanvasRenderingContext2D, node: LGraphNode, widgetWidth: number, y: number, widgetHeight: number) => void; - onRemove?: () => void; + type: string; + name: string; + computedHeight?: number; + element?: T; + options: any; + value?: any; + y?: number; + callback?: (value: any) => void; + draw?: ( + ctx: CanvasRenderingContext2D, + node: LGraphNode, + widgetWidth: number, + y: number, + widgetHeight: number + ) => void; + onRemove?: () => void; } - function intersect(a: Rect, b: Rect): Vector4 | null { - const x = Math.max(a.x, b.x); - const num1 = Math.min(a.x + a.width, b.x + b.width); - const y = Math.max(a.y, b.y); - const num2 = Math.min(a.y + a.height, b.y + b.height); - if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y]; - else return null; + const x = Math.max(a.x, b.x); + const num1 = Math.min(a.x + a.width, b.x + b.width); + const y = Math.max(a.y, b.y); + const num2 = Math.min(a.y + a.height, b.y + b.height); + if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y]; + else return null; } function getClipPath(node: LGraphNode, element: HTMLElement): string { - const selectedNode: LGraphNode = Object.values(app.canvas.selected_nodes)[0] as LGraphNode; - if (selectedNode && selectedNode !== node) { - const elRect = element.getBoundingClientRect(); - const MARGIN = 7; - const scale = app.canvas.ds.scale; + const selectedNode: LGraphNode = Object.values( + app.canvas.selected_nodes + )[0] as LGraphNode; + if (selectedNode && selectedNode !== node) { + const elRect = element.getBoundingClientRect(); + const MARGIN = 7; + const scale = app.canvas.ds.scale; - const bounding = selectedNode.getBounding(); - const intersection = intersect( - { x: elRect.x / scale, y: elRect.y / scale, width: elRect.width / scale, height: elRect.height / scale }, - { - x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN, - y: selectedNode.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN, - width: bounding[2] + MARGIN + MARGIN, - height: bounding[3] + MARGIN + MARGIN, - } - ); + const bounding = selectedNode.getBounding(); + const intersection = intersect( + { + x: elRect.x / scale, + y: elRect.y / scale, + width: elRect.width / scale, + height: elRect.height / scale, + }, + { + x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN, + y: + selectedNode.pos[1] + + app.canvas.ds.offset[1] - + LiteGraph.NODE_TITLE_HEIGHT - + MARGIN, + width: bounding[2] + MARGIN + MARGIN, + height: bounding[3] + MARGIN + MARGIN, + } + ); - if (!intersection) { - return ""; - } + if (!intersection) { + return ""; + } - const widgetRect = element.getBoundingClientRect(); - const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px"; - const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px"; - const clipWidth = intersection[2] + "px"; - const clipHeight = intersection[3] + "px"; - const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`; - return path; - } - return ""; + const widgetRect = element.getBoundingClientRect(); + const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px"; + const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px"; + const clipWidth = intersection[2] + "px"; + const clipHeight = intersection[3] + "px"; + const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`; + return path; + } + return ""; } function computeSize(size: [number, number]): void { - if (this.widgets?.[0]?.last_y == null) return; + if (this.widgets?.[0]?.last_y == null) return; - let y = this.widgets[0].last_y; - let freeSpace = size[1] - y; + let y = this.widgets[0].last_y; + let freeSpace = size[1] - y; - let widgetHeight = 0; - let dom = []; - for (const w of this.widgets) { - if (w.type === "converted-widget") { - // Ignore - delete w.computedHeight; - } else if (w.computeSize) { - widgetHeight += w.computeSize()[1] + 4; - } else if (w.element) { - // Extract DOM widget size info - const styles = getComputedStyle(w.element); - let minHeight = w.options.getMinHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-min-height")); - let maxHeight = w.options.getMaxHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-max-height")); + let widgetHeight = 0; + let dom = []; + for (const w of this.widgets) { + if (w.type === "converted-widget") { + // Ignore + delete w.computedHeight; + } else if (w.computeSize) { + widgetHeight += w.computeSize()[1] + 4; + } else if (w.element) { + // Extract DOM widget size info + const styles = getComputedStyle(w.element); + let minHeight = + w.options.getMinHeight?.() ?? + parseInt(styles.getPropertyValue("--comfy-widget-min-height")); + let maxHeight = + w.options.getMaxHeight?.() ?? + parseInt(styles.getPropertyValue("--comfy-widget-max-height")); - let prefHeight = w.options.getHeight?.() ?? styles.getPropertyValue("--comfy-widget-height"); - if (prefHeight.endsWith?.("%")) { - prefHeight = size[1] * (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100); - } else { - prefHeight = parseInt(prefHeight); - if (isNaN(minHeight)) { - minHeight = prefHeight; - } - } - if (isNaN(minHeight)) { - minHeight = 50; - } - if (!isNaN(maxHeight)) { - if (!isNaN(prefHeight)) { - prefHeight = Math.min(prefHeight, maxHeight); - } else { - prefHeight = maxHeight; - } - } - dom.push({ - minHeight, - prefHeight, - w, - }); - } else { - widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } + let prefHeight = + w.options.getHeight?.() ?? + styles.getPropertyValue("--comfy-widget-height"); + if (prefHeight.endsWith?.("%")) { + prefHeight = + size[1] * + (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100); + } else { + prefHeight = parseInt(prefHeight); + if (isNaN(minHeight)) { + minHeight = prefHeight; + } + } + if (isNaN(minHeight)) { + minHeight = 50; + } + if (!isNaN(maxHeight)) { + if (!isNaN(prefHeight)) { + prefHeight = Math.min(prefHeight, maxHeight); + } else { + prefHeight = maxHeight; + } + } + dom.push({ + minHeight, + prefHeight, + w, + }); + } else { + widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; + } + } - freeSpace -= widgetHeight; + freeSpace -= widgetHeight; - // Calculate sizes with all widgets at their min height - const prefGrow = []; // Nodes that want to grow to their prefd size - const canGrow = []; // Nodes that can grow to auto size - let growBy = 0; - for (const d of dom) { - freeSpace -= d.minHeight; - if (isNaN(d.prefHeight)) { - canGrow.push(d); - d.w.computedHeight = d.minHeight; - } else { - const diff = d.prefHeight - d.minHeight; - if (diff > 0) { - prefGrow.push(d); - growBy += diff; - d.diff = diff; - } else { - d.w.computedHeight = d.minHeight; - } - } - } + // Calculate sizes with all widgets at their min height + const prefGrow = []; // Nodes that want to grow to their prefd size + const canGrow = []; // Nodes that can grow to auto size + let growBy = 0; + for (const d of dom) { + freeSpace -= d.minHeight; + if (isNaN(d.prefHeight)) { + canGrow.push(d); + d.w.computedHeight = d.minHeight; + } else { + const diff = d.prefHeight - d.minHeight; + if (diff > 0) { + prefGrow.push(d); + growBy += diff; + d.diff = diff; + } else { + d.w.computedHeight = d.minHeight; + } + } + } - if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) { - // Allocate space for image - freeSpace -= 220; - } + if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) { + // Allocate space for image + freeSpace -= 220; + } - this.freeWidgetSpace = freeSpace; + this.freeWidgetSpace = freeSpace; - if (freeSpace < 0) { - // Not enough space for all widgets so we need to grow - size[1] -= freeSpace; - this.graph.setDirtyCanvas(true); - } else { - // Share the space between each - const growDiff = freeSpace - growBy; - if (growDiff > 0) { - // All pref sizes can be fulfilled - freeSpace = growDiff; - for (const d of prefGrow) { - d.w.computedHeight = d.prefHeight; - } - } else { - // We need to grow evenly - const shared = -growDiff / prefGrow.length; - for (const d of prefGrow) { - d.w.computedHeight = d.prefHeight - shared; - } - freeSpace = 0; - } + if (freeSpace < 0) { + // Not enough space for all widgets so we need to grow + size[1] -= freeSpace; + this.graph.setDirtyCanvas(true); + } else { + // Share the space between each + const growDiff = freeSpace - growBy; + if (growDiff > 0) { + // All pref sizes can be fulfilled + freeSpace = growDiff; + for (const d of prefGrow) { + d.w.computedHeight = d.prefHeight; + } + } else { + // We need to grow evenly + const shared = -growDiff / prefGrow.length; + for (const d of prefGrow) { + d.w.computedHeight = d.prefHeight - shared; + } + freeSpace = 0; + } - if (freeSpace > 0 && canGrow.length) { - // Grow any that are auto height - const shared = freeSpace / canGrow.length; - for (const d of canGrow) { - d.w.computedHeight += shared; - } - } - } + if (freeSpace > 0 && canGrow.length) { + // Grow any that are auto height + const shared = freeSpace / canGrow.length; + for (const d of canGrow) { + d.w.computedHeight += shared; + } + } + } - // Position each of the widgets - for (const w of this.widgets) { - w.y = y; - if (w.computedHeight) { - y += w.computedHeight; - } else if (w.computeSize) { - y += w.computeSize()[1] + 4; - } else { - y += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } + // Position each of the widgets + for (const w of this.widgets) { + w.y = y; + if (w.computedHeight) { + y += w.computedHeight; + } else if (w.computeSize) { + y += w.computeSize()[1] + 4; + } else { + y += LiteGraph.NODE_WIDGET_HEIGHT + 4; + } + } } // Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen @@ -197,170 +219,179 @@ const elementWidgets = new Set(); const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes; //@ts-ignore LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] { - const visibleNodes = computeVisibleNodes.apply(this, arguments); - // @ts-ignore - for (const node of app.graph._nodes) { - if (elementWidgets.has(node)) { - const hidden = visibleNodes.indexOf(node) === -1; - for (const w of node.widgets) { - // @ts-ignore - if (w.element) { - // @ts-ignore - w.element.hidden = hidden; - // @ts-ignore - w.element.style.display = hidden ? "none" : undefined; - if (hidden) { - w.options.onHide?.(w); - } - } - } - } - } + const visibleNodes = computeVisibleNodes.apply(this, arguments); + // @ts-ignore + for (const node of app.graph._nodes) { + if (elementWidgets.has(node)) { + const hidden = visibleNodes.indexOf(node) === -1; + for (const w of node.widgets) { + // @ts-ignore + if (w.element) { + // @ts-ignore + w.element.hidden = hidden; + // @ts-ignore + w.element.style.display = hidden ? "none" : undefined; + if (hidden) { + w.options.onHide?.(w); + } + } + } + } + } - return visibleNodes; + return visibleNodes; }; let enableDomClipping = true; export function addDomClippingSetting(): void { - app.ui.settings.addSetting({ - id: "Comfy.DOMClippingEnabled", - name: "Enable DOM element clipping (enabling may reduce performance)", - type: "boolean", - defaultValue: enableDomClipping, - onChange(value) { - enableDomClipping = !!value; - }, - }); + app.ui.settings.addSetting({ + id: "Comfy.DOMClippingEnabled", + name: "Enable DOM element clipping (enabling may reduce performance)", + type: "boolean", + defaultValue: enableDomClipping, + onChange(value) { + enableDomClipping = !!value; + }, + }); } //@ts-ignore LGraphNode.prototype.addDOMWidget = function ( - name: string, - type: string, - element: HTMLElement, - options: Record + name: string, + type: string, + element: HTMLElement, + options: Record ): DOMWidget { - options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options }; + options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options }; - if (!element.parentElement) { - document.body.append(element); - } - element.hidden = true; - element.style.display = "none"; + if (!element.parentElement) { + document.body.append(element); + } + element.hidden = true; + element.style.display = "none"; - let mouseDownHandler; - if (element.blur) { - mouseDownHandler = (event) => { - if (!element.contains(event.target)) { - element.blur(); - } - }; - document.addEventListener("mousedown", mouseDownHandler); - } + let mouseDownHandler; + if (element.blur) { + mouseDownHandler = (event) => { + if (!element.contains(event.target)) { + element.blur(); + } + }; + document.addEventListener("mousedown", mouseDownHandler); + } - const widget: DOMWidget = { - type, - name, - get value() { - return options.getValue?.() ?? undefined; - }, - set value(v) { - options.setValue?.(v); - widget.callback?.(widget.value); - }, - draw: function (ctx: CanvasRenderingContext2D, node: LGraphNode, widgetWidth: number, y: number, widgetHeight: number) { - if (widget.computedHeight == null) { - computeSize.call(node, node.size); - } + const widget: DOMWidget = { + type, + name, + get value() { + return options.getValue?.() ?? undefined; + }, + set value(v) { + options.setValue?.(v); + widget.callback?.(widget.value); + }, + draw: function ( + ctx: CanvasRenderingContext2D, + node: LGraphNode, + widgetWidth: number, + y: number, + widgetHeight: number + ) { + if (widget.computedHeight == null) { + computeSize.call(node, node.size); + } - const hidden = - node.flags?.collapsed || - (!!options.hideOnZoom && app.canvas.ds.scale < 0.5) || - widget.computedHeight <= 0 || - widget.type === "converted-widget" || - widget.type === "hidden"; - element.hidden = hidden; - element.style.display = hidden ? "none" : null; - if (hidden) { - widget.options.onHide?.(widget); - return; - } + const hidden = + node.flags?.collapsed || + (!!options.hideOnZoom && app.canvas.ds.scale < 0.5) || + widget.computedHeight <= 0 || + widget.type === "converted-widget" || + widget.type === "hidden"; + element.hidden = hidden; + element.style.display = hidden ? "none" : null; + if (hidden) { + widget.options.onHide?.(widget); + return; + } - const margin = 10; - const elRect = ctx.canvas.getBoundingClientRect(); - const transform = new DOMMatrix() - .scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height) - .multiplySelf(ctx.getTransform()) - .translateSelf(margin, margin + y); + const margin = 10; + const elRect = ctx.canvas.getBoundingClientRect(); + const transform = new DOMMatrix() + .scaleSelf( + elRect.width / ctx.canvas.width, + elRect.height / ctx.canvas.height + ) + .multiplySelf(ctx.getTransform()) + .translateSelf(margin, margin + y); - const scale = new DOMMatrix().scaleSelf(transform.a, transform.d); + const scale = new DOMMatrix().scaleSelf(transform.a, transform.d); - Object.assign(element.style, { - transformOrigin: "0 0", - transform: scale, - left: `${transform.a + transform.e + elRect.left}px`, - top: `${transform.d + transform.f + elRect.top}px`, - width: `${widgetWidth - margin * 2}px`, - height: `${(widget.computedHeight ?? 50) - margin * 2}px`, - position: "absolute", - // @ts-ignore - zIndex: app.graph._nodes.indexOf(node), - }); + Object.assign(element.style, { + transformOrigin: "0 0", + transform: scale, + left: `${transform.a + transform.e + elRect.left}px`, + top: `${transform.d + transform.f + elRect.top}px`, + width: `${widgetWidth - margin * 2}px`, + height: `${(widget.computedHeight ?? 50) - margin * 2}px`, + position: "absolute", + // @ts-ignore + zIndex: app.graph._nodes.indexOf(node), + }); - if (enableDomClipping) { - element.style.clipPath = getClipPath(node, element); - element.style.willChange = "clip-path"; - } + if (enableDomClipping) { + element.style.clipPath = getClipPath(node, element); + element.style.willChange = "clip-path"; + } - this.options.onDraw?.(widget); - }, - element, - options, - onRemove() { - if (mouseDownHandler) { - document.removeEventListener("mousedown", mouseDownHandler); - } - element.remove(); - }, - }; + this.options.onDraw?.(widget); + }, + element, + options, + onRemove() { + if (mouseDownHandler) { + document.removeEventListener("mousedown", mouseDownHandler); + } + element.remove(); + }, + }; - for (const evt of options.selectOn) { - element.addEventListener(evt, () => { - app.canvas.selectNode(this); - app.canvas.bringToFront(this); - }); - } + for (const evt of options.selectOn) { + element.addEventListener(evt, () => { + app.canvas.selectNode(this); + app.canvas.bringToFront(this); + }); + } - this.addCustomWidget(widget); - elementWidgets.add(this); + this.addCustomWidget(widget); + elementWidgets.add(this); - const collapse = this.collapse; - this.collapse = function () { - collapse.apply(this, arguments); - if (this.flags?.collapsed) { - element.hidden = true; - element.style.display = "none"; - } - } + const collapse = this.collapse; + this.collapse = function () { + collapse.apply(this, arguments); + if (this.flags?.collapsed) { + element.hidden = true; + element.style.display = "none"; + } + }; - const onRemoved = this.onRemoved; - this.onRemoved = function () { - element.remove(); - elementWidgets.delete(this); - onRemoved?.apply(this, arguments); - }; + const onRemoved = this.onRemoved; + this.onRemoved = function () { + element.remove(); + elementWidgets.delete(this); + onRemoved?.apply(this, arguments); + }; - if (!this[SIZE]) { - this[SIZE] = true; - const onResize = this.onResize; - this.onResize = function (size) { - options.beforeResize?.call(widget, this); - computeSize.call(this, size); - onResize?.apply(this, arguments); - options.afterResize?.call(widget, this); - }; - } + if (!this[SIZE]) { + this[SIZE] = true; + const onResize = this.onResize; + this.onResize = function (size) { + options.beforeResize?.call(widget, this); + computeSize.call(this, size); + onResize?.apply(this, arguments); + options.afterResize?.call(widget, this); + }; + } - return widget; + return widget; }; diff --git a/src/scripts/logging.ts b/src/scripts/logging.ts index 791e9f4f1..ded3cf9da 100644 --- a/src/scripts/logging.ts +++ b/src/scripts/logging.ts @@ -41,13 +41,13 @@ function stringify(val, depth, replacer, space, onGetObjID?) { r ? (o = (onGetObjID && onGetObjID(val)) || null) : JSON.stringify(val, function (k, v) { - if (a || depth > 0) { - if (replacer) v = replacer(k, v); - if (!k) return (a = Array.isArray(v)), (val = v); - !o && (o = a ? [] : {}); - o[k] = _build(v, a ? depth : depth - 1); - } - }), + if (a || depth > 0) { + if (replacer) v = replacer(k, v); + if (!k) return (a = Array.isArray(v)), (val = v); + !o && (o = a ? [] : {}); + o[k] = _build(v, a ? depth : depth - 1); + } + }), o === void 0 ? (a ? [] : {}) : o); } return JSON.stringify(_build(val, depth), null, space); @@ -97,9 +97,12 @@ class ComfyLoggingDialog extends ComfyDialog { } export() { - const blob = new Blob([stringify([...this.logging.entries], 20, jsonReplacer, "\t")], { - type: "application/json", - }); + const blob = new Blob( + [stringify([...this.logging.entries], 20, jsonReplacer, "\t")], + { + type: "application/json", + } + ); const url = URL.createObjectURL(blob); const a = $el("a", { href: url, @@ -147,230 +150,234 @@ class ComfyLoggingDialog extends ComfyDialog { textContent: "Export logs...", onclick: () => this.export(), }), - $el("button", { - type: "button", - textContent: "View exported logs...", - onclick: () => this.import(), - }), - ...super.createButtons(), - ]; - } + $el("button", { + type: "button", + textContent: "View exported logs...", + onclick: () => this.import(), + }), + ...super.createButtons(), + ]; + } - getTypeColor(type) { - switch (type) { - case "error": - return "red"; - case "warn": - return "orange"; - case "debug": - return "dodgerblue"; - } - } + getTypeColor(type) { + switch (type) { + case "error": + return "red"; + case "warn": + return "orange"; + case "debug": + return "dodgerblue"; + } + } - show(entries?: any[]) { - if (!entries) entries = this.logging.entries; - this.element.style.width = "100%"; - const cols = { - source: "Source", - type: "Type", - timestamp: "Timestamp", - message: "Message", - }; - const keys = Object.keys(cols); - const headers = Object.values(cols).map((title) => - $el("div.comfy-logging-title", { - textContent: title, - }) - ); - const rows = entries.map((entry, i) => { - return $el( - "div.comfy-logging-log", - { - $: (el) => el.style.setProperty("--row-bg", `var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`), - }, - keys.map((key) => { - let v = entry[key]; - let color; - if (key === "type") { - color = this.getTypeColor(v); - } else { - v = jsonReplacer(key, v, true); + show(entries?: any[]) { + if (!entries) entries = this.logging.entries; + this.element.style.width = "100%"; + const cols = { + source: "Source", + type: "Type", + timestamp: "Timestamp", + message: "Message", + }; + const keys = Object.keys(cols); + const headers = Object.values(cols).map((title) => + $el("div.comfy-logging-title", { + textContent: title, + }) + ); + const rows = entries.map((entry, i) => { + return $el( + "div.comfy-logging-log", + { + $: (el) => + el.style.setProperty( + "--row-bg", + `var(--tr-${i % 2 ? "even" : "odd"}-bg-color)` + ), + }, + keys.map((key) => { + let v = entry[key]; + let color; + if (key === "type") { + color = this.getTypeColor(v); + } else { + v = jsonReplacer(key, v, true); - if (typeof v === "object") { - v = stringify(v, 5, jsonReplacer, " "); - } - } + if (typeof v === "object") { + v = stringify(v, 5, jsonReplacer, " "); + } + } - return $el("div", { - style: { - color, - }, - textContent: v, - }); - }) - ); - }); + return $el("div", { + style: { + color, + }, + textContent: v, + }); + }) + ); + }); - const grid = $el( - "div.comfy-logging-logs", - { - style: { - gridTemplateColumns: `repeat(${headers.length}, 1fr)`, - }, - }, - [...headers, ...rows] - ); - const els = [grid]; - if (!this.logging.enabled) { - els.unshift( - $el("h3", { - style: { textAlign: "center" }, - textContent: "Logging is disabled", - }) - ); - } - super.show($el("div", els)); - } + const grid = $el( + "div.comfy-logging-logs", + { + style: { + gridTemplateColumns: `repeat(${headers.length}, 1fr)`, + }, + }, + [...headers, ...rows] + ); + const els = [grid]; + if (!this.logging.enabled) { + els.unshift( + $el("h3", { + style: { textAlign: "center" }, + textContent: "Logging is disabled", + }) + ); + } + super.show($el("div", els)); + } } export class ComfyLogging { - /** - * @type Array<{ source: string, type: string, timestamp: Date, message: any }> - */ - entries = []; + /** + * @type Array<{ source: string, type: string, timestamp: Date, message: any }> + */ + entries = []; - #enabled; - #console = {}; + #enabled; + #console = {}; - app: ComfyApp; - dialog: ComfyLoggingDialog; + app: ComfyApp; + dialog: ComfyLoggingDialog; - get enabled() { - return this.#enabled; - } + get enabled() { + return this.#enabled; + } - set enabled(value) { - if (value === this.#enabled) return; - if (value) { - this.patchConsole(); - } else { - this.unpatchConsole(); - } - this.#enabled = value; - } + set enabled(value) { + if (value === this.#enabled) return; + if (value) { + this.patchConsole(); + } else { + this.unpatchConsole(); + } + this.#enabled = value; + } - constructor(app) { - this.app = app; + constructor(app) { + this.app = app; - this.dialog = new ComfyLoggingDialog(this); - this.addSetting(); - this.catchUnhandled(); - this.addInitData(); - } + this.dialog = new ComfyLoggingDialog(this); + this.addSetting(); + this.catchUnhandled(); + this.addInitData(); + } - addSetting() { - const settingId: string = "Comfy.Logging.Enabled"; - const htmlSettingId = settingId.replaceAll(".", "-"); - const setting = this.app.ui.settings.addSetting({ - id: settingId, - name: settingId, - defaultValue: true, - onChange: (value) => { - this.enabled = value; - }, - type: (name, setter, value) => { - return $el("tr", [ - $el("td", [ - $el("label", { - textContent: "Logging", - for: htmlSettingId, - }), - ]), - $el("td", [ - $el("input", { - id: htmlSettingId, - type: "checkbox", - checked: value, - onchange: (event) => { - setter(event.target.checked); - }, - }), - $el("button", { - textContent: "View Logs", - onclick: () => { - this.app.ui.settings.element.close(); - this.dialog.show(); - }, - style: { - fontSize: "14px", - display: "block", - marginTop: "5px", - }, - }), - ]), - ]); - }, - }); - this.enabled = setting.value; - } + addSetting() { + const settingId: string = "Comfy.Logging.Enabled"; + const htmlSettingId = settingId.replaceAll(".", "-"); + const setting = this.app.ui.settings.addSetting({ + id: settingId, + name: settingId, + defaultValue: true, + onChange: (value) => { + this.enabled = value; + }, + type: (name, setter, value) => { + return $el("tr", [ + $el("td", [ + $el("label", { + textContent: "Logging", + for: htmlSettingId, + }), + ]), + $el("td", [ + $el("input", { + id: htmlSettingId, + type: "checkbox", + checked: value, + onchange: (event) => { + setter(event.target.checked); + }, + }), + $el("button", { + textContent: "View Logs", + onclick: () => { + this.app.ui.settings.element.close(); + this.dialog.show(); + }, + style: { + fontSize: "14px", + display: "block", + marginTop: "5px", + }, + }), + ]), + ]); + }, + }); + this.enabled = setting.value; + } - patchConsole() { - // Capture common console outputs - const self = this; - for (const type of ["log", "warn", "error", "debug"]) { - const orig = console[type]; - this.#console[type] = orig; - console[type] = function () { - orig.apply(console, arguments); - self.addEntry("console", type, ...arguments); - }; - } - } + patchConsole() { + // Capture common console outputs + const self = this; + for (const type of ["log", "warn", "error", "debug"]) { + const orig = console[type]; + this.#console[type] = orig; + console[type] = function () { + orig.apply(console, arguments); + self.addEntry("console", type, ...arguments); + }; + } + } - unpatchConsole() { - // Restore original console functions - for (const type of Object.keys(this.#console)) { - console[type] = this.#console[type]; - } - this.#console = {}; - } + unpatchConsole() { + // Restore original console functions + for (const type of Object.keys(this.#console)) { + console[type] = this.#console[type]; + } + this.#console = {}; + } - catchUnhandled() { - // Capture uncaught errors - window.addEventListener("error", (e) => { - this.addEntry("window", "error", e.error ?? "Unknown error"); - return false; - }); + catchUnhandled() { + // Capture uncaught errors + window.addEventListener("error", (e) => { + this.addEntry("window", "error", e.error ?? "Unknown error"); + return false; + }); - window.addEventListener("unhandledrejection", (e) => { - this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error"); - }); - } + window.addEventListener("unhandledrejection", (e) => { + this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error"); + }); + } - clear() { - this.entries = []; - } + clear() { + this.entries = []; + } - addEntry(source, type, ...args) { - if (this.enabled) { - this.entries.push({ - source, - type, - timestamp: new Date(), - message: args, - }); - } - } + addEntry(source, type, ...args) { + if (this.enabled) { + this.entries.push({ + source, + type, + timestamp: new Date(), + message: args, + }); + } + } - log(source, ...args) { - this.addEntry(source, "log", ...args); - } + log(source, ...args) { + this.addEntry(source, "log", ...args); + } - async addInitData() { - if (!this.enabled) return; - const source = "ComfyUI.Logging"; - this.addEntry(source, "debug", { UserAgent: navigator.userAgent }); - const systemStats = await api.getSystemStats(); - this.addEntry(source, "debug", systemStats); - } + async addInitData() { + if (!this.enabled) return; + const source = "ComfyUI.Logging"; + this.addEntry(source, "debug", { UserAgent: navigator.userAgent }); + const systemStats = await api.getSystemStats(); + this.addEntry(source, "debug", systemStats); + } } diff --git a/src/scripts/pnginfo.ts b/src/scripts/pnginfo.ts index 7a9c90e9f..f63df0b42 100644 --- a/src/scripts/pnginfo.ts +++ b/src/scripts/pnginfo.ts @@ -23,17 +23,26 @@ export function getPngMetadata(file) { // Get the length of the chunk const length = dataView.getUint32(offset); // Get the chunk type - const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8)); + const type = String.fromCharCode( + ...pngData.slice(offset + 4, offset + 8) + ); if (type === "tEXt" || type == "comf" || type === "iTXt") { // Get the keyword let keyword_end = offset + 8; while (pngData[keyword_end] !== 0) { keyword_end++; } - const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end)); + const keyword = String.fromCharCode( + ...pngData.slice(offset + 8, keyword_end) + ); // Get the text - const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length); - const contentJson = new TextDecoder("utf-8").decode(contentArraySegment); + const contentArraySegment = pngData.slice( + keyword_end + 1, + offset + 8 + length + ); + const contentJson = new TextDecoder("utf-8").decode( + contentArraySegment + ); txt_chunks[keyword] = contentJson; } @@ -53,11 +62,17 @@ function parseExifData(exifData) { // Function to read 16-bit and 32-bit integers from binary data function readInt(offset, isLittleEndian, length) { - let arr = exifData.slice(offset, offset + length) + let arr = exifData.slice(offset, offset + length); if (length === 2) { - return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(0, isLittleEndian); + return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16( + 0, + isLittleEndian + ); } else if (length === 4) { - return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(0, isLittleEndian); + return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32( + 0, + isLittleEndian + ); } } @@ -79,7 +94,9 @@ function parseExifData(exifData) { let value; if (type === 2) { // ASCII string - value = String.fromCharCode(...exifData.slice(valueOffset, valueOffset + numValues - 1)); + value = String.fromCharCode( + ...exifData.slice(valueOffset, valueOffset + numValues - 1) + ); } result[tag] = value; @@ -94,13 +111,13 @@ function parseExifData(exifData) { } function splitValues(input) { - var output = {}; - for (var key in input) { + var output = {}; + for (var key in input) { var value = input[key]; - var splitValues = value.split(':', 2); + var splitValues = value.split(":", 2); output[splitValues[0]] = splitValues[1]; - } - return output; + } + return output; } export function getWebpMetadata(file) { @@ -111,7 +128,10 @@ export function getWebpMetadata(file) { const dataView = new DataView(webp.buffer); // Check that the WEBP signature is present - if (dataView.getUint32(0) !== 0x52494646 || dataView.getUint32(8) !== 0x57454250) { + if ( + dataView.getUint32(0) !== 0x52494646 || + dataView.getUint32(8) !== 0x57454250 + ) { console.error("Not a valid WEBP file"); r({}); return; @@ -123,15 +143,22 @@ export function getWebpMetadata(file) { // Loop through the chunks in the WEBP file while (offset < webp.length) { const chunk_length = dataView.getUint32(offset + 4, true); - const chunk_type = String.fromCharCode(...webp.slice(offset, offset + 4)); + const chunk_type = String.fromCharCode( + ...webp.slice(offset, offset + 4) + ); if (chunk_type === "EXIF") { - if (String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) == "Exif\0\0") { + if ( + String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) == + "Exif\0\0" + ) { offset += 6; } - let data = parseExifData(webp.slice(offset + 8, offset + 8 + chunk_length)); + let data = parseExifData( + webp.slice(offset + 8, offset + 8 + chunk_length) + ); for (var key in data) { var value = data[key] as string; - let index = value.indexOf(':'); + let index = value.indexOf(":"); txt_chunks[value.slice(0, index)] = value.slice(index + 1); } } @@ -150,11 +177,17 @@ export function getLatentMetadata(file) { return new Promise((r) => { const reader = new FileReader(); reader.onload = (event) => { - const safetensorsData = new Uint8Array(event.target.result as ArrayBuffer); + const safetensorsData = new Uint8Array( + event.target.result as ArrayBuffer + ); const dataView = new DataView(safetensorsData.buffer); let header_size = dataView.getUint32(0, true); let offset = 8; - let header = JSON.parse(new TextDecoder().decode(safetensorsData.slice(offset, offset + header_size))); + let header = JSON.parse( + new TextDecoder().decode( + safetensorsData.slice(offset, offset + header_size) + ) + ); r(header.__metadata__); }; @@ -164,7 +197,7 @@ export function getLatentMetadata(file) { } function getString(dataView: DataView, offset: number, length: number): string { - let string = ''; + let string = ""; for (let i = 0; i < length; i++) { string += String.fromCharCode(dataView.getUint8(offset + i)); } @@ -188,7 +221,7 @@ function parseVorbisComment(dataView: DataView): Record { const comment = getString(dataView, offset, commentLength); offset += commentLength; - const [key, value] = comment.split('='); + const [key, value] = comment.split("="); comments[key] = value; } @@ -200,14 +233,16 @@ function parseVorbisComment(dataView: DataView): Record { export function getFlacMetadata(file: Blob): Promise> { return new Promise((r) => { const reader = new FileReader(); - reader.onload = function(event) { + reader.onload = function (event) { const arrayBuffer = event.target.result as ArrayBuffer; const dataView = new DataView(arrayBuffer); // Verify the FLAC signature - const signature = String.fromCharCode(...new Uint8Array(arrayBuffer, 0, 4)); - if (signature !== 'fLaC') { - console.error('Not a valid FLAC file'); + const signature = String.fromCharCode( + ...new Uint8Array(arrayBuffer, 0, 4) + ); + if (signature !== "fLaC") { + console.error("Not a valid FLAC file"); return; } @@ -216,12 +251,15 @@ export function getFlacMetadata(file: Blob): Promise> { let vorbisComment = null; while (offset < dataView.byteLength) { const isLastBlock = dataView.getUint8(offset) & 0x80; - const blockType = dataView.getUint8(offset) & 0x7F; - const blockSize = dataView.getUint32(offset, false) & 0xFFFFFF; + const blockType = dataView.getUint8(offset) & 0x7f; + const blockSize = dataView.getUint32(offset, false) & 0xffffff; offset += 4; - if (blockType === 4) { // Vorbis Comment block type - vorbisComment = parseVorbisComment(new DataView(arrayBuffer, offset, blockSize)); + if (blockType === 4) { + // Vorbis Comment block type + vorbisComment = parseVorbisComment( + new DataView(arrayBuffer, offset, blockSize) + ); } offset += blockSize; @@ -241,11 +279,13 @@ export async function importA1111(graph, parameters) { const opts = parameters .substr(p) .split("\n")[1] - .match(new RegExp("\\s*([^:]+:\\s*([^\"\\{].*?|\".*?\"|\\{.*?\\}))\\s*(,|$)", "g")) + .match( + new RegExp('\\s*([^:]+:\\s*([^"\\{].*?|".*?"|\\{.*?\\}))\\s*(,|$)', "g") + ) .reduce((p, n) => { const s = n.split(":"); - if (s[1].endsWith(',')) { - s[1] = s[1].substr(0, s[1].length -1); + if (s[1].endsWith(",")) { + s[1] = s[1].substr(0, s[1].length - 1); } p[s[0].trim().toLowerCase()] = s[1].trim(); return p; @@ -271,7 +311,7 @@ export async function importA1111(graph, parameters) { const getWidget = (node, name) => { return node.widgets.find((w) => w.name === name); - } + }; const setWidgetValue = (node, name, value, isOptionPrefix?) => { const w = getWidget(node, name); @@ -286,7 +326,7 @@ export async function importA1111(graph, parameters) { } else { w.value = value; } - } + }; const createLoraNodes = (clipNode, text, prevClip, prevModel) => { const loras = []; @@ -320,24 +360,28 @@ export async function importA1111(graph, parameters) { } return { text, prevModel, prevClip }; - } + }; const replaceEmbeddings = (text) => { - if(!embeddings.length) return text; + if (!embeddings.length) return text; return text.replaceAll( new RegExp( - "\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b", + "\\b(" + + embeddings + .map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) + .join("\\b|\\b") + + ")\\b", "ig" ), "embedding:$1" ); - } + }; const popOpt = (name) => { const v = opts[name]; delete opts[name]; return v; - } + }; graph.clear(); graph.add(ckptNode); @@ -365,7 +409,7 @@ export async function importA1111(graph, parameters) { model(v) { setWidgetValue(ckptNode, "ckpt_name", v, true); }, - "vae"(v) { + vae(v) { setWidgetValue(vaeLoaderNode, "vae_name", v, true); }, "cfg scale"(v) { @@ -383,7 +427,9 @@ export async function importA1111(graph, parameters) { setWidgetValue(samplerNode, "scheduler", "normal"); } const w = getWidget(samplerNode, "sampler_name"); - const o = w.options.values.find((w) => w === name || w === "sample_" + name); + const o = w.options.values.find( + (w) => w === name || w === "sample_" + name + ); if (o) { setWidgetValue(samplerNode, "sampler_name", o); } @@ -431,11 +477,14 @@ export async function importA1111(graph, parameters) { samplerNode.connect(0, decode, 0); vaeLoaderNode.connect(0, decode, 1); - const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader"); + const upscaleLoaderNode = + LiteGraph.createNode("UpscaleModelLoader"); graph.add(upscaleLoaderNode); setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true); - const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel"); + const modelUpscaleNode = LiteGraph.createNode( + "ImageUpscaleWithModel" + ); graph.add(modelUpscaleNode); decode.connect(0, modelUpscaleNode, 1); upscaleLoaderNode.connect(0, modelUpscaleNode, 0); @@ -444,7 +493,8 @@ export async function importA1111(graph, parameters) { graph.add(upscaleNode); modelUpscaleNode.connect(0, upscaleNode, 0); - const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled")); + const vaeEncodeNode = (latentNode = + LiteGraph.createNode("VAEEncodeTiled")); graph.add(vaeEncodeNode); upscaleNode.connect(0, vaeEncodeNode, 0); vaeLoaderNode.connect(0, vaeEncodeNode, 1); @@ -477,14 +527,39 @@ export async function importA1111(graph, parameters) { } if (hrSamplerNode) { - setWidgetValue(hrSamplerNode, "steps", hrSteps? +hrSteps : getWidget(samplerNode, "steps").value); - setWidgetValue(hrSamplerNode, "cfg", getWidget(samplerNode, "cfg").value); - setWidgetValue(hrSamplerNode, "scheduler", getWidget(samplerNode, "scheduler").value); - setWidgetValue(hrSamplerNode, "sampler_name", getWidget(samplerNode, "sampler_name").value); - setWidgetValue(hrSamplerNode, "denoise", +(popOpt("denoising strength") || "1")); + setWidgetValue( + hrSamplerNode, + "steps", + hrSteps ? +hrSteps : getWidget(samplerNode, "steps").value + ); + setWidgetValue( + hrSamplerNode, + "cfg", + getWidget(samplerNode, "cfg").value + ); + setWidgetValue( + hrSamplerNode, + "scheduler", + getWidget(samplerNode, "scheduler").value + ); + setWidgetValue( + hrSamplerNode, + "sampler_name", + getWidget(samplerNode, "sampler_name").value + ); + setWidgetValue( + hrSamplerNode, + "denoise", + +(popOpt("denoising strength") || "1") + ); } - let n = createLoraNodes(positiveNode, positive, { node: clipSkipNode, index: 0 }, { node: ckptNode, index: 0 }); + let n = createLoraNodes( + positiveNode, + positive, + { node: clipSkipNode, index: 0 }, + { node: ckptNode, index: 0 } + ); positive = n.text; n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel); negative = n.text; @@ -494,7 +569,15 @@ export async function importA1111(graph, parameters) { graph.arrange(); - for (const opt of ["model hash", "ensd", "version", "vae hash", "ti hashes", "lora hashes", "hashes"]) { + for (const opt of [ + "model hash", + "ensd", + "version", + "vae hash", + "ti hashes", + "lora hashes", + "hashes", + ]) { delete opts[opt]; } diff --git a/src/scripts/ui.ts b/src/scripts/ui.ts index 8f9ba1f46..3a5292af7 100644 --- a/src/scripts/ui.ts +++ b/src/scripts/ui.ts @@ -8,67 +8,77 @@ import { TaskItem } from "/types/apiTypes"; export const ComfyDialog = _ComfyDialog; type Position2D = { - x: number, - y: number + x: number; + y: number; }; type Props = { - parent?: HTMLElement, - $?: (el: HTMLElement) => void, - dataset?: DOMStringMap, - style?: Partial, - for?: string, - textContent?: string, - [key: string]: any + parent?: HTMLElement; + $?: (el: HTMLElement) => void; + dataset?: DOMStringMap; + style?: Partial; + for?: string; + textContent?: string; + [key: string]: any; }; type Children = Element[] | Element | string | string[]; -export function $el(tag: string, propsOrChildren?: Children | Props, children?: Children): HTMLElement { - const split = tag.split("."); - const element = document.createElement(split.shift() as string); - if (split.length > 0) { - element.classList.add(...split); +export function $el( + tag: string, + propsOrChildren?: Children | Props, + children?: Children +): HTMLElement { + const split = tag.split("."); + const element = document.createElement(split.shift() as string); + if (split.length > 0) { + element.classList.add(...split); + } + + if (propsOrChildren) { + if (typeof propsOrChildren === "string") { + propsOrChildren = { textContent: propsOrChildren }; + } else if (propsOrChildren instanceof Element) { + propsOrChildren = [propsOrChildren]; } + if (Array.isArray(propsOrChildren)) { + element.append(...propsOrChildren); + } else { + const { + parent, + $: cb, + dataset, + style, + ...rest + } = propsOrChildren as Props; - if (propsOrChildren) { - if (typeof propsOrChildren === "string") { - propsOrChildren = { textContent: propsOrChildren }; - } else if (propsOrChildren instanceof Element) { - propsOrChildren = [propsOrChildren]; - } - if (Array.isArray(propsOrChildren)) { - element.append(...propsOrChildren); - } else { - const { parent, $: cb, dataset, style, ...rest } = propsOrChildren as Props; + if (rest.for) { + element.setAttribute("for", rest.for); + } - if (rest.for) { - element.setAttribute("for", rest.for) - } + if (style) { + Object.assign(element.style, style); + } - if (style) { - Object.assign(element.style, style); - } + if (dataset) { + Object.assign(element.dataset, dataset); + } - if (dataset) { - Object.assign(element.dataset, dataset); - } + Object.assign(element, rest); + if (children) { + element.append(...(Array.isArray(children) ? children : [children])); + } - Object.assign(element, rest); - if (children) { - element.append(...(Array.isArray(children) ? children : [children])); - } + if (parent) { + parent.append(element); + } - if (parent) { - parent.append(element); - } - - if (cb) { - cb(element); - } - } + if (cb) { + cb(element); + } } - return element; + } + return element; } function dragElement(dragEl, settings) { @@ -93,12 +103,17 @@ function dragElement(dragEl, settings) { function ensureInBounds() { try { - newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft)); - newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop)); + newPosX = Math.min( + document.body.clientWidth - dragEl.clientWidth, + Math.max(0, dragEl.offsetLeft) + ); + newPosY = Math.min( + document.body.clientHeight - dragEl.clientHeight, + Math.max(0, dragEl.offsetTop) + ); positionElement(); - } - catch (exception) { + } catch (exception) { // robust } } @@ -112,7 +127,8 @@ function dragElement(dragEl, settings) { // set the element's new position: if (anchorRight) { dragEl.style.left = "unset"; - dragEl.style.right = document.body.clientWidth - newPosX - dragEl.clientWidth + "px"; + dragEl.style.right = + document.body.clientWidth - newPosX - dragEl.clientWidth + "px"; } else { dragEl.style.left = newPosX + "px"; dragEl.style.right = "unset"; @@ -180,8 +196,14 @@ function dragElement(dragEl, settings) { posStartX = e.clientX; posStartY = e.clientY; - newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft + posDiffX)); - newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop + posDiffY)); + newPosX = Math.min( + document.body.clientWidth - dragEl.clientWidth, + Math.max(0, dragEl.offsetLeft + posDiffX) + ); + newPosY = Math.min( + document.body.clientHeight - dragEl.clientHeight, + Math.max(0, dragEl.offsetTop + posDiffY) + ); positionElement(); } @@ -226,31 +248,40 @@ class ComfyList { textContent: section, }), $el("div.comfy-list-items", [ - ...(this.#reverse ? items[section].reverse() : items[section]).map((item: TaskItem) => { - // Allow items to specify a custom remove action (e.g. for interrupt current prompt) - const removeAction = "remove" in item ? item.remove : { - name: "Delete", - cb: () => api.deleteItem(this.#type, item.prompt[1]), - }; - return $el("div", { textContent: item.prompt[0] + ": " }, [ - $el("button", { - textContent: "Load", - onclick: async () => { - await app.loadGraphData(item.prompt[3].extra_pnginfo.workflow, true, false); - if ("outputs" in item) { - app.nodeOutputs = item.outputs; - } - }, - }), - $el("button", { - textContent: removeAction.name, - onclick: async () => { - await removeAction.cb(); - await this.update(); - }, - }), - ]); - }), + ...(this.#reverse ? items[section].reverse() : items[section]).map( + (item: TaskItem) => { + // Allow items to specify a custom remove action (e.g. for interrupt current prompt) + const removeAction = + "remove" in item + ? item.remove + : { + name: "Delete", + cb: () => api.deleteItem(this.#type, item.prompt[1]), + }; + return $el("div", { textContent: item.prompt[0] + ": " }, [ + $el("button", { + textContent: "Load", + onclick: async () => { + await app.loadGraphData( + item.prompt[3].extra_pnginfo.workflow, + true, + false + ); + if ("outputs" in item) { + app.nodeOutputs = item.outputs; + } + }, + }), + $el("button", { + textContent: removeAction.name, + onclick: async () => { + await removeAction.cb(); + await this.update(); + }, + }), + ]); + } + ), ]), ]), $el("div.comfy-list-actions", [ @@ -400,8 +431,15 @@ export class ComfyUI { const autoQueueModeEl = toggleSwitch( "autoQueueMode", [ - { text: "instant", tooltip: "A new prompt will be queued as soon as the queue reaches 0" }, - { text: "change", tooltip: "A new prompt will be queued when the queue is at 0 and the graph is/has changed" }, + { + text: "instant", + tooltip: "A new prompt will be queued as soon as the queue reaches 0", + }, + { + text: "change", + tooltip: + "A new prompt will be queued when the queue is at 0 and the graph is/has changed", + }, ], { onChange: (value) => { @@ -435,30 +473,34 @@ export class ComfyUI { ) as HTMLDivElement; this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [ - $el("div.drag-handle.comfy-menu-header", { - style: { - overflow: "hidden", - position: "relative", - width: "100%", - cursor: "default" - } - }, [ - $el("span.drag-handle"), - $el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }), - $el("div.comfy-menu-actions", [ - $el("button.comfy-settings-btn", { - textContent: "⚙️", - onclick: () => this.settings.show(), - }), - $el("button.comfy-close-menu-btn", { - textContent: "\u00d7", - onclick: () => { - this.menuContainer.style.display = "none"; - this.menuHamburger.style.display = "flex"; - }, - }), - ]), - ]), + $el( + "div.drag-handle.comfy-menu-header", + { + style: { + overflow: "hidden", + position: "relative", + width: "100%", + cursor: "default", + }, + }, + [ + $el("span.drag-handle"), + $el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }), + $el("div.comfy-menu-actions", [ + $el("button.comfy-settings-btn", { + textContent: "⚙️", + onclick: () => this.settings.show(), + }), + $el("button.comfy-close-menu-btn", { + textContent: "\u00d7", + onclick: () => { + this.menuContainer.style.display = "none"; + this.menuHamburger.style.display = "flex"; + }, + }), + ]), + ] + ), $el("button.comfy-queue-btn", { id: "queue-button", textContent: "Queue Prompt", @@ -469,70 +511,95 @@ export class ComfyUI { $el("input", { type: "checkbox", onchange: (i) => { - document.getElementById("extraOptions").style.display = i.srcElement.checked ? "block" : "none"; - this.batchCount = i.srcElement.checked ? - Number.parseInt((document.getElementById("batchCountInputRange") as HTMLInputElement).value) : 1; - (document.getElementById("autoQueueCheckbox") as HTMLInputElement).checked = false; + document.getElementById("extraOptions").style.display = i + .srcElement.checked + ? "block" + : "none"; + this.batchCount = i.srcElement.checked + ? Number.parseInt( + ( + document.getElementById( + "batchCountInputRange" + ) as HTMLInputElement + ).value + ) + : 1; + ( + document.getElementById("autoQueueCheckbox") as HTMLInputElement + ).checked = false; this.autoQueueEnabled = false; }, }), ]), ]), - $el("div", { id: "extraOptions", style: { width: "100%", display: "none" } }, [ - $el("div", [ - - $el("label", { innerHTML: "Batch count" }), - $el("input", { - id: "batchCountInputNumber", - type: "number", - value: this.batchCount, - min: "1", - style: { width: "35%", "marginLeft": "0.4em" }, - oninput: (i) => { - this.batchCount = i.target.value; - /* Even though an element with a type of range logically represents a number (since + $el( + "div", + { id: "extraOptions", style: { width: "100%", display: "none" } }, + [ + $el("div", [ + $el("label", { innerHTML: "Batch count" }), + $el("input", { + id: "batchCountInputNumber", + type: "number", + value: this.batchCount, + min: "1", + style: { width: "35%", marginLeft: "0.4em" }, + oninput: (i) => { + this.batchCount = i.target.value; + /* Even though an element with a type of range logically represents a number (since it's used for numeric input), the value it holds is still treated as a string in HTML and JavaScript. This behavior is consistent across all elements regardless of their type (like text, number, or range), where the .value property is always a string. */ - (document.getElementById("batchCountInputRange") as HTMLInputElement).value = this.batchCount.toString(); - }, - }), - $el("input", { - id: "batchCountInputRange", - type: "range", - min: "1", - max: "100", - value: this.batchCount, - oninput: (i) => { - this.batchCount = i.srcElement.value; - // Note - (document.getElementById("batchCountInputNumber") as HTMLInputElement).value = i.srcElement.value; - }, - }), - ]), - $el("div", [ - $el("label", { - for: "autoQueueCheckbox", - innerHTML: "Auto Queue" - }), - $el("input", { - id: "autoQueueCheckbox", - type: "checkbox", - checked: false, - title: "Automatically queue prompt when the queue size hits 0", - onchange: (e) => { - this.autoQueueEnabled = e.target.checked; - autoQueueModeEl.style.display = this.autoQueueEnabled ? "" : "none"; - } - }), - autoQueueModeEl - ]) - ]), + ( + document.getElementById( + "batchCountInputRange" + ) as HTMLInputElement + ).value = this.batchCount.toString(); + }, + }), + $el("input", { + id: "batchCountInputRange", + type: "range", + min: "1", + max: "100", + value: this.batchCount, + oninput: (i) => { + this.batchCount = i.srcElement.value; + // Note + ( + document.getElementById( + "batchCountInputNumber" + ) as HTMLInputElement + ).value = i.srcElement.value; + }, + }), + ]), + $el("div", [ + $el("label", { + for: "autoQueueCheckbox", + innerHTML: "Auto Queue", + }), + $el("input", { + id: "autoQueueCheckbox", + type: "checkbox", + checked: false, + title: "Automatically queue prompt when the queue size hits 0", + onchange: (e) => { + this.autoQueueEnabled = e.target.checked; + autoQueueModeEl.style.display = this.autoQueueEnabled + ? "" + : "none"; + }, + }), + autoQueueModeEl, + ]), + ] + ), $el("div.comfy-menu-btns", [ $el("button", { id: "queue-front-button", textContent: "Queue Front", - onclick: () => app.queuePrompt(-1, this.batchCount) + onclick: () => app.queuePrompt(-1, this.batchCount), }), $el("button", { $: (b) => (this.queue.button = b as HTMLButtonElement), @@ -567,7 +634,7 @@ export class ComfyUI { filename += ".json"; } } - app.graphToPrompt().then(p => { + app.graphToPrompt().then((p) => { const json = JSON.stringify(p.workflow, null, 2); // convert the data to a JSON string const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); @@ -598,7 +665,7 @@ export class ComfyUI { filename += ".json"; } } - app.graphToPrompt().then(p => { + app.graphToPrompt().then((p) => { const json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); @@ -616,34 +683,48 @@ export class ComfyUI { }); }, }), - $el("button", { id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click() }), + $el("button", { + id: "comfy-load-button", + textContent: "Load", + onclick: () => fileInput.click(), + }), $el("button", { id: "comfy-refresh-button", textContent: "Refresh", - onclick: () => app.refreshComboInNodes() + onclick: () => app.refreshComboInNodes(), }), - $el("button", { id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace() }), $el("button", { - id: "comfy-clear-button", textContent: "Clear", onclick: () => { + id: "comfy-clipspace-button", + textContent: "Clipspace", + onclick: () => app.openClipspace(), + }), + $el("button", { + id: "comfy-clear-button", + textContent: "Clear", + onclick: () => { if (!confirmClear.value || confirm("Clear workflow?")) { app.clean(); app.graph.clear(); app.resetView(); } - } + }, }), $el("button", { - id: "comfy-load-default-button", textContent: "Load Default", onclick: async () => { + id: "comfy-load-default-button", + textContent: "Load Default", + onclick: async () => { if (!confirmClear.value || confirm("Load default workflow?")) { app.resetView(); - await app.loadGraphData() + await app.loadGraphData(); } - } + }, }), $el("button", { - id: "comfy-reset-view-button", textContent: "Reset View", onclick: async () => { + id: "comfy-reset-view-button", + textContent: "Reset View", + onclick: async () => { app.resetView(); - } + }, }), ]) as HTMLDivElement; @@ -652,7 +733,10 @@ export class ComfyUI { name: "Enable Dev mode Options", type: "boolean", defaultValue: false, - onChange: function (value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "flex" : "none" }, + onChange: function (value) { + document.getElementById("comfy-dev-save-api-button").style.display = + value ? "flex" : "none"; + }, }); // @ts-ignore @@ -662,7 +746,8 @@ export class ComfyUI { } setStatus(status) { - this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR"); + this.queueSize.textContent = + "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR"); if (status) { if ( this.lastQueueSize != 0 && diff --git a/src/scripts/ui/components/asyncDialog.js b/src/scripts/ui/components/asyncDialog.js index 7ef93c8cc..4c22afd21 100644 --- a/src/scripts/ui/components/asyncDialog.js +++ b/src/scripts/ui/components/asyncDialog.js @@ -2,63 +2,63 @@ import { ComfyDialog } from "../dialog"; import { $el } from "../../ui"; export class ComfyAsyncDialog extends ComfyDialog { - #resolve; + #resolve; - constructor(actions) { - super( - "dialog.comfy-dialog.comfyui-dialog", - actions?.map((opt) => { - if (typeof opt === "string") { - opt = { text: opt }; - } - return $el("button.comfyui-button", { - type: "button", - textContent: opt.text, - onclick: () => this.close(opt.value ?? opt.text), - }); - }) - ); - } + constructor(actions) { + super( + "dialog.comfy-dialog.comfyui-dialog", + actions?.map((opt) => { + if (typeof opt === "string") { + opt = { text: opt }; + } + return $el("button.comfyui-button", { + type: "button", + textContent: opt.text, + onclick: () => this.close(opt.value ?? opt.text), + }); + }) + ); + } - show(html) { - this.element.addEventListener("close", () => { - this.close(); - }); + show(html) { + this.element.addEventListener("close", () => { + this.close(); + }); - super.show(html); + super.show(html); - return new Promise((resolve) => { - this.#resolve = resolve; - }); - } + return new Promise((resolve) => { + this.#resolve = resolve; + }); + } - showModal(html) { - this.element.addEventListener("close", () => { - this.close(); - }); + showModal(html) { + this.element.addEventListener("close", () => { + this.close(); + }); - super.show(html); - this.element.showModal(); + super.show(html); + this.element.showModal(); - return new Promise((resolve) => { - this.#resolve = resolve; - }); - } + return new Promise((resolve) => { + this.#resolve = resolve; + }); + } - close(result = null) { - this.#resolve(result); - this.element.close(); - super.close(); - } + close(result = null) { + this.#resolve(result); + this.element.close(); + super.close(); + } - static async prompt({ title = null, message, actions }) { - const dialog = new ComfyAsyncDialog(actions); - const content = [$el("span", message)]; - if (title) { - content.unshift($el("h3", title)); - } - const res = await dialog.showModal(content); - dialog.element.remove(); - return res; - } + static async prompt({ title = null, message, actions }) { + const dialog = new ComfyAsyncDialog(actions); + const content = [$el("span", message)]; + if (title) { + content.unshift($el("h3", title)); + } + const res = await dialog.showModal(content); + dialog.element.remove(); + return res; + } } diff --git a/src/scripts/ui/components/button.js b/src/scripts/ui/components/button.js index 9ee572daf..e3749731b 100644 --- a/src/scripts/ui/components/button.js +++ b/src/scripts/ui/components/button.js @@ -19,145 +19,159 @@ import { prop } from "../../utils"; * }} ComfyButtonProps */ export class ComfyButton { - #over = 0; - #popupOpen = false; - isOver = false; - iconElement = $el("i.mdi"); - contentElement = $el("span"); - /** - * @type {import("./popup").ComfyPopup} - */ - popup; + #over = 0; + #popupOpen = false; + isOver = false; + iconElement = $el("i.mdi"); + contentElement = $el("span"); + /** + * @type {import("./popup").ComfyPopup} + */ + popup; - /** - * @param {ComfyButtonProps} opts - */ - constructor({ - icon, - overIcon, - iconSize, - content, - tooltip, - action, - classList = "comfyui-button", - visibilitySetting, - app, - enabled = true, - }) { - this.element = $el("button", { - onmouseenter: () => { - this.isOver = true; - if(this.overIcon) { - this.updateIcon(); - } - }, - onmouseleave: () => { - this.isOver = false; - if(this.overIcon) { - this.updateIcon(); - } - } + /** + * @param {ComfyButtonProps} opts + */ + constructor({ + icon, + overIcon, + iconSize, + content, + tooltip, + action, + classList = "comfyui-button", + visibilitySetting, + app, + enabled = true, + }) { + this.element = $el( + "button", + { + onmouseenter: () => { + this.isOver = true; + if (this.overIcon) { + this.updateIcon(); + } + }, + onmouseleave: () => { + this.isOver = false; + if (this.overIcon) { + this.updateIcon(); + } + }, + }, + [this.iconElement, this.contentElement] + ); - }, [this.iconElement, this.contentElement]); + this.icon = prop( + this, + "icon", + icon, + toggleElement(this.iconElement, { onShow: this.updateIcon }) + ); + this.overIcon = prop(this, "overIcon", overIcon, () => { + if (this.isOver) { + this.updateIcon(); + } + }); + this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon); + this.content = prop( + this, + "content", + content, + toggleElement(this.contentElement, { + onShow: (el, v) => { + if (typeof v === "string") { + el.textContent = v; + } else { + el.replaceChildren(v); + } + }, + }) + ); - this.icon = prop(this, "icon", icon, toggleElement(this.iconElement, { onShow: this.updateIcon })); - this.overIcon = prop(this, "overIcon", overIcon, () => { - if(this.isOver) { - this.updateIcon(); - } - }); - this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon); - this.content = prop( - this, - "content", - content, - toggleElement(this.contentElement, { - onShow: (el, v) => { - if (typeof v === "string") { - el.textContent = v; - } else { - el.replaceChildren(v); - } - }, - }) - ); + this.tooltip = prop(this, "tooltip", tooltip, (v) => { + if (v) { + this.element.title = v; + } else { + this.element.removeAttribute("title"); + } + }); + this.classList = prop(this, "classList", classList, this.updateClasses); + this.hidden = prop(this, "hidden", false, this.updateClasses); + this.enabled = prop(this, "enabled", enabled, () => { + this.updateClasses(); + this.element.disabled = !this.enabled; + }); + this.action = prop(this, "action", action); + this.element.addEventListener("click", (e) => { + if (this.popup) { + // we are either a touch device or triggered by click not hover + if (!this.#over) { + this.popup.toggle(); + } + } + this.action?.(e, this); + }); - this.tooltip = prop(this, "tooltip", tooltip, (v) => { - if (v) { - this.element.title = v; - } else { - this.element.removeAttribute("title"); - } - }); - this.classList = prop(this, "classList", classList, this.updateClasses); - this.hidden = prop(this, "hidden", false, this.updateClasses); - this.enabled = prop(this, "enabled", enabled, () => { - this.updateClasses(); - this.element.disabled = !this.enabled; - }); - this.action = prop(this, "action", action); - this.element.addEventListener("click", (e) => { - if (this.popup) { - // we are either a touch device or triggered by click not hover - if (!this.#over) { - this.popup.toggle(); - } - } - this.action?.(e, this); - }); + if (visibilitySetting?.id) { + const settingUpdated = () => { + this.hidden = + app.ui.settings.getSettingValue(visibilitySetting.id) !== + visibilitySetting.showValue; + }; + app.ui.settings.addEventListener( + visibilitySetting.id + ".change", + settingUpdated + ); + settingUpdated(); + } + } - if (visibilitySetting?.id) { - const settingUpdated = () => { - this.hidden = app.ui.settings.getSettingValue(visibilitySetting.id) !== visibilitySetting.showValue; - }; - app.ui.settings.addEventListener(visibilitySetting.id + ".change", settingUpdated); - settingUpdated(); - } - } + updateIcon = () => + (this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`); + updateClasses = () => { + const internalClasses = []; + if (this.hidden) { + internalClasses.push("hidden"); + } + if (!this.enabled) { + internalClasses.push("disabled"); + } + if (this.popup) { + if (this.#popupOpen) { + internalClasses.push("popup-open"); + } else { + internalClasses.push("popup-closed"); + } + } + applyClasses(this.element, this.classList, ...internalClasses); + }; - updateIcon = () => (this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`); - updateClasses = () => { - const internalClasses = []; - if (this.hidden) { - internalClasses.push("hidden"); - } - if (!this.enabled) { - internalClasses.push("disabled"); - } - if (this.popup) { - if (this.#popupOpen) { - internalClasses.push("popup-open"); - } else { - internalClasses.push("popup-closed"); - } - } - applyClasses(this.element, this.classList, ...internalClasses); - }; + /** + * + * @param { import("./popup").ComfyPopup } popup + * @param { "click" | "hover" } mode + */ + withPopup(popup, mode = "click") { + this.popup = popup; - /** - * - * @param { import("./popup").ComfyPopup } popup - * @param { "click" | "hover" } mode - */ - withPopup(popup, mode = "click") { - this.popup = popup; + if (mode === "hover") { + for (const el of [this.element, this.popup.element]) { + el.addEventListener("mouseenter", () => { + this.popup.open = !!++this.#over; + }); + el.addEventListener("mouseleave", () => { + this.popup.open = !!--this.#over; + }); + } + } - if (mode === "hover") { - for (const el of [this.element, this.popup.element]) { - el.addEventListener("mouseenter", () => { - this.popup.open = !!++this.#over; - }); - el.addEventListener("mouseleave", () => { - this.popup.open = !!--this.#over; - }); - } - } + popup.addEventListener("change", () => { + this.#popupOpen = popup.open; + this.updateClasses(); + }); - popup.addEventListener("change", () => { - this.#popupOpen = popup.open; - this.updateClasses(); - }); - - return this; - } + return this; + } } diff --git a/src/scripts/ui/components/buttonGroup.js b/src/scripts/ui/components/buttonGroup.js index 788c67f11..335c70910 100644 --- a/src/scripts/ui/components/buttonGroup.js +++ b/src/scripts/ui/components/buttonGroup.js @@ -5,41 +5,41 @@ import { ComfyButton } from "./button"; import { prop } from "../../utils"; export class ComfyButtonGroup { - element = $el("div.comfyui-button-group"); + element = $el("div.comfyui-button-group"); - /** @param {Array} buttons */ - constructor(...buttons) { - this.buttons = prop(this, "buttons", buttons, () => this.update()); - } + /** @param {Array} buttons */ + constructor(...buttons) { + this.buttons = prop(this, "buttons", buttons, () => this.update()); + } - /** - * @param {ComfyButton} button - * @param {number} index - */ - insert(button, index) { - this.buttons.splice(index, 0, button); - this.update(); - } + /** + * @param {ComfyButton} button + * @param {number} index + */ + insert(button, index) { + this.buttons.splice(index, 0, button); + this.update(); + } - /** @param {ComfyButton} button */ - append(button) { - this.buttons.push(button); - this.update(); - } + /** @param {ComfyButton} button */ + append(button) { + this.buttons.push(button); + this.update(); + } - /** @param {ComfyButton|number} indexOrButton */ - remove(indexOrButton) { - if (typeof indexOrButton !== "number") { - indexOrButton = this.buttons.indexOf(indexOrButton); - } - if (indexOrButton > -1) { - const r = this.buttons.splice(indexOrButton, 1); - this.update(); - return r; - } - } + /** @param {ComfyButton|number} indexOrButton */ + remove(indexOrButton) { + if (typeof indexOrButton !== "number") { + indexOrButton = this.buttons.indexOf(indexOrButton); + } + if (indexOrButton > -1) { + const r = this.buttons.splice(indexOrButton, 1); + this.update(); + return r; + } + } - update() { - this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b)); - } + update() { + this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b)); + } } diff --git a/src/scripts/ui/components/popup.js b/src/scripts/ui/components/popup.js index dd86340d9..e02e9620b 100644 --- a/src/scripts/ui/components/popup.js +++ b/src/scripts/ui/components/popup.js @@ -5,124 +5,133 @@ import { $el } from "../../ui"; import { applyClasses } from "../utils"; export class ComfyPopup extends EventTarget { - element = $el("div.comfyui-popup"); + element = $el("div.comfyui-popup"); - /** - * @param {{ - * target: HTMLElement, - * container?: HTMLElement, - * classList?: import("../utils").ClassList, - * ignoreTarget?: boolean, - * closeOnEscape?: boolean, - * position?: "absolute" | "relative", - * horizontal?: "left" | "right" - * }} param0 - * @param {...HTMLElement} children - */ - constructor( - { - target, - container = document.body, - classList = "", - ignoreTarget = true, - closeOnEscape = true, - position = "absolute", - horizontal = "left", - }, - ...children - ) { - super(); - this.target = target; - this.ignoreTarget = ignoreTarget; - this.container = container; - this.position = position; - this.closeOnEscape = closeOnEscape; - this.horizontal = horizontal; + /** + * @param {{ + * target: HTMLElement, + * container?: HTMLElement, + * classList?: import("../utils").ClassList, + * ignoreTarget?: boolean, + * closeOnEscape?: boolean, + * position?: "absolute" | "relative", + * horizontal?: "left" | "right" + * }} param0 + * @param {...HTMLElement} children + */ + constructor( + { + target, + container = document.body, + classList = "", + ignoreTarget = true, + closeOnEscape = true, + position = "absolute", + horizontal = "left", + }, + ...children + ) { + super(); + this.target = target; + this.ignoreTarget = ignoreTarget; + this.container = container; + this.position = position; + this.closeOnEscape = closeOnEscape; + this.horizontal = horizontal; - container.append(this.element); + container.append(this.element); - this.children = prop(this, "children", children, () => { - this.element.replaceChildren(...this.children); - this.update(); - }); - this.classList = prop(this, "classList", classList, () => applyClasses(this.element, this.classList, "comfyui-popup", horizontal)); - this.open = prop(this, "open", false, (v, o) => { - if (v === o) return; - if (v) { - this.#show(); - } else { - this.#hide(); - } - }); - } + this.children = prop(this, "children", children, () => { + this.element.replaceChildren(...this.children); + this.update(); + }); + this.classList = prop(this, "classList", classList, () => + applyClasses(this.element, this.classList, "comfyui-popup", horizontal) + ); + this.open = prop(this, "open", false, (v, o) => { + if (v === o) return; + if (v) { + this.#show(); + } else { + this.#hide(); + } + }); + } - toggle() { - this.open = !this.open; - } + toggle() { + this.open = !this.open; + } - #hide() { - this.element.classList.remove("open"); - window.removeEventListener("resize", this.update); - window.removeEventListener("click", this.#clickHandler, { capture: true }); - window.removeEventListener("keydown", this.#escHandler, { capture: true }); + #hide() { + this.element.classList.remove("open"); + window.removeEventListener("resize", this.update); + window.removeEventListener("click", this.#clickHandler, { capture: true }); + window.removeEventListener("keydown", this.#escHandler, { capture: true }); - this.dispatchEvent(new CustomEvent("close")); - this.dispatchEvent(new CustomEvent("change")); - } + this.dispatchEvent(new CustomEvent("close")); + this.dispatchEvent(new CustomEvent("change")); + } - #show() { - this.element.classList.add("open"); - this.update(); + #show() { + this.element.classList.add("open"); + this.update(); - window.addEventListener("resize", this.update); - window.addEventListener("click", this.#clickHandler, { capture: true }); - if (this.closeOnEscape) { - window.addEventListener("keydown", this.#escHandler, { capture: true }); - } + window.addEventListener("resize", this.update); + window.addEventListener("click", this.#clickHandler, { capture: true }); + if (this.closeOnEscape) { + window.addEventListener("keydown", this.#escHandler, { capture: true }); + } - this.dispatchEvent(new CustomEvent("open")); - this.dispatchEvent(new CustomEvent("change")); - } + this.dispatchEvent(new CustomEvent("open")); + this.dispatchEvent(new CustomEvent("change")); + } - #escHandler = (e) => { - if (e.key === "Escape") { - this.open = false; - e.preventDefault(); - e.stopImmediatePropagation(); - } - }; + #escHandler = (e) => { + if (e.key === "Escape") { + this.open = false; + e.preventDefault(); + e.stopImmediatePropagation(); + } + }; - #clickHandler = (e) => { - /** @type {any} */ - const target = e.target; - if (!this.element.contains(target) && this.ignoreTarget && !this.target.contains(target)) { - this.open = false; - } - }; + #clickHandler = (e) => { + /** @type {any} */ + const target = e.target; + if ( + !this.element.contains(target) && + this.ignoreTarget && + !this.target.contains(target) + ) { + this.open = false; + } + }; - update = () => { - const rect = this.target.getBoundingClientRect(); - this.element.style.setProperty("--bottom", "unset"); - if (this.position === "absolute") { - if (this.horizontal === "left") { - this.element.style.setProperty("--left", rect.left + "px"); - } else { - this.element.style.setProperty("--left", rect.right - this.element.clientWidth + "px"); - } - this.element.style.setProperty("--top", rect.bottom + "px"); - this.element.style.setProperty("--limit", rect.bottom + "px"); - } else { - this.element.style.setProperty("--left", 0 + "px"); - this.element.style.setProperty("--top", rect.height + "px"); - this.element.style.setProperty("--limit", rect.height + "px"); - } + update = () => { + const rect = this.target.getBoundingClientRect(); + this.element.style.setProperty("--bottom", "unset"); + if (this.position === "absolute") { + if (this.horizontal === "left") { + this.element.style.setProperty("--left", rect.left + "px"); + } else { + this.element.style.setProperty( + "--left", + rect.right - this.element.clientWidth + "px" + ); + } + this.element.style.setProperty("--top", rect.bottom + "px"); + this.element.style.setProperty("--limit", rect.bottom + "px"); + } else { + this.element.style.setProperty("--left", 0 + "px"); + this.element.style.setProperty("--top", rect.height + "px"); + this.element.style.setProperty("--limit", rect.height + "px"); + } - const thisRect = this.element.getBoundingClientRect(); - if (thisRect.height < 30) { - // Move up instead - this.element.style.setProperty("--top", "unset"); - this.element.style.setProperty("--bottom", rect.height + 5 + "px"); - this.element.style.setProperty("--limit", rect.height + 5 + "px"); - } - }; + const thisRect = this.element.getBoundingClientRect(); + if (thisRect.height < 30) { + // Move up instead + this.element.style.setProperty("--top", "unset"); + this.element.style.setProperty("--bottom", rect.height + 5 + "px"); + this.element.style.setProperty("--limit", rect.height + 5 + "px"); + } + }; } diff --git a/src/scripts/ui/components/splitButton.js b/src/scripts/ui/components/splitButton.js index 84b16e2c0..789063350 100644 --- a/src/scripts/ui/components/splitButton.js +++ b/src/scripts/ui/components/splitButton.js @@ -6,38 +6,47 @@ import { prop } from "../../utils"; import { ComfyPopup } from "./popup"; export class ComfySplitButton { - /** - * @param {{ - * primary: ComfyButton, - * mode?: "hover" | "click", - * horizontal?: "left" | "right", - * position?: "relative" | "absolute" - * }} param0 - * @param {Array | Array} items - */ - constructor({ primary, mode, horizontal = "left", position = "relative" }, ...items) { - this.arrow = new ComfyButton({ - icon: "chevron-down", - }); - this.element = $el("div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""), [ - $el("div.comfyui-split-primary", primary.element), - $el("div.comfyui-split-arrow", this.arrow.element), - ]); - this.popup = new ComfyPopup({ - target: this.element, - container: position === "relative" ? this.element : document.body, - classList: "comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""), - closeOnEscape: mode === "click", - position, - horizontal, - }); + /** + * @param {{ + * primary: ComfyButton, + * mode?: "hover" | "click", + * horizontal?: "left" | "right", + * position?: "relative" | "absolute" + * }} param0 + * @param {Array | Array} items + */ + constructor( + { primary, mode, horizontal = "left", position = "relative" }, + ...items + ) { + this.arrow = new ComfyButton({ + icon: "chevron-down", + }); + this.element = $el( + "div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""), + [ + $el("div.comfyui-split-primary", primary.element), + $el("div.comfyui-split-arrow", this.arrow.element), + ] + ); + this.popup = new ComfyPopup({ + target: this.element, + container: position === "relative" ? this.element : document.body, + classList: + "comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""), + closeOnEscape: mode === "click", + position, + horizontal, + }); - this.arrow.withPopup(this.popup, mode); + this.arrow.withPopup(this.popup, mode); - this.items = prop(this, "items", items, () => this.update()); - } + this.items = prop(this, "items", items, () => this.update()); + } - update() { - this.popup.element.replaceChildren(...this.items.map((b) => b.element ?? b)); - } + update() { + this.popup.element.replaceChildren( + ...this.items.map((b) => b.element ?? b) + ); + } } diff --git a/src/scripts/ui/dialog.ts b/src/scripts/ui/dialog.ts index 8a90d2555..5bbe046b2 100644 --- a/src/scripts/ui/dialog.ts +++ b/src/scripts/ui/dialog.ts @@ -1,6 +1,8 @@ import { $el } from "../ui"; -export class ComfyDialog extends EventTarget { +export class ComfyDialog< + T extends HTMLElement = HTMLElement, +> extends EventTarget { element: T; textElement: HTMLElement; #buttons: HTMLButtonElement[] | null; @@ -9,7 +11,10 @@ export class ComfyDialog extends EventTarge super(); this.#buttons = buttons; this.element = $el(type + ".comfy-modal", { parent: document.body }, [ - $el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]), + $el("div.comfy-modal-content", [ + $el("p", { $: (p) => (this.textElement = p) }), + ...this.createButtons(), + ]), ]) as T; } @@ -33,7 +38,9 @@ export class ComfyDialog extends EventTarge if (typeof html === "string") { this.textElement.innerHTML = html; } else { - this.textElement.replaceChildren(...(html instanceof Array ? html : [html])); + this.textElement.replaceChildren( + ...(html instanceof Array ? html : [html]) + ); } this.element.style.display = "flex"; } diff --git a/src/scripts/ui/draggableList.ts b/src/scripts/ui/draggableList.ts index 81c26c34e..758be8b94 100644 --- a/src/scripts/ui/draggableList.ts +++ b/src/scripts/ui/draggableList.ts @@ -27,8 +27,8 @@ import { $el } from "../ui"; $el("style", { - parent: document.head, - textContent: ` + parent: document.head, + textContent: ` .draggable-item { position: relative; will-change: transform; @@ -40,7 +40,7 @@ $el("style", { .draggable-item.is-draggable { z-index: 10; } - ` + `, }); export class DraggableList extends EventTarget { @@ -57,9 +57,9 @@ export class DraggableList extends EventTarget { offDrag = []; constructor(element, itemSelector) { - super(); + super(); this.listContainer = element; - this.itemSelector = itemSelector; + this.itemSelector = itemSelector; if (!this.listContainer) return; @@ -71,7 +71,9 @@ export class DraggableList extends EventTarget { getAllItems() { if (!this.items?.length) { - this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector)); + this.items = Array.from( + this.listContainer.querySelectorAll(this.itemSelector) + ); this.items.forEach((element) => { element.classList.add("is-idle"); }); @@ -80,7 +82,9 @@ export class DraggableList extends EventTarget { } getIdleItems() { - return this.getAllItems().filter((item) => item.classList.contains("is-idle")); + return this.getAllItems().filter((item) => + item.classList.contains("is-idle") + ); } isItemAbove(item) { @@ -106,18 +110,24 @@ export class DraggableList extends EventTarget { this.pointerStartX = e.clientX || e.touches[0].clientX; this.pointerStartY = e.clientY || e.touches[0].clientY; - this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight; + this.scrollYMax = + this.listContainer.scrollHeight - this.listContainer.clientHeight; this.setItemsGap(); this.initDraggableItem(); this.initItemsState(); this.offDrag.push(this.on(document, "mousemove", this.drag)); - this.offDrag.push(this.on(document, "touchmove", this.drag, { passive: false })); + this.offDrag.push( + this.on(document, "touchmove", this.drag, { passive: false }) + ); this.dispatchEvent( new CustomEvent("dragstart", { - detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) }, + detail: { + element: this.draggableItem, + position: this.getAllItems().indexOf(this.draggableItem), + }, }) ); } @@ -250,7 +260,11 @@ export class DraggableList extends EventTarget { this.dispatchEvent( new CustomEvent("dragend", { - detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) }, + detail: { + element: this.draggableItem, + oldPosition, + newPosition: reorderedItems.indexOf(this.draggableItem), + }, }) ); } diff --git a/src/scripts/ui/imagePreview.ts b/src/scripts/ui/imagePreview.ts index 0f20f6729..024244c35 100644 --- a/src/scripts/ui/imagePreview.ts +++ b/src/scripts/ui/imagePreview.ts @@ -57,7 +57,11 @@ export function createImageHost(node) { } const nw = node.size[0]; - ({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH)); + ({ cellWidth: w, cellHeight: h } = calculateImageGrid( + currentImgs, + nw - 20, + elH + )); w += "px"; h += "px"; @@ -86,10 +90,13 @@ export function createImageHost(node) { onDraw() { // Element from point uses a hittest find elements so we need to toggle pointer events el.style.pointerEvents = "all"; - const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]); + const over = document.elementFromPoint( + app.canvas.mouse[0], + app.canvas.mouse[1] + ); el.style.pointerEvents = "none"; - if(!over) return; + if (!over) return; // Set the overIndex so Open Image etc work const idx = currentImgs.indexOf(over); node.overIndex = idx; diff --git a/src/scripts/ui/menu/index.js b/src/scripts/ui/menu/index.js index ddaf861cb..e708d0884 100644 --- a/src/scripts/ui/menu/index.js +++ b/src/scripts/ui/menu/index.js @@ -48,7 +48,11 @@ export class ComfyAppMenu { content: t, }); - this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI"); + this.logo = $el( + "h1.comfyui-logo.nlg-hide", + { title: "ComfyUI" }, + "ComfyUI" + ); this.saveButton = new ComfySplitButton( { primary: getSaveButton(), @@ -71,7 +75,8 @@ export class ComfyAppMenu { new ComfyButton({ icon: "api", content: "Export (API Format)", - tooltip: "Export the current workflow as JSON for use with the ComfyUI API", + tooltip: + "Export the current workflow as JSON for use with the ComfyUI API", action: () => this.exportWorkflow("workflow_api", "output"), visibilitySetting: { id: "Comfy.DevMode", showValue: true }, app, @@ -101,7 +106,10 @@ export class ComfyAppMenu { content: "Clear", tooltip: "Clears current workflow", action: () => { - if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) { + if ( + !app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || + confirm("Clear workflow?") + ) { app.clean(); app.graph.clear(); } @@ -126,7 +134,9 @@ export class ComfyAppMenu { this.mobileMenuButton = new ComfyButton({ icon: "menu", action: (_, btn) => { - btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu"; + btn.icon = this.element.classList.toggle("expanded") + ? "menu-open" + : "menu"; window.dispatchEvent(new Event("resize")); }, classList: "comfyui-button comfyui-menu-button", @@ -239,7 +249,10 @@ export class ComfyAppMenu { idx--; } } else if (innerSize > this.element.clientWidth) { - this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize); + this.#lastSizeBreaks[this.#sizeBreak] = Math.max( + window.innerWidth, + innerSize + ); // We need to shrink if (idx < this.#sizeBreaks.length - 1) { idx++; @@ -254,19 +267,26 @@ export class ComfyAppMenu { clearTimeout(this.#cacheTimeout); if (this.#cachedInnerSize) { // Extend cache time - this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100); + this.#cacheTimeout = setTimeout( + () => (this.#cachedInnerSize = null), + 100 + ); } else { let innerSize = 0; let count = 1; for (const c of this.element.children) { if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push - if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue; // ignore collapse items + if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) + continue; // ignore collapse items innerSize += c.clientWidth; count++; } innerSize += 8 * count; this.#cachedInnerSize = innerSize; - this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100); + this.#cacheTimeout = setTimeout( + () => (this.#cachedInnerSize = null), + 100 + ); } return this.#cachedInnerSize; } diff --git a/src/scripts/ui/menu/queueButton.js b/src/scripts/ui/menu/queueButton.js index 48501aa31..ad204f09b 100644 --- a/src/scripts/ui/menu/queueButton.js +++ b/src/scripts/ui/menu/queueButton.js @@ -14,7 +14,10 @@ export class ComfyQueueButton { queuePrompt = async (e) => { this.#internalQueueSize += this.queueOptions.batchCount; // Hold shift to queue front, event is undefined when auto-queue is enabled - await this.app.queuePrompt(e?.shiftKey ? -1 : 0, this.queueOptions.batchCount); + await this.app.queuePrompt( + e?.shiftKey ? -1 : 0, + this.queueOptions.batchCount + ); }; constructor(app) { @@ -63,7 +66,10 @@ export class ComfyQueueButton { } }); - this.queueOptions.addEventListener("autoQueueMode", (e) => (this.autoQueueMode = e["detail"])); + this.queueOptions.addEventListener( + "autoQueueMode", + (e) => (this.autoQueueMode = e["detail"]) + ); api.addEventListener("graphChanged", () => { if (this.autoQueueMode === "change") { @@ -79,10 +85,14 @@ export class ComfyQueueButton { api.addEventListener("status", ({ detail }) => { this.#internalQueueSize = detail?.exec_info?.queue_remaining; if (this.#internalQueueSize != null) { - this.queueSizeElement.textContent = this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + ""; + this.queueSizeElement.textContent = + this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + ""; this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`; if (!this.#internalQueueSize && !app.lastExecutionError) { - if (this.autoQueueMode === "instant" || (this.autoQueueMode === "change" && this.graphHasChanged)) { + if ( + this.autoQueueMode === "instant" || + (this.autoQueueMode === "change" && this.graphHasChanged) + ) { this.graphHasChanged = false; this.queuePrompt(); } diff --git a/src/scripts/ui/menu/viewList.js b/src/scripts/ui/menu/viewList.js index 69d307277..07ad1b879 100644 --- a/src/scripts/ui/menu/viewList.js +++ b/src/scripts/ui/menu/viewList.js @@ -69,11 +69,14 @@ export class ComfyViewList { }, }); - this.element = $el(`div.comfyui-${this.type}-popup.comfyui-view-list-popup`, [ - $el("h3", mode), - $el("header", [this.clear.element, this.refresh.element]), - this.items, - ]); + this.element = $el( + `div.comfyui-${this.type}-popup.comfyui-view-list-popup`, + [ + $el("h3", mode), + $el("header", [this.clear.element, this.refresh.element]), + this.items, + ] + ); api.addEventListener("status", () => { if (this.popup.open) { @@ -155,7 +158,9 @@ export class ComfyViewList { text: "Load", action: async () => { try { - await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); + await this.app.loadGraphData( + item.prompt[3].extra_pnginfo.workflow + ); if (item.outputs) { this.app.nodeOutputs = item.outputs; } diff --git a/src/scripts/ui/menu/viewQueue.js b/src/scripts/ui/menu/viewQueue.js index 945b040df..85df4d31a 100644 --- a/src/scripts/ui/menu/viewQueue.js +++ b/src/scripts/ui/menu/viewQueue.js @@ -31,7 +31,9 @@ export class ComfyViewQueueList extends ComfyViewList { text: "Load", action: async () => { try { - await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); + await this.app.loadGraphData( + item.prompt[3].extra_pnginfo.workflow + ); if (item.outputs) { this.app.nodeOutputs = item.outputs; } @@ -51,5 +53,5 @@ export class ComfyViewQueueList extends ComfyViewList { }, ], }; - } + }; } diff --git a/src/scripts/ui/menu/workflows.js b/src/scripts/ui/menu/workflows.js index ed3a56d89..a500fa34f 100644 --- a/src/scripts/ui/menu/workflows.js +++ b/src/scripts/ui/menu/workflows.js @@ -37,14 +37,21 @@ export class ComfyWorkflowsMenu { this.buttonProgress = $el("div.comfyui-workflows-button-progress"); this.workflowLabel = $el("span.comfyui-workflows-label", ""); this.button = new ComfyButton({ - content: $el("div.comfyui-workflows-button-inner", [$el("i.mdi.mdi-graph"), this.workflowLabel, this.buttonProgress]), + content: $el("div.comfyui-workflows-button-inner", [ + $el("i.mdi.mdi-graph"), + this.workflowLabel, + this.buttonProgress, + ]), icon: "chevron-down", classList, }); this.element.append(this.button.element); - this.popup = new ComfyPopup({ target: this.element, classList: "comfyui-workflows-popup" }); + this.popup = new ComfyPopup({ + target: this.element, + classList: "comfyui-workflows-popup", + }); this.content = new ComfyWorkflowsContent(app, this.popup); this.popup.children = [this.content.element]; this.popup.addEventListener("change", () => { @@ -85,7 +92,10 @@ export class ComfyWorkflowsMenu { }; #bindEvents() { - this.app.workflowManager.addEventListener("changeWorkflow", this.#updateActive); + this.app.workflowManager.addEventListener( + "changeWorkflow", + this.#updateActive + ); this.app.workflowManager.addEventListener("rename", this.#updateActive); this.app.workflowManager.addEventListener("delete", this.#updateActive); @@ -157,10 +167,15 @@ export class ComfyWorkflowsMenu { name: "Comfy.Workflows", async beforeRegisterNodeDef(nodeType) { function getImageWidget(node) { - const inputs = { ...node.constructor?.nodeData?.input?.required, ...node.constructor?.nodeData?.input?.optional }; + const inputs = { + ...node.constructor?.nodeData?.input?.required, + ...node.constructor?.nodeData?.input?.optional, + }; for (const input in inputs) { if (inputs[input][0] === "IMAGEUPLOAD") { - const imageWidget = node.widgets.find((w) => w.name === (inputs[input]?.[1]?.widget ?? "image")); + const imageWidget = node.widgets.find( + (w) => w.name === (inputs[input]?.[1]?.widget ?? "image") + ); if (imageWidget) return imageWidget; } } @@ -213,8 +228,13 @@ export class ComfyWorkflowsMenu { const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"]; nodeType.prototype["getExtraMenuOptions"] = function (_, options) { const r = getExtraMenuOptions?.apply?.(this, arguments); - if (app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true) { - const t = /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (this); + if ( + app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true + ) { + const t = + /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ ( + this + ); let img; if (t.imageIndex != null) { // An image is selected so select that @@ -238,10 +258,13 @@ export class ComfyWorkflowsMenu { submenu: { options: [ { - callback: () => sendToWorkflow(img, app.workflowManager.activeWorkflow), + callback: () => + sendToWorkflow(img, app.workflowManager.activeWorkflow), title: "[Current workflow]", }, - ...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)), + ...self.#getFavoriteMenuOptions( + sendToWorkflow.bind(null, img) + ), null, ...self.#getMenuOptions(sendToWorkflow.bind(null, img)), ], @@ -315,7 +338,9 @@ export class ComfyWorkflowsContent { this.element.replaceChildren(this.actions, this.spinner); this.popup.addEventListener("open", () => this.load()); - this.popup.addEventListener("close", () => this.element.replaceChildren(this.actions, this.spinner)); + this.popup.addEventListener("close", () => + this.element.replaceChildren(this.actions, this.spinner) + ); this.app.workflowManager.addEventListener("favorite", (e) => { const workflow = e["detail"]; @@ -331,7 +356,9 @@ export class ComfyWorkflowsContent { app.workflowManager.addEventListener(e, () => this.updateOpen()); } this.app.workflowManager.addEventListener("rename", () => this.load()); - this.app.workflowManager.addEventListener("execute", (e) => this.#updateActive()); + this.app.workflowManager.addEventListener("execute", (e) => + this.#updateActive() + ); } async load() { @@ -339,7 +366,12 @@ export class ComfyWorkflowsContent { this.updateTree(); this.updateFavorites(); this.updateOpen(); - this.element.replaceChildren(this.actions, this.openElement, this.favoritesElement, this.treeElement); + this.element.replaceChildren( + this.actions, + this.openElement, + this.favoritesElement, + this.treeElement + ); } updateOpen() { @@ -368,7 +400,7 @@ export class ComfyWorkflowsContent { if (w.unsaved) { wrapper.element.classList.add("unsaved"); } - if(w === this.app.workflowManager.activeWorkflow) { + if (w === this.app.workflowManager.activeWorkflow) { wrapper.element.classList.add("active"); } @@ -383,7 +415,9 @@ export class ComfyWorkflowsContent { updateFavorites() { const current = this.favoritesElement; - const favorites = [...this.app.workflowManager.workflows.filter((w) => w.isFavorite)]; + const favorites = [ + ...this.app.workflowManager.workflows.filter((w) => w.isFavorite), + ]; this.favoritesElement = $el("div.comfyui-workflows-favorites", [ $el("h3", "Favorites"), @@ -437,7 +471,10 @@ export class ComfyWorkflowsContent { hideTreeParents(element) { // Hide all parents if no children are visible - if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) { + if ( + element.parentElement?.classList.contains("comfyui-workflows-tree") === + false + ) { for (let i = 1; i < element.parentElement.children.length; i++) { const c = element.parentElement.children[i]; if (c.style.display !== "none") { @@ -450,7 +487,10 @@ export class ComfyWorkflowsContent { } showTreeParents(element) { - if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) { + if ( + element.parentElement?.classList.contains("comfyui-workflows-tree") === + false + ) { element.parentElement.style.removeProperty("display"); this.showTreeParents(element.parentElement); } @@ -490,7 +530,9 @@ export class ComfyWorkflowsContent { for (let i = 0; i < workflow.pathParts.length; i++) { currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i]; - const parentNode = nodes[currentPath] ?? this.#createNode(currentPath, workflow, i, currentRoot); + const parentNode = + nodes[currentPath] ?? + this.#createNode(currentPath, workflow, i, currentRoot); nodes[currentPath] = parentNode; currentRoot = parentNode; @@ -559,7 +601,9 @@ export class ComfyWorkflowsContent { /** @param {ComfyWorkflow} workflow */ #getFavoriteTooltip(workflow) { - return workflow.isFavorite ? "Remove this workflow from your favorites" : "Add this workflow to your favorites"; + return workflow.isFavorite + ? "Remove this workflow from your favorites" + : "Add this workflow to your favorites"; } /** @param {ComfyWorkflow} workflow */ @@ -568,7 +612,9 @@ export class ComfyWorkflowsContent { icon: this.#getFavoriteIcon(workflow), overIcon: this.#getFavoriteOverIcon(workflow), iconSize: 18, - classList: "comfyui-button comfyui-workflows-file-action-favorite" + (primary ? " comfyui-workflows-file-action-primary" : ""), + classList: + "comfyui-button comfyui-workflows-file-action-favorite" + + (primary ? " comfyui-workflows-file-action-primary" : ""), tooltip: this.#getFavoriteTooltip(workflow), action: (e) => { e.stopImmediatePropagation(); @@ -628,7 +674,9 @@ export class ComfyWorkflowsContent { #getRenameButton(workflow) { return new ComfyButton({ icon: "pencil", - tooltip: workflow.path ? "Rename this workflow" : "This workflow can't be renamed as it hasn't been saved.", + tooltip: workflow.path + ? "Rename this workflow" + : "This workflow can't be renamed as it hasn't been saved.", classList: "comfyui-button comfyui-workflows-file-action", iconSize: 18, enabled: !!workflow.path, @@ -646,7 +694,11 @@ export class ComfyWorkflowsContent { #getWorkflowElement(workflow) { return new WorkflowElement(this, workflow, { primary: this.#getFavoriteButton(workflow, true), - buttons: [this.#getInsertButton(workflow), this.#getRenameButton(workflow), this.#getDeleteButton(workflow)], + buttons: [ + this.#getInsertButton(workflow), + this.#getRenameButton(workflow), + this.#getDeleteButton(workflow), + ], }); } @@ -660,14 +712,17 @@ export class ComfyWorkflowsContent { #createNode(currentPath, workflow, i, currentRoot) { const part = workflow.pathParts[i]; - const parentNode = $el("ul" + (this.treeState[currentPath] ? "" : ".closed"), { - $: (el) => { - el.onclick = (e) => { - this.#expandNode(el, workflow, currentPath, i); - e.stopImmediatePropagation(); - }; - }, - }); + const parentNode = $el( + "ul" + (this.treeState[currentPath] ? "" : ".closed"), + { + $: (el) => { + el.onclick = (e) => { + this.#expandNode(el, workflow, currentPath, i); + e.stopImmediatePropagation(); + }; + }, + } + ); currentRoot.append(parentNode); // Create a node for the current part and an inner UL for its children if it isnt a leaf node @@ -676,7 +731,10 @@ export class ComfyWorkflowsContent { if (leaf) { nodeElement = this.#createLeafNode(workflow).element; } else { - nodeElement = $el("li", [$el("i.mdi.mdi-18px.mdi-folder"), $el("span", part)]); + nodeElement = $el("li", [ + $el("i.mdi.mdi-18px.mdi-folder"), + $el("span", part), + ]); } parentNode.append(nodeElement); return parentNode; @@ -703,7 +761,11 @@ class WorkflowElement { }, title: this.workflow.path, }, - [this.primary?.element, $el("span", workflow.name), ...buttons.map((b) => b.element)] + [ + this.primary?.element, + $el("span", workflow.name), + ...buttons.map((b) => b.element), + ] ); } } @@ -732,7 +794,11 @@ class WidgetSelectionDialog extends ComfyAsyncDialog { "section", this.#options.map((opt) => { return $el("div.comfy-widget-selection-item", [ - $el("span", { dataset: { id: opt.node.id } }, `${opt.node.title ?? opt.node.type} ${opt.widget.name}`), + $el( + "span", + { dataset: { id: opt.node.id } }, + `${opt.node.title ?? opt.node.type} ${opt.widget.name}` + ), $el( "button.comfyui-button", { @@ -760,4 +826,4 @@ class WidgetSelectionDialog extends ComfyAsyncDialog { ]) ); } -} \ No newline at end of file +} diff --git a/src/scripts/ui/settings.ts b/src/scripts/ui/settings.ts index 03687d872..ceb941947 100644 --- a/src/scripts/ui/settings.ts +++ b/src/scripts/ui/settings.ts @@ -19,7 +19,14 @@ interface SettingOption { interface SettingParams { id: string; name: string; - type: string | ((name: string, setter: (v: any) => void, value: any, attrs: any) => HTMLElement); + type: + | string + | (( + name: string, + setter: (v: any) => void, + value: any, + attrs: any + ) => HTMLElement); defaultValue: any; onChange?: (newValue: any, oldValue?: any) => void; attrs?: any; @@ -81,7 +88,7 @@ export class ComfySettingsDialog extends ComfyDialog { new CustomEvent(id + ".change", { detail: { value, - oldValue + oldValue, }, }) ); @@ -115,8 +122,7 @@ export class ComfySettingsDialog extends ComfyDialog { if (this.app.storageLocation === "browser") { try { value = JSON.parse(value); - } catch (error) { - } + } catch (error) {} } } return value ?? defaultValue; @@ -145,7 +151,16 @@ export class ComfySettingsDialog extends ComfyDialog { } addSetting(params: SettingParams) { - const { id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined } = params; + const { + id, + name, + type, + defaultValue, + onChange, + attrs = {}, + tooltip = "", + options = undefined, + } = params; if (!id) { throw new Error("Settings must have an ID"); } @@ -272,7 +287,8 @@ export class ComfySettingsDialog extends ComfyDialog { style: { maxWidth: "4rem" }, oninput: (e) => { setter(e.target.value); - e.target.previousElementSibling.value = e.target.value; + e.target.previousElementSibling.value = + e.target.value; }, }), ] @@ -291,7 +307,10 @@ export class ComfySettingsDialog extends ComfyDialog { setter(e.target.value); }, }, - (typeof options === "function" ? options(value) : options || []).map((opt) => { + (typeof options === "function" + ? options(value) + : options || [] + ).map((opt) => { if (typeof opt === "string") { opt = { text: opt }; } @@ -309,7 +328,9 @@ export class ComfySettingsDialog extends ComfyDialog { case "text": default: if (type !== "text") { - console.warn(`Unsupported setting type '${type}, defaulting to text`); + console.warn( + `Unsupported setting type '${type}, defaulting to text` + ); } element = $el("tr", [ @@ -356,7 +377,10 @@ export class ComfySettingsDialog extends ComfyDialog { }, [$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(); } diff --git a/src/scripts/ui/spinner.ts b/src/scripts/ui/spinner.ts index 91fdaf9c0..09ad055a1 100644 --- a/src/scripts/ui/spinner.ts +++ b/src/scripts/ui/spinner.ts @@ -1,6 +1,5 @@ import "./spinner.css"; - export function createSpinner() { const div = document.createElement("div"); div.innerHTML = `
`; diff --git a/src/scripts/ui/toggleSwitch.ts b/src/scripts/ui/toggleSwitch.ts index 689528705..48dcd6258 100644 --- a/src/scripts/ui/toggleSwitch.ts +++ b/src/scripts/ui/toggleSwitch.ts @@ -20,7 +20,10 @@ export function toggleSwitch(name, items, e?) { if (selectedIndex != null) { elements[selectedIndex].classList.remove("comfy-toggle-selected"); } - onChange?.({ item: items[index], prev: selectedIndex == null ? undefined : items[selectedIndex] }); + onChange?.({ + item: items[index], + prev: selectedIndex == null ? undefined : items[selectedIndex], + }); selectedIndex = index; elements[selectedIndex].classList.add("comfy-toggle-selected"); } diff --git a/src/scripts/ui/userSelection.ts b/src/scripts/ui/userSelection.ts index 1d997c7ff..005c9027c 100644 --- a/src/scripts/ui/userSelection.ts +++ b/src/scripts/ui/userSelection.ts @@ -3,16 +3,14 @@ import { $el } from "../ui"; import { createSpinner } from "./spinner"; import "./userSelection.css"; - interface SelectedUser { username: string; userId: string; created: boolean; } - export class UserSelectionScreen { - async show(users, user): Promise{ + async show(users, user): Promise { const userSelection = document.getElementById("comfy-user-selection"); userSelection.style.display = ""; return new Promise((resolve) => { @@ -22,7 +20,9 @@ export class UserSelectionScreen { const selectSection = select.closest("section"); const form = userSelection.getElementsByTagName("form")[0]; const error = userSelection.getElementsByClassName("comfy-user-error")[0]; - const button = userSelection.getElementsByClassName("comfy-user-button-next")[0]; + const button = userSelection.getElementsByClassName( + "comfy-user-button-next" + )[0]; let inputActive = null; input.addEventListener("focus", () => { @@ -45,7 +45,8 @@ export class UserSelectionScreen { form.addEventListener("submit", async (e) => { e.preventDefault(); if (inputActive == null) { - error.textContent = "Please enter a username or select an existing user."; + error.textContent = + "Please enter a username or select an existing user."; } else if (inputActive) { const username = input.value.trim(); if (!username) { @@ -54,41 +55,59 @@ export class UserSelectionScreen { } // Create new user - // @ts-ignore // Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339) // Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551) - input.disabled = select.disabled = input.readonly = select.readonly = true; + input.disabled = + select.disabled = + // @ts-ignore + input.readonly = + // @ts-ignore + select.readonly = + true; const spinner = createSpinner(); button.prepend(spinner); try { const resp = await api.createUser(username); if (resp.status >= 300) { - let message = "Error creating user: " + resp.status + " " + resp.statusText; + let message = + "Error creating user: " + resp.status + " " + resp.statusText; try { const res = await resp.json(); - if(res.error) { + if (res.error) { message = res.error; } - } catch (error) { - } + } catch (error) {} throw new Error(message); } resolve({ username, userId: await resp.json(), created: true }); } catch (err) { spinner.remove(); - error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred."; - // @ts-ignore + error.textContent = + err.message ?? + err.statusText ?? + err ?? + "An unknown error occurred."; // Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339) // Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551) - input.disabled = select.disabled = input.readonly = select.readonly = false; + input.disabled = + select.disabled = + // @ts-ignore + input.readonly = + // @ts-ignore + select.readonly = + false; return; } } else if (!select.value) { error.textContent = "Please select an existing user."; return; } else { - resolve({ username: users[select.value], userId: select.value, created: false }); + resolve({ + username: users[select.value], + userId: select.value, + created: false, + }); } }); diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts index d602f4991..a97a986e5 100644 --- a/src/scripts/utils.ts +++ b/src/scripts/utils.ts @@ -57,7 +57,9 @@ export function applyTextReplacements(app: ComfyApp, value: string): string { // Find node with matching S&R property name // @ts-ignore - let nodes = app.graph._nodes.filter((n) => n.properties?.["Node name for S&R"] === split[0]); + let nodes = app.graph._nodes.filter( + (n) => n.properties?.["Node name for S&R"] === split[0] + ); // If we cant, see if there is a node with that title if (!nodes.length) { // @ts-ignore @@ -76,7 +78,13 @@ export function applyTextReplacements(app: ComfyApp, value: string): string { const widget = node.widgets?.find((w) => w.name === split[1]); if (!widget) { - console.warn("Unable to find widget", split[1], "on node", split[0], node); + console.warn( + "Unable to find widget", + split[1], + "on node", + split[0], + node + ); return match; } @@ -84,13 +92,19 @@ export function applyTextReplacements(app: ComfyApp, value: string): string { }); } -export async function addStylesheet(urlOrFile: string, relativeTo?: string): Promise { +export async function addStylesheet( + urlOrFile: string, + relativeTo?: string +): Promise { return new Promise((res, rej) => { let url; if (urlOrFile.endsWith(".js")) { url = urlOrFile.substr(0, urlOrFile.length - 2) + "css"; } else { - url = new URL(urlOrFile, relativeTo ?? `${window.location.protocol}//${window.location.host}`).toString(); + url = new URL( + urlOrFile, + relativeTo ?? `${window.location.protocol}//${window.location.host}` + ).toString(); } $el("link", { parent: document.head, @@ -103,7 +117,6 @@ export async function addStylesheet(urlOrFile: string, relativeTo?: string): Pro }); } - /** * @param { string } filename * @param { Blob } blob @@ -147,7 +160,10 @@ export function prop(target, name, defaultValue, onChanged) { export function getStorageValue(id) { const clientId = api.clientId ?? api.initialClientId; - return (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? localStorage.getItem(id); + return ( + (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? + localStorage.getItem(id) + ); } export function setStorageValue(id, value) { @@ -156,4 +172,4 @@ export function setStorageValue(id, value) { sessionStorage.setItem(`${id}:${clientId}`, value); } localStorage.setItem(id, value); -} \ No newline at end of file +} diff --git a/src/scripts/widgets.ts b/src/scripts/widgets.ts index a92f94181..5b5a5ee24 100644 --- a/src/scripts/widgets.ts +++ b/src/scripts/widgets.ts @@ -1,20 +1,23 @@ -import { api } from "./api" +import { api } from "./api"; import "./domWidget"; import type { ComfyApp } from "./app"; import type { IWidget, LGraphNode } from "/types/litegraph"; import { ComfyNodeDef } from "/types/apiTypes"; export type ComfyWidgetConstructor = ( - node: LGraphNode, inputName: string, inputData: ComfyNodeDef, app?: ComfyApp, widgetName?: string) => - {widget: IWidget, minWidth?: number; minHeight?: number }; - + node: LGraphNode, + inputName: string, + inputData: ComfyNodeDef, + app?: ComfyApp, + widgetName?: string +) => { widget: IWidget; minWidth?: number; minHeight?: number }; let controlValueRunBefore = false; export function updateControlWidgetLabel(widget) { let replacement = "after"; let find = "before"; if (controlValueRunBefore) { - [find, replacement] = [replacement, find] + [find, replacement] = [replacement, find]; } widget.label = (widget.label ?? widget.name).replace(find, replacement); } @@ -22,9 +25,14 @@ export function updateControlWidgetLabel(widget) { const IS_CONTROL_WIDGET = Symbol(); const HAS_EXECUTED = Symbol(); -function getNumberDefaults(inputData: ComfyNodeDef, defaultStep, precision, enable_rounding) { +function getNumberDefaults( + inputData: ComfyNodeDef, + defaultStep, + precision, + enable_rounding +) { let defaultVal = inputData[1]["default"]; - let { min, max, step, round} = inputData[1]; + let { min, max, step, round } = inputData[1]; if (defaultVal == undefined) defaultVal = 0; if (min == undefined) min = 0; @@ -33,30 +41,52 @@ function getNumberDefaults(inputData: ComfyNodeDef, defaultStep, precision, enab // precision is the number of decimal places to show. // by default, display the the smallest number of decimal places such that changes of size step are visible. if (precision == undefined) { - precision = Math.max(-Math.floor(Math.log10(step)),0); + precision = Math.max(-Math.floor(Math.log10(step)), 0); } if (enable_rounding && (round == undefined || round === true)) { // by default, round the value to those decimal places shown. - round = Math.round(1000000*Math.pow(0.1,precision))/1000000; + round = Math.round(1000000 * Math.pow(0.1, precision)) / 1000000; } - return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } }; + return { + val: defaultVal, + config: { min, max, step: 10.0 * step, round, precision }, + }; } -export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values, widgetName, inputData: ComfyNodeDef) { +export function addValueControlWidget( + node, + targetWidget, + defaultValue = "randomize", + values, + widgetName, + inputData: ComfyNodeDef +) { let name = inputData[1]?.control_after_generate; - if(typeof name !== "string") { + if (typeof name !== "string") { name = widgetName; } - const widgets = addValueControlWidgets(node, targetWidget, defaultValue, { - addFilterList: false, - controlAfterGenerateName: name - }, inputData); + const widgets = addValueControlWidgets( + node, + targetWidget, + defaultValue, + { + addFilterList: false, + controlAfterGenerateName: name, + }, + inputData + ); return widgets[0]; } -export function addValueControlWidgets(node, targetWidget, defaultValue = "randomize", options, inputData: ComfyNodeDef) { +export function addValueControlWidgets( + node, + targetWidget, + defaultValue = "randomize", + options, + inputData: ComfyNodeDef +) { if (!defaultValue) defaultValue = "randomize"; if (!options) options = {}; @@ -67,10 +97,10 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando } else if (typeof inputData?.[1]?.[defaultName] === "string") { name = inputData?.[1]?.[defaultName]; } else if (inputData?.[1]?.control_prefix) { - name = inputData?.[1]?.control_prefix + " " + name + name = inputData?.[1]?.control_prefix + " " + name; } return name; - } + }; const widgets = []; const valueControl = node.addWidget( @@ -120,16 +150,23 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando const regex = new RegExp(filter.substring(1, filter.length - 1)); check = (item) => regex.test(item); } catch (error) { - console.error("Error constructing RegExp filter for node " + node.id, filter, error); + console.error( + "Error constructing RegExp filter for node " + node.id, + filter, + error + ); } } if (!check) { const lower = filter.toLocaleLowerCase(); check = (item) => item.toLocaleLowerCase().includes(lower); } - values = values.filter(item => check(item)); + values = values.filter((item) => check(item)); if (!values.length && targetWidget.options.values.length) { - console.warn("Filter for node " + node.id + " has filtered out all items", filter); + console.warn( + "Filter for node " + node.id + " has filtered out all items", + filter + ); } } let current_index = values.indexOf(targetWidget.value); @@ -141,8 +178,8 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando break; case "increment-wrap": current_index += 1; - if ( current_index >= current_length ) { - current_index = 0; + if (current_index >= current_length) { + current_index = 0; } break; case "decrement": @@ -181,7 +218,10 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando targetWidget.value -= targetWidget.options.step / 10; break; case "randomize": - targetWidget.value = Math.floor(Math.random() * range) * (targetWidget.options.step / 10) + min; + targetWidget.value = + Math.floor(Math.random() * range) * + (targetWidget.options.step / 10) + + min; break; default: break; @@ -190,8 +230,7 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando * ranges and set them to min or max.*/ if (targetWidget.value < min) targetWidget.value = min; - if (targetWidget.value > max) - targetWidget.value = max; + if (targetWidget.value > max) targetWidget.value = max; targetWidget.callback(targetWidget.value); } }; @@ -213,20 +252,39 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando }; return widgets; -}; +} function seedWidget(node, inputName, inputData: ComfyNodeDef, app, widgetName) { const seed = createIntWidget(node, inputName, inputData, app, true); - const seedControl = addValueControlWidget(node, seed.widget, "randomize", undefined, widgetName, inputData); + const seedControl = addValueControlWidget( + node, + seed.widget, + "randomize", + undefined, + widgetName, + inputData + ); seed.widget.linkedWidgets = [seedControl]; return seed; } -function createIntWidget(node, inputName, inputData: ComfyNodeDef, app, isSeedInput: boolean = false) { +function createIntWidget( + node, + inputName, + inputData: ComfyNodeDef, + app, + isSeedInput: boolean = false +) { const control = inputData[1]?.control_after_generate; if (!isSeedInput && control) { - return seedWidget(node, inputName, inputData, app, typeof control === "string" ? control : undefined); + return seedWidget( + node, + inputName, + inputData, + app, + typeof control === "string" ? control : undefined + ); } let widgetType = isSlider(inputData[1]["display"], app); @@ -275,10 +333,10 @@ function addMultilineWidget(node, name, opts, app) { function isSlider(display, app) { if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) { - return "number" + return "number"; } - return (display==="slider") ? "slider" : "number" + return display === "slider" ? "slider" : "number"; } export function initWidgets(app) { @@ -288,7 +346,8 @@ export function initWidgets(app) { type: "combo", defaultValue: "after", options: ["before", "after"], - tooltip: "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.", + tooltip: + "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.", onChange(value) { controlValueRunBefore = value === "before"; for (const n of app.graph._nodes) { @@ -313,21 +372,41 @@ export const ComfyWidgets: Record = { "INT:seed": seedWidget, "INT:noise_seed": seedWidget, FLOAT(node, inputName, inputData: ComfyNodeDef, app) { - let widgetType: "number" | "slider" = isSlider(inputData[1]["display"], app); - let precision = app.ui.settings.getSettingValue("Comfy.FloatRoundingPrecision"); - let disable_rounding = app.ui.settings.getSettingValue("Comfy.DisableFloatRounding") + let widgetType: "number" | "slider" = isSlider( + inputData[1]["display"], + app + ); + let precision = app.ui.settings.getSettingValue( + "Comfy.FloatRoundingPrecision" + ); + let disable_rounding = app.ui.settings.getSettingValue( + "Comfy.DisableFloatRounding" + ); if (precision == 0) precision = undefined; - const { val, config } = getNumberDefaults(inputData, 0.5, precision, !disable_rounding); - return { widget: node.addWidget(widgetType, inputName, val, - function (v) { - if (config.round) { - this.value = Math.round((v + Number.EPSILON)/config.round)*config.round; - if (this.value > config.max) this.value = config.max; - if (this.value < config.min) this.value = config.min; - } else { - this.value = v; - } - }, config) }; + const { val, config } = getNumberDefaults( + inputData, + 0.5, + precision, + !disable_rounding + ); + return { + widget: node.addWidget( + widgetType, + inputName, + val, + function (v) { + if (config.round) { + this.value = + Math.round((v + Number.EPSILON) / config.round) * config.round; + if (this.value > config.max) this.value = config.max; + if (this.value < config.min) this.value = config.min; + } else { + this.value = v; + } + }, + config + ), + }; }, INT(node, inputName, inputData: ComfyNodeDef, app) { return createIntWidget(node, inputName, inputData, app); @@ -336,12 +415,9 @@ export const ComfyWidgets: Record = { let defaultVal = false; let options = {}; if (inputData[1]) { - if (inputData[1].default) - defaultVal = inputData[1].default; - if (inputData[1].label_on) - options["on"] = inputData[1].label_on; - if (inputData[1].label_off) - options["off"] = inputData[1].label_off; + if (inputData[1].default) defaultVal = inputData[1].default; + if (inputData[1].label_on) options["on"] = inputData[1].label_on; + if (inputData[1].label_off) options["off"] = inputData[1].label_off; } return { widget: node.addWidget( @@ -349,8 +425,8 @@ export const ComfyWidgets: Record = { inputName, defaultVal, () => {}, - options, - ) + options + ), }; }, STRING(node, inputName, inputData: ComfyNodeDef, app) { @@ -359,12 +435,19 @@ export const ComfyWidgets: Record = { let res; if (multiline) { - res = addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app); + res = addMultilineWidget( + node, + inputName, + { defaultVal, ...inputData[1] }, + app + ); } else { - res = { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) }; + res = { + widget: node.addWidget("text", inputName, defaultVal, () => {}, {}), + }; } - if(inputData[1].dynamicPrompts != undefined) + if (inputData[1].dynamicPrompts != undefined) res.widget.dynamicPrompts = inputData[1].dynamicPrompts; return res; @@ -375,18 +458,35 @@ export const ComfyWidgets: Record = { if (inputData[1] && inputData[1].default) { defaultValue = inputData[1].default; } - const res = { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) }; + const res = { + widget: node.addWidget("combo", inputName, defaultValue, () => {}, { + values: type, + }), + }; if (inputData[1]?.control_after_generate) { // TODO make combo handle a widget node type? // @ts-ignore - res.widget.linkedWidgets = addValueControlWidgets(node, res.widget, undefined, undefined, inputData); + res.widget.linkedWidgets = addValueControlWidgets( + node, + res.widget, + undefined, + undefined, + inputData + ); } return res; }, - IMAGEUPLOAD(node: LGraphNode, inputName: string, inputData: ComfyNodeDef, app) { + IMAGEUPLOAD( + node: LGraphNode, + inputName: string, + inputData: ComfyNodeDef, + app + ) { // TODO make image upload handle a custom node type? // @ts-ignore - const imageWidget = node.widgets.find((w) => w.name === (inputData[1]?.widget ?? "image")); + const imageWidget = node.widgets.find( + (w) => w.name === (inputData[1]?.widget ?? "image") + ); let uploadWidget; function showImage(name) { @@ -402,18 +502,20 @@ export const ComfyWidgets: Record = { subfolder = name.substring(0, folder_separator); name = name.substring(folder_separator + 1); } - img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`); + img.src = api.apiURL( + `/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}` + ); // @ts-ignore node.setSizeForImage?.(); } var default_value = imageWidget.value; Object.defineProperty(imageWidget, "value", { - set : function(value) { + set: function (value) { this._real_value = value; }, - get : function() { + get: function () { if (!this._real_value) { return default_value; } @@ -428,11 +530,11 @@ export const ComfyWidgets: Record = { value += real_value.filename; - if(real_value.type && real_value.type !== "input") + if (real_value.type && real_value.type !== "input") value += ` [${real_value.type}]`; } return value; - } + }, }); // Add our own callback to the combo widget to render an image when it changes @@ -535,15 +637,15 @@ export const ComfyWidgets: Record = { }; // @ts-ignore - node.pasteFile = function(file) { + node.pasteFile = function (file) { if (file.type.startsWith("image/")) { - const is_pasted = (file.name === "image.png") && - (file.lastModified - Date.now() < 2000); + const is_pasted = + file.name === "image.png" && file.lastModified - Date.now() < 2000; uploadFile(file, true, is_pasted); return true; } return false; - } + }; return { widget: uploadWidget }; }, diff --git a/src/scripts/workflows.js b/src/scripts/workflows.js index 1f17f151f..e0b8d5636 100644 --- a/src/scripts/workflows.js +++ b/src/scripts/workflows.js @@ -57,7 +57,10 @@ export class ComfyWorkflowManager extends EventTarget { #bindExecutionEvents() { // TODO: on reload, set active prompt based on the latest ws message - const emit = () => this.dispatchEvent(new CustomEvent("execute", { detail: this.activePrompt })); + const emit = () => + this.dispatchEvent( + new CustomEvent("execute", { detail: this.activePrompt }) + ); let executing = null; api.addEventListener("execution_start", (e) => { this.#activePromptId = e.detail.prompt_id; @@ -106,14 +109,21 @@ export class ComfyWorkflowManager extends EventTarget { favorites = new Set(); } - const workflows = (await api.listUserData("workflows", true, true)).map((w) => { - let workflow = this.workflowLookup[w[0]]; - if (!workflow) { - workflow = new ComfyWorkflow(this, w[0], w.slice(1), favorites.has(w[0])); - this.workflowLookup[workflow.path] = workflow; + const workflows = (await api.listUserData("workflows", true, true)).map( + (w) => { + let workflow = this.workflowLookup[w[0]]; + if (!workflow) { + workflow = new ComfyWorkflow( + this, + w[0], + w.slice(1), + favorites.has(w[0]) + ); + this.workflowLookup[workflow.path] = workflow; + } + return workflow; } - return workflow; - }); + ); this.workflows = workflows; } catch (error) { @@ -124,7 +134,9 @@ export class ComfyWorkflowManager extends EventTarget { async saveWorkflowMetadata() { await api.storeUserData("workflows/.index.json", { - favorites: [...this.workflows.filter((w) => w.isFavorite).map((w) => w.path)], + favorites: [ + ...this.workflows.filter((w) => w.isFavorite).map((w) => w.path), + ], }); } @@ -137,13 +149,20 @@ export class ComfyWorkflowManager extends EventTarget { const found = this.workflows.find((w) => w.path === workflow); if (found) { workflow = found; - workflow.unsaved = !workflow || getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true"; + workflow.unsaved = + !workflow || + getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true"; } } if (!(workflow instanceof ComfyWorkflow)) { // Still not found, either reloading a deleted workflow or blank - workflow = new ComfyWorkflow(this, workflow || "Unsaved Workflow" + (this.#unsavedCount++ ? ` (${this.#unsavedCount})` : "")); + workflow = new ComfyWorkflow( + this, + workflow || + "Unsaved Workflow" + + (this.#unsavedCount++ ? ` (${this.#unsavedCount})` : "") + ); } const index = this.openWorkflows.indexOf(workflow); @@ -293,7 +312,9 @@ export class ComfyWorkflow { async getWorkflowData() { const resp = await api.getUserData("workflows/" + this.path); if (resp.status !== 200) { - alert(`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`); + alert( + `Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}` + ); return; } return await resp.json(); @@ -301,7 +322,12 @@ export class ComfyWorkflow { load = async () => { if (this.isOpen) { - await this.manager.app.loadGraphData(this.changeTracker.activeState, true, true, this); + await this.manager.app.loadGraphData( + this.changeTracker.activeState, + true, + true, + this + ); } else { const data = await this.getWorkflowData(); if (!data) return; @@ -327,7 +353,12 @@ export class ComfyWorkflow { await this.manager.saveWorkflowMetadata(); this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this })); } catch (error) { - alert("Error favoriting workflow " + this.path + "\n" + (error.message ?? error)); + alert( + "Error favoriting workflow " + + this.path + + "\n" + + (error.message ?? error) + ); } } @@ -336,15 +367,29 @@ export class ComfyWorkflow { */ async rename(path) { path = appendJsonExt(path); - let resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path); + let resp = await api.moveUserData( + "workflows/" + this.path, + "workflows/" + path + ); if (resp.status === 409) { - if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return resp; - resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path, { overwrite: true }); + if ( + !confirm( + `Workflow '${path}' already exists, do you want to overwrite it?` + ) + ) + return resp; + resp = await api.moveUserData( + "workflows/" + this.path, + "workflows/" + path, + { overwrite: true } + ); } if (resp.status !== 200) { - alert(`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`); + alert( + `Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}` + ); return; } @@ -367,7 +412,10 @@ export class ComfyWorkflow { const old = localStorage.getItem("litegrapheditor_clipboard"); const graph = new LGraph(data); - const canvas = new LGraphCanvas(null, graph, { skip_events: true, skip_render: true }); + const canvas = new LGraphCanvas(null, graph, { + skip_events: true, + skip_render: true, + }); canvas.selectNodes(); canvas.copyToClipboard(); this.manager.app.canvas.pasteFromClipboard(); @@ -406,7 +454,10 @@ export class ComfyWorkflow { */ async #save(path, overwrite) { if (!path) { - path = prompt("Save workflow as:", trimJsonExt(this.path) ?? this.name ?? "workflow"); + path = prompt( + "Save workflow as:", + trimJsonExt(this.path) ?? this.name ?? "workflow" + ); if (!path) return; } @@ -414,14 +465,27 @@ export class ComfyWorkflow { const p = await this.manager.app.graphToPrompt(); const json = JSON.stringify(p.workflow, null, 2); - let resp = await api.storeUserData("workflows/" + path, json, { stringify: false, throwOnError: false, overwrite }); + let resp = await api.storeUserData("workflows/" + path, json, { + stringify: false, + throwOnError: false, + overwrite, + }); if (resp.status === 409) { - if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return; - resp = await api.storeUserData("workflows/" + path, json, { stringify: false }); + if ( + !confirm( + `Workflow '${path}' already exists, do you want to overwrite it?` + ) + ) + return; + resp = await api.storeUserData("workflows/" + path, json, { + stringify: false, + }); } if (resp.status !== 200) { - alert(`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`); + alert( + `Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}` + ); return; } diff --git a/src/types/apiTypes.ts b/src/types/apiTypes.ts index b2ac38e14..72221acf2 100644 --- a/src/types/apiTypes.ts +++ b/src/types/apiTypes.ts @@ -7,107 +7,113 @@ const zQueueIndex = z.number(); const zPromptId = z.string(); const zPromptItem = z.object({ - inputs: z.record(z.string(), z.any()), - class_type: zNodeType, + inputs: z.record(z.string(), z.any()), + class_type: zNodeType, }); const zPrompt = z.array(zPromptItem); -const zExtraPngInfo = z.object({ +const zExtraPngInfo = z + .object({ workflow: zComfyWorkflow, -}).passthrough(); + }) + .passthrough(); const zExtraData = z.object({ - extra_pnginfo: zExtraPngInfo, - client_id: z.string(), + extra_pnginfo: zExtraPngInfo, + client_id: z.string(), }); const zOutputsToExecute = z.array(zNodeId); const zExecutionStartMessage = z.tuple([ - z.literal("execution_start"), - z.object({ - prompt_id: zPromptId, - }), + z.literal("execution_start"), + z.object({ + prompt_id: zPromptId, + }), ]); const zExecutionCachedMessage = z.tuple([ - z.literal("execution_cached"), - z.object({ - prompt_id: zPromptId, - nodes: z.array(zNodeId), - }), + z.literal("execution_cached"), + z.object({ + prompt_id: zPromptId, + nodes: z.array(zNodeId), + }), ]); const zExecutionInterruptedMessage = z.tuple([ - z.literal("execution_interrupted"), - z.object({ - // InterruptProcessingException - prompt_id: zPromptId, - node_id: zNodeId, - node_type: zNodeType, - executed: z.array(zNodeId), - }), + z.literal("execution_interrupted"), + z.object({ + // InterruptProcessingException + prompt_id: zPromptId, + node_id: zNodeId, + node_type: zNodeType, + executed: z.array(zNodeId), + }), ]); const zExecutionErrorMessage = z.tuple([ - z.literal("execution_error"), - z.object({ - prompt_id: zPromptId, - node_id: zNodeId, - node_type: zNodeType, - executed: z.array(zNodeId), + z.literal("execution_error"), + z.object({ + prompt_id: zPromptId, + node_id: zNodeId, + node_type: zNodeType, + executed: z.array(zNodeId), - exception_message: z.string(), - exception_type: z.string(), - traceback: z.string(), - current_inputs: z.any(), - current_outputs: z.any(), - }), + exception_message: z.string(), + exception_type: z.string(), + traceback: z.string(), + current_inputs: z.any(), + current_outputs: z.any(), + }), ]); const zStatusMessage = z.union([ - zExecutionStartMessage, - zExecutionCachedMessage, - zExecutionInterruptedMessage, - zExecutionErrorMessage, + zExecutionStartMessage, + zExecutionCachedMessage, + zExecutionInterruptedMessage, + zExecutionErrorMessage, ]); const zStatus = z.object({ - status_str: z.enum(["success", "error"]), - completed: z.boolean(), - messages: z.array(zStatusMessage), + status_str: z.enum(["success", "error"]), + completed: z.boolean(), + messages: z.array(zStatusMessage), }); // TODO: this is a placeholder const zOutput = z.any(); const zTaskPrompt = z.tuple([ - zQueueIndex, - zPromptId, - zPrompt, - zExtraData, - zOutputsToExecute, + zQueueIndex, + zPromptId, + zPrompt, + zExtraData, + zOutputsToExecute, ]); const zRunningTaskItem = z.object({ - prompt: zTaskPrompt, - remove: z.object({ - name: z.literal("Cancel"), - cb: z.function(), - }), + prompt: zTaskPrompt, + remove: z.object({ + name: z.literal("Cancel"), + cb: z.function(), + }), }); const zPendingTaskItem = z.object({ - prompt: zTaskPrompt, + prompt: zTaskPrompt, }); const zHistoryTaskItem = z.object({ - prompt: zTaskPrompt, - status: zStatus.optional(), - outputs: z.record(zNodeId, zOutput), + prompt: zTaskPrompt, + status: zStatus.optional(), + outputs: z.record(zNodeId, zOutput), }); -const zTaskItem = z.union([zRunningTaskItem, zPendingTaskItem, zHistoryTaskItem]); +const zTaskItem = z.union([ + zRunningTaskItem, + zPendingTaskItem, + zHistoryTaskItem, +]); // `/queue` export type RunningTaskItem = z.infer; @@ -119,101 +125,100 @@ export type TaskItem = z.infer; // TODO: validate `/history` `/queue` API endpoint responses. function inputSpec(spec: [ZodType, ZodType]): ZodType { - const [inputType, inputSpec] = spec; - return z.union([ - z.tuple([inputType, inputSpec]), - z.tuple([inputType]), - ]); + const [inputType, inputSpec] = spec; + return z.union([z.tuple([inputType, inputSpec]), z.tuple([inputType])]); } const zIntInputSpec = inputSpec([ - z.literal("INT"), - z.object({ - min: z.number().optional(), - max: z.number().optional(), - step: z.number().optional(), - default: z.number().optional(), - forceInput: z.boolean().optional(), - }), + z.literal("INT"), + z.object({ + min: z.number().optional(), + max: z.number().optional(), + step: z.number().optional(), + default: z.number().optional(), + forceInput: z.boolean().optional(), + }), ]); const zFloatInputSpec = inputSpec([ - z.literal("FLOAT"), - z.object({ - min: z.number().optional(), - max: z.number().optional(), - step: z.number().optional(), - round: z.number().optional(), - default: z.number().optional(), - forceInput: z.boolean().optional(), - }), + z.literal("FLOAT"), + z.object({ + min: z.number().optional(), + max: z.number().optional(), + step: z.number().optional(), + round: z.number().optional(), + default: z.number().optional(), + forceInput: z.boolean().optional(), + }), ]); const zBooleanInputSpec = inputSpec([ - z.literal("BOOLEAN"), - z.object({ - label_on: z.string().optional(), - label_off: z.string().optional(), - default: z.boolean().optional(), - forceInput: z.boolean().optional(), - }) + z.literal("BOOLEAN"), + z.object({ + label_on: z.string().optional(), + label_off: z.string().optional(), + default: z.boolean().optional(), + forceInput: z.boolean().optional(), + }), ]); const zStringInputSpec = inputSpec([ - z.literal("STRING"), - z.object({ - default: z.string().optional(), - multiline: z.boolean().optional(), - dynamicPrompts: z.boolean().optional(), - forceInput: z.boolean().optional(), - }), + z.literal("STRING"), + z.object({ + default: z.string().optional(), + multiline: z.boolean().optional(), + dynamicPrompts: z.boolean().optional(), + forceInput: z.boolean().optional(), + }), ]); // Dropdown Selection. const zComboInputSpec = inputSpec([ - z.array(z.any()), - z.object({ - default: z.any().optional(), - control_after_generate: z.boolean().optional(), - image_upload: z.boolean().optional(), - forceInput: z.boolean().optional(), - }), + z.array(z.any()), + z.object({ + default: z.any().optional(), + control_after_generate: z.boolean().optional(), + image_upload: z.boolean().optional(), + forceInput: z.boolean().optional(), + }), ]); const zCustomInputSpec = inputSpec([ - z.string(), - z.object({ - default: z.any().optional(), - forceInput: z.boolean().optional(), - }), + z.string(), + z.object({ + default: z.any().optional(), + forceInput: z.boolean().optional(), + }), ]); const zInputSpec = z.union([ - zIntInputSpec, - zFloatInputSpec, - zBooleanInputSpec, - zStringInputSpec, - zComboInputSpec, - zCustomInputSpec, + zIntInputSpec, + zFloatInputSpec, + zBooleanInputSpec, + zStringInputSpec, + zComboInputSpec, + zCustomInputSpec, ]); const zComfyNodeDataType = z.string(); const zComfyComboOutput = z.array(z.any()); -const zComfyOutputSpec = z.array(z.union([zComfyNodeDataType, zComfyComboOutput])); +const zComfyOutputSpec = z.array( + z.union([zComfyNodeDataType, zComfyComboOutput]) +); const zComfyNodeDef = z.object({ - input: z.object({ - required: z.record(zInputSpec).optional(), - optional: z.record(zInputSpec).optional(), - }), - output: zComfyOutputSpec, - output_is_list: z.array(z.boolean()), - output_name: z.array(z.string()), - name: z.string(), - display_name: z.string(), - description: z.string(), - category: z.string(), - output_node: z.boolean(), + input: z.object({ + required: z.record(zInputSpec).optional(), + optional: z.record(zInputSpec).optional(), + }), + output: zComfyOutputSpec, + output_is_list: z.array(z.boolean()), + output_name: z.array(z.string()), + name: z.string(), + display_name: z.string(), + description: z.string(), + category: z.string(), + output_node: z.boolean(), }); // `/object_info` diff --git a/src/types/colorPalette.ts b/src/types/colorPalette.ts index aef025318..a7cbe8036 100644 --- a/src/types/colorPalette.ts +++ b/src/types/colorPalette.ts @@ -1,6 +1,7 @@ -import { z } from 'zod'; +import { z } from "zod"; -const nodeSlotSchema = z.object({ +const nodeSlotSchema = z + .object({ BOOLEAN: z.string().optional(), CLIP: z.string(), CLIP_VISION: z.string(), @@ -25,10 +26,12 @@ const nodeSlotSchema = z.object({ TAESD: z.string(), TIMESTEP_KEYFRAME: z.string().optional(), UPSCALE_MODEL: z.string().optional(), - VAE: z.string() -}).passthrough(); + VAE: z.string(), + }) + .passthrough(); -const litegraphBaseSchema = z.object({ +const litegraphBaseSchema = z + .object({ BACKGROUND_IMAGE: z.string(), CLEAR_BACKGROUND_COLOR: z.string(), NODE_TITLE_COLOR: z.string(), @@ -49,37 +52,40 @@ const litegraphBaseSchema = z.object({ WIDGET_SECONDARY_TEXT_COLOR: z.string(), LINK_COLOR: z.string(), EVENT_LINK_COLOR: z.string(), - CONNECTING_LINK_COLOR: z.string() -}).passthrough(); + CONNECTING_LINK_COLOR: z.string(), + }) + .passthrough(); const comfyBaseSchema = z.object({ - ["fg-color"]: z.string(), - ["bg-color"]: z.string(), - ["comfy-menu-bg"]: z.string(), - ["comfy-input-bg"]: z.string(), - ["input-text"]: z.string(), - ["descrip-text"]: z.string(), - ["drag-text"]: z.string(), - ["error-text"]: z.string(), - ["border-color"]: z.string(), - ["tr-even-bg-color"]: z.string(), - ["tr-odd-bg-color"]: z.string(), - ["content-bg"]: z.string(), - ["content-fg"]: z.string(), - ["content-hover-bg"]: z.string(), - ["content-hover-fg"]: z.string(), + ["fg-color"]: z.string(), + ["bg-color"]: z.string(), + ["comfy-menu-bg"]: z.string(), + ["comfy-input-bg"]: z.string(), + ["input-text"]: z.string(), + ["descrip-text"]: z.string(), + ["drag-text"]: z.string(), + ["error-text"]: z.string(), + ["border-color"]: z.string(), + ["tr-even-bg-color"]: z.string(), + ["tr-odd-bg-color"]: z.string(), + ["content-bg"]: z.string(), + ["content-fg"]: z.string(), + ["content-hover-bg"]: z.string(), + ["content-hover-fg"]: z.string(), }); -const colorsSchema = z.object({ +const colorsSchema = z + .object({ node_slot: nodeSlotSchema, litegraph_base: litegraphBaseSchema, - comfy_base: comfyBaseSchema -}).passthrough(); + comfy_base: comfyBaseSchema, + }) + .passthrough(); const paletteSchema = z.object({ - id: z.string(), - name: z.string(), - colors: colorsSchema + id: z.string(), + name: z.string(), + colors: colorsSchema, }); const colorPalettesSchema = z.record(paletteSchema); diff --git a/src/types/comfy.d.ts b/src/types/comfy.d.ts index 9a338b349..b9f3505a9 100644 --- a/src/types/comfy.d.ts +++ b/src/types/comfy.d.ts @@ -2,75 +2,90 @@ import { LGraphNode, IWidget } from "./litegraph"; import { ComfyApp } from "../../scripts/app"; export interface ComfyExtension { - /** - * The name of the extension - */ - name: string; - /** - * Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added - * @param app The ComfyUI app instance - */ - init?(app: ComfyApp): Promise; - /** - * Allows any additonal setup, called after the application is fully set up and running - * @param app The ComfyUI app instance - */ - setup?(app: ComfyApp): Promise; - /** - * Called before nodes are registered with the graph - * @param defs The collection of node definitions, add custom ones or edit existing ones - * @param app The ComfyUI app instance - */ - addCustomNodeDefs?(defs: Record, app: ComfyApp): Promise; - /** - * Allows the extension to add custom widgets - * @param app The ComfyUI app instance - * @returns An array of {[widget name]: widget data} - */ - getCustomWidgets?( - app: ComfyApp - ): Promise< - Record { widget?: IWidget; minWidth?: number; minHeight?: number }> - >; - /** - * Allows the extension to add additional handling to the node before it is registered with LGraph - * @param nodeType The node class (not an instance) - * @param nodeData The original node object info config object - * @param app The ComfyUI app instance - */ - beforeRegisterNodeDef?(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp): Promise; - /** - * Allows the extension to register additional nodes with LGraph after standard nodes are added - * @param app The ComfyUI app instance - */ - registerCustomNodes?(app: ComfyApp): Promise; - /** - * Allows the extension to modify a node that has been reloaded onto the graph. - * If you break something in the backend and want to patch workflows in the frontend - * This is the place to do this - * @param node The node that has been loaded - * @param app The ComfyUI app instance - */ - loadedGraphNode?(node: LGraphNode, app: ComfyApp); - /** - * Allows the extension to run code after the constructor of the node - * @param node The node that has been created - * @param app The ComfyUI app instance - */ - nodeCreated?(node: LGraphNode, app: ComfyApp); + /** + * The name of the extension + */ + name: string; + /** + * Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added + * @param app The ComfyUI app instance + */ + init?(app: ComfyApp): Promise; + /** + * Allows any additonal setup, called after the application is fully set up and running + * @param app The ComfyUI app instance + */ + setup?(app: ComfyApp): Promise; + /** + * Called before nodes are registered with the graph + * @param defs The collection of node definitions, add custom ones or edit existing ones + * @param app The ComfyUI app instance + */ + addCustomNodeDefs?( + defs: Record, + app: ComfyApp + ): Promise; + /** + * Allows the extension to add custom widgets + * @param app The ComfyUI app instance + * @returns An array of {[widget name]: widget data} + */ + getCustomWidgets?( + app: ComfyApp + ): Promise< + Record< + string, + ( + node, + inputName, + inputData, + app + ) => { widget?: IWidget; minWidth?: number; minHeight?: number } + > + >; + /** + * Allows the extension to add additional handling to the node before it is registered with LGraph + * @param nodeType The node class (not an instance) + * @param nodeData The original node object info config object + * @param app The ComfyUI app instance + */ + beforeRegisterNodeDef?( + nodeType: typeof LGraphNode, + nodeData: ComfyObjectInfo, + app: ComfyApp + ): Promise; + /** + * Allows the extension to register additional nodes with LGraph after standard nodes are added + * @param app The ComfyUI app instance + */ + registerCustomNodes?(app: ComfyApp): Promise; + /** + * Allows the extension to modify a node that has been reloaded onto the graph. + * If you break something in the backend and want to patch workflows in the frontend + * This is the place to do this + * @param node The node that has been loaded + * @param app The ComfyUI app instance + */ + loadedGraphNode?(node: LGraphNode, app: ComfyApp); + /** + * Allows the extension to run code after the constructor of the node + * @param node The node that has been created + * @param app The ComfyUI app instance + */ + nodeCreated?(node: LGraphNode, app: ComfyApp); } export type ComfyObjectInfo = { - name: string; - display_name?: string; - description?: string; - category: string; - input?: { - required?: Record; - optional?: Record; - }; - output?: string[]; - output_name: string[]; + name: string; + display_name?: string; + description?: string; + category: string; + input?: { + required?: Record; + optional?: Record; + }; + output?: string[]; + output_name: string[]; }; export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any]; diff --git a/src/types/comfyWorkflow.ts b/src/types/comfyWorkflow.ts index 3b99e0acd..62baa107b 100644 --- a/src/types/comfyWorkflow.ts +++ b/src/types/comfyWorkflow.ts @@ -1,47 +1,56 @@ -import { z } from 'zod'; -import { fromZodError } from 'zod-validation-error'; +import { z } from "zod"; +import { fromZodError } from "zod-validation-error"; const zComfyLink = z.tuple([ - z.number(), // Link id - z.number(), // Node id of source node - z.number(), // Output slot# of source node - z.number(), // Node id of destination node - z.number(), // Input slot# of destination node - z.string(), // Data type + z.number(), // Link id + z.number(), // Node id of source node + z.number(), // Output slot# of source node + z.number(), // Node id of destination node + z.number(), // Input slot# of destination node + z.string(), // Data type ]); -const zNodeOutput = z.object({ +const zNodeOutput = z + .object({ name: z.string(), type: z.string(), links: z.array(z.number()).nullable(), slot_index: z.number().optional(), -}).passthrough(); + }) + .passthrough(); -const zNodeInput = z.object({ +const zNodeInput = z + .object({ name: z.string(), type: z.string(), link: z.number().nullable(), slot_index: z.number().optional(), -}).passthrough(); + }) + .passthrough(); -const zFlags = z.object({ +const zFlags = z + .object({ collapsed: z.boolean().optional(), pinned: z.boolean().optional(), allow_interaction: z.boolean().optional(), horizontal: z.boolean().optional(), skip_repeated_outputs: z.boolean().optional(), -}).passthrough(); + }) + .passthrough(); -const zProperties = z.object({ +const zProperties = z + .object({ ["Node name for S&R"]: z.string().optional(), -}).passthrough(); + }) + .passthrough(); const zVector2 = z.union([ - z.object({ 0: z.number(), 1: z.number() }), - z.tuple([z.number(), z.number()]), + z.object({ 0: z.number(), 1: z.number() }), + z.tuple([z.number(), z.number()]), ]); -const zComfyNode = z.object({ +const zComfyNode = z + .object({ id: z.number(), type: z.string(), pos: z.tuple([z.number(), z.number()]), @@ -52,20 +61,24 @@ const zComfyNode = z.object({ inputs: z.array(zNodeInput).optional(), outputs: z.array(zNodeOutput).optional(), properties: zProperties, - widgets_values: z.array(z.any()).optional(), // This could contain mixed types + widgets_values: z.array(z.any()).optional(), // This could contain mixed types color: z.string().optional(), bgcolor: z.string().optional(), -}).passthrough(); + }) + .passthrough(); -const zGroup = z.object({ +const zGroup = z + .object({ title: z.string(), bounding: z.tuple([z.number(), z.number(), z.number(), z.number()]), color: z.string(), font_size: z.number(), locked: z.boolean().optional(), -}).passthrough(); + }) + .passthrough(); -const zInfo = z.object({ +const zInfo = z + .object({ name: z.string(), author: z.string(), description: z.string(), @@ -73,24 +86,32 @@ const zInfo = z.object({ created: z.string(), modified: z.string(), software: z.string(), -}).passthrough(); + }) + .passthrough(); -const zDS = z.object({ +const zDS = z + .object({ scale: z.number(), offset: zVector2, -}).passthrough(); + }) + .passthrough(); -const zConfig = z.object({ +const zConfig = z + .object({ links_ontop: z.boolean().optional(), align_to_grid: z.boolean().optional(), -}).passthrough(); + }) + .passthrough(); -const zExtra = z.object({ +const zExtra = z + .object({ ds: zDS.optional(), info: zInfo.optional(), -}).passthrough(); + }) + .passthrough(); -export const zComfyWorkflow = z.object({ +export const zComfyWorkflow = z + .object({ last_node_id: z.number(), last_link_id: z.number(), nodes: z.array(zComfyNode), @@ -99,7 +120,8 @@ export const zComfyWorkflow = z.object({ config: zConfig.optional().nullable(), extra: zExtra.optional().nullable(), version: z.number(), -}).passthrough(); + }) + .passthrough(); export type NodeInput = z.infer; export type NodeOutput = z.infer; @@ -107,15 +129,14 @@ export type ComfyLink = z.infer; export type ComfyNode = z.infer; export type ComfyWorkflow = z.infer; - export async function parseComfyWorkflow(data: string): Promise { - // Validate - const result = await zComfyWorkflow.safeParseAsync(JSON.parse(data)); - if (!result.success) { - // TODO: Pretty print the error on UI modal. - const error = fromZodError(result.error); - alert(`Invalid workflow against zod schema:\n${error}`); - throw error; - } - return result.data; + // Validate + const result = await zComfyWorkflow.safeParseAsync(JSON.parse(data)); + if (!result.success) { + // TODO: Pretty print the error on UI modal. + const error = fromZodError(result.error); + alert(`Invalid workflow against zod schema:\n${error}`); + throw error; + } + return result.data; } diff --git a/src/types/litegraph.d.ts b/src/types/litegraph.d.ts index 30954d961..e384f79e6 100644 --- a/src/types/litegraph.d.ts +++ b/src/types/litegraph.d.ts @@ -5,903 +5,895 @@ export type Vector2 = [number, number]; export type Vector4 = [number, number, number, number]; export type widgetTypes = - | "number" - | "slider" - | "combo" - | "text" - | "toggle" - | "button"; + | "number" + | "slider" + | "combo" + | "text" + | "toggle" + | "button"; export type SlotShape = - | typeof LiteGraph.BOX_SHAPE - | typeof LiteGraph.CIRCLE_SHAPE - | typeof LiteGraph.ARROW_SHAPE - | typeof LiteGraph.SQUARE_SHAPE - | number; // For custom shapes + | typeof LiteGraph.BOX_SHAPE + | typeof LiteGraph.CIRCLE_SHAPE + | typeof LiteGraph.ARROW_SHAPE + | typeof LiteGraph.SQUARE_SHAPE + | number; // For custom shapes /** https://github.com/jagenjo/litegraph.js/tree/master/guides#node-slots */ export interface INodeSlot { - name: string; - type: string | -1; - label?: string; - dir?: - | typeof LiteGraph.UP - | typeof LiteGraph.RIGHT - | typeof LiteGraph.DOWN - | typeof LiteGraph.LEFT; - color_on?: string; - color_off?: string; - shape?: SlotShape; - locked?: boolean; - nameLocked?: boolean; + name: string; + type: string | -1; + label?: string; + dir?: + | typeof LiteGraph.UP + | typeof LiteGraph.RIGHT + | typeof LiteGraph.DOWN + | typeof LiteGraph.LEFT; + color_on?: string; + color_off?: string; + shape?: SlotShape; + locked?: boolean; + nameLocked?: boolean; } export interface INodeInputSlot extends INodeSlot { - link: LLink["id"] | null; + link: LLink["id"] | null; } export interface INodeOutputSlot extends INodeSlot { - links: LLink["id"][] | null; + links: LLink["id"][] | null; } export type WidgetCallback = ( - this: T, - value: T["value"], - graphCanvas: LGraphCanvas, - node: LGraphNode, - pos: Vector2, - event?: MouseEvent + this: T, + value: T["value"], + graphCanvas: LGraphCanvas, + node: LGraphNode, + pos: Vector2, + event?: MouseEvent ) => void; export interface IWidget { - // linked widgets, e.g. seed+seedControl + // linked widgets, e.g. seed+seedControl linkedWidgets: IWidget[]; - name: string | null; - value: TValue; - options?: TOptions; - type?: widgetTypes; - y?: number; - property?: string; - last_y?: number; - clicked?: boolean; - marker?: boolean; - callback?: WidgetCallback; - /** Called by `LGraphCanvas.drawNodeWidgets` */ - draw?( - ctx: CanvasRenderingContext2D, - node: LGraphNode, - width: number, - posY: number, - height: number - ): void; - /** - * Called by `LGraphCanvas.processNodeWidgets` - * https://github.com/jagenjo/litegraph.js/issues/76 - */ - mouse?( - event: MouseEvent, - pos: Vector2, - node: LGraphNode - ): boolean; - /** Called by `LGraphNode.computeSize` */ - computeSize?(width: number): [number, number]; + name: string | null; + value: TValue; + options?: TOptions; + type?: widgetTypes; + y?: number; + property?: string; + last_y?: number; + clicked?: boolean; + marker?: boolean; + callback?: WidgetCallback; + /** Called by `LGraphCanvas.drawNodeWidgets` */ + draw?( + ctx: CanvasRenderingContext2D, + node: LGraphNode, + width: number, + posY: number, + height: number + ): void; + /** + * Called by `LGraphCanvas.processNodeWidgets` + * https://github.com/jagenjo/litegraph.js/issues/76 + */ + mouse?(event: MouseEvent, pos: Vector2, node: LGraphNode): boolean; + /** Called by `LGraphNode.computeSize` */ + computeSize?(width: number): [number, number]; } export interface IButtonWidget extends IWidget { - type: "button"; + type: "button"; } export interface IToggleWidget - extends IWidget { - type: "toggle"; + extends IWidget { + type: "toggle"; } export interface ISliderWidget - extends IWidget { - type: "slider"; + extends IWidget { + type: "slider"; } export interface INumberWidget extends IWidget { - type: "number"; + type: "number"; } export interface IComboWidget - extends IWidget< - string[], - { - values: - | string[] - | ((widget: IComboWidget, node: LGraphNode) => string[]); - } - > { - type: "combo"; + extends IWidget< + string[], + { + values: string[] | ((widget: IComboWidget, node: LGraphNode) => string[]); + } + > { + type: "combo"; } export interface ITextWidget extends IWidget { - type: "text"; + type: "text"; } export interface IContextMenuItem { - content: string; - callback?: ContextMenuEventListener; - /** Used as innerHTML for extra child element */ - title?: string; - disabled?: boolean; - has_submenu?: boolean; - submenu?: { - options: ContextMenuItem[]; - } & IContextMenuOptions; - className?: string; + content: string; + callback?: ContextMenuEventListener; + /** Used as innerHTML for extra child element */ + title?: string; + disabled?: boolean; + has_submenu?: boolean; + submenu?: { + options: ContextMenuItem[]; + } & IContextMenuOptions; + className?: string; } export interface IContextMenuOptions { - callback?: ContextMenuEventListener; - ignore_item_callbacks?: Boolean; - event?: MouseEvent | CustomEvent; - parentMenu?: ContextMenu; - autoopen?: boolean; - title?: string; - extra?: any; + callback?: ContextMenuEventListener; + ignore_item_callbacks?: Boolean; + event?: MouseEvent | CustomEvent; + parentMenu?: ContextMenu; + autoopen?: boolean; + title?: string; + extra?: any; } export type ContextMenuItem = IContextMenuItem | null; export type ContextMenuEventListener = ( - value: ContextMenuItem, - options: IContextMenuOptions, - event: MouseEvent, - parentMenu: ContextMenu | undefined, - node: LGraphNode + value: ContextMenuItem, + options: IContextMenuOptions, + event: MouseEvent, + parentMenu: ContextMenu | undefined, + node: LGraphNode ) => boolean | void; export type serializedLGraph< - TNode = ReturnType, - // https://github.com/jagenjo/litegraph.js/issues/74 - TLink = [number, number, number, number, number, string], - TGroup = ReturnType + TNode = ReturnType, + // https://github.com/jagenjo/litegraph.js/issues/74 + TLink = [number, number, number, number, number, string], + TGroup = ReturnType, > = { - last_node_id: LGraph["last_node_id"]; - last_link_id: LGraph["last_link_id"]; - nodes: TNode[]; - links: TLink[]; - groups: TGroup[]; - config: LGraph["config"]; - version: typeof LiteGraph.VERSION; + last_node_id: LGraph["last_node_id"]; + last_link_id: LGraph["last_link_id"]; + nodes: TNode[]; + links: TLink[]; + groups: TGroup[]; + config: LGraph["config"]; + version: typeof LiteGraph.VERSION; }; export declare class LGraph { - static supported_types: string[]; - static STATUS_STOPPED: 1; - static STATUS_RUNNING: 2; + static supported_types: string[]; + static STATUS_STOPPED: 1; + static STATUS_RUNNING: 2; extra: any; - constructor(o?: object); + constructor(o?: object); - filter: string; - catch_errors: boolean; - /** custom data */ - config: object; - elapsed_time: number; - fixedtime: number; - fixedtime_lapse: number; - globaltime: number; - inputs: any; - iteration: number; - last_link_id: number; - last_node_id: number; - last_update_time: number; - links: Record; - list_of_graphcanvas: LGraphCanvas[]; - outputs: any; - runningtime: number; - starttime: number; - status: typeof LGraph.STATUS_RUNNING | typeof LGraph.STATUS_STOPPED; + filter: string; + catch_errors: boolean; + /** custom data */ + config: object; + elapsed_time: number; + fixedtime: number; + fixedtime_lapse: number; + globaltime: number; + inputs: any; + iteration: number; + last_link_id: number; + last_node_id: number; + last_update_time: number; + links: Record; + list_of_graphcanvas: LGraphCanvas[]; + outputs: any; + runningtime: number; + starttime: number; + status: typeof LGraph.STATUS_RUNNING | typeof LGraph.STATUS_STOPPED; - private _nodes: LGraphNode[]; - private _groups: LGraphGroup[]; - private _nodes_by_id: Record; - /** nodes that are executable sorted in execution order */ - private _nodes_executable: - | (LGraphNode & { onExecute: NonNullable }[]) - | null; - /** nodes that contain onExecute */ - private _nodes_in_order: LGraphNode[]; - private _version: number; + private _nodes: LGraphNode[]; + private _groups: LGraphGroup[]; + private _nodes_by_id: Record; + /** nodes that are executable sorted in execution order */ + private _nodes_executable: + | (LGraphNode & { onExecute: NonNullable }[]) + | null; + /** nodes that contain onExecute */ + private _nodes_in_order: LGraphNode[]; + private _version: number; - getSupportedTypes(): string[]; - /** Removes all nodes from this graph */ - clear(): void; - /** Attach Canvas to this graph */ - attachCanvas(graphCanvas: LGraphCanvas): void; - /** Detach Canvas to this graph */ - detachCanvas(graphCanvas: LGraphCanvas): void; - /** - * Starts running this graph every interval milliseconds. - * @param interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate - */ - start(interval?: number): void; - /** Stops the execution loop of the graph */ - stop(): void; - /** - * Run N steps (cycles) of the graph - * @param num number of steps to run, default is 1 - */ - runStep(num?: number, do_not_catch_errors?: boolean): void; - /** - * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than - * nodes with only inputs. - */ - updateExecutionOrder(): void; - /** This is more internal, it computes the executable nodes in order and returns it */ - computeExecutionOrder(only_onExecute: boolean, set_level?: any): T; - /** - * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. - * It doesn't include the node itself - * @return an array with all the LGraphNodes that affect this node, in order of execution - */ - getAncestors(node: LGraphNode): LGraphNode[]; - /** - * Positions every node in a more readable manner - */ - arrange(margin?: number,layout?: string): void; - /** - * Returns the amount of time the graph has been running in milliseconds - * @return number of milliseconds the graph has been running - */ - getTime(): number; + getSupportedTypes(): string[]; + /** Removes all nodes from this graph */ + clear(): void; + /** Attach Canvas to this graph */ + attachCanvas(graphCanvas: LGraphCanvas): void; + /** Detach Canvas to this graph */ + detachCanvas(graphCanvas: LGraphCanvas): void; + /** + * Starts running this graph every interval milliseconds. + * @param interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate + */ + start(interval?: number): void; + /** Stops the execution loop of the graph */ + stop(): void; + /** + * Run N steps (cycles) of the graph + * @param num number of steps to run, default is 1 + */ + runStep(num?: number, do_not_catch_errors?: boolean): void; + /** + * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than + * nodes with only inputs. + */ + updateExecutionOrder(): void; + /** This is more internal, it computes the executable nodes in order and returns it */ + computeExecutionOrder(only_onExecute: boolean, set_level?: any): T; + /** + * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. + * It doesn't include the node itself + * @return an array with all the LGraphNodes that affect this node, in order of execution + */ + getAncestors(node: LGraphNode): LGraphNode[]; + /** + * Positions every node in a more readable manner + */ + arrange(margin?: number, layout?: string): void; + /** + * Returns the amount of time the graph has been running in milliseconds + * @return number of milliseconds the graph has been running + */ + getTime(): number; - /** - * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant - * @return number of milliseconds the graph has been running - */ - getFixedTime(): number; + /** + * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant + * @return number of milliseconds the graph has been running + */ + getFixedTime(): number; - /** - * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct - * if the nodes are using graphical actions - * @return number of milliseconds it took the last cycle - */ - getElapsedTime(): number; - /** - * Sends an event to all the nodes, useful to trigger stuff - * @param eventName the name of the event (function to be called) - * @param params parameters in array format - */ - sendEventToAllNodes(eventName: string, params: any[], mode?: any): void; + /** + * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct + * if the nodes are using graphical actions + * @return number of milliseconds it took the last cycle + */ + getElapsedTime(): number; + /** + * Sends an event to all the nodes, useful to trigger stuff + * @param eventName the name of the event (function to be called) + * @param params parameters in array format + */ + sendEventToAllNodes(eventName: string, params: any[], mode?: any): void; - sendActionToCanvas(action: any, params: any[]): void; - /** - * Adds a new node instance to this graph - * @param node the instance of the node - */ - add(node: LGraphNode, skip_compute_order?: boolean): void; - /** - * Called when a new node is added - * @param node the instance of the node - */ - onNodeAdded(node: LGraphNode): void; - /** Removes a node from the graph */ - remove(node: LGraphNode): void; - /** Returns a node by its id. */ - getNodeById(id: number): LGraphNode | undefined; - /** - * Returns a list of nodes that matches a class - * @param classObject the class itself (not an string) - * @return a list with all the nodes of this type - */ - findNodesByClass( - classObject: LGraphNodeConstructor - ): T[]; - /** - * Returns a list of nodes that matches a type - * @param type the name of the node type - * @return a list with all the nodes of this type - */ - findNodesByType(type: string): T[]; - /** - * Returns the first node that matches a name in its title - * @param title the name of the node to search - * @return the node or null - */ - findNodeByTitle(title: string): T | null; - /** - * Returns a list of nodes that matches a name - * @param title the name of the node to search - * @return a list with all the nodes with this name - */ - findNodesByTitle(title: string): T[]; - /** - * Returns the top-most node in this position of the canvas - * @param x the x coordinate in canvas space - * @param y the y coordinate in canvas space - * @param nodes_list a list with all the nodes to search from, by default is all the nodes in the graph - * @return the node at this position or null - */ - getNodeOnPos( - x: number, - y: number, - node_list?: LGraphNode[], - margin?: number - ): T | null; - /** - * Returns the top-most group in that position - * @param x the x coordinate in canvas space - * @param y the y coordinate in canvas space - * @return the group or null - */ - getGroupOnPos(x: number, y: number): LGraphGroup | null; + sendActionToCanvas(action: any, params: any[]): void; + /** + * Adds a new node instance to this graph + * @param node the instance of the node + */ + add(node: LGraphNode, skip_compute_order?: boolean): void; + /** + * Called when a new node is added + * @param node the instance of the node + */ + onNodeAdded(node: LGraphNode): void; + /** Removes a node from the graph */ + remove(node: LGraphNode): void; + /** Returns a node by its id. */ + getNodeById(id: number): LGraphNode | undefined; + /** + * Returns a list of nodes that matches a class + * @param classObject the class itself (not an string) + * @return a list with all the nodes of this type + */ + findNodesByClass( + classObject: LGraphNodeConstructor + ): T[]; + /** + * Returns a list of nodes that matches a type + * @param type the name of the node type + * @return a list with all the nodes of this type + */ + findNodesByType(type: string): T[]; + /** + * Returns the first node that matches a name in its title + * @param title the name of the node to search + * @return the node or null + */ + findNodeByTitle(title: string): T | null; + /** + * Returns a list of nodes that matches a name + * @param title the name of the node to search + * @return a list with all the nodes with this name + */ + findNodesByTitle(title: string): T[]; + /** + * Returns the top-most node in this position of the canvas + * @param x the x coordinate in canvas space + * @param y the y coordinate in canvas space + * @param nodes_list a list with all the nodes to search from, by default is all the nodes in the graph + * @return the node at this position or null + */ + getNodeOnPos( + x: number, + y: number, + node_list?: LGraphNode[], + margin?: number + ): T | null; + /** + * Returns the top-most group in that position + * @param x the x coordinate in canvas space + * @param y the y coordinate in canvas space + * @return the group or null + */ + getGroupOnPos(x: number, y: number): LGraphGroup | null; - onAction(action: any, param: any): void; - trigger(action: any, param: any): void; - /** Tell this graph it has a global graph input of this type */ - addInput(name: string, type: string, value?: any): void; - /** Assign a data to the global graph input */ - setInputData(name: string, data: any): void; - /** Returns the current value of a global graph input */ - getInputData(name: string): T; - /** Changes the name of a global graph input */ - renameInput(old_name: string, name: string): false | undefined; - /** Changes the type of a global graph input */ - changeInputType(name: string, type: string): false | undefined; - /** Removes a global graph input */ - removeInput(name: string): boolean; - /** Creates a global graph output */ - addOutput(name: string, type: string, value: any): void; - /** Assign a data to the global output */ - setOutputData(name: string, value: string): void; - /** Returns the current value of a global graph output */ - getOutputData(name: string): T; + onAction(action: any, param: any): void; + trigger(action: any, param: any): void; + /** Tell this graph it has a global graph input of this type */ + addInput(name: string, type: string, value?: any): void; + /** Assign a data to the global graph input */ + setInputData(name: string, data: any): void; + /** Returns the current value of a global graph input */ + getInputData(name: string): T; + /** Changes the name of a global graph input */ + renameInput(old_name: string, name: string): false | undefined; + /** Changes the type of a global graph input */ + changeInputType(name: string, type: string): false | undefined; + /** Removes a global graph input */ + removeInput(name: string): boolean; + /** Creates a global graph output */ + addOutput(name: string, type: string, value: any): void; + /** Assign a data to the global output */ + setOutputData(name: string, value: string): void; + /** Returns the current value of a global graph output */ + getOutputData(name: string): T; - /** Renames a global graph output */ - renameOutput(old_name: string, name: string): false | undefined; - /** Changes the type of a global graph output */ - changeOutputType(name: string, type: string): false | undefined; - /** Removes a global graph output */ - removeOutput(name: string): boolean; - triggerInput(name: string, value: any): void; - setCallback(name: string, func: (...args: any[]) => any): void; - beforeChange(info?: LGraphNode): void; - afterChange(info?: LGraphNode): void; - connectionChange(node: LGraphNode): void; - /** returns if the graph is in live mode */ - isLive(): boolean; - /** clears the triggered slot animation in all links (stop visual animation) */ - clearTriggeredSlots(): void; - /* Called when something visually changed (not the graph!) */ - change(): void; - setDirtyCanvas(fg: boolean, bg?: boolean): void; - /** Destroys a link */ - removeLink(link_id: number): void; - /** Creates a Object containing all the info about this graph, it can be serialized */ - serialize(): T; - /** - * Configure a graph from a JSON string - * @param data configure a graph from a JSON string - * @returns if there was any error parsing - */ - configure(data: object, keep_old?: boolean): boolean | undefined; - load(url: string): void; + /** Renames a global graph output */ + renameOutput(old_name: string, name: string): false | undefined; + /** Changes the type of a global graph output */ + changeOutputType(name: string, type: string): false | undefined; + /** Removes a global graph output */ + removeOutput(name: string): boolean; + triggerInput(name: string, value: any): void; + setCallback(name: string, func: (...args: any[]) => any): void; + beforeChange(info?: LGraphNode): void; + afterChange(info?: LGraphNode): void; + connectionChange(node: LGraphNode): void; + /** returns if the graph is in live mode */ + isLive(): boolean; + /** clears the triggered slot animation in all links (stop visual animation) */ + clearTriggeredSlots(): void; + /* Called when something visually changed (not the graph!) */ + change(): void; + setDirtyCanvas(fg: boolean, bg?: boolean): void; + /** Destroys a link */ + removeLink(link_id: number): void; + /** Creates a Object containing all the info about this graph, it can be serialized */ + serialize(): T; + /** + * Configure a graph from a JSON string + * @param data configure a graph from a JSON string + * @returns if there was any error parsing + */ + configure(data: object, keep_old?: boolean): boolean | undefined; + load(url: string): void; } export type SerializedLLink = [number, string, number, number, number, number]; export declare class LLink { - id: number; - type: string; - origin_id: number; - origin_slot: number; - target_id: number; - target_slot: number; - constructor( - id: number, - type: string, - origin_id: number, - origin_slot: number, - target_id: number, - target_slot: number - ); - configure(o: LLink | SerializedLLink): void; - serialize(): SerializedLLink; + id: number; + type: string; + origin_id: number; + origin_slot: number; + target_id: number; + target_slot: number; + constructor( + id: number, + type: string, + origin_id: number, + origin_slot: number, + target_id: number, + target_slot: number + ); + configure(o: LLink | SerializedLLink): void; + serialize(): SerializedLLink; } export type SerializedLGraphNode = { - id: T["id"]; - type: T["type"]; - pos: T["pos"]; - size: T["size"]; - flags: T["flags"]; - mode: T["mode"]; - inputs: T["inputs"]; - outputs: T["outputs"]; - title: T["title"]; - properties: T["properties"]; - widgets_values?: IWidget["value"][]; + id: T["id"]; + type: T["type"]; + pos: T["pos"]; + size: T["size"]; + flags: T["flags"]; + mode: T["mode"]; + inputs: T["inputs"]; + outputs: T["outputs"]; + title: T["title"]; + properties: T["properties"]; + widgets_values?: IWidget["value"][]; }; /** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */ export declare class LGraphNode { onResize?: Function; - // Used in group node + // Used in group node setInnerNodes(nodes: any) { throw new Error("Method not implemented."); } - static title_color: string; - static title: string; - static type: null | string; - static widgets_up: boolean; - constructor(title?: string); + static title_color: string; + static title: string; + static type: null | string; + static widgets_up: boolean; + constructor(title?: string); - title: string; - type: null | string; - size: Vector2; - graph: null | LGraph; - graph_version: number; + title: string; + type: null | string; + size: Vector2; + graph: null | LGraph; + graph_version: number; + pos: Vector2; + is_selected: boolean; + mouseOver: boolean; + + id: number; + + //inputs available: array of inputs + inputs: INodeInputSlot[]; + outputs: INodeOutputSlot[]; + connections: any[]; + + //local data + properties: Record; + properties_info: any[]; + + flags: Partial<{ + collapsed: boolean; + }>; + + color: string; + bgcolor: string; + boxcolor: string; + shape: + | typeof LiteGraph.BOX_SHAPE + | typeof LiteGraph.ROUND_SHAPE + | typeof LiteGraph.CIRCLE_SHAPE + | typeof LiteGraph.CARD_SHAPE + | typeof LiteGraph.ARROW_SHAPE; + + serialize_widgets: boolean; + skip_list: boolean; + + /** Used in `LGraphCanvas.onMenuNodeMode` */ + mode?: + | typeof LiteGraph.ON_EVENT + | typeof LiteGraph.ON_TRIGGER + | typeof LiteGraph.NEVER + | typeof LiteGraph.ALWAYS; + + widgets?: IWidget[]; + + /** If set to true widgets do not start after the slots */ + widgets_up: boolean; + /** widgets start at y distance from the top of the node */ + widgets_start_y: number; + /** if you render outside the node, it will be clipped */ + clip_area: boolean; + /** if set to false it wont be resizable with the mouse */ + resizable: boolean; + /** slots are distributed horizontally */ + horizontal: boolean; + /** if true, the node will show the bgcolor as 'red' */ + has_errors?: boolean; + + /** configure a node from an object containing the serialized info */ + configure(info: SerializedLGraphNode): void; + /** serialize the content */ + serialize(): SerializedLGraphNode; + /** Creates a clone of this node */ + clone(): this; + /** serialize and stringify */ + toString(): string; + /** get the title string */ + getTitle(): string; + /** sets the value of a property */ + setProperty(name: string, value: any): void; + /** sets the output data */ + setOutputData(slot: number, data: any): void; + /** sets the output data */ + setOutputDataType(slot: number, type: string): void; + /** + * Retrieves the input data (data traveling through the connection) from one slot + * @param slot + * @param force_update if set to true it will force the connected node of this slot to output data into this link + * @return data or if it is not connected returns undefined + */ + getInputData(slot: number, force_update?: boolean): T; + /** + * Retrieves the input data type (in case this supports multiple input types) + * @param slot + * @return datatype in string format + */ + getInputDataType(slot: number): string; + /** + * Retrieves the input data from one slot using its name instead of slot number + * @param slot_name + * @param force_update if set to true it will force the connected node of this slot to output data into this link + * @return data or if it is not connected returns null + */ + getInputDataByName(slot_name: string, force_update?: boolean): T; + /** tells you if there is a connection in one input slot */ + isInputConnected(slot: number): boolean; + /** tells you info about an input connection (which node, type, etc) */ + getInputInfo( + slot: number + ): { link: number; name: string; type: string | 0 } | null; + /** returns the node connected in the input slot */ + getInputNode(slot: number): LGraphNode | null; + /** returns the value of an input with this name, otherwise checks if there is a property with that name */ + getInputOrProperty(name: string): T; + /** tells you the last output data that went in that slot */ + getOutputData(slot: number): T | null; + /** tells you info about an output connection (which node, type, etc) */ + getOutputInfo( + slot: number + ): { name: string; type: string; links: number[] } | null; + /** tells you if there is a connection in one output slot */ + isOutputConnected(slot: number): boolean; + /** tells you if there is any connection in the output slots */ + isAnyOutputConnected(): boolean; + /** retrieves all the nodes connected to this output slot */ + getOutputNodes(slot: number): LGraphNode[]; + /** Triggers an event in this node, this will trigger any output with the same name */ + trigger(action: string, param: any): void; + /** + * Triggers an slot event in this node + * @param slot the index of the output slot + * @param param + * @param link_id in case you want to trigger and specific output link in a slot + */ + triggerSlot(slot: number, param: any, link_id?: number): void; + /** + * clears the trigger slot animation + * @param slot the index of the output slot + * @param link_id in case you want to trigger and specific output link in a slot + */ + clearTriggeredSlot(slot: number, link_id?: number): void; + /** + * add a new property to this node + * @param name + * @param default_value + * @param type string defining the output type ("vec3","number",...) + * @param extra_info this can be used to have special properties of the property (like values, etc) + */ + addProperty( + name: string, + default_value: any, + type: string, + extra_info?: object + ): T; + /** + * add a new output slot to use in this node + * @param name + * @param type string defining the output type ("vec3","number",...) + * @param extra_info this can be used to have special properties of an output (label, special color, position, etc) + */ + addOutput( + name: string, + type: string | -1, + extra_info?: Partial + ): INodeOutputSlot; + /** + * add a new output slot to use in this node + * @param array of triplets like [[name,type,extra_info],[...]] + */ + addOutputs( + array: [string, string | -1, Partial | undefined][] + ): void; + /** remove an existing output slot */ + removeOutput(slot: number): void; + /** + * add a new input slot to use in this node + * @param name + * @param type string defining the input type ("vec3","number",...), it its a generic one use 0 + * @param extra_info this can be used to have special properties of an input (label, color, position, etc) + */ + addInput( + name: string, + type: string | -1, + extra_info?: Partial + ): INodeInputSlot; + /** + * add several new input slots in this node + * @param array of triplets like [[name,type,extra_info],[...]] + */ + addInputs( + array: [string, string | -1, Partial | undefined][] + ): void; + /** remove an existing input slot */ + removeInput(slot: number): void; + /** + * add an special connection to this node (used for special kinds of graphs) + * @param name + * @param type string defining the input type ("vec3","number",...) + * @param pos position of the connection inside the node + * @param direction if is input or output + */ + addConnection( + name: string, + type: string, + pos: Vector2, + direction: string + ): { + name: string; + type: string; pos: Vector2; - is_selected: boolean; - mouseOver: boolean; + direction: string; + links: null; + }; + setValue(v: any): void; + /** computes the size of a node according to its inputs and output slots */ + computeSize(): [number, number]; + /** + * https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#node-widgets + * @return created widget + */ + addWidget( + type: T["type"], + name: string, + value: T["value"], + callback?: WidgetCallback | string, + options?: T["options"] + ): T; - id: number; + addCustomWidget(customWidget: T): T; - //inputs available: array of inputs - inputs: INodeInputSlot[]; - outputs: INodeOutputSlot[]; - connections: any[]; + /** + * returns the bounding of the object, used for rendering purposes + * @return [x, y, width, height] + */ + getBounding(): Vector4; + /** checks if a point is inside the shape of a node */ + isPointInside( + x: number, + y: number, + margin?: number, + skipTitle?: boolean + ): boolean; + /** checks if a point is inside a node slot, and returns info about which slot */ + getSlotInPosition( + x: number, + y: number + ): { + input?: INodeInputSlot; + output?: INodeOutputSlot; + slot: number; + link_pos: Vector2; + }; + /** + * returns the input slot with a given name (used for dynamic slots), -1 if not found + * @param name the name of the slot + * @return the slot (-1 if not found) + */ + findInputSlot(name: string): number; + /** + * returns the output slot with a given name (used for dynamic slots), -1 if not found + * @param name the name of the slot + * @return the slot (-1 if not found) + */ + findOutputSlot(name: string): number; + /** + * connect this node output to the input of another node + * @param slot (could be the number of the slot or the string with the name of the slot) + * @param targetNode the target node + * @param targetSlot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) + * @return {Object} the link_info is created, otherwise null + */ + connect( + slot: number | string, + targetNode: LGraphNode, + targetSlot: number | string + ): T | null; + /** + * disconnect one output to an specific node + * @param slot (could be the number of the slot or the string with the name of the slot) + * @param target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] + * @return if it was disconnected successfully + */ + disconnectOutput(slot: number | string, targetNode?: LGraphNode): boolean; + /** + * disconnect one input + * @param slot (could be the number of the slot or the string with the name of the slot) + * @return if it was disconnected successfully + */ + disconnectInput(slot: number | string): boolean; + /** + * returns the center of a connection point in canvas coords + * @param is_input true if if a input slot, false if it is an output + * @param slot (could be the number of the slot or the string with the name of the slot) + * @param out a place to store the output, to free garbage + * @return the position + **/ + getConnectionPos( + is_input: boolean, + slot: number | string, + out?: Vector2 + ): Vector2; + /** Force align to grid */ + alignToGrid(): void; + /** Console output */ + trace(msg: string): void; + /** Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ + setDirtyCanvas(fg: boolean, bg: boolean): void; + loadImage(url: string): void; + /** Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ + captureInput(v: any): void; + /** Collapse the node to make it smaller on the canvas */ + collapse(force: boolean): void; + /** Forces the node to do not move or realign on Z */ + pin(v?: boolean): void; + localToScreen(x: number, y: number, graphCanvas: LGraphCanvas): Vector2; - //local data - properties: Record; - properties_info: any[]; + // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-appearance + onDrawBackground?( + ctx: CanvasRenderingContext2D, + canvas: HTMLCanvasElement + ): void; + onDrawForeground?( + ctx: CanvasRenderingContext2D, + canvas: HTMLCanvasElement + ): void; - flags: Partial<{ - collapsed: boolean - }>; + // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-behaviour + onMouseDown?( + event: MouseEvent, + pos: Vector2, + graphCanvas: LGraphCanvas + ): void; + onMouseMove?( + event: MouseEvent, + pos: Vector2, + graphCanvas: LGraphCanvas + ): void; + onMouseUp?(event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas): void; + onMouseEnter?( + event: MouseEvent, + pos: Vector2, + graphCanvas: LGraphCanvas + ): void; + onMouseLeave?( + event: MouseEvent, + pos: Vector2, + graphCanvas: LGraphCanvas + ): void; + onKey?(event: KeyboardEvent, pos: Vector2, graphCanvas: LGraphCanvas): void; - color: string; - bgcolor: string; - boxcolor: string; - shape: - | typeof LiteGraph.BOX_SHAPE - | typeof LiteGraph.ROUND_SHAPE - | typeof LiteGraph.CIRCLE_SHAPE - | typeof LiteGraph.CARD_SHAPE - | typeof LiteGraph.ARROW_SHAPE; + /** Called by `LGraphCanvas.selectNodes` */ + onSelected?(): void; + /** Called by `LGraphCanvas.deselectNode` */ + onDeselected?(): void; + /** Called by `LGraph.runStep` `LGraphNode.getInputData` */ + onExecute?(): void; + /** Called by `LGraph.serialize` */ + onSerialize?(o: SerializedLGraphNode): void; + /** Called by `LGraph.configure` */ + onConfigure?(o: SerializedLGraphNode): void; + /** + * when added to graph (warning: this is called BEFORE the node is configured when loading) + * Called by `LGraph.add` + */ + onAdded?(graph: LGraph): void; + /** + * when removed from graph + * Called by `LGraph.remove` `LGraph.clear` + */ + onRemoved?(): void; + /** + * if returns false the incoming connection will be canceled + * Called by `LGraph.connect` + * @param inputIndex target input slot number + * @param outputType type of output slot + * @param outputSlot output slot object + * @param outputNode node containing the output + * @param outputIndex index of output slot + */ + onConnectInput?( + inputIndex: number, + outputType: INodeOutputSlot["type"], + outputSlot: INodeOutputSlot, + outputNode: LGraphNode, + outputIndex: number + ): boolean; + /** + * if returns false the incoming connection will be canceled + * Called by `LGraph.connect` + * @param outputIndex target output slot number + * @param inputType type of input slot + * @param inputSlot input slot object + * @param inputNode node containing the input + * @param inputIndex index of input slot + */ + onConnectOutput?( + outputIndex: number, + inputType: INodeInputSlot["type"], + inputSlot: INodeInputSlot, + inputNode: LGraphNode, + inputIndex: number + ): boolean; - serialize_widgets: boolean; - skip_list: boolean; + /** + * Called just before connection (or disconnect - if input is linked). + * A convenient place to switch to another input, or create new one. + * This allow for ability to automatically add slots if needed + * @param inputIndex + * @return selected input slot index, can differ from parameter value + */ + onBeforeConnectInput?(inputIndex: number): number; - /** Used in `LGraphCanvas.onMenuNodeMode` */ - mode?: - | typeof LiteGraph.ON_EVENT - | typeof LiteGraph.ON_TRIGGER - | typeof LiteGraph.NEVER - | typeof LiteGraph.ALWAYS; + /** a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info or output_info ) */ + onConnectionsChange( + type: number, + slotIndex: number, + isConnected: boolean, + link: LLink, + ioSlot: INodeOutputSlot | INodeInputSlot + ): void; - widgets?: IWidget[]; + /** + * if returns false, will abort the `LGraphNode.setProperty` + * Called when a property is changed + * @param property + * @param value + * @param prevValue + */ + onPropertyChanged?( + property: string, + value: any, + prevValue: any + ): void | boolean; - /** If set to true widgets do not start after the slots */ - widgets_up: boolean; - /** widgets start at y distance from the top of the node */ - widgets_start_y: number; - /** if you render outside the node, it will be clipped */ - clip_area: boolean; - /** if set to false it wont be resizable with the mouse */ - resizable: boolean; - /** slots are distributed horizontally */ - horizontal: boolean; - /** if true, the node will show the bgcolor as 'red' */ - has_errors?: boolean; - - /** configure a node from an object containing the serialized info */ - configure(info: SerializedLGraphNode): void; - /** serialize the content */ - serialize(): SerializedLGraphNode; - /** Creates a clone of this node */ - clone(): this; - /** serialize and stringify */ - toString(): string; - /** get the title string */ - getTitle(): string; - /** sets the value of a property */ - setProperty(name: string, value: any): void; - /** sets the output data */ - setOutputData(slot: number, data: any): void; - /** sets the output data */ - setOutputDataType(slot: number, type: string): void; - /** - * Retrieves the input data (data traveling through the connection) from one slot - * @param slot - * @param force_update if set to true it will force the connected node of this slot to output data into this link - * @return data or if it is not connected returns undefined - */ - getInputData(slot: number, force_update?: boolean): T; - /** - * Retrieves the input data type (in case this supports multiple input types) - * @param slot - * @return datatype in string format - */ - getInputDataType(slot: number): string; - /** - * Retrieves the input data from one slot using its name instead of slot number - * @param slot_name - * @param force_update if set to true it will force the connected node of this slot to output data into this link - * @return data or if it is not connected returns null - */ - getInputDataByName(slot_name: string, force_update?: boolean): T; - /** tells you if there is a connection in one input slot */ - isInputConnected(slot: number): boolean; - /** tells you info about an input connection (which node, type, etc) */ - getInputInfo( - slot: number - ): { link: number; name: string; type: string | 0 } | null; - /** returns the node connected in the input slot */ - getInputNode(slot: number): LGraphNode | null; - /** returns the value of an input with this name, otherwise checks if there is a property with that name */ - getInputOrProperty(name: string): T; - /** tells you the last output data that went in that slot */ - getOutputData(slot: number): T | null; - /** tells you info about an output connection (which node, type, etc) */ - getOutputInfo( - slot: number - ): { name: string; type: string; links: number[] } | null; - /** tells you if there is a connection in one output slot */ - isOutputConnected(slot: number): boolean; - /** tells you if there is any connection in the output slots */ - isAnyOutputConnected(): boolean; - /** retrieves all the nodes connected to this output slot */ - getOutputNodes(slot: number): LGraphNode[]; - /** Triggers an event in this node, this will trigger any output with the same name */ - trigger(action: string, param: any): void; - /** - * Triggers an slot event in this node - * @param slot the index of the output slot - * @param param - * @param link_id in case you want to trigger and specific output link in a slot - */ - triggerSlot(slot: number, param: any, link_id?: number): void; - /** - * clears the trigger slot animation - * @param slot the index of the output slot - * @param link_id in case you want to trigger and specific output link in a slot - */ - clearTriggeredSlot(slot: number, link_id?: number): void; - /** - * add a new property to this node - * @param name - * @param default_value - * @param type string defining the output type ("vec3","number",...) - * @param extra_info this can be used to have special properties of the property (like values, etc) - */ - addProperty( - name: string, - default_value: any, - type: string, - extra_info?: object - ): T; - /** - * add a new output slot to use in this node - * @param name - * @param type string defining the output type ("vec3","number",...) - * @param extra_info this can be used to have special properties of an output (label, special color, position, etc) - */ - addOutput( - name: string, - type: string | -1, - extra_info?: Partial - ): INodeOutputSlot; - /** - * add a new output slot to use in this node - * @param array of triplets like [[name,type,extra_info],[...]] - */ - addOutputs( - array: [string, string | -1, Partial | undefined][] - ): void; - /** remove an existing output slot */ - removeOutput(slot: number): void; - /** - * add a new input slot to use in this node - * @param name - * @param type string defining the input type ("vec3","number",...), it its a generic one use 0 - * @param extra_info this can be used to have special properties of an input (label, color, position, etc) - */ - addInput( - name: string, - type: string | -1, - extra_info?: Partial - ): INodeInputSlot; - /** - * add several new input slots in this node - * @param array of triplets like [[name,type,extra_info],[...]] - */ - addInputs( - array: [string, string | -1, Partial | undefined][] - ): void; - /** remove an existing input slot */ - removeInput(slot: number): void; - /** - * add an special connection to this node (used for special kinds of graphs) - * @param name - * @param type string defining the input type ("vec3","number",...) - * @param pos position of the connection inside the node - * @param direction if is input or output - */ - addConnection( - name: string, - type: string, - pos: Vector2, - direction: string - ): { - name: string; - type: string; - pos: Vector2; - direction: string; - links: null; - }; - setValue(v: any): void; - /** computes the size of a node according to its inputs and output slots */ - computeSize(): [number, number]; - /** - * https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#node-widgets - * @return created widget - */ - addWidget( - type: T["type"], - name: string, - value: T["value"], - callback?: WidgetCallback | string, - options?: T["options"] - ): T; - - addCustomWidget(customWidget: T): T; - - /** - * returns the bounding of the object, used for rendering purposes - * @return [x, y, width, height] - */ - getBounding(): Vector4; - /** checks if a point is inside the shape of a node */ - isPointInside( - x: number, - y: number, - margin?: number, - skipTitle?: boolean - ): boolean; - /** checks if a point is inside a node slot, and returns info about which slot */ - getSlotInPosition( - x: number, - y: number - ): { - input?: INodeInputSlot; - output?: INodeOutputSlot; - slot: number; - link_pos: Vector2; - }; - /** - * returns the input slot with a given name (used for dynamic slots), -1 if not found - * @param name the name of the slot - * @return the slot (-1 if not found) - */ - findInputSlot(name: string): number; - /** - * returns the output slot with a given name (used for dynamic slots), -1 if not found - * @param name the name of the slot - * @return the slot (-1 if not found) - */ - findOutputSlot(name: string): number; - /** - * connect this node output to the input of another node - * @param slot (could be the number of the slot or the string with the name of the slot) - * @param targetNode the target node - * @param targetSlot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) - * @return {Object} the link_info is created, otherwise null - */ - connect( - slot: number | string, - targetNode: LGraphNode, - targetSlot: number | string - ): T | null; - /** - * disconnect one output to an specific node - * @param slot (could be the number of the slot or the string with the name of the slot) - * @param target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] - * @return if it was disconnected successfully - */ - disconnectOutput(slot: number | string, targetNode?: LGraphNode): boolean; - /** - * disconnect one input - * @param slot (could be the number of the slot or the string with the name of the slot) - * @return if it was disconnected successfully - */ - disconnectInput(slot: number | string): boolean; - /** - * returns the center of a connection point in canvas coords - * @param is_input true if if a input slot, false if it is an output - * @param slot (could be the number of the slot or the string with the name of the slot) - * @param out a place to store the output, to free garbage - * @return the position - **/ - getConnectionPos( - is_input: boolean, - slot: number | string, - out?: Vector2 - ): Vector2; - /** Force align to grid */ - alignToGrid(): void; - /** Console output */ - trace(msg: string): void; - /** Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ - setDirtyCanvas(fg: boolean, bg: boolean): void; - loadImage(url: string): void; - /** Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ - captureInput(v: any): void; - /** Collapse the node to make it smaller on the canvas */ - collapse(force: boolean): void; - /** Forces the node to do not move or realign on Z */ - pin(v?: boolean): void; - localToScreen(x: number, y: number, graphCanvas: LGraphCanvas): Vector2; - - // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-appearance - onDrawBackground?( - ctx: CanvasRenderingContext2D, - canvas: HTMLCanvasElement - ): void; - onDrawForeground?( - ctx: CanvasRenderingContext2D, - canvas: HTMLCanvasElement - ): void; - - // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-behaviour - onMouseDown?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseMove?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseUp?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseEnter?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseLeave?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onKey?(event: KeyboardEvent, pos: Vector2, graphCanvas: LGraphCanvas): void; - - /** Called by `LGraphCanvas.selectNodes` */ - onSelected?(): void; - /** Called by `LGraphCanvas.deselectNode` */ - onDeselected?(): void; - /** Called by `LGraph.runStep` `LGraphNode.getInputData` */ - onExecute?(): void; - /** Called by `LGraph.serialize` */ - onSerialize?(o: SerializedLGraphNode): void; - /** Called by `LGraph.configure` */ - onConfigure?(o: SerializedLGraphNode): void; - /** - * when added to graph (warning: this is called BEFORE the node is configured when loading) - * Called by `LGraph.add` - */ - onAdded?(graph: LGraph): void; - /** - * when removed from graph - * Called by `LGraph.remove` `LGraph.clear` - */ - onRemoved?(): void; - /** - * if returns false the incoming connection will be canceled - * Called by `LGraph.connect` - * @param inputIndex target input slot number - * @param outputType type of output slot - * @param outputSlot output slot object - * @param outputNode node containing the output - * @param outputIndex index of output slot - */ - onConnectInput?( - inputIndex: number, - outputType: INodeOutputSlot["type"], - outputSlot: INodeOutputSlot, - outputNode: LGraphNode, - outputIndex: number - ): boolean; - /** - * if returns false the incoming connection will be canceled - * Called by `LGraph.connect` - * @param outputIndex target output slot number - * @param inputType type of input slot - * @param inputSlot input slot object - * @param inputNode node containing the input - * @param inputIndex index of input slot - */ - onConnectOutput?( - outputIndex: number, - inputType: INodeInputSlot["type"], - inputSlot: INodeInputSlot, - inputNode: LGraphNode, - inputIndex: number - ): boolean; - - /** - * Called just before connection (or disconnect - if input is linked). - * A convenient place to switch to another input, or create new one. - * This allow for ability to automatically add slots if needed - * @param inputIndex - * @return selected input slot index, can differ from parameter value - */ - onBeforeConnectInput?( - inputIndex: number - ): number; - - /** a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info or output_info ) */ - onConnectionsChange( - type: number, - slotIndex: number, - isConnected: boolean, - link: LLink, - ioSlot: (INodeOutputSlot | INodeInputSlot) - ): void; - - /** - * if returns false, will abort the `LGraphNode.setProperty` - * Called when a property is changed - * @param property - * @param value - * @param prevValue - */ - onPropertyChanged?(property: string, value: any, prevValue: any): void | boolean; - - /** Called by `LGraphCanvas.processContextMenu` */ - getMenuOptions?(graphCanvas: LGraphCanvas): ContextMenuItem[]; - getSlotMenuOptions?(slot: INodeSlot): ContextMenuItem[]; + /** Called by `LGraphCanvas.processContextMenu` */ + getMenuOptions?(graphCanvas: LGraphCanvas): ContextMenuItem[]; + getSlotMenuOptions?(slot: INodeSlot): ContextMenuItem[]; } export type LGraphNodeConstructor = { - nodeData: any; // Used by group node. - new (): T; + nodeData: any; // Used by group node. + new (): T; }; export type SerializedLGraphGroup = { - title: LGraphGroup["title"]; - bounding: LGraphGroup["_bounding"]; - color: LGraphGroup["color"]; - font: LGraphGroup["font"]; + title: LGraphGroup["title"]; + bounding: LGraphGroup["_bounding"]; + color: LGraphGroup["color"]; + font: LGraphGroup["font"]; }; export declare class LGraphGroup { - title: string; - private _bounding: Vector4; - color: string; - font: string; + title: string; + private _bounding: Vector4; + color: string; + font: string; - configure(o: SerializedLGraphGroup): void; - serialize(): SerializedLGraphGroup; - move(deltaX: number, deltaY: number, ignoreNodes?: boolean): void; - recomputeInsideNodes(): void; - isPointInside: LGraphNode["isPointInside"]; - setDirtyCanvas: LGraphNode["setDirtyCanvas"]; + configure(o: SerializedLGraphGroup): void; + serialize(): SerializedLGraphGroup; + move(deltaX: number, deltaY: number, ignoreNodes?: boolean): void; + recomputeInsideNodes(): void; + isPointInside: LGraphNode["isPointInside"]; + setDirtyCanvas: LGraphNode["setDirtyCanvas"]; } export declare class DragAndScale { - constructor(element?: HTMLElement, skipEvents?: boolean); - offset: [number, number]; - scale: number; - max_scale: number; - min_scale: number; - onredraw: Function | null; - enabled: boolean; - last_mouse: Vector2; - element: HTMLElement | null; - visible_area: Vector4; - bindEvents(element: HTMLElement): void; - computeVisibleArea(): void; - onMouse(e: MouseEvent): void; - toCanvasContext(ctx: CanvasRenderingContext2D): void; - convertOffsetToCanvas(pos: Vector2): Vector2; - convertCanvasToOffset(pos: Vector2): Vector2; - mouseDrag(x: number, y: number): void; - changeScale(value: number, zooming_center?: Vector2): void; - changeDeltaScale(value: number, zooming_center?: Vector2): void; - reset(): void; + constructor(element?: HTMLElement, skipEvents?: boolean); + offset: [number, number]; + scale: number; + max_scale: number; + min_scale: number; + onredraw: Function | null; + enabled: boolean; + last_mouse: Vector2; + element: HTMLElement | null; + visible_area: Vector4; + bindEvents(element: HTMLElement): void; + computeVisibleArea(): void; + onMouse(e: MouseEvent): void; + toCanvasContext(ctx: CanvasRenderingContext2D): void; + convertOffsetToCanvas(pos: Vector2): Vector2; + convertCanvasToOffset(pos: Vector2): Vector2; + mouseDrag(x: number, y: number): void; + changeScale(value: number, zooming_center?: Vector2): void; + changeDeltaScale(value: number, zooming_center?: Vector2): void; + reset(): void; } /** @@ -913,621 +905,628 @@ export declare class DragAndScale { * @param options { skip_rendering, autoresize } */ export declare class LGraphCanvas { - static node_colors: Record< - string, - { - color: string; - bgcolor: string; - groupcolor: string; - } - >; - static link_type_colors: Record; - static gradients: object; - static search_limit: number; + static node_colors: Record< + string, + { + color: string; + bgcolor: string; + groupcolor: string; + } + >; + static link_type_colors: Record; + static gradients: object; + static search_limit: number; - static getFileExtension(url: string): string; - static decodeHTML(str: string): string; + static getFileExtension(url: string): string; + static decodeHTML(str: string): string; - static onMenuCollapseAll(): void; - static onMenuNodeEdit(): void; - static onShowPropertyEditor( - item: any, - options: any, - e: any, - menu: any, - node: any - ): void; - /** Create menu for `Add Group` */ - static onGroupAdd: ContextMenuEventListener; - /** Create menu for `Add Node` */ - static onMenuAdd: ContextMenuEventListener; - static showMenuNodeOptionalInputs: ContextMenuEventListener; - static showMenuNodeOptionalOutputs: ContextMenuEventListener; - static onShowMenuNodeProperties: ContextMenuEventListener; - static onResizeNode: ContextMenuEventListener; - static onMenuNodeCollapse: ContextMenuEventListener; - static onMenuNodePin: ContextMenuEventListener; - static onMenuNodeMode: ContextMenuEventListener; - static onMenuNodeColors: ContextMenuEventListener; - static onMenuNodeShapes: ContextMenuEventListener; - static onMenuNodeRemove: ContextMenuEventListener; - static onMenuNodeClone: ContextMenuEventListener; + static onMenuCollapseAll(): void; + static onMenuNodeEdit(): void; + static onShowPropertyEditor( + item: any, + options: any, + e: any, + menu: any, + node: any + ): void; + /** Create menu for `Add Group` */ + static onGroupAdd: ContextMenuEventListener; + /** Create menu for `Add Node` */ + static onMenuAdd: ContextMenuEventListener; + static showMenuNodeOptionalInputs: ContextMenuEventListener; + static showMenuNodeOptionalOutputs: ContextMenuEventListener; + static onShowMenuNodeProperties: ContextMenuEventListener; + static onResizeNode: ContextMenuEventListener; + static onMenuNodeCollapse: ContextMenuEventListener; + static onMenuNodePin: ContextMenuEventListener; + static onMenuNodeMode: ContextMenuEventListener; + static onMenuNodeColors: ContextMenuEventListener; + static onMenuNodeShapes: ContextMenuEventListener; + static onMenuNodeRemove: ContextMenuEventListener; + static onMenuNodeClone: ContextMenuEventListener; - constructor( - canvas: HTMLCanvasElement | string, - graph?: LGraph, - options?: { - skip_render?: boolean; - autoresize?: boolean; - } - ); + constructor( + canvas: HTMLCanvasElement | string, + graph?: LGraph, + options?: { + skip_render?: boolean; + autoresize?: boolean; + } + ); - static active_canvas: HTMLCanvasElement; - - allow_dragcanvas: boolean; - allow_dragnodes: boolean; - /** allow to control widgets, buttons, collapse, etc */ - allow_interaction: boolean; - /** allows to change a connection with having to redo it again */ - allow_reconnect_links: boolean; - /** allow selecting multi nodes without pressing extra keys */ - multi_select: boolean; - /** No effect */ - allow_searchbox: boolean; - always_render_background: boolean; - autoresize?: boolean; - background_image: string; - bgcanvas: HTMLCanvasElement; - bgctx: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; - canvas_mouse: Vector2; - clear_background: boolean; - connecting_node: LGraphNode | null; - connections_width: number; - ctx: CanvasRenderingContext2D; - current_node: LGraphNode | null; - default_connection_color: { - input_off: string; - input_on: string; - output_off: string; - output_on: string; - }; - default_link_color: string; - dirty_area: Vector4 | null; - dirty_bgcanvas?: boolean; - dirty_canvas?: boolean; - drag_mode: boolean; - dragging_canvas: boolean; - dragging_rectangle: Vector4 | null; - ds: DragAndScale; - /** used for transition */ - editor_alpha: number; - filter: any; - fps: number; - frame: number; - graph: LGraph; - highlighted_links: Record; - highquality_render: boolean; - inner_text_font: string; - is_rendering: boolean; - last_draw_time: number; - last_mouse: Vector2; - /** - * Possible duplicated with `last_mouse` - * https://github.com/jagenjo/litegraph.js/issues/70 - */ - last_mouse_position: Vector2; - /** Timestamp of last mouse click, defaults to 0 */ - last_mouseclick: number; - links_render_mode: - | typeof LiteGraph.STRAIGHT_LINK - | typeof LiteGraph.LINEAR_LINK - | typeof LiteGraph.SPLINE_LINK; - live_mode: boolean; - node_capturing_input: LGraphNode | null; - node_dragged: LGraphNode | null; - node_in_panel: LGraphNode | null; - node_over: LGraphNode | null; - node_title_color: string; - node_widget: [LGraphNode, IWidget] | null; - last_mouse_dragging: boolean; + static active_canvas: HTMLCanvasElement; - /** Called by `LGraphCanvas.drawBackCanvas` */ - onDrawBackground: - | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) - | null; - /** Called by `LGraphCanvas.drawFrontCanvas` */ - onDrawForeground: - | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) - | null; - onDrawOverlay: ((ctx: CanvasRenderingContext2D) => void) | null; - /** Called by `LGraphCanvas.processMouseDown` */ - onMouse: ((event: MouseEvent) => boolean) | null; - /** Called by `LGraphCanvas.drawFrontCanvas` and `LGraphCanvas.drawLinkTooltip` */ - onDrawLinkTooltip: ((ctx: CanvasRenderingContext2D, link: LLink, _this: this) => void) | null; - /** Called by `LGraphCanvas.selectNodes` */ - onNodeMoved: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.processNodeSelected` */ - onNodeSelected: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.deselectNode` */ - onNodeDeselected: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.processNodeDblClicked` */ - onShowNodePanel: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.processNodeDblClicked` */ - onNodeDblClicked: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.selectNodes` */ - onSelectionChange: ((nodes: Record) => void) | null; - /** Called by `LGraphCanvas.showSearchBox` */ - onSearchBox: - | (( - helper: Element, - value: string, - graphCanvas: LGraphCanvas - ) => string[]) - | null; - onSearchBoxSelection: - | ((name: string, event: MouseEvent, graphCanvas: LGraphCanvas) => void) - | null; - pause_rendering: boolean; - render_canvas_border: boolean; - render_collapsed_slots: boolean; - render_connection_arrows: boolean; - render_connections_border: boolean; - render_connections_shadows: boolean; - render_curved_connections: boolean; - render_execution_order: boolean; - render_only_selected: boolean; - render_shadows: boolean; - render_title_colored: boolean; - round_radius: number; - selected_group: null | LGraphGroup; - selected_group_resizing: boolean; - selected_nodes: Record; - show_info: boolean; - title_text_font: string; - /** set to true to render title bar with gradients */ - use_gradients: boolean; - visible_area: DragAndScale["visible_area"]; - visible_links: LLink[]; - visible_nodes: LGraphNode[]; - zoom_modify_alpha: boolean; - //mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle - mouse: Vector2; - //mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle - graph_mouse: Vector2; + allow_dragcanvas: boolean; + allow_dragnodes: boolean; + /** allow to control widgets, buttons, collapse, etc */ + allow_interaction: boolean; + /** allows to change a connection with having to redo it again */ + allow_reconnect_links: boolean; + /** allow selecting multi nodes without pressing extra keys */ + multi_select: boolean; + /** No effect */ + allow_searchbox: boolean; + always_render_background: boolean; + autoresize?: boolean; + background_image: string; + bgcanvas: HTMLCanvasElement; + bgctx: CanvasRenderingContext2D; + canvas: HTMLCanvasElement; + canvas_mouse: Vector2; + clear_background: boolean; + connecting_node: LGraphNode | null; + connections_width: number; + ctx: CanvasRenderingContext2D; + current_node: LGraphNode | null; + default_connection_color: { + input_off: string; + input_on: string; + output_off: string; + output_on: string; + }; + default_link_color: string; + dirty_area: Vector4 | null; + dirty_bgcanvas?: boolean; + dirty_canvas?: boolean; + drag_mode: boolean; + dragging_canvas: boolean; + dragging_rectangle: Vector4 | null; + ds: DragAndScale; + /** used for transition */ + editor_alpha: number; + filter: any; + fps: number; + frame: number; + graph: LGraph; + highlighted_links: Record; + highquality_render: boolean; + inner_text_font: string; + is_rendering: boolean; + last_draw_time: number; + last_mouse: Vector2; + /** + * Possible duplicated with `last_mouse` + * https://github.com/jagenjo/litegraph.js/issues/70 + */ + last_mouse_position: Vector2; + /** Timestamp of last mouse click, defaults to 0 */ + last_mouseclick: number; + links_render_mode: + | typeof LiteGraph.STRAIGHT_LINK + | typeof LiteGraph.LINEAR_LINK + | typeof LiteGraph.SPLINE_LINK; + live_mode: boolean; + node_capturing_input: LGraphNode | null; + node_dragged: LGraphNode | null; + node_in_panel: LGraphNode | null; + node_over: LGraphNode | null; + node_title_color: string; + node_widget: [LGraphNode, IWidget] | null; + last_mouse_dragging: boolean; - pointer_is_down?: boolean; + /** Called by `LGraphCanvas.drawBackCanvas` */ + onDrawBackground: + | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) + | null; + /** Called by `LGraphCanvas.drawFrontCanvas` */ + onDrawForeground: + | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) + | null; + onDrawOverlay: ((ctx: CanvasRenderingContext2D) => void) | null; + /** Called by `LGraphCanvas.processMouseDown` */ + onMouse: ((event: MouseEvent) => boolean) | null; + /** Called by `LGraphCanvas.drawFrontCanvas` and `LGraphCanvas.drawLinkTooltip` */ + onDrawLinkTooltip: + | ((ctx: CanvasRenderingContext2D, link: LLink, _this: this) => void) + | null; + /** Called by `LGraphCanvas.selectNodes` */ + onNodeMoved: ((node: LGraphNode) => void) | null; + /** Called by `LGraphCanvas.processNodeSelected` */ + onNodeSelected: ((node: LGraphNode) => void) | null; + /** Called by `LGraphCanvas.deselectNode` */ + onNodeDeselected: ((node: LGraphNode) => void) | null; + /** Called by `LGraphCanvas.processNodeDblClicked` */ + onShowNodePanel: ((node: LGraphNode) => void) | null; + /** Called by `LGraphCanvas.processNodeDblClicked` */ + onNodeDblClicked: ((node: LGraphNode) => void) | null; + /** Called by `LGraphCanvas.selectNodes` */ + onSelectionChange: ((nodes: Record) => void) | null; + /** Called by `LGraphCanvas.showSearchBox` */ + onSearchBox: + | ((helper: Element, value: string, graphCanvas: LGraphCanvas) => string[]) + | null; + onSearchBoxSelection: + | ((name: string, event: MouseEvent, graphCanvas: LGraphCanvas) => void) + | null; + pause_rendering: boolean; + render_canvas_border: boolean; + render_collapsed_slots: boolean; + render_connection_arrows: boolean; + render_connections_border: boolean; + render_connections_shadows: boolean; + render_curved_connections: boolean; + render_execution_order: boolean; + render_only_selected: boolean; + render_shadows: boolean; + render_title_colored: boolean; + round_radius: number; + selected_group: null | LGraphGroup; + selected_group_resizing: boolean; + selected_nodes: Record; + show_info: boolean; + title_text_font: string; + /** set to true to render title bar with gradients */ + use_gradients: boolean; + visible_area: DragAndScale["visible_area"]; + visible_links: LLink[]; + visible_nodes: LGraphNode[]; + zoom_modify_alpha: boolean; + //mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle + mouse: Vector2; + //mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle + graph_mouse: Vector2; - /** clears all the data inside */ - clear(): void; - /** assigns a graph, you can reassign graphs to the same canvas */ - setGraph(graph: LGraph, skipClear?: boolean): void; - /** opens a graph contained inside a node in the current graph */ - openSubgraph(graph: LGraph): void; - /** closes a subgraph contained inside a node */ - closeSubgraph(): void; - /** assigns a canvas */ - setCanvas(canvas: HTMLCanvasElement, skipEvents?: boolean): void; - /** binds mouse, keyboard, touch and drag events to the canvas */ - bindEvents(): void; - /** unbinds mouse events from the canvas */ - unbindEvents(): void; + pointer_is_down?: boolean; - /** - * this function allows to render the canvas using WebGL instead of Canvas2D - * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL - **/ - enableWebGL(): void; + /** clears all the data inside */ + clear(): void; + /** assigns a graph, you can reassign graphs to the same canvas */ + setGraph(graph: LGraph, skipClear?: boolean): void; + /** opens a graph contained inside a node in the current graph */ + openSubgraph(graph: LGraph): void; + /** closes a subgraph contained inside a node */ + closeSubgraph(): void; + /** assigns a canvas */ + setCanvas(canvas: HTMLCanvasElement, skipEvents?: boolean): void; + /** binds mouse, keyboard, touch and drag events to the canvas */ + bindEvents(): void; + /** unbinds mouse events from the canvas */ + unbindEvents(): void; - /** - * marks as dirty the canvas, this way it will be rendered again - * @param fg if the foreground canvas is dirty (the one containing the nodes) - * @param bg if the background canvas is dirty (the one containing the wires) - */ - setDirty(fg: boolean, bg: boolean): void; + /** + * this function allows to render the canvas using WebGL instead of Canvas2D + * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL + **/ + enableWebGL(): void; - /** - * Used to attach the canvas in a popup - * @return the window where the canvas is attached (the DOM root node) - */ - getCanvasWindow(): Window; - /** starts rendering the content of the canvas when needed */ - startRendering(): void; - /** stops rendering the content of the canvas (to save resources) */ - stopRendering(): void; + /** + * marks as dirty the canvas, this way it will be rendered again + * @param fg if the foreground canvas is dirty (the one containing the nodes) + * @param bg if the background canvas is dirty (the one containing the wires) + */ + setDirty(fg: boolean, bg: boolean): void; - processMouseDown(e: MouseEvent): boolean | undefined; - processMouseMove(e: MouseEvent): boolean | undefined; - processMouseUp(e: MouseEvent): boolean | undefined; - processMouseWheel(e: MouseEvent): boolean | undefined; + /** + * Used to attach the canvas in a popup + * @return the window where the canvas is attached (the DOM root node) + */ + getCanvasWindow(): Window; + /** starts rendering the content of the canvas when needed */ + startRendering(): void; + /** stops rendering the content of the canvas (to save resources) */ + stopRendering(): void; - /** returns true if a position (in graph space) is on top of a node little corner box */ - isOverNodeBox(node: LGraphNode, canvasX: number, canvasY: number): boolean; - /** returns true if a position (in graph space) is on top of a node input slot */ - isOverNodeInput( - node: LGraphNode, - canvasX: number, - canvasY: number, - slotPos: Vector2 - ): boolean; + processMouseDown(e: MouseEvent): boolean | undefined; + processMouseMove(e: MouseEvent): boolean | undefined; + processMouseUp(e: MouseEvent): boolean | undefined; + processMouseWheel(e: MouseEvent): boolean | undefined; - /** process a key event */ - processKey(e: KeyboardEvent): boolean | undefined; + /** returns true if a position (in graph space) is on top of a node little corner box */ + isOverNodeBox(node: LGraphNode, canvasX: number, canvasY: number): boolean; + /** returns true if a position (in graph space) is on top of a node input slot */ + isOverNodeInput( + node: LGraphNode, + canvasX: number, + canvasY: number, + slotPos: Vector2 + ): boolean; - copyToClipboard(): void; - pasteFromClipboard(): void; - processDrop(e: DragEvent): void; - checkDropItem(e: DragEvent): void; - processNodeDblClicked(n: LGraphNode): void; - processNodeSelected(n: LGraphNode, e: MouseEvent): void; - processNodeDeselected(node: LGraphNode): void; + /** process a key event */ + processKey(e: KeyboardEvent): boolean | undefined; - /** selects a given node (or adds it to the current selection) */ - selectNode(node: LGraphNode, add?: boolean): void; - /** selects several nodes (or adds them to the current selection) */ - selectNodes(nodes?: LGraphNode[], add?: boolean): void; - /** removes a node from the current selection */ - deselectNode(node: LGraphNode): void; - /** removes all nodes from the current selection */ - deselectAllNodes(): void; - /** deletes all nodes in the current selection from the graph */ - deleteSelectedNodes(): void; + copyToClipboard(): void; + pasteFromClipboard(): void; + processDrop(e: DragEvent): void; + checkDropItem(e: DragEvent): void; + processNodeDblClicked(n: LGraphNode): void; + processNodeSelected(n: LGraphNode, e: MouseEvent): void; + processNodeDeselected(node: LGraphNode): void; - /** centers the camera on a given node */ - centerOnNode(node: LGraphNode): void; - /** changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom */ - setZoom(value: number, center: Vector2): void; - /** brings a node to front (above all other nodes) */ - bringToFront(node: LGraphNode): void; - /** sends a node to the back (below all other nodes) */ - sendToBack(node: LGraphNode): void; - /** checks which nodes are visible (inside the camera area) */ - computeVisibleNodes(nodes: LGraphNode[]): LGraphNode[]; - /** renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) */ - draw(forceFG?: boolean, forceBG?: boolean): void; - /** draws the front canvas (the one containing all the nodes) */ - drawFrontCanvas(): void; - /** draws some useful stats in the corner of the canvas */ - renderInfo(ctx: CanvasRenderingContext2D, x: number, y: number): void; - /** draws the back canvas (the one containing the background and the connections) */ - drawBackCanvas(): void; - /** draws the given node inside the canvas */ - drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void; - /** draws graphic for node's slot */ - drawSlotGraphic(ctx: CanvasRenderingContext2D, pos: number[], shape: SlotShape, horizontal: boolean): void; - /** draws the shape of the given node in the canvas */ - drawNodeShape( - node: LGraphNode, - ctx: CanvasRenderingContext2D, - size: [number, number], - fgColor: string, - bgColor: string, - selected: boolean, - mouseOver: boolean - ): void; - /** draws every connection visible in the canvas */ - drawConnections(ctx: CanvasRenderingContext2D): void; - /** - * draws a link between two points - * @param a start pos - * @param b end pos - * @param link the link object with all the link info - * @param skipBorder ignore the shadow of the link - * @param flow show flow animation (for events) - * @param color the color for the link - * @param startDir the direction enum - * @param endDir the direction enum - * @param numSublines number of sublines (useful to represent vec3 or rgb) - **/ - renderLink( - a: Vector2, - b: Vector2, - link: object, - skipBorder: boolean, - flow: boolean, - color?: string, - startDir?: number, - endDir?: number, - numSublines?: number - ): void; + /** selects a given node (or adds it to the current selection) */ + selectNode(node: LGraphNode, add?: boolean): void; + /** selects several nodes (or adds them to the current selection) */ + selectNodes(nodes?: LGraphNode[], add?: boolean): void; + /** removes a node from the current selection */ + deselectNode(node: LGraphNode): void; + /** removes all nodes from the current selection */ + deselectAllNodes(): void; + /** deletes all nodes in the current selection from the graph */ + deleteSelectedNodes(): void; - computeConnectionPoint( - a: Vector2, - b: Vector2, - t: number, - startDir?: number, - endDir?: number - ): void; + /** centers the camera on a given node */ + centerOnNode(node: LGraphNode): void; + /** changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom */ + setZoom(value: number, center: Vector2): void; + /** brings a node to front (above all other nodes) */ + bringToFront(node: LGraphNode): void; + /** sends a node to the back (below all other nodes) */ + sendToBack(node: LGraphNode): void; + /** checks which nodes are visible (inside the camera area) */ + computeVisibleNodes(nodes: LGraphNode[]): LGraphNode[]; + /** renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) */ + draw(forceFG?: boolean, forceBG?: boolean): void; + /** draws the front canvas (the one containing all the nodes) */ + drawFrontCanvas(): void; + /** draws some useful stats in the corner of the canvas */ + renderInfo(ctx: CanvasRenderingContext2D, x: number, y: number): void; + /** draws the back canvas (the one containing the background and the connections) */ + drawBackCanvas(): void; + /** draws the given node inside the canvas */ + drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void; + /** draws graphic for node's slot */ + drawSlotGraphic( + ctx: CanvasRenderingContext2D, + pos: number[], + shape: SlotShape, + horizontal: boolean + ): void; + /** draws the shape of the given node in the canvas */ + drawNodeShape( + node: LGraphNode, + ctx: CanvasRenderingContext2D, + size: [number, number], + fgColor: string, + bgColor: string, + selected: boolean, + mouseOver: boolean + ): void; + /** draws every connection visible in the canvas */ + drawConnections(ctx: CanvasRenderingContext2D): void; + /** + * draws a link between two points + * @param a start pos + * @param b end pos + * @param link the link object with all the link info + * @param skipBorder ignore the shadow of the link + * @param flow show flow animation (for events) + * @param color the color for the link + * @param startDir the direction enum + * @param endDir the direction enum + * @param numSublines number of sublines (useful to represent vec3 or rgb) + **/ + renderLink( + a: Vector2, + b: Vector2, + link: object, + skipBorder: boolean, + flow: boolean, + color?: string, + startDir?: number, + endDir?: number, + numSublines?: number + ): void; - drawExecutionOrder(ctx: CanvasRenderingContext2D): void; - /** draws the widgets stored inside a node */ - drawNodeWidgets( - node: LGraphNode, - posY: number, - ctx: CanvasRenderingContext2D, - activeWidget: object - ): void; - /** process an event on widgets */ - processNodeWidgets( - node: LGraphNode, - pos: Vector2, - event: Event, - activeWidget: object - ): void; - /** draws every group area in the background */ - drawGroups(canvas: any, ctx: CanvasRenderingContext2D): void; - adjustNodesSize(): void; - /** resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode */ - resize(width?: number, height?: number): void; - /** - * switches to live mode (node shapes are not rendered, only the content) - * this feature was designed when graphs where meant to create user interfaces - **/ - switchLiveMode(transition?: boolean): void; - onNodeSelectionChange(): void; - touchHandler(event: TouchEvent): void; + computeConnectionPoint( + a: Vector2, + b: Vector2, + t: number, + startDir?: number, + endDir?: number + ): void; - showLinkMenu(link: LLink, e: any): false; - prompt( - title: string, - value: any, - callback: Function, - event: any - ): HTMLDivElement; - showSearchBox(event?: MouseEvent): void; - showEditPropertyValue(node: LGraphNode, property: any, options: any): void; - createDialog( - html: string, - options?: { position?: Vector2; event?: MouseEvent } - ): void; + drawExecutionOrder(ctx: CanvasRenderingContext2D): void; + /** draws the widgets stored inside a node */ + drawNodeWidgets( + node: LGraphNode, + posY: number, + ctx: CanvasRenderingContext2D, + activeWidget: object + ): void; + /** process an event on widgets */ + processNodeWidgets( + node: LGraphNode, + pos: Vector2, + event: Event, + activeWidget: object + ): void; + /** draws every group area in the background */ + drawGroups(canvas: any, ctx: CanvasRenderingContext2D): void; + adjustNodesSize(): void; + /** resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode */ + resize(width?: number, height?: number): void; + /** + * switches to live mode (node shapes are not rendered, only the content) + * this feature was designed when graphs where meant to create user interfaces + **/ + switchLiveMode(transition?: boolean): void; + onNodeSelectionChange(): void; + touchHandler(event: TouchEvent): void; - convertOffsetToCanvas: DragAndScale["convertOffsetToCanvas"]; - convertCanvasToOffset: DragAndScale["convertCanvasToOffset"]; - /** converts event coordinates from canvas2D to graph coordinates */ - convertEventToCanvasOffset(e: MouseEvent): Vector2; - /** adds some useful properties to a mouse event, like the position in graph coordinates */ - adjustMouseEvent(e: MouseEvent): void; + showLinkMenu(link: LLink, e: any): false; + prompt( + title: string, + value: any, + callback: Function, + event: any + ): HTMLDivElement; + showSearchBox(event?: MouseEvent): void; + showEditPropertyValue(node: LGraphNode, property: any, options: any): void; + createDialog( + html: string, + options?: { position?: Vector2; event?: MouseEvent } + ): void; - getCanvasMenuOptions(): ContextMenuItem[]; - getNodeMenuOptions(node: LGraphNode): ContextMenuItem[]; - getGroupMenuOptions(): ContextMenuItem[]; - /** Called by `getCanvasMenuOptions`, replace default options */ - getMenuOptions?(): ContextMenuItem[]; - /** Called by `getCanvasMenuOptions`, append to default options */ - getExtraMenuOptions?(): ContextMenuItem[]; - /** Called when mouse right click */ - processContextMenu(node: LGraphNode, event: Event): void; + convertOffsetToCanvas: DragAndScale["convertOffsetToCanvas"]; + convertCanvasToOffset: DragAndScale["convertCanvasToOffset"]; + /** converts event coordinates from canvas2D to graph coordinates */ + convertEventToCanvasOffset(e: MouseEvent): Vector2; + /** adds some useful properties to a mouse event, like the position in graph coordinates */ + adjustMouseEvent(e: MouseEvent): void; + + getCanvasMenuOptions(): ContextMenuItem[]; + getNodeMenuOptions(node: LGraphNode): ContextMenuItem[]; + getGroupMenuOptions(): ContextMenuItem[]; + /** Called by `getCanvasMenuOptions`, replace default options */ + getMenuOptions?(): ContextMenuItem[]; + /** Called by `getCanvasMenuOptions`, append to default options */ + getExtraMenuOptions?(): ContextMenuItem[]; + /** Called when mouse right click */ + processContextMenu(node: LGraphNode, event: Event): void; } declare class ContextMenu { - static trigger( - element: HTMLElement, - event_name: string, - params: any, - origin: any - ): void; - static isCursorOverElement(event: MouseEvent, element: HTMLElement): void; - static closeAllContextMenus(window: Window): void; - constructor(values: ContextMenuItem[], options?: IContextMenuOptions, window?: Window); - options: IContextMenuOptions; - parentMenu?: ContextMenu; - lock: boolean; - current_submenu?: ContextMenu; - addItem( - name: string, - value: ContextMenuItem, - options?: IContextMenuOptions - ): void; - close(e?: MouseEvent, ignore_parent_menu?: boolean): void; - getTopMenu(): void; - getFirstEvent(): void; + static trigger( + element: HTMLElement, + event_name: string, + params: any, + origin: any + ): void; + static isCursorOverElement(event: MouseEvent, element: HTMLElement): void; + static closeAllContextMenus(window: Window): void; + constructor( + values: ContextMenuItem[], + options?: IContextMenuOptions, + window?: Window + ); + options: IContextMenuOptions; + parentMenu?: ContextMenu; + lock: boolean; + current_submenu?: ContextMenu; + addItem( + name: string, + value: ContextMenuItem, + options?: IContextMenuOptions + ): void; + close(e?: MouseEvent, ignore_parent_menu?: boolean): void; + getTopMenu(): void; + getFirstEvent(): void; } declare global { - interface CanvasRenderingContext2D { - /** like rect but rounded corners */ - roundRect( - x: number, - y: number, - width: number, - height: number, - radius: number | Vector4, - radiusLow?: number - ): void; - } + interface CanvasRenderingContext2D { + /** like rect but rounded corners */ + roundRect( + x: number, + y: number, + width: number, + height: number, + radius: number | Vector4, + radiusLow?: number + ): void; + } - interface Math { - clamp(v: number, min: number, max: number): number; - } + interface Math { + clamp(v: number, min: number, max: number): number; + } - const LiteGraph: { + const LiteGraph: { DEFAULT_GROUP_FONT_SIZE: any; overlapBounding(visible_area: any, _bounding: any): unknown; release_link_on_empty_shows_menu: boolean; alt_drag_do_clone_nodes: boolean; GRID_SHAPE: number; - VERSION: number; - - CANVAS_GRID_SIZE: number; - - NODE_TITLE_HEIGHT: number; - NODE_TITLE_TEXT_Y: number; - NODE_SLOT_HEIGHT: number; - NODE_WIDGET_HEIGHT: number; - NODE_WIDTH: number; - NODE_MIN_WIDTH: number; - NODE_COLLAPSED_RADIUS: number; - NODE_COLLAPSED_WIDTH: number; - NODE_TITLE_COLOR: string; - NODE_TEXT_SIZE: number; - NODE_TEXT_COLOR: string; - NODE_SUBTEXT_SIZE: number; - NODE_DEFAULT_COLOR: string; - NODE_DEFAULT_BGCOLOR: string; - NODE_DEFAULT_BOXCOLOR: string; - NODE_DEFAULT_SHAPE: string; - DEFAULT_SHADOW_COLOR: string; - DEFAULT_GROUP_FONT: number; - - LINK_COLOR: string; - EVENT_LINK_COLOR: string; - CONNECTING_LINK_COLOR: string; - - MAX_NUMBER_OF_NODES: number; //avoid infinite loops - DEFAULT_POSITION: Vector2; //default node position - VALID_SHAPES: ["default", "box", "round", "card"]; //,"circle" - - //shapes are used for nodes but also for slots - BOX_SHAPE: 1; - ROUND_SHAPE: 2; - CIRCLE_SHAPE: 3; - CARD_SHAPE: 4; - ARROW_SHAPE: 5; - SQUARE_SHAPE: 6; - - //enums - INPUT: 1; - OUTPUT: 2; - - EVENT: -1; //for outputs - ACTION: -1; //for inputs - - ALWAYS: 0; - ON_EVENT: 1; - NEVER: 2; - ON_TRIGGER: 3; - - UP: 1; - DOWN: 2; - LEFT: 3; - RIGHT: 4; - CENTER: 5; - - STRAIGHT_LINK: 0; - LINEAR_LINK: 1; - SPLINE_LINK: 2; - - NORMAL_TITLE: 0; - NO_TITLE: 1; - TRANSPARENT_TITLE: 2; - AUTOHIDE_TITLE: 3; - - node_images_path: string; - - debug: boolean; - catch_exceptions: boolean; - throw_errors: boolean; - /** if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits */ - allow_scripts: boolean; - /** node types by string */ - registered_node_types: Record; - /** used for dropping files in the canvas */ - node_types_by_file_extension: Record; - /** node types by class name */ - Nodes: Record; - - /** used to add extra features to the search box */ - searchbox_extras: Record< - string, - { - data: { outputs: string[][]; title: string }; - desc: string; - type: string; - } - >; - - createNode(type: string): T; - /** Register a node class so it can be listed when the user wants to create a new one */ - registerNodeType(type: string, base: { new (): LGraphNode }): void; - /** removes a node type from the system */ - unregisterNodeType(type: string): void; - /** Removes all previously registered node's types. */ - clearRegisteredTypes(): void; - /** - * Create a new node type by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function. - * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. - * @param name node name with namespace (p.e.: 'math/sum') - * @param func - * @param param_types an array containing the type of every parameter, otherwise parameters will accept any type - * @param return_type string with the return type, otherwise it will be generic - * @param properties properties to be configurable - */ - wrapFunctionAsNode( - name: string, - func: (...args: any[]) => any, - param_types?: string[], - return_type?: string, - properties?: object - ): void; - - /** - * Adds this method to all node types, existing and to be created - * (You can add it to LGraphNode.prototype but then existing node types wont have it) - */ - addNodeMethod(name: string, func: (...args: any[]) => any): void; - - /** - * Create a node of a given type with a name. The node is not attached to any graph yet. - * @param type full name of the node class. p.e. "math/sin" - * @param name a name to distinguish from other nodes - * @param options to set options - */ - createNode( - type: string, - title: string, - options: object - ): T; - - /** - * Returns a registered node type with a given name - * @param type full name of the node class. p.e. "math/sin" - */ - getNodeType(type: string): LGraphNodeConstructor; - - /** - * Returns a list of node types matching one category - * @method getNodeTypesInCategory - * @param {String} category category name - * @param {String} filter only nodes with ctor.filter equal can be shown - * @return {Array} array with all the node classes - */ - getNodeTypesInCategory( - category: string, - filter: string - ): LGraphNodeConstructor[]; - - /** - * Returns a list with all the node type categories - * @method getNodeTypesCategories - * @param {String} filter only nodes with ctor.filter equal can be shown - * @return {Array} array with all the names of the categories - */ - getNodeTypesCategories(filter: string): string[]; - - /** debug purposes: reloads all the js scripts that matches a wildcard */ - reloadNodes(folder_wildcard: string): void; - - getTime(): number; - LLink: typeof LLink; - LGraph: typeof LGraph; - DragAndScale: typeof DragAndScale; - compareObjects(a: object, b: object): boolean; - distance(a: Vector2, b: Vector2): number; - colorToString(c: string): string; - isInsideRectangle( - x: number, - y: number, - left: number, - top: number, - width: number, - height: number - ): boolean; - growBounding(bounding: Vector4, x: number, y: number): Vector4; - isInsideBounding(p: Vector2, bb: Vector4): boolean; - hex2num(hex: string): [number, number, number]; - num2hex(triplet: [number, number, number]): string; - ContextMenu: typeof ContextMenu; - extendClass(target: A, origin: B): A & B; - getParameterNames(func: string): string[]; - }; + VERSION: number; + + CANVAS_GRID_SIZE: number; + + NODE_TITLE_HEIGHT: number; + NODE_TITLE_TEXT_Y: number; + NODE_SLOT_HEIGHT: number; + NODE_WIDGET_HEIGHT: number; + NODE_WIDTH: number; + NODE_MIN_WIDTH: number; + NODE_COLLAPSED_RADIUS: number; + NODE_COLLAPSED_WIDTH: number; + NODE_TITLE_COLOR: string; + NODE_TEXT_SIZE: number; + NODE_TEXT_COLOR: string; + NODE_SUBTEXT_SIZE: number; + NODE_DEFAULT_COLOR: string; + NODE_DEFAULT_BGCOLOR: string; + NODE_DEFAULT_BOXCOLOR: string; + NODE_DEFAULT_SHAPE: string; + DEFAULT_SHADOW_COLOR: string; + DEFAULT_GROUP_FONT: number; + + LINK_COLOR: string; + EVENT_LINK_COLOR: string; + CONNECTING_LINK_COLOR: string; + + MAX_NUMBER_OF_NODES: number; //avoid infinite loops + DEFAULT_POSITION: Vector2; //default node position + VALID_SHAPES: ["default", "box", "round", "card"]; //,"circle" + + //shapes are used for nodes but also for slots + BOX_SHAPE: 1; + ROUND_SHAPE: 2; + CIRCLE_SHAPE: 3; + CARD_SHAPE: 4; + ARROW_SHAPE: 5; + SQUARE_SHAPE: 6; + + //enums + INPUT: 1; + OUTPUT: 2; + + EVENT: -1; //for outputs + ACTION: -1; //for inputs + + ALWAYS: 0; + ON_EVENT: 1; + NEVER: 2; + ON_TRIGGER: 3; + + UP: 1; + DOWN: 2; + LEFT: 3; + RIGHT: 4; + CENTER: 5; + + STRAIGHT_LINK: 0; + LINEAR_LINK: 1; + SPLINE_LINK: 2; + + NORMAL_TITLE: 0; + NO_TITLE: 1; + TRANSPARENT_TITLE: 2; + AUTOHIDE_TITLE: 3; + + node_images_path: string; + + debug: boolean; + catch_exceptions: boolean; + throw_errors: boolean; + /** if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits */ + allow_scripts: boolean; + /** node types by string */ + registered_node_types: Record; + /** used for dropping files in the canvas */ + node_types_by_file_extension: Record; + /** node types by class name */ + Nodes: Record; + + /** used to add extra features to the search box */ + searchbox_extras: Record< + string, + { + data: { outputs: string[][]; title: string }; + desc: string; + type: string; + } + >; + + createNode(type: string): T; + /** Register a node class so it can be listed when the user wants to create a new one */ + registerNodeType(type: string, base: { new (): LGraphNode }): void; + /** removes a node type from the system */ + unregisterNodeType(type: string): void; + /** Removes all previously registered node's types. */ + clearRegisteredTypes(): void; + /** + * Create a new node type by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function. + * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. + * @param name node name with namespace (p.e.: 'math/sum') + * @param func + * @param param_types an array containing the type of every parameter, otherwise parameters will accept any type + * @param return_type string with the return type, otherwise it will be generic + * @param properties properties to be configurable + */ + wrapFunctionAsNode( + name: string, + func: (...args: any[]) => any, + param_types?: string[], + return_type?: string, + properties?: object + ): void; + + /** + * Adds this method to all node types, existing and to be created + * (You can add it to LGraphNode.prototype but then existing node types wont have it) + */ + addNodeMethod(name: string, func: (...args: any[]) => any): void; + + /** + * Create a node of a given type with a name. The node is not attached to any graph yet. + * @param type full name of the node class. p.e. "math/sin" + * @param name a name to distinguish from other nodes + * @param options to set options + */ + createNode( + type: string, + title: string, + options: object + ): T; + + /** + * Returns a registered node type with a given name + * @param type full name of the node class. p.e. "math/sin" + */ + getNodeType(type: string): LGraphNodeConstructor; + + /** + * Returns a list of node types matching one category + * @method getNodeTypesInCategory + * @param {String} category category name + * @param {String} filter only nodes with ctor.filter equal can be shown + * @return {Array} array with all the node classes + */ + getNodeTypesInCategory( + category: string, + filter: string + ): LGraphNodeConstructor[]; + + /** + * Returns a list with all the node type categories + * @method getNodeTypesCategories + * @param {String} filter only nodes with ctor.filter equal can be shown + * @return {Array} array with all the names of the categories + */ + getNodeTypesCategories(filter: string): string[]; + + /** debug purposes: reloads all the js scripts that matches a wildcard */ + reloadNodes(folder_wildcard: string): void; + + getTime(): number; + LLink: typeof LLink; + LGraph: typeof LGraph; + DragAndScale: typeof DragAndScale; + compareObjects(a: object, b: object): boolean; + distance(a: Vector2, b: Vector2): number; + colorToString(c: string): string; + isInsideRectangle( + x: number, + y: number, + left: number, + top: number, + width: number, + height: number + ): boolean; + growBounding(bounding: Vector4, x: number, y: number): Vector4; + isInsideBounding(p: Vector2, bb: Vector4): boolean; + hex2num(hex: string): [number, number, number]; + num2hex(triplet: [number, number, number]): string; + ContextMenu: typeof ContextMenu; + extendClass(target: A, origin: B): A & B; + getParameterNames(func: string): string[]; + }; }