import os import shutil import stat import subprocess import webbrowser import bpy from bpy.types import Menu, Panel, UIList from bpy.props import * import lnx.assets as assets import lnx.utils import lnx.utils_vs if lnx.is_reload(__name__): assets = lnx.reload_module(assets) lnx.utils = lnx.reload_module(lnx.utils) lnx.utils_vs = lnx.reload_module(lnx.utils_vs) else: lnx.enable_reload(__name__) def remove_readonly(func, path, excinfo): os.chmod(path, stat.S_IWRITE) func(path) def update_gapi_custom(self, context): bpy.data.worlds['Lnx'].lnx_recompile = True assets.invalidate_compiled_data(self, context) def update_gapi_win(self, context): if os.path.isdir(lnx.utils.get_fp_build() + '/windows-build'): shutil.rmtree(lnx.utils.get_fp_build() + '/windows-build', onerror=remove_readonly) bpy.data.worlds['Lnx'].lnx_recompile = True assets.invalidate_compiled_data(self, context) def update_gapi_linux(self, context): if os.path.isdir(lnx.utils.get_fp_build() + '/linux-build'): shutil.rmtree(lnx.utils.get_fp_build() + '/linux-build', onerror=remove_readonly) bpy.data.worlds['Lnx'].lnx_recompile = True assets.invalidate_compiled_data(self, context) def update_gapi_mac(self, context): if os.path.isdir(lnx.utils.get_fp_build() + '/osx-build'): shutil.rmtree(lnx.utils.get_fp_build() + '/osx-build', onerror=remove_readonly) bpy.data.worlds['Lnx'].lnx_recompile = True assets.invalidate_compiled_data(self, context) def update_gapi_android(self, context): if os.path.isdir(lnx.utils.get_fp_build() + '/android-build'): shutil.rmtree(lnx.utils.get_fp_build() + '/android-build', onerror=remove_readonly) bpy.data.worlds['Lnx'].lnx_recompile = True assets.invalidate_compiled_data(self, context) def update_gapi_ios(self, context): if os.path.isdir(lnx.utils.get_fp_build() + '/ios-build'): shutil.rmtree(lnx.utils.get_fp_build() + '/ios-build', onerror=remove_readonly) bpy.data.worlds['Lnx'].lnx_recompile = True assets.invalidate_compiled_data(self, context) def update_gapi_html5(self, context): bpy.data.worlds['Lnx'].lnx_recompile = True assets.invalidate_compiled_data(self, context) class LnxExporterListItem(bpy.types.PropertyGroup): name: StringProperty( name="Name", description="A name for this item", default="Preset") lnx_project_rp: StringProperty( name="Render Path", description="A name for this item", default="") lnx_project_scene: PointerProperty( name="Scene", description="Scene to load when launching", type=bpy.types.Scene) lnx_project_target: EnumProperty( items = [('html5', 'HTML5 (JS)', 'html5'), ('windows-hl', 'Windows (C)', 'windows-hl'), ('krom-windows', 'Windows (Krom)', 'krom-windows'), ('macos-hl', 'macOS (C)', 'macos-hl'), ('krom-macos', 'macOS (Krom)', 'krom-macos'), ('linux-hl', 'Linux (C)', 'linux-hl'), ('krom-linux', 'Linux (Krom)', 'krom-linux'), ('ios-hl', 'iOS (C)', 'ios-hl'), ('android-hl', 'Android (C)', 'android-hl'), ('node', 'Node (JS)', 'node'), ('custom', 'Custom', 'custom'),], name="Target", default='html5', description='Build platform') lnx_project_khamake: StringProperty(name="Khamake", description="Specify arguments for the 'node Kha/make' call") lnx_gapi_custom: EnumProperty( items = [('opengl', 'OpenGL', 'opengl'), ('vulkan', 'Vulkan', 'vulkan'), ('direct3d11', 'Direct3D11', 'direct3d11'), ('direct3d12', 'Direct3D12', 'direct3d12'), ('metal', 'Metal', 'metal')], name="Graphics API", default='opengl', description='Based on currently selected target', update=update_gapi_custom) lnx_gapi_win: EnumProperty( items = [('direct3d11', 'Auto', 'direct3d11'), ('opengl', 'OpenGL', 'opengl'), ('vulkan', 'Vulkan', 'vulkan'), ('direct3d11', 'Direct3D11', 'direct3d11'), ('direct3d12', 'Direct3D12', 'direct3d12')], name="Graphics API", default='direct3d11', description='Based on currently selected target', update=update_gapi_win) lnx_gapi_linux: EnumProperty( items = [('opengl', 'Auto', 'opengl'), ('opengl', 'OpenGL', 'opengl'), ('vulkan', 'Vulkan', 'vulkan')], name="Graphics API", default='opengl', description='Based on currently selected target', update=update_gapi_linux) lnx_gapi_android: EnumProperty( items = [('opengl', 'Auto', 'opengl'), ('opengl', 'OpenGL', 'opengl'), ('vulkan', 'Vulkan', 'vulkan')], name="Graphics API", default='opengl', description='Based on currently selected target', update=update_gapi_android) lnx_gapi_mac: EnumProperty( items = [('opengl', 'Auto', 'opengl'), ('opengl', 'OpenGL', 'opengl'), ('metal', 'Metal', 'metal')], name="Graphics API", default='opengl', description='Based on currently selected target', update=update_gapi_mac) lnx_gapi_ios: EnumProperty( items = [('opengl', 'Auto', 'opengl'), ('opengl', 'OpenGL', 'opengl'), ('metal', 'Metal', 'metal')], name="Graphics API", default='opengl', description='Based on currently selected target', update=update_gapi_ios) lnx_gapi_html5: EnumProperty( items = [('webgl', 'Auto', 'webgl'), ('webgl', 'WebGL2', 'webgl')], name="Graphics API", default='webgl', description='Based on currently selected target', update=update_gapi_html5) class LnxExporterAndroidPermissionListItem(bpy.types.PropertyGroup): lnx_android_permissions: EnumProperty( items = [('ACCESS_COARSE_LOCATION ', 'Access Coarse Location', 'Allows an app to access approximate location'), ('ACCESS_NETWORK_STATE', 'Access Network State', 'Allows applications to access information about networks'), ('ACCESS_FINE_LOCATION', 'Access Fine Location', 'Allows an app to access precise location'), ('ACCESS_WIFI_STATE', 'Access Wi-Fi State', 'Allows applications to access information about Wi-Fi networks'), ('BLUETOOTH', 'Bluetooth', 'Allows applications to connect to paired bluetooth devices'), ('BLUETOOTH_ADMIN', 'Bluetooth Admin', 'Allows applications to discover and pair bluetooth devices'), ('CAMERA', 'Camera', 'Required to be able to access the camera device'), ('EXPAND_STATUS_BAR', 'Expand Status Bar', 'Allows an application to expand or collapse the status bar'), ('FOREGROUND_SERVICE', 'Foreground Service', 'Allows a regular application to use Service.startForeground'), ('GET_ACCOUNTS', 'Get Accounts', 'Allows access to the list of accounts in the Accounts Service'), ('INTERNET', 'Internet', 'Allows applications to open network sockets'), ('READ_EXTERNAL_STORAGE', 'Read External Storage', 'Allows an application to read from external storage.'), ('VIBRATE', 'Vibrate', 'Allows access to the vibrator'), ('WRITE_EXTERNAL_STORAGE', 'Write External Storage', 'Allows an application to write to external storage')], name="Permission", default='VIBRATE', description='Android Permission') class LnxExporterAndroidAbiListItem(bpy.types.PropertyGroup): lnx_android_abi: EnumProperty( items = [('arm64-v8a', 'arm64-v8a', 'This ABI is for ARMv8-A based CPUs, which support the 64-bit AArch64 architecture'), ('armeabi-v7a', 'armeabi-v7a', 'This ABI is for 32-bit ARM-based CPUs'), ('x86', 'x86', 'This ABI is for CPUs supporting the instruction set commonly known as x86, i386, or IA-32'), ('x86_64', 'x86_64', 'This ABI is for CPUs supporting the instruction set commonly referred to as x86-64')], name="Android ABI", default='arm64-v8a', description='Android ABI') class LNX_UL_ExporterList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): # We could write some code to decide which icon to use here... custom_icon = 'DOT' # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: row = layout.row() row.prop(item, "name", text="", emboss=False, icon=custom_icon) col = row.column() col.alignment = 'RIGHT' col.label(text=item.lnx_project_target) elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' layout.label(text="", icon = custom_icon) class LNX_UL_Exporter_AndroidPermissionList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): # We could write some code to decide which icon to use here... custom_icon = 'DOT' # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: row = layout.row() row.prop(item, "name", text="", emboss=False, icon=custom_icon) col = row.column() col.alignment = 'RIGHT' col.label(text=item.lnx_android_permissions) elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' layout.label(text="", icon = custom_icon) class LNX_UL_Exporter_AndroidAbiList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): # We could write some code to decide which icon to use here... custom_icon = 'DOT' # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: row = layout.row() row.prop(item, "name", text="", emboss=False, icon=custom_icon) col = row.column() col.alignment = 'RIGHT' col.label(text=item.lnx_android_abi) elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' layout.label(text="", icon = custom_icon) class LnxExporterListNewItem(bpy.types.Operator): # Add a new item to the list bl_idname = "lnx_exporterlist.new_item" bl_label = "Add a new item" def execute(self, context): mdata = bpy.data.worlds['Lnx'] mdata.lnx_exporterlist.add() mdata.lnx_exporterlist_index = len(mdata.lnx_exporterlist) - 1 if len(mdata.lnx_rplist) > mdata.lnx_exporterlist_index: mdata.lnx_exporterlist[-1].lnx_project_rp = mdata.lnx_rplist[mdata.lnx_rplist_index].name mdata.lnx_exporterlist[-1].lnx_project_scene = context.scene return{'FINISHED'} class LnxExporterListDeleteItem(bpy.types.Operator): # Delete the selected item from the list bl_idname = "lnx_exporterlist.delete_item" bl_label = "Deletes an item" @classmethod def poll(self, context): """ Enable if there's something in the list """ mdata = bpy.data.worlds['Lnx'] return len(mdata.lnx_exporterlist) > 0 def execute(self, context): mdata = bpy.data.worlds['Lnx'] list = mdata.lnx_exporterlist index = mdata.lnx_exporterlist_index list.remove(index) if index > 0: index = index - 1 mdata.lnx_exporterlist_index = index return{'FINISHED'} class LnxExporterListMoveItem(bpy.types.Operator): # Move an item in the list bl_idname = "lnx_exporterlist.move_item" bl_label = "Move an item in the list" direction: EnumProperty( items=( ('UP', 'Up', ""), ('DOWN', 'Down', ""),)) def move_index(self): # Move index of an item render queue while clamping it mdata = bpy.data.worlds['Lnx'] index = mdata.lnx_exporterlist_index list_length = len(mdata.lnx_exporterlist) - 1 new_index = 0 if self.direction == 'UP': new_index = index - 1 elif self.direction == 'DOWN': new_index = index + 1 new_index = max(0, min(new_index, list_length)) mdata.lnx_exporterlist.move(index, new_index) mdata.lnx_exporterlist_index = new_index def execute(self, context): mdata = bpy.data.worlds['Lnx'] list = mdata.lnx_exporterlist index = mdata.lnx_exporterlist_index if self.direction == 'DOWN': neighbor = index + 1 self.move_index() elif self.direction == 'UP': neighbor = index - 1 self.move_index() else: return{'CANCELLED'} return{'FINISHED'} class LnxExporter_AndroidPermissionListNewItem(bpy.types.Operator): # Add a new item to the list bl_idname = "lnx_exporter_android_permission_list.new_item" bl_label = "Add a new item" def execute(self, context): mdata = bpy.data.worlds['Lnx'] mdata.lnx_exporter_android_permission_list.add() mdata.lnx_exporter_android_permission_list_index = len(mdata.lnx_exporter_android_permission_list) - 1 return{'FINISHED'} class LnxExporter_AndroidPermissionListDeleteItem(bpy.types.Operator): # Delete the selected item from the list bl_idname = "lnx_exporter_android_permission_list.delete_item" bl_label = "Deletes an item" @classmethod def poll(self, context): """ Enable if there's something in the list """ mdata = bpy.data.worlds['Lnx'] return len(mdata.lnx_exporter_android_permission_list) > 0 def execute(self, context): mdata = bpy.data.worlds['Lnx'] list = mdata.lnx_exporter_android_permission_list index = mdata.lnx_exporter_android_permission_list_index list.remove(index) if index > 0: index = index - 1 mdata.lnx_exporter_android_permission_list_index = index return{'FINISHED'} class LnxExporter_AndroidAbiListNewItem(bpy.types.Operator): # Add a new item to the list bl_idname = "lnx_exporter_android_abi_list.new_item" bl_label = "Add a new item" def execute(self, context): mdata = bpy.data.worlds['Lnx'] mdata.lnx_exporter_android_abi_list.add() mdata.lnx_exporter_android_abi_list_index = len(mdata.lnx_exporter_android_abi_list) - 1 return{'FINISHED'} class LnxExporter_AndroidAbiListDeleteItem(bpy.types.Operator): # Delete the selected item from the list bl_idname = "lnx_exporter_android_abi_list.delete_item" bl_label = "Deletes an item" @classmethod def poll(self, context): """ Enable if there's something in the list """ mdata = bpy.data.worlds['Lnx'] return len(mdata.lnx_exporter_android_abi_list) > 0 def execute(self, context): mdata = bpy.data.worlds['Lnx'] list = mdata.lnx_exporter_android_abi_list index = mdata.lnx_exporter_android_abi_list_index list.remove(index) if index > 0: index = index - 1 mdata.lnx_exporter_android_abi_list_index = index return{'FINISHED'} class LnxExporterSpecialsMenu(bpy.types.Menu): bl_label = "More" bl_idname = "LNX_MT_ExporterListSpecials" def draw(self, context): layout = self.layout layout.operator("lnx.exporter_open_folder") layout.operator("lnx.exporter_gpuprofile") if lnx.utils.get_os_is_windows(): layout.operator("lnx.exporter_open_vs") class LeenkxExporterOpenFolderButton(bpy.types.Operator): """Open published folder""" bl_idname = 'lnx.exporter_open_folder' bl_label = 'Open Folder' def execute(self, context): wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_exporterlist) == 0: return {'CANCELLED'} item = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index] p = os.path.join(lnx.utils.get_fp_build(), item.lnx_project_target) if os.path.exists(p): webbrowser.open('file://' + p) return{'FINISHED'} class LnxExporterGpuProfileButton(bpy.types.Operator): '''GPU profile''' bl_idname = 'lnx.exporter_gpuprofile' bl_label = 'Open in RenderDoc' def execute(self, context): p = lnx.utils.get_renderdoc_path() if p == '': self.report({'ERROR'}, 'Configure RenderDoc path in Leenkx add-on preferences') return {'CANCELLED'} pbin = '' base = lnx.utils.get_fp_build() ext1 = '/krom-windows/' + lnx.utils.safestr(bpy.data.worlds['Lnx'].lnx_project_name) + '.exe' ext2 = '/krom-linux/' + lnx.utils.safestr(bpy.data.worlds['Lnx'].lnx_project_name) if os.path.exists(base + ext1): pbin = base + ext1 elif os.path.exists(base + ext2): pbin = base + ext2 if pbin == '': self.report({'ERROR'}, 'Publish project using Krom target first') return {'CANCELLED'} subprocess.Popen([p, pbin]) return{'FINISHED'} class LNX_OT_ExporterOpenVS(bpy.types.Operator): """Open the generated project in Visual Studio, if installed""" bl_idname = 'lnx.exporter_open_vs' bl_label = 'Open in Visual Studio' @classmethod def poll(cls, context): if not lnx.utils.get_os_is_windows(): cls.poll_message_set('This operator is only supported on Windows') return False wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_exporterlist) == 0: cls.poll_message_set('No export configuration exists') return False if wrd.lnx_exporterlist[wrd.lnx_exporterlist_index].lnx_project_target != 'windows-hl': cls.poll_message_set('This operator only works with the Windows (C) target') return False return True def execute(self, context): version_major, version_min_full, err = lnx.utils_vs.fetch_project_version() if err is not None: if err == 'err_file_not_found': self.report({'ERROR'}, 'Publish project using Windows (C) target first') elif err.startswith('err_invalid_version'): self.report({'ERROR'}, 'Could not parse Visual Studio version, check console for details') return {'CANCELLED'} success = lnx.utils_vs.open_project_in_vs(version_major, version_min_full) if not success: self.report({'ERROR'}, 'Could not open the project in Visual Studio, check console for details') return {'CANCELLED'} return{'FINISHED'} __REG_CLASSES = ( LnxExporterListItem, LnxExporterAndroidPermissionListItem, LnxExporterAndroidAbiListItem, LNX_UL_ExporterList, LNX_UL_Exporter_AndroidPermissionList, LNX_UL_Exporter_AndroidAbiList, LnxExporterListNewItem, LnxExporterListDeleteItem, LnxExporterListMoveItem, LnxExporter_AndroidPermissionListNewItem, LnxExporter_AndroidPermissionListDeleteItem, LnxExporter_AndroidAbiListNewItem, LnxExporter_AndroidAbiListDeleteItem, LnxExporterSpecialsMenu, LnxExporterGpuProfileButton, LeenkxExporterOpenFolderButton, LNX_OT_ExporterOpenVS ) __reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) def register(): __reg_classes() bpy.types.World.lnx_exporterlist = CollectionProperty(type=LnxExporterListItem) bpy.types.World.lnx_exporterlist_index = IntProperty(name="Index for my_list", default=0) bpy.types.World.lnx_exporter_android_permission_list = CollectionProperty(type=LnxExporterAndroidPermissionListItem) bpy.types.World.lnx_exporter_android_permission_list_index = IntProperty(name="Index for my_list", default=0) bpy.types.World.lnx_exporter_android_abi_list = CollectionProperty(type=LnxExporterAndroidAbiListItem) bpy.types.World.lnx_exporter_android_abi_list_index = IntProperty(name="Index for my_list", default=0) def unregister(): __unreg_classes()