mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-26 22:30:19 +08:00
Merge remote-tracking branch 'upstream/master' into addBatchIndex
This commit is contained in:
commit
a3e8713c6d
@ -56,6 +56,7 @@ Workflow examples can be found on the [Examples page](https://comfyanonymous.git
|
|||||||
| Q | Toggle visibility of the queue |
|
| Q | Toggle visibility of the queue |
|
||||||
| H | Toggle visibility of history |
|
| H | Toggle visibility of history |
|
||||||
| R | Refresh graph |
|
| R | Refresh graph |
|
||||||
|
| Double-Click LMB | Open node quick search palette |
|
||||||
|
|
||||||
Ctrl can also be replaced with Cmd instead for MacOS users
|
Ctrl can also be replaced with Cmd instead for MacOS users
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ parser.add_argument("--port", type=int, default=8188, help="Set the listen port.
|
|||||||
parser.add_argument("--enable-cors-header", type=str, default=None, metavar="ORIGIN", nargs="?", const="*", help="Enable CORS (Cross-Origin Resource Sharing) with optional origin or allow all with default '*'.")
|
parser.add_argument("--enable-cors-header", type=str, default=None, metavar="ORIGIN", nargs="?", const="*", help="Enable CORS (Cross-Origin Resource Sharing) with optional origin or allow all with default '*'.")
|
||||||
parser.add_argument("--extra-model-paths-config", type=str, default=None, metavar="PATH", nargs='+', action='append', help="Load one or more extra_model_paths.yaml files.")
|
parser.add_argument("--extra-model-paths-config", type=str, default=None, metavar="PATH", nargs='+', action='append', help="Load one or more extra_model_paths.yaml files.")
|
||||||
parser.add_argument("--output-directory", type=str, default=None, help="Set the ComfyUI output directory.")
|
parser.add_argument("--output-directory", type=str, default=None, help="Set the ComfyUI output directory.")
|
||||||
|
parser.add_argument("--auto-launch", action="store_true", help="Automatically launch ComfyUI in the default browser.")
|
||||||
parser.add_argument("--cuda-device", type=int, default=None, metavar="DEVICE_ID", help="Set the id of the cuda device this instance will use.")
|
parser.add_argument("--cuda-device", type=int, default=None, metavar="DEVICE_ID", help="Set the id of the cuda device this instance will use.")
|
||||||
parser.add_argument("--dont-upcast-attention", action="store_true", help="Disable upcasting of attention. Can boost speed but increase the chances of black images.")
|
parser.add_argument("--dont-upcast-attention", action="store_true", help="Disable upcasting of attention. Can boost speed but increase the chances of black images.")
|
||||||
parser.add_argument("--force-fp32", action="store_true", help="Force fp32 (If this makes your GPU work better please report it).")
|
parser.add_argument("--force-fp32", action="store_true", help="Force fp32 (If this makes your GPU work better please report it).")
|
||||||
@ -30,3 +31,6 @@ parser.add_argument("--quick-test-for-ci", action="store_true", help="Quick test
|
|||||||
parser.add_argument("--windows-standalone-build", action="store_true", help="Windows standalone build: Enable convenient things that most people using the standalone windows build will probably enjoy (like auto opening the page on startup).")
|
parser.add_argument("--windows-standalone-build", action="store_true", help="Windows standalone build: Enable convenient things that most people using the standalone windows build will probably enjoy (like auto opening the page on startup).")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.windows_standalone_build:
|
||||||
|
args.auto_launch = True
|
||||||
|
|||||||
@ -242,14 +242,28 @@ class Gligen(nn.Module):
|
|||||||
self.position_net = position_net
|
self.position_net = position_net
|
||||||
self.key_dim = key_dim
|
self.key_dim = key_dim
|
||||||
self.max_objs = 30
|
self.max_objs = 30
|
||||||
|
self.lowvram = False
|
||||||
|
|
||||||
def _set_position(self, boxes, masks, positive_embeddings):
|
def _set_position(self, boxes, masks, positive_embeddings):
|
||||||
|
if self.lowvram == True:
|
||||||
|
self.position_net.to(boxes.device)
|
||||||
|
|
||||||
objs = self.position_net(boxes, masks, positive_embeddings)
|
objs = self.position_net(boxes, masks, positive_embeddings)
|
||||||
|
|
||||||
def func(key, x):
|
if self.lowvram == True:
|
||||||
module = self.module_list[key]
|
self.position_net.cpu()
|
||||||
return module(x, objs)
|
def func_lowvram(key, x):
|
||||||
return func
|
module = self.module_list[key]
|
||||||
|
module.to(x.device)
|
||||||
|
r = module(x, objs)
|
||||||
|
module.cpu()
|
||||||
|
return r
|
||||||
|
return func_lowvram
|
||||||
|
else:
|
||||||
|
def func(key, x):
|
||||||
|
module = self.module_list[key]
|
||||||
|
return module(x, objs)
|
||||||
|
return func
|
||||||
|
|
||||||
def set_position(self, latent_image_shape, position_params, device):
|
def set_position(self, latent_image_shape, position_params, device):
|
||||||
batch, c, h, w = latent_image_shape
|
batch, c, h, w = latent_image_shape
|
||||||
@ -294,8 +308,11 @@ class Gligen(nn.Module):
|
|||||||
masks.to(device),
|
masks.to(device),
|
||||||
conds.to(device))
|
conds.to(device))
|
||||||
|
|
||||||
|
def set_lowvram(self, value=True):
|
||||||
|
self.lowvram = value
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
pass
|
self.lowvram = False
|
||||||
|
|
||||||
def get_models(self):
|
def get_models(self):
|
||||||
return [self]
|
return [self]
|
||||||
|
|||||||
@ -572,9 +572,6 @@ class BasicTransformerBlock(nn.Module):
|
|||||||
|
|
||||||
x += n
|
x += n
|
||||||
x = self.ff(self.norm3(x)) + x
|
x = self.ff(self.norm3(x)) + x
|
||||||
|
|
||||||
if current_index is not None:
|
|
||||||
transformer_options["current_index"] += 1
|
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -88,6 +88,19 @@ class TimestepEmbedSequential(nn.Sequential, TimestepBlock):
|
|||||||
x = layer(x)
|
x = layer(x)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
#This is needed because accelerate makes a copy of transformer_options which breaks "current_index"
|
||||||
|
def forward_timestep_embed(ts, x, emb, context=None, transformer_options={}, output_shape=None):
|
||||||
|
for layer in ts:
|
||||||
|
if isinstance(layer, TimestepBlock):
|
||||||
|
x = layer(x, emb)
|
||||||
|
elif isinstance(layer, SpatialTransformer):
|
||||||
|
x = layer(x, context, transformer_options)
|
||||||
|
transformer_options["current_index"] += 1
|
||||||
|
elif isinstance(layer, Upsample):
|
||||||
|
x = layer(x, output_shape=output_shape)
|
||||||
|
else:
|
||||||
|
x = layer(x)
|
||||||
|
return x
|
||||||
|
|
||||||
class Upsample(nn.Module):
|
class Upsample(nn.Module):
|
||||||
"""
|
"""
|
||||||
@ -805,13 +818,13 @@ class UNetModel(nn.Module):
|
|||||||
|
|
||||||
h = x.type(self.dtype)
|
h = x.type(self.dtype)
|
||||||
for id, module in enumerate(self.input_blocks):
|
for id, module in enumerate(self.input_blocks):
|
||||||
h = module(h, emb, context, transformer_options)
|
h = forward_timestep_embed(module, h, emb, context, transformer_options)
|
||||||
if control is not None and 'input' in control and len(control['input']) > 0:
|
if control is not None and 'input' in control and len(control['input']) > 0:
|
||||||
ctrl = control['input'].pop()
|
ctrl = control['input'].pop()
|
||||||
if ctrl is not None:
|
if ctrl is not None:
|
||||||
h += ctrl
|
h += ctrl
|
||||||
hs.append(h)
|
hs.append(h)
|
||||||
h = self.middle_block(h, emb, context, transformer_options)
|
h = forward_timestep_embed(self.middle_block, h, emb, context, transformer_options)
|
||||||
if control is not None and 'middle' in control and len(control['middle']) > 0:
|
if control is not None and 'middle' in control and len(control['middle']) > 0:
|
||||||
h += control['middle'].pop()
|
h += control['middle'].pop()
|
||||||
|
|
||||||
@ -828,7 +841,7 @@ class UNetModel(nn.Module):
|
|||||||
output_shape = hs[-1].shape
|
output_shape = hs[-1].shape
|
||||||
else:
|
else:
|
||||||
output_shape = None
|
output_shape = None
|
||||||
h = module(h, emb, context, transformer_options, output_shape)
|
h = forward_timestep_embed(module, h, emb, context, transformer_options, output_shape)
|
||||||
h = h.type(x.dtype)
|
h = h.type(x.dtype)
|
||||||
if self.predict_codebook_ids:
|
if self.predict_codebook_ids:
|
||||||
return self.id_predictor(h)
|
return self.id_predictor(h)
|
||||||
|
|||||||
@ -201,6 +201,9 @@ def load_controlnet_gpu(control_models):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if vram_state == VRAMState.LOW_VRAM or vram_state == VRAMState.NO_VRAM:
|
if vram_state == VRAMState.LOW_VRAM or vram_state == VRAMState.NO_VRAM:
|
||||||
|
for m in control_models:
|
||||||
|
if hasattr(m, 'set_lowvram'):
|
||||||
|
m.set_lowvram(True)
|
||||||
#don't load controlnets like this if low vram because they will be loaded right before running and unloaded right after
|
#don't load controlnets like this if low vram because they will be loaded right before running and unloaded right after
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -272,8 +275,17 @@ def xformers_enabled_vae():
|
|||||||
return XFORMERS_ENABLED_VAE
|
return XFORMERS_ENABLED_VAE
|
||||||
|
|
||||||
def pytorch_attention_enabled():
|
def pytorch_attention_enabled():
|
||||||
|
global ENABLE_PYTORCH_ATTENTION
|
||||||
return ENABLE_PYTORCH_ATTENTION
|
return ENABLE_PYTORCH_ATTENTION
|
||||||
|
|
||||||
|
def pytorch_attention_flash_attention():
|
||||||
|
global ENABLE_PYTORCH_ATTENTION
|
||||||
|
if ENABLE_PYTORCH_ATTENTION:
|
||||||
|
#TODO: more reliable way of checking for flash attention?
|
||||||
|
if torch.version.cuda: #pytorch flash attention only works on Nvidia
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def get_free_memory(dev=None, torch_free_too=False):
|
def get_free_memory(dev=None, torch_free_too=False):
|
||||||
global xpu_available
|
global xpu_available
|
||||||
global directml_enabled
|
global directml_enabled
|
||||||
@ -309,7 +321,12 @@ def maximum_batch_area():
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
memory_free = get_free_memory() / (1024 * 1024)
|
memory_free = get_free_memory() / (1024 * 1024)
|
||||||
area = ((memory_free - 1024) * 0.9) / (0.6)
|
if xformers_enabled() or pytorch_attention_flash_attention():
|
||||||
|
#TODO: this needs to be tweaked
|
||||||
|
area = 20 * memory_free
|
||||||
|
else:
|
||||||
|
#TODO: this formula is because AMD sucks and has memory management issues which might be fixed in the future
|
||||||
|
area = ((memory_free - 1024) * 0.9) / (0.6)
|
||||||
return int(max(area, 0))
|
return int(max(area, 0))
|
||||||
|
|
||||||
def cpu_mode():
|
def cpu_mode():
|
||||||
|
|||||||
@ -362,19 +362,8 @@ def resolve_cond_masks(conditions, h, w, device):
|
|||||||
else:
|
else:
|
||||||
box = boxes[0]
|
box = boxes[0]
|
||||||
H, W, Y, X = (box[3] - box[1] + 1, box[2] - box[0] + 1, box[1], box[0])
|
H, W, Y, X = (box[3] - box[1] + 1, box[2] - box[0] + 1, box[1], box[0])
|
||||||
# Make sure the height and width are divisible by 8
|
H = max(8, H)
|
||||||
if X % 8 != 0:
|
W = max(8, W)
|
||||||
newx = X // 8 * 8
|
|
||||||
W = W + (X - newx)
|
|
||||||
X = newx
|
|
||||||
if Y % 8 != 0:
|
|
||||||
newy = Y // 8 * 8
|
|
||||||
H = H + (Y - newy)
|
|
||||||
Y = newy
|
|
||||||
if H % 8 != 0:
|
|
||||||
H = H + (8 - (H % 8))
|
|
||||||
if W % 8 != 0:
|
|
||||||
W = W + (8 - (W % 8))
|
|
||||||
area = (int(H), int(W), int(Y), int(X))
|
area = (int(H), int(W), int(Y), int(X))
|
||||||
modified['area'] = area
|
modified['area'] = area
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,12 @@ class Downsample(nn.Module):
|
|||||||
|
|
||||||
def forward(self, x):
|
def forward(self, x):
|
||||||
assert x.shape[1] == self.channels
|
assert x.shape[1] == self.channels
|
||||||
return self.op(x)
|
if not self.use_conv:
|
||||||
|
padding = [x.shape[2] % 2, x.shape[3] % 2]
|
||||||
|
self.op.padding = padding
|
||||||
|
|
||||||
|
x = self.op(x)
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
class ResnetBlock(nn.Module):
|
class ResnetBlock(nn.Module):
|
||||||
|
|||||||
13
main.py
13
main.py
@ -91,23 +91,16 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
threading.Thread(target=prompt_worker, daemon=True, args=(q,server,)).start()
|
threading.Thread(target=prompt_worker, daemon=True, args=(q,server,)).start()
|
||||||
|
|
||||||
address = args.listen
|
|
||||||
|
|
||||||
dont_print = args.dont_print_server
|
|
||||||
|
|
||||||
|
|
||||||
if args.output_directory:
|
if args.output_directory:
|
||||||
output_dir = os.path.abspath(args.output_directory)
|
output_dir = os.path.abspath(args.output_directory)
|
||||||
print(f"Setting output directory to: {output_dir}")
|
print(f"Setting output directory to: {output_dir}")
|
||||||
folder_paths.set_output_directory(output_dir)
|
folder_paths.set_output_directory(output_dir)
|
||||||
|
|
||||||
port = args.port
|
|
||||||
|
|
||||||
if args.quick_test_for_ci:
|
if args.quick_test_for_ci:
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
call_on_start = None
|
call_on_start = None
|
||||||
if args.windows_standalone_build:
|
if args.auto_launch:
|
||||||
def startup_server(address, port):
|
def startup_server(address, port):
|
||||||
import webbrowser
|
import webbrowser
|
||||||
webbrowser.open("http://{}:{}".format(address, port))
|
webbrowser.open("http://{}:{}".format(address, port))
|
||||||
@ -115,10 +108,10 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(run(server, address=address, port=port, verbose=not dont_print, call_on_start=call_on_start))
|
loop.run_until_complete(run(server, address=args.listen, port=args.port, verbose=not args.dont_print_server, call_on_start=call_on_start))
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
loop.run_until_complete(run(server, address=address, port=port, verbose=not dont_print, call_on_start=call_on_start))
|
loop.run_until_complete(run(server, address=args.listen, port=args.port, verbose=not args.dont_print_server, call_on_start=call_on_start))
|
||||||
|
|
||||||
cleanup_temp()
|
cleanup_temp()
|
||||||
|
|||||||
13
nodes.py
13
nodes.py
@ -105,15 +105,13 @@ class ConditioningSetArea:
|
|||||||
|
|
||||||
CATEGORY = "conditioning"
|
CATEGORY = "conditioning"
|
||||||
|
|
||||||
def append(self, conditioning, width, height, x, y, strength, min_sigma=0.0, max_sigma=99.0):
|
def append(self, conditioning, width, height, x, y, strength):
|
||||||
c = []
|
c = []
|
||||||
for t in conditioning:
|
for t in conditioning:
|
||||||
n = [t[0], t[1].copy()]
|
n = [t[0], t[1].copy()]
|
||||||
n[1]['area'] = (height // 8, width // 8, y // 8, x // 8)
|
n[1]['area'] = (height // 8, width // 8, y // 8, x // 8)
|
||||||
n[1]['strength'] = strength
|
n[1]['strength'] = strength
|
||||||
n[1]['set_area_to_bounds'] = False
|
n[1]['set_area_to_bounds'] = False
|
||||||
n[1]['min_sigma'] = min_sigma
|
|
||||||
n[1]['max_sigma'] = max_sigma
|
|
||||||
c.append(n)
|
c.append(n)
|
||||||
return (c, )
|
return (c, )
|
||||||
|
|
||||||
@ -445,7 +443,6 @@ class ControlNetApply:
|
|||||||
def apply_controlnet(self, conditioning, control_net, image, strength):
|
def apply_controlnet(self, conditioning, control_net, image, strength):
|
||||||
c = []
|
c = []
|
||||||
control_hint = image.movedim(-1,1)
|
control_hint = image.movedim(-1,1)
|
||||||
print(control_hint.shape)
|
|
||||||
for t in conditioning:
|
for t in conditioning:
|
||||||
n = [t[0], t[1].copy()]
|
n = [t[0], t[1].copy()]
|
||||||
c_net = control_net.copy().set_cond_hint(control_hint, strength)
|
c_net = control_net.copy().set_cond_hint(control_hint, strength)
|
||||||
@ -975,8 +972,9 @@ class LoadImage:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
input_dir = folder_paths.get_input_directory()
|
input_dir = folder_paths.get_input_directory()
|
||||||
|
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
|
||||||
return {"required":
|
return {"required":
|
||||||
{"image": (sorted(os.listdir(input_dir)), )},
|
{"image": (sorted(files), )},
|
||||||
}
|
}
|
||||||
|
|
||||||
CATEGORY = "image"
|
CATEGORY = "image"
|
||||||
@ -1016,9 +1014,10 @@ class LoadImageMask:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
input_dir = folder_paths.get_input_directory()
|
input_dir = folder_paths.get_input_directory()
|
||||||
|
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
|
||||||
return {"required":
|
return {"required":
|
||||||
{"image": (sorted(os.listdir(input_dir)), ),
|
{"image": (sorted(files), ),
|
||||||
"channel": (s._color_channels, ),}
|
"channel": (s._color_channels, ), }
|
||||||
}
|
}
|
||||||
|
|
||||||
CATEGORY = "mask"
|
CATEGORY = "mask"
|
||||||
|
|||||||
130
server.py
130
server.py
@ -7,6 +7,9 @@ import execution
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
import glob
|
import glob
|
||||||
|
from PIL import Image
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@ -110,49 +113,90 @@ class PromptServer():
|
|||||||
files = glob.glob(os.path.join(self.web_root, 'extensions/**/*.js'), recursive=True)
|
files = glob.glob(os.path.join(self.web_root, 'extensions/**/*.js'), recursive=True)
|
||||||
return web.json_response(list(map(lambda f: "/" + os.path.relpath(f, self.web_root).replace("\\", "/"), files)))
|
return web.json_response(list(map(lambda f: "/" + os.path.relpath(f, self.web_root).replace("\\", "/"), files)))
|
||||||
|
|
||||||
@routes.post("/upload/image")
|
def get_dir_by_type(dir_type):
|
||||||
async def upload_image(request):
|
if dir_type is None:
|
||||||
post = await request.post()
|
type_dir = folder_paths.get_input_directory()
|
||||||
|
elif dir_type == "input":
|
||||||
|
type_dir = folder_paths.get_input_directory()
|
||||||
|
elif dir_type == "temp":
|
||||||
|
type_dir = folder_paths.get_temp_directory()
|
||||||
|
elif dir_type == "output":
|
||||||
|
type_dir = folder_paths.get_output_directory()
|
||||||
|
|
||||||
|
return type_dir
|
||||||
|
|
||||||
|
def image_upload(post, image_save_function=None):
|
||||||
image = post.get("image")
|
image = post.get("image")
|
||||||
|
|
||||||
if post.get("type") is None:
|
image_upload_type = post.get("type")
|
||||||
upload_dir = folder_paths.get_input_directory()
|
upload_dir = get_dir_by_type(image_upload_type)
|
||||||
elif post.get("type") == "input":
|
|
||||||
upload_dir = folder_paths.get_input_directory()
|
|
||||||
elif post.get("type") == "temp":
|
|
||||||
upload_dir = folder_paths.get_temp_directory()
|
|
||||||
elif post.get("type") == "output":
|
|
||||||
upload_dir = folder_paths.get_output_directory()
|
|
||||||
|
|
||||||
if not os.path.exists(upload_dir):
|
|
||||||
os.makedirs(upload_dir)
|
|
||||||
|
|
||||||
if image and image.file:
|
if image and image.file:
|
||||||
filename = image.filename
|
filename = image.filename
|
||||||
if not filename:
|
if not filename:
|
||||||
return web.Response(status=400)
|
return web.Response(status=400)
|
||||||
|
|
||||||
|
subfolder = post.get("subfolder", "")
|
||||||
|
full_output_folder = os.path.join(upload_dir, os.path.normpath(subfolder))
|
||||||
|
|
||||||
|
if os.path.commonpath((upload_dir, os.path.abspath(full_output_folder))) != upload_dir:
|
||||||
|
return web.Response(status=400)
|
||||||
|
|
||||||
|
if not os.path.exists(full_output_folder):
|
||||||
|
os.makedirs(full_output_folder)
|
||||||
|
|
||||||
split = os.path.splitext(filename)
|
split = os.path.splitext(filename)
|
||||||
|
filepath = os.path.join(full_output_folder, filename)
|
||||||
|
|
||||||
i = 1
|
i = 1
|
||||||
while os.path.exists(os.path.join(upload_dir, filename)):
|
while os.path.exists(filepath):
|
||||||
filename = f"{split[0]} ({i}){split[1]}"
|
filename = f"{split[0]} ({i}){split[1]}"
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
filepath = os.path.join(upload_dir, filename)
|
if image_save_function is not None:
|
||||||
|
image_save_function(image, post, filepath)
|
||||||
|
else:
|
||||||
|
with open(filepath, "wb") as f:
|
||||||
|
f.write(image.file.read())
|
||||||
|
|
||||||
with open(filepath, "wb") as f:
|
return web.json_response({"name" : filename, "subfolder": subfolder, "type": image_upload_type})
|
||||||
f.write(image.file.read())
|
|
||||||
|
|
||||||
return web.json_response({"name" : filename})
|
|
||||||
else:
|
else:
|
||||||
return web.Response(status=400)
|
return web.Response(status=400)
|
||||||
|
|
||||||
|
@routes.post("/upload/image")
|
||||||
|
async def upload_image(request):
|
||||||
|
post = await request.post()
|
||||||
|
return image_upload(post)
|
||||||
|
|
||||||
|
@routes.post("/upload/mask")
|
||||||
|
async def upload_mask(request):
|
||||||
|
post = await request.post()
|
||||||
|
|
||||||
|
def image_save_function(image, post, filepath):
|
||||||
|
original_pil = Image.open(post.get("original_image").file).convert('RGBA')
|
||||||
|
mask_pil = Image.open(image.file).convert('RGBA')
|
||||||
|
|
||||||
|
# alpha copy
|
||||||
|
new_alpha = mask_pil.getchannel('A')
|
||||||
|
original_pil.putalpha(new_alpha)
|
||||||
|
original_pil.save(filepath, compress_level=4)
|
||||||
|
|
||||||
|
return image_upload(post, image_save_function)
|
||||||
|
|
||||||
@routes.get("/view")
|
@routes.get("/view")
|
||||||
async def view_image(request):
|
async def view_image(request):
|
||||||
if "filename" in request.rel_url.query:
|
if "filename" in request.rel_url.query:
|
||||||
type = request.rel_url.query.get("type", "output")
|
filename = request.rel_url.query["filename"]
|
||||||
output_dir = folder_paths.get_directory_by_type(type)
|
filename,output_dir = folder_paths.annotated_filepath(filename)
|
||||||
|
|
||||||
|
# validation for security: prevent accessing arbitrary path
|
||||||
|
if filename[0] == '/' or '..' in filename:
|
||||||
|
return web.Response(status=400)
|
||||||
|
|
||||||
|
if output_dir is None:
|
||||||
|
type = request.rel_url.query.get("type", "output")
|
||||||
|
output_dir = folder_paths.get_directory_by_type(type)
|
||||||
|
|
||||||
if output_dir is None:
|
if output_dir is None:
|
||||||
return web.Response(status=400)
|
return web.Response(status=400)
|
||||||
|
|
||||||
@ -162,13 +206,49 @@ class PromptServer():
|
|||||||
return web.Response(status=403)
|
return web.Response(status=403)
|
||||||
output_dir = full_output_dir
|
output_dir = full_output_dir
|
||||||
|
|
||||||
filename = request.rel_url.query["filename"]
|
|
||||||
filename = os.path.basename(filename)
|
filename = os.path.basename(filename)
|
||||||
file = os.path.join(output_dir, filename)
|
file = os.path.join(output_dir, filename)
|
||||||
|
|
||||||
if os.path.isfile(file):
|
if os.path.isfile(file):
|
||||||
return web.FileResponse(file, headers={"Content-Disposition": f"filename=\"{filename}\""})
|
if 'channel' not in request.rel_url.query:
|
||||||
|
channel = 'rgba'
|
||||||
|
else:
|
||||||
|
channel = request.rel_url.query["channel"]
|
||||||
|
|
||||||
|
if channel == 'rgb':
|
||||||
|
with Image.open(file) as img:
|
||||||
|
if img.mode == "RGBA":
|
||||||
|
r, g, b, a = img.split()
|
||||||
|
new_img = Image.merge('RGB', (r, g, b))
|
||||||
|
else:
|
||||||
|
new_img = img.convert("RGB")
|
||||||
|
|
||||||
|
buffer = BytesIO()
|
||||||
|
new_img.save(buffer, format='PNG')
|
||||||
|
buffer.seek(0)
|
||||||
|
|
||||||
|
return web.Response(body=buffer.read(), content_type='image/png',
|
||||||
|
headers={"Content-Disposition": f"filename=\"{filename}\""})
|
||||||
|
|
||||||
|
elif channel == 'a':
|
||||||
|
with Image.open(file) as img:
|
||||||
|
if img.mode == "RGBA":
|
||||||
|
_, _, _, a = img.split()
|
||||||
|
else:
|
||||||
|
a = Image.new('L', img.size, 255)
|
||||||
|
|
||||||
|
# alpha img
|
||||||
|
alpha_img = Image.new('RGBA', img.size)
|
||||||
|
alpha_img.putalpha(a)
|
||||||
|
alpha_buffer = BytesIO()
|
||||||
|
alpha_img.save(alpha_buffer, format='PNG')
|
||||||
|
alpha_buffer.seek(0)
|
||||||
|
|
||||||
|
return web.Response(body=alpha_buffer.read(), content_type='image/png',
|
||||||
|
headers={"Content-Disposition": f"filename=\"{filename}\""})
|
||||||
|
else:
|
||||||
|
return web.FileResponse(file, headers={"Content-Disposition": f"filename=\"{filename}\""})
|
||||||
|
|
||||||
return web.Response(status=404)
|
return web.Response(status=404)
|
||||||
|
|
||||||
@routes.get("/prompt")
|
@routes.get("/prompt")
|
||||||
|
|||||||
166
web/extensions/core/clipspace.js
Normal file
166
web/extensions/core/clipspace.js
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { app } from "/scripts/app.js";
|
||||||
|
import { ComfyDialog, $el } from "/scripts/ui.js";
|
||||||
|
import { ComfyApp } from "/scripts/app.js";
|
||||||
|
|
||||||
|
export class ClipspaceDialog extends ComfyDialog {
|
||||||
|
static items = [];
|
||||||
|
static instance = null;
|
||||||
|
|
||||||
|
static registerButton(name, contextPredicate, callback) {
|
||||||
|
const item =
|
||||||
|
$el("button", {
|
||||||
|
type: "button",
|
||||||
|
textContent: name,
|
||||||
|
contextPredicate: contextPredicate,
|
||||||
|
onclick: callback
|
||||||
|
})
|
||||||
|
|
||||||
|
ClipspaceDialog.items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
static invalidatePreview() {
|
||||||
|
if(ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0) {
|
||||||
|
const img_preview = document.getElementById("clipspace_preview");
|
||||||
|
if(img_preview) {
|
||||||
|
img_preview.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
|
||||||
|
img_preview.style.maxHeight = "100%";
|
||||||
|
img_preview.style.maxWidth = "100%";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static invalidate() {
|
||||||
|
if(ClipspaceDialog.instance) {
|
||||||
|
const self = ClipspaceDialog.instance;
|
||||||
|
// allow reconstruct controls when copying from non-image to image content.
|
||||||
|
const children = $el("div.comfy-modal-content", [ self.createImgSettings(), ...self.createButtons() ]);
|
||||||
|
|
||||||
|
if(self.element) {
|
||||||
|
// update
|
||||||
|
self.element.removeChild(self.element.firstChild);
|
||||||
|
self.element.appendChild(children);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// new
|
||||||
|
self.element = $el("div.comfy-modal", { parent: document.body }, [children,]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(self.element.children[0].children.length <= 1) {
|
||||||
|
self.element.children[0].appendChild($el("p", {}, ["Unable to find the features to edit content of a format stored in the current Clipspace."]));
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipspaceDialog.invalidatePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
createButtons(self) {
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
|
for(let idx in ClipspaceDialog.items) {
|
||||||
|
const item = ClipspaceDialog.items[idx];
|
||||||
|
if(!item.contextPredicate || item.contextPredicate())
|
||||||
|
buttons.push(ClipspaceDialog.items[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
$el("button", {
|
||||||
|
type: "button",
|
||||||
|
textContent: "Close",
|
||||||
|
onclick: () => { this.close(); }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
createImgSettings() {
|
||||||
|
if(ComfyApp.clipspace.imgs) {
|
||||||
|
const combo_items = [];
|
||||||
|
const imgs = ComfyApp.clipspace.imgs;
|
||||||
|
|
||||||
|
for(let i=0; i < imgs.length; i++) {
|
||||||
|
combo_items.push($el("option", {value:i}, [`${i}`]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const combo1 = $el("select",
|
||||||
|
{id:"clipspace_img_selector", onchange:(event) => {
|
||||||
|
ComfyApp.clipspace['selectedIndex'] = event.target.selectedIndex;
|
||||||
|
ClipspaceDialog.invalidatePreview();
|
||||||
|
} }, combo_items);
|
||||||
|
|
||||||
|
const row1 =
|
||||||
|
$el("tr", {},
|
||||||
|
[
|
||||||
|
$el("td", {}, [$el("font", {color:"white"}, ["Select Image"])]),
|
||||||
|
$el("td", {}, [combo1])
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
const combo2 = $el("select",
|
||||||
|
{id:"clipspace_img_paste_mode", onchange:(event) => {
|
||||||
|
ComfyApp.clipspace['img_paste_mode'] = event.target.value;
|
||||||
|
} },
|
||||||
|
[
|
||||||
|
$el("option", {value:'selected'}, 'selected'),
|
||||||
|
$el("option", {value:'all'}, 'all')
|
||||||
|
]);
|
||||||
|
combo2.value = ComfyApp.clipspace['img_paste_mode'];
|
||||||
|
|
||||||
|
const row2 =
|
||||||
|
$el("tr", {},
|
||||||
|
[
|
||||||
|
$el("td", {}, [$el("font", {color:"white"}, ["Paste Mode"])]),
|
||||||
|
$el("td", {}, [combo2])
|
||||||
|
]);
|
||||||
|
|
||||||
|
const td = $el("td", {align:'center', width:'100px', height:'100px', colSpan:'2'},
|
||||||
|
[ $el("img",{id:"clipspace_preview", ondragstart:() => false},[]) ]);
|
||||||
|
|
||||||
|
const row3 =
|
||||||
|
$el("tr", {}, [td]);
|
||||||
|
|
||||||
|
return $el("table", {}, [row1, row2, row3]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createImgPreview() {
|
||||||
|
if(ComfyApp.clipspace.imgs) {
|
||||||
|
return $el("img",{id:"clipspace_preview", ondragstart:() => false});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
const img_preview = document.getElementById("clipspace_preview");
|
||||||
|
ClipspaceDialog.invalidate();
|
||||||
|
|
||||||
|
this.element.style.display = "block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "Comfy.Clipspace",
|
||||||
|
init(app) {
|
||||||
|
app.openClipspace =
|
||||||
|
function () {
|
||||||
|
if(!ClipspaceDialog.instance) {
|
||||||
|
ClipspaceDialog.instance = new ClipspaceDialog(app);
|
||||||
|
ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ComfyApp.clipspace) {
|
||||||
|
ClipspaceDialog.instance.show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
app.ui.dialog.show("Clipspace is Empty!");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
590
web/extensions/core/maskeditor.js
Normal file
590
web/extensions/core/maskeditor.js
Normal file
@ -0,0 +1,590 @@
|
|||||||
|
import { app } from "/scripts/app.js";
|
||||||
|
import { ComfyDialog, $el } from "/scripts/ui.js";
|
||||||
|
import { ComfyApp } from "/scripts/app.js";
|
||||||
|
import { ClipspaceDialog } from "/extensions/core/clipspace.js";
|
||||||
|
|
||||||
|
// Helper function to convert a data URL to a Blob object
|
||||||
|
function dataURLToBlob(dataURL) {
|
||||||
|
const parts = dataURL.split(';base64,');
|
||||||
|
const contentType = parts[0].split(':')[1];
|
||||||
|
const byteString = atob(parts[1]);
|
||||||
|
const arrayBuffer = new ArrayBuffer(byteString.length);
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
for (let i = 0; i < byteString.length; i++) {
|
||||||
|
uint8Array[i] = byteString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return new Blob([arrayBuffer], { type: contentType });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadedImageToBlob(image) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
|
||||||
|
const dataURL = canvas.toDataURL('image/png', 1);
|
||||||
|
const blob = dataURLToBlob(dataURL);
|
||||||
|
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadMask(filepath, formData) {
|
||||||
|
await fetch('/upload/mask', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).then(response => {}).catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image();
|
||||||
|
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = "/view?" + new URLSearchParams(filepath).toString();
|
||||||
|
|
||||||
|
if(ComfyApp.clipspace.images)
|
||||||
|
ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath;
|
||||||
|
|
||||||
|
ClipspaceDialog.invalidatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareRGB(image, backupCanvas, backupCtx) {
|
||||||
|
// paste mask data into alpha channel
|
||||||
|
backupCtx.drawImage(image, 0, 0, backupCanvas.width, backupCanvas.height);
|
||||||
|
const backupData = backupCtx.getImageData(0, 0, backupCanvas.width, backupCanvas.height);
|
||||||
|
|
||||||
|
// refine mask image
|
||||||
|
for (let i = 0; i < backupData.data.length; i += 4) {
|
||||||
|
if(backupData.data[i+3] == 255)
|
||||||
|
backupData.data[i+3] = 0;
|
||||||
|
else
|
||||||
|
backupData.data[i+3] = 255;
|
||||||
|
|
||||||
|
backupData.data[i] = 0;
|
||||||
|
backupData.data[i+1] = 0;
|
||||||
|
backupData.data[i+2] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
backupCtx.globalCompositeOperation = 'source-over';
|
||||||
|
backupCtx.putImageData(backupData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MaskEditorDialog extends ComfyDialog {
|
||||||
|
static instance = null;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.element = $el("div.comfy-modal", { parent: document.body },
|
||||||
|
[ $el("div.comfy-modal-content",
|
||||||
|
[...this.createButtons()]),
|
||||||
|
]);
|
||||||
|
MaskEditorDialog.instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
createButtons() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMask(self) {
|
||||||
|
}
|
||||||
|
|
||||||
|
createButton(name, callback) {
|
||||||
|
var button = document.createElement("button");
|
||||||
|
button.innerText = name;
|
||||||
|
button.addEventListener("click", callback);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
createLeftButton(name, callback) {
|
||||||
|
var button = this.createButton(name, callback);
|
||||||
|
button.style.cssFloat = "left";
|
||||||
|
button.style.marginRight = "4px";
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
createRightButton(name, callback) {
|
||||||
|
var button = this.createButton(name, callback);
|
||||||
|
button.style.cssFloat = "right";
|
||||||
|
button.style.marginLeft = "4px";
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
createLeftSlider(self, name, callback) {
|
||||||
|
const divElement = document.createElement('div');
|
||||||
|
divElement.id = "maskeditor-slider";
|
||||||
|
divElement.style.cssFloat = "left";
|
||||||
|
divElement.style.fontFamily = "sans-serif";
|
||||||
|
divElement.style.marginRight = "4px";
|
||||||
|
divElement.style.color = "var(--input-text)";
|
||||||
|
divElement.style.backgroundColor = "var(--comfy-input-bg)";
|
||||||
|
divElement.style.borderRadius = "8px";
|
||||||
|
divElement.style.borderColor = "var(--border-color)";
|
||||||
|
divElement.style.borderStyle = "solid";
|
||||||
|
divElement.style.fontSize = "15px";
|
||||||
|
divElement.style.height = "21px";
|
||||||
|
divElement.style.padding = "1px 6px";
|
||||||
|
divElement.style.display = "flex";
|
||||||
|
divElement.style.position = "relative";
|
||||||
|
divElement.style.top = "2px";
|
||||||
|
self.brush_slider_input = document.createElement('input');
|
||||||
|
self.brush_slider_input.setAttribute('type', 'range');
|
||||||
|
self.brush_slider_input.setAttribute('min', '1');
|
||||||
|
self.brush_slider_input.setAttribute('max', '100');
|
||||||
|
self.brush_slider_input.setAttribute('value', '10');
|
||||||
|
const labelElement = document.createElement("label");
|
||||||
|
labelElement.textContent = name;
|
||||||
|
|
||||||
|
divElement.appendChild(labelElement);
|
||||||
|
divElement.appendChild(self.brush_slider_input);
|
||||||
|
|
||||||
|
self.brush_slider_input.addEventListener("change", callback);
|
||||||
|
|
||||||
|
return divElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
setlayout(imgCanvas, maskCanvas) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
// If it is specified as relative, using it only as a hidden placeholder for padding is recommended
|
||||||
|
// to prevent anomalies where it exceeds a certain size and goes outside of the window.
|
||||||
|
var placeholder = document.createElement("div");
|
||||||
|
placeholder.style.position = "relative";
|
||||||
|
placeholder.style.height = "50px";
|
||||||
|
|
||||||
|
var bottom_panel = document.createElement("div");
|
||||||
|
bottom_panel.style.position = "absolute";
|
||||||
|
bottom_panel.style.bottom = "0px";
|
||||||
|
bottom_panel.style.left = "20px";
|
||||||
|
bottom_panel.style.right = "20px";
|
||||||
|
bottom_panel.style.height = "50px";
|
||||||
|
|
||||||
|
var brush = document.createElement("div");
|
||||||
|
brush.id = "brush";
|
||||||
|
brush.style.backgroundColor = "transparent";
|
||||||
|
brush.style.outline = "1px dashed black";
|
||||||
|
brush.style.boxShadow = "0 0 0 1px white";
|
||||||
|
brush.style.borderRadius = "50%";
|
||||||
|
brush.style.MozBorderRadius = "50%";
|
||||||
|
brush.style.WebkitBorderRadius = "50%";
|
||||||
|
brush.style.position = "absolute";
|
||||||
|
brush.style.zIndex = 100;
|
||||||
|
brush.style.pointerEvents = "none";
|
||||||
|
this.brush = brush;
|
||||||
|
this.element.appendChild(imgCanvas);
|
||||||
|
this.element.appendChild(maskCanvas);
|
||||||
|
this.element.appendChild(placeholder); // must below z-index than bottom_panel to avoid covering button
|
||||||
|
this.element.appendChild(bottom_panel);
|
||||||
|
document.body.appendChild(brush);
|
||||||
|
|
||||||
|
var brush_size_slider = this.createLeftSlider(self, "Thickness", (event) => {
|
||||||
|
self.brush_size = event.target.value;
|
||||||
|
self.updateBrushPreview(self, null, null);
|
||||||
|
});
|
||||||
|
var clearButton = this.createLeftButton("Clear",
|
||||||
|
() => {
|
||||||
|
self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
|
||||||
|
self.backupCtx.clearRect(0, 0, self.backupCanvas.width, self.backupCanvas.height);
|
||||||
|
});
|
||||||
|
var cancelButton = this.createRightButton("Cancel", () => {
|
||||||
|
document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
|
||||||
|
document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
|
||||||
|
self.close();
|
||||||
|
});
|
||||||
|
var saveButton = this.createRightButton("Save", () => {
|
||||||
|
document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
|
||||||
|
document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
|
||||||
|
self.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.element.appendChild(imgCanvas);
|
||||||
|
this.element.appendChild(maskCanvas);
|
||||||
|
this.element.appendChild(placeholder); // must below z-index than bottom_panel to avoid covering button
|
||||||
|
this.element.appendChild(bottom_panel);
|
||||||
|
|
||||||
|
bottom_panel.appendChild(clearButton);
|
||||||
|
bottom_panel.appendChild(saveButton);
|
||||||
|
bottom_panel.appendChild(cancelButton);
|
||||||
|
bottom_panel.appendChild(brush_size_slider);
|
||||||
|
|
||||||
|
this.element.style.display = "block";
|
||||||
|
imgCanvas.style.position = "relative";
|
||||||
|
imgCanvas.style.top = "200";
|
||||||
|
imgCanvas.style.left = "0";
|
||||||
|
|
||||||
|
maskCanvas.style.position = "absolute";
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
// layout
|
||||||
|
const imgCanvas = document.createElement('canvas');
|
||||||
|
const maskCanvas = document.createElement('canvas');
|
||||||
|
const backupCanvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
imgCanvas.id = "imageCanvas";
|
||||||
|
maskCanvas.id = "maskCanvas";
|
||||||
|
backupCanvas.id = "backupCanvas";
|
||||||
|
|
||||||
|
this.setlayout(imgCanvas, maskCanvas);
|
||||||
|
|
||||||
|
// prepare content
|
||||||
|
this.maskCanvas = maskCanvas;
|
||||||
|
this.backupCanvas = backupCanvas;
|
||||||
|
this.maskCtx = maskCanvas.getContext('2d');
|
||||||
|
this.backupCtx = backupCanvas.getContext('2d');
|
||||||
|
|
||||||
|
this.setImages(imgCanvas, backupCanvas);
|
||||||
|
this.setEventHandler(maskCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
setImages(imgCanvas, backupCanvas) {
|
||||||
|
const imgCtx = imgCanvas.getContext('2d');
|
||||||
|
const backupCtx = backupCanvas.getContext('2d');
|
||||||
|
const maskCtx = this.maskCtx;
|
||||||
|
const maskCanvas = this.maskCanvas;
|
||||||
|
|
||||||
|
// image load
|
||||||
|
const orig_image = new Image();
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
// repositioning
|
||||||
|
imgCanvas.width = window.innerWidth - 250;
|
||||||
|
imgCanvas.height = window.innerHeight - 200;
|
||||||
|
|
||||||
|
// redraw image
|
||||||
|
let drawWidth = orig_image.width;
|
||||||
|
let drawHeight = orig_image.height;
|
||||||
|
if (orig_image.width > imgCanvas.width) {
|
||||||
|
drawWidth = imgCanvas.width;
|
||||||
|
drawHeight = (drawWidth / orig_image.width) * orig_image.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawHeight > imgCanvas.height) {
|
||||||
|
drawHeight = imgCanvas.height;
|
||||||
|
drawWidth = (drawHeight / orig_image.height) * orig_image.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
imgCtx.drawImage(orig_image, 0, 0, drawWidth, drawHeight);
|
||||||
|
|
||||||
|
// update mask
|
||||||
|
backupCtx.drawImage(maskCanvas, 0, 0, maskCanvas.width, maskCanvas.height, 0, 0, backupCanvas.width, backupCanvas.height);
|
||||||
|
maskCanvas.width = drawWidth;
|
||||||
|
maskCanvas.height = drawHeight;
|
||||||
|
maskCanvas.style.top = imgCanvas.offsetTop + "px";
|
||||||
|
maskCanvas.style.left = imgCanvas.offsetLeft + "px";
|
||||||
|
maskCtx.drawImage(backupCanvas, 0, 0, backupCanvas.width, backupCanvas.height, 0, 0, maskCanvas.width, maskCanvas.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filepath = ComfyApp.clipspace.images;
|
||||||
|
|
||||||
|
const touched_image = new Image();
|
||||||
|
|
||||||
|
touched_image.onload = function() {
|
||||||
|
backupCanvas.width = touched_image.width;
|
||||||
|
backupCanvas.height = touched_image.height;
|
||||||
|
|
||||||
|
prepareRGB(touched_image, backupCanvas, backupCtx);
|
||||||
|
};
|
||||||
|
|
||||||
|
const alpha_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src)
|
||||||
|
alpha_url.searchParams.delete('channel');
|
||||||
|
alpha_url.searchParams.set('channel', 'a');
|
||||||
|
touched_image.src = alpha_url;
|
||||||
|
|
||||||
|
// original image load
|
||||||
|
orig_image.onload = function() {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const rgb_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src);
|
||||||
|
rgb_url.searchParams.delete('channel');
|
||||||
|
rgb_url.searchParams.set('channel', 'rgb');
|
||||||
|
orig_image.src = rgb_url;
|
||||||
|
this.image = orig_image;
|
||||||
|
}g
|
||||||
|
|
||||||
|
|
||||||
|
setEventHandler(maskCanvas) {
|
||||||
|
maskCanvas.addEventListener("contextmenu", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
maskCanvas.addEventListener('wheel', (event) => this.handleWheelEvent(self,event));
|
||||||
|
maskCanvas.addEventListener('pointerdown', (event) => this.handlePointerDown(self,event));
|
||||||
|
document.addEventListener('pointerup', MaskEditorDialog.handlePointerUp);
|
||||||
|
maskCanvas.addEventListener('pointermove', (event) => this.draw_move(self,event));
|
||||||
|
maskCanvas.addEventListener('touchmove', (event) => this.draw_move(self,event));
|
||||||
|
maskCanvas.addEventListener('pointerover', (event) => { this.brush.style.display = "block"; });
|
||||||
|
maskCanvas.addEventListener('pointerleave', (event) => { this.brush.style.display = "none"; });
|
||||||
|
document.addEventListener('keydown', MaskEditorDialog.handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
brush_size = 10;
|
||||||
|
drawing_mode = false;
|
||||||
|
lastx = -1;
|
||||||
|
lasty = -1;
|
||||||
|
lasttime = 0;
|
||||||
|
|
||||||
|
static handleKeyDown(event) {
|
||||||
|
const self = MaskEditorDialog.instance;
|
||||||
|
if (event.key === ']') {
|
||||||
|
self.brush_size = Math.min(self.brush_size+2, 100);
|
||||||
|
} else if (event.key === '[') {
|
||||||
|
self.brush_size = Math.max(self.brush_size-2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updateBrushPreview(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
static handlePointerUp(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
MaskEditorDialog.instance.drawing_mode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBrushPreview(self) {
|
||||||
|
const brush = self.brush;
|
||||||
|
|
||||||
|
var centerX = self.cursorX;
|
||||||
|
var centerY = self.cursorY;
|
||||||
|
|
||||||
|
brush.style.width = self.brush_size * 2 + "px";
|
||||||
|
brush.style.height = self.brush_size * 2 + "px";
|
||||||
|
brush.style.left = (centerX - self.brush_size) + "px";
|
||||||
|
brush.style.top = (centerY - self.brush_size) + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWheelEvent(self, event) {
|
||||||
|
if(event.deltaY < 0)
|
||||||
|
self.brush_size = Math.min(self.brush_size+2, 100);
|
||||||
|
else
|
||||||
|
self.brush_size = Math.max(self.brush_size-2, 1);
|
||||||
|
|
||||||
|
self.brush_slider_input.value = self.brush_size;
|
||||||
|
|
||||||
|
self.updateBrushPreview(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_move(self, event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.cursorX = event.pageX;
|
||||||
|
this.cursorY = event.pageY;
|
||||||
|
|
||||||
|
self.updateBrushPreview(self);
|
||||||
|
|
||||||
|
if (window.TouchEvent && event instanceof TouchEvent || event.buttons == 1) {
|
||||||
|
var diff = performance.now() - self.lasttime;
|
||||||
|
|
||||||
|
const maskRect = self.maskCanvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
var x = event.offsetX;
|
||||||
|
var y = event.offsetY
|
||||||
|
|
||||||
|
if(event.offsetX == null) {
|
||||||
|
x = event.targetTouches[0].clientX - maskRect.left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(event.offsetY == null) {
|
||||||
|
y = event.targetTouches[0].clientY - maskRect.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
var brush_size = this.brush_size;
|
||||||
|
if(event instanceof PointerEvent && event.pointerType == 'pen') {
|
||||||
|
brush_size *= event.pressure;
|
||||||
|
this.last_pressure = event.pressure;
|
||||||
|
}
|
||||||
|
else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){
|
||||||
|
// The firing interval of PointerEvents in Pen is unreliable, so it is supplemented by TouchEvents.
|
||||||
|
brush_size *= this.last_pressure;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
brush_size = this.brush_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(diff > 20 && !this.drawing_mode)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
self.maskCtx.beginPath();
|
||||||
|
self.maskCtx.fillStyle = "rgb(0,0,0)";
|
||||||
|
self.maskCtx.globalCompositeOperation = "source-over";
|
||||||
|
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
||||||
|
self.maskCtx.fill();
|
||||||
|
self.lastx = x;
|
||||||
|
self.lasty = y;
|
||||||
|
});
|
||||||
|
else
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
self.maskCtx.beginPath();
|
||||||
|
self.maskCtx.fillStyle = "rgb(0,0,0)";
|
||||||
|
self.maskCtx.globalCompositeOperation = "source-over";
|
||||||
|
|
||||||
|
var dx = x - self.lastx;
|
||||||
|
var dy = y - self.lasty;
|
||||||
|
|
||||||
|
var distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
var directionX = dx / distance;
|
||||||
|
var directionY = dy / distance;
|
||||||
|
|
||||||
|
for (var i = 0; i < distance; i+=5) {
|
||||||
|
var px = self.lastx + (directionX * i);
|
||||||
|
var py = self.lasty + (directionY * i);
|
||||||
|
self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
|
||||||
|
self.maskCtx.fill();
|
||||||
|
}
|
||||||
|
self.lastx = x;
|
||||||
|
self.lasty = y;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.lasttime = performance.now();
|
||||||
|
}
|
||||||
|
else if(event.buttons == 2 || event.buttons == 5 || event.buttons == 32) {
|
||||||
|
const maskRect = self.maskCanvas.getBoundingClientRect();
|
||||||
|
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
|
||||||
|
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
|
||||||
|
|
||||||
|
var brush_size = this.brush_size;
|
||||||
|
if(event instanceof PointerEvent && event.pointerType == 'pen') {
|
||||||
|
brush_size *= event.pressure;
|
||||||
|
this.last_pressure = event.pressure;
|
||||||
|
}
|
||||||
|
else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){
|
||||||
|
brush_size *= this.last_pressure;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
brush_size = this.brush_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(diff > 20 && !drawing_mode) // cannot tracking drawing_mode for touch event
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
self.maskCtx.beginPath();
|
||||||
|
self.maskCtx.globalCompositeOperation = "destination-out";
|
||||||
|
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
||||||
|
self.maskCtx.fill();
|
||||||
|
self.lastx = x;
|
||||||
|
self.lasty = y;
|
||||||
|
});
|
||||||
|
else
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
self.maskCtx.beginPath();
|
||||||
|
self.maskCtx.globalCompositeOperation = "destination-out";
|
||||||
|
|
||||||
|
var dx = x - self.lastx;
|
||||||
|
var dy = y - self.lasty;
|
||||||
|
|
||||||
|
var distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
var directionX = dx / distance;
|
||||||
|
var directionY = dy / distance;
|
||||||
|
|
||||||
|
for (var i = 0; i < distance; i+=5) {
|
||||||
|
var px = self.lastx + (directionX * i);
|
||||||
|
var py = self.lasty + (directionY * i);
|
||||||
|
self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
|
||||||
|
self.maskCtx.fill();
|
||||||
|
}
|
||||||
|
self.lastx = x;
|
||||||
|
self.lasty = y;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.lasttime = performance.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerDown(self, event) {
|
||||||
|
var brush_size = this.brush_size;
|
||||||
|
if(event instanceof PointerEvent && event.pointerType == 'pen') {
|
||||||
|
brush_size *= event.pressure;
|
||||||
|
this.last_pressure = event.pressure;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([0, 2, 5].includes(event.button)) {
|
||||||
|
self.drawing_mode = true;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const maskRect = self.maskCanvas.getBoundingClientRect();
|
||||||
|
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
|
||||||
|
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
|
||||||
|
|
||||||
|
self.maskCtx.beginPath();
|
||||||
|
if (event.button == 0) {
|
||||||
|
self.maskCtx.fillStyle = "rgb(0,0,0)";
|
||||||
|
self.maskCtx.globalCompositeOperation = "source-over";
|
||||||
|
} else {
|
||||||
|
self.maskCtx.globalCompositeOperation = "destination-out";
|
||||||
|
}
|
||||||
|
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
||||||
|
self.maskCtx.fill();
|
||||||
|
self.lastx = x;
|
||||||
|
self.lasty = y;
|
||||||
|
self.lasttime = performance.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const backupCtx = this.backupCanvas.getContext('2d', {willReadFrequently:true});
|
||||||
|
|
||||||
|
backupCtx.clearRect(0,0,this.backupCanvas.width,this.backupCanvas.height);
|
||||||
|
backupCtx.drawImage(this.maskCanvas,
|
||||||
|
0, 0, this.maskCanvas.width, this.maskCanvas.height,
|
||||||
|
0, 0, this.backupCanvas.width, this.backupCanvas.height);
|
||||||
|
|
||||||
|
// paste mask data into alpha channel
|
||||||
|
const backupData = backupCtx.getImageData(0, 0, this.backupCanvas.width, this.backupCanvas.height);
|
||||||
|
|
||||||
|
// refine mask image
|
||||||
|
for (let i = 0; i < backupData.data.length; i += 4) {
|
||||||
|
if(backupData.data[i+3] == 255)
|
||||||
|
backupData.data[i+3] = 0;
|
||||||
|
else
|
||||||
|
backupData.data[i+3] = 255;
|
||||||
|
|
||||||
|
backupData.data[i] = 0;
|
||||||
|
backupData.data[i+1] = 0;
|
||||||
|
backupData.data[i+2] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
backupCtx.globalCompositeOperation = 'source-over';
|
||||||
|
backupCtx.putImageData(backupData, 0, 0);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
const filename = "clipspace-mask-" + performance.now() + ".png";
|
||||||
|
|
||||||
|
const item =
|
||||||
|
{
|
||||||
|
"filename": filename,
|
||||||
|
"subfolder": "clipspace",
|
||||||
|
"type": "input",
|
||||||
|
};
|
||||||
|
|
||||||
|
if(ComfyApp.clipspace.images)
|
||||||
|
ComfyApp.clipspace.images[0] = item;
|
||||||
|
|
||||||
|
if(ComfyApp.clipspace.widgets) {
|
||||||
|
const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');
|
||||||
|
|
||||||
|
if(index >= 0)
|
||||||
|
ComfyApp.clipspace.widgets[index].value = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataURL = this.backupCanvas.toDataURL();
|
||||||
|
const blob = dataURLToBlob(dataURL);
|
||||||
|
|
||||||
|
const original_blob = loadedImageToBlob(this.image);
|
||||||
|
|
||||||
|
formData.append('image', blob, filename);
|
||||||
|
formData.append('original_image', original_blob);
|
||||||
|
formData.append('type', "input");
|
||||||
|
formData.append('subfolder', "clipspace");
|
||||||
|
|
||||||
|
uploadMask(item, formData);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "Comfy.MaskEditor",
|
||||||
|
init(app) {
|
||||||
|
const callback =
|
||||||
|
function () {
|
||||||
|
let dlg = new MaskEditorDialog(app);
|
||||||
|
dlg.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0
|
||||||
|
ClipspaceDialog.registerButton("MaskEditor", context_predicate, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -25,6 +25,7 @@ export class ComfyApp {
|
|||||||
* @type {serialized node object}
|
* @type {serialized node object}
|
||||||
*/
|
*/
|
||||||
static clipspace = null;
|
static clipspace = null;
|
||||||
|
static clipspace_invalidate_handler = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.ui = new ComfyUI(this);
|
this.ui = new ComfyUI(this);
|
||||||
@ -143,22 +144,34 @@ export class ComfyApp {
|
|||||||
callback: (obj) => {
|
callback: (obj) => {
|
||||||
var widgets = null;
|
var widgets = null;
|
||||||
if(this.widgets) {
|
if(this.widgets) {
|
||||||
widgets = this.widgets.map(({ type, name, value }) => ({ type, name, value }));
|
widgets = this.widgets.map(({ type, name, value }) => ({ type, name, value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
let img = new Image();
|
|
||||||
var imgs = undefined;
|
var imgs = undefined;
|
||||||
|
var orig_imgs = undefined;
|
||||||
if(this.imgs != undefined) {
|
if(this.imgs != undefined) {
|
||||||
img.src = this.imgs[0].src;
|
imgs = [];
|
||||||
imgs = [img];
|
orig_imgs = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.imgs.length; i++) {
|
||||||
|
imgs[i] = new Image();
|
||||||
|
imgs[i].src = this.imgs[i].src;
|
||||||
|
orig_imgs[i] = imgs[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ComfyApp.clipspace = {
|
ComfyApp.clipspace = {
|
||||||
'widgets': widgets,
|
'widgets': widgets,
|
||||||
'imgs': imgs,
|
'imgs': imgs,
|
||||||
'original_imgs': imgs,
|
'original_imgs': orig_imgs,
|
||||||
'images': this.images
|
'images': this.images,
|
||||||
|
'selectedIndex': 0,
|
||||||
|
'img_paste_mode': 'selected' // reset to default im_paste_mode state on copy action
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(ComfyApp.clipspace_invalidate_handler) {
|
||||||
|
ComfyApp.clipspace_invalidate_handler();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -167,48 +180,57 @@ export class ComfyApp {
|
|||||||
{
|
{
|
||||||
content: "Paste (Clipspace)",
|
content: "Paste (Clipspace)",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
if(ComfyApp.clipspace != null) {
|
if(ComfyApp.clipspace) {
|
||||||
if(ComfyApp.clipspace.widgets != null && this.widgets != null) {
|
|
||||||
ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => {
|
|
||||||
const prop = Object.values(this.widgets).find(obj => obj.type === type && obj.name === name);
|
|
||||||
if (prop) {
|
|
||||||
prop.callback(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// image paste
|
// image paste
|
||||||
if(ComfyApp.clipspace.imgs != undefined && this.imgs != undefined && this.widgets != null) {
|
if(ComfyApp.clipspace.imgs && this.imgs) {
|
||||||
var filename = "";
|
|
||||||
if(this.images && ComfyApp.clipspace.images) {
|
if(this.images && ComfyApp.clipspace.images) {
|
||||||
this.images = ComfyApp.clipspace.images;
|
if(ComfyApp.clipspace['img_paste_mode'] == 'selected') {
|
||||||
}
|
app.nodeOutputs[this.id + ""].images = this.images = [ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']]];
|
||||||
|
|
||||||
if(ComfyApp.clipspace.images != undefined) {
|
|
||||||
const clip_image = ComfyApp.clipspace.images[0];
|
|
||||||
if(clip_image.subfolder != '')
|
|
||||||
filename = `${clip_image.subfolder}/`;
|
|
||||||
filename += `${clip_image.filename} [${clip_image.type}]`;
|
|
||||||
}
|
|
||||||
else if(ComfyApp.clipspace.widgets != undefined) {
|
|
||||||
const index_in_clip = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');
|
|
||||||
if(index_in_clip >= 0) {
|
|
||||||
filename = `${ComfyApp.clipspace.widgets[index_in_clip].value}`;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
app.nodeOutputs[this.id + ""].images = this.images = ComfyApp.clipspace.images;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = this.widgets.findIndex(obj => obj.name === 'image');
|
if(ComfyApp.clipspace.imgs) {
|
||||||
if(index >= 0 && filename != "" && ComfyApp.clipspace.imgs != undefined) {
|
// deep-copy to cut link with clipspace
|
||||||
this.imgs = ComfyApp.clipspace.imgs;
|
if(ComfyApp.clipspace['img_paste_mode'] == 'selected') {
|
||||||
|
const img = new Image();
|
||||||
this.widgets[index].value = filename;
|
img.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
|
||||||
if(this.widgets_values != undefined) {
|
this.imgs = [img];
|
||||||
this.widgets_values[index] = filename;
|
}
|
||||||
|
else {
|
||||||
|
const imgs = [];
|
||||||
|
for(let i=0; i<ComfyApp.clipspace.imgs.length; i++) {
|
||||||
|
imgs[i] = new Image();
|
||||||
|
imgs[i].src = ComfyApp.clipspace.imgs[i].src;
|
||||||
|
this.imgs = imgs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.trigger('changed');
|
|
||||||
|
if(this.widgets) {
|
||||||
|
if(ComfyApp.clipspace.images) {
|
||||||
|
const clip_image = ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']];
|
||||||
|
const index = this.widgets.findIndex(obj => obj.name === 'image');
|
||||||
|
if(index >= 0) {
|
||||||
|
this.widgets[index].value = clip_image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(ComfyApp.clipspace.widgets) {
|
||||||
|
ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => {
|
||||||
|
const prop = Object.values(this.widgets).find(obj => obj.type === type && obj.name === name);
|
||||||
|
if (prop && prop.type != 'button') {
|
||||||
|
prop.value = value;
|
||||||
|
prop.callback(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.graph.setDirtyCanvas(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -724,7 +746,7 @@ export class ComfyApp {
|
|||||||
ctx.globalAlpha = 0.8;
|
ctx.globalAlpha = 0.8;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
if (shape == LiteGraph.BOX_SHAPE)
|
if (shape == LiteGraph.BOX_SHAPE)
|
||||||
ctx.rect(-6, -6 + LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT);
|
ctx.rect(-6, -6 - LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT);
|
||||||
else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed))
|
else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed))
|
||||||
ctx.roundRect(
|
ctx.roundRect(
|
||||||
-6,
|
-6,
|
||||||
@ -736,12 +758,11 @@ export class ComfyApp {
|
|||||||
else if (shape == LiteGraph.CARD_SHAPE)
|
else if (shape == LiteGraph.CARD_SHAPE)
|
||||||
ctx.roundRect(
|
ctx.roundRect(
|
||||||
-6,
|
-6,
|
||||||
-6 + LiteGraph.NODE_TITLE_HEIGHT,
|
-6 - LiteGraph.NODE_TITLE_HEIGHT,
|
||||||
12 + size[0] + 1,
|
12 + size[0] + 1,
|
||||||
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
|
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
|
||||||
this.round_radius * 2,
|
[this.round_radius * 2, this.round_radius * 2, 2, 2]
|
||||||
2
|
);
|
||||||
);
|
|
||||||
else if (shape == LiteGraph.CIRCLE_SHAPE)
|
else if (shape == LiteGraph.CIRCLE_SHAPE)
|
||||||
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2);
|
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2);
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
@ -1292,12 +1313,12 @@ export class ComfyApp {
|
|||||||
|
|
||||||
for(const widgetNum in node.widgets) {
|
for(const widgetNum in node.widgets) {
|
||||||
const widget = node.widgets[widgetNum]
|
const widget = node.widgets[widgetNum]
|
||||||
|
|
||||||
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
|
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
|
||||||
widget.options.values = def["input"]["required"][widget.name][0];
|
widget.options.values = def["input"]["required"][widget.name][0];
|
||||||
|
|
||||||
if(!widget.options.values.includes(widget.value)) {
|
if(widget.name != 'image' && !widget.options.values.includes(widget.value)) {
|
||||||
widget.value = widget.options.values[0];
|
widget.value = widget.options.values[0];
|
||||||
|
widget.callback(widget.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -581,6 +581,7 @@ export class ComfyUI {
|
|||||||
}),
|
}),
|
||||||
$el("button", { id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click() }),
|
$el("button", { id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click() }),
|
||||||
$el("button", { id: "comfy-refresh-button", textContent: "Refresh", onclick: () => app.refreshComboInNodes() }),
|
$el("button", { id: "comfy-refresh-button", textContent: "Refresh", onclick: () => app.refreshComboInNodes() }),
|
||||||
|
$el("button", { id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace() }),
|
||||||
$el("button", { id: "comfy-clear-button", textContent: "Clear", onclick: () => {
|
$el("button", { id: "comfy-clear-button", textContent: "Clear", onclick: () => {
|
||||||
if (!confirmClear.value || confirm("Clear workflow?")) {
|
if (!confirmClear.value || confirm("Clear workflow?")) {
|
||||||
app.clean();
|
app.clean();
|
||||||
|
|||||||
@ -266,10 +266,46 @@ export const ComfyWidgets = {
|
|||||||
node.imgs = [img];
|
node.imgs = [img];
|
||||||
app.graph.setDirtyCanvas(true);
|
app.graph.setDirtyCanvas(true);
|
||||||
};
|
};
|
||||||
img.src = `/view?filename=${name}&type=input`;
|
let folder_separator = name.lastIndexOf("/");
|
||||||
|
let subfolder = "";
|
||||||
|
if (folder_separator > -1) {
|
||||||
|
subfolder = name.substring(0, folder_separator);
|
||||||
|
name = name.substring(folder_separator + 1);
|
||||||
|
}
|
||||||
|
img.src = `/view?filename=${name}&type=input&subfolder=${subfolder}`;
|
||||||
node.setSizeForImage?.();
|
node.setSizeForImage?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var default_value = imageWidget.value;
|
||||||
|
Object.defineProperty(imageWidget, "value", {
|
||||||
|
set : function(value) {
|
||||||
|
this._real_value = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
get : function() {
|
||||||
|
let value = "";
|
||||||
|
if (this._real_value) {
|
||||||
|
value = this._real_value;
|
||||||
|
} else {
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.filename) {
|
||||||
|
let real_value = value;
|
||||||
|
value = "";
|
||||||
|
if (real_value.subfolder) {
|
||||||
|
value = real_value.subfolder + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
value += real_value.filename;
|
||||||
|
|
||||||
|
if(real_value.type && real_value.type !== "input")
|
||||||
|
value += ` [${real_value.type}]`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Add our own callback to the combo widget to render an image when it changes
|
// Add our own callback to the combo widget to render an image when it changes
|
||||||
const cb = node.callback;
|
const cb = node.callback;
|
||||||
imageWidget.callback = function () {
|
imageWidget.callback = function () {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user