# #### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### import os import queue import threading from subprocess import PIPE, Popen, TimeoutExpired from typing import Dict, List, Optional, Tuple import bpy from ..modules.poliigon_core.assets import ( AssetData, AssetType) from ..dialogs.dlg_assets import determine_hdri_sizes from .. import reporting from .asset_browser_sync_commands import ( SyncCmd, SyncAssetBrowserCmd, CMD_MARKER_START, CMD_MARKER_END) from ..notifications import ( build_lost_client_notification, build_new_catalogue_notification, build_no_refresh_notification) from ..constants import ASSET_ID_ALL from ..toolbox import get_context MAX_IDLE_TIME = 60.0 TIMEOUT_ASSET_BROWSER_CLIENT_STARTUP = 60.0 DEBUG_HOST = False if bpy.app.version >= (4, 0): type_FileSelectEntry = bpy.types.AssetRepresentation type_UserAssetLibrary = bpy.types.UserAssetLibrary elif bpy.app.version >= (3, 0): # The exact version is probably around 2.93, # but as the entire asset browser connection needs 3.0 this should do type_FileSelectEntry = bpy.types.FileSelectEntry type_UserAssetLibrary = bpy.types.UserAssetLibrary else: # To avoid issues, if types no present type_UserAssetLibrary = any type_FileSelectEntry = any # TODO(Andreas): To be replaced by appropriate logger function def print_debug(*args) -> None: if not DEBUG_HOST: return print("H:", *args) def cmd_hello(cmd: SyncAssetBrowserCmd) -> None: """Handles a HELLO command.""" print_debug("Received HELLO") cTB.event_hello.set() cTB.blender_client_starting = False # One shot timer bpy.app.timers.register(t_refresh_ui, first_interval=0.1, persistent=True) cTB.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.HELLO_OK)) def cmd_asset_ok(cmd: SyncAssetBrowserCmd) -> None: """Handles an ASSET_OK command.""" asset_id = cmd.data["asset_id"] asset_data = cTB._asset_index.get_asset(asset_id) asset_data.runtime.set_in_asset_browser(in_asset_browser=True) print_debug("Received ASSET_OK: ", asset_id) cTB.queue_ack.put(SyncAssetBrowserCmd(code=SyncCmd.CMD_DONE)) cTB.num_jobs_ok += 1 def cmd_asset_error(cmd: SyncAssetBrowserCmd) -> None: """Handles an ASSET_ERROR command.""" asset_id = cmd.data["asset_id"] print_debug("Received ASSET_ERROR: ", asset_id) cTB.queue_ack.put(SyncAssetBrowserCmd(code=SyncCmd.CMD_DONE)) error_msg = f"Asset {asset_id} failed to process." reporting.capture_message("asset_browser_process_fail", error_msg, "error") cTB.num_jobs_error += 1 def cmd_exit_ack(cmd: SyncAssetBrowserCmd, proc: Popen) -> None: """Handles an EXIT_ACK command.""" print_debug("Received EXIT_ACK: Client Blender exiting") try: _, _ = proc.communicate() except TimeoutExpired: proc.kill() outs, errs = proc.communicate() cTB.thd_listener = None cTB.listener_running = False cTB.thd_sender = None cTB.sender_running = False def check_command(buf: str) -> Tuple[Optional[SyncAssetBrowserCmd], str]: """Returns a valid command, otherwise None. Upon detecting a corrupted command, CMD_ERROR gets sent. Return value: Tuple with two entries: Tuple[0]: A valid command or None Tuple[1]: Remaining buf after either a valid command got detected or an broken command got removed """ if CMD_MARKER_END not in buf: return None, buf pos_delimiter = buf.find(CMD_MARKER_END, 1) cmd_json = buf[:pos_delimiter] buf = buf[pos_delimiter + len(CMD_MARKER_END):] if CMD_MARKER_START in cmd_json: pos_marker_start = cmd_json.find(CMD_MARKER_START, 1) cmd_json = cmd_json[pos_marker_start + len(CMD_MARKER_START):] else: cTB.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.CMD_ERROR)) cmd_json = None return cmd_json, buf @reporting.handle_function(silent=True) def thread_listener(proc: Popen) -> None: """Listens to commands sent by host and checks their integrity. In case of error requests a command to be re-send from host via CMD_ERROR. Valid acks are then sorted into a queue to forward them to unblock the sender. Opposed to client side, other commands are handled directly in here since there are no longer running commands which could block this listener for long. """ print_debug("thread_listener") cTB.listener_running = True buf = "" while cTB.listener_running: try: buf += proc.stderr.readline() except ValueError: break # stderr handle got closed except KeyboardInterrupt as e: reporting.capture_exception(e) continue cmd_json, buf = check_command(buf) if cmd_json is None: continue try: cmd = SyncAssetBrowserCmd.from_json(cmd_json) if cmd.code == SyncCmd.CMD_ERROR: # Forward ack to thread_sender cTB.queue_ack.put(cmd) elif cmd.code == SyncCmd.ASSET_OK: cmd_asset_ok(cmd) elif cmd.code == SyncCmd.ASSET_ERROR: cmd_asset_error(cmd) elif cmd.code == SyncCmd.EXIT_ACK: cmd_exit_ack(cmd, proc) elif cmd.code == SyncCmd.HELLO: cmd_hello(cmd) elif cmd.code in [SyncCmd.HELLO_OK, SyncCmd.ASSET, SyncCmd.EXIT]: print_debug("Unexpected cmd:", cmd.code) else: print_debug("Unexpected cmd: UNKNOWN command", cmd.code) except Exception as e: cTB.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.CMD_ERROR)) print("HOST CMD ERROR", cmd_json) print(" exc", e) print_debug("thread_listener EXIT") cTB.thd_listener = None cTB.listener_running = False def start_listener(proc: Popen) -> None: """Starts thread_listener()""" cTB.thd_listener = threading.Thread(target=thread_listener, args=(proc, ), daemon=True) cTB.thd_listener.start() def flush_queue_ack() -> None: """Removes all content from ack queue""" while not cTB.queue_ack.empty(): try: cTB.queue_ack.get_nowait() except cTB.queue_ack.Empty: break def flush_queue_send() -> None: """Removes all content from send queue""" reported = False while not cTB.queue_send.empty(): try: cTB.queue_send.get_nowait() except cTB.queue_send.Empty: break if not reported: error_msg = "Stray ACK encountered" reporting.capture_message( "asset_browser_stray_ack", error_msg, "error") reported = True cTB.num_asset_browser_jobs = 0 cTB.num_jobs_ok = 0 cTB.num_jobs_error = 0 def t_status_bar_update(interval=0.5): """Timer function forces a redraw of Blender's status bar""" if cTB.asset_browser_quitting: return None # Forces statusbar redraw bpy.context.workspace.status_text_set_internal(None) # Force asset browser redraw for area in bpy.context.screen.areas: if area.type == "FILE_BROWSER" and area.ui_type == "ASSETS": area.tag_redraw() return interval def refresh_asset_browser(): area = None for win in bpy.context.window_manager.windows: for this_area in win.screen.areas: if this_area.type == "FILE_BROWSER" and this_area.ui_type == "ASSETS": area = this_area break # TODO(Andreas): Auto-refresh currently completely disabled in Blender 4.0 # I somehow need to learn how to do it there. if area is None or bpy.app.version >= (4, 0): build_no_refresh_notification(cTB) return # Check if hasattr(bpy.context, "temp_override") and if not, # do an "old style" override (ie pre blender 3.2) unable_to_refresh = False try: # If we changed the type, we'll need it to redraw to get context. area.tag_redraw() if hasattr(bpy.context, "temp_override"): # Conditional is equal to bpy.app.version >= (3, 2, 0) with bpy.context.temp_override(area=area): # Unfortunately MUST also do another step here where we # set at least one library to be active. # Ideally, we only do this if we can detect nothing is # currently active. # Following doesn't work: # bpy.context.asset_library_ref = name # bpy.context.space_data.params.asset_library_ref = name # Works, if we already started with an asset browser window # *somewhere*, but isn't initially working (ie: exception) # if we are flipping from another type. if hasattr(area.spaces.active.params, "asset_library_reference"): area.spaces.active.params.asset_library_reference = cTB.prefs.asset_browser_library_name elif hasattr(area.spaces.active.params, "asset_library_ref"): area.spaces.active.params.asset_library_ref = cTB.prefs.asset_browser_library_name # Works if the above works. # will work if any non-current file lib active. bpy.ops.asset.library_refresh("INVOKE_DEFAULT") else: unable_to_refresh = True else: # See explanatory comments in temp_overide branch above if hasattr(area.spaces.active.params, "asset_library_reference"): area.spaces.active.params.asset_library_reference = cTB.prefs.asset_browser_library_name elif hasattr(area.spaces.active.params, "asset_library_ref"): area.spaces.active.params.asset_library_ref = cTB.prefs.asset_browser_library_name bpy.ops.asset.library_refresh({"area": area}, "INVOKE_DEFAULT") else: unable_to_refresh = True except Exception as e: # deliberately catch any exception reporting.capture_exception(e) unable_to_refresh = True if unable_to_refresh: build_no_refresh_notification(cTB) def t_refresh_ui() -> Optional[float]: """Refreshes all parts of UI involved in Asset Browser syncing. Namely: P4B main panel, status bar, Asset Browser and preferences. """ cTB.refresh_ui() # Force statusbar redraw bpy.context.workspace.status_text_set_internal(None) if cTB.num_asset_browser_jobs > 0: refresh_asset_browser() # Force asset browser+preferences redraw for area in bpy.context.screen.areas: if area.type == "FILE_BROWSER" and area.ui_type == "ASSETS": area.tag_redraw() elif area.type == "PREFERENCES" and bpy.context.preferences.active_section == "ADDONS": # TODO(Andreas): Can we somehow find out if P4B page is shown? area.tag_redraw() return None # one-shot, auto-unregister def t_after_processing_done() -> Optional[float]: """One-shot timer function doing final steps after processing finished.""" if cTB.asset_browser_quitting: return None if bpy.app.timers.is_registered(t_status_bar_update): bpy.app.timers.unregister(t_status_bar_update) t_refresh_ui() cTB.num_asset_browser_jobs = 0 cTB.num_jobs_ok = 0 cTB.num_jobs_error = 0 return None # one-shot, auto-unregister def after_processing_done() -> None: """Final steps after asset processing finished.""" DONE_SECONDS = 5.0 print_debug("LIB UPDATE") # Make sure, the asset browser blend files get picked up, # so quick menu shows correct options # TODO(Andreas): This will not completely work, yet!!! # AssetIndex/AssetData won't pick up info about # _LIB.blend files cTB._asset_index.update_all_local_assets( library_dirs=cTB.get_library_paths()) # Final actions in main thread bpy.app.timers.register(t_after_processing_done, first_interval=DONE_SECONDS, persistent=True) def check_sender_continue() -> bool: """Checks if thread_sender is supposed to continue.""" if not cTB.sender_running: return False # normal exit condition if cTB.proc_blender_client is None: print_debug("Client process is None!") error_msg = "Client process is None" reporting.capture_message( "asset_browser_client_none", error_msg, "error") return False exit_code_client = cTB.proc_blender_client.poll() if exit_code_client is not None: print_debug("Client exited unexpectedly", exit_code_client) build_lost_client_notification(cTB) error_msg = f"Client exited unexpectedly ({exit_code_client})" reporting.capture_message( "asset_browser_client_exit", error_msg, "error") # Close client's "back channel" to unblock thread_listener cTB.listener_running = False cTB.proc_blender_client.stderr.close() return False return True @reporting.handle_function(silent=True) def thread_sender() -> None: """Sends commands to client. For commands expecting an acknowledge message the thread will then block until the ack is received (or possibly resend the command if CMD_ERROR is received). """ TIMEOUT_QUEUE = 1.0 # Used to poll forr flags while waiting NUM_ACK_RETRIES = 60 # NUM_ACK_RETRIES * TIMEOUT_QUEUE = time to ack timeout print_debug("thread_sender") cTB.sender_running = True time_remaining = MAX_IDLE_TIME while cTB.sender_running: # Get rid of any unwanted acks from previous commands flush_queue_ack() if cTB.asset_browser_jobs_cancelled: flush_queue_send() after_processing_done() cTB.asset_browser_jobs_cancelled = False # Wait for something to send try: cmd_send = cTB.queue_send.get(timeout=TIMEOUT_QUEUE) cTB.queue_send.task_done() time_remaining = MAX_IDLE_TIME except queue.Empty: if cTB.sender_running: time_remaining -= 1.0 if time_remaining > 0.0: continue else: print_debug("Idle time reached") SyncAssetBrowserCmd(code=SyncCmd.EXIT).send_to_process( cTB.proc_blender_client) cTB.sender_running = False if not check_sender_continue(): break print_debug("Send:", cmd_send.code.name) cmd_send.send_to_process(cTB.proc_blender_client) # Depending on sent command code, we are already done if cmd_send.code in [SyncCmd.HELLO_OK, SyncCmd.STILL_THERE]: # HELLO_OK and STILL_THERE are fire and forget continue # Wait for acknowledge message retries = NUM_ACK_RETRIES while retries > 0 and cTB.sender_running: try: cmd_ack = cTB.queue_ack.get(timeout=TIMEOUT_QUEUE) cTB.queue_ack.task_done() except queue.Empty: cmd_ack = None if not check_sender_continue(): break retries -= 1 if cmd_ack is None: # queue timeout, unless retries are exhausted continue to wait if retries == 0: cTB.asset_browser_jobs_cancelled = True error_msg = "ACK timeout" reporting.capture_message( "asset_browser_ack_timeout", error_msg, "error") elif cmd_ack.code == SyncCmd.CMD_ERROR: # last sent command was not received well -> resend if retries > 0: cmd_send.send_to_process(cTB.proc_blender_client) else: cTB.asset_browser_jobs_cancelled = True error_msg = "ACK error, no more retries" reporting.capture_message( "asset_browser_retry_max", error_msg, "error") elif cmd_ack.code == SyncCmd.CMD_DONE: # last sent command was ok, continue with next break cTB.refresh_ui() # Finally if there are no more commands in queue # refresh Blender's Asset Browser library if cTB.queue_send.empty(): after_processing_done() print_debug("thread_sender EXIT") cTB.blender_client_starting = False cTB.num_asset_browser_jobs = 0 cTB.num_jobs_ok = 0 cTB.num_jobs_error = 0 cTB.thd_sender = None cTB.sender_running = False def start_sender(proc: Popen) -> None: """Starts thread_sender().""" cTB.thd_sender = threading.Thread(target=thread_sender, # args=(proc, ), daemon=True) cTB.thd_sender.start() def get_poliigon_library() -> type_UserAssetLibrary: """Returns Poliigons's Asset Browser library, if any.""" lib_poliigon = None addon_lib_path = cTB.get_library_path(primary=True) if addon_lib_path is None: return lib_poliigon for lib in bpy.context.preferences.filepaths.asset_libraries: if os.path.normpath(lib.path) == os.path.normpath(addon_lib_path): # Not checking name here, user may have manually renamed library lib_poliigon = lib break elif lib.name == cTB.prefs.asset_browser_library_name: lib_poliigon = None # TODO(Andreas): check with Patrick # So, here we have a library with correct name, but wrong path. # Either user created one manually, in which case I'm not sure, # we should touch the path. Or user changed the primary P4B library # directrory, in which case we would actually need to change the # path? # A conundrum... break return lib_poliigon def check_library_name_exists(library_name: str) -> bool: for lib in bpy.context.preferences.filepaths.asset_libraries: if lib.name == library_name: return True return False def create_poliigon_library(force: bool = False): """Creates a´new library in Blender's Asset Browser, if not already done so before. """ path_lib_primary = cTB.get_library_path(primary=True) lib_poliigon = get_poliigon_library() if lib_poliigon is not None: if force: lib_poliigon.path = path_lib_primary return lib_poliigon if cTB.get_library_path(primary=True) in [None, ""]: return None # Create new library libraries_before = list(bpy.context.preferences.filepaths.asset_libraries) # From: https://blender.stackexchange.com/questions/267676/create-a-new-asset-library-and-get-it-in-a-variable result = bpy.ops.preferences.asset_library_add(directory=path_lib_primary) if result == {"CANCELLED"}: error_msg = "Operator asset_library_add failed!" reporting.capture_message( "asset_browser_lib_create", error_msg, "error") return None libraries_after = list(bpy.context.preferences.filepaths.asset_libraries) libraries_new = [ lib for lib in libraries_after if lib not in libraries_before] if len(libraries_new) == 0: error_msg = "Failed to find freshly created library!" reporting.capture_message( "asset_browser_lib_create_find", error_msg, "error") return None lib_poliigon = libraries_new[0] path_lib_norm = os.path.normpath(lib_poliigon.path) path_lib_settings_norm = os.path.normpath(path_lib_primary) if path_lib_norm == path_lib_settings_norm: library_name = cTB.prefs.asset_browser_library_name count = 1 while check_library_name_exists(library_name): library_name = f"{cTB.prefs.asset_browser_library_name}.{count:03}" count += 1 lib_poliigon.name = library_name path_cat = os.path.join(path_lib_primary, "blender_assets.cats.txt") if not os.path.exists(path_cat): build_new_catalogue_notification(cTB) return lib_poliigon def start_blender_client() -> bool: """Starts a Blender subprocess.""" if cTB.proc_blender_client is not None: print_debug("Blender client still running") error_msg = "Process not None, when starting fresh!" reporting.capture_message( "asset_browser_client_running", error_msg, "error") print_debug("Starting Blender client...") cwd = os.path.join(os.path.dirname(os.path.abspath(__file__))) path_client_script = os.path.join(cwd, "asset_browser_sync_client.py") if not os.path.isfile(path_client_script): cTB.proc_blender_client = None error_msg = "Client script missing!" reporting.capture_message( "asset_browser_script_missing", error_msg, "error") return False path_lib_primary = cTB.get_library_path(primary=True) cTB.blender_client_starting = True cmd_blender = [bpy.app.binary_path] cmd_blender.append("--background") cmd_blender.append("--factory-startup") cmd_blender.append("--python") cmd_blender.append(path_client_script) cmd_blender.append("--") # Blender ignores all following command line args path_cat = os.path.join(path_lib_primary, "blender_assets.cats.txt") cmd_blender.append("--poliigon_cat_file") cmd_blender.append(path_cat) path_categories = os.path.join(cTB.dir_settings, "TB_Categories.json") cmd_blender.append("--poliigon_categories") cmd_blender.append(path_categories) cTB.proc_blender_client = Popen(cmd_blender, cwd=cwd, stdin=PIPE, stderr=PIPE, text=True) return True def wait_for_client(timeout: float = None) -> bool: """Waits for hello event, which gets set upon receiving a HELLO message.""" print_debug("Waiting for Blender client...") event_set = cTB.event_hello.wait(timeout) if event_set: cTB.event_hello.clear() return event_set def get_blender_process() -> bool: """Checks if the Blender client is already running and starts one (including all other infrastructure) if needed. """ if cTB.proc_blender_client is not None: old_exit_code = cTB.proc_blender_client.poll() if cTB.proc_blender_client is None or old_exit_code is not None: if cTB.thd_listener is not None: print_debug("listener still running") error_msg = "get_blender_process(): listener still running!" reporting.capture_message( "asset_browser_listener_running", error_msg, "error") if cTB.thd_sender is not None: print_debug("sender still running") error_msg = "get_blender_process(): sender still running!" reporting.capture_message( "asset_browser_process_running", error_msg, "error") if not start_blender_client(): return False cTB.queue_send = queue.Queue() cTB.queue_ack = queue.Queue() cTB.event_hello = threading.Event() start_listener(cTB.proc_blender_client) start_sender(cTB.proc_blender_client) else: cTB.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.STILL_THERE)) return True g_ev_thumb_download = threading.Event() def _callback_thumb_download_done(job: any) -> None: # No need to call standard callback done, as we are not interested in UI # updates here. g_ev_thumb_download.set() def thread_prepare_local_assets(asset_id: int) -> None: cTB._asset_index.update_all_local_assets( library_dirs=cTB.get_library_paths()) if asset_id == ASSET_ID_ALL: asset_ids_local = cTB._asset_index.get_asset_id_list( purchased=True, local=True) else: asset_ids_local = [asset_id] for _asset_id in asset_ids_local: path_thumb, url_thumb = cTB._asset_index.get_cf_thumbnail_info( _asset_id) if os.path.exists(path_thumb): continue asset_data = cTB._asset_index.get_asset(_asset_id) g_ev_thumb_download.clear() cTB.start_thumb_download( asset_data, path_thumb, url_thumb, callback_done=_callback_thumb_download_done) g_ev_thumb_download.wait(10.0) if not os.path.exists(path_thumb): # In this case we must rely on automatic thumb generation on client asset_name = asset_data.asset_name error_msg = ("send_asset_data(): Thumbnail failed to download " f"for {asset_name} ({_asset_id})!") reporting.capture_message( "asset_browser_thumb_dl", error_msg, "error") def thread_start_sync_local_client(): if not get_blender_process(): print_debug("Failed to get Blender client process!") error_msg = "Failed to get Blender client process." reporting.capture_message( "asset_browser_fail_client_p", error_msg, "error") return if not wait_for_client(timeout=TIMEOUT_ASSET_BROWSER_CLIENT_STARTUP): print_debug("Blender client failed to say hello!") error_msg = "No Hello message from Blender client." reporting.capture_message("asset_browser_no_hello", error_msg, "error") return def thread_initiate_asset_synchronization(asset_id: int, force: bool) -> None: with cTB.lock_client_start: thd_start_client = threading.Thread( target=thread_start_sync_local_client) thd_start_client.start() thd_prepare_assets = threading.Thread( target=thread_prepare_local_assets, args=(asset_id, )) thd_prepare_assets.start() thd_start_client.join() thd_prepare_assets.join() if cTB.proc_blender_client is None: return if asset_id == ASSET_ID_ALL: send_all_local_assets(force) else: if not send_single_asset(asset_id, force): error_msg = f"send_single_asset() failed unexpectedly: {asset_id}." reporting.capture_message( "asset_browser_single_asset", error_msg, "error") def get_asset_job_parameters(asset_data: AssetData) -> Optional[Dict]: """Provides size and (if needed) lod for Asset Browser generation.""" asset_id = asset_data.asset_id asset_name = asset_data.asset_name asset_type = asset_data.asset_type asset_type_data = asset_data.get_type_data() addon_convention = cTB.addon_convention local_convention = asset_data.get_convention(local=True) sizes_local = asset_type_data.get_size_list( local_only=True, addon_convention=addon_convention, local_convention=local_convention) if len(sizes_local) == 0: error_msg = ("get_asset_job_parameters(): No local sizes for asset " f"{asset_name}!") reporting.capture_message("asset_browser_no_sizes", error_msg, "error") return None params = {} if asset_type == AssetType.TEXTURE: params["size"] = asset_type_data.get_size( cTB.settings["res"], local_only=True, addon_convention=addon_convention, local_convention=local_convention) params["size_bg"] = None params["lod"] = None elif asset_type == AssetType.MODEL: params["size"] = asset_type_data.get_size( cTB.settings["mres"], local_only=True, addon_convention=addon_convention, local_convention=local_convention) params["size_bg"] = None lod = asset_type_data.get_lod(cTB.settings["lod"]) if lod is None: lod = "NONE" params["lod"] = lod elif asset_type == AssetType.HDRI: size_light, size_bg = determine_hdri_sizes( asset_data, size_light_default=cTB.settings["hdri"], size_bg_default=cTB.settings["hdrib"], use_jpg_bg=cTB.settings["hdri_use_jpg_bg"], addon_convention=cTB.addon_convention) # size = asset_type_data.get_size( # cTB.settings["hdri"], # local_only=True, # addon_convention=addon_convention, # local_convention=local_convention) params["size"] = size_light params["size_bg"] = size_bg params["lod"] = None params["thumb"], _ = cTB._asset_index.get_cf_thumbnail_info(asset_id) return params def send_asset_data(asset_id: int, force: bool) -> int: """Queues a single ASSET job to be send to the client. Return value: 0: Error 1: Job started 2: Job skipped, already existing """ asset_data = cTB._asset_index.get_asset(asset_id) asset_name = asset_data.asset_name asset_type = asset_data.asset_type params = get_asset_job_parameters(asset_data) if params is None: return 0 if not os.path.exists(params["thumb"]): # In this case we must rely on automatic thumb generation on client error_msg = ("send_asset_data(): No thumbnail for " f"{asset_name}({asset_id})!") reporting.capture_message("asset_browser_no_thumb", error_msg, "error") directory = asset_data.get_asset_directory() filename = f"{asset_name}_LIB.blend" params["path_result"] = os.path.join(directory, filename) if not force and os.path.exists(params["path_result"]): print_debug("Skipping already existing:", asset_id) return 2 if asset_type == AssetType.HDRI: pass # nothing to do here elif asset_type == AssetType.MODEL: params["lod"] = "NONE" elif asset_type == AssetType.TEXTURE: pass # nothing to do here # Register timer to get status bar redrawn in regular intervals if not bpy.app.timers.is_registered(t_status_bar_update): bpy.app.timers.register( t_status_bar_update, first_interval=0.5, persistent=True) cTB.num_asset_browser_jobs += 1 cTB.queue_send.put( SyncAssetBrowserCmd( code=SyncCmd.ASSET, data={"asset_id": asset_id}, params=params) ) return 1 def send_all_local_assets(force: bool) -> int: """Queues all local assets, which have not been processed before. Arguments: force: Set to True to process _all_ local assets. Even if processed before. """ asset_ids_local = cTB._asset_index.get_asset_id_list( purchased=True, local=True) num_assets = 0 for _asset_id in asset_ids_local: result = send_asset_data(_asset_id, force) if result == 1: num_assets += 1 cTB.refresh_ui() return num_assets def send_single_asset(asset_id: int, force: bool) -> bool: """Queues one local asset, which has not been processed before. Arguments: force: Set to True to process the local assets, even if processed before. """ asset_ids_local = cTB._asset_index.get_asset_id_list( purchased=True, local=True) if asset_id not in asset_ids_local: error_msg = f"send_single_asset(): Asset {asset_id} not found!" reporting.capture_message("asset_browser_no_asset", error_msg, "error") return False result = send_asset_data(asset_id, force) # Here we are fine with either "job started" (1) or "already existing" (2) result = result > 0 cTB.refresh_ui() return result # Following functions are needed by Asset Browser operators and panels def get_asset_name_from_browser_asset( asset_file: type_FileSelectEntry) -> str: """Returns Poliigon's asset name for an asset file in the Asset Browser""" if hasattr(asset_file, "relative_path"): # pre blender 4.0 path_asset = asset_file.relative_path else: # full_library_path is the .blend path, while full_path includes # the "subpath" like "....blend/Material/Material_1K" path_asset = asset_file.full_library_path pos = path_asset.find("_LIB.blend", 1) path_asset = path_asset[:pos] return os.path.basename(path_asset) def get_asset_id_from_browser_asset( asset_file: type_FileSelectEntry) -> Optional[int]: """Returns Poliigon's asset ID for an asset file in the Asset Browser.""" asset_name = get_asset_name_from_browser_asset(asset_file) asset_ids_local = cTB._asset_index.get_asset_id_list( purchased=True, local=True) asset_id_found = None for _asset_id in asset_ids_local: asset_data = cTB._asset_index.get_asset(_asset_id) if asset_data.asset_name == asset_name: asset_id_found = asset_data.asset_id break return asset_id_found def get_asset_data_from_browser_asset(asset_file: type_FileSelectEntry, # asset_name: str ) -> Optional[AssetData]: """Returns asset data dict for an asset file in Blender's Asset Browser.""" asset_id = get_asset_id_from_browser_asset(asset_file) if asset_id is None: print(f"ASSET NOT FOUND {asset_id}") # TODO(Andreas): user notification return None asset_data = cTB._asset_index.get_asset(asset_id) return asset_data def is_asset_browser(context) -> bool: """Returns true, if the area is an Asset Browser.""" is_file_browser = context.area.type == "FILE_BROWSER" _is_asset_browser = context.area.ui_type == "ASSETS" if not (is_file_browser and _is_asset_browser): return False if bpy.app.version > (4, 0): has_asset_browser_ref = hasattr(context, "asset_library_reference") else: has_asset_browser_ref = hasattr(context.space_data.params, "asset_library_ref") if not has_asset_browser_ref: return False return True def is_poliigon_library(context, incl_all_libs: bool = True) -> bool: """Returns True, if the active library is a Poliigon library.""" if bpy.app.version >= (4, 0): if not isinstance(context.area.spaces.active, bpy.types.SpaceFileBrowser): # Known issue in 4.0, if wrong space cannot fetch libname. return incl_all_libs try: library_name = context.area.spaces.active.params.asset_library_reference except Exception as e: reporting.capture_exception(e) return True else: library_name = context.space_data.params.asset_library_ref # TODO(Andreas): Use context.area.spaces.active.params.asset_library_ref instead? is_poliigon_lib = library_name == cTB.prefs.asset_browser_library_name is_all_libs = library_name == "ALL" return is_poliigon_lib or (incl_all_libs and is_all_libs) def is_only_poliigon_selected(context) -> bool: """Returns True, if only Poliigon assets are selected in Asset Browser.""" asset_files = get_selected_assets(context) is_poliigon_only = True for _asset_file in asset_files: asset_data = get_asset_data_from_browser_asset(_asset_file) if asset_data is None: is_poliigon_only = False break return is_poliigon_only def check_handles_and_selected_files(context) -> Tuple: selected_asset_files = None asset_file_handle = None if bpy.app.version >= (4, 0): has_selected_assets = hasattr(context, "selected_assets") if has_selected_assets: selected_asset_files = context.selected_assets has_asset_handle = hasattr(context, "asset") if has_asset_handle: asset_file_handle = context.asset else: has_selected_assets = hasattr(context, "selected_asset_files") if has_selected_assets: selected_asset_files = context.selected_asset_files has_asset_handle = hasattr(context, "asset_file_handle") if has_asset_handle: asset_file_handle = context.asset_file_handle return selected_asset_files, asset_file_handle def get_selected_assets(context) -> List: selected_asset_files, asset_file_handle = check_handles_and_selected_files( context) if selected_asset_files is not None and len(selected_asset_files) > 0: return selected_asset_files elif asset_file_handle is not None: return [asset_file_handle] else: return [] def get_num_selected_assets(context) -> int: selected_assets = get_selected_assets(context) return len(selected_assets) cTB = None def register(addon_version: str) -> None: global cTB cTB = get_context(addon_version) def unregister() -> None: # Nothing to do here. Function only exists for orthogonality reasons # during init and shutdown. pass