Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +22 -0
- ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_multiple_images_workflow.png +3 -0
- ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_single_image_workflow.png +3 -0
- ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_text_workflow.png +3 -0
- ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_video_workflow.png +3 -0
- ComfyUI_Qwen2-VL-Instruct/examples/ComfyUI_00508_.png +3 -0
- ComfyUI_Qwen2-VL-Instruct/examples/ComfyUI_00509_.png +3 -0
- comfyui_layerstyle/image/segment_anything_ultra_node.jpg +3 -0
- comfyui_layerstyle/py/__pycache__/color_name.cpython-312.pyc +3 -0
- comfyui_layerstyle/py/__pycache__/imagefunc.cpython-312.pyc +3 -0
- comfyui_layerstyle/workflow/1344x768_redcar.png +3 -0
- comfyui_layerstyle/workflow/512x512.png +3 -0
- comfyui_layerstyle/workflow/768x1344_beach.png +3 -0
- comfyui_layerstyle/workflow/768x1344_dress.png +3 -0
- comfyui_layerstyle/workflow/fox_512x512.png +3 -0
- comfyui_layerstyle/workflow/girl_dino_1024.png +3 -0
- comfyui_tensorrt/readme_images/image1.png +3 -0
- comfyui_tensorrt/readme_images/image11.png +3 -0
- comfyui_tensorrt/readme_images/image4.png +3 -0
- comfyui_tensorrt/readme_images/image6.png +3 -0
- rgthree-comfy/py/__pycache__/__init__.cpython-312.pyc +0 -0
- rgthree-comfy/py/__pycache__/utils_userdata.cpython-312.pyc +0 -0
- rgthree-comfy/py/server/__pycache__/rgthree_server.cpython-312.pyc +0 -0
- rgthree-comfy/py/server/__pycache__/routes_config.cpython-312.pyc +0 -0
- rgthree-comfy/py/server/__pycache__/routes_model_info.cpython-312.pyc +0 -0
- rgthree-comfy/py/server/__pycache__/utils_info.cpython-312.pyc +0 -0
- rgthree-comfy/py/server/__pycache__/utils_server.cpython-312.pyc +0 -0
- rgthree-comfy/src_web/comfyui/any_switch.ts +101 -0
- rgthree-comfy/src_web/comfyui/base_any_input_connected_node.ts +345 -0
- rgthree-comfy/src_web/comfyui/base_node.ts +462 -0
- rgthree-comfy/src_web/comfyui/base_node_collector.ts +98 -0
- rgthree-comfy/src_web/comfyui/base_node_mode_changer.ts +120 -0
- rgthree-comfy/src_web/comfyui/base_power_prompt.ts +364 -0
- rgthree-comfy/src_web/comfyui/bookmark.ts +163 -0
- rgthree-comfy/src_web/comfyui/bypasser.ts +51 -0
- rgthree-comfy/src_web/comfyui/comfy_ui_bar.ts +120 -0
- rgthree-comfy/src_web/comfyui/config.ts +406 -0
- rgthree-comfy/src_web/comfyui/constants.ts +62 -0
- rgthree-comfy/src_web/comfyui/context.ts +483 -0
- rgthree-comfy/src_web/comfyui/dialog_info.ts +421 -0
- rgthree-comfy/src_web/comfyui/display_any.ts +73 -0
- rgthree-comfy/src_web/comfyui/dynamic_context.ts +297 -0
- rgthree-comfy/src_web/comfyui/dynamic_context_base.ts +237 -0
- rgthree-comfy/src_web/comfyui/dynamic_context_switch.ts +207 -0
- rgthree-comfy/src_web/comfyui/fast_actions_button.ts +343 -0
- rgthree-comfy/src_web/comfyui/fast_groups_bypasser.ts +37 -0
- rgthree-comfy/src_web/comfyui/fast_groups_muter.ts +502 -0
- rgthree-comfy/src_web/comfyui/feature_group_fast_toggle.ts +304 -0
- rgthree-comfy/src_web/comfyui/feature_import_individual_nodes.ts +69 -0
- rgthree-comfy/src_web/comfyui/image_comparer.ts +474 -0
.gitattributes
CHANGED
|
@@ -255,3 +255,25 @@ comfyui_layerstyle/workflow/1280x720_seven_person.jpg filter=lfs diff=lfs merge=
|
|
| 255 |
comfyui_layerstyle/workflow/1280x720car.jpg filter=lfs diff=lfs merge=lfs -text
|
| 256 |
comfyui_layerstyle/workflow/1280x768_city.png filter=lfs diff=lfs merge=lfs -text
|
| 257 |
comfyui_layerstyle/workflow/1344x768_girl2.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
comfyui_layerstyle/workflow/1280x720car.jpg filter=lfs diff=lfs merge=lfs -text
|
| 256 |
comfyui_layerstyle/workflow/1280x768_city.png filter=lfs diff=lfs merge=lfs -text
|
| 257 |
comfyui_layerstyle/workflow/1344x768_girl2.png filter=lfs diff=lfs merge=lfs -text
|
| 258 |
+
comfyui_layerstyle/workflow/1344x768_redcar.png filter=lfs diff=lfs merge=lfs -text
|
| 259 |
+
comfyui_layerstyle/workflow/1920x1080table.png filter=lfs diff=lfs merge=lfs -text
|
| 260 |
+
comfyui_layerstyle/image/segment_anything_ultra_node.jpg filter=lfs diff=lfs merge=lfs -text
|
| 261 |
+
comfyui_layerstyle/workflow/512x512.png filter=lfs diff=lfs merge=lfs -text
|
| 262 |
+
comfyui_layerstyle/workflow/768x1344_dress.png filter=lfs diff=lfs merge=lfs -text
|
| 263 |
+
comfyui_layerstyle/workflow/768x1344_beach.png filter=lfs diff=lfs merge=lfs -text
|
| 264 |
+
comfyui_layerstyle/workflow/fox_512x512.png filter=lfs diff=lfs merge=lfs -text
|
| 265 |
+
comfyui_layerstyle/workflow/girl_dino_1024.png filter=lfs diff=lfs merge=lfs -text
|
| 266 |
+
comfyui_layerstyle/py/__pycache__/imagefunc.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 267 |
+
comfyui_layerstyle/py/__pycache__/color_name.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 268 |
+
ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_single_image_workflow.png filter=lfs diff=lfs merge=lfs -text
|
| 269 |
+
ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_video_workflow.png filter=lfs diff=lfs merge=lfs -text
|
| 270 |
+
ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_multiple_images_workflow.png filter=lfs diff=lfs merge=lfs -text
|
| 271 |
+
ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_text_workflow.png filter=lfs diff=lfs merge=lfs -text
|
| 272 |
+
ComfyUI_Qwen2-VL-Instruct/examples/ComfyUI_00509_.png filter=lfs diff=lfs merge=lfs -text
|
| 273 |
+
ComfyUI_Qwen2-VL-Instruct/examples/ComfyUI_00532_.png filter=lfs diff=lfs merge=lfs -text
|
| 274 |
+
ComfyUI_Qwen2-VL-Instruct/examples/ComfyUI_00508_.png filter=lfs diff=lfs merge=lfs -text
|
| 275 |
+
comfyui_tensorrt/readme_images/image1.png filter=lfs diff=lfs merge=lfs -text
|
| 276 |
+
comfyui_tensorrt/readme_images/image10.png filter=lfs diff=lfs merge=lfs -text
|
| 277 |
+
comfyui_tensorrt/readme_images/image6.png filter=lfs diff=lfs merge=lfs -text
|
| 278 |
+
comfyui_tensorrt/readme_images/image4.png filter=lfs diff=lfs merge=lfs -text
|
| 279 |
+
comfyui_tensorrt/readme_images/image11.png filter=lfs diff=lfs merge=lfs -text
|
ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_multiple_images_workflow.png
ADDED
|
Git LFS Details
|
ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_single_image_workflow.png
ADDED
|
Git LFS Details
|
ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_text_workflow.png
ADDED
|
Git LFS Details
|
ComfyUI_Qwen2-VL-Instruct/examples/Chat_with_video_workflow.png
ADDED
|
Git LFS Details
|
ComfyUI_Qwen2-VL-Instruct/examples/ComfyUI_00508_.png
ADDED
|
Git LFS Details
|
ComfyUI_Qwen2-VL-Instruct/examples/ComfyUI_00509_.png
ADDED
|
Git LFS Details
|
comfyui_layerstyle/image/segment_anything_ultra_node.jpg
ADDED
|
Git LFS Details
|
comfyui_layerstyle/py/__pycache__/color_name.cpython-312.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:16469a66469399acb0769c5d47b332f5cd184833e183d28ab4766046c8ec1300
|
| 3 |
+
size 102641
|
comfyui_layerstyle/py/__pycache__/imagefunc.cpython-312.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:03e3bcec87456dfadefc28a7d89d2d66a374b44f340e25222d3b2e68aedee619
|
| 3 |
+
size 151881
|
comfyui_layerstyle/workflow/1344x768_redcar.png
ADDED
|
Git LFS Details
|
comfyui_layerstyle/workflow/512x512.png
ADDED
|
Git LFS Details
|
comfyui_layerstyle/workflow/768x1344_beach.png
ADDED
|
Git LFS Details
|
comfyui_layerstyle/workflow/768x1344_dress.png
ADDED
|
Git LFS Details
|
comfyui_layerstyle/workflow/fox_512x512.png
ADDED
|
Git LFS Details
|
comfyui_layerstyle/workflow/girl_dino_1024.png
ADDED
|
Git LFS Details
|
comfyui_tensorrt/readme_images/image1.png
ADDED
|
Git LFS Details
|
comfyui_tensorrt/readme_images/image11.png
ADDED
|
Git LFS Details
|
comfyui_tensorrt/readme_images/image4.png
ADDED
|
Git LFS Details
|
comfyui_tensorrt/readme_images/image6.png
ADDED
|
Git LFS Details
|
rgthree-comfy/py/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (165 Bytes). View file
|
|
|
rgthree-comfy/py/__pycache__/utils_userdata.cpython-312.pyc
ADDED
|
Binary file (2.78 kB). View file
|
|
|
rgthree-comfy/py/server/__pycache__/rgthree_server.cpython-312.pyc
ADDED
|
Binary file (956 Bytes). View file
|
|
|
rgthree-comfy/py/server/__pycache__/routes_config.cpython-312.pyc
ADDED
|
Binary file (2.09 kB). View file
|
|
|
rgthree-comfy/py/server/__pycache__/routes_model_info.cpython-312.pyc
ADDED
|
Binary file (7.36 kB). View file
|
|
|
rgthree-comfy/py/server/__pycache__/utils_info.cpython-312.pyc
ADDED
|
Binary file (16.4 kB). View file
|
|
|
rgthree-comfy/py/server/__pycache__/utils_server.cpython-312.pyc
ADDED
|
Binary file (4.06 kB). View file
|
|
|
rgthree-comfy/src_web/comfyui/any_switch.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { INodeInputSlot, INodeOutputSlot, LLink } from "typings/litegraph.js";
|
| 2 |
+
import type { ComfyApp, ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
|
| 3 |
+
|
| 4 |
+
import { app } from "scripts/app.js";
|
| 5 |
+
import { IoDirection, addConnectionLayoutSupport, followConnectionUntilType } from "./utils.js";
|
| 6 |
+
import { RgthreeBaseServerNode } from "./base_node.js";
|
| 7 |
+
import { NodeTypesString } from "./constants.js";
|
| 8 |
+
import { removeUnusedInputsFromEnd } from "./utils_inputs_outputs.js";
|
| 9 |
+
import { debounce } from "rgthree/common/shared_utils.js";
|
| 10 |
+
|
| 11 |
+
class RgthreeAnySwitch extends RgthreeBaseServerNode {
|
| 12 |
+
static override title = NodeTypesString.ANY_SWITCH;
|
| 13 |
+
static override type = NodeTypesString.ANY_SWITCH;
|
| 14 |
+
static comfyClass = NodeTypesString.ANY_SWITCH;
|
| 15 |
+
|
| 16 |
+
private stabilizeBound = this.stabilize.bind(this);
|
| 17 |
+
private nodeType: string | string[] | null = null;
|
| 18 |
+
|
| 19 |
+
constructor(title = RgthreeAnySwitch.title) {
|
| 20 |
+
super(title);
|
| 21 |
+
// Adding five. Note, configure will add as many as was in the stored workflow automatically.
|
| 22 |
+
this.addAnyInput(5);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
override onConnectionsChange(
|
| 26 |
+
type: number,
|
| 27 |
+
slotIndex: number,
|
| 28 |
+
isConnected: boolean,
|
| 29 |
+
linkInfo: LLink,
|
| 30 |
+
ioSlot: INodeOutputSlot | INodeInputSlot,
|
| 31 |
+
) {
|
| 32 |
+
super.onConnectionsChange?.(type, slotIndex, isConnected, linkInfo, ioSlot);
|
| 33 |
+
this.scheduleStabilize();
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
onConnectionsChainChange() {
|
| 37 |
+
this.scheduleStabilize();
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
scheduleStabilize(ms = 64) {
|
| 41 |
+
return debounce(this.stabilizeBound, ms);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
private addAnyInput(num = 1) {
|
| 45 |
+
for (let i = 0; i < num; i++) {
|
| 46 |
+
this.addInput(
|
| 47 |
+
`any_${String(this.inputs.length + 1).padStart(2, "0")}`,
|
| 48 |
+
(this.nodeType || "*") as string,
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
stabilize() {
|
| 54 |
+
// First, clean up the dynamic number of inputs.
|
| 55 |
+
removeUnusedInputsFromEnd(this, 4);
|
| 56 |
+
this.addAnyInput();
|
| 57 |
+
|
| 58 |
+
// We prefer the inputs, then the output.
|
| 59 |
+
let connectedType = followConnectionUntilType(this, IoDirection.INPUT, undefined, true);
|
| 60 |
+
if (!connectedType) {
|
| 61 |
+
connectedType = followConnectionUntilType(this, IoDirection.OUTPUT, undefined, true);
|
| 62 |
+
}
|
| 63 |
+
// TODO: What this doesn't do is broadcast to other nodes when its type changes. Reroute node
|
| 64 |
+
// does, but, for now, if this was connected to another Any Switch, say, the second one wouldn't
|
| 65 |
+
// change its type when the first does. The user would need to change the connections.
|
| 66 |
+
this.nodeType = connectedType?.type || "*";
|
| 67 |
+
for (const input of this.inputs) {
|
| 68 |
+
input.type = this.nodeType as string; // So, types can indeed be arrays,,
|
| 69 |
+
}
|
| 70 |
+
for (const output of this.outputs) {
|
| 71 |
+
output.type = this.nodeType as string; // So, types can indeed be arrays,,
|
| 72 |
+
output.label =
|
| 73 |
+
output.type === "RGTHREE_CONTEXT"
|
| 74 |
+
? "CONTEXT"
|
| 75 |
+
: Array.isArray(this.nodeType) || this.nodeType.includes(",")
|
| 76 |
+
? connectedType?.label || String(this.nodeType)
|
| 77 |
+
: String(this.nodeType);
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 82 |
+
RgthreeBaseServerNode.registerForOverride(comfyClass, nodeData, RgthreeAnySwitch);
|
| 83 |
+
addConnectionLayoutSupport(RgthreeAnySwitch, app, [
|
| 84 |
+
["Left", "Right"],
|
| 85 |
+
["Right", "Left"],
|
| 86 |
+
]);
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
app.registerExtension({
|
| 91 |
+
name: "rgthree.AnySwitch",
|
| 92 |
+
async beforeRegisterNodeDef(
|
| 93 |
+
nodeType: ComfyNodeConstructor,
|
| 94 |
+
nodeData: ComfyObjectInfo,
|
| 95 |
+
app: ComfyApp,
|
| 96 |
+
) {
|
| 97 |
+
if (nodeData.name === "Any Switch (rgthree)") {
|
| 98 |
+
RgthreeAnySwitch.setUp(nodeType, nodeData);
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
});
|
rgthree-comfy/src_web/comfyui/base_any_input_connected_node.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js";
|
| 2 |
+
import type {
|
| 3 |
+
Vector2,
|
| 4 |
+
LLink,
|
| 5 |
+
INodeInputSlot,
|
| 6 |
+
INodeOutputSlot,
|
| 7 |
+
LGraphNode as TLGraphNode,
|
| 8 |
+
IWidget,
|
| 9 |
+
} from "typings/litegraph.js";
|
| 10 |
+
|
| 11 |
+
import { app } from "scripts/app.js";
|
| 12 |
+
import { RgthreeBaseVirtualNode } from "./base_node.js";
|
| 13 |
+
import { rgthree } from "./rgthree.js";
|
| 14 |
+
|
| 15 |
+
import {
|
| 16 |
+
PassThroughFollowing,
|
| 17 |
+
addConnectionLayoutSupport,
|
| 18 |
+
addMenuItem,
|
| 19 |
+
getConnectedInputNodes,
|
| 20 |
+
getConnectedInputNodesAndFilterPassThroughs,
|
| 21 |
+
getConnectedOutputNodes,
|
| 22 |
+
getConnectedOutputNodesAndFilterPassThroughs,
|
| 23 |
+
} from "./utils.js";
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* A Virtual Node that allows any node's output to connect to it.
|
| 27 |
+
*/
|
| 28 |
+
export class BaseAnyInputConnectedNode extends RgthreeBaseVirtualNode {
|
| 29 |
+
override isVirtualNode = true;
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Whether inputs show the immediate nodes, or follow and show connected nodes through
|
| 33 |
+
* passthrough nodes.
|
| 34 |
+
*/
|
| 35 |
+
readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.NONE;
|
| 36 |
+
|
| 37 |
+
debouncerTempWidth: number = 0;
|
| 38 |
+
schedulePromise: Promise<void> | null = null;
|
| 39 |
+
|
| 40 |
+
constructor(title = BaseAnyInputConnectedNode.title) {
|
| 41 |
+
super(title);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
override onConstructed() {
|
| 45 |
+
this.addInput("", "*");
|
| 46 |
+
return super.onConstructed();
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
override clone() {
|
| 50 |
+
const cloned = super.clone();
|
| 51 |
+
// Copying to clipboard (and also, creating node templates) work by cloning nodes and, for some
|
| 52 |
+
// reason, it manually manipulates the cloned data. So, we want to keep the present input slots
|
| 53 |
+
// so if it's pasted/templatized the data is correct. Otherwise, clear the inputs and so the new
|
| 54 |
+
// node is ready to go, fresh.
|
| 55 |
+
if (!rgthree.canvasCurrentlyCopyingToClipboardWithMultipleNodes) {
|
| 56 |
+
while (cloned.inputs.length > 1) {
|
| 57 |
+
cloned.removeInput(cloned.inputs.length - 1);
|
| 58 |
+
}
|
| 59 |
+
if (cloned.inputs[0]) {
|
| 60 |
+
cloned.inputs[0].label = "";
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
return cloned;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Schedules a promise to run a stabilization, debouncing duplicate requests.
|
| 68 |
+
*/
|
| 69 |
+
scheduleStabilizeWidgets(ms = 100) {
|
| 70 |
+
if (!this.schedulePromise) {
|
| 71 |
+
this.schedulePromise = new Promise((resolve) => {
|
| 72 |
+
setTimeout(() => {
|
| 73 |
+
this.schedulePromise = null;
|
| 74 |
+
this.doStablization();
|
| 75 |
+
resolve();
|
| 76 |
+
}, ms);
|
| 77 |
+
});
|
| 78 |
+
}
|
| 79 |
+
return this.schedulePromise;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/**
|
| 83 |
+
* Ensures we have at least one empty input at the end, returns true if changes were made, or false
|
| 84 |
+
* if no changes were needed.
|
| 85 |
+
*/
|
| 86 |
+
private stabilizeInputsOutputs() : boolean {
|
| 87 |
+
let changed = false;
|
| 88 |
+
const hasEmptyInput = !this.inputs[this.inputs.length - 1]?.link;
|
| 89 |
+
if (!hasEmptyInput) {
|
| 90 |
+
this.addInput("", "*");
|
| 91 |
+
changed = true;
|
| 92 |
+
}
|
| 93 |
+
for (let index = this.inputs.length - 2; index >= 0; index--) {
|
| 94 |
+
const input = this.inputs[index]!;
|
| 95 |
+
if (!input.link) {
|
| 96 |
+
this.removeInput(index);
|
| 97 |
+
changed = true;
|
| 98 |
+
} else {
|
| 99 |
+
const node = getConnectedInputNodesAndFilterPassThroughs(
|
| 100 |
+
this,
|
| 101 |
+
this,
|
| 102 |
+
index,
|
| 103 |
+
this.inputsPassThroughFollowing,
|
| 104 |
+
)[0];
|
| 105 |
+
const newName = node?.title || "";
|
| 106 |
+
if (input.name !== newName) {
|
| 107 |
+
input.name = node?.title || "";
|
| 108 |
+
changed = true;
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
return changed;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
/**
|
| 116 |
+
* Stabilizes the node's inputs and widgets.
|
| 117 |
+
*/
|
| 118 |
+
private doStablization() {
|
| 119 |
+
if (!this.graph) {
|
| 120 |
+
return;
|
| 121 |
+
}
|
| 122 |
+
let dirty = false;
|
| 123 |
+
|
| 124 |
+
// When we add/remove widgets, litegraph is going to mess up the size, so we
|
| 125 |
+
// store it so we can retrieve it in computeSize. Hacky..
|
| 126 |
+
(this as any)._tempWidth = this.size[0];
|
| 127 |
+
|
| 128 |
+
dirty = this.stabilizeInputsOutputs();
|
| 129 |
+
const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this);
|
| 130 |
+
dirty = this.handleLinkedNodesStabilization(linkedNodes) || dirty;
|
| 131 |
+
|
| 132 |
+
// Only mark dirty if something's changed.
|
| 133 |
+
if (dirty) {
|
| 134 |
+
app.graph.setDirtyCanvas(true, true);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Schedule another stabilization in the future.
|
| 138 |
+
this.scheduleStabilizeWidgets(500);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/**
|
| 142 |
+
* Handles stabilization of linked nodes. To be overridden. Should return true if changes were
|
| 143 |
+
* made, or false if no changes were needed.
|
| 144 |
+
*/
|
| 145 |
+
handleLinkedNodesStabilization(linkedNodes: TLGraphNode[]) : boolean {
|
| 146 |
+
linkedNodes; // No-op, but makes overridding in VSCode cleaner.
|
| 147 |
+
throw new Error("handleLinkedNodesStabilization should be overridden.");
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
onConnectionsChainChange() {
|
| 151 |
+
this.scheduleStabilizeWidgets();
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
override onConnectionsChange(
|
| 155 |
+
type: number,
|
| 156 |
+
index: number,
|
| 157 |
+
connected: boolean,
|
| 158 |
+
linkInfo: LLink,
|
| 159 |
+
ioSlot: INodeOutputSlot | INodeInputSlot,
|
| 160 |
+
) {
|
| 161 |
+
super.onConnectionsChange &&
|
| 162 |
+
super.onConnectionsChange(type, index, connected, linkInfo, ioSlot);
|
| 163 |
+
if (!linkInfo) return;
|
| 164 |
+
// Follow outputs to see if we need to trigger an onConnectionChange.
|
| 165 |
+
const connectedNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
|
| 166 |
+
for (const node of connectedNodes) {
|
| 167 |
+
if ((node as BaseAnyInputConnectedNode).onConnectionsChainChange) {
|
| 168 |
+
(node as BaseAnyInputConnectedNode).onConnectionsChainChange();
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
this.scheduleStabilizeWidgets();
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
override removeInput(slot: number) {
|
| 175 |
+
(this as any)._tempWidth = this.size[0];
|
| 176 |
+
return super.removeInput(slot);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
override addInput(name: string, type: string | -1, extra_info?: Partial<INodeInputSlot>) {
|
| 180 |
+
(this as any)._tempWidth = this.size[0];
|
| 181 |
+
return super.addInput(name, type, extra_info);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
override addWidget<T extends IWidget>(
|
| 185 |
+
type: T["type"],
|
| 186 |
+
name: string,
|
| 187 |
+
value: T["value"],
|
| 188 |
+
callback?: T["callback"] | string,
|
| 189 |
+
options?: T["options"],
|
| 190 |
+
) {
|
| 191 |
+
(this as any)._tempWidth = this.size[0];
|
| 192 |
+
return super.addWidget(type, name, value, callback, options);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/**
|
| 196 |
+
* Guess this doesn't exist in Litegraph...
|
| 197 |
+
*/
|
| 198 |
+
override removeWidget(widgetOrSlot?: IWidget | number) {
|
| 199 |
+
(this as any)._tempWidth = this.size[0];
|
| 200 |
+
super.removeWidget(widgetOrSlot);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
override computeSize(out: Vector2) {
|
| 204 |
+
let size = super.computeSize(out);
|
| 205 |
+
if ((this as any)._tempWidth) {
|
| 206 |
+
size[0] = (this as any)._tempWidth;
|
| 207 |
+
// We sometimes get repeated calls to compute size, so debounce before clearing.
|
| 208 |
+
this.debouncerTempWidth && clearTimeout(this.debouncerTempWidth);
|
| 209 |
+
this.debouncerTempWidth = setTimeout(() => {
|
| 210 |
+
(this as any)._tempWidth = null;
|
| 211 |
+
}, 32);
|
| 212 |
+
}
|
| 213 |
+
// If we're collapsed, then subtract the total calculated height of the other input slots.
|
| 214 |
+
if (this.properties["collapse_connections"]) {
|
| 215 |
+
const rows = Math.max(this.inputs?.length || 0, this.outputs?.length || 0, 1) - 1;
|
| 216 |
+
size[1] = size[1] - rows * LiteGraph.NODE_SLOT_HEIGHT;
|
| 217 |
+
}
|
| 218 |
+
setTimeout(() => {
|
| 219 |
+
app.graph.setDirtyCanvas(true, true);
|
| 220 |
+
}, 16);
|
| 221 |
+
return size;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
/**
|
| 225 |
+
* When we connect our output, check our inputs and make sure we're not trying to connect a loop.
|
| 226 |
+
*/
|
| 227 |
+
override onConnectOutput(
|
| 228 |
+
outputIndex: number,
|
| 229 |
+
inputType: string | -1,
|
| 230 |
+
inputSlot: INodeInputSlot,
|
| 231 |
+
inputNode: TLGraphNode,
|
| 232 |
+
inputIndex: number,
|
| 233 |
+
): boolean {
|
| 234 |
+
let canConnect = true;
|
| 235 |
+
if (super.onConnectOutput) {
|
| 236 |
+
canConnect = super.onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex);
|
| 237 |
+
}
|
| 238 |
+
if (canConnect) {
|
| 239 |
+
const nodes = getConnectedInputNodes(this); // We want passthrough nodes, since they will loop.
|
| 240 |
+
if (nodes.includes(inputNode)) {
|
| 241 |
+
alert(
|
| 242 |
+
`Whoa, whoa, whoa. You've just tried to create a connection that loops back on itself, ` +
|
| 243 |
+
`a situation that could create a time paradox, the results of which could cause a ` +
|
| 244 |
+
`chain reaction that would unravel the very fabric of the space time continuum, ` +
|
| 245 |
+
`and destroy the entire universe!`,
|
| 246 |
+
);
|
| 247 |
+
canConnect = false;
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
return canConnect;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
override onConnectInput(
|
| 254 |
+
inputIndex: number,
|
| 255 |
+
outputType: string | -1,
|
| 256 |
+
outputSlot: INodeOutputSlot,
|
| 257 |
+
outputNode: TLGraphNode,
|
| 258 |
+
outputIndex: number,
|
| 259 |
+
): boolean {
|
| 260 |
+
let canConnect = true;
|
| 261 |
+
if (super.onConnectInput) {
|
| 262 |
+
canConnect = super.onConnectInput(
|
| 263 |
+
inputIndex,
|
| 264 |
+
outputType,
|
| 265 |
+
outputSlot,
|
| 266 |
+
outputNode,
|
| 267 |
+
outputIndex,
|
| 268 |
+
);
|
| 269 |
+
}
|
| 270 |
+
if (canConnect) {
|
| 271 |
+
const nodes = getConnectedOutputNodes(this); // We want passthrough nodes, since they will loop.
|
| 272 |
+
if (nodes.includes(outputNode)) {
|
| 273 |
+
alert(
|
| 274 |
+
`Whoa, whoa, whoa. You've just tried to create a connection that loops back on itself, ` +
|
| 275 |
+
`a situation that could create a time paradox, the results of which could cause a ` +
|
| 276 |
+
`chain reaction that would unravel the very fabric of the space time continuum, ` +
|
| 277 |
+
`and destroy the entire universe!`,
|
| 278 |
+
);
|
| 279 |
+
canConnect = false;
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
return canConnect;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/**
|
| 286 |
+
* If something is dropped on us, just add it to the bottom. onConnectInput should already cancel
|
| 287 |
+
* if it's disallowed.
|
| 288 |
+
*/
|
| 289 |
+
override connectByTypeOutput<T = any>(
|
| 290 |
+
slot: string | number,
|
| 291 |
+
sourceNode: TLGraphNode,
|
| 292 |
+
sourceSlotType: string,
|
| 293 |
+
optsIn: string,
|
| 294 |
+
): T | null {
|
| 295 |
+
const lastInput = this.inputs[this.inputs.length - 1];
|
| 296 |
+
if (!lastInput?.link && lastInput?.type === "*") {
|
| 297 |
+
var sourceSlot = sourceNode.findOutputSlotByType(sourceSlotType, false, true);
|
| 298 |
+
return sourceNode.connect(sourceSlot, this, slot);
|
| 299 |
+
}
|
| 300 |
+
return super.connectByTypeOutput(slot, sourceNode, sourceSlotType, optsIn);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
static override setUp() {
|
| 304 |
+
super.setUp();
|
| 305 |
+
addConnectionLayoutSupport(this, app, [
|
| 306 |
+
["Left", "Right"],
|
| 307 |
+
["Right", "Left"],
|
| 308 |
+
]);
|
| 309 |
+
addMenuItem(this, app, {
|
| 310 |
+
name: (node) =>
|
| 311 |
+
`${node.properties?.["collapse_connections"] ? "Show" : "Collapse"} Connections`,
|
| 312 |
+
property: "collapse_connections",
|
| 313 |
+
prepareValue: (_value, node) => !node.properties?.["collapse_connections"],
|
| 314 |
+
callback: (_node) => {
|
| 315 |
+
app.graph.setDirtyCanvas(true, true);
|
| 316 |
+
},
|
| 317 |
+
});
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
// Ok, hack time! LGraphNode's connectByType is powerful, but for our nodes, that have multiple "*"
|
| 322 |
+
// input types, it seems it just takes the first one, and disconnects it. I'd rather we don't do
|
| 323 |
+
// that and instead take the next free one. If that doesn't work, then we'll give it to the old
|
| 324 |
+
// method.
|
| 325 |
+
const oldLGraphNodeConnectByType = LGraphNode.prototype.connectByType;
|
| 326 |
+
LGraphNode.prototype.connectByType = function connectByType<T = any>(
|
| 327 |
+
slot: string | number,
|
| 328 |
+
sourceNode: TLGraphNode,
|
| 329 |
+
sourceSlotType: string,
|
| 330 |
+
optsIn: string,
|
| 331 |
+
): T | null {
|
| 332 |
+
// If we're droppiong on a node, and the last input is free and an "*" type, then connect there
|
| 333 |
+
// first...
|
| 334 |
+
if (sourceNode.inputs) {
|
| 335 |
+
for (const [index, input] of sourceNode.inputs.entries()) {
|
| 336 |
+
if (!input.link && input.type === "*") {
|
| 337 |
+
this.connect(slot, sourceNode, index);
|
| 338 |
+
return null;
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
return ((oldLGraphNodeConnectByType &&
|
| 343 |
+
oldLGraphNodeConnectByType.call(this, slot, sourceNode, sourceSlotType, optsIn)) ||
|
| 344 |
+
null) as T;
|
| 345 |
+
};
|
rgthree-comfy/src_web/comfyui/base_node.ts
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ComfyNodeConstructor, ComfyObjectInfo, NodeMode } from "typings/comfy.js";
|
| 2 |
+
import type {
|
| 3 |
+
IWidget,
|
| 4 |
+
SerializedLGraphNode,
|
| 5 |
+
LGraphNode as TLGraphNode,
|
| 6 |
+
LGraphCanvas,
|
| 7 |
+
ContextMenuItem,
|
| 8 |
+
INodeOutputSlot,
|
| 9 |
+
INodeInputSlot,
|
| 10 |
+
} from "typings/litegraph.js";
|
| 11 |
+
import type { RgthreeBaseServerNodeConstructor, RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js";
|
| 12 |
+
|
| 13 |
+
import { ComfyWidgets } from "scripts/widgets.js";
|
| 14 |
+
import { SERVICE as KEY_EVENT_SERVICE } from "./services/key_events_services.js";
|
| 15 |
+
import { app } from "scripts/app.js";
|
| 16 |
+
import { LogLevel, rgthree } from "./rgthree.js";
|
| 17 |
+
import { addHelpMenuItem } from "./utils.js";
|
| 18 |
+
import { RgthreeHelpDialog } from "rgthree/common/dialog.js";
|
| 19 |
+
import {
|
| 20 |
+
importIndividualNodesInnerOnDragDrop,
|
| 21 |
+
importIndividualNodesInnerOnDragOver,
|
| 22 |
+
} from "./feature_import_individual_nodes.js";
|
| 23 |
+
import { defineProperty } from "rgthree/common/shared_utils.js";
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* A base node with standard methods, directly extending the LGraphNode.
|
| 27 |
+
* This can be used for ui-nodes and a further base for server nodes.
|
| 28 |
+
*/
|
| 29 |
+
export abstract class RgthreeBaseNode extends LGraphNode {
|
| 30 |
+
/**
|
| 31 |
+
* Action strings that can be exposed and triggered from other nodes, like Fast Actions Button.
|
| 32 |
+
*/
|
| 33 |
+
static exposedActions: string[] = [];
|
| 34 |
+
|
| 35 |
+
static override title: string = "__NEED_CLASS_TITLE__";
|
| 36 |
+
static category = "rgthree";
|
| 37 |
+
static _category = "rgthree"; // `category` seems to get reset by comfy, so reset to this after.
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* The comfyClass is property ComfyUI and extensions may care about, even through it is only for
|
| 41 |
+
* server nodes. RgthreeBaseServerNode below overrides this with the expected value and we just
|
| 42 |
+
* set it here so extensions that are none the wiser don't break on some unchecked string method
|
| 43 |
+
* call on an undefined calue.
|
| 44 |
+
*/
|
| 45 |
+
comfyClass: string = "__NEED_COMFY_CLASS__";
|
| 46 |
+
|
| 47 |
+
/** Used by the ComfyUI-Manager badge. */
|
| 48 |
+
readonly nickname = "rgthree";
|
| 49 |
+
/** Are we a virtual node? */
|
| 50 |
+
readonly isVirtualNode: boolean = false;
|
| 51 |
+
/** Are we able to be dropped on (if config is enabled too). */
|
| 52 |
+
isDropEnabled = false;
|
| 53 |
+
/** A state member determining if we're currently removed. */
|
| 54 |
+
removed = false;
|
| 55 |
+
/** A state member determining if we're currently "configuring."" */
|
| 56 |
+
configuring = false;
|
| 57 |
+
/** A temporary width value that can be used to ensure compute size operates correctly. */
|
| 58 |
+
_tempWidth = 0;
|
| 59 |
+
|
| 60 |
+
/** Private Mode member so we can override the setter/getter and call an `onModeChange`. */
|
| 61 |
+
private rgthree_mode: NodeMode;
|
| 62 |
+
/** An internal bool set when `onConstructed` is run. */
|
| 63 |
+
private __constructed__ = false;
|
| 64 |
+
/** The help dialog. */
|
| 65 |
+
private helpDialog: RgthreeHelpDialog | null = null;
|
| 66 |
+
|
| 67 |
+
constructor(title = RgthreeBaseNode.title, skipOnConstructedCall = true) {
|
| 68 |
+
super(title);
|
| 69 |
+
if (title == "__NEED_CLASS_TITLE__") {
|
| 70 |
+
throw new Error("RgthreeBaseNode needs overrides.");
|
| 71 |
+
}
|
| 72 |
+
// Ensure these exist since some other extensions will break in their onNodeCreated.
|
| 73 |
+
this.widgets = this.widgets || [];
|
| 74 |
+
this.properties = this.properties || {};
|
| 75 |
+
|
| 76 |
+
// Some checks we want to do after we're constructed, looking that data is set correctly and
|
| 77 |
+
// that our base's `onConstructed` was called (if not, set a DEV warning).
|
| 78 |
+
setTimeout(() => {
|
| 79 |
+
// Check we have a comfyClass defined.
|
| 80 |
+
if (this.comfyClass == "__NEED_COMFY_CLASS__") {
|
| 81 |
+
throw new Error("RgthreeBaseNode needs a comfy class override.");
|
| 82 |
+
}
|
| 83 |
+
// Ensure we've called onConstructed before we got here.
|
| 84 |
+
this.checkAndRunOnConstructed();
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
defineProperty(this, 'mode', {
|
| 88 |
+
get: () => {
|
| 89 |
+
return this.rgthree_mode;
|
| 90 |
+
},
|
| 91 |
+
set: (mode: NodeMode) => {
|
| 92 |
+
if (this.rgthree_mode != mode) {
|
| 93 |
+
const oldMode = this.rgthree_mode;
|
| 94 |
+
this.rgthree_mode = mode;
|
| 95 |
+
this.onModeChange(oldMode, mode);
|
| 96 |
+
}
|
| 97 |
+
},
|
| 98 |
+
});
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
private checkAndRunOnConstructed() {
|
| 102 |
+
if (!this.__constructed__) {
|
| 103 |
+
this.onConstructed();
|
| 104 |
+
const [n, v] = rgthree.logger.logParts(
|
| 105 |
+
LogLevel.DEV,
|
| 106 |
+
`[RgthreeBaseNode] Child class did not call onConstructed for "${this.type}.`,
|
| 107 |
+
);
|
| 108 |
+
console[n]?.(...v);
|
| 109 |
+
}
|
| 110 |
+
return this.__constructed__;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
onDragOver(e: DragEvent): boolean {
|
| 114 |
+
if (!this.isDropEnabled) return false;
|
| 115 |
+
return importIndividualNodesInnerOnDragOver(this, e);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
async onDragDrop(e: DragEvent): Promise<boolean> {
|
| 119 |
+
if (!this.isDropEnabled) return false;
|
| 120 |
+
return importIndividualNodesInnerOnDragDrop(this, e);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/**
|
| 124 |
+
* When a node is finished with construction, we must call this. Failure to do so will result in
|
| 125 |
+
* an error message from the timeout in this base class. This is broken out and becomes the
|
| 126 |
+
* responsibility of the child class because
|
| 127 |
+
*/
|
| 128 |
+
onConstructed() {
|
| 129 |
+
if (this.__constructed__) return false;
|
| 130 |
+
// This is kinda a hack, but if this.type is still null, then set it to undefined to match.
|
| 131 |
+
this.type = this.type ?? undefined;
|
| 132 |
+
this.__constructed__ = true;
|
| 133 |
+
rgthree.invokeExtensionsAsync("nodeCreated", this);
|
| 134 |
+
return this.__constructed__;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
override configure(info: SerializedLGraphNode<TLGraphNode>): void {
|
| 138 |
+
this.configuring = true;
|
| 139 |
+
super.configure(info);
|
| 140 |
+
// Fix https://github.com/comfyanonymous/ComfyUI/issues/1448 locally.
|
| 141 |
+
// Can removed when fixed and adopted.
|
| 142 |
+
for (const w of this.widgets || []) {
|
| 143 |
+
w.last_y = w.last_y || 0;
|
| 144 |
+
}
|
| 145 |
+
this.configuring = false;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/**
|
| 149 |
+
* Override clone for, at the least, deep-copying properties.
|
| 150 |
+
*/
|
| 151 |
+
override clone() {
|
| 152 |
+
const cloned = super.clone();
|
| 153 |
+
// This is whild, but LiteGraph clone doesn't deep clone data, so we will. We'll use structured
|
| 154 |
+
// clone, which most browsers in 2022 support, but but we'll check.
|
| 155 |
+
if (cloned.properties && !!window.structuredClone) {
|
| 156 |
+
cloned.properties = structuredClone(cloned.properties);
|
| 157 |
+
}
|
| 158 |
+
return cloned;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/** When a mode change, we want all connected nodes to match. */
|
| 162 |
+
onModeChange(from: NodeMode, to: NodeMode) {
|
| 163 |
+
// Override
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/**
|
| 167 |
+
* Given a string, do something. At the least, handle any `exposedActions` that may be called and
|
| 168 |
+
* passed into from other nodes, like Fast Actions Button
|
| 169 |
+
*/
|
| 170 |
+
async handleAction(action: string) {
|
| 171 |
+
action; // No-op. Should be overridden but OK if not.
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/**
|
| 175 |
+
* Guess this doesn't exist in Litegraph...
|
| 176 |
+
*/
|
| 177 |
+
removeWidget(widgetOrSlot?: IWidget | number) {
|
| 178 |
+
if (typeof widgetOrSlot === "number") {
|
| 179 |
+
this.widgets.splice(widgetOrSlot, 1);
|
| 180 |
+
} else if (widgetOrSlot) {
|
| 181 |
+
const index = this.widgets.indexOf(widgetOrSlot);
|
| 182 |
+
if (index > -1) {
|
| 183 |
+
this.widgets.splice(index, 1);
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/**
|
| 189 |
+
* A default version of the logive when a node does not set `getSlotMenuOptions`. This is
|
| 190 |
+
* necessary because child nodes may want to define getSlotMenuOptions but LiteGraph then won't do
|
| 191 |
+
* it's default logic. This bakes it so child nodes can call this instead (and this doesn't set
|
| 192 |
+
* getSlotMenuOptions for all child nodes in case it doesn't exist).
|
| 193 |
+
*/
|
| 194 |
+
defaultGetSlotMenuOptions(slot: {
|
| 195 |
+
input?: INodeInputSlot;
|
| 196 |
+
output?: INodeOutputSlot;
|
| 197 |
+
}): ContextMenuItem[] | null {
|
| 198 |
+
const menu_info: ContextMenuItem[] = [];
|
| 199 |
+
if (slot?.output?.links?.length) {
|
| 200 |
+
menu_info.push({ content: "Disconnect Links", slot: slot });
|
| 201 |
+
}
|
| 202 |
+
let inputOrOutput = slot.input || slot.output;
|
| 203 |
+
if (inputOrOutput) {
|
| 204 |
+
if (inputOrOutput.removable) {
|
| 205 |
+
menu_info.push(
|
| 206 |
+
inputOrOutput.locked ? { content: "Cannot remove" } : { content: "Remove Slot", slot },
|
| 207 |
+
);
|
| 208 |
+
}
|
| 209 |
+
if (!inputOrOutput.nameLocked) {
|
| 210 |
+
menu_info.push({ content: "Rename Slot", slot });
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
return menu_info;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
override onRemoved(): void {
|
| 217 |
+
super.onRemoved?.();
|
| 218 |
+
this.removed = true;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
static setUp<T extends RgthreeBaseNode>(...args: any[]) {
|
| 222 |
+
// No-op.
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
/**
|
| 226 |
+
* A function to provide help text to be overridden.
|
| 227 |
+
*/
|
| 228 |
+
getHelp() {
|
| 229 |
+
return "";
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
showHelp() {
|
| 233 |
+
const help = this.getHelp() || (this.constructor as any).help;
|
| 234 |
+
if (help) {
|
| 235 |
+
this.helpDialog = new RgthreeHelpDialog(this, help).show();
|
| 236 |
+
this.helpDialog.addEventListener("close", (e) => {
|
| 237 |
+
this.helpDialog = null;
|
| 238 |
+
});
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
override onKeyDown(event: KeyboardEvent): void {
|
| 243 |
+
KEY_EVENT_SERVICE.handleKeyDownOrUp(event);
|
| 244 |
+
if (event.key == "?" && !this.helpDialog) {
|
| 245 |
+
this.showHelp();
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
override onKeyUp(event: KeyboardEvent): void {
|
| 250 |
+
KEY_EVENT_SERVICE.handleKeyDownOrUp(event);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
override getExtraMenuOptions(canvas: LGraphCanvas, options: ContextMenuItem[]): void {
|
| 254 |
+
// Some other extensions override getExtraMenuOptions on the nodeType as it comes through from
|
| 255 |
+
// the server, so we can call out to that if we don't have our own.
|
| 256 |
+
if (super.getExtraMenuOptions) {
|
| 257 |
+
super.getExtraMenuOptions?.apply(this, [canvas, options]);
|
| 258 |
+
} else if ((this.constructor as any).nodeType?.prototype?.getExtraMenuOptions) {
|
| 259 |
+
(this.constructor as any).nodeType?.prototype?.getExtraMenuOptions?.apply(this, [
|
| 260 |
+
canvas,
|
| 261 |
+
options,
|
| 262 |
+
]);
|
| 263 |
+
}
|
| 264 |
+
// If we have help content, then add a menu item.
|
| 265 |
+
const help = this.getHelp() || (this.constructor as any).help;
|
| 266 |
+
if (help) {
|
| 267 |
+
addHelpMenuItem(this, help, options);
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/**
|
| 273 |
+
* A virtual node. Right now, this is just a wrapper for RgthreeBaseNode (which was the initial
|
| 274 |
+
* base virtual node).
|
| 275 |
+
*
|
| 276 |
+
* TODO: Make RgthreeBaseNode private and move all virtual nodes to this class; cleanup
|
| 277 |
+
* RgthreeBaseNode assumptions that its virtual.
|
| 278 |
+
*/
|
| 279 |
+
export class RgthreeBaseVirtualNode extends RgthreeBaseNode {
|
| 280 |
+
override isVirtualNode = true;
|
| 281 |
+
|
| 282 |
+
constructor(title = RgthreeBaseNode.title) {
|
| 283 |
+
super(title, false);
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
static override setUp() {
|
| 287 |
+
if (!this.type) {
|
| 288 |
+
throw new Error(`Missing type for RgthreeBaseVirtualNode: ${this.title}`);
|
| 289 |
+
}
|
| 290 |
+
LiteGraph.registerNodeType(this.type, this);
|
| 291 |
+
if (this._category) {
|
| 292 |
+
this.category = this._category;
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
/**
|
| 298 |
+
* A base node with standard methods, extending the LGraphNode.
|
| 299 |
+
* This is somewhat experimental, but if comfyui is going to keep breaking widgets and inputs, it
|
| 300 |
+
* seems safer than NOT overriding.
|
| 301 |
+
*/
|
| 302 |
+
export class RgthreeBaseServerNode extends RgthreeBaseNode {
|
| 303 |
+
static nodeData: ComfyObjectInfo | null = null;
|
| 304 |
+
static nodeType: ComfyNodeConstructor | null = null;
|
| 305 |
+
|
| 306 |
+
// Drop is enabled by default for server nodes.
|
| 307 |
+
override isDropEnabled = true;
|
| 308 |
+
|
| 309 |
+
constructor(title: string) {
|
| 310 |
+
super(title, true);
|
| 311 |
+
this.serialize_widgets = true;
|
| 312 |
+
this.setupFromServerNodeData();
|
| 313 |
+
this.onConstructed();
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
getWidgets() {
|
| 317 |
+
return ComfyWidgets;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/**
|
| 321 |
+
* This takes the server data and builds out the inputs, outputs and widgets. It's similar to the
|
| 322 |
+
* ComfyNode constructor in registerNodes in ComfyUI's app.js, but is more stable and thus
|
| 323 |
+
* shouldn't break as often when it modifyies widgets and types.
|
| 324 |
+
*/
|
| 325 |
+
async setupFromServerNodeData() {
|
| 326 |
+
const nodeData = (this.constructor as any).nodeData;
|
| 327 |
+
if (!nodeData) {
|
| 328 |
+
throw Error("No node data");
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
// Necessary for serialization so Comfy backend can check types.
|
| 332 |
+
// Serialized as `class_type`. See app.js#graphToPrompt
|
| 333 |
+
this.comfyClass = nodeData.name;
|
| 334 |
+
|
| 335 |
+
let inputs = nodeData["input"]["required"];
|
| 336 |
+
if (nodeData["input"]["optional"] != undefined) {
|
| 337 |
+
inputs = Object.assign({}, inputs, nodeData["input"]["optional"]);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
const WIDGETS = this.getWidgets();
|
| 341 |
+
|
| 342 |
+
const config: { minWidth: number; minHeight: number; widget?: null | { options: any } } = {
|
| 343 |
+
minWidth: 1,
|
| 344 |
+
minHeight: 1,
|
| 345 |
+
widget: null,
|
| 346 |
+
};
|
| 347 |
+
for (const inputName in inputs) {
|
| 348 |
+
const inputData = inputs[inputName];
|
| 349 |
+
const type = inputData[0];
|
| 350 |
+
// If we're forcing the input, just do it now and forget all that widget stuff.
|
| 351 |
+
// This is one of the differences from ComfyNode and provides smoother experience for inputs
|
| 352 |
+
// that are going to remain inputs anyway.
|
| 353 |
+
// Also, it fixes https://github.com/comfyanonymous/ComfyUI/issues/1404 (for rgthree nodes)
|
| 354 |
+
if (inputData[1]?.forceInput) {
|
| 355 |
+
this.addInput(inputName, type);
|
| 356 |
+
} else {
|
| 357 |
+
let widgetCreated = true;
|
| 358 |
+
if (Array.isArray(type)) {
|
| 359 |
+
// Enums
|
| 360 |
+
Object.assign(config, WIDGETS.COMBO(this, inputName, inputData, app) || {});
|
| 361 |
+
} else if (`${type}:${inputName}` in WIDGETS) {
|
| 362 |
+
// Support custom widgets by Type:Name
|
| 363 |
+
Object.assign(
|
| 364 |
+
config,
|
| 365 |
+
WIDGETS[`${type}:${inputName}`]!(this, inputName, inputData, app) || {},
|
| 366 |
+
);
|
| 367 |
+
} else if (type in WIDGETS) {
|
| 368 |
+
// Standard type widgets
|
| 369 |
+
Object.assign(config, WIDGETS[type]!(this, inputName, inputData, app) || {});
|
| 370 |
+
} else {
|
| 371 |
+
// Node connection inputs
|
| 372 |
+
this.addInput(inputName, type);
|
| 373 |
+
widgetCreated = false;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
// Don't actually need this right now, but ported it over from ComfyWidget.
|
| 377 |
+
if (widgetCreated && inputData[1]?.forceInput && config?.widget) {
|
| 378 |
+
if (!config.widget.options) config.widget.options = {};
|
| 379 |
+
config.widget.options.forceInput = inputData[1].forceInput;
|
| 380 |
+
}
|
| 381 |
+
if (widgetCreated && inputData[1]?.defaultInput && config?.widget) {
|
| 382 |
+
if (!config.widget.options) config.widget.options = {};
|
| 383 |
+
config.widget.options.defaultInput = inputData[1].defaultInput;
|
| 384 |
+
}
|
| 385 |
+
}
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
for (const o in nodeData["output"]) {
|
| 389 |
+
let output = nodeData["output"][o];
|
| 390 |
+
if (output instanceof Array) output = "COMBO";
|
| 391 |
+
const outputName = nodeData["output_name"][o] || output;
|
| 392 |
+
const outputShape = nodeData["output_is_list"][o]
|
| 393 |
+
? LiteGraph.GRID_SHAPE
|
| 394 |
+
: LiteGraph.CIRCLE_SHAPE;
|
| 395 |
+
this.addOutput(outputName, output, { shape: outputShape });
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
const s = this.computeSize();
|
| 399 |
+
s[0] = Math.max(config.minWidth, s[0] * 1.5);
|
| 400 |
+
s[1] = Math.max(config.minHeight, s[1]);
|
| 401 |
+
this.size = s;
|
| 402 |
+
this.serialize_widgets = true;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
static __registeredForOverride__: boolean = false;
|
| 406 |
+
static registerForOverride(
|
| 407 |
+
comfyClass: ComfyNodeConstructor,
|
| 408 |
+
nodeData: ComfyObjectInfo,
|
| 409 |
+
rgthreeClass: RgthreeBaseServerNodeConstructor,
|
| 410 |
+
) {
|
| 411 |
+
if (OVERRIDDEN_SERVER_NODES.has(comfyClass)) {
|
| 412 |
+
throw Error(
|
| 413 |
+
`Already have a class to override ${
|
| 414 |
+
comfyClass.type || comfyClass.name || comfyClass.title
|
| 415 |
+
}`,
|
| 416 |
+
);
|
| 417 |
+
}
|
| 418 |
+
OVERRIDDEN_SERVER_NODES.set(comfyClass, rgthreeClass);
|
| 419 |
+
// Mark the rgthreeClass as `__registeredForOverride__` because ComfyUI will repeatedly call
|
| 420 |
+
// this and certain setups will only want to setup once (like adding context menus, etc).
|
| 421 |
+
if (!rgthreeClass.__registeredForOverride__) {
|
| 422 |
+
rgthreeClass.__registeredForOverride__ = true;
|
| 423 |
+
rgthreeClass.nodeType = comfyClass;
|
| 424 |
+
rgthreeClass.nodeData = nodeData;
|
| 425 |
+
rgthreeClass.onRegisteredForOverride(comfyClass, rgthreeClass);
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
static onRegisteredForOverride(comfyClass: any, rgthreeClass: any) {
|
| 430 |
+
// To be overridden
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
/**
|
| 435 |
+
* Keeps track of the rgthree-comfy nodes that come from the server (and want to be ComfyNodes) that
|
| 436 |
+
* we override into a own, more flexible and cleaner nodes.
|
| 437 |
+
*/
|
| 438 |
+
const OVERRIDDEN_SERVER_NODES = new Map<any, any>();
|
| 439 |
+
|
| 440 |
+
const oldregisterNodeType = LiteGraph.registerNodeType;
|
| 441 |
+
/**
|
| 442 |
+
* ComfyUI calls registerNodeType with its ComfyNode, but we don't trust that will remain stable, so
|
| 443 |
+
* we need to identify it, intercept it, and supply our own class for the node.
|
| 444 |
+
*/
|
| 445 |
+
LiteGraph.registerNodeType = async function (nodeId: string, baseClass: any) {
|
| 446 |
+
const clazz = OVERRIDDEN_SERVER_NODES.get(baseClass) || baseClass;
|
| 447 |
+
if (clazz !== baseClass) {
|
| 448 |
+
const classLabel = clazz.type || clazz.name || clazz.title;
|
| 449 |
+
const [n, v] = rgthree.logger.logParts(
|
| 450 |
+
LogLevel.DEBUG,
|
| 451 |
+
`${nodeId}: replacing default ComfyNode implementation with custom ${classLabel} class.`,
|
| 452 |
+
);
|
| 453 |
+
console[n]?.(...v);
|
| 454 |
+
// Note, we don't currently call our rgthree.invokeExtensionsAsync w/ beforeRegisterNodeDef as
|
| 455 |
+
// this runs right after that. However, this does mean that extensions cannot actually change
|
| 456 |
+
// anything about overriden server rgthree nodes in their beforeRegisterNodeDef (as when comfy
|
| 457 |
+
// calls it, it's for the wrong ComfyNode class). Calling it here, however, would re-run
|
| 458 |
+
// everything causing more issues than not. If we wanted to support beforeRegisterNodeDef then
|
| 459 |
+
// it would mean rewriting ComfyUI's registerNodeDef which, frankly, is not worth it.
|
| 460 |
+
}
|
| 461 |
+
return oldregisterNodeType.call(LiteGraph, nodeId, clazz);
|
| 462 |
+
};
|
rgthree-comfy/src_web/comfyui/base_node_collector.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { INodeOutputSlot, LGraphNode } from "typings/litegraph.js";
|
| 2 |
+
import { rgthree } from "./rgthree.js";
|
| 3 |
+
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
|
| 4 |
+
import {
|
| 5 |
+
PassThroughFollowing,
|
| 6 |
+
getConnectedInputNodes,
|
| 7 |
+
getConnectedInputNodesAndFilterPassThroughs,
|
| 8 |
+
shouldPassThrough,
|
| 9 |
+
} from "./utils.js";
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Base collector node that monitors changing inputs and outputs.
|
| 13 |
+
*/
|
| 14 |
+
export class BaseCollectorNode extends BaseAnyInputConnectedNode {
|
| 15 |
+
/**
|
| 16 |
+
* We only want to show nodes through re_route nodes, other pass through nodes show each input.
|
| 17 |
+
*/
|
| 18 |
+
override readonly inputsPassThroughFollowing: PassThroughFollowing =
|
| 19 |
+
PassThroughFollowing.REROUTE_ONLY;
|
| 20 |
+
|
| 21 |
+
readonly logger = rgthree.newLogSession("[BaseCollectorNode]");
|
| 22 |
+
|
| 23 |
+
constructor(title?: string) {
|
| 24 |
+
super(title);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
override clone() {
|
| 28 |
+
const cloned = super.clone();
|
| 29 |
+
return cloned;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
override handleLinkedNodesStabilization(linkedNodes: LGraphNode[]) {
|
| 33 |
+
return false; // No-op, no widgets.
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* When we connect an input, check to see if it's already connected and cancel it.
|
| 38 |
+
*/
|
| 39 |
+
override onConnectInput(
|
| 40 |
+
inputIndex: number,
|
| 41 |
+
outputType: string | -1,
|
| 42 |
+
outputSlot: INodeOutputSlot,
|
| 43 |
+
outputNode: LGraphNode,
|
| 44 |
+
outputIndex: number,
|
| 45 |
+
): boolean {
|
| 46 |
+
let canConnect = super.onConnectInput(
|
| 47 |
+
inputIndex,
|
| 48 |
+
outputType,
|
| 49 |
+
outputSlot,
|
| 50 |
+
outputNode,
|
| 51 |
+
outputIndex,
|
| 52 |
+
);
|
| 53 |
+
if (canConnect) {
|
| 54 |
+
const allConnectedNodes = getConnectedInputNodes(this); // We want passthrough nodes, since they will loop.
|
| 55 |
+
const nodesAlreadyInSlot = getConnectedInputNodes(this, undefined, inputIndex);
|
| 56 |
+
if (allConnectedNodes.includes(outputNode)) {
|
| 57 |
+
// If we're connecting to the same slot, then allow it by replacing the one we have.
|
| 58 |
+
// const slotsOriginNode = getOriginNodeByLink(this.inputs[inputIndex]?.link);
|
| 59 |
+
const [n, v] = this.logger.debugParts(
|
| 60 |
+
`${outputNode.title} is already connected to ${this.title}.`,
|
| 61 |
+
);
|
| 62 |
+
console[n]?.(...v);
|
| 63 |
+
if (nodesAlreadyInSlot.includes(outputNode)) {
|
| 64 |
+
const [n, v] = this.logger.debugParts(
|
| 65 |
+
`... but letting it slide since it's for the same slot.`,
|
| 66 |
+
);
|
| 67 |
+
console[n]?.(...v);
|
| 68 |
+
} else {
|
| 69 |
+
canConnect = false;
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
if (canConnect && shouldPassThrough(outputNode, PassThroughFollowing.REROUTE_ONLY)) {
|
| 73 |
+
const connectedNode = getConnectedInputNodesAndFilterPassThroughs(
|
| 74 |
+
outputNode,
|
| 75 |
+
undefined,
|
| 76 |
+
undefined,
|
| 77 |
+
PassThroughFollowing.REROUTE_ONLY,
|
| 78 |
+
)[0];
|
| 79 |
+
if (connectedNode && allConnectedNodes.includes(connectedNode)) {
|
| 80 |
+
// If we're connecting to the same slot, then allow it by replacing the one we have.
|
| 81 |
+
const [n, v] = this.logger.debugParts(
|
| 82 |
+
`${connectedNode.title} is already connected to ${this.title}.`,
|
| 83 |
+
);
|
| 84 |
+
console[n]?.(...v);
|
| 85 |
+
if (nodesAlreadyInSlot.includes(connectedNode)) {
|
| 86 |
+
const [n, v] = this.logger.debugParts(
|
| 87 |
+
`... but letting it slide since it's for the same slot.`,
|
| 88 |
+
);
|
| 89 |
+
console[n]?.(...v);
|
| 90 |
+
} else {
|
| 91 |
+
canConnect = false;
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
return canConnect;
|
| 97 |
+
}
|
| 98 |
+
}
|
rgthree-comfy/src_web/comfyui/base_node_mode_changer.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js";
|
| 2 |
+
import type {
|
| 3 |
+
LGraphNode as TLGraphNode,
|
| 4 |
+
IWidget,
|
| 5 |
+
SerializedLGraphNode,
|
| 6 |
+
} from "typings/litegraph.js";
|
| 7 |
+
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
|
| 8 |
+
import { PassThroughFollowing } from "./utils.js";
|
| 9 |
+
import { wait } from "rgthree/common/shared_utils.js";
|
| 10 |
+
|
| 11 |
+
export class BaseNodeModeChanger extends BaseAnyInputConnectedNode {
|
| 12 |
+
override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.ALL;
|
| 13 |
+
|
| 14 |
+
static collapsible = false;
|
| 15 |
+
override isVirtualNode = true;
|
| 16 |
+
|
| 17 |
+
// These Must be overriden
|
| 18 |
+
readonly modeOn: number = -1;
|
| 19 |
+
readonly modeOff: number = -1;
|
| 20 |
+
|
| 21 |
+
static "@toggleRestriction" = {
|
| 22 |
+
type: "combo",
|
| 23 |
+
values: ["default", "max one", "always one"],
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
constructor(title?: string) {
|
| 27 |
+
super(title);
|
| 28 |
+
this.properties["toggleRestriction"] = "default";
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
override onConstructed(): boolean {
|
| 32 |
+
wait(10).then(() => {
|
| 33 |
+
if (this.modeOn < 0 || this.modeOff < 0) {
|
| 34 |
+
throw new Error("modeOn and modeOff must be overridden.");
|
| 35 |
+
}
|
| 36 |
+
});
|
| 37 |
+
this.addOutput("OPT_CONNECTION", "*");
|
| 38 |
+
return super.onConstructed();
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
override configure(info: SerializedLGraphNode<TLGraphNode>): void {
|
| 42 |
+
// Patch a small issue (~14h) where multiple OPT_CONNECTIONS may have been created.
|
| 43 |
+
// https://github.com/rgthree/rgthree-comfy/issues/206
|
| 44 |
+
// TODO: This can probably be removed within a few weeks.
|
| 45 |
+
if (info.outputs?.length) {
|
| 46 |
+
info.outputs.length = 1;
|
| 47 |
+
}
|
| 48 |
+
super.configure(info);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
override handleLinkedNodesStabilization(linkedNodes: TLGraphNode[]) {
|
| 52 |
+
let changed = false;
|
| 53 |
+
for (const [index, node] of linkedNodes.entries()) {
|
| 54 |
+
let widget = this.widgets && this.widgets[index];
|
| 55 |
+
if (!widget) {
|
| 56 |
+
// When we add a widget, litegraph is going to mess up the size, so we
|
| 57 |
+
// store it so we can retrieve it in computeSize. Hacky..
|
| 58 |
+
(this as any)._tempWidth = this.size[0];
|
| 59 |
+
widget = this.addWidget("toggle", "", false, "", { on: "yes", off: "no" });
|
| 60 |
+
changed = true;
|
| 61 |
+
}
|
| 62 |
+
if (node) {
|
| 63 |
+
changed = this.setWidget(widget, node) || changed;
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
if (this.widgets && this.widgets.length > linkedNodes.length) {
|
| 67 |
+
this.widgets.length = linkedNodes.length;
|
| 68 |
+
changed = true;
|
| 69 |
+
}
|
| 70 |
+
return changed;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
private setWidget(widget: IWidget, linkedNode: TLGraphNode, forceValue?: boolean) {
|
| 74 |
+
let changed = false;
|
| 75 |
+
const value = forceValue == null ? linkedNode.mode === this.modeOn : forceValue;
|
| 76 |
+
let name = `Enable ${linkedNode.title}`;
|
| 77 |
+
// Need to set initally
|
| 78 |
+
if (widget.name !== name) {
|
| 79 |
+
widget.name = `Enable ${linkedNode.title}`;
|
| 80 |
+
widget.options = { on: "yes", off: "no" };
|
| 81 |
+
widget.value = value;
|
| 82 |
+
(widget as any).doModeChange = (forceValue?: boolean, skipOtherNodeCheck?: boolean) => {
|
| 83 |
+
let newValue = forceValue == null ? linkedNode.mode === this.modeOff : forceValue;
|
| 84 |
+
if (skipOtherNodeCheck !== true) {
|
| 85 |
+
if (newValue && this.properties?.["toggleRestriction"]?.includes(" one")) {
|
| 86 |
+
for (const widget of this.widgets) {
|
| 87 |
+
(widget as any).doModeChange(false, true);
|
| 88 |
+
}
|
| 89 |
+
} else if (!newValue && this.properties?.["toggleRestriction"] === "always one") {
|
| 90 |
+
newValue = this.widgets.every((w) => !w.value || w === widget);
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
linkedNode.mode = (newValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4;
|
| 94 |
+
widget.value = newValue;
|
| 95 |
+
};
|
| 96 |
+
widget.callback = () => {
|
| 97 |
+
(widget as any).doModeChange();
|
| 98 |
+
};
|
| 99 |
+
changed = true;
|
| 100 |
+
}
|
| 101 |
+
if (forceValue != null) {
|
| 102 |
+
const newMode = (forceValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4;
|
| 103 |
+
if (linkedNode.mode !== newMode) {
|
| 104 |
+
linkedNode.mode = newMode;
|
| 105 |
+
changed = true;
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
return changed;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
forceWidgetOff(widget: IWidget, skipOtherNodeCheck?: boolean) {
|
| 112 |
+
(widget as any).doModeChange(false, skipOtherNodeCheck);
|
| 113 |
+
}
|
| 114 |
+
forceWidgetOn(widget: IWidget, skipOtherNodeCheck?: boolean) {
|
| 115 |
+
(widget as any).doModeChange(true, skipOtherNodeCheck);
|
| 116 |
+
}
|
| 117 |
+
forceWidgetToggle(widget: IWidget, skipOtherNodeCheck?: boolean) {
|
| 118 |
+
(widget as any).doModeChange(!widget.value, skipOtherNodeCheck);
|
| 119 |
+
}
|
| 120 |
+
}
|
rgthree-comfy/src_web/comfyui/base_power_prompt.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { api } from "scripts/api.js";
|
| 2 |
+
import type {
|
| 3 |
+
LLink,
|
| 4 |
+
IComboWidget,
|
| 5 |
+
LGraphNode,
|
| 6 |
+
INodeOutputSlot,
|
| 7 |
+
INodeInputSlot,
|
| 8 |
+
IWidget,
|
| 9 |
+
SerializedLGraphNode,
|
| 10 |
+
} from "typings/litegraph.js";
|
| 11 |
+
import type { ComfyObjectInfo, ComfyGraphNode } from "typings/comfy.js";
|
| 12 |
+
import { wait } from "rgthree/common/shared_utils.js";
|
| 13 |
+
import { rgthree } from "./rgthree.js";
|
| 14 |
+
|
| 15 |
+
/** Wraps a node instance keeping closure without mucking the finicky types. */
|
| 16 |
+
export class PowerPrompt {
|
| 17 |
+
readonly isSimple: boolean;
|
| 18 |
+
readonly node: ComfyGraphNode;
|
| 19 |
+
readonly promptEl: HTMLTextAreaElement;
|
| 20 |
+
nodeData: ComfyObjectInfo;
|
| 21 |
+
readonly combos: { [key: string]: IComboWidget } = {};
|
| 22 |
+
readonly combosValues: { [key: string]: string[] } = {};
|
| 23 |
+
boundOnFreshNodeDefs!: (event: CustomEvent) => void;
|
| 24 |
+
|
| 25 |
+
private configuring = false;
|
| 26 |
+
|
| 27 |
+
constructor(node: ComfyGraphNode, nodeData: ComfyObjectInfo) {
|
| 28 |
+
this.node = node;
|
| 29 |
+
this.node.properties = this.node.properties || {};
|
| 30 |
+
|
| 31 |
+
this.node.properties["combos_filter"] = "";
|
| 32 |
+
|
| 33 |
+
this.nodeData = nodeData;
|
| 34 |
+
this.isSimple = this.nodeData.name.includes("Simple");
|
| 35 |
+
|
| 36 |
+
this.promptEl = (node.widgets[0]! as any).inputEl;
|
| 37 |
+
this.addAndHandleKeyboardLoraEditWeight();
|
| 38 |
+
|
| 39 |
+
this.patchNodeRefresh();
|
| 40 |
+
|
| 41 |
+
const oldConfigure = this.node.configure;
|
| 42 |
+
this.node.configure = (info: SerializedLGraphNode) => {
|
| 43 |
+
this.configuring = true;
|
| 44 |
+
oldConfigure?.apply(this.node, [info]);
|
| 45 |
+
this.configuring = false;
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
const oldOnConnectionsChange = this.node.onConnectionsChange;
|
| 49 |
+
this.node.onConnectionsChange = (
|
| 50 |
+
type: number,
|
| 51 |
+
slotIndex: number,
|
| 52 |
+
isConnected: boolean,
|
| 53 |
+
link_info: LLink,
|
| 54 |
+
_ioSlot: INodeOutputSlot | INodeInputSlot,
|
| 55 |
+
) => {
|
| 56 |
+
oldOnConnectionsChange?.apply(this.node, [type, slotIndex, isConnected, link_info, _ioSlot]);
|
| 57 |
+
this.onNodeConnectionsChange(type, slotIndex, isConnected, link_info, _ioSlot);
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
const oldOnConnectInput = this.node.onConnectInput;
|
| 61 |
+
this.node.onConnectInput = (
|
| 62 |
+
inputIndex: number,
|
| 63 |
+
outputType: INodeOutputSlot["type"],
|
| 64 |
+
outputSlot: INodeOutputSlot,
|
| 65 |
+
outputNode: LGraphNode,
|
| 66 |
+
outputIndex: number,
|
| 67 |
+
) => {
|
| 68 |
+
let canConnect = true;
|
| 69 |
+
if (oldOnConnectInput) {
|
| 70 |
+
canConnect = oldOnConnectInput.apply(this.node, [
|
| 71 |
+
inputIndex,
|
| 72 |
+
outputType,
|
| 73 |
+
outputSlot,
|
| 74 |
+
outputNode,
|
| 75 |
+
outputIndex,
|
| 76 |
+
]);
|
| 77 |
+
}
|
| 78 |
+
return (
|
| 79 |
+
this.configuring ||
|
| 80 |
+
rgthree.loadingApiJson ||
|
| 81 |
+
(canConnect && !this.node.inputs[inputIndex]!.disabled)
|
| 82 |
+
);
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const oldOnConnectOutput = this.node.onConnectOutput;
|
| 86 |
+
this.node.onConnectOutput = (
|
| 87 |
+
outputIndex: number,
|
| 88 |
+
inputType: INodeInputSlot["type"],
|
| 89 |
+
inputSlot: INodeInputSlot,
|
| 90 |
+
inputNode: LGraphNode,
|
| 91 |
+
inputIndex: number,
|
| 92 |
+
) => {
|
| 93 |
+
let canConnect = true;
|
| 94 |
+
if (oldOnConnectOutput) {
|
| 95 |
+
canConnect = oldOnConnectOutput?.apply(this.node, [
|
| 96 |
+
outputIndex,
|
| 97 |
+
inputType,
|
| 98 |
+
inputSlot,
|
| 99 |
+
inputNode,
|
| 100 |
+
inputIndex,
|
| 101 |
+
]);
|
| 102 |
+
}
|
| 103 |
+
return (
|
| 104 |
+
this.configuring ||
|
| 105 |
+
rgthree.loadingApiJson ||
|
| 106 |
+
(canConnect && !this.node.outputs[outputIndex]!.disabled)
|
| 107 |
+
);
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
const onPropertyChanged = this.node.onPropertyChanged;
|
| 111 |
+
this.node.onPropertyChanged = (property: string, value: any, prevValue: any) => {
|
| 112 |
+
onPropertyChanged && onPropertyChanged.call(this, property, value, prevValue);
|
| 113 |
+
if (property === "combos_filter") {
|
| 114 |
+
this.refreshCombos(this.nodeData);
|
| 115 |
+
}
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
// Strip all widgets but prompt (we'll re-add them in refreshCombos)
|
| 119 |
+
// this.node.widgets.splice(1);
|
| 120 |
+
for (let i = this.node.widgets.length - 1; i >= 0; i--) {
|
| 121 |
+
if (this.shouldRemoveServerWidget(this.node.widgets[i]!)) {
|
| 122 |
+
this.node.widgets.splice(i, 1);
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
this.refreshCombos(nodeData);
|
| 127 |
+
setTimeout(() => {
|
| 128 |
+
this.stabilizeInputsOutputs();
|
| 129 |
+
}, 32);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/**
|
| 133 |
+
* Cleans up optional out puts when we don't have the optional input. Purely a vanity function.
|
| 134 |
+
*/
|
| 135 |
+
onNodeConnectionsChange(
|
| 136 |
+
_type: number,
|
| 137 |
+
_slotIndex: number,
|
| 138 |
+
_isConnected: boolean,
|
| 139 |
+
_linkInfo: LLink,
|
| 140 |
+
_ioSlot: INodeOutputSlot | INodeInputSlot,
|
| 141 |
+
) {
|
| 142 |
+
this.stabilizeInputsOutputs();
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
private stabilizeInputsOutputs() {
|
| 146 |
+
// If we are currently "configuring" then skip this stabilization. The connected nodes may
|
| 147 |
+
// not yet be configured.
|
| 148 |
+
if (this.configuring || rgthree.loadingApiJson) {
|
| 149 |
+
return;
|
| 150 |
+
}
|
| 151 |
+
// If our first input is connected, then we can show the proper output.
|
| 152 |
+
const clipLinked = this.node.inputs.some((i) => i.name.includes("clip") && !!i.link);
|
| 153 |
+
const modelLinked = this.node.inputs.some((i) => i.name.includes("model") && !!i.link);
|
| 154 |
+
for (const output of this.node.outputs) {
|
| 155 |
+
const type = (output.type as string).toLowerCase();
|
| 156 |
+
if (type.includes("model")) {
|
| 157 |
+
output.disabled = !modelLinked;
|
| 158 |
+
} else if (type.includes("conditioning")) {
|
| 159 |
+
output.disabled = !clipLinked;
|
| 160 |
+
} else if (type.includes("clip")) {
|
| 161 |
+
output.disabled = !clipLinked;
|
| 162 |
+
} else if (type.includes("string")) {
|
| 163 |
+
// Our text prompt is always enabled, but let's color it so it stands out
|
| 164 |
+
// if the others are disabled. #7F7 is Litegraph's default.
|
| 165 |
+
output.color_off = "#7F7";
|
| 166 |
+
output.color_on = "#7F7";
|
| 167 |
+
}
|
| 168 |
+
if (output.disabled) {
|
| 169 |
+
// this.node.disconnectOutput(index);
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
onFreshNodeDefs(event: CustomEvent) {
|
| 175 |
+
this.refreshCombos(event.detail[this.nodeData.name]);
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
shouldRemoveServerWidget(widget: IWidget) {
|
| 179 |
+
return (
|
| 180 |
+
widget.name?.startsWith("insert_") ||
|
| 181 |
+
widget.name?.startsWith("target_") ||
|
| 182 |
+
widget.name?.startsWith("crop_") ||
|
| 183 |
+
widget.name?.startsWith("values_")
|
| 184 |
+
);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
refreshCombos(nodeData: ComfyObjectInfo) {
|
| 188 |
+
this.nodeData = nodeData;
|
| 189 |
+
let filter: RegExp | null = null;
|
| 190 |
+
if (this.node.properties["combos_filter"]?.trim()) {
|
| 191 |
+
try {
|
| 192 |
+
filter = new RegExp(this.node.properties["combos_filter"].trim(), "i");
|
| 193 |
+
} catch (e) {
|
| 194 |
+
console.error(`Could not parse "${filter}" for Regular Expression`, e);
|
| 195 |
+
filter = null;
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
// Add the combo for hidden inputs of nodeData
|
| 200 |
+
let data = Object.assign(
|
| 201 |
+
{},
|
| 202 |
+
this.nodeData.input?.optional || {},
|
| 203 |
+
this.nodeData.input?.hidden || {},
|
| 204 |
+
);
|
| 205 |
+
|
| 206 |
+
for (const [key, value] of Object.entries(data)) {
|
| 207 |
+
//Object.entries(this.nodeData.input?.hidden || {})) {
|
| 208 |
+
if (Array.isArray(value[0])) {
|
| 209 |
+
let values = value[0] as string[];
|
| 210 |
+
if (key.startsWith("insert")) {
|
| 211 |
+
values = filter
|
| 212 |
+
? values.filter(
|
| 213 |
+
(v, i) => i < 1 || (i == 1 && v.match(/^disable\s[a-z]/i)) || filter?.test(v),
|
| 214 |
+
)
|
| 215 |
+
: values;
|
| 216 |
+
const shouldShow =
|
| 217 |
+
values.length > 2 || (values.length > 1 && !values[1]!.match(/^disable\s[a-z]/i));
|
| 218 |
+
if (shouldShow) {
|
| 219 |
+
if (!this.combos[key]) {
|
| 220 |
+
this.combos[key] = this.node.addWidget(
|
| 221 |
+
"combo",
|
| 222 |
+
key,
|
| 223 |
+
values,
|
| 224 |
+
(selected) => {
|
| 225 |
+
if (selected !== values[0] && !selected.match(/^disable\s[a-z]/i)) {
|
| 226 |
+
// We wait a frame because if we use a keydown event to call, it'll wipe out
|
| 227 |
+
// the selection.
|
| 228 |
+
wait().then(() => {
|
| 229 |
+
if (key.includes("embedding")) {
|
| 230 |
+
this.insertSelectionText(`embedding:${selected}`);
|
| 231 |
+
} else if (key.includes("saved")) {
|
| 232 |
+
this.insertSelectionText(
|
| 233 |
+
this.combosValues[`values_${key}`]![values.indexOf(selected)]!,
|
| 234 |
+
);
|
| 235 |
+
} else if (key.includes("lora")) {
|
| 236 |
+
this.insertSelectionText(`<lora:${selected}:1.0>`);
|
| 237 |
+
}
|
| 238 |
+
this.combos[key]!.value = values[0];
|
| 239 |
+
});
|
| 240 |
+
}
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
values,
|
| 244 |
+
serialize: true, // Don't include this in prompt.
|
| 245 |
+
},
|
| 246 |
+
);
|
| 247 |
+
(this.combos[key]! as any).oldComputeSize = this.combos[key]!.computeSize;
|
| 248 |
+
let node = this.node;
|
| 249 |
+
this.combos[key]!.computeSize = function (width: number) {
|
| 250 |
+
const size = (this as any).oldComputeSize?.(width) || [
|
| 251 |
+
width,
|
| 252 |
+
LiteGraph.NODE_WIDGET_HEIGHT,
|
| 253 |
+
];
|
| 254 |
+
if (this === node.widgets[node.widgets.length - 1]) {
|
| 255 |
+
size[1] += 10;
|
| 256 |
+
}
|
| 257 |
+
return size;
|
| 258 |
+
};
|
| 259 |
+
}
|
| 260 |
+
this.combos[key]!.options!.values = values;
|
| 261 |
+
this.combos[key]!.value = values[0];
|
| 262 |
+
} else if (!shouldShow && this.combos[key]) {
|
| 263 |
+
this.node.widgets.splice(this.node.widgets.indexOf(this.combos[key]!), 1);
|
| 264 |
+
delete this.combos[key];
|
| 265 |
+
}
|
| 266 |
+
} else if (key.startsWith("values")) {
|
| 267 |
+
this.combosValues[key] = values;
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
insertSelectionText(text: string) {
|
| 274 |
+
if (!this.promptEl) {
|
| 275 |
+
console.error("Asked to insert text, but no textbox found.");
|
| 276 |
+
return;
|
| 277 |
+
}
|
| 278 |
+
let prompt = this.promptEl.value;
|
| 279 |
+
// Use selectionEnd as the split; if we have highlighted text, then we likely don't want to
|
| 280 |
+
// overwrite it (we could have just deleted it more easily).
|
| 281 |
+
let first = prompt.substring(0, this.promptEl.selectionEnd).replace(/ +$/, "");
|
| 282 |
+
first = first + (["\n"].includes(first[first.length - 1]!) ? "" : first.length ? " " : "");
|
| 283 |
+
let second = prompt.substring(this.promptEl.selectionEnd).replace(/^ +/, "");
|
| 284 |
+
second = (["\n"].includes(second[0]!) ? "" : second.length ? " " : "") + second;
|
| 285 |
+
this.promptEl.value = first + text + second;
|
| 286 |
+
this.promptEl.focus();
|
| 287 |
+
this.promptEl.selectionStart = first.length;
|
| 288 |
+
this.promptEl.selectionEnd = first.length + text.length;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
/**
|
| 292 |
+
* Adds a keydown event listener to our prompt so we can see if we're using the
|
| 293 |
+
* ctrl/cmd + up/down arrows shortcut. This kind of competes with the core extension
|
| 294 |
+
* "Comfy.EditAttention" but since that only handles parenthesis and listens on window, we should
|
| 295 |
+
* be able to intercept and cancel the bubble if we're doing the same action within the lora tag.
|
| 296 |
+
*/
|
| 297 |
+
addAndHandleKeyboardLoraEditWeight() {
|
| 298 |
+
this.promptEl.addEventListener("keydown", (event: KeyboardEvent) => {
|
| 299 |
+
// If we're not doing a ctrl/cmd + arrow key, then bail.
|
| 300 |
+
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
|
| 301 |
+
if (!event.ctrlKey && !event.metaKey) return;
|
| 302 |
+
// Unfortunately, we can't see Comfy.EditAttention delta in settings, so we hardcode to 0.01.
|
| 303 |
+
// We can acutally do better too, let's make it .1 by default, and .01 if also holding shift.
|
| 304 |
+
const delta = event.shiftKey ? 0.01 : 0.1;
|
| 305 |
+
|
| 306 |
+
let start = this.promptEl.selectionStart;
|
| 307 |
+
let end = this.promptEl.selectionEnd;
|
| 308 |
+
let fullText = this.promptEl.value;
|
| 309 |
+
let selectedText = fullText.substring(start, end);
|
| 310 |
+
|
| 311 |
+
// We don't care about fully rewriting Comfy.EditAttention, we just want to see if our
|
| 312 |
+
// selected text is a lora, which will always start with "<lora:". So work backwards until we
|
| 313 |
+
// find something that we know can't be a lora, or a "<".
|
| 314 |
+
if (!selectedText) {
|
| 315 |
+
const stopOn = "<>()\r\n\t"; // Allow spaces, since they can be in the filename
|
| 316 |
+
if (fullText[start] == ">") {
|
| 317 |
+
start -= 2;
|
| 318 |
+
end -= 2;
|
| 319 |
+
}
|
| 320 |
+
if (fullText[end - 1] == "<") {
|
| 321 |
+
start += 2;
|
| 322 |
+
end += 2;
|
| 323 |
+
}
|
| 324 |
+
while (!stopOn.includes(fullText[start]!) && start > 0) {
|
| 325 |
+
start--;
|
| 326 |
+
}
|
| 327 |
+
while (!stopOn.includes(fullText[end - 1]!) && end < fullText.length) {
|
| 328 |
+
end++;
|
| 329 |
+
}
|
| 330 |
+
selectedText = fullText.substring(start, end);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
// Bail if this isn't a lora.
|
| 334 |
+
if (!selectedText.startsWith("<lora:") || !selectedText.endsWith(">")) {
|
| 335 |
+
return;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
let weight = Number(selectedText.match(/:(-?\d*(\.\d*)?)>$/)?.[1]) ?? 1;
|
| 339 |
+
weight += event.key === "ArrowUp" ? delta : -delta;
|
| 340 |
+
const updatedText = selectedText.replace(/(:-?\d*(\.\d*)?)?>$/, `:${weight.toFixed(2)}>`);
|
| 341 |
+
|
| 342 |
+
// Handle the new value and cancel the bubble so Comfy.EditAttention doesn't also try.
|
| 343 |
+
this.promptEl.setRangeText(updatedText, start, end, "select");
|
| 344 |
+
event.preventDefault();
|
| 345 |
+
event.stopPropagation();
|
| 346 |
+
});
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
/**
|
| 350 |
+
* Patches over api.getNodeDefs in comfy's api.js to fire a custom event that we can listen to
|
| 351 |
+
* here and manually refresh our combos when a request comes in to fetch the node data; which
|
| 352 |
+
* only happens once at startup (but before custom nodes js runs), and then after clicking
|
| 353 |
+
* the "Refresh" button in the floating menu, which is what we care about.
|
| 354 |
+
*/
|
| 355 |
+
patchNodeRefresh() {
|
| 356 |
+
this.boundOnFreshNodeDefs = this.onFreshNodeDefs.bind(this);
|
| 357 |
+
api.addEventListener("fresh-node-defs", this.boundOnFreshNodeDefs as EventListener);
|
| 358 |
+
const oldNodeRemoved = this.node.onRemoved;
|
| 359 |
+
this.node.onRemoved = () => {
|
| 360 |
+
oldNodeRemoved?.call(this.node);
|
| 361 |
+
api.removeEventListener("fresh-node-defs", this.boundOnFreshNodeDefs as EventListener);
|
| 362 |
+
};
|
| 363 |
+
}
|
| 364 |
+
}
|
rgthree-comfy/src_web/comfyui/bookmark.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { app } from "scripts/app.js";
|
| 2 |
+
import { RgthreeBaseVirtualNode } from "./base_node.js";
|
| 3 |
+
import { SERVICE as KEY_EVENT_SERVICE } from "./services/key_events_services.js";
|
| 4 |
+
import { NodeTypesString } from "./constants.js";
|
| 5 |
+
import type {
|
| 6 |
+
LGraph,
|
| 7 |
+
LGraphCanvas,
|
| 8 |
+
INumberWidget,
|
| 9 |
+
LGraphNode,
|
| 10 |
+
Vector2,
|
| 11 |
+
} from "typings/litegraph.js";
|
| 12 |
+
import { getClosestOrSelf, queryOne } from "rgthree/common/utils_dom.js";
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* A bookmark node. Can be placed anywhere in the workflow, and given a shortcut key that will
|
| 16 |
+
* navigate to that node, with it in the top-left corner.
|
| 17 |
+
*/
|
| 18 |
+
export class Bookmark extends RgthreeBaseVirtualNode {
|
| 19 |
+
static override type = NodeTypesString.BOOKMARK;
|
| 20 |
+
static override title = NodeTypesString.BOOKMARK;
|
| 21 |
+
override comfyClass = NodeTypesString.BOOKMARK;
|
| 22 |
+
|
| 23 |
+
// Really silly, but Litegraph assumes we have at least one input/output... so we need to
|
| 24 |
+
// counteract it's computeSize calculation by offsetting the start.
|
| 25 |
+
static slot_start_y = -20;
|
| 26 |
+
|
| 27 |
+
// LiteGraph adds mroe spacing than we want when calculating a nodes' `_collapsed_width`, so we'll
|
| 28 |
+
// override it with a setter and re-set it measured exactly as we want.
|
| 29 |
+
___collapsed_width: number = 0;
|
| 30 |
+
|
| 31 |
+
override isVirtualNode = true;
|
| 32 |
+
override serialize_widgets = true;
|
| 33 |
+
|
| 34 |
+
//@ts-ignore - TS Doesn't like us overriding a property with accessors but, too bad.
|
| 35 |
+
override get _collapsed_width() {
|
| 36 |
+
return this.___collapsed_width;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
override set _collapsed_width(width: number) {
|
| 40 |
+
const canvas = app.canvas as LGraphCanvas;
|
| 41 |
+
const ctx = canvas.canvas.getContext("2d")!;
|
| 42 |
+
const oldFont = ctx.font;
|
| 43 |
+
ctx.font = canvas.title_text_font;
|
| 44 |
+
this.___collapsed_width = 40 + ctx.measureText(this.title).width;
|
| 45 |
+
ctx.font = oldFont;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
readonly keypressBound;
|
| 49 |
+
|
| 50 |
+
constructor(title = Bookmark.title) {
|
| 51 |
+
super(title);
|
| 52 |
+
const nextShortcutChar = getNextShortcut();
|
| 53 |
+
this.addWidget(
|
| 54 |
+
"text",
|
| 55 |
+
"shortcut_key",
|
| 56 |
+
nextShortcutChar,
|
| 57 |
+
(value: string, ...args) => {
|
| 58 |
+
value = value.trim()[0] || "1";
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
y: 8,
|
| 62 |
+
},
|
| 63 |
+
);
|
| 64 |
+
this.addWidget<INumberWidget>("number", "zoom", 1, (value: number) => {}, {
|
| 65 |
+
y: 8 + LiteGraph.NODE_WIDGET_HEIGHT + 4,
|
| 66 |
+
max: 2,
|
| 67 |
+
min: 0.5,
|
| 68 |
+
precision: 2,
|
| 69 |
+
});
|
| 70 |
+
this.keypressBound = this.onKeypress.bind(this);
|
| 71 |
+
this.title = "🔖";
|
| 72 |
+
this.onConstructed();
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// override computeSize(out?: Vector2 | undefined): Vector2 {
|
| 76 |
+
// super.computeSize(out);
|
| 77 |
+
// const minHeight = (this.widgets?.length || 0) * (LiteGraph.NODE_WIDGET_HEIGHT + 4) + 16;
|
| 78 |
+
// this.size[1] = Math.max(minHeight, this.size[1]);
|
| 79 |
+
// }
|
| 80 |
+
|
| 81 |
+
get shortcutKey(): string {
|
| 82 |
+
return this.widgets[0]?.value?.toLocaleLowerCase() ?? "";
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
override onAdded(graph: LGraph): void {
|
| 86 |
+
KEY_EVENT_SERVICE.addEventListener("keydown", this.keypressBound as EventListener);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
override onRemoved(): void {
|
| 90 |
+
KEY_EVENT_SERVICE.removeEventListener("keydown", this.keypressBound as EventListener);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
onKeypress(event: CustomEvent<{ originalEvent: KeyboardEvent }>) {
|
| 94 |
+
const originalEvent = event.detail.originalEvent;
|
| 95 |
+
const target = (originalEvent.target as HTMLElement)!;
|
| 96 |
+
if (getClosestOrSelf(target, 'input,textarea,[contenteditable="true"]')) {
|
| 97 |
+
return;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Only the shortcut keys are held down, otionally including "shift".
|
| 101 |
+
if (KEY_EVENT_SERVICE.areOnlyKeysDown(this.widgets[0]!.value, true)) {
|
| 102 |
+
this.canvasToBookmark();
|
| 103 |
+
originalEvent.preventDefault();
|
| 104 |
+
originalEvent.stopPropagation();
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/**
|
| 109 |
+
* Called from LiteGraph's `processMouseDown` after it would invoke the input box for the
|
| 110 |
+
* shortcut_key, so we check if it exists and then add our own event listener so we can track the
|
| 111 |
+
* keys down for the user.
|
| 112 |
+
*/
|
| 113 |
+
override onMouseDown(event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas): void {
|
| 114 |
+
const input = queryOne<HTMLInputElement>(".graphdialog > input.value");
|
| 115 |
+
if (input && input.value === this.widgets[0]?.value) {
|
| 116 |
+
input.addEventListener("keydown", (e) => {
|
| 117 |
+
// ComfyUI swallows keydown on inputs, so we need to call out to rgthree to use downkeys.
|
| 118 |
+
KEY_EVENT_SERVICE.handleKeyDownOrUp(e);
|
| 119 |
+
e.preventDefault();
|
| 120 |
+
e.stopPropagation();
|
| 121 |
+
input.value = Object.keys(KEY_EVENT_SERVICE.downKeys).join(" + ");
|
| 122 |
+
});
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
canvasToBookmark() {
|
| 127 |
+
const canvas = app.canvas as LGraphCanvas;
|
| 128 |
+
// ComfyUI seemed to break us again, but couldn't repro. No reason to not check, I guess.
|
| 129 |
+
// https://github.com/rgthree/rgthree-comfy/issues/71
|
| 130 |
+
if (canvas?.ds?.offset) {
|
| 131 |
+
canvas.ds.offset[0] = -this.pos[0] + 16;
|
| 132 |
+
canvas.ds.offset[1] = -this.pos[1] + 40;
|
| 133 |
+
}
|
| 134 |
+
if (canvas?.ds?.scale != null) {
|
| 135 |
+
canvas.ds.scale = Number(this.widgets[1]!.value || 1);
|
| 136 |
+
}
|
| 137 |
+
canvas.setDirty(true, true);
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
app.registerExtension({
|
| 142 |
+
name: "rgthree.Bookmark",
|
| 143 |
+
registerCustomNodes() {
|
| 144 |
+
Bookmark.setUp();
|
| 145 |
+
},
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
function isBookmark(node: LGraphNode): node is Bookmark {
|
| 149 |
+
return node.type === NodeTypesString.BOOKMARK;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
function getExistingShortcuts() {
|
| 153 |
+
const graph: LGraph = app.graph;
|
| 154 |
+
const bookmarkNodes = graph._nodes.filter(isBookmark);
|
| 155 |
+
const usedShortcuts = new Set(bookmarkNodes.map((n) => n.shortcutKey));
|
| 156 |
+
return usedShortcuts;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
const SHORTCUT_DEFAULTS = "1234567890abcdefghijklmnopqrstuvwxyz".split("");
|
| 160 |
+
function getNextShortcut() {
|
| 161 |
+
const existingShortcuts = getExistingShortcuts();
|
| 162 |
+
return SHORTCUT_DEFAULTS.find((char) => !existingShortcuts.has(char)) ?? "1";
|
| 163 |
+
}
|
rgthree-comfy/src_web/comfyui/bypasser.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { app } from "scripts/app.js";
|
| 2 |
+
import { BaseNodeModeChanger } from "./base_node_mode_changer.js";
|
| 3 |
+
import { NodeTypesString } from "./constants.js";
|
| 4 |
+
import type { LGraphNode } from "typings/litegraph.js";
|
| 5 |
+
|
| 6 |
+
const MODE_BYPASS = 4;
|
| 7 |
+
const MODE_ALWAYS = 0;
|
| 8 |
+
|
| 9 |
+
class BypasserNode extends BaseNodeModeChanger {
|
| 10 |
+
static override exposedActions = ["Bypass all", "Enable all", "Toggle all"];
|
| 11 |
+
|
| 12 |
+
static override type = NodeTypesString.FAST_BYPASSER;
|
| 13 |
+
static override title = NodeTypesString.FAST_BYPASSER;
|
| 14 |
+
override comfyClass = NodeTypesString.FAST_BYPASSER;
|
| 15 |
+
|
| 16 |
+
override readonly modeOn = MODE_ALWAYS;
|
| 17 |
+
override readonly modeOff = MODE_BYPASS;
|
| 18 |
+
|
| 19 |
+
constructor(title = BypasserNode.title) {
|
| 20 |
+
super(title);
|
| 21 |
+
this.onConstructed();
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
override async handleAction(action: string) {
|
| 25 |
+
if (action === "Bypass all") {
|
| 26 |
+
for (const widget of this.widgets) {
|
| 27 |
+
this.forceWidgetOff(widget, true);
|
| 28 |
+
}
|
| 29 |
+
} else if (action === "Enable all") {
|
| 30 |
+
for (const widget of this.widgets) {
|
| 31 |
+
this.forceWidgetOn(widget, true);
|
| 32 |
+
}
|
| 33 |
+
} else if (action === "Toggle all") {
|
| 34 |
+
for (const widget of this.widgets) {
|
| 35 |
+
this.forceWidgetToggle(widget, true);
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
app.registerExtension({
|
| 42 |
+
name: "rgthree.Bypasser",
|
| 43 |
+
registerCustomNodes() {
|
| 44 |
+
BypasserNode.setUp();
|
| 45 |
+
},
|
| 46 |
+
loadedGraphNode(node: LGraphNode) {
|
| 47 |
+
if (node.type == BypasserNode.title) {
|
| 48 |
+
(node as any)._tempWidth = node.size[0];
|
| 49 |
+
}
|
| 50 |
+
},
|
| 51 |
+
});
|
rgthree-comfy/src_web/comfyui/comfy_ui_bar.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { app } from "scripts/app.js";
|
| 2 |
+
import { ComfyButtonGroup } from "scripts/ui/components/buttonGroup.js";
|
| 3 |
+
import { ComfyButton } from "scripts/ui/components/button.js";
|
| 4 |
+
import { iconGear, iconStarFilled, logoRgthree } from "rgthree/common/media/svgs.js";
|
| 5 |
+
import { createElement, empty, queryOne } from "rgthree/common/utils_dom.js";
|
| 6 |
+
import { SERVICE as BOOKMARKS_SERVICE } from "./services/bookmarks_services.js";
|
| 7 |
+
import { SERVICE as CONFIG_SERVICE } from "./services/config_service.js";
|
| 8 |
+
import { ComfyPopup } from "scripts/ui/components/popup.js";
|
| 9 |
+
import { RgthreeConfigDialog } from "./config.js";
|
| 10 |
+
|
| 11 |
+
let rgthreeButtonGroup: ComfyButtonGroup | null = null;
|
| 12 |
+
|
| 13 |
+
function addRgthreeTopBarButtons() {
|
| 14 |
+
if (!CONFIG_SERVICE.getFeatureValue("comfy_top_bar_menu.enabled")) {
|
| 15 |
+
if (rgthreeButtonGroup?.element?.parentElement) {
|
| 16 |
+
rgthreeButtonGroup.element.parentElement.removeChild(rgthreeButtonGroup.element);
|
| 17 |
+
}
|
| 18 |
+
return;
|
| 19 |
+
} else if (rgthreeButtonGroup) {
|
| 20 |
+
app.menu?.settingsGroup.element.before(rgthreeButtonGroup.element);
|
| 21 |
+
return;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const buttons = [];
|
| 25 |
+
|
| 26 |
+
const rgthreeButton = new ComfyButton({
|
| 27 |
+
icon: "rgthree",
|
| 28 |
+
tooltip: "rgthree-comfy",
|
| 29 |
+
// content: 'rgthree-comfy',
|
| 30 |
+
app,
|
| 31 |
+
enabled: true,
|
| 32 |
+
classList: "comfyui-button comfyui-menu-mobile-collapse primary",
|
| 33 |
+
});
|
| 34 |
+
buttons.push(rgthreeButton);
|
| 35 |
+
rgthreeButton.iconElement.style.width = "1.2rem";
|
| 36 |
+
rgthreeButton.iconElement.innerHTML = logoRgthree;
|
| 37 |
+
rgthreeButton.withPopup(
|
| 38 |
+
new ComfyPopup(
|
| 39 |
+
{ target: rgthreeButton.element, classList: "rgthree-top-menu" },
|
| 40 |
+
createElement("menu", {
|
| 41 |
+
children: [
|
| 42 |
+
createElement("li", {
|
| 43 |
+
child: createElement("button.rgthree-button-reset", {
|
| 44 |
+
html: iconGear + "Settings (rgthree-comfy)",
|
| 45 |
+
onclick: () => new RgthreeConfigDialog().show(),
|
| 46 |
+
}),
|
| 47 |
+
}),
|
| 48 |
+
createElement("li", {
|
| 49 |
+
child: createElement("button.rgthree-button-reset", {
|
| 50 |
+
html: iconStarFilled + "Star on Github",
|
| 51 |
+
onclick: () => window.open("https://github.com/rgthree/rgthree-comfy", "_blank"),
|
| 52 |
+
}),
|
| 53 |
+
}),
|
| 54 |
+
],
|
| 55 |
+
}),
|
| 56 |
+
),
|
| 57 |
+
"click",
|
| 58 |
+
);
|
| 59 |
+
|
| 60 |
+
if (CONFIG_SERVICE.getFeatureValue("comfy_top_bar_menu.button_bookmarks.enabled")) {
|
| 61 |
+
const bookmarksListEl = createElement("menu");
|
| 62 |
+
bookmarksListEl.appendChild(
|
| 63 |
+
createElement("li.rgthree-message", {
|
| 64 |
+
child: createElement("span", { text: "No bookmarks in current workflow." }),
|
| 65 |
+
}),
|
| 66 |
+
);
|
| 67 |
+
const bookmarksButton = new ComfyButton({
|
| 68 |
+
icon: "bookmark",
|
| 69 |
+
tooltip: "Workflow Bookmarks (rgthree-comfy)",
|
| 70 |
+
app,
|
| 71 |
+
});
|
| 72 |
+
const bookmarksPopup = new ComfyPopup(
|
| 73 |
+
{ target: bookmarksButton.element, classList: "rgthree-top-menu" },
|
| 74 |
+
bookmarksListEl,
|
| 75 |
+
);
|
| 76 |
+
bookmarksPopup.addEventListener("open", () => {
|
| 77 |
+
const bookmarks = BOOKMARKS_SERVICE.getCurrentBookmarks();
|
| 78 |
+
empty(bookmarksListEl);
|
| 79 |
+
if (bookmarks.length) {
|
| 80 |
+
for (const b of bookmarks) {
|
| 81 |
+
bookmarksListEl.appendChild(
|
| 82 |
+
createElement("li", {
|
| 83 |
+
child: createElement("button.rgthree-button-reset", {
|
| 84 |
+
text: `[${b.shortcutKey}] ${b.title}`,
|
| 85 |
+
onclick: () => {
|
| 86 |
+
b.canvasToBookmark();
|
| 87 |
+
},
|
| 88 |
+
}),
|
| 89 |
+
}),
|
| 90 |
+
);
|
| 91 |
+
}
|
| 92 |
+
} else {
|
| 93 |
+
bookmarksListEl.appendChild(
|
| 94 |
+
createElement("li.rgthree-message", {
|
| 95 |
+
child: createElement("span", { text: "No bookmarks in current workflow." }),
|
| 96 |
+
}),
|
| 97 |
+
);
|
| 98 |
+
}
|
| 99 |
+
bookmarksPopup.update();
|
| 100 |
+
});
|
| 101 |
+
bookmarksButton.withPopup(bookmarksPopup, "hover");
|
| 102 |
+
buttons.push(bookmarksButton);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
rgthreeButtonGroup = new ComfyButtonGroup(...buttons);
|
| 106 |
+
app.menu?.settingsGroup.element.before(rgthreeButtonGroup.element);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
app.registerExtension({
|
| 110 |
+
name: "rgthree.TopMenu",
|
| 111 |
+
async setup() {
|
| 112 |
+
addRgthreeTopBarButtons();
|
| 113 |
+
|
| 114 |
+
CONFIG_SERVICE.addEventListener("config-change", ((e: CustomEvent) => {
|
| 115 |
+
if (e.detail?.key?.includes("features.comfy_top_bar_menu")) {
|
| 116 |
+
addRgthreeTopBarButtons();
|
| 117 |
+
}
|
| 118 |
+
}) as EventListener);
|
| 119 |
+
},
|
| 120 |
+
});
|
rgthree-comfy/src_web/comfyui/config.ts
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { app } from "scripts/app.js";
|
| 2 |
+
import { RgthreeDialog, RgthreeDialogOptions } from "rgthree/common/dialog.js";
|
| 3 |
+
import { createElement as $el, query as $$ } from "rgthree/common/utils_dom.js";
|
| 4 |
+
import { checkmark, logoRgthree } from "rgthree/common/media/svgs.js";
|
| 5 |
+
import { LogLevel, rgthree } from "./rgthree.js";
|
| 6 |
+
import { SERVICE as CONFIG_SERVICE } from "./services/config_service.js";
|
| 7 |
+
|
| 8 |
+
/** Types of config used as a hint for the form handling. */
|
| 9 |
+
enum ConfigType {
|
| 10 |
+
UNKNOWN,
|
| 11 |
+
BOOLEAN,
|
| 12 |
+
STRING,
|
| 13 |
+
NUMBER,
|
| 14 |
+
ARRAY,
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
enum ConfigInputType {
|
| 18 |
+
UNKNOWN,
|
| 19 |
+
CHECKLIST, // Which is a multiselect array.
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const TYPE_TO_STRING = {
|
| 23 |
+
[ConfigType.UNKNOWN]: "unknown",
|
| 24 |
+
[ConfigType.BOOLEAN]: "boolean",
|
| 25 |
+
[ConfigType.STRING]: "string",
|
| 26 |
+
[ConfigType.NUMBER]: "number",
|
| 27 |
+
[ConfigType.ARRAY]: "array",
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
type ConfigurationSchema = {
|
| 31 |
+
key: string;
|
| 32 |
+
type: ConfigType;
|
| 33 |
+
label: string;
|
| 34 |
+
inputType?: ConfigInputType,
|
| 35 |
+
options?: string[] | number[] | ConfigurationSchemaOption[];
|
| 36 |
+
description?: string;
|
| 37 |
+
subconfig?: ConfigurationSchema[];
|
| 38 |
+
isDevOnly?: boolean;
|
| 39 |
+
onSave?: (value: any) => void;
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
type ConfigurationSchemaOption = { value: any; label: string };
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* A static schema of sorts to layout options found in the config.
|
| 46 |
+
*/
|
| 47 |
+
const CONFIGURABLE: { [key: string]: ConfigurationSchema[] } = {
|
| 48 |
+
features: [
|
| 49 |
+
{
|
| 50 |
+
key: "features.progress_bar.enabled",
|
| 51 |
+
type: ConfigType.BOOLEAN,
|
| 52 |
+
label: "Prompt Progress Bar",
|
| 53 |
+
description: `Shows a minimal progress bar for nodes and steps at the top of the app.`,
|
| 54 |
+
subconfig: [
|
| 55 |
+
{
|
| 56 |
+
key: "features.progress_bar.height",
|
| 57 |
+
type: ConfigType.NUMBER,
|
| 58 |
+
label: "Height of the bar",
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
key: "features.progress_bar.position",
|
| 62 |
+
type: ConfigType.STRING,
|
| 63 |
+
label: "Position at top or bottom of window",
|
| 64 |
+
options: ["top", "bottom"],
|
| 65 |
+
},
|
| 66 |
+
],
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
key: "features.import_individual_nodes.enabled",
|
| 70 |
+
type: ConfigType.BOOLEAN,
|
| 71 |
+
label: "Import Individual Nodes Widgets",
|
| 72 |
+
description:
|
| 73 |
+
"Dragging & Dropping a similar image/JSON workflow onto (most) current workflow nodes" +
|
| 74 |
+
"will allow you to import that workflow's node's widgets when it has the same " +
|
| 75 |
+
"id and type. This is useful when you have several images and you'd like to import just " +
|
| 76 |
+
"one part of a previous iteration, like a seed, or prompt.",
|
| 77 |
+
},
|
| 78 |
+
],
|
| 79 |
+
menus: [
|
| 80 |
+
{
|
| 81 |
+
key: "features.comfy_top_bar_menu.enabled",
|
| 82 |
+
type: ConfigType.BOOLEAN,
|
| 83 |
+
label: "Enable Top Bar Menu",
|
| 84 |
+
description:
|
| 85 |
+
"Have quick access from ComfyUI's new top bar to rgthree-comfy bookmarks, settings " +
|
| 86 |
+
"(and more to come).",
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
key: "features.menu_queue_selected_nodes",
|
| 90 |
+
type: ConfigType.BOOLEAN,
|
| 91 |
+
label: "Show 'Queue Selected Output Nodes'",
|
| 92 |
+
description:
|
| 93 |
+
"Will show a menu item in the right-click context menus to queue (only) the selected " +
|
| 94 |
+
"output nodes.",
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
key: "features.menu_auto_nest.subdirs",
|
| 98 |
+
type: ConfigType.BOOLEAN,
|
| 99 |
+
label: "Auto Nest Subdirectories in Menus",
|
| 100 |
+
description:
|
| 101 |
+
"When a large, flat list of values contain sub-directories, auto nest them. (Like, for " +
|
| 102 |
+
"a large list of checkpoints).",
|
| 103 |
+
subconfig: [
|
| 104 |
+
{
|
| 105 |
+
key: "features.menu_auto_nest.threshold",
|
| 106 |
+
type: ConfigType.NUMBER,
|
| 107 |
+
label: "Number of items needed to trigger nesting.",
|
| 108 |
+
},
|
| 109 |
+
],
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
key: "features.menu_bookmarks.enabled",
|
| 113 |
+
type: ConfigType.BOOLEAN,
|
| 114 |
+
label: "Show Bookmarks in context menu",
|
| 115 |
+
description: "Will list bookmarks in the rgthree-comfy right-click context menu.",
|
| 116 |
+
},
|
| 117 |
+
],
|
| 118 |
+
groups: [
|
| 119 |
+
{
|
| 120 |
+
key: "features.group_header_fast_toggle.enabled",
|
| 121 |
+
type: ConfigType.BOOLEAN,
|
| 122 |
+
label: "Show fast toggles in Group Headers",
|
| 123 |
+
description: "Show quick toggles in Groups' Headers to quickly mute, bypass or queue.",
|
| 124 |
+
subconfig: [
|
| 125 |
+
{
|
| 126 |
+
key: "features.group_header_fast_toggle.toggles",
|
| 127 |
+
type: ConfigType.ARRAY,
|
| 128 |
+
label: "Which toggles to show.",
|
| 129 |
+
inputType: ConfigInputType.CHECKLIST,
|
| 130 |
+
options: [
|
| 131 |
+
{ value: "queue", label: "queue" },
|
| 132 |
+
{ value: "bypass", label: "bypass" },
|
| 133 |
+
{ value: "mute", label: "mute" },
|
| 134 |
+
],
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
key: "features.group_header_fast_toggle.show",
|
| 138 |
+
type: ConfigType.STRING,
|
| 139 |
+
label: "When to show them.",
|
| 140 |
+
options: [
|
| 141 |
+
{ value: "hover", label: "on hover" },
|
| 142 |
+
{ value: "always", label: "always" },
|
| 143 |
+
],
|
| 144 |
+
},
|
| 145 |
+
],
|
| 146 |
+
},
|
| 147 |
+
],
|
| 148 |
+
advanced: [
|
| 149 |
+
{
|
| 150 |
+
key: "features.show_alerts_for_corrupt_workflows",
|
| 151 |
+
type: ConfigType.BOOLEAN,
|
| 152 |
+
label: "Detect Corrupt Workflows",
|
| 153 |
+
description:
|
| 154 |
+
"Will show a message at the top of the screen when loading a workflow that has " +
|
| 155 |
+
"corrupt linking data.",
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
key: "log_level",
|
| 159 |
+
type: ConfigType.STRING,
|
| 160 |
+
label: "Log level for browser dev console.",
|
| 161 |
+
description:
|
| 162 |
+
"Further down the list, the more verbose logs to the console will be. For instance, " +
|
| 163 |
+
"selecting 'IMPORTANT' means only important message will be logged to the browser " +
|
| 164 |
+
"console, while selecting 'WARN' will log all messages at or higher than WARN, including " +
|
| 165 |
+
"'ERROR' and 'IMPORTANT' etc.",
|
| 166 |
+
options: ["IMPORTANT", "ERROR", "WARN", "INFO", "DEBUG", "DEV"],
|
| 167 |
+
isDevOnly: true,
|
| 168 |
+
onSave: function (value: LogLevel) {
|
| 169 |
+
rgthree.setLogLevel(value);
|
| 170 |
+
},
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
key: "features.invoke_extensions_async.node_created",
|
| 174 |
+
type: ConfigType.BOOLEAN,
|
| 175 |
+
label: "Allow other extensions to call nodeCreated on rgthree-nodes.",
|
| 176 |
+
isDevOnly: true,
|
| 177 |
+
description:
|
| 178 |
+
"Do not disable unless you are having trouble (and then file an issue at rgthree-comfy)." +
|
| 179 |
+
"Prior to Apr 2024 it was not possible for other extensions to invoke their nodeCreated " +
|
| 180 |
+
"event on some rgthree-comfy nodes. Now it's possible and this option is only here in " +
|
| 181 |
+
"for easy if something is wrong.",
|
| 182 |
+
},
|
| 183 |
+
],
|
| 184 |
+
};
|
| 185 |
+
|
| 186 |
+
/**
|
| 187 |
+
* Creates a new fieldrow for main or sub configuration items.
|
| 188 |
+
*/
|
| 189 |
+
function fieldrow(item: ConfigurationSchema) {
|
| 190 |
+
const initialValue = CONFIG_SERVICE.getConfigValue(item.key);
|
| 191 |
+
const container = $el(`div.fieldrow.-type-${TYPE_TO_STRING[item.type]}`, {
|
| 192 |
+
dataset: {
|
| 193 |
+
name: item.key,
|
| 194 |
+
initial: initialValue,
|
| 195 |
+
type: item.type,
|
| 196 |
+
},
|
| 197 |
+
});
|
| 198 |
+
|
| 199 |
+
$el(`label[for="${item.key}"]`, {
|
| 200 |
+
children: [
|
| 201 |
+
$el(`span[text="${item.label}"]`),
|
| 202 |
+
item.description ? $el("small", { html: item.description }) : null,
|
| 203 |
+
],
|
| 204 |
+
parent: container,
|
| 205 |
+
});
|
| 206 |
+
|
| 207 |
+
let input;
|
| 208 |
+
if (item.options?.length) {
|
| 209 |
+
if (item.inputType === ConfigInputType.CHECKLIST) {
|
| 210 |
+
const initialValueList = initialValue || [];
|
| 211 |
+
input = $el<HTMLSelectElement>(`fieldset.rgthree-checklist-group[id="${item.key}"]`, {
|
| 212 |
+
parent: container,
|
| 213 |
+
children: item.options.map((o) => {
|
| 214 |
+
const label = (o as ConfigurationSchemaOption).label || String(o);
|
| 215 |
+
const value = (o as ConfigurationSchemaOption).value || o;
|
| 216 |
+
const id = `${item.key}_${value}`;
|
| 217 |
+
return $el<HTMLSpanElement>(`span.rgthree-checklist-item`, {
|
| 218 |
+
children: [
|
| 219 |
+
$el<HTMLInputElement>(`input[type="checkbox"][value="${value}"]`, {
|
| 220 |
+
id,
|
| 221 |
+
checked: initialValueList.includes(value),
|
| 222 |
+
}),
|
| 223 |
+
$el<HTMLInputElement>(`label`, {
|
| 224 |
+
for: id,
|
| 225 |
+
text: label,
|
| 226 |
+
})
|
| 227 |
+
]
|
| 228 |
+
});
|
| 229 |
+
}),
|
| 230 |
+
});
|
| 231 |
+
} else {
|
| 232 |
+
input = $el<HTMLSelectElement>(`select[id="${item.key}"]`, {
|
| 233 |
+
parent: container,
|
| 234 |
+
children: item.options.map((o) => {
|
| 235 |
+
const label = (o as ConfigurationSchemaOption).label || String(o);
|
| 236 |
+
const value = (o as ConfigurationSchemaOption).value || o;
|
| 237 |
+
const valueSerialized = JSON.stringify({ value: value });
|
| 238 |
+
return $el<HTMLOptionElement>(`option[value="${valueSerialized}"]`, {
|
| 239 |
+
text: label,
|
| 240 |
+
selected: valueSerialized === JSON.stringify({ value: initialValue }),
|
| 241 |
+
});
|
| 242 |
+
}),
|
| 243 |
+
});
|
| 244 |
+
}
|
| 245 |
+
} else if (item.type === ConfigType.BOOLEAN) {
|
| 246 |
+
container.classList.toggle("-checked", !!initialValue);
|
| 247 |
+
input = $el<HTMLInputElement>(`input[type="checkbox"][id="${item.key}"]`, {
|
| 248 |
+
parent: container,
|
| 249 |
+
checked: initialValue,
|
| 250 |
+
});
|
| 251 |
+
} else {
|
| 252 |
+
input = $el(`input[id="${item.key}"]`, {
|
| 253 |
+
parent: container,
|
| 254 |
+
value: initialValue,
|
| 255 |
+
});
|
| 256 |
+
}
|
| 257 |
+
$el("div.fieldrow-value", { children: [input], parent: container });
|
| 258 |
+
return container;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/**
|
| 262 |
+
* A dialog to edit rgthree-comfy settings and config.
|
| 263 |
+
*/
|
| 264 |
+
export class RgthreeConfigDialog extends RgthreeDialog {
|
| 265 |
+
constructor() {
|
| 266 |
+
const content = $el("div");
|
| 267 |
+
|
| 268 |
+
content.appendChild(RgthreeConfigDialog.buildFieldset(CONFIGURABLE["features"]!, "Features"));
|
| 269 |
+
content.appendChild(RgthreeConfigDialog.buildFieldset(CONFIGURABLE["menus"]!, "Menus"));
|
| 270 |
+
content.appendChild(RgthreeConfigDialog.buildFieldset(CONFIGURABLE["groups"]!, "Groups"));
|
| 271 |
+
content.appendChild(RgthreeConfigDialog.buildFieldset(CONFIGURABLE["advanced"]!, "Advanced"));
|
| 272 |
+
|
| 273 |
+
content.addEventListener("input", (e) => {
|
| 274 |
+
const changed = this.getChangedFormData();
|
| 275 |
+
($$(".save-button", this.element)[0] as HTMLButtonElement).disabled =
|
| 276 |
+
!Object.keys(changed).length;
|
| 277 |
+
});
|
| 278 |
+
content.addEventListener("change", (e) => {
|
| 279 |
+
const changed = this.getChangedFormData();
|
| 280 |
+
($$(".save-button", this.element)[0] as HTMLButtonElement).disabled =
|
| 281 |
+
!Object.keys(changed).length;
|
| 282 |
+
});
|
| 283 |
+
|
| 284 |
+
const dialogOptions: RgthreeDialogOptions = {
|
| 285 |
+
class: "-iconed -settings",
|
| 286 |
+
title: logoRgthree + `<h2>Settings - rgthree-comfy</h2>`,
|
| 287 |
+
content,
|
| 288 |
+
onBeforeClose: () => {
|
| 289 |
+
const changed = this.getChangedFormData();
|
| 290 |
+
if (Object.keys(changed).length) {
|
| 291 |
+
return confirm("Looks like there are unsaved changes. Are you sure you want close?");
|
| 292 |
+
}
|
| 293 |
+
return true;
|
| 294 |
+
},
|
| 295 |
+
buttons: [
|
| 296 |
+
{
|
| 297 |
+
label: "Save",
|
| 298 |
+
disabled: true,
|
| 299 |
+
className: "rgthree-button save-button -blue",
|
| 300 |
+
callback: async (e) => {
|
| 301 |
+
const changed = this.getChangedFormData();
|
| 302 |
+
if (!Object.keys(changed).length) {
|
| 303 |
+
this.close();
|
| 304 |
+
return;
|
| 305 |
+
}
|
| 306 |
+
const success = await CONFIG_SERVICE.setConfigValues(changed);
|
| 307 |
+
if (success) {
|
| 308 |
+
for (const key of Object.keys(changed)) {
|
| 309 |
+
Object.values(CONFIGURABLE)
|
| 310 |
+
.flat()
|
| 311 |
+
.find((f) => f.key === key)
|
| 312 |
+
?.onSave?.(changed[key]);
|
| 313 |
+
}
|
| 314 |
+
this.close();
|
| 315 |
+
rgthree.showMessage({
|
| 316 |
+
id: "config-success",
|
| 317 |
+
message: `${checkmark} Successfully saved rgthree-comfy settings!`,
|
| 318 |
+
timeout: 4000,
|
| 319 |
+
});
|
| 320 |
+
($$(".save-button", this.element)[0] as HTMLButtonElement).disabled = true;
|
| 321 |
+
} else {
|
| 322 |
+
alert("There was an error saving rgthree-comfy configuration.");
|
| 323 |
+
}
|
| 324 |
+
},
|
| 325 |
+
},
|
| 326 |
+
],
|
| 327 |
+
};
|
| 328 |
+
super(dialogOptions);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
private static buildFieldset(datas: ConfigurationSchema[], label: string) {
|
| 332 |
+
const fieldset = $el(`fieldset`, { children: [$el(`legend[text="${label}"]`)] });
|
| 333 |
+
for (const data of datas) {
|
| 334 |
+
if (data.isDevOnly && !rgthree.isDevMode()) {
|
| 335 |
+
continue;
|
| 336 |
+
}
|
| 337 |
+
const container = $el("div.formrow");
|
| 338 |
+
container.appendChild(fieldrow(data));
|
| 339 |
+
|
| 340 |
+
if (data.subconfig) {
|
| 341 |
+
for (const subfeature of data.subconfig) {
|
| 342 |
+
container.appendChild(fieldrow(subfeature));
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
fieldset.appendChild(container);
|
| 346 |
+
}
|
| 347 |
+
return fieldset;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
getChangedFormData() {
|
| 351 |
+
return $$("[data-name]", this.contentElement).reduce((acc: { [key: string]: any }, el) => {
|
| 352 |
+
const name = el.dataset["name"]!;
|
| 353 |
+
const type = el.dataset["type"]!;
|
| 354 |
+
const initialValue = CONFIG_SERVICE.getConfigValue(name);
|
| 355 |
+
let currentValueEl = $$("fieldset.rgthree-checklist-group, input, textarea, select", el)[0] as HTMLInputElement;
|
| 356 |
+
let currentValue: any = null;
|
| 357 |
+
if (type === String(ConfigType.BOOLEAN)) {
|
| 358 |
+
currentValue = currentValueEl.checked;
|
| 359 |
+
// Not sure I like this side effect in here, but it's easy to just do it now.
|
| 360 |
+
el.classList.toggle("-checked", currentValue);
|
| 361 |
+
} else {
|
| 362 |
+
currentValue = currentValueEl?.value;
|
| 363 |
+
if (currentValueEl.nodeName === "SELECT") {
|
| 364 |
+
currentValue = JSON.parse(currentValue).value;
|
| 365 |
+
} else if (currentValueEl.classList.contains('rgthree-checklist-group')) {
|
| 366 |
+
currentValue = [];
|
| 367 |
+
for (const check of $$<HTMLInputElement>('input[type="checkbox"]', currentValueEl)) {
|
| 368 |
+
if (check.checked) {
|
| 369 |
+
currentValue.push(check.value);
|
| 370 |
+
}
|
| 371 |
+
}
|
| 372 |
+
} else if (type === String(ConfigType.NUMBER)) {
|
| 373 |
+
currentValue = Number(currentValue) || initialValue;
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
if (JSON.stringify(currentValue) !== JSON.stringify(initialValue)) {
|
| 377 |
+
acc[name] = currentValue;
|
| 378 |
+
}
|
| 379 |
+
return acc;
|
| 380 |
+
}, {});
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
app.ui.settings.addSetting({
|
| 385 |
+
id: "rgthree.config",
|
| 386 |
+
name: "Open rgthree-comfy config",
|
| 387 |
+
type: () => {
|
| 388 |
+
// Adds a row to open the dialog from the ComfyUI settings.
|
| 389 |
+
return $el("tr.rgthree-comfyui-settings-row", {
|
| 390 |
+
children: [
|
| 391 |
+
$el("td", {
|
| 392 |
+
child: `<div>${logoRgthree} [rgthree-comfy] configuration / settings</div>`,
|
| 393 |
+
}),
|
| 394 |
+
$el("td", {
|
| 395 |
+
child: $el('button.rgthree-button.-blue[text="rgthree-comfy settings"]', {
|
| 396 |
+
events: {
|
| 397 |
+
click: (e: PointerEvent) => {
|
| 398 |
+
new RgthreeConfigDialog().show();
|
| 399 |
+
},
|
| 400 |
+
},
|
| 401 |
+
}),
|
| 402 |
+
}),
|
| 403 |
+
],
|
| 404 |
+
});
|
| 405 |
+
},
|
| 406 |
+
});
|
rgthree-comfy/src_web/comfyui/constants.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js";
|
| 2 |
+
|
| 3 |
+
export function addRgthree(str: string) {
|
| 4 |
+
return str + " (rgthree)";
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export function stripRgthree(str: string) {
|
| 8 |
+
return str.replace(/\s*\(rgthree\)$/, "");
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export const NodeTypesString = {
|
| 12 |
+
ANY_SWITCH: addRgthree("Any Switch"),
|
| 13 |
+
CONTEXT: addRgthree("Context"),
|
| 14 |
+
CONTEXT_BIG: addRgthree("Context Big"),
|
| 15 |
+
CONTEXT_SWITCH: addRgthree("Context Switch"),
|
| 16 |
+
CONTEXT_SWITCH_BIG: addRgthree("Context Switch Big"),
|
| 17 |
+
CONTEXT_MERGE: addRgthree("Context Merge"),
|
| 18 |
+
CONTEXT_MERGE_BIG: addRgthree("Context Merge Big"),
|
| 19 |
+
DYNAMIC_CONTEXT: addRgthree("Dynamic Context"),
|
| 20 |
+
DYNAMIC_CONTEXT_SWITCH: addRgthree("Dynamic Context Switch"),
|
| 21 |
+
DISPLAY_ANY: addRgthree("Display Any"),
|
| 22 |
+
NODE_MODE_RELAY: addRgthree("Mute / Bypass Relay"),
|
| 23 |
+
NODE_MODE_REPEATER: addRgthree("Mute / Bypass Repeater"),
|
| 24 |
+
FAST_MUTER: addRgthree("Fast Muter"),
|
| 25 |
+
FAST_BYPASSER: addRgthree("Fast Bypasser"),
|
| 26 |
+
FAST_GROUPS_MUTER: addRgthree("Fast Groups Muter"),
|
| 27 |
+
FAST_GROUPS_BYPASSER: addRgthree("Fast Groups Bypasser"),
|
| 28 |
+
FAST_ACTIONS_BUTTON: addRgthree("Fast Actions Button"),
|
| 29 |
+
LABEL: addRgthree("Label"),
|
| 30 |
+
POWER_PROMPT: addRgthree("Power Prompt"),
|
| 31 |
+
POWER_PROMPT_SIMPLE: addRgthree("Power Prompt - Simple"),
|
| 32 |
+
SDXL_EMPTY_LATENT_IMAGE: addRgthree("SDXL Empty Latent Image"),
|
| 33 |
+
SDXL_POWER_PROMPT_POSITIVE: addRgthree("SDXL Power Prompt - Positive"),
|
| 34 |
+
SDXL_POWER_PROMPT_NEGATIVE: addRgthree("SDXL Power Prompt - Simple / Negative"),
|
| 35 |
+
POWER_LORA_LOADER: addRgthree("Power Lora Loader"),
|
| 36 |
+
KSAMPLER_CONFIG: addRgthree("KSampler Config"),
|
| 37 |
+
NODE_COLLECTOR: addRgthree("Node Collector"),
|
| 38 |
+
REROUTE: addRgthree("Reroute"),
|
| 39 |
+
RANDOM_UNMUTER: addRgthree("Random Unmuter"),
|
| 40 |
+
SEED: addRgthree("Seed"),
|
| 41 |
+
BOOKMARK: addRgthree("Bookmark"),
|
| 42 |
+
IMAGE_COMPARER: addRgthree("Image Comparer"),
|
| 43 |
+
IMAGE_INSET_CROP: addRgthree("Image Inset Crop"),
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Gets the list of nodes from NoteTypeString above, filtering any that are not applicable.
|
| 48 |
+
*/
|
| 49 |
+
export function getNodeTypeStrings() {
|
| 50 |
+
return Object.values(NodeTypesString)
|
| 51 |
+
.map((i) => stripRgthree(i))
|
| 52 |
+
.filter((i) => {
|
| 53 |
+
if (
|
| 54 |
+
i.startsWith("Dynamic Context") &&
|
| 55 |
+
!CONFIG_SERVICE.getConfigValue("unreleased.dynamic_context.enabled")
|
| 56 |
+
) {
|
| 57 |
+
return false;
|
| 58 |
+
}
|
| 59 |
+
return true;
|
| 60 |
+
})
|
| 61 |
+
.sort();
|
| 62 |
+
}
|
rgthree-comfy/src_web/comfyui/context.ts
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
INodeInputSlot,
|
| 3 |
+
INodeOutputSlot,
|
| 4 |
+
LGraphCanvas as TLGraphCanvas,
|
| 5 |
+
LGraphNode as TLGraphNode,
|
| 6 |
+
LLink,
|
| 7 |
+
} from "typings/litegraph.js";
|
| 8 |
+
import type { ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
|
| 9 |
+
import { app } from "scripts/app.js";
|
| 10 |
+
import {
|
| 11 |
+
IoDirection,
|
| 12 |
+
addConnectionLayoutSupport,
|
| 13 |
+
addMenuItem,
|
| 14 |
+
matchLocalSlotsToServer,
|
| 15 |
+
replaceNode,
|
| 16 |
+
} from "./utils.js";
|
| 17 |
+
import { RgthreeBaseServerNode } from "./base_node.js";
|
| 18 |
+
import { SERVICE as KEY_EVENT_SERVICE } from "./services/key_events_services.js";
|
| 19 |
+
import { RgthreeBaseServerNodeConstructor } from "typings/rgthree.js";
|
| 20 |
+
import { debounce, wait } from "rgthree/common/shared_utils.js";
|
| 21 |
+
import { removeUnusedInputsFromEnd } from "./utils_inputs_outputs.js";
|
| 22 |
+
import { NodeTypesString } from "./constants.js";
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Takes a non-context node and determins for its input or output slot, if there is a valid
|
| 26 |
+
* connection for an opposite context output or input slot.
|
| 27 |
+
*/
|
| 28 |
+
function findMatchingIndexByTypeOrName(
|
| 29 |
+
otherNode: TLGraphNode,
|
| 30 |
+
otherSlot: INodeInputSlot | INodeOutputSlot,
|
| 31 |
+
ctxSlots: INodeInputSlot[] | INodeOutputSlot[],
|
| 32 |
+
) {
|
| 33 |
+
const otherNodeType = (otherNode.type || "").toUpperCase();
|
| 34 |
+
const otherNodeName = (otherNode.title || "").toUpperCase();
|
| 35 |
+
let otherSlotType = otherSlot.type as string;
|
| 36 |
+
if (Array.isArray(otherSlotType) || otherSlotType.includes(",")) {
|
| 37 |
+
otherSlotType = "COMBO";
|
| 38 |
+
}
|
| 39 |
+
const otherSlotName = otherSlot.name.toUpperCase().replace("OPT_", "").replace("_NAME", "");
|
| 40 |
+
let ctxSlotIndex = -1;
|
| 41 |
+
if (["CONDITIONING", "INT", "STRING", "FLOAT", "COMBO"].includes(otherSlotType)) {
|
| 42 |
+
ctxSlotIndex = ctxSlots.findIndex((ctxSlot) => {
|
| 43 |
+
const ctxSlotName = ctxSlot.name.toUpperCase().replace("OPT_", "").replace("_NAME", "");
|
| 44 |
+
let ctxSlotType = ctxSlot.type as string;
|
| 45 |
+
if (Array.isArray(ctxSlotType) || ctxSlotType.includes(",")) {
|
| 46 |
+
ctxSlotType = "COMBO";
|
| 47 |
+
}
|
| 48 |
+
if (ctxSlotType !== otherSlotType) {
|
| 49 |
+
return false;
|
| 50 |
+
}
|
| 51 |
+
// Straightforward matches.
|
| 52 |
+
if (
|
| 53 |
+
ctxSlotName === otherSlotName ||
|
| 54 |
+
(ctxSlotName === "SEED" && otherSlotName.includes("SEED")) ||
|
| 55 |
+
(ctxSlotName === "STEP_REFINER" && otherSlotName.includes("AT_STEP")) ||
|
| 56 |
+
(ctxSlotName === "STEP_REFINER" && otherSlotName.includes("REFINER_STEP"))
|
| 57 |
+
) {
|
| 58 |
+
return true;
|
| 59 |
+
}
|
| 60 |
+
// If postive other node, try to match conditining and text.
|
| 61 |
+
if (
|
| 62 |
+
(otherNodeType.includes("POSITIVE") || otherNodeName.includes("POSITIVE")) &&
|
| 63 |
+
((ctxSlotName === "POSITIVE" && otherSlotType === "CONDITIONING") ||
|
| 64 |
+
(ctxSlotName === "TEXT_POS_G" && otherSlotName.includes("TEXT_G")) ||
|
| 65 |
+
(ctxSlotName === "TEXT_POS_L" && otherSlotName.includes("TEXT_L")))
|
| 66 |
+
) {
|
| 67 |
+
return true;
|
| 68 |
+
}
|
| 69 |
+
if (
|
| 70 |
+
(otherNodeType.includes("NEGATIVE") || otherNodeName.includes("NEGATIVE")) &&
|
| 71 |
+
((ctxSlotName === "NEGATIVE" && otherSlotType === "CONDITIONING") ||
|
| 72 |
+
(ctxSlotName === "TEXT_NEG_G" && otherSlotName.includes("TEXT_G")) ||
|
| 73 |
+
(ctxSlotName === "TEXT_NEG_L" && otherSlotName.includes("TEXT_L")))
|
| 74 |
+
) {
|
| 75 |
+
return true;
|
| 76 |
+
}
|
| 77 |
+
return false;
|
| 78 |
+
});
|
| 79 |
+
} else {
|
| 80 |
+
ctxSlotIndex = ctxSlots.map((s) => s.type).indexOf(otherSlotType);
|
| 81 |
+
}
|
| 82 |
+
return ctxSlotIndex;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* A Base Context node for other context based nodes to extend.
|
| 87 |
+
*/
|
| 88 |
+
export class BaseContextNode extends RgthreeBaseServerNode {
|
| 89 |
+
constructor(title: string) {
|
| 90 |
+
super(title);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// LiteGraph adds more spacing than we want when calculating a nodes' `_collapsed_width`, so we'll
|
| 94 |
+
// override it with a setter and re-set it measured exactly as we want.
|
| 95 |
+
___collapsed_width: number = 0;
|
| 96 |
+
|
| 97 |
+
//@ts-ignore - TS Doesn't like us overriding a property with accessors but, too bad.
|
| 98 |
+
override get _collapsed_width() {
|
| 99 |
+
return this.___collapsed_width;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
override set _collapsed_width(width: number) {
|
| 103 |
+
const canvas = app.canvas as TLGraphCanvas;
|
| 104 |
+
const ctx = canvas.canvas.getContext("2d")!;
|
| 105 |
+
const oldFont = ctx.font;
|
| 106 |
+
ctx.font = canvas.title_text_font;
|
| 107 |
+
let title = this.title.trim();
|
| 108 |
+
this.___collapsed_width = 30 + (title ? 10 + ctx.measureText(title).width : 0);
|
| 109 |
+
ctx.font = oldFont;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
override connectByType<T = any>(
|
| 113 |
+
slot: string | number,
|
| 114 |
+
sourceNode: TLGraphNode,
|
| 115 |
+
sourceSlotType: string,
|
| 116 |
+
optsIn: string,
|
| 117 |
+
): T | null {
|
| 118 |
+
let canConnect =
|
| 119 |
+
super.connectByType &&
|
| 120 |
+
super.connectByType.call(this, slot, sourceNode, sourceSlotType, optsIn);
|
| 121 |
+
if (!super.connectByType) {
|
| 122 |
+
canConnect = LGraphNode.prototype.connectByType.call(
|
| 123 |
+
this,
|
| 124 |
+
slot,
|
| 125 |
+
sourceNode,
|
| 126 |
+
sourceSlotType,
|
| 127 |
+
optsIn,
|
| 128 |
+
);
|
| 129 |
+
}
|
| 130 |
+
if (!canConnect && slot === 0) {
|
| 131 |
+
const ctrlKey = KEY_EVENT_SERVICE.ctrlKey;
|
| 132 |
+
// Okay, we've dragged a context and it can't connect.. let's connect all the other nodes.
|
| 133 |
+
// Unfortunately, we don't know which are null now, so we'll just connect any that are
|
| 134 |
+
// not already connected.
|
| 135 |
+
for (const [index, input] of (sourceNode.inputs || []).entries()) {
|
| 136 |
+
if (input.link && !ctrlKey) {
|
| 137 |
+
continue;
|
| 138 |
+
}
|
| 139 |
+
const thisOutputSlot = findMatchingIndexByTypeOrName(sourceNode, input, this.outputs);
|
| 140 |
+
if (thisOutputSlot > -1) {
|
| 141 |
+
this.connect(thisOutputSlot, sourceNode, index);
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
return null;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
override connectByTypeOutput<T = any>(
|
| 149 |
+
slot: string | number,
|
| 150 |
+
sourceNode: TLGraphNode,
|
| 151 |
+
sourceSlotType: string,
|
| 152 |
+
optsIn: string,
|
| 153 |
+
): T | null {
|
| 154 |
+
let canConnect =
|
| 155 |
+
super.connectByTypeOutput &&
|
| 156 |
+
super.connectByTypeOutput.call(this, slot, sourceNode, sourceSlotType, optsIn);
|
| 157 |
+
if (!super.connectByType) {
|
| 158 |
+
canConnect = LGraphNode.prototype.connectByTypeOutput.call(
|
| 159 |
+
this,
|
| 160 |
+
slot,
|
| 161 |
+
sourceNode,
|
| 162 |
+
sourceSlotType,
|
| 163 |
+
optsIn,
|
| 164 |
+
);
|
| 165 |
+
}
|
| 166 |
+
if (!canConnect && slot === 0) {
|
| 167 |
+
const ctrlKey = KEY_EVENT_SERVICE.ctrlKey;
|
| 168 |
+
// Okay, we've dragged a context and it can't connect.. let's connect all the other nodes.
|
| 169 |
+
// Unfortunately, we don't know which are null now, so we'll just connect any that are
|
| 170 |
+
// not already connected.
|
| 171 |
+
for (const [index, output] of (sourceNode.outputs || []).entries()) {
|
| 172 |
+
if (output.links?.length && !ctrlKey) {
|
| 173 |
+
continue;
|
| 174 |
+
}
|
| 175 |
+
const thisInputSlot = findMatchingIndexByTypeOrName(sourceNode, output, this.inputs);
|
| 176 |
+
if (thisInputSlot > -1) {
|
| 177 |
+
sourceNode.connect(index, this, thisInputSlot);
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
return null;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
static override setUp(
|
| 185 |
+
comfyClass: ComfyNodeConstructor,
|
| 186 |
+
nodeData: ComfyObjectInfo,
|
| 187 |
+
ctxClass: RgthreeBaseServerNodeConstructor,
|
| 188 |
+
) {
|
| 189 |
+
RgthreeBaseServerNode.registerForOverride(comfyClass, nodeData, ctxClass);
|
| 190 |
+
// [🤮] ComfyUI only adds "required" inputs to the outputs list when dragging an output to
|
| 191 |
+
// empty space, but since RGTHREE_CONTEXT is optional, it doesn't get added to the menu because
|
| 192 |
+
// ...of course. So, we'll manually add it. Of course, we also have to do this in a timeout
|
| 193 |
+
// because ComfyUI clears out `LiteGraph.slot_types_default_out` in its own 'Comfy.SlotDefaults'
|
| 194 |
+
// extension and we need to wait for that to happen.
|
| 195 |
+
wait(500).then(() => {
|
| 196 |
+
LiteGraph.slot_types_default_out["RGTHREE_CONTEXT"] =
|
| 197 |
+
LiteGraph.slot_types_default_out["RGTHREE_CONTEXT"] || [];
|
| 198 |
+
LiteGraph.slot_types_default_out["RGTHREE_CONTEXT"].push(comfyClass.comfyClass);
|
| 199 |
+
});
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
|
| 203 |
+
addConnectionLayoutSupport(ctxClass, app, [
|
| 204 |
+
["Left", "Right"],
|
| 205 |
+
["Right", "Left"],
|
| 206 |
+
]);
|
| 207 |
+
setTimeout(() => {
|
| 208 |
+
ctxClass.category = comfyClass.category;
|
| 209 |
+
});
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/**
|
| 214 |
+
* The original Context node.
|
| 215 |
+
*/
|
| 216 |
+
class ContextNode extends BaseContextNode {
|
| 217 |
+
static override title = NodeTypesString.CONTEXT;
|
| 218 |
+
static override type = NodeTypesString.CONTEXT;
|
| 219 |
+
static comfyClass = NodeTypesString.CONTEXT;
|
| 220 |
+
|
| 221 |
+
constructor(title = ContextNode.title) {
|
| 222 |
+
super(title);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 226 |
+
BaseContextNode.setUp(comfyClass, nodeData, ContextNode);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
|
| 230 |
+
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
|
| 231 |
+
addMenuItem(ContextNode, app, {
|
| 232 |
+
name: "Convert To Context Big",
|
| 233 |
+
callback: (node) => {
|
| 234 |
+
replaceNode(node, ContextBigNode.type);
|
| 235 |
+
},
|
| 236 |
+
});
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
/**
|
| 241 |
+
* The Context Big node.
|
| 242 |
+
*/
|
| 243 |
+
class ContextBigNode extends BaseContextNode {
|
| 244 |
+
static override title = NodeTypesString.CONTEXT_BIG;
|
| 245 |
+
static override type = NodeTypesString.CONTEXT_BIG;
|
| 246 |
+
static comfyClass = NodeTypesString.CONTEXT_BIG;
|
| 247 |
+
|
| 248 |
+
constructor(title = ContextBigNode.title) {
|
| 249 |
+
super(title);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 253 |
+
BaseContextNode.setUp(comfyClass, nodeData, ContextBigNode);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
|
| 257 |
+
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
|
| 258 |
+
addMenuItem(ContextBigNode, app, {
|
| 259 |
+
name: "Convert To Context (Original)",
|
| 260 |
+
callback: (node) => {
|
| 261 |
+
replaceNode(node, ContextNode.type);
|
| 262 |
+
},
|
| 263 |
+
});
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
/**
|
| 268 |
+
* A base node for Context Switche nodes and Context Merges nodes that will always add another empty
|
| 269 |
+
* ctx input, no less than five.
|
| 270 |
+
*/
|
| 271 |
+
class BaseContextMultiCtxInputNode extends BaseContextNode {
|
| 272 |
+
private stabilizeBound = this.stabilize.bind(this);
|
| 273 |
+
|
| 274 |
+
constructor(title: string) {
|
| 275 |
+
super(title);
|
| 276 |
+
// Adding five. Note, configure will add as many as was in the stored workflow automatically.
|
| 277 |
+
this.addContextInput(5);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
private addContextInput(num = 1) {
|
| 281 |
+
for (let i = 0; i < num; i++) {
|
| 282 |
+
this.addInput(`ctx_${String(this.inputs.length + 1).padStart(2, "0")}`, "RGTHREE_CONTEXT");
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
override onConnectionsChange(
|
| 287 |
+
type: number,
|
| 288 |
+
slotIndex: number,
|
| 289 |
+
isConnected: boolean,
|
| 290 |
+
link: LLink,
|
| 291 |
+
ioSlot: INodeInputSlot | INodeOutputSlot,
|
| 292 |
+
): void {
|
| 293 |
+
super.onConnectionsChange?.apply(this, [...arguments] as any);
|
| 294 |
+
if (type === LiteGraph.INPUT) {
|
| 295 |
+
this.scheduleStabilize();
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
private scheduleStabilize(ms = 64) {
|
| 300 |
+
return debounce(this.stabilizeBound, 64);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/**
|
| 304 |
+
* Stabilizes the inputs; removing any disconnected ones from the bottom, then adding an empty
|
| 305 |
+
* one to the end so we always have one empty one to expand.
|
| 306 |
+
*/
|
| 307 |
+
private stabilize() {
|
| 308 |
+
removeUnusedInputsFromEnd(this, 4);
|
| 309 |
+
this.addContextInput();
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
/**
|
| 314 |
+
* The Context Switch (original) node.
|
| 315 |
+
*/
|
| 316 |
+
class ContextSwitchNode extends BaseContextMultiCtxInputNode {
|
| 317 |
+
static override title = NodeTypesString.CONTEXT_SWITCH;
|
| 318 |
+
static override type = NodeTypesString.CONTEXT_SWITCH;
|
| 319 |
+
static comfyClass = NodeTypesString.CONTEXT_SWITCH;
|
| 320 |
+
|
| 321 |
+
constructor(title = ContextSwitchNode.title) {
|
| 322 |
+
super(title);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 326 |
+
BaseContextNode.setUp(comfyClass, nodeData, ContextSwitchNode);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
|
| 330 |
+
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
|
| 331 |
+
addMenuItem(ContextSwitchNode, app, {
|
| 332 |
+
name: "Convert To Context Switch Big",
|
| 333 |
+
callback: (node) => {
|
| 334 |
+
replaceNode(node, ContextSwitchBigNode.type);
|
| 335 |
+
},
|
| 336 |
+
});
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/**
|
| 341 |
+
* The Context Switch Big node.
|
| 342 |
+
*/
|
| 343 |
+
class ContextSwitchBigNode extends BaseContextMultiCtxInputNode {
|
| 344 |
+
static override title = NodeTypesString.CONTEXT_SWITCH_BIG;
|
| 345 |
+
static override type = NodeTypesString.CONTEXT_SWITCH_BIG;
|
| 346 |
+
static comfyClass = NodeTypesString.CONTEXT_SWITCH_BIG;
|
| 347 |
+
|
| 348 |
+
constructor(title = ContextSwitchBigNode.title) {
|
| 349 |
+
super(title);
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 353 |
+
BaseContextNode.setUp(comfyClass, nodeData, ContextSwitchBigNode);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
|
| 357 |
+
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
|
| 358 |
+
addMenuItem(ContextSwitchBigNode, app, {
|
| 359 |
+
name: "Convert To Context Switch",
|
| 360 |
+
callback: (node) => {
|
| 361 |
+
replaceNode(node, ContextSwitchNode.type);
|
| 362 |
+
},
|
| 363 |
+
});
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
/**
|
| 368 |
+
* The Context Merge (original) node.
|
| 369 |
+
*/
|
| 370 |
+
class ContextMergeNode extends BaseContextMultiCtxInputNode {
|
| 371 |
+
static override title = NodeTypesString.CONTEXT_MERGE;
|
| 372 |
+
static override type = NodeTypesString.CONTEXT_MERGE;
|
| 373 |
+
static comfyClass = NodeTypesString.CONTEXT_MERGE;
|
| 374 |
+
|
| 375 |
+
constructor(title = ContextMergeNode.title) {
|
| 376 |
+
super(title);
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 380 |
+
BaseContextNode.setUp(comfyClass, nodeData, ContextMergeNode);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
|
| 384 |
+
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
|
| 385 |
+
addMenuItem(ContextMergeNode, app, {
|
| 386 |
+
name: "Convert To Context Merge Big",
|
| 387 |
+
callback: (node) => {
|
| 388 |
+
replaceNode(node, ContextMergeBigNode.type);
|
| 389 |
+
},
|
| 390 |
+
});
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
/**
|
| 395 |
+
* The Context Switch Big node.
|
| 396 |
+
*/
|
| 397 |
+
class ContextMergeBigNode extends BaseContextMultiCtxInputNode {
|
| 398 |
+
static override title = NodeTypesString.CONTEXT_MERGE_BIG;
|
| 399 |
+
static override type = NodeTypesString.CONTEXT_MERGE_BIG;
|
| 400 |
+
static comfyClass = NodeTypesString.CONTEXT_MERGE_BIG;
|
| 401 |
+
|
| 402 |
+
constructor(title = ContextMergeBigNode.title) {
|
| 403 |
+
super(title);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 407 |
+
BaseContextNode.setUp(comfyClass, nodeData, ContextMergeBigNode);
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
|
| 411 |
+
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
|
| 412 |
+
addMenuItem(ContextMergeBigNode, app, {
|
| 413 |
+
name: "Convert To Context Switch",
|
| 414 |
+
callback: (node) => {
|
| 415 |
+
replaceNode(node, ContextMergeNode.type);
|
| 416 |
+
},
|
| 417 |
+
});
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
const contextNodes = [
|
| 422 |
+
ContextNode,
|
| 423 |
+
ContextBigNode,
|
| 424 |
+
ContextSwitchNode,
|
| 425 |
+
ContextSwitchBigNode,
|
| 426 |
+
ContextMergeNode,
|
| 427 |
+
ContextMergeBigNode,
|
| 428 |
+
];
|
| 429 |
+
const contextTypeToServerDef: { [type: string]: ComfyObjectInfo } = {};
|
| 430 |
+
|
| 431 |
+
function fixBadConfigs(node: ContextNode) {
|
| 432 |
+
// Dumb mistake, but let's fix our mispelling. This will probably need to stay in perpetuity to
|
| 433 |
+
// keep any old workflows operating.
|
| 434 |
+
const wrongName = node.outputs.find((o, i) => o.name === "CLIP_HEIGTH");
|
| 435 |
+
if (wrongName) {
|
| 436 |
+
wrongName.name = "CLIP_HEIGHT";
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
app.registerExtension({
|
| 441 |
+
name: "rgthree.Context",
|
| 442 |
+
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 443 |
+
// Loop over out context nodes and see if any match the server data.
|
| 444 |
+
for (const ctxClass of contextNodes) {
|
| 445 |
+
if (nodeData.name === ctxClass.type) {
|
| 446 |
+
contextTypeToServerDef[ctxClass.type] = nodeData;
|
| 447 |
+
ctxClass.setUp(nodeType, nodeData);
|
| 448 |
+
break;
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
},
|
| 452 |
+
|
| 453 |
+
async nodeCreated(node: TLGraphNode) {
|
| 454 |
+
const type = node.type || (node.constructor as any).type;
|
| 455 |
+
const serverDef = type && contextTypeToServerDef[type];
|
| 456 |
+
if (serverDef) {
|
| 457 |
+
fixBadConfigs(node as ContextNode);
|
| 458 |
+
matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef);
|
| 459 |
+
// Switches don't need to change inputs, only context outputs
|
| 460 |
+
if (!type!.includes("Switch") && !type!.includes("Merge")) {
|
| 461 |
+
matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef);
|
| 462 |
+
}
|
| 463 |
+
// }, 100);
|
| 464 |
+
}
|
| 465 |
+
},
|
| 466 |
+
|
| 467 |
+
/**
|
| 468 |
+
* When we're loaded from the server, check if we're using an out of date version and update our
|
| 469 |
+
* inputs / outputs to match.
|
| 470 |
+
*/
|
| 471 |
+
async loadedGraphNode(node: TLGraphNode) {
|
| 472 |
+
const type = node.type || (node.constructor as any).type;
|
| 473 |
+
const serverDef = type && contextTypeToServerDef[type];
|
| 474 |
+
if (serverDef) {
|
| 475 |
+
fixBadConfigs(node as ContextNode);
|
| 476 |
+
matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef);
|
| 477 |
+
// Switches don't need to change inputs, only context outputs
|
| 478 |
+
if (!type!.includes("Switch") && !type!.includes("Merge")) {
|
| 479 |
+
matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef);
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
},
|
| 483 |
+
});
|
rgthree-comfy/src_web/comfyui/dialog_info.ts
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {RgthreeDialog, RgthreeDialogOptions} from "rgthree/common/dialog.js";
|
| 2 |
+
import {
|
| 3 |
+
createElement as $el,
|
| 4 |
+
empty,
|
| 5 |
+
appendChildren,
|
| 6 |
+
getClosestOrSelf,
|
| 7 |
+
queryOne,
|
| 8 |
+
query,
|
| 9 |
+
setAttributes,
|
| 10 |
+
} from "rgthree/common/utils_dom.js";
|
| 11 |
+
import {
|
| 12 |
+
logoCivitai,
|
| 13 |
+
link,
|
| 14 |
+
pencilColored,
|
| 15 |
+
diskColored,
|
| 16 |
+
dotdotdot,
|
| 17 |
+
} from "rgthree/common/media/svgs.js";
|
| 18 |
+
import {RgthreeModelInfo} from "typings/rgthree.js";
|
| 19 |
+
import {LORA_INFO_SERVICE} from "rgthree/common/model_info_service.js";
|
| 20 |
+
import {rgthree} from "./rgthree.js";
|
| 21 |
+
import {MenuButton} from "rgthree/common/menu.js";
|
| 22 |
+
import {generateId, injectCss} from "rgthree/common/shared_utils.js";
|
| 23 |
+
import {rgthreeApi} from "rgthree/common/rgthree_api.js";
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* A dialog that displays information about a model/lora/etc.
|
| 27 |
+
*/
|
| 28 |
+
abstract class RgthreeInfoDialog extends RgthreeDialog {
|
| 29 |
+
private modifiedModelData = false;
|
| 30 |
+
private modelInfo: RgthreeModelInfo | null = null;
|
| 31 |
+
|
| 32 |
+
constructor(file: string, type: string = "lora") {
|
| 33 |
+
const dialogOptions: RgthreeDialogOptions = {
|
| 34 |
+
class: "rgthree-info-dialog",
|
| 35 |
+
title: `<h2>Loading...</h2>`,
|
| 36 |
+
content: "<center>Loading..</center>",
|
| 37 |
+
onBeforeClose: () => {
|
| 38 |
+
return true;
|
| 39 |
+
},
|
| 40 |
+
};
|
| 41 |
+
super(dialogOptions);
|
| 42 |
+
this.init(file);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
abstract getModelInfo(file: string): Promise<RgthreeModelInfo | null>;
|
| 46 |
+
abstract refreshModelInfo(file: string): Promise<RgthreeModelInfo | null>;
|
| 47 |
+
abstract clearModelInfo(file: string): Promise<RgthreeModelInfo | null>;
|
| 48 |
+
|
| 49 |
+
private async init(file: string) {
|
| 50 |
+
const cssPromise = injectCss("rgthree/common/css/dialog_model_info.css");
|
| 51 |
+
this.modelInfo = await this.getModelInfo(file);
|
| 52 |
+
await cssPromise;
|
| 53 |
+
this.setContent(this.getInfoContent());
|
| 54 |
+
this.setTitle(this.modelInfo?.["name"] || this.modelInfo?.["file"] || "Unknown");
|
| 55 |
+
this.attachEvents();
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
protected override getCloseEventDetail(): {detail: any} {
|
| 59 |
+
const detail = {
|
| 60 |
+
dirty: this.modifiedModelData,
|
| 61 |
+
};
|
| 62 |
+
return {detail};
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
private attachEvents() {
|
| 66 |
+
this.contentElement.addEventListener("click", async (e: MouseEvent) => {
|
| 67 |
+
const target = getClosestOrSelf(e.target as HTMLElement, "[data-action]");
|
| 68 |
+
const action = target?.getAttribute("data-action");
|
| 69 |
+
if (!target || !action) {
|
| 70 |
+
return;
|
| 71 |
+
}
|
| 72 |
+
await this.handleEventAction(action, target, e);
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
private async handleEventAction(action: string, target: HTMLElement, e?: Event) {
|
| 77 |
+
const info = this.modelInfo!;
|
| 78 |
+
if (!info?.file) {
|
| 79 |
+
return;
|
| 80 |
+
}
|
| 81 |
+
if (action === "fetch-civitai") {
|
| 82 |
+
this.modelInfo = await this.refreshModelInfo(info.file);
|
| 83 |
+
this.setContent(this.getInfoContent());
|
| 84 |
+
this.setTitle(this.modelInfo?.["name"] || this.modelInfo?.["file"] || "Unknown");
|
| 85 |
+
} else if (action === "copy-trained-words") {
|
| 86 |
+
const selected = query(".-rgthree-is-selected", target.closest("tr")!);
|
| 87 |
+
const text = selected.map((el) => el.getAttribute("data-word")).join(", ");
|
| 88 |
+
await navigator.clipboard.writeText(text);
|
| 89 |
+
rgthree.showMessage({
|
| 90 |
+
id: "copy-trained-words-" + generateId(4),
|
| 91 |
+
type: "success",
|
| 92 |
+
message: `Successfully copied ${selected.length} key word${
|
| 93 |
+
selected.length === 1 ? "" : "s"
|
| 94 |
+
}.`,
|
| 95 |
+
timeout: 4000,
|
| 96 |
+
});
|
| 97 |
+
} else if (action === "toggle-trained-word") {
|
| 98 |
+
target?.classList.toggle("-rgthree-is-selected");
|
| 99 |
+
const tr = target.closest("tr");
|
| 100 |
+
if (tr) {
|
| 101 |
+
const span = queryOne("td:first-child > *", tr)!;
|
| 102 |
+
let small = queryOne("small", span);
|
| 103 |
+
if (!small) {
|
| 104 |
+
small = $el("small", {parent: span});
|
| 105 |
+
}
|
| 106 |
+
const num = query(".-rgthree-is-selected", tr).length;
|
| 107 |
+
small.innerHTML = num
|
| 108 |
+
? `${num} selected | <span role="button" data-action="copy-trained-words">Copy</span>`
|
| 109 |
+
: "";
|
| 110 |
+
// this.handleEventAction('copy-trained-words', target, e);
|
| 111 |
+
}
|
| 112 |
+
} else if (action === "edit-row") {
|
| 113 |
+
const tr = target!.closest("tr")!;
|
| 114 |
+
const td = queryOne("td:nth-child(2)", tr)!;
|
| 115 |
+
const input = td.querySelector("input,textarea");
|
| 116 |
+
if (!input) {
|
| 117 |
+
const fieldName = tr.dataset["fieldName"] as string;
|
| 118 |
+
tr.classList.add("-rgthree-editing");
|
| 119 |
+
const isTextarea = fieldName === "userNote";
|
| 120 |
+
const input = $el(`${isTextarea ? "textarea" : 'input[type="text"]'}`, {
|
| 121 |
+
value: td.textContent,
|
| 122 |
+
});
|
| 123 |
+
input.addEventListener("keydown", (e) => {
|
| 124 |
+
if (!isTextarea && e.key === "Enter") {
|
| 125 |
+
const modified = saveEditableRow(info!, tr, true);
|
| 126 |
+
this.modifiedModelData = this.modifiedModelData || modified;
|
| 127 |
+
e.stopPropagation();
|
| 128 |
+
e.preventDefault();
|
| 129 |
+
} else if (e.key === "Escape") {
|
| 130 |
+
const modified = saveEditableRow(info!, tr, false);
|
| 131 |
+
this.modifiedModelData = this.modifiedModelData || modified;
|
| 132 |
+
e.stopPropagation();
|
| 133 |
+
e.preventDefault();
|
| 134 |
+
}
|
| 135 |
+
});
|
| 136 |
+
appendChildren(empty(td), [input]);
|
| 137 |
+
input.focus();
|
| 138 |
+
} else if (target!.nodeName.toLowerCase() === "button") {
|
| 139 |
+
const modified = saveEditableRow(info!, tr, true);
|
| 140 |
+
this.modifiedModelData = this.modifiedModelData || modified;
|
| 141 |
+
}
|
| 142 |
+
e?.preventDefault();
|
| 143 |
+
e?.stopPropagation();
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
private getInfoContent() {
|
| 148 |
+
const info = this.modelInfo || {};
|
| 149 |
+
const civitaiLink = info.links?.find((i) => i.includes("civitai.com/models"));
|
| 150 |
+
const html = `
|
| 151 |
+
<ul class="rgthree-info-area">
|
| 152 |
+
<li title="Type" class="rgthree-info-tag -type -type-${(
|
| 153 |
+
info.type || ""
|
| 154 |
+
).toLowerCase()}"><span>${info.type || ""}</span></li>
|
| 155 |
+
<li title="Base Model" class="rgthree-info-tag -basemodel -basemodel-${(
|
| 156 |
+
info.baseModel || ""
|
| 157 |
+
).toLowerCase()}"><span>${info.baseModel || ""}</span></li>
|
| 158 |
+
<li class="rgthree-info-menu" stub="menu"></li>
|
| 159 |
+
${
|
| 160 |
+
""
|
| 161 |
+
// !civitaiLink
|
| 162 |
+
// ? ""
|
| 163 |
+
// : `
|
| 164 |
+
// <li title="Visit on Civitai" class="-link -civitai"><a href="${civitaiLink}" target="_blank">Civitai ${link}</a></li>
|
| 165 |
+
// `
|
| 166 |
+
}
|
| 167 |
+
</ul>
|
| 168 |
+
|
| 169 |
+
<table class="rgthree-info-table">
|
| 170 |
+
${infoTableRow("File", info.file || "")}
|
| 171 |
+
${infoTableRow("Hash (sha256)", info.sha256 || "")}
|
| 172 |
+
${
|
| 173 |
+
civitaiLink
|
| 174 |
+
? infoTableRow(
|
| 175 |
+
"Civitai",
|
| 176 |
+
`<a href="${civitaiLink}" target="_blank">${logoCivitai}View on Civitai</a>`,
|
| 177 |
+
)
|
| 178 |
+
: info.raw?.civitai?.error === "Model not found"
|
| 179 |
+
? infoTableRow(
|
| 180 |
+
"Civitai",
|
| 181 |
+
'<i>Model not found</i> <span class="-help" title="The model was not found on civitai with the sha256 hash. It\'s possible the model was removed, re-uploaded, or was never on civitai to begin with."></span>',
|
| 182 |
+
)
|
| 183 |
+
: info.raw?.civitai?.error
|
| 184 |
+
? infoTableRow("Civitai", info.raw?.civitai?.error)
|
| 185 |
+
: !info.raw?.civitai
|
| 186 |
+
? infoTableRow(
|
| 187 |
+
"Civitai",
|
| 188 |
+
`<button class="rgthree-button" data-action="fetch-civitai">Fetch info from civitai</button>`,
|
| 189 |
+
)
|
| 190 |
+
: ""
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
${infoTableRow(
|
| 194 |
+
"Name",
|
| 195 |
+
info.name || info.raw?.metadata?.ss_output_name || "",
|
| 196 |
+
"The name for display.",
|
| 197 |
+
"name",
|
| 198 |
+
)}
|
| 199 |
+
|
| 200 |
+
${
|
| 201 |
+
!info.baseModelFile && !info.baseModelFile
|
| 202 |
+
? ""
|
| 203 |
+
: infoTableRow(
|
| 204 |
+
"Base Model",
|
| 205 |
+
(info.baseModel || "") + (info.baseModelFile ? ` (${info.baseModelFile})` : ""),
|
| 206 |
+
)
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
${
|
| 211 |
+
!info.trainedWords?.length
|
| 212 |
+
? ""
|
| 213 |
+
: infoTableRow(
|
| 214 |
+
"Trained Words",
|
| 215 |
+
getTrainedWordsMarkup(info.trainedWords) ?? "",
|
| 216 |
+
"Trained words from the metadata and/or civitai. Click to select for copy.",
|
| 217 |
+
)
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
${
|
| 221 |
+
!info.raw?.metadata?.ss_clip_skip || info.raw?.metadata?.ss_clip_skip == "None"
|
| 222 |
+
? ""
|
| 223 |
+
: infoTableRow("Clip Skip", info.raw?.metadata?.ss_clip_skip)
|
| 224 |
+
}
|
| 225 |
+
${infoTableRow(
|
| 226 |
+
"Strength Min",
|
| 227 |
+
info.strengthMin ?? "",
|
| 228 |
+
"The recommended minimum strength, In the Power Lora Loader node, strength will signal when it is below this threshold.",
|
| 229 |
+
"strengthMin",
|
| 230 |
+
)}
|
| 231 |
+
${infoTableRow(
|
| 232 |
+
"Strength Max",
|
| 233 |
+
info.strengthMax ?? "",
|
| 234 |
+
"The recommended maximum strength. In the Power Lora Loader node, strength will signal when it is above this threshold.",
|
| 235 |
+
"strengthMax",
|
| 236 |
+
)}
|
| 237 |
+
${
|
| 238 |
+
"" /*infoTableRow(
|
| 239 |
+
"User Tags",
|
| 240 |
+
info.userTags?.join(", ") ?? "",
|
| 241 |
+
"A list of tags to make filtering easier in the Power Lora Chooser.",
|
| 242 |
+
"userTags",
|
| 243 |
+
)*/
|
| 244 |
+
}
|
| 245 |
+
${infoTableRow(
|
| 246 |
+
"Additional Notes",
|
| 247 |
+
info.userNote ?? "",
|
| 248 |
+
"Additional notes you'd like to keep and reference in the info dialog.",
|
| 249 |
+
"userNote",
|
| 250 |
+
)}
|
| 251 |
+
|
| 252 |
+
</table>
|
| 253 |
+
|
| 254 |
+
<ul class="rgthree-info-images">${
|
| 255 |
+
info.images
|
| 256 |
+
?.map(
|
| 257 |
+
(img) => `
|
| 258 |
+
<li>
|
| 259 |
+
<figure>
|
| 260 |
+
<img src="${img.url}" />
|
| 261 |
+
<figcaption><!--
|
| 262 |
+
-->${imgInfoField(
|
| 263 |
+
"",
|
| 264 |
+
img.civitaiUrl
|
| 265 |
+
? `<a href="${img.civitaiUrl}" target="_blank">civitai${link}</a>`
|
| 266 |
+
: undefined,
|
| 267 |
+
)}<!--
|
| 268 |
+
-->${imgInfoField("seed", img.seed)}<!--
|
| 269 |
+
-->${imgInfoField("steps", img.steps)}<!--
|
| 270 |
+
-->${imgInfoField("cfg", img.cfg)}<!--
|
| 271 |
+
-->${imgInfoField("sampler", img.sampler)}<!--
|
| 272 |
+
-->${imgInfoField("model", img.model)}<!--
|
| 273 |
+
-->${imgInfoField("positive", img.positive)}<!--
|
| 274 |
+
-->${imgInfoField("negative", img.negative)}<!--
|
| 275 |
+
--><!--${
|
| 276 |
+
""
|
| 277 |
+
// img.resources?.length
|
| 278 |
+
// ? `
|
| 279 |
+
// <tr><td>Resources</td><td><ul>
|
| 280 |
+
// ${(img.resources || [])
|
| 281 |
+
// .map(
|
| 282 |
+
// (r) => `
|
| 283 |
+
// <li>[${r.type || ""}] ${r.name || ""} ${
|
| 284 |
+
// r.weight != null ? `@ ${r.weight}` : ""
|
| 285 |
+
// }</li>
|
| 286 |
+
// `,
|
| 287 |
+
// )
|
| 288 |
+
// .join("")}
|
| 289 |
+
// </ul></td></tr>
|
| 290 |
+
// `
|
| 291 |
+
// : ""
|
| 292 |
+
}--></figcaption>
|
| 293 |
+
</figure>
|
| 294 |
+
</li>`,
|
| 295 |
+
)
|
| 296 |
+
.join("") ?? ""
|
| 297 |
+
}</ul>
|
| 298 |
+
`;
|
| 299 |
+
|
| 300 |
+
const div = $el("div", {html});
|
| 301 |
+
|
| 302 |
+
if (rgthree.isDevMode()) {
|
| 303 |
+
setAttributes(queryOne('[stub="menu"]', div)!, {
|
| 304 |
+
children: [
|
| 305 |
+
new MenuButton({
|
| 306 |
+
icon: dotdotdot,
|
| 307 |
+
options: [
|
| 308 |
+
{label: "More Actions", type: "title"},
|
| 309 |
+
{
|
| 310 |
+
label: "Open API JSON",
|
| 311 |
+
callback: async (e: PointerEvent) => {
|
| 312 |
+
if (this.modelInfo?.file) {
|
| 313 |
+
window.open(
|
| 314 |
+
`rgthree/api/loras/info?file=${encodeURIComponent(this.modelInfo.file)}`,
|
| 315 |
+
);
|
| 316 |
+
}
|
| 317 |
+
},
|
| 318 |
+
},
|
| 319 |
+
{
|
| 320 |
+
label: "Clear all local info",
|
| 321 |
+
callback: async (e: PointerEvent) => {
|
| 322 |
+
if (this.modelInfo?.file) {
|
| 323 |
+
this.modelInfo = await LORA_INFO_SERVICE.clearFetchedInfo(this.modelInfo.file);
|
| 324 |
+
this.setContent(this.getInfoContent());
|
| 325 |
+
this.setTitle(
|
| 326 |
+
this.modelInfo?.["name"] || this.modelInfo?.["file"] || "Unknown",
|
| 327 |
+
);
|
| 328 |
+
}
|
| 329 |
+
},
|
| 330 |
+
},
|
| 331 |
+
],
|
| 332 |
+
}),
|
| 333 |
+
],
|
| 334 |
+
});
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
return div;
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
export class RgthreeLoraInfoDialog extends RgthreeInfoDialog {
|
| 342 |
+
override async getModelInfo(file: string) {
|
| 343 |
+
return LORA_INFO_SERVICE.getInfo(file, false, false);
|
| 344 |
+
}
|
| 345 |
+
override async refreshModelInfo(file: string) {
|
| 346 |
+
return LORA_INFO_SERVICE.refreshInfo(file);
|
| 347 |
+
}
|
| 348 |
+
override async clearModelInfo(file: string) {
|
| 349 |
+
return LORA_INFO_SERVICE.clearFetchedInfo(file);
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
/**
|
| 354 |
+
* Generates a uniform markup string for a table row.
|
| 355 |
+
*/
|
| 356 |
+
function infoTableRow(
|
| 357 |
+
name: string,
|
| 358 |
+
value: string | number,
|
| 359 |
+
help: string = "",
|
| 360 |
+
editableFieldName = "",
|
| 361 |
+
) {
|
| 362 |
+
return `
|
| 363 |
+
<tr class="${editableFieldName ? "editable" : ""}" ${
|
| 364 |
+
editableFieldName ? `data-field-name="${editableFieldName}"` : ""
|
| 365 |
+
}>
|
| 366 |
+
<td><span>${name} ${help ? `<span class="-help" title="${help}"></span>` : ""}<span></td>
|
| 367 |
+
<td ${editableFieldName ? "" : 'colspan="2"'}>${
|
| 368 |
+
String(value).startsWith("<") ? value : `<span>${value}<span>`
|
| 369 |
+
}</td>
|
| 370 |
+
${
|
| 371 |
+
editableFieldName
|
| 372 |
+
? `<td style="width: 24px;"><button class="rgthree-button-reset rgthree-button-edit" data-action="edit-row">${pencilColored}${diskColored}</button></td>`
|
| 373 |
+
: ""
|
| 374 |
+
}
|
| 375 |
+
</tr>`;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
function getTrainedWordsMarkup(words: RgthreeModelInfo["trainedWords"]) {
|
| 379 |
+
let markup = `<ul class="rgthree-info-trained-words-list">`;
|
| 380 |
+
for (const wordData of words || []) {
|
| 381 |
+
markup += `<li title="${wordData.word}" data-word="${
|
| 382 |
+
wordData.word
|
| 383 |
+
}" class="rgthree-info-trained-words-list-item" data-action="toggle-trained-word">
|
| 384 |
+
<span>${wordData.word}</span>
|
| 385 |
+
${wordData.civitai ? logoCivitai : ""}
|
| 386 |
+
${wordData.count != null ? `<small>${wordData.count}</small>` : ""}
|
| 387 |
+
</li>`;
|
| 388 |
+
}
|
| 389 |
+
markup += `</ul>`;
|
| 390 |
+
return markup;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
/**
|
| 394 |
+
* Saves / cancels an editable row. Returns a boolean if the data was modified.
|
| 395 |
+
*/
|
| 396 |
+
function saveEditableRow(info: RgthreeModelInfo, tr: HTMLElement, saving = true): boolean {
|
| 397 |
+
const fieldName = tr.dataset["fieldName"] as "file";
|
| 398 |
+
const input = queryOne<HTMLInputElement>("input,textarea", tr)!;
|
| 399 |
+
let newValue = info[fieldName] ?? "";
|
| 400 |
+
let modified = false;
|
| 401 |
+
if (saving) {
|
| 402 |
+
newValue = input!.value;
|
| 403 |
+
if (fieldName.startsWith("strength")) {
|
| 404 |
+
if (Number.isNaN(Number(newValue))) {
|
| 405 |
+
alert(`You must enter a number into the ${fieldName} field.`);
|
| 406 |
+
return false;
|
| 407 |
+
}
|
| 408 |
+
newValue = (Math.round(Number(newValue) * 100) / 100).toFixed(2);
|
| 409 |
+
}
|
| 410 |
+
LORA_INFO_SERVICE.savePartialInfo(info.file!, {[fieldName]: newValue});
|
| 411 |
+
modified = true;
|
| 412 |
+
}
|
| 413 |
+
tr.classList.remove("-rgthree-editing");
|
| 414 |
+
const td = queryOne("td:nth-child(2)", tr)!;
|
| 415 |
+
appendChildren(empty(td), [$el("span", {text: newValue})]);
|
| 416 |
+
return modified;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
function imgInfoField(label: string, value?: string | number) {
|
| 420 |
+
return value != null ? `<span>${label ? `<label>${label} </label>` : ""}${value}</span>` : "";
|
| 421 |
+
}
|
rgthree-comfy/src_web/comfyui/display_any.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { app } from "scripts/app.js";
|
| 2 |
+
import { ComfyWidgets } from "scripts/widgets.js";
|
| 3 |
+
import type { LGraphNode as TLGraphNode } from "typings/litegraph.js";
|
| 4 |
+
import type { ComfyApp, ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
|
| 5 |
+
import { addConnectionLayoutSupport } from "./utils.js";
|
| 6 |
+
import { rgthree } from "./rgthree.js";
|
| 7 |
+
|
| 8 |
+
let hasShownAlertForUpdatingInt = false;
|
| 9 |
+
|
| 10 |
+
app.registerExtension({
|
| 11 |
+
name: "rgthree.DisplayAny",
|
| 12 |
+
async beforeRegisterNodeDef(
|
| 13 |
+
nodeType: ComfyNodeConstructor,
|
| 14 |
+
nodeData: ComfyObjectInfo,
|
| 15 |
+
app: ComfyApp,
|
| 16 |
+
) {
|
| 17 |
+
if (nodeData.name === "Display Any (rgthree)" || nodeData.name === "Display Int (rgthree)") {
|
| 18 |
+
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
| 19 |
+
nodeType.prototype.onNodeCreated = function () {
|
| 20 |
+
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
|
| 21 |
+
|
| 22 |
+
(this as any).showValueWidget = ComfyWidgets["STRING"](
|
| 23 |
+
this,
|
| 24 |
+
"output",
|
| 25 |
+
["STRING", { multiline: true }],
|
| 26 |
+
app,
|
| 27 |
+
).widget;
|
| 28 |
+
(this as any).showValueWidget.inputEl!.readOnly = true;
|
| 29 |
+
(this as any).showValueWidget.serializeValue = async (node: TLGraphNode, index: number) => {
|
| 30 |
+
const n =
|
| 31 |
+
rgthree.getNodeFromInitialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff(node);
|
| 32 |
+
if (n) {
|
| 33 |
+
// Since we need a round trip to get the value, the serizalized value means nothing, and
|
| 34 |
+
// saving it to the metadata would just be confusing. So, we clear it here.
|
| 35 |
+
n.widgets_values![index] = "";
|
| 36 |
+
} else {
|
| 37 |
+
console.warn(
|
| 38 |
+
"No serialized node found in workflow. May be attributed to " +
|
| 39 |
+
"https://github.com/comfyanonymous/ComfyUI/issues/2193",
|
| 40 |
+
);
|
| 41 |
+
}
|
| 42 |
+
return "";
|
| 43 |
+
};
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
addConnectionLayoutSupport(nodeType, app, [["Left"], ["Right"]]);
|
| 47 |
+
|
| 48 |
+
const onExecuted = nodeType.prototype.onExecuted;
|
| 49 |
+
nodeType.prototype.onExecuted = function (message: any) {
|
| 50 |
+
onExecuted?.apply(this, [message]);
|
| 51 |
+
(this as any).showValueWidget.value = message.text[0];
|
| 52 |
+
};
|
| 53 |
+
}
|
| 54 |
+
},
|
| 55 |
+
|
| 56 |
+
// This ports Display Int to DisplayAny, but ComfyUI still shows an error.
|
| 57 |
+
// If https://github.com/comfyanonymous/ComfyUI/issues/1527 is fixed, this could work.
|
| 58 |
+
// async loadedGraphNode(node: TLGraphNode) {
|
| 59 |
+
// if (node.type === "Display Int (rgthree)") {
|
| 60 |
+
// replaceNode(node, "Display Any (rgthree)", new Map([["input", "source"]]));
|
| 61 |
+
// if (!hasShownAlertForUpdatingInt) {
|
| 62 |
+
// hasShownAlertForUpdatingInt = true;
|
| 63 |
+
// setTimeout(() => {
|
| 64 |
+
// alert(
|
| 65 |
+
// "Don't worry, your 'Display Int' nodes have been updated to the new " +
|
| 66 |
+
// "'Display Any' nodes! You can ignore the error message underneath (for that node)." +
|
| 67 |
+
// "\n\nThanks.\n- rgthree",
|
| 68 |
+
// );
|
| 69 |
+
// }, 128);
|
| 70 |
+
// }
|
| 71 |
+
// }
|
| 72 |
+
// },
|
| 73 |
+
});
|
rgthree-comfy/src_web/comfyui/dynamic_context.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {app} from "scripts/app.js";
|
| 2 |
+
import {
|
| 3 |
+
IoDirection,
|
| 4 |
+
followConnectionUntilType,
|
| 5 |
+
getConnectedInputInfosAndFilterPassThroughs,
|
| 6 |
+
} from "./utils.js";
|
| 7 |
+
import {rgthree} from "./rgthree.js";
|
| 8 |
+
import {
|
| 9 |
+
SERVICE as CONTEXT_SERVICE,
|
| 10 |
+
InputMutation,
|
| 11 |
+
InputMutationOperation,
|
| 12 |
+
} from "./services/context_service.js";
|
| 13 |
+
import {NodeTypesString} from "./constants.js";
|
| 14 |
+
import {removeUnusedInputsFromEnd} from "./utils_inputs_outputs.js";
|
| 15 |
+
import {INodeInputSlot, INodeOutputSlot, INodeSlot, LGraphNode, LLink} from "typings/litegraph.js";
|
| 16 |
+
import {ComfyNodeConstructor, ComfyObjectInfo} from "typings/comfy.js";
|
| 17 |
+
import {DynamicContextNodeBase} from "./dynamic_context_base.js";
|
| 18 |
+
import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js";
|
| 19 |
+
|
| 20 |
+
const OWNED_PREFIX = "+";
|
| 21 |
+
const REGEX_OWNED_PREFIX = /^\+\s*/;
|
| 22 |
+
const REGEX_EMPTY_INPUT = /^\+\s*$/;
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* The Dynamic Context node.
|
| 26 |
+
*/
|
| 27 |
+
export class DynamicContextNode extends DynamicContextNodeBase {
|
| 28 |
+
static override title = NodeTypesString.DYNAMIC_CONTEXT;
|
| 29 |
+
static override type = NodeTypesString.DYNAMIC_CONTEXT;
|
| 30 |
+
static comfyClass = NodeTypesString.DYNAMIC_CONTEXT;
|
| 31 |
+
|
| 32 |
+
constructor(title = DynamicContextNode.title) {
|
| 33 |
+
super(title);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
override onNodeCreated() {
|
| 37 |
+
this.addInput("base_ctx", "RGTHREE_DYNAMIC_CONTEXT");
|
| 38 |
+
this.ensureOneRemainingNewInputSlot();
|
| 39 |
+
super.onNodeCreated();
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
override onConnectionsChange(
|
| 43 |
+
type: number,
|
| 44 |
+
slotIndex: number,
|
| 45 |
+
isConnected: boolean,
|
| 46 |
+
link: LLink,
|
| 47 |
+
ioSlot: INodeSlot,
|
| 48 |
+
): void {
|
| 49 |
+
super.onConnectionsChange?.call(this, type, slotIndex, isConnected, link, ioSlot);
|
| 50 |
+
if (this.configuring) {
|
| 51 |
+
return;
|
| 52 |
+
}
|
| 53 |
+
if (type === LiteGraph.INPUT) {
|
| 54 |
+
if (isConnected) {
|
| 55 |
+
this.handleInputConnected(slotIndex);
|
| 56 |
+
} else {
|
| 57 |
+
this.handleInputDisconnected(slotIndex);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
override onConnectInput(
|
| 63 |
+
inputIndex: number,
|
| 64 |
+
outputType: INodeOutputSlot["type"],
|
| 65 |
+
outputSlot: INodeOutputSlot,
|
| 66 |
+
outputNode: LGraphNode,
|
| 67 |
+
outputIndex: number,
|
| 68 |
+
): boolean {
|
| 69 |
+
let canConnect = true;
|
| 70 |
+
if (super.onConnectInput) {
|
| 71 |
+
canConnect = super.onConnectInput.apply(this, [...arguments] as any);
|
| 72 |
+
}
|
| 73 |
+
if (
|
| 74 |
+
canConnect &&
|
| 75 |
+
outputNode instanceof DynamicContextNode &&
|
| 76 |
+
outputIndex === 0 &&
|
| 77 |
+
inputIndex !== 0
|
| 78 |
+
) {
|
| 79 |
+
const [n, v] = rgthree.logger.warnParts(
|
| 80 |
+
"Currently, you can only connect a context node in the first slot.",
|
| 81 |
+
);
|
| 82 |
+
console[n]?.call(console, ...v);
|
| 83 |
+
canConnect = false;
|
| 84 |
+
}
|
| 85 |
+
return canConnect;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
handleInputConnected(slotIndex: number) {
|
| 89 |
+
const ioSlot = this.inputs[slotIndex];
|
| 90 |
+
const connectedIndexes = [];
|
| 91 |
+
if (slotIndex === 0) {
|
| 92 |
+
let baseNodeInfos = getConnectedInputInfosAndFilterPassThroughs(this, this, 0);
|
| 93 |
+
const baseNodes = baseNodeInfos.map((n) => n.node)!;
|
| 94 |
+
const baseNodesDynamicCtx = baseNodes[0] as DynamicContextNodeBase;
|
| 95 |
+
if (baseNodesDynamicCtx?.provideInputsData) {
|
| 96 |
+
const inputsData = CONTEXT_SERVICE.getDynamicContextInputsData(baseNodesDynamicCtx);
|
| 97 |
+
console.log("inputsData", inputsData);
|
| 98 |
+
for (const input of baseNodesDynamicCtx.provideInputsData()) {
|
| 99 |
+
if (input.name === "base_ctx" || input.name === "+") {
|
| 100 |
+
continue;
|
| 101 |
+
}
|
| 102 |
+
this.addContextInput(input.name, input.type, input.index);
|
| 103 |
+
this.stabilizeNames();
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
} else if (this.isInputSlotForNewInput(slotIndex)) {
|
| 107 |
+
this.handleNewInputConnected(slotIndex);
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
isInputSlotForNewInput(slotIndex: number) {
|
| 112 |
+
const ioSlot = this.inputs[slotIndex];
|
| 113 |
+
return ioSlot && ioSlot.name === "+" && ioSlot.type === "*";
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
handleNewInputConnected(slotIndex: number) {
|
| 117 |
+
if (!this.isInputSlotForNewInput(slotIndex)) {
|
| 118 |
+
throw new Error('Expected the incoming slot index to be the "new input" input.');
|
| 119 |
+
}
|
| 120 |
+
const ioSlot = this.inputs[slotIndex]!;
|
| 121 |
+
let cxn = null;
|
| 122 |
+
if (ioSlot.link != null) {
|
| 123 |
+
cxn = followConnectionUntilType(this, IoDirection.INPUT, slotIndex, true);
|
| 124 |
+
}
|
| 125 |
+
if (cxn?.type && cxn?.name) {
|
| 126 |
+
let name = this.addOwnedPrefix(this.getNextUniqueNameForThisNode(cxn.name));
|
| 127 |
+
if (name.match(/^\+\s*[A-Z_]+(\.\d+)?$/)) {
|
| 128 |
+
name = name.toLowerCase();
|
| 129 |
+
}
|
| 130 |
+
ioSlot.name = name;
|
| 131 |
+
ioSlot.type = cxn.type as string;
|
| 132 |
+
ioSlot.removable = true;
|
| 133 |
+
while (!this.outputs[slotIndex]) {
|
| 134 |
+
this.addOutput("*", "*");
|
| 135 |
+
}
|
| 136 |
+
this.outputs[slotIndex]!.type = cxn.type as string;
|
| 137 |
+
this.outputs[slotIndex]!.name = this.stripOwnedPrefix(name).toLocaleUpperCase();
|
| 138 |
+
// This is a dumb override for ComfyUI's widgetinputs issues.
|
| 139 |
+
if (cxn.type === "COMBO" || cxn.type.includes(",") || Array.isArray(cxn.type)) {
|
| 140 |
+
(this.outputs[slotIndex] as any).widget = true;
|
| 141 |
+
}
|
| 142 |
+
this.inputsMutated({
|
| 143 |
+
operation: InputMutationOperation.ADDED,
|
| 144 |
+
node: this,
|
| 145 |
+
slotIndex,
|
| 146 |
+
slot: ioSlot,
|
| 147 |
+
});
|
| 148 |
+
this.stabilizeNames();
|
| 149 |
+
this.ensureOneRemainingNewInputSlot();
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
handleInputDisconnected(slotIndex: number) {
|
| 154 |
+
const inputs = this.getContextInputsList();
|
| 155 |
+
if (slotIndex === 0) {
|
| 156 |
+
for (let index = inputs.length - 1; index > 0; index--) {
|
| 157 |
+
if (index === 0 || index === inputs.length - 1) {
|
| 158 |
+
continue;
|
| 159 |
+
}
|
| 160 |
+
const input = inputs[index]!;
|
| 161 |
+
if (!this.isOwnedInput(input.name)) {
|
| 162 |
+
if (input.link || this.outputs[index]?.links?.length) {
|
| 163 |
+
this.renameContextInput(index, input.name, true);
|
| 164 |
+
} else {
|
| 165 |
+
this.removeContextInput(index);
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
this.setSize(this.computeSize());
|
| 170 |
+
this.setDirtyCanvas(true, true);
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
ensureOneRemainingNewInputSlot() {
|
| 175 |
+
removeUnusedInputsFromEnd(this, 1, REGEX_EMPTY_INPUT);
|
| 176 |
+
this.addInput(OWNED_PREFIX, "*");
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
getNextUniqueNameForThisNode(desiredName: string) {
|
| 180 |
+
const inputs = this.getContextInputsList();
|
| 181 |
+
const allExistingKeys = inputs.map((i) => this.stripOwnedPrefix(i.name).toLocaleUpperCase());
|
| 182 |
+
desiredName = this.stripOwnedPrefix(desiredName);
|
| 183 |
+
let newName = desiredName;
|
| 184 |
+
let n = 0;
|
| 185 |
+
while (allExistingKeys.includes(newName.toLocaleUpperCase())) {
|
| 186 |
+
newName = `${desiredName}.${++n}`;
|
| 187 |
+
}
|
| 188 |
+
return newName;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
override removeInput(slotIndex: number) {
|
| 192 |
+
const slot = this.inputs[slotIndex]!;
|
| 193 |
+
super.removeInput(slotIndex);
|
| 194 |
+
if (this.outputs[slotIndex]) {
|
| 195 |
+
this.removeOutput(slotIndex);
|
| 196 |
+
}
|
| 197 |
+
this.inputsMutated({operation: InputMutationOperation.REMOVED, node: this, slotIndex, slot});
|
| 198 |
+
this.stabilizeNames();
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
stabilizeNames() {
|
| 202 |
+
const inputs = this.getContextInputsList();
|
| 203 |
+
const names: string[] = [];
|
| 204 |
+
for (const [index, input] of inputs.entries()) {
|
| 205 |
+
if (index === 0 || index === inputs.length - 1) {
|
| 206 |
+
continue;
|
| 207 |
+
}
|
| 208 |
+
input.label = undefined;
|
| 209 |
+
this.outputs[index]!.label = undefined;
|
| 210 |
+
let origName = this.stripOwnedPrefix(input.name).replace(/\.\d+$/, "");
|
| 211 |
+
let name = input.name;
|
| 212 |
+
if (!this.isOwnedInput(name)) {
|
| 213 |
+
names.push(name.toLocaleUpperCase());
|
| 214 |
+
} else {
|
| 215 |
+
let n = 0;
|
| 216 |
+
name = this.addOwnedPrefix(origName);
|
| 217 |
+
while (names.includes(this.stripOwnedPrefix(name).toLocaleUpperCase())) {
|
| 218 |
+
name = `${this.addOwnedPrefix(origName)}.${++n}`;
|
| 219 |
+
}
|
| 220 |
+
names.push(this.stripOwnedPrefix(name).toLocaleUpperCase());
|
| 221 |
+
if (input.name !== name) {
|
| 222 |
+
this.renameContextInput(index, name);
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
override getSlotMenuOptions(slot: {
|
| 229 |
+
slot: number;
|
| 230 |
+
input?: INodeInputSlot | undefined;
|
| 231 |
+
output?: INodeOutputSlot | undefined;
|
| 232 |
+
}) {
|
| 233 |
+
const editable = this.isOwnedInput(slot.input!.name) && this.type !== "*";
|
| 234 |
+
return [
|
| 235 |
+
{
|
| 236 |
+
content: "✏️ Rename Input",
|
| 237 |
+
disabled: !editable,
|
| 238 |
+
callback: () => {
|
| 239 |
+
var dialog = app.canvas.createDialog(
|
| 240 |
+
"<span class='name'>Name</span><input autofocus type='text'/><button>OK</button>",
|
| 241 |
+
{},
|
| 242 |
+
);
|
| 243 |
+
var dialogInput = dialog.querySelector("input")!;
|
| 244 |
+
if (dialogInput) {
|
| 245 |
+
dialogInput.value = this.stripOwnedPrefix(slot.input!.name || "");
|
| 246 |
+
}
|
| 247 |
+
var inner = () => {
|
| 248 |
+
this.handleContextMenuRenameInputDialog(slot.slot, dialogInput.value);
|
| 249 |
+
dialog.close();
|
| 250 |
+
};
|
| 251 |
+
dialog.querySelector("button")!.addEventListener("click", inner);
|
| 252 |
+
dialogInput.addEventListener("keydown", (e) => {
|
| 253 |
+
dialog.is_modified = true;
|
| 254 |
+
if (e.keyCode == 27) {
|
| 255 |
+
dialog.close();
|
| 256 |
+
} else if (e.keyCode == 13) {
|
| 257 |
+
inner();
|
| 258 |
+
} else if (e.keyCode != 13 && (e.target as HTMLElement)?.localName != "textarea") {
|
| 259 |
+
return;
|
| 260 |
+
}
|
| 261 |
+
e.preventDefault();
|
| 262 |
+
e.stopPropagation();
|
| 263 |
+
});
|
| 264 |
+
dialogInput.focus();
|
| 265 |
+
},
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
content: "🗑️ Delete Input",
|
| 269 |
+
disabled: !editable,
|
| 270 |
+
callback: () => {
|
| 271 |
+
this.removeInput(slot.slot);
|
| 272 |
+
},
|
| 273 |
+
},
|
| 274 |
+
];
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
handleContextMenuRenameInputDialog(slotIndex: number, value: string) {
|
| 278 |
+
app.graph.beforeChange();
|
| 279 |
+
this.renameContextInput(slotIndex, value);
|
| 280 |
+
this.stabilizeNames();
|
| 281 |
+
this.setDirtyCanvas(true, true);
|
| 282 |
+
app.graph.afterChange();
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
const contextDynamicNodes = [DynamicContextNode];
|
| 287 |
+
app.registerExtension({
|
| 288 |
+
name: "rgthree.DynamicContext",
|
| 289 |
+
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 290 |
+
if (!CONFIG_SERVICE.getConfigValue("unreleased.dynamic_context.enabled")) {
|
| 291 |
+
return;
|
| 292 |
+
}
|
| 293 |
+
if (nodeData.name === DynamicContextNode.type) {
|
| 294 |
+
DynamicContextNode.setUp(nodeType, nodeData);
|
| 295 |
+
}
|
| 296 |
+
},
|
| 297 |
+
});
|
rgthree-comfy/src_web/comfyui/dynamic_context_base.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {INodeInputSlot} from "typings/litegraph.js";
|
| 2 |
+
|
| 3 |
+
import {BaseContextNode} from "./context.js";
|
| 4 |
+
import {ComfyNodeConstructor, ComfyObjectInfo} from "typings/comfy.js";
|
| 5 |
+
import {RgthreeBaseServerNode} from "./base_node.js";
|
| 6 |
+
import {moveArrayItem, wait} from "rgthree/common/shared_utils.js";
|
| 7 |
+
import {RgthreeInvisibleWidget} from "./utils_widgets.js";
|
| 8 |
+
import {
|
| 9 |
+
getContextOutputName,
|
| 10 |
+
InputMutation,
|
| 11 |
+
InputMutationOperation,
|
| 12 |
+
} from "./services/context_service.js";
|
| 13 |
+
import {app} from "scripts/app.js";
|
| 14 |
+
import {SERVICE as CONTEXT_SERVICE} from "./services/context_service.js";
|
| 15 |
+
|
| 16 |
+
const OWNED_PREFIX = "+";
|
| 17 |
+
const REGEX_OWNED_PREFIX = /^\+\s*/;
|
| 18 |
+
const REGEX_EMPTY_INPUT = /^\+\s*$/;
|
| 19 |
+
|
| 20 |
+
export type InputLike = {
|
| 21 |
+
name: string;
|
| 22 |
+
type: string | -1;
|
| 23 |
+
label?: string;
|
| 24 |
+
link: number | null;
|
| 25 |
+
removable?: boolean;
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* The base context node that contains some shared between DynamicContext nodes. Not labels
|
| 30 |
+
* `abstract` so we can reference `this` in static methods.
|
| 31 |
+
*/
|
| 32 |
+
export class DynamicContextNodeBase extends BaseContextNode {
|
| 33 |
+
protected readonly hasShadowInputs: boolean = false;
|
| 34 |
+
|
| 35 |
+
getContextInputsList(): InputLike[] {
|
| 36 |
+
return this.inputs;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
provideInputsData() {
|
| 40 |
+
const inputs = this.getContextInputsList();
|
| 41 |
+
return inputs
|
| 42 |
+
.map((input, index) => ({
|
| 43 |
+
name: this.stripOwnedPrefix(input.name),
|
| 44 |
+
type: String(input.type),
|
| 45 |
+
index,
|
| 46 |
+
}))
|
| 47 |
+
.filter((i) => i.type !== "*");
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
addOwnedPrefix(name: string) {
|
| 51 |
+
return `+ ${this.stripOwnedPrefix(name)}`;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
isOwnedInput(inputOrName: string | null | INodeInputSlot) {
|
| 55 |
+
const name = typeof inputOrName == "string" ? inputOrName : inputOrName?.name || "";
|
| 56 |
+
return REGEX_OWNED_PREFIX.test(name);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
stripOwnedPrefix(name: string) {
|
| 60 |
+
return name.replace(REGEX_OWNED_PREFIX, "");
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// handleUpstreamMutation(mutation: InputMutation) {
|
| 64 |
+
// throw new Error('handleUpstreamMutation not overridden!')
|
| 65 |
+
// }
|
| 66 |
+
|
| 67 |
+
handleUpstreamMutation(mutation: InputMutation) {
|
| 68 |
+
console.log(`[node ${this.id}] handleUpstreamMutation`, mutation);
|
| 69 |
+
if (mutation.operation === InputMutationOperation.ADDED) {
|
| 70 |
+
const slot = mutation.slot;
|
| 71 |
+
if (!slot) {
|
| 72 |
+
throw new Error("Cannot have an ADDED mutation without a provided slot data.");
|
| 73 |
+
}
|
| 74 |
+
this.addContextInput(
|
| 75 |
+
this.stripOwnedPrefix(slot.name),
|
| 76 |
+
slot.type as string,
|
| 77 |
+
mutation.slotIndex,
|
| 78 |
+
);
|
| 79 |
+
return;
|
| 80 |
+
}
|
| 81 |
+
if (mutation.operation === InputMutationOperation.REMOVED) {
|
| 82 |
+
const slot = mutation.slot;
|
| 83 |
+
if (!slot) {
|
| 84 |
+
throw new Error("Cannot have an REMOVED mutation without a provided slot data.");
|
| 85 |
+
}
|
| 86 |
+
this.removeContextInput(mutation.slotIndex);
|
| 87 |
+
return;
|
| 88 |
+
}
|
| 89 |
+
if (mutation.operation === InputMutationOperation.RENAMED) {
|
| 90 |
+
const slot = mutation.slot;
|
| 91 |
+
if (!slot) {
|
| 92 |
+
throw new Error("Cannot have an RENAMED mutation without a provided slot data.");
|
| 93 |
+
}
|
| 94 |
+
this.renameContextInput(mutation.slotIndex, slot.name);
|
| 95 |
+
return;
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
override clone() {
|
| 99 |
+
const cloned = super.clone();
|
| 100 |
+
while (cloned.inputs.length > 1) {
|
| 101 |
+
cloned.removeInput(cloned.inputs.length - 1);
|
| 102 |
+
}
|
| 103 |
+
while (cloned.widgets.length > 1) {
|
| 104 |
+
cloned.removeWidget(cloned.widgets.length - 1);
|
| 105 |
+
}
|
| 106 |
+
while (cloned.outputs.length > 1) {
|
| 107 |
+
cloned.removeOutput(cloned.outputs.length - 1);
|
| 108 |
+
}
|
| 109 |
+
return cloned;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/**
|
| 113 |
+
* Adds the basic output_keys widget. Should be called _after_ specific nodes setup their inputs
|
| 114 |
+
* or widgets.
|
| 115 |
+
*/
|
| 116 |
+
override onNodeCreated() {
|
| 117 |
+
const node = this;
|
| 118 |
+
this.addCustomWidget(
|
| 119 |
+
new RgthreeInvisibleWidget("output_keys", "RGTHREE_DYNAMIC_CONTEXT_OUTPUTS", "", () => {
|
| 120 |
+
return (node.outputs || [])
|
| 121 |
+
.map((o, i) => i > 0 && o.name)
|
| 122 |
+
.filter((n) => n !== false)
|
| 123 |
+
.join(",");
|
| 124 |
+
}),
|
| 125 |
+
);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
addContextInput(name: string, type: string, slot = -1) {
|
| 129 |
+
const inputs = this.getContextInputsList();
|
| 130 |
+
if (this.hasShadowInputs) {
|
| 131 |
+
inputs.push({name, type, link: null});
|
| 132 |
+
} else {
|
| 133 |
+
this.addInput(name, type);
|
| 134 |
+
}
|
| 135 |
+
if (slot > -1) {
|
| 136 |
+
moveArrayItem(inputs, inputs.length - 1, slot);
|
| 137 |
+
} else {
|
| 138 |
+
slot = inputs.length - 1;
|
| 139 |
+
}
|
| 140 |
+
if (type !== "*") {
|
| 141 |
+
const output = this.addOutput(getContextOutputName(name), type);
|
| 142 |
+
if (type === "COMBO" || String(type).includes(",") || Array.isArray(type)) {
|
| 143 |
+
(output as any).widget = true;
|
| 144 |
+
}
|
| 145 |
+
if (slot > -1) {
|
| 146 |
+
moveArrayItem(this.outputs, this.outputs.length - 1, slot);
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
this.fixInputsOutputsLinkSlots();
|
| 150 |
+
this.inputsMutated({
|
| 151 |
+
operation: InputMutationOperation.ADDED,
|
| 152 |
+
node: this,
|
| 153 |
+
slotIndex: slot,
|
| 154 |
+
slot: inputs[slot]!,
|
| 155 |
+
});
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
removeContextInput(slotIndex: number) {
|
| 159 |
+
if (this.hasShadowInputs) {
|
| 160 |
+
const inputs = this.getContextInputsList();
|
| 161 |
+
const input = inputs.splice(slotIndex, 1)[0];
|
| 162 |
+
if (this.outputs[slotIndex]) {
|
| 163 |
+
this.removeOutput(slotIndex);
|
| 164 |
+
}
|
| 165 |
+
} else {
|
| 166 |
+
this.removeInput(slotIndex);
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
renameContextInput(index: number, newName: string, forceOwnBool: boolean | null = null) {
|
| 171 |
+
const inputs = this.getContextInputsList();
|
| 172 |
+
const input = inputs[index]!;
|
| 173 |
+
const oldName = input.name;
|
| 174 |
+
newName = this.stripOwnedPrefix(newName.trim() || this.getSlotDefaultInputLabel(index));
|
| 175 |
+
if (forceOwnBool === true || (this.isOwnedInput(oldName) && forceOwnBool !== false)) {
|
| 176 |
+
newName = this.addOwnedPrefix(newName);
|
| 177 |
+
}
|
| 178 |
+
if (oldName !== newName) {
|
| 179 |
+
input.name = newName;
|
| 180 |
+
input.removable = this.isOwnedInput(newName);
|
| 181 |
+
this.outputs[index]!.name = getContextOutputName(inputs[index]!.name);
|
| 182 |
+
this.inputsMutated({
|
| 183 |
+
node: this,
|
| 184 |
+
operation: InputMutationOperation.RENAMED,
|
| 185 |
+
slotIndex: index,
|
| 186 |
+
slot: input,
|
| 187 |
+
});
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
getSlotDefaultInputLabel(slotIndex: number) {
|
| 192 |
+
const inputs = this.getContextInputsList();
|
| 193 |
+
const input = inputs[slotIndex]!;
|
| 194 |
+
let defaultLabel = this.stripOwnedPrefix(input.name).toLowerCase();
|
| 195 |
+
return defaultLabel.toLocaleLowerCase();
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
inputsMutated(mutation: InputMutation) {
|
| 199 |
+
CONTEXT_SERVICE.onInputChanges(this, mutation);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
fixInputsOutputsLinkSlots() {
|
| 203 |
+
if (!this.hasShadowInputs) {
|
| 204 |
+
const inputs = this.getContextInputsList();
|
| 205 |
+
for (let index = inputs.length - 1; index > 0; index--) {
|
| 206 |
+
const input = inputs[index]!;
|
| 207 |
+
if ((input === null || input === void 0 ? void 0 : input.link) != null) {
|
| 208 |
+
app.graph.links[input.link!]!.target_slot = index;
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
const outputs = this.outputs;
|
| 213 |
+
for (let index = outputs.length - 1; index > 0; index--) {
|
| 214 |
+
const output = outputs[index];
|
| 215 |
+
if (output) {
|
| 216 |
+
output.nameLocked = true;
|
| 217 |
+
for (const link of output.links || []) {
|
| 218 |
+
app.graph.links[link!]!.origin_slot = index;
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 225 |
+
RgthreeBaseServerNode.registerForOverride(comfyClass, nodeData, this);
|
| 226 |
+
// [🤮] ComfyUI only adds "required" inputs to the outputs list when dragging an output to
|
| 227 |
+
// empty space, but since RGTHREE_CONTEXT is optional, it doesn't get added to the menu because
|
| 228 |
+
// ...of course. So, we'll manually add it. Of course, we also have to do this in a timeout
|
| 229 |
+
// because ComfyUI clears out `LiteGraph.slot_types_default_out` in its own 'Comfy.SlotDefaults'
|
| 230 |
+
// extension and we need to wait for that to happen.
|
| 231 |
+
wait(500).then(() => {
|
| 232 |
+
LiteGraph.slot_types_default_out["RGTHREE_DYNAMIC_CONTEXT"] =
|
| 233 |
+
LiteGraph.slot_types_default_out["RGTHREE_DYNAMIC_CONTEXT"] || [];
|
| 234 |
+
LiteGraph.slot_types_default_out["RGTHREE_DYNAMIC_CONTEXT"].push(comfyClass.comfyClass);
|
| 235 |
+
});
|
| 236 |
+
}
|
| 237 |
+
}
|
rgthree-comfy/src_web/comfyui/dynamic_context_switch.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {ComfyNodeConstructor, ComfyObjectInfo} from "typings/comfy.js";
|
| 2 |
+
import type {INodeSlot, LGraphNode, LLink, LGraphCanvas} from "typings/litegraph.js";
|
| 3 |
+
|
| 4 |
+
import {app} from "scripts/app.js";
|
| 5 |
+
import {DynamicContextNodeBase, InputLike} from "./dynamic_context_base.js";
|
| 6 |
+
import {NodeTypesString} from "./constants.js";
|
| 7 |
+
import {
|
| 8 |
+
InputMutation,
|
| 9 |
+
SERVICE as CONTEXT_SERVICE,
|
| 10 |
+
stripContextInputPrefixes,
|
| 11 |
+
getContextOutputName,
|
| 12 |
+
} from "./services/context_service.js";
|
| 13 |
+
import {getConnectedInputNodesAndFilterPassThroughs} from "./utils.js";
|
| 14 |
+
import {debounce, moveArrayItem} from "rgthree/common/shared_utils.js";
|
| 15 |
+
import {measureText} from "./utils_canvas.js";
|
| 16 |
+
import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js";
|
| 17 |
+
|
| 18 |
+
type ShadowInputData = {
|
| 19 |
+
node: LGraphNode;
|
| 20 |
+
slot: number;
|
| 21 |
+
shadowIndex: number;
|
| 22 |
+
shadowIndexIfShownSingularly: number;
|
| 23 |
+
shadowIndexFull: number;
|
| 24 |
+
nodeIndex: number;
|
| 25 |
+
type: string | -1;
|
| 26 |
+
name: string;
|
| 27 |
+
key: string;
|
| 28 |
+
// isDuplicatedBefore: boolean,
|
| 29 |
+
duplicatesBefore: number[];
|
| 30 |
+
duplicatesAfter: number[];
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* The Context Switch node.
|
| 35 |
+
*/
|
| 36 |
+
class DynamicContextSwitchNode extends DynamicContextNodeBase {
|
| 37 |
+
static override title = NodeTypesString.DYNAMIC_CONTEXT_SWITCH;
|
| 38 |
+
static override type = NodeTypesString.DYNAMIC_CONTEXT_SWITCH;
|
| 39 |
+
static comfyClass = NodeTypesString.DYNAMIC_CONTEXT_SWITCH;
|
| 40 |
+
|
| 41 |
+
protected override readonly hasShadowInputs = true;
|
| 42 |
+
|
| 43 |
+
// override hasShadowInputs = true;
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* We should be able to assume that `lastInputsList` is the input list after the last, major
|
| 47 |
+
* synchronous change. Which should mean, if we're handling a change that is currently live, but
|
| 48 |
+
* not represented in our node (like, an upstream node has already removed an input), then we
|
| 49 |
+
* should be able to compar the current InputList to this `lastInputsList`.
|
| 50 |
+
*/
|
| 51 |
+
lastInputsList: ShadowInputData[] = [];
|
| 52 |
+
|
| 53 |
+
private shadowInputs: (InputLike & {count: number})[] = [
|
| 54 |
+
{name: "base_ctx", type: "RGTHREE_DYNAMIC_CONTEXT", link: null, count: 0},
|
| 55 |
+
];
|
| 56 |
+
|
| 57 |
+
constructor(title = DynamicContextSwitchNode.title) {
|
| 58 |
+
super(title);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
override getContextInputsList() {
|
| 62 |
+
return this.shadowInputs;
|
| 63 |
+
}
|
| 64 |
+
override handleUpstreamMutation(mutation: InputMutation) {
|
| 65 |
+
this.scheduleHardRefresh();
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
override onConnectionsChange(
|
| 69 |
+
type: number,
|
| 70 |
+
slotIndex: number,
|
| 71 |
+
isConnected: boolean,
|
| 72 |
+
link: LLink,
|
| 73 |
+
ioSlot: INodeSlot,
|
| 74 |
+
): void {
|
| 75 |
+
super.onConnectionsChange?.call(this, type, slotIndex, isConnected, link, ioSlot);
|
| 76 |
+
if (this.configuring) {
|
| 77 |
+
return;
|
| 78 |
+
}
|
| 79 |
+
if (type === LiteGraph.INPUT) {
|
| 80 |
+
this.scheduleHardRefresh();
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
scheduleHardRefresh(ms = 64) {
|
| 85 |
+
return debounce(() => {
|
| 86 |
+
this.refreshInputsAndOutputs();
|
| 87 |
+
}, ms);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
override onNodeCreated() {
|
| 91 |
+
this.addInput("ctx_1", "RGTHREE_DYNAMIC_CONTEXT");
|
| 92 |
+
this.addInput("ctx_2", "RGTHREE_DYNAMIC_CONTEXT");
|
| 93 |
+
this.addInput("ctx_3", "RGTHREE_DYNAMIC_CONTEXT");
|
| 94 |
+
this.addInput("ctx_4", "RGTHREE_DYNAMIC_CONTEXT");
|
| 95 |
+
this.addInput("ctx_5", "RGTHREE_DYNAMIC_CONTEXT");
|
| 96 |
+
super.onNodeCreated();
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
override addContextInput(name: string, type: string, slot?: number): void {}
|
| 100 |
+
|
| 101 |
+
/**
|
| 102 |
+
* This is a "hard" refresh of the list, but looping over the actual context inputs, and
|
| 103 |
+
* recompiling the shadowInputs and outputs.
|
| 104 |
+
*/
|
| 105 |
+
private refreshInputsAndOutputs() {
|
| 106 |
+
const inputs: (InputLike & {count: number})[] = [
|
| 107 |
+
{name: "base_ctx", type: "RGTHREE_DYNAMIC_CONTEXT", link: null, count: 0},
|
| 108 |
+
];
|
| 109 |
+
let numConnected = 0;
|
| 110 |
+
for (let i = 0; i < this.inputs.length; i++) {
|
| 111 |
+
const childCtxs = getConnectedInputNodesAndFilterPassThroughs(
|
| 112 |
+
this,
|
| 113 |
+
this,
|
| 114 |
+
i,
|
| 115 |
+
) as DynamicContextNodeBase[];
|
| 116 |
+
if (childCtxs.length > 1) {
|
| 117 |
+
throw new Error("How is there more than one input?");
|
| 118 |
+
}
|
| 119 |
+
const ctx = childCtxs[0];
|
| 120 |
+
if (!ctx) continue;
|
| 121 |
+
numConnected++;
|
| 122 |
+
const slotsData = CONTEXT_SERVICE.getDynamicContextInputsData(ctx);
|
| 123 |
+
console.log(slotsData);
|
| 124 |
+
for (const slotData of slotsData) {
|
| 125 |
+
const found = inputs.find(
|
| 126 |
+
(n) => getContextOutputName(slotData.name) === getContextOutputName(n.name),
|
| 127 |
+
);
|
| 128 |
+
if (found) {
|
| 129 |
+
found.count += 1;
|
| 130 |
+
continue;
|
| 131 |
+
}
|
| 132 |
+
inputs.push({
|
| 133 |
+
name: slotData.name,
|
| 134 |
+
type: slotData.type,
|
| 135 |
+
link: null,
|
| 136 |
+
count: 1,
|
| 137 |
+
});
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
this.shadowInputs = inputs;
|
| 141 |
+
// First output is always CONTEXT, so "p" is the offset.
|
| 142 |
+
let i = 0;
|
| 143 |
+
for (i; i < this.shadowInputs.length; i++) {
|
| 144 |
+
const data = this.shadowInputs[i]!;
|
| 145 |
+
let existing = this.outputs.find(
|
| 146 |
+
(o) => getContextOutputName(o.name) === getContextOutputName(data.name),
|
| 147 |
+
);
|
| 148 |
+
if (!existing) {
|
| 149 |
+
existing = this.addOutput(getContextOutputName(data.name), data.type);
|
| 150 |
+
}
|
| 151 |
+
moveArrayItem(this.outputs, existing, i);
|
| 152 |
+
delete existing.rgthree_status;
|
| 153 |
+
if (data.count !== numConnected) {
|
| 154 |
+
existing.rgthree_status = "WARN";
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
while (this.outputs[i]) {
|
| 158 |
+
const output = this.outputs[i];
|
| 159 |
+
if (output?.links?.length) {
|
| 160 |
+
output.rgthree_status = "ERROR";
|
| 161 |
+
i++;
|
| 162 |
+
} else {
|
| 163 |
+
this.removeOutput(i);
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
this.fixInputsOutputsLinkSlots();
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
override onDrawForeground(ctx: CanvasRenderingContext2D, canvas: LGraphCanvas): void {
|
| 170 |
+
const low_quality = (canvas?.ds?.scale ?? 1) < 0.6;
|
| 171 |
+
if (low_quality || this.size[0] <= 10) {
|
| 172 |
+
return;
|
| 173 |
+
}
|
| 174 |
+
let y = LiteGraph.NODE_SLOT_HEIGHT - 1;
|
| 175 |
+
const w = this.size[0];
|
| 176 |
+
ctx.save();
|
| 177 |
+
ctx.font = "normal " + LiteGraph.NODE_SUBTEXT_SIZE + "px Arial";
|
| 178 |
+
ctx.textAlign = "right";
|
| 179 |
+
|
| 180 |
+
for (const output of this.outputs) {
|
| 181 |
+
if (!output.rgthree_status) {
|
| 182 |
+
y += LiteGraph.NODE_SLOT_HEIGHT;
|
| 183 |
+
continue;
|
| 184 |
+
}
|
| 185 |
+
const x = w - 20 - measureText(ctx, output.name);
|
| 186 |
+
if (output.rgthree_status === "ERROR") {
|
| 187 |
+
ctx.fillText("🛑", x, y);
|
| 188 |
+
} else if (output.rgthree_status === "WARN") {
|
| 189 |
+
ctx.fillText("⚠️", x, y);
|
| 190 |
+
}
|
| 191 |
+
y += LiteGraph.NODE_SLOT_HEIGHT;
|
| 192 |
+
}
|
| 193 |
+
ctx.restore();
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
app.registerExtension({
|
| 198 |
+
name: "rgthree.DynamicContextSwitch",
|
| 199 |
+
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 200 |
+
if (!CONFIG_SERVICE.getConfigValue("unreleased.dynamic_context.enabled")) {
|
| 201 |
+
return;
|
| 202 |
+
}
|
| 203 |
+
if (nodeData.name === DynamicContextSwitchNode.type) {
|
| 204 |
+
DynamicContextSwitchNode.setUp(nodeType, nodeData);
|
| 205 |
+
}
|
| 206 |
+
},
|
| 207 |
+
});
|
rgthree-comfy/src_web/comfyui/fast_actions_button.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js";
|
| 2 |
+
import type { ComfyApp, ComfyWidget } from "typings/comfy.js";
|
| 3 |
+
import type { IWidget, LGraph, LGraphNode, SerializedLGraphNode } from "typings/litegraph.js";
|
| 4 |
+
import type { RgthreeBaseNode } from "./base_node.js";
|
| 5 |
+
|
| 6 |
+
import { app } from "scripts/app.js";
|
| 7 |
+
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
|
| 8 |
+
import { NodeTypesString } from "./constants.js";
|
| 9 |
+
import { addMenuItem } from "./utils.js";
|
| 10 |
+
import { rgthree } from "./rgthree.js";
|
| 11 |
+
|
| 12 |
+
const MODE_ALWAYS = 0;
|
| 13 |
+
const MODE_MUTE = 2;
|
| 14 |
+
const MODE_BYPASS = 4;
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* The Fast Actions Button.
|
| 18 |
+
*
|
| 19 |
+
* This adds a button that the user can connect any node to and then choose an action to take on
|
| 20 |
+
* that node when the button is pressed. Default actions are "Mute," "Bypass," and "Enable," but
|
| 21 |
+
* Nodes can expose actions additional actions that can then be called back.
|
| 22 |
+
*/
|
| 23 |
+
class FastActionsButton extends BaseAnyInputConnectedNode {
|
| 24 |
+
static override type = NodeTypesString.FAST_ACTIONS_BUTTON;
|
| 25 |
+
static override title = NodeTypesString.FAST_ACTIONS_BUTTON;
|
| 26 |
+
override comfyClass = NodeTypesString.FAST_ACTIONS_BUTTON;
|
| 27 |
+
|
| 28 |
+
readonly logger = rgthree.newLogSession("[FastActionsButton]");
|
| 29 |
+
|
| 30 |
+
static "@buttonText" = { type: "string" };
|
| 31 |
+
static "@shortcutModifier" = {
|
| 32 |
+
type: "combo",
|
| 33 |
+
values: ["ctrl", "alt", "shift"],
|
| 34 |
+
};
|
| 35 |
+
static "@shortcutKey" = { type: "string" };
|
| 36 |
+
|
| 37 |
+
static collapsible = false;
|
| 38 |
+
|
| 39 |
+
override readonly isVirtualNode = true;
|
| 40 |
+
|
| 41 |
+
override serialize_widgets = true;
|
| 42 |
+
|
| 43 |
+
readonly buttonWidget: IWidget;
|
| 44 |
+
|
| 45 |
+
readonly widgetToData = new Map<IWidget, { comfy?: ComfyApp; node?: LGraphNode }>();
|
| 46 |
+
readonly nodeIdtoFunctionCache = new Map<number, string>();
|
| 47 |
+
|
| 48 |
+
readonly keypressBound;
|
| 49 |
+
readonly keyupBound;
|
| 50 |
+
|
| 51 |
+
private executingFromShortcut = false;
|
| 52 |
+
|
| 53 |
+
constructor(title?: string) {
|
| 54 |
+
super(title);
|
| 55 |
+
this.properties["buttonText"] = "🎬 Action!";
|
| 56 |
+
this.properties["shortcutModifier"] = "alt";
|
| 57 |
+
this.properties["shortcutKey"] = "";
|
| 58 |
+
this.buttonWidget = this.addWidget(
|
| 59 |
+
"button",
|
| 60 |
+
this.properties["buttonText"],
|
| 61 |
+
null,
|
| 62 |
+
() => {
|
| 63 |
+
this.executeConnectedNodes();
|
| 64 |
+
},
|
| 65 |
+
{ serialize: false },
|
| 66 |
+
);
|
| 67 |
+
|
| 68 |
+
this.keypressBound = this.onKeypress.bind(this);
|
| 69 |
+
this.keyupBound = this.onKeyup.bind(this);
|
| 70 |
+
this.onConstructed();
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/** When we're given data to configure, like from a PNG or JSON. */
|
| 74 |
+
override configure(info: SerializedLGraphNode<LGraphNode>): void {
|
| 75 |
+
super.configure(info);
|
| 76 |
+
// Since we add the widgets dynamically, we need to wait to set their values
|
| 77 |
+
// with a short timeout.
|
| 78 |
+
setTimeout(() => {
|
| 79 |
+
if (info.widgets_values) {
|
| 80 |
+
for (let [index, value] of info.widgets_values.entries()) {
|
| 81 |
+
if (index > 0) {
|
| 82 |
+
if (value.startsWith("comfy_action:")) {
|
| 83 |
+
value = value.replace("comfy_action:", "");
|
| 84 |
+
this.addComfyActionWidget(index, value);
|
| 85 |
+
}
|
| 86 |
+
if (this.widgets[index]) {
|
| 87 |
+
this.widgets[index]!.value = value;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
}, 100);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
override clone() {
|
| 96 |
+
const cloned = super.clone();
|
| 97 |
+
cloned.properties["buttonText"] = "🎬 Action!";
|
| 98 |
+
cloned.properties["shortcutKey"] = "";
|
| 99 |
+
return cloned;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
override onAdded(graph: LGraph): void {
|
| 103 |
+
window.addEventListener("keydown", this.keypressBound);
|
| 104 |
+
window.addEventListener("keyup", this.keyupBound);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
override onRemoved(): void {
|
| 108 |
+
window.removeEventListener("keydown", this.keypressBound);
|
| 109 |
+
window.removeEventListener("keyup", this.keyupBound);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
async onKeypress(event: KeyboardEvent) {
|
| 113 |
+
const target = (event.target as HTMLElement)!;
|
| 114 |
+
if (
|
| 115 |
+
this.executingFromShortcut ||
|
| 116 |
+
target.localName == "input" ||
|
| 117 |
+
target.localName == "textarea"
|
| 118 |
+
) {
|
| 119 |
+
return;
|
| 120 |
+
}
|
| 121 |
+
if (
|
| 122 |
+
this.properties["shortcutKey"].trim() &&
|
| 123 |
+
this.properties["shortcutKey"].toLowerCase() === event.key.toLowerCase()
|
| 124 |
+
) {
|
| 125 |
+
const shortcutModifier = this.properties["shortcutModifier"];
|
| 126 |
+
let good = shortcutModifier === "ctrl" && event.ctrlKey;
|
| 127 |
+
good = good || (shortcutModifier === "alt" && event.altKey);
|
| 128 |
+
good = good || (shortcutModifier === "shift" && event.shiftKey);
|
| 129 |
+
good = good || (shortcutModifier === "meta" && event.metaKey);
|
| 130 |
+
if (good) {
|
| 131 |
+
setTimeout(() => {
|
| 132 |
+
this.executeConnectedNodes();
|
| 133 |
+
}, 20);
|
| 134 |
+
this.executingFromShortcut = true;
|
| 135 |
+
event.preventDefault();
|
| 136 |
+
event.stopImmediatePropagation();
|
| 137 |
+
app.canvas.dirty_canvas = true;
|
| 138 |
+
return false;
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
return;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
onKeyup(event: KeyboardEvent) {
|
| 145 |
+
const target = (event.target as HTMLElement)!;
|
| 146 |
+
if (target.localName == "input" || target.localName == "textarea") {
|
| 147 |
+
return;
|
| 148 |
+
}
|
| 149 |
+
this.executingFromShortcut = false;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
override onPropertyChanged(property: string, value: any, _prevValue: any): boolean | void {
|
| 153 |
+
if (property == "buttonText") {
|
| 154 |
+
this.buttonWidget.name = value;
|
| 155 |
+
}
|
| 156 |
+
if (property == "shortcutKey") {
|
| 157 |
+
value = value.trim();
|
| 158 |
+
this.properties["shortcutKey"] = (value && value[0].toLowerCase()) || "";
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
override handleLinkedNodesStabilization(linkedNodes: LGraphNode[]) {
|
| 163 |
+
let changed = false;
|
| 164 |
+
// Remove any widgets and data for widgets that are no longer linked.
|
| 165 |
+
for (const [widget, data] of this.widgetToData.entries()) {
|
| 166 |
+
if (!data.node) {
|
| 167 |
+
continue;
|
| 168 |
+
}
|
| 169 |
+
if (!linkedNodes.includes(data.node)) {
|
| 170 |
+
const index = this.widgets.indexOf(widget);
|
| 171 |
+
if (index > -1) {
|
| 172 |
+
this.widgetToData.delete(widget);
|
| 173 |
+
this.removeWidget(widget);
|
| 174 |
+
changed = true;
|
| 175 |
+
} else {
|
| 176 |
+
const [m, a] = this.logger.debugParts("Connected widget is not in widgets... weird.");
|
| 177 |
+
console[m]?.(...a);
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
const badNodes: LGraphNode[] = []; // Nodes that are deleted elsewhere may not exist in linkedNodes.
|
| 183 |
+
let indexOffset = 1; // Start with button, increment when we hit a non-node widget (like comfy)
|
| 184 |
+
for (const [index, node] of linkedNodes.entries()) {
|
| 185 |
+
// Sometimes linkedNodes is stale.
|
| 186 |
+
if (!node) {
|
| 187 |
+
const [m, a] = this.logger.debugParts("linkedNode provided that does not exist. ");
|
| 188 |
+
console[m]?.(...a);
|
| 189 |
+
badNodes.push(node);
|
| 190 |
+
continue;
|
| 191 |
+
}
|
| 192 |
+
let widgetAtSlot = this.widgets[index + indexOffset];
|
| 193 |
+
if (widgetAtSlot && this.widgetToData.get(widgetAtSlot)?.comfy) {
|
| 194 |
+
indexOffset++;
|
| 195 |
+
widgetAtSlot = this.widgets[index + indexOffset];
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
if (!widgetAtSlot || this.widgetToData.get(widgetAtSlot)?.node?.id !== node.id) {
|
| 199 |
+
// Find the next widget that matches the node.
|
| 200 |
+
let widget: IWidget | null = null;
|
| 201 |
+
for (let i = index + indexOffset; i < this.widgets.length; i++) {
|
| 202 |
+
if (this.widgetToData.get(this.widgets[i]!)?.node?.id === node.id) {
|
| 203 |
+
widget = this.widgets.splice(i, 1)[0]!;
|
| 204 |
+
this.widgets.splice(index + indexOffset, 0, widget);
|
| 205 |
+
changed = true;
|
| 206 |
+
break;
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
if (!widget) {
|
| 210 |
+
// Add a widget at this spot.
|
| 211 |
+
const exposedActions: string[] = (node.constructor as any).exposedActions || [];
|
| 212 |
+
widget = this.addWidget("combo", node.title, "None", "", {
|
| 213 |
+
values: ["None", "Mute", "Bypass", "Enable", ...exposedActions],
|
| 214 |
+
});
|
| 215 |
+
(widget as ComfyWidget).serializeValue = async (_node: LGraphNode, _index: number) => {
|
| 216 |
+
return widget?.value;
|
| 217 |
+
};
|
| 218 |
+
this.widgetToData.set(widget, { node });
|
| 219 |
+
changed = true;
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// Go backwards through widgets, and remove any that are not in out widgetToData
|
| 225 |
+
for (let i = this.widgets.length - 1; i > linkedNodes.length + indexOffset - 1; i--) {
|
| 226 |
+
const widgetAtSlot = this.widgets[i];
|
| 227 |
+
if (widgetAtSlot && this.widgetToData.get(widgetAtSlot)?.comfy) {
|
| 228 |
+
continue;
|
| 229 |
+
}
|
| 230 |
+
this.removeWidget(widgetAtSlot);
|
| 231 |
+
changed = true;
|
| 232 |
+
}
|
| 233 |
+
return changed;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
override removeWidget(widgetOrSlot?: number | IWidget): void {
|
| 237 |
+
const widget = typeof widgetOrSlot === "number" ? this.widgets[widgetOrSlot] : widgetOrSlot;
|
| 238 |
+
if (widget && this.widgetToData.has(widget)) {
|
| 239 |
+
this.widgetToData.delete(widget);
|
| 240 |
+
}
|
| 241 |
+
super.removeWidget(widgetOrSlot);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/**
|
| 245 |
+
* Runs through the widgets, and executes the actions.
|
| 246 |
+
*/
|
| 247 |
+
async executeConnectedNodes() {
|
| 248 |
+
for (const widget of this.widgets) {
|
| 249 |
+
if (widget == this.buttonWidget) {
|
| 250 |
+
continue;
|
| 251 |
+
}
|
| 252 |
+
const action = widget.value;
|
| 253 |
+
const { comfy, node } = this.widgetToData.get(widget) ?? {};
|
| 254 |
+
if (comfy) {
|
| 255 |
+
if (action === "Queue Prompt") {
|
| 256 |
+
await comfy.queuePrompt(0);
|
| 257 |
+
}
|
| 258 |
+
continue;
|
| 259 |
+
}
|
| 260 |
+
if (node) {
|
| 261 |
+
if (action === "Mute") {
|
| 262 |
+
node.mode = MODE_MUTE;
|
| 263 |
+
} else if (action === "Bypass") {
|
| 264 |
+
node.mode = MODE_BYPASS;
|
| 265 |
+
} else if (action === "Enable") {
|
| 266 |
+
node.mode = MODE_ALWAYS;
|
| 267 |
+
}
|
| 268 |
+
// If there's a handleAction, always call it.
|
| 269 |
+
if ((node as RgthreeBaseNode).handleAction) {
|
| 270 |
+
await (node as RgthreeBaseNode).handleAction(action);
|
| 271 |
+
}
|
| 272 |
+
app.graph.change();
|
| 273 |
+
continue;
|
| 274 |
+
}
|
| 275 |
+
console.warn("Fast Actions Button has a widget without correct data.");
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/**
|
| 280 |
+
* Adds a ComfyActionWidget at the provided slot (or end).
|
| 281 |
+
*/
|
| 282 |
+
addComfyActionWidget(slot?: number, value?: string) {
|
| 283 |
+
let widget = this.addWidget(
|
| 284 |
+
"combo",
|
| 285 |
+
"Comfy Action",
|
| 286 |
+
"None",
|
| 287 |
+
() => {
|
| 288 |
+
if (widget.value.startsWith("MOVE ")) {
|
| 289 |
+
this.widgets.push(this.widgets.splice(this.widgets.indexOf(widget), 1)[0]!);
|
| 290 |
+
widget.value = (widget as any)["lastValue_"];
|
| 291 |
+
} else if (widget.value.startsWith("REMOVE ")) {
|
| 292 |
+
this.removeWidget(widget);
|
| 293 |
+
}
|
| 294 |
+
(widget as any)["lastValue_"] = widget.value;
|
| 295 |
+
},
|
| 296 |
+
{
|
| 297 |
+
values: ["None", "Queue Prompt", "REMOVE Comfy Action", "MOVE to end"],
|
| 298 |
+
},
|
| 299 |
+
);
|
| 300 |
+
(widget as any)["lastValue_"] = value;
|
| 301 |
+
|
| 302 |
+
(widget as ComfyWidget).serializeValue = async (_node: LGraphNode, _index: number) => {
|
| 303 |
+
return `comfy_app:${widget?.value}`;
|
| 304 |
+
};
|
| 305 |
+
this.widgetToData.set(widget, { comfy: app });
|
| 306 |
+
|
| 307 |
+
if (slot != null) {
|
| 308 |
+
this.widgets.splice(slot, 0, this.widgets.splice(this.widgets.indexOf(widget), 1)[0]!);
|
| 309 |
+
}
|
| 310 |
+
return widget;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
override onSerialize(o: SerializedLGraphNode) {
|
| 314 |
+
super.onSerialize && super.onSerialize(o);
|
| 315 |
+
for (let [index, value] of (o.widgets_values || []).entries()) {
|
| 316 |
+
if (this.widgets[index]?.name === "Comfy Action") {
|
| 317 |
+
o.widgets_values![index] = `comfy_action:${value}`;
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
static override setUp() {
|
| 323 |
+
super.setUp();
|
| 324 |
+
addMenuItem(this, app, {
|
| 325 |
+
name: "➕ Append a Comfy Action",
|
| 326 |
+
callback: (nodeArg: LGraphNode) => {
|
| 327 |
+
(nodeArg as FastActionsButton).addComfyActionWidget();
|
| 328 |
+
},
|
| 329 |
+
});
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
app.registerExtension({
|
| 334 |
+
name: "rgthree.FastActionsButton",
|
| 335 |
+
registerCustomNodes() {
|
| 336 |
+
FastActionsButton.setUp();
|
| 337 |
+
},
|
| 338 |
+
loadedGraphNode(node: LGraphNode) {
|
| 339 |
+
if (node.type == FastActionsButton.title) {
|
| 340 |
+
(node as FastActionsButton)._tempWidth = node.size[0];
|
| 341 |
+
}
|
| 342 |
+
},
|
| 343 |
+
});
|
rgthree-comfy/src_web/comfyui/fast_groups_bypasser.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js";
|
| 2 |
+
import { app } from "scripts/app.js";
|
| 3 |
+
import { NodeTypesString } from "./constants.js";
|
| 4 |
+
import { BaseFastGroupsModeChanger } from "./fast_groups_muter.js";
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Fast Bypasser implementation that looks for groups in the workflow and adds toggles to mute them.
|
| 8 |
+
*/
|
| 9 |
+
export class FastGroupsBypasser extends BaseFastGroupsModeChanger {
|
| 10 |
+
static override type = NodeTypesString.FAST_GROUPS_BYPASSER;
|
| 11 |
+
static override title = NodeTypesString.FAST_GROUPS_BYPASSER;
|
| 12 |
+
override comfyClass = NodeTypesString.FAST_GROUPS_BYPASSER;
|
| 13 |
+
|
| 14 |
+
static override exposedActions = ["Bypass all", "Enable all", "Toggle all"];
|
| 15 |
+
|
| 16 |
+
protected override helpActions = "bypass and enable";
|
| 17 |
+
|
| 18 |
+
override readonly modeOn = LiteGraph.ALWAYS;
|
| 19 |
+
override readonly modeOff = 4; // Used by Comfy for "bypass"
|
| 20 |
+
|
| 21 |
+
constructor(title = FastGroupsBypasser.title) {
|
| 22 |
+
super(title);
|
| 23 |
+
this.onConstructed();
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
app.registerExtension({
|
| 28 |
+
name: "rgthree.FastGroupsBypasser",
|
| 29 |
+
registerCustomNodes() {
|
| 30 |
+
FastGroupsBypasser.setUp();
|
| 31 |
+
},
|
| 32 |
+
loadedGraphNode(node: FastGroupsBypasser) {
|
| 33 |
+
if (node.type == FastGroupsBypasser.title) {
|
| 34 |
+
node.tempSize = [...node.size];
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
});
|
rgthree-comfy/src_web/comfyui/fast_groups_muter.ts
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { app } from "scripts/app.js";
|
| 2 |
+
import { RgthreeBaseVirtualNode } from "./base_node.js";
|
| 3 |
+
import { NodeTypesString } from "./constants.js";
|
| 4 |
+
import {
|
| 5 |
+
type LGraphNode,
|
| 6 |
+
type LGraph as TLGraph,
|
| 7 |
+
LGraphCanvas as TLGraphCanvas,
|
| 8 |
+
Vector2,
|
| 9 |
+
SerializedLGraphNode,
|
| 10 |
+
IWidget,
|
| 11 |
+
} from "typings/litegraph.js";
|
| 12 |
+
import { SERVICE as FAST_GROUPS_SERVICE } from "./services/fast_groups_service.js";
|
| 13 |
+
import { drawNodeWidget, fitString } from "./utils_canvas.js";
|
| 14 |
+
import { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js";
|
| 15 |
+
|
| 16 |
+
const PROPERTY_SORT = "sort";
|
| 17 |
+
const PROPERTY_SORT_CUSTOM_ALPHA = "customSortAlphabet";
|
| 18 |
+
const PROPERTY_MATCH_COLORS = "matchColors";
|
| 19 |
+
const PROPERTY_MATCH_TITLE = "matchTitle";
|
| 20 |
+
const PROPERTY_SHOW_NAV = "showNav";
|
| 21 |
+
const PROPERTY_RESTRICTION = "toggleRestriction";
|
| 22 |
+
|
| 23 |
+
/**
|
| 24 |
+
* Fast Muter implementation that looks for groups in the workflow and adds toggles to mute them.
|
| 25 |
+
*/
|
| 26 |
+
export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode {
|
| 27 |
+
static override type = NodeTypesString.FAST_GROUPS_MUTER;
|
| 28 |
+
static override title = NodeTypesString.FAST_GROUPS_MUTER;
|
| 29 |
+
|
| 30 |
+
static override exposedActions = ["Mute all", "Enable all", "Toggle all"];
|
| 31 |
+
|
| 32 |
+
readonly modeOn: number = LiteGraph.ALWAYS;
|
| 33 |
+
readonly modeOff: number = LiteGraph.NEVER;
|
| 34 |
+
|
| 35 |
+
private debouncerTempWidth: number = 0;
|
| 36 |
+
tempSize: Vector2 | null = null;
|
| 37 |
+
|
| 38 |
+
// We don't need to serizalize since we'll just be checking group data on startup anyway
|
| 39 |
+
override serialize_widgets = false;
|
| 40 |
+
|
| 41 |
+
protected helpActions = "mute and unmute";
|
| 42 |
+
|
| 43 |
+
static "@matchColors" = { type: "string" };
|
| 44 |
+
static "@matchTitle" = { type: "string" };
|
| 45 |
+
static "@showNav" = { type: "boolean" };
|
| 46 |
+
static "@sort" = {
|
| 47 |
+
type: "combo",
|
| 48 |
+
values: ["position", "alphanumeric", "custom alphabet"],
|
| 49 |
+
};
|
| 50 |
+
static "@customSortAlphabet" = { type: "string" };
|
| 51 |
+
|
| 52 |
+
static "@toggleRestriction" = {
|
| 53 |
+
type: "combo",
|
| 54 |
+
values: ["default", "max one", "always one"],
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
constructor(title = FastGroupsMuter.title) {
|
| 58 |
+
super(title);
|
| 59 |
+
this.properties[PROPERTY_MATCH_COLORS] = "";
|
| 60 |
+
this.properties[PROPERTY_MATCH_TITLE] = "";
|
| 61 |
+
this.properties[PROPERTY_SHOW_NAV] = true;
|
| 62 |
+
this.properties[PROPERTY_SORT] = "position";
|
| 63 |
+
this.properties[PROPERTY_SORT_CUSTOM_ALPHA] = "";
|
| 64 |
+
this.properties[PROPERTY_RESTRICTION] = "default";
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
override onConstructed(): boolean {
|
| 68 |
+
this.addOutput("OPT_CONNECTION", "*");
|
| 69 |
+
return super.onConstructed();
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
override configure(info: SerializedLGraphNode<LGraphNode>): void {
|
| 73 |
+
// Patch a small issue (~14h) where multiple OPT_CONNECTIONS may have been created.
|
| 74 |
+
// https://github.com/rgthree/rgthree-comfy/issues/206
|
| 75 |
+
// TODO: This can probably be removed within a few weeks.
|
| 76 |
+
if (info.outputs?.length) {
|
| 77 |
+
info.outputs.length = 1;
|
| 78 |
+
}
|
| 79 |
+
super.configure(info);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
override onAdded(graph: TLGraph): void {
|
| 83 |
+
FAST_GROUPS_SERVICE.addFastGroupNode(this);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
override onRemoved(): void {
|
| 87 |
+
FAST_GROUPS_SERVICE.removeFastGroupNode(this);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
refreshWidgets() {
|
| 91 |
+
const canvas = app.canvas as TLGraphCanvas;
|
| 92 |
+
let sort = this.properties?.[PROPERTY_SORT] || "position";
|
| 93 |
+
let customAlphabet: string[] | null = null;
|
| 94 |
+
if (sort === "custom alphabet") {
|
| 95 |
+
const customAlphaStr = this.properties?.[PROPERTY_SORT_CUSTOM_ALPHA]?.replace(/\n/g, "");
|
| 96 |
+
if (customAlphaStr && customAlphaStr.trim()) {
|
| 97 |
+
customAlphabet = customAlphaStr.includes(",")
|
| 98 |
+
? customAlphaStr.toLocaleLowerCase().split(",")
|
| 99 |
+
: customAlphaStr.toLocaleLowerCase().trim().split("");
|
| 100 |
+
}
|
| 101 |
+
if (!customAlphabet?.length) {
|
| 102 |
+
sort = "alphanumeric";
|
| 103 |
+
customAlphabet = null;
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
const groups = [...FAST_GROUPS_SERVICE.getGroups(sort)];
|
| 108 |
+
// The service will return pre-sorted groups for alphanumeric and position. If this node has a
|
| 109 |
+
// custom sort, then we need to sort it manually.
|
| 110 |
+
if (customAlphabet?.length) {
|
| 111 |
+
groups.sort((a, b) => {
|
| 112 |
+
let aIndex = -1;
|
| 113 |
+
let bIndex = -1;
|
| 114 |
+
// Loop and find indexes. As we're finding multiple, a single for loop is more efficient.
|
| 115 |
+
for (const [index, alpha] of customAlphabet!.entries()) {
|
| 116 |
+
aIndex =
|
| 117 |
+
aIndex < 0 ? (a.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : aIndex;
|
| 118 |
+
bIndex =
|
| 119 |
+
bIndex < 0 ? (b.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : bIndex;
|
| 120 |
+
if (aIndex > -1 && bIndex > -1) {
|
| 121 |
+
break;
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
// Now compare.
|
| 125 |
+
if (aIndex > -1 && bIndex > -1) {
|
| 126 |
+
const ret = aIndex - bIndex;
|
| 127 |
+
if (ret === 0) {
|
| 128 |
+
return a.title.localeCompare(b.title);
|
| 129 |
+
}
|
| 130 |
+
return ret;
|
| 131 |
+
} else if (aIndex > -1) {
|
| 132 |
+
return -1;
|
| 133 |
+
} else if (bIndex > -1) {
|
| 134 |
+
return 1;
|
| 135 |
+
}
|
| 136 |
+
return a.title.localeCompare(b.title);
|
| 137 |
+
});
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// See if we're filtering by colors, and match against the built-in keywords and actuial hex
|
| 141 |
+
// values.
|
| 142 |
+
let filterColors = (
|
| 143 |
+
(this.properties?.[PROPERTY_MATCH_COLORS] as string)?.split(",") || []
|
| 144 |
+
).filter((c) => c.trim());
|
| 145 |
+
if (filterColors.length) {
|
| 146 |
+
filterColors = filterColors.map((color) => {
|
| 147 |
+
color = color.trim().toLocaleLowerCase();
|
| 148 |
+
if (LGraphCanvas.node_colors[color]) {
|
| 149 |
+
color = LGraphCanvas.node_colors[color]!.groupcolor;
|
| 150 |
+
}
|
| 151 |
+
color = color.replace("#", "").toLocaleLowerCase();
|
| 152 |
+
if (color.length === 3) {
|
| 153 |
+
color = color.replace(/(.)(.)(.)/, "$1$1$2$2$3$3");
|
| 154 |
+
}
|
| 155 |
+
return `#${color}`;
|
| 156 |
+
});
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// Go over the groups
|
| 160 |
+
let index = 0;
|
| 161 |
+
for (const group of groups) {
|
| 162 |
+
if (filterColors.length) {
|
| 163 |
+
let groupColor = group.color?.replace("#", "").trim().toLocaleLowerCase();
|
| 164 |
+
if (!groupColor) {
|
| 165 |
+
continue;
|
| 166 |
+
}
|
| 167 |
+
if (groupColor.length === 3) {
|
| 168 |
+
groupColor = groupColor.replace(/(.)(.)(.)/, "$1$1$2$2$3$3");
|
| 169 |
+
}
|
| 170 |
+
groupColor = `#${groupColor}`;
|
| 171 |
+
if (!filterColors.includes(groupColor)) {
|
| 172 |
+
continue;
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
if (this.properties?.[PROPERTY_MATCH_TITLE]?.trim()) {
|
| 176 |
+
try {
|
| 177 |
+
if (!new RegExp(this.properties[PROPERTY_MATCH_TITLE], "i").exec(group.title)) {
|
| 178 |
+
continue;
|
| 179 |
+
}
|
| 180 |
+
} catch (e) {
|
| 181 |
+
console.error(e);
|
| 182 |
+
continue;
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
const widgetName = `Enable ${group.title}`;
|
| 186 |
+
let widget = this.widgets.find((w) => w.name === widgetName);
|
| 187 |
+
if (!widget) {
|
| 188 |
+
// When we add a widget, litegraph is going to mess up the size, so we
|
| 189 |
+
// store it so we can retrieve it in computeSize. Hacky..
|
| 190 |
+
this.tempSize = [...this.size];
|
| 191 |
+
widget = this.addCustomWidget<IWidget<boolean>>({
|
| 192 |
+
name: "RGTHREE_TOGGLE_AND_NAV",
|
| 193 |
+
label: "",
|
| 194 |
+
value: false,
|
| 195 |
+
disabled: false,
|
| 196 |
+
options: { on: "yes", off: "no" },
|
| 197 |
+
draw: function (
|
| 198 |
+
ctx: CanvasRenderingContext2D,
|
| 199 |
+
node: LGraphNode,
|
| 200 |
+
width: number,
|
| 201 |
+
posY: number,
|
| 202 |
+
height: number,
|
| 203 |
+
) {
|
| 204 |
+
const widgetData = drawNodeWidget(ctx, {
|
| 205 |
+
width,
|
| 206 |
+
height,
|
| 207 |
+
posY,
|
| 208 |
+
});
|
| 209 |
+
|
| 210 |
+
const showNav = node.properties?.[PROPERTY_SHOW_NAV] !== false;
|
| 211 |
+
|
| 212 |
+
// Render from right to left, since the text on left will take available space.
|
| 213 |
+
// `currentX` markes the current x position moving backwards.
|
| 214 |
+
let currentX = widgetData.width - widgetData.margin;
|
| 215 |
+
|
| 216 |
+
// The nav arrow
|
| 217 |
+
if (!widgetData.lowQuality && showNav) {
|
| 218 |
+
currentX -= 7; // Arrow space margin
|
| 219 |
+
const midY = widgetData.posY + widgetData.height * 0.5;
|
| 220 |
+
ctx.fillStyle = ctx.strokeStyle = "#89A";
|
| 221 |
+
ctx.lineJoin = "round";
|
| 222 |
+
ctx.lineCap = "round";
|
| 223 |
+
const arrow = new Path2D(`M${currentX} ${midY} l -7 6 v -3 h -7 v -6 h 7 v -3 z`);
|
| 224 |
+
ctx.fill(arrow);
|
| 225 |
+
ctx.stroke(arrow);
|
| 226 |
+
currentX -= 14;
|
| 227 |
+
|
| 228 |
+
currentX -= 7;
|
| 229 |
+
ctx.strokeStyle = widgetData.colorOutline;
|
| 230 |
+
ctx.stroke(new Path2D(`M ${currentX} ${widgetData.posY} v ${widgetData.height}`));
|
| 231 |
+
} else if (widgetData.lowQuality && showNav) {
|
| 232 |
+
currentX -= 28;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// The toggle itself.
|
| 236 |
+
currentX -= 7;
|
| 237 |
+
ctx.fillStyle = this.value ? "#89A" : "#333";
|
| 238 |
+
ctx.beginPath();
|
| 239 |
+
const toggleRadius = height * 0.36;
|
| 240 |
+
ctx.arc(currentX - toggleRadius, posY + height * 0.5, toggleRadius, 0, Math.PI * 2);
|
| 241 |
+
ctx.fill();
|
| 242 |
+
currentX -= toggleRadius * 2;
|
| 243 |
+
|
| 244 |
+
if (!widgetData.lowQuality) {
|
| 245 |
+
currentX -= 4;
|
| 246 |
+
ctx.textAlign = "right";
|
| 247 |
+
ctx.fillStyle = this.value ? widgetData.colorText : widgetData.colorTextSecondary;
|
| 248 |
+
const label = this.label || this.name;
|
| 249 |
+
const toggleLabelOn = this.options.on || "true";
|
| 250 |
+
const toggleLabelOff = this.options.off || "false";
|
| 251 |
+
ctx.fillText(
|
| 252 |
+
this.value ? toggleLabelOn : toggleLabelOff,
|
| 253 |
+
currentX,
|
| 254 |
+
posY + height * 0.7,
|
| 255 |
+
);
|
| 256 |
+
currentX -= Math.max(
|
| 257 |
+
ctx.measureText(toggleLabelOn).width,
|
| 258 |
+
ctx.measureText(toggleLabelOff).width,
|
| 259 |
+
);
|
| 260 |
+
|
| 261 |
+
currentX -= 7;
|
| 262 |
+
ctx.textAlign = "left";
|
| 263 |
+
let maxLabelWidth =
|
| 264 |
+
widgetData.width - widgetData.margin - 10 - (widgetData.width - currentX);
|
| 265 |
+
if (label != null) {
|
| 266 |
+
ctx.fillText(
|
| 267 |
+
fitString(ctx, label, maxLabelWidth),
|
| 268 |
+
widgetData.margin + 10,
|
| 269 |
+
posY + height * 0.7,
|
| 270 |
+
);
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
},
|
| 274 |
+
serializeValue(serializedNode: SerializedLGraphNode, widgetIndex: number) {
|
| 275 |
+
return this.value;
|
| 276 |
+
},
|
| 277 |
+
mouse(event: PointerEvent, pos: Vector2, node: LGraphNode) {
|
| 278 |
+
if (event.type == "pointerdown") {
|
| 279 |
+
if (
|
| 280 |
+
node.properties?.[PROPERTY_SHOW_NAV] !== false &&
|
| 281 |
+
pos[0] >= node.size[0] - 15 - 28 - 1
|
| 282 |
+
) {
|
| 283 |
+
const canvas = app.canvas as TLGraphCanvas;
|
| 284 |
+
const lowQuality = (canvas.ds?.scale || 1) <= 0.5;
|
| 285 |
+
if (!lowQuality) {
|
| 286 |
+
// Clicked on right half with nav arrow, go to the group, center on group and set
|
| 287 |
+
// zoom to see it all.
|
| 288 |
+
canvas.centerOnNode(group);
|
| 289 |
+
const zoomCurrent = canvas.ds?.scale || 1;
|
| 290 |
+
const zoomX = canvas.canvas.width / group._size[0] - 0.02;
|
| 291 |
+
const zoomY = canvas.canvas.height / group._size[1] - 0.02;
|
| 292 |
+
canvas.setZoom(Math.min(zoomCurrent, zoomX, zoomY), [
|
| 293 |
+
canvas.canvas.width / 2,
|
| 294 |
+
canvas.canvas.height / 2,
|
| 295 |
+
]);
|
| 296 |
+
canvas.setDirty(true, true);
|
| 297 |
+
}
|
| 298 |
+
} else {
|
| 299 |
+
this.value = !this.value;
|
| 300 |
+
setTimeout(() => {
|
| 301 |
+
this.callback?.(this.value, app.canvas, node, pos, event);
|
| 302 |
+
}, 20);
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
return true;
|
| 306 |
+
},
|
| 307 |
+
});
|
| 308 |
+
(widget as any).doModeChange = (force?: boolean, skipOtherNodeCheck?: boolean) => {
|
| 309 |
+
group.recomputeInsideNodes();
|
| 310 |
+
const hasAnyActiveNodes = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS);
|
| 311 |
+
let newValue = force != null ? force : !hasAnyActiveNodes;
|
| 312 |
+
if (skipOtherNodeCheck !== true) {
|
| 313 |
+
if (newValue && this.properties?.[PROPERTY_RESTRICTION]?.includes(" one")) {
|
| 314 |
+
for (const widget of this.widgets) {
|
| 315 |
+
(widget as any).doModeChange(false, true);
|
| 316 |
+
}
|
| 317 |
+
} else if (!newValue && this.properties?.[PROPERTY_RESTRICTION] === "always one") {
|
| 318 |
+
newValue = this.widgets.every((w) => !w.value || w === widget);
|
| 319 |
+
}
|
| 320 |
+
}
|
| 321 |
+
for (const node of group._nodes) {
|
| 322 |
+
node.mode = (newValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4;
|
| 323 |
+
}
|
| 324 |
+
(group as any)._rgthreeHasAnyActiveNode = newValue;
|
| 325 |
+
widget!.value = newValue;
|
| 326 |
+
app.graph.setDirtyCanvas(true, false);
|
| 327 |
+
};
|
| 328 |
+
widget.callback = () => {
|
| 329 |
+
(widget as any).doModeChange();
|
| 330 |
+
};
|
| 331 |
+
|
| 332 |
+
this.setSize(this.computeSize());
|
| 333 |
+
}
|
| 334 |
+
if (widget.name != widgetName) {
|
| 335 |
+
widget.name = widgetName;
|
| 336 |
+
this.setDirtyCanvas(true, false);
|
| 337 |
+
}
|
| 338 |
+
if (widget.value != (group as any)._rgthreeHasAnyActiveNode) {
|
| 339 |
+
widget.value = (group as any)._rgthreeHasAnyActiveNode;
|
| 340 |
+
this.setDirtyCanvas(true, false);
|
| 341 |
+
}
|
| 342 |
+
if (this.widgets[index] !== widget) {
|
| 343 |
+
const oldIndex = this.widgets.findIndex((w) => w === widget);
|
| 344 |
+
this.widgets.splice(index, 0, this.widgets.splice(oldIndex, 1)[0]!);
|
| 345 |
+
this.setDirtyCanvas(true, false);
|
| 346 |
+
}
|
| 347 |
+
index++;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
// Everything should now be in order, so let's remove all remaining widgets.
|
| 351 |
+
while ((this.widgets || [])[index]) {
|
| 352 |
+
this.removeWidget(index++);
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
override computeSize(out?: Vector2) {
|
| 357 |
+
let size = super.computeSize(out);
|
| 358 |
+
if (this.tempSize) {
|
| 359 |
+
size[0] = Math.max(this.tempSize[0], size[0]);
|
| 360 |
+
size[1] = Math.max(this.tempSize[1], size[1]);
|
| 361 |
+
// We sometimes get repeated calls to compute size, so debounce before clearing.
|
| 362 |
+
this.debouncerTempWidth && clearTimeout(this.debouncerTempWidth);
|
| 363 |
+
this.debouncerTempWidth = setTimeout(() => {
|
| 364 |
+
this.tempSize = null;
|
| 365 |
+
}, 32);
|
| 366 |
+
}
|
| 367 |
+
setTimeout(() => {
|
| 368 |
+
app.graph.setDirtyCanvas(true, true);
|
| 369 |
+
}, 16);
|
| 370 |
+
return size;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
override async handleAction(action: string) {
|
| 374 |
+
if (action === "Mute all" || action === "Bypass all") {
|
| 375 |
+
const alwaysOne = this.properties?.[PROPERTY_RESTRICTION] === "always one";
|
| 376 |
+
for (const [index, widget] of this.widgets.entries()) {
|
| 377 |
+
(widget as any)?.doModeChange(alwaysOne && !index ? true : false, true);
|
| 378 |
+
}
|
| 379 |
+
} else if (action === "Enable all") {
|
| 380 |
+
const onlyOne = this.properties?.[PROPERTY_RESTRICTION].includes(" one");
|
| 381 |
+
for (const [index, widget] of this.widgets.entries()) {
|
| 382 |
+
(widget as any)?.doModeChange(onlyOne && index > 0 ? false : true, true);
|
| 383 |
+
}
|
| 384 |
+
} else if (action === "Toggle all") {
|
| 385 |
+
const onlyOne = this.properties?.[PROPERTY_RESTRICTION].includes(" one");
|
| 386 |
+
let foundOne = false;
|
| 387 |
+
for (const [index, widget] of this.widgets.entries()) {
|
| 388 |
+
// If you have only one, then we'll stop at the first.
|
| 389 |
+
let newValue: boolean = onlyOne && foundOne ? false : !widget.value;
|
| 390 |
+
foundOne = foundOne || newValue;
|
| 391 |
+
(widget as any)?.doModeChange(newValue, true);
|
| 392 |
+
}
|
| 393 |
+
// And if you have always one, then we'll flip the last
|
| 394 |
+
if (!foundOne && this.properties?.[PROPERTY_RESTRICTION] === "always one") {
|
| 395 |
+
(this.widgets[this.widgets.length - 1] as any)?.doModeChange(true, true);
|
| 396 |
+
}
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
override getHelp() {
|
| 401 |
+
return `
|
| 402 |
+
<p>The ${this.type!.replace(
|
| 403 |
+
"(rgthree)",
|
| 404 |
+
"",
|
| 405 |
+
)} is an input-less node that automatically collects all groups in your current
|
| 406 |
+
workflow and allows you to quickly ${this.helpActions} all nodes within the group.</p>
|
| 407 |
+
<ul>
|
| 408 |
+
<li>
|
| 409 |
+
<p>
|
| 410 |
+
<strong>Properties.</strong> You can change the following properties (by right-clicking
|
| 411 |
+
on the node, and select "Properties" or "Properties Panel" from the menu):
|
| 412 |
+
</p>
|
| 413 |
+
<ul>
|
| 414 |
+
<li><p>
|
| 415 |
+
<code>${PROPERTY_MATCH_COLORS}</code> - Only add groups that match the provided
|
| 416 |
+
colors. Can be ComfyUI colors (red, pale_blue) or hex codes (#a4d399). Multiple can be
|
| 417 |
+
added, comma delimited.
|
| 418 |
+
</p></li>
|
| 419 |
+
<li><p>
|
| 420 |
+
<code>${PROPERTY_MATCH_TITLE}</code> - Filter the list of toggles by title match
|
| 421 |
+
(string match, or regular expression).
|
| 422 |
+
</p></li>
|
| 423 |
+
<li><p>
|
| 424 |
+
<code>${PROPERTY_SHOW_NAV}</code> - Add / remove a quick navigation arrow to take you
|
| 425 |
+
to the group. <i>(default: true)</i>
|
| 426 |
+
</p></li>
|
| 427 |
+
<li><p>
|
| 428 |
+
<code>${PROPERTY_SORT}</code> - Sort the toggles' order by "alphanumeric", graph
|
| 429 |
+
"position", or "custom alphabet". <i>(default: "position")</i>
|
| 430 |
+
</p></li>
|
| 431 |
+
<li>
|
| 432 |
+
<p>
|
| 433 |
+
<code>${PROPERTY_SORT_CUSTOM_ALPHA}</code> - When the
|
| 434 |
+
<code>${PROPERTY_SORT}</code> property is "custom alphabet" you can define the
|
| 435 |
+
alphabet to use here, which will match the <i>beginning</i> of each group name and
|
| 436 |
+
sort against it. If group titles do not match any custom alphabet entry, then they
|
| 437 |
+
will be put after groups that do, ordered alphanumerically.
|
| 438 |
+
</p>
|
| 439 |
+
<p>
|
| 440 |
+
This can be a list of single characters, like "zyxw..." or comma delimited strings
|
| 441 |
+
for more control, like "sdxl,pro,sd,n,p".
|
| 442 |
+
</p>
|
| 443 |
+
<p>
|
| 444 |
+
Note, when two group title match the same custom alphabet entry, the <i>normal
|
| 445 |
+
alphanumeric alphabet</i> breaks the tie. For instance, a custom alphabet of
|
| 446 |
+
"e,s,d" will order groups names like "SDXL, SEGS, Detailer" eventhough the custom
|
| 447 |
+
alphabet has an "e" before "d" (where one may expect "SE" to be before "SD").
|
| 448 |
+
</p>
|
| 449 |
+
<p>
|
| 450 |
+
To have "SEGS" appear before "SDXL" you can use longer strings. For instance, the
|
| 451 |
+
custom alphabet value of "se,s,f" would work here.
|
| 452 |
+
</p>
|
| 453 |
+
</li>
|
| 454 |
+
<li><p>
|
| 455 |
+
<code>${PROPERTY_RESTRICTION}</code> - Optionally, attempt to restrict the number of
|
| 456 |
+
widgets that can be enabled to a maximum of one, or always one.
|
| 457 |
+
</p>
|
| 458 |
+
<p><em><strong>Note:</strong> If using "max one" or "always one" then this is only
|
| 459 |
+
enforced when clicking a toggle on this node; if nodes within groups are changed
|
| 460 |
+
outside of the initial toggle click, then these restriction will not be enforced, and
|
| 461 |
+
could result in a state where more than one toggle is enabled. This could also happen
|
| 462 |
+
if nodes are overlapped with multiple groups.
|
| 463 |
+
</p></li>
|
| 464 |
+
|
| 465 |
+
</ul>
|
| 466 |
+
</li>
|
| 467 |
+
</ul>`;
|
| 468 |
+
}
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
/**
|
| 472 |
+
* Fast Bypasser implementation that looks for groups in the workflow and adds toggles to mute them.
|
| 473 |
+
*/
|
| 474 |
+
export class FastGroupsMuter extends BaseFastGroupsModeChanger {
|
| 475 |
+
static override type = NodeTypesString.FAST_GROUPS_MUTER;
|
| 476 |
+
static override title = NodeTypesString.FAST_GROUPS_MUTER;
|
| 477 |
+
override comfyClass = NodeTypesString.FAST_GROUPS_MUTER;
|
| 478 |
+
|
| 479 |
+
static override exposedActions = ["Bypass all", "Enable all", "Toggle all"];
|
| 480 |
+
|
| 481 |
+
protected override helpActions = "mute and unmute";
|
| 482 |
+
|
| 483 |
+
override readonly modeOn: number = LiteGraph.ALWAYS;
|
| 484 |
+
override readonly modeOff: number = LiteGraph.NEVER;
|
| 485 |
+
|
| 486 |
+
constructor(title = FastGroupsMuter.title) {
|
| 487 |
+
super(title);
|
| 488 |
+
this.onConstructed();
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
app.registerExtension({
|
| 493 |
+
name: "rgthree.FastGroupsMuter",
|
| 494 |
+
registerCustomNodes() {
|
| 495 |
+
FastGroupsMuter.setUp();
|
| 496 |
+
},
|
| 497 |
+
loadedGraphNode(node: LGraphNode) {
|
| 498 |
+
if (node.type == FastGroupsMuter.title) {
|
| 499 |
+
(node as FastGroupsMuter).tempSize = [...node.size];
|
| 500 |
+
}
|
| 501 |
+
},
|
| 502 |
+
});
|
rgthree-comfy/src_web/comfyui/feature_group_fast_toggle.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
LGraphCanvas as TLGraphCanvas,
|
| 3 |
+
LGraphGroup as TLGraphGroup,
|
| 4 |
+
LGraph as TLGraph,
|
| 5 |
+
AdjustedMouseEvent,
|
| 6 |
+
Vector2,
|
| 7 |
+
} from "typings/litegraph.js";
|
| 8 |
+
import type {AdjustedMouseCustomEvent} from "typings/rgthree.js";
|
| 9 |
+
|
| 10 |
+
import {app} from "scripts/app.js";
|
| 11 |
+
import {rgthree} from "./rgthree.js";
|
| 12 |
+
import {getOutputNodes} from "./utils.js";
|
| 13 |
+
import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js";
|
| 14 |
+
|
| 15 |
+
const BTN_SIZE = 20;
|
| 16 |
+
const BTN_MARGIN: Vector2 = [6, 6];
|
| 17 |
+
const BTN_SPACING = 8;
|
| 18 |
+
const BTN_GRID = BTN_SIZE / 8;
|
| 19 |
+
|
| 20 |
+
const TOGGLE_TO_MODE = new Map([
|
| 21 |
+
["MUTE", LiteGraph.NEVER],
|
| 22 |
+
["BYPASS", 4],
|
| 23 |
+
]);
|
| 24 |
+
|
| 25 |
+
function getToggles() {
|
| 26 |
+
return [...CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.toggles", [])].reverse();
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/**
|
| 30 |
+
* Determines if the user clicked on an fast header icon.
|
| 31 |
+
*/
|
| 32 |
+
function clickedOnToggleButton(e: AdjustedMouseEvent, group: TLGraphGroup): string | null {
|
| 33 |
+
const toggles = getToggles();
|
| 34 |
+
const pos = group.pos;
|
| 35 |
+
const size = group.size;
|
| 36 |
+
for (let i = 0; i < toggles.length; i++) {
|
| 37 |
+
const toggle = toggles[i];
|
| 38 |
+
if (
|
| 39 |
+
LiteGraph.isInsideRectangle(
|
| 40 |
+
e.canvasX,
|
| 41 |
+
e.canvasY,
|
| 42 |
+
pos[0] + size[0] - (BTN_SIZE + BTN_MARGIN[0]) * (i + 1),
|
| 43 |
+
pos[1] + BTN_MARGIN[1],
|
| 44 |
+
BTN_SIZE,
|
| 45 |
+
BTN_SIZE,
|
| 46 |
+
)
|
| 47 |
+
) {
|
| 48 |
+
return toggle;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
return null;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Registers the GroupHeaderToggles which places a mute and/or bypass icons in groups headers for
|
| 56 |
+
* quick, single-click ability to mute/bypass.
|
| 57 |
+
*/
|
| 58 |
+
app.registerExtension({
|
| 59 |
+
name: "rgthree.GroupHeaderToggles",
|
| 60 |
+
async setup() {
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* LiteGraph won't call `drawGroups` unless the canvas is dirty. Other nodes will do this, but
|
| 64 |
+
* in small workflows, we'll want to trigger it dirty so we can be drawn if we're in hover mode.
|
| 65 |
+
*/
|
| 66 |
+
setInterval(() => {
|
| 67 |
+
if (
|
| 68 |
+
CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled") &&
|
| 69 |
+
CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.show") !== "always"
|
| 70 |
+
) {
|
| 71 |
+
app.canvas.setDirty(true, true);
|
| 72 |
+
}
|
| 73 |
+
}, 250);
|
| 74 |
+
|
| 75 |
+
/**
|
| 76 |
+
* Handles a click on the icon area if the user has the extension enable from settings.
|
| 77 |
+
* Hooks into the already overriden mouse down processor from rgthree.
|
| 78 |
+
*/
|
| 79 |
+
rgthree.addEventListener("on-process-mouse-down", ((e: AdjustedMouseCustomEvent) => {
|
| 80 |
+
if (!CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled")) return;
|
| 81 |
+
|
| 82 |
+
const canvas = app.canvas as TLGraphCanvas;
|
| 83 |
+
if (canvas.selected_group) {
|
| 84 |
+
const originalEvent = e.detail.originalEvent;
|
| 85 |
+
const group = canvas.selected_group;
|
| 86 |
+
const clickedOnToggle = clickedOnToggleButton(originalEvent, group) || "";
|
| 87 |
+
const toggleAction = clickedOnToggle?.toLocaleUpperCase();
|
| 88 |
+
if (toggleAction) {
|
| 89 |
+
if (toggleAction === "QUEUE") {
|
| 90 |
+
const outputNodes = getOutputNodes(group._nodes);
|
| 91 |
+
if (!outputNodes?.length) {
|
| 92 |
+
rgthree.showMessage({
|
| 93 |
+
id: "no-output-in-group",
|
| 94 |
+
type: "warn",
|
| 95 |
+
timeout: 4000,
|
| 96 |
+
message: "No output nodes for group!",
|
| 97 |
+
});
|
| 98 |
+
} else {
|
| 99 |
+
rgthree.queueOutputNodes(outputNodes.map((n) => n.id));
|
| 100 |
+
}
|
| 101 |
+
} else {
|
| 102 |
+
const toggleMode = TOGGLE_TO_MODE.get(toggleAction);
|
| 103 |
+
if (toggleMode) {
|
| 104 |
+
group.recomputeInsideNodes();
|
| 105 |
+
const hasAnyActiveNodes = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS);
|
| 106 |
+
const isAllMuted =
|
| 107 |
+
!hasAnyActiveNodes && group._nodes.every((n) => n.mode === LiteGraph.NEVER);
|
| 108 |
+
const isAllBypassed =
|
| 109 |
+
!hasAnyActiveNodes && !isAllMuted && group._nodes.every((n) => n.mode === 4);
|
| 110 |
+
|
| 111 |
+
let newMode: 0 | 1 | 2 | 3 | 4 = LiteGraph.ALWAYS;
|
| 112 |
+
if (toggleMode === LiteGraph.NEVER) {
|
| 113 |
+
newMode = isAllMuted ? LiteGraph.ALWAYS : LiteGraph.NEVER;
|
| 114 |
+
} else {
|
| 115 |
+
newMode = isAllBypassed ? LiteGraph.ALWAYS : 4;
|
| 116 |
+
}
|
| 117 |
+
for (const node of group._nodes) {
|
| 118 |
+
node.mode = newMode;
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
// Make it such that we're not then moving the group on drag.
|
| 123 |
+
canvas.selected_group = null;
|
| 124 |
+
canvas.dragging_canvas = false;
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
}) as EventListener);
|
| 128 |
+
|
| 129 |
+
/**
|
| 130 |
+
* Overrides LiteGraph's Canvas method for drawingGroups and, after calling the original, checks
|
| 131 |
+
* that the user has enabled fast toggles and draws them on the top-right of the app..
|
| 132 |
+
*/
|
| 133 |
+
const drawGroups = LGraphCanvas.prototype.drawGroups;
|
| 134 |
+
LGraphCanvas.prototype.drawGroups = function (
|
| 135 |
+
canvasEl: HTMLCanvasElement,
|
| 136 |
+
ctx: CanvasRenderingContext2D,
|
| 137 |
+
) {
|
| 138 |
+
drawGroups.apply(this, [...arguments] as any);
|
| 139 |
+
|
| 140 |
+
if (
|
| 141 |
+
!CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled") ||
|
| 142 |
+
!rgthree.lastAdjustedMouseEvent
|
| 143 |
+
) {
|
| 144 |
+
return;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
const graph = app.graph as TLGraph;
|
| 148 |
+
|
| 149 |
+
let groups: TLGraphGroup[];
|
| 150 |
+
// Default to hover if not always.
|
| 151 |
+
if (CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.show") !== "always") {
|
| 152 |
+
const hoverGroup = graph.getGroupOnPos(
|
| 153 |
+
rgthree.lastAdjustedMouseEvent.canvasX,
|
| 154 |
+
rgthree.lastAdjustedMouseEvent.canvasY,
|
| 155 |
+
);
|
| 156 |
+
groups = hoverGroup ? [hoverGroup] : [];
|
| 157 |
+
} else {
|
| 158 |
+
groups = graph._groups || [];
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
if (!groups.length) {
|
| 162 |
+
return;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
const toggles = getToggles();
|
| 166 |
+
|
| 167 |
+
ctx.save();
|
| 168 |
+
for (const group of groups || []) {
|
| 169 |
+
let anyActive = false;
|
| 170 |
+
let allMuted = !!group._nodes.length;
|
| 171 |
+
let allBypassed = allMuted;
|
| 172 |
+
|
| 173 |
+
// Find the current state of the group's nodes.
|
| 174 |
+
for (const node of group._nodes) {
|
| 175 |
+
anyActive = anyActive || node.mode === LiteGraph.ALWAYS;
|
| 176 |
+
allMuted = allMuted && node.mode === LiteGraph.NEVER;
|
| 177 |
+
allBypassed = allBypassed && node.mode === 4;
|
| 178 |
+
if (anyActive || (!allMuted && !allBypassed)) {
|
| 179 |
+
break;
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
// Display each toggle.
|
| 184 |
+
for (let i = 0; i < toggles.length; i++) {
|
| 185 |
+
const toggle = toggles[i];
|
| 186 |
+
const pos = group._pos;
|
| 187 |
+
const size = group._size;
|
| 188 |
+
ctx.fillStyle = ctx.strokeStyle = group.color || "#335";
|
| 189 |
+
const x = pos[0] + size[0] - BTN_MARGIN[0] - BTN_SIZE - (BTN_SPACING + BTN_SIZE) * i;
|
| 190 |
+
const y = pos[1] + BTN_MARGIN[1];
|
| 191 |
+
const midX = x + BTN_SIZE / 2;
|
| 192 |
+
const midY = y + BTN_SIZE / 2;
|
| 193 |
+
if (toggle === "queue") {
|
| 194 |
+
const outputNodes = getOutputNodes(group._nodes);
|
| 195 |
+
const oldGlobalAlpha = ctx.globalAlpha;
|
| 196 |
+
if (!outputNodes?.length) {
|
| 197 |
+
ctx.globalAlpha = 0.5;
|
| 198 |
+
}
|
| 199 |
+
ctx.lineJoin = "round";
|
| 200 |
+
ctx.lineCap = "round";
|
| 201 |
+
const arrowSizeX = BTN_SIZE * 0.6;
|
| 202 |
+
const arrowSizeY = BTN_SIZE * 0.7;
|
| 203 |
+
const arrow = new Path2D(
|
| 204 |
+
`M ${x + arrowSizeX / 2} ${midY} l 0 -${arrowSizeY / 2} l ${arrowSizeX} ${arrowSizeY / 2} l -${arrowSizeX} ${arrowSizeY / 2} z`,
|
| 205 |
+
);
|
| 206 |
+
ctx.stroke(arrow);
|
| 207 |
+
if (outputNodes?.length) {
|
| 208 |
+
ctx.fill(arrow);
|
| 209 |
+
}
|
| 210 |
+
ctx.globalAlpha = oldGlobalAlpha;
|
| 211 |
+
} else {
|
| 212 |
+
const on = toggle === "bypass" ? allBypassed : allMuted;
|
| 213 |
+
|
| 214 |
+
ctx.beginPath();
|
| 215 |
+
ctx.lineJoin = "round";
|
| 216 |
+
ctx.rect(x, y, BTN_SIZE, BTN_SIZE);
|
| 217 |
+
|
| 218 |
+
ctx.lineWidth = 2;
|
| 219 |
+
if (toggle === "mute") {
|
| 220 |
+
ctx.lineJoin = "round";
|
| 221 |
+
ctx.lineCap = "round";
|
| 222 |
+
|
| 223 |
+
if (on) {
|
| 224 |
+
ctx.stroke(
|
| 225 |
+
new Path2D(`
|
| 226 |
+
${eyeFrame(midX, midY)}
|
| 227 |
+
${eyeLashes(midX, midY)}
|
| 228 |
+
`),
|
| 229 |
+
);
|
| 230 |
+
} else {
|
| 231 |
+
const radius = BTN_GRID * 1.5;
|
| 232 |
+
|
| 233 |
+
// Eyeball fill
|
| 234 |
+
ctx.fill(
|
| 235 |
+
new Path2D(`
|
| 236 |
+
${eyeFrame(midX, midY)}
|
| 237 |
+
${eyeFrame(midX, midY, -1)}
|
| 238 |
+
${circlePath(midX, midY, radius)}
|
| 239 |
+
${circlePath(midX + BTN_GRID / 2, midY - BTN_GRID / 2, BTN_GRID * 0.375)}
|
| 240 |
+
`),
|
| 241 |
+
"evenodd",
|
| 242 |
+
);
|
| 243 |
+
|
| 244 |
+
// Eye Outline Stroke
|
| 245 |
+
ctx.stroke(new Path2D(`${eyeFrame(midX, midY)} ${eyeFrame(midX, midY, -1)}`));
|
| 246 |
+
|
| 247 |
+
// Eye lashes (faded)
|
| 248 |
+
ctx.globalAlpha = this.editor_alpha * 0.5;
|
| 249 |
+
ctx.stroke(new Path2D(`${eyeLashes(midX, midY)} ${eyeLashes(midX, midY, -1)}`));
|
| 250 |
+
ctx.globalAlpha = this.editor_alpha;
|
| 251 |
+
}
|
| 252 |
+
} else {
|
| 253 |
+
const lineChanges = on
|
| 254 |
+
? `a ${BTN_GRID * 3}, ${BTN_GRID * 3} 0 1, 1 ${BTN_GRID * 3 * 2},0
|
| 255 |
+
l ${BTN_GRID * 2.0} 0`
|
| 256 |
+
: `l ${BTN_GRID * 8} 0`;
|
| 257 |
+
|
| 258 |
+
ctx.stroke(
|
| 259 |
+
new Path2D(`
|
| 260 |
+
M ${x} ${midY}
|
| 261 |
+
${lineChanges}
|
| 262 |
+
M ${x + BTN_SIZE} ${midY} l -2 2
|
| 263 |
+
M ${x + BTN_SIZE} ${midY} l -2 -2
|
| 264 |
+
`),
|
| 265 |
+
);
|
| 266 |
+
ctx.fill(new Path2D(`${circlePath(x + BTN_GRID * 3, midY, BTN_GRID * 1.8)}`));
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
ctx.restore();
|
| 272 |
+
};
|
| 273 |
+
},
|
| 274 |
+
});
|
| 275 |
+
|
| 276 |
+
function eyeFrame(midX: number, midY: number, yFlip = 1) {
|
| 277 |
+
return `
|
| 278 |
+
M ${midX - BTN_SIZE / 2} ${midY}
|
| 279 |
+
c ${BTN_GRID * 1.5} ${yFlip * BTN_GRID * 2.5}, ${BTN_GRID * (8 - 1.5)} ${
|
| 280 |
+
yFlip * BTN_GRID * 2.5
|
| 281 |
+
}, ${BTN_GRID * 8} 0
|
| 282 |
+
`;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
function eyeLashes(midX: number, midY: number, yFlip = 1) {
|
| 286 |
+
return `
|
| 287 |
+
M ${midX - BTN_GRID * 3.46} ${midY + yFlip * BTN_GRID * 0.9} l -1.15 ${1.25 * yFlip}
|
| 288 |
+
M ${midX - BTN_GRID * 2.38} ${midY + yFlip * BTN_GRID * 1.6} l -0.90 ${1.5 * yFlip}
|
| 289 |
+
M ${midX - BTN_GRID * 1.15} ${midY + yFlip * BTN_GRID * 1.95} l -0.50 ${1.75 * yFlip}
|
| 290 |
+
M ${midX + BTN_GRID * 0.0} ${midY + yFlip * BTN_GRID * 2.0} l 0.00 ${2.0 * yFlip}
|
| 291 |
+
M ${midX + BTN_GRID * 1.15} ${midY + yFlip * BTN_GRID * 1.95} l 0.50 ${1.75 * yFlip}
|
| 292 |
+
M ${midX + BTN_GRID * 2.38} ${midY + yFlip * BTN_GRID * 1.6} l 0.90 ${1.5 * yFlip}
|
| 293 |
+
M ${midX + BTN_GRID * 3.46} ${midY + yFlip * BTN_GRID * 0.9} l 1.15 ${1.25 * yFlip}
|
| 294 |
+
`;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
function circlePath(cx: number, cy: number, radius: number) {
|
| 298 |
+
return `
|
| 299 |
+
M ${cx} ${cy}
|
| 300 |
+
m ${radius}, 0
|
| 301 |
+
a ${radius},${radius} 0 1, 1 -${radius * 2},0
|
| 302 |
+
a ${radius},${radius} 0 1, 1 ${radius * 2},0
|
| 303 |
+
`;
|
| 304 |
+
}
|
rgthree-comfy/src_web/comfyui/feature_import_individual_nodes.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { tryToGetWorkflowDataFromEvent } from "rgthree/common/utils_workflow.js";
|
| 2 |
+
import { app } from "scripts/app.js";
|
| 3 |
+
import type { ComfyNode, ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
|
| 4 |
+
import { SERVICE as CONFIG_SERVICE } from "./services/config_service.js";
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Registers the GroupHeaderToggles which places a mute and/or bypass icons in groups headers for
|
| 8 |
+
* quick, single-click ability to mute/bypass.
|
| 9 |
+
*/
|
| 10 |
+
app.registerExtension({
|
| 11 |
+
name: "rgthree.ImportIndividualNodes",
|
| 12 |
+
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 13 |
+
const onDragOver = nodeType.prototype.onDragOver;
|
| 14 |
+
nodeType.prototype.onDragOver = function (e: DragEvent) {
|
| 15 |
+
let handled = onDragOver?.apply?.(this, [...arguments] as any);
|
| 16 |
+
if (handled != null) {
|
| 17 |
+
return handled;
|
| 18 |
+
}
|
| 19 |
+
return importIndividualNodesInnerOnDragOver(this, e);
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
const onDragDrop = nodeType.prototype.onDragDrop;
|
| 23 |
+
nodeType.prototype.onDragDrop = async function (e: DragEvent) {
|
| 24 |
+
const alreadyHandled = await onDragDrop?.apply?.(this, [...arguments] as any);
|
| 25 |
+
if (alreadyHandled) {
|
| 26 |
+
return alreadyHandled;
|
| 27 |
+
}
|
| 28 |
+
return importIndividualNodesInnerOnDragDrop(this, e);
|
| 29 |
+
};
|
| 30 |
+
},
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
export function importIndividualNodesInnerOnDragOver(node: ComfyNode, e: DragEvent): boolean {
|
| 34 |
+
return (
|
| 35 |
+
(node.widgets?.length && !!CONFIG_SERVICE.getFeatureValue("import_individual_nodes.enabled")) ||
|
| 36 |
+
false
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export async function importIndividualNodesInnerOnDragDrop(node: ComfyNode, e: DragEvent) {
|
| 41 |
+
if (!node.widgets?.length || !CONFIG_SERVICE.getFeatureValue("import_individual_nodes.enabled")) {
|
| 42 |
+
return false;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
let handled = false;
|
| 46 |
+
const { workflow, prompt } = await tryToGetWorkflowDataFromEvent(e);
|
| 47 |
+
if (!handled && workflow) {
|
| 48 |
+
const exact = (workflow.nodes || []).find((n) => n.id === node.id && n.type === node.type);
|
| 49 |
+
if (
|
| 50 |
+
exact?.widgets_values?.length &&
|
| 51 |
+
confirm(
|
| 52 |
+
"Found a node match from embedded workflow (same id & type) in this workflow. Would you like to set the widget values?",
|
| 53 |
+
)
|
| 54 |
+
) {
|
| 55 |
+
node.configure({
|
| 56 |
+
// Title is overridden if it's not supplied; set it to the current then.
|
| 57 |
+
title: node.title,
|
| 58 |
+
widgets_values: [...(exact?.widgets_values || [])]
|
| 59 |
+
} as any);
|
| 60 |
+
handled = true;
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
if (!handled && workflow) {
|
| 64 |
+
handled = !confirm(
|
| 65 |
+
"No exact match found in workflow. Would you like to replace the whole workflow?",
|
| 66 |
+
);
|
| 67 |
+
}
|
| 68 |
+
return handled;
|
| 69 |
+
}
|
rgthree-comfy/src_web/comfyui/image_comparer.ts
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { app } from "scripts/app.js";
|
| 2 |
+
import { api } from "scripts/api.js";
|
| 3 |
+
import { RgthreeBaseServerNode } from "./base_node.js";
|
| 4 |
+
import { NodeTypesString } from "./constants.js";
|
| 5 |
+
import { ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
|
| 6 |
+
import {
|
| 7 |
+
AdjustedMouseEvent,
|
| 8 |
+
LGraphCanvas,
|
| 9 |
+
LGraphNode,
|
| 10 |
+
SerializedLGraphNode,
|
| 11 |
+
Vector2,
|
| 12 |
+
} from "typings/litegraph.js";
|
| 13 |
+
import { addConnectionLayoutSupport } from "./utils.js";
|
| 14 |
+
import {
|
| 15 |
+
RgthreeBaseHitAreas,
|
| 16 |
+
RgthreeBaseWidget,
|
| 17 |
+
RgthreeBaseWidgetBounds,
|
| 18 |
+
} from "./utils_widgets.js";
|
| 19 |
+
import { measureText } from "./utils_canvas.js";
|
| 20 |
+
|
| 21 |
+
type ComfyImageServerData = { filename: string; type: string; subfolder: string };
|
| 22 |
+
type ComfyImageData = { name: string; selected: boolean; url: string; img?: HTMLImageElement };
|
| 23 |
+
type OldExecutedPayload = {
|
| 24 |
+
images: ComfyImageServerData[];
|
| 25 |
+
};
|
| 26 |
+
type ExecutedPayload = {
|
| 27 |
+
a_images?: ComfyImageServerData[];
|
| 28 |
+
b_images?: ComfyImageServerData[];
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
function imageDataToUrl(data: ComfyImageServerData) {
|
| 32 |
+
return api.apiURL(
|
| 33 |
+
`/view?filename=${encodeURIComponent(data.filename)}&type=${data.type}&subfolder=${
|
| 34 |
+
data.subfolder
|
| 35 |
+
}${app.getPreviewFormatParam()}${app.getRandParam()}`,
|
| 36 |
+
);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Compares two images in one canvas node.
|
| 41 |
+
*/
|
| 42 |
+
export class RgthreeImageComparer extends RgthreeBaseServerNode {
|
| 43 |
+
static override title = NodeTypesString.IMAGE_COMPARER;
|
| 44 |
+
static override type = NodeTypesString.IMAGE_COMPARER;
|
| 45 |
+
static comfyClass = NodeTypesString.IMAGE_COMPARER;
|
| 46 |
+
|
| 47 |
+
// These is what the core preview image node uses to show the context menu. May not be that helpful
|
| 48 |
+
// since it likely will always be "0" when a context menu is invoked without manually changing
|
| 49 |
+
// something.
|
| 50 |
+
imageIndex: number = 0;
|
| 51 |
+
imgs: InstanceType<typeof Image>[] = [];
|
| 52 |
+
|
| 53 |
+
override serialize_widgets = true;
|
| 54 |
+
|
| 55 |
+
isPointerDown = false;
|
| 56 |
+
isPointerOver = false;
|
| 57 |
+
pointerOverPos: Vector2 = [0, 0];
|
| 58 |
+
|
| 59 |
+
private canvasWidget: RgthreeImageComparerWidget | null = null;
|
| 60 |
+
|
| 61 |
+
static "@comparer_mode" = {
|
| 62 |
+
type: "combo",
|
| 63 |
+
values: ["Slide", "Click"],
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
constructor(title = RgthreeImageComparer.title) {
|
| 67 |
+
super(title);
|
| 68 |
+
this.properties["comparer_mode"] = "Slide";
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
override onExecuted(output: ExecutedPayload | OldExecutedPayload) {
|
| 72 |
+
super.onExecuted?.(output);
|
| 73 |
+
if ("images" in output) {
|
| 74 |
+
this.canvasWidget!.value = {
|
| 75 |
+
images: (output.images || []).map((d, i) => {
|
| 76 |
+
return {
|
| 77 |
+
name: i === 0 ? "A" : "B",
|
| 78 |
+
selected: true,
|
| 79 |
+
url: imageDataToUrl(d),
|
| 80 |
+
};
|
| 81 |
+
}),
|
| 82 |
+
};
|
| 83 |
+
} else {
|
| 84 |
+
output.a_images = output.a_images || [];
|
| 85 |
+
output.b_images = output.b_images || [];
|
| 86 |
+
const imagesToChoose: ComfyImageData[] = [];
|
| 87 |
+
const multiple = output.a_images.length + output.b_images.length > 2;
|
| 88 |
+
for (const [i, d] of output.a_images.entries()) {
|
| 89 |
+
imagesToChoose.push({
|
| 90 |
+
name: output.a_images.length > 1 || multiple ? `A${i + 1}` : "A",
|
| 91 |
+
selected: i === 0,
|
| 92 |
+
url: imageDataToUrl(d),
|
| 93 |
+
});
|
| 94 |
+
}
|
| 95 |
+
for (const [i, d] of output.b_images.entries()) {
|
| 96 |
+
imagesToChoose.push({
|
| 97 |
+
name: output.b_images.length > 1 || multiple ? `B${i + 1}` : "B",
|
| 98 |
+
selected: i === 0,
|
| 99 |
+
url: imageDataToUrl(d),
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
this.canvasWidget!.value = { images: imagesToChoose };
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
override onSerialize(o: SerializedLGraphNode) {
|
| 107 |
+
super.onSerialize && super.onSerialize(o);
|
| 108 |
+
for (let [index, widget_value] of (o.widgets_values || []).entries()) {
|
| 109 |
+
if (this.widgets[index]?.name === "rgthree_comparer") {
|
| 110 |
+
o.widgets_values![index] = (
|
| 111 |
+
this.widgets[index] as RgthreeImageComparerWidget
|
| 112 |
+
).value.images.map((d) => {
|
| 113 |
+
d = { ...d };
|
| 114 |
+
delete d.img;
|
| 115 |
+
return d;
|
| 116 |
+
});
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
override onNodeCreated() {
|
| 122 |
+
this.canvasWidget = this.addCustomWidget(
|
| 123 |
+
new RgthreeImageComparerWidget("rgthree_comparer", this),
|
| 124 |
+
);
|
| 125 |
+
this.setSize(this.computeSize());
|
| 126 |
+
this.setDirtyCanvas(true, true);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/**
|
| 130 |
+
* Sets mouse as down or up based on param. If it's down, we also loop to check pointer is still
|
| 131 |
+
* down. This is because LiteGraph doesn't fire `onMouseUp` every time there's a mouse up, so we
|
| 132 |
+
* need to manually monitor `pointer_is_down` and, when it's no longer true, set mouse as up here.
|
| 133 |
+
*/
|
| 134 |
+
private setIsPointerDown(down: boolean = this.isPointerDown) {
|
| 135 |
+
const newIsDown = down && !!app.canvas.pointer_is_down;
|
| 136 |
+
if (this.isPointerDown !== newIsDown) {
|
| 137 |
+
this.isPointerDown = newIsDown;
|
| 138 |
+
this.setDirtyCanvas(true, false);
|
| 139 |
+
}
|
| 140 |
+
this.imageIndex = this.isPointerDown ? 1 : 0;
|
| 141 |
+
if (this.isPointerDown) {
|
| 142 |
+
requestAnimationFrame(() => {
|
| 143 |
+
this.setIsPointerDown();
|
| 144 |
+
});
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
override onMouseDown(event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas): void {
|
| 149 |
+
super.onMouseDown?.(event, pos, graphCanvas);
|
| 150 |
+
this.setIsPointerDown(true);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
override onMouseEnter(event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas): void {
|
| 154 |
+
super.onMouseEnter?.(event, pos, graphCanvas);
|
| 155 |
+
this.setIsPointerDown(!!app.canvas.pointer_is_down);
|
| 156 |
+
this.isPointerOver = true;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
override onMouseLeave(event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas): void {
|
| 160 |
+
super.onMouseLeave?.(event, pos, graphCanvas);
|
| 161 |
+
this.setIsPointerDown(false);
|
| 162 |
+
this.isPointerOver = false;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
override onMouseMove(event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas): void {
|
| 166 |
+
super.onMouseMove?.(event, pos, graphCanvas);
|
| 167 |
+
this.pointerOverPos = [...pos];
|
| 168 |
+
this.imageIndex = this.pointerOverPos[0] > this.size[0] / 2 ? 1 : 0;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
override getHelp(): string {
|
| 172 |
+
return `
|
| 173 |
+
<p>
|
| 174 |
+
The ${this.type!.replace("(rgthree)", "")} node compares two images on top of each other.
|
| 175 |
+
</p>
|
| 176 |
+
<ul>
|
| 177 |
+
<li>
|
| 178 |
+
<p>
|
| 179 |
+
<strong>Notes</strong>
|
| 180 |
+
</p>
|
| 181 |
+
<ul>
|
| 182 |
+
<li><p>
|
| 183 |
+
The right-click menu may show image options (Open Image, Save Image, etc.) which will
|
| 184 |
+
correspond to the first image (image_a) if clicked on the left-half of the node, or
|
| 185 |
+
the second image if on the right half of the node.
|
| 186 |
+
</p></li>
|
| 187 |
+
</ul>
|
| 188 |
+
</li>
|
| 189 |
+
<li>
|
| 190 |
+
<p>
|
| 191 |
+
<strong>Inputs</strong>
|
| 192 |
+
</p>
|
| 193 |
+
<ul>
|
| 194 |
+
<li><p>
|
| 195 |
+
<code>image_a</code> <i>Optional.</i> The first image to use to compare.
|
| 196 |
+
image_a.
|
| 197 |
+
</p></li>
|
| 198 |
+
<li><p>
|
| 199 |
+
<code>image_b</code> <i>Optional.</i> The second image to use to compare.
|
| 200 |
+
</p></li>
|
| 201 |
+
<li><p>
|
| 202 |
+
<b>Note</b> <code>image_a</code> and <code>image_b</code> work best when a single
|
| 203 |
+
image is provided. However, if each/either are a batch, you can choose which item
|
| 204 |
+
from each batch are chosen to be compared. If either <code>image_a</code> or
|
| 205 |
+
<code>image_b</code> are not provided, the node will choose the first two from the
|
| 206 |
+
provided input if it's a batch, otherwise only show the single image (just as
|
| 207 |
+
Preview Image would).
|
| 208 |
+
</p></li>
|
| 209 |
+
</ul>
|
| 210 |
+
</li>
|
| 211 |
+
<li>
|
| 212 |
+
<p>
|
| 213 |
+
<strong>Properties.</strong> You can change the following properties (by right-clicking
|
| 214 |
+
on the node, and select "Properties" or "Properties Panel" from the menu):
|
| 215 |
+
</p>
|
| 216 |
+
<ul>
|
| 217 |
+
<li><p>
|
| 218 |
+
<code>comparer_mode</code> - Choose between "Slide" and "Click". Defaults to "Slide".
|
| 219 |
+
</p></li>
|
| 220 |
+
</ul>
|
| 221 |
+
</li>
|
| 222 |
+
</ul>`;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 226 |
+
RgthreeBaseServerNode.registerForOverride(comfyClass, nodeData, RgthreeImageComparer);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
static override onRegisteredForOverride(comfyClass: any) {
|
| 230 |
+
addConnectionLayoutSupport(RgthreeImageComparer, app, [
|
| 231 |
+
["Left", "Right"],
|
| 232 |
+
["Right", "Left"],
|
| 233 |
+
]);
|
| 234 |
+
setTimeout(() => {
|
| 235 |
+
RgthreeImageComparer.category = comfyClass.category;
|
| 236 |
+
});
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
type RgthreeImageComparerWidgetValue = {
|
| 241 |
+
images: ComfyImageData[];
|
| 242 |
+
};
|
| 243 |
+
|
| 244 |
+
class RgthreeImageComparerWidget extends RgthreeBaseWidget<RgthreeImageComparerWidgetValue> {
|
| 245 |
+
private node: RgthreeImageComparer;
|
| 246 |
+
|
| 247 |
+
protected override hitAreas: RgthreeBaseHitAreas<any> = {
|
| 248 |
+
// We dynamically set this when/if we draw the labels.
|
| 249 |
+
};
|
| 250 |
+
|
| 251 |
+
private selected: [ComfyImageData?, ComfyImageData?] = [];
|
| 252 |
+
|
| 253 |
+
constructor(name: string, node: RgthreeImageComparer) {
|
| 254 |
+
super(name);
|
| 255 |
+
this.node = node;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
private _value: RgthreeImageComparerWidgetValue = { images: [] };
|
| 259 |
+
|
| 260 |
+
set value(v: RgthreeImageComparerWidgetValue) {
|
| 261 |
+
// Despite `v` typed as RgthreeImageComparerWidgetValue, we may have gotten an array of strings
|
| 262 |
+
// from previous versions. We can handle that gracefully.
|
| 263 |
+
let cleanedVal;
|
| 264 |
+
if (Array.isArray(v)) {
|
| 265 |
+
cleanedVal = v.map((d, i) => {
|
| 266 |
+
if (!d || typeof d === "string") {
|
| 267 |
+
// We usually only have two here, so they're selected.
|
| 268 |
+
d = { url: d, name: i == 0 ? "A" : "B", selected: true };
|
| 269 |
+
}
|
| 270 |
+
return d;
|
| 271 |
+
});
|
| 272 |
+
} else {
|
| 273 |
+
cleanedVal = v.images || [];
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// If we have multiple items in our sent value but we don't have both an "A" and a "B" then
|
| 277 |
+
// just simplify it down to the first two in the list.
|
| 278 |
+
if (cleanedVal.length > 2) {
|
| 279 |
+
const hasAAndB =
|
| 280 |
+
cleanedVal.some((i) => i.name.startsWith("A")) &&
|
| 281 |
+
cleanedVal.some((i) => i.name.startsWith("B"));
|
| 282 |
+
if (!hasAAndB) {
|
| 283 |
+
cleanedVal = [cleanedVal[0], cleanedVal[1]];
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
let selected = cleanedVal.filter((d) => d.selected);
|
| 288 |
+
// None are selected.
|
| 289 |
+
if (!selected.length && cleanedVal.length) {
|
| 290 |
+
cleanedVal[0]!.selected = true;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
selected = cleanedVal.filter((d) => d.selected);
|
| 294 |
+
if (selected.length === 1 && cleanedVal.length > 1) {
|
| 295 |
+
cleanedVal.find((d) => !d.selected)!.selected = true;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
this._value.images = cleanedVal;
|
| 299 |
+
|
| 300 |
+
selected = cleanedVal.filter((d) => d.selected);
|
| 301 |
+
this.setSelected(selected as [ComfyImageData, ComfyImageData]);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
get value() {
|
| 305 |
+
return this._value;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
setSelected(selected: [ComfyImageData, ComfyImageData]) {
|
| 309 |
+
this._value.images.forEach((d) => (d.selected = false));
|
| 310 |
+
this.node.imgs.length = 0;
|
| 311 |
+
for (const sel of selected) {
|
| 312 |
+
if (!sel.img) {
|
| 313 |
+
sel.img = new Image();
|
| 314 |
+
sel.img.src = sel.url;
|
| 315 |
+
this.node.imgs.push(sel.img);
|
| 316 |
+
}
|
| 317 |
+
sel.selected = true;
|
| 318 |
+
}
|
| 319 |
+
this.selected = selected;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
draw(ctx: CanvasRenderingContext2D, node: RgthreeImageComparer, width: number, y: number) {
|
| 323 |
+
this.hitAreas = {};
|
| 324 |
+
if (this.value.images.length > 2) {
|
| 325 |
+
ctx.textAlign = "left";
|
| 326 |
+
ctx.textBaseline = "top";
|
| 327 |
+
ctx.font = `14px Arial`;
|
| 328 |
+
// Let's calculate the widths of all the labels.
|
| 329 |
+
const drawData: any = [];
|
| 330 |
+
const spacing = 5;
|
| 331 |
+
let x = 0;
|
| 332 |
+
for (const img of this.value.images) {
|
| 333 |
+
const width = measureText(ctx, img.name);
|
| 334 |
+
drawData.push({
|
| 335 |
+
img,
|
| 336 |
+
text: img.name,
|
| 337 |
+
x,
|
| 338 |
+
width: measureText(ctx, img.name),
|
| 339 |
+
});
|
| 340 |
+
x += width + spacing;
|
| 341 |
+
}
|
| 342 |
+
x = (node.size[0] - (x - spacing)) / 2;
|
| 343 |
+
for (const d of drawData) {
|
| 344 |
+
ctx.fillStyle = d.img.selected ? "rgba(180, 180, 180, 1)" : "rgba(180, 180, 180, 0.5)";
|
| 345 |
+
ctx.fillText(d.text, x, y);
|
| 346 |
+
this.hitAreas[d.text] = {
|
| 347 |
+
bounds: [x, y, d.width, 14],
|
| 348 |
+
data: d.img,
|
| 349 |
+
onDown: this.onSelectionDown,
|
| 350 |
+
};
|
| 351 |
+
x += d.width + spacing;
|
| 352 |
+
}
|
| 353 |
+
y += 20;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
if (node.properties?.["comparer_mode"] === "Click") {
|
| 357 |
+
this.drawImage(ctx, this.selected[this.node.isPointerDown ? 1 : 0], y);
|
| 358 |
+
} else {
|
| 359 |
+
this.drawImage(ctx, this.selected[0], y);
|
| 360 |
+
if (node.isPointerOver) {
|
| 361 |
+
this.drawImage(ctx, this.selected[1], y, this.node.pointerOverPos[0]);
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
private onSelectionDown(
|
| 367 |
+
event: AdjustedMouseEvent,
|
| 368 |
+
pos: Vector2,
|
| 369 |
+
node: LGraphNode,
|
| 370 |
+
bounds?: RgthreeBaseWidgetBounds,
|
| 371 |
+
) {
|
| 372 |
+
const selected = [...this.selected];
|
| 373 |
+
if (bounds?.data.name.startsWith("A")) {
|
| 374 |
+
selected[0] = bounds.data;
|
| 375 |
+
} else if (bounds?.data.name.startsWith("B")) {
|
| 376 |
+
selected[1] = bounds.data;
|
| 377 |
+
}
|
| 378 |
+
this.setSelected(selected as [ComfyImageData, ComfyImageData]);
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
private drawImage(
|
| 382 |
+
ctx: CanvasRenderingContext2D,
|
| 383 |
+
image: ComfyImageData | undefined,
|
| 384 |
+
y: number,
|
| 385 |
+
cropX?: number,
|
| 386 |
+
) {
|
| 387 |
+
if (!image?.img?.naturalWidth || !image?.img?.naturalHeight) {
|
| 388 |
+
return;
|
| 389 |
+
}
|
| 390 |
+
let [nodeWidth, nodeHeight] = this.node.size;
|
| 391 |
+
const imageAspect = image?.img.naturalWidth / image?.img.naturalHeight;
|
| 392 |
+
let height = nodeHeight - y;
|
| 393 |
+
const widgetAspect = nodeWidth / height;
|
| 394 |
+
let targetWidth, targetHeight;
|
| 395 |
+
let offsetX = 0;
|
| 396 |
+
if (imageAspect > widgetAspect) {
|
| 397 |
+
targetWidth = nodeWidth;
|
| 398 |
+
targetHeight = nodeWidth / imageAspect;
|
| 399 |
+
} else {
|
| 400 |
+
targetHeight = height;
|
| 401 |
+
targetWidth = height * imageAspect;
|
| 402 |
+
offsetX = (nodeWidth - targetWidth) / 2;
|
| 403 |
+
}
|
| 404 |
+
const widthMultiplier = image?.img.naturalWidth / targetWidth;
|
| 405 |
+
|
| 406 |
+
const sourceX = 0;
|
| 407 |
+
const sourceY = 0;
|
| 408 |
+
const sourceWidth =
|
| 409 |
+
cropX != null ? (cropX - offsetX) * widthMultiplier : image?.img.naturalWidth;
|
| 410 |
+
const sourceHeight = image?.img.naturalHeight;
|
| 411 |
+
const destX = (nodeWidth - targetWidth) / 2;
|
| 412 |
+
const destY = y + (height - targetHeight) / 2;
|
| 413 |
+
const destWidth = cropX != null ? cropX - offsetX : targetWidth;
|
| 414 |
+
const destHeight = targetHeight;
|
| 415 |
+
ctx.save();
|
| 416 |
+
ctx.beginPath();
|
| 417 |
+
let globalCompositeOperation = ctx.globalCompositeOperation;
|
| 418 |
+
if (cropX) {
|
| 419 |
+
ctx.rect(destX, destY, destWidth, destHeight);
|
| 420 |
+
ctx.clip();
|
| 421 |
+
}
|
| 422 |
+
ctx.drawImage(
|
| 423 |
+
image?.img,
|
| 424 |
+
sourceX,
|
| 425 |
+
sourceY,
|
| 426 |
+
sourceWidth,
|
| 427 |
+
sourceHeight,
|
| 428 |
+
destX,
|
| 429 |
+
destY,
|
| 430 |
+
destWidth,
|
| 431 |
+
destHeight,
|
| 432 |
+
);
|
| 433 |
+
// Shows a label overlayed on the image. Not perfect, keeping commented out.
|
| 434 |
+
// ctx.globalCompositeOperation = "difference";
|
| 435 |
+
// ctx.fillStyle = "rgba(180, 180, 180, 1)";
|
| 436 |
+
// ctx.textAlign = "center";
|
| 437 |
+
// ctx.font = `32px Arial`;
|
| 438 |
+
// ctx.fillText(image.name, nodeWidth / 2, y + 32);
|
| 439 |
+
if (cropX != null && cropX >= (nodeWidth - targetWidth) / 2 && cropX <= targetWidth + offsetX) {
|
| 440 |
+
ctx.beginPath();
|
| 441 |
+
ctx.moveTo(cropX, destY);
|
| 442 |
+
ctx.lineTo(cropX, destY + destHeight);
|
| 443 |
+
ctx.globalCompositeOperation = "difference";
|
| 444 |
+
ctx.strokeStyle = "rgba(255,255,255, 1)";
|
| 445 |
+
ctx.stroke();
|
| 446 |
+
}
|
| 447 |
+
ctx.globalCompositeOperation = globalCompositeOperation;
|
| 448 |
+
ctx.restore();
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
computeSize(width: number): Vector2 {
|
| 452 |
+
return [width, 20];
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
serializeValue(serializedNode: SerializedLGraphNode, widgetIndex: number) {
|
| 456 |
+
const v = [];
|
| 457 |
+
for (const data of this._value.images) {
|
| 458 |
+
// Remove the img since it can't serialize.
|
| 459 |
+
const d = { ...data };
|
| 460 |
+
delete d.img;
|
| 461 |
+
v.push(d);
|
| 462 |
+
}
|
| 463 |
+
return { images: v };
|
| 464 |
+
}
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
app.registerExtension({
|
| 468 |
+
name: "rgthree.ImageComparer",
|
| 469 |
+
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
|
| 470 |
+
if (nodeData.name === RgthreeImageComparer.type) {
|
| 471 |
+
RgthreeImageComparer.setUp(nodeType, nodeData);
|
| 472 |
+
}
|
| 473 |
+
},
|
| 474 |
+
});
|