From a62f6332fe598f6c5410bf05c8de127440f241b2 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:22:35 -0500 Subject: [PATCH 01/10] feat: option to include import statements in script mode --- blender_manifest.toml | 34 ++++++++++++++++++++++++++++++++++ compositor/operator.py | 2 ++ geometry/operator.py | 3 +++ options.py | 8 +++++++- shader/operator.py | 2 ++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 blender_manifest.toml diff --git a/blender_manifest.toml b/blender_manifest.toml new file mode 100644 index 0000000..c25016c --- /dev/null +++ b/blender_manifest.toml @@ -0,0 +1,34 @@ +schema_version = "1.0.0" + +# Example of manifest file for a Blender extension +# Change the values according to your extension +id = "node_to_python" +version = "3.2.0" +name = "Node To Python" +tagline = "Turn node groups into Python code" +maintainer = "Brendan Parmer " +# Supported types: "add-on", "theme" +type = "add-on" + +# Optional: add-ons can list which resources they will require: +# * "files" (for access of any filesystem operations) +# * "network" (for internet access) +# * "clipboard" (to read and/or write the system clipboard) +# * "camera" (to capture photos and videos) +# * "microphone" (to capture audio) +# permissions = ["files"] + +# Optional link to documentation, support, source files, etc +# website = "https://github.com/BrendanParmer/NodeToPython" + +# Optional list defined by Blender and server, see: +# https://docs.blender.org/manual/en/dev/extensions/tags.html +tags = ["Development", "Compositing", "Geometry Nodes", "Material"] + +blender_version_min = "4.2.0" +# Optional: maximum supported Blender version +# blender_version_max = "5.1.0" + +license = [ + "SPDX:MIT", +] \ No newline at end of file diff --git a/compositor/operator.py b/compositor/operator.py index 91f9adf..92d6373 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -219,6 +219,8 @@ def execute(self, context): self._write("def execute(self, context):", "\t") else: self._file = StringIO("") + if context.scene.ntp_options.include_imports: + self._file.write("import bpy, mathutils\n\n") if self.is_scene: if self.mode == 'ADDON': diff --git a/geometry/operator.py b/geometry/operator.py index b359bf5..b0903d5 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -200,6 +200,9 @@ def execute(self, context): self._write("def execute(self, context):", "\t") else: self._file = StringIO("") + if context.scene.ntp_options.include_imports: + self._file.write("import bpy, mathutils\n\n") + node_trees_to_process = self._topological_sort(nt) diff --git a/options.py b/options.py index 09c8310..e223d7c 100644 --- a/options.py +++ b/options.py @@ -10,6 +10,11 @@ class NTPOptions(bpy.types.PropertyGroup): description="Save location if generating an add-on", default = "//" ) + include_imports : bpy.props.BoolProperty( + name = "Include imports", + description="Generate necessary import statements", + default = True + ) class NTPOptionsPanel(bpy.types.Panel): bl_label = "Options" @@ -25,4 +30,5 @@ def poll(cls, context): def draw(self, context): layout = self.layout layout.operator_context = 'INVOKE_DEFAULT' - layout.prop(context.scene.ntp_options, "dir_path") \ No newline at end of file + layout.prop(context.scene.ntp_options, "dir_path") + layout.prop(context.scene.ntp_options, "include_imports") \ No newline at end of file diff --git a/shader/operator.py b/shader/operator.py index 7232fa8..28e1c62 100644 --- a/shader/operator.py +++ b/shader/operator.py @@ -149,6 +149,8 @@ def execute(self, context): self._write("def execute(self, context):", "\t") else: self._file = StringIO("") + if context.scene.ntp_options.include_imports: + self._file.write("import bpy, mathutils\n\n") if self.mode == 'ADDON': self._create_material("\t\t") From 537e754fd609be18d02f7ca16cd35f7323764476 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 24 Jun 2024 22:03:21 -0500 Subject: [PATCH 02/10] feat: options for included socket values, setting dimensions, and not setting hidden socket defaults --- compositor/operator.py | 4 +++- geometry/operator.py | 4 +++- ntp_operator.py | 30 ++++++++++++++++++++++++++++++ options.py | 29 +++++++++++++++++++++++++++-- shader/operator.py | 4 +++- 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index 92d6373..d5f7f36 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -187,6 +187,8 @@ def _process_node_tree(self, node_tree: CompositorNodeTree): self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) def execute(self, context): + self._setup_options(context.scene.ntp_options) + #find node group to replicate if self.is_scene: self._base_node_tree = bpy.data.scenes[self.compositor_name].node_tree @@ -219,7 +221,7 @@ def execute(self, context): self._write("def execute(self, context):", "\t") else: self._file = StringIO("") - if context.scene.ntp_options.include_imports: + if self._include_imports: self._file.write("import bpy, mathutils\n\n") if self.is_scene: diff --git a/geometry/operator.py b/geometry/operator.py index b0903d5..91d66ae 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -179,6 +179,8 @@ def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): def execute(self, context): + self._setup_options(context.scene.ntp_options) + #find node group to replicate nt = bpy.data.node_groups[self.geo_nodes_group_name] @@ -200,7 +202,7 @@ def execute(self, context): self._write("def execute(self, context):", "\t") else: self._file = StringIO("") - if context.scene.ntp_options.include_imports: + if self._include_imports: self._file.write("import bpy, mathutils\n\n") diff --git a/ntp_operator.py b/ntp_operator.py index 18c274f..2b3f80c 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -14,6 +14,7 @@ import shutil from .ntp_node_tree import NTP_NodeTree +from .options import NTPOptions from .utils import * INDEX = "i" @@ -106,11 +107,28 @@ def __init__(self): for name in RESERVED_NAMES: self._used_vars[name] = 0 + # Generate socket default, min, and max values + self._include_socket_values = True + + # Set dimensions of generated nodes + self._should_set_dimensions = True + + if bpy.app.version >= (3, 4, 0): + # Set default values for hidden sockets + self._set_hidden_defaults = False + def _write(self, string: str, indent: str = None): if indent is None: indent = self._inner self._file.write(f"{indent}{string}\n") + def _setup_options(self, options: NTPOptions) -> None: + self._include_imports = options.include_imports + self._include_socket_values = options.include_socket_values + self._should_set_dimensions = options.set_dimensions + if bpy.app.version >= (3, 4, 0): + self._set_hidden_defaults = options.set_hidden_defaults + def _setup_addon_directories(self, context: Context, nt_var: str) -> bool: """ Finds/creates directories to save add-on to @@ -381,6 +399,9 @@ def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface, with the input/output socket_var (str): variable name for the socket """ + if not self._include_socket_values: + return + if socket_interface.type not in self.default_sockets_v3: return @@ -477,6 +498,8 @@ def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, with the input/output socket_var (str): variable name for the socket """ + if not self._include_socket_values: + return if type(socket_interface) in self.nondefault_sockets_v4: return @@ -707,6 +730,10 @@ def _set_input_defaults(self, node: Node) -> None: for i, input in enumerate(node.inputs): if input.bl_idname not in DONT_SET_DEFAULTS and not input.is_linked: + if bpy.app.version >= (3, 4, 0): + if (not self._set_hidden_defaults) and input.is_unavailable: + continue + # TODO: this could be cleaner socket_var = f"{node_var}.inputs[{i}]" @@ -1187,6 +1214,9 @@ def _set_dimensions(self, node_tree: NodeTree) -> None: Parameters: node_tree (NodeTree): node tree we're obtaining nodes from """ + if not self._should_set_dimensions: + return + self._write(f"#Set dimensions") for node in node_tree.nodes: node_var = self._node_vars[node] diff --git a/options.py b/options.py index e223d7c..a6b0839 100644 --- a/options.py +++ b/options.py @@ -15,6 +15,22 @@ class NTPOptions(bpy.types.PropertyGroup): description="Generate necessary import statements", default = True ) + include_socket_values : bpy.props.BoolProperty( + name = "Include socket values", + description = "Generate group socket default, min, and max values", + default = True + ) + set_dimensions : bpy.props.BoolProperty( + name = "Set dimensions", + description = "Set dimensions of generated nodes", + default = True + ) + if bpy.app.version >= (3, 4, 0): + set_hidden_defaults : bpy.props.BoolProperty( + name = "Set hidden defaults", + description = "Set default values for hidden sockets", + default = False + ) class NTPOptionsPanel(bpy.types.Panel): bl_label = "Options" @@ -30,5 +46,14 @@ def poll(cls, context): def draw(self, context): layout = self.layout layout.operator_context = 'INVOKE_DEFAULT' - layout.prop(context.scene.ntp_options, "dir_path") - layout.prop(context.scene.ntp_options, "include_imports") \ No newline at end of file + ntp_options = context.scene.ntp_options + + option_list = [ + "dir_path", + "include_imports", "include_socket_values", + "set_dimensions" + ] + if bpy.app.version >= (3, 4, 0): + option_list.append("set_hidden_defaults") + for option in option_list: + layout.prop(ntp_options, option) \ No newline at end of file diff --git a/shader/operator.py b/shader/operator.py index 28e1c62..94d5eb1 100644 --- a/shader/operator.py +++ b/shader/operator.py @@ -123,6 +123,8 @@ def _process_node_tree(self, node_tree: ShaderNodeTree) -> None: def execute(self, context): + self._setup_options(context.scene.ntp_options) + #find node group to replicate self._base_node_tree = bpy.data.materials[self.material_name].node_tree if self._base_node_tree is None: @@ -149,7 +151,7 @@ def execute(self, context): self._write("def execute(self, context):", "\t") else: self._file = StringIO("") - if context.scene.ntp_options.include_imports: + if self._include_imports: self._file.write("import bpy, mathutils\n\n") if self.mode == 'ADDON': From 3d9c29b76a9f2f829deeb9d9693cf62b41895025 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 24 Jun 2024 22:15:48 -0500 Subject: [PATCH 03/10] style: made variable names clearer --- ntp_operator.py | 14 +++++++------- options.py | 13 +++++++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/ntp_operator.py b/ntp_operator.py index 2b3f80c..64a1c89 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -108,14 +108,14 @@ def __init__(self): self._used_vars[name] = 0 # Generate socket default, min, and max values - self._include_socket_values = True + self._include_group_socket_values = True # Set dimensions of generated nodes self._should_set_dimensions = True if bpy.app.version >= (3, 4, 0): # Set default values for hidden sockets - self._set_hidden_defaults = False + self._set_unavailable_defaults = False def _write(self, string: str, indent: str = None): if indent is None: @@ -124,10 +124,10 @@ def _write(self, string: str, indent: str = None): def _setup_options(self, options: NTPOptions) -> None: self._include_imports = options.include_imports - self._include_socket_values = options.include_socket_values + self._include_group_socket_values = options.include_group_socket_values self._should_set_dimensions = options.set_dimensions if bpy.app.version >= (3, 4, 0): - self._set_hidden_defaults = options.set_hidden_defaults + self._set_unavailable_defaults = options.set_unavailable_defaults def _setup_addon_directories(self, context: Context, nt_var: str) -> bool: """ @@ -399,7 +399,7 @@ def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface, with the input/output socket_var (str): variable name for the socket """ - if not self._include_socket_values: + if not self._include_group_socket_values: return if socket_interface.type not in self.default_sockets_v3: @@ -498,7 +498,7 @@ def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, with the input/output socket_var (str): variable name for the socket """ - if not self._include_socket_values: + if not self._include_group_socket_values: return if type(socket_interface) in self.nondefault_sockets_v4: return @@ -731,7 +731,7 @@ def _set_input_defaults(self, node: Node) -> None: for i, input in enumerate(node.inputs): if input.bl_idname not in DONT_SET_DEFAULTS and not input.is_linked: if bpy.app.version >= (3, 4, 0): - if (not self._set_hidden_defaults) and input.is_unavailable: + if (not self._set_unavailable_defaults) and input.is_unavailable: continue # TODO: this could be cleaner diff --git a/options.py b/options.py index a6b0839..ba88d24 100644 --- a/options.py +++ b/options.py @@ -15,8 +15,8 @@ class NTPOptions(bpy.types.PropertyGroup): description="Generate necessary import statements", default = True ) - include_socket_values : bpy.props.BoolProperty( - name = "Include socket values", + include_group_socket_values : bpy.props.BoolProperty( + name = "Include group socket values", description = "Generate group socket default, min, and max values", default = True ) @@ -26,9 +26,9 @@ class NTPOptions(bpy.types.PropertyGroup): default = True ) if bpy.app.version >= (3, 4, 0): - set_hidden_defaults : bpy.props.BoolProperty( - name = "Set hidden defaults", - description = "Set default values for hidden sockets", + set_unavailable_defaults : bpy.props.BoolProperty( + name = "Set unavailable defaults", + description = "Set default values for unavailable sockets", default = False ) @@ -50,7 +50,8 @@ def draw(self, context): option_list = [ "dir_path", - "include_imports", "include_socket_values", + "include_imports", + "include_group_socket_values", "set_dimensions" ] if bpy.app.version >= (3, 4, 0): From c015bbfd6291b15f652bfb10c521859da5233a3a Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 20 Jul 2024 15:21:44 -0500 Subject: [PATCH 04/10] fix: correct unavailable defaults option name --- options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/options.py b/options.py index ba88d24..ab28565 100644 --- a/options.py +++ b/options.py @@ -55,6 +55,6 @@ def draw(self, context): "set_dimensions" ] if bpy.app.version >= (3, 4, 0): - option_list.append("set_hidden_defaults") + option_list.append("set_unavailable_defaults") for option in option_list: layout.prop(ntp_options, option) \ No newline at end of file From 057303e314ec231555e36bdb09c0559ebd7b5db3 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 20 Jul 2024 15:55:31 -0500 Subject: [PATCH 05/10] feat: file permission explanation now included in manifest file --- blender_manifest.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blender_manifest.toml b/blender_manifest.toml index 51840c2..7443d4c 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -7,9 +7,6 @@ tagline = "Turn node groups into Python code" maintainer = "Brendan Parmer " type = "add-on" -# In add-on mode, NodeToPython will create and write to files at a specified directory -permissions = ["files"] - website = "https://github.com/BrendanParmer/NodeToPython" tags = ["Development", "Compositing", "Geometry Nodes", "Material", "Node"] @@ -19,4 +16,7 @@ blender_version_max = "4.3.0" license = [ "SPDX:MIT", -] \ No newline at end of file +] + +[permissions] +files = "In add-on mode, NodeToPython will create and write to files in a specified directory" \ No newline at end of file From 1784a80d43df786aa3b0fe29492db47bca6a2858 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 3 Aug 2024 14:16:58 -0500 Subject: [PATCH 06/10] feat: addon vs script option rework --- compositor/operator.py | 8 -------- geometry/operator.py | 8 -------- ntp_operator.py | 15 +------------- options.py | 46 +++++++++++++++++++++++++++++++----------- 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index 74df619..14bf36f 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -20,14 +20,6 @@ class NTPCompositorOperator(NTP_Operator): bl_idname = "node.ntp_compositor" bl_label = "Compositor to Python" bl_options = {'REGISTER', 'UNDO'} - - mode : bpy.props.EnumProperty( - name = "Mode", - items = [ - ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full addon") - ] - ) compositor_name: bpy.props.StringProperty(name="Node Group") is_scene : bpy.props.BoolProperty(name="Is Scene", description="Blender stores compositing node trees differently for scenes and in groups") diff --git a/geometry/operator.py b/geometry/operator.py index 330f4ab..5a1362d 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -21,14 +21,6 @@ class NTPGeoNodesOperator(NTP_Operator): bl_label = "Geo Nodes to Python" bl_options = {'REGISTER', 'UNDO'} - mode: bpy.props.EnumProperty( - name = "Mode", - items = [ - ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full addon") - ] - ) - geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") def __init__(self): diff --git a/ntp_operator.py b/ntp_operator.py index fbbd02c..feaa8eb 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -47,14 +47,6 @@ class NTP_Operator(Operator): bl_idname = "" bl_label = "" - mode: bpy.props.EnumProperty( - name="Mode", - items=[ - ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full addon") - ] - ) - # node tree input sockets that have default properties if bpy.app.version < (4, 0, 0): default_sockets_v3 = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'} @@ -125,6 +117,7 @@ def _write(self, string: str, indent: str = None): self._file.write(f"{indent}{string}\n") def _setup_options(self, options: NTPOptions) -> None: + self.mode = options.mode self._include_imports = options.include_imports self._include_group_socket_values = options.include_group_socket_values self._should_set_dimensions = options.set_dimensions @@ -1386,9 +1379,3 @@ def _report_finished(self, object: str): # ABSTRACT def execute(self): return {'FINISHED'} - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - self.layout.prop(self, "mode") diff --git a/options.py b/options.py index ab28565..5d44712 100644 --- a/options.py +++ b/options.py @@ -4,16 +4,13 @@ class NTPOptions(bpy.types.PropertyGroup): """ Property group used during conversion of node group to python """ - dir_path : bpy.props.StringProperty( - name = "Save Location", - subtype='DIR_PATH', - description="Save location if generating an add-on", - default = "//" - ) - include_imports : bpy.props.BoolProperty( - name = "Include imports", - description="Generate necessary import statements", - default = True + # General properties + mode: bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full add-on") + ] ) include_group_socket_values : bpy.props.BoolProperty( name = "Include group socket values", @@ -32,6 +29,20 @@ class NTPOptions(bpy.types.PropertyGroup): default = False ) + #Script properties + include_imports : bpy.props.BoolProperty( + name = "Include imports", + description="Generate necessary import statements", + default = True + ) + # Addon properties + dir_path : bpy.props.StringProperty( + name = "Save Location", + subtype='DIR_PATH', + description="Save location if generating an add-on", + default = "//" + ) + class NTPOptionsPanel(bpy.types.Panel): bl_label = "Options" bl_idname = "NODE_PT_ntp_options" @@ -49,12 +60,23 @@ def draw(self, context): ntp_options = context.scene.ntp_options option_list = [ - "dir_path", - "include_imports", + "mode", "include_group_socket_values", "set_dimensions" ] if bpy.app.version >= (3, 4, 0): option_list.append("set_unavailable_defaults") + + if ntp_options.mode == 'SCRIPT': + script_options = [ + "include_imports" + ] + option_list += script_options + elif ntp_options.mode == 'ADDON': + addon_options = [ + "dir_path" + ] + option_list += addon_options + for option in option_list: layout.prop(ntp_options, option) \ No newline at end of file From e560f4f3fb45e047d3b33537e69dd2ecaf0ffab7 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 3 Aug 2024 14:27:02 -0500 Subject: [PATCH 07/10] feat: author and version fields --- compositor/operator.py | 10 +++++----- geometry/operator.py | 8 ++++---- ntp_operator.py | 12 +++++++----- options.py | 14 +++++++++++++- shader/operator.py | 10 +++++----- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index 14bf36f..126c0b1 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -203,7 +203,7 @@ def execute(self, context): #set up names to use in generated addon comp_var = clean_string(self.compositor_name) - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._outer = "\t\t" self._inner = "\t\t\t" @@ -223,9 +223,9 @@ def execute(self, context): self._file.write("import bpy, mathutils\n\n") if self.is_scene: - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._create_scene("\t\t") - elif self.mode == 'SCRIPT': + elif self._mode == 'SCRIPT': self._create_scene("") node_trees_to_process = self._topological_sort(self._base_node_tree) @@ -233,7 +233,7 @@ def execute(self, context): for node_tree in node_trees_to_process: self._process_node_tree(node_tree) - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._write("return {'FINISHED'}\n", self._outer) self._create_menu_func() @@ -245,7 +245,7 @@ def execute(self, context): self._file.close() - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._zip_addon() self._report_finished("compositor nodes") diff --git a/geometry/operator.py b/geometry/operator.py index 5a1362d..788f344 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -20,7 +20,7 @@ class NTPGeoNodesOperator(NTP_Operator): bl_idname = "node.ntp_geo_nodes" bl_label = "Geo Nodes to Python" bl_options = {'REGISTER', 'UNDO'} - + geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") def __init__(self): @@ -179,7 +179,7 @@ def execute(self, context): #set up names to use in generated addon nt_var = clean_string(nt.name) - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._outer = "\t\t" self._inner = "\t\t\t" @@ -203,7 +203,7 @@ def execute(self, context): for node_tree in node_trees_to_process: self._process_node_tree(node_tree) - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._apply_modifier(nt, nt_var) self._write("return {'FINISHED'}\n", self._outer) self._create_menu_func() @@ -214,7 +214,7 @@ def execute(self, context): context.window_manager.clipboard = self._file.getvalue() self._file.close() - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._zip_addon() self._report_finished("geometry node group") diff --git a/ntp_operator.py b/ntp_operator.py index feaa8eb..38584a1 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -117,12 +117,14 @@ def _write(self, string: str, indent: str = None): self._file.write(f"{indent}{string}\n") def _setup_options(self, options: NTPOptions) -> None: - self.mode = options.mode + self._mode = options.mode self._include_imports = options.include_imports self._include_group_socket_values = options.include_group_socket_values self._should_set_dimensions = options.set_dimensions if bpy.app.version >= (3, 4, 0): self._set_unavailable_defaults = options.set_unavailable_defaults + self._author_name = options.author_name + self._version = options.version def _setup_addon_directories(self, context: Context, nt_var: str) -> bool: """ @@ -161,9 +163,9 @@ def _create_header(self, name: str) -> None: """ self._write("bl_info = {", "") - self._write(f"\t\"name\" : \"{name}\",", "") - self._write("\t\"author\" : \"Node To Python\",", "") - self._write("\t\"version\" : (1, 0, 0),", "") + self._write(f"\t\"name\" : {str_to_py_str(name)},", "") + self._write(f"\t\"author\" : {str_to_py_str(self._author_name)},", "") + self._write(f"\t\"version\" : {vec3_to_py_str(self._version)},", "") self._write(f"\t\"blender\" : {bpy.app.version},", "") self._write("\t\"location\" : \"Object\",", "") # TODO self._write("\t\"category\" : \"Node\"", "") @@ -1370,7 +1372,7 @@ def _report_finished(self, object: str): object (str): the copied node tree or encapsulating structure (geometry node modifier, material, scene, etc.) """ - if self.mode == 'SCRIPT': + if self._mode == 'SCRIPT': location = "clipboard" else: location = self._dir diff --git a/options.py b/options.py index 5d44712..3b64e05 100644 --- a/options.py +++ b/options.py @@ -42,6 +42,16 @@ class NTPOptions(bpy.types.PropertyGroup): description="Save location if generating an add-on", default = "//" ) + author_name : bpy.props.StringProperty( + name = "Author", + description = "Name used for the author/maintainer of the add-on", + default = "Node To Python" + ) + version: bpy.props.IntVectorProperty( + name = "Version", + description="Version of the add-on", + default = (1, 0, 0) + ) class NTPOptionsPanel(bpy.types.Panel): bl_label = "Options" @@ -74,7 +84,9 @@ def draw(self, context): option_list += script_options elif ntp_options.mode == 'ADDON': addon_options = [ - "dir_path" + "dir_path", + "author_name", + "version" ] option_list += addon_options diff --git a/shader/operator.py b/shader/operator.py index 858883e..3a36c7e 100644 --- a/shader/operator.py +++ b/shader/operator.py @@ -140,7 +140,7 @@ def execute(self, context): #set up names to use in generated addon mat_var = clean_string(self.material_name) - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._outer = "\t\t" self._inner = "\t\t\t" @@ -159,9 +159,9 @@ def execute(self, context): if self._include_imports: self._file.write("import bpy, mathutils\n\n") - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._create_material("\t\t") - elif self.mode == 'SCRIPT': + elif self._mode == 'SCRIPT': self._create_material("") node_trees_to_process = self._topological_sort(self._base_node_tree) @@ -169,7 +169,7 @@ def execute(self, context): for node_tree in node_trees_to_process: self._process_node_tree(node_tree) - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._write("return {'FINISHED'}", self._outer) self._create_menu_func() self._create_register_func() @@ -180,7 +180,7 @@ def execute(self, context): self._file.close() - if self.mode == 'ADDON': + if self._mode == 'ADDON': self._zip_addon() self._report_finished("material") From ead5f942f1e13fc2afe2a4621f62ab5cecb5c1cb Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 3 Aug 2024 15:51:05 -0500 Subject: [PATCH 08/10] feat: more bl_info fields, option for menu location --- ntp_operator.py | 49 +++++++++++++++++++++------------ options.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 19 deletions(-) diff --git a/ntp_operator.py b/ntp_operator.py index 38584a1..94341ac 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -67,9 +67,6 @@ def __init__(self): # File (TextIO) or string (StringIO) the add-on/script is generated into self._file: TextIO = None - # Path to the current directory - self._dir: str = None - # Path to the directory of the zip file self._zip_dir: str = None @@ -117,14 +114,27 @@ def _write(self, string: str, indent: str = None): self._file.write(f"{indent}{string}\n") def _setup_options(self, options: NTPOptions) -> None: + # General self._mode = options.mode - self._include_imports = options.include_imports self._include_group_socket_values = options.include_group_socket_values self._should_set_dimensions = options.set_dimensions if bpy.app.version >= (3, 4, 0): self._set_unavailable_defaults = options.set_unavailable_defaults - self._author_name = options.author_name - self._version = options.version + + #Script + if options.mode == 'SCRIPT': + self._include_imports = options.include_imports + #Addon + elif options.mode == 'ADDON': + self._dir_path = bpy.path.abspath(options.dir_path) + self._name_override = options.name_override + self._description = options.description + self._author_name = options.author_name + self._version = options.version + self._location = options.location + self._category = options.category + self._custom_category = options.custom_category + self._menu_id = options.menu_id def _setup_addon_directories(self, context: Context, nt_var: str) -> bool: """ @@ -137,15 +147,13 @@ def _setup_addon_directories(self, context: Context, nt_var: str) -> bool: Returns: (bool): success of addon directory setup """ - # find base directory to save new addon - self._dir = bpy.path.abspath(context.scene.ntp_options.dir_path) - if not self._dir or self._dir == "": + if not self._dir_path or self._dir_path == "": self.report({'ERROR'}, ("NodeToPython: No save location found. Please select " "one in the NodeToPython Options panel")) return False - self._zip_dir = os.path.join(self._dir, nt_var) + self._zip_dir = os.path.join(self._dir_path, nt_var) self._addon_dir = os.path.join(self._zip_dir, nt_var) if not os.path.exists(self._addon_dir): @@ -163,12 +171,19 @@ def _create_header(self, name: str) -> None: """ self._write("bl_info = {", "") + if self._name_override and self._name_override != "": + name = self._name_override self._write(f"\t\"name\" : {str_to_py_str(name)},", "") + if self._description and self._description != "": + self.write(f"\t\"description\" : {str_to_py_str(self._description)}," "") self._write(f"\t\"author\" : {str_to_py_str(self._author_name)},", "") self._write(f"\t\"version\" : {vec3_to_py_str(self._version)},", "") self._write(f"\t\"blender\" : {bpy.app.version},", "") - self._write("\t\"location\" : \"Object\",", "") # TODO - self._write("\t\"category\" : \"Node\"", "") + self._write(f"\t\"location\" : {str_to_py_str(self._location)},", "") + category = self._category + if category == "Custom": + category = self._custom_category + self._write(f"\t\"category\" : {str_to_py_str(category)},", "") self._write("}\n", "") self._write("import bpy", "") self._write("import mathutils", "") @@ -185,8 +200,8 @@ def _init_operator(self, idname: str, label: str) -> None: label (str): appearence inside Blender """ self._write(f"class {self._class_name}(bpy.types.Operator):", "") - self._write(f"\tbl_idname = \"object.{idname}\"", "") - self._write(f"\tbl_label = \"{label}\"", "") + self._write(f"\tbl_idname = \"node.{idname}\"", "") + self._write(f"\tbl_label = {str_to_py_str(label)}", "") self._write("\tbl_options = {\'REGISTER\', \'UNDO\'}", "") self._write("") @@ -1330,7 +1345,7 @@ def _create_register_func(self) -> None: """ self._write("def register():", "") self._write(f"bpy.utils.register_class({self._class_name})", "\t") - self._write("bpy.types.VIEW3D_MT_object.append(menu_func)", "\t") + self._write(f"bpy.types.{self._menu_id}.append(menu_func)", "\t") self._write("") def _create_unregister_func(self) -> None: @@ -1339,7 +1354,7 @@ def _create_unregister_func(self) -> None: """ self._write("def unregister():", "") self._write(f"bpy.utils.unregister_class({self._class_name})", "\t") - self._write("bpy.types.VIEW3D_MT_object.remove(menu_func)", "\t") + self._write(f"bpy.types.{self._menu_id}.remove(menu_func)", "\t") self._write("") def _create_main_func(self) -> None: @@ -1375,7 +1390,7 @@ def _report_finished(self, object: str): if self._mode == 'SCRIPT': location = "clipboard" else: - location = self._dir + location = self._dir_path self.report({'INFO'}, f"NodeToPython: Saved {object} to {location}") # ABSTRACT diff --git a/options.py b/options.py index 3b64e05..47871df 100644 --- a/options.py +++ b/options.py @@ -42,6 +42,16 @@ class NTPOptions(bpy.types.PropertyGroup): description="Save location if generating an add-on", default = "//" ) + name_override : bpy.props.StringProperty( + name = "Name Override", + description="Name used for the add-on's, default is node group name", + default = "" + ) + description : bpy.props.StringProperty( + name = "Description", + description="Description used for the add-on", + default="" + ) author_name : bpy.props.StringProperty( name = "Author", description = "Name used for the author/maintainer of the add-on", @@ -52,7 +62,61 @@ class NTPOptions(bpy.types.PropertyGroup): description="Version of the add-on", default = (1, 0, 0) ) - + location: bpy.props.StringProperty( + name = "Location", + description="Location of the addon", + default="Node" + ) + menu_id: bpy.props.StringProperty( + name = "Menu ID", + description = "Python ID of the menu you'd like to register the add-on " + "to. You can find this by enabling Python tooltips " + "(Preferences > Interface > Python tooltips) and " + "hovering over the desired menu", + default="NODE_MT_add" + ) + category: bpy.props.EnumProperty( + name = "Category", + items = [ + ('Custom', "Custom", "Use an unofficial category"), + ('3D View', "3D View", ""), + ('Add Curve', "Add Curve", ""), + ('Add Mesh', "Add Mesh", ""), + ('Animation', "Animation", ""), + ('Bake', "Bake", ""), + ('Compositing', "Compositing", ""), + ('Development', "Development", ""), + ('Game Engine', "Game Engine", ""), + ('Geometry Nodes', "Geometry Nodes", ""), + ("Grease Pencil", "Grease Pencil", ""), + ('Import-Export', "Import-Export", ""), + ('Lighting', "Lighting", ""), + ('Material', "Material", ""), + ('Mesh', "Mesh", ""), + ('Modeling', "Modeling", ""), + ('Node', "Node", ""), + ('Object', "Object", ""), + ('Paint', "Paint", ""), + ('Pipeline', "Pipeline", ""), + ('Physics', "Physics", ""), + ('Render', "Render", ""), + ('Rigging', "Rigging", ""), + ('Scene', "Scene", ""), + ('Sculpt', "Sculpt", ""), + ('Sequencer', "Sequencer", ""), + ('System', "System", ""), + ('Text Editor', "Text Editor", ""), + ('Tracking', "Tracking", ""), + ('UV', "UV", ""), + ('User Interface', "User Interface", ""), + ], + default = 'Node' + ) + custom_category: bpy.props.StringProperty( + name="Custom Category", + description="Custom category", + default = "" + ) class NTPOptionsPanel(bpy.types.Panel): bl_label = "Options" bl_idname = "NODE_PT_ntp_options" @@ -86,9 +150,14 @@ def draw(self, context): addon_options = [ "dir_path", "author_name", - "version" + "version", + "location", + "menu_id", + "category" ] option_list += addon_options + if ntp_options.category == 'CUSTOM': + option_list.append("custom_category") for option in option_list: layout.prop(ntp_options, option) \ No newline at end of file From fbcf139347d23694ccecb0dec7a32702b43b1f61 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 3 Aug 2024 15:57:42 -0500 Subject: [PATCH 09/10] feat: error checking on menu type --- compositor/operator.py | 3 ++- geometry/operator.py | 3 ++- ntp_operator.py | 9 +++++++-- shader/operator.py | 3 ++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index 126c0b1..f7ff414 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -185,7 +185,8 @@ def _process_node_tree(self, node_tree: CompositorNodeTree): self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) def execute(self, context): - self._setup_options(context.scene.ntp_options) + if not self._setup_options(context.scene.ntp_options): + return {'CANCELLED'} #find node group to replicate if self.is_scene: diff --git a/geometry/operator.py b/geometry/operator.py index 788f344..508fd08 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -171,7 +171,8 @@ def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): def execute(self, context): - self._setup_options(context.scene.ntp_options) + if not self._setup_options(context.scene.ntp_options): + return {'CANCELLED'} #find node group to replicate nt = bpy.data.node_groups[self.geo_nodes_group_name] diff --git a/ntp_operator.py b/ntp_operator.py index 94341ac..6f7a113 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -113,7 +113,7 @@ def _write(self, string: str, indent: str = None): indent = self._inner self._file.write(f"{indent}{string}\n") - def _setup_options(self, options: NTPOptions) -> None: + def _setup_options(self, options: NTPOptions) -> bool: # General self._mode = options.mode self._include_group_socket_values = options.include_group_socket_values @@ -134,7 +134,12 @@ def _setup_options(self, options: NTPOptions) -> None: self._location = options.location self._category = options.category self._custom_category = options.custom_category - self._menu_id = options.menu_id + if options.menu_id in dir(bpy.types): + self._menu_id = options.menu_id + else: + self.report({'ERROR'}, f"{options.menu_id} is not a valid menu") + return False + return True def _setup_addon_directories(self, context: Context, nt_var: str) -> bool: """ diff --git a/shader/operator.py b/shader/operator.py index 3a36c7e..bd136fe 100644 --- a/shader/operator.py +++ b/shader/operator.py @@ -128,7 +128,8 @@ def _process_node_tree(self, node_tree: ShaderNodeTree) -> None: def execute(self, context): - self._setup_options(context.scene.ntp_options) + if not self._setup_options(context.scene.ntp_options): + return {'CANCELLED'} #find node group to replicate self._base_node_tree = bpy.data.materials[self.material_name].node_tree From e97aad738491896aa2073b4b5e9ecea42f5e0262 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 3 Aug 2024 16:01:19 -0500 Subject: [PATCH 10/10] fix: throw warning if image data is invalid --- ntp_operator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ntp_operator.py b/ntp_operator.py index 6f7a113..adee2b8 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -1048,13 +1048,19 @@ def _save_image(self, img: bpy.types.Image) -> None: if img is None: return + img_str = img_to_py_str(img) + + if not img.has_data: + self.report({'WARNING'}, f"{img_str} has no data") + return + # create image dir if one doesn't exist img_dir = os.path.join(self._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)