Files
blender-portable-repo/scripts/addons/blender_visual_scripting_addon/nodes/compiler.py
T
2026-03-17 14:30:01 -06:00

475 lines
15 KiB
Python

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 <http://www.gnu.org/licenses/>.
"""
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)