├── .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 | } --------------------------------------------------------------------------------