| 
									
										
										
										
											2025-01-22 16:18:30 +01:00
										 |  |  | from enum import Enum, unique | 
					
						
							|  |  |  | import glob | 
					
						
							|  |  |  | import itertools | 
					
						
							|  |  |  | import json | 
					
						
							|  |  |  | import locale | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import platform | 
					
						
							|  |  |  | import random | 
					
						
							|  |  |  | import re | 
					
						
							|  |  |  | import shlex | 
					
						
							|  |  |  | import shutil | 
					
						
							|  |  |  | import subprocess | 
					
						
							|  |  |  | from typing import Any, Dict, Iterable, List, Optional, Tuple, Union | 
					
						
							|  |  |  | import webbrowser | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import numpy as np | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import bpy | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import lnx.lib.lnxpack | 
					
						
							|  |  |  | from lnx.lib.lz4 import LZ4 | 
					
						
							|  |  |  | import lnx.log as log | 
					
						
							|  |  |  | import lnx.make_state as state | 
					
						
							|  |  |  | import lnx.props_renderpath | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if lnx.is_reload(__name__): | 
					
						
							|  |  |  |     lnx.lib.lnxpack = lnx.reload_module(lnx.lib.lnxpack) | 
					
						
							|  |  |  |     lnx.lib.lz4 = lnx.reload_module(lnx.lib.lz4) | 
					
						
							|  |  |  |     from lnx.lib.lz4 import LZ4 | 
					
						
							|  |  |  |     log = lnx.reload_module(log) | 
					
						
							|  |  |  |     state = lnx.reload_module(state) | 
					
						
							|  |  |  |     lnx.props_renderpath = lnx.reload_module(lnx.props_renderpath) | 
					
						
							|  |  |  | else: | 
					
						
							|  |  |  |     lnx.enable_reload(__name__) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class NumpyEncoder(json.JSONEncoder): | 
					
						
							|  |  |  |     def default(self, obj): | 
					
						
							|  |  |  |         if isinstance(obj, np.ndarray): | 
					
						
							|  |  |  |             return obj.tolist() | 
					
						
							|  |  |  |         return json.JSONEncoder.default(self, obj) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class WorkingDir: | 
					
						
							|  |  |  |     """Context manager for safely changing the current working directory.""" | 
					
						
							|  |  |  |     def __init__(self, cwd: str): | 
					
						
							|  |  |  |         self.cwd = cwd | 
					
						
							|  |  |  |         self.prev_cwd = os.getcwd() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __enter__(self): | 
					
						
							|  |  |  |         os.chdir(self.cwd) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __exit__(self, exc_type, exc_val, exc_tb): | 
					
						
							|  |  |  |         os.chdir(self.prev_cwd) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def write_lnx(filepath, output): | 
					
						
							|  |  |  |     if filepath.endswith('.lz4'): | 
					
						
							|  |  |  |         with open(filepath, 'wb') as f: | 
					
						
							|  |  |  |             packed = lnx.lib.lnxpack.packb(output) | 
					
						
							|  |  |  |             # Prepend packed data size for decoding. Haxe can't unpack | 
					
						
							|  |  |  |             # an unsigned int64 so we use a signed int64 here | 
					
						
							|  |  |  |             f.write(np.int64(LZ4.encode_bound(len(packed))).tobytes()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             f.write(LZ4.encode(packed)) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         if bpy.data.worlds['Lnx'].lnx_minimize: | 
					
						
							|  |  |  |             with open(filepath, 'wb') as f: | 
					
						
							|  |  |  |                 f.write(lnx.lib.lnxpack.packb(output)) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             filepath_json = filepath.split('.lnx')[0] + '.json' | 
					
						
							|  |  |  |             with open(filepath_json, 'w') as f: | 
					
						
							|  |  |  |                 f.write(json.dumps(output, sort_keys=True, indent=4, cls=NumpyEncoder)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def unpack_image(image, path, file_format='JPEG'): | 
					
						
							|  |  |  |     print('Leenkx Info: Unpacking to ' + path) | 
					
						
							|  |  |  |     image.filepath_raw = path | 
					
						
							|  |  |  |     image.file_format = file_format | 
					
						
							|  |  |  |     image.save() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def convert_image(image, path, file_format='JPEG'): | 
					
						
							|  |  |  |     # Convert image to compatible format | 
					
						
							|  |  |  |     print('Leenkx Info: Converting to ' + path) | 
					
						
							|  |  |  |     ren = bpy.context.scene.render | 
					
						
							|  |  |  |     orig_quality = ren.image_settings.quality | 
					
						
							|  |  |  |     orig_file_format = ren.image_settings.file_format | 
					
						
							|  |  |  |     orig_color_mode = ren.image_settings.color_mode | 
					
						
							|  |  |  |     ren.image_settings.quality = get_texture_quality_percentage() | 
					
						
							|  |  |  |     ren.image_settings.file_format = file_format | 
					
						
							|  |  |  |     if file_format == 'PNG': | 
					
						
							|  |  |  |         ren.image_settings.color_mode = 'RGBA' | 
					
						
							|  |  |  |     orig_image_colorspace = image.colorspace_settings.name | 
					
						
							|  |  |  |     image.colorspace_settings.name = 'Non-Color' | 
					
						
							|  |  |  |     image.save_render(path, scene=bpy.context.scene) | 
					
						
							|  |  |  |     image.colorspace_settings.name = orig_image_colorspace | 
					
						
							|  |  |  |     ren.image_settings.quality = orig_quality | 
					
						
							|  |  |  |     ren.image_settings.file_format = orig_file_format | 
					
						
							|  |  |  |     ren.image_settings.color_mode = orig_color_mode | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_random_color_rgb() -> list[float]: | 
					
						
							|  |  |  |     """Return a random RGB color with values in range [0, 1].""" | 
					
						
							|  |  |  |     return [random.random(), random.random(), random.random()] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def is_livepatch_enabled(): | 
					
						
							|  |  |  |     """Returns whether live patch is enabled and can be used.""" | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     # If the game is published, the target is krom-[OS] and not krom, | 
					
						
							|  |  |  |     # so there is no live patch when publishing | 
					
						
							|  |  |  |     return wrd.lnx_live_patch and state.target == 'krom' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def blend_name(): | 
					
						
							|  |  |  |     return bpy.path.basename(bpy.context.blend_data.filepath).rsplit('.', 1)[0] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def build_dir(): | 
					
						
							|  |  |  |     return 'build_' + safestr(blend_name()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_fp() -> str: | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     if wrd.lnx_project_root != '': | 
					
						
							|  |  |  |         return bpy.path.abspath(wrd.lnx_project_root) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         s = None | 
					
						
							|  |  |  |         if use_local_sdk and bpy.data.filepath == '': | 
					
						
							|  |  |  |             s = os.getcwd() | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             s = bpy.data.filepath.split(os.path.sep) | 
					
						
							|  |  |  |             s.pop() | 
					
						
							|  |  |  |             s = os.path.sep.join(s) | 
					
						
							|  |  |  |         if get_os_is_windows() and len(s) == 2 and s[1] == ':': | 
					
						
							|  |  |  |             # If the project is located at a drive root (C:/ for example), | 
					
						
							|  |  |  |             # then s = "C:". If joined later with another path, no path | 
					
						
							|  |  |  |             # separator is added by default because C:some_path is valid | 
					
						
							|  |  |  |             # Windows path syntax (some_path is then relative to the CWD on the | 
					
						
							|  |  |  |             # C drive). We prevent this by manually adding the path separator | 
					
						
							|  |  |  |             # in these cases. Please refer to the Python doc of os.path.join() | 
					
						
							|  |  |  |             # for more details. | 
					
						
							|  |  |  |             s += os.path.sep | 
					
						
							|  |  |  |         return s | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_fp_build(): | 
					
						
							|  |  |  |     return os.path.join(get_fp(), build_dir()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def to_absolute_path(path: str, from_library: Optional[bpy.types.Library] = None) -> str: | 
					
						
							|  |  |  |     """Convert the given absolute or relative path into an absolute path.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     - If `from_library` is not set (default), a given relative path will be | 
					
						
							|  |  |  |       interpreted as relative to the project directory. | 
					
						
							|  |  |  |     - If `from_library` is set, a given relative path will be interpreted as | 
					
						
							|  |  |  |       relative to the filepath of the specified library. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     return os.path.normpath(bpy.path.abspath(path, start=get_fp(), library=from_library)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_os() -> str: | 
					
						
							|  |  |  |     s = platform.system() | 
					
						
							|  |  |  |     if s == 'Windows': | 
					
						
							|  |  |  |         return 'win' | 
					
						
							|  |  |  |     elif s == 'Darwin': | 
					
						
							|  |  |  |         return 'mac' | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return 'linux' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_os_is_windows() -> bool: | 
					
						
							|  |  |  |     return get_os() == 'win' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_os_is_windows_64() -> bool: | 
					
						
							|  |  |  |     if platform.machine().endswith('64'): | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  |     # Checks if Python (32 bit) is running on Windows (64 bit) | 
					
						
							|  |  |  |     if 'PROCESSOR_ARCHITEW6432' in os.environ: | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  |     if os.environ['PROCESSOR_ARCHITECTURE'].endswith('64'): | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  |     if 'PROGRAMFILES(X86)' in os.environ: | 
					
						
							|  |  |  |         if os.environ['PROGRAMW6432'] is not None: | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_gapi(): | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     if state.is_export: | 
					
						
							|  |  |  |         item = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index] | 
					
						
							|  |  |  |         return getattr(item, target_to_gapi(item.lnx_project_target)) | 
					
						
							|  |  |  |     if wrd.lnx_runtime == 'Browser': | 
					
						
							|  |  |  |         return 'webgl' | 
					
						
							| 
									
										
										
										
											2025-03-31 13:51:58 +00:00
										 |  |  |     if get_os() == 'win' and lnx.utils.get_rp().rp_voxels == 'Off': | 
					
						
							|  |  |  |         return 'direct3d11' | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return 'opengl' | 
					
						
							| 
									
										
										
										
											2025-01-22 16:18:30 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def is_gapi_gl_es() -> bool: | 
					
						
							|  |  |  |     """Return whether the currently targeted graphics API is using OpenGL ES.""" | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if state.is_export: | 
					
						
							|  |  |  |         item_exporter = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # See Khamake's ShaderCompiler.findType() and krafix::Target.es in krafix.cpp ("target.es") | 
					
						
							|  |  |  |         if state.target == 'android-hl': | 
					
						
							|  |  |  |             return item_exporter.lnx_gapi_android == 'opengl' | 
					
						
							|  |  |  |         if state.target == 'ios-hl': | 
					
						
							|  |  |  |             return item_exporter.lnx_gapi_ios == 'opengl' | 
					
						
							|  |  |  |         elif state.target == 'html5': | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return wrd.lnx_runtime == 'Browser' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_rp() -> lnx.props_renderpath.LnxRPListItem: | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							| 
									
										
										
										
											2025-02-05 23:03:31 +00:00
										 |  |  |     if not state.is_export and wrd.lnx_play_renderpath != '' and lnx.props_renderpath.LnxRPListItem.get_by_name(wrd.lnx_play_renderpath) is not None: | 
					
						
							| 
									
										
										
										
											2025-01-22 16:18:30 +01:00
										 |  |  |         return lnx.props_renderpath.LnxRPListItem.get_by_name(wrd.lnx_play_renderpath) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return wrd.lnx_rplist[wrd.lnx_rplist_index] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Passed by load_post handler when lnxsdk is found in project folder | 
					
						
							|  |  |  | use_local_sdk = False | 
					
						
							|  |  |  | def get_sdk_path(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     if use_local_sdk: | 
					
						
							|  |  |  |         return os.path.normpath(get_fp() + '/lnxsdk/') | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return os.path.normpath(addon_prefs.sdk_path) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_last_commit(): | 
					
						
							|  |  |  |     p = get_sdk_path() + 'leenkx/.git/refs/heads/main' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         file = open(p, 'r') | 
					
						
							|  |  |  |         commit = file.readline() | 
					
						
							|  |  |  |     except: | 
					
						
							|  |  |  |         commit = '' | 
					
						
							|  |  |  |     return commit | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_lnx_preferences() -> bpy.types.AddonPreferences: | 
					
						
							|  |  |  |     preferences = bpy.context.preferences | 
					
						
							|  |  |  |     return preferences.addons["leenkx"].preferences | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_ide_bin(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return '' if not hasattr(addon_prefs, 'ide_bin') else addon_prefs.ide_bin | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_ffmpeg_path(): | 
					
						
							|  |  |  |     path = get_lnx_preferences().ffmpeg_path | 
					
						
							|  |  |  |     if path == "": path = shutil.which("ffmpeg") | 
					
						
							|  |  |  |     return path | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_renderdoc_path(): | 
					
						
							|  |  |  |     p = get_lnx_preferences().renderdoc_path | 
					
						
							|  |  |  |     if p == '' and get_os() == 'win': | 
					
						
							|  |  |  |         pdefault = 'C:\\Program Files\\RenderDoc\\qrenderdoc.exe' | 
					
						
							|  |  |  |         if os.path.exists(pdefault): | 
					
						
							|  |  |  |             p = pdefault | 
					
						
							|  |  |  |     return p | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_code_editor(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return 'kodestudio' if not hasattr(addon_prefs, 'code_editor') else addon_prefs.code_editor | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_ui_scale(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return 1.0 if not hasattr(addon_prefs, 'ui_scale') else addon_prefs.ui_scale | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_khamake_threads() -> int: | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     if hasattr(addon_prefs, 'khamake_threads_use_auto') and addon_prefs.khamake_threads_use_auto: | 
					
						
							|  |  |  |         return -1 | 
					
						
							|  |  |  |     return 1 if not hasattr(addon_prefs, 'khamake_threads') else addon_prefs.khamake_threads | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_compilation_server(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return False if not hasattr(addon_prefs, 'compilation_server') else addon_prefs.compilation_server | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_save_on_build(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return False if not hasattr(addon_prefs, 'save_on_build') else addon_prefs.save_on_build | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_debug_console_auto(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return False if not hasattr(addon_prefs, 'debug_console_auto') else addon_prefs.debug_console_auto | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_debug_console_visible_sc(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return 192 if not hasattr(addon_prefs, 'debug_console_visible_sc') else addon_prefs.debug_console_visible_sc | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_debug_console_scale_in_sc(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return 219 if not hasattr(addon_prefs, 'debug_console_scale_in_sc') else addon_prefs.debug_console_scale_in_sc | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_debug_console_scale_out_sc(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return 221 if not hasattr(addon_prefs, 'debug_console_scale_out_sc') else addon_prefs.debug_console_scale_out_sc | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_viewport_controls(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return 'qwerty' if not hasattr(addon_prefs, 'viewport_controls') else addon_prefs.viewport_controls | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_legacy_shaders(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return False if not hasattr(addon_prefs, 'legacy_shaders') else addon_prefs.legacy_shaders | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_relative_paths(): | 
					
						
							|  |  |  |     """Whether to convert absolute paths to relative""" | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return False if not hasattr(addon_prefs, 'relative_paths') else addon_prefs.relative_paths | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_pref_or_default(prop_name: str, default: Any) -> Any: | 
					
						
							|  |  |  |     """Return the preference setting for prop_name, or the value given as default if the property does not exist.""" | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return getattr(addon_prefs, prop_name, default) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_node_path(): | 
					
						
							|  |  |  |     if get_os() == 'win': | 
					
						
							|  |  |  |         return get_sdk_path() + '/nodejs/node.exe' | 
					
						
							|  |  |  |     elif get_os() == 'mac': | 
					
						
							|  |  |  |         return get_sdk_path() + '/nodejs/node-osx' | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return get_sdk_path() + '/nodejs/node-linux64' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_kha_path(): | 
					
						
							|  |  |  |     if os.path.exists('Kha'): | 
					
						
							|  |  |  |         return 'Kha' | 
					
						
							|  |  |  |     return get_sdk_path() + '/Kha' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_haxe_path(): | 
					
						
							|  |  |  |     if get_os() == 'win': | 
					
						
							|  |  |  |         return get_kha_path() + '/Tools/windows_x64/haxe.exe' | 
					
						
							|  |  |  |     elif get_os() == 'mac': | 
					
						
							|  |  |  |         return get_kha_path() + '/Tools/macos/haxe' | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return get_kha_path() + '/Tools/linux_x64/haxe' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_khamake_path(): | 
					
						
							|  |  |  |     return get_kha_path() + '/make' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def krom_paths(): | 
					
						
							|  |  |  |     sdk_path = get_sdk_path() | 
					
						
							|  |  |  |     if lnx.utils.get_os() == 'win': | 
					
						
							|  |  |  |         krom_location = sdk_path + '/Krom' | 
					
						
							| 
									
										
										
										
											2025-03-31 13:51:58 +00:00
										 |  |  |         if lnx.utils.get_rp().rp_voxels == 'Off': | 
					
						
							|  |  |  |             krom_path = krom_location + '/Krom.exe' | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             krom_path = krom_location + '/Krom_opengl.exe' | 
					
						
							| 
									
										
										
										
											2025-01-22 16:18:30 +01:00
										 |  |  |     elif lnx.utils.get_os() == 'mac': | 
					
						
							|  |  |  |         krom_location = sdk_path + '/Krom/Krom.app/Contents/MacOS' | 
					
						
							|  |  |  |         krom_path = krom_location + '/Krom' | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         krom_location = sdk_path + '/Krom' | 
					
						
							|  |  |  |         krom_path = krom_location + '/Krom' | 
					
						
							|  |  |  |     return krom_location, krom_path | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-09 20:44:54 +00:00
										 |  |  | def hashlink_paths(target): | 
					
						
							|  |  |  |     """Returns path for Hashlink runtime target.""" | 
					
						
							|  |  |  |     sdk_path = get_sdk_path() | 
					
						
							|  |  |  |     proj_name = blend_name() | 
					
						
							|  |  |  |     build_base_dir = get_fp_build() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if target in ('windows-hl', 'linux-hl', 'macos-hl'):  | 
					
						
							|  |  |  |         hl_build_dir = os.path.join(build_base_dir, target + '-build') | 
					
						
							|  |  |  |         log.info(f"Identified Hashlink/C build directory: {hl_build_dir}") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return hl_build_dir, None, None # Return build_dir, no file, no interpreter | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return '', None, None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-22 16:18:30 +01:00
										 |  |  | def fetch_bundled_script_names(): | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     wrd.lnx_bundled_scripts_list.clear() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with WorkingDir(get_sdk_path() + '/leenkx/Sources/leenkx/trait'): | 
					
						
							|  |  |  |         for file in glob.glob('*.hx'): | 
					
						
							|  |  |  |             wrd.lnx_bundled_scripts_list.add().name = file.rsplit('.', 1)[0] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | script_props = {} | 
					
						
							|  |  |  | script_props_defaults = {} | 
					
						
							|  |  |  | script_warnings: Dict[str, List[Tuple[str, str]]] = {}  # Script name -> List of (identifier, warning message) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # See https://regex101.com/r/bbrCzN/8 | 
					
						
							|  |  |  | RX_MODIFIERS = r'(?P<modifiers>(?:public\s+|private\s+|static\s+|inline\s+|final\s+)*)?'  # Optional modifiers | 
					
						
							|  |  |  | RX_IDENTIFIER = r'(?P<identifier>[_$a-z]+[_a-z0-9]*)'  # Variable name, follow Haxe rules | 
					
						
							|  |  |  | RX_TYPE = r'(?:\s*:\s*(?P<type>[_a-z]+[\._a-z0-9]*))?'  # Optional type annotation | 
					
						
							|  |  |  | RX_VALUE = r'(?:\s*=\s*(?P<value>(?:\".*\")|(?:[^;]+)|))?'  # Optional default value | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | PROP_REGEX_RAW = fr'@prop\s+{RX_MODIFIERS}(?P<attr_type>var|final)\s+{RX_IDENTIFIER}{RX_TYPE}{RX_VALUE};' | 
					
						
							|  |  |  | PROP_REGEX = re.compile(PROP_REGEX_RAW, re.IGNORECASE) | 
					
						
							|  |  |  | def fetch_script_props(filename: str): | 
					
						
							|  |  |  |     """Parses @prop declarations from the given Haxe script.""" | 
					
						
							|  |  |  |     with open(filename, 'r', encoding='utf-8') as sourcefile: | 
					
						
							|  |  |  |         source = sourcefile.read() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if source == '': | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     name = filename.rsplit('.', 1)[0] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Convert the name into a package path relative to the "Sources" dir | 
					
						
							|  |  |  |     if 'Sources' in name: | 
					
						
							|  |  |  |         name = name[name.index('Sources') + 8:] | 
					
						
							|  |  |  |     if '/' in name: | 
					
						
							|  |  |  |         name = name.replace('/', '.') | 
					
						
							|  |  |  |     if '\\' in filename: | 
					
						
							|  |  |  |         name = name.replace('\\', '.') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     script_props[name] = [] | 
					
						
							|  |  |  |     script_props_defaults[name] = [] | 
					
						
							|  |  |  |     script_warnings[name] = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for match in re.finditer(PROP_REGEX, source): | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         p_modifiers: Optional[str] = match.group('modifiers') | 
					
						
							|  |  |  |         p_identifier: str = match.group('identifier') | 
					
						
							|  |  |  |         p_type: Optional[str] = match.group('type') | 
					
						
							|  |  |  |         p_default_val: Optional[str] = match.group('value') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if p_modifiers is not None: | 
					
						
							|  |  |  |             if 'static' in p_modifiers: | 
					
						
							|  |  |  |                 script_warnings[name].append((p_identifier, '`static` modifier might cause unwanted behaviour!')) | 
					
						
							|  |  |  |             if 'inline' in p_modifiers: | 
					
						
							|  |  |  |                 script_warnings[name].append((p_identifier, '`inline` modifier is not supported!')) | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             if 'final' in p_modifiers or match.group('attr_type') == 'final': | 
					
						
							|  |  |  |                 script_warnings[name].append((p_identifier, '`final` properties are not supported!')) | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Property type is annotated | 
					
						
							|  |  |  |         if p_type is not None: | 
					
						
							|  |  |  |             if p_type.startswith("iron.object."): | 
					
						
							|  |  |  |                 p_type = p_type[12:] | 
					
						
							|  |  |  |             elif p_type.startswith("iron.math."): | 
					
						
							|  |  |  |                 p_type = p_type[10:] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             type_default_val = get_type_default_value(p_type) | 
					
						
							|  |  |  |             if type_default_val is None: | 
					
						
							|  |  |  |                 script_warnings[name].append((p_identifier, f'unsupported type `{p_type}`!')) | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # Default value exists | 
					
						
							|  |  |  |             if p_default_val is not None: | 
					
						
							|  |  |  |                 # Remove string quotes | 
					
						
							|  |  |  |                 p_default_val = p_default_val.replace('\'', '').replace('"', '') | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 p_default_val = type_default_val | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Default value is given instead, try to infer the properties type from it | 
					
						
							|  |  |  |         elif p_default_val is not None: | 
					
						
							|  |  |  |             p_type = get_prop_type_from_value(p_default_val) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # Type is not recognized | 
					
						
							|  |  |  |             if p_type is None: | 
					
						
							|  |  |  |                 script_warnings[name].append((p_identifier, 'could not infer property type from given value!')) | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             if p_type == "String": | 
					
						
							|  |  |  |                 p_default_val = p_default_val.replace('\'', '').replace('"', '') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             script_warnings[name].append((p_identifier, 'missing type or default value!')) | 
					
						
							|  |  |  |             continue | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Register prop | 
					
						
							|  |  |  |         prop = (p_identifier, p_type) | 
					
						
							|  |  |  |         script_props[name].append(prop) | 
					
						
							|  |  |  |         script_props_defaults[name].append(p_default_val) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_prop_type_from_value(value: str): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Returns the property type based on its representation in the code. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     If the type is not supported, `None` is returned. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     # Maybe ast.literal_eval() is better here? | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         int(value) | 
					
						
							|  |  |  |         return "Int" | 
					
						
							|  |  |  |     except ValueError: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             float(value) | 
					
						
							|  |  |  |             return "Float" | 
					
						
							|  |  |  |         except ValueError: | 
					
						
							|  |  |  |             # "" is required, " alone will not work | 
					
						
							|  |  |  |             if len(value) > 1 and value.startswith(("\"", "'")) and value.endswith(("\"", "'")): | 
					
						
							|  |  |  |                 return "String" | 
					
						
							|  |  |  |             if value in ("true", "false"): | 
					
						
							|  |  |  |                 return "Bool" | 
					
						
							|  |  |  |             if value.startswith("new "): | 
					
						
							|  |  |  |                 value = value.split()[1].split("(")[0] | 
					
						
							|  |  |  |                 if value.startswith("Vec"): | 
					
						
							|  |  |  |                     return value | 
					
						
							|  |  |  |                 if value.startswith("iron.math.Vec"): | 
					
						
							|  |  |  |                     return value[10:] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_type_default_value(prop_type: str): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Returns the default value of the given Haxe type. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     If the type is not supported, `None` is returned: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if prop_type == "Int": | 
					
						
							|  |  |  |         return 0 | 
					
						
							|  |  |  |     if prop_type == "Float": | 
					
						
							|  |  |  |         return 0.0 | 
					
						
							|  |  |  |     if prop_type == "String" or prop_type in ( | 
					
						
							|  |  |  |             "Object", "CameraObject", "LightObject", "MeshObject", "SpeakerObject"): | 
					
						
							|  |  |  |         return "" | 
					
						
							|  |  |  |     if prop_type == "Bool": | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if prop_type == "Vec2": | 
					
						
							|  |  |  |         return [0.0, 0.0] | 
					
						
							|  |  |  |     if prop_type == "Vec3": | 
					
						
							|  |  |  |         return [0.0, 0.0, 0.0] | 
					
						
							|  |  |  |     if prop_type == "Vec4": | 
					
						
							|  |  |  |         return [0.0, 0.0, 0.0, 0.0] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def fetch_script_names(): | 
					
						
							|  |  |  |     if bpy.data.filepath == "": | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     # Sources | 
					
						
							|  |  |  |     wrd.lnx_scripts_list.clear() | 
					
						
							|  |  |  |     sources_path = os.path.join(get_fp(), 'Sources', safestr(wrd.lnx_project_package)) | 
					
						
							|  |  |  |     if os.path.isdir(sources_path): | 
					
						
							|  |  |  |         with WorkingDir(sources_path): | 
					
						
							|  |  |  |             # Glob supports recursive search since python 3.5 so it should cover both blender 2.79 and 2.8 integrated python | 
					
						
							|  |  |  |             for file in glob.glob('**/*.hx', recursive=True): | 
					
						
							|  |  |  |                 mod = file.rsplit('.', 1)[0] | 
					
						
							|  |  |  |                 mod = mod.replace('\\', '/') | 
					
						
							|  |  |  |                 mod_parts = mod.rsplit('/') | 
					
						
							|  |  |  |                 if re.match('^[A-Z][A-Za-z0-9_]*$', mod_parts[-1]): | 
					
						
							|  |  |  |                     wrd.lnx_scripts_list.add().name = mod.replace('/', '.') | 
					
						
							|  |  |  |                     fetch_script_props(file) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Canvas | 
					
						
							|  |  |  |     wrd.lnx_canvas_list.clear() | 
					
						
							|  |  |  |     canvas_path = get_fp() + '/Bundled/canvas' | 
					
						
							|  |  |  |     if os.path.isdir(canvas_path): | 
					
						
							|  |  |  |         with WorkingDir(canvas_path): | 
					
						
							|  |  |  |             for file in glob.glob('*.json'): | 
					
						
							|  |  |  |                 if file == "_themes.json": | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 wrd.lnx_canvas_list.add().name = file.rsplit('.', 1)[0] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def fetch_wasm_names(): | 
					
						
							|  |  |  |     if bpy.data.filepath == "": | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     # WASM modules | 
					
						
							|  |  |  |     wrd.lnx_wasm_list.clear() | 
					
						
							|  |  |  |     sources_path = get_fp() + '/Bundled' | 
					
						
							|  |  |  |     if os.path.isdir(sources_path): | 
					
						
							|  |  |  |         with WorkingDir(sources_path): | 
					
						
							|  |  |  |             for file in glob.glob('*.wasm'): | 
					
						
							|  |  |  |                 name = file.rsplit('.', 1)[0] | 
					
						
							|  |  |  |                 wrd.lnx_wasm_list.add().name = name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def fetch_trait_props(): | 
					
						
							|  |  |  |     for o in bpy.data.objects: | 
					
						
							|  |  |  |         if o.override_library is None: | 
					
						
							|  |  |  |             # We can't update the list of trait properties for linked | 
					
						
							|  |  |  |             # objects because Blender doesn't allow to remove items from | 
					
						
							|  |  |  |             # overridden lists | 
					
						
							|  |  |  |             fetch_prop(o) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for s in bpy.data.scenes: | 
					
						
							|  |  |  |         fetch_prop(s) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def fetch_prop(o: Union[bpy.types.Object, bpy.types.Scene]): | 
					
						
							|  |  |  |     for item in o.lnx_traitlist: | 
					
						
							|  |  |  |         if item.type_prop == 'Bundled Script': | 
					
						
							|  |  |  |             name = 'leenkx.trait.' + item.name | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             name = item.name | 
					
						
							|  |  |  |         if name not in script_props: | 
					
						
							|  |  |  |             continue | 
					
						
							|  |  |  |         props = script_props[name] | 
					
						
							|  |  |  |         defaults = script_props_defaults[name] | 
					
						
							|  |  |  |         warnings = script_warnings[name] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Remove old props | 
					
						
							|  |  |  |         for i in range(len(item.lnx_traitpropslist) - 1, -1, -1): | 
					
						
							|  |  |  |             ip = item.lnx_traitpropslist[i] | 
					
						
							|  |  |  |             if ip.name not in [p[0] for p in props]: | 
					
						
							|  |  |  |                 item.lnx_traitpropslist.remove(i) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Add new props | 
					
						
							|  |  |  |         for index, p in enumerate(props): | 
					
						
							|  |  |  |             found_prop = False | 
					
						
							|  |  |  |             for i_prop in item.lnx_traitpropslist: | 
					
						
							|  |  |  |                 if i_prop.name == p[0]: | 
					
						
							|  |  |  |                     if i_prop.type == p[1]: | 
					
						
							|  |  |  |                         found_prop = i_prop | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         item.lnx_traitpropslist.remove(item.lnx_traitpropslist.find(i_prop.name)) | 
					
						
							|  |  |  |                     break | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # Not in list | 
					
						
							|  |  |  |             if not found_prop: | 
					
						
							|  |  |  |                 prop = item.lnx_traitpropslist.add() | 
					
						
							|  |  |  |                 prop.name = p[0] | 
					
						
							|  |  |  |                 prop.type = p[1] | 
					
						
							|  |  |  |                 prop.set_value(defaults[index]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if found_prop: | 
					
						
							|  |  |  |                 prop = item.lnx_traitpropslist[found_prop.name] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 # Default value added and current value is blank (no override) | 
					
						
							|  |  |  |                 if (found_prop.get_value() is None | 
					
						
							|  |  |  |                         or found_prop.get_value() == "") and defaults[index]: | 
					
						
							|  |  |  |                     prop.set_value(defaults[index]) | 
					
						
							|  |  |  |                 # Type has changed, update displayed name | 
					
						
							|  |  |  |                 if len(found_prop.name) == 1 or (len(found_prop.name) > 1 and found_prop.name[1] != p[1]): | 
					
						
							|  |  |  |                     prop.name = p[0] | 
					
						
							|  |  |  |                     prop.type = p[1] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             item.lnx_traitpropswarnings.clear() | 
					
						
							|  |  |  |             for warning in warnings: | 
					
						
							|  |  |  |                 entry = item.lnx_traitpropswarnings.add() | 
					
						
							|  |  |  |                 entry.propName = warning[0] | 
					
						
							|  |  |  |                 entry.warning = warning[1] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def fetch_bundled_trait_props(): | 
					
						
							|  |  |  |     # Bundled script props | 
					
						
							|  |  |  |     for o in bpy.data.objects: | 
					
						
							|  |  |  |         for t in o.lnx_traitlist: | 
					
						
							|  |  |  |             if t.type_prop == 'Bundled Script': | 
					
						
							|  |  |  |                 file_path = get_sdk_path() + '/leenkx/Sources/leenkx/trait/' + t.name + '.hx' | 
					
						
							|  |  |  |                 if os.path.exists(file_path): | 
					
						
							|  |  |  |                     fetch_script_props(file_path) | 
					
						
							|  |  |  |                     fetch_prop(o) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def update_trait_collections(): | 
					
						
							|  |  |  |     for col in bpy.data.collections: | 
					
						
							|  |  |  |         if col.name.startswith('Trait|'): | 
					
						
							|  |  |  |             bpy.data.collections.remove(col) | 
					
						
							|  |  |  |     for o in bpy.data.objects: | 
					
						
							|  |  |  |         for t in o.lnx_traitlist: | 
					
						
							|  |  |  |             if 'Trait|' + t.name not in bpy.data.collections: | 
					
						
							|  |  |  |                 col = bpy.data.collections.new('Trait|' + t.name) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 col = bpy.data.collections['Trait|' + t.name] | 
					
						
							|  |  |  |             col.objects.link(o) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def to_hex(val): | 
					
						
							|  |  |  |     return '#%02x%02x%02x%02x' % (int(val[3] * 255), int(val[0] * 255), int(val[1] * 255), int(val[2] * 255)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def color_to_int(val) -> int: | 
					
						
							|  |  |  |     # Clamp values, otherwise the return value might not fit in 32 bit | 
					
						
							|  |  |  |     # (and later cause problems, e.g. in the .arm file reader) | 
					
						
							|  |  |  |     val = [max(0.0, min(v, 1.0)) for v in val] | 
					
						
							|  |  |  |     return (int(val[3] * 255) << 24) + (int(val[0] * 255) << 16) + (int(val[1] * 255) << 8) + int(val[2] * 255) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def unique_name_in_lists(item_lists: Iterable[list], name_attr: str, wanted_name: str, ignore_item: Optional[Any] = None) -> str: | 
					
						
							|  |  |  |     """Creates a unique name that no item in the given lists already has.
 | 
					
						
							|  |  |  |     The format follows Blender's behaviour when handling duplicate | 
					
						
							|  |  |  |     object names. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @param item_lists An iterable of item lists (any type). | 
					
						
							|  |  |  |     @param name_attr The attribute of the items that holds the name. | 
					
						
							|  |  |  |     @param wanted_name The name that should be preferably returned, if | 
					
						
							|  |  |  |         no name collision occurs. | 
					
						
							|  |  |  |     @param ignore_item (Optional) Ignore this item in the list when | 
					
						
							|  |  |  |         comparing names. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     def _has_collision(name: str) -> bool: | 
					
						
							|  |  |  |         for item in itertools.chain(*item_lists): | 
					
						
							|  |  |  |             if item == ignore_item: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             if getattr(item, name_attr) == name: | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Check this once at the beginning to make sure the user can use | 
					
						
							|  |  |  |     # a wanted name like "XY.001" if they want, even if "XY" alone does | 
					
						
							|  |  |  |     # not collide | 
					
						
							|  |  |  |     if not _has_collision(wanted_name): | 
					
						
							|  |  |  |         return wanted_name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Get base name without numeric suffix | 
					
						
							|  |  |  |     base_name = wanted_name | 
					
						
							|  |  |  |     dot_pos = base_name.rfind('.') | 
					
						
							|  |  |  |     if dot_pos != -1: | 
					
						
							|  |  |  |         if base_name[dot_pos + 1:].isdecimal(): | 
					
						
							|  |  |  |             base_name = base_name[:dot_pos] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     num_collisions = 0 | 
					
						
							|  |  |  |     out_name = base_name | 
					
						
							|  |  |  |     while _has_collision(out_name): | 
					
						
							|  |  |  |         num_collisions += 1 | 
					
						
							|  |  |  |         out_name = f'{base_name}.{num_collisions:03d}' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return out_name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def merge_into_collection(col_src, col_dst, clear_dst=True): | 
					
						
							|  |  |  |     """Merges the items of the `col_src` collection property into the
 | 
					
						
							|  |  |  |     `col_dst` collection property. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     If `clear_dst` is true, the destination collection is cleared before | 
					
						
							|  |  |  |     merging. Otherwise, new items are added on top of the existing items | 
					
						
							|  |  |  |     in `col_dst`. There is no check for duplicates. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if clear_dst: | 
					
						
							|  |  |  |         col_dst.clear() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for item_src in col_src: | 
					
						
							|  |  |  |         item_dst = col_dst.add() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # collect names of writable properties | 
					
						
							|  |  |  |         prop_names = [p.identifier for p in item_src.bl_rna.properties | 
					
						
							|  |  |  |                       if not p.is_readonly] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # copy those properties | 
					
						
							|  |  |  |         for prop_name in prop_names: | 
					
						
							|  |  |  |             setattr(item_dst, prop_name, getattr(item_src, prop_name)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def safesrc(s): | 
					
						
							|  |  |  |     s = safestr(s).replace('.', '_').replace('-', '_').replace(' ', '') | 
					
						
							|  |  |  |     if s[0].isdigit(): | 
					
						
							|  |  |  |         s = '_' + s | 
					
						
							|  |  |  |     return s | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def safestr(s: str) -> str: | 
					
						
							|  |  |  |     """Outputs a string where special characters have been replaced with
 | 
					
						
							|  |  |  |     '_', which can be safely used in file and path names."""
 | 
					
						
							|  |  |  |     for c in r'''[]/\;,><&*:§$%=+@!#^()|?^'"''': | 
					
						
							|  |  |  |         s = s.replace(c, '_') | 
					
						
							|  |  |  |     return ''.join([i if ord(i) < 128 else '_' for i in s]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_haxe_json_string(d: dict) -> str: | 
					
						
							|  |  |  |     s = str(d) | 
					
						
							|  |  |  |     s = s.replace('True', 'true') | 
					
						
							|  |  |  |     s = s.replace('False', 'false') | 
					
						
							|  |  |  |     s = s.replace("'", '"') | 
					
						
							|  |  |  |     return s | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def asset_name(bdata): | 
					
						
							|  |  |  |     if bdata == None: | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  |     s = bdata.name | 
					
						
							|  |  |  |     # Append library name if linked | 
					
						
							|  |  |  |     if bdata.library is not None: | 
					
						
							|  |  |  |         s += '_' + bdata.library.name | 
					
						
							|  |  |  |     return s | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def asset_path(s): | 
					
						
							|  |  |  |     """Remove leading '//'""" | 
					
						
							|  |  |  |     return s[2:] if s[:2] == '//' else s | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def extract_filename(s): | 
					
						
							|  |  |  |     return os.path.basename(asset_path(s)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_render_resolution(scene): | 
					
						
							|  |  |  |     render = scene.render | 
					
						
							|  |  |  |     scale = render.resolution_percentage / 100 | 
					
						
							|  |  |  |     return int(render.resolution_x * scale), int(render.resolution_y * scale) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_texture_quality_percentage() -> int: | 
					
						
							|  |  |  |     return int(bpy.data.worlds['Lnx'].lnx_texture_quality * 100) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_project_scene_name(): | 
					
						
							|  |  |  |     return get_active_scene().name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_active_scene() -> bpy.types.Scene: | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     if not state.is_export: | 
					
						
							|  |  |  |         if wrd.lnx_play_scene is None: | 
					
						
							|  |  |  |             return bpy.context.scene | 
					
						
							|  |  |  |         return wrd.lnx_play_scene | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         item = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index] | 
					
						
							|  |  |  |         return item.lnx_project_scene | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def logic_editor_space(context_screen=None): | 
					
						
							|  |  |  |     if context_screen == None: | 
					
						
							|  |  |  |         context_screen = bpy.context.screen | 
					
						
							|  |  |  |     if context_screen != None: | 
					
						
							|  |  |  |         areas = context_screen.areas | 
					
						
							|  |  |  |         for area in areas: | 
					
						
							|  |  |  |             for space in area.spaces: | 
					
						
							|  |  |  |                 if space.type == 'NODE_EDITOR': | 
					
						
							|  |  |  |                     if space.node_tree != None and space.node_tree.bl_idname == 'LnxLogicTreeType': | 
					
						
							|  |  |  |                         return space | 
					
						
							|  |  |  |     return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def voxel_support(): | 
					
						
							|  |  |  |     # macos does not support opengl 4.5, needs metal | 
					
						
							|  |  |  |     return state.target != 'html5' and get_os() != 'mac' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_cascade_size(rpdat): | 
					
						
							|  |  |  |     cascade_size = int(rpdat.rp_shadowmap_cascade) | 
					
						
							|  |  |  |     # Clamp to 4096 per cascade | 
					
						
							|  |  |  |     if int(rpdat.rp_shadowmap_cascades) > 1 and cascade_size > 4096: | 
					
						
							|  |  |  |         cascade_size = 4096 | 
					
						
							|  |  |  |     return cascade_size | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def check_blender_version(op: bpy.types.Operator): | 
					
						
							|  |  |  |     """Check whether the Blender version is supported by Leenkx,
 | 
					
						
							|  |  |  |     if not, report in UI. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2025-05-10 16:30:16 +00:00
										 |  |  |     if bpy.app.version[:2] not in [(4, 4), (4, 2), (3, 6), (3, 3)]: | 
					
						
							|  |  |  |         op.report({'INFO'}, 'INFO: For Leenkx to work correctly, use a Blender LTS version') | 
					
						
							| 
									
										
										
										
											2025-01-22 16:18:30 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def check_saved(self): | 
					
						
							|  |  |  |     if bpy.data.filepath == "": | 
					
						
							|  |  |  |         msg = "Save blend file first" | 
					
						
							|  |  |  |         self.report({"ERROR"}, msg) if self is not None else log.warn(msg) | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def check_path(s): | 
					
						
							|  |  |  |     for c in r'[];><&*%=+@!#^()|?^': | 
					
						
							|  |  |  |         if c in s: | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  |     for c in s: | 
					
						
							|  |  |  |         if ord(c) > 127: | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  |     return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def check_sdkpath(self): | 
					
						
							|  |  |  |     s = get_sdk_path() | 
					
						
							|  |  |  |     if not check_path(s): | 
					
						
							|  |  |  |         msg = f"SDK path '{s}' contains special characters. Please move SDK to different path for now." | 
					
						
							|  |  |  |         self.report({"ERROR"}, msg) if self is not None else log.warn(msg) | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def check_projectpath(self): | 
					
						
							|  |  |  |     s = get_fp() | 
					
						
							|  |  |  |     if not check_path(s): | 
					
						
							|  |  |  |         msg = f"Project path '{s}' contains special characters, build process may fail." | 
					
						
							|  |  |  |         self.report({"ERROR"}, msg) if self is not None else log.warn(msg) | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def disp_enabled(target): | 
					
						
							|  |  |  |     rpdat = get_rp() | 
					
						
							|  |  |  |     if rpdat.lnx_rp_displacement == 'Tessellation': | 
					
						
							|  |  |  |         return target == 'krom' or target == 'native' | 
					
						
							|  |  |  |     return rpdat.lnx_rp_displacement != 'Off' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def is_object_animation_enabled(bobject): | 
					
						
							|  |  |  |     # Checks if animation is present and enabled | 
					
						
							|  |  |  |     if bobject.lnx_animation_enabled == False or bobject.type == 'BONE' or bobject.type == 'ARMATURE': | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if bobject.animation_data and bobject.animation_data.action: | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  |     return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def is_bone_animation_enabled(bobject): | 
					
						
							|  |  |  |     # Checks if animation is present and enabled for parented armature | 
					
						
							|  |  |  |     if bobject.parent and bobject.parent.type == 'ARMATURE': | 
					
						
							|  |  |  |         if bobject.parent.lnx_animation_enabled == False: | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  |         # Check for present actions | 
					
						
							|  |  |  |         adata = bobject.parent.animation_data | 
					
						
							|  |  |  |         has_actions = adata != None and adata.action != None | 
					
						
							|  |  |  |         if not has_actions and adata != None: | 
					
						
							|  |  |  |             if hasattr(adata, 'nla_tracks') and adata.nla_tracks != None: | 
					
						
							|  |  |  |                 for track in adata.nla_tracks: | 
					
						
							|  |  |  |                     if track.strips == None: | 
					
						
							|  |  |  |                         continue | 
					
						
							|  |  |  |                     for strip in track.strips: | 
					
						
							|  |  |  |                         if strip.action == None: | 
					
						
							|  |  |  |                             continue | 
					
						
							|  |  |  |                         has_actions = True | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |                     if has_actions: | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |         if adata != None and has_actions: | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |     return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def export_bone_data(bobject: bpy.types.Object) -> bool: | 
					
						
							|  |  |  |     """Returns whether the bone data of the given object should be exported.""" | 
					
						
							|  |  |  |     return bobject.find_armature() and is_bone_animation_enabled(bobject) and get_rp().lnx_skin == 'On' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def export_morph_targets(bobject: bpy.types.Object) -> bool: | 
					
						
							|  |  |  |     if get_rp().lnx_morph_target != 'On': | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not hasattr(bobject.data, 'shape_keys'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     shape_keys = bobject.data.shape_keys | 
					
						
							|  |  |  |     if not shape_keys: | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if len(shape_keys.key_blocks) < 2: | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     for shape_key in shape_keys.key_blocks[1:]: | 
					
						
							|  |  |  |             if(not shape_key.mute): | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |     return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def export_vcols(bobject: bpy.types.Object) -> bool: | 
					
						
							|  |  |  |     for material in bobject.data.materials: | 
					
						
							|  |  |  |         if material is not None and material.export_vcols: | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |     return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def open_editor(hx_path=None): | 
					
						
							|  |  |  |     ide_bin = get_ide_bin() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if hx_path is None: | 
					
						
							|  |  |  |         hx_path = lnx.utils.get_fp() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if get_code_editor() == 'default': | 
					
						
							|  |  |  |         # Get editor environment variables | 
					
						
							|  |  |  |         # https://unix.stackexchange.com/q/4859 | 
					
						
							|  |  |  |         env_v_editor = os.environ.get('VISUAL') | 
					
						
							|  |  |  |         env_editor = os.environ.get('EDITOR') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if env_v_editor is not None: | 
					
						
							|  |  |  |             ide_bin = env_v_editor | 
					
						
							|  |  |  |         elif env_editor is not None: | 
					
						
							|  |  |  |             ide_bin = env_editor | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # No environment variables set -> Let the system decide how to | 
					
						
							|  |  |  |         # open the file | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             webbrowser.open('file://' + hx_path) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if os.path.exists(ide_bin): | 
					
						
							|  |  |  |         args = [ide_bin, lnx.utils.get_fp()] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Sublime Text | 
					
						
							|  |  |  |         if get_code_editor() == 'sublime': | 
					
						
							|  |  |  |             project_name = lnx.utils.safestr(bpy.data.worlds['Lnx'].lnx_project_name) | 
					
						
							|  |  |  |             subl_project_path = lnx.utils.get_fp() + f'/{project_name}.sublime-project' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if not os.path.exists(subl_project_path): | 
					
						
							|  |  |  |                 generate_sublime_project(subl_project_path) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             args += ['--project', subl_project_path] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             args.append('--add') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         args.append(hx_path) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if lnx.utils.get_os() == 'mac': | 
					
						
							|  |  |  |             argstr = "" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             for arg in args: | 
					
						
							|  |  |  |                 if not (arg.startswith('-') or arg.startswith('--')): | 
					
						
							|  |  |  |                     argstr += '"' + arg + '"' | 
					
						
							|  |  |  |                 argstr += ' ' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             subprocess.Popen(argstr[:-1], shell=True) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             subprocess.Popen(args) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         raise FileNotFoundError(f'Code editor executable not found: {ide_bin}. You can change the path in the Leenkx preferences.') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def open_folder(folder_path: str): | 
					
						
							|  |  |  |     if lnx.utils.get_os() == 'win': | 
					
						
							|  |  |  |         subprocess.run(['explorer', folder_path]) | 
					
						
							|  |  |  |     elif lnx.utils.get_os() == 'mac': | 
					
						
							|  |  |  |         subprocess.run(['open', folder_path]) | 
					
						
							|  |  |  |     elif lnx.utils.get_os() == 'linux': | 
					
						
							|  |  |  |         subprocess.run(['xdg-open', folder_path]) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         webbrowser.open('file://' + folder_path) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def generate_sublime_project(subl_project_path): | 
					
						
							|  |  |  |     """Generates a [project_name].sublime-project file.""" | 
					
						
							|  |  |  |     print('Generating Sublime Text project file') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     project_data = { | 
					
						
							|  |  |  |         "folders": [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "path": ".", | 
					
						
							|  |  |  |                 "file_exclude_patterns": ["*.blend*", "*.lnx"] | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(subl_project_path, 'w', encoding='utf-8') as project_file: | 
					
						
							|  |  |  |         json.dump(project_data, project_file, ensure_ascii=False, indent=4) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def def_strings_to_array(strdefs): | 
					
						
							|  |  |  |     defs = strdefs.split('_') | 
					
						
							|  |  |  |     defs = defs[1:] | 
					
						
							|  |  |  |     defs = ['_' + d for d in defs] # Restore _ | 
					
						
							|  |  |  |     return defs | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_kha_target(target_name): # TODO: remove | 
					
						
							|  |  |  |     if target_name == 'macos-hl': | 
					
						
							|  |  |  |         return 'osx-hl' | 
					
						
							|  |  |  |     elif target_name.startswith('krom'): # krom-windows | 
					
						
							|  |  |  |         return 'krom' | 
					
						
							|  |  |  |     elif target_name == 'custom': | 
					
						
							|  |  |  |         return '' | 
					
						
							|  |  |  |     return target_name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def target_to_gapi(lnx_project_target: str) -> str: | 
					
						
							|  |  |  |     # TODO: align target names | 
					
						
							|  |  |  |     if lnx_project_target == 'krom': | 
					
						
							|  |  |  |         return 'lnx_gapi_' + lnx.utils.get_os() | 
					
						
							|  |  |  |     elif lnx_project_target == 'krom-windows': | 
					
						
							|  |  |  |         return 'lnx_gapi_win' | 
					
						
							|  |  |  |     elif lnx_project_target == 'windows-hl': | 
					
						
							|  |  |  |         return 'lnx_gapi_win' | 
					
						
							|  |  |  |     elif lnx_project_target == 'krom-linux': | 
					
						
							|  |  |  |         return 'lnx_gapi_linux' | 
					
						
							|  |  |  |     elif lnx_project_target == 'linux-hl': | 
					
						
							|  |  |  |         return 'lnx_gapi_linux' | 
					
						
							|  |  |  |     elif lnx_project_target == 'krom-macos': | 
					
						
							|  |  |  |         return 'lnx_gapi_mac' | 
					
						
							|  |  |  |     elif lnx_project_target == 'macos-hl': | 
					
						
							|  |  |  |         return 'lnx_gapi_mac' | 
					
						
							|  |  |  |     elif lnx_project_target == 'android-hl': | 
					
						
							|  |  |  |         return 'lnx_gapi_android' | 
					
						
							|  |  |  |     elif lnx_project_target == 'ios-hl': | 
					
						
							|  |  |  |         return 'lnx_gapi_ios' | 
					
						
							|  |  |  |     elif lnx_project_target == 'node': | 
					
						
							|  |  |  |         return 'lnx_gapi_html5' | 
					
						
							|  |  |  |     else: # html5, custom | 
					
						
							|  |  |  |         return 'lnx_gapi_' + lnx_project_target | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def check_default_props(): | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     if len(wrd.lnx_rplist) == 0: | 
					
						
							|  |  |  |         wrd.lnx_rplist.add() | 
					
						
							|  |  |  |         wrd.lnx_rplist_index = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if wrd.lnx_project_name == '': | 
					
						
							|  |  |  |         # Take blend file name | 
					
						
							|  |  |  |         wrd.lnx_project_name = lnx.utils.blend_name() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Enum Permissions Name | 
					
						
							|  |  |  | class PermissionName(Enum): | 
					
						
							|  |  |  |     ACCESS_COARSE_LOCATION = 'ACCESS_COARSE_LOCATION' | 
					
						
							|  |  |  |     ACCESS_NETWORK_STATE = 'ACCESS_NETWORK_STATE' | 
					
						
							|  |  |  |     ACCESS_FINE_LOCATION = 'ACCESS_FINE_LOCATION' | 
					
						
							|  |  |  |     ACCESS_WIFI_STATE = 'ACCESS_WIFI_STATE' | 
					
						
							|  |  |  |     BLUETOOTH = 'BLUETOOTH' | 
					
						
							|  |  |  |     BLUETOOTH_ADMIN = 'BLUETOOTH_ADMIN' | 
					
						
							|  |  |  |     CAMERA = 'CAMERA' | 
					
						
							|  |  |  |     EXPAND_STATUS_BAR = 'EXPAND_STATUS_BAR' | 
					
						
							|  |  |  |     FOREGROUND_SERVICE = 'FOREGROUND_SERVICE' | 
					
						
							|  |  |  |     GET_ACCOUNTS = 'GET_ACCOUNTS' | 
					
						
							|  |  |  |     INTERNET = 'INTERNET' | 
					
						
							|  |  |  |     READ_EXTERNAL_STORAGE = 'READ_EXTERNAL_STORAGE' | 
					
						
							|  |  |  |     VIBRATE = 'VIBRATE' | 
					
						
							|  |  |  |     WRITE_EXTERNAL_STORAGE = 'WRITE_EXTERNAL_STORAGE' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Add permission for target android | 
					
						
							|  |  |  | def add_permission_target_android(permission_name_enum): | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     check = False | 
					
						
							|  |  |  |     for item in wrd.lnx_exporter_android_permission_list: | 
					
						
							|  |  |  |         if (item.lnx_android_permissions.upper() == str(permission_name_enum.value).upper()): | 
					
						
							|  |  |  |             check = True | 
					
						
							|  |  |  |             break | 
					
						
							|  |  |  |     if not check: | 
					
						
							|  |  |  |         wrd.lnx_exporter_android_permission_list.add() | 
					
						
							|  |  |  |         wrd.lnx_exporter_android_permission_list[len(wrd.lnx_exporter_android_permission_list) - 1].lnx_android_permissions = str(permission_name_enum.value).upper() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_project_android_build_apk(): | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     return wrd.lnx_project_android_build_apk | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_android_sdk_root_path(): | 
					
						
							|  |  |  |     if os.getenv('ANDROID_SDK_ROOT') == None: | 
					
						
							|  |  |  |         addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |         return '' if not hasattr(addon_prefs, 'android_sdk_root_path') else addon_prefs.android_sdk_root_path | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return os.getenv('ANDROID_SDK_ROOT') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_android_apk_copy_path(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return '' if not hasattr(addon_prefs, 'android_apk_copy_path') else addon_prefs.android_apk_copy_path | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_android_apk_copy_open_directory(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return False if not hasattr(addon_prefs, 'android_apk_copy_open_directory') else addon_prefs.android_apk_copy_open_directory | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_android_emulators_list(): | 
					
						
							|  |  |  |     err = '' | 
					
						
							|  |  |  |     items = [] | 
					
						
							|  |  |  |     path_file = get_android_emulator_file() | 
					
						
							|  |  |  |     if len(path_file) > 0: | 
					
						
							|  |  |  |         cmd = path_file + " -list-avds" | 
					
						
							|  |  |  |         if get_os_is_windows(): | 
					
						
							|  |  |  |             process = subprocess.Popen(cmd, stdout=subprocess.PIPE) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE) | 
					
						
							|  |  |  |         while True: | 
					
						
							|  |  |  |             output = process.stdout.readline().decode("utf-8") | 
					
						
							|  |  |  |             if len(output.strip()) == 0 and process.poll() is not None: | 
					
						
							|  |  |  |                 break | 
					
						
							|  |  |  |             if output: | 
					
						
							|  |  |  |                 items.append(output.strip()) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         err = 'File "'+ path_file +'" not found.' | 
					
						
							|  |  |  |     return items, err | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_android_emulator_path(): | 
					
						
							|  |  |  |     return os.path.join(get_android_sdk_root_path(), "emulator") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_android_emulator_file(): | 
					
						
							|  |  |  |     path_file = '' | 
					
						
							|  |  |  |     if get_os_is_windows(): | 
					
						
							|  |  |  |         path_file = os.path.join(get_android_emulator_path(), "emulator.exe") | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         path_file = os.path.join(get_android_emulator_path(), "emulator") | 
					
						
							|  |  |  |     # File Exists | 
					
						
							|  |  |  |     return '' if not os.path.isfile(path_file) else path_file | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_android_emulator_name(): | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     return '' if not len(wrd.lnx_project_android_list_avd.strip()) > 0 else wrd.lnx_project_android_list_avd.strip() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_android_open_build_apk_directory(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return False if not hasattr(addon_prefs, 'android_open_build_apk_directory') else addon_prefs.android_open_build_apk_directory | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_html5_copy_path(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return '' if not hasattr(addon_prefs, 'html5_copy_path') else addon_prefs.html5_copy_path | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_link_web_server(): | 
					
						
							|  |  |  |     addon_prefs = get_lnx_preferences() | 
					
						
							|  |  |  |     return '' if not hasattr(addon_prefs, 'link_web_server') else addon_prefs.link_web_server | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_file_lnx_version_tuple() -> tuple[int]: | 
					
						
							|  |  |  |     wrd = bpy.data.worlds['Lnx'] | 
					
						
							|  |  |  |     return tuple(map(int, wrd.lnx_version.split('.'))) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def type_name_to_type(name: str) -> bpy.types.bpy_struct: | 
					
						
							|  |  |  |     """Return the Blender type given by its name, if registered.""" | 
					
						
							|  |  |  |     return bpy.types.bpy_struct.bl_rna_get_subclass_py(name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def change_version_project(version: str) -> str: | 
					
						
							|  |  |  |     ver = version.strip().replace(' ', '').split('.') | 
					
						
							|  |  |  |     v_i = int(ver[len(ver) - 1]) + 1 | 
					
						
							|  |  |  |     ver[len(ver) - 1] = str(v_i) | 
					
						
							|  |  |  |     version = '' | 
					
						
							|  |  |  |     for i in ver: | 
					
						
							|  |  |  |         if len(version) > 0: | 
					
						
							|  |  |  |             version += '.' | 
					
						
							|  |  |  |         version += i | 
					
						
							|  |  |  |     return version | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def cpu_count(*, physical_only=False) -> Optional[int]: | 
					
						
							|  |  |  |     """Returns the number of logical (default) or physical CPUs.
 | 
					
						
							|  |  |  |     The result can be `None` if `os.cpu_count()` was not able to get the | 
					
						
							|  |  |  |     correct count of logical CPUs. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if not physical_only: | 
					
						
							|  |  |  |         return os.cpu_count() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     err_reason = '' | 
					
						
							|  |  |  |     command = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _os = get_os() | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         if _os == 'win': | 
					
						
							|  |  |  |             sysroot = os.environ.get("SYSTEMROOT", default="C:\\WINDOWS") | 
					
						
							|  |  |  |             command = [f'{sysroot}\\System32\\wbem\\wmic.exe', 'cpu', 'get', 'NumberOfCores'] | 
					
						
							|  |  |  |             result = subprocess.check_output(command) | 
					
						
							|  |  |  |             result = result.decode('utf-8').splitlines() | 
					
						
							|  |  |  |             result = int(result[2]) | 
					
						
							|  |  |  |             if result > 0: | 
					
						
							|  |  |  |                 return result | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         elif _os == 'linux': | 
					
						
							|  |  |  |             command = ["grep -P '^core id' /proc/cpuinfo | sort -u | wc -l"] | 
					
						
							|  |  |  |             result = subprocess.check_output(command[0], shell=True) | 
					
						
							|  |  |  |             result = result.decode('utf-8').splitlines() | 
					
						
							|  |  |  |             result = int(result[0]) | 
					
						
							|  |  |  |             if result > 0: | 
					
						
							|  |  |  |                 return result | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # macOS | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             command = ['sysctl', '-n', 'hw.physicalcpu'] | 
					
						
							|  |  |  |             return int(subprocess.check_output(command)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     except subprocess.CalledProcessError as e: | 
					
						
							|  |  |  |         err_reason = f'Reason: command {command} exited with code {e.returncode}.' | 
					
						
							|  |  |  |     except FileNotFoundError as e: | 
					
						
							|  |  |  |         err_reason = f'Reason: couldn\'t open file from command {command} ({e.errno=}).' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Last resort even though it can be wrong | 
					
						
							|  |  |  |     log.warn("Could not retrieve count of physical CPUs, using logical CPU count instead.\n\t" + err_reason) | 
					
						
							|  |  |  |     return os.cpu_count() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def register(local_sdk=False): | 
					
						
							|  |  |  |     global use_local_sdk | 
					
						
							|  |  |  |     use_local_sdk = local_sdk | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def unregister(): | 
					
						
							|  |  |  |     pass |