from pydoc import doc import bpy import time from ..utils import indent_code, normalize_code from ..node_tree.sockets.conversions import CONVERT_UTILS from ..addon.properties.compiler_properties import ( property_imperative_code, property_register_code, property_unregister_code, ) from ..addon.variables.compiler_variables import ( ntree_variable_register_code, variable_register_code, ) def unregister_addon(): """Unregisters this addon""" sn = bpy.context.scene.sn t1 = time.time() # if sn.addon_unregister: # try: # sn.addon_unregister[0]() # except Exception as error: # print("error when unregister:", error) # sn.addon_unregister.clear() if sn.addon_modules: try: sn.addon_modules[0].unregister() except Exception as error: print("error when unregister:", error) sn.addon_modules.clear() if sn.debug_compile_time: print(f"---\nUnregister took {round((time.time()-t1)*1000, 2)}ms") def compile_addon(): """Reregisters the current addon code and stores results""" if ( not bpy.context.scene.sn.pause_reregister and not bpy.context.scene.sn.is_exporting ): t1 = time.time() sn = bpy.context.scene.sn # Unregister previous version unregister_addon() # create text file txt = bpy.data.texts.new("tmp_serpens") txt.use_fake_user = False t2 = time.time() code = format_single_file() # code += "\nbpy.context.scene.sn.addon_unregister.append(unregister)" # code += "\nregister()" if sn.debug_compile_time: print(f"Generating code took {round((time.time()-t2)*1000, 2)}ms") txt.write(code) if sn.debug_code: if not "serpens_code_log" in bpy.data.texts: log = bpy.data.texts.new("serpens_code_log") log = bpy.data.texts["serpens_code_log"] log.clear() log.write(code) # run text file t2 = time.time() # ctx = bpy.context.copy() # ctx['edit_text'] = txt try: # exec(txt.as_string()) # bpy.ops.text.run_script(ctx) mod = bpy.data.texts["tmp_serpens"].as_module() sn.addon_modules.append(mod) mod.register() print( "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" ) print( "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" ) print("Compiled successfully!") except Exception as error: print(error) print("^ ERROR WHEN REGISTERING SERPENS ADDON ^\n") if bpy.context.preferences.addons[ __name__.partition(".")[0] ].preferences.keep_last_error_file: if not "serpens_error" in bpy.data.texts: bpy.data.texts.new("serpens_error") err = bpy.data.texts["serpens_error"] err.clear() err.write(code) if sn.debug_compile_time: print(f"Register took {round((time.time()-t2)*1000, 2)}ms\n---") # remove text file bpy.data.texts.remove(txt) sn.compile_time = time.time() - t1 LICENSE = """# This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ DEFAULT_IMPORTS = """ import bpy import bpy.utils.previews """ GLOBAL_VARS = """ addon_keymaps = {} _icons = None """ REGISTER = """ global _icons _icons = bpy.utils.previews.new() """ UNREGISTER = """ global _icons bpy.utils.previews.remove(_icons) wm = bpy.context.window_manager kc = wm.keyconfigs.addon for km, kmi in addon_keymaps.values(): km.keymap_items.remove(kmi) addon_keymaps.clear() """ def format_single_file(): """Returns the entire addon code (for development) formatted for a single python file""" sn = bpy.context.scene.sn imports, imperative, main, register, unregister = ( DEFAULT_IMPORTS, CONVERT_UTILS + GLOBAL_VARS, "", REGISTER, UNREGISTER, ) # add property and variable code t1 = time.time() imperative += variable_register_code() + "\n" t2 = time.time() register += property_register_code() + "\n" t3 = time.time() unregister += property_unregister_code() + "\n" t4 = time.time() # add node code for node in get_trigger_nodes(): if node.code_import and not node.code_import in imports: imports += "\n" + node.code_import if node.code_imperative and not node.code_imperative in imperative: imperative += "\n" + node.code_imperative if node.code: main += "\n" + node.code if node.code_register: register += "\n" + node.code_register if node.code_unregister: unregister += "\n" + node.code_unregister t5 = time.time() # add property code main += "\n" + property_imperative_code() + "\n" t6 = time.time() # add module store code if not sn.is_exporting: register += ( "\n\nimport sys\nbpy.context.scene.sn.module_store.append([globals()])\n" ) unregister += "\n\nbpy.context.scene.sn.module_store.clear()\n" # format register functions if not register.strip(): register = "pass\n" if not unregister.strip(): unregister = "pass\n" code = f"{imports}\n{imperative}\n{main}\n\ndef register():\n{indent_code(register, 1, 0)}\n\ndef unregister():\n{indent_code(unregister, 1, 0)}\n\n" t7 = time.time() if (sn.remove_duplicate_code and sn.debug_code) or sn.is_exporting: code = remove_duplicates(code) t8 = time.time() if (sn.format_code and sn.debug_code) or sn.is_exporting: code = format_linebreaks(code) t9 = time.time() if sn.is_exporting: code = f"{info()}\n{code}" code = f"{LICENSE}\n{code}" if sn.debug_compile_time: print(f"--Variable register code generation took {round((t2-t1)*1000, 2)}ms") print(f"--Property register code generation took {round((t3-t2)*1000, 2)}ms") print(f"--Property unregister code generation took {round((t4-t3)*1000, 2)}ms") print(f"--Node code generation took {round((t5-t4)*1000, 2)}ms") print(f"--Property imperative code generation took {round((t6-t5)*1000, 2)}ms") print(f"--Joining code took {round((t7-t6)*1000, 2)}ms") print(f"--Removing duplicate code took {round((t8-t7)*1000, 2)}ms") print(f"--Formatting linebreaks took {round((t9-t8)*1000, 2)}ms") return code def remove_duplicates(code): code = remove_duplicate_functions(code) if bpy.context.scene.sn.is_exporting: code = remove_duplicate_functions(code) code = remove_duplicate_imports(code) return code def remove_duplicate_functions(code): lines = code.split("\n") functions = [] remove = [] for line in lines: if len(line) > 3 and line[:3] == "def": func = line.split("(")[0].split(" ")[-1] if func in functions or code.count(func) == 1: if not func in ["register", "unregister"]: remove.append(func) else: functions.append(func) newLines = [] inFunc = False for line in lines: if inFunc and len(line) - len(line.lstrip()) == 0: inFunc = False if not inFunc: if len(line) > 3 and line[:3] == "def" and "(" in line: for func in remove: if line.split("(")[0] == f"def {func}": remove.remove(func) inFunc = True break if not inFunc: newLines.append(line) return "\n".join(newLines) def remove_duplicate_imports(code): imports = [] newLines = [] for line in code.split("\n"): if line[:6] == "import" or (line[:4] == "from" and "import" in line): if not line.strip() in imports: imports.append(line.strip()) newLines.append(line) else: newLines.append(line) return "\n".join(newLines) def format_linebreaks(code): lines = code.split("\n") newLines = [] for line in lines: if line.strip(): # insert linebreaks for lines with no indent if len(line) - len(line.lstrip()) == 0: # linebreak for going from indents to no indents if newLines and len(newLines[-1]) - len(newLines[-1].lstrip()) > 0: newLines.append("\n") # linebreak for imperative functions elif ( newLines and len(line) > 3 and len(newLines[-1].strip()) and line[:3] == "def" and not newLines[-1][0] == "@" ): newLines.append("\n") # linebreak for imperative functions elif newLines and "import" in line and not "import" in newLines[-1]: newLines.append("\n") # insert linebreaks for lines with indent elif newLines: # linebreak for decorated functions in classes if line and line.lstrip()[0] == "@": newLines.append("") # linebreak for functions without decorator elif ( len(line) > 3 and len(newLines[-1].strip()) and line.lstrip()[:3] == "def" and not newLines[-1].lstrip()[0] == "@" ): newLines.append("") newLines.append(line) # insert linebreaks after last import for i in range(len(newLines)): if ( "import" in newLines[i] and i < len(newLines) - 1 and not "import" in newLines[i + 1] ): newLines.insert(i + 1, "\n") break return "\n".join(newLines) + "\n" def get_trigger_nodes(): """Returns a list of all trigger nodes in all node trees""" nodes = [] for ntree in bpy.data.node_groups: if ntree.bl_idname == "ScriptingNodesTree": for node in ntree.nodes: if getattr(node, "is_trigger", False): nodes.append(node) nodes = sorted(nodes, key=lambda node: node.order) return nodes def info(): """Returns the bl_info for this addon""" sn = bpy.context.scene.sn info = f""" bl_info = {{ "name" : "{sn.addon_name}", "author" : "{sn.author}", "description" : "{sn.description}", "blender" : {tuple(sn.blender)}, "version" : {tuple(sn.version)}, "location" : "{sn.location}", "warning" : "{sn.warning}", "doc_url": "{sn.doc_url}", "tracker_url": "{sn.tracker_url}", "category" : "{sn.category if not sn.category == 'CUSTOM' else sn.custom_category}" }} """ return normalize_code(info) + "\n" + "\n" def format_multifile(): """Returns the code for the entire addon as a dictionary of multiple files""" files = {} register, unregister = "", "" for ntree in bpy.data.node_groups: if ntree.bl_idname == "ScriptingNodesTree": code, ntree_register, ntree_unregister = format_node_tree(ntree) files[ntree.python_name + ".py"] = code register += "\n" + ntree_register + "\n" unregister += "\n" + ntree_unregister + "\n" files["__init__.py"] = format_multifile_init(register, unregister) files["blender_manifest.toml"] = format_blender_manifest() return files def format_node_tree(ntree): imperative, main, register, unregister = (CONVERT_UTILS, "", "", "") imports = "import bpy\nfrom . import addon_keymaps, _icons\n" import_ntrees = "" for group in bpy.data.node_groups: if group != ntree and group.bl_idname == "ScriptingNodesTree": imports += f"from .{group.python_name} import *\n" import_ntrees += f"{group.python_name}, " if import_ntrees: imports += f"from . import {import_ntrees[:-2]}\n" imperative += ntree_variable_register_code(ntree) + "\n" nodes = [] for node in ntree.nodes: if getattr(node, "is_trigger", False): nodes.append(node) for node in nodes: if node.code_import and not node.code_import in imports: imports += "\n" + node.code_import if node.code_imperative and not node.code_imperative in imperative: imperative += "\n" + node.code_imperative if node.code: main += "\n" + node.code if node.code_register: register += "\n" + node.code_register if node.code_unregister: unregister += "\n" + node.code_unregister code = imperative + "\n" + main for group in bpy.data.node_groups: if group != ntree and group.bl_idname == "ScriptingNodesTree": code = code.replace( group.python_name, f"{group.python_name}.{group.python_name}" ) code = imports + "\n" + code code = remove_duplicates(code) code = format_linebreaks(code) return code, register, unregister def format_multifile_init(node_register, node_unregister): imports, imperative, main, register, unregister = ( DEFAULT_IMPORTS, CONVERT_UTILS + GLOBAL_VARS, "", REGISTER, UNREGISTER, ) for ntree in bpy.data.node_groups: if ntree.bl_idname == "ScriptingNodesTree": imports += f"from .{ntree.python_name} import *\n" main += "\n" + property_imperative_code() + "\n" register += property_register_code() + "\n" + node_register + "\n" unregister += property_unregister_code() + "\n" + node_unregister + "\n" register = "def register():\n" + indent_code(register, 1, 0) unregister = "def unregister():\n" + indent_code(unregister, 1, 0) code = ( imports + "\n" + imperative + "\n" + main + "\n" + register + "\n" + unregister ) code = remove_duplicates(code) code = format_linebreaks(code) code = f"{info()}\n{code}" code = f"{LICENSE}\n{code}" return code def format_blender_manifest(): sn = bpy.context.scene.sn manifest = f""" schema_version = "1.0.0" id = "{sn.module_name}" version = "{'.'.join(map(str, tuple(sn.version)))}" name = "{sn.addon_name}" tagline = "{sn.description if sn.description else sn.addon_name}" maintainer = "{sn.author}" type = "add-on" {'website = "'+sn.doc_url+'"' if sn.doc_url else ""} tags = ["{sn.category if not sn.category == 'CUSTOM' else sn.custom_category}"] blender_version_min = "{'.'.join(map(str, tuple(sn.blender)))}" license = [ "SPDX:GPL-2.0-or-later", ] """ return normalize_code(manifest)