Files
blender-portable-repo/scripts/addons/poliigon-addon-blender/asset_browser/asset_browser.py
T
2026-03-17 14:30:01 -06:00

1088 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# #### 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