diff --git a/.gitignore b/.gitignore index 1e4ee2c2..590fac33 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ CMakeFiles cmake_install.cmake CMakeCache.txt Makefile +lib + +#Selenium +__hubs_selenium_profile \ No newline at end of file diff --git a/addons/io_hubs_addon/__init__.py b/addons/io_hubs_addon/__init__.py index e9029d0e..f1ec75d2 100644 --- a/addons/io_hubs_addon/__init__.py +++ b/addons/io_hubs_addon/__init__.py @@ -1,8 +1,13 @@ +from .utils import create_prefs_dir +from .utils import get_user_python_path +import sys import bpy from .io import gltf_exporter from . import (nodes, components) from . import preferences from . import third_party +from . import debugger +from . import icons bl_info = { "name": "Hubs Blender Addon", "author": "Mozilla Hubs", @@ -17,13 +22,19 @@ "category": "Generic" } +sys.path.insert(0, get_user_python_path()) + +create_prefs_dir() + def register(): + icons.register() preferences.register() nodes.register() components.register() gltf_exporter.register() third_party.register() + debugger.register() # Migrate components if the add-on is enabled in the middle of a session. if bpy.context.preferences.is_dirty: @@ -40,6 +51,8 @@ def unregister(): components.unregister() nodes.unregister() preferences.unregister() + debugger.unregister() + icons.unregister() # called by gltf-blender-io after it has loaded diff --git a/addons/io_hubs_addon/components/components_registry.py b/addons/io_hubs_addon/components/components_registry.py index f7ab3e99..553e44ca 100644 --- a/addons/io_hubs_addon/components/components_registry.py +++ b/addons/io_hubs_addon/components/components_registry.py @@ -1,5 +1,4 @@ from .types import NodeType -import bpy.utils.previews import bpy from bpy.props import BoolProperty, StringProperty, CollectionProperty, PointerProperty from bpy.types import PropertyGroup @@ -7,7 +6,6 @@ import importlib import inspect import os -from os import listdir from os.path import join, isfile, isdir, dirname, realpath from .hubs_component import HubsComponent @@ -125,25 +123,6 @@ def unload_components_registry(): module.unregister_module() -def load_icons(): - global __component_icons - __component_icons = {} - pcoll = bpy.utils.previews.new() - icons_dir = os.path.join(os.path.dirname(__file__), "icons") - icons = [f for f in listdir(icons_dir) if isfile(join(icons_dir, f))] - for icon in icons: - pcoll.load(icon, os.path.join(icons_dir, icon), 'IMAGE') - print("Loading icon: " + icon) - __component_icons["hubs"] = pcoll - - -def unload_icons(): - global __component_icons - __component_icons["hubs"].close() - del __component_icons - - -__component_icons = {} __components_registry = {} @@ -152,11 +131,6 @@ def get_components_registry(): return __components_registry -def get_components_icons(): - global __component_icons - return __component_icons["hubs"] - - def get_component_by_name(component_name): global __components_registry return next( @@ -166,7 +140,6 @@ def get_component_by_name(component_name): def register(): - load_icons() load_components_registry() bpy.utils.register_class(HubsComponentName) @@ -201,7 +174,6 @@ def unregister(): glTF2ExportUserExtension.remove_excluded_property("hubs_component_list") unload_components_registry() - unload_icons() global __components_registry del __components_registry diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index c0f0c2a7..86a2caf4 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -5,11 +5,12 @@ from .types import PanelType, MigrationType from .utils import get_object_source, has_component, add_component, remove_component, wrap_text, display_wrapped_text, is_dep_required, update_image_editors -from .components_registry import get_components_registry, get_components_icons, get_component_by_name +from .components_registry import get_components_registry, get_component_by_name from ..preferences import get_addon_pref from .handlers import migrate_components from .gizmos import update_gizmos from .utils import is_linked, redraw_component_ui +from ..icons import get_hubs_icons import os @@ -30,22 +31,26 @@ def poll(cls, context): if panel_type == PanelType.SCENE: if is_linked(context.scene): if bpy.app.version >= (3, 0, 0): - cls.poll_message_set("Cannot add components to linked scenes") + cls.poll_message_set( + "Cannot add components to linked scenes") return False elif panel_type == PanelType.OBJECT: if is_linked(context.active_object): if bpy.app.version >= (3, 0, 0): - cls.poll_message_set("Cannot add components to linked objects") + cls.poll_message_set( + "Cannot add components to linked objects") return False elif panel_type == PanelType.MATERIAL: if is_linked(context.active_object.active_material): if bpy.app.version >= (3, 0, 0): - cls.poll_message_set("Cannot add components to linked materials") + cls.poll_message_set( + "Cannot add components to linked materials") return False elif panel_type == PanelType.BONE: if is_linked(context.active_bone): if bpy.app.version >= (3, 0, 0): - cls.poll_message_set("Cannot add components to linked bones") + cls.poll_message_set( + "Cannot add components to linked bones") return False return True @@ -72,7 +77,7 @@ def filter_source_type(cmp): return not component_class.is_dep_only() and PanelType(panel_type) in component_class.get_panel_type() and component_class.poll(PanelType(panel_type), host, ob=context.object) components_registry = get_components_registry() - components_icons = get_components_icons() + hubs_icons = get_hubs_icons() filtered_components = dict( filter(filter_source_type, components_registry.items())) @@ -152,11 +157,11 @@ def draw(self, context): if icon.find('.') != -1: if has_component(obj, component_name): op = column.label( - text=component_display_name, icon_value=components_icons[icon].icon_id) + text=component_display_name, icon_value=hubs_icons[icon].icon_id) else: op = column.operator( AddHubsComponent.bl_idname, text=component_display_name, - icon_value=components_icons[icon].icon_id) + icon_value=hubs_icons[icon].icon_id) op.component_name = component_name op.panel_type = panel_type else: @@ -214,22 +219,26 @@ def poll(cls, context): if panel_type == PanelType.SCENE: if is_linked(context.scene): if bpy.app.version >= (3, 0, 0): - cls.poll_message_set("Cannot remove components from linked scenes") + cls.poll_message_set( + "Cannot remove components from linked scenes") return False elif panel_type == PanelType.OBJECT: if is_linked(context.active_object): if bpy.app.version >= (3, 0, 0): - cls.poll_message_set("Cannot remove components from linked objects") + cls.poll_message_set( + "Cannot remove components from linked objects") return False elif panel_type == PanelType.MATERIAL: if is_linked(context.active_object.active_material): if bpy.app.version >= (3, 0, 0): - cls.poll_message_set("Cannot remove components from linked materials") + cls.poll_message_set( + "Cannot remove components from linked materials") return False elif panel_type == PanelType.BONE: if is_linked(context.active_bone): if bpy.app.version >= (3, 0, 0): - cls.poll_message_set("Cannot add components to linked bones") + cls.poll_message_set( + "Cannot add components to linked bones") return False return True @@ -257,7 +266,8 @@ class MigrateHubsComponents(Operator): def execute(self, context): if self.is_registration: - migrate_components(MigrationType.REGISTRATION, do_beta_versioning=True) + migrate_components(MigrationType.REGISTRATION, + do_beta_versioning=True) else: migrate_components(MigrationType.LOCAL, do_beta_versioning=True) @@ -288,7 +298,8 @@ def execute(self, context): wm = context.window_manager title = wm.hubs_report_last_title report_string = wm.hubs_report_last_report_string - bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title=title, report_string=report_string) + bpy.ops.wm.hubs_report_viewer( + 'INVOKE_DEFAULT', title=title, report_string=report_string) return {'FINISHED'} @@ -314,12 +325,15 @@ def highlight_info_report(self): while bpy.ops.info.select_pick( context_override, report_index=index, extend=False) != {'CANCELLED'}: index += 1 - bpy.ops.info.select_pick(context_override, report_index=index, extend=False) + bpy.ops.info.select_pick( + context_override, report_index=index, extend=False) def execute(self, context): messages = split_and_prefix_report_messages(self.report_string) - info_report_string = '\n'.join([message.replace('\n', ' ') for message in messages]) - self.report({'INFO'}, f"Hubs {self.title}\n{info_report_string}\nEnd of Hubs {self.title}") + info_report_string = '\n'.join( + [message.replace('\n', ' ') for message in messages]) + self.report( + {'INFO'}, f"Hubs {self.title}\n{info_report_string}\nEnd of Hubs {self.title}") bpy.ops.screen.info_log_show() bpy.app.timers.register(self.highlight_info_report) return {'FINISHED'} @@ -403,13 +417,15 @@ def draw(self, context): scroll_up = scroll_column.row() scroll_up.enabled = start_index > 0 - op = scroll_up.operator(ReportScroller.bl_idname, text="", icon="TRIA_UP") + op = scroll_up.operator(ReportScroller.bl_idname, + text="", icon="TRIA_UP") op.increment = -1 op.maximum = maximum_scrolling scroll_down = scroll_column.row() scroll_down.enabled = start_index < maximum_scrolling - op = scroll_down.operator(ReportScroller.bl_idname, text="", icon="TRIA_DOWN") + op = scroll_down.operator( + ReportScroller.bl_idname, text="", icon="TRIA_DOWN") op.increment = 1 op.maximum = maximum_scrolling @@ -419,7 +435,8 @@ def draw(self, context): scroll_percentage = column.row() scroll_percentage.enabled = False - scroll_percentage.prop(wm, "hubs_report_scroll_percentage", slider=True) + scroll_percentage.prop( + wm, "hubs_report_scroll_percentage", slider=True) layout.separator() @@ -457,7 +474,8 @@ def init_report_display_blocks(self): if last_message is None: final_block = True - current_block_lines = sum([len(message) for message in block_messages]) + current_block_lines = sum([len(message) + for message in block_messages]) needed_padding_lines = self.lines_to_show - current_block_lines message_iter = iter(block_messages) @@ -506,7 +524,8 @@ class CopyHubsComponent(Operator): def poll(cls, context): if is_linked(context.scene): if bpy.app.version >= (3, 0, 0): - cls.poll_message_set("Cannot copy components when in linked scenes") + cls.poll_message_set( + "Cannot copy components when in linked scenes") return False if hasattr(context, "panel"): @@ -518,14 +537,17 @@ def poll(cls, context): def get_selected_bones(self, context): selected_bones = context.selected_pose_bones if context.mode == "POSE" else context.selected_editable_bones - selected_armatures = [sel_ob for sel_ob in context.selected_objects if sel_ob.type == "ARMATURE"] + selected_armatures = [ + sel_ob for sel_ob in context.selected_objects if sel_ob.type == "ARMATURE"] selected_hosts = [] for armature in selected_armatures: armature_bones = armature.pose.bones if context.mode == "POSE" else armature.data.edit_bones target_armature_bones = armature.data.bones if context.mode == "POSE" else armature.data.edit_bones - target_bones = [bone for bone in armature_bones if bone in selected_bones] + target_bones = [ + bone for bone in armature_bones if bone in selected_bones] for target_bone in target_bones: - selected_hosts.extend([bone for bone in target_armature_bones if target_bone.name == bone.name]) + selected_hosts.extend( + [bone for bone in target_armature_bones if target_bone.name == bone.name]) return selected_hosts def get_selected_hosts(self, context): @@ -628,12 +650,14 @@ def execute(self, context): # Load/Reload the first image and assign it to the target property, then load the rest of the images if they're not already loaded. This mimics Blender's default open files behavior. primary_filepath = os.path.join(dirname, self.files[0].name) - primary_img = bpy.data.images.load(filepath=primary_filepath, check_existing=True) + primary_img = bpy.data.images.load( + filepath=primary_filepath, check_existing=True) primary_img.reload() self.hubs_component[self.target_property] = primary_img for f in self.files[1:]: - bpy.data.images.load(filepath=os.path.join(dirname, f.name), check_existing=True) + bpy.data.images.load(filepath=os.path.join( + dirname, f.name), check_existing=True) update_image_editors(old_img, primary_img) redraw_component_ui(context) @@ -657,7 +681,8 @@ def register(): bpy.utils.register_class(ViewReportInInfoEditor) bpy.utils.register_class(CopyHubsComponent) bpy.utils.register_class(OpenImage) - bpy.types.WindowManager.hubs_report_scroll_index = IntProperty(default=0, min=0) + bpy.types.WindowManager.hubs_report_scroll_index = IntProperty( + default=0, min=0) bpy.types.WindowManager.hubs_report_scroll_percentage = IntProperty( name="Scroll Position", default=0, min=0, max=100, subtype='PERCENTAGE') bpy.types.WindowManager.hubs_report_last_title = StringProperty() diff --git a/addons/io_hubs_addon/components/ui.py b/addons/io_hubs_addon/components/ui.py index 34c620da..9dbb41fd 100644 --- a/addons/io_hubs_addon/components/ui.py +++ b/addons/io_hubs_addon/components/ui.py @@ -142,6 +142,18 @@ def draw(self, context): draw_components_list(self, context) +class HUBS_PT_ToolsPanel(bpy.types.Panel): + bl_idname = "HUBS_PT_ToolsPanel" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Hubs" + bl_category = "Hubs" + bl_context = 'objectmode' + + def draw(self, context): + pass + + class HubsScenePanel(bpy.types.Panel): bl_label = 'Hubs' bl_idname = "SCENE_PT_hubs" @@ -217,6 +229,7 @@ def register(): bpy.utils.register_class(HubsMaterialPanel) bpy.utils.register_class(HubsBonePanel) bpy.utils.register_class(TooltipLabel) + bpy.utils.register_class(HUBS_PT_ToolsPanel) bpy.types.TOPBAR_MT_window.append(window_menu_addition) bpy.types.VIEW3D_MT_object.append(object_menu_addition) @@ -229,6 +242,7 @@ def unregister(): bpy.utils.unregister_class(HubsMaterialPanel) bpy.utils.unregister_class(HubsBonePanel) bpy.utils.unregister_class(TooltipLabel) + bpy.utils.unregister_class(HUBS_PT_ToolsPanel) bpy.types.TOPBAR_MT_window.remove(window_menu_addition) bpy.types.VIEW3D_MT_object.remove(object_menu_addition) diff --git a/addons/io_hubs_addon/components/utils.py b/addons/io_hubs_addon/components/utils.py index f392aea0..92483d5f 100644 --- a/addons/io_hubs_addon/components/utils.py +++ b/addons/io_hubs_addon/components/utils.py @@ -215,7 +215,7 @@ def redirect_c_stdout(binary_stream): try: # Flush the C-level buffer of stdout before redirecting. This should make sure that only the desired data is captured. c_fflush() - #  Move the file pointer to the start of the file + # Move the file pointer to the start of the file __stack_tmp_file.seek(0) # Redirect stdout to your pipe. os.dup2(__stack_tmp_file.fileno(), stdout_file_descriptor) @@ -227,7 +227,7 @@ def redirect_c_stdout(binary_stream): os.dup2(original_stdout_file_descriptor_copy, stdout_file_descriptor) # Truncate file to the written amount of bytes __stack_tmp_file.truncate() - #  Move the file pointer to the start of the file + # Move the file pointer to the start of the file __stack_tmp_file.seek(0) # Write back to the input stream binary_stream.write(__stack_tmp_file.read()) @@ -310,7 +310,8 @@ def get_host_reference_message(panel_type, host, ob=None): def register(): global __stack_tmp_file - __stack_tmp_file = tempfile.NamedTemporaryFile(mode='w+b', buffering=0, delete=False, dir=bpy.app.tempdir) + __stack_tmp_file = tempfile.NamedTemporaryFile( + mode='w+b', buffering=0, delete=False, dir=bpy.app.tempdir) def unregister(): diff --git a/addons/io_hubs_addon/debugger.py b/addons/io_hubs_addon/debugger.py new file mode 100644 index 00000000..1721cd98 --- /dev/null +++ b/addons/io_hubs_addon/debugger.py @@ -0,0 +1,853 @@ +from bpy.app.handlers import persistent +import bpy +from bpy.types import Context +from .preferences import get_addon_pref, EXPORT_TMP_FILE_NAME +from .utils import isModuleAvailable, get_browser_profile_directory, save_prefs +from .icons import get_hubs_icons + +ROOM_FLAGS_DOC_URL = "https://hubs.mozilla.com/docs/hubs-query-string-parameters.html" +PARAMS_TO_STRING = { + "newLoader": { + "name": "Use New Loader", + "description": "Creates the room using the new bitECS loader" + }, + "ecsDebug": { + "name": "Show ECS Debug Panel", + "description": "Enables the ECS debugging side panel" + }, + "vr_entry_type": { + "name": "Skip Entry", + "description": "Omits the entry setup panel and goes straight into the room", + "value": "2d_now" + }, + "debugLocalScene": { + "name": "Allow Scene Update", + "description": "Allows scene override. Use this if you want to update the scene. If you just want to spawn an object disable it." + }, +} + + +JS_DROP_FILE = """ + var target = arguments[0], + offsetX = arguments[1], + offsetY = arguments[2], + document = target.ownerDocument || document, + window = document.defaultView || window; + + var input = document.createElement('INPUT'); + input.type = 'file'; + input.onchange = function () { + var rect = target.getBoundingClientRect(), + x = rect.left + (offsetX || (rect.width >> 1)), + y = rect.top + (offsetY || (rect.height >> 1)), + dataTransfer = { files: this.files }; + dataTransfer.getData = o => undefined; + + ['dragenter', 'dragover', 'drop'].forEach(function (name) { + var evt = document.createEvent('MouseEvent'); + evt.initMouseEvent(name, !0, !0, window, 0, 0, 0, x, y, !1, !1, !1, !1, 0, null); + evt.dataTransfer = dataTransfer; + target.dispatchEvent(evt); + }); + + setTimeout(function () { document.body.removeChild(input); }, 25); + }; + document.body.appendChild(input); + return input; +""" + +web_driver = None + + +def export_scene(context): + export_prefs = context.scene.hubs_scene_debugger_room_export_prefs + import os + extension = '.glb' + args = { + # Settings from "Remember Export Settings" + **dict(bpy.context.scene.get('glTF2ExportSettings', {})), + + 'export_format': ('GLB' if extension == '.glb' else 'GLTF_SEPARATE'), + 'filepath': os.path.join(bpy.app.tempdir, EXPORT_TMP_FILE_NAME), + 'export_cameras': export_prefs.export_cameras, + 'export_lights': export_prefs.export_lights, + 'use_selection': export_prefs.use_selection, + 'use_visible': export_prefs.use_visible, + 'use_renderable': export_prefs.use_renderable, + 'use_active_collection': export_prefs.use_active_collection, + 'export_apply': export_prefs.export_apply, + 'export_force_sampling': False, + 'use_active_scene': True + } + bpy.ops.export_scene.gltf(**args) + + +def refresh_scene_viewer(): + import os + document = web_driver.find_element("tag name", "html") + file_input = web_driver.execute_script(JS_DROP_FILE, document, 0, 0) + file_input.send_keys(os.path.join(bpy.app.tempdir, EXPORT_TMP_FILE_NAME)) + + +def isWebdriverAlive(): + try: + if not web_driver or not isModuleAvailable("selenium"): + return False + else: + return bool(web_driver.current_url) + except Exception: + return False + + +def get_local_storage(): + storage = None + if isWebdriverAlive(): + storage = web_driver.execute_script("return window.localStorage;") + + return storage + + +def get_current_room_params(): + url = web_driver.current_url + from urllib.parse import urlparse + from urllib.parse import parse_qs + parsed = urlparse(url) + params = parse_qs(parsed.query, keep_blank_values=True) + return {k: v for k, v in params.items() if k != "hub_id"} + + +def is_user_logged_in(): + if "debugLocalScene" in get_current_room_params(): + return bool(web_driver.execute_script('try { return APP?.hubChannel?.signedIn; } catch(e) { return false; }')) + else: + return True + + +def is_user_in_room(): + return bool(web_driver.execute_script('try { return APP?.scene?.is("entered"); } catch(e) { return false; }')) + + +def get_room_name(): + return web_driver.execute_script('try { return APP?.hub?.name || APP?.hub?.slug || APP?.hub?.hub_id; } catch(e) { return ""; }') + + +def bring_to_front(context): + # In some systems switch_to doesn't work, the code below is a hack to make it work + # for the affected platforms/browsers that we have detected so far. + browser = get_addon_pref(context).browser + import platform + if browser == "Firefox" or platform.system == "Windows": + ws = web_driver.get_window_size() + web_driver.minimize_window() + web_driver.set_window_size(ws['width'], ws['height']) + web_driver.switch_to.window(web_driver.current_window_handle) + + +def is_instance_set(context): + prefs = context.window_manager.hubs_scene_debugger_prefs + return prefs.hubs_instance_idx != -1 + + +def is_room_set(context): + prefs = context.window_manager.hubs_scene_debugger_prefs + return prefs.hubs_room_idx != -1 + + +class HubsUpdateSceneOperator(bpy.types.Operator): + bl_idname = "hubs_scene.update_scene" + bl_label = "View Scene" + bl_description = "Update scene" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context): + return isWebdriverAlive() and is_user_logged_in() and is_user_in_room() + + def execute(self, context): + try: + export_scene(context) + refresh_scene_viewer() + bring_to_front(context) + + return {'FINISHED'} + except Exception as err: + print(err) + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string='\n\n'.join(["The scene export has failed", + "Check the export logs or quit the browser instance and try again", + f'{err}'])) + return {'CANCELLED'} + + +def get_url_params(context): + params = "" + keys = list(PARAMS_TO_STRING.keys()) + for key in keys: + if getattr(context.scene.hubs_scene_debugger_room_create_prefs, key): + value = f'={PARAMS_TO_STRING[key]["value"]}' if "value" in PARAMS_TO_STRING[key] else "" + key = key if not params else f'&{key}' + params = f'{params}{key}{value}' + + return params + + +def init_browser(context): + browser = get_addon_pref(context).browser + if isWebdriverAlive(): + if web_driver.name != browser.lower(): + close_browser() + create_browser_instance(context) + return False + return True + else: + create_browser_instance(context) + return False + + +def close_browser(): + global web_driver + if web_driver: + # Hack, without this the browser instances don't close the session correctly and + # you get a "[Browser] didn't shutdown correctly" message on reopen. + # Only seen in Windows so far so limiting to it for now. + import platform + if platform == "windows": + windows = web_driver.window_handles + for w in windows: + web_driver.switch_to.window(w) + web_driver.close() + + web_driver.quit() + web_driver = None + + +def create_browser_instance(context): + global web_driver + if not web_driver or not isWebdriverAlive(): + close_browser() + browser = get_addon_pref(context).browser + import os + file_path = get_browser_profile_directory(browser) + if not os.path.exists(file_path): + os.mkdir(file_path) + if browser == "Firefox": + from selenium import webdriver + options = webdriver.FirefoxOptions() + override_ff_path = get_addon_pref( + context).override_firefox_path + ff_path = get_addon_pref(context).firefox_path + if override_ff_path and ff_path: + options.binary_location = ff_path + # This should work but it doesn't https://github.com/SeleniumHQ/selenium/issues/11028 so using arguments instead + # firefox_profile = webdriver.FirefoxProfile(file_path) + # firefox_profile.accept_untrusted_certs = True + # firefox_profile.assume_untrusted_cert_issuer = True + # options.profile = firefox_profile + options.add_argument("-profile") + options.add_argument(file_path) + options.set_preference("javascript.options.shared_memory", True) + web_driver = webdriver.Firefox(options=options) + else: + from selenium import webdriver + options = webdriver.ChromeOptions() + options.add_argument('--enable-features=SharedArrayBuffer') + options.add_argument('--ignore-certificate-errors') + options.add_argument( + f'user-data-dir={file_path}') + override_chrome_path = get_addon_pref( + context).override_chrome_path + chrome_path = get_addon_pref(context).chrome_path + if override_chrome_path and chrome_path: + options.binary_location = chrome_path + web_driver = webdriver.Chrome(options=options) + + +class HubsCreateRoomOperator(bpy.types.Operator): + bl_idname = "hubs_scene.create_room" + bl_label = "Create Room" + bl_description = "Create room" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context): + return is_instance_set(context) + + def execute(self, context): + try: + was_alive = init_browser(context) + + prefs = context.window_manager.hubs_scene_debugger_prefs + hubs_instance_url = prefs.hubs_instances[prefs.hubs_instance_idx].url + web_driver.get( + f'{hubs_instance_url}?new&{get_url_params(context)}') + + if was_alive: + bring_to_front(context) + + return {'FINISHED'} + + except Exception as err: + close_browser() + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string=f'The room creation has failed: {err}') + return {"CANCELLED"} + + +class HubsOpenRoomOperator(bpy.types.Operator): + bl_idname = "hubs_scene.open_room" + bl_label = "Open Room" + bl_description = "Open room" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context): + return is_room_set(context) + + def execute(self, context): + try: + was_alive = init_browser(context) + + prefs = context.window_manager.hubs_scene_debugger_prefs + room_url = prefs.hubs_rooms[prefs.hubs_room_idx].url + + params = get_url_params(context) + if params: + if "?" in room_url: + web_driver.get(f'{room_url}&{params}') + else: + web_driver.get(f'{room_url}?{params}') + else: + web_driver.get(room_url) + + if was_alive: + bring_to_front(context) + + return {'FINISHED'} + + except Exception as err: + close_browser() + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string=f'An error happened while opening the room: {err}') + return {"CANCELLED"} + + +class HubsCloseRoomOperator(bpy.types.Operator): + bl_idname = "hubs_scene.close_room" + bl_label = "Close" + bl_description = "Close browser window" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context): + return isWebdriverAlive() + + def execute(self, context): + try: + close_browser() + return {'FINISHED'} + + except Exception as err: + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string=f'An error happened while closing the browser window: {err}') + return {"CANCELLED"} + + +class HubsOpenAddonPrefsOperator(bpy.types.Operator): + bl_idname = "hubs_scene.open_addon_prefs" + bl_label = "Open Preferences" + bl_description = "Open Preferences" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context): + return not isWebdriverAlive() + + def execute(self, context): + bpy.ops.screen.userpref_show('INVOKE_DEFAULT') + context.preferences.active_section + bpy.ops.preferences.addon_expand(module=__package__) + bpy.ops.preferences.addon_show(module=__package__) + return {'FINISHED'} + + +class HUBS_PT_ToolsSceneDebuggerCreatePanel(bpy.types.Panel): + bl_idname = "HUBS_PT_ToolsSceneDebuggerCreatePanel" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Create Room" + bl_context = 'objectmode' + bl_parent_id = "HUBS_PT_ToolsSceneDebuggerPanel" + + @classmethod + def poll(cls, context: Context): + return isModuleAvailable("selenium") + + def draw(self, context: Context): + prefs = context.window_manager.hubs_scene_debugger_prefs + box = self.layout.box() + row = box.row() + row.label(text="Instances:") + row = box.row() + list_row = row.row() + list_row.template_list(HUBS_UL_ToolsSceneDebuggerServers.bl_idname, "", prefs, + "hubs_instances", prefs, "hubs_instance_idx", rows=3) + col = row.column() + col.operator(HubsSceneDebuggerInstanceAdd.bl_idname, + icon='ADD', text="") + col.operator(HubsSceneDebuggerInstanceRemove.bl_idname, + icon='REMOVE', text="") + + row = box.row() + col = row.column() + col.operator(HubsCreateRoomOperator.bl_idname, + text='Create') + col = row.column() + col.operator(HubsCloseRoomOperator.bl_idname, + text='Close') + + +class HUBS_PT_ToolsSceneDebuggerOpenPanel(bpy.types.Panel): + bl_idname = "HUBS_PT_ToolsSceneDebuggerOpenPanel" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Open Room" + bl_context = 'objectmode' + bl_parent_id = "HUBS_PT_ToolsSceneDebuggerPanel" + + @classmethod + def poll(cls, context: Context): + return isModuleAvailable("selenium") + + def draw(self, context: Context): + box = self.layout.box() + prefs = context.window_manager.hubs_scene_debugger_prefs + row = box.row() + row.label(text="Rooms:") + row = box.row() + list_row = row.row() + list_row.template_list(HUBS_UL_ToolsSceneDebuggerRooms.bl_idname, "", prefs, + "hubs_rooms", prefs, "hubs_room_idx", rows=3) + col = row.column() + op = col.operator(HubsSceneDebuggerRoomAdd.bl_idname, + icon='ADD', text="") + op.url = "https://hubs.mozilla.com/demo" + col.operator(HubsSceneDebuggerRoomRemove.bl_idname, + icon='REMOVE', text="") + + row = box.row() + col = row.column() + col.operator(HubsOpenRoomOperator.bl_idname, + text='Open') + col = row.column() + col.operator(HubsCloseRoomOperator.bl_idname, + text='Close') + + +class HUBS_PT_ToolsSceneDebuggerUpdatePanel(bpy.types.Panel): + bl_idname = "HUBS_PT_ToolsSceneDebuggerUpdatePanel" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Update Scene" + bl_context = 'objectmode' + bl_parent_id = "HUBS_PT_ToolsSceneDebuggerPanel" + + @classmethod + def poll(cls, context: Context): + return isModuleAvailable("selenium") + + def draw(self, context: Context): + box = self.layout.box() + row = box.row() + row.label( + text="Set the default export options in the glTF export panel") + row = box.row() + col = row.column(heading="Limit To:") + col.use_property_split = True + col.prop(context.scene.hubs_scene_debugger_room_export_prefs, + "use_selection") + col.prop(context.scene.hubs_scene_debugger_room_export_prefs, + "use_visible") + col.prop(context.scene.hubs_scene_debugger_room_export_prefs, + "use_renderable") + col.prop(context.scene.hubs_scene_debugger_room_export_prefs, + "use_active_collection") + row = box.row() + col = row.column(heading="Data:") + col.use_property_split = True + col.prop(context.scene.hubs_scene_debugger_room_export_prefs, + "export_cameras") + col.prop(context.scene.hubs_scene_debugger_room_export_prefs, + "export_lights") + row = box.row() + col = row.column(heading="Mesh:") + col.use_property_split = True + col.prop(context.scene.hubs_scene_debugger_room_export_prefs, + "export_apply") + row = box.row() + + update_mode = "Update Scene" if context.scene.hubs_scene_debugger_room_create_prefs.debugLocalScene else "Spawn as object" + if isWebdriverAlive(): + room_params = get_current_room_params() + update_mode = "Update Scene" if "debugLocalScene" in room_params else "Spawn as object" + row.operator(HubsUpdateSceneOperator.bl_idname, + text=f'{update_mode}') + + +class HUBS_PT_ToolsSceneDebuggerPanel(bpy.types.Panel): + bl_idname = "HUBS_PT_ToolsSceneDebuggerPanel" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Scene Debugger" + bl_context = 'objectmode' + bl_parent_id = "HUBS_PT_ToolsPanel" + + def draw(self, context): + main_box = self.layout.box() + + if isModuleAvailable("selenium"): + row = main_box.row(align=True) + row.alignment = "CENTER" + col = row.column() + col.alignment = "LEFT" + col.label(text="Connection Status:") + hubs_icons = get_hubs_icons() + if isWebdriverAlive(): + if is_user_logged_in(): + if is_user_in_room(): + col = row.column() + col.alignment = "LEFT" + col.active_default = True + col.label( + icon_value=hubs_icons["green-dot.png"].icon_id) + row = main_box.row(align=True) + row.alignment = "CENTER" + row.label(text=f'In room: {get_room_name()}') + + else: + col = row.column() + col.alignment = "LEFT" + col.label( + icon_value=hubs_icons["orange-dot.png"].icon_id) + row = main_box.row(align=True) + row.alignment = "CENTER" + row.label(text="Entering the room...") + else: + col = row.column() + col.alignment = "LEFT" + col.alert = True + col.label(icon_value=hubs_icons["orange-dot.png"].icon_id) + row = main_box.row(align=True) + row.alignment = "CENTER" + row.label(text="Waiting for sign in...") + + else: + col = row.column() + col.alignment = "LEFT" + col.alert = True + col.label(icon_value=hubs_icons["red-dot.png"].icon_id) + row = main_box.row(align=True) + row.alignment = "CENTER" + row.label(text="Waiting for room...") + + params_icons = {} + for key in PARAMS_TO_STRING.keys(): + params_icons.update( + {key: hubs_icons["red-dot-small.png"].icon_id}) + if isWebdriverAlive(): + params = get_current_room_params() + for param in params: + if param in params_icons: + params_icons[param] = hubs_icons["green-dot-small.png"].icon_id + + box = self.layout.box() + row = box.row(align=True) + row.alignment = "EXPAND" + grid = row.grid_flow(columns=2, align=True, + even_rows=False, even_columns=False) + grid.alignment = "CENTER" + flags_row = grid.row() + flags_row.label(text="Room flags") + op = flags_row.operator("wm.url_open", text="", icon="HELP") + op.url = ROOM_FLAGS_DOC_URL + for key in PARAMS_TO_STRING.keys(): + grid.prop(context.scene.hubs_scene_debugger_room_create_prefs, + key) + grid.label(text="Is Active?") + for key in PARAMS_TO_STRING.keys(): + grid.label(icon_value=params_icons[key]) + + else: + row = main_box.row() + row.alert = True + row.label( + text="Selenium needs to be installed for the scene debugger functionality. Install from preferences.") + row = main_box.row() + row.operator(HubsOpenAddonPrefsOperator.bl_idname, + text='Setup') + + +class HubsSceneDebuggerInstanceAdd(bpy.types.Operator): + bl_idname = "hubs_scene.scene_debugger_instance_add" + bl_label = "Add Server Instance" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + prefs = context.window_manager.hubs_scene_debugger_prefs + new_instance = prefs.hubs_instances.add() + new_instance.name = "Demo Hub" + new_instance.url = "https://hubs.mozilla.com/demo" + prefs.hubs_instance_idx = len( + prefs.hubs_instances) - 1 + + save_prefs(context) + + return {'FINISHED'} + + +class HubsSceneDebuggerInstanceRemove(bpy.types.Operator): + bl_idname = "hubs_scene.scene_debugger_instance_remove" + bl_label = "Remove Server Instance" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + prefs = context.window_manager.hubs_scene_debugger_prefs + prefs.hubs_instances.remove(prefs.hubs_instance_idx) + + if prefs.hubs_instance_idx >= len(prefs.hubs_instances): + prefs.hubs_instance_idx -= 1 + + save_prefs(context) + + return {'FINISHED'} + + +class HubsSceneDebuggerRoomAdd(bpy.types.Operator): + bl_idname = "hubs_scene.scene_debugger_room_add" + bl_label = "Add Room" + bl_description = "Adds the current active room url to the list, if there is no active room it will add an empty string" + bl_options = {'REGISTER', 'UNDO'} + + url: bpy.props.StringProperty(name="Room Url") + + def execute(self, context): + prefs = context.window_manager.hubs_scene_debugger_prefs + new_room = prefs.hubs_rooms.add() + url = self.url + if isWebdriverAlive(): + if web_driver.current_url: + url = web_driver.current_url + if "hub_id=" in url: + url = url.split("&")[0] + else: + url = url.split("?")[0] + + new_room.name = "Room Name" + if isWebdriverAlive(): + room_name = get_room_name() + if room_name: + new_room.name = room_name + new_room.url = url + prefs.hubs_room_idx = len( + prefs.hubs_rooms) - 1 + + save_prefs(context) + + return {'FINISHED'} + + +class HubsSceneDebuggerRoomRemove(bpy.types.Operator): + bl_idname = "hubs_scene.scene_debugger_room_remove" + bl_label = "Remove Room" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context): + prefs = context.window_manager.hubs_scene_debugger_prefs + return prefs.hubs_room_idx >= 0 + + def execute(self, context): + prefs = context.window_manager.hubs_scene_debugger_prefs + prefs.hubs_rooms.remove(prefs.hubs_room_idx) + + if prefs.hubs_room_idx >= len(prefs.hubs_rooms): + prefs.hubs_room_idx -= 1 + + save_prefs(context) + + return {'FINISHED'} + + +class HUBS_UL_ToolsSceneDebuggerServers(bpy.types.UIList): + bl_idname = "HUBS_UL_ToolsSceneDebuggerServers" + bl_label = "Instances" + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + split = layout.split(factor=0.25) + split.prop(item, "name", text="", emboss=False) + split.prop(item, "url", text="", emboss=False) + + +class HUBS_UL_ToolsSceneDebuggerRooms(bpy.types.UIList): + bl_idname = "HUBS_UL_ToolsSceneDebuggerRooms" + bl_label = "Rooms" + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + split = layout.split(factor=0.25) + split.prop(item, "name", text="", emboss=False) + split.prop(item, "url", text="", emboss=False) + + +def set_url(self, value): + try: + import urllib + parsed = urllib.parse.urlparse(value) + parsed = parsed._replace(scheme="https") + self.url_ = urllib.parse.urlunparse(parsed) + except Exception: + self.url_ = "https://hubs.mozilla.com/demo" + + +def get_url(self): + return self.url_ + + +def save_prefs_on_prop_update(self, context): + save_prefs(context) + + +class HubsUrl(bpy.types.PropertyGroup): + name: bpy.props.StringProperty(update=save_prefs_on_prop_update) + url: bpy.props.StringProperty( + set=set_url, get=get_url, update=save_prefs_on_prop_update) + url_: bpy.props.StringProperty(options={"HIDDEN"}) + + +class HubsSceneDebuggerPrefs(bpy.types.PropertyGroup): + hubs_instances: bpy.props.CollectionProperty( + type=HubsUrl) + + hubs_instance_idx: bpy.props.IntProperty( + default=-1, update=save_prefs_on_prop_update) + + hubs_room_idx: bpy.props.IntProperty( + default=-1, update=save_prefs_on_prop_update) + + hubs_rooms: bpy.props.CollectionProperty( + type=HubsUrl) + + +class HubsSceneDebuggerRoomCreatePrefs(bpy.types.PropertyGroup): + newLoader: bpy.props.BoolProperty( + name=PARAMS_TO_STRING["newLoader"]["name"], default=True, description=PARAMS_TO_STRING["newLoader"]["description"]) + ecsDebug: bpy.props.BoolProperty( + name=PARAMS_TO_STRING["ecsDebug"]["name"], default=True, description=PARAMS_TO_STRING["ecsDebug"]["description"]) + vr_entry_type: bpy.props.BoolProperty( + name=PARAMS_TO_STRING["vr_entry_type"]["name"], default=True, description=PARAMS_TO_STRING["vr_entry_type"]["description"]) + debugLocalScene: bpy.props.BoolProperty(name=PARAMS_TO_STRING["debugLocalScene"]["name"], default=True, + description=PARAMS_TO_STRING["debugLocalScene"]["description"]) + + +class HubsSceneDebuggerRoomExportPrefs(bpy.types.PropertyGroup): + export_cameras: bpy.props.BoolProperty(name="Export Cameras", default=True, + description="Export cameras", options=set()) + export_lights: bpy.props.BoolProperty(name="Export Lights", + default=True, description="Punctual Lights, Export directional, point, and spot lights. Uses \"KHR_lights_punctual\" glTF extension", options=set()) + use_selection: bpy.props.BoolProperty(name="Selection Only", default=False, + description="Selection Only, Export selected objects only.", + options=set()) + export_apply: bpy.props.BoolProperty(name="Apply Modifiers", default=True, + description="Apply Modifiers, Apply modifiers (excluding Armatures) to mesh objects -WARNING: prevents exporting shape keys.", + options=set()) + use_visible: bpy.props.BoolProperty( + name='Visible Objects', + description='Export visible objects only', + default=False, + options=set() + ) + + use_renderable: bpy.props.BoolProperty( + name='Renderable Objects', + description='Export renderable objects only', + default=False, + options=set() + ) + + use_active_collection: bpy.props.BoolProperty( + name='Active Collection', + description='Export objects in the active collection only', + default=False, + options=set() + ) + + +@persistent +def load_post(dummy): + from .utils import load_prefs + load_prefs(bpy.context) + + prefs = bpy.context.window_manager.hubs_scene_debugger_prefs + if len(prefs.hubs_instances) == 0: + bpy.ops.hubs_scene.scene_debugger_instance_add('INVOKE_DEFAULT') + + +def register(): + bpy.utils.register_class(HubsUrl) + bpy.utils.register_class(HubsSceneDebuggerPrefs) + bpy.utils.register_class(HubsCreateRoomOperator) + bpy.utils.register_class(HubsOpenRoomOperator) + bpy.utils.register_class(HubsCloseRoomOperator) + bpy.utils.register_class(HubsUpdateSceneOperator) + bpy.utils.register_class(HUBS_PT_ToolsSceneDebuggerPanel) + bpy.utils.register_class(HUBS_PT_ToolsSceneDebuggerCreatePanel) + bpy.utils.register_class(HUBS_PT_ToolsSceneDebuggerOpenPanel) + bpy.utils.register_class(HUBS_PT_ToolsSceneDebuggerUpdatePanel) + bpy.utils.register_class(HubsSceneDebuggerRoomCreatePrefs) + bpy.utils.register_class(HubsOpenAddonPrefsOperator) + bpy.utils.register_class(HubsSceneDebuggerRoomExportPrefs) + bpy.utils.register_class(HubsSceneDebuggerInstanceAdd) + bpy.utils.register_class(HubsSceneDebuggerInstanceRemove) + bpy.utils.register_class(HubsSceneDebuggerRoomAdd) + bpy.utils.register_class(HubsSceneDebuggerRoomRemove) + bpy.utils.register_class(HUBS_UL_ToolsSceneDebuggerServers) + bpy.utils.register_class(HUBS_UL_ToolsSceneDebuggerRooms) + + bpy.types.Scene.hubs_scene_debugger_room_create_prefs = bpy.props.PointerProperty( + type=HubsSceneDebuggerRoomCreatePrefs) + bpy.types.Scene.hubs_scene_debugger_room_export_prefs = bpy.props.PointerProperty( + type=HubsSceneDebuggerRoomExportPrefs) + bpy.types.WindowManager.hubs_scene_debugger_prefs = bpy.props.PointerProperty( + type=HubsSceneDebuggerPrefs) + + if load_post not in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.append(load_post) + + +def unregister(): + bpy.utils.unregister_class(HubsUpdateSceneOperator) + bpy.utils.unregister_class(HubsOpenRoomOperator) + bpy.utils.unregister_class(HubsCloseRoomOperator) + bpy.utils.unregister_class(HubsCreateRoomOperator) + bpy.utils.unregister_class(HUBS_PT_ToolsSceneDebuggerCreatePanel) + bpy.utils.unregister_class(HUBS_PT_ToolsSceneDebuggerOpenPanel) + bpy.utils.unregister_class(HUBS_PT_ToolsSceneDebuggerUpdatePanel) + bpy.utils.unregister_class(HUBS_PT_ToolsSceneDebuggerPanel) + bpy.utils.unregister_class(HubsSceneDebuggerRoomCreatePrefs) + bpy.utils.unregister_class(HubsOpenAddonPrefsOperator) + bpy.utils.unregister_class(HubsSceneDebuggerRoomExportPrefs) + bpy.utils.unregister_class(HUBS_UL_ToolsSceneDebuggerServers) + bpy.utils.unregister_class(HUBS_UL_ToolsSceneDebuggerRooms) + bpy.utils.unregister_class(HubsSceneDebuggerInstanceAdd) + bpy.utils.unregister_class(HubsSceneDebuggerInstanceRemove) + bpy.utils.unregister_class(HubsSceneDebuggerRoomAdd) + bpy.utils.unregister_class(HubsSceneDebuggerRoomRemove) + bpy.utils.unregister_class(HubsSceneDebuggerPrefs) + bpy.utils.unregister_class(HubsUrl) + + del bpy.types.Scene.hubs_scene_debugger_room_create_prefs + del bpy.types.Scene.hubs_scene_debugger_room_export_prefs + del bpy.types.WindowManager.hubs_scene_debugger_prefs + + if load_post in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(load_post) + + close_browser() diff --git a/addons/io_hubs_addon/icons.py b/addons/io_hubs_addon/icons.py new file mode 100644 index 00000000..b1921672 --- /dev/null +++ b/addons/io_hubs_addon/icons.py @@ -0,0 +1,39 @@ +import bpy +import os +from os import listdir +from os.path import join, isfile +import bpy.utils.previews + + +def load_icons(): + global __hubs_icons + __hubs_icons = {} + pcoll = bpy.utils.previews.new() + icons_dir = os.path.join(os.path.dirname(__file__), "icons") + icons = [f for f in listdir(icons_dir) if isfile(join(icons_dir, f))] + for icon in icons: + pcoll.load(icon, os.path.join(icons_dir, icon), 'IMAGE') + print("Loading icon: " + icon) + __hubs_icons["hubs"] = pcoll + + +def unload_icons(): + global __hubs_icons + bpy.utils.previews.remove(__hubs_icons["hubs"]) + del __hubs_icons + + +def get_hubs_icons(): + global __hubs_icons + return __hubs_icons["hubs"] + + +__hubs_icons = {} + + +def register(): + load_icons() + + +def unregister(): + unload_icons() diff --git a/addons/io_hubs_addon/icons/green-dot-small.png b/addons/io_hubs_addon/icons/green-dot-small.png new file mode 100644 index 00000000..961ee601 Binary files /dev/null and b/addons/io_hubs_addon/icons/green-dot-small.png differ diff --git a/addons/io_hubs_addon/icons/green-dot.png b/addons/io_hubs_addon/icons/green-dot.png new file mode 100644 index 00000000..beb81265 Binary files /dev/null and b/addons/io_hubs_addon/icons/green-dot.png differ diff --git a/addons/io_hubs_addon/icons/orange-dot.png b/addons/io_hubs_addon/icons/orange-dot.png new file mode 100644 index 00000000..71bb4571 Binary files /dev/null and b/addons/io_hubs_addon/icons/orange-dot.png differ diff --git a/addons/io_hubs_addon/icons/red-dot-small.png b/addons/io_hubs_addon/icons/red-dot-small.png new file mode 100644 index 00000000..87bc95f8 Binary files /dev/null and b/addons/io_hubs_addon/icons/red-dot-small.png differ diff --git a/addons/io_hubs_addon/icons/red-dot.png b/addons/io_hubs_addon/icons/red-dot.png new file mode 100644 index 00000000..616ef32f Binary files /dev/null and b/addons/io_hubs_addon/icons/red-dot.png differ diff --git a/addons/io_hubs_addon/components/icons/spawn-point.png b/addons/io_hubs_addon/icons/spawn-point.png similarity index 100% rename from addons/io_hubs_addon/components/icons/spawn-point.png rename to addons/io_hubs_addon/icons/spawn-point.png diff --git a/addons/io_hubs_addon/preferences.py b/addons/io_hubs_addon/preferences.py index 41e9f004..80ea0416 100644 --- a/addons/io_hubs_addon/preferences.py +++ b/addons/io_hubs_addon/preferences.py @@ -1,10 +1,12 @@ import bpy -from bpy.types import AddonPreferences -from bpy.props import IntProperty, StringProperty -from .utils import get_addon_package +from bpy.types import AddonPreferences, Context +from bpy.props import IntProperty, StringProperty, EnumProperty, BoolProperty, CollectionProperty +from .utils import get_addon_package, isModuleAvailable, get_browser_profile_directory import platform from os.path import join, dirname, realpath +EXPORT_TMP_FILE_NAME = "__hubs_tmp_scene_.glb" + def get_addon_pref(context): addon_package = get_addon_package() @@ -25,6 +27,143 @@ def get_recast_lib_path(): return join(recast_lib, file_name) +class DepsProperty(bpy.types.PropertyGroup): + name: StringProperty(default=" ") + version: StringProperty(default="") + + +class InstallDepsOperator(bpy.types.Operator): + bl_idname = "pref.hubs_prefs_install_dep" + bl_label = "Install a python dependency through pip" + bl_property = "dep_names" + bl_options = {'REGISTER', 'UNDO'} + + dep_names: CollectionProperty(type=DepsProperty) + + def execute(self, context): + import subprocess + import sys + + result = subprocess.run([sys.executable, '-m', 'ensurepip'], + capture_output=False, text=True, input="y") + if result.returncode < 0: + print(result.stderr) + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string='\n\n'.join(["Dependencies install has failed", + f'{result.stderr}'])) + return {'CANCELLED'} + + deps = [] + for _, dep in self.dep_names.items(): + if dep.version: + deps.append(f'{dep.name}=={dep.version}') + else: + deps.append(dep.name) + + from .utils import get_user_python_path + result = subprocess.run( + [sys.executable, '-m', 'pip', 'install', *deps, + '-t', get_user_python_path()], + capture_output=True, text=True, input="y") + failed = False + for _, dep in self.dep_names.items(): + if not isModuleAvailable(dep.name): + failed = True + if result.returncode != 0 or failed: + print(result.stderr) + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string='\n\n'.join(["Dependencies install has failed", + f'{result.stderr}'])) + return {'CANCELLED'} + else: + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string="Dependencies installed successfully") + return {'FINISHED'} + + +class UninstallDepsOperator(bpy.types.Operator): + bl_idname = "pref.hubs_prefs_uninstall_dep" + bl_label = "Uninstall a python dependency through pip" + bl_property = "dep_names" + bl_options = {'REGISTER', 'UNDO'} + + dep_names: CollectionProperty(type=DepsProperty) + force: BoolProperty(default=False) + + def execute(self, context): + import subprocess + import sys + + result = subprocess.run([sys.executable, '-m', 'ensurepip'], + capture_output=False, text=True, input="y") + if result.returncode < 0: + print(result.stderr) + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string='\n\n'.join(["Dependencies uninstall has failed", + f'{result.stderr}'])) + return {'CANCELLED'} + + for name, _ in self.dep_names.items(): + del name + + result = subprocess.run( + [sys.executable, '-m', 'pip', 'uninstall', * + [name for name, _ in self.dep_names.items()]], + capture_output=True, text=True, input="y") + + failed = False + for name, _ in self.dep_names.items(): + if isModuleAvailable(name): + failed = True + if result.returncode != 0 or failed: + print(result.stderr) + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string='\n\n'.join(["Dependencies install has failed", + f'{result.stderr}'])) + return {'CANCELLED'} + + if self.force: + import os + from .utils import get_user_python_path + deps_paths = [os.path.join(get_user_python_path(), name) + for name, _ in self.dep_names.items()] + import shutil + for dep_path in deps_paths: + shutil.rmtree(dep_path) + + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string="Dependencies uninstalled successfully") + return {'FINISHED'} + + +class DeleteProfileOperator(bpy.types.Operator): + bl_idname = "pref.hubs_prefs_remove_profile" + bl_label = "Delete" + bl_description = "Delete Browser profile" + bl_options = {'REGISTER', 'UNDO'} + + browser: StringProperty() + + @classmethod + def poll(cls, context: Context): + if hasattr(context, "prefs"): + prefs = getattr(context, 'prefs') + path = get_browser_profile_directory(prefs.browser) + import os + return os.path.exists(path) + + return False + + def execute(self, context): + path = get_browser_profile_directory(self.browser) + import os + if os.path.exists(path): + import shutil + shutil.rmtree(path) + + return {'FINISHED'} + + class HubsPreferences(AddonPreferences): bl_idname = __package__ @@ -41,6 +180,27 @@ class HubsPreferences(AddonPreferences): default=get_recast_lib_path() ) + viewer_available: BoolProperty() + + browser: EnumProperty( + name="Choose a browser", description="Type", + items=[("Firefox", "Firefox", "Use Firefox as the viewer browser"), + ("Chrome", "Chrome", "Use Chrome as the viewer browser")], + default="Firefox") + + force_uninstall: BoolProperty( + default=False, name="Force", + description="Force uninstall of the selenium dependencies by deleting the module directory") + + override_firefox_path: BoolProperty( + name="Override Firefox executable path", description="Override Firefox executable path", default=False) + firefox_path: StringProperty( + name="Firefox executable path", description="Binary path", subtype='FILE_PATH') + override_chrome_path: BoolProperty( + name="Override Chrome executable path", description="Override Chrome executable path", default=False) + chrome_path: StringProperty( + name="Chrome executable path", description="Binary path", subtype='FILE_PATH') + def draw(self, context): layout = self.layout box = layout.box() @@ -48,10 +208,84 @@ def draw(self, context): box.row().prop(self, "row_length") box.row().prop(self, "recast_lib_path") + selenium_available = isModuleAvailable("selenium") + modules_available = selenium_available + box = layout.box() + box.label(text="Scene debugger configuration") + + if modules_available: + browser_box = box.box() + row = browser_box.row() + row.prop(self, "browser") + row = browser_box.row() + col = row.column() + col.label(text=f'Delete {self.browser} profile') + col = row.column() + col.context_pointer_set("prefs", self) + op = col.operator(DeleteProfileOperator.bl_idname) + row = browser_box.row() + row.label( + text="This will only delete the Hubs related profile, not your local browser profile") + op.browser = self.browser + if self.browser == "Firefox": + row = browser_box.row() + row.prop(self, "override_firefox_path") + if self.override_firefox_path: + row = browser_box.row() + row.label( + text="In some cases the browser binary might not be located automatically, in those cases you'll need to specify the binary location manually below") + row = browser_box.row() + row.alert = True + row.label( + text="You don't need to set a path below unless the binary cannot be located automatically.") + row = browser_box.row() + row.prop(self, "firefox_path",) + elif self.browser == "Chrome": + row = browser_box.row() + row.prop(self, "override_chrome_path") + if self.override_chrome_path: + row = browser_box.row() + row.label( + text="In some cases the browser binary might not be located automatically, in those cases you'll need to specify the binary location manually below") + row = browser_box.row() + row.alert = True + row.label( + text="You don't need to set a path below unless the binary cannot be located automatically.") + row = browser_box.row() + row.prop(self, "chrome_path") + + modules_box = box.box() + row = modules_box.row() + row.alert = not modules_available + row.label( + text="Modules found." + if modules_available else + "Selenium module not found. These modules are required to run the viewer") + row = modules_box.row() + if modules_available: + row.prop(self, "force_uninstall") + op = row.operator(UninstallDepsOperator.bl_idname, + text="Uninstall dependencies (selenium)") + op.dep_names.add().name = "selenium" + else: + op = row.operator(InstallDepsOperator.bl_idname, + text="Install dependencies (selenium)") + dep = op.dep_names.add() + dep.name = "selenium" + dep.version = "4.15.2" + def register(): + bpy.utils.register_class(DepsProperty) bpy.utils.register_class(HubsPreferences) + bpy.utils.register_class(InstallDepsOperator) + bpy.utils.register_class(UninstallDepsOperator) + bpy.utils.register_class(DeleteProfileOperator) def unregister(): + bpy.utils.unregister_class(DeleteProfileOperator) + bpy.utils.unregister_class(UninstallDepsOperator) + bpy.utils.unregister_class(InstallDepsOperator) bpy.utils.unregister_class(HubsPreferences) + bpy.utils.unregister_class(DepsProperty) diff --git a/addons/io_hubs_addon/utils.py b/addons/io_hubs_addon/utils.py index fd5d30eb..0c0b2864 100644 --- a/addons/io_hubs_addon/utils.py +++ b/addons/io_hubs_addon/utils.py @@ -24,3 +24,152 @@ def gather(): gather.delayed_gather = True return gather return wrapper_delayed_gather + + +user_python_path = None + + +def get_user_python_path(): + global user_python_path + if not user_python_path: + import sys + import subprocess + result = subprocess.run([sys.executable, '-m', 'site', + '--user-site'], capture_output=True, text=True, input="y") + user_python_path = result.stdout.strip("\n") + return user_python_path + + +def isModuleAvailable(name): + import importlib + loader = importlib.util.find_spec(name) + import os + from .utils import get_user_python_path + path = os.path.join(get_user_python_path(), name) + return loader and os.path.exists(path) + + +HUBS_PREFS_DIR = ".__hubs_blender_addon_preferences" +HUBS_SELENIUM_PROFILE_FIREFOX = "hubs_selenium_profile.firefox" +HUBS_SELENIUM_PROFILE_CHROME = "hubs_selenium_profile.chrome" +HUBS_PREFS = "hubs_prefs.json" + + +def get_prefs_dir_path(): + import os + home_directory = os.path.expanduser("~") + prefs_dir_path = os.path.join(home_directory, HUBS_PREFS_DIR) + return os.path.normpath(prefs_dir_path) + + +def create_prefs_dir(): + import os + prefs_dir = get_prefs_dir_path() + if not os.path.exists(prefs_dir): + os.makedirs(prefs_dir) + + +def get_browser_profile_directory(browser): + import os + prefs_folder = get_prefs_dir_path() + file_path = "" + if browser == "Firefox": + file_path = os.path.join( + prefs_folder, HUBS_SELENIUM_PROFILE_FIREFOX) + elif browser == "Chrome": + file_path = os.path.join( + prefs_folder, HUBS_SELENIUM_PROFILE_CHROME) + + return os.path.normpath(file_path) + + +def get_prefs_path(): + import os + prefs_folder = get_prefs_dir_path() + prefs_path = os.path.join(prefs_folder, HUBS_PREFS) + return os.path.normpath(prefs_path) + + +def save_prefs(context): + prefs = context.window_manager.hubs_scene_debugger_prefs + + data = { + "scene_debugger": {} + } + + instances_array = [] + for instance in prefs.hubs_instances: + instances_array.append({ + "name": instance.name, + "url": instance.url + }) + data["scene_debugger"] = { + "hubs_instance_idx": prefs.hubs_instance_idx, + "hubs_instances": instances_array + } + + rooms_array = [] + for room in prefs.hubs_rooms: + rooms_array.append({ + "name": room.name, + "url": room.url + }) + data["scene_debugger"].update({ + "hubs_room_idx": prefs.hubs_room_idx, + "hubs_rooms": rooms_array + }) + + out_path = get_prefs_path() + try: + import json + json_data = json.dumps(data) + with open(out_path, "w") as outfile: + outfile.write(json_data) + + except Exception as err: + import bpy + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string=f'An error happened while saving the preferences from {out_path}: {err}') + + +def load_prefs(context): + data = {} + + out_path = get_prefs_path() + import os + if not os.path.isfile(out_path): + return + + try: + import json + import os + with open(out_path, "r") as outfile: + if (os.path.getsize(out_path)) == 0: + return + data = json.load(outfile) + + except Exception as err: + import bpy + bpy.ops.wm.hubs_report_viewer('INVOKE_DEFAULT', title="Hubs scene debugger report", + report_string=f'An error happened while loading the preferences from {out_path}: {err}') + + if not data: + return + + prefs = context.window_manager.hubs_scene_debugger_prefs + scene_debugger = data["scene_debugger"] + prefs["hubs_instance_idx"] = scene_debugger["hubs_instance_idx"] + prefs.hubs_instances.clear() + instances = scene_debugger["hubs_instances"] + for instance in instances: + new_instance = prefs.hubs_instances.add() + new_instance.name = instance["name"] + new_instance.url = instance["url"] + + prefs["hubs_room_idx"] = scene_debugger["hubs_room_idx"] + prefs.hubs_rooms.clear() + rooms = scene_debugger["hubs_rooms"] + for room in rooms: + new_room = prefs.hubs_rooms.add() + new_room.name = room["name"] + new_room.url = room["url"]