From 3c72f84170b6230a3262c02c866c54a40394d28c Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 3 Jun 2023 17:48:37 -0500 Subject: [PATCH 1/2] feat: added UI for script mode --- __init__.py | 5 +++-- geo_nodes.py | 19 ++++++++++++++++++- materials.py | 18 +++++++++++++++++- utils.py | 2 -- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/__init__.py b/__init__.py index dacc2e7..0ccd674 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ "name": "Node to Python", "description": "Convert Blender node groups to a Python add-on!", "author": "Brendan Parmer", - "version": (2, 1, 0), + "version": (2, 2, 0), "blender": (3, 0, 0), "location": "Node", "category": "Node", @@ -36,7 +36,8 @@ def draw(self, context): geo_nodes.GeoNodesToPythonPanel, materials.MaterialToPython, materials.SelectMaterialMenu, - materials.MaterialToPythonPanel] + materials.MaterialToPythonPanel + ] def register(): for cls in classes: diff --git a/geo_nodes.py b/geo_nodes.py index 93c4cc3..0ca27ff 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -168,9 +168,20 @@ class GeoNodesToPython(bpy.types.Operator): bl_label = "Geo Nodes to Python" bl_options = {'REGISTER', 'UNDO'} + mode : bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full addon") + ] + ) geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") def execute(self, context): + if self.mode == 'SCRIPT': + print("Script!") + elif self.mode == 'ADDON': + print("Addon!") #find node group to replicate nt = bpy.data.node_groups[self.geo_nodes_group_name] @@ -335,9 +346,15 @@ def apply_modifier(): file.close() zip_addon(zip_dir) - + self.report({'INFO'}, "NodeToPython: Saved geometry nodes group") return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + def draw(self, context): + self.layout.prop(self, "mode") + class SelectGeoNodesMenu(bpy.types.Menu): bl_idname = "NODE_MT_ntp_geo_nodes_selection" bl_label = "Select Geo Nodes" diff --git a/materials.py b/materials.py index 7b6b803..50213a1 100644 --- a/materials.py +++ b/materials.py @@ -81,9 +81,19 @@ class MaterialToPython(bpy.types.Operator): bl_label = "Material to Python" bl_options = {'REGISTER', 'UNDO'} + mode : bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full addon") + ] + ) material_name: bpy.props.StringProperty(name="Node Group") - def execute(self, context): + if self.mode == 'SCRIPT': + print("Script!") + elif self.mode == 'ADDON': + print("Addon!") #find node group to replicate nt = bpy.data.materials[self.material_name].node_tree if nt is None: @@ -220,7 +230,13 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): file.close() zip_addon(zip_dir) + self.report({'INFO'}, "NodeToPython: Saved material") return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + def draw(self, context): + self.layout.prop(self, "mode") class SelectMaterialMenu(bpy.types.Menu): bl_idname = "NODE_MT_npt_mat_selection" diff --git a/utils.py b/utils.py index 738c948..c81be18 100644 --- a/utils.py +++ b/utils.py @@ -286,9 +286,7 @@ def group_io_settings(node, file: TextIO, inner: str, io: str, node_tree_var: st socket = ntio[i] socket_var = f"{node_tree_var}.{io}s[{i}]" - print(f"{io} {i}: {name}") if inout.type in default_sockets: - print(socket.default_value) #default value if inout.type == 'RGBA': dv = vec4_to_py_str(socket.default_value) From a505ce6a02ea140042033b285c934d91a9084516 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 4 Jun 2023 14:16:02 -0500 Subject: [PATCH 2/2] feat: copies script to clipboard --- geo_nodes.py | 94 ++++++++++++++++++++++------------------ materials.py | 118 +++++++++++++++++++++++++++++++-------------------- utils.py | 4 +- 3 files changed, 128 insertions(+), 88 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 0ca27ff..e20b985 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -2,6 +2,7 @@ import os from .utils import * +from io import StringIO geo_node_settings = { # Attribute nodes @@ -178,37 +179,35 @@ class GeoNodesToPython(bpy.types.Operator): geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") def execute(self, context): - if self.mode == 'SCRIPT': - print("Script!") - elif self.mode == 'ADDON': - print("Addon!") #find node group to replicate nt = bpy.data.node_groups[self.geo_nodes_group_name] #set up names to use in generated addon nt_var = clean_string(nt.name) - class_name = clean_string(nt.name.replace(" ", "").replace('.', ""), - lower = False) - - #find base directory to save new addon - base_dir = bpy.path.abspath("//") - if not base_dir or base_dir == "": - self.report({'ERROR'}, - ("NodeToPython: Save your blend file before using " - "NodeToPython!")) - return {'CANCELLED'} - - #save in /addons/ subdirectory - zip_dir = os.path.join(base_dir, "addons", nt_var) - addon_dir = os.path.join(zip_dir, nt_var) - if not os.path.exists(addon_dir): - os.makedirs(addon_dir) - file = open(f"{addon_dir}/__init__.py", "w") - - create_header(file, nt.name) - init_operator(file, class_name, nt_var, nt.name) - file.write("\tdef execute(self, context):\n") + if self.mode == 'ADDON': + #find base directory to save new addon + base_dir = bpy.path.abspath("//") + if not base_dir or base_dir == "": + self.report({'ERROR'}, + ("NodeToPython: Save your blend file before using " + "NodeToPython!")) + return {'CANCELLED'} + + #save in addons/ subdirectory + zip_dir = os.path.join(base_dir, "addons", nt_var) + addon_dir = os.path.join(zip_dir, nt_var) + if not os.path.exists(addon_dir): + os.makedirs(addon_dir) + file = open(f"{addon_dir}/__init__.py", "w") + + create_header(file, nt.name) + class_name = clean_string(nt.name.replace(" ", "").replace('.', ""), + lower = False) + init_operator(file, class_name, nt_var, nt.name) + file.write("\tdef execute(self, context):\n") + else: + file = StringIO("") #set to keep track of already created node trees node_trees = set() @@ -229,8 +228,8 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): file.write(f"{outer}def {nt_var}_node_group():\n") file.write((f"{inner}{nt_var}" f"= bpy.data.node_groups.new(" - f"type = \"GeometryNodeTree\", " - f"name = \"{node_tree.name}\")\n")) + f"type = \'GeometryNodeTree\', " + f"name = {str_to_py_str(node_tree.name)})\n")) file.write("\n") inputs_set = False @@ -267,12 +266,12 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): if node.node_tree is not None: file.write((f"{inner}{node_var}.node_tree = " f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) + f"[{str_to_py_str(node.node_tree.name)}]\n")) elif node.bl_idname == 'ShaderNodeValToRGB': color_ramp_settings(node, file, inner, node_var) elif node.bl_idname in curve_nodes: curve_node_settings(node, file, inner, node_var) - elif node.bl_idname in image_nodes: + elif node.bl_idname in image_nodes and self.mode == 'ADDON': img = node.image if img is not None and img.source in {'FILE', 'GENERATED', 'TILED'}: save_image(img, addon_dir) @@ -295,7 +294,10 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): f"{attr_domain}\n")) if node.bl_idname != 'GeometryNodeSimulationInput': - set_input_defaults(node, file, inner, node_var, addon_dir) + if self.mode == 'ADDON': + set_input_defaults(node, file, inner, node_var, addon_dir) + else: + set_input_defaults(node, file, inner, node_var) set_output_defaults(node, file, inner, node_var) #create simulation zones @@ -306,7 +308,10 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): f"({sim_output_var})\n")) #must set defaults after paired with output - set_input_defaults(sim_input, file, inner, sim_input_var, addon_dir) + if self.mode == 'ADDON': + set_input_defaults(node, file, inner, node_var, addon_dir) + else: + set_input_defaults(node, file, inner, node_var) set_output_defaults(sim_input, file, inner, sim_input_var) set_parents(node_tree, file, inner, node_vars) @@ -322,7 +327,11 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): f"{nt_var}_node_group()\n\n")) return used_vars - process_geo_nodes_group(nt, 2, node_vars, used_vars) + if self.mode == 'ADDON': + level = 2 + else: + level = 0 + process_geo_nodes_group(nt, level, node_vars, used_vars) def apply_modifier(): #get object @@ -334,18 +343,21 @@ def apply_modifier(): file.write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, " f"type = 'NODES')\n")) file.write(f"\t\tmod.node_group = {nt_var}\n") - apply_modifier() - - file.write("\t\treturn {'FINISHED'}\n\n") - - create_menu_func(file, class_name) - create_register_func(file, class_name) - create_unregister_func(file, class_name) - create_main_func(file) + if self.mode == 'ADDON': + apply_modifier() + file.write("\t\treturn {'FINISHED'}\n\n") + + create_menu_func(file, class_name) + create_register_func(file, class_name) + create_unregister_func(file, class_name) + create_main_func(file) + else: + context.window_manager.clipboard = file.getvalue() file.close() - zip_addon(zip_dir) + if self.mode == 'ADDON': + zip_addon(zip_dir) self.report({'INFO'}, "NodeToPython: Saved geometry nodes group") return {'FINISHED'} diff --git a/materials.py b/materials.py index 50213a1..eea30fb 100644 --- a/materials.py +++ b/materials.py @@ -2,6 +2,7 @@ import os from .utils import * +from io import StringIO node_settings = { #input @@ -89,45 +90,49 @@ class MaterialToPython(bpy.types.Operator): ] ) material_name: bpy.props.StringProperty(name="Node Group") + def execute(self, context): - if self.mode == 'SCRIPT': - print("Script!") - elif self.mode == 'ADDON': - print("Addon!") #find node group to replicate nt = bpy.data.materials[self.material_name].node_tree if nt is None: - self.report({'ERROR'}, - ("NodeToPython: This doesn't seem to be a valid " - "material. Is Use Nodes selected?")) + self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " + "valid material. Is Use Nodes selected?")) return {'CANCELLED'} #set up names to use in generated addon mat_var = clean_string(self.material_name) - class_name = clean_string(self.material_name, lower=False) - dir = bpy.path.abspath("//") - if not dir or dir == "": - self.report({'ERROR'}, - ("NodeToPython: Save your blender file before using " - "NodeToPython!")) - return {'CANCELLED'} - zip_dir = os.path.join(dir, "addons", mat_var) - addon_dir = os.path.join(zip_dir, mat_var) - if not os.path.exists(addon_dir): - os.makedirs(addon_dir) - file = open(f"{addon_dir}/__init__.py", "w") - - create_header(file, self.material_name) - init_operator(file, class_name, mat_var, self.material_name) - - file.write("\tdef execute(self, context):\n") - - def create_material(): - file.write((f"\t\tmat = bpy.data.materials.new(" - f"name = \"{self.material_name}\")\n")) - file.write(f"\t\tmat.use_nodes = True\n") - create_material() + if self.mode == 'ADDON': + dir = bpy.path.abspath("//") + if not dir or dir == "": + self.report({'ERROR'}, + ("NodeToPython: Save your blender file before using " + "NodeToPython!")) + return {'CANCELLED'} + + #save in addons/ subdirectory + zip_dir = os.path.join(dir, "addons", mat_var) + addon_dir = os.path.join(zip_dir, mat_var) + if not os.path.exists(addon_dir): + os.makedirs(addon_dir) + file = open(f"{addon_dir}/__init__.py", "w") + + create_header(file, self.material_name) + class_name = clean_string(self.material_name, lower=False) + init_operator(file, class_name, mat_var, self.material_name) + + file.write("\tdef execute(self, context):\n") + else: + file = StringIO("") + + def create_material(indent: str): + file.write((f"{indent}mat = bpy.data.materials.new(" + f"name = {str_to_py_str(self.material_name)})\n")) + file.write(f"{indent}mat.use_nodes = True\n") + if self.mode == 'ADDON': + create_material("\t\t") + elif self.mode == 'SCRIPT': + create_material("") #set to keep track of already created node trees node_trees = set() @@ -138,9 +143,15 @@ def create_material(): #keeps track of all used variables used_vars = set() - def process_mat_node_group(node_tree, level, node_vars, used_vars): + def is_outermost_node_group(level: int) -> bool: + if self.mode == 'ADDON' and level == 2: + return True + elif self.mode == 'SCRIPT' and level == 0: + return True + return False - if level == 2: #outermost node group + def process_mat_node_group(node_tree, level, node_vars, used_vars): + if is_outermost_node_group(level): nt_var = create_var(self.material_name, used_vars) nt_name = self.material_name else: @@ -153,7 +164,7 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): file.write(f"{outer}#initialize {nt_var} node group\n") file.write(f"{outer}def {nt_var}_node_group():\n") - if level == 2: #outermost node group + if is_outermost_node_group(level): #outermost node group file.write(f"{inner}{nt_var} = mat.node_tree\n") file.write(f"{inner}#start with a clean node tree\n") file.write(f"{inner}for node in {nt_var}.nodes:\n") @@ -161,8 +172,8 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): else: file.write((f"{inner}{nt_var}" f"= bpy.data.node_groups.new(" - f"type = \"ShaderNodeTree\", " - f"name = \"{nt_name}\")\n")) + f"type = \'ShaderNodeTree\', " + f"name = {str_to_py_str(nt_name)})\n")) file.write("\n") inputs_set = False @@ -171,7 +182,9 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): #initialize nodes file.write(f"{inner}#initialize {nt_var} nodes\n") + #dictionary to keep track of node->variable name pairs node_vars = {} + for node in node_tree.nodes: if node.bl_idname == 'ShaderNodeGroup': node_nt = node.node_tree @@ -193,22 +206,28 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): elif node.bl_idname == 'NodeGroupInput' and not inputs_set: group_io_settings(node, file, inner, "input", nt_var, node_tree) inputs_set = True + elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: group_io_settings(node, file, inner, "output", nt_var, node_tree) outputs_set = True - elif node.bl_idname in image_nodes: + elif node.bl_idname in image_nodes and self.mode == 'ADDON': img = node.image if img is not None and img.source in {'FILE', 'GENERATED', 'TILED'}: save_image(img, addon_dir) load_image(img, file, inner, f"{node_var}.image") image_user_settings(node, file, inner, node_var) + elif node.bl_idname == 'ShaderNodeValToRGB': color_ramp_settings(node, file, inner, node_var) + elif node.bl_idname in curve_nodes: curve_node_settings(node, file, inner, node_var) - set_input_defaults(node, file, inner, node_var, addon_dir) + if self.mode == 'ADDON': + set_input_defaults(node, file, inner, node_var, addon_dir) + else: + set_input_defaults(node, file, inner, node_var) set_output_defaults(node, file, inner, node_var) set_parents(node_tree, file, inner, node_vars) @@ -218,18 +237,27 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): init_links(node_tree, file, inner, nt_var, node_vars) file.write(f"\n{outer}{nt_var}_node_group()\n\n") - - process_mat_node_group(nt, 2, node_vars, used_vars) - file.write("\t\treturn {'FINISHED'}\n\n") + if self.mode == 'ADDON': + level = 2 + else: + level = 0 + process_mat_node_group(nt, level, node_vars, used_vars) - create_menu_func(file, class_name) - create_register_func(file, class_name) - create_unregister_func(file, class_name) - create_main_func(file) + if self.mode == 'ADDON': + file.write("\t\treturn {'FINISHED'}\n\n") + + create_menu_func(file, class_name) + create_register_func(file, class_name) + create_unregister_func(file, class_name) + create_main_func(file) + else: + context.window_manager.clipboard = file.getvalue() file.close() - zip_addon(zip_dir) + + if self.mode == 'ADDON': + zip_addon(zip_dir) self.report({'INFO'}, "NodeToPython: Saved material") return {'FINISHED'} diff --git a/utils.py b/utils.py index c81be18..41d831b 100644 --- a/utils.py +++ b/utils.py @@ -441,7 +441,7 @@ def curve_node_settings(node, file: TextIO, inner: str, node_var: str): file.write(f"{mapping_var}.update()\n") def set_input_defaults(node, file: TextIO, inner: str, node_var: str, - addon_dir: str): + addon_dir: str = ""): """ Sets defaults for input sockets @@ -476,7 +476,7 @@ def set_input_defaults(node, file: TextIO, inner: str, node_var: str, elif input.bl_idname == 'NodeSocketImage': print("Input is linked: ", input.is_linked) img = input.default_value - if img is not None: + if img is not None and addon_dir != "": #write in a better way save_image(img, addon_dir) load_image(img, file, inner, f"{socket_var}.default_value") default_val = None