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/14] 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 147a1efe823e0c8662cd68fee6f0db3a15c1da42 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 2 Jan 2023 21:27:18 -0600 Subject: [PATCH 02/14] Revert "docs: update README" This reverts commit 7d448b03c73fab16160868130740dc272ee5d9fb. --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 88341e6..b2d79df 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A Blender add-on to create add-ons! This script will take your Geometry Node gro 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. I wanted to make scripting node groups 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 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 * `for` loops * different node trees for different versions or settings * interfacing with other parts of the software. @@ -34,14 +34,15 @@ Download `node_to_python.py`, and install it to Blender like other add-ons. Then * Automatically format code to be PEP8 compliant ## Potential Issues -* As of version 1.2.1, the add-on will not set default values for +* 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 * Collections * Images * Materials * Objects * Textures - as they won't exist in every blend file. I plan on implementing these soon. + as they won't exist in every blend file. In the future, I may have the script automatically recreate these assets, espcially with materials. ## Bug Reports and Suggestions @@ -51,7 +52,5 @@ When submitting an issue, please include * Your operating system * A short description of what you were trying to accomplish, or steps to reproduce the issue -If you don't mind sharing a blend file, that helps a lot! - Suggestions for how to improve the add-on are more than welcome! From 71f08157a85bc2d7c336168a8f6710684f165aaa Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 2 Jan 2023 21:36:12 -0600 Subject: [PATCH 03/14] fix: updated bl_info for correct version --- node_to_python.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/node_to_python.py b/node_to_python.py index abf834a..85d30ee 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": (1, 2, 1), "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 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/14] 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 ebd080e2faf3bbbb7fe7f1cba5381038d595ed33 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 8 Jan 2023 19:53:14 -0600 Subject: [PATCH 05/14] fix: wouldn't properly handle node groups with periods in the name --- node_to_python.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/node_to_python.py b/node_to_python.py index 85d30ee..04699bb 100644 --- a/node_to_python.py +++ b/node_to_python.py @@ -2,7 +2,7 @@ "name": "Node to Python", "description": "Convert Geometry Node Groups to a Python add-on", "author": "Brendan Parmer", - "version": (1, 2, 1), + "version": (1, 2, 2), "blender": (3, 0, 0), "location": "Node", "category": "Node", @@ -153,6 +153,9 @@ 'ShaderNodeVectorCurve', 'ShaderNodeRGBCurve'} +def cleanup_string(string: str): + return string.lower().replace(' ', '_').replace('.', '_') + class NodeToPython(bpy.types.Operator): bl_idname = "object.node_to_python" bl_label = "Node to Python" @@ -164,8 +167,8 @@ 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 = ng.name.lower().replace(' ', '_') - class_name = ng.name.replace(" ", "") + ng_name = cleanup_string(ng.name) + class_name = ng.name.replace(" ", "").replace('.', "") dir = bpy.path.abspath("//") if not dir or dir == "": self.report({'ERROR'}, @@ -205,7 +208,7 @@ def init_class(): file.write("\tdef execute(self, context):\n") def process_node_group(node_group, level): - ng_name = node_group.name.lower().replace(' ', '_') + ng_name = cleanup_string(node_group.name) outer = "\t"*level #outer indentation inner = "\t"*(level + 1) #inner indentation @@ -271,8 +274,7 @@ def process_node_group(node_group, level): file.write("\n") #create node - node_name = node.name.lower() - node_name = node_name.replace(' ', '_').replace('.', '_') + node_name = cleanup_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")) @@ -387,8 +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 = link.from_node.name.lower() - input_node = input_node.replace(' ', '_').replace('.', '_') + input_node = cleanup_string(link.from_node.name) input_socket = link.from_socket """ @@ -402,8 +403,7 @@ def process_node_group(node_group, level): input_idx = i break - output_node = link.to_node.name.lower() - output_node = output_node.replace(' ', '_').replace('.', '_') + output_node = cleanup_string(link.to_node.name) output_socket = link.to_socket for i, item in enumerate(link.to_node.inputs.items()): 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 06/14] 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 07/14] 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 08/14] 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 09/14] 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 10/14] 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 11/14] 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 12/14] 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 13/14] 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 14/14] 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")