├── .gitignore
├── LICENSE
├── MultiAreaConditioning.py
├── MultiLatentComposite.py
├── README.md
├── __init__.py
├── images
├── ConditioningUpscale_node.png
├── MultiAreaConditioning_node.png
├── MultiAreaConditioning_result.png
├── MultiAreaConditioning_workflow.svg
├── MultiLatentComposite_node.png
├── MultiLatentComposite_result.png
├── MultiLatentComposite_workflow.svg
└── RightClickMenu.png
└── javascript
├── MultiAreaConditioning.js
├── MultiLatentComposite.js
└── utils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | civitai/
2 | models/
3 |
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GLWT(Good Luck With That) Public License
2 | Copyright (c) Everyone, except Author
3 |
4 | Everyone is permitted to copy, distribute, modify, merge, sell, publish,
5 | sublicense or whatever they want with this software but at their OWN RISK.
6 |
7 | Preamble
8 |
9 | The author has absolutely no clue what the code in this project does.
10 | It might just work or not, there is no third option.
11 |
12 |
13 | GOOD LUCK WITH THAT PUBLIC LICENSE
14 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION, AND MODIFICATION
15 |
16 | 0. You just DO WHATEVER YOU WANT TO as long as you NEVER LEAVE A
17 | TRACE TO TRACK THE AUTHOR of the original product to blame for or hold
18 | responsible.
19 |
20 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 | DEALINGS IN THE SOFTWARE.
24 |
25 | Good luck and Godspeed.
26 |
--------------------------------------------------------------------------------
/MultiAreaConditioning.py:
--------------------------------------------------------------------------------
1 | # Made by Davemane42#0042 for ComfyUI
2 | # 02/04/2023
3 |
4 | import torch
5 | from nodes import MAX_RESOLUTION
6 |
7 | class MultiAreaConditioning:
8 | def __init__(self) -> None:
9 | pass
10 |
11 | @classmethod
12 | def INPUT_TYPES(cls):
13 | return {
14 | "required": {
15 | "conditioning0": ("CONDITIONING", ),
16 | "conditioning1": ("CONDITIONING", )
17 | },
18 | "hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"},
19 | }
20 |
21 |
22 |
23 | RETURN_TYPES = ("CONDITIONING", "INT", "INT")
24 | RETURN_NAMES = (None, "resolutionX", "resolutionY")
25 | FUNCTION = "doStuff"
26 | CATEGORY = "Davemane42"
27 |
28 | def doStuff(self, extra_pnginfo, unique_id, **kwargs):
29 |
30 | c = []
31 | values = []
32 | resolutionX = 512
33 | resolutionY = 512
34 |
35 | for node in extra_pnginfo["workflow"]["nodes"]:
36 | if node["id"] == int(unique_id):
37 | values = node["properties"]["values"]
38 | resolutionX = node["properties"]["width"]
39 | resolutionY = node["properties"]["height"]
40 | break
41 | k = 0
42 | for arg in kwargs:
43 | if k > len(values): break;
44 | if not torch.is_tensor(kwargs[arg][0][0]): continue;
45 |
46 | x, y = values[k][0], values[k][1]
47 | w, h = values[k][2], values[k][3]
48 |
49 | # If fullscreen
50 | if (x == 0 and y == 0 and w == resolutionX and h == resolutionY):
51 | for t in kwargs[arg]:
52 | c.append(t)
53 | k += 1
54 | continue
55 |
56 | if x+w > resolutionX:
57 | w = max(0, resolutionX-x)
58 |
59 | if y+h > resolutionY:
60 | h = max(0, resolutionY-y)
61 |
62 | if w == 0 or h == 0: continue;
63 |
64 | for t in kwargs[arg]:
65 | n = [t[0], t[1].copy()]
66 | n[1]['area'] = (h // 8, w // 8, y // 8, x // 8)
67 | n[1]['strength'] = values[k][4]
68 | n[1]['min_sigma'] = 0.0
69 | n[1]['max_sigma'] = 99.0
70 |
71 | c.append(n)
72 |
73 | k += 1
74 |
75 |
76 | return (c, resolutionX, resolutionY)
77 |
78 | class ConditioningUpscale():
79 | def __init__(self) -> None:
80 | pass
81 |
82 | @classmethod
83 | def INPUT_TYPES(s):
84 | return {
85 | "required": {
86 | "conditioning": ("CONDITIONING", ),
87 | "scalar": ("INT", {"default": 2, "min": 1, "max": 100, "step": 0.5}),
88 | },
89 | }
90 |
91 | RETURN_TYPES = ("CONDITIONING",)
92 | CATEGORY = "Davemane42"
93 |
94 | FUNCTION = 'upscale'
95 |
96 | def upscale(self, conditioning, scalar):
97 | c = []
98 | for t in conditioning:
99 |
100 | n = [t[0], t[1].copy()]
101 | if 'area' in n[1]:
102 |
103 | n[1]['area'] = tuple(map(lambda x: ((x*scalar + 7) >> 3) << 3, n[1]['area']))
104 |
105 | c.append(n)
106 |
107 | return (c, )
108 |
109 | class ConditioningStretch():
110 | def __init__(self) -> None:
111 | pass
112 |
113 | @classmethod
114 | def INPUT_TYPES(s):
115 | return {
116 | "required": {
117 | "conditioning": ("CONDITIONING", ),
118 | "resolutionX": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
119 | "resolutionY": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
120 | "newWidth": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
121 | "newHeight": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
122 | #"scalar": ("INT", {"default": 2, "min": 1, "max": 100, "step": 0.5}),
123 | },
124 | }
125 |
126 | RETURN_TYPES = ("CONDITIONING",)
127 | CATEGORY = "Davemane42"
128 |
129 | FUNCTION = 'upscale'
130 |
131 | def upscale(self, conditioning, resolutionX, resolutionY, newWidth, newHeight, scalar=1):
132 | c = []
133 | for t in conditioning:
134 |
135 | n = [t[0], t[1].copy()]
136 | if 'area' in n[1]:
137 |
138 | newWidth *= scalar
139 | newHeight *= scalar
140 |
141 | #n[1]['area'] = tuple(map(lambda x: ((x*scalar + 32) >> 6) << 6, n[1]['area']))
142 | x = ((n[1]['area'][3]*8)*newWidth/resolutionX) // 8
143 | y = ((n[1]['area'][2]*8)*newHeight/resolutionY) // 8
144 | w = ((n[1]['area'][1]*8)*newWidth/resolutionX) // 8
145 | h = ((n[1]['area'][0]*8)*newHeight/resolutionY) // 8
146 |
147 | n[1]['area'] = tuple(map(lambda x: (((int(x) + 7) >> 3) << 3), [h, w, y, x]))
148 |
149 | c.append(n)
150 |
151 | return (c, )
152 |
153 | class ConditioningDebug():
154 | def __init__(self) -> None:
155 | pass
156 |
157 | @classmethod
158 | def INPUT_TYPES(s):
159 | return {
160 | "required": {
161 | "conditioning": ("CONDITIONING", ),
162 | }
163 | }
164 |
165 | RETURN_TYPES = ()
166 | FUNCTION = "debug"
167 |
168 | OUTPUT_NODE = True
169 |
170 | CATEGORY = "Davemane42"
171 |
172 | def debug(self, conditioning):
173 | print("\nDebug")
174 | for i, t in enumerate(conditioning):
175 | print(f"{i}:")
176 | if "area" in t[1]:
177 | print(f"\tx{t[1]['area'][3]*8} y{t[1]['area'][2]*8} \n\tw{t[1]['area'][1]*8} h{t[1]['area'][0]*8} \n\tstrength: {t[1]['strength']}")
178 | else:
179 | print(f"\tFullscreen")
180 |
181 | return (None, )
--------------------------------------------------------------------------------
/MultiLatentComposite.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 | class MultiLatentComposite:
4 | @classmethod
5 | def INPUT_TYPES(s):
6 | return {
7 | "required": {
8 | "samples_to": ("LATENT",),
9 | "samples_from0": ("LATENT",),
10 | },
11 | "hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"},
12 | }
13 | RETURN_TYPES = ("LATENT",)
14 | FUNCTION = "composite"
15 |
16 | CATEGORY = "Davemane42"
17 |
18 | def composite(self, samples_to, extra_pnginfo, unique_id, **kwargs):
19 |
20 | values = []
21 |
22 | for node in extra_pnginfo["workflow"]["nodes"]:
23 | if node["id"] == int(unique_id):
24 | values = node["properties"]["values"]
25 | break
26 |
27 |
28 | samples_out = samples_to.copy()
29 | s = samples_to["samples"].clone()
30 | samples_to = samples_to["samples"]
31 |
32 | k = 0
33 | for arg in kwargs:
34 | if k > len(values): break;
35 |
36 | x = values[k][0] // 8
37 | y = values[k][1] // 8
38 | feather = values[k][2] // 8
39 |
40 | samples_from = kwargs[arg]["samples"]
41 | if feather == 0:
42 | s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x]
43 | else:
44 | samples_from = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x]
45 | mask = torch.ones_like(samples_from)
46 | for t in range(feather):
47 | if y != 0:
48 | mask[:,:,t:1+t,:] *= ((1.0/feather) * (t + 1))
49 |
50 | if y + samples_from.shape[2] < samples_to.shape[2]:
51 | mask[:,:,mask.shape[2] -1 -t: mask.shape[2]-t,:] *= ((1.0/feather) * (t + 1))
52 | if x != 0:
53 | mask[:,:,:,t:1+t] *= ((1.0/feather) * (t + 1))
54 | if x + samples_from.shape[3] < samples_to.shape[3]:
55 | mask[:,:,:,mask.shape[3]- 1 - t: mask.shape[3]- t] *= ((1.0/feather) * (t + 1))
56 | rev_mask = torch.ones_like(mask) - mask
57 | s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x] * mask + s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] * rev_mask
58 | k += 1
59 |
60 | samples_out["samples"] = s
61 | return (samples_out,)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Davemane42's Custom Node for [ComfyUI](https://github.com/comfyanonymous/ComfyUI)
2 |
3 | ## Instalation:
4 |
5 | - Navigate to the `/ComfyUI/custom_nodes/` folder
6 | - `git clone git clone https://github.com/Davemane42/ComfyUI_Dave_CustomNode`
7 | - Start ComfyUI
8 | - all require file should be downloaded/copied from there.
9 | - no need to manually copy/paste .js files anymore
10 |
11 | ___
12 | # MultiAreaConditioning 2.4
13 |
14 | Let you visualize the ConditioningSetArea node for better control
15 |
16 | Right click menu to add/remove/swap layers:
17 |
18 |
19 | Display what node is associated with current input selected
20 |
21 |
22 |
23 | this also come with a ConditioningUpscale node.
24 | useseful for hires fix workflow
25 |
26 |
27 |
28 | Result example:
29 |
30 |
31 |
32 | Workflow example:
33 |
34 |
35 |
36 | ___
37 | # MultiLatentComposite 1.1
38 |
39 | Let you visualize the MultiLatentComposite node for better control
40 |
41 | Right click menu to add/remove/swap layers:
42 |
43 |
44 | Display what node is associated with current input selected
45 |
46 |
47 |
48 |
49 | Result example:
50 |
51 |
52 |
53 | Workflow example:
54 |
55 |
56 |
57 | ___
58 | # Known issues
59 |
60 | ## MultiAreaComposition 2.4
61 | -
62 | ## MultiLatentComposite 1.1
63 | - no check for out of bound layers
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # Made by Davemane42#0042 for ComfyUI
2 | import os
3 | import subprocess
4 | import importlib.util
5 | import sys
6 | import filecmp
7 | import shutil
8 |
9 | import __main__
10 |
11 | python = sys.executable
12 |
13 |
14 | extentions_folder = os.path.join(os.path.dirname(os.path.realpath(__main__.__file__)),
15 | "web" + os.sep + "extensions" + os.sep + "Davemane42")
16 | javascript_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "javascript")
17 |
18 | if not os.path.exists(extentions_folder):
19 | print('Making the "web\extensions\Davemane42" folder')
20 | os.mkdir(extentions_folder)
21 |
22 | result = filecmp.dircmp(javascript_folder, extentions_folder)
23 |
24 | if result.left_only or result.diff_files:
25 | print('Update to javascripts files detected')
26 | file_list = list(result.left_only)
27 | file_list.extend(x for x in result.diff_files if x not in file_list)
28 |
29 | for file in file_list:
30 | print(f'Copying {file} to extensions folder')
31 | src_file = os.path.join(javascript_folder, file)
32 | dst_file = os.path.join(extentions_folder, file)
33 | if os.path.exists(dst_file):
34 | os.remove(dst_file)
35 | #print("disabled")
36 | shutil.copy(src_file, dst_file)
37 |
38 |
39 | def is_installed(package, package_overwrite=None):
40 | try:
41 | spec = importlib.util.find_spec(package)
42 | except ModuleNotFoundError:
43 | pass
44 |
45 | package = package_overwrite or package
46 |
47 | if spec is None:
48 | print(f"Installing {package}...")
49 | command = f'"{python}" -m pip install {package}'
50 |
51 | result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=os.environ)
52 |
53 | if result.returncode != 0:
54 | print(f"Couldn't install\nCommand: {command}\nError code: {result.returncode}")
55 |
56 | # is_installed("huggingface_hub")
57 | # is_installed("onnx")
58 | # is_installed("onnxruntime", "onnxruntime-gpu")
59 |
60 | from .MultiAreaConditioning import MultiAreaConditioning, ConditioningUpscale, ConditioningStretch, ConditioningDebug
61 | from .MultiLatentComposite import MultiLatentComposite
62 | #from .ABGRemover import ABGRemover
63 |
64 | NODE_CLASS_MAPPINGS = {
65 | "MultiLatentComposite": MultiLatentComposite,
66 | "MultiAreaConditioning": MultiAreaConditioning,
67 | "ConditioningUpscale": ConditioningUpscale,
68 | "ConditioningStretch": ConditioningStretch,
69 | #"ABGRemover": ABGRemover,
70 | }
71 |
72 | print('\033[34mDavemane42 Custom Nodes: \033[92mLoaded\033[0m')
--------------------------------------------------------------------------------
/images/ConditioningUpscale_node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Davemane42/ComfyUI_Dave_CustomNode/de6b112ee214420d283ff15ffe8302a667d207dd/images/ConditioningUpscale_node.png
--------------------------------------------------------------------------------
/images/MultiAreaConditioning_node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Davemane42/ComfyUI_Dave_CustomNode/de6b112ee214420d283ff15ffe8302a667d207dd/images/MultiAreaConditioning_node.png
--------------------------------------------------------------------------------
/images/MultiAreaConditioning_result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Davemane42/ComfyUI_Dave_CustomNode/de6b112ee214420d283ff15ffe8302a667d207dd/images/MultiAreaConditioning_result.png
--------------------------------------------------------------------------------
/images/MultiLatentComposite_node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Davemane42/ComfyUI_Dave_CustomNode/de6b112ee214420d283ff15ffe8302a667d207dd/images/MultiLatentComposite_node.png
--------------------------------------------------------------------------------
/images/MultiLatentComposite_result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Davemane42/ComfyUI_Dave_CustomNode/de6b112ee214420d283ff15ffe8302a667d207dd/images/MultiLatentComposite_result.png
--------------------------------------------------------------------------------
/images/RightClickMenu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Davemane42/ComfyUI_Dave_CustomNode/de6b112ee214420d283ff15ffe8302a667d207dd/images/RightClickMenu.png
--------------------------------------------------------------------------------
/javascript/MultiAreaConditioning.js:
--------------------------------------------------------------------------------
1 | import { app } from "/scripts/app.js";
2 | import {CUSTOM_INT, recursiveLinkUpstream, transformFunc, swapInputs, renameNodeInputs, removeNodeInputs, getDrawColor, computeCanvasSize} from "./utils.js"
3 |
4 | function addMultiAreaConditioningCanvas(node, app) {
5 |
6 | const widget = {
7 | type: "customCanvas",
8 | name: "MultiAreaConditioning-Canvas",
9 | get value() {
10 | return this.canvas.value;
11 | },
12 | set value(x) {
13 | this.canvas.value = x;
14 | },
15 | draw: function (ctx, node, widgetWidth, widgetY) {
16 |
17 | // If we are initially offscreen when created we wont have received a resize event
18 | // Calculate it here instead
19 | if (!node.canvasHeight) {
20 | computeCanvasSize(node, node.size)
21 | }
22 |
23 | const visible = true //app.canvasblank.ds.scale > 0.5 && this.type === "customCanvas";
24 | const t = ctx.getTransform();
25 | const margin = 10
26 | const border = 2
27 |
28 | const widgetHeight = node.canvasHeight
29 | const values = node.properties["values"]
30 | const width = Math.round(node.properties["width"])
31 | const height = Math.round(node.properties["height"])
32 |
33 | const scale = Math.min((widgetWidth-margin*2)/width, (widgetHeight-margin*2)/height)
34 |
35 | const index = Math.round(node.widgets[node.index].value)
36 |
37 | Object.assign(this.canvas.style, {
38 | left: `${t.e}px`,
39 | top: `${t.f + (widgetY*t.d)}px`,
40 | width: `${widgetWidth * t.a}px`,
41 | height: `${widgetHeight * t.d}px`,
42 | position: "absolute",
43 | zIndex: 1,
44 | fontSize: `${t.d * 10.0}px`,
45 | pointerEvents: "none",
46 | });
47 |
48 | this.canvas.hidden = !visible;
49 |
50 | let backgroudWidth = width * scale
51 | let backgroundHeight = height * scale
52 |
53 | let xOffset = margin
54 | if (backgroudWidth < widgetWidth) {
55 | xOffset += (widgetWidth-backgroudWidth)/2 - margin
56 | }
57 | let yOffset = margin
58 | if (backgroundHeight < widgetHeight) {
59 | yOffset += (widgetHeight-backgroundHeight)/2 - margin
60 | }
61 |
62 | let widgetX = xOffset
63 | widgetY = widgetY + yOffset
64 |
65 | ctx.fillStyle = "#000000"
66 | ctx.fillRect(widgetX-border, widgetY-border, backgroudWidth+border*2, backgroundHeight+border*2)
67 |
68 | ctx.fillStyle = globalThis.LiteGraph.NODE_DEFAULT_BGCOLOR
69 | ctx.fillRect(widgetX, widgetY, backgroudWidth, backgroundHeight);
70 |
71 | function getDrawArea(v) {
72 | let x = v[0]*backgroudWidth/width
73 | let y = v[1]*backgroundHeight/height
74 | let w = v[2]*backgroudWidth/width
75 | let h = v[3]*backgroundHeight/height
76 |
77 | if (x > backgroudWidth) { x = backgroudWidth}
78 | if (y > backgroundHeight) { y = backgroundHeight}
79 |
80 | if (x+w > backgroudWidth) {
81 | w = Math.max(0, backgroudWidth-x)
82 | }
83 |
84 | if (y+h > backgroundHeight) {
85 | h = Math.max(0, backgroundHeight-y)
86 | }
87 |
88 | return [x, y, w, h]
89 | }
90 |
91 | // Draw all the conditioning zones
92 | for (const [k, v] of values.entries()) {
93 |
94 | if (k == index) {continue}
95 |
96 | const [x, y, w, h] = getDrawArea(v)
97 |
98 | ctx.fillStyle = getDrawColor(k/values.length, "80") //colors[k] + "B0"
99 | ctx.fillRect(widgetX+x, widgetY+y, w, h)
100 |
101 | }
102 |
103 | ctx.beginPath();
104 | ctx.lineWidth = 1;
105 |
106 | for (let x = 0; x <= width/64; x += 1) {
107 | ctx.moveTo(widgetX+x*64*scale, widgetY);
108 | ctx.lineTo(widgetX+x*64*scale, widgetY+backgroundHeight);
109 | }
110 |
111 | for (let y = 0; y <= height/64; y += 1) {
112 | ctx.moveTo(widgetX, widgetY+y*64*scale);
113 | ctx.lineTo(widgetX+backgroudWidth, widgetY+y*64*scale);
114 | }
115 |
116 | ctx.strokeStyle = "#00000050";
117 | ctx.stroke();
118 | ctx.closePath();
119 |
120 | // Draw currently selected zone
121 | console.log(index)
122 | let [x, y, w, h] = getDrawArea(values[index])
123 |
124 | w = Math.max(32*scale, w)
125 | h = Math.max(32*scale, h)
126 |
127 | //ctx.fillStyle = "#"+(Number(`0x1${colors[index].substring(1)}`) ^ 0xFFFFFF).toString(16).substring(1).toUpperCase()
128 | ctx.fillStyle = "#ffffff"
129 | ctx.fillRect(widgetX+x, widgetY+y, w, h)
130 |
131 | const selectedColor = getDrawColor(index/values.length, "FF")
132 | ctx.fillStyle = selectedColor
133 | ctx.fillRect(widgetX+x+border, widgetY+y+border, w-border*2, h-border*2)
134 |
135 | // Display
136 | ctx.beginPath();
137 |
138 | ctx.arc(LiteGraph.NODE_SLOT_HEIGHT*0.5, LiteGraph.NODE_SLOT_HEIGHT*(index + 0.5)+4, 4, 0, Math.PI * 2);
139 | ctx.fill();
140 |
141 | ctx.lineWidth = 1;
142 | ctx.strokeStyle = "white";
143 | ctx.stroke();
144 |
145 | if (node.selected) {
146 | const connectedNodes = recursiveLinkUpstream(node, node.inputs[index].type, 0, index)
147 |
148 | if (connectedNodes.length !== 0) {
149 | for (let [node_ID, depth] of connectedNodes) {
150 | let connectedNode = node.graph._nodes_by_id[node_ID]
151 | if (connectedNode.type != node.type) {
152 | const [x, y] = connectedNode.pos
153 | const [w, h] = connectedNode.size
154 | const offset = 5
155 | const titleHeight = LiteGraph.NODE_TITLE_HEIGHT * (connectedNode.type === "Reroute" ? 0 : 1)
156 |
157 | ctx.strokeStyle = selectedColor
158 | ctx.lineWidth = 5;
159 | ctx.strokeRect(x-offset-node.pos[0], y-offset-node.pos[1]-titleHeight, w+offset*2, h+offset*2+titleHeight)
160 | }
161 | }
162 | }
163 | }
164 | ctx.lineWidth = 1;
165 | ctx.closePath();
166 |
167 | },
168 | };
169 |
170 | widget.canvas = document.createElement("canvas");
171 | widget.canvas.className = "dave-custom-canvas";
172 |
173 | widget.parent = node;
174 | document.body.appendChild(widget.canvas);
175 |
176 | node.addCustomWidget(widget);
177 |
178 | app.canvas.onDrawBackground = function () {
179 | // Draw node isnt fired once the node is off the screen
180 | // if it goes off screen quickly, the input may not be removed
181 | // this shifts it off screen so it can be moved back if the node is visible.
182 | for (let n in app.graph._nodes) {
183 | n = graph._nodes[n];
184 | for (let w in n.widgets) {
185 | let wid = n.widgets[w];
186 | if (Object.hasOwn(wid, "canvas")) {
187 | wid.canvas.style.left = -8000 + "px";
188 | wid.canvas.style.position = "absolute";
189 | }
190 | }
191 | }
192 | };
193 |
194 | node.onResize = function (size) {
195 | computeCanvasSize(node, size);
196 | }
197 |
198 | return { minWidth: 200, minHeight: 200, widget }
199 | }
200 |
201 | app.registerExtension({
202 | name: "Comfy.Davemane42.MultiAreaConditioning",
203 | async beforeRegisterNodeDef(nodeType, nodeData, app) {
204 | if (nodeData.name === "MultiAreaConditioning") {
205 | const onNodeCreated = nodeType.prototype.onNodeCreated;
206 | nodeType.prototype.onNodeCreated = function () {
207 | const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
208 |
209 | this.setProperty("width", 512)
210 | this.setProperty("height", 512)
211 | this.setProperty("values", [[0, 0, 0, 0, 1.0], [0, 0, 0, 0, 1.0]])
212 |
213 | this.selected = false
214 | this.index = 3
215 |
216 | this.serialize_widgets = true;
217 |
218 | CUSTOM_INT(this, "resolutionX", 512, function (v, _, node) {const s = this.options.step / 10; this.value = Math.round(v / s) * s; node.properties["width"] = this.value})
219 | CUSTOM_INT(this, "resolutionY", 512, function (v, _, node) {const s = this.options.step / 10; this.value = Math.round(v / s) * s; node.properties["height"] = this.value})
220 |
221 | addMultiAreaConditioningCanvas(this, app)
222 |
223 | CUSTOM_INT(
224 | this,
225 | "index",
226 | 0,
227 | function (v, _, node) {
228 |
229 | let values = node.properties["values"]
230 |
231 | node.widgets[4].value = values[v][0]
232 | node.widgets[5].value = values[v][1]
233 | node.widgets[6].value = values[v][2]
234 | node.widgets[7].value = values[v][3]
235 | if (!values[v][4]) {values[v][4] = 1.0}
236 | node.widgets[8].value = values[v][4]
237 | },
238 | { step: 10, max: 1 }
239 |
240 | )
241 |
242 | CUSTOM_INT(this, "x", 0, function (v, _, node) {transformFunc(this, v, node, 0)})
243 | CUSTOM_INT(this, "y", 0, function (v, _, node) {transformFunc(this, v, node, 1)})
244 | CUSTOM_INT(this, "width", 0, function (v, _, node) {transformFunc(this, v, node, 2)})
245 | CUSTOM_INT(this, "height", 0, function (v, _, node) {transformFunc(this, v, node, 3)})
246 | CUSTOM_INT(this, "strength", 1, function (v, _, node) {transformFunc(this, v, node, 4)}, {"min": 0.0, "max": 10.0, "step": 0.1, "precision": 2})
247 |
248 | this.getExtraMenuOptions = function(_, options) {
249 | options.unshift(
250 | {
251 | content: `insert input above ${this.widgets[this.index].value} /\\`,
252 | callback: () => {
253 | this.addInput("conditioning", "CONDITIONING")
254 |
255 | const inputLenth = this.inputs.length-1
256 | const index = this.widgets[this.index].value
257 |
258 | for (let i = inputLenth; i > index; i--) {
259 | swapInputs(this, i, i-1)
260 | }
261 | renameNodeInputs(this, "conditioning")
262 |
263 | this.properties["values"].splice(index, 0, [0, 0, 0, 0, 1])
264 | this.widgets[this.index].options.max = inputLenth
265 |
266 | this.setDirtyCanvas(true);
267 |
268 | },
269 | },
270 | {
271 | content: `insert input below ${this.widgets[this.index].value} \\/`,
272 | callback: () => {
273 | this.addInput("conditioning", "CONDITIONING")
274 |
275 | const inputLenth = this.inputs.length-1
276 | const index = this.widgets[this.index].value
277 |
278 | for (let i = inputLenth; i > index+1; i--) {
279 | swapInputs(this, i, i-1)
280 | }
281 | renameNodeInputs(this, "conditioning")
282 |
283 | this.properties["values"].splice(index+1, 0, [0, 0, 0, 0, 1])
284 | this.widgets[this.index].options.max = inputLenth
285 |
286 | this.setDirtyCanvas(true);
287 | },
288 | },
289 | {
290 | content: `swap with input above ${this.widgets[this.index].value} /\\`,
291 | callback: () => {
292 | const index = this.widgets[this.index].value
293 | if (index !== 0) {
294 | swapInputs(this, index, index-1)
295 |
296 | renameNodeInputs(this, "conditioning")
297 |
298 | this.properties["values"].splice(index-1,0,this.properties["values"].splice(index,1)[0]);
299 | this.widgets[this.index].value = index-1
300 |
301 | this.setDirtyCanvas(true);
302 | }
303 | },
304 | },
305 | {
306 | content: `swap with input below ${this.widgets[this.index].value} \\/`,
307 | callback: () => {
308 | const index = this.widgets[this.index].value
309 | if (index !== this.inputs.length-1) {
310 | swapInputs(this, index, index+1)
311 |
312 | renameNodeInputs(this, "conditioning")
313 |
314 | this.properties["values"].splice(index+1,0,this.properties["values"].splice(index,1)[0]);
315 | this.widgets[this.index].value = index+1
316 |
317 | this.setDirtyCanvas(true);
318 | }
319 | },
320 | },
321 | {
322 | content: `remove currently selected input ${this.widgets[this.index].value}`,
323 | callback: () => {
324 | const index = this.widgets[this.index].value
325 | removeNodeInputs(this, [index])
326 | renameNodeInputs(this, "conditioning")
327 | },
328 | },
329 | {
330 | content: "remove all unconnected inputs",
331 | callback: () => {
332 | let indexesToRemove = []
333 |
334 | for (let i = 0; i < this.inputs.length; i++) {
335 | if (!this.inputs[i].link) {
336 | indexesToRemove.push(i)
337 | }
338 | }
339 |
340 | if (indexesToRemove.length) {
341 | removeNodeInputs(this, indexesToRemove, "conditioning")
342 | }
343 | renameNodeInputs(this, "conditioning")
344 | },
345 | },
346 | );
347 | }
348 |
349 | this.onRemoved = function () {
350 | // When removing this node we need to remove the input from the DOM
351 | for (let y in this.widgets) {
352 | if (this.widgets[y].canvas) {
353 | this.widgets[y].canvas.remove();
354 | }
355 | }
356 | };
357 |
358 | this.onSelected = function () {
359 | this.selected = true
360 | }
361 | this.onDeselected = function () {
362 | this.selected = false
363 | }
364 |
365 | return r;
366 | };
367 | }
368 | },
369 | loadedGraphNode(node, _) {
370 | if (node.type === "MultiAreaConditioning") {
371 | node.widgets[node.index].options["max"] = node.properties["values"].length-1
372 | }
373 | },
374 |
375 | });
--------------------------------------------------------------------------------
/javascript/MultiLatentComposite.js:
--------------------------------------------------------------------------------
1 | import { app } from "/scripts/app.js";
2 | import {CUSTOM_INT, recursiveLinkUpstream, transformFunc, swapInputs, renameNodeInputs, removeNodeInputs, getDrawColor, computeCanvasSize} from "./utils.js"
3 |
4 | function addMultiLatentCompositeCanvas(node, app) {
5 |
6 | function findSizingNode(node, index=null) {
7 |
8 | const inputList = (index !== null) ? [index] : [...Array(node.inputs.length).keys()]
9 | if (inputList.length === 0) { return }
10 |
11 | for (let i of inputList) {
12 | const connectedNodes = recursiveLinkUpstream(node, node.inputs[i].type, 0, i)
13 |
14 | if (connectedNodes.length !== 0) {
15 | for (let [node_ID, depth] of connectedNodes) {
16 | const connectedNode = node.graph._nodes_by_id[node_ID]
17 |
18 | if (connectedNode.type !== "MultiLatentComposite") {
19 |
20 | const [endWidth, endHeight] = getSizeFromNode(connectedNode)
21 |
22 | if (endWidth && endHeight) {
23 | if (i === 0) {
24 | node.sampleToID = connectedNode.id
25 | } else {
26 | node.properties["values"][i-1][3] = connectedNode.id
27 | }
28 | break
29 | }
30 | }
31 | }
32 | } else { // if previous connection is broken
33 | if (i !== 0 && node.properties["values"][i-1][3]) {
34 | node.properties["values"][i-1][3] = null
35 | }
36 | }
37 | }
38 | }
39 |
40 | function getSizeFromNode(node) {
41 | if (!node.widgets) { return [null, null] }
42 |
43 | let endWidth = null
44 | let endHeight = null
45 |
46 | for (let widget of node.widgets) {
47 | if (widget.name.toLowerCase() === "width") {endWidth = widget.value}
48 | if (widget.name.toLowerCase() === "height") {endHeight = widget.value}
49 | }
50 |
51 | return [endWidth, endHeight]
52 | }
53 |
54 | const widget = {
55 | type: "customCanvas",
56 | name: "MultiLatentComposite-Canvas",
57 | get value() {
58 | return this.canvas.value;
59 | },
60 | set value(x) {
61 | this.canvas.value = x;
62 | },
63 | draw: function (ctx, node, widgetWidth, widgetY) {
64 |
65 | // If we are initially offscreen when created we wont have received a resize event
66 | // Calculate it here instead
67 | if (!node.canvasHeight) {
68 | computeCanvasSize(node, node.size)
69 | }
70 |
71 | const widgetHeight = node.canvasHeight
72 |
73 | // if (node.selected || !node.sampleToID) {
74 | // findSizingNode(node)
75 | // }
76 |
77 | findSizingNode(node)
78 |
79 | const t = ctx.getTransform();
80 | Object.assign(this.canvas.style, {
81 | left: `${t.e}px`,
82 | top: `${t.f + (widgetY*t.d)}px`,
83 | width: `${widgetWidth * t.a}px`,
84 | height: `${widgetHeight * t.d}px`,
85 | position: "absolute",
86 | zIndex: 1,
87 | fontSize: `${t.d * 10.0}px`,
88 | pointerEvents: "none",
89 | });
90 |
91 | const visible = true //app.canvasblank.ds.scale > 0.5 && this.type === "customCanvas";
92 | this.canvas.hidden = !visible;
93 |
94 | const margin = 10
95 | const border = 2
96 |
97 | const values = node.properties["values"]
98 | const index = Math.round(node.widgets[node.index].value)
99 |
100 | let width = null
101 | let height = null
102 | if (node.sampleToID && node.graph._nodes_by_id[node.sampleToID]) {
103 | [width, height] = getSizeFromNode(node.graph._nodes_by_id[node.sampleToID])
104 | }
105 |
106 | if (width && height) {
107 | const scale = Math.min((widgetWidth-margin*2)/width, (widgetHeight-margin*2)/height)
108 |
109 | let backgroudWidth = width * scale
110 | let backgroundHeight = height * scale
111 |
112 | let xOffset = margin
113 | if (backgroudWidth < widgetWidth) {
114 | xOffset += (widgetWidth-backgroudWidth)/2 - margin
115 | }
116 | let yOffset = margin
117 | if (backgroundHeight < widgetHeight) {
118 | yOffset += (widgetHeight-backgroundHeight)/2 - margin
119 | }
120 |
121 | let widgetX = xOffset
122 | widgetY = widgetY + yOffset
123 |
124 | ctx.fillStyle = "#000000"
125 | ctx.fillRect(widgetX-border, widgetY-border, backgroudWidth+border*2, backgroundHeight+border*2)
126 |
127 | ctx.fillStyle = globalThis.LiteGraph.NODE_DEFAULT_BGCOLOR
128 | ctx.fillRect(widgetX, widgetY, backgroudWidth, backgroundHeight);
129 |
130 | function getDrawArea(v) {
131 | let x = v[0]*backgroudWidth/width
132 | let y = v[1]*backgroundHeight/height
133 |
134 | if (x > backgroudWidth) { x = backgroudWidth}
135 | if (y > backgroundHeight) { y = backgroundHeight}
136 |
137 | if (!v[3]) {return [x, y, 0, 0]}
138 | let [w, h] = getSizeFromNode(node.graph._nodes_by_id[v[3]])
139 |
140 | w *= backgroudWidth/width
141 | h *= backgroundHeight/height
142 |
143 | if (x+w > backgroudWidth) {
144 | w = Math.max(0, backgroudWidth-x)
145 | }
146 |
147 | if (y+h > backgroundHeight) {
148 | h = Math.max(0, backgroundHeight-y)
149 | }
150 |
151 | return [x, y, w, h]
152 | }
153 |
154 | // Draw all the conditioning zones
155 | for (const [k, v] of values.entries()) {
156 |
157 | if (k == index) {continue}
158 |
159 | const [x, y, w, h] = getDrawArea(v)
160 |
161 | ctx.fillStyle = getDrawColor(k/values.length, "80") //colors[k] + "B0"
162 | ctx.fillRect(widgetX+x, widgetY+y, w, h)
163 |
164 | }
165 |
166 | ctx.beginPath();
167 | ctx.lineWidth = 1;
168 |
169 | for (let x = 0; x <= width/64; x += 1) {
170 | ctx.moveTo(widgetX+x*64*scale, widgetY);
171 | ctx.lineTo(widgetX+x*64*scale, widgetY+backgroundHeight);
172 | }
173 |
174 | for (let y = 0; y <= height/64; y += 1) {
175 | ctx.moveTo(widgetX, widgetY+y*64*scale);
176 | ctx.lineTo(widgetX+backgroudWidth, widgetY+y*64*scale);
177 | }
178 |
179 | ctx.strokeStyle = "#00000050";
180 | ctx.stroke();
181 | ctx.closePath();
182 |
183 | // Draw currently selected zone
184 | let [x, y, w, h] = getDrawArea(values[index])
185 |
186 | w = Math.max(32*scale, w)
187 | h = Math.max(32*scale, h)
188 |
189 | //ctx.fillStyle = "#"+(Number(`0x1${colors[index].substring(1)}`) ^ 0xFFFFFF).toString(16).substring(1).toUpperCase()
190 | ctx.fillStyle = "#ffffff"
191 | ctx.fillRect(widgetX+x, widgetY+y, w, h)
192 |
193 | const selectedColor = getDrawColor(index/values.length, "FF")
194 | ctx.fillStyle = selectedColor
195 | ctx.fillRect(widgetX+x+border, widgetY+y+border, w-border*2, h-border*2)
196 |
197 | // Display
198 | ctx.beginPath();
199 |
200 | ctx.arc(LiteGraph.NODE_SLOT_HEIGHT*0.5, LiteGraph.NODE_SLOT_HEIGHT*(index + 1.5)+4, 4, 0, Math.PI * 2);
201 | ctx.fill();
202 |
203 | ctx.lineWidth = 1;
204 | ctx.strokeStyle = "white";
205 | ctx.stroke();
206 | }
207 |
208 | if (node.selected) {
209 | const selectedNode = values[index][3]
210 | if (selectedNode) {
211 | const selectedColor = getDrawColor(index/values.length, "FF")
212 | ctx.lineWidth = 5;
213 |
214 | const [x, y, w, h] = node.graph._nodes_by_id[selectedNode].getBounding()
215 | const offset = 5
216 |
217 | ctx.strokeStyle = selectedColor
218 | ctx.strokeRect(x-offset-node.pos[0], y-offset-node.pos[1], w+offset*2, h+offset*2)
219 |
220 | ctx.lineWidth = 1;
221 | ctx.closePath();
222 | }
223 | }
224 | },
225 | };
226 |
227 | widget.canvas = document.createElement("canvas");
228 | widget.canvas.className = "dave-custom-canvas";
229 |
230 | widget.parent = node;
231 | document.body.appendChild(widget.canvas);
232 |
233 | node.addCustomWidget(widget);
234 |
235 | app.canvas.onDrawBackground = function () {
236 | // Draw node isnt fired once the node is off the screen
237 | // if it goes off screen quickly, the input may not be removed
238 | // this shifts it off screen so it can be moved back if the node is visible.
239 | for (let n in app.graph._nodes) {
240 | n = graph._nodes[n];
241 | for (let w in n.widgets) {
242 | let wid = n.widgets[w];
243 | if (Object.hasOwn(wid, "canvas")) {
244 | wid.canvas.style.left = -8000 + "px";
245 | wid.canvas.style.position = "absolute";
246 | }
247 | }
248 | }
249 | };
250 |
251 | node.onResize = function (size) {
252 | computeCanvasSize(node, size);
253 | }
254 |
255 | return { minWidth: 200, minHeight: 200, widget }
256 | }
257 |
258 | app.registerExtension({
259 | name: "Comfy.Davemane42.MultiLatentComposite",
260 | async beforeRegisterNodeDef(nodeType, nodeData, app) {
261 | if (nodeData.name === "MultiLatentComposite") {
262 | const onNodeCreated = nodeType.prototype.onNodeCreated;
263 | nodeType.prototype.onNodeCreated = function () {
264 | const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
265 |
266 | this.setProperty("values", [[0, 0, 0, null]])
267 |
268 | this.selected = false
269 | this.index = 1
270 |
271 | this.serialize_widgets = true;
272 |
273 | addMultiLatentCompositeCanvas(this, app)
274 |
275 | CUSTOM_INT(
276 | this,
277 | "index",
278 | 0,
279 | function (v, _, node) {
280 |
281 | let values = node.properties["values"]
282 |
283 | node.widgets[2].value = values[v][0]
284 | node.widgets[3].value = values[v][1]
285 | node.widgets[4].value = values[v][2]
286 | },
287 | { step: 10, max: 1 }
288 |
289 | )
290 |
291 | CUSTOM_INT(this, "x", 0, function (v, _, node) {transformFunc(this, v, node, 0)}, {step: 80})
292 | CUSTOM_INT(this, "y", 0, function (v, _, node) {transformFunc(this, v, node, 1)}, {step: 80})
293 | CUSTOM_INT(this, "feather", 1, function (v, _, node) {transformFunc(this, v, node, 2)}, {"min": 0.0, "max": 4096, "step": 80, "precision": 0})
294 |
295 | this.getExtraMenuOptions = function(_, options) {
296 | options.unshift(
297 | {
298 | content: `insert input above ${this.widgets[this.index].value} /\\`,
299 | callback: () => {
300 | this.addInput("samples_from", "LATENT")
301 |
302 | const inputLenth = this.inputs.length-1
303 | const index = this.widgets[this.index].value
304 |
305 | for (let i = inputLenth; i > index+1; i--) {
306 | swapInputs(this, i, i-1)
307 | }
308 | renameNodeInputs(this, "samples_from", 1)
309 |
310 | this.properties["values"].splice(index, 0, [0, 0, 0, null])
311 | this.widgets[this.index].options.max = inputLenth-1
312 |
313 | this.setDirtyCanvas(true);
314 |
315 | },
316 | },
317 | {
318 | content: `insert input below ${this.widgets[this.index].value} \\/`,
319 | callback: () => {
320 | this.addInput("samples_from", "LATENT")
321 |
322 | const inputLenth = this.inputs.length-1
323 | const index = this.widgets[this.index].value
324 |
325 | for (let i = inputLenth; i > index+2; i--) {
326 | swapInputs(this, i, i-1)
327 | }
328 | renameNodeInputs(this, "samples_from", 1)
329 |
330 | this.properties["values"].splice(index+1, 0, [0, 0, 0, null])
331 | this.widgets[this.index].options.max = inputLenth-1
332 |
333 | this.setDirtyCanvas(true);
334 | },
335 | },
336 | {
337 | content: `swap with input above ${this.widgets[this.index].value} /\\`,
338 | callback: () => {
339 | const index = this.widgets[this.index].value
340 | if (index !== 0) {
341 | swapInputs(this, index+1, index)
342 |
343 | renameNodeInputs(this, "samples_from", 1)
344 |
345 | this.properties["values"].splice(index-1,0,this.properties["values"].splice(index,1)[0]);
346 | this.widgets[this.index].value = index-1
347 |
348 | this.setDirtyCanvas(true);
349 | }
350 | },
351 | },
352 | {
353 | content: `swap with input below ${this.widgets[this.index].value} \\/`,
354 | callback: () => {
355 | const index = this.widgets[this.index].value
356 | if (index !== this.properties["values"].length-1) {
357 | swapInputs(this, index+1, index+2)
358 |
359 | renameNodeInputs(this, "samples_from", 1)
360 |
361 | this.properties["values"].splice(index+1,0,this.properties["values"].splice(index,1)[0]);
362 | this.widgets[this.index].value = index+1
363 |
364 | this.setDirtyCanvas(true);
365 | }
366 | },
367 | },
368 | {
369 | content: `remove currently selected input ${this.widgets[this.index].value}`,
370 | callback: () => {
371 | const index = this.widgets[this.index].value
372 | removeNodeInputs(this, [index+1], 1)
373 | renameNodeInputs(this, "samples_from", 1)
374 | },
375 | },
376 | {
377 | content: "remove all unconnected inputs",
378 | callback: () => {
379 | let indexesToRemove = []
380 |
381 | for (let i = 1; i <= this.inputs.length-1; i++) {
382 | if (!this.inputs[i].link) {
383 | indexesToRemove.push(i)
384 | }
385 | }
386 |
387 | if (indexesToRemove.length) {
388 | removeNodeInputs(this, indexesToRemove, 1)
389 | renameNodeInputs(this, "samples_from", 1)
390 | }
391 |
392 | },
393 | },
394 | );
395 | }
396 |
397 | this.onRemoved = function () {
398 | // When removing this node we need to remove the input from the DOM
399 | for (let y in this.widgets) {
400 | if (this.widgets[y].canvas) {
401 | this.widgets[y].canvas.remove();
402 | }
403 | }
404 | };
405 |
406 | this.onSelected = function () {
407 | this.selected = true
408 | }
409 | this.onDeselected = function () {
410 | this.selected = false
411 | }
412 |
413 | return r
414 | }
415 | }
416 | },
417 | loadedGraphNode(node, _) {
418 | if (node.type === "MultiLatentComposite") {
419 | node.widgets[node.index].options["max"] = node.properties["values"].length-1
420 | }
421 | },
422 |
423 | });
--------------------------------------------------------------------------------
/javascript/utils.js:
--------------------------------------------------------------------------------
1 | export function CUSTOM_INT(node, inputName, val, func, config = {}) {
2 | return {
3 | widget: node.addWidget(
4 | "number",
5 | inputName,
6 | val,
7 | func,
8 | Object.assign({}, { min: 0, max: 4096, step: 640, precision: 0 }, config)
9 | ),
10 | };
11 | }
12 |
13 | export function recursiveLinkUpstream(node, type, depth, index=null) {
14 | depth += 1
15 | let connections = []
16 | const inputList = (index !== null) ? [index] : [...Array(node.inputs.length).keys()]
17 | if (inputList.length === 0) { return }
18 | for (let i of inputList) {
19 | const link = node.inputs[i].link
20 | if (link) {
21 | const nodeID = node.graph.links[link].origin_id
22 | const slotID = node.graph.links[link].origin_slot
23 | const connectedNode = node.graph._nodes_by_id[nodeID]
24 |
25 | if (connectedNode.outputs[slotID].type === type) {
26 |
27 | connections.push([connectedNode.id, depth])
28 |
29 | if (connectedNode.inputs) {
30 | const index = (connectedNode.type === "LatentComposite") ? 0 : null
31 | connections = connections.concat(recursiveLinkUpstream(connectedNode, type, depth, index))
32 | } else {
33 |
34 | }
35 | }
36 | }
37 | }
38 | return connections
39 | }
40 |
41 | export function transformFunc(widget, value, node, index) {
42 | const s = widget.options.step / 10;
43 | widget.value = Math.round(value / s) * s;
44 | node.properties["values"][node.widgets[node.index].value][index] = widget.value
45 | if (node.widgets_values) {
46 | node.widgets_values[2] = node.properties["values"].join()
47 | }
48 | }
49 |
50 | export function swapInputs(node, indexA, indexB) {
51 | const linkA = node.inputs[indexA].link
52 | let origin_slotA = null
53 | let node_IDA = null
54 | let connectedNodeA = null
55 | let labelA = node.inputs[indexA].label || null
56 |
57 | const linkB = node.inputs[indexB].link
58 | let origin_slotB = null
59 | let node_IDB = null
60 | let connectedNodeB = null
61 | let labelB = node.inputs[indexB].label || null
62 |
63 | if (linkA) {
64 | node_IDA = node.graph.links[linkA].origin_id
65 | origin_slotA = node.graph.links[linkA].origin_slot
66 | connectedNodeA = node.graph._nodes_by_id[node_IDA]
67 |
68 | node.disconnectInput(indexA)
69 | }
70 |
71 | if (linkB) {
72 | node_IDB = node.graph.links[linkB].origin_id
73 | origin_slotB = node.graph.links[linkB].origin_slot
74 | connectedNodeB = node.graph._nodes_by_id[node_IDB]
75 |
76 | node.disconnectInput(indexB)
77 | }
78 |
79 | if (linkA) {
80 | connectedNodeA.connect(origin_slotA, node, indexB)
81 | }
82 |
83 | if (linkB) {
84 | connectedNodeB.connect(origin_slotB, node, indexA)
85 | }
86 |
87 | node.inputs[indexA].label = labelB
88 | node.inputs[indexB].label = labelA
89 |
90 | }
91 |
92 | export function renameNodeInputs(node, name, offset=0) {
93 | for (let i=offset; i < node.inputs.length; i++) {
94 | node.inputs[i].name = `${name}${i-offset}`
95 | }
96 | }
97 |
98 | export function removeNodeInputs(node, indexesToRemove, offset=0) {
99 | indexesToRemove.sort((a, b) => b - a);
100 |
101 | for (let i of indexesToRemove) {
102 | if (node.inputs.length <= 2) { console.log("too short"); continue } // if only 2 left
103 | node.removeInput(i)
104 | node.properties.values.splice(i-offset, 1)
105 | }
106 |
107 | const inputLenght = node.properties["values"].length-1
108 |
109 | node.widgets[node.index].options.max = inputLenght
110 | if (node.widgets[node.index].value > inputLenght) {
111 | node.widgets[node.index].value = inputLenght
112 | }
113 |
114 | node.onResize(node.size)
115 | }
116 |
117 | export function getDrawColor(percent, alpha) {
118 | let h = 360*percent
119 | let s = 50;
120 | let l = 50;
121 | l /= 100;
122 | const a = s * Math.min(l, 1 - l) / 100;
123 | const f = n => {
124 | const k = (n + h / 30) % 12;
125 | const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
126 | return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
127 | };
128 | return `#${f(0)}${f(8)}${f(4)}${alpha}`;
129 | }
130 |
131 | export function computeCanvasSize(node, size) {
132 | if (node.widgets[0].last_y == null) return;
133 |
134 | const MIN_SIZE = 200;
135 |
136 | let y = LiteGraph.NODE_WIDGET_HEIGHT * Math.max(node.inputs.length, node.outputs.length) + 5;
137 | let freeSpace = size[1] - y;
138 |
139 | // Compute the height of all non customtext widgets
140 | let widgetHeight = 0;
141 | for (let i = 0; i < node.widgets.length; i++) {
142 | const w = node.widgets[i];
143 | if (w.type !== "customCanvas") {
144 | if (w.computeSize) {
145 | widgetHeight += w.computeSize()[1] + 4;
146 | } else {
147 | widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 5;
148 | }
149 | }
150 | }
151 |
152 | // See how large the canvas can be
153 | freeSpace -= widgetHeight;
154 |
155 | // There isnt enough space for all the widgets, increase the size of the node
156 | if (freeSpace < MIN_SIZE) {
157 | freeSpace = MIN_SIZE;
158 | node.size[1] = y + widgetHeight + freeSpace;
159 | node.graph.setDirtyCanvas(true);
160 | }
161 |
162 | // Position each of the widgets
163 | for (const w of node.widgets) {
164 | w.y = y;
165 | if (w.type === "customCanvas") {
166 | y += freeSpace;
167 | } else if (w.computeSize) {
168 | y += w.computeSize()[1] + 4;
169 | } else {
170 | y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
171 | }
172 | }
173 |
174 | node.canvasHeight = freeSpace;
175 | }
--------------------------------------------------------------------------------