From 2b769e03b73970956f5ffc85229438e55f92bb81 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 24 Jun 2023 19:33:20 -0500 Subject: [PATCH 1/4] feat: added option panel and save directory --- __init__.py | 35 +++++++++++++++++++++++++++++++++-- geo_nodes.py | 11 +++++++---- materials.py | 6 +++--- utils.py | 2 -- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/__init__.py b/__init__.py index 0ccd674..9f77d96 100644 --- a/__init__.py +++ b/__init__.py @@ -21,7 +21,7 @@ class NodeToPythonMenu(bpy.types.Menu): bl_idname = "NODE_MT_node_to_python" bl_label = "Node To Python" - + @classmethod def poll(cls, context): return True @@ -30,21 +30,52 @@ def draw(self, context): layout = self.layout.column_flow(columns=1) layout.operator_context = 'INVOKE_DEFAULT' + +class NTPOptions(bpy.types.PropertyGroup): + 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" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + @classmethod + def poll(cls, context): + return True + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_DEFAULT' + layout.prop(context.scene.ntp_options, "dir_path") + classes = [NodeToPythonMenu, + NTPOptions, geo_nodes.GeoNodesToPython, geo_nodes.SelectGeoNodesMenu, geo_nodes.GeoNodesToPythonPanel, materials.MaterialToPython, materials.SelectMaterialMenu, - materials.MaterialToPythonPanel + materials.MaterialToPythonPanel, + NTPOptionsPanel ] def register(): for cls in classes: bpy.utils.register_class(cls) + scene = bpy.types.Scene + scene.ntp_options = bpy.props.PointerProperty(type=NTPOptions) + def unregister(): for cls in classes: bpy.utils.unregister_class(cls) + del bpy.types.Scene.ntp_options if __name__ == "__main__": register() \ No newline at end of file diff --git a/geo_nodes.py b/geo_nodes.py index e20b985..07d5a56 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -176,6 +176,7 @@ class GeoNodesToPython(bpy.types.Operator): ('ADDON', "Addon", "Create a full addon") ] ) + geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") def execute(self, context): @@ -187,16 +188,17 @@ def execute(self, context): if self.mode == 'ADDON': #find base directory to save new addon - base_dir = bpy.path.abspath("//") - if not base_dir or base_dir == "": + dir = bpy.path.abspath(context.scene.ntp_options.dir_path) + if not dir or dir == "": self.report({'ERROR'}, ("NodeToPython: Save your blend file before using " "NodeToPython!")) return {'CANCELLED'} #save in addons/ subdirectory - zip_dir = os.path.join(base_dir, "addons", nt_var) + zip_dir = os.path.join(dir, nt_var) addon_dir = os.path.join(zip_dir, nt_var) + if not os.path.exists(addon_dir): os.makedirs(addon_dir) file = open(f"{addon_dir}/__init__.py", "w") @@ -358,7 +360,8 @@ def apply_modifier(): if self.mode == 'ADDON': zip_addon(zip_dir) - self.report({'INFO'}, "NodeToPython: Saved geometry nodes group") + self.report({'INFO'}, + f"NodeToPython: Saved geometry nodes group to {dir}") return {'FINISHED'} def invoke(self, context, event): diff --git a/materials.py b/materials.py index eea30fb..7f244b8 100644 --- a/materials.py +++ b/materials.py @@ -103,7 +103,7 @@ def execute(self, context): mat_var = clean_string(self.material_name) if self.mode == 'ADDON': - dir = bpy.path.abspath("//") + dir = bpy.path.abspath(context.scene.ntp_options.dir_path) if not dir or dir == "": self.report({'ERROR'}, ("NodeToPython: Save your blender file before using " @@ -111,7 +111,7 @@ def execute(self, context): return {'CANCELLED'} #save in addons/ subdirectory - zip_dir = os.path.join(dir, "addons", mat_var) + zip_dir = os.path.join(dir, mat_var) addon_dir = os.path.join(zip_dir, mat_var) if not os.path.exists(addon_dir): os.makedirs(addon_dir) @@ -258,7 +258,7 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): if self.mode == 'ADDON': zip_addon(zip_dir) - self.report({'INFO'}, "NodeToPython: Saved material") + self.report({'INFO'}, f"NodeToPython: Saved material to {dir}") return {'FINISHED'} def invoke(self, context, event): diff --git a/utils.py b/utils.py index 41d831b..1b20533 100644 --- a/utils.py +++ b/utils.py @@ -474,7 +474,6 @@ def set_input_defaults(node, file: TextIO, inner: str, node_var: str, #images elif input.bl_idname == 'NodeSocketImage': - print("Input is linked: ", input.is_linked) img = input.default_value if img is not None and addon_dir != "": #write in a better way save_image(img, addon_dir) @@ -719,7 +718,6 @@ def save_image(img, addon_dir: str): #save the image img_str = img_to_py_str(img) img_path = f"{img_dir}/{img_str}" - print("Image Path: ", img_path) if not os.path.exists(img_path): img.save_render(img_path) From 199661b960224fe0e4ac98881601dac6d754759f Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 24 Jun 2023 19:40:31 -0500 Subject: [PATCH 2/4] fix: info popup now chooses between clipboard and directory --- geo_nodes.py | 8 +++++++- materials.py | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 07d5a56..ffda531 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -360,8 +360,14 @@ def apply_modifier(): if self.mode == 'ADDON': zip_addon(zip_dir) + + #alert user that NTP is finished + if self.mode == 'SCRIPT': + location = "clipboard" + else: + location = dir self.report({'INFO'}, - f"NodeToPython: Saved geometry nodes group to {dir}") + f"NodeToPython: Saved geometry nodes group to {location}") return {'FINISHED'} def invoke(self, context, event): diff --git a/materials.py b/materials.py index 7f244b8..46be769 100644 --- a/materials.py +++ b/materials.py @@ -258,7 +258,11 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): if self.mode == 'ADDON': zip_addon(zip_dir) - self.report({'INFO'}, f"NodeToPython: Saved material to {dir}") + if self.mode == 'SCRIPT': + location = "clipboard" + else: + location = dir + self.report({'INFO'}, f"NodeToPython: Saved material to {location}") return {'FINISHED'} def invoke(self, context, event): From aa0aa442febd32257e20712bb20c1846f8270080 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Tue, 4 Jul 2023 20:39:28 -0500 Subject: [PATCH 3/4] refactor: moved options to separate module --- __init__.py | 30 +++++------------------------- options.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 25 deletions(-) create mode 100644 options.py diff --git a/__init__.py b/__init__.py index 9f77d96..201080e 100644 --- a/__init__.py +++ b/__init__.py @@ -12,9 +12,11 @@ import importlib importlib.reload(materials) importlib.reload(geo_nodes) + importlib.reload(options) else: from . import materials from . import geo_nodes + from . import options import bpy @@ -31,46 +33,24 @@ def draw(self, context): layout.operator_context = 'INVOKE_DEFAULT' -class NTPOptions(bpy.types.PropertyGroup): - 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" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_context = '' - bl_category = "NodeToPython" - - @classmethod - def poll(cls, context): - return True - def draw(self, context): - layout = self.layout - layout.operator_context = 'INVOKE_DEFAULT' - layout.prop(context.scene.ntp_options, "dir_path") classes = [NodeToPythonMenu, - NTPOptions, + options.NTPOptions, geo_nodes.GeoNodesToPython, geo_nodes.SelectGeoNodesMenu, geo_nodes.GeoNodesToPythonPanel, materials.MaterialToPython, materials.SelectMaterialMenu, materials.MaterialToPythonPanel, - NTPOptionsPanel + options.NTPOptionsPanel ] def register(): for cls in classes: bpy.utils.register_class(cls) scene = bpy.types.Scene - scene.ntp_options = bpy.props.PointerProperty(type=NTPOptions) + scene.ntp_options = bpy.props.PointerProperty(type=options.NTPOptions) def unregister(): for cls in classes: diff --git a/options.py b/options.py new file mode 100644 index 0000000..09c8310 --- /dev/null +++ b/options.py @@ -0,0 +1,28 @@ +import bpy + +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 = "//" + ) + +class NTPOptionsPanel(bpy.types.Panel): + bl_label = "Options" + bl_idname = "NODE_PT_ntp_options" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + @classmethod + def poll(cls, context): + return True + 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 From 7ac4da5874f236762e16ed1dddd5214351024a38 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Tue, 4 Jul 2023 21:16:16 -0500 Subject: [PATCH 4/4] refactor: variable names use dictionary with counts instead of set, cleanup --- geo_nodes.py | 25 ++++++++++++++++++------- materials.py | 7 ++++--- utils.py | 17 ++++++++--------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index ffda531..c70f219 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -217,8 +217,8 @@ def execute(self, context): #dictionary to keep track of node->variable name pairs node_vars = {} - #keeps track of all used variables - used_vars = set() + #dictionary to keep track of variables->usage count pairs + used_vars = {} def process_geo_nodes_group(node_tree, level, node_vars, used_vars): nt_var = create_var(node_tree.name, used_vars) @@ -250,18 +250,20 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): used_vars) node_trees.add(node_nt) elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - group_io_settings(node, file, inner, "input", nt_var, node_tree) + group_io_settings(node, file, inner, "input", nt_var, + node_tree) inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: - group_io_settings(node, file, inner, "output", nt_var, node_tree) + group_io_settings(node, file, inner, "output", nt_var, + node_tree) outputs_set = True #create node node_var = create_node(node, file, inner, nt_var, - node_vars, used_vars) + node_vars, used_vars) set_settings_defaults(node, geo_node_settings, file, inner, - node_var) + node_var) hide_sockets(node, file, inner, node_var) if node.bl_idname == 'GeometryNodeGroup': @@ -269,21 +271,27 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): file.write((f"{inner}{node_var}.node_tree = " f"bpy.data.node_groups" f"[{str_to_py_str(node.node_tree.name)}]\n")) + elif node.bl_idname == 'ShaderNodeValToRGB': color_ramp_settings(node, file, inner, node_var) + elif node.bl_idname in curve_nodes: curve_node_settings(node, file, inner, node_var) + elif node.bl_idname in image_nodes and self.mode == 'ADDON': img = node.image if img is not None and img.source in {'FILE', 'GENERATED', 'TILED'}: save_image(img, addon_dir) load_image(img, file, inner, f"{node_var}.image") + elif node.bl_idname == 'GeometryNodeSimulationInput': sim_inputs.append(node) + elif node.bl_idname == 'GeometryNodeSimulationOutput': file.write(f"{inner}#remove generated sim state items\n") file.write(f"{inner}for item in {node_var}.state_items:\n") file.write(f"{inner}\t{node_var}.state_items.remove(item)\n") + for i, si in enumerate(node.state_items): socket_type = enum_to_py_str(si.socket_type) name = str_to_py_str(si.name) @@ -316,10 +324,12 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): set_input_defaults(node, file, inner, node_var) set_output_defaults(sim_input, file, inner, sim_input_var) + #set look of nodes set_parents(node_tree, file, inner, node_vars) set_locations(node_tree, file, inner, node_vars) set_dimensions(node_tree, file, inner, node_vars) + #create connections init_links(node_tree, file, inner, nt_var, node_vars) file.write(f"{inner}return {nt_var}\n") @@ -388,7 +398,8 @@ def draw(self, context): 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'] + 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(GeoNodesToPython.bl_idname, text=geo_ng.name) diff --git a/materials.py b/materials.py index 46be769..466bee5 100644 --- a/materials.py +++ b/materials.py @@ -110,7 +110,6 @@ def execute(self, context): "NodeToPython!")) return {'CANCELLED'} - #save in addons/ subdirectory zip_dir = os.path.join(dir, mat_var) addon_dir = os.path.join(zip_dir, mat_var) if not os.path.exists(addon_dir): @@ -129,6 +128,7 @@ def create_material(indent: str): file.write((f"{indent}mat = bpy.data.materials.new(" f"name = {str_to_py_str(self.material_name)})\n")) file.write(f"{indent}mat.use_nodes = True\n") + if self.mode == 'ADDON': create_material("\t\t") elif self.mode == 'SCRIPT': @@ -141,7 +141,7 @@ def create_material(indent: str): node_vars = {} #keeps track of all used variables - used_vars = set() + used_vars = {} def is_outermost_node_group(level: int) -> bool: if self.mode == 'ADDON' and level == 2: @@ -189,7 +189,8 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): if node.bl_idname == 'ShaderNodeGroup': node_nt = node.node_tree if node_nt is not None and node_nt not in node_trees: - process_mat_node_group(node_nt, level + 1, node_vars, used_vars) + process_mat_node_group(node_nt, level + 1, node_vars, + used_vars) node_trees.add(node_nt) node_var = create_node(node, file, inner, nt_var, node_vars, diff --git a/utils.py b/utils.py index 1b20533..d416d6b 100644 --- a/utils.py +++ b/utils.py @@ -132,13 +132,13 @@ def init_operator(file: TextIO, name: str, idname: str, label: str): file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") file.write("\n") -def create_var(name: str, used_vars: set) -> str: +def create_var(name: str, used_vars: dict) -> str: """ Creates a unique variable name for a node tree Parameters: name (str): basic string we'd like to create the variable name out of - used_vars (set): set containing all used variable names so far + used_vars (dict): dictionary containing variable names and usage counts Returns: clean_name (str): variable name for the node tree @@ -147,13 +147,12 @@ def create_var(name: str, used_vars: set) -> str: name = "unnamed" clean_name = clean_string(name) var = clean_name - i = 0 - while var in used_vars: - i += 1 - var = f"{clean_name}_{i}" - - used_vars.add(var) - return var + if var in used_vars: + used_vars[var] += 1 + return f"{clean_name}_{used_vars[var]}" + else: + used_vars[var] = 0 + return clean_name def make_indents(level: int) -> Tuple[str, str]: """