Buckets:
| import bpy | |
| import re | |
| import json | |
| import tempfile | |
| import time | |
| import uuid | |
| from typing import Any | |
| from pathlib import Path | |
| from bpy.types import Context, Event | |
| from mathutils import Vector | |
| from functools import partial | |
| from .translations import ctxt | |
| from .prop import Prop | |
| from .utils import _T, logger, FSWatcher, read_json | |
| from .timer import Timer, Worker, WorkerFunc | |
| from .SDNode import TaskManager | |
| from .SDNode.history import History | |
| from .SDNode.tree import InvalidNodeType, CFNodeTree, TREE_TYPE, rtnode_rereg | |
| from .SDNode.utils import get_default_tree, WindowLogger | |
| from .datas import IMG_SUFFIX | |
| from .preference import get_pref | |
| def find_nodes_by_idname(tree, idname, find_nodes=None): | |
| if find_nodes is None: | |
| find_nodes = [] | |
| for node in tree.nodes: | |
| if node.bl_idname == idname: | |
| find_nodes.append(node) | |
| if node.type == "GROUP": | |
| find_nodes_by_idname(node.node_tree, idname, find_nodes) | |
| return find_nodes | |
| class LoopExec(WorkerFunc): | |
| args = {} | |
| # 单例 | |
| instance = None | |
| def __new__(cls, *args, **kwargs): | |
| if not cls.instance: | |
| cls.instance = super().__new__(cls) | |
| cls.instance.args = {} | |
| return cls.instance | |
| def __call__(self, *args: Any, **kwargs: Any) -> Any: | |
| if TaskManager.get_task_num() != 0: | |
| return | |
| tree: CFNodeTree = LoopExec.args.get("tree", None) | |
| try: | |
| tree.load_json | |
| except ReferenceError: | |
| return | |
| except AttributeError: | |
| return | |
| with bpy.context.temp_override(sdn_tree=tree): | |
| bpy.ops.sdn.ops(action="Submit") | |
| class Ops(bpy.types.Operator): | |
| bl_idname = "sdn.ops" | |
| bl_description = "SD Node" | |
| bl_label = "SD Node" | |
| bl_translation_context = ctxt | |
| action: bpy.props.StringProperty(default="") | |
| save_name: bpy.props.StringProperty(name="Preset Name", default="预设") | |
| alt: bpy.props.BoolProperty(default=False) | |
| is_advanced_enable = False | |
| def description(cls, context: bpy.types.Context, | |
| properties: bpy.types.OperatorProperties) -> str: | |
| desc = "SD Node" | |
| action = properties.get("action", "") | |
| if action == "PresetFromBookmark": | |
| desc = _T("Load from Bookmark") | |
| if action == "PresetFromClipBoard": | |
| desc = _T("Load from ClipBoard") | |
| elif action == "Launch": | |
| desc = _T("Launch/Connect to ComfyUI") | |
| elif action == "Restart": | |
| desc = _T("Restart ComfyUI") | |
| elif action == "Connect": | |
| desc = _T("Connect to existing & running ComfyUI server") | |
| elif action == "Submit": | |
| desc = _T("Submit Task, with Clear Cache if Alt pressed") | |
| elif action == "Close": | |
| desc = _T("Close/Disconnect from ComfyUI") | |
| return desc | |
| def draw(self, context): | |
| layout = self.layout | |
| if self.action == "Submit" and not TaskManager.is_launched(): | |
| layout.label(text=_T("ComfyUI not running, run?"), icon="INFO", text_ctxt=ctxt) | |
| if self.action == "Save": | |
| if (Path(bpy.context.scene.sdn.presets_dir) / f"{self.save_name}.json").exists(): | |
| layout.alert = True | |
| layout.label(text=f"{_T('Preset')}<{self.save_name}>{_T('exists, Click Ok to Overwrite!')}", icon="ERROR", text_ctxt=ctxt) | |
| layout.label(text="Click Outside to Cancel!", icon="ERROR", text_ctxt=ctxt) | |
| layout.prop(self, "save_name", text_ctxt=ctxt) | |
| if self.action == "SaveGroup": | |
| if (Path(bpy.context.scene.sdn.groups_dir) / f"{self.save_name}.json").exists(): | |
| layout.alert = True | |
| layout.label(text=f"{_T('Preset')}<{self.save_name}>{_T('exists, Click Ok to Overwrite!')}", icon="ERROR", text_ctxt=ctxt) | |
| layout.label(text="Click Outside to Cancel!", icon="ERROR", text_ctxt=ctxt) | |
| layout.prop(self, "save_name", text_ctxt=ctxt) | |
| if self.action == "Del": | |
| layout.alert = True | |
| layout.label(text=f"{_T('Preset')}<{Path(bpy.context.scene.sdn.presets).stem}>{_T('will be removed?')}", icon="ERROR", text_ctxt=ctxt) | |
| layout.label(text="Click Outside to Cancel!", icon="ERROR", text_ctxt=ctxt) | |
| if self.action == "DelGroup": | |
| layout.alert = True | |
| layout.label(text=f"{_T('Preset')}<{Path(bpy.context.scene.sdn.groups).stem}>{_T('will be removed?')}", icon="ERROR", text_ctxt=ctxt) | |
| layout.label(text="Click Outside to Cancel!", icon="ERROR", text_ctxt=ctxt) | |
| if self.action == "PresetFromBookmark": | |
| layout.label(text="Click Folder Icon to Select Bookmark:", text_ctxt=ctxt) | |
| layout.prop(bpy.context.scene.sdn, "import_bookmark", text_ctxt=ctxt) | |
| def invoke(self, context, event: bpy.types.Event): | |
| self.exec_tree: CFNodeTree = None | |
| wm = bpy.context.window_manager | |
| self.alt = event.alt | |
| self.select_nodes = [] | |
| self.init_pos = (0, 0) | |
| if hasattr(context.space_data, "cursor_location"): | |
| self.init_pos = context.space_data.cursor_location.copy() | |
| if self.action == "Submit" and not TaskManager.is_launched(): | |
| return wm.invoke_props_dialog(self, width=200) | |
| if self.action in {"Load", "Del"}: | |
| if not bpy.context.scene.sdn.presets: | |
| self.report({"ERROR"}, _T("Preset Not Selected!")) | |
| return {"FINISHED"} | |
| if self.action in {"LoadGroup", "DelGroup"}: | |
| if not bpy.context.scene.sdn.groups: | |
| self.report({"ERROR"}, _T("Preset Not Selected!")) | |
| return {"FINISHED"} | |
| if self.action in {"Save", "SaveGroup", "Del", "DelGroup", "PresetFromBookmark"}: | |
| return wm.invoke_props_dialog(self, width=200) | |
| return self.execute(context) | |
| def execute(self, context: bpy.types.Context): | |
| try: | |
| if res := getattr(self, self.action)(): | |
| return res | |
| return {"FINISHED"} | |
| except InvalidNodeType as e: | |
| self.report({"ERROR"}, str(e.args)) | |
| return {"FINISHED"} | |
| def modal(self, context, event: bpy.types.Event): | |
| if not self.select_nodes: | |
| return {"FINISHED"} | |
| if event.type == "MOUSEMOVE": | |
| self.update_nodes_pos(event) | |
| # exit | |
| if event.value == "PRESS" and event.type in {"ESC", "LEFTMOUSE", "ENTER"}: | |
| return {"FINISHED"} | |
| if event.value == "PRESS" and event.type in {"RIGHTMOUSE"}: | |
| self.exec_tree.safe_remove_nodes(self.select_nodes[:]) | |
| return {"CANCELLED"} | |
| return {"RUNNING_MODAL"} | |
| def find_frames_nodes(self, tree): | |
| nodes = [n for n in find_nodes_by_idname(tree, "输入图像") if n.mode == "序列图"] | |
| return nodes | |
| def find_mat_image_nodes(self, tree): | |
| nodes = find_nodes_by_idname(tree, "材质图") | |
| return nodes[0] if nodes else None | |
| def ensure_tree(self) -> CFNodeTree: | |
| tree = get_default_tree() | |
| if not tree: | |
| bpy.ops.node.new_node_tree(type=TREE_TYPE, name="NodeTree") | |
| tree = get_default_tree() | |
| return tree | |
| def submit(self): | |
| tree: CFNodeTree = get_default_tree() | |
| if not tree: | |
| logger.error(_T("No Node Tree Found!")) | |
| self.report({"ERROR"}, _T("No Node Tree Found!")) | |
| return | |
| tree.reset_error_mark() | |
| WindowLogger.clear() | |
| if bpy.context.scene.sdn.advanced_exe and not Ops.is_advanced_enable: | |
| Ops.is_advanced_enable = True | |
| if bpy.context.scene.sdn.loop_exec: | |
| loop_exec = LoopExec() | |
| loop_exec.args["tree"] = tree | |
| Worker.push_worker(loop_exec) | |
| return {"FINISHED"} | |
| for _ in range(bpy.context.scene.sdn.batch_count): | |
| bpy.ops.sdn.ops(action="Submit") | |
| Ops.is_advanced_enable = False | |
| return {"FINISHED"} | |
| elif frames_nodes := self.find_frames_nodes(tree): | |
| def find_frames(path): | |
| frames_map = {} | |
| for file in Path(path).iterdir(): | |
| if file.suffix.lower() not in IMG_SUFFIX: | |
| continue | |
| piece = file.stem.split("_") | |
| try: | |
| frames_map[int(piece[-1])] = file.as_posix() | |
| except BaseException: | |
| ... | |
| return frames_map | |
| node_frames = {} | |
| old_cfg = {} | |
| for fnode in frames_nodes: | |
| frames_dir = fnode.frames_dir | |
| if not frames_dir: | |
| self.report({"ERROR"}, _T("Node<{}>Directory is Empty!").format(fnode.name)) | |
| return {"FINISHED"} | |
| if not Path(frames_dir).exists(): | |
| self.report({"ERROR"}, _T("Node<{}>Directory Not Exists!").format(fnode.name)) | |
| return {"FINISHED"} | |
| node_frames[fnode] = find_frames(frames_dir) | |
| old_cfg[fnode] = {} | |
| old_cfg[fnode]["mode"] = fnode.mode | |
| old_cfg[fnode]["image"] = fnode.image | |
| pnode, pframes = node_frames.popitem() | |
| pnode.mode = "输入" | |
| for frame in pframes: | |
| pnode.image = pframes[frame] | |
| # logger.debug(f"F {frame}: {pnode.image}") | |
| pre_img_map = {pnode: pframes[frame]} | |
| for fnode, node_frame in node_frames.items(): | |
| if not (fpath := node_frame.get(frame, "")): | |
| error_info = _T("Frame <{}> Not Found in <{}> Node Path!").format(frame, fnode.name) | |
| # self.report({"ERROR"}, error_info) | |
| logger.error(error_info) | |
| break | |
| fnode.mode = "输入" | |
| fnode.image = fpath | |
| pre_img_map[fnode] = fpath | |
| # logger.debug(f"F {frame}: {fnode.image}") | |
| else: | |
| logger.debug(_T("Frame Task <{}> Added!").format(frame)) | |
| def pre(pre_img_map: dict): | |
| for fnode, fpath in pre_img_map.items(): | |
| try: | |
| fnode.image = fpath | |
| except Exception: | |
| ... | |
| TaskManager.push_task(tree.get_task(), tree=tree, pre=partial(pre, pre_img_map)) | |
| # restore config | |
| for fnode, cfg in old_cfg.items(): | |
| setattr(fnode, "mode", cfg["mode"]) | |
| setattr(fnode, "image", cfg["image"]) | |
| return {"FINISHED"} | |
| elif mat_image_node := self.find_mat_image_nodes(tree): | |
| def recursive_node_parent(node, find_nodes=None): | |
| # 查找 node 相连的所有父级节点 | |
| if find_nodes is None: | |
| find_nodes = [] | |
| for inp in node.inputs: | |
| if not inp.is_linked: | |
| continue | |
| for link in inp.links: | |
| if link.from_node in find_nodes: | |
| continue | |
| find_nodes.append(link.from_node) | |
| recursive_node_parent(link.from_node, find_nodes) | |
| return find_nodes | |
| def find_obj_mat_images(obj): | |
| # 搜索 Mesh Object的所有材质的所有图片节点 | |
| images = [] | |
| image_nodes = [] | |
| for mat in obj.data.materials: | |
| fnodes = find_nodes_by_idname(mat.node_tree, "ShaderNodeTexImage") | |
| image_nodes.extend(fnodes) | |
| for img_node in image_nodes: | |
| if not img_node.image: | |
| continue | |
| images.append(img_node.image) | |
| return images | |
| save_nodes = find_nodes_by_idname(tree, "存储") | |
| images = [] | |
| query_objs = [] | |
| if mat_image_node.mode == "Object": | |
| query_objs.append(mat_image_node.obj) | |
| elif mat_image_node.mode == "Selected Objects": | |
| query_objs.extend(bpy.context.selectable_objects) | |
| elif mat_image_node.mode == "Collection": | |
| collection: bpy.types.Collection = mat_image_node.collection | |
| query_objs.extend(collection.objects) | |
| for obj in query_objs: | |
| if obj.type != "MESH": | |
| continue | |
| images.extend(find_obj_mat_images(obj)) | |
| if not images: | |
| self.report({"ERROR"}, _T("No Images Found!")) | |
| return {"FINISHED"} | |
| marked_sn = [] | |
| for sn in save_nodes: | |
| if sn.mode != "ToImage": | |
| continue | |
| p = recursive_node_parent(sn) | |
| if mat_image_node not in p: | |
| continue | |
| marked_sn.append(sn) | |
| # 批量构造任务 | |
| for img in set(images): | |
| for sn in marked_sn: | |
| sn.image = img | |
| # 过滤 Render Result | |
| if img.source == "VIEWER": | |
| continue | |
| # mat_image_node的image必须为路径, 因此需要将img存储到本地(如果不是本地图片) | |
| filepath = Path(img.filepath) | |
| if not filepath.exists() or filepath.suffix.lower() not in IMG_SUFFIX: | |
| filepath = Path(tempfile.gettempdir()) / img.name | |
| filepath = filepath.with_suffix(".png") | |
| tf = filepath.resolve().as_posix() | |
| img.save(filepath=tf) | |
| img.filepath = filepath.resolve().as_posix() | |
| img.filepath_raw = img.filepath | |
| mat_image_node.image = FSWatcher.to_str(filepath) | |
| TaskManager.push_task(tree.get_task(), tree=tree) | |
| return {"FINISHED"} | |
| else: | |
| if self.alt: | |
| TaskManager.clear_cache() | |
| if bpy.context.scene.sdn.frame_mode == "MultiFrame": | |
| sf = bpy.context.scene.frame_start | |
| ef = bpy.context.scene.frame_end | |
| for cf in range(sf, ef + 1): | |
| def pre(cf): | |
| bpy.context.scene.frame_set(cf) | |
| pre = partial(pre, cf) | |
| TaskManager.push_task(tree.get_task, pre, tree=tree) | |
| elif bpy.context.scene.sdn.frame_mode == "Batch": | |
| batch_dir = bpy.context.scene.sdn.batch_dir | |
| select_node = tree.nodes.active | |
| if not select_node or select_node.bl_idname != "输入图像": | |
| self.report({"ERROR"}, "Input Image Node Not Selected!") | |
| return {"FINISHED"} | |
| if not batch_dir or not Path(batch_dir).exists(): | |
| self.report({"ERROR"}, "Batch Directory Not Set!") | |
| return {"FINISHED"} | |
| old_mode, old_image = select_node.mode, select_node.image | |
| select_node.mode = "输入" | |
| for file in Path(batch_dir).iterdir(): | |
| if file.is_dir(): | |
| continue | |
| if file.suffix not in IMG_SUFFIX: | |
| continue | |
| select_node.image = file.as_posix() | |
| TaskManager.push_task(tree.get_task(), tree=tree) | |
| select_node.mode, select_node.image = old_mode, old_image | |
| else: | |
| TaskManager.push_task(tree.get_task(), tree=tree) | |
| return {"FINISHED"} | |
| def update_nodes_pos(self, event): | |
| for n in self.select_nodes: | |
| n.location = self.init_node_pos[n] + bpy.context.space_data.cursor_location - self.init_pos | |
| def Launch(self): | |
| if TaskManager.is_launched(): | |
| self.report({"ERROR"}, "服务已经启动!") | |
| return | |
| self.Restart() | |
| def Submit(self): | |
| if not TaskManager.is_launched(): | |
| self.Launch() | |
| self.submit() | |
| def Connect(self): | |
| TaskManager.connect_existing = not TaskManager.connect_existing | |
| if TaskManager.connect_existing: | |
| TaskManager.launch_ip = get_pref().get_ip() | |
| TaskManager.launch_port = get_pref().port | |
| TaskManager.launch_url = f"http://{get_pref().get_ip()}:{get_pref().port}" | |
| rtnode_rereg() | |
| CFNodeTree.refresh_current_tree() | |
| TaskManager.start_polling() | |
| def Close(self): | |
| if tree := get_default_tree(): | |
| # 恢复节点颜色 | |
| for n in tree.nodes: | |
| if not n.label.endswith(("-EXEC", "-ERROR")): | |
| continue | |
| n.use_custom_color = False | |
| n.label = "" | |
| TaskManager.close_server() | |
| # hack fix tree update crash | |
| CFNodeTree.refresh_current_tree() | |
| def Restart(self): | |
| TaskManager.restart_server() | |
| # hack fix tree update crash | |
| CFNodeTree.refresh_current_tree() | |
| def Cancel(self): | |
| TaskManager.interrupt() | |
| def StopLoop(self): | |
| Ops.is_advanced_enable = False | |
| Worker.remove_worker(LoopExec()) | |
| def ClearTask(self): | |
| TaskManager.clear_all() | |
| def Save(self): | |
| tree = get_default_tree() | |
| if not tree: | |
| logger.error(_T("No Node Tree Found!")) | |
| self.report({"ERROR"}, _T("No Node Tree Found!")) | |
| return | |
| if not self.save_name: | |
| self.report({"ERROR"}, _T("Invalid Preset Name!")) | |
| return | |
| data = tree.save_json() | |
| file = Path(bpy.context.scene.sdn.presets_dir) / f"{self.save_name}.json" | |
| with open(file, "w", encoding="utf8") as f: | |
| json.dump(data, f, ensure_ascii=False, indent=2) | |
| Prop.mark_dirty() | |
| def Del(self): | |
| if not bpy.context.scene.sdn.presets: | |
| self.report({"ERROR"}, _T("Preset Not Selected!")) | |
| return | |
| preset = Path(bpy.context.scene.sdn.presets) | |
| preset.unlink() | |
| self.report({"INFO"}, f"{preset.stem} {_T('Removed')}") | |
| Prop.mark_dirty() | |
| def Load(self): | |
| if not bpy.context.scene.sdn.presets: | |
| self.report({"ERROR"}, _T("Preset Not Selected!")) | |
| return | |
| tree = self.ensure_tree() | |
| tree.load_json(read_json(bpy.context.scene.sdn.presets)) | |
| def SaveGroup(self): | |
| tree = get_default_tree() | |
| if not tree: | |
| logger.error(_T("No Node Tree Found!")) | |
| self.report({"ERROR"}, _T("No Node Tree Found!")) | |
| return | |
| if not self.save_name: | |
| self.report({"ERROR"}, _T("Invalid Preset Name!")) | |
| return | |
| data = tree.save_json_group() | |
| file = Path(bpy.context.scene.sdn.groups_dir) / f"{self.save_name}.json" | |
| with open(file, "w", encoding="utf8") as f: | |
| json.dump(data, f, ensure_ascii=False, indent=4) | |
| Prop.mark_dirty() | |
| def DelGroup(self): | |
| if not bpy.context.scene.sdn.groups: | |
| self.report({"ERROR"}, _T("Preset Not Selected!")) | |
| return | |
| preset = Path(bpy.context.scene.sdn.groups) | |
| preset.unlink() | |
| self.report({"INFO"}, f"{preset.stem} {_T('Removed')}") | |
| Prop.mark_dirty() | |
| def LoadGroup(self): | |
| if not bpy.context.scene.sdn.groups: | |
| self.report({"ERROR"}, _T("Preset Not Selected!")) | |
| return | |
| tree = self.ensure_tree() | |
| select_nodes = tree.load_json_group(read_json(bpy.context.scene.sdn.groups)) | |
| if not select_nodes: | |
| return | |
| self.select_nodes = select_nodes | |
| v = Vector([0, 0]) | |
| for n in select_nodes: | |
| v += n.location | |
| v /= len(select_nodes) | |
| self.init_node_pos = {} | |
| for n in select_nodes: | |
| n.location += bpy.context.space_data.cursor_location - v | |
| self.init_node_pos[n] = n.location.copy() | |
| bpy.context.window_manager.modal_handler_add(self) | |
| self.exec_tree = tree | |
| return {"RUNNING_MODAL"} | |
| def PresetFromBookmark(self): | |
| return | |
| def PresetFromClipBoard(self): | |
| try: | |
| data = bpy.context.window_manager.clipboard | |
| from .utils import WebUIToComfyUI | |
| web_parser = WebUIToComfyUI(data) | |
| if web_parser.is_webui_format(): | |
| web_parser.parse(data) | |
| data = web_parser.to_comfyui_format() | |
| data = json.loads(data) | |
| if not isinstance(data, dict): | |
| self.report({"ERROR"}, _T("ClipBoard Content Format Error")) | |
| return | |
| if "workflow" in data: | |
| data = data["workflow"] | |
| tree = self.ensure_tree() | |
| tree.load_json(data) | |
| except json.JSONDecodeError: | |
| self.report({"ERROR"}, _T("ClipBoard Content Format Error")) | |
| class Ops_Mask(bpy.types.Operator): | |
| bl_idname = "sdn.mask" | |
| bl_label = "蒙版" | |
| bl_description = "Mask Operator" | |
| action: bpy.props.StringProperty(default="add") | |
| node_name: bpy.props.StringProperty() | |
| cam_name: bpy.props.StringProperty(default="") | |
| def execute(self, context): | |
| tree = get_default_tree(context) | |
| if not tree: | |
| self.report({"ERROR"}, "No NodeTree Found") | |
| return {"FINISHED"} | |
| node = tree.nodes.get(self.node_name) | |
| if not node: | |
| self.report({"ERROR"}, _T("Node Not Found: ") + self.node_name) | |
| return {"FINISHED"} | |
| cam = bpy.context.scene.camera | |
| if self.cam_name: | |
| cam = bpy.context.scene.objects.get(self.cam_name) | |
| self.cam_name = "" | |
| if self.action == "add": | |
| if bpy.context.area.type == "IMAGE_EDITOR": | |
| # 激活笔刷 | |
| context.space_data.ui_mode = "PAINT" | |
| context.space_data.display_channels = "COLOR" | |
| bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw") | |
| brush = context.tool_settings.image_paint.brush | |
| brush.blend = "ERASE_ALPHA" | |
| brush.color = 1, 1, 1 | |
| brush.strength = 1 | |
| brush.curve_preset = 'CONSTANT' | |
| context.tool_settings.unified_paint_settings.size = 50 | |
| return {"FINISHED"} | |
| gp = bpy.data.grease_pencils.new("AI_GP") | |
| layer = gp.layers.new("GP_Layer") | |
| layer.frames.new(bpy.context.scene.frame_current) | |
| # for frame in layer.frames: | |
| # print("Layer: %s, Frame: %d" % (layer.info, frame.frame_number)) | |
| gpo = bpy.data.objects.new(name=gp.name, object_data=gp) | |
| gpo.hide_render = True | |
| node.gp = gpo | |
| bpy.context.scene.collection.objects.link(gpo) | |
| bpy.context.view_layer.objects.active = gpo | |
| mat = bpy.data.materials.new(gpo.name) | |
| bpy.data.materials.create_gpencil_data(mat) | |
| gpo.data.materials.append(mat) | |
| gpo.active_material = mat | |
| mat.grease_pencil.color = (1, 1, 1, 1) | |
| gpo.show_in_front = True | |
| bpy.ops.object.mode_set(mode="PAINT_GPENCIL") | |
| brush = bpy.context.tool_settings.gpencil_paint.brush | |
| brush.gpencil_settings.pen_strength = 1 | |
| brush.size = 250 | |
| if cam: | |
| mask = cam.get("SD_Mask", []) | |
| if None in mask: | |
| mask.remove(None) | |
| mask.append(gpo) | |
| cam["SD_Mask"] = mask | |
| return {"FINISHED"} | |
| class Load_History(bpy.types.Operator): | |
| bl_idname = "sdn.load_history" | |
| bl_label = "Load History" | |
| bl_description = "Load History Workflow" | |
| name: bpy.props.StringProperty() | |
| def poll(cls, context: Context): | |
| return get_default_tree(context) | |
| def execute(self, context): | |
| tree = get_default_tree(context) | |
| if not tree: | |
| self.report({"ERROR"}, _T("No Node Tree Found!")) | |
| return {"FINISHED"} | |
| data = History.get_history_by_name(self.name) | |
| if not data: | |
| self.report({"ERROR"}, _T("History Not Found: ") + self.name) | |
| return {"FINISHED"} | |
| if "workflow" in data: | |
| data = data["workflow"] | |
| tree.load_json(data) | |
| return {"FINISHED"} | |
| class Popup_Load(bpy.types.Operator): | |
| bl_idname = "sdn.popup_load" | |
| bl_label = "Popup Load" | |
| bl_description = "Popup Load" | |
| filepath: bpy.props.StringProperty() | |
| def execute(self, context): | |
| file = Path(self.filepath) | |
| if file.suffix == ".csv": | |
| bpy.ops.sdn.load_batch("EXEC_DEFAULT", filepath=file.as_posix()) | |
| elif file.suffix in {".png", ".json"}: | |
| bpy.context.scene.sdn.import_bookmark = file.as_posix() | |
| return {"FINISHED"} | |
| class Copy_Tree(bpy.types.Operator): | |
| bl_idname = "sdn.copy_tree" | |
| bl_label = "Copy Tree to ClipBoard" | |
| bl_description = "Copy Tree to ClipBoard" | |
| bl_translation_context = ctxt | |
| name: bpy.props.StringProperty() | |
| def poll(cls, context: Context): | |
| return getattr(bpy.context.space_data, "edit_tree", False) | |
| def execute(self, context): | |
| tree: CFNodeTree = get_default_tree(context) | |
| try: | |
| workflow = tree.save_json() | |
| except Exception as e: | |
| self.report({"ERROR"}, str(e.args)) | |
| return {"FINISHED"} | |
| bpy.context.window_manager.clipboard = json.dumps(workflow, ensure_ascii=False) | |
| # 弹出提示 已复制到剪切板 | |
| def draw(pm: bpy.types.UIPopupMenu, context): | |
| layout = pm.layout | |
| layout.label(text=_T("Tree Copied to ClipBoard")) | |
| bpy.context.window_manager.popup_menu(draw, title=_T("Tree Copied to ClipBoard"), icon="INFO") | |
| return {"FINISHED"} | |
| class Load_Batch(bpy.types.Operator): | |
| bl_idname = "sdn.load_batch" | |
| bl_label = "Load Batch Task" | |
| bl_description = "Load Batch Task" | |
| bl_translation_context = ctxt | |
| filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) | |
| filepath: bpy.props.StringProperty() | |
| def poll(cls, context: Context): | |
| return getattr(bpy.context.space_data, "edit_tree", False) | |
| def invoke(self, context: Context, event: Event): | |
| # 弹出文件选择框 | |
| wm = bpy.context.window_manager | |
| wm.fileselect_add(self) | |
| return {"RUNNING_MODAL"} | |
| def execute(self, context): | |
| import csv | |
| # 批量任务格式 | |
| # 任务索引, 节点名.参数名, 参数值, 节点名.参数名, 参数值, ... | |
| csv_path = Path(self.filepath) | |
| if not csv_path.exists(): | |
| self.report({"ERROR"}, _T("File Not Found: ") + self.filepath) | |
| return {"FINISHED"} | |
| tree = get_default_tree(context) | |
| if not tree: | |
| self.report({"ERROR"}, _T("No Node Tree Found!")) | |
| return {"FINISHED"} | |
| tasks = [] | |
| for coding in ["utf-8", "gbk"]: | |
| try: | |
| tasks.clear() | |
| with open(csv_path, "r", encoding=coding) as f: | |
| reader = csv.reader(f) | |
| for row in reader: | |
| tasks.append(row) | |
| break | |
| except UnicodeDecodeError: | |
| continue | |
| for task in tasks: | |
| if not task: | |
| continue | |
| task_index = task[0] | |
| pairs = task[1:] | |
| if set(pairs) == {""}: | |
| continue | |
| for i in range(len(pairs) // 2): | |
| n_dot_pname, pvalue = pairs[i * 2: i * 2 + 2] | |
| if not n_dot_pname or not pvalue: | |
| continue | |
| # nodes["nname"].pname | |
| nname, pname = re.match(r"nodes\[\"(.+)\"\]\.(.+)", n_dot_pname).groups() | |
| node = tree.nodes.get(nname) | |
| if not node or not node.get_meta(pname): | |
| continue | |
| ptype = type(getattr(node, pname)) | |
| # print(node, pname, pvalue, ptype) | |
| setattr(node, pname, ptype(pvalue)) | |
| # 提交任务 | |
| bpy.ops.sdn.ops("INVOKE_DEFAULT", action="Submit") | |
| return {"FINISHED"} | |
| class Fetch_Node_Status(bpy.types.Operator): | |
| bl_idname = "sdn.fetch_node_status" | |
| bl_label = "Fetch Node Status" | |
| bl_description = "Fetch Node Status" | |
| bl_translation_context = ctxt | |
| def poll(cls, context: Context): | |
| return getattr(bpy.context.space_data, "edit_tree", False) | |
| def execute(self, context): | |
| Timer.clear() | |
| t1 = time.time() | |
| rtnode_rereg() | |
| t2 = time.time() | |
| logger.info(_T("RegNode Time:") + f" {t2-t1:.2f}s") | |
| CFNodeTree.refresh_current_tree() | |
| return {"FINISHED"} | |
| class NodeSearch(bpy.types.Operator): | |
| bl_idname = "sdn.node_search" | |
| bl_label = "Node Search" | |
| bl_options = {"REGISTER"} | |
| bl_property = "item" | |
| def node_items(self, context): | |
| from .SDNode.tree import NodeBase | |
| from .utils import _T2 | |
| return [(sb.class_type, _T2(sb.class_type), "") for sb in NodeBase.__subclasses__()] | |
| item: bpy.props.EnumProperty(items=node_items) | |
| def invoke(self, context, event): | |
| context.window_manager.invoke_search_popup(self) | |
| return {"CANCELLED"} | |
| def execute(self, context): | |
| if not get_default_tree(): | |
| self.report({'ERROR'}, _T("No NodeTree Found")) | |
| return {"FINISHED"} | |
| try: | |
| bpy.ops.node.add_node(use_transform=True, settings=[], type=self.item) | |
| except BaseException: | |
| self.report({'WARNING'}, f"未定义的节点 > {self.item}") | |
| try: | |
| bpy.ops.node.translate_attach("INVOKE_DEFAULT") | |
| except RuntimeError: | |
| ... | |
| return {"FINISHED"} | |
| class Clear_Node_Cache(bpy.types.Operator): | |
| bl_idname = "sdn.clear_node_cache" | |
| bl_label = "Clear Node Cache" | |
| bl_description = "If there is a node parsing error, you can delete the node cache with this button, then restart Blender" | |
| bl_translation_context = ctxt | |
| def execute(self, context): | |
| node_cache_path = Path(__file__).parent.joinpath("SDNode", "object_info.json") | |
| if not node_cache_path.exists(): | |
| return {"FINISHED"} | |
| try: | |
| node_cache_path.unlink() | |
| self.report({"INFO"}, _T("Node Cache Cleared!")) | |
| except Exception as e: | |
| self.report({"ERROR"}, _T("Node Cache Clear Failed!") + f" {e}") | |
| return {"FINISHED"} | |
| class CopyToClipboard(bpy.types.Operator): | |
| bl_idname = "sdn.copy_iname_to_clipboard" | |
| bl_label = "Copy Image Name to Clipboard" | |
| bl_translation_context = ctxt | |
| info: bpy.props.StringProperty(default="") | |
| def execute(self, context): | |
| bpy.context.window_manager.clipboard = self.info | |
| return {"FINISHED"} | |
| class Sync_Stencil_Image(bpy.types.Operator): | |
| bl_idname = "sdn.sync_stencil_image" | |
| bl_label = "Sync Stencil Image" | |
| bl_description = "Sync Stencil Image" | |
| bl_translation_context = ctxt | |
| action: bpy.props.StringProperty(default="") | |
| areas = set() | |
| def execute(self, context: Context): | |
| if self.action == "Clear": | |
| self.areas.discard(context.area) | |
| return {"FINISHED"} | |
| self.areas.add(context.area) | |
| wm = context.window_manager | |
| wm.modal_handler_add(self) | |
| self._timer = wm.event_timer_add(1 / 60, window=context.window) | |
| return {"RUNNING_MODAL"} | |
| def modal(self, context: Context, event: Event): | |
| if context.area not in self.areas: | |
| return {"FINISHED"} | |
| if not context.area: | |
| return {"FINISHED"} | |
| if context.area.type != "VIEW_3D": | |
| return {"PASS_THROUGH"} | |
| # 鼠标不在当前viewport则返回 | |
| in_area = context.area.x + context.area.width > event.mouse_x > context.area.x and context.area.y + context.area.height > event.mouse_y > context.area.y | |
| if not in_area: | |
| return {"PASS_THROUGH"} | |
| from bl_ui.properties_paint_common import UnifiedPaintPanel | |
| rv3d: bpy.types.RegionView3D = bpy.context.space_data.region_3d | |
| area = context.area | |
| # zoom to fac powf((float(M_SQRT2) + camzoom / 50.0f), 2.0f) / 4.0f; | |
| # max(area.width, area.height) * fac | |
| fac = (2**0.5 + rv3d.view_camera_zoom / 50)**2 / 4 | |
| length = max(area.width, area.height) * fac | |
| settings = UnifiedPaintPanel.paint_settings(context) | |
| brush: bpy.types.Brush = settings.brush # 可能报错 没brush(settings为空) | |
| width, height = area.width, area.height | |
| if not brush: | |
| return {"PASS_THROUGH"} | |
| sos = get_pref().stencil_offset_size_xy | |
| offset_top = Vector(sos) * bpy.context.preferences.view.ui_scale | |
| enable_cam_offset = False | |
| if enable_cam_offset: | |
| coffx, coffy = rv3d.view_camera_offset | |
| coffw = coffx * width | |
| coffh = coffy * height | |
| hwidth = width / 2 | |
| hheight = height / 2 | |
| fac = (2**0.5 + rv3d.view_camera_zoom / 50)**2 / 4 | |
| brush.stencil_pos = (hwidth - coffw, hheight - offset_top.y - coffh) | |
| else: | |
| rv3d.view_camera_offset = (0, 0) | |
| pos = (width / 2 - offset_top.x, height / 2 - offset_top.y) | |
| dim = (length / 2, length / 2) | |
| self.update_brush(brush, pos, dim) | |
| if rv3d.view_perspective != "CAMERA": | |
| rv3d.view_perspective = "CAMERA" | |
| return {"PASS_THROUGH"} | |
| def update_brush(self, brush: bpy.types.Brush, pos, dim): | |
| pos = Vector(pos) | |
| dim = Vector(dim) | |
| if brush.stencil_dimension != dim: | |
| brush.stencil_dimension = dim | |
| if brush.stencil_pos != pos: | |
| brush.stencil_pos = pos | |
| def menu_sync_stencil_image(self: bpy.types.Menu, context: bpy.types.Context): | |
| if context.space_data.type != "VIEW_3D": | |
| return | |
| if context.area in Sync_Stencil_Image.areas: | |
| col = self.layout.column() | |
| col.alert = True | |
| col.operator(Sync_Stencil_Image.bl_idname, text="Stop Syncing Stencil Image", icon="PAUSE").action = "Clear" | |
| else: | |
| self.layout.operator(Sync_Stencil_Image.bl_idname, icon="PLAY") | |
| bpy.types.VIEW3D_PT_tools_brush_settings.append(menu_sync_stencil_image) | |
| # bpy.types.VIEW3D_PT_tools_brush_display.append(menu_sync_stencil_image) | |
| def sdn_get_image(node: bpy.types.Node): | |
| if node.bl_idname in ('PreviewImage', '预览') and len(node.prev) > 0: # '预览' "Preview" Blender-side node | |
| return node.prev[0].image | |
| if node.bl_idname == '输入图像': # "Input Image" Blender-side node | |
| return node.prev | |
| if node.bl_idname == '存储' and node.mode == 'ToImage': # "Save" Blender-side node | |
| return node.image | |
| image = None | |
| if node.bl_idname == 'SaveImage' or (node.bl_idname == '存储' and node.mode == 'Save'): | |
| path = Path(node.output_dir) | |
| prefix = node.filename_prefix | |
| file_re = re.compile(f'{prefix}_([0-9]+)') # Should the _ at the end be included? | |
| biggest = (None, -1) | |
| if (len(node.inputs) == 1 or node.inputs[1].connections == ()) and path.is_dir(): | |
| # Find newest image, by filename | |
| for f in path.iterdir(): | |
| if f.is_file(): | |
| match = file_re.match(f.stem) | |
| if match and int(match.groups()[0]) > biggest[1]: | |
| biggest = (f, int(match.groups()[0])) | |
| # If found, see if loaded, create if not | |
| if biggest[0] is not None: | |
| for im in bpy.data.images: | |
| if im.filepath == f.as_posix(): | |
| image = im | |
| break | |
| if image is None: | |
| image = bpy.data.images.new(f.stem + f.suffix, 32, 32) | |
| image.source = 'FILE' | |
| image.filepath = f.as_posix() | |
| return image | |
| return image | |
| def get_imeditor(context: bpy.types.Context, check_for_image: bool = False): | |
| for window in context.window_manager.windows: | |
| for area in window.screen.areas: | |
| if check_for_image: | |
| if area.type == 'IMAGE_EDITOR' and area.spaces[0].image and len(area.spaces[0].image.pixels) > 0: | |
| return area | |
| else: | |
| if area.type == 'IMAGE_EDITOR' and not area.spaces[0].use_image_pin: | |
| return area | |
| return None | |
| def get_sdneditor(context: bpy.types.Context): | |
| for window in context.window_manager.windows: | |
| for area in window.screen.areas: | |
| if area.type == 'NODE_EDITOR' and area.spaces[0].tree_type == TREE_TYPE and area.spaces[0].edit_tree: | |
| return area | |
| return None | |
| class SDNode_To_Image(bpy.types.Operator): | |
| bl_idname = "sdn.sdn_to_image" | |
| bl_label = "ComfyUI node to Image Editor" | |
| bl_description = "Open the selected Save/Preview/Input Image node's image in an Image Editor" | |
| bl_translation_context = ctxt | |
| def poll(cls, context: Context): | |
| return (context.space_data.type == 'IMAGE_EDITOR' and context.space_data.image and get_sdneditor(context)) or \ | |
| (context.space_data.type == 'NODE_EDITOR' and context.space_data.tree_type == TREE_TYPE and get_imeditor(context)) | |
| def execute(self, context): | |
| node = None | |
| ime_area = None | |
| if context.area.type == 'NODE_EDITOR': | |
| node = context.active_node | |
| ime_area = get_imeditor(context, False) | |
| if context.area.type == 'IMAGE_EDITOR': | |
| ime_area = context.area | |
| sdn_area = get_sdneditor(context) | |
| if not sdn_area: | |
| self.report({'ERROR'}, "No ComfyUI Node Editor found!") | |
| node = sdn_area.spaces[0].node_tree.nodes.active | |
| if not node: | |
| self.report({'ERROR'}, "No active ComfyUI node!") | |
| return {'CANCELLED'} | |
| if not ime_area: | |
| self.report({'ERROR'}, "No Image Editor with an open unpinned image found!") | |
| return {'CANCELLED'} | |
| image = sdn_get_image(node) | |
| if not image: | |
| self.report({'ERROR'}, "Could not retrieve node image!") | |
| return {'CANCELLED'} | |
| ime_area.spaces[0].image = image | |
| ime_area.spaces[0].image.alpha_mode = 'CHANNEL_PACKED' | |
| return {'FINISHED'} | |
| MODIFIED_IMAGE_SUFFIX = '_m' | |
| class Image_To_SDNode(bpy.types.Operator): | |
| bl_idname = "sdn.image_to_sdn" | |
| bl_label = "Image Editor to ComfyUI node" | |
| bl_description = "Move the current image to a ComfyUI Node Editor node" | |
| bl_translation_context = ctxt | |
| force_centered: bpy.props.BoolProperty(name="Force Centered", description="If creating a new node, put it in the centre of the editor", | |
| translation_context=ctxt, default=False) | |
| def poll(cls, context: Context): | |
| return (context.space_data.type == 'IMAGE_EDITOR' and context.space_data.image and get_sdneditor(context)) or \ | |
| (context.space_data.type == 'NODE_EDITOR' and context.space_data.tree_type == TREE_TYPE and get_imeditor(context, True)) | |
| def execute(self, context): | |
| sdn_area = None | |
| ime_area = None | |
| image = None | |
| new_node_loc = None | |
| if context.area.type == 'IMAGE_EDITOR': | |
| ime_area = context.area | |
| image = context.space_data.image | |
| sdn_area = get_sdneditor(context) | |
| if not sdn_area: | |
| self.report({'ERROR'}, "No ComfyUI Node Editor found!") | |
| return {'CANCELLED'} | |
| new_node_loc = sdn_area.regions[3].view2d.region_to_view(sdn_area.x + sdn_area.width / 2.0, sdn_area.y + sdn_area.height / 2.0) | |
| if context.area.type == 'NODE_EDITOR': | |
| sdn_area = context.area | |
| ime_area = get_imeditor(context, True) | |
| image = ime_area.spaces[0].image | |
| if not ime_area: | |
| self.report({'ERROR'}, "No Image Editor with an open image found!") | |
| return {'CANCELLED'} | |
| if not self.force_centered: | |
| new_node_loc = sdn_area.spaces[0].cursor_location | |
| else: | |
| new_node_loc = sdn_area.regions[3].view2d.region_to_view(sdn_area.x + sdn_area.width / 2.0, sdn_area.y + sdn_area.height / 2.0) | |
| if image.is_dirty: | |
| image.file_format = 'PNG' | |
| if image.alpha_mode != 'CHANNEL_PACKED': | |
| self.report({'WARNING'}, "The image is not using channel packed alpha. If you have painted a mask, the color underneath is black!") | |
| if image.source in ['VIEWER', 'GENERATED']: # viewer = render result | |
| filename = f"render_{uuid.uuid4()}" | |
| newpath = f"/tmp/{filename}.png" | |
| image.alpha_mode = 'CHANNEL_PACKED' | |
| image.save_render(filepath=newpath, scene=context.scene) | |
| newim = bpy.data.images.new(image.name + MODIFIED_IMAGE_SUFFIX, 32, 32, alpha=True) | |
| newim.source = 'FILE' | |
| newim.filepath = newpath | |
| ime_area.spaces[0].image = newim | |
| else: | |
| extensionless = image.filepath_raw[:image.filepath_raw.rfind(".")] | |
| if not extensionless.endswith(MODIFIED_IMAGE_SUFFIX): | |
| newpath = extensionless + MODIFIED_IMAGE_SUFFIX + ".png" | |
| image.save_render(filepath=newpath, scene=context.scene) # save_render is needed to properly save channel packed images | |
| image.filepath = newpath | |
| image.name = image.name | |
| else: | |
| image.save() | |
| active = sdn_area.spaces[0].node_tree.nodes.active | |
| if active and active.bl_idname == '输入图像' and active.select: # "Input Image" Blender-side node | |
| active.image = image.filepath_raw | |
| else: | |
| new_node = sdn_area.spaces[0].node_tree.nodes.new('输入图像') | |
| new_node.location = new_node_loc | |
| new_node.image = image.filepath_raw | |
| for n in sdn_area.spaces[0].node_tree.nodes: | |
| n.select = False | |
| new_node.select = True | |
| sdn_area.spaces[0].node_tree.nodes.active = new_node | |
| return {'FINISHED'} | |
| # There are a few cases in which images can't have their alpha changed through the UI, and channel packed alpha is needed to paint them properly. | |
| # This operator lets the user change the alpha forcefully. | |
| class Image_Set_Channel_Packed(bpy.types.Operator): | |
| bl_idname = "sdn.image_set_channel_packed" | |
| bl_label = "Set Image Alpha to Channel Packed" | |
| bl_description = "Set the current image's alpha to channel packed, even if the option is not displayed in the UI.\nThis allows masks with color to be properly painted onto the image" | |
| bl_translation_context = ctxt | |
| def poll(cls, context: Context): | |
| return context.space_data.type == 'IMAGE_EDITOR' and context.space_data.image and context.space_data.image.alpha_mode != 'CHANNEL_PACKED' | |
| def execute(self, context): | |
| context.space_data.image.alpha_mode = 'CHANNEL_PACKED' | |
| return {'FINISHED'} | |
| class Open_Log_Window(bpy.types.Operator): | |
| bl_idname = "sdn.open_log_window" | |
| bl_label = "" | |
| def execute(self, context: bpy.types.Context): | |
| WindowLogger.open_window() | |
| return {'FINISHED'} | |
| class CleanVRam(bpy.types.Operator): | |
| bl_idname = "sdn.clean_vram" | |
| bl_label = "Clean VRAM" | |
| bl_translation_context = ctxt | |
| bl_description = "Clean VRAM" | |
| def execute(self, context): | |
| TaskManager.clear_vram() | |
| return {"FINISHED"} | |
| def clear(_): | |
| Ops.is_advanced_enable = False | |
| bpy.app.handlers.load_pre.append(clear) | |
Xet Storage Details
- Size:
- 45.4 kB
- Xet hash:
- ac43076ec879629233180e7aa4f4562b0aeed78609f79a83698c606a41e512f0
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.