From ab694440155075f4ddc2b31e9aac37926cf0ea79 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 23 Dec 2022 22:31:34 -0600 Subject: [PATCH 01/60] feat: initial material->python setup --- material_to_python.py | 185 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 material_to_python.py diff --git a/material_to_python.py b/material_to_python.py new file mode 100644 index 0000000..7522587 --- /dev/null +++ b/material_to_python.py @@ -0,0 +1,185 @@ +import bpy +import os + +#node input sockets that are messy to set default values for +dont_set_defaults = {'NodeSocketCollection', + 'NodeSocketGeometry', + 'NodeSocketImage', + 'NodeSocketMaterial', + 'NodeSocketObject', + 'NodeSocketTexture', + 'NodeSocketVirtual'} + +def execute(self, material_name): + #validate material name + if material_name not in bpy.data.materials: + return {'FINISHED'} + + print("_________________________________") + #set up addon file + ng = bpy.data.materials[material_name].node_tree + ng_name = ng.name.lower().replace(' ', '_') + class_name = ng.name.replace(" ", "") + """ + dir = bpy.path.abspath("//") + if not dir or dir == "": + self.report({'ERROR'}, + ("NodeToPython: Save your blender file before using " + "NodeToPython!")) + return {'CANCELLED'} + addon_dir = os.path.join(dir, "addons") + if not os.path.exists(addon_dir): + os.mkdir(addon_dir) + file = open(f"{addon_dir}/{mat_ng_name}_addon.py", "w") + """ + #file = open("/home/Documents/Repos/NodeToPython/test.py", "w") + + """Sets up bl_info and imports Blender""" + def header(): + print("bl_info = {\n") + print(f"\t\"name\" : \"{ng.name}\",\n") + print("\t\"author\" : \"Node To Python\",\n") + print("\t\"version\" : (1, 0, 0),\n") + print(f"\t\"blender\" : {bpy.app.version},\n") + print("\t\"location\" : \"Object\",\n") + print("\t\"category\" : \"Object\"\n") + print("}\n") + print("\n") + print("import bpy\n") + print("\n") + header() + + """Creates the class and its variables""" + def init_class(): + print(f"class {class_name}(bpy.types.Operator):\n") + print(f"\tbl_idname = \"object.{ng_name}\"\n") + print(f"\tbl_label = \"{ng.name}\"\n") + print("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") + print("\n") + init_class() + + print("\tdef execute(self, context):\n") + + def process_mat_node_group(node_group, level): + ng_name = node_group.name.lower().replace(' ', '_') + + outer = "\t"*level + inner = "\t"*(level + 1) + + #initialize node group + print(f"{outer}#initialize {ng_name} node group\n") + print(f"{outer}def {ng_name}_node_group():\n") + print((f"{inner}{ng_name}" + f"= bpy.data.node_groups.new(" + f"type = \"ShaderNodeGroup\", " + f"name = \"{node_group.name}\")\n")) + print("\n") + + #initialize nodes + print(f"{inner}#initialize {ng_name} nodes\n") + + for node in node_group.nodes: + if node.bl_idname == 'ShaderNodeGroup': + process_mat_node_group(node.node_tree, level + 1) + #create node + node_name = node.name.lower() + node_name = node_name.replace(' ', '_').replace('.', '_') + print(f"{inner}#node {node.name}\n") + print((f"{inner}{node_name} " + f"= {ng_name}.nodes.new(\"{node.bl_idname}\")\n")) + print((f"{inner}{node_name}.location " + f"= ({node.location.x}, {node.location.y})\n")) + print((f"{inner}{node_name}.width, {node_name}.height " + f"= {node.width}, {node.height}\n")) + if node.label: + print(f"{inner}{node_name}.label = \"{node.label}\"\n") + + for i, input in enumerate(node.inputs): + if input.bl_idname not in dont_set_defaults: + dv = None + if input.bl_idname == 'NodeSocketColor': + col = input.default_value + dv = f"({col[0]}, {col[1]}, {col[2]}, {col[3]})" + elif "Vector" in input.bl_idname: + vector = input.default_value + dv = f"({vector[0]}, {vector[1]}, {vector[2]})" + elif input.bl_idname == 'NodeSocketString': + dv = f"\"\"" + else: + #TODO: fix this later + if input.bl_idname != 'NodeSocketShader': + dv = input.default_value + if dv is not None: + print(f"{inner}#{input.identifier}\n") + print((f"{inner}{node_name}" + f".inputs[{i}]" + f".default_value = {dv}\n")) + + #initialize links + if node_group.links: + print(f"{inner}#initialize {ng_name} links\n") + for link in node_group.links: + input_node = link.from_node.name.lower() + input_node = input_node.replace(' ', '_').replace('.', '_') + input_socket = link.from_socket + + """ + Blender's socket dictionary doesn't guarantee + unique keys, which has caused much wailing and + gnashing of teeth. This is a quick fix that + doesn't run quick + """ + for i, item in enumerate(link.from_node.outputs.items()): + if item[1] == input_socket: + input_idx = i + break + + output_node = link.to_node.name.lower() + output_node = output_node.replace(' ', '_').replace('.', '_') + output_socket = link.to_socket + + for i, item in enumerate(link.to_node.inputs.items()): + if item[1] == output_socket: + output_idx = i + break + + print((f"{inner}#{input_node}.{input_socket.name} " + f"-> {output_node}.{output_socket.name}\n")) + print((f"{inner}{ng_name}.links.new({input_node}" + f".outputs[{input_idx}], " + f"{output_node}.inputs[{output_idx}])\n")) + print(f"{outer}{ng_name}_node_group()\n") + + process_mat_node_group(ng, 2) + print("\t\treturn {'FINISHED'}\n\n") + + """Create the function that adds the addon to the menu""" + def create_menu_func(): + print("def menu_func(self, context):\n") + print(f"\tself.layout.operator({class_name}.bl_idname)\n") + print("\n") + create_menu_func() + + """Create the register function""" + def create_register(): + print("def register():\n") + print(f"\tbpy.utils.register_class({class_name})\n") + print("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") + print("\n") + create_register() + + """Create the unregister function""" + def create_unregister(): + print("def unregister():\n") + print(f"\tbpy.utils.unregister_class({class_name})\n") + print("\tbpy.types.VIEW3D_MT_objects.remove(menu_func)\n") + print("\n") + create_unregister() + + """Create the main function""" + def create_main(): + print("if __name__ == \"__main__\":\n") + print("\tregister()") + create_main() + +execute(None, "Material") \ No newline at end of file From 81fb0436b900b0ee57a2222cc7bd1c07499444c9 Mon Sep 17 00:00:00 2001 From: Carlsu~ <104013959+carls3d@users.noreply.github.com> Date: Mon, 2 Jan 2023 14:26:32 +0100 Subject: [PATCH 02/60] Added quick dropdown menu Added a dropdown menu for choosing which node tree to convert. Located in a new panel 'Node To Python' in the nodes space --- node_to_python.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/node_to_python.py b/node_to_python.py index abf834a..8c34c71 100644 --- a/node_to_python.py +++ b/node_to_python.py @@ -462,14 +462,62 @@ def create_main(): file.close() return {'FINISHED'} +class NodeToPythonMenu(bpy.types.Menu): + bl_idname = "NodeToPythonMenu" + bl_label = "" + + @classmethod + def poll(cls, context): + return not (False) + + def draw(self, context): + geo_node_groups = [node for node in bpy.data.node_groups if node.type == 'GEOMETRY'] + + layout = self.layout.column_flow(columns=1) + layout.operator_context = "INVOKE_DEFAULT" + for i in range(len(geo_node_groups)): + op = layout.operator(NodeToPython.bl_idname, text=geo_node_groups[i].name) + op.node_group_name = geo_node_groups[i].name + +class NodeToPythonPanel(bpy.types.Panel): + bl_label = 'Node To Python' + bl_idname = 'NodeToPythonPanel' + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = 'NodeToPython' + + @classmethod + def poll(cls, context): + return not (False) + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + geo_node_groups_exist = len([node for node in bpy.data.node_groups if node.type == 'GEOMETRY']) > 0 + menu_text = 'Nodes' + + layout = self.layout + col = layout.column() + row = col.row() + row.enabled = geo_node_groups_exist # Disables menu when len of geometry nodes is 0 + row.alignment = 'Expand'.upper() + row.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + row.menu('NodeToPythonMenu', text=menu_text) + def menu_func(self, context): self.layout.operator(NodeToPython.bl_idname, text=NodeToPython.bl_label) def register(): + bpy.utils.register_class(NodeToPythonMenu) + bpy.utils.register_class(NodeToPythonPanel) bpy.utils.register_class(NodeToPython) bpy.types.VIEW3D_MT_object.append(menu_func) def unregister(): + bpy.utils.unregister_class(NodeToPythonMenu) + bpy.utils.unregister_class(NodeToPythonPanel) bpy.utils.unregister_class(NodeToPython) bpy.types.VIEW3D_MT_object.remove(menu_func) From da4ea05931cb5a22b9574e2543526338e737dd82 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 2 Jan 2023 21:33:44 -0600 Subject: [PATCH 03/60] style: ui code style updates --- node_to_python.py | 71 +++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/node_to_python.py b/node_to_python.py index 8c34c71..99c3f3d 100644 --- a/node_to_python.py +++ b/node_to_python.py @@ -2,16 +2,12 @@ "name": "Node to Python", "description": "Convert Geometry Node Groups to a Python add-on", "author": "Brendan Parmer", - "version": (1, 0, 0), + "version": (2, 0, 0), "blender": (3, 0, 0), - "location": "Object", - "category": "Object", + "location": "Node", + "category": "Node", } - -"""TODO: compositing node tree""" -# https://blender.stackexchange.com/questions/62701/modify-nodes-in-compositing-nodetree-using-python - import bpy import os @@ -158,7 +154,7 @@ 'ShaderNodeRGBCurve'} class NodeToPython(bpy.types.Operator): - bl_idname = "object.node_to_python" + bl_idname = "node.node_to_python" bl_label = "Node to Python" bl_options = {'REGISTER', 'UNDO'} @@ -463,63 +459,60 @@ def create_main(): return {'FINISHED'} class NodeToPythonMenu(bpy.types.Menu): - bl_idname = "NodeToPythonMenu" - bl_label = "" - + bl_idname = "NODE_MT_node_to_python" + bl_label = "Node To Python" + @classmethod def poll(cls, context): - return not (False) + return True def draw(self, context): geo_node_groups = [node for node in bpy.data.node_groups if node.type == 'GEOMETRY'] - + layout = self.layout.column_flow(columns=1) - layout.operator_context = "INVOKE_DEFAULT" - for i in range(len(geo_node_groups)): - op = layout.operator(NodeToPython.bl_idname, text=geo_node_groups[i].name) - op.node_group_name = geo_node_groups[i].name + layout.operator_context = 'INVOKE_DEFAULT' + for geo_ng in geo_node_groups: + op = layout.operator(NodeToPython.bl_idname, text=geo_ng.name) + op.node_group_name = geo_ng.name class NodeToPythonPanel(bpy.types.Panel): - bl_label = 'Node To Python' - bl_idname = 'NodeToPythonPanel' + bl_label = "Node To Python" + bl_idname = "NODE_PT_node_to_python" bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' bl_context = '' - bl_category = 'NodeToPython' + bl_category = "NodeToPython" @classmethod def poll(cls, context): - return not (False) - + return True + def draw_header(self, context): layout = self.layout def draw(self, context): - geo_node_groups_exist = len([node for node in bpy.data.node_groups if node.type == 'GEOMETRY']) > 0 - menu_text = 'Nodes' - layout = self.layout col = layout.column() row = col.row() - row.enabled = geo_node_groups_exist # Disables menu when len of geometry nodes is 0 - row.alignment = 'Expand'.upper() - row.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" - row.menu('NodeToPythonMenu', text=menu_text) + + # Disables menu when len of geometry nodes is 0 + geo_node_groups = [node for node in bpy.data.node_groups if node.type == 'GEOMETRY'] + geo_node_groups_exist = len(geo_node_groups) > 0 + row.enabled = geo_node_groups_exist + + row.alignment = 'EXPAND' + row.operator_context = 'INVOKE_DEFAULT' + row.menu("NODE_MT_node_to_python", text="Geometry Node Groups") -def menu_func(self, context): - self.layout.operator(NodeToPython.bl_idname, text=NodeToPython.bl_label) +classes = [NodeToPythonMenu, NodeToPythonPanel, NodeToPython] def register(): - bpy.utils.register_class(NodeToPythonMenu) - bpy.utils.register_class(NodeToPythonPanel) - bpy.utils.register_class(NodeToPython) - bpy.types.VIEW3D_MT_object.append(menu_func) + for cls in classes: + bpy.utils.register_class(cls) def unregister(): - bpy.utils.unregister_class(NodeToPythonMenu) - bpy.utils.unregister_class(NodeToPythonPanel) - bpy.utils.unregister_class(NodeToPython) - bpy.types.VIEW3D_MT_object.remove(menu_func) + for cls in classes: + bpy.utils.unregister_class(cls) if __name__ == "__main__": register() \ No newline at end of file From ec92a1b087285f25d2340d0c68df8dd3320b8416 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 8 Jan 2023 19:43:52 -0600 Subject: [PATCH 04/60] feat: material update addon fluff --- material_to_python.py | 391 ++++++++++++++++++++++++------------------ 1 file changed, 220 insertions(+), 171 deletions(-) diff --git a/material_to_python.py b/material_to_python.py index 7522587..6ac2a9c 100644 --- a/material_to_python.py +++ b/material_to_python.py @@ -7,179 +7,228 @@ 'NodeSocketImage', 'NodeSocketMaterial', 'NodeSocketObject', + 'NodeSocketShader', 'NodeSocketTexture', 'NodeSocketVirtual'} + +curve_nodes = {'ShaderNodeFloatCurve', + 'ShaderNodeVectorCurve', + 'ShaderNodeRGBCurve'} + +class MaterialToPython(bpy.types.Operator): + bl_idname = "node.material_to_python" + bl_label = "Material to Python" + bl_options = {'REGISTER', 'UNDO'} + + material_name: bpy.props.StringProperty(name="Node Group") + + def execute(self, context): + if self.material_name not in bpy.data.materials: + return {'FINISHED'} -def execute(self, material_name): - #validate material name - if material_name not in bpy.data.materials: + #set up addon file + ng = bpy.data.materials[self.material_name].node_tree + ng_name = ng.name.lower().replace(' ', '_') + class_name = ng.name.replace(" ", "") + + dir = bpy.path.abspath("//") + if not dir or dir == "": + self.report({'ERROR'}, + ("NodeToPython: Save your blender file before using " + "NodeToPython!")) + return {'CANCELLED'} + addon_dir = os.path.join(dir, "addons") + if not os.path.exists(addon_dir): + os.mkdir(addon_dir) + file = open(f"{addon_dir}/{ng_name}_addon.py", "w") + + """Sets up bl_info and imports Blender""" + def header(): + file.write("bl_info = {\n") + file.write(f"\t\"name\" : \"{ng.name}\",\n") + file.write("\t\"author\" : \"Node To Python\",\n") + file.write("\t\"version\" : (1, 0, 0),\n") + file.write(f"\t\"blender\" : {bpy.app.version},\n") + file.write("\t\"location\" : \"Object\",\n") + file.write("\t\"category\" : \"Object\"\n") + file.write("}\n") + file.write("\n") + file.write("import bpy\n") + file.write("\n") + header() + + """Creates the class and its variables""" + def init_class(): + file.write(f"class {class_name}(bpy.types.Operator):\n") + file.write(f"\tbl_idname = \"object.{ng_name}\"\n") + file.write(f"\tbl_label = \"{ng.name}\"\n") + file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") + file.write("\n") + init_class() + + file.write("\tdef execute(self, context):\n") + + def process_mat_node_group(node_group, level): + ng_name = node_group.name.lower().replace(' ', '_') + + outer = "\t"*level + inner = "\t"*(level + 1) + + #initialize node group + file.write(f"{outer}#initialize {ng_name} node group\n") + file.write(f"{outer}def {ng_name}_node_group():\n") + file.write((f"{inner}{ng_name}" + f"= bpy.data.node_groups.new(" + f"type = \"ShaderNodeGroup\", " + f"name = \"{node_group.name}\")\n")) + file.write("\n") + + #initialize nodes + file.write(f"{inner}#initialize {ng_name} nodes\n") + + for node in node_group.nodes: + if node.bl_idname == 'ShaderNodeGroup': + process_mat_node_group(node.node_tree, level + 1) + #create node + node_name = node.name.lower() + node_name = node_name.replace(' ', '_').replace('.', '_') + file.write(f"{inner}#node {node.name}\n") + file.write((f"{inner}{node_name} " + f"= {ng_name}.nodes.new(\"{node.bl_idname}\")\n")) + file.write((f"{inner}{node_name}.location " + f"= ({node.location.x}, {node.location.y})\n")) + file.write((f"{inner}{node_name}.width, {node_name}.height " + f"= {node.width}, {node.height}\n")) + if node.label: + file.write(f"{inner}{node_name}.label = \"{node.label}\"\n") + + for i, input in enumerate(node.inputs): + if input.bl_idname not in dont_set_defaults: + dv = None + if input.bl_idname == 'NodeSocketColor': + col = input.default_value + dv = f"({col[0]}, {col[1]}, {col[2]}, {col[3]})" + elif "Vector" in input.bl_idname: + vector = input.default_value + dv = f"({vector[0]}, {vector[1]}, {vector[2]})" + elif input.bl_idname == 'NodeSocketString': + dv = f"\"\"" + else: + dv = input.default_value + if dv is not None: + file.write(f"{inner}#{input.identifier}\n") + file.write((f"{inner}{node_name}" + f".inputs[{i}]" + f".default_value = {dv}\n")) + + #initialize links + if node_group.links: + file.write(f"{inner}#initialize {ng_name} links\n") + for link in node_group.links: + input_node = link.from_node.name.lower() + input_node = input_node.replace(' ', '_').replace('.', '_') + input_socket = link.from_socket + + """ + Blender's socket dictionary doesn't guarantee + unique keys, which has caused much wailing and + gnashing of teeth. This is a quick fix that + doesn't run quick + """ + for i, item in enumerate(link.from_node.outputs.items()): + if item[1] == input_socket: + input_idx = i + break + + output_node = link.to_node.name.lower() + output_node = output_node.replace(' ', '_').replace('.', '_') + output_socket = link.to_socket + + for i, item in enumerate(link.to_node.inputs.items()): + if item[1] == output_socket: + output_idx = i + break + + file.write((f"{inner}#{input_node}.{input_socket.name} " + f"-> {output_node}.{output_socket.name}\n")) + file.write((f"{inner}{ng_name}.links.new({input_node}" + f".outputs[{input_idx}], " + f"{output_node}.inputs[{output_idx}])\n")) + file.write(f"{outer}{ng_name}_node_group()\n") + + process_mat_node_group(ng, 2) + file.write("\t\treturn {'FINISHED'}\n\n") + + """Create the function that adds the addon to the menu""" + def create_menu_func(): + file.write("def menu_func(self, context):\n") + file.write(f"\tself.layout.operator({class_name}.bl_idname)\n") + file.write("\n") + create_menu_func() + + """Create the register function""" + def create_register(): + file.write("def register():\n") + file.write(f"\tbpy.utils.register_class({class_name})\n") + file.write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") + file.write("\n") + create_register() + + """Create the unregister function""" + def create_unregister(): + file.write("def unregister():\n") + file.write(f"\tbpy.utils.unregister_class({class_name})\n") + file.write("\tbpy.types.VIEW3D_MT_objects.remove(menu_func)\n") + file.write("\n") + create_unregister() + + """Create the main function""" + def create_main(): + file.write("if __name__ == \"__main__\":\n") + file.write("\tregister()") + create_main() + + file.close() return {'FINISHED'} + +class NodeToPythonPanel(bpy.types.Panel): + bl_label = "Node To Python" + bl_idname = "NODE_PT_node_to_python" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + @classmethod + def poll(cls, context): + return True - print("_________________________________") - #set up addon file - ng = bpy.data.materials[material_name].node_tree - ng_name = ng.name.lower().replace(' ', '_') - class_name = ng.name.replace(" ", "") - """ - dir = bpy.path.abspath("//") - if not dir or dir == "": - self.report({'ERROR'}, - ("NodeToPython: Save your blender file before using " - "NodeToPython!")) - return {'CANCELLED'} - addon_dir = os.path.join(dir, "addons") - if not os.path.exists(addon_dir): - os.mkdir(addon_dir) - file = open(f"{addon_dir}/{mat_ng_name}_addon.py", "w") - """ - #file = open("/home/Documents/Repos/NodeToPython/test.py", "w") - - """Sets up bl_info and imports Blender""" - def header(): - print("bl_info = {\n") - print(f"\t\"name\" : \"{ng.name}\",\n") - print("\t\"author\" : \"Node To Python\",\n") - print("\t\"version\" : (1, 0, 0),\n") - print(f"\t\"blender\" : {bpy.app.version},\n") - print("\t\"location\" : \"Object\",\n") - print("\t\"category\" : \"Object\"\n") - print("}\n") - print("\n") - print("import bpy\n") - print("\n") - header() - - """Creates the class and its variables""" - def init_class(): - print(f"class {class_name}(bpy.types.Operator):\n") - print(f"\tbl_idname = \"object.{ng_name}\"\n") - print(f"\tbl_label = \"{ng.name}\"\n") - print("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") - print("\n") - init_class() - - print("\tdef execute(self, context):\n") - - def process_mat_node_group(node_group, level): - ng_name = node_group.name.lower().replace(' ', '_') - - outer = "\t"*level - inner = "\t"*(level + 1) - - #initialize node group - print(f"{outer}#initialize {ng_name} node group\n") - print(f"{outer}def {ng_name}_node_group():\n") - print((f"{inner}{ng_name}" - f"= bpy.data.node_groups.new(" - f"type = \"ShaderNodeGroup\", " - f"name = \"{node_group.name}\")\n")) - print("\n") - - #initialize nodes - print(f"{inner}#initialize {ng_name} nodes\n") - - for node in node_group.nodes: - if node.bl_idname == 'ShaderNodeGroup': - process_mat_node_group(node.node_tree, level + 1) - #create node - node_name = node.name.lower() - node_name = node_name.replace(' ', '_').replace('.', '_') - print(f"{inner}#node {node.name}\n") - print((f"{inner}{node_name} " - f"= {ng_name}.nodes.new(\"{node.bl_idname}\")\n")) - print((f"{inner}{node_name}.location " - f"= ({node.location.x}, {node.location.y})\n")) - print((f"{inner}{node_name}.width, {node_name}.height " - f"= {node.width}, {node.height}\n")) - if node.label: - print(f"{inner}{node_name}.label = \"{node.label}\"\n") - - for i, input in enumerate(node.inputs): - if input.bl_idname not in dont_set_defaults: - dv = None - if input.bl_idname == 'NodeSocketColor': - col = input.default_value - dv = f"({col[0]}, {col[1]}, {col[2]}, {col[3]})" - elif "Vector" in input.bl_idname: - vector = input.default_value - dv = f"({vector[0]}, {vector[1]}, {vector[2]})" - elif input.bl_idname == 'NodeSocketString': - dv = f"\"\"" - else: - #TODO: fix this later - if input.bl_idname != 'NodeSocketShader': - dv = input.default_value - if dv is not None: - print(f"{inner}#{input.identifier}\n") - print((f"{inner}{node_name}" - f".inputs[{i}]" - f".default_value = {dv}\n")) - - #initialize links - if node_group.links: - print(f"{inner}#initialize {ng_name} links\n") - for link in node_group.links: - input_node = link.from_node.name.lower() - input_node = input_node.replace(' ', '_').replace('.', '_') - input_socket = link.from_socket - - """ - Blender's socket dictionary doesn't guarantee - unique keys, which has caused much wailing and - gnashing of teeth. This is a quick fix that - doesn't run quick - """ - for i, item in enumerate(link.from_node.outputs.items()): - if item[1] == input_socket: - input_idx = i - break - - output_node = link.to_node.name.lower() - output_node = output_node.replace(' ', '_').replace('.', '_') - output_socket = link.to_socket - - for i, item in enumerate(link.to_node.inputs.items()): - if item[1] == output_socket: - output_idx = i - break - - print((f"{inner}#{input_node}.{input_socket.name} " - f"-> {output_node}.{output_socket.name}\n")) - print((f"{inner}{ng_name}.links.new({input_node}" - f".outputs[{input_idx}], " - f"{output_node}.inputs[{output_idx}])\n")) - print(f"{outer}{ng_name}_node_group()\n") - - process_mat_node_group(ng, 2) - print("\t\treturn {'FINISHED'}\n\n") - - """Create the function that adds the addon to the menu""" - def create_menu_func(): - print("def menu_func(self, context):\n") - print(f"\tself.layout.operator({class_name}.bl_idname)\n") - print("\n") - create_menu_func() - - """Create the register function""" - def create_register(): - print("def register():\n") - print(f"\tbpy.utils.register_class({class_name})\n") - print("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") - print("\n") - create_register() - - """Create the unregister function""" - def create_unregister(): - print("def unregister():\n") - print(f"\tbpy.utils.unregister_class({class_name})\n") - print("\tbpy.types.VIEW3D_MT_objects.remove(menu_func)\n") - print("\n") - create_unregister() - - """Create the main function""" - def create_main(): - print("if __name__ == \"__main__\":\n") - print("\tregister()") - create_main() - -execute(None, "Material") \ No newline at end of file + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + col = layout.column() + row = col.row() + + # Disables menu when len of geometry nodes is 0 + geo_node_groups = [node for node in bpy.data.node_groups if node.type == 'GEOMETRY'] + geo_node_groups_exist = len(geo_node_groups) > 0 + row.enabled = geo_node_groups_exist + + row.alignment = 'EXPAND' + row.operator_context = 'INVOKE_DEFAULT' + row.menu("NODE_MT_node_to_python", text="Geometry Node Groups") + +classes = [NodeToPythonMenu, NodeToPythonPanel, NodeToPython] + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) + +if __name__ == "__main__": + register() \ No newline at end of file From 5d40641b4e0ec8db5d4f1a99b98f9dddbb893180 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 8 Jan 2023 23:04:37 -0600 Subject: [PATCH 05/60] feat: basic material recreation --- README.md | 3 + material_to_python.py | 287 +++++++++++++++++++++++++++++++++++------- node_to_python.py | 6 +- 3 files changed, 250 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index b2d79df..2a55cd8 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ Download `node_to_python.py`, and install it to Blender like other add-ons. Then * Materials * Objects * Textures + * Scripts + * IES + * Filepaths as they won't exist in every blend file. In the future, I may have the script automatically recreate these assets, espcially with materials. diff --git a/material_to_python.py b/material_to_python.py index 6ac2a9c..88e92d0 100644 --- a/material_to_python.py +++ b/material_to_python.py @@ -1,6 +1,19 @@ import bpy +import mathutils import os +#node tree input sockets that have default properties +default_sockets = {'NodeSocketBool', + 'NodeSocketColor', + 'NodeSocketFloat', + 'NodeSocketInt', + 'NodeSocketVector'} + +#node tree input sockets that have min/max properties +value_sockets = {'NodeSocketInt', + 'NodeSocketFloat', + 'NodeSocketVector'} + #node input sockets that are messy to set default values for dont_set_defaults = {'NodeSocketCollection', 'NodeSocketGeometry', @@ -11,10 +24,82 @@ 'NodeSocketTexture', 'NodeSocketVirtual'} +node_settings = { + #input + "ShaderNodeAmbientOcclusion" : ["samples", "inside", "only_local"], + "ShaderNodeAttribute" : ["attribute_type", "attribute_name"], + "ShaderNodeBevel" : ["samples"], + "ShaderNodeVertexColor" : ["layer_name"], + "ShaderNodeTangent" : ["direction_type", "axis"], + "ShaderNodeTexCoord" : ["from_instancer"], + "ShaderNodeUVMap" : ["from_instancer", "uv_map"], + "ShaderNodeWireframe" : ["use_pixel_size"], + + #output + "ShaderNodeOutputAOV" : ["name"], + "ShaderNodeOutputMaterial" : ["target"], + + #shader + "ShaderNodeBsdfGlass" : ["distribution"], + "ShaderNodeBsdfGlossy" : ["distribution"], + "ShaderNodeBsdfPrincipled" : ["distribution", "subsurface_method"], + "ShaderNodeBsdfRefraction" : ["distribution"], + "ShaderNodeSubsurfaceScattering" : ["falloff"], + + #texture + "ShaderNodeTexBrick" : ["offset", "offset_frequency", "squash", "squash_frequency"], + "ShaderNodeTexEnvironment" : ["interpolation", "projection", "image_user.frame_duration", "image_user.frame_start", "image_user.frame_offset", "image_user.use_cyclic", "image_user.use_auto_refresh"], + "ShaderNodeTexGradient" : ["gradient_type"], + "ShaderNodeTexIES" : ["mode"], + "ShaderNodeTexImage" : ["interpolation", "projection", "projection_blend", + "extension"], + "ShaderNodeTexMagic" : ["turbulence_depth"], + "ShaderNodeTexMusgrave" : ["musgrave_dimensions", "musgrave_type"], + "ShaderNodeTexNoise" : ["noise_dimensions"], + "ShaderNodeTexPointDensity" : ["point_source", "space", "radius", + "interpolation", "resolution", + "vertex_color_source"], + "ShaderNodeTexSky" : ["sky_type", "sun_direction", "turbidity", + "ground_albedo", "sun_disc", "sun_elevation", + "sun_rotation", "altitude", "air_density", + "dust_density", "ozone_density"], + "ShaderNodeTexVoronoi" : ["voronoi_dimensions", "feature", "distance"], + "ShaderNodeTexWave" : ["wave_type", "rings_direction", "wave_profile"], + "ShaderNodeTexWhiteNoise" : ["noise_dimensions"], + + #color + "ShaderNodeMix" : ["data_type", "clamp_factor", "factor_mode", "blend_type", + "clamp_result"], + + #vector + "ShaderNodeBump" : ["invert"], + "ShaderNodeDisplacement" : ["space"], + "ShaderNodeMapping" : ["vector_type"], + "ShaderNodeNormalMap" : ["space", "uv_map"], + "ShaderNodeVectorDisplacement" : ["space"], + "ShaderNodeVectorRotate" : ["rotation_type", "invert"], + "ShaderNodeVectorTransform" : ["vector_type", "convert_from", "convert_to"], + + #converter + "ShaderNodeClamp" : ["clamp_type"], + "ShaderNodeCombineColor" : ["mode"], + "ShaderNodeMapRange" : ["data_type", "interpolation_type", "clamp"], + "ShaderNodeMath" : ["operation", "use_clamp"], + "ShaderNodeSeparateColor" : ["mode"], + "ShaderNodeVectorMath" : ["operation"] +} + curve_nodes = {'ShaderNodeFloatCurve', 'ShaderNodeVectorCurve', 'ShaderNodeRGBCurve'} +def clean_string(string: str): + bad_chars = [' ', '.', '/'] + clean_str = string.lower() + for char in bad_chars: + clean_str = clean_str.replace(char, '_') + return clean_str + class MaterialToPython(bpy.types.Operator): bl_idname = "node.material_to_python" bl_label = "Material to Python" @@ -28,7 +113,7 @@ def execute(self, context): #set up addon file ng = bpy.data.materials[self.material_name].node_tree - ng_name = ng.name.lower().replace(' ', '_') + ng_name = clean_string(self.material_name) class_name = ng.name.replace(" ", "") dir = bpy.path.abspath("//") @@ -45,7 +130,7 @@ def execute(self, context): """Sets up bl_info and imports Blender""" def header(): file.write("bl_info = {\n") - file.write(f"\t\"name\" : \"{ng.name}\",\n") + file.write(f"\t\"name\" : \"{self.material_name}\",\n") file.write("\t\"author\" : \"Node To Python\",\n") file.write("\t\"version\" : (1, 0, 0),\n") file.write(f"\t\"blender\" : {bpy.app.version},\n") @@ -61,7 +146,7 @@ def header(): def init_class(): file.write(f"class {class_name}(bpy.types.Operator):\n") file.write(f"\tbl_idname = \"object.{ng_name}\"\n") - file.write(f"\tbl_label = \"{ng.name}\"\n") + file.write(f"\tbl_label = \"{self.material_name}\"\n") file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") file.write("\n") init_class() @@ -69,7 +154,9 @@ def init_class(): file.write("\tdef execute(self, context):\n") def process_mat_node_group(node_group, level): - ng_name = node_group.name.lower().replace(' ', '_') + ng_name = clean_string(node_group.name) + if level == 2: + ng_name = clean_string(self.material_name) outer = "\t"*level inner = "\t"*(level + 1) @@ -79,54 +166,155 @@ def process_mat_node_group(node_group, level): file.write(f"{outer}def {ng_name}_node_group():\n") file.write((f"{inner}{ng_name}" f"= bpy.data.node_groups.new(" - f"type = \"ShaderNodeGroup\", " + f"type = \"ShaderNodeTree\", " f"name = \"{node_group.name}\")\n")) file.write("\n") #initialize nodes file.write(f"{inner}#initialize {ng_name} nodes\n") + """ + The bl_idname for AOV output nodes is the name field. + I've been using these for the variable names, but if you don't name + the AOV node it just doesn't assign anything, so we need to do it + manually. + """ + unnamed_index = 0 for node in node_group.nodes: if node.bl_idname == 'ShaderNodeGroup': process_mat_node_group(node.node_tree, level + 1) #create node - node_name = node.name.lower() - node_name = node_name.replace(' ', '_').replace('.', '_') + node_name = clean_string(node.name) + if node_name == "": + node_name = f"node_{unnamed_index}" + unnamed_index += 1 + file.write(f"{inner}#node {node.name}\n") file.write((f"{inner}{node_name} " - f"= {ng_name}.nodes.new(\"{node.bl_idname}\")\n")) + f"= {ng_name}.nodes.new(\"{node.bl_idname}\")\n")) file.write((f"{inner}{node_name}.location " - f"= ({node.location.x}, {node.location.y})\n")) + f"= ({node.location.x}, {node.location.y})\n")) file.write((f"{inner}{node_name}.width, {node_name}.height " - f"= {node.width}, {node.height}\n")) + f"= {node.width}, {node.height}\n")) if node.label: file.write(f"{inner}{node_name}.label = \"{node.label}\"\n") + + #special nodes + if node.bl_idname in node_settings: + for setting in node_settings[node.bl_idname]: + attr = getattr(node, setting, None) + if attr: + if type(attr) == str: + attr = f"\'{attr}\'" + if type(attr) == mathutils.Vector: + attr = f"({attr[0]}, {attr[1]}, {attr[2]})" + file.write((f"{inner}{node_name}.{setting} " + f"= {attr}\n")) + elif node.bl_idname == 'ShaderNodeGroup': + file.write((f"{inner}{node_name}.node_tree = " + f"bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) + elif node.bl_idname == 'ShaderNodeValToRGB': + color_ramp = node.color_ramp + file.write("\n") + file.write((f"{inner}{node_name}.color_ramp.color_mode = " + f"\'{color_ramp.color_mode}\'\n")) + file.write((f"{inner}{node_name}.color_ramp" + f".hue_interpolation = " + f"\'{color_ramp.hue_interpolation}\'\n")) + file.write((f"{inner}{node_name}.color_ramp.interpolation " + f"= '{color_ramp.interpolation}'\n")) + file.write("\n") + for i, element in enumerate(color_ramp.elements): + file.write((f"{inner}{node_name}_cre_{i} = " + f"{node_name}.color_ramp.elements" + f".new({element.position})\n")) + file.write((f"{inner}{node_name}_cre_{i}.alpha = " + f"{element.alpha}\n")) + col = element.color + r, g, b, a = col[0], col[1], col[2], col[3] + file.write((f"{inner}{node_name}_cre_{i}.color = " + f"({r}, {g}, {b}, {a})\n\n")) + elif node.bl_idname in curve_nodes: + file.write(f"{inner}#mapping settings\n") + mapping = f"{inner}{node_name}.mapping" + + extend = f"\'{node.mapping.extend}\'" + file.write(f"{mapping}.extend = {extend}\n") + tone = f"\'{node.mapping.tone}\'" + file.write(f"{mapping}.tone = {tone}\n") + + b_lvl = node.mapping.black_level + b_lvl_str = f"({b_lvl[0]}, {b_lvl[1]}, {b_lvl[2]})" + file.write((f"{mapping}.black_level = {b_lvl_str}\n")) + w_lvl = node.mapping.white_level + w_lvl_str = f"({w_lvl[0]}, {w_lvl[1]}, {w_lvl[2]})" + file.write((f"{mapping}.white_level = {w_lvl_str}\n")) - for i, input in enumerate(node.inputs): - if input.bl_idname not in dont_set_defaults: - dv = None - if input.bl_idname == 'NodeSocketColor': - col = input.default_value - dv = f"({col[0]}, {col[1]}, {col[2]}, {col[3]})" - elif "Vector" in input.bl_idname: - vector = input.default_value - dv = f"({vector[0]}, {vector[1]}, {vector[2]})" - elif input.bl_idname == 'NodeSocketString': - dv = f"\"\"" - else: - dv = input.default_value - if dv is not None: - file.write(f"{inner}#{input.identifier}\n") - file.write((f"{inner}{node_name}" - f".inputs[{i}]" - f".default_value = {dv}\n")) + min_x = node.mapping.clip_min_x + file.write(f"{mapping}.clip_min_x = {min_x}\n") + min_y = node.mapping.clip_min_y + file.write(f"{mapping}.clip_min_y = {min_y}\n") + max_x = node.mapping.clip_max_x + file.write(f"{mapping}.clip_max_x = {max_x}\n") + max_y = node.mapping.clip_max_y + file.write(f"{mapping}.clip_max_y = {max_y}\n") + + use_clip = node.mapping.use_clip + file.write(f"{mapping}.use_clip = {use_clip}\n") + + for i, curve in enumerate(node.mapping.curves): + file.write(f"{inner}#curve {i}\n") + curve_i = f"{node_name}_curve_{i}" + file.write((f"{inner}{curve_i} = " + f"{node_name}.mapping.curves[{i}]\n")) + for j, point in enumerate(curve.points): + point_j = f"{inner}{curve_i}_point_{j}" + + loc = point.location + file.write((f"{point_j} = " + f"{curve_i}.points.new" + f"({loc[0]}, {loc[1]})\n")) + + handle = f"\'{point.handle_type}\'" + file.write(f"{point_j}.handle_type = {handle}\n") + file.write(f"{inner}#update curve after changes") + file.write(f"{mapping}.update()\n") + + if node.bl_idname != 'NodeReroute': + def default_value(i, socket, list_name): + if socket.bl_idname not in dont_set_defaults: + dv = None + if socket.bl_idname == 'NodeSocketColor': + col = socket.default_value + dv = f"({col[0]}, {col[1]}, {col[2]}, {col[3]})" + elif "Vector" in socket.bl_idname: + vector = socket.default_value + dv = f"({vector[0]}, {vector[1]}, {vector[2]})" + elif socket.bl_idname == 'NodeSocketString': + dv = f"\"\"" + else: + dv = socket.default_value + if dv is not None: + file.write(f"{inner}#{socket.identifier}\n") + file.write((f"{inner}{node_name}" + f".{list_name}[{i}]" + f".default_value = {dv}\n")) + for i, input in enumerate(node.inputs): + default_value(i, input, "inputs") + """ + TODO: some shader nodes require you set the default value in the output. + this will need to be handled case by case it looks like though + + for i, output in enumerate(node.outputs): + default_value(i, output, "outputs") + """ #initialize links if node_group.links: file.write(f"{inner}#initialize {ng_name} links\n") for link in node_group.links: - input_node = link.from_node.name.lower() - input_node = input_node.replace(' ', '_').replace('.', '_') + input_node = clean_string(link.from_node.name) input_socket = link.from_socket """ @@ -140,8 +328,7 @@ def process_mat_node_group(node_group, level): input_idx = i break - output_node = link.to_node.name.lower() - output_node = output_node.replace(' ', '_').replace('.', '_') + output_node = clean_string(link.to_node.name) output_socket = link.to_socket for i, item in enumerate(link.to_node.inputs.items()): @@ -191,9 +378,24 @@ def create_main(): file.close() return {'FINISHED'} -class NodeToPythonPanel(bpy.types.Panel): - bl_label = "Node To Python" - bl_idname = "NODE_PT_node_to_python" +class SelectMaterialMenu(bpy.types.Menu): + bl_idname = "NODE_MT_npt_mat_selection" + bl_label = "Select Material" + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + for mat in bpy.data.materials: + op = layout.operator(MaterialToPython.bl_idname, text=mat.name) + op.material_name = mat.name + +class MaterialToPythonPanel(bpy.types.Panel): + bl_label = "Material to Python" + bl_idname = "NODE_PT_mat_to_python" bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' bl_context = '' @@ -208,19 +410,18 @@ def draw_header(self, context): def draw(self, context): layout = self.layout - col = layout.column() - row = col.row() + row = layout.row() - # Disables menu when len of geometry nodes is 0 - geo_node_groups = [node for node in bpy.data.node_groups if node.type == 'GEOMETRY'] - geo_node_groups_exist = len(geo_node_groups) > 0 - row.enabled = geo_node_groups_exist + # Disables menu when there are no materials + materials = [mat for mat in bpy.data.materials] + materials_exist = len(materials) > 0 + row.enabled = materials_exist row.alignment = 'EXPAND' row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_node_to_python", text="Geometry Node Groups") + row.menu("NODE_MT_npt_mat_selection", text="Materials") -classes = [NodeToPythonMenu, NodeToPythonPanel, NodeToPython] +classes = [MaterialToPythonPanel, MaterialToPython] def register(): for cls in classes: diff --git a/node_to_python.py b/node_to_python.py index 3cb309f..dfec67e 100644 --- a/node_to_python.py +++ b/node_to_python.py @@ -320,7 +320,7 @@ def process_node_group(node_group, level): file.write((f"{inner}{node_name}_cre_{i}.color = " f"({r}, {g}, {b}, {a})\n\n")) elif node.bl_idname in curve_nodes: - file.write(f"{inner}#mapping settings") + file.write(f"{inner}#mapping settings\n") mapping = f"{inner}{node_name}.mapping" extend = f"\'{node.mapping.extend}\'" @@ -348,7 +348,7 @@ def process_node_group(node_group, level): file.write(f"{mapping}.use_clip = {use_clip}\n") for i, curve in enumerate(node.mapping.curves): - file.write(f"{inner}#curve {i}") + file.write(f"{inner}#curve {i}\n") curve_i = f"{node_name}_curve_{i}" file.write((f"{inner}{curve_i} = " f"{node_name}.mapping.curves[{i}]\n")) @@ -476,7 +476,7 @@ def draw(self, context): op.node_group_name = geo_ng.name class NodeToPythonPanel(bpy.types.Panel): - bl_label = "Node To Python" + bl_label = "Geometry Nodes to Python" bl_idname = "NODE_PT_node_to_python" bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' From fb635be1cbaf9d4bac70244243e95498be84d020 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 9 Jan 2023 21:55:28 -0600 Subject: [PATCH 06/60] refactor: geo_nodes logic now handled in separate file --- __init__.py | 49 +++++++ __pycache__/geo_nodes.cpython-310.pyc | Bin 0 -> 15522 bytes node_to_python.py => geo_nodes.py | 189 +++++++++++++------------- material_to_python.py => materials.py | 8 +- 4 files changed, 149 insertions(+), 97 deletions(-) create mode 100644 __init__.py create mode 100644 __pycache__/geo_nodes.cpython-310.pyc rename node_to_python.py => geo_nodes.py (77%) rename material_to_python.py => materials.py (99%) diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..513d9d3 --- /dev/null +++ b/__init__.py @@ -0,0 +1,49 @@ +bl_info = { + "name": "Node to Python", + "description": "Convert Blender node groups to a Python add-on!", + "author": "Brendan Parmer", + "version": (2, 0, 0), + "blender": (3, 0, 0), + "location": "Node", + "category": "Node", +} + +if "bpy" in locals(): + import importlib + importlib.reload(materials) + importlib.reload(geo_nodes) +else: + from . import materials + from . import geo_nodes + +import bpy + +class NodeToPythonMenu(bpy.types.Menu): + bl_idname = "NODE_MT_node_to_python" + bl_label = "Node To Python" + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + +classes = [NodeToPythonMenu, + geo_nodes.GeoNodesToPython, + geo_nodes.SelectGeoNodesMenu, + geo_nodes.GeoNodesToPythonPanel, + materials.MaterialToPython, + materials.SelectMaterialMenu, + materials.MaterialToPythonPanel] + +def register(): + for cls in classes: + bpy.utils.register_class(cls) +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/__pycache__/geo_nodes.cpython-310.pyc b/__pycache__/geo_nodes.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..300890bab1f23ffefccf9d5e5503ddaa2fed466e GIT binary patch literal 15522 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 Pv<{fU)GZxbfDir=EEW4c literal 0 HcmV?d00001 diff --git a/node_to_python.py b/geo_nodes.py similarity index 77% rename from node_to_python.py rename to geo_nodes.py index dfec67e..551ad84 100644 --- a/node_to_python.py +++ b/geo_nodes.py @@ -32,13 +32,13 @@ 'NodeSocketTexture', 'NodeSocketVirtual'} -node_settings = { +geo_node_settings = { #attribute - "GeometryNodeAttributeStatistic" : ["data_type", "domain"], - "GeometryNodeCaptureAttribute" : ["data_type", "domain"], - "GeometryNodeAttributeDomainSize" : ["component"], - "GeometryNodeStoreNamedAttribute" : ["data_type", "domain"], - "GeometryNodeAttributeTransfer" : ["data_type", "mapping"], + "GeometryNodeAttributeStatistic" : ["data_type", "domain"], + "GeometryNodeCaptureAttribute" : ["data_type", "domain"], + "GeometryNodeAttributeDomainSize" : ["component"], + "GeometryNodeStoreNamedAttribute" : ["data_type", "domain"], + "GeometryNodeAttributeTransfer" : ["data_type", "mapping"], #color "ShaderNodeMixRGB" : ["blend_type", "use_clamp"], @@ -46,104 +46,101 @@ "FunctionNodeSeparateColor" : ["mode"], #curve - "GeometryNodeCurveToPoints" : ["mode"], - "GeometryNodeFillCurve" : ["mode"], - "GeometryNodeFilletCurve" : ["mode"], - "GeometryNodeResampleCurve" : ["mode"], - "GeometryNodeSampleCurve" : ["data_type", "mode", - "use_all_curves"], - "GeometryNodeTrimCurve" : ["mode"], - "GeometryNodeSetCurveNormal" : ["mode"], - "GeometryNodeCurveHandleTypeSelection" : ["mode", "handle_type"], - "GeometryNodeSetCurveHandlePositions" : ["mode"], - "GeometryNodeCurveSetHandles" : ["mode", "handle_type"], - "GeometryNodeCurveSplineType" : ["spline_type"], + "GeometryNodeCurveToPoints" : ["mode"], + "GeometryNodeFillCurve" : ["mode"], + "GeometryNodeFilletCurve" : ["mode"], + "GeometryNodeResampleCurve" : ["mode"], + "GeometryNodeSampleCurve" : ["data_type", "mode", "use_all_curves"], + "GeometryNodeTrimCurve" : ["mode"], + "GeometryNodeSetCurveNormal" : ["mode"], + "GeometryNodeCurveHandleTypeSelection" : ["mode", "handle_type"], + "GeometryNodeSetCurveHandlePositions" : ["mode"], + "GeometryNodeCurveSetHandles" : ["mode", "handle_type"], + "GeometryNodeCurveSplineType" : ["spline_type"], #curve primitives - "GeometryNodeCurveArc" : ["mode"], - "GeometryNodeCurvePrimitiveBezierSegment" : ["mode"], - "GeometryNodeCurvePrimitiveCircle" : ["mode"], - "GeometryNodeCurvePrimitiveLine" : ["mode"], - "GeometryNodeCurvePrimitiveQuadrilateral" : ["mode"], + "GeometryNodeCurveArc" : ["mode"], + "GeometryNodeCurvePrimitiveBezierSegment" : ["mode"], + "GeometryNodeCurvePrimitiveCircle" : ["mode"], + "GeometryNodeCurvePrimitiveLine" : ["mode"], + "GeometryNodeCurvePrimitiveQuadrilateral" : ["mode"], #geometry - "GeometryNodeDeleteGeometry" : ["domain", "mode"], + "GeometryNodeDeleteGeometry" : ["domain", "mode"], "GeometryNodeDuplicateElements" : ["domain"], - "GeometryNodeProximity" : ["target_element"], - "GeometryNodeMergeByDistance" : ["mode"], - "GeometryNodeRaycast" : ["data_type", "mapping"], - "GeometryNodeSampleIndex" : ["data_type", "domain", "clamp"], - "GeometryNodeSampleNearest" : ["domain"], - "GeometryNodeSeparateGeometry" : ["domain"], + "GeometryNodeProximity" : ["target_element"], + "GeometryNodeMergeByDistance" : ["mode"], + "GeometryNodeRaycast" : ["data_type", "mapping"], + "GeometryNodeSampleIndex" : ["data_type", "domain", "clamp"], + "GeometryNodeSampleNearest" : ["domain"], + "GeometryNodeSeparateGeometry" : ["domain"], #input - "GeometryNodeCollectionInfo" : ["transform_space"], - "GeometryNodeObjectInfo" : ["transform_space"], - "GeometryNodeInputNamedAttribute" : ["data_type"], + "GeometryNodeCollectionInfo" : ["transform_space"], + "GeometryNodeObjectInfo" : ["transform_space"], + "GeometryNodeInputNamedAttribute" : ["data_type"], #mesh - "GeometryNodeExtrudeMesh" : ["mode"], - "GeometryNodeMeshBoolean" : ["operation"], - "GeometryNodeMeshToPoints" : ["mode"], - "GeometryNodeMeshToVolume" : ["resolution_mode"], - "GeometryNodeSampleNearestSurface" : ["data_type"], - "GeometryNodeSampleUVSurface" : ["data_type"], - "GeometryNodeSubdivisionSurface" : ["uv_smooth", "boundary_smooth"], - "GeometryNodeTriangulate" : ["quad_method", "ngon_method"], - "GeometryNodeScaleElements" : ["domain", "scale_mode"], + "GeometryNodeExtrudeMesh" : ["mode"], + "GeometryNodeMeshBoolean" : ["operation"], + "GeometryNodeMeshToPoints" : ["mode"], + "GeometryNodeMeshToVolume" : ["resolution_mode"], + "GeometryNodeSampleNearestSurface" : ["data_type"], + "GeometryNodeSampleUVSurface" : ["data_type"], + "GeometryNodeSubdivisionSurface" : ["uv_smooth", "boundary_smooth"], + "GeometryNodeTriangulate" : ["quad_method", "ngon_method"], + "GeometryNodeScaleElements" : ["domain", "scale_mode"], #mesh primitives - "GeometryNodeMeshCone" : ["fill_type"], - "GeometryNodeMeshCylinder" : ["fill_type"], - "GeometryNodeMeshCircle" : ["fill_type"], - "GeometryNodeMeshLine" : ["mode"], + "GeometryNodeMeshCone" : ["fill_type"], + "GeometryNodeMeshCylinder" : ["fill_type"], + "GeometryNodeMeshCircle" : ["fill_type"], + "GeometryNodeMeshLine" : ["mode"], #output - "GeometryNodeViewer" : ["domain"], + "GeometryNodeViewer" : ["domain"], #point - "GeometryNodeDistributePointsInVolume" : ["mode"], - "GeometryNodeDistributePointsOnFaces" : ["distribute_method"], - "GeometryNodePointsToVolume" : ["resolution_mode"], + "GeometryNodeDistributePointsInVolume" : ["mode"], + "GeometryNodeDistributePointsOnFaces" : ["distribute_method"], + "GeometryNodePointsToVolume" : ["resolution_mode"], #text "GeometryNodeStringToCurves" : ["overflow", "align_x", "align_y", "pivot_mode"], #texture - "ShaderNodeTexBrick" : ["offset", "offset_frequency", "squash", - "squash_frequency"], - "ShaderNodeTexGradient" : ["gradient_type"], - "GeometryNodeImageTexture" : ["interpolation", "extension"], - "ShaderNodeTexMagic" : ["turbulence_depth"], - "ShaderNodeTexNoise" : ["noise_dimensions"], - "ShaderNodeTexVoronoi" : ["voronoi_dimensions", "feature", "distance"], - "ShaderNodeTexWave" : ["wave_type", "bands_direction", - "wave_profile"], - "ShaderNodeTexWhiteNoise" : ["noise_dimensions"], + "ShaderNodeTexBrick" : ["offset", "offset_frequency", "squash", + "squash_frequency"], + "ShaderNodeTexGradient" : ["gradient_type"], + "GeometryNodeImageTexture" : ["interpolation", "extension"], + "ShaderNodeTexMagic" : ["turbulence_depth"], + "ShaderNodeTexNoise" : ["noise_dimensions"], + "ShaderNodeTexVoronoi" : ["voronoi_dimensions", "feature", "distance"], + "ShaderNodeTexWave" : ["wave_type", "bands_direction", "wave_profile"], + "ShaderNodeTexWhiteNoise" : ["noise_dimensions"], #utilities - "GeometryNodeAccumulateField" : ["data_type", "domain"], - "FunctionNodeAlignEulerToVector" : ["axis", "pivot_axis"], - "FunctionNodeBooleanMath" : ["operation"], - "ShaderNodeClamp" : ["clamp_type"], - "FunctionNodeCompare" : ["data_type", "operation", "mode"], - "GeometryNodeFieldAtIndex" : ["data_type", "domain"], - "FunctionNodeFloatToInt" : ["rounding_mode"], - "GeometryNodeFieldOnDomain" : ["data_type", "domain" ], - "ShaderNodeMapRange" : ["data_type", "interpolation_type", - "clamp"], - "ShaderNodeMath" : ["operation", "use_clamp"], - "FunctionNodeRandomValue" : ["data_type"], - "FunctionNodeRotateEuler" : ["type", "space"], - "GeometryNodeSwitch" : ["input_type"], + "GeometryNodeAccumulateField" : ["data_type", "domain"], + "FunctionNodeAlignEulerToVector" : ["axis", "pivot_axis"], + "FunctionNodeBooleanMath" : ["operation"], + "ShaderNodeClamp" : ["clamp_type"], + "FunctionNodeCompare" : ["data_type", "operation", "mode"], + "GeometryNodeFieldAtIndex" : ["data_type", "domain"], + "FunctionNodeFloatToInt" : ["rounding_mode"], + "GeometryNodeFieldOnDomain" : ["data_type", "domain" ], + "ShaderNodeMapRange" : ["data_type", "interpolation_type", "clamp"], + "ShaderNodeMath" : ["operation", "use_clamp"], + "FunctionNodeRandomValue" : ["data_type"], + "FunctionNodeRotateEuler" : ["type", "space"], + "GeometryNodeSwitch" : ["input_type"], #uv "GeometryNodeUVUnwrap" : ["method"], #vector - "ShaderNodeVectorMath" : ["operation"], - "ShaderNodeVectorRotate" : ["rotation_type", "invert"], + "ShaderNodeVectorMath" : ["operation"], + "ShaderNodeVectorRotate" : ["rotation_type", "invert"], #volume "GeometryNodeVolumeToMesh" : ["resolution_mode"] @@ -156,9 +153,9 @@ def cleanup_string(string: str): return string.lower().replace(' ', '_').replace('.', '_') -class NodeToPython(bpy.types.Operator): - bl_idname = "node.node_to_python" - bl_label = "Node to Python" +class GeoNodesToPython(bpy.types.Operator): + bl_idname = "node.geo_nodes_to_python" + bl_label = "Geo Node to Python" bl_options = {'REGISTER', 'UNDO'} node_group_name: bpy.props.StringProperty(name="Node Group") @@ -286,8 +283,8 @@ def process_node_group(node_group, level): file.write(f"{inner}{node_name}.label = \"{node.label}\"\n") #special nodes - if node.bl_idname in node_settings: - for setting in node_settings[node.bl_idname]: + if node.bl_idname in geo_node_settings: + for setting in geo_node_settings[node.bl_idname]: attr = getattr(node, setting, None) if attr: if type(attr) == str: @@ -458,26 +455,27 @@ def create_main(): file.close() return {'FINISHED'} -class NodeToPythonMenu(bpy.types.Menu): - bl_idname = "NODE_MT_node_to_python" - bl_label = "Node To Python" +class SelectGeoNodesMenu(bpy.types.Menu): + bl_idname = "NODE_MT_ntp_geo_nodes_selection" + bl_label = "Select Geo Nodes" @classmethod def poll(cls, context): return True def draw(self, context): - geo_node_groups = [node for node in bpy.data.node_groups if node.type == 'GEOMETRY'] - layout = self.layout.column_flow(columns=1) layout.operator_context = 'INVOKE_DEFAULT' + + geo_node_groups = [node for node in bpy.data.node_groups if node.type == 'GEOMETRY'] + for geo_ng in geo_node_groups: - op = layout.operator(NodeToPython.bl_idname, text=geo_ng.name) + op = layout.operator(GeoNodesToPython.bl_idname, text=geo_ng.name) op.node_group_name = geo_ng.name -class NodeToPythonPanel(bpy.types.Panel): +class GeoNodesToPythonPanel(bpy.types.Panel): bl_label = "Geometry Nodes to Python" - bl_idname = "NODE_PT_node_to_python" + bl_idname = "NODE_PT_geo_nodes_to_python" bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' bl_context = '' @@ -496,15 +494,17 @@ def draw(self, context): row = col.row() # Disables menu when len of geometry nodes is 0 - geo_node_groups = [node for node in bpy.data.node_groups if node.type == 'GEOMETRY'] + geo_node_groups = [node + for node in bpy.data.node_groups + if node.type == 'GEOMETRY'] geo_node_groups_exist = len(geo_node_groups) > 0 row.enabled = geo_node_groups_exist row.alignment = 'EXPAND' row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_node_to_python", text="Geometry Node Groups") - -classes = [NodeToPythonMenu, NodeToPythonPanel, NodeToPython] + row.menu("NODE_MT_ntp_geo_nodes_selection", text="Geometry Nodes") +""" +classes = [SelectGeoNodesMenu, GeoNodesToPythonPanel, GeoNodesToPython] def register(): for cls in classes: @@ -515,4 +515,5 @@ def unregister(): bpy.utils.unregister_class(cls) if __name__ == "__main__": - register() \ No newline at end of file + register() +""" \ No newline at end of file diff --git a/material_to_python.py b/materials.py similarity index 99% rename from material_to_python.py rename to materials.py index 88e92d0..0c4a887 100644 --- a/material_to_python.py +++ b/materials.py @@ -413,7 +413,7 @@ def draw(self, context): row = layout.row() # Disables menu when there are no materials - materials = [mat for mat in bpy.data.materials] + materials = bpy.data.materials materials_exist = len(materials) > 0 row.enabled = materials_exist @@ -421,7 +421,8 @@ def draw(self, context): row.operator_context = 'INVOKE_DEFAULT' row.menu("NODE_MT_npt_mat_selection", text="Materials") -classes = [MaterialToPythonPanel, MaterialToPython] +""" +classes = [MaterialToPythonPanel, MaterialToPython, SelectMaterialMenu] def register(): for cls in classes: @@ -432,4 +433,5 @@ def unregister(): bpy.utils.unregister_class(cls) if __name__ == "__main__": - register() \ No newline at end of file + register() +""" \ No newline at end of file From d7d2edf3647db13ff6c18a4edfe3e4149550682d Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 9 Jan 2023 22:01:59 -0600 Subject: [PATCH 07/60] refactor: added utils module for common functionality --- geo_nodes.py | 19 +++++++++++-------- materials.py | 25 ++++++++++++------------- utils.py | 6 ++++++ 3 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 utils.py diff --git a/geo_nodes.py b/geo_nodes.py index 551ad84..a684b01 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -11,6 +11,12 @@ import bpy import os +if "bpy" in locals(): + import importlib + importlib.reload(utils) +else: + from . import utils + #node tree input sockets that have default properties default_sockets = {'NodeSocketBool', 'NodeSocketColor', @@ -150,9 +156,6 @@ 'ShaderNodeVectorCurve', 'ShaderNodeRGBCurve'} -def cleanup_string(string: str): - return string.lower().replace(' ', '_').replace('.', '_') - class GeoNodesToPython(bpy.types.Operator): bl_idname = "node.geo_nodes_to_python" bl_label = "Geo Node to Python" @@ -164,7 +167,7 @@ def execute(self, context): if self.node_group_name not in bpy.data.node_groups: return {'FINISHED'} ng = bpy.data.node_groups[self.node_group_name] - ng_name = cleanup_string(ng.name) + ng_name = utils.clean_string(ng.name) class_name = ng.name.replace(" ", "").replace('.', "") dir = bpy.path.abspath("//") if not dir or dir == "": @@ -205,7 +208,7 @@ def init_class(): file.write("\tdef execute(self, context):\n") def process_node_group(node_group, level): - ng_name = cleanup_string(node_group.name) + ng_name = utils.clean_string(node_group.name) outer = "\t"*level #outer indentation inner = "\t"*(level + 1) #inner indentation @@ -271,7 +274,7 @@ def process_node_group(node_group, level): file.write("\n") #create node - node_name = cleanup_string(node.name) + node_name = utils.clean_string(node.name) file.write(f"{inner}#node {node.name}\n") file.write((f"{inner}{node_name} " f"= {ng_name}.nodes.new(\"{node.bl_idname}\")\n")) @@ -386,7 +389,7 @@ def process_node_group(node_group, level): if node_group.links: file.write(f"{inner}#initialize {ng_name} links\n") for link in node_group.links: - input_node = cleanup_string(link.from_node.name) + input_node = utils.clean_string(link.from_node.name) input_socket = link.from_socket """ @@ -400,7 +403,7 @@ def process_node_group(node_group, level): input_idx = i break - output_node = cleanup_string(link.to_node.name) + output_node = utils.clean_string(link.to_node.name) output_socket = link.to_socket for i, item in enumerate(link.to_node.inputs.items()): diff --git a/materials.py b/materials.py index 0c4a887..13229aa 100644 --- a/materials.py +++ b/materials.py @@ -2,6 +2,12 @@ import mathutils import os +if "bpy" in locals(): + import importlib + importlib.reload(utils) +else: + from . import utils + #node tree input sockets that have default properties default_sockets = {'NodeSocketBool', 'NodeSocketColor', @@ -93,13 +99,6 @@ 'ShaderNodeVectorCurve', 'ShaderNodeRGBCurve'} -def clean_string(string: str): - bad_chars = [' ', '.', '/'] - clean_str = string.lower() - for char in bad_chars: - clean_str = clean_str.replace(char, '_') - return clean_str - class MaterialToPython(bpy.types.Operator): bl_idname = "node.material_to_python" bl_label = "Material to Python" @@ -113,7 +112,7 @@ def execute(self, context): #set up addon file ng = bpy.data.materials[self.material_name].node_tree - ng_name = clean_string(self.material_name) + ng_name = utils.clean_string(self.material_name) class_name = ng.name.replace(" ", "") dir = bpy.path.abspath("//") @@ -154,9 +153,9 @@ def init_class(): file.write("\tdef execute(self, context):\n") def process_mat_node_group(node_group, level): - ng_name = clean_string(node_group.name) + ng_name = utils.clean_string(node_group.name) if level == 2: - ng_name = clean_string(self.material_name) + ng_name = utils.clean_string(self.material_name) outer = "\t"*level inner = "\t"*(level + 1) @@ -184,7 +183,7 @@ def process_mat_node_group(node_group, level): if node.bl_idname == 'ShaderNodeGroup': process_mat_node_group(node.node_tree, level + 1) #create node - node_name = clean_string(node.name) + node_name = utils.clean_string(node.name) if node_name == "": node_name = f"node_{unnamed_index}" unnamed_index += 1 @@ -314,7 +313,7 @@ def default_value(i, socket, list_name): if node_group.links: file.write(f"{inner}#initialize {ng_name} links\n") for link in node_group.links: - input_node = clean_string(link.from_node.name) + input_node = utils.clean_string(link.from_node.name) input_socket = link.from_socket """ @@ -328,7 +327,7 @@ def default_value(i, socket, list_name): input_idx = i break - output_node = clean_string(link.to_node.name) + output_node = utils.clean_string(link.to_node.name) output_socket = link.to_socket for i, item in enumerate(link.to_node.inputs.items()): diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..6afff6b --- /dev/null +++ b/utils.py @@ -0,0 +1,6 @@ +def clean_string(string: str): + bad_chars = [' ', '.', '/'] + clean_str = string.lower() + for char in bad_chars: + clean_str = clean_str.replace(char, '_') + return clean_str \ No newline at end of file From 1c6bead625146f9103b16b697cdb68a2ea6f49b0 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 9 Jan 2023 22:06:44 -0600 Subject: [PATCH 08/60] style: removed unused logic that was commented out --- geo_nodes.py | 16 +--------------- materials.py | 17 +---------------- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index a684b01..f65e2b0 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -505,18 +505,4 @@ def draw(self, context): row.alignment = 'EXPAND' row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_ntp_geo_nodes_selection", text="Geometry Nodes") -""" -classes = [SelectGeoNodesMenu, GeoNodesToPythonPanel, GeoNodesToPython] - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - -def unregister(): - for cls in classes: - bpy.utils.unregister_class(cls) - -if __name__ == "__main__": - register() -""" \ No newline at end of file + row.menu("NODE_MT_ntp_geo_nodes_selection", text="Geometry Nodes") \ No newline at end of file diff --git a/materials.py b/materials.py index 13229aa..e4e3157 100644 --- a/materials.py +++ b/materials.py @@ -418,19 +418,4 @@ def draw(self, context): row.alignment = 'EXPAND' row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_npt_mat_selection", text="Materials") - -""" -classes = [MaterialToPythonPanel, MaterialToPython, SelectMaterialMenu] - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - -def unregister(): - for cls in classes: - bpy.utils.unregister_class(cls) - -if __name__ == "__main__": - register() -""" \ No newline at end of file + row.menu("NODE_MT_npt_mat_selection", text="Materials") \ No newline at end of file From f610177b368e85b24ffd9d343beefc4a6fcd759c Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 9 Jan 2023 22:28:14 -0600 Subject: [PATCH 09/60] docs: update README --- README.md | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2a55cd8..d5a9bbc 100644 --- a/README.md +++ b/README.md @@ -9,43 +9,53 @@ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/releases) [![GitHub](https://img.shields.io/github/license/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/blob/main/LICENSE) ![](https://visitor-badge.laobi.icu/badge?page_id=BrendanParmer.NodeToPython) ## About -A Blender add-on to create add-ons! This script will take your Geometry Node group and convert it into a legible Python script. +A Blender add-on to create add-ons! This script will take your Geometry Nodes or Materials and convert them into legible Python add-ons! It automatically handles node layout, default values, sub-node groups, naming, and more! -I think Geometry Nodes is a powerful tool that's fairly accessible to people, and I wanted to create a lightweight, faster way of distributing them than just passing around blend files. It also makes scripting Geometry Nodes easier for add-on creators in cases when Python is needed, as you don't need to recreate the whole node tree from scratch to do things like +I think Blender's node-based editors are powerful, yet accessible tools, and I wanted to make scripting them for add-on creators. Combining Python with node based setups allows you to do things that would otherwise be tedious or impossible, such as * `for` loops * different node trees for different versions or settings * interfacing with other parts of the software. -NodeToPython is compatible with Blender 3.0-3.4 +NodeToPython recreates the node networks for you, so you can focus on the good stuff. ## Supported Versions -Blender 3.0 - 3.4 +NodeToPython v2.0 is compatible with Blender 3.0 - 3.4 on Windows, macOS, and Linux. I generally try to update the addon to handle new nodes around the beta release of each update. -* Once the 3.5 beta drops, I'll start adding nodes from that release +## Installation +1. Download the .zip file from the [latest release](https://github.com/BrendanParmer/NodeToPython/releases) +2. In Blender, navigate to `Edit > Preferences > Add-ons` +3. Click Install, and find where you downloaded the zip file. Then hit the `Install Add-on` button, and you're done! -## Installation and Usage -Download `node_to_python.py`, and install it to Blender like other add-ons. Then, go to `Object > Node to Python`, and type in the name of your node group. It will then save an add-on to where your blend file is stored. +## Usage +Once you've installed the add-on, you'll see a new tab to the side of a Node Editor. + +In the tab, there's panels to create add-ons for Geometry Nodes and Materials, each with a drop-down menu. + +Just select the one you want, and soon a python file will be created in an `addons` folder located in the folder where your blend file is. + +From here, you can install it like a regular add-on. ## Future -* Expansion to Shader and Compositing nodes +* Expansion to Compositing nodes * Copy over referenced assets in the scene (Collections, Objects, Materials, Textures, etc.) * Automatically format code to be PEP8 compliant ## Potential Issues -* This should work on Unix-like systems (macOS, Linux), but I haven't tested it on Windows yet. If you use Windows, please let me know if it does! -* As of version 1.0.0, the add-on will not set default values for +* As of version 2.0.0, the add-on will not set default values for * Collections * Images * Materials * Objects * Textures * Scripts - * IES + * IES files * Filepaths - as they won't exist in every blend file. In the future, I may have the script automatically recreate these assets, espcially with materials. + as they won't exist in every blend file. I'm expecting to support some of these in the future. + + There are a few nodes that don't set their default values like other ones, though these should also soon be supported. ## Bug Reports and Suggestions @@ -53,7 +63,7 @@ When submitting an issue, please include * Your version of Blender * Your operating system -* A short description of what you were trying to accomplish, or steps to reproduce the issue - -Suggestions for how to improve the add-on are more than welcome! +* A short description of what you were trying to accomplish, or steps to reproduce the issue. +* Sample blend files are more than welcome! +Suggestions for how to improve the add-on are more than welcome! \ No newline at end of file From 5c7be170a6a9c1305ad71baa68e7950428eb0cd6 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 9 Jan 2023 22:38:36 -0600 Subject: [PATCH 10/60] fix: properly imports utils file --- geo_nodes.py | 6 +----- materials.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index f65e2b0..2a6fba7 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -11,11 +11,7 @@ import bpy import os -if "bpy" in locals(): - import importlib - importlib.reload(utils) -else: - from . import utils +import utils #node tree input sockets that have default properties default_sockets = {'NodeSocketBool', diff --git a/materials.py b/materials.py index e4e3157..ac87a53 100644 --- a/materials.py +++ b/materials.py @@ -2,11 +2,7 @@ import mathutils import os -if "bpy" in locals(): - import importlib - importlib.reload(utils) -else: - from . import utils +import .utils #node tree input sockets that have default properties default_sockets = {'NodeSocketBool', From 2ca7a7e372dbc7b9de8bfabe8a575b2e24f22dab Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 9 Jan 2023 22:39:47 -0600 Subject: [PATCH 11/60] fix: materials properly imports utils --- materials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/materials.py b/materials.py index ac87a53..1d77999 100644 --- a/materials.py +++ b/materials.py @@ -2,7 +2,7 @@ import mathutils import os -import .utils +import utils #node tree input sockets that have default properties default_sockets = {'NodeSocketBool', From ddda89c3053d47a82e579c0142eccc518248eb71 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 9 Jan 2023 23:46:48 -0600 Subject: [PATCH 12/60] feat: addon now creates material as well as shader node group --- geo_nodes.py | 4 ++-- materials.py | 46 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 2a6fba7..606ee9a 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -11,7 +11,7 @@ import bpy import os -import utils +from . import utils #node tree input sockets that have default properties default_sockets = {'NodeSocketBool', @@ -358,7 +358,7 @@ def process_node_group(node_group, level): handle = f"\'{point.handle_type}\'" file.write(f"{point_j}.handle_type = {handle}\n") - file.write(f"{inner}#update curve after changes") + file.write(f"{inner}#update curve after changes\n") file.write(f"{mapping}.update()\n") if node.bl_idname != 'NodeReroute': diff --git a/materials.py b/materials.py index 1d77999..9d1ba87 100644 --- a/materials.py +++ b/materials.py @@ -2,7 +2,7 @@ import mathutils import os -import utils +from . import utils #node tree input sockets that have default properties default_sockets = {'NodeSocketBool', @@ -108,6 +108,11 @@ def execute(self, context): #set up addon file ng = bpy.data.materials[self.material_name].node_tree + if ng is None: + self.report({'ERROR'}, + ("NodeToPython: This doesn't seem to be a valid " + "material. Is Use Nodes selected?")) + return {'CANCELLED'} ng_name = utils.clean_string(self.material_name) class_name = ng.name.replace(" ", "") @@ -148,10 +153,19 @@ def init_class(): 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() + def process_mat_node_group(node_group, level): ng_name = utils.clean_string(node_group.name) - if level == 2: + ng_label = node_group.name + + if level == 2: #outermost node group ng_name = utils.clean_string(self.material_name) + ng_label = self.material_name outer = "\t"*level inner = "\t"*(level + 1) @@ -159,11 +173,15 @@ def process_mat_node_group(node_group, level): #initialize node group file.write(f"{outer}#initialize {ng_name} node group\n") file.write(f"{outer}def {ng_name}_node_group():\n") - file.write((f"{inner}{ng_name}" - f"= bpy.data.node_groups.new(" - f"type = \"ShaderNodeTree\", " - f"name = \"{node_group.name}\")\n")) - file.write("\n") + + if level == 2: #outermost node group + file.write(f"{inner}{ng_name} = mat.node_tree\n") + else: + file.write((f"{inner}{ng_name}" + f"= bpy.data.node_groups.new(" + f"type = \"ShaderNodeTree\", " + f"name = \"{ng_label}\")\n")) + file.write("\n") #initialize nodes file.write(f"{inner}#initialize {ng_name} nodes\n") @@ -177,7 +195,8 @@ def process_mat_node_group(node_group, level): unnamed_index = 0 for node in node_group.nodes: if node.bl_idname == 'ShaderNodeGroup': - process_mat_node_group(node.node_tree, level + 1) + if node.node_tree is not None: + process_mat_node_group(node.node_tree, level + 1) #create node node_name = utils.clean_string(node.name) if node_name == "": @@ -206,9 +225,10 @@ def process_mat_node_group(node_group, level): file.write((f"{inner}{node_name}.{setting} " f"= {attr}\n")) elif node.bl_idname == 'ShaderNodeGroup': - file.write((f"{inner}{node_name}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) + if node.node_tree is not None: + file.write((f"{inner}{node_name}.node_tree = " + f"bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'ShaderNodeValToRGB': color_ramp = node.color_ramp file.write("\n") @@ -273,7 +293,7 @@ def process_mat_node_group(node_group, level): handle = f"\'{point.handle_type}\'" file.write(f"{point_j}.handle_type = {handle}\n") - file.write(f"{inner}#update curve after changes") + file.write(f"{inner}#update curve after changes\n") file.write(f"{mapping}.update()\n") if node.bl_idname != 'NodeReroute': @@ -336,9 +356,11 @@ def default_value(i, socket, list_name): file.write((f"{inner}{ng_name}.links.new({input_node}" f".outputs[{input_idx}], " f"{output_node}.inputs[{output_idx}])\n")) + file.write(f"{outer}{ng_name}_node_group()\n") process_mat_node_group(ng, 2) + file.write("\t\treturn {'FINISHED'}\n\n") """Create the function that adds the addon to the menu""" From a29af8f5de51c428317c2d65ec9897e156b795c9 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 9 Jan 2023 23:50:19 -0600 Subject: [PATCH 13/60] fix: issue where deleted node groups would still be referenced --- geo_nodes.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 606ee9a..cdbcfcd 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -222,7 +222,8 @@ def process_node_group(node_group, level): file.write(f"{inner}#initialize {ng_name} nodes\n") for node in node_group.nodes: if node.bl_idname == 'GeometryNodeGroup': - process_node_group(node.node_tree, level + 1) + if node.node_tree is not None: + process_node_group(node.node_tree, level + 1) elif node.bl_idname == 'NodeGroupInput': file.write(f"{inner}#{ng_name} inputs\n") for i, input in enumerate(node.outputs): @@ -291,9 +292,10 @@ def process_node_group(node_group, level): file.write((f"{inner}{node_name}.{setting} " f"= {attr}\n")) elif node.bl_idname == 'GeometryNodeGroup': - file.write((f"{inner}{node_name}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) + if node.node_tree is not None: + file.write((f"{inner}{node_name}.node_tree = " + f"bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'ShaderNodeValToRGB': color_ramp = node.color_ramp file.write("\n") From f91ef2ce9dfa091387bc1ae80f721f1e5743be83 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 00:58:56 -0600 Subject: [PATCH 14/60] feat: geo node input/output update --- geo_nodes.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index cdbcfcd..bfa2a00 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -218,13 +218,16 @@ def process_node_group(node_group, level): f"name = \"{node_group.name}\")\n")) file.write("\n") + inputs_set = False + outputs_set = False + #initialize nodes file.write(f"{inner}#initialize {ng_name} nodes\n") for node in node_group.nodes: if node.bl_idname == 'GeometryNodeGroup': if node.node_tree is not None: process_node_group(node.node_tree, level + 1) - elif node.bl_idname == 'NodeGroupInput': + elif node.bl_idname == 'NodeGroupInput' and not inputs_set: file.write(f"{inner}#{ng_name} inputs\n") for i, input in enumerate(node.outputs): if input.bl_idname != "NodeSocketVirtual": @@ -232,8 +235,8 @@ def process_node_group(node_group, level): file.write((f"{inner}{ng_name}.inputs.new" f"(\"{input.bl_idname}\", " f"\"{input.name}\")\n")) - if input.bl_idname in default_sockets: - socket = node_group.inputs[i] + socket = node_group.inputs[i] + if input.bl_idname in default_sockets: if input.bl_idname == 'NodeSocketColor': col = socket.default_value r, g, b, a = col[0], col[1], col[2], col[3] @@ -248,27 +251,81 @@ def process_node_group(node_group, level): file.write((f"{inner}{ng_name}" f".inputs[{i}]" f".default_value = {dv}\n")) - if input.bl_idname in value_sockets: - #min value + + #min value + if hasattr(socket, "min_value"): file.write((f"{inner}{ng_name}" f".inputs[{i}]" f".min_value = " f"{socket.min_value}\n")) - #max value + #max value + if hasattr(socket, "max_value"): file.write((f"{inner}{ng_name}" f".inputs[{i}]" f".max_value = " f"{socket.max_value}\n")) + #default attribute name + if hasattr(socket, "default_attribute_name"): + if socket.default_attribute_name != "": + file.write((f"{inner}{ng_name}" + f".inputs[{i}]" + f".default_attribute_name = \"" + f"{socket.default_attribute_name}" + f"\"\n")) + #description + if socket.description != "": + file.write((f"{inner}{ng_name}" + f".inputs[{i}]" + f".description = " + f"\"{socket.description}\"\n")) + #hide value + if socket.hide_value is True: + file.write((f"{inner}{ng_name}" + f".inputs[{i}]" + f".hide_value = " + f"{socket.hide_value}\n")) file.write("\n") file.write("\n") - elif node.bl_idname == 'NodeGroupOutput': + inputs_set = True + + elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: file.write(f"{inner}#{ng_name} outputs\n") - for output in node.inputs: + for i, output in enumerate(node.inputs): if output.bl_idname != 'NodeSocketVirtual': file.write((f"{inner}{ng_name}.outputs" f".new(\"{output.bl_idname}\", " f"\"{output.name}\")\n")) + + socket = node_group.outputs[i] + #description + if socket.description != "": + file.write((f"{inner}{ng_name}" + f".outputs[{i}]" + f".description = " + f"\"{socket.description}\"\n")) + #hide value + if socket.hide_value is True: + file.write((f"{inner}{ng_name}" + f".outputs[{i}]" + f".hide_value = " + f"{socket.hide_value}\n")) + + #default attribute name + if hasattr(socket, "default_attribute_name"): + if socket.default_attribute_name != "": + file.write((f"{inner}{ng_name}" + f".outputs[{i}]" + f".default_attribute_name = \"" + f"{socket.default_attribute_name}" + f"\"\n")) + #attribute domain + if hasattr(socket, "attribute_domain"): + file.write((f"{inner}{ng_name}" + f".outputs[{i}]" + f".attribute_domain = " + f"\'{socket.attribute_domain}\'\n")) file.write("\n") + outputs_set = True #create node node_name = utils.clean_string(node.name) From 5ae1843e306933330ee8ecf2ee6e70416af89b60 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 01:28:47 -0600 Subject: [PATCH 15/60] style: deleted unused sets --- geo_nodes.py | 5 ----- materials.py | 12 ------------ 2 files changed, 17 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index bfa2a00..0aa3779 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -20,11 +20,6 @@ 'NodeSocketInt', 'NodeSocketVector'} -#node tree input sockets that have min/max properties -value_sockets = {'NodeSocketInt', - 'NodeSocketFloat', - 'NodeSocketVector'} - #node input sockets that are messy to set default values for dont_set_defaults = {'NodeSocketCollection', 'NodeSocketGeometry', diff --git a/materials.py b/materials.py index 9d1ba87..4a0eb8f 100644 --- a/materials.py +++ b/materials.py @@ -4,18 +4,6 @@ from . import utils -#node tree input sockets that have default properties -default_sockets = {'NodeSocketBool', - 'NodeSocketColor', - 'NodeSocketFloat', - 'NodeSocketInt', - 'NodeSocketVector'} - -#node tree input sockets that have min/max properties -value_sockets = {'NodeSocketInt', - 'NodeSocketFloat', - 'NodeSocketVector'} - #node input sockets that are messy to set default values for dont_set_defaults = {'NodeSocketCollection', 'NodeSocketGeometry', From b5149b389b330c355ce90e3035536d88df3f5869 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 01:49:00 -0600 Subject: [PATCH 16/60] refactor: create_header moved to utils --- geo_nodes.py | 33 +++++++++++++-------------------- materials.py | 22 ++++------------------ utils.py | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 0aa3779..6d1deee 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -149,42 +149,35 @@ class GeoNodesToPython(bpy.types.Operator): bl_idname = "node.geo_nodes_to_python" - bl_label = "Geo Node to Python" + bl_label = "Geo Nodes to Python" bl_options = {'REGISTER', 'UNDO'} - node_group_name: bpy.props.StringProperty(name="Node Group") + geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") def execute(self, context): - if self.node_group_name not in bpy.data.node_groups: - return {'FINISHED'} - ng = bpy.data.node_groups[self.node_group_name] + #find node group to replicate + ng = bpy.data.node_groups[self.geo_nodes_group_name] + + #set up names to use in generated addon ng_name = utils.clean_string(ng.name) class_name = ng.name.replace(" ", "").replace('.', "") + + #find base directory to save new addon dir = bpy.path.abspath("//") if not dir or dir == "": self.report({'ERROR'}, ("NodeToPython: Save your blend file before using " "NodeToPython!")) return {'CANCELLED'} + + #save in /addons/ subdirectory addon_dir = os.path.join(dir, "addons") if not os.path.exists(addon_dir): os.mkdir(addon_dir) file = open(f"{addon_dir}/{ng_name}_addon.py", "w") - """Sets up bl_info and imports Blender""" - def header(): - file.write("bl_info = {\n") - file.write(f"\t\"name\" : \"{ng.name}\",\n") - file.write("\t\"author\" : \"Node To Python\",\n") - file.write("\t\"version\" : (1, 0, 0),\n") - file.write(f"\t\"blender\" : {bpy.app.version},\n") - file.write("\t\"location\" : \"Object\",\n") - file.write("\t\"category\" : \"Object\"\n") - file.write("}\n") - file.write("\n") - file.write("import bpy\n") - file.write("\n") - header() + #Sets up bl_info and imports the Blender API + utils.create_header(file, ng) """Creates the class and its variables""" def init_class(): @@ -195,7 +188,7 @@ def init_class(): file.write("\n") init_class() - """Construct the execute function""" + #Construct the execute function file.write("\tdef execute(self, context):\n") def process_node_group(node_group, level): diff --git a/materials.py b/materials.py index 4a0eb8f..e522ad2 100644 --- a/materials.py +++ b/materials.py @@ -91,16 +91,15 @@ class MaterialToPython(bpy.types.Operator): material_name: bpy.props.StringProperty(name="Node Group") def execute(self, context): - if self.material_name not in bpy.data.materials: - return {'FINISHED'} - - #set up addon file + #find node group to replicate ng = bpy.data.materials[self.material_name].node_tree if ng is None: 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 ng_name = utils.clean_string(self.material_name) class_name = ng.name.replace(" ", "") @@ -115,20 +114,7 @@ def execute(self, context): os.mkdir(addon_dir) file = open(f"{addon_dir}/{ng_name}_addon.py", "w") - """Sets up bl_info and imports Blender""" - def header(): - file.write("bl_info = {\n") - file.write(f"\t\"name\" : \"{self.material_name}\",\n") - file.write("\t\"author\" : \"Node To Python\",\n") - file.write("\t\"version\" : (1, 0, 0),\n") - file.write(f"\t\"blender\" : {bpy.app.version},\n") - file.write("\t\"location\" : \"Object\",\n") - file.write("\t\"category\" : \"Object\"\n") - file.write("}\n") - file.write("\n") - file.write("import bpy\n") - file.write("\n") - header() + utils.create_header(file, ng) """Creates the class and its variables""" def init_class(): diff --git a/utils.py b/utils.py index 6afff6b..50bb856 100644 --- a/utils.py +++ b/utils.py @@ -1,6 +1,41 @@ -def clean_string(string: str): +import bpy +import typing + +def clean_string(string: str) -> str: + """ + Cleans up a string for use as a variable or file name + + Parameters: + string (str): The input string + + Returns: + str: The input string with nasty characters converted to underscores + """ + bad_chars = [' ', '.', '/'] clean_str = string.lower() for char in bad_chars: clean_str = clean_str.replace(char, '_') - return clean_str \ No newline at end of file + return clean_str + +def create_header(file: typing.TextIO, node_group): + """ + Sets up the bl_info and imports the Blender API + + Parameters: + file (typing.TextIO): the file for the generated add-on + node_group: the node group object we're converting into an add-on + """ + + file.write("bl_info = {\n") + file.write(f"\t\"name\" : \"{node_group.name}\",\n") + file.write("\t\"author\" : \"Node To Python\",\n") + file.write("\t\"version\" : (1, 0, 0),\n") + file.write(f"\t\"blender\" : {bpy.app.version},\n") + file.write("\t\"location\" : \"Object\",\n") + file.write("\t\"category\" : \"Object\"\n") + file.write("}\n") + file.write("\n") + file.write("import bpy\n") + file.write("\n") + From c1964e8614f891fb0785bafb123ae219d87b124d Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 02:55:02 -0600 Subject: [PATCH 17/60] refactor: init_operator, make_indents, and create_node moved to utils --- geo_nodes.py | 55 ++++++++++++----------------------- materials.py | 36 +++-------------------- utils.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 98 insertions(+), 74 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 6d1deee..ce6e2c8 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -176,26 +176,15 @@ def execute(self, context): os.mkdir(addon_dir) file = open(f"{addon_dir}/{ng_name}_addon.py", "w") - #Sets up bl_info and imports the Blender API utils.create_header(file, ng) + utils.init_operator(file, class_name, ng_name, ng.name) - """Creates the class and its variables""" - def init_class(): - file.write(f"class {class_name}(bpy.types.Operator):\n") - file.write(f"\tbl_idname = \"object.{ng_name}\"\n") - file.write(f"\tbl_label = \"{ng.name}\"\n") - file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") - file.write("\n") - init_class() - - #Construct the execute function file.write("\tdef execute(self, context):\n") def process_node_group(node_group, level): ng_name = utils.clean_string(node_group.name) - outer = "\t"*level #outer indentation - inner = "\t"*(level + 1) #inner indentation + outer, inner = utils.make_indents(level) #initialize node group file.write(f"{outer}#initialize {ng_name} node group\n") @@ -315,18 +304,10 @@ def process_node_group(node_group, level): file.write("\n") outputs_set = True + unnamed_idx = 0 #create node - node_name = utils.clean_string(node.name) - file.write(f"{inner}#node {node.name}\n") - file.write((f"{inner}{node_name} " - f"= {ng_name}.nodes.new(\"{node.bl_idname}\")\n")) - file.write((f"{inner}{node_name}.location " - f"= ({node.location.x}, {node.location.y})\n")) - file.write((f"{inner}{node_name}.width, {node_name}.height " - f"= {node.width}, {node.height}\n")) - if node.label: - file.write(f"{inner}{node_name}.label = \"{node.label}\"\n") - + node_var, unnamed_idx = utils.create_node(node, file, inner, ng_name) + #special nodes if node.bl_idname in geo_node_settings: for setting in geo_node_settings[node.bl_idname]: @@ -334,37 +315,37 @@ def process_node_group(node_group, level): if attr: if type(attr) == str: attr = f"\'{attr}\'" - file.write((f"{inner}{node_name}.{setting} " + file.write((f"{inner}{node_var}.{setting} " f"= {attr}\n")) elif node.bl_idname == 'GeometryNodeGroup': if node.node_tree is not None: - file.write((f"{inner}{node_name}.node_tree = " + file.write((f"{inner}{node_var}.node_tree = " f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'ShaderNodeValToRGB': color_ramp = node.color_ramp file.write("\n") - file.write((f"{inner}{node_name}.color_ramp.color_mode = " + file.write((f"{inner}{node_var}.color_ramp.color_mode = " f"\'{color_ramp.color_mode}\'\n")) - file.write((f"{inner}{node_name}.color_ramp" + file.write((f"{inner}{node_var}.color_ramp" f".hue_interpolation = " f"\'{color_ramp.hue_interpolation}\'\n")) - file.write((f"{inner}{node_name}.color_ramp.interpolation " + file.write((f"{inner}{node_var}.color_ramp.interpolation " f"= '{color_ramp.interpolation}'\n")) file.write("\n") for i, element in enumerate(color_ramp.elements): - file.write((f"{inner}{node_name}_cre_{i} = " - f"{node_name}.color_ramp.elements" + file.write((f"{inner}{node_var}_cre_{i} = " + f"{node_var}.color_ramp.elements" f".new({element.position})\n")) - file.write((f"{inner}{node_name}_cre_{i}.alpha = " + file.write((f"{inner}{node_var}_cre_{i}.alpha = " f"{element.alpha}\n")) col = element.color r, g, b, a = col[0], col[1], col[2], col[3] - file.write((f"{inner}{node_name}_cre_{i}.color = " + file.write((f"{inner}{node_var}_cre_{i}.color = " f"({r}, {g}, {b}, {a})\n\n")) elif node.bl_idname in curve_nodes: file.write(f"{inner}#mapping settings\n") - mapping = f"{inner}{node_name}.mapping" + mapping = f"{inner}{node_var}.mapping" extend = f"\'{node.mapping.extend}\'" file.write(f"{mapping}.extend = {extend}\n") @@ -392,9 +373,9 @@ def process_node_group(node_group, level): for i, curve in enumerate(node.mapping.curves): file.write(f"{inner}#curve {i}\n") - curve_i = f"{node_name}_curve_{i}" + curve_i = f"{node_var}_curve_{i}" file.write((f"{inner}{curve_i} = " - f"{node_name}.mapping.curves[{i}]\n")) + f"{node_var}.mapping.curves[{i}]\n")) for j, point in enumerate(curve.points): point_j = f"{inner}{curve_i}_point_{j}" @@ -423,7 +404,7 @@ def process_node_group(node_group, level): dv = input.default_value if dv is not None: file.write(f"{inner}#{input.identifier}\n") - file.write((f"{inner}{node_name}" + file.write((f"{inner}{node_var}" f".inputs[{i}]" f".default_value = {dv}\n")) file.write("\n") diff --git a/materials.py b/materials.py index e522ad2..d2d843e 100644 --- a/materials.py +++ b/materials.py @@ -115,15 +115,7 @@ def execute(self, context): file = open(f"{addon_dir}/{ng_name}_addon.py", "w") utils.create_header(file, ng) - - """Creates the class and its variables""" - def init_class(): - file.write(f"class {class_name}(bpy.types.Operator):\n") - file.write(f"\tbl_idname = \"object.{ng_name}\"\n") - file.write(f"\tbl_label = \"{self.material_name}\"\n") - file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") - file.write("\n") - init_class() + utils.init_operator(file, class_name, ng_name, self.material_name) file.write("\tdef execute(self, context):\n") @@ -141,8 +133,7 @@ def process_mat_node_group(node_group, level): ng_name = utils.clean_string(self.material_name) ng_label = self.material_name - outer = "\t"*level - inner = "\t"*(level + 1) + outer, inner = utils.make_indents(level) #initialize node group file.write(f"{outer}#initialize {ng_name} node group\n") @@ -160,32 +151,13 @@ def process_mat_node_group(node_group, level): #initialize nodes file.write(f"{inner}#initialize {ng_name} nodes\n") - """ - The bl_idname for AOV output nodes is the name field. - I've been using these for the variable names, but if you don't name - the AOV node it just doesn't assign anything, so we need to do it - manually. - """ - unnamed_index = 0 + unnamed_idx = 0 for node in node_group.nodes: if node.bl_idname == 'ShaderNodeGroup': if node.node_tree is not None: process_mat_node_group(node.node_tree, level + 1) - #create node - node_name = utils.clean_string(node.name) - if node_name == "": - node_name = f"node_{unnamed_index}" - unnamed_index += 1 - file.write(f"{inner}#node {node.name}\n") - file.write((f"{inner}{node_name} " - f"= {ng_name}.nodes.new(\"{node.bl_idname}\")\n")) - file.write((f"{inner}{node_name}.location " - f"= ({node.location.x}, {node.location.y})\n")) - file.write((f"{inner}{node_name}.width, {node_name}.height " - f"= {node.width}, {node.height}\n")) - if node.label: - file.write(f"{inner}{node_name}.label = \"{node.label}\"\n") + node_name, unnamed_idx = utils.create_node(node, file, inner, ng_name, unnamed_idx) #special nodes if node.bl_idname in node_settings: diff --git a/utils.py b/utils.py index 50bb856..875a164 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,5 @@ import bpy -import typing +from typing import TextIO, Tuple def clean_string(string: str) -> str: """ @@ -18,17 +18,17 @@ def clean_string(string: str) -> str: clean_str = clean_str.replace(char, '_') return clean_str -def create_header(file: typing.TextIO, node_group): +def create_header(file: TextIO, node_tree: bpy.types.NodeTree): """ Sets up the bl_info and imports the Blender API Parameters: - file (typing.TextIO): the file for the generated add-on - node_group: the node group object we're converting into an add-on + file (TextIO): the file for the generated add-on + node_tree (bpy.types.NodeTree): the node group object we're converting into an add-on """ file.write("bl_info = {\n") - file.write(f"\t\"name\" : \"{node_group.name}\",\n") + file.write(f"\t\"name\" : \"{node_tree.name}\",\n") file.write("\t\"author\" : \"Node To Python\",\n") file.write("\t\"version\" : (1, 0, 0),\n") file.write(f"\t\"blender\" : {bpy.app.version},\n") @@ -39,3 +39,74 @@ def create_header(file: typing.TextIO, node_group): file.write("import bpy\n") file.write("\n") +def init_operator(file: TextIO, name: str, idname: str, label: str): + """ + Initializes the add-on's operator + + Parameters: + file (TextIO): the file for the generated add-on + name (str): name for the class + idname (str): name for the operator + label (str): appearence inside Blender + """ + file.write(f"class {name}(bpy.types.Operator):\n") + file.write(f"\tbl_idname = \"object.{idname}\"\n") + file.write(f"\tbl_label = \"{label}\"\n") + file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") + file.write("\n") + +def make_indents(level: int) -> Tuple[str, str]: + """ + Returns strings with the correct number of indentations + given the level in the function. + + Node groups need processed recursively, + so there can sometimes be functions in functions. + + Parameters: + level (int): base number of indentations need + + Returns: + outer (str): a basic level of indentation for a node group. + inner (str): a level of indentation beyond outer + """ + outer = "\t"*level + inner = "\t"*(level + 1) + return outer, inner + +def create_node(node: bpy.types.Node, file: TextIO, inner: str, node_tree_var: str, unnamed_idx: int = 0) -> Tuple[str, int]: + """ + Initializes a new node with location, dimension, and label info + + Parameters: + node (bpy.types.Node): node to be copied + file (TextIO): file containing the generated add-on + inner (str): indentation level for this logic + node_tree_var (str): variable name for the node tree + + Returns: + node_var (str): variable name for the node + unnamed_idx (int): unnamed index. if a node doesn't have a name, this will be used + """ + + file.write(f"{inner}#node {node.name}\n") + + node_var = clean_string(node.name) + if node_var == "": + node_var = f"node_{unnamed_idx}" + unnamed_idx += 1 + + file.write((f"{inner}{node_var} " + f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n")) + + #location + file.write((f"{inner}{node_var}.location " + f"= ({node.location.x}, {node.location.y})\n")) + #dimensions + file.write((f"{inner}{node_var}.width, {node_var}.height " + f"= {node.width}, {node.height}\n")) + #label + if node.label: + file.write(f"{inner}{node_var}.label = \"{node.label}\"\n") + + return node_var, unnamed_idx \ No newline at end of file From f2d12cd26dd92ca689345b6621a4dd658379b5b3 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 03:01:45 -0600 Subject: [PATCH 18/60] feat: more comprehensive string cleaning function --- geo_nodes.py | 2 +- utils.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index ce6e2c8..a982d8a 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -306,7 +306,7 @@ def process_node_group(node_group, level): unnamed_idx = 0 #create node - node_var, unnamed_idx = utils.create_node(node, file, inner, ng_name) + node_var, unnamed_idx = utils.create_node(node, file, inner, ng_name, unnamed_idx) #special nodes if node.bl_idname in geo_node_settings: diff --git a/utils.py b/utils.py index 875a164..c2e171d 100644 --- a/utils.py +++ b/utils.py @@ -1,4 +1,5 @@ import bpy +import re from typing import TextIO, Tuple def clean_string(string: str) -> str: @@ -12,11 +13,8 @@ def clean_string(string: str) -> str: str: The input string with nasty characters converted to underscores """ - bad_chars = [' ', '.', '/'] clean_str = string.lower() - for char in bad_chars: - clean_str = clean_str.replace(char, '_') - return clean_str + return re.sub(r"[^a-zA-Z0-9_]", '_', clean_str) def create_header(file: TextIO, node_tree: bpy.types.NodeTree): """ @@ -86,7 +84,7 @@ def create_node(node: bpy.types.Node, file: TextIO, inner: str, node_tree_var: s Returns: node_var (str): variable name for the node - unnamed_idx (int): unnamed index. if a node doesn't have a name, this will be used + unnamed_idx (int): unnamed index. if a node doesn't have a name, this will be used to give it a variable name """ file.write(f"{inner}#node {node.name}\n") From 5ce0cb4dc784ad517354592fd9ea9602144cce13 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 03:11:41 -0600 Subject: [PATCH 19/60] refactor: moved set_settings_defaults to utils --- geo_nodes.py | 13 +++---------- materials.py | 41 ++++++++++++++++------------------------- utils.py | 36 +++++++++++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index a982d8a..6e22bbf 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -308,16 +308,9 @@ def process_node_group(node_group, level): #create node node_var, unnamed_idx = utils.create_node(node, file, inner, ng_name, unnamed_idx) - #special nodes - if node.bl_idname in geo_node_settings: - for setting in geo_node_settings[node.bl_idname]: - attr = getattr(node, setting, None) - if attr: - if type(attr) == str: - attr = f"\'{attr}\'" - file.write((f"{inner}{node_var}.{setting} " - f"= {attr}\n")) - elif node.bl_idname == 'GeometryNodeGroup': + utils.set_settings_defaults(node, geo_node_settings, file, inner, node_var) + + if node.bl_idname == 'GeometryNodeGroup': if node.node_tree is not None: file.write((f"{inner}{node_var}.node_tree = " f"bpy.data.node_groups" diff --git a/materials.py b/materials.py index d2d843e..149da6e 100644 --- a/materials.py +++ b/materials.py @@ -157,48 +157,39 @@ def process_mat_node_group(node_group, level): if node.node_tree is not None: process_mat_node_group(node.node_tree, level + 1) - node_name, unnamed_idx = utils.create_node(node, file, inner, ng_name, unnamed_idx) + node_var, unnamed_idx = utils.create_node(node, file, inner, ng_name, unnamed_idx) - #special nodes - if node.bl_idname in node_settings: - for setting in node_settings[node.bl_idname]: - attr = getattr(node, setting, None) - if attr: - if type(attr) == str: - attr = f"\'{attr}\'" - if type(attr) == mathutils.Vector: - attr = f"({attr[0]}, {attr[1]}, {attr[2]})" - file.write((f"{inner}{node_name}.{setting} " - f"= {attr}\n")) - elif node.bl_idname == 'ShaderNodeGroup': + utils.set_settings_defaults(node, node_settings, file, inner, node_var) + + if node.bl_idname == 'ShaderNodeGroup': if node.node_tree is not None: - file.write((f"{inner}{node_name}.node_tree = " + file.write((f"{inner}{node_var}.node_tree = " f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'ShaderNodeValToRGB': color_ramp = node.color_ramp file.write("\n") - file.write((f"{inner}{node_name}.color_ramp.color_mode = " + file.write((f"{inner}{node_var}.color_ramp.color_mode = " f"\'{color_ramp.color_mode}\'\n")) - file.write((f"{inner}{node_name}.color_ramp" + file.write((f"{inner}{node_var}.color_ramp" f".hue_interpolation = " f"\'{color_ramp.hue_interpolation}\'\n")) - file.write((f"{inner}{node_name}.color_ramp.interpolation " + file.write((f"{inner}{node_var}.color_ramp.interpolation " f"= '{color_ramp.interpolation}'\n")) file.write("\n") for i, element in enumerate(color_ramp.elements): - file.write((f"{inner}{node_name}_cre_{i} = " - f"{node_name}.color_ramp.elements" + file.write((f"{inner}{node_var}_cre_{i} = " + f"{node_var}.color_ramp.elements" f".new({element.position})\n")) - file.write((f"{inner}{node_name}_cre_{i}.alpha = " + file.write((f"{inner}{node_var}_cre_{i}.alpha = " f"{element.alpha}\n")) col = element.color r, g, b, a = col[0], col[1], col[2], col[3] - file.write((f"{inner}{node_name}_cre_{i}.color = " + file.write((f"{inner}{node_var}_cre_{i}.color = " f"({r}, {g}, {b}, {a})\n\n")) elif node.bl_idname in curve_nodes: file.write(f"{inner}#mapping settings\n") - mapping = f"{inner}{node_name}.mapping" + mapping = f"{inner}{node_var}.mapping" extend = f"\'{node.mapping.extend}\'" file.write(f"{mapping}.extend = {extend}\n") @@ -226,9 +217,9 @@ def process_mat_node_group(node_group, level): for i, curve in enumerate(node.mapping.curves): file.write(f"{inner}#curve {i}\n") - curve_i = f"{node_name}_curve_{i}" + curve_i = f"{node_var}_curve_{i}" file.write((f"{inner}{curve_i} = " - f"{node_name}.mapping.curves[{i}]\n")) + f"{node_var}.mapping.curves[{i}]\n")) for j, point in enumerate(curve.points): point_j = f"{inner}{curve_i}_point_{j}" @@ -258,7 +249,7 @@ def default_value(i, socket, list_name): dv = socket.default_value if dv is not None: file.write(f"{inner}#{socket.identifier}\n") - file.write((f"{inner}{node_name}" + file.write((f"{inner}{node_var}" f".{list_name}[{i}]" f".default_value = {dv}\n")) for i, input in enumerate(node.inputs): diff --git a/utils.py b/utils.py index c2e171d..66eddf3 100644 --- a/utils.py +++ b/utils.py @@ -1,4 +1,6 @@ import bpy +import mathutils + import re from typing import TextIO, Tuple @@ -10,11 +12,11 @@ def clean_string(string: str) -> str: string (str): The input string Returns: - str: The input string with nasty characters converted to underscores + clean_str: The input string with nasty characters converted to underscores """ - clean_str = string.lower() - return re.sub(r"[^a-zA-Z0-9_]", '_', clean_str) + clean_str = re.sub(r"[^a-zA-Z0-9_]", '_', string.lower()) + return clean_str def create_header(file: TextIO, node_tree: bpy.types.NodeTree): """ @@ -72,7 +74,8 @@ def make_indents(level: int) -> Tuple[str, str]: inner = "\t"*(level + 1) return outer, inner -def create_node(node: bpy.types.Node, file: TextIO, inner: str, node_tree_var: str, unnamed_idx: int = 0) -> Tuple[str, int]: +def create_node(node: bpy.types.Node, file: TextIO, inner: str, + node_tree_var: str, unnamed_idx: int = 0) -> Tuple[str, int]: """ Initializes a new node with location, dimension, and label info @@ -107,4 +110,27 @@ def create_node(node: bpy.types.Node, file: TextIO, inner: str, node_tree_var: s if node.label: file.write(f"{inner}{node_var}.label = \"{node.label}\"\n") - return node_var, unnamed_idx \ No newline at end of file + return node_var, unnamed_idx + +def set_settings_defaults(node: bpy.types.Node, settings: dict, file: TextIO, + inner: str, node_var: str): + """ + Sets the defaults for any settings a node may have + + Parameters: + node (bpy.types.Node): the node object we're copying settings from + settings (dict): a predefined dictionary of all settings every node has + file (TextIO): file we're generating the add-on into + inner (str): indentation level + node_var (str): name of the variable we're using for the node in our add-on + """ + if node.bl_idname in settings: + for setting in settings[node.bl_idname]: + attr = getattr(node, setting, None) + if attr: + if type(attr) == str: + attr = f"\'{attr}\'" + if type(attr) == mathutils.Vector: + attr = f"({attr[0]}, {attr[1]}, {attr[2]})" + file.write((f"{inner}{node_var}.{setting} " + f"= {attr}\n")) \ No newline at end of file From bb537e772d19f6f2cd3a44b9b28111859a6054d9 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 03:22:50 -0600 Subject: [PATCH 20/60] refactor: moved special color ramp and curve node logic to utils) --- geo_nodes.py | 66 ++---------------------------------- materials.py | 66 ++---------------------------------- utils.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 97 insertions(+), 130 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 6e22bbf..4e7eb4d 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -316,71 +316,9 @@ def process_node_group(node_group, level): f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'ShaderNodeValToRGB': - color_ramp = node.color_ramp - file.write("\n") - file.write((f"{inner}{node_var}.color_ramp.color_mode = " - f"\'{color_ramp.color_mode}\'\n")) - file.write((f"{inner}{node_var}.color_ramp" - f".hue_interpolation = " - f"\'{color_ramp.hue_interpolation}\'\n")) - file.write((f"{inner}{node_var}.color_ramp.interpolation " - f"= '{color_ramp.interpolation}'\n")) - file.write("\n") - for i, element in enumerate(color_ramp.elements): - file.write((f"{inner}{node_var}_cre_{i} = " - f"{node_var}.color_ramp.elements" - f".new({element.position})\n")) - file.write((f"{inner}{node_var}_cre_{i}.alpha = " - f"{element.alpha}\n")) - col = element.color - r, g, b, a = col[0], col[1], col[2], col[3] - file.write((f"{inner}{node_var}_cre_{i}.color = " - f"({r}, {g}, {b}, {a})\n\n")) + utils.color_ramp_settings(node, file, inner, node_var) elif node.bl_idname in curve_nodes: - file.write(f"{inner}#mapping settings\n") - mapping = f"{inner}{node_var}.mapping" - - extend = f"\'{node.mapping.extend}\'" - file.write(f"{mapping}.extend = {extend}\n") - tone = f"\'{node.mapping.tone}\'" - file.write(f"{mapping}.tone = {tone}\n") - - b_lvl = node.mapping.black_level - b_lvl_str = f"({b_lvl[0]}, {b_lvl[1]}, {b_lvl[2]})" - file.write((f"{mapping}.black_level = {b_lvl_str}\n")) - w_lvl = node.mapping.white_level - w_lvl_str = f"({w_lvl[0]}, {w_lvl[1]}, {w_lvl[2]})" - file.write((f"{mapping}.white_level = {w_lvl_str}\n")) - - min_x = node.mapping.clip_min_x - file.write(f"{mapping}.clip_min_x = {min_x}\n") - min_y = node.mapping.clip_min_y - file.write(f"{mapping}.clip_min_y = {min_y}\n") - max_x = node.mapping.clip_max_x - file.write(f"{mapping}.clip_max_x = {max_x}\n") - max_y = node.mapping.clip_max_y - file.write(f"{mapping}.clip_max_y = {max_y}\n") - - use_clip = node.mapping.use_clip - file.write(f"{mapping}.use_clip = {use_clip}\n") - - for i, curve in enumerate(node.mapping.curves): - file.write(f"{inner}#curve {i}\n") - curve_i = f"{node_var}_curve_{i}" - file.write((f"{inner}{curve_i} = " - f"{node_var}.mapping.curves[{i}]\n")) - for j, point in enumerate(curve.points): - point_j = f"{inner}{curve_i}_point_{j}" - - loc = point.location - file.write((f"{point_j} = " - f"{curve_i}.points.new" - f"({loc[0]}, {loc[1]})\n")) - - handle = f"\'{point.handle_type}\'" - file.write(f"{point_j}.handle_type = {handle}\n") - file.write(f"{inner}#update curve after changes\n") - file.write(f"{mapping}.update()\n") + utils.curve_node_settings(node, file, inner, node_var) if node.bl_idname != 'NodeReroute': for i, input in enumerate(node.inputs): diff --git a/materials.py b/materials.py index 149da6e..4ad6188 100644 --- a/materials.py +++ b/materials.py @@ -167,71 +167,9 @@ def process_mat_node_group(node_group, level): f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'ShaderNodeValToRGB': - color_ramp = node.color_ramp - file.write("\n") - file.write((f"{inner}{node_var}.color_ramp.color_mode = " - f"\'{color_ramp.color_mode}\'\n")) - file.write((f"{inner}{node_var}.color_ramp" - f".hue_interpolation = " - f"\'{color_ramp.hue_interpolation}\'\n")) - file.write((f"{inner}{node_var}.color_ramp.interpolation " - f"= '{color_ramp.interpolation}'\n")) - file.write("\n") - for i, element in enumerate(color_ramp.elements): - file.write((f"{inner}{node_var}_cre_{i} = " - f"{node_var}.color_ramp.elements" - f".new({element.position})\n")) - file.write((f"{inner}{node_var}_cre_{i}.alpha = " - f"{element.alpha}\n")) - col = element.color - r, g, b, a = col[0], col[1], col[2], col[3] - file.write((f"{inner}{node_var}_cre_{i}.color = " - f"({r}, {g}, {b}, {a})\n\n")) + utils.color_ramp_settings(node, file, inner, node_var) elif node.bl_idname in curve_nodes: - file.write(f"{inner}#mapping settings\n") - mapping = f"{inner}{node_var}.mapping" - - extend = f"\'{node.mapping.extend}\'" - file.write(f"{mapping}.extend = {extend}\n") - tone = f"\'{node.mapping.tone}\'" - file.write(f"{mapping}.tone = {tone}\n") - - b_lvl = node.mapping.black_level - b_lvl_str = f"({b_lvl[0]}, {b_lvl[1]}, {b_lvl[2]})" - file.write((f"{mapping}.black_level = {b_lvl_str}\n")) - w_lvl = node.mapping.white_level - w_lvl_str = f"({w_lvl[0]}, {w_lvl[1]}, {w_lvl[2]})" - file.write((f"{mapping}.white_level = {w_lvl_str}\n")) - - min_x = node.mapping.clip_min_x - file.write(f"{mapping}.clip_min_x = {min_x}\n") - min_y = node.mapping.clip_min_y - file.write(f"{mapping}.clip_min_y = {min_y}\n") - max_x = node.mapping.clip_max_x - file.write(f"{mapping}.clip_max_x = {max_x}\n") - max_y = node.mapping.clip_max_y - file.write(f"{mapping}.clip_max_y = {max_y}\n") - - use_clip = node.mapping.use_clip - file.write(f"{mapping}.use_clip = {use_clip}\n") - - for i, curve in enumerate(node.mapping.curves): - file.write(f"{inner}#curve {i}\n") - curve_i = f"{node_var}_curve_{i}" - file.write((f"{inner}{curve_i} = " - f"{node_var}.mapping.curves[{i}]\n")) - for j, point in enumerate(curve.points): - point_j = f"{inner}{curve_i}_point_{j}" - - loc = point.location - file.write((f"{point_j} = " - f"{curve_i}.points.new" - f"({loc[0]}, {loc[1]})\n")) - - handle = f"\'{point.handle_type}\'" - file.write(f"{point_j}.handle_type = {handle}\n") - file.write(f"{inner}#update curve after changes\n") - file.write(f"{mapping}.update()\n") + utils.curve_node_settings(node, file, inner, node_var) if node.bl_idname != 'NodeReroute': def default_value(i, socket, list_name): diff --git a/utils.py b/utils.py index 66eddf3..e501119 100644 --- a/utils.py +++ b/utils.py @@ -121,7 +121,7 @@ def set_settings_defaults(node: bpy.types.Node, settings: dict, file: TextIO, node (bpy.types.Node): the node object we're copying settings from settings (dict): a predefined dictionary of all settings every node has file (TextIO): file we're generating the add-on into - inner (str): indentation level + inner (str): indentation node_var (str): name of the variable we're using for the node in our add-on """ if node.bl_idname in settings: @@ -133,4 +133,95 @@ def set_settings_defaults(node: bpy.types.Node, settings: dict, file: TextIO, if type(attr) == mathutils.Vector: attr = f"({attr[0]}, {attr[1]}, {attr[2]})" file.write((f"{inner}{node_var}.{setting} " - f"= {attr}\n")) \ No newline at end of file + f"= {attr}\n")) + +def color_ramp_settings(node: bpy.types.Node, file: TextIO, inner: str, + node_var: str): + """ + node (bpy.types.Node): node object we're copying settings from + file (TextIO): file we're generating the add-on into + inner (str): indentation + node_var (str): name of the variable we're using for the color ramp + """ + + color_ramp = node.color_ramp + #settings + file.write((f"{inner}{node_var}.color_ramp.color_mode = " + f"\'{color_ramp.color_mode}\'\n")) + file.write((f"{inner}{node_var}.color_ramp.hue_interpolation = " + f"\'{color_ramp.hue_interpolation}\'\n")) + file.write((f"{inner}{node_var}.color_ramp.interpolation " + f"= '{color_ramp.interpolation}'\n")) + file.write("\n") + + #key points + for i, element in enumerate(color_ramp.elements): + file.write((f"{inner}{node_var}_cre_{i} = " + f"{node_var}.color_ramp.elements" + f".new({element.position})\n")) + file.write((f"{inner}{node_var}_cre_{i}.alpha = " + f"{element.alpha}\n")) + col = element.color + r, g, b, a = col[0], col[1], col[2], col[3] + file.write((f"{inner}{node_var}_cre_{i}.color = " + f"({r}, {g}, {b}, {a})\n\n")) + +def curve_node_settings(node: bpy.types.Node, file: TextIO, inner: str, node_var: str): + """ + Sets defaults for Float, Vector, and Color curves + + Parameters: + node (bpy.types.Node): curve node we're copying settings from + file (TextIO): file we're generating the add-on into + inner (str): indentation + node_var (str): variable name for the add-on's curve node + """ + + #mapping settings + file.write(f"{inner}#mapping settings\n") + mapping = f"{inner}{node_var}.mapping" + + extend = f"\'{node.mapping.extend}\'" + file.write(f"{mapping}.extend = {extend}\n") + tone = f"\'{node.mapping.tone}\'" + file.write(f"{mapping}.tone = {tone}\n") + + b_lvl = node.mapping.black_level + b_lvl_str = f"({b_lvl[0]}, {b_lvl[1]}, {b_lvl[2]})" + file.write((f"{mapping}.black_level = {b_lvl_str}\n")) + w_lvl = node.mapping.white_level + w_lvl_str = f"({w_lvl[0]}, {w_lvl[1]}, {w_lvl[2]})" + file.write((f"{mapping}.white_level = {w_lvl_str}\n")) + + min_x = node.mapping.clip_min_x + file.write(f"{mapping}.clip_min_x = {min_x}\n") + min_y = node.mapping.clip_min_y + file.write(f"{mapping}.clip_min_y = {min_y}\n") + max_x = node.mapping.clip_max_x + file.write(f"{mapping}.clip_max_x = {max_x}\n") + max_y = node.mapping.clip_max_y + file.write(f"{mapping}.clip_max_y = {max_y}\n") + + use_clip = node.mapping.use_clip + file.write(f"{mapping}.use_clip = {use_clip}\n") + + #create curves + for i, curve in enumerate(node.mapping.curves): + file.write(f"{inner}#curve {i}\n") + curve_i = f"{node_var}_curve_{i}" + file.write((f"{inner}{curve_i} = " + f"{node_var}.mapping.curves[{i}]\n")) + for j, point in enumerate(curve.points): + point_j = f"{inner}{curve_i}_point_{j}" + + loc = point.location + file.write((f"{point_j} = " + f"{curve_i}.points.new" + f"({loc[0]}, {loc[1]})\n")) + + handle = f"\'{point.handle_type}\'" + file.write(f"{point_j}.handle_type = {handle}\n") + + #update curve + file.write(f"{inner}#update curve after changes\n") + file.write(f"{mapping}.update()\n") \ No newline at end of file From c4339b01f88c9fe4794ca8478f1d9a814ef8d2c1 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 03:25:24 -0600 Subject: [PATCH 21/60] style: cleaned up curve node code --- utils.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/utils.py b/utils.py index e501119..29d3ae1 100644 --- a/utils.py +++ b/utils.py @@ -181,18 +181,23 @@ def curve_node_settings(node: bpy.types.Node, file: TextIO, inner: str, node_var file.write(f"{inner}#mapping settings\n") mapping = f"{inner}{node_var}.mapping" + #extend extend = f"\'{node.mapping.extend}\'" file.write(f"{mapping}.extend = {extend}\n") + #tone tone = f"\'{node.mapping.tone}\'" file.write(f"{mapping}.tone = {tone}\n") + #black level b_lvl = node.mapping.black_level b_lvl_str = f"({b_lvl[0]}, {b_lvl[1]}, {b_lvl[2]})" file.write((f"{mapping}.black_level = {b_lvl_str}\n")) + #white level w_lvl = node.mapping.white_level w_lvl_str = f"({w_lvl[0]}, {w_lvl[1]}, {w_lvl[2]})" file.write((f"{mapping}.white_level = {w_lvl_str}\n")) + #minima and maxima min_x = node.mapping.clip_min_x file.write(f"{mapping}.clip_min_x = {min_x}\n") min_y = node.mapping.clip_min_y @@ -202,6 +207,7 @@ def curve_node_settings(node: bpy.types.Node, file: TextIO, inner: str, node_var max_y = node.mapping.clip_max_y file.write(f"{mapping}.clip_max_y = {max_y}\n") + #use_clip use_clip = node.mapping.use_clip file.write(f"{mapping}.use_clip = {use_clip}\n") @@ -209,19 +215,17 @@ def curve_node_settings(node: bpy.types.Node, file: TextIO, inner: str, node_var for i, curve in enumerate(node.mapping.curves): file.write(f"{inner}#curve {i}\n") curve_i = f"{node_var}_curve_{i}" - file.write((f"{inner}{curve_i} = " - f"{node_var}.mapping.curves[{i}]\n")) + file.write((f"{inner}{curve_i} = {node_var}.mapping.curves[{i}]\n")) for j, point in enumerate(curve.points): point_j = f"{inner}{curve_i}_point_{j}" loc = point.location - file.write((f"{point_j} = " - f"{curve_i}.points.new" - f"({loc[0]}, {loc[1]})\n")) + file.write((f"{point_j} = {curve_i}.points.new({loc[0]}, {loc[1]})\n")) handle = f"\'{point.handle_type}\'" file.write(f"{point_j}.handle_type = {handle}\n") #update curve file.write(f"{inner}#update curve after changes\n") - file.write(f"{mapping}.update()\n") \ No newline at end of file + file.write(f"{mapping}.update()\n") + From cfbe293f9fe1323c71e43c401cd21776e84e1a20 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 03:36:50 -0600 Subject: [PATCH 22/60] refactor: a few conversion functions for different types of objects --- materials.py | 6 ++--- utils.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/materials.py b/materials.py index 4ad6188..4730e41 100644 --- a/materials.py +++ b/materials.py @@ -177,10 +177,10 @@ def default_value(i, socket, list_name): dv = None if socket.bl_idname == 'NodeSocketColor': col = socket.default_value - dv = f"({col[0]}, {col[1]}, {col[2]}, {col[3]})" + dv = utils.vec4_to_py_str(col) elif "Vector" in socket.bl_idname: - vector = socket.default_value - dv = f"({vector[0]}, {vector[1]}, {vector[2]})" + vec = socket.default_value + dv = utils.vec3_to_py_str(vec) elif socket.bl_idname == 'NodeSocketString': dv = f"\"\"" else: diff --git a/utils.py b/utils.py index 29d3ae1..2d17bd3 100644 --- a/utils.py +++ b/utils.py @@ -18,6 +18,54 @@ def clean_string(string: str) -> str: clean_str = re.sub(r"[^a-zA-Z0-9_]", '_', string.lower()) return clean_str +def enum_to_py_str(enum: str) -> str: + """ + Converts an enum into a string usuable in the add-on + + Parameters: + enum (str): enum to be converted + + Returns: + (str): converted string + """ + return f"\'{enum}\'" + +def str_to_py_str(string: str) -> str: + """ + Converts a regular string into one usuable in the add-on + + Parameters: + string (str): string to be converted + + Returns: + (str): converted string + """ + return f"\"{string}\"" + +def vec3_to_py_str(vec: mathutils.Vector) -> str: + """ + Converts a 3D vector to a string usable by the add-on + + Parameters: + vec (mathutils.Vector): a 3d vector + + Returns: + (str): string version + """ + return f"({vec[0]}, {vec[1]}, {vec[2]})" + +def vec4_to_py_str(vec: mathutils.Vector) -> str: + """ + Converts a 4D vector to a string usable by the add-on + + Parameters: + vec (mathutils.Vector): a 4d vector + + Returns: + (str): string version + """ + return f"({vec[0]}, {vec[1]}, {vec[2]}, {vec[3]})" + def create_header(file: TextIO, node_tree: bpy.types.NodeTree): """ Sets up the bl_info and imports the Blender API @@ -166,7 +214,8 @@ def color_ramp_settings(node: bpy.types.Node, file: TextIO, inner: str, file.write((f"{inner}{node_var}_cre_{i}.color = " f"({r}, {g}, {b}, {a})\n\n")) -def curve_node_settings(node: bpy.types.Node, file: TextIO, inner: str, node_var: str): +def curve_node_settings(node: bpy.types.Node, file: TextIO, inner: str, + node_var: str): """ Sets defaults for Float, Vector, and Color curves @@ -229,3 +278,19 @@ def curve_node_settings(node: bpy.types.Node, file: TextIO, inner: str, node_var file.write(f"{inner}#update curve after changes\n") file.write(f"{mapping}.update()\n") +def set_input_defaults(node: bpy.typesNode, dont_set_defaults: dict, + file: TextIO, inner: str, node_var: str): + for i, input in enumerate(node.inputs): + if input.bl_idname not in dont_set_defaults: + if input.bl_idname == 'NodeSocketColor': + dv = vec4_to_py_str(input.default_value) + elif "Vector" in input.bl_idname: + dv = vec3_to_py_str(input.default_value) + elif input.bl_idname == 'NodeSocketString': + dv = f"\"{input.default_value}\"" + else: + dv = input.default_value + if dv is not None: + file.write(f"{inner}#{input.identifier}\n") + file.write((f"{inner}{node_var}.inputs[{i}].default_value = " + f"{dv}\n")) \ No newline at end of file From ef2b7fbe06cbda2b0f4fb0daf2eaee11cab9aec1 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 03:39:20 -0600 Subject: [PATCH 23/60] refactor: input defaults moved to utils --- geo_nodes.py | 20 ++------------------ materials.py | 30 ++---------------------------- utils.py | 29 +++++++++++++++-------------- 3 files changed, 19 insertions(+), 60 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 4e7eb4d..264626d 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -320,24 +320,8 @@ def process_node_group(node_group, level): elif node.bl_idname in curve_nodes: utils.curve_node_settings(node, file, inner, node_var) - if node.bl_idname != 'NodeReroute': - for i, input in enumerate(node.inputs): - if input.bl_idname not in dont_set_defaults: - if input.bl_idname == 'NodeSocketColor': - col = input.default_value - dv = f"({col[0]}, {col[1]}, {col[2]}, {col[3]})" - elif "Vector" in input.bl_idname: - vector = input.default_value - dv = f"({vector[0]}, {vector[1]}, {vector[2]})" - elif input.bl_idname == 'NodeSocketString': - dv = f"\"\"" - else: - dv = input.default_value - if dv is not None: - file.write(f"{inner}#{input.identifier}\n") - file.write((f"{inner}{node_var}" - f".inputs[{i}]" - f".default_value = {dv}\n")) + utils.set_input_defaults(node, dont_set_defaults, file, inner, + node_var) file.write("\n") #initialize links diff --git a/materials.py b/materials.py index 4730e41..c68f552 100644 --- a/materials.py +++ b/materials.py @@ -171,34 +171,8 @@ def process_mat_node_group(node_group, level): elif node.bl_idname in curve_nodes: utils.curve_node_settings(node, file, inner, node_var) - if node.bl_idname != 'NodeReroute': - def default_value(i, socket, list_name): - if socket.bl_idname not in dont_set_defaults: - dv = None - if socket.bl_idname == 'NodeSocketColor': - col = socket.default_value - dv = utils.vec4_to_py_str(col) - elif "Vector" in socket.bl_idname: - vec = socket.default_value - dv = utils.vec3_to_py_str(vec) - elif socket.bl_idname == 'NodeSocketString': - dv = f"\"\"" - else: - dv = socket.default_value - if dv is not None: - file.write(f"{inner}#{socket.identifier}\n") - file.write((f"{inner}{node_var}" - f".{list_name}[{i}]" - f".default_value = {dv}\n")) - for i, input in enumerate(node.inputs): - default_value(i, input, "inputs") - """ - TODO: some shader nodes require you set the default value in the output. - this will need to be handled case by case it looks like though - - for i, output in enumerate(node.outputs): - default_value(i, output, "outputs") - """ + utils.set_input_defaults(node, dont_set_defaults, file, inner, + node_var) #initialize links if node_group.links: diff --git a/utils.py b/utils.py index 2d17bd3..e2180a9 100644 --- a/utils.py +++ b/utils.py @@ -280,17 +280,18 @@ def curve_node_settings(node: bpy.types.Node, file: TextIO, inner: str, def set_input_defaults(node: bpy.typesNode, dont_set_defaults: dict, file: TextIO, inner: str, node_var: str): - for i, input in enumerate(node.inputs): - if input.bl_idname not in dont_set_defaults: - if input.bl_idname == 'NodeSocketColor': - dv = vec4_to_py_str(input.default_value) - elif "Vector" in input.bl_idname: - dv = vec3_to_py_str(input.default_value) - elif input.bl_idname == 'NodeSocketString': - dv = f"\"{input.default_value}\"" - else: - dv = input.default_value - if dv is not None: - file.write(f"{inner}#{input.identifier}\n") - file.write((f"{inner}{node_var}.inputs[{i}].default_value = " - f"{dv}\n")) \ No newline at end of file + if node.bl_idname != 'NodeReroute': + for i, input in enumerate(node.inputs): + if input.bl_idname not in dont_set_defaults: + if input.bl_idname == 'NodeSocketColor': + default_val = vec4_to_py_str(input.default_value) + elif "Vector" in input.bl_idname: + default_val = vec3_to_py_str(input.default_value) + elif input.bl_idname == 'NodeSocketString': + default_val = str_to_py_str(input.default_value) + else: + default_val = input.default_value + if default_val is not None: + file.write(f"{inner}#{input.identifier}\n") + file.write((f"{inner}{node_var}.inputs[{i}].default_value" + f" = {default_val}\n")) \ No newline at end of file From be850ae0f5188f4b16a179d06161bde77393695b Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 04:06:39 -0600 Subject: [PATCH 24/60] refactor: links initialization moved to utils --- geo_nodes.py | 103 +++++++++++++++++++-------------------------------- materials.py | 52 +++++++------------------- utils.py | 37 +++++++++++++++++- 3 files changed, 88 insertions(+), 104 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 264626d..a674529 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -181,38 +181,43 @@ def execute(self, context): file.write("\tdef execute(self, context):\n") - def process_node_group(node_group, level): - ng_name = utils.clean_string(node_group.name) + #set to keep track of already created node trees + node_trees = {} + + def process_geo_nodes_group(node_tree, level): + node_tree_var = utils.clean_string(node_tree.name) outer, inner = utils.make_indents(level) #initialize node group - file.write(f"{outer}#initialize {ng_name} node group\n") - file.write(f"{outer}def {ng_name}_node_group():\n") - file.write((f"{inner}{ng_name}" + file.write(f"{outer}#initialize {node_tree_var} node group\n") + file.write(f"{outer}def {node_tree_var}_node_group():\n") + file.write((f"{inner}{node_tree_var}" f"= bpy.data.node_groups.new(" f"type = \"GeometryNodeTree\", " - f"name = \"{node_group.name}\")\n")) + f"name = \"{node_tree.name}\")\n")) file.write("\n") inputs_set = False outputs_set = False #initialize nodes - file.write(f"{inner}#initialize {ng_name} nodes\n") - for node in node_group.nodes: + file.write(f"{inner}#initialize {node_tree_var} nodes\n") + for node in node_tree.nodes: if node.bl_idname == 'GeometryNodeGroup': - if node.node_tree is not None: - process_node_group(node.node_tree, level + 1) + node_nt = node.node_tree + if node_nt is not None and node_nt not in node_trees: + process_geo_nodes_group(node_nt, level + 1) + node_trees.add(node_nt) elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - file.write(f"{inner}#{ng_name} inputs\n") + file.write(f"{inner}#{node_tree_var} inputs\n") for i, input in enumerate(node.outputs): if input.bl_idname != "NodeSocketVirtual": file.write(f"{inner}#input {input.name}\n") - file.write((f"{inner}{ng_name}.inputs.new" + file.write((f"{inner}{node_tree_var}.inputs.new" f"(\"{input.bl_idname}\", " f"\"{input.name}\")\n")) - socket = node_group.inputs[i] + socket = node_tree.inputs[i] if input.bl_idname in default_sockets: if input.bl_idname == 'NodeSocketColor': col = socket.default_value @@ -225,39 +230,39 @@ def process_node_group(node_group, level): dv = socket.default_value #default value - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{node_tree_var}" f".inputs[{i}]" f".default_value = {dv}\n")) #min value if hasattr(socket, "min_value"): - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{node_tree_var}" f".inputs[{i}]" f".min_value = " f"{socket.min_value}\n")) #max value if hasattr(socket, "max_value"): - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{node_tree_var}" f".inputs[{i}]" f".max_value = " f"{socket.max_value}\n")) #default attribute name if hasattr(socket, "default_attribute_name"): if socket.default_attribute_name != "": - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{node_tree_var}" f".inputs[{i}]" f".default_attribute_name = \"" f"{socket.default_attribute_name}" f"\"\n")) #description if socket.description != "": - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{node_tree_var}" f".inputs[{i}]" f".description = " f"\"{socket.description}\"\n")) #hide value if socket.hide_value is True: - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{node_tree_var}" f".inputs[{i}]" f".hide_value = " f"{socket.hide_value}\n")) @@ -266,23 +271,23 @@ def process_node_group(node_group, level): inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: - file.write(f"{inner}#{ng_name} outputs\n") + file.write(f"{inner}#{node_tree_var} outputs\n") for i, output in enumerate(node.inputs): if output.bl_idname != 'NodeSocketVirtual': - file.write((f"{inner}{ng_name}.outputs" + file.write((f"{inner}{node_tree_var}.outputs" f".new(\"{output.bl_idname}\", " f"\"{output.name}\")\n")) - socket = node_group.outputs[i] + socket = node_tree.outputs[i] #description if socket.description != "": - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{node_tree_var}" f".outputs[{i}]" f".description = " f"\"{socket.description}\"\n")) #hide value if socket.hide_value is True: - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{node_tree_var}" f".outputs[{i}]" f".hide_value = " f"{socket.hide_value}\n")) @@ -290,14 +295,14 @@ def process_node_group(node_group, level): #default attribute name if hasattr(socket, "default_attribute_name"): if socket.default_attribute_name != "": - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{node_tree_var}" f".outputs[{i}]" f".default_attribute_name = \"" f"{socket.default_attribute_name}" f"\"\n")) #attribute domain if hasattr(socket, "attribute_domain"): - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{node_tree_var}" f".outputs[{i}]" f".attribute_domain = " f"\'{socket.attribute_domain}\'\n")) @@ -306,9 +311,12 @@ def process_node_group(node_group, level): unnamed_idx = 0 #create node - node_var, unnamed_idx = utils.create_node(node, file, inner, ng_name, unnamed_idx) + node_var, unnamed_idx = utils.create_node(node, file, inner, + node_tree_var, + unnamed_idx) - utils.set_settings_defaults(node, geo_node_settings, file, inner, node_var) + utils.set_settings_defaults(node, geo_node_settings, file, + inner, node_var) if node.bl_idname == 'GeometryNodeGroup': if node.node_tree is not None: @@ -322,46 +330,13 @@ def process_node_group(node_group, level): utils.set_input_defaults(node, dont_set_defaults, file, inner, node_var) - file.write("\n") - #initialize links - if node_group.links: - file.write(f"{inner}#initialize {ng_name} links\n") - for link in node_group.links: - input_node = utils.clean_string(link.from_node.name) - input_socket = link.from_socket - - """ - Blender's socket dictionary doesn't guarantee - unique keys, which has caused much wailing and - gnashing of teeth. This is a quick fix that - doesn't run quick - """ - for i, item in enumerate(link.from_node.outputs.items()): - if item[1] == input_socket: - input_idx = i - break - - output_node = utils.clean_string(link.to_node.name) - output_socket = link.to_socket - - for i, item in enumerate(link.to_node.inputs.items()): - if item[1] == output_socket: - output_idx = i - break - - file.write((f"{inner}#{input_node}.{input_socket.name} " - f"-> {output_node}.{output_socket.name}\n")) - file.write((f"{inner}{ng_name}.links.new({input_node}" - f".outputs[{input_idx}], " - f"{output_node}.inputs[{output_idx}])\n")) + utils.init_links(node_tree, file, inner, node_tree_var) #create node group - file.write("\n") - file.write(f"{outer}{ng_name}_node_group()\n") - file.write("\n") + file.write(f"\n{outer}{node_tree_var}_node_group()\n\n") - process_node_group(ng, 2) + process_geo_nodes_group(ng, 2) file.write("\t\treturn {'FINISHED'}\n\n") diff --git a/materials.py b/materials.py index c68f552..49d826e 100644 --- a/materials.py +++ b/materials.py @@ -125,9 +125,11 @@ def create_material(): file.write(f"\t\tmat.use_nodes = True\n") create_material() - def process_mat_node_group(node_group, level): - ng_name = utils.clean_string(node_group.name) - ng_label = node_group.name + node_trees = {} + + def process_node_group(node_tree, level): + ng_name = utils.clean_string(node_tree.name) + ng_label = node_tree.name if level == 2: #outermost node group ng_name = utils.clean_string(self.material_name) @@ -152,10 +154,12 @@ def process_mat_node_group(node_group, level): file.write(f"{inner}#initialize {ng_name} nodes\n") unnamed_idx = 0 - for node in node_group.nodes: + for node in node_tree.nodes: if node.bl_idname == 'ShaderNodeGroup': - if node.node_tree is not None: - process_mat_node_group(node.node_tree, level + 1) + node_nt = node.node_tree + if node_nt is not None and node_nt not in node_trees: + process_node_group(node_nt, level + 1) + node_trees.add(node_nt) node_var, unnamed_idx = utils.create_node(node, file, inner, ng_name, unnamed_idx) @@ -174,41 +178,11 @@ def process_mat_node_group(node_group, level): utils.set_input_defaults(node, dont_set_defaults, file, inner, node_var) - #initialize links - if node_group.links: - file.write(f"{inner}#initialize {ng_name} links\n") - for link in node_group.links: - input_node = utils.clean_string(link.from_node.name) - input_socket = link.from_socket - - """ - Blender's socket dictionary doesn't guarantee - unique keys, which has caused much wailing and - gnashing of teeth. This is a quick fix that - doesn't run quick - """ - for i, item in enumerate(link.from_node.outputs.items()): - if item[1] == input_socket: - input_idx = i - break - - output_node = utils.clean_string(link.to_node.name) - output_socket = link.to_socket - - for i, item in enumerate(link.to_node.inputs.items()): - if item[1] == output_socket: - output_idx = i - break - - file.write((f"{inner}#{input_node}.{input_socket.name} " - f"-> {output_node}.{output_socket.name}\n")) - file.write((f"{inner}{ng_name}.links.new({input_node}" - f".outputs[{input_idx}], " - f"{output_node}.inputs[{output_idx}])\n")) + utils.init_links(node_tree, file, inner, ng_name) - file.write(f"{outer}{ng_name}_node_group()\n") + file.write(f"\n{outer}{ng_name}_node_group()\n\n") - process_mat_node_group(ng, 2) + process_node_group(ng, 2) file.write("\t\treturn {'FINISHED'}\n\n") diff --git a/utils.py b/utils.py index e2180a9..0764b67 100644 --- a/utils.py +++ b/utils.py @@ -294,4 +294,39 @@ def set_input_defaults(node: bpy.typesNode, dont_set_defaults: dict, if default_val is not None: file.write(f"{inner}#{input.identifier}\n") file.write((f"{inner}{node_var}.inputs[{i}].default_value" - f" = {default_val}\n")) \ No newline at end of file + f" = {default_val}\n")) + file.write("\n") + +def init_links(node_tree: bpy.types.NodeTree, file: TextIO, inner: str, + node_tree_var: str): + if node_tree.links: + file.write(f"{inner}#initialize {node_tree_var} links\n") + for link in node_tree.links: + input_node = clean_string(link.from_node.name) + input_socket = link.from_socket + + """ + Blender's socket dictionary doesn't guarantee + unique keys, which has caused much wailing and + gnashing of teeth. This is a quick fix that + doesn't run quick + """ + for i, item in enumerate(link.from_node.outputs.items()): + if item[1] == input_socket: + input_idx = i + break + + output_node = clean_string(link.to_node.name) + output_socket = link.to_socket + + for i, item in enumerate(link.to_node.inputs.items()): + if item[1] == output_socket: + output_idx = i + break + + file.write((f"{inner}#{input_node}.{input_socket.name} " + f"-> {output_node}.{output_socket.name}\n")) + file.write((f"{inner}{node_tree_var}.links.new({input_node}" + f".outputs[{input_idx}], " + f"{output_node}.inputs[{output_idx}])\n")) + From 3a6a6c96dc55975a983636948729cde168b78a6e Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 04:16:06 -0600 Subject: [PATCH 25/60] refactor: moved menu, register, unregister, and main func creation into utils --- geo_nodes.py | 32 ++++------------------------ materials.py | 38 +++++++-------------------------- utils.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 59 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index a674529..95d23c6 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -340,34 +340,10 @@ def process_geo_nodes_group(node_tree, level): file.write("\t\treturn {'FINISHED'}\n\n") - """Create the function that adds the addon to the menu""" - def create_menu_func(): - file.write("def menu_func(self, context):\n") - file.write(f"\tself.layout.operator({class_name}.bl_idname)\n") - file.write("\n") - create_menu_func() - - """Create the register function""" - def create_register(): - file.write("def register():\n") - file.write(f"\tbpy.utils.register_class({class_name})\n") - file.write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") - file.write("\n") - create_register() - - """Create the unregister function""" - def create_unregister(): - file.write("def unregister():\n") - file.write(f"\tbpy.utils.unregister_class({class_name})\n") - file.write("\tbpy.types.VIEW3D_MT_objects.remove(menu_func)\n") - file.write("\n") - create_unregister() - - """Create the main function""" - def create_main(): - file.write("if __name__ == \"__main__\":\n") - file.write("\tregister()") - create_main() + utils.create_menu_func(file, class_name) + utils.create_register_func(file, class_name) + utils.create_unregister_func(file, class_name) + utils.create_main_func(file, class_name) file.close() return {'FINISHED'} diff --git a/materials.py b/materials.py index 49d826e..ae7380a 100644 --- a/materials.py +++ b/materials.py @@ -127,7 +127,7 @@ def create_material(): node_trees = {} - def process_node_group(node_tree, level): + def process_mat_node_group(node_tree, level): ng_name = utils.clean_string(node_tree.name) ng_label = node_tree.name @@ -158,7 +158,7 @@ def process_node_group(node_tree, level): if node.bl_idname == 'ShaderNodeGroup': node_nt = node.node_tree if node_nt is not None and node_nt not in node_trees: - process_node_group(node_nt, level + 1) + process_mat_node_group(node_nt, level + 1) node_trees.add(node_nt) node_var, unnamed_idx = utils.create_node(node, file, inner, ng_name, unnamed_idx) @@ -182,38 +182,14 @@ def process_node_group(node_tree, level): file.write(f"\n{outer}{ng_name}_node_group()\n\n") - process_node_group(ng, 2) + process_mat_node_group(ng, 2) file.write("\t\treturn {'FINISHED'}\n\n") - """Create the function that adds the addon to the menu""" - def create_menu_func(): - file.write("def menu_func(self, context):\n") - file.write(f"\tself.layout.operator({class_name}.bl_idname)\n") - file.write("\n") - create_menu_func() - - """Create the register function""" - def create_register(): - file.write("def register():\n") - file.write(f"\tbpy.utils.register_class({class_name})\n") - file.write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") - file.write("\n") - create_register() - - """Create the unregister function""" - def create_unregister(): - file.write("def unregister():\n") - file.write(f"\tbpy.utils.unregister_class({class_name})\n") - file.write("\tbpy.types.VIEW3D_MT_objects.remove(menu_func)\n") - file.write("\n") - create_unregister() - - """Create the main function""" - def create_main(): - file.write("if __name__ == \"__main__\":\n") - file.write("\tregister()") - create_main() + utils.create_menu_func(file, class_name) + utils.create_register_func(file, class_name) + utils.create_unregister_func(file, class_name) + utils.create_main_func(file, class_name) file.close() return {'FINISHED'} diff --git a/utils.py b/utils.py index 0764b67..5c18856 100644 --- a/utils.py +++ b/utils.py @@ -299,6 +299,16 @@ def set_input_defaults(node: bpy.typesNode, dont_set_defaults: dict, def init_links(node_tree: bpy.types.NodeTree, file: TextIO, inner: str, node_tree_var: str): + """ + Create all the links between nodes + + Parameters: + node_tree (bpy.types.NodeTree): node tree we're copying + file (TextIO): file we're generating the add-on into + inner (str): indentation + node_tree_var (str): variable name we're using for the copied node tree + """ + if node_tree.links: file.write(f"{inner}#initialize {node_tree_var} links\n") for link in node_tree.links: @@ -330,3 +340,52 @@ def init_links(node_tree: bpy.types.NodeTree, file: TextIO, inner: str, f".outputs[{input_idx}], " f"{output_node}.inputs[{output_idx}])\n")) +def create_menu_func(file: TextIO, name: str): + """ + Creates the menu function + + Parameters: + file (TextIO): file we're generating the add-on into + name (str): name of the generated operator class + """ + + file.write("def menu_func(self, context):\n") + file.write(f"\tself.layout.operator({name}.bl_idname)\n") + file.write("\n") + +def create_register_func(file: TextIO, name: str): + """ + Creates the register function + + Parameters: + file (TextIO): file we're generating the add-on into + name (str): name of the generated operator class + """ + file.write("def register():\n") + file.write(f"\tbpy.utils.register_class({name})\n") + file.write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") + file.write("\n") + +def create_unregister_func(file: TextIO, name: str): + """ + Creates the unregister function + + Parameters: + file (TextIO): file we're generating the add-on into + name (str): name of the generated operator class + """ + file.write("def unregister():\n") + file.write(f"\tbpy.utils.unregister_class({name})\n") + file.write("\tbpy.types.VIEW3D_MT_objects.remove(menu_func)\n") + file.write("\n") + +def create_main_func(file: TextIO, name: str): + """ + Creates the main function + + Parameters: + file (TextIO): file we're generating the add-on into + name (str): name of the generated operator class + """ + file.write("if __name__ == \"__main__\":\n") + file.write("\tregister()") \ No newline at end of file From d2f4272d28e473f2da304c2a711360e9b6b1b348 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 05:00:19 -0600 Subject: [PATCH 26/60] fix: a few import errors and other bugs --- __pycache__/utils.cpython-310.pyc | Bin 0 -> 11991 bytes geo_nodes.py | 56 +++++++++++-------------- materials.py | 65 +++++++++++++++--------------- utils.py | 29 ++++++------- 4 files changed, 68 insertions(+), 82 deletions(-) create mode 100644 __pycache__/utils.cpython-310.pyc diff --git a/__pycache__/utils.cpython-310.pyc b/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df5fe1d1c26eb03315a92f3aa8eb3f00bffbb6b7 GIT binary patch literal 11991 zcmd5?&2t+^cE=0`1I*xqAVg85K1ZZ>N!S!*uas!5iXC|+?UuH(v`f;iQ(P;^5HlnJ zff%S~K#4Gr+QeI%Tyx5Y!!0?fO2z*|4ynp5e?TstYpRksS6?lNWm+`+Dyxhj)|Cf?VIVneJr#9rTvZ3(1 zx}oA%ZKrp&4Q)5Gkx^1k+SMJ+)weV!uOHLO|^=x5@tB%%%OFLTTf$_c|2z@cg|UG7SU67&bqU}nsIfs zs*5R%Ip>^5PsK<%-*zrArc*rrBF0|AV@58>voV)IFT1Cc5m$iMbiUzy z6FBD+2wElKqu!je;;f?g%n7|!=N0rWaPOk?>Ji*)&Q8nLbw3cz&UUB^H|U8@18kA}si^6A?%?s~Q3RVYf#Tl^jk zU&U|rmi1`IwVIu7FNn}G2p11{)zP2^&0oimqa&W@REUQLIpaKp6U+YB>p@0lyAs?b1~ z&`9R~Bqj1_IlO*t$J=$UZFX(3>xyf4y+&`>?F9a{2X5E%udREI`^bBDKiKg)*Lp#- z?bo{dVIe_-GszO!340~4DWXWzXl|#sJ8?#IUTHjZMxPf@ILaXP5z2!!PDbtjoVRjE z?h}X5u?PjIi47;G)bo3MF3pY=fYi3*T=hD~9}JIE7GWGNiD-Q{U8fY$@X7N)9U(N3%COa>m;u*t8DInC97btwYqBuJCePB z#JC{MwsqZ!FrUmV5nUmjeiOV%>g7;b6~yj`>YDW#RX!tDI8>@kuTK=JQ)e1x4-C*Sca(-Veq%Je@oUjgkQ!$yiZfJO zpkfilXKAXPrGnI#IG5D;RZ1ghd1FM&Hvq!4teN5>>Y+wTJv2H{ZS_F7Zi5IN|4}`I zhl%@VsKAJTz#l7$tf>Q~r4WlAq?0x+sb!Ly9;mH!O9Nzf3^JGW8%fPfYWbvANNU{X z6me{&{-?}e58Z%`7pcI_cD>ozf{N@oR&%%O33O5WciJvl3G3~5?;o!p*{-ch8w*~i zG2BFkkY2Xkjw@`kIFXhm&Q2(1t!{SrYr%fk^=oV)K*b0P!C2eEgLU;bTaYE|nY$tq z%?o=GDfP}c=6)gG$(z;(WglTzxa~FU020gS-rt>3@U^pk&7H+3OKAYbJLq$TvpnaXuTmAVlR(~hj zUiFsQKWz}l@=;iCT1%4GYlrF*aU8njTCHz)3$u)G`VwH+271R9F#3=x;r1%CPe2S?mW<{w;o>{{y{6BXO zY(jP@{v#Di&mBBISr@V&bEzSGX0pI%s17wZP`6YZdfNVT3}&K4?o#BH_|jSBqws_v zQHK|_39aL8fus(+NER{Q;=$X^Jy-f|ydpqIt<&o?h%`0HGA!AwK76a=Lj81w*KmCw zHO$b1mBnuM*Ca~cBNU`u;4k`cV}s@{SQ38TR)p23X_A*{j%g=eu4ffa7IfIAjWFPZ zeb9oAaRs~{ob-gP2t%`BNu-mMVAi!+jY<_+W6Cr;(6yuS$Bx`|_dV!%o{9$@Du%R* z#1_@GAdex;aDSL#jMEHL*tMUyFc)+%ev8hYf~MR|LCGoNbsDab@)oaAwMbRAj&+1X zVZPTP*>k}0&l{v-;y*$`D_~vb{bp*Y#bXB9>*LiUk$l>+f+M*(cHFy7uprsuDFe>02wegjY5_L(L>oTAY-3UyF|4%zP2XyJ z+sy{gG{Ffmnxgi4l6g)?xBlrS0$`)Uk4>lnjXk!VIutXrPftmc*#$&%x_erF}H zcI-Vwg&eN0fdbgYvu3+Z`T(&#$IhgLtY!e9i4E|KeSMkN&$>8-F!736$PUv`-2Fn0 zTKpQ0dzIXhDpY+fK5YQ3vafomqCHo8)^viMHLGvdcHHLnPCy9ET4EK%E0C3{$@-q{ zx(u90MkUO}m_y}xsO-blAdI0d(XmOW>f&2KBdrCAtw1g+>Tc@s+rvzNH|PW9!}L%e=;A)Kg#Mutkbba`zafc+ z<1&OZ&FzqO^t^%i5Q#pHXfmkD*oJCZZiB7>HuVK-5YjP@!5YA1e36EDYEr_9lTHQu zXR%zfZ%wk}xLbCw4R4wR!tU%_@n&P5+qL(Z-+m=gCd@?lY1g>BPcoZKu_e4+X33;w z)oC`!I@z#?F4orUfS;)i%EcD@WN~e9dIArdk5-;)$CgggaSB7a=8>--s}1SRLCOM; z7jcrt;Le!KVyR|zfYs1W6A_cs^n98j4lsCaFwR~N^p%MmQfup?40bs&{NuMR3GXxte5^(R*KErgoFKamL1~aFdLCD)Ck$M z?3_k#JtlHJ=8?a{K=z}|LP;qo8lD1t_AAPqqC$GqpJ=~8V(j~Xn()>sR;A6dkSu`% zNif(5!u9$n#pX{LXf5*9)yNbnFGhW7^pR;it`DY_W6!ZZhmBbPW7dL*LHyo(AEp3NS>UqBWr_0N?`y-{&={J- z{4sOR3|Y-5-|3NK2QReiwb|!punXfLN4lk%%hX{G>MTzE z#c_DI?Q$NNt)2Kb__S}->~?p@25y;|Ns=<+0(F~mIJ%`F?5C?Hf@Cu^M>}EpiyLS^ z;p~yMhsBAth6W{9IT;b=;#Gw?B&QLCdYv%CiwZM5NwqL-5lO1W@1Y2lW|)n}hiO2E ziU^hMP}vL>ds@&>jh39q`#%DSe;b8vlvFm5W2LE9(j@LMovQf-8BFt~u1Xc4y`*;e z6kck-5bp!Ygeo9Pk~&~jFhB|__D7K)SBd=-g+SI&qx@(~Va0$n6#QrMxyjCHEWR1( zaYs<3A_$5ET|&XqqAh}AOxh4vWf0jM+fM^EGpRwC0bPJi%_SI$EUe=gGD8Cxj0A&` zU@%A>L>SBjLy1Lv9D~kkL9TFSK%zh1n;jOCvE@Nwsy8<*CcU$R;#99OEG4~_L20Vj z9L^-Yrv@{l-XPDkDX_9^Kv|ZDvkXZJd2Uc1L9)6$O)a2vC59PP05Q*fXBc`xs!8CT zZ50{oO<+AWoa56a=*7V);LwJr2Xn2`aGo)pW-Qcub}*mNcRr%;xdToq;;iRdw01h_ zGs(E~gEMH0==k*Wr~eYK&AAZqRm>9?<-2nUO4hQ;8nGIBNV&$?&av(OZrihiHA|XP z8Q0ul)rH`E4>wXgY&ybbV?UNiS%>7qo8ci*DD>5dMR$`{MgKTsg$piRdg!%k(uj?Sw86CI1McEQuZg#f8E+CNX zU9G}al%;*hMkEY%Nj&&;pjnt7qez$^qXrC0LW!~Zeq1LCjMewUT)cDfF3u#><=#SF zZbsaq#{Aeestd=Ol{(E5?(E(hynz9Ee%PdY-f>z zzE%n$WEDPa+*)E-4X`%?n6(I&N3kcG>JdKmh4g4?9C?|#G>~ySo6ano&fI`fM7U~X zpVK4=hcn?$it^vyNU4=A05Zhuji; z)q^NQ2Nzr)WQT^5>L^dY!59q2U}7Ai4jyl`@+XYXWBfymFYx$#$qFN!=tpObOdO$ zw629^y5W%bOt4RqE~ZR+{{$Q*zJsE#Sy$hROl!Wzq~nMM!3snapW;B+(vR0H@y7(5 zeoR)TfJ+ggG0RVsccYXGD@=LGmgje$MO#W_HbRHwdf@K*VHRI|5P?Gjl?d=pFh{j0 zZ~I1h1d{RtUH^8R)r8;^M=M80AM@6STkfCA9Zz!rk@u++T(G^AYjKrX%t{1^ESCFo=WW$eu038Ah)b}A)^u>8#m zn@|48QM#n$0ZR{+KefuR^o6-~SW5CrzaH^b>WHQPy z7zqQBd&#zl6`~+nXK2JbvX8m4(|e47Z))^3_VQQk*aT54OIp;{X5v literal 0 HcmV?d00001 diff --git a/geo_nodes.py b/geo_nodes.py index 95d23c6..26e2717 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -1,17 +1,7 @@ -bl_info = { - "name": "Node to Python", - "description": "Convert Geometry Node Groups to a Python add-on", - "author": "Brendan Parmer", - "version": (2, 0, 0), - "blender": (3, 0, 0), - "location": "Node", - "category": "Node", -} - import bpy import os -from . import utils +from .utils import * #node tree input sockets that have default properties default_sockets = {'NodeSocketBool', @@ -156,11 +146,11 @@ class GeoNodesToPython(bpy.types.Operator): def execute(self, context): #find node group to replicate - ng = bpy.data.node_groups[self.geo_nodes_group_name] + nt = bpy.data.node_groups[self.geo_nodes_group_name] #set up names to use in generated addon - ng_name = utils.clean_string(ng.name) - class_name = ng.name.replace(" ", "").replace('.', "") + nt_var = clean_string(nt.name) + class_name = nt.name.replace(" ", "").replace('.', "") #find base directory to save new addon dir = bpy.path.abspath("//") @@ -174,20 +164,20 @@ def execute(self, context): addon_dir = os.path.join(dir, "addons") if not os.path.exists(addon_dir): os.mkdir(addon_dir) - file = open(f"{addon_dir}/{ng_name}_addon.py", "w") + file = open(f"{addon_dir}/{nt_var}_addon.py", "w") - utils.create_header(file, ng) - utils.init_operator(file, class_name, ng_name, ng.name) + create_header(file, nt) + init_operator(file, class_name, nt_var, nt.name) file.write("\tdef execute(self, context):\n") #set to keep track of already created node trees - node_trees = {} + node_trees = set() def process_geo_nodes_group(node_tree, level): - node_tree_var = utils.clean_string(node_tree.name) + node_tree_var = clean_string(node_tree.name) - outer, inner = utils.make_indents(level) + outer, inner = make_indents(level) #initialize node group file.write(f"{outer}#initialize {node_tree_var} node group\n") @@ -311,11 +301,11 @@ def process_geo_nodes_group(node_tree, level): unnamed_idx = 0 #create node - node_var, unnamed_idx = utils.create_node(node, file, inner, + node_var, unnamed_idx = create_node(node, file, inner, node_tree_var, unnamed_idx) - utils.set_settings_defaults(node, geo_node_settings, file, + set_settings_defaults(node, geo_node_settings, file, inner, node_var) if node.bl_idname == 'GeometryNodeGroup': @@ -324,27 +314,27 @@ def process_geo_nodes_group(node_tree, level): f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'ShaderNodeValToRGB': - utils.color_ramp_settings(node, file, inner, node_var) + color_ramp_settings(node, file, inner, node_var) elif node.bl_idname in curve_nodes: - utils.curve_node_settings(node, file, inner, node_var) + curve_node_settings(node, file, inner, node_var) - utils.set_input_defaults(node, dont_set_defaults, file, inner, + set_input_defaults(node, dont_set_defaults, file, inner, node_var) - utils.init_links(node_tree, file, inner, node_tree_var) + init_links(node_tree, file, inner, node_tree_var) #create node group file.write(f"\n{outer}{node_tree_var}_node_group()\n\n") - process_geo_nodes_group(ng, 2) + process_geo_nodes_group(nt, 2) file.write("\t\treturn {'FINISHED'}\n\n") - utils.create_menu_func(file, class_name) - utils.create_register_func(file, class_name) - utils.create_unregister_func(file, class_name) - utils.create_main_func(file, class_name) - + create_menu_func(file, class_name) + create_register_func(file, class_name) + create_unregister_func(file, class_name) + create_main_func(file) + file.close() return {'FINISHED'} @@ -364,7 +354,7 @@ def draw(self, context): for geo_ng in geo_node_groups: op = layout.operator(GeoNodesToPython.bl_idname, text=geo_ng.name) - op.node_group_name = geo_ng.name + op.geo_nodes_group_name = geo_ng.name class GeoNodesToPythonPanel(bpy.types.Panel): bl_label = "Geometry Nodes to Python" diff --git a/materials.py b/materials.py index ae7380a..1206802 100644 --- a/materials.py +++ b/materials.py @@ -1,8 +1,7 @@ import bpy -import mathutils import os -from . import utils +from .utils import * #node input sockets that are messy to set default values for dont_set_defaults = {'NodeSocketCollection', @@ -92,16 +91,16 @@ class MaterialToPython(bpy.types.Operator): def execute(self, context): #find node group to replicate - ng = bpy.data.materials[self.material_name].node_tree - if ng is None: + 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?")) return {'CANCELLED'} #set up names to use in generated addon - ng_name = utils.clean_string(self.material_name) - class_name = ng.name.replace(" ", "") + mat_var = clean_string(self.material_name) + class_name = nt.name.replace(" ", "") dir = bpy.path.abspath("//") if not dir or dir == "": @@ -112,10 +111,10 @@ def execute(self, context): addon_dir = os.path.join(dir, "addons") if not os.path.exists(addon_dir): os.mkdir(addon_dir) - file = open(f"{addon_dir}/{ng_name}_addon.py", "w") + file = open(f"{addon_dir}/{mat_var}_addon.py", "w") - utils.create_header(file, ng) - utils.init_operator(file, class_name, ng_name, self.material_name) + create_header(file, nt) + init_operator(file, class_name, mat_var, self.material_name) file.write("\tdef execute(self, context):\n") @@ -125,33 +124,33 @@ def create_material(): file.write(f"\t\tmat.use_nodes = True\n") create_material() - node_trees = {} + node_trees = set() def process_mat_node_group(node_tree, level): - ng_name = utils.clean_string(node_tree.name) - ng_label = node_tree.name + nt_var = clean_string(node_tree.name) + nt_name = node_tree.name if level == 2: #outermost node group - ng_name = utils.clean_string(self.material_name) - ng_label = self.material_name + nt_var = clean_string(self.material_name) + nt_name = self.material_name - outer, inner = utils.make_indents(level) + outer, inner = make_indents(level) #initialize node group - file.write(f"{outer}#initialize {ng_name} node group\n") - file.write(f"{outer}def {ng_name}_node_group():\n") + 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 - file.write(f"{inner}{ng_name} = mat.node_tree\n") + file.write(f"{inner}{nt_var} = mat.node_tree\n") else: - file.write((f"{inner}{ng_name}" + file.write((f"{inner}{nt_var}" f"= bpy.data.node_groups.new(" f"type = \"ShaderNodeTree\", " - f"name = \"{ng_label}\")\n")) + f"name = \"{nt_name}\")\n")) file.write("\n") #initialize nodes - file.write(f"{inner}#initialize {ng_name} nodes\n") + file.write(f"{inner}#initialize {nt_var} nodes\n") unnamed_idx = 0 for node in node_tree.nodes: @@ -161,9 +160,9 @@ def process_mat_node_group(node_tree, level): process_mat_node_group(node_nt, level + 1) node_trees.add(node_nt) - node_var, unnamed_idx = utils.create_node(node, file, inner, ng_name, unnamed_idx) + node_var, unnamed_idx = create_node(node, file, inner, nt_var, unnamed_idx) - utils.set_settings_defaults(node, node_settings, file, inner, node_var) + set_settings_defaults(node, node_settings, file, inner, node_var) if node.bl_idname == 'ShaderNodeGroup': if node.node_tree is not None: @@ -171,25 +170,25 @@ def process_mat_node_group(node_tree, level): f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'ShaderNodeValToRGB': - utils.color_ramp_settings(node, file, inner, node_var) + color_ramp_settings(node, file, inner, node_var) elif node.bl_idname in curve_nodes: - utils.curve_node_settings(node, file, inner, node_var) + curve_node_settings(node, file, inner, node_var) - utils.set_input_defaults(node, dont_set_defaults, file, inner, + set_input_defaults(node, dont_set_defaults, file, inner, node_var) - utils.init_links(node_tree, file, inner, ng_name) + init_links(node_tree, file, inner, nt_var) - file.write(f"\n{outer}{ng_name}_node_group()\n\n") + file.write(f"\n{outer}{nt_var}_node_group()\n\n") - process_mat_node_group(ng, 2) + process_mat_node_group(nt, 2) file.write("\t\treturn {'FINISHED'}\n\n") - utils.create_menu_func(file, class_name) - utils.create_register_func(file, class_name) - utils.create_unregister_func(file, class_name) - utils.create_main_func(file, class_name) + create_menu_func(file, class_name) + create_register_func(file, class_name) + create_unregister_func(file, class_name) + create_main_func(file) file.close() return {'FINISHED'} diff --git a/utils.py b/utils.py index 5c18856..b66b7bb 100644 --- a/utils.py +++ b/utils.py @@ -42,7 +42,7 @@ def str_to_py_str(string: str) -> str: """ return f"\"{string}\"" -def vec3_to_py_str(vec: mathutils.Vector) -> str: +def vec3_to_py_str(vec) -> str: """ Converts a 3D vector to a string usable by the add-on @@ -54,7 +54,7 @@ def vec3_to_py_str(vec: mathutils.Vector) -> str: """ return f"({vec[0]}, {vec[1]}, {vec[2]})" -def vec4_to_py_str(vec: mathutils.Vector) -> str: +def vec4_to_py_str(vec) -> str: """ Converts a 4D vector to a string usable by the add-on @@ -66,7 +66,7 @@ def vec4_to_py_str(vec: mathutils.Vector) -> str: """ return f"({vec[0]}, {vec[1]}, {vec[2]}, {vec[3]})" -def create_header(file: TextIO, node_tree: bpy.types.NodeTree): +def create_header(file: TextIO, node_tree): """ Sets up the bl_info and imports the Blender API @@ -122,8 +122,8 @@ def make_indents(level: int) -> Tuple[str, str]: inner = "\t"*(level + 1) return outer, inner -def create_node(node: bpy.types.Node, file: TextIO, inner: str, - node_tree_var: str, unnamed_idx: int = 0) -> Tuple[str, int]: +def create_node(node, file: TextIO, inner: str, node_tree_var: str, + unnamed_idx: int = 0) -> Tuple[str, int]: """ Initializes a new node with location, dimension, and label info @@ -160,8 +160,8 @@ def create_node(node: bpy.types.Node, file: TextIO, inner: str, return node_var, unnamed_idx -def set_settings_defaults(node: bpy.types.Node, settings: dict, file: TextIO, - inner: str, node_var: str): +def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, + node_var: str): """ Sets the defaults for any settings a node may have @@ -183,7 +183,7 @@ def set_settings_defaults(node: bpy.types.Node, settings: dict, file: TextIO, file.write((f"{inner}{node_var}.{setting} " f"= {attr}\n")) -def color_ramp_settings(node: bpy.types.Node, file: TextIO, inner: str, +def color_ramp_settings(node, file: TextIO, inner: str, node_var: str): """ node (bpy.types.Node): node object we're copying settings from @@ -214,8 +214,7 @@ def color_ramp_settings(node: bpy.types.Node, file: TextIO, inner: str, file.write((f"{inner}{node_var}_cre_{i}.color = " f"({r}, {g}, {b}, {a})\n\n")) -def curve_node_settings(node: bpy.types.Node, file: TextIO, inner: str, - node_var: str): +def curve_node_settings(node, file: TextIO, inner: str, node_var: str): """ Sets defaults for Float, Vector, and Color curves @@ -278,8 +277,8 @@ def curve_node_settings(node: bpy.types.Node, file: TextIO, inner: str, file.write(f"{inner}#update curve after changes\n") file.write(f"{mapping}.update()\n") -def set_input_defaults(node: bpy.typesNode, dont_set_defaults: dict, - file: TextIO, inner: str, node_var: str): +def set_input_defaults(node, dont_set_defaults: dict, file: TextIO, inner: str, + node_var: str): if node.bl_idname != 'NodeReroute': for i, input in enumerate(node.inputs): if input.bl_idname not in dont_set_defaults: @@ -297,8 +296,7 @@ def set_input_defaults(node: bpy.typesNode, dont_set_defaults: dict, f" = {default_val}\n")) file.write("\n") -def init_links(node_tree: bpy.types.NodeTree, file: TextIO, inner: str, - node_tree_var: str): +def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str): """ Create all the links between nodes @@ -379,13 +377,12 @@ def create_unregister_func(file: TextIO, name: str): file.write("\tbpy.types.VIEW3D_MT_objects.remove(menu_func)\n") file.write("\n") -def create_main_func(file: TextIO, name: str): +def create_main_func(file: TextIO): """ Creates the main function Parameters: file (TextIO): file we're generating the add-on into - name (str): name of the generated operator class """ file.write("if __name__ == \"__main__\":\n") file.write("\tregister()") \ No newline at end of file From e75c3839b3b4d4f53b89fd135722c02c1632d205 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 05:05:59 -0600 Subject: [PATCH 27/60] misc: removed pycache folder --- __pycache__/geo_nodes.cpython-310.pyc | Bin 15522 -> 0 bytes __pycache__/utils.cpython-310.pyc | Bin 11991 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 __pycache__/geo_nodes.cpython-310.pyc delete mode 100644 __pycache__/utils.cpython-310.pyc diff --git a/__pycache__/geo_nodes.cpython-310.pyc b/__pycache__/geo_nodes.cpython-310.pyc deleted file mode 100644 index 300890bab1f23ffefccf9d5e5503ddaa2fed466e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15522 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 Pv<{fU)GZxbfDir=EEW4c diff --git a/__pycache__/utils.cpython-310.pyc b/__pycache__/utils.cpython-310.pyc deleted file mode 100644 index df5fe1d1c26eb03315a92f3aa8eb3f00bffbb6b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11991 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 From 1c74bd9975c0d60f25fbc831f6bad8cbea0b8fd0 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 05:22:50 -0600 Subject: [PATCH 28/60] fix: temporarily removed frame nodes --- README.md | 1 + geo_nodes.py | 2 ++ materials.py | 2 ++ 3 files changed, 5 insertions(+) diff --git a/README.md b/README.md index d5a9bbc..5fb0b97 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ From here, you can install it like a regular add-on. as they won't exist in every blend file. I'm expecting to support some of these in the future. There are a few nodes that don't set their default values like other ones, though these should also soon be supported. +* NodeToPython does not currently support Frame nodes, as they tend to mess up links when added. ## Bug Reports and Suggestions diff --git a/geo_nodes.py b/geo_nodes.py index 26e2717..69b8fb2 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -298,6 +298,8 @@ def process_geo_nodes_group(node_tree, level): f"\'{socket.attribute_domain}\'\n")) file.write("\n") outputs_set = True + elif node.bl_idname == 'NodeFrame': + continue unnamed_idx = 0 #create node diff --git a/materials.py b/materials.py index 1206802..2b08bc8 100644 --- a/materials.py +++ b/materials.py @@ -159,6 +159,8 @@ def process_mat_node_group(node_tree, level): if node_nt is not None and node_nt not in node_trees: process_mat_node_group(node_nt, level + 1) node_trees.add(node_nt) + elif node.bl_idname == 'NodeFrame': + continue node_var, unnamed_idx = create_node(node, file, inner, nt_var, unnamed_idx) From 60908bd8e424ab7cc0783f791be75fd223c1463c Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 23:33:50 -0600 Subject: [PATCH 29/60] fix: node spacing and frames are now functional --- geo_nodes.py | 24 +++++++------- materials.py | 13 ++++---- utils.py | 91 ++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 92 insertions(+), 36 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 69b8fb2..4e61ec1 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -193,6 +193,7 @@ def process_geo_nodes_group(node_tree, level): #initialize nodes file.write(f"{inner}#initialize {node_tree_var} nodes\n") + node_vars = {} for node in node_tree.nodes: if node.bl_idname == 'GeometryNodeGroup': node_nt = node.node_tree @@ -298,18 +299,14 @@ def process_geo_nodes_group(node_tree, level): f"\'{socket.attribute_domain}\'\n")) file.write("\n") outputs_set = True - elif node.bl_idname == 'NodeFrame': - continue - unnamed_idx = 0 #create node - node_var, unnamed_idx = create_node(node, file, inner, - node_tree_var, - unnamed_idx) - - set_settings_defaults(node, geo_node_settings, file, - inner, node_var) - + node_var, node_vars = create_node(node, file, inner, + node_tree_var, node_vars) + set_settings_defaults(node, geo_node_settings, file, inner, + node_var) + hide_sockets(node, file, inner, node_var) + if node.bl_idname == 'GeometryNodeGroup': if node.node_tree is not None: file.write((f"{inner}{node_var}.node_tree = " @@ -321,9 +318,10 @@ def process_geo_nodes_group(node_tree, level): curve_node_settings(node, file, inner, node_var) set_input_defaults(node, dont_set_defaults, file, inner, - node_var) - - init_links(node_tree, file, inner, node_tree_var) + node_var) + set_parents(node_tree, file, inner, node_vars) + set_locations(node_tree, file, inner, node_vars) + init_links(node_tree, file, inner, node_tree_var, node_vars) #create node group file.write(f"\n{outer}{node_tree_var}_node_group()\n\n") diff --git a/materials.py b/materials.py index 2b08bc8..b52fdf1 100644 --- a/materials.py +++ b/materials.py @@ -152,19 +152,19 @@ def process_mat_node_group(node_tree, level): #initialize nodes file.write(f"{inner}#initialize {nt_var} nodes\n") - unnamed_idx = 0 + node_vars = {} for node in node_tree.nodes: if node.bl_idname == 'ShaderNodeGroup': node_nt = node.node_tree if node_nt is not None and node_nt not in node_trees: process_mat_node_group(node_nt, level + 1) node_trees.add(node_nt) - elif node.bl_idname == 'NodeFrame': - continue - node_var, unnamed_idx = create_node(node, file, inner, nt_var, unnamed_idx) + node_var, node_vars = create_node(node, file, inner, nt_var, + node_vars) set_settings_defaults(node, node_settings, file, inner, node_var) + hide_sockets(node, file, inner, node_var) if node.bl_idname == 'ShaderNodeGroup': if node.node_tree is not None: @@ -178,8 +178,9 @@ def process_mat_node_group(node_tree, level): set_input_defaults(node, dont_set_defaults, file, inner, node_var) - - init_links(node_tree, file, inner, nt_var) + set_parents(node_tree, file, inner, node_vars) + set_locations(node_tree, file, inner, node_vars) + init_links(node_tree, file, inner, nt_var, node_vars) file.write(f"\n{outer}{nt_var}_node_group()\n\n") diff --git a/utils.py b/utils.py index b66b7bb..9ebed86 100644 --- a/utils.py +++ b/utils.py @@ -123,7 +123,7 @@ def make_indents(level: int) -> Tuple[str, str]: return outer, inner def create_node(node, file: TextIO, inner: str, node_tree_var: str, - unnamed_idx: int = 0) -> Tuple[str, int]: + node_vars: dict) -> Tuple[str, dict]: """ Initializes a new node with location, dimension, and label info @@ -132,25 +132,29 @@ def create_node(node, file: TextIO, inner: str, node_tree_var: str, file (TextIO): file containing the generated add-on inner (str): indentation level for this logic node_tree_var (str): variable name for the node tree + node_vars (dict): dictionary containing (bpy.types.Node, str) key-value + pairs, where the key is the node and the value its corresponding + variable name in the addon Returns: node_var (str): variable name for the node - unnamed_idx (int): unnamed index. if a node doesn't have a name, this will be used to give it a variable name + node_vars (dict): the updated variable name dictionary """ file.write(f"{inner}#node {node.name}\n") node_var = clean_string(node.name) if node_var == "": - node_var = f"node_{unnamed_idx}" - unnamed_idx += 1 + i = 0 + while node_var in node_vars: + node_var = f"unnamed_node_{i}" + i += 1 + + node_vars[node] = node_var file.write((f"{inner}{node_var} " f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n")) - #location - file.write((f"{inner}{node_var}.location " - f"= ({node.location.x}, {node.location.y})\n")) #dimensions file.write((f"{inner}{node_var}.width, {node_var}.height " f"= {node.width}, {node.height}\n")) @@ -158,7 +162,7 @@ def create_node(node, file: TextIO, inner: str, node_tree_var: str, if node.label: file.write(f"{inner}{node_var}.label = \"{node.label}\"\n") - return node_var, unnamed_idx + return node_var, node_vars def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, node_var: str): @@ -183,9 +187,28 @@ def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, file.write((f"{inner}{node_var}.{setting} " f"= {attr}\n")) -def color_ramp_settings(node, file: TextIO, inner: str, - node_var: str): +def hide_sockets(node, file: TextIO, inner: str, node_var: str): + """ + Hide hidden sockets + + Parameters: + node (bpy.types.Node): node object we're copying socket settings from + file (TextIO): file we're generating the add-on into + inner (str): indentation string + node_var (str): name of the variable we're using for this node + """ + for i, socket in enumerate(node.inputs): + if socket.hide is True: + file.write(f"{inner}{node_var}.inputs[{i}].hide = True\n") + for i, socket in enumerate(node.outputs): + if socket.hide is True: + file.write(f"{inner}{node_var}.outputs[{i}].hide = True\n") + +def color_ramp_settings(node, file: TextIO, inner: str, node_var: str): """ + Replicate a color ramp node + + Parameters node (bpy.types.Node): node object we're copying settings from file (TextIO): file we're generating the add-on into inner (str): indentation @@ -296,7 +319,40 @@ def set_input_defaults(node, dont_set_defaults: dict, file: TextIO, inner: str, f" = {default_val}\n")) file.write("\n") -def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str): +def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): + """ + Sets parents for all nodes, mostly used to put nodes in frames + + Parameters: + node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from + file (TextIO): file for the generated add-on + inner (str): indentation string + node_vars (dict): dictionary for (node, variable) name pairs + """ + for node in node_tree.nodes: + if node is not None and node.parent is not None: + node_var = node_vars[node] + parent_var = node_vars[node.parent] + file.write(f"{inner}{node_var}.parent = {parent_var}\n") + +def set_locations(node_tree, file: TextIO, inner: str, node_vars: dict): + """ + Set locations for all nodes + + Parameters: + node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from + file (TextIO): file for the generated add-on + inner (str): indentation string + node_vars (dict): dictionary for (node, variable) name pairs + """ + + for node in node_tree.nodes: + node_var = node_vars[node] + file.write((f"{inner}{node_var}.location " + f"= ({node.location.x}, {node.location.y})\n")) + +def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, + node_vars: dict): """ Create all the links between nodes @@ -305,12 +361,13 @@ def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str): file (TextIO): file we're generating the add-on into inner (str): indentation node_tree_var (str): variable name we're using for the copied node tree + node_vars (dict): dictionary containing node to variable name pairs """ if node_tree.links: file.write(f"{inner}#initialize {node_tree_var} links\n") for link in node_tree.links: - input_node = clean_string(link.from_node.name) + in_node_var = node_vars[link.from_node] input_socket = link.from_socket """ @@ -324,7 +381,7 @@ def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str): input_idx = i break - output_node = clean_string(link.to_node.name) + out_node_var = node_vars[link.to_node] output_socket = link.to_socket for i, item in enumerate(link.to_node.inputs.items()): @@ -332,11 +389,11 @@ def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str): output_idx = i break - file.write((f"{inner}#{input_node}.{input_socket.name} " - f"-> {output_node}.{output_socket.name}\n")) - file.write((f"{inner}{node_tree_var}.links.new({input_node}" + file.write((f"{inner}#{in_node_var}.{input_socket.name} " + f"-> {out_node_var}.{output_socket.name}\n")) + file.write((f"{inner}{node_tree_var}.links.new({in_node_var}" f".outputs[{input_idx}], " - f"{output_node}.inputs[{output_idx}])\n")) + f"{out_node_var}.inputs[{output_idx}])\n")) def create_menu_func(file: TextIO, name: str): """ From 2a986f71cef0d039f5722aceca4ed6e78778ce13 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 23:36:19 -0600 Subject: [PATCH 30/60] docs: updated README to reflect support for frame nodes --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 5fb0b97..d5a9bbc 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ From here, you can install it like a regular add-on. as they won't exist in every blend file. I'm expecting to support some of these in the future. There are a few nodes that don't set their default values like other ones, though these should also soon be supported. -* NodeToPython does not currently support Frame nodes, as they tend to mess up links when added. ## Bug Reports and Suggestions From 9d14730d05cb2199a42889d5030ffb87e86b905f Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 13 Jan 2023 23:56:45 -0600 Subject: [PATCH 31/60] feat: node colors now replicated --- geo_nodes.py | 3 +++ materials.py | 2 ++ utils.py | 25 +++++++++++++++++++++---- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 4e61ec1..d584bd6 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -319,8 +319,11 @@ def process_geo_nodes_group(node_tree, level): set_input_defaults(node, dont_set_defaults, file, inner, node_var) + set_parents(node_tree, file, inner, node_vars) set_locations(node_tree, file, inner, node_vars) + set_dimensions(node_tree, file, inner, node_vars) + init_links(node_tree, file, inner, node_tree_var, node_vars) #create node group diff --git a/materials.py b/materials.py index b52fdf1..04ae8d9 100644 --- a/materials.py +++ b/materials.py @@ -180,6 +180,8 @@ def process_mat_node_group(node_tree, level): node_var) set_parents(node_tree, file, inner, node_vars) set_locations(node_tree, file, inner, node_vars) + set_dimensions(node_tree, file, inner, node_vars) + init_links(node_tree, file, inner, nt_var, node_vars) file.write(f"\n{outer}{nt_var}_node_group()\n\n") diff --git a/utils.py b/utils.py index 9ebed86..28e3813 100644 --- a/utils.py +++ b/utils.py @@ -154,14 +154,14 @@ def create_node(node, file: TextIO, inner: str, node_tree_var: str, file.write((f"{inner}{node_var} " f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n")) - - #dimensions - file.write((f"{inner}{node_var}.width, {node_var}.height " - f"= {node.width}, {node.height}\n")) #label if node.label: file.write(f"{inner}{node_var}.label = \"{node.label}\"\n") + #color + if node.use_custom_color: + file.write(f"{inner}{node_var}.use_custom_color = True\n") + file.write(f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") return node_var, node_vars def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, @@ -351,6 +351,23 @@ def set_locations(node_tree, file: TextIO, inner: str, node_vars: dict): file.write((f"{inner}{node_var}.location " f"= ({node.location.x}, {node.location.y})\n")) +def set_dimensions(node_tree, file: TextIO, inner: str, node_vars: dict): + """ + Set dimensions for all nodes + + Parameters: + node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from + file (TextIO): file for the generated add-on + inner (str): indentation string + node_vars (dict): dictionary for (node, variable) name pairs + """ + + for node in node_tree.nodes: + node_var = node_vars[node] + file.write((f"{inner}{node_var}.width, {node_var}.height " + f"= {node.width}, {node.height}\n")) + + def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, node_vars: dict): """ From a8ff077dde1ccfe412368b28334fa9efa9395704 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 14 Jan 2023 00:12:33 -0600 Subject: [PATCH 32/60] feat: nodes can now be muted --- utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/utils.py b/utils.py index 28e3813..1e5fb75 100644 --- a/utils.py +++ b/utils.py @@ -162,6 +162,11 @@ def create_node(node, file: TextIO, inner: str, node_tree_var: str, if node.use_custom_color: file.write(f"{inner}{node_var}.use_custom_color = True\n") file.write(f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") + + #mute + if node.mute: + file.write(f"{inner}{node_var}.mute = True\n") + return node_var, node_vars def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, From 1dda44ecb1a550fa8b9aa935f327db7341d5e03a Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 16 Jan 2023 19:14:15 -0600 Subject: [PATCH 33/60] feat: image saving and zipping --- geo_nodes.py | 15 ++++---- materials.py | 9 ++--- utils.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 106 insertions(+), 16 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index d584bd6..276bb13 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -13,7 +13,7 @@ #node input sockets that are messy to set default values for dont_set_defaults = {'NodeSocketCollection', 'NodeSocketGeometry', - 'NodeSocketImage', + #'NodeSocketImage', 'NodeSocketMaterial', 'NodeSocketObject', 'NodeSocketTexture', @@ -153,18 +153,18 @@ def execute(self, context): class_name = nt.name.replace(" ", "").replace('.', "") #find base directory to save new addon - dir = bpy.path.abspath("//") - if not dir or dir == "": + 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 - addon_dir = os.path.join(dir, "addons") + addon_dir = os.path.join(base_dir, "addons", nt_var) if not os.path.exists(addon_dir): os.mkdir(addon_dir) - file = open(f"{addon_dir}/{nt_var}_addon.py", "w") + file = open(f"{addon_dir}/__init__.py", "w") create_header(file, nt) init_operator(file, class_name, nt_var, nt.name) @@ -318,7 +318,7 @@ def process_geo_nodes_group(node_tree, level): curve_node_settings(node, file, inner, node_var) set_input_defaults(node, dont_set_defaults, file, inner, - node_var) + node_var, addon_dir) set_parents(node_tree, file, inner, node_vars) set_locations(node_tree, file, inner, node_vars) @@ -339,6 +339,9 @@ def process_geo_nodes_group(node_tree, level): create_main_func(file) file.close() + + zip_addon(addon_dir) + return {'FINISHED'} class SelectGeoNodesMenu(bpy.types.Menu): diff --git a/materials.py b/materials.py index 04ae8d9..f72127a 100644 --- a/materials.py +++ b/materials.py @@ -6,7 +6,7 @@ #node input sockets that are messy to set default values for dont_set_defaults = {'NodeSocketCollection', 'NodeSocketGeometry', - 'NodeSocketImage', + #'NodeSocketImage', 'NodeSocketMaterial', 'NodeSocketObject', 'NodeSocketShader', @@ -108,10 +108,10 @@ def execute(self, context): ("NodeToPython: Save your blender file before using " "NodeToPython!")) return {'CANCELLED'} - addon_dir = os.path.join(dir, "addons") + addon_dir = os.path.join(dir, "addons", mat_var) if not os.path.exists(addon_dir): os.mkdir(addon_dir) - file = open(f"{addon_dir}/{mat_var}_addon.py", "w") + file = open(f"{addon_dir}/__init__.py", "w") create_header(file, nt) init_operator(file, class_name, mat_var, self.material_name) @@ -177,7 +177,7 @@ def process_mat_node_group(node_tree, level): curve_node_settings(node, file, inner, node_var) set_input_defaults(node, dont_set_defaults, file, inner, - node_var) + node_var, addon_dir) set_parents(node_tree, file, inner, node_vars) set_locations(node_tree, file, inner, node_vars) set_dimensions(node_tree, file, inner, node_vars) @@ -196,6 +196,7 @@ def process_mat_node_group(node_tree, level): create_main_func(file) file.close() + zip_addon(addon_dir) return {'FINISHED'} class SelectMaterialMenu(bpy.types.Menu): diff --git a/utils.py b/utils.py index 1e5fb75..9315c88 100644 --- a/utils.py +++ b/utils.py @@ -1,9 +1,13 @@ import bpy import mathutils +import os import re +import shutil from typing import TextIO, Tuple +image_dir_name = "imgs" + def clean_string(string: str) -> str: """ Cleans up a string for use as a variable or file name @@ -66,6 +70,20 @@ def vec4_to_py_str(vec) -> str: """ return f"({vec[0]}, {vec[1]}, {vec[2]}, {vec[3]})" +def img_to_py_str(img) -> str: + """ + Converts a Blender image into its string + + Paramters: + img (bpy.types.Image): a Blender image + + Returns: + (str): string version + """ + name = img.name + format = img.file_format.lower() + return f"{name}.{format}" + def create_header(file: TextIO, node_tree): """ Sets up the bl_info and imports the Blender API @@ -85,6 +103,7 @@ def create_header(file: TextIO, node_tree): file.write("}\n") file.write("\n") file.write("import bpy\n") + file.write("import os") file.write("\n") def init_operator(file: TextIO, name: str, idname: str, label: str): @@ -229,7 +248,6 @@ def color_ramp_settings(node, file: TextIO, inner: str, node_var: str): file.write((f"{inner}{node_var}.color_ramp.interpolation " f"= '{color_ramp.interpolation}'\n")) file.write("\n") - #key points for i, element in enumerate(color_ramp.elements): file.write((f"{inner}{node_var}_cre_{i} = " @@ -305,22 +323,41 @@ def curve_node_settings(node, file: TextIO, inner: str, node_var: str): file.write(f"{inner}#update curve after changes\n") file.write(f"{mapping}.update()\n") -def set_input_defaults(node, dont_set_defaults: dict, file: TextIO, inner: str, - node_var: str): +def set_input_defaults(node, dont_set_defaults: set, file: TextIO, inner: str, + node_var: str, addon_dir: str): + """ + Sets defaults for input sockets + + Parameters: + node (bpy.types.Node): node we're setting inputs for + dont_set_defaults (set): set of sockets we shouldn't attempt to set + default values for + file (TextIO): file we're generating the add-on into + inner (str): indentation + node_var (str): variable name we're using for the copied node + addon_dir (str): directory of the add-on, for if we need to save other + objects for the add-on + """ if node.bl_idname != 'NodeReroute': for i, input in enumerate(node.inputs): if input.bl_idname not in dont_set_defaults: + socket_var = f"{node_var}.inputs[{i}]" if input.bl_idname == 'NodeSocketColor': default_val = vec4_to_py_str(input.default_value) elif "Vector" in input.bl_idname: default_val = vec3_to_py_str(input.default_value) elif input.bl_idname == 'NodeSocketString': default_val = str_to_py_str(input.default_value) + elif input.bl_idname == 'NodeSocketImage': + img = input.default_value + save_image(img, addon_dir) + load_image(img, file, inner, f"{socket_var}.default_value") + default_val = None else: default_val = input.default_value if default_val is not None: file.write(f"{inner}#{input.identifier}\n") - file.write((f"{inner}{node_var}.inputs[{i}].default_value" + file.write((f"{inner}{socket_var}.default_value" f" = {default_val}\n")) file.write("\n") @@ -372,7 +409,6 @@ def set_dimensions(node_tree, file: TextIO, inner: str, node_vars: dict): file.write((f"{inner}{node_var}.width, {node_var}.height " f"= {node.width}, {node.height}\n")) - def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, node_vars: dict): """ @@ -464,4 +500,54 @@ def create_main_func(file: TextIO): file (TextIO): file we're generating the add-on into """ file.write("if __name__ == \"__main__\":\n") - file.write("\tregister()") \ No newline at end of file + file.write("\tregister()") + +def save_image(img, addon_dir: str): + """ + Saves an image to an image directory of the add-on + + Parameters: + img (bpy.types.Image): image to be saved + addon_dir (str): directory of the addon + """ + #create image dir if one doesn't exist + img_dir = os.path.join(addon_dir, image_dir_name) + if not os.path.exists(img_dir): + os.mkdir(img_dir) + + #save the image + img_str = img_to_py_str(img) + img_path = f"{img_dir}/{img_str}" + if not os.path.exists(img_path): + img.save_render(img_path) + +def load_image(img, file: TextIO, inner: str, img_var: str): + """ + Loads an image from the add-on into a blend file and assigns it + + Parameters: + img (bpy.types.Image): Blender image from the original node group + file (TextIO): file for the generated add-on + inner (str): indentation string + img_var (str): variable name to be used for the image + """ + img_str = img_to_py_str(img) + + file.write(f"{inner}#load image {img_str}\n") + file.write((f"{inner}base_dir = " + f"os.path.dirname(os.path.abspath(__file__))\n")) + file.write((f"{inner}image_path = " + f"os.path.join(base_dir, \"{image_dir_name}\", " + f"\"{img_str}\")\n")) + file.write((f"{inner}{img_var} = " + f"bpy.data.images.load(image_path, check_existing = True)\n")) + +def zip_addon(addon_dir: str): + """ + Zips up the addon and removes the directory + + Parameters: + addon_dir (str): path to the directory of the addon + """ + shutil.make_archive(addon_dir, "zip", addon_dir) + shutil.rmtree(addon_dir) \ No newline at end of file From f6632b253fc314aad3063283af0bcae445ee3b60 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 16 Jan 2023 19:31:03 -0600 Subject: [PATCH 34/60] fix: zip files now contain folder as their top level --- geo_nodes.py | 2 +- materials.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 276bb13..7a805a1 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -161,7 +161,7 @@ def execute(self, context): return {'CANCELLED'} #save in /addons/ subdirectory - addon_dir = os.path.join(base_dir, "addons", nt_var) + addon_dir = os.path.join(base_dir, "addons", nt_var, nt_var) if not os.path.exists(addon_dir): os.mkdir(addon_dir) file = open(f"{addon_dir}/__init__.py", "w") diff --git a/materials.py b/materials.py index f72127a..495d345 100644 --- a/materials.py +++ b/materials.py @@ -108,7 +108,7 @@ def execute(self, context): ("NodeToPython: Save your blender file before using " "NodeToPython!")) return {'CANCELLED'} - addon_dir = os.path.join(dir, "addons", mat_var) + addon_dir = os.path.join(dir, "addons", mat_var, mat_var) if not os.path.exists(addon_dir): os.mkdir(addon_dir) file = open(f"{addon_dir}/__init__.py", "w") From f804010965898bed15f57264b53898d83ec02563 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:01:41 -0600 Subject: [PATCH 35/60] fix: image filenames extensions no longer doubled --- geo_nodes.py | 7 ++++--- materials.py | 10 +++++++--- utils.py | 10 +++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 7a805a1..2f6951f 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -161,9 +161,10 @@ def execute(self, context): return {'CANCELLED'} #save in /addons/ subdirectory - addon_dir = os.path.join(base_dir, "addons", nt_var, nt_var) + 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.mkdir(addon_dir) + os.makedirs(addon_dir) file = open(f"{addon_dir}/__init__.py", "w") create_header(file, nt) @@ -340,7 +341,7 @@ def process_geo_nodes_group(node_tree, level): file.close() - zip_addon(addon_dir) + zip_addon(zip_dir) return {'FINISHED'} diff --git a/materials.py b/materials.py index 495d345..1c410fd 100644 --- a/materials.py +++ b/materials.py @@ -108,9 +108,10 @@ def execute(self, context): ("NodeToPython: Save your blender file before using " "NodeToPython!")) return {'CANCELLED'} - addon_dir = os.path.join(dir, "addons", mat_var, mat_var) + 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.mkdir(addon_dir) + os.makedirs(addon_dir) file = open(f"{addon_dir}/__init__.py", "w") create_header(file, nt) @@ -171,6 +172,9 @@ def process_mat_node_group(node_tree, level): file.write((f"{inner}{node_var}.node_tree = " f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) + elif node.bl_idname == 'ShaderNodeTexImage': + save_image(node.image, addon_dir) + load_image(node.image, file, inner, f"{node_var}.image") elif node.bl_idname == 'ShaderNodeValToRGB': color_ramp_settings(node, file, inner, node_var) elif node.bl_idname in curve_nodes: @@ -196,7 +200,7 @@ def process_mat_node_group(node_tree, level): create_main_func(file) file.close() - zip_addon(addon_dir) + zip_addon(zip_dir) return {'FINISHED'} class SelectMaterialMenu(bpy.types.Menu): diff --git a/utils.py b/utils.py index 9315c88..b651d6a 100644 --- a/utils.py +++ b/utils.py @@ -80,7 +80,7 @@ def img_to_py_str(img) -> str: Returns: (str): string version """ - name = img.name + name = img.name.split('.', 1)[0] format = img.file_format.lower() return f"{name}.{format}" @@ -542,12 +542,12 @@ def load_image(img, file: TextIO, inner: str, img_var: str): file.write((f"{inner}{img_var} = " f"bpy.data.images.load(image_path, check_existing = True)\n")) -def zip_addon(addon_dir: str): +def zip_addon(zip_dir: str): """ Zips up the addon and removes the directory Parameters: - addon_dir (str): path to the directory of the addon + zip_dir (str): path to the top-level addon directory """ - shutil.make_archive(addon_dir, "zip", addon_dir) - shutil.rmtree(addon_dir) \ No newline at end of file + shutil.make_archive(zip_dir, "zip", zip_dir) + shutil.rmtree(zip_dir) \ No newline at end of file From 13e391c54be44df3eae0ed9bf849fece1700dd29 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:27:05 -0600 Subject: [PATCH 36/60] fix: issue with base material node tree automatically adding two nodes --- materials.py | 3 +++ utils.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/materials.py b/materials.py index 1c410fd..fb3d646 100644 --- a/materials.py +++ b/materials.py @@ -143,6 +143,9 @@ def process_mat_node_group(node_tree, level): if level == 2: #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") + file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") else: file.write((f"{inner}{nt_var}" f"= bpy.data.node_groups.new(" diff --git a/utils.py b/utils.py index b651d6a..542f2f0 100644 --- a/utils.py +++ b/utils.py @@ -340,7 +340,7 @@ def set_input_defaults(node, dont_set_defaults: set, file: TextIO, inner: str, """ if node.bl_idname != 'NodeReroute': for i, input in enumerate(node.inputs): - if input.bl_idname not in dont_set_defaults: + if input.bl_idname not in dont_set_defaults and not input.is_linked: socket_var = f"{node_var}.inputs[{i}]" if input.bl_idname == 'NodeSocketColor': default_val = vec4_to_py_str(input.default_value) From a9fea3e13cda19d3e05d4c788ee707801379987a Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:39:47 -0600 Subject: [PATCH 37/60] fix: material add-on names now use the name of the material instead of the shader node tree --- geo_nodes.py | 2 +- materials.py | 2 +- utils.py | 52 ++++++++++++++++++++++++++++------------------------ 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 2f6951f..cdace52 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -167,7 +167,7 @@ def execute(self, context): os.makedirs(addon_dir) file = open(f"{addon_dir}/__init__.py", "w") - create_header(file, nt) + create_header(file, nt.name) init_operator(file, class_name, nt_var, nt.name) file.write("\tdef execute(self, context):\n") diff --git a/materials.py b/materials.py index fb3d646..08d36bf 100644 --- a/materials.py +++ b/materials.py @@ -114,7 +114,7 @@ def execute(self, context): os.makedirs(addon_dir) file = open(f"{addon_dir}/__init__.py", "w") - create_header(file, nt) + create_header(file, self.material_name) init_operator(file, class_name, mat_var, self.material_name) file.write("\tdef execute(self, context):\n") diff --git a/utils.py b/utils.py index 542f2f0..d04239b 100644 --- a/utils.py +++ b/utils.py @@ -84,17 +84,17 @@ def img_to_py_str(img) -> str: format = img.file_format.lower() return f"{name}.{format}" -def create_header(file: TextIO, node_tree): +def create_header(file: TextIO, name: str): """ Sets up the bl_info and imports the Blender API Parameters: file (TextIO): the file for the generated add-on - node_tree (bpy.types.NodeTree): the node group object we're converting into an add-on + name (str): name of the add-on """ file.write("bl_info = {\n") - file.write(f"\t\"name\" : \"{node_tree.name}\",\n") + file.write(f"\t\"name\" : \"{name}\",\n") file.write("\t\"author\" : \"Node To Python\",\n") file.write("\t\"version\" : (1, 0, 0),\n") file.write(f"\t\"blender\" : {bpy.app.version},\n") @@ -338,27 +338,30 @@ def set_input_defaults(node, dont_set_defaults: set, file: TextIO, inner: str, addon_dir (str): directory of the add-on, for if we need to save other objects for the add-on """ - if node.bl_idname != 'NodeReroute': - for i, input in enumerate(node.inputs): - if input.bl_idname not in dont_set_defaults and not input.is_linked: - socket_var = f"{node_var}.inputs[{i}]" - if input.bl_idname == 'NodeSocketColor': - default_val = vec4_to_py_str(input.default_value) - elif "Vector" in input.bl_idname: - default_val = vec3_to_py_str(input.default_value) - elif input.bl_idname == 'NodeSocketString': - default_val = str_to_py_str(input.default_value) - elif input.bl_idname == 'NodeSocketImage': - img = input.default_value - save_image(img, addon_dir) - load_image(img, file, inner, f"{socket_var}.default_value") - default_val = None - else: - default_val = input.default_value - if default_val is not None: - file.write(f"{inner}#{input.identifier}\n") - file.write((f"{inner}{socket_var}.default_value" - f" = {default_val}\n")) + if node.bl_idname == 'NodeReroute': + return + + for i, input in enumerate(node.inputs): + if input.bl_idname not in dont_set_defaults and not input.is_linked: + socket_var = f"{node_var}.inputs[{i}]" + if input.bl_idname == 'NodeSocketColor': + default_val = vec4_to_py_str(input.default_value) + elif "Vector" in input.bl_idname: + default_val = vec3_to_py_str(input.default_value) + elif input.bl_idname == 'NodeSocketString': + default_val = str_to_py_str(input.default_value) + elif input.bl_idname == 'NodeSocketImage': + print("Input is linked: ", input.is_linked) + img = input.default_value + save_image(img, addon_dir) + load_image(img, file, inner, f"{socket_var}.default_value") + default_val = None + else: + default_val = input.default_value + if default_val is not None: + file.write(f"{inner}#{input.identifier}\n") + file.write((f"{inner}{socket_var}.default_value" + f" = {default_val}\n")) file.write("\n") def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): @@ -518,6 +521,7 @@ def save_image(img, addon_dir: str): #save the image img_str = img_to_py_str(img) img_path = f"{img_dir}/{img_str}" + print("Image Path: ", img_path) if not os.path.exists(img_path): img.save_render(img_path) From b35866cdf1d24f2b2a01ee85b9ec013bd00eac55 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:56:36 -0600 Subject: [PATCH 38/60] fix: issue with non-existent images --- geo_nodes.py | 1 - materials.py | 1 - utils.py | 13 +++++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index cdace52..9e75917 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -13,7 +13,6 @@ #node input sockets that are messy to set default values for dont_set_defaults = {'NodeSocketCollection', 'NodeSocketGeometry', - #'NodeSocketImage', 'NodeSocketMaterial', 'NodeSocketObject', 'NodeSocketTexture', diff --git a/materials.py b/materials.py index 08d36bf..90c28b5 100644 --- a/materials.py +++ b/materials.py @@ -6,7 +6,6 @@ #node input sockets that are messy to set default values for dont_set_defaults = {'NodeSocketCollection', 'NodeSocketGeometry', - #'NodeSocketImage', 'NodeSocketMaterial', 'NodeSocketObject', 'NodeSocketShader', diff --git a/utils.py b/utils.py index d04239b..a0035eb 100644 --- a/utils.py +++ b/utils.py @@ -353,8 +353,9 @@ def set_input_defaults(node, dont_set_defaults: set, file: TextIO, inner: str, elif input.bl_idname == 'NodeSocketImage': print("Input is linked: ", input.is_linked) img = input.default_value - save_image(img, addon_dir) - load_image(img, file, inner, f"{socket_var}.default_value") + if img is not None: + save_image(img, addon_dir) + load_image(img, file, inner, f"{socket_var}.default_value") default_val = None else: default_val = input.default_value @@ -513,6 +514,10 @@ def save_image(img, addon_dir: str): img (bpy.types.Image): image to be saved addon_dir (str): directory of the addon """ + + if img is None: + return + #create image dir if one doesn't exist img_dir = os.path.join(addon_dir, image_dir_name) if not os.path.exists(img_dir): @@ -535,6 +540,10 @@ def load_image(img, file: TextIO, inner: str, img_var: str): inner (str): indentation string img_var (str): variable name to be used for the image """ + + if img is None: + return + img_str = img_to_py_str(img) file.write(f"{inner}#load image {img_str}\n") From a960ae0d1c3b4c144363cb3ebc42feb0dce060ab Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Wed, 18 Jan 2023 11:50:42 -0600 Subject: [PATCH 39/60] docs: update README --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d5a9bbc..eb4f3a0 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,14 @@ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/releases) [![GitHub](https://img.shields.io/github/license/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/blob/main/LICENSE) ![](https://visitor-badge.laobi.icu/badge?page_id=BrendanParmer.NodeToPython) ## About -A Blender add-on to create add-ons! This script will take your Geometry Nodes or Materials and convert them into legible Python add-ons! +A Blender add-on to create add-ons! This addo-on will take your Geometry Nodes or Materials and convert them into legible Python add-ons! It automatically handles node layout, default values, sub-node groups, naming, and more! -I think Blender's node-based editors are powerful, yet accessible tools, and I wanted to make scripting them for add-on creators. Combining Python with node based setups allows you to do things that would otherwise be tedious or impossible, such as +I think Blender's node-based editors are powerful, yet accessible tools, and I wanted to make scripting them easier for add-on creators. Combining Python with node based setups allows you to do things that would otherwise be tedious or impossible, such as * `for` loops -* different node trees for different versions or settings -* interfacing with other parts of the software. +* creating different node trees for different versions or settings +* interfacing with other parts of the software or properties of an object NodeToPython recreates the node networks for you, so you can focus on the good stuff. @@ -45,7 +45,6 @@ From here, you can install it like a regular add-on. ## Potential Issues * As of version 2.0.0, the add-on will not set default values for * Collections - * Images * Materials * Objects * Textures From b6f5ba871c906c32f7db52aac916c98f61c21c3d Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 20 Jan 2023 00:04:45 -0600 Subject: [PATCH 40/60] feat: copy image source, colorspace, and alpha mode --- geo_nodes.py | 2 +- materials.py | 2 +- utils.py | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 9e75917..8f09190 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -340,7 +340,7 @@ def process_geo_nodes_group(node_tree, level): file.close() - zip_addon(zip_dir) + #zip_addon(zip_dir) return {'FINISHED'} diff --git a/materials.py b/materials.py index 90c28b5..09b5893 100644 --- a/materials.py +++ b/materials.py @@ -202,7 +202,7 @@ def process_mat_node_group(node_tree, level): create_main_func(file) file.close() - zip_addon(zip_dir) + #zip_addon(zip_dir) return {'FINISHED'} class SelectMaterialMenu(bpy.types.Menu): diff --git a/utils.py b/utils.py index a0035eb..ff9c146 100644 --- a/utils.py +++ b/utils.py @@ -555,6 +555,17 @@ def load_image(img, file: TextIO, inner: str, img_var: str): file.write((f"{inner}{img_var} = " f"bpy.data.images.load(image_path, check_existing = True)\n")) + #copy image settings + #source + file.write(f"{inner}{img_var}.source = \'{img.source}\'\n") + + #color space settings + file.write((f"{inner}{img_var}.colorspace_settings.name = " + f"\'{img.colorspace_settings.name}\'\n")) + + #alpha mode + file.write(f"{inner}{img_var}.alpha_mode = \'{img.alpha_mode}\'\n") + def zip_addon(zip_dir: str): """ Zips up the addon and removes the directory From 90870cf975068f78dab7faac073fa5a4f585d9f0 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 20 Jan 2023 00:25:40 -0600 Subject: [PATCH 41/60] feat: image user copying --- utils.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/utils.py b/utils.py index ff9c146..bbf358d 100644 --- a/utils.py +++ b/utils.py @@ -566,6 +566,30 @@ def load_image(img, file: TextIO, inner: str, img_var: str): #alpha mode file.write(f"{inner}{img_var}.alpha_mode = \'{img.alpha_mode}\'\n") +def image_user_settings(node, file: TextIO, inner: str, node_var: str): + """ + Replicate the image user + + Parameters + node (bpy.types.Node): node object we're copying settings from + file (TextIO): file we're generating the add-on into + inner (str): indentation + node_var (str): name of the variable we're using for the color ramp + """ + + if not hasattr(node, "image_user"): + raise ValueError("Node must have attribute \"image_user\"") + + img_usr = node.image_user + img_usr_var = f"{node_var}.image_user" + + img_usr_attrs = ["frame_current", "frame_duration", "frame_offset", + "frame_start", "tile", "use_auto_refresh", "use_cyclic"] + + for img_usr_attr in img_usr_attrs: + file.write((f"{inner}{img_usr_var}.{img_usr_attr} = " + f"{getattr(img_usr, img_usr_attr)}\n")) + def zip_addon(zip_dir: str): """ Zips up the addon and removes the directory From dcc5f5c9a9ce5300fa44aa2918b4851b487c77eb Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 20 Jan 2023 00:38:43 -0600 Subject: [PATCH 42/60] feat: added environment texture and restricted image copying to single images --- materials.py | 13 +++++++++---- utils.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/materials.py b/materials.py index 09b5893..2fc56d7 100644 --- a/materials.py +++ b/materials.py @@ -79,7 +79,10 @@ curve_nodes = {'ShaderNodeFloatCurve', 'ShaderNodeVectorCurve', - 'ShaderNodeRGBCurve'} + 'ShaderNodeRGBCurve'} + +image_nodes = {'ShaderNodeTexEnvironment', + 'ShaderNodeTexImage'} class MaterialToPython(bpy.types.Operator): bl_idname = "node.material_to_python" @@ -174,9 +177,11 @@ def process_mat_node_group(node_tree, level): file.write((f"{inner}{node_var}.node_tree = " f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) - elif node.bl_idname == 'ShaderNodeTexImage': - save_image(node.image, addon_dir) - load_image(node.image, file, inner, f"{node_var}.image") + elif node.bl_idname in image_nodes: + img = node.image + if img.source in {'FILE', 'GENERATED', 'TILED'}: + save_image(img, addon_dir) + load_image(img, file, inner, f"{node_var}.image") elif node.bl_idname == 'ShaderNodeValToRGB': color_ramp_settings(node, file, inner, node_var) elif node.bl_idname in curve_nodes: diff --git a/utils.py b/utils.py index bbf358d..cfa4255 100644 --- a/utils.py +++ b/utils.py @@ -568,7 +568,7 @@ def load_image(img, file: TextIO, inner: str, img_var: str): def image_user_settings(node, file: TextIO, inner: str, node_var: str): """ - Replicate the image user + Replicate the image user of an image node Parameters node (bpy.types.Node): node object we're copying settings from From 1e54f0cf77cfb1715943a19f838e7c46b46167f3 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 20 Jan 2023 00:50:53 -0600 Subject: [PATCH 43/60] fix: material class name no longer derives from node tree name --- materials.py | 3 ++- utils.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/materials.py b/materials.py index 2fc56d7..8f99f4c 100644 --- a/materials.py +++ b/materials.py @@ -102,7 +102,7 @@ def execute(self, context): #set up names to use in generated addon mat_var = clean_string(self.material_name) - class_name = nt.name.replace(" ", "") + class_name = clean_string(self.material_name, lower=False) dir = bpy.path.abspath("//") if not dir or dir == "": @@ -182,6 +182,7 @@ def process_mat_node_group(node_tree, level): if 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: diff --git a/utils.py b/utils.py index cfa4255..05e5c13 100644 --- a/utils.py +++ b/utils.py @@ -8,7 +8,7 @@ image_dir_name = "imgs" -def clean_string(string: str) -> str: +def clean_string(string: str, lower: bool = True) -> str: """ Cleans up a string for use as a variable or file name @@ -19,7 +19,9 @@ def clean_string(string: str) -> str: clean_str: The input string with nasty characters converted to underscores """ - clean_str = re.sub(r"[^a-zA-Z0-9_]", '_', string.lower()) + if lower: + string = string.lower() + clean_str = re.sub(r"[^a-zA-Z0-9_]", '_', string) return clean_str def enum_to_py_str(enum: str) -> str: @@ -556,6 +558,8 @@ def load_image(img, file: TextIO, inner: str, img_var: str): f"bpy.data.images.load(image_path, check_existing = True)\n")) #copy image settings + file.write(f"{inner}#set image settings\n") + #source file.write(f"{inner}{img_var}.source = \'{img.source}\'\n") From ae6e36b18aec4ec44b50d1bc7d6c267f71fc7731 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 20 Jan 2023 00:57:40 -0600 Subject: [PATCH 44/60] feat: zips addon again --- geo_nodes.py | 2 +- materials.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 8f09190..9e75917 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -340,7 +340,7 @@ def process_geo_nodes_group(node_tree, level): file.close() - #zip_addon(zip_dir) + zip_addon(zip_dir) return {'FINISHED'} diff --git a/materials.py b/materials.py index 8f99f4c..17d74d1 100644 --- a/materials.py +++ b/materials.py @@ -208,7 +208,7 @@ def process_mat_node_group(node_tree, level): create_main_func(file) file.close() - #zip_addon(zip_dir) + zip_addon(zip_dir) return {'FINISHED'} class SelectMaterialMenu(bpy.types.Menu): From 89ee0bb4d8082ac7e7954efe3af58469b4d01a35 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 21 Jan 2023 14:17:24 -0600 Subject: [PATCH 45/60] feat: set defaults for some attributes if they exist in a blend file --- materials.py | 6 +++--- utils.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/materials.py b/materials.py index 17d74d1..4fa843a 100644 --- a/materials.py +++ b/materials.py @@ -4,10 +4,10 @@ from .utils import * #node input sockets that are messy to set default values for -dont_set_defaults = {'NodeSocketCollection', +dont_set_defaults = {#'NodeSocketCollection', 'NodeSocketGeometry', - 'NodeSocketMaterial', - 'NodeSocketObject', + #'NodeSocketMaterial', + #'NodeSocketObject', 'NodeSocketShader', 'NodeSocketTexture', 'NodeSocketVirtual'} diff --git a/utils.py b/utils.py index 05e5c13..5f41944 100644 --- a/utils.py +++ b/utils.py @@ -346,12 +346,20 @@ def set_input_defaults(node, dont_set_defaults: set, file: TextIO, inner: str, for i, input in enumerate(node.inputs): if input.bl_idname not in dont_set_defaults and not input.is_linked: socket_var = f"{node_var}.inputs[{i}]" + + #colors if input.bl_idname == 'NodeSocketColor': default_val = vec4_to_py_str(input.default_value) + + #vector types elif "Vector" in input.bl_idname: default_val = vec3_to_py_str(input.default_value) + + #strings elif input.bl_idname == 'NodeSocketString': default_val = str_to_py_str(input.default_value) + + #images elif input.bl_idname == 'NodeSocketImage': print("Input is linked: ", input.is_linked) img = input.default_value @@ -359,6 +367,22 @@ def set_input_defaults(node, dont_set_defaults: set, file: TextIO, inner: str, save_image(img, addon_dir) load_image(img, file, inner, f"{socket_var}.default_value") default_val = None + + #materials + elif input.bl_idname == 'NodeSocketMaterial': + in_file_inputs(input, file, inner, socket_var, "materials") + default_val = None + + #collections + elif input.bl_idname == 'NodeSocketCollection': + in_file_inputs(input, file, inner, socket_var, "collections") + default_val = None + + #objects + elif input.bl_idname == 'NodeSocketObject': + in_file_inputs(input, file, inner, socket_var, "objects") + default_val = None + else: default_val = input.default_value if default_val is not None: @@ -367,6 +391,23 @@ def set_input_defaults(node, dont_set_defaults: set, file: TextIO, inner: str, f" = {default_val}\n")) file.write("\n") +def in_file_inputs(input, file: TextIO, inner: str, socket_var: str, type: str): + """ + Sets inputs for a node input if one already exists in the blend file + + Parameters: + input (bpy.types.NodeSocket): input socket we're setting the value for + file (TextIO): file we're writing the add-on into + inner (str): indentation string + socket_var (str): variable name we're using for the socket + type (str): from what section of bpy.data to pull the default value from + """ + + name = str_to_py_str(input.default_value.name) + file.write(f"{inner}if {name} in bpy.data.{type}:\n") + file.write((f"{inner}\t{socket_var}.default_value = " + f"bpy.data.{type}[{name}]\n")) + def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): """ Sets parents for all nodes, mostly used to put nodes in frames From 5807de02d27119714f478a7a741cf8f18a2671ab Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 21 Jan 2023 14:19:12 -0600 Subject: [PATCH 46/60] feat: set defaults for textures if they exist in a blend file --- materials.py | 2 +- utils.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/materials.py b/materials.py index 4fa843a..411a92f 100644 --- a/materials.py +++ b/materials.py @@ -9,7 +9,7 @@ #'NodeSocketMaterial', #'NodeSocketObject', 'NodeSocketShader', - 'NodeSocketTexture', + #'NodeSocketTexture', 'NodeSocketVirtual'} node_settings = { diff --git a/utils.py b/utils.py index 5f41944..7eabffa 100644 --- a/utils.py +++ b/utils.py @@ -382,7 +382,12 @@ def set_input_defaults(node, dont_set_defaults: set, file: TextIO, inner: str, elif input.bl_idname == 'NodeSocketObject': in_file_inputs(input, file, inner, socket_var, "objects") default_val = None - + + #textures + elif input.bl_idname == 'NodeSocketTexture': + in_file_inputs(input, file, inner, socket_var, "textures") + default_val = None + else: default_val = input.default_value if default_val is not None: From 3c96b43c7e2a3942a6671baadf6dbb3fd72f78cb Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 21 Jan 2023 14:30:22 -0600 Subject: [PATCH 47/60] refactor: dont_set_defaults set moved to utils.py --- geo_nodes.py | 11 +---------- materials.py | 12 +----------- utils.py | 11 +++++++---- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 9e75917..dc3ea13 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -10,14 +10,6 @@ 'NodeSocketInt', 'NodeSocketVector'} -#node input sockets that are messy to set default values for -dont_set_defaults = {'NodeSocketCollection', - 'NodeSocketGeometry', - 'NodeSocketMaterial', - 'NodeSocketObject', - 'NodeSocketTexture', - 'NodeSocketVirtual'} - geo_node_settings = { #attribute "GeometryNodeAttributeStatistic" : ["data_type", "domain"], @@ -317,8 +309,7 @@ def process_geo_nodes_group(node_tree, level): elif node.bl_idname in curve_nodes: curve_node_settings(node, file, inner, node_var) - set_input_defaults(node, dont_set_defaults, file, inner, - node_var, addon_dir) + set_input_defaults(node, file, inner, node_var, addon_dir) set_parents(node_tree, file, inner, node_vars) set_locations(node_tree, file, inner, node_vars) diff --git a/materials.py b/materials.py index 411a92f..d3310df 100644 --- a/materials.py +++ b/materials.py @@ -3,15 +3,6 @@ from .utils import * -#node input sockets that are messy to set default values for -dont_set_defaults = {#'NodeSocketCollection', - 'NodeSocketGeometry', - #'NodeSocketMaterial', - #'NodeSocketObject', - 'NodeSocketShader', - #'NodeSocketTexture', - 'NodeSocketVirtual'} - node_settings = { #input "ShaderNodeAmbientOcclusion" : ["samples", "inside", "only_local"], @@ -188,8 +179,7 @@ def process_mat_node_group(node_tree, level): elif node.bl_idname in curve_nodes: curve_node_settings(node, file, inner, node_var) - set_input_defaults(node, dont_set_defaults, file, inner, - node_var, addon_dir) + set_input_defaults(node, file, inner, node_var, addon_dir) set_parents(node_tree, file, inner, node_vars) set_locations(node_tree, file, inner, node_vars) set_dimensions(node_tree, file, inner, node_vars) diff --git a/utils.py b/utils.py index 7eabffa..a97713f 100644 --- a/utils.py +++ b/utils.py @@ -8,6 +8,11 @@ image_dir_name = "imgs" +#node input sockets that are messy to set default values for +dont_set_defaults = {'NodeSocketGeometry', + 'NodeSocketShader', + 'NodeSocketVirtual'} + def clean_string(string: str, lower: bool = True) -> str: """ Cleans up a string for use as a variable or file name @@ -325,15 +330,13 @@ def curve_node_settings(node, file: TextIO, inner: str, node_var: str): file.write(f"{inner}#update curve after changes\n") file.write(f"{mapping}.update()\n") -def set_input_defaults(node, dont_set_defaults: set, file: TextIO, inner: str, - node_var: str, addon_dir: str): +def set_input_defaults(node, file: TextIO, inner: str, node_var: str, + addon_dir: str): """ Sets defaults for input sockets Parameters: node (bpy.types.Node): node we're setting inputs for - dont_set_defaults (set): set of sockets we shouldn't attempt to set - default values for file (TextIO): file we're generating the add-on into inner (str): indentation node_var (str): variable name we're using for the copied node From 6376db868f826cd00085e9f43a3a248b9e647e11 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 21 Jan 2023 19:25:48 -0600 Subject: [PATCH 48/60] fix: issue with no default value set --- utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/utils.py b/utils.py index a97713f..ea8d756 100644 --- a/utils.py +++ b/utils.py @@ -411,10 +411,11 @@ def in_file_inputs(input, file: TextIO, inner: str, socket_var: str, type: str): type (str): from what section of bpy.data to pull the default value from """ - name = str_to_py_str(input.default_value.name) - file.write(f"{inner}if {name} in bpy.data.{type}:\n") - file.write((f"{inner}\t{socket_var}.default_value = " - f"bpy.data.{type}[{name}]\n")) + if input.default_value is not None: + name = str_to_py_str(input.default_value.name) + file.write(f"{inner}if {name} in bpy.data.{type}:\n") + file.write((f"{inner}\t{socket_var}.default_value = " + f"bpy.data.{type}[{name}]\n")) def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): """ From 648e892b42cdc8225d4467b0841aaf6a64b51cdb Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 21 Jan 2023 19:55:26 -0600 Subject: [PATCH 49/60] feat: geo nodes node automatically apply to the object --- geo_nodes.py | 17 ++++++++++++++--- utils.py | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index dc3ea13..b63025d 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -141,7 +141,8 @@ def execute(self, context): #set up names to use in generated addon nt_var = clean_string(nt.name) - class_name = nt.name.replace(" ", "").replace('.', "") + class_name = clean_string(nt.name.replace(" ", "").replace('.', ""), + lower = False) #find base directory to save new addon base_dir = bpy.path.abspath("//") @@ -317,11 +318,21 @@ def process_geo_nodes_group(node_tree, level): init_links(node_tree, file, inner, node_tree_var, node_vars) + file.write(f"{inner}return {node_tree_var}\n") + #create node group - file.write(f"\n{outer}{node_tree_var}_node_group()\n\n") + file.write((f"\n{outer}{node_tree_var} = " + f"{node_tree_var}_node_group()\n\n")) process_geo_nodes_group(nt, 2) + file.write(f"\t\tname = bpy.context.object.name\n") + file.write(f"\t\tobj = bpy.data.objects[name]\n") + mod_name = str_to_py_str(nt.name) + 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") + file.write("\t\treturn {'FINISHED'}\n\n") create_menu_func(file, class_name) @@ -331,7 +342,7 @@ def process_geo_nodes_group(node_tree, level): file.close() - zip_addon(zip_dir) + #zip_addon(zip_dir) return {'FINISHED'} diff --git a/utils.py b/utils.py index ea8d756..8de3d09 100644 --- a/utils.py +++ b/utils.py @@ -110,7 +110,7 @@ def create_header(file: TextIO, name: str): file.write("}\n") file.write("\n") file.write("import bpy\n") - file.write("import os") + file.write("import os\n") file.write("\n") def init_operator(file: TextIO, name: str, idname: str, label: str): From b8ced76113f8676d662a3d48b5c071c250f07d96 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 21 Jan 2023 21:44:58 -0600 Subject: [PATCH 50/60] feat: settings for input geo nodes --- geo_nodes.py | 24 ++++++++++++++++++------ utils.py | 12 ++++++++++-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index b63025d..26c632a 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -54,8 +54,15 @@ "GeometryNodeSeparateGeometry" : ["domain"], #input + "FunctionNodeInputBool" : ["boolean"], "GeometryNodeCollectionInfo" : ["transform_space"], + "FunctionNodeInputColor" : ["color"], + "FunctionNodeInputInt" : ["integer"], + "GeometryNodeInputMaterial" : ["material"], "GeometryNodeObjectInfo" : ["transform_space"], + "FunctionNodeInputString" : ["string"], + "ShaderNodeValue" : ["outputs[0].default_value"], + "FunctionNodeInputVector" : ["vector"], "GeometryNodeInputNamedAttribute" : ["data_type"], #mesh @@ -326,12 +333,17 @@ def process_geo_nodes_group(node_tree, level): process_geo_nodes_group(nt, 2) - file.write(f"\t\tname = bpy.context.object.name\n") - file.write(f"\t\tobj = bpy.data.objects[name]\n") - mod_name = str_to_py_str(nt.name) - 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") + def apply_modifier(): + #get object + file.write(f"\t\tname = bpy.context.object.name\n") + file.write(f"\t\tobj = bpy.data.objects[name]\n") + + #set modifier to the one we just created + mod_name = str_to_py_str(nt.name) + 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") diff --git a/utils.py b/utils.py index 8de3d09..08ad64d 100644 --- a/utils.py +++ b/utils.py @@ -212,9 +212,17 @@ def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, attr = getattr(node, setting, None) if attr: if type(attr) == str: - attr = f"\'{attr}\'" + attr = enum_to_py_str(attr) if type(attr) == mathutils.Vector: - attr = f"({attr[0]}, {attr[1]}, {attr[2]})" + attr = vec3_to_py_str(attr) + if type(attr) == mathutils.Color: + attr = vec4_to_py_str(attr) + if type(attr) == bpy.types.Material: + name = str_to_py_str(attr.name) + file.write((f"{inner}if {name} in bpy.data.materials:\n")) + file.write((f"{inner}\t{node_var}.{setting} = " + f"bpy.data.materials[{name}]\n")) + continue file.write((f"{inner}{node_var}.{setting} " f"= {attr}\n")) From e748ffa61fb989672146d86475b4493b64e36c23 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 21 Jan 2023 22:01:55 -0600 Subject: [PATCH 51/60] feat: basic function for nodes that need outputs set --- geo_nodes.py | 3 +-- utils.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 26c632a..0b15cfb 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -61,7 +61,6 @@ "GeometryNodeInputMaterial" : ["material"], "GeometryNodeObjectInfo" : ["transform_space"], "FunctionNodeInputString" : ["string"], - "ShaderNodeValue" : ["outputs[0].default_value"], "FunctionNodeInputVector" : ["vector"], "GeometryNodeInputNamedAttribute" : ["data_type"], @@ -318,7 +317,7 @@ def process_geo_nodes_group(node_tree, level): curve_node_settings(node, file, inner, node_var) set_input_defaults(node, file, inner, node_var, addon_dir) - + set_output_defaults(node, file, inner, node_var) set_parents(node_tree, file, inner, node_vars) set_locations(node_tree, file, inner, node_vars) set_dimensions(node_tree, file, inner, node_vars) diff --git a/utils.py b/utils.py index 08ad64d..64f2760 100644 --- a/utils.py +++ b/utils.py @@ -215,7 +215,7 @@ def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, attr = enum_to_py_str(attr) if type(attr) == mathutils.Vector: attr = vec3_to_py_str(attr) - if type(attr) == mathutils.Color: + if type(attr) == bpy.types.bpy_prop_array: attr = vec4_to_py_str(attr) if type(attr) == bpy.types.Material: name = str_to_py_str(attr.name) @@ -425,6 +425,20 @@ def in_file_inputs(input, file: TextIO, inner: str, socket_var: str, type: str): file.write((f"{inner}\t{socket_var}.default_value = " f"bpy.data.{type}[{name}]\n")) +def set_output_defaults(node, file: TextIO, inner: str, node_var: str): + """ + Some output sockets need default values set. It's rather annoying + + Parameters: + node (bpy.types.Node): node for the output we're setting + file (TextIO): file we're generating the add-on into + inner (str): indentation string + node_var (str): variable name for the node we're setting output defaults for + """ + if node.bl_idname == 'ShaderNodeValue': + dv = node.outputs[0].default_value + file.write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) + def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): """ Sets parents for all nodes, mostly used to put nodes in frames From cda8090e883648931920b68950fc1d9f75e5a799 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 21 Jan 2023 22:15:19 -0600 Subject: [PATCH 52/60] fix: issue with color defaults --- utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/utils.py b/utils.py index 64f2760..7b3ddb5 100644 --- a/utils.py +++ b/utils.py @@ -216,7 +216,7 @@ def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, if type(attr) == mathutils.Vector: attr = vec3_to_py_str(attr) if type(attr) == bpy.types.bpy_prop_array: - attr = vec4_to_py_str(attr) + attr = vec4_to_py_str(list(attr)) if type(attr) == bpy.types.Material: name = str_to_py_str(attr.name) file.write((f"{inner}if {name} in bpy.data.materials:\n")) @@ -449,11 +449,13 @@ def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): inner (str): indentation string node_vars (dict): dictionary for (node, variable) name pairs """ + file.write(f"{inner}#Set parents") for node in node_tree.nodes: if node is not None and node.parent is not None: node_var = node_vars[node] parent_var = node_vars[node.parent] file.write(f"{inner}{node_var}.parent = {parent_var}\n") + file.write("\n") def set_locations(node_tree, file: TextIO, inner: str, node_vars: dict): """ @@ -466,10 +468,12 @@ def set_locations(node_tree, file: TextIO, inner: str, node_vars: dict): node_vars (dict): dictionary for (node, variable) name pairs """ + file.write(f"{inner}#Set locations") for node in node_tree.nodes: node_var = node_vars[node] file.write((f"{inner}{node_var}.location " f"= ({node.location.x}, {node.location.y})\n")) + file.write("\n") def set_dimensions(node_tree, file: TextIO, inner: str, node_vars: dict): """ @@ -482,11 +486,13 @@ def set_dimensions(node_tree, file: TextIO, inner: str, node_vars: dict): node_vars (dict): dictionary for (node, variable) name pairs """ + file.write(f"{inner}Set dimensions") for node in node_tree.nodes: node_var = node_vars[node] file.write((f"{inner}{node_var}.width, {node_var}.height " f"= {node.width}, {node.height}\n")) - + file.write("\n") + def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, node_vars: dict): """ From 3fa45d2fca5b3c6dbffaba32552810579a791afb Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 21 Jan 2023 23:16:46 -0600 Subject: [PATCH 53/60] fix: variable names now checked for uniqueness --- geo_nodes.py | 24 +++++++++++++++-------- materials.py | 26 ++++++++++++++++--------- utils.py | 55 +++++++++++++++++++++++++++++++++++----------------- 3 files changed, 70 insertions(+), 35 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 0b15cfb..e9eff4c 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -173,8 +173,14 @@ def execute(self, context): #set to keep track of already created node trees node_trees = set() - def process_geo_nodes_group(node_tree, level): - node_tree_var = clean_string(node_tree.name) + #dictionary to keep track of node->variable name pairs + node_vars = {} + + #keeps track of all used variables + used_vars = set() + + def process_geo_nodes_group(node_tree, level, node_vars, used_vars): + node_tree_var = create_var(node_tree.name, used_vars) outer, inner = make_indents(level) @@ -192,12 +198,13 @@ def process_geo_nodes_group(node_tree, level): #initialize nodes file.write(f"{inner}#initialize {node_tree_var} nodes\n") - node_vars = {} + for node in node_tree.nodes: if node.bl_idname == 'GeometryNodeGroup': node_nt = node.node_tree if node_nt is not None and node_nt not in node_trees: - process_geo_nodes_group(node_nt, level + 1) + process_geo_nodes_group(node_nt, level + 1, node_vars, + used_vars) node_trees.add(node_nt) elif node.bl_idname == 'NodeGroupInput' and not inputs_set: file.write(f"{inner}#{node_tree_var} inputs\n") @@ -300,8 +307,8 @@ def process_geo_nodes_group(node_tree, level): outputs_set = True #create node - node_var, node_vars = create_node(node, file, inner, - node_tree_var, node_vars) + node_var = create_node(node, file, inner, node_tree_var, + node_vars, used_vars) set_settings_defaults(node, geo_node_settings, file, inner, node_var) hide_sockets(node, file, inner, node_var) @@ -329,8 +336,9 @@ def process_geo_nodes_group(node_tree, level): #create node group file.write((f"\n{outer}{node_tree_var} = " f"{node_tree_var}_node_group()\n\n")) + return used_vars - process_geo_nodes_group(nt, 2) + process_geo_nodes_group(nt, 2, node_vars, used_vars) def apply_modifier(): #get object @@ -353,7 +361,7 @@ def apply_modifier(): file.close() - #zip_addon(zip_dir) + zip_addon(zip_dir) return {'FINISHED'} diff --git a/materials.py b/materials.py index d3310df..b91c325 100644 --- a/materials.py +++ b/materials.py @@ -118,15 +118,23 @@ def create_material(): file.write(f"\t\tmat.use_nodes = True\n") create_material() + #set to keep track of already created node trees node_trees = set() - def process_mat_node_group(node_tree, level): - nt_var = clean_string(node_tree.name) - nt_name = node_tree.name + #dictionary to keep track of node->variable name pairs + node_vars = {} + + #keeps track of all used variables + used_vars = set() + + def process_mat_node_group(node_tree, level, node_vars, used_vars): if level == 2: #outermost node group - nt_var = clean_string(self.material_name) + nt_var = create_var(self.material_name, used_vars) nt_name = self.material_name + else: + nt_var = create_var(node_tree.name, used_vars) + nt_name = node_tree.name outer, inner = make_indents(level) @@ -154,11 +162,11 @@ def process_mat_node_group(node_tree, level): if node.bl_idname == 'ShaderNodeGroup': node_nt = node.node_tree if node_nt is not None and node_nt not in node_trees: - process_mat_node_group(node_nt, level + 1) + process_mat_node_group(node_nt, level + 1, node_vars, used_vars) node_trees.add(node_nt) - node_var, node_vars = create_node(node, file, inner, nt_var, - node_vars) + node_var = create_node(node, file, inner, nt_var, node_vars, + used_vars) set_settings_defaults(node, node_settings, file, inner, node_var) hide_sockets(node, file, inner, node_var) @@ -179,7 +187,7 @@ def process_mat_node_group(node_tree, level): 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) + set_input_defaults(node, file, inner, node_var, addon_dir) set_parents(node_tree, file, inner, node_vars) set_locations(node_tree, file, inner, node_vars) set_dimensions(node_tree, file, inner, node_vars) @@ -188,7 +196,7 @@ def process_mat_node_group(node_tree, level): file.write(f"\n{outer}{nt_var}_node_group()\n\n") - process_mat_node_group(nt, 2) + process_mat_node_group(nt, 2, node_vars, used_vars) file.write("\t\treturn {'FINISHED'}\n\n") diff --git a/utils.py b/utils.py index 7b3ddb5..b361f0a 100644 --- a/utils.py +++ b/utils.py @@ -129,6 +129,29 @@ def init_operator(file: TextIO, name: str, idname: str, label: str): file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") file.write("\n") +def create_var(name: str, used_vars: set) -> str: + """ + Creates a unique variable name for a node tree + + Parameters: + name (str): basic string we'd like to create the variable name out of + used_vars (set): set containing all used variable names so far + + Returns: + clean_name (str): variable name for the node tree + """ + if name == "": + name = "unnamed" + clean_name = clean_string(name) + var = clean_name + i = 0 + while var in used_vars: + i += 1 + var = f"{clean_name}_{i}" + + used_vars.add(var) + return var + def make_indents(level: int) -> Tuple[str, str]: """ Returns strings with the correct number of indentations @@ -149,7 +172,7 @@ def make_indents(level: int) -> Tuple[str, str]: return outer, inner def create_node(node, file: TextIO, inner: str, node_tree_var: str, - node_vars: dict) -> Tuple[str, dict]: + node_vars: dict, used_vars: set) -> str: """ Initializes a new node with location, dimension, and label info @@ -158,24 +181,17 @@ def create_node(node, file: TextIO, inner: str, node_tree_var: str, file (TextIO): file containing the generated add-on inner (str): indentation level for this logic node_tree_var (str): variable name for the node tree - node_vars (dict): dictionary containing (bpy.types.Node, str) key-value - pairs, where the key is the node and the value its corresponding - variable name in the addon + node_vars (dict): dictionary containing (bpy.types.Node, str) + pairs, with a Node and its corresponding variable name + used_vars (set): set of used variable names Returns: node_var (str): variable name for the node - node_vars (dict): the updated variable name dictionary """ file.write(f"{inner}#node {node.name}\n") - node_var = clean_string(node.name) - if node_var == "": - i = 0 - while node_var in node_vars: - node_var = f"unnamed_node_{i}" - i += 1 - + node_var = create_var(node.name, used_vars) node_vars[node] = node_var file.write((f"{inner}{node_var} " @@ -193,7 +209,7 @@ def create_node(node, file: TextIO, inner: str, node_tree_var: str, if node.mute: file.write(f"{inner}{node_var}.mute = True\n") - return node_var, node_vars + return node_var def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, node_var: str): @@ -449,7 +465,7 @@ def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): inner (str): indentation string node_vars (dict): dictionary for (node, variable) name pairs """ - file.write(f"{inner}#Set parents") + file.write(f"{inner}#Set parents\n") for node in node_tree.nodes: if node is not None and node.parent is not None: node_var = node_vars[node] @@ -468,7 +484,7 @@ def set_locations(node_tree, file: TextIO, inner: str, node_vars: dict): node_vars (dict): dictionary for (node, variable) name pairs """ - file.write(f"{inner}#Set locations") + file.write(f"{inner}#Set locations\n") for node in node_tree.nodes: node_var = node_vars[node] file.write((f"{inner}{node_var}.location " @@ -486,13 +502,13 @@ def set_dimensions(node_tree, file: TextIO, inner: str, node_vars: dict): node_vars (dict): dictionary for (node, variable) name pairs """ - file.write(f"{inner}Set dimensions") + file.write(f"{inner}#sSet dimensions\n") for node in node_tree.nodes: node_var = node_vars[node] file.write((f"{inner}{node_var}.width, {node_var}.height " f"= {node.width}, {node.height}\n")) file.write("\n") - + def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, node_vars: dict): """ @@ -673,11 +689,14 @@ def image_user_settings(node, file: TextIO, inner: str, node_var: str): f"{getattr(img_usr, img_usr_attr)}\n")) def zip_addon(zip_dir: str): + pass """ Zips up the addon and removes the directory Parameters: zip_dir (str): path to the top-level addon directory """ + """ shutil.make_archive(zip_dir, "zip", zip_dir) - shutil.rmtree(zip_dir) \ No newline at end of file + shutil.rmtree(zip_dir) + """ \ No newline at end of file From 477605fb36dd537b25a6604a2d5aecc426a1af97 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 21 Jan 2023 23:54:40 -0600 Subject: [PATCH 54/60] docs: updated README to include current status on asset copying --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index eb4f3a0..d391830 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ## About A Blender add-on to create add-ons! This addo-on will take your Geometry Nodes or Materials and convert them into legible Python add-ons! -It automatically handles node layout, default values, sub-node groups, naming, and more! +It automatically handles node layout, default values, subgroups, naming, colors, and more! I think Blender's node-based editors are powerful, yet accessible tools, and I wanted to make scripting them easier for add-on creators. Combining Python with node based setups allows you to do things that would otherwise be tedious or impossible, such as * `for` loops @@ -33,28 +33,28 @@ Once you've installed the add-on, you'll see a new tab to the side of a Node Edi In the tab, there's panels to create add-ons for Geometry Nodes and Materials, each with a drop-down menu. -Just select the one you want, and soon a python file will be created in an `addons` folder located in the folder where your blend file is. +Just select the one you want, and soon a zip file will be created in an `addons` folder located in the folder where your blend file is. From here, you can install it like a regular add-on. ## Future * Expansion to Compositing nodes -* Copy over referenced assets in the scene (Collections, Objects, Materials, Textures, etc.) +* Add all referenced assets to the Asset Library for use outside of the original blend file * Automatically format code to be PEP8 compliant ## Potential Issues * As of version 2.0.0, the add-on will not set default values for - * Collections - * Materials - * Objects - * Textures * Scripts * IES files * Filepaths + * UV maps +* Currently when setting default values for the following, the add-on must be run in the same blend file as the node group was created in to set the default, otherwise it will just set it to `None`: + * Materials + * Objects + * Collections + * Textures - as they won't exist in every blend file. I'm expecting to support some of these in the future. - - There are a few nodes that don't set their default values like other ones, though these should also soon be supported. +* In a future version, I plan on having the add-on adding all of the above to the Asset Library for reference ## Bug Reports and Suggestions From a134c845e93ea3c0ef10f45940b25c4a2f6e7200 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 22 Jan 2023 00:29:08 -0600 Subject: [PATCH 55/60] fix: geo nodes and materials are now stable --- materials.py | 11 +++++++---- utils.py | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/materials.py b/materials.py index b91c325..b03fa4d 100644 --- a/materials.py +++ b/materials.py @@ -10,7 +10,7 @@ "ShaderNodeBevel" : ["samples"], "ShaderNodeVertexColor" : ["layer_name"], "ShaderNodeTangent" : ["direction_type", "axis"], - "ShaderNodeTexCoord" : ["from_instancer"], + "ShaderNodeTexCoord" : ["object", "from_instancer"], "ShaderNodeUVMap" : ["from_instancer", "uv_map"], "ShaderNodeWireframe" : ["use_pixel_size"], @@ -35,11 +35,12 @@ "ShaderNodeTexMagic" : ["turbulence_depth"], "ShaderNodeTexMusgrave" : ["musgrave_dimensions", "musgrave_type"], "ShaderNodeTexNoise" : ["noise_dimensions"], - "ShaderNodeTexPointDensity" : ["point_source", "space", "radius", + "ShaderNodeTexPointDensity" : ["point_source", "object", "space", "radius", "interpolation", "resolution", "vertex_color_source"], "ShaderNodeTexSky" : ["sky_type", "sun_direction", "turbidity", - "ground_albedo", "sun_disc", "sun_elevation", + "ground_albedo", "sun_disc", "sun_size", + "sun_intensity", "sun_elevation", "sun_rotation", "altitude", "air_density", "dust_density", "ozone_density"], "ShaderNodeTexVoronoi" : ["voronoi_dimensions", "feature", "distance"], @@ -187,7 +188,9 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): 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) + set_input_defaults(node, file, inner, node_var, addon_dir) + set_output_defaults(node, file, inner, node_var) + set_parents(node_tree, file, inner, node_vars) set_locations(node_tree, file, inner, node_vars) set_dimensions(node_tree, file, inner, node_vars) diff --git a/utils.py b/utils.py index b361f0a..7a08447 100644 --- a/utils.py +++ b/utils.py @@ -239,6 +239,12 @@ def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, file.write((f"{inner}\t{node_var}.{setting} = " f"bpy.data.materials[{name}]\n")) continue + if type(attr) == bpy.types.Object: + name = str_to_py_str(attr.name) + file.write((f"{inner}if {name} in bpy.data.objects:\n")) + file.write((f"{inner}\t{node_var}.{setting} = " + f"bpy.data.objects[{name}]\n")) + continue file.write((f"{inner}{node_var}.{setting} " f"= {attr}\n")) @@ -451,8 +457,16 @@ def set_output_defaults(node, file: TextIO, inner: str, node_var: str): inner (str): indentation string node_var (str): variable name for the node we're setting output defaults for """ - if node.bl_idname == 'ShaderNodeValue': + output_default_nodes = {'ShaderNodeValue', + 'ShaderNodeRGB', + 'ShaderNodeNormal'} + + if node.bl_idname in output_default_nodes: dv = node.outputs[0].default_value + if node.bl_idname == 'ShaderNodeRGB': + dv = vec4_to_py_str(list(dv)) + if node.bl_idname == 'ShaderNodeNormal': + dv = vec3_to_py_str(dv) file.write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): From 7fd3b0e0cc69517e98751a4287547e27f3e39ab5 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 22 Jan 2023 16:29:52 -0600 Subject: [PATCH 56/60] docs: image showing new location --- img/location.png | Bin 0 -> 47953 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/location.png diff --git a/img/location.png b/img/location.png new file mode 100644 index 0000000000000000000000000000000000000000..98c8b797db8d2a989e14867ccb4e3d88c5ab6d50 GIT binary patch literal 47953 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 literal 0 HcmV?d00001 From 1fb9651eb29868186e1fb836ac7727ddfe63301c Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 22 Jan 2023 16:31:03 -0600 Subject: [PATCH 57/60] fix: zips addons again --- utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/utils.py b/utils.py index 7a08447..81de806 100644 --- a/utils.py +++ b/utils.py @@ -703,14 +703,11 @@ def image_user_settings(node, file: TextIO, inner: str, node_var: str): f"{getattr(img_usr, img_usr_attr)}\n")) def zip_addon(zip_dir: str): - pass """ Zips up the addon and removes the directory Parameters: zip_dir (str): path to the top-level addon directory """ - """ shutil.make_archive(zip_dir, "zip", zip_dir) - shutil.rmtree(zip_dir) - """ \ No newline at end of file + shutil.rmtree(zip_dir) \ No newline at end of file From 28ab84f2308dadb18ee8652230dcd573b3618fa5 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 22 Jan 2023 16:38:13 -0600 Subject: [PATCH 58/60] docs: clarification on where to find add-on --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d391830..676f672 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ NodeToPython v2.0 is compatible with Blender 3.0 - 3.4 on Windows, macOS, and Li 3. Click Install, and find where you downloaded the zip file. Then hit the `Install Add-on` button, and you're done! ## Usage -Once you've installed the add-on, you'll see a new tab to the side of a Node Editor. +Once you've installed the add-on, you'll see a new tab in the Node Editor's sidebar, which you can open with keyboard shortcut `N`. In the tab, there's panels to create add-ons for Geometry Nodes and Materials, each with a drop-down menu. From 4612d111c0802cd23e45e8a866c52f88ad67a443 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 22 Jan 2023 16:42:46 -0600 Subject: [PATCH 59/60] docs: images now responsive --- README.md => docs/README.md | 12 +++++------- {img => docs/img}/location.png | Bin {img => docs/img}/ntp.jpg | Bin 3 files changed, 5 insertions(+), 7 deletions(-) rename README.md => docs/README.md (95%) rename {img => docs/img}/location.png (100%) rename {img => docs/img}/ntp.jpg (100%) diff --git a/README.md b/docs/README.md similarity index 95% rename from README.md rename to docs/README.md index 676f672..08d59f5 100644 --- a/README.md +++ b/docs/README.md @@ -1,11 +1,7 @@ # Node to Python -Node To Python - + +![Node To Python Logo](./img/ntp.jpg "") + [![GitHub release (latest by date)](https://img.shields.io/github/v/release/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/releases) [![GitHub](https://img.shields.io/github/license/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/blob/main/LICENSE) ![](https://visitor-badge.laobi.icu/badge?page_id=BrendanParmer.NodeToPython) ## About @@ -33,6 +29,8 @@ Once you've installed the add-on, you'll see a new tab in the Node Editor's side In the tab, there's panels to create add-ons for Geometry Nodes and Materials, each with a drop-down menu. +![Add-on Location](./img/location.png "") + Just select the one you want, and soon a zip file will be created in an `addons` folder located in the folder where your blend file is. From here, you can install it like a regular add-on. diff --git a/img/location.png b/docs/img/location.png similarity index 100% rename from img/location.png rename to docs/img/location.png diff --git a/img/ntp.jpg b/docs/img/ntp.jpg similarity index 100% rename from img/ntp.jpg rename to docs/img/ntp.jpg From 4caa9e1bda2f124f2bc8f46ca4acfedd47ae7f21 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 22 Jan 2023 16:48:14 -0600 Subject: [PATCH 60/60] docs: misc README updates --- docs/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 08d59f5..74d6bdd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,11 +1,11 @@ # Node to Python -![Node To Python Logo](./img/ntp.jpg "") +![Node To Python Logo](./img/ntp.jpg "Node To Python Logo") [![GitHub release (latest by date)](https://img.shields.io/github/v/release/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/releases) [![GitHub](https://img.shields.io/github/license/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/blob/main/LICENSE) ![](https://visitor-badge.laobi.icu/badge?page_id=BrendanParmer.NodeToPython) ## About -A Blender add-on to create add-ons! This addo-on will take your Geometry Nodes or Materials and convert them into legible Python add-ons! +A Blender add-on to create add-ons! This add-on will take your Geometry Nodes or Materials and convert them into legible Python add-ons! It automatically handles node layout, default values, subgroups, naming, colors, and more! @@ -25,11 +25,11 @@ NodeToPython v2.0 is compatible with Blender 3.0 - 3.4 on Windows, macOS, and Li 3. Click Install, and find where you downloaded the zip file. Then hit the `Install Add-on` button, and you're done! ## Usage -Once you've installed the add-on, you'll see a new tab in the Node Editor's sidebar, which you can open with keyboard shortcut `N`. +Once you've installed the add-on, you'll see a new tab in any Node Editor's sidebar. You can open this with keyboard shortcut `N` when focused in the Node Editor. In the tab, there's panels to create add-ons for Geometry Nodes and Materials, each with a drop-down menu. -![Add-on Location](./img/location.png "") +![Add-on Location](./img/location.png "Add-on Location") Just select the one you want, and soon a zip file will be created in an `addons` folder located in the folder where your blend file is. @@ -38,7 +38,9 @@ From here, you can install it like a regular add-on. ## Future * Expansion to Compositing nodes * Add all referenced assets to the Asset Library for use outside of the original blend file +* Auto-set handle movies and image sequences * Automatically format code to be PEP8 compliant +* Automatically detect the minimum version of Blender needed to run the add-on ## Potential Issues * As of version 2.0.0, the add-on will not set default values for