Merge branch 'comfyanonymous:master' into feature/blockweights

This commit is contained in:
ltdrdata 2023-04-14 21:36:07 +09:00 committed by GitHub
commit 34309b3d64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 637 additions and 162 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ temp/
custom_nodes/
!custom_nodes/example_node.py.example
extra_model_paths.yaml
/.vs

View File

@ -35,6 +35,11 @@ Workflow examples can be found on the [Examples page](https://comfyanonymous.git
- **Ctrl + A** select all nodes
- **Ctrl + M** mute/unmute selected nodes
- **Delete** or **Backspace** delete selected nodes
- **Space** Holding space key while moving the cursor moves the canvas around. It works when holding the mouse button down so it is easier to connect different nodes when the canvas gets too large.
- **Ctrl/Shift + Click** Add clicked node to selection.
- **Ctrl + C/Ctrl + V** - Copy and paste selected nodes, without maintaining the connection to the outputs of unselected nodes.
- **Ctrl + C/Ctrl + Shift + V** - Copy and paste selected nodes, and maintaining the connection from the outputs of unselected nodes to the inputs of the newly pasted nodes.
- Holding **Shift** and drag selected nodes - Move multiple selected nodes at the same time.
# Installing
@ -92,6 +97,11 @@ Install the dependencies by opening your terminal inside the ComfyUI folder and:
After this you should have everything installed and can proceed to running ComfyUI.
### Others:
[Intel Arc](https://github.com/comfyanonymous/ComfyUI/discussions/476)
Mac/MPS: There is basic support in the code but until someone makes some install instruction you are on your own.
### I already have another UI for Stable Diffusion installed do I really have to install all of these dependencies?

263
comfy_extras/nodes_mask.py Normal file
View File

@ -0,0 +1,263 @@
import torch
from nodes import MAX_RESOLUTION
class LatentCompositeMasked:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"destination": ("LATENT",),
"source": ("LATENT",),
"x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
"y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
},
"optional": {
"mask": ("MASK",),
}
}
RETURN_TYPES = ("LATENT",)
FUNCTION = "composite"
CATEGORY = "latent"
def composite(self, destination, source, x, y, mask = None):
output = destination.copy()
destination = destination["samples"].clone()
source = source["samples"]
x = max(-source.shape[3] * 8, min(x, destination.shape[3] * 8))
y = max(-source.shape[2] * 8, min(y, destination.shape[2] * 8))
left, top = (x // 8, y // 8)
right, bottom = (left + source.shape[3], top + source.shape[2],)
if mask is None:
mask = torch.ones_like(source)
else:
mask = mask.clone()
mask = torch.nn.functional.interpolate(mask[None, None], size=(source.shape[2], source.shape[3]), mode="bilinear")
mask = mask.repeat((source.shape[0], source.shape[1], 1, 1))
# calculate the bounds of the source that will be overlapping the destination
# this prevents the source trying to overwrite latent pixels that are out of bounds
# of the destination
visible_width, visible_height = (destination.shape[3] - left + min(0, x), destination.shape[2] - top + min(0, y),)
mask = mask[:, :, :visible_height, :visible_width]
inverse_mask = torch.ones_like(mask) - mask
source_portion = mask * source[:, :, :visible_height, :visible_width]
destination_portion = inverse_mask * destination[:, :, top:bottom, left:right]
destination[:, :, top:bottom, left:right] = source_portion + destination_portion
output["samples"] = destination
return (output,)
class MaskToImage:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mask": ("MASK",),
}
}
CATEGORY = "mask"
RETURN_TYPES = ("IMAGE",)
FUNCTION = "mask_to_image"
def mask_to_image(self, mask):
result = mask[None, :, :, None].expand(-1, -1, -1, 3)
return (result,)
class ImageToMask:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"channel": (["red", "green", "blue"],),
}
}
CATEGORY = "mask"
RETURN_TYPES = ("MASK",)
FUNCTION = "image_to_mask"
def image_to_mask(self, image, channel):
channels = ["red", "green", "blue"]
mask = image[0, :, :, channels.index(channel)]
return (mask,)
class SolidMask:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
"height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
}
}
CATEGORY = "mask"
RETURN_TYPES = ("MASK",)
FUNCTION = "solid"
def solid(self, value, width, height):
out = torch.full((height, width), value, dtype=torch.float32, device="cpu")
return (out,)
class InvertMask:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
}
}
CATEGORY = "mask"
RETURN_TYPES = ("MASK",)
FUNCTION = "invert"
def invert(self, mask):
out = 1.0 - mask
return (out,)
class CropMask:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
"y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
"width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
"height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
}
}
CATEGORY = "mask"
RETURN_TYPES = ("MASK",)
FUNCTION = "crop"
def crop(self, mask, x, y, width, height):
out = mask[y:y + height, x:x + width]
return (out,)
class MaskComposite:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"destination": ("MASK",),
"source": ("MASK",),
"x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
"y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
"operation": (["multiply", "add", "subtract"],),
}
}
CATEGORY = "mask"
RETURN_TYPES = ("MASK",)
FUNCTION = "combine"
def combine(self, destination, source, x, y, operation):
output = destination.clone()
left, top = (x, y,)
right, bottom = (min(left + source.shape[1], destination.shape[1]), min(top + source.shape[0], destination.shape[0]))
visible_width, visible_height = (right - left, bottom - top,)
source_portion = source[:visible_height, :visible_width]
destination_portion = destination[top:bottom, left:right]
match operation:
case "multiply":
output[top:bottom, left:right] = destination_portion * source_portion
case "add":
output[top:bottom, left:right] = destination_portion + source_portion
case "subtract":
output[top:bottom, left:right] = destination_portion - source_portion
output = torch.clamp(output, 0.0, 1.0)
return (output,)
class FeatherMask:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"left": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
"top": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
"right": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
"bottom": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
}
}
CATEGORY = "mask"
RETURN_TYPES = ("MASK",)
FUNCTION = "feather"
def feather(self, mask, left, top, right, bottom):
output = mask.clone()
left = min(left, output.shape[1])
right = min(right, output.shape[1])
top = min(top, output.shape[0])
bottom = min(bottom, output.shape[0])
for x in range(left):
feather_rate = (x + 1.0) / left
output[:, x] *= feather_rate
for x in range(right):
feather_rate = (x + 1) / right
output[:, -x] *= feather_rate
for y in range(top):
feather_rate = (y + 1) / top
output[y, :] *= feather_rate
for y in range(bottom):
feather_rate = (y + 1) / bottom
output[-y, :] *= feather_rate
return (output,)
NODE_CLASS_MAPPINGS = {
"LatentCompositeMasked": LatentCompositeMasked,
"MaskToImage": MaskToImage,
"ImageToMask": ImageToMask,
"SolidMask": SolidMask,
"InvertMask": InvertMask,
"CropMask": CropMask,
"MaskComposite": MaskComposite,
"FeatherMask": FeatherMask,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ImageToMask": "Convert Image to Mask",
"MaskToImage": "Convert Mask to Image",
}

View File

@ -947,7 +947,7 @@ class SaveImage:
"filename": file,
"subfolder": subfolder,
"type": self.type
});
})
counter += 1
return { "ui": { "images": results } }
@ -1008,7 +1008,7 @@ class LoadImageMask:
"channel": (["alpha", "red", "green", "blue"], ),}
}
CATEGORY = "image"
CATEGORY = "mask"
RETURN_TYPES = ("MASK",)
FUNCTION = "load_image"
@ -1016,6 +1016,8 @@ class LoadImageMask:
input_dir = folder_paths.get_input_directory()
image_path = os.path.join(input_dir, image)
i = Image.open(image_path)
if i.getbands() != ("R", "G", "B", "A"):
i = i.convert("RGBA")
mask = None
c = channel[0].upper()
if c in i.getbands():
@ -1267,3 +1269,4 @@ def init_custom_nodes():
load_custom_nodes()
load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_upscale_model.py"))
load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_post_processing.py"))
load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_mask.py"))

View File

@ -119,9 +119,20 @@
"\n",
"\n",
"# ControlNet\n",
"#!wget -c https://huggingface.co/webui/ControlNet-modules-safetensors/resolve/main/control_depth-fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/webui/ControlNet-modules-safetensors/resolve/main/control_scribble-fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/webui/ControlNet-modules-safetensors/resolve/main/control_openpose-fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_ip2p_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_shuffle_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_canny_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_depth_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_inpaint_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_lineart_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_mlsd_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_normalbae_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_seg_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_softedge_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15s2_lineart_anime_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11u_sd15_tile_fp16.safetensors -P ./models/controlnet/\n",
"\n",
"\n",
"# Controlnet Preprocessor nodes by Fannovel16\n",

View File

@ -5,9 +5,9 @@ import { api } from "/scripts/api.js";
// Manage color palettes
const colorPalettes = {
"palette_1": {
"id": "palette_1",
"name": "Palette 1",
"dark": {
"id": "dark",
"name": "Dark (Default)",
"colors": {
"node_slot": {
"CLIP": "#FFD500", // bright yellow
@ -45,6 +45,70 @@ const colorPalettes = {
"EVENT_LINK_COLOR": "#A86",
"CONNECTING_LINK_COLOR": "#AFA",
},
"comfy_base": {
"fg-color": "#fff",
"bg-color": "#202020",
"comfy-menu-bg": "#353535",
"comfy-input-bg": "#222",
"input-text": "#ddd",
"descrip-text": "#999",
"drag-text": "#ccc",
"error-text": "#ff4444",
"border-color": "#4e4e4e"
}
},
},
"light": {
"id": "light",
"name": "Light",
"colors": {
"node_slot": {
"CLIP": "#FFA726", // orange
"CLIP_VISION": "#5C6BC0", // indigo
"CLIP_VISION_OUTPUT": "#8D6E63", // brown
"CONDITIONING": "#EF5350", // red
"CONTROL_NET": "#66BB6A", // green
"IMAGE": "#42A5F5", // blue
"LATENT": "#AB47BC", // purple
"MASK": "#9CCC65", // light green
"MODEL": "#7E57C2", // deep purple
"STYLE_MODEL": "#D4E157", // lime
"VAE": "#FF7043", // deep orange
},
"litegraph_base": {
"NODE_TITLE_COLOR": "#222",
"NODE_SELECTED_TITLE_COLOR": "#000",
"NODE_TEXT_SIZE": 14,
"NODE_TEXT_COLOR": "#444",
"NODE_SUBTEXT_SIZE": 12,
"NODE_DEFAULT_COLOR": "#F7F7F7",
"NODE_DEFAULT_BGCOLOR": "#F5F5F5",
"NODE_DEFAULT_BOXCOLOR": "#CCC",
"NODE_DEFAULT_SHAPE": "box",
"NODE_BOX_OUTLINE_COLOR": "#000",
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.1)",
"DEFAULT_GROUP_FONT": 24,
"WIDGET_BGCOLOR": "#D4D4D4",
"WIDGET_OUTLINE_COLOR": "#999",
"WIDGET_TEXT_COLOR": "#222",
"WIDGET_SECONDARY_TEXT_COLOR": "#555",
"LINK_COLOR": "#4CAF50",
"EVENT_LINK_COLOR": "#FF9800",
"CONNECTING_LINK_COLOR": "#2196F3",
},
"comfy_base": {
"fg-color": "#222",
"bg-color": "#DDD",
"comfy-menu-bg": "#F5F5F5",
"comfy-input-bg": "#C9C9C9",
"input-text": "#222",
"descrip-text": "#444",
"drag-text": "#555",
"error-text": "#F44336",
"border-color": "#888"
}
},
},
"solarized": {
@ -52,49 +116,60 @@ const colorPalettes = {
"name": "Solarized",
"colors": {
"node_slot": {
"CLIP": "#859900", // Green
"CLIP_VISION": "#6c71c4", // Indigo
"CLIP_VISION_OUTPUT": "#859900", // Green
"CONDITIONING": "#d33682", // Magenta
"CONTROL_NET": "#cb4b16", // Orange
"IMAGE": "#dc322f", // Red
"LATENT": "#268bd2", // Blue
"MASK": "#073642", // Base02
"MODEL": "#cb4b16", // Orange
"STYLE_MODEL": "#073642", // Base02
"UPSCALE_MODEL": "#6c71c4", // Indigo
"VAE": "#586e75", // Base1
"CLIP": "#2AB7CA", // light blue
"CLIP_VISION": "#6c71c4", // blue violet
"CLIP_VISION_OUTPUT": "#859900", // olive green
"CONDITIONING": "#d33682", // magenta
"CONTROL_NET": "#d1ffd7", // light mint green
"IMAGE": "#5940bb", // deep blue violet
"LATENT": "#268bd2", // blue
"MASK": "#CCC9E7", // light purple-gray
"MODEL": "#dc322f", // red
"STYLE_MODEL": "#1a998a", // teal
"UPSCALE_MODEL": "#054A29", // dark green
"VAE": "#facfad", // light pink-orange
},
"litegraph_base": {
"NODE_TITLE_COLOR": "#fdf6e3",
"NODE_SELECTED_TITLE_COLOR": "#b58900",
"NODE_TITLE_COLOR": "#fdf6e3", // Base3
"NODE_SELECTED_TITLE_COLOR": "#A9D400",
"NODE_TEXT_SIZE": 14,
"NODE_TEXT_COLOR": "#657b83",
"NODE_TEXT_COLOR": "#657b83", // Base00
"NODE_SUBTEXT_SIZE": 12,
"NODE_DEFAULT_COLOR": "#586e75",
"NODE_DEFAULT_BGCOLOR": "#073642",
"NODE_DEFAULT_BOXCOLOR": "#839496",
"NODE_DEFAULT_COLOR": "#094656",
"NODE_DEFAULT_BGCOLOR": "#073642", // Base02
"NODE_DEFAULT_BOXCOLOR": "#839496", // Base0
"NODE_DEFAULT_SHAPE": "box",
"NODE_BOX_OUTLINE_COLOR": "#fdf6e3",
"NODE_BOX_OUTLINE_COLOR": "#fdf6e3", // Base3
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
"DEFAULT_GROUP_FONT": 24,
"WIDGET_BGCOLOR": "#002b36",
"WIDGET_OUTLINE_COLOR": "#839496",
"WIDGET_TEXT_COLOR": "#fdf6e3",
"WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1",
"WIDGET_BGCOLOR": "#002b36", // Base03
"WIDGET_OUTLINE_COLOR": "#839496", // Base0
"WIDGET_TEXT_COLOR": "#fdf6e3", // Base3
"WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1", // Base1
"LINK_COLOR": "#2aa198",
"EVENT_LINK_COLOR": "#268bd2",
"CONNECTING_LINK_COLOR": "#859900",
"LINK_COLOR": "#2aa198", // Solarized Cyan
"EVENT_LINK_COLOR": "#268bd2", // Solarized Blue
"CONNECTING_LINK_COLOR": "#859900", // Solarized Green
},
"comfy_base": {
"fg-color": "#fdf6e3", // Base3
"bg-color": "#002b36", // Base03
"comfy-menu-bg": "#073642", // Base02
"comfy-input-bg": "#002b36", // Base03
"input-text": "#93a1a1", // Base1
"descrip-text": "#586e75", // Base01
"drag-text": "#839496", // Base0
"error-text": "#dc322f", // Solarized Red
"border-color": "#657b83" // Base00
}
},
}
};
const id = "Comfy.ColorPalette";
const idCustomColorPalettes = "Comfy.CustomColorPalettes";
const defaultColorPaletteId = "palette_1";
const defaultColorPaletteId = "dark";
const els = {}
// const ctxMenu = LiteGraph.ContextMenu;
app.registerExtension({
@ -236,10 +311,12 @@ app.registerExtension({
const loadColorPalette = async (colorPalette) => {
colorPalette = await completeColorPalette(colorPalette);
if (colorPalette.colors) {
// Sets the colors of node slots and links
if (colorPalette.colors.node_slot) {
Object.assign(app.canvas.default_connection_color_byType, colorPalette.colors.node_slot);
Object.assign(LGraphCanvas.link_type_colors, colorPalette.colors.node_slot);
}
// Sets the colors of the LiteGraph objects
if (colorPalette.colors.litegraph_base) {
// Everything updates correctly in the loop, except the Node Title and Link Color for some reason
app.canvas.node_title_color = colorPalette.colors.litegraph_base.NODE_TITLE_COLOR;
@ -251,6 +328,13 @@ app.registerExtension({
}
}
}
// Sets the color of ComfyUI elements
if (colorPalette.colors.comfy_base) {
const rootStyle = document.documentElement.style;
for (const key in colorPalette.colors.comfy_base) {
rootStyle.setProperty('--' + key, colorPalette.colors.comfy_base[key]);
}
}
app.canvas.draw(true, true);
}
};

View File

@ -90,7 +90,7 @@ app.registerExtension({
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
if (!this.properties || !("Node name for S&R" in this.properties)) {
this.addProperty("Node name for S&R", this.title, "string");
this.addProperty("Node name for S&R", this.constructor.type, "string");
}
return r;

View File

@ -1,4 +1,4 @@
import { ComfyWidgets, addRandomizeWidget } from "/scripts/widgets.js";
import { ComfyWidgets, addValueControlWidget } from "/scripts/widgets.js";
import { app } from "/scripts/app.js";
const CONVERTED_TYPE = "converted-widget";
@ -23,7 +23,7 @@ function hideWidget(node, widget, suffix = "") {
return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
};
// Hide any linked widgets, e.g. seed+randomize
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidget(node, w, ":" + widget.name);
@ -40,7 +40,7 @@ function showWidget(widget) {
delete widget.origComputeSize;
delete widget.origSerializeValue;
// Hide any linked widgets, e.g. seed+randomize
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
showWidget(w);
@ -159,27 +159,31 @@ app.registerExtension({
const r = origOnInputDblClick ? origOnInputDblClick.apply(this, arguments) : undefined;
const input = this.inputs[slot];
if (input.widget && !input[ignoreDblClick]) {
const node = LiteGraph.createNode("PrimitiveNode");
app.graph.add(node);
// Calculate a position that wont directly overlap another node
const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]];
while (isNodeAtPos(pos)) {
pos[1] += LiteGraph.NODE_TITLE_HEIGHT;
}
node.pos = pos;
node.connect(0, this, slot);
node.title = input.name;
// Prevent adding duplicates due to triple clicking
input[ignoreDblClick] = true;
setTimeout(() => {
delete input[ignoreDblClick];
}, 300);
if (!input.widget || !input[ignoreDblClick])// Not a widget input or already handled input
{
if (!(input.type in ComfyWidgets)) return r;//also Not a ComfyWidgets input (do nothing)
}
// Create a primitive node
const node = LiteGraph.createNode("PrimitiveNode");
app.graph.add(node);
// Calculate a position that wont directly overlap another node
const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]];
while (isNodeAtPos(pos)) {
pos[1] += LiteGraph.NODE_TITLE_HEIGHT;
}
node.pos = pos;
node.connect(0, this, slot);
node.title = input.name;
// Prevent adding duplicates due to triple clicking
input[ignoreDblClick] = true;
setTimeout(() => {
delete input[ignoreDblClick];
}, 300);
return r;
};
},
@ -233,7 +237,9 @@ app.registerExtension({
// Fires before the link is made allowing us to reject it if it isn't valid
// No widget, we cant connect
if (!input.widget) return false;
if (!input.widget) {
if (!(input.type in ComfyWidgets)) return false;
}
if (this.outputs[slot].links?.length) {
return this.#isValidConnection(input);
@ -252,9 +258,17 @@ app.registerExtension({
const input = theirNode.inputs[link.target_slot];
if (!input) return;
const widget = input.widget;
const { type, linkType } = getWidgetType(widget.config);
var _widget;
if (!input.widget) {
if (!(input.type in ComfyWidgets)) return;
_widget = { "name": input.name, "config": [input.type, {}] }//fake widget
} else {
_widget = input.widget;
}
const widget = _widget;
const { type, linkType } = getWidgetType(widget.config);
// Update our output to restrict to the widget type
this.outputs[0].type = linkType;
this.outputs[0].name = type;
@ -274,7 +288,7 @@ app.registerExtension({
if (type in ComfyWidgets) {
widget = (ComfyWidgets[type](this, "value", inputData, app) || {}).widget;
} else {
widget = this.addWidget(type, "value", null, () => {}, {});
widget = this.addWidget(type, "value", null, () => { }, {});
}
if (node?.widgets && widget) {
@ -285,7 +299,7 @@ app.registerExtension({
}
if (widget.type === "number") {
addRandomizeWidget(this, widget, "Random after every gen");
addValueControlWidget(this, widget, "fixed");
}
// When our value changes, update other widgets to reflect our changes

View File

@ -142,6 +142,8 @@
pointerevents_method: "pointer", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now)
// TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary)
ctrl_shift_v_paste_connect_unselected_outputs: true, //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes
/**
* Register a node class so it can be listed when the user wants to create a new one
* @method registerNodeType
@ -253,13 +255,18 @@
* @param {String|Object} type name of the node or the node constructor itself
*/
unregisterNodeType: function(type) {
var base_class = type.constructor === String ? this.registered_node_types[type] : type;
if(!base_class)
throw("node type not found: " + type );
delete this.registered_node_types[base_class.type];
if(base_class.constructor.name)
delete this.Nodes[base_class.constructor.name];
},
const base_class =
type.constructor === String
? this.registered_node_types[type]
: type;
if (!base_class) {
throw "node type not found: " + type;
}
delete this.registered_node_types[base_class.type];
if (base_class.constructor.name) {
delete this.Nodes[base_class.constructor.name];
}
},
/**
* Save a slot type and his node
@ -267,38 +274,49 @@
* @param {String|Object} type name of the node or the node constructor itself
* @param {String} slot_type name of the slot type (variable type), eg. string, number, array, boolean, ..
*/
registerNodeAndSlotType: function(type,slot_type,out){
registerNodeAndSlotType: function(type, slot_type, out){
out = out || false;
var base_class = type.constructor === String && this.registered_node_types[type] !== "anonymous" ? this.registered_node_types[type] : type;
var sCN = base_class.constructor.type;
if (typeof slot_type == "string"){
var aTypes = slot_type.split(",");
}else if (slot_type == this.EVENT || slot_type == this.ACTION){
var aTypes = ["_event_"];
}else{
var aTypes = ["*"];
const base_class =
type.constructor === String &&
this.registered_node_types[type] !== "anonymous"
? this.registered_node_types[type]
: type;
const class_type = base_class.constructor.type;
let allTypes = [];
if (typeof slot_type === "string") {
allTypes = slot_type.split(",");
} else if (slot_type == this.EVENT || slot_type == this.ACTION) {
allTypes = ["_event_"];
} else {
allTypes = ["*"];
}
for (var i = 0; i < aTypes.length; ++i) {
var sT = aTypes[i]; //.toLowerCase();
if (sT === ""){
sT = "*";
for (let i = 0; i < allTypes.length; ++i) {
let slotType = allTypes[i];
if (slotType === "") {
slotType = "*";
}
var registerTo = out ? "registered_slot_out_types" : "registered_slot_in_types";
if (typeof this[registerTo][sT] == "undefined") this[registerTo][sT] = {nodes: []};
this[registerTo][sT].nodes.push(sCN);
const registerTo = out
? "registered_slot_out_types"
: "registered_slot_in_types";
if (this[registerTo][slotType] === undefined) {
this[registerTo][slotType] = { nodes: [] };
}
if (!this[registerTo][slotType].nodes.includes(class_type)) {
this[registerTo][slotType].nodes.push(class_type);
}
// check if is a new type
if (!out){
if (!this.slot_types_in.includes(sT.toLowerCase())){
this.slot_types_in.push(sT.toLowerCase());
if (!out) {
if (!this.slot_types_in.includes(slotType.toLowerCase())) {
this.slot_types_in.push(slotType.toLowerCase());
this.slot_types_in.sort();
}
}else{
if (!this.slot_types_out.includes(sT.toLowerCase())){
this.slot_types_out.push(sT.toLowerCase());
} else {
if (!this.slot_types_out.includes(slotType.toLowerCase())) {
this.slot_types_out.push(slotType.toLowerCase());
this.slot_types_out.sort();
}
}
@ -1616,7 +1634,8 @@
var nRet = null;
for (var i = nodes_list.length - 1; i >= 0; i--) {
var n = nodes_list[i];
if (n.isPointInside(x, y, margin)) {
var skip_title = n.constructor.title_mode == LiteGraph.NO_TITLE;
if (n.isPointInside(x, y, margin, skip_title)) {
// check for lesser interest nodes (TODO check for overlapping, use the top)
/*if (typeof n == "LGraphGroup"){
nRet = n;
@ -3967,8 +3986,8 @@
var aSource = (type+"").toLowerCase().split(",");
var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type;
aDest = (aDest+"").toLowerCase().split(",");
for(sI=0;sI<aSource.length;sI++){
for(dI=0;dI<aDest.length;dI++){
for(var sI=0;sI<aSource.length;sI++){
for(var dI=0;dI<aDest.length;dI++){
if (aSource[sI]=="_event_") aSource[sI] = LiteGraph.EVENT;
if (aDest[sI]=="_event_") aDest[sI] = LiteGraph.EVENT;
if (aSource[sI]=="*") aSource[sI] = 0;
@ -3987,8 +4006,8 @@
var aSource = (type+"").toLowerCase().split(",");
var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type;
aDest = (aDest+"").toLowerCase().split(",");
for(sI=0;sI<aSource.length;sI++){
for(dI=0;dI<aDest.length;dI++){
for(var sI=0;sI<aSource.length;sI++){
for(var dI=0;dI<aDest.length;dI++){
if (aSource[sI]=="*") aSource[sI] = 0;
if (aDest[sI]=="*") aDest[sI] = 0;
if (aSource[sI] == aDest[dI]) {
@ -4019,7 +4038,7 @@
if (target_node && target_node.constructor === Number) {
target_node = this.graph.getNodeById(target_node);
}
target_slot = target_node.findInputSlotByType(target_slotType, false, true);
var target_slot = target_node.findInputSlotByType(target_slotType, false, true);
if (target_slot >= 0 && target_slot !== null){
//console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot)
return this.connect(slot, target_node, target_slot);
@ -4072,7 +4091,7 @@
if (source_node && source_node.constructor === Number) {
source_node = this.graph.getNodeById(source_node);
}
source_slot = source_node.findOutputSlotByType(source_slotType, false, true);
var source_slot = source_node.findOutputSlotByType(source_slotType, false, true);
if (source_slot >= 0 && source_slot !== null){
//console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot)
return source_node.connect(source_slot, this, slot);
@ -5184,6 +5203,7 @@ LGraphNode.prototype.executeAction = function(action)
this.editor_alpha = 1; //used for transition
this.pause_rendering = false;
this.clear_background = true;
this.clear_background_color = "#222";
this.read_only = false; //if set to true users cannot modify the graph
this.render_only_selected = true;
@ -6986,7 +7006,7 @@ LGraphNode.prototype.executeAction = function(action)
block_default = true;
}
if (e.code == "KeyC" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
if ((e.keyCode === 67) && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
//copy
if (this.selected_nodes) {
this.copyToClipboard();
@ -6994,9 +7014,9 @@ LGraphNode.prototype.executeAction = function(action)
}
}
if (e.code == "KeyV" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
if ((e.keyCode === 86) && (e.metaKey || e.ctrlKey)) {
//paste
this.pasteFromClipboard();
this.pasteFromClipboard(e.shiftKey);
}
//delete or backspace
@ -7081,15 +7101,15 @@ LGraphNode.prototype.executeAction = function(action)
var target_node = this.graph.getNodeById(
link_info.origin_id
);
if (!target_node || !this.selected_nodes[target_node.id]) {
//improve this by allowing connections to non-selected nodes
if (!target_node) {
continue;
} //not selected
}
clipboard_info.links.push([
target_node._relative_id,
link_info.origin_slot, //j,
node._relative_id,
link_info.target_slot
link_info.target_slot,
target_node.id
]);
}
}
@ -7100,7 +7120,11 @@ LGraphNode.prototype.executeAction = function(action)
);
};
LGraphCanvas.prototype.pasteFromClipboard = function() {
LGraphCanvas.prototype.pasteFromClipboard = function(isConnectUnselected = false) {
// if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior
if (!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) {
return;
}
var data = localStorage.getItem("litegrapheditor_clipboard");
if (!data) {
return;
@ -7149,7 +7173,16 @@ LGraphNode.prototype.executeAction = function(action)
//create links
for (var i = 0; i < clipboard_info.links.length; ++i) {
var link_info = clipboard_info.links[i];
var origin_node = nodes[link_info[0]];
var origin_node;
var origin_node_relative_id = link_info[0];
if (origin_node_relative_id != null) {
origin_node = nodes[origin_node_relative_id];
} else if (LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) {
var origin_node_id = link_info[4];
if (origin_node_id) {
origin_node = this.graph.getNodeById(origin_node_id);
}
}
var target_node = nodes[link_info[2]];
if( origin_node && target_node )
origin_node.connect(link_info[1], target_node, link_info[3]);
@ -8212,6 +8245,17 @@ LGraphNode.prototype.executeAction = function(action)
this.ds.toCanvasContext(ctx);
//render BG
if ( this.ds.scale < 1.5 && !bg_already_painted && this.clear_background_color )
{
ctx.fillStyle = this.clear_background_color;
ctx.fillRect(
this.visible_area[0],
this.visible_area[1],
this.visible_area[2],
this.visible_area[3]
);
}
if (
this.background_image &&
this.ds.scale > 0.5 &&
@ -12274,7 +12318,7 @@ LGraphNode.prototype.executeAction = function(action)
var aProps = LiteGraph.availableCanvasOptions;
aProps.sort();
for(pI in aProps){
for(var pI in aProps){
var pX = aProps[pI];
panel.addWidget( "boolean", pX, graphcanvas[pX], {key: pX, on: "True", off: "False"}, fUpdate);
}

View File

@ -362,8 +362,20 @@ class ComfyApp {
if (n && n.onDragDrop && (await n.onDragDrop(event))) {
return;
}
// Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that
if (event.dataTransfer.files.length && event.dataTransfer.files[0].type !== "image/bmp") {
await this.handleFile(event.dataTransfer.files[0]);
} else {
// Try loading the first URI in the transfer list
const validTypes = ["text/uri-list", "text/x-moz-url"];
const match = [...event.dataTransfer.types].find((t) => validTypes.find(v => t === v));
if (match) {
const uri = event.dataTransfer.getData(match)?.split("\n")?.[0];
if (uri) {
await this.handleFile(await (await fetch(uri)).blob());
}
}
}
});
// Always clear over node on drag leave
@ -938,6 +950,15 @@ class ComfyApp {
}
}
}
if (node.type == "KSampler" || node.type == "KSamplerAdvanced" || node.type == "PrimitiveNode") {
if (widget.name == "control_after_generate") {
if (widget.value === true) {
widget.value = "randomize";
} else if (widget.value === false) {
widget.value = "fixed";
}
}
}
}
}
@ -1090,7 +1111,7 @@ class ComfyApp {
importA1111(this.graph, pngInfo.parameters);
}
}
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
} else if (file.type === "application/json" || file.name?.endsWith(".json")) {
const reader = new FileReader();
reader.onload = () => {
this.loadGraphData(JSON.parse(reader.result));

View File

@ -10,37 +10,54 @@ function getNumberDefaults(inputData, defaultStep) {
return { val: defaultVal, config: { min, max, step: 10.0 * step } };
}
export function addRandomizeWidget(node, targetWidget, name, defaultValue = false) {
const randomize = node.addWidget("toggle", name, defaultValue, function (v) {}, {
on: "enabled",
off: "disabled",
serialize: false, // Don't include this in prompt.
});
export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values) {
const valueControl = node.addWidget("combo", "control_after_generate", defaultValue, function (v) { }, {
values: ["fixed", "increment", "decrement", "randomize"],
serialize: false, // Don't include this in prompt.
});
valueControl.afterQueued = () => {
randomize.afterQueued = () => {
if (randomize.value) {
const min = targetWidget.options?.min;
let max = targetWidget.options?.max;
if (min != null || max != null) {
if (max) {
// limit max to something that javascript can handle
max = Math.min(1125899906842624, max);
}
targetWidget.value = Math.floor(Math.random() * ((max ?? 9999999999) - (min ?? 0) + 1) + (min ?? 0));
} else {
targetWidget.value = Math.floor(Math.random() * 1125899906842624);
}
var v = valueControl.value;
let min = targetWidget.options.min;
let max = targetWidget.options.max;
// limit to something that javascript can handle
max = Math.min(1125899906842624, max);
min = Math.max(-1125899906842624, min);
let range = (max - min) / (targetWidget.options.step / 10);
//adjust values based on valueControl Behaviour
switch (v) {
case "fixed":
break;
case "increment":
targetWidget.value += targetWidget.options.step / 10;
break;
case "decrement":
targetWidget.value -= targetWidget.options.step / 10;
break;
case "randomize":
targetWidget.value = Math.floor(Math.random() * range) * (targetWidget.options.step / 10) + min;
default:
break;
}
};
return randomize;
}
/*check if values are over or under their respective
* ranges and set them to min or max.*/
if (targetWidget.value < min)
targetWidget.value = min;
if (targetWidget.value > max)
targetWidget.value = max;
}
return valueControl;
};
function seedWidget(node, inputName, inputData) {
const seed = ComfyWidgets.INT(node, inputName, inputData);
const randomize = addRandomizeWidget(node, seed.widget, "Random seed after every gen", true);
const seedControl = addValueControlWidget(node, seed.widget, "randomize");
seed.widget.linkedWidgets = [randomize];
return { widget: seed, randomize };
seed.widget.linkedWidgets = [seedControl];
return seed;
}
const MultilineSymbol = Symbol();

View File

@ -1,6 +1,13 @@
:root {
--fg-color: #000;
--bg-color: #fff;
--comfy-menu-bg: #353535;
--comfy-input-bg: #222;
--input-text: #ddd;
--descrip-text: #999;
--drag-text: #ccc;
--error-text: #ff4444;
--border-color: #4e4e4e;
}
@media (prefers-color-scheme: dark) {
@ -25,8 +32,8 @@ body {
}
.comfy-multiline-input {
background-color: var(--bg-color);
color: var(--fg-color);
background-color: var(--comfy-input-bg);
color: var(--input-text);
overflow: hidden;
overflow-y: auto;
padding: 2px;
@ -39,8 +46,8 @@ body {
position: fixed; /* Stay in place */
z-index: 100; /* Sit on top */
padding: 30px 30px 10px 30px;
background-color: #353535; /* Modal background */
color: #ff4444;
background-color: var(--comfy-menu-bg); /* Modal background */
color: var(--error-text);
box-shadow: 0px 0px 20px #888888;
border-radius: 10px;
top: 50%;
@ -82,8 +89,8 @@ body {
display: flex;
flex-direction: column;
align-items: center;
color: #999;
background-color: #353535;
color: var(--descrip-text);
background-color: var(--comfy-menu-bg);
font-family: sans-serif;
padding: 10px;
border-radius: 0 8px 8px 8px;
@ -103,7 +110,7 @@ body {
.comfy-menu-btns button {
font-size: 10px;
width: 50%;
color: #999 !important;
color: var(--descrip-text) !important;
}
.comfy-menu > button {
@ -114,10 +121,10 @@ body {
.comfy-menu-btns button,
.comfy-menu .comfy-list button,
.comfy-modal button{
color: #ddd;
background-color: #222;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: #4e4e4e;
border-color: var(--border-color);
border-style: solid;
margin-top: 2px;
}
@ -136,7 +143,7 @@ body {
font-size: 12px;
font-family: sans-serif;
letter-spacing: 2px;
color: #cccccc;
color: var(--drag-text);
text-shadow: 1px 0 1px black;
position: absolute;
top: 0;
@ -152,10 +159,10 @@ body {
}
.comfy-list {
color: #999;
background-color: #333;
color: var(--descrip-text);
background-color: var(--comfy-menu-bg);
margin-bottom: 10px;
border-color: #4e4e4e;
border-color: var(--border-color);
border-style: solid;
}
@ -163,7 +170,7 @@ body {
overflow-y: scroll;
max-height: 100px;
min-height: 25px;
background-color: #222;
background-color: var(--comfy-input-bg);
padding: 5px;
}
@ -206,16 +213,16 @@ button.comfy-queue-btn {
.comfy-modal.comfy-manage-templates {
text-align: center;
font-family: sans-serif;
color: #999;
color: var(--descrip-text);
z-index: 99;
}
.comfy-modal input,
.comfy-modal select {
color: #ddd;
background-color: #222;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: #4e4e4e;
border-color: var(--border-color);
border-style: solid;
font-size: inherit;
}
@ -240,7 +247,7 @@ button.comfy-queue-btn {
.graphdialog .name {
font-size: 14px;
font-family: sans-serif;
color: #999999;
color: var(--descrip-text);
}
.graphdialog button {
@ -251,10 +258,10 @@ button.comfy-queue-btn {
}
.graphdialog input, .graphdialog textarea, .graphdialog select {
background-color: #222;
background-color: var(--comfy-input-bg);
border: 2px solid;
border-color: #444444;
color: #ddd;
border-color: var(--border-color);
color: var(--input-text);
border-radius: 12px 0 0 12px;
}