diff --git a/__init__.py b/__init__.py index 0ccd674..201080e 100644 --- a/__init__.py +++ b/__init__.py @@ -12,16 +12,18 @@ 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 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 +32,30 @@ def draw(self, context): layout = self.layout.column_flow(columns=1) layout.operator_context = 'INVOKE_DEFAULT' + + + classes = [NodeToPythonMenu, + options.NTPOptions, geo_nodes.GeoNodesToPython, geo_nodes.SelectGeoNodesMenu, geo_nodes.GeoNodesToPythonPanel, materials.MaterialToPython, materials.SelectMaterialMenu, - materials.MaterialToPythonPanel + materials.MaterialToPythonPanel, + options.NTPOptionsPanel ] def register(): for cls in classes: bpy.utils.register_class(cls) + scene = bpy.types.Scene + scene.ntp_options = bpy.props.PointerProperty(type=options.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..c70f219 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") @@ -215,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) @@ -248,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': @@ -267,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) @@ -314,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") @@ -358,7 +370,14 @@ def apply_modifier(): if self.mode == 'ADDON': zip_addon(zip_dir) - self.report({'INFO'}, "NodeToPython: Saved geometry nodes group") + + #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 {location}") return {'FINISHED'} def invoke(self, context, event): @@ -379,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 eea30fb..466bee5 100644 --- a/materials.py +++ b/materials.py @@ -103,15 +103,14 @@ 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 " "NodeToPython!")) 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) @@ -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, @@ -258,7 +259,11 @@ 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") + 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): 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 diff --git a/utils.py b/utils.py index 41d831b..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]: """ @@ -474,7 +473,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 +717,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)