| 
									
										
										
										
											2025-01-22 16:18:30 +01:00
										 |  |  | import importlib | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import queue | 
					
						
							|  |  |  | import sys | 
					
						
							|  |  |  | import types | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import bpy | 
					
						
							|  |  |  | from bpy.app.handlers import persistent | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import lnx | 
					
						
							|  |  |  | import lnx.api | 
					
						
							|  |  |  | import lnx.nodes_logic | 
					
						
							|  |  |  | import lnx.make_state as state | 
					
						
							|  |  |  | import lnx.utils | 
					
						
							|  |  |  | import lnx.utils_vs | 
					
						
							|  |  |  | from lnx import live_patch, log, make, props | 
					
						
							|  |  |  | from lnx.logicnode import lnx_nodes | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if lnx.is_reload(__name__): | 
					
						
							|  |  |  |     lnx.api = lnx.reload_module(lnx.api) | 
					
						
							|  |  |  |     live_patch = lnx.reload_module(live_patch) | 
					
						
							|  |  |  |     log = lnx.reload_module(log) | 
					
						
							|  |  |  |     lnx_nodes = lnx.reload_module(lnx_nodes) | 
					
						
							|  |  |  |     lnx.nodes_logic = lnx.reload_module(lnx.nodes_logic) | 
					
						
							|  |  |  |     make = lnx.reload_module(make) | 
					
						
							|  |  |  |     state = lnx.reload_module(state) | 
					
						
							|  |  |  |     props = lnx.reload_module(props) | 
					
						
							|  |  |  |     lnx.utils = lnx.reload_module(lnx.utils) | 
					
						
							|  |  |  |     lnx.utils_vs = lnx.reload_module(lnx.utils_vs) | 
					
						
							|  |  |  | else: | 
					
						
							|  |  |  |     lnx.enable_reload(__name__) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @persistent | 
					
						
							|  |  |  | def on_depsgraph_update_post(self): | 
					
						
							|  |  |  |     if state.proc_build is not None: | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Recache | 
					
						
							|  |  |  |     depsgraph = bpy.context.evaluated_depsgraph_get() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for update in depsgraph.updates: | 
					
						
							|  |  |  |         uid = update.id | 
					
						
							|  |  |  |         if hasattr(uid, 'lnx_cached'): | 
					
						
							|  |  |  |             # uid.lnx_cached = False # TODO: does not trigger update | 
					
						
							|  |  |  |             if isinstance(uid, bpy.types.Mesh) and uid.name in bpy.data.meshes: | 
					
						
							|  |  |  |                 bpy.data.meshes[uid.name].lnx_cached = False | 
					
						
							|  |  |  |             elif isinstance(uid, bpy.types.Curve) and uid.name in bpy.data.curves: | 
					
						
							|  |  |  |                 bpy.data.curves[uid.name].lnx_cached = False | 
					
						
							|  |  |  |             elif isinstance(uid, bpy.types.MetaBall) and uid.name in bpy.data.metaballs: | 
					
						
							|  |  |  |                 bpy.data.metaballs[uid.name].lnx_cached = False | 
					
						
							|  |  |  |             elif isinstance(uid, bpy.types.Armature) and uid.name in bpy.data.armatures: | 
					
						
							|  |  |  |                 bpy.data.armatures[uid.name].lnx_cached = False | 
					
						
							|  |  |  |             elif isinstance(uid, bpy.types.NodeTree) and uid.name in bpy.data.node_groups: | 
					
						
							|  |  |  |                 bpy.data.node_groups[uid.name].lnx_cached = False | 
					
						
							|  |  |  |             elif isinstance(uid, bpy.types.Material) and uid.name in bpy.data.materials: | 
					
						
							|  |  |  |                 bpy.data.materials[uid.name].lnx_cached = False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Send last operator to Krom | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     if state.proc_play is not None and state.target == 'krom' and wrd.lnx_live_patch: | 
					
						
							|  |  |  |         ops = bpy.context.window_manager.operators | 
					
						
							|  |  |  |         if len(ops) > 0 and ops[-1] is not None: | 
					
						
							|  |  |  |             live_patch.on_operator(ops[-1].bl_idname) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Hacky solution to update leenkx props after operator executions. | 
					
						
							|  |  |  |     # bpy.context.active_operator doesn't always exist, in some cases | 
					
						
							|  |  |  |     # like marking assets for example, this code is also executed before | 
					
						
							|  |  |  |     # the operator actually finishes and sets the variable | 
					
						
							|  |  |  |     last_operator = getattr(bpy.context, 'active_operator', None) | 
					
						
							|  |  |  |     if last_operator is not None: | 
					
						
							|  |  |  |         on_operator_post(last_operator.bl_idname) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def on_operator_post(operator_id: str) -> None: | 
					
						
							|  |  |  |     """Called after operator execution. Does not work for operators
 | 
					
						
							|  |  |  |     executed in another context. Warning: this function is also called | 
					
						
							|  |  |  |     when the operator execution raised an exception!"""
 | 
					
						
							|  |  |  |     # 3D View > Object > Rigid Body > Copy from Active | 
					
						
							|  |  |  |     if operator_id == "RIGIDBODY_OT_object_settings_copy": | 
					
						
							|  |  |  |         # Copy leenkx rigid body settings | 
					
						
							|  |  |  |         source_obj = bpy.context.active_object | 
					
						
							|  |  |  |         for target_obj in bpy.context.selected_objects: | 
					
						
							|  |  |  |             target_obj.lnx_rb_linear_factor = source_obj.lnx_rb_linear_factor | 
					
						
							|  |  |  |             target_obj.lnx_rb_angular_factor = source_obj.lnx_rb_angular_factor | 
					
						
							|  |  |  |             target_obj.lnx_rb_angular_friction = source_obj.lnx_rb_angular_friction | 
					
						
							|  |  |  |             target_obj.lnx_rb_trigger = source_obj.lnx_rb_trigger | 
					
						
							|  |  |  |             target_obj.lnx_rb_deactivation_time = source_obj.lnx_rb_deactivation_time | 
					
						
							|  |  |  |             target_obj.lnx_rb_ccd = source_obj.lnx_rb_ccd | 
					
						
							| 
									
										
										
										
											2025-06-02 06:31:14 +00:00
										 |  |  |             target_obj.lnx_rb_interpolate = source_obj.lnx_rb_interpolate | 
					
						
							| 
									
										
										
										
											2025-01-22 16:18:30 +01:00
										 |  |  |             target_obj.lnx_rb_collision_filter_mask = source_obj.lnx_rb_collision_filter_mask | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     elif operator_id == "NODE_OT_new_node_tree": | 
					
						
							|  |  |  |         if bpy.context.space_data.tree_type == lnx.nodes_logic.LnxLogicTree.bl_idname: | 
					
						
							|  |  |  |             # In Blender 3.5+, new node trees are no longer called "NodeTree" | 
					
						
							|  |  |  |             # but follow the bl_label attribute by default. New logic trees | 
					
						
							|  |  |  |             # are thus called "Leenkx Logic Editor" which conflicts with Haxe's | 
					
						
							|  |  |  |             # class naming convention. To avoid this, we listen for the | 
					
						
							|  |  |  |             # creation of a node tree and then rename it. | 
					
						
							|  |  |  |             # Unfortunately, manually naming the tree has the unfortunate | 
					
						
							|  |  |  |             # side effect of not basing the new name on the name of the | 
					
						
							|  |  |  |             # previously opened node tree, as it is the case for Blender trees... | 
					
						
							|  |  |  |             bpy.context.space_data.edit_tree.name = "LogicTree" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def send_operator(op): | 
					
						
							|  |  |  |     if hasattr(bpy.context, 'object') and bpy.context.object is not None: | 
					
						
							|  |  |  |         obj = bpy.context.object.name | 
					
						
							|  |  |  |         if op.name == 'Move': | 
					
						
							|  |  |  |             vec = bpy.context.object.location | 
					
						
							|  |  |  |             js = 'var o = iron.Scene.active.getChild("' + obj + '"); o.transform.loc.set(' + str(vec[0]) + ', ' + str(vec[1]) + ', ' + str(vec[2]) + '); o.transform.dirty = true;' | 
					
						
							|  |  |  |             make.write_patch(js) | 
					
						
							|  |  |  |         elif op.name == 'Resize': | 
					
						
							|  |  |  |             vec = bpy.context.object.scale | 
					
						
							|  |  |  |             js = 'var o = iron.Scene.active.getChild("' + obj + '"); o.transform.scale.set(' + str(vec[0]) + ', ' + str(vec[1]) + ', ' + str(vec[2]) + '); o.transform.dirty = true;' | 
					
						
							|  |  |  |             make.write_patch(js) | 
					
						
							|  |  |  |         elif op.name == 'Rotate': | 
					
						
							|  |  |  |             vec = bpy.context.object.rotation_euler.to_quaternion() | 
					
						
							|  |  |  |             js = 'var o = iron.Scene.active.getChild("' + obj + '"); o.transform.rot.set(' + str(vec[1]) + ', ' + str(vec[2]) + ', ' + str(vec[3]) + ' ,' + str(vec[0]) + '); o.transform.dirty = true;' | 
					
						
							|  |  |  |             make.write_patch(js) | 
					
						
							|  |  |  |         else: # Rebuild | 
					
						
							|  |  |  |             make.patch() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def always() -> float: | 
					
						
							|  |  |  |     # Force ui redraw | 
					
						
							|  |  |  |     if state.redraw_ui: | 
					
						
							|  |  |  |         for area in bpy.context.screen.areas: | 
					
						
							|  |  |  |             if area.type in ('NODE_EDITOR', 'PROPERTIES', 'VIEW_3D'): | 
					
						
							|  |  |  |                 area.tag_redraw() | 
					
						
							|  |  |  |         state.redraw_ui = False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return 0.5 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-11 21:16:04 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-22 16:18:30 +01:00
										 |  |  | def poll_threads() -> float: | 
					
						
							|  |  |  |     """Polls the thread callback queue and if a thread has finished, it
 | 
					
						
							|  |  |  |     is joined with the main thread and the corresponding callback is | 
					
						
							|  |  |  |     executed in the main thread. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         thread, callback = make.thread_callback_queue.get(block=False) | 
					
						
							|  |  |  |     except queue.Empty: | 
					
						
							|  |  |  |         return 0.25 | 
					
						
							| 
									
										
										
										
											2025-04-11 21:16:04 +00:00
										 |  |  |     if thread.is_alive(): | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             make.thread_callback_queue.put((thread, callback), block=False) | 
					
						
							|  |  |  |         except queue.Full: | 
					
						
							|  |  |  |             return 0.5 | 
					
						
							|  |  |  |         return 0.1  | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             thread.join() | 
					
						
							|  |  |  |             callback() | 
					
						
							|  |  |  |         except Exception as e: | 
					
						
							|  |  |  |             # If there is an exception, we can no longer return the time to | 
					
						
							|  |  |  |             # the next call to this polling function, so to keep it running | 
					
						
							|  |  |  |             # we re-register it and then raise the original exception. | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 bpy.app.timers.unregister(poll_threads) | 
					
						
							|  |  |  |             except ValueError: | 
					
						
							|  |  |  |                 pass | 
					
						
							|  |  |  |             bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True) | 
					
						
							|  |  |  |         # Quickly check if another thread has finished | 
					
						
							|  |  |  |         return 0.01 | 
					
						
							| 
									
										
										
										
											2025-01-22 16:18:30 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | loaded_py_libraries: dict[str, types.ModuleType] = {} | 
					
						
							|  |  |  | context_screen = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @persistent | 
					
						
							|  |  |  | def on_save_pre(context): | 
					
						
							|  |  |  |     # Ensure that files are saved with the correct version number | 
					
						
							|  |  |  |     # (e.g. startup files with an "Arm" world may have old version numbers) | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     wrd.lnx_version = props.lnx_version | 
					
						
							|  |  |  |     wrd.lnx_commit = props.lnx_commit | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @persistent | 
					
						
							|  |  |  | def on_load_pre(context): | 
					
						
							|  |  |  |     unload_py_libraries() | 
					
						
							|  |  |  |     log.clear(clear_warnings=True, clear_errors=True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @persistent | 
					
						
							|  |  |  | def on_load_post(context): | 
					
						
							|  |  |  |     global context_screen | 
					
						
							|  |  |  |     context_screen = bpy.context.screen | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     props.init_properties_on_load() | 
					
						
							|  |  |  |     reload_blend_data() | 
					
						
							|  |  |  |     lnx.utils.fetch_bundled_script_names() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     wrd.lnx_recompile = True | 
					
						
							|  |  |  |     lnx.api.remove_drivers() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     load_py_libraries() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Show trait users as collections | 
					
						
							|  |  |  |     lnx.utils.update_trait_collections() | 
					
						
							|  |  |  |     props.update_leenkx_world() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def load_py_libraries(): | 
					
						
							|  |  |  |     if bpy.data.filepath == '': | 
					
						
							|  |  |  |         # When a blend file is opened from the file explorer, Blender | 
					
						
							|  |  |  |         # first opens the default file and then the actual blend file, | 
					
						
							|  |  |  |         # so this function is called twice. Because the cwd is already | 
					
						
							|  |  |  |         # that of the folder containing the blend file, libraries would | 
					
						
							|  |  |  |         # be loaded/unloaded once for the default file which is not needed. | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     lib_path = os.path.join(lnx.utils.get_fp(), 'Libraries') | 
					
						
							|  |  |  |     if os.path.exists(lib_path): | 
					
						
							|  |  |  |         # Don't register nodes twice when calling register_nodes() | 
					
						
							|  |  |  |         lnx_nodes.reset_globals() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Make sure that Leenkx's categories are registered first (on top of the menu) | 
					
						
							|  |  |  |         lnx.logicnode.init_categories() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         libs = os.listdir(lib_path) | 
					
						
							|  |  |  |         for lib_name in libs: | 
					
						
							|  |  |  |             fp = os.path.join(lib_path, lib_name) | 
					
						
							|  |  |  |             if os.path.isdir(fp): | 
					
						
							|  |  |  |                 if os.path.exists(os.path.join(fp, 'blender.py')): | 
					
						
							|  |  |  |                     sys.path.append(fp) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     lib_module = importlib.import_module('blender') | 
					
						
							|  |  |  |                     importlib.reload(lib_module) | 
					
						
							|  |  |  |                     if hasattr(lib_module, 'register'): | 
					
						
							|  |  |  |                         lib_module.register() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     log.debug(f'Leenkx: Loaded Python library {lib_name}') | 
					
						
							|  |  |  |                     loaded_py_libraries[lib_name] = lib_module | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     sys.path.remove(fp) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Register newly added nodes and node categories | 
					
						
							|  |  |  |         lnx.nodes_logic.register_nodes() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def unload_py_libraries(): | 
					
						
							|  |  |  |     for lib_name, lib_module in loaded_py_libraries.items(): | 
					
						
							|  |  |  |         if hasattr(lib_module, 'unregister'): | 
					
						
							|  |  |  |             lib_module.unregister() | 
					
						
							|  |  |  |         lnx.log.debug(f'Leenkx: Unloaded Python library {lib_name}') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     loaded_py_libraries.clear() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def reload_blend_data(): | 
					
						
							|  |  |  |     leenkx_pbr = bpy.data.node_groups.get('Leenkx PBR') | 
					
						
							|  |  |  |     if leenkx_pbr is None: | 
					
						
							|  |  |  |         load_library('Leenkx PBR') | 
					
						
							|  |  |  |     custom_tilesheet = bpy.data.node_groups.get('CustomTilesheet') | 
					
						
							|  |  |  |     if custom_tilesheet is None: | 
					
						
							|  |  |  |         load_library('CustomTilesheet') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def load_library(asset_name): | 
					
						
							|  |  |  |     if bpy.data.filepath.endswith('lnx_data.blend'): # Prevent load in library itself | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  |     sdk_path = lnx.utils.get_sdk_path() | 
					
						
							|  |  |  |     data_path = sdk_path + '/leenkx/blender/data/lnx_data.blend' | 
					
						
							|  |  |  |     data_names = [asset_name] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Import | 
					
						
							|  |  |  |     data_refs = data_names.copy() | 
					
						
							|  |  |  |     with bpy.data.libraries.load(data_path, link=False) as (data_from, data_to): | 
					
						
							|  |  |  |         data_to.node_groups = data_refs | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for ref in data_refs: | 
					
						
							|  |  |  |         ref.use_fake_user = True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def post_register(): | 
					
						
							|  |  |  |     """Called in start.py after all Leenkx modules have been registered.
 | 
					
						
							|  |  |  |     It is also called in case of add-on reloads. Put code here that | 
					
						
							|  |  |  |     needs to be run once at the beginning of each session. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if lnx.utils.get_os_is_windows(): | 
					
						
							|  |  |  |         lnx.utils_vs.fetch_installed_vs(silent=True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def register(): | 
					
						
							|  |  |  |     bpy.app.handlers.save_pre.append(on_save_pre) | 
					
						
							|  |  |  |     bpy.app.handlers.load_pre.append(on_load_pre) | 
					
						
							|  |  |  |     bpy.app.handlers.load_post.append(on_load_post) | 
					
						
							|  |  |  |     bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update_post) | 
					
						
							|  |  |  |     # bpy.app.handlers.undo_post.append(on_undo_post) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     bpy.app.timers.register(always, persistent=True) | 
					
						
							|  |  |  |     bpy.app.timers.register(poll_threads, persistent=True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if lnx.utils.get_fp() != '': | 
					
						
							|  |  |  |         # TODO: On windows, on_load_post is not called when opening .blend file from explorer | 
					
						
							|  |  |  |         if lnx.utils.get_os() == 'win': | 
					
						
							|  |  |  |             on_load_post(None) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             # load_py_libraries() is called by on_load_post(). This call makes sure that libraries are also loaded | 
					
						
							|  |  |  |             # when a file is already opened during add-on registration | 
					
						
							|  |  |  |             load_py_libraries() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     reload_blend_data() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def unregister(): | 
					
						
							|  |  |  |     unload_py_libraries() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     bpy.app.timers.unregister(poll_threads) | 
					
						
							|  |  |  |     bpy.app.timers.unregister(always) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     bpy.app.handlers.load_post.remove(on_load_post) | 
					
						
							|  |  |  |     bpy.app.handlers.load_pre.remove(on_load_pre) | 
					
						
							|  |  |  |     bpy.app.handlers.save_pre.remove(on_save_pre) | 
					
						
							|  |  |  |     bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update_post) | 
					
						
							|  |  |  |     # bpy.app.handlers.undo_post.remove(on_undo_post) |