2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
+207 -153
View File
@@ -44,11 +44,12 @@ from .modules.poliigon_core.api_remote_control import (
JobType)
from .modules.poliigon_core.api_remote_control_params import (
CATEGORY_ALL,
CATEGORY_FREE,
get_search_key,
KEY_TAB_IMPORTED,
KEY_TAB_MY_ASSETS,
KEY_TAB_RECENT_DOWNLOADS,
KEY_TAB_ONLINE,
KEY_TAB_LOCAL,
IDX_PAGE_ACCUMULATED,
PAGE_SIZE_ACCUMULATED)
from .modules.poliigon_core.assets import (
@@ -96,6 +97,8 @@ from .utils import (
# build_survey_notification
# )
ADDON_PATH = os.path.dirname(os.path.abspath(__file__))
def panel_update(context=None) -> None:
"""Force a redraw of the 3D and preferences panel from operator calls."""
@@ -193,7 +196,8 @@ class c_Toolbox(PoliigonAddon):
addon_env=environment,
addon_settings=settings,
addon_convention=SUPPORTED_CONVENTION,
addon_supported_model=[ModelType.BLEND, ModelType.FBX]
addon_supported_model=[ModelType.BLEND, ModelType.FBX],
addon_root_path=ADDON_PATH
)
if api_service is not None:
@@ -206,12 +210,15 @@ class c_Toolbox(PoliigonAddon):
report_exception=reporting.capture_exception,
report_thread=reporting.handle_thread,
status_listener=self.api_status_listener,
get_renderer_name=self.api_get_renderer,
urls_dcc=URLS_BLENDER,
notify_icon_info="INFO",
notify_icon_no_connection="NONE",
notify_icon_survey="NONE",
notify_icon_warn="ERROR",
notify_update_body=_t("Download the {0} update")
notify_icon_wm_onboarding="HIDE_OFF",
notify_update_body=_t("Download the {0} update"),
onboarding_wm_title=_t("Preview textures in 1K by clicking the eye icon"),
)
def register(self, version: str) -> None:
@@ -263,12 +270,14 @@ class c_Toolbox(PoliigonAddon):
self.login_cancelled = False
self.login_time_start = 0
self.login_in_progress = False
self.login_is_signup = False # Track if signup or login was clicked
self.last_login_error = None
self.did_show_profile_survey = False # To avoid repeating survey
self.signal_for_profile_survey_emitted = False
self.msg_download_limit = None
self.vSearch = self._init_tabs_dict("")
self.search_free = False
self.vLastSearch = self._init_tabs_dict("")
self.vPage = self._init_tabs_dict(0)
self.vPages = self._init_tabs_dict(0)
@@ -293,7 +302,7 @@ class c_Toolbox(PoliigonAddon):
self.ui_errors = []
self.vActiveCat = self.settings["category"][self.settings["area"]]
self.vActiveCat = self.settings["category"]
self.vAssetType = self.vActiveCat[0]
# Initial value to use for linking set by prefrences.
@@ -304,6 +313,7 @@ class c_Toolbox(PoliigonAddon):
self.vCategories = self._init_tabs_dict_dict()
self.vCategories["new"] = {} # TODO(Andreas): ???
self.category_ids = {}
self.num_assets = self._init_tabs_dict(0)
self.num_assets_current_query = 0
@@ -419,15 +429,19 @@ class c_Toolbox(PoliigonAddon):
self.error_plan_upgrade = None
self.msg_plan_upgrade_finished = None
self.fetching_user_data = True
self.api_rc.add_job_get_user_data(
user_name,
user_id,
callback_cancel=None,
callback_progress=None,
callback_done=self.callback_get_user_data_done,
force=True
)
# If no token available, there is no reason to add a /me job - will
# be redirected to login page;
if self._api.token:
self.fetching_user_data = True
self.api_rc.add_job_get_user_data(
user_name,
user_id,
callback_cancel=None,
callback_progress=None,
do_fetch_asset_data=not self.all_assets_fetched,
callback_done=self.callback_get_user_data_done,
force=True
)
self.initial_screen_viewed = False
self.initial_register_complete = True
@@ -479,7 +493,7 @@ class c_Toolbox(PoliigonAddon):
self.ui_icons.load(_name, path_icon, _icontype)
@staticmethod
def _init_tabs_dict(value: any) -> None:
def _init_tabs_dict(value: any) -> Dict:
"""Initializes a dictionary with the main tabs as keys and an arbitrary
value per tab.
"""
@@ -487,7 +501,9 @@ class c_Toolbox(PoliigonAddon):
dict_tabs = {
KEY_TAB_ONLINE: value,
KEY_TAB_MY_ASSETS: value,
KEY_TAB_IMPORTED: value
KEY_TAB_RECENT_DOWNLOADS: value,
KEY_TAB_IMPORTED: value,
KEY_TAB_LOCAL: value
}
return dict_tabs
@@ -500,7 +516,9 @@ class c_Toolbox(PoliigonAddon):
dict_tabs = {
KEY_TAB_ONLINE: {},
KEY_TAB_MY_ASSETS: {},
KEY_TAB_IMPORTED: {}
KEY_TAB_RECENT_DOWNLOADS: {},
KEY_TAB_IMPORTED: {},
KEY_TAB_LOCAL: {}
}
return dict_tabs
@@ -567,18 +585,13 @@ class c_Toolbox(PoliigonAddon):
self.vPage[KEY_TAB_ONLINE] = 0
self.vPages[KEY_TAB_ONLINE] = 1
self.settings["category"][KEY_TAB_ONLINE] = [CATEGORY_ALL]
self.settings["category"][KEY_TAB_MY_ASSETS] = [CATEGORY_ALL]
self.settings["category"][KEY_TAB_IMPORTED] = [CATEGORY_ALL]
self.settings["category"] = [CATEGORY_ALL]
self.vActiveAsset = None
self.vActiveMat = None
self.vActiveMode = None
props = bpy.context.window_manager.poliigon_props
props.search_poliigon = ""
props.search_my_assets = ""
props.search_imported = ""
self.search_free = False
props.search = ""
def _init_upgrade_content(self) -> None:
"""Initializes UpgradeContent with P4B specifics."""
@@ -626,6 +639,14 @@ class c_Toolbox(PoliigonAddon):
for _path_library in paths_additional:
self.add_library_path(_path_library, primary=False)
@staticmethod
def api_get_renderer() -> Optional[str]:
try:
renderer = bpy.context.scene.render.engine
except AttributeError: # Startup condition
renderer = None
return renderer
def api_status_listener(self, status: ApiStatus) -> None:
"""Updates notifications according to the form of the API event.
@@ -661,6 +682,7 @@ class c_Toolbox(PoliigonAddon):
self._asset_index.flush()
self._asset_index.flush_is_local()
self._asset_index.flush_is_purchased()
self._asset_index.flush_state()
self.refresh_ui()
@@ -691,7 +713,14 @@ class c_Toolbox(PoliigonAddon):
self.login_in_progress = False
self.fetching_user_data = False
reporting.assign_user(self.user.user_id)
if self.user is not None:
reporting.assign_user(self.user.user_id)
else:
reporting.assign_user(None)
reporting.capture_message(
"none_user_after_get_user_data_done",
"User is none after logging in with OK response",
"error")
self.refresh_ui()
@@ -761,7 +790,7 @@ class c_Toolbox(PoliigonAddon):
self.login_in_progress = False
token = job.result.body["access_token"]
token = job.result.body.get("access_token", "")
self.settings_config.set("user", "token", token)
# Clear time since install since successful.
@@ -770,18 +799,25 @@ class c_Toolbox(PoliigonAddon):
self._settings.save_settings()
user_info = job.result.body["user"]
reporting.assign_user(self.user.user_id)
user_info = job.result.body.get("user", {})
name_user = user_info.get("name", "")
id_user = user_info.get("id", -1)
if self.user is not None:
reporting.assign_user(self.user.user_id)
else:
reporting.assign_user(None)
key_fetch_all = ((CATEGORY_ALL, ), "")
self.fetching_asset_data[KEY_TAB_MY_ASSETS][key_fetch_all] = True
self.fetching_asset_data[KEY_TAB_ONLINE][key_fetch_all] = True
self.fetching_user_data = True
self.api_rc.add_job_get_user_data(
user_info["name"],
user_info["id"],
name_user,
id_user,
callback_cancel=None,
callback_progress=None,
do_fetch_asset_data=not self.all_assets_fetched,
callback_done=self.callback_get_user_data_done,
force=True
)
@@ -837,11 +873,16 @@ class c_Toolbox(PoliigonAddon):
f"callback_get_categories_done: ERROR {error}\n {body}")
for _category in body:
type_cat = _category["name"]
type_cat = _category.get("name", "Unknown Category")
self.logger.debug(
f"callback_get_categories_done: Type: {type_cat}")
if type_cat not in self.vCategories[KEY_TAB_ONLINE].keys():
self.vCategories[KEY_TAB_ONLINE][type_cat] = {}
id_category = _category.get("id", -1)
cat_path = _category.get("path", "")
cat_path = cat_path.replace("/hdrs", "/HDRIs")
self.category_ids[id_category] = cat_path
self.f_GetCategoryChildren(type_cat, _category)
path_category_file = os.path.join(
@@ -876,7 +917,14 @@ class c_Toolbox(PoliigonAddon):
params = job.params
key_fetch = (tuple(params.category_list), params.search)
if params.already_in_index:
if not job.result.ok:
if key_fetch in self.fetching_asset_data[params.tab]:
del self.fetching_asset_data[params.tab][key_fetch]
self.refresh_ui()
return
if params.already_in_index and not params.force_request:
if key_fetch in self.fetching_asset_data[params.tab]:
del self.fetching_asset_data[params.tab][key_fetch]
self.refresh_ui()
@@ -909,8 +957,10 @@ class c_Toolbox(PoliigonAddon):
self.num_assets[params.tab] = job.result.body.get("total", -1)
tab_active = self.settings["area"]
tab_refresh = tab_active == params.tab
tab_refresh |= tab_active == KEY_TAB_IMPORTED and params.tab == KEY_TAB_MY_ASSETS
is_imported_request = tab_active == KEY_TAB_IMPORTED
imported_refresh = is_imported_request and params.tab in [KEY_TAB_MY_ASSETS,
KEY_TAB_RECENT_DOWNLOADS]
tab_refresh = tab_active == params.tab or imported_refresh
idx_ui_page_current = self.vPage[tab_active] # this is a UI page index
num_per_ui_page = self.settings["page"]
@@ -926,7 +976,8 @@ class c_Toolbox(PoliigonAddon):
# be spread across two API requests
ui_page_in_job |= idx_first_job_asset <= idx_last_ui_asset < idx_last_job_asset
if params.tab == KEY_TAB_MY_ASSETS or self.is_unlimited_user():
user_assets_tab = params.tab in [KEY_TAB_MY_ASSETS, KEY_TAB_RECENT_DOWNLOADS]
if user_assets_tab or self.is_unlimited_user():
self.f_GetSceneAssets()
self._check_asset_browser(asset_id_list)
@@ -1004,95 +1055,53 @@ class c_Toolbox(PoliigonAddon):
area = self.settings["area"]
if area == KEY_TAB_ONLINE:
self.track_screen("home")
self.signal_view_screen("home")
elif area == KEY_TAB_MY_ASSETS:
self.track_screen(KEY_TAB_MY_ASSETS)
self.signal_view_screen(KEY_TAB_MY_ASSETS)
elif area == KEY_TAB_IMPORTED:
self.track_screen(KEY_TAB_IMPORTED)
self.signal_view_screen(KEY_TAB_IMPORTED)
elif area == KEY_TAB_LOCAL:
self.signal_view_screen(KEY_TAB_LOCAL)
elif area == KEY_TAB_RECENT_DOWNLOADS:
# Can't it be only "downloads", since that is the tab used for all requests?
self.signal_view_screen(KEY_TAB_RECENT_DOWNLOADS)
elif area == "account":
self.track_screen("my_account")
def track_screen(self, area: str) -> None:
"""Signals input screen area in a background thread if opted in."""
if not self._api._is_opted_in():
return
thread = threading.Thread(
target=self._api.signal_view_screen,
args=(area,),
)
thread.daemon = 1
thread.start()
self.threads.append(thread)
self.signal_view_screen("my_account")
def signal_popup(
self, *, popup: str, click: Optional[str] = None) -> None:
"""Signals an onboarding popup being viewed or clicked in the
background, if user opted in.
background
"""
if not self._api._is_opted_in():
return
if click is not None:
target = self._api.signal_click_notification
args = (popup, click,)
self.signal_click_notification(popup, click)
else:
target = self._api.signal_view_notification
args = (popup,)
thread = threading.Thread(
target=target,
args=args,
)
thread.daemon = 1
thread.start()
self.threads.append(thread)
def signal_import_asset(self, asset_id: int) -> None:
"""Signals an asset import in the background if user opted in."""
if not self._api._is_opted_in() or asset_id == 0:
return
thread = threading.Thread(
target=self._api.signal_import_asset,
args=(asset_id,),
)
thread.daemon = 1
thread.start()
self.threads.append(thread)
def signal_preview_asset(self, asset_id: int) -> None:
"""Signals an asset preview in the background if user opted in."""
if not self._api._is_opted_in():
return
thread = threading.Thread(
target=self._api.signal_preview_asset,
args=(asset_id,),
)
thread.daemon = 1
thread.start()
self.threads.append(thread)
self.signal_view_notification(popup)
def f_GetCategoryChildren(self, type_cat: str, category: Dict) -> None:
children = category["children"]
children = category.get("children", [])
for _child in children:
cat_path = []
for _path_parts in _child["path"].split("/"):
child_path = _child["path"]
for _path_parts in child_path.split("/"):
_path_parts = " ".join([_part.capitalize()
for _part in _path_parts.split("-")])
cat_path.append(_path_parts)
cat_path = ("/".join(cat_path)).replace("/" + type_cat + "/", "/")
cat_path = cat_path.replace("/Hdrs/", "/")
cat_path = cat_path.replace("/Hdris/", "/")
if "Generators" in cat_path:
continue
self.logger.debug(f"f_GetCategoryChildren {cat_path}")
id_category = _child.get("id", -1)
child_path = child_path.replace("/hdrs", "/HDRIs")
self.category_ids[id_category] = child_path
self.vCategories[KEY_TAB_ONLINE][type_cat][cat_path] = []
if len(_child["children"]) > 0:
if len(_child.get("children", [])) > 0:
self.f_GetCategoryChildren(type_cat, _child)
# @timer
@@ -1102,8 +1111,10 @@ class c_Toolbox(PoliigonAddon):
categories: Optional[List[str]] = None,
search: Optional[str] = None,
force: bool = False,
force_request: bool = False,
callback_done: Optional[Callable] = None
) -> None:
self.logger.debug(
f"f_GetAssets area={area}, force={force}")
self.logger.debug(
@@ -1115,7 +1126,13 @@ class c_Toolbox(PoliigonAddon):
area = self.settings["area"]
if categories is None:
categories = self.settings["category"][area].copy()
categories = self.settings["category"].copy()
if area == KEY_TAB_LOCAL:
# For Local assets we should always force the request (and not rely
# on cached queries). There is no Api request involved - so there
# is no major counterpoint in this case;
force_request = True
if search is None:
search = self.vSearch[area]
@@ -1125,14 +1142,13 @@ class c_Toolbox(PoliigonAddon):
if callback_done is None:
callback_done = self.callback_get_asset_done
do_my_assets = False
if area == KEY_TAB_IMPORTED:
if self.is_unlimited_user():
area = KEY_TAB_ONLINE
else:
area = KEY_TAB_MY_ASSETS
if self.search_free:
categories = [CATEGORY_FREE]
area = KEY_TAB_RECENT_DOWNLOADS
do_my_assets = self.user_legacy_own_assets()
key_fetch = (tuple(categories), search)
if key_fetch in self.fetching_asset_data[area]:
@@ -1146,11 +1162,12 @@ class c_Toolbox(PoliigonAddon):
search=search,
idx_page=1, # Always fetch all beginning from page 1
page_size=None,
force_request=False,
force_request=force_request,
do_get_all=True,
callback_cancel=None,
callback_progress=None,
callback_done=callback_done,
do_my_assets=do_my_assets,
force=force)
def flush_thumb_prefetch_queue(self) -> None:
@@ -1181,7 +1198,7 @@ class c_Toolbox(PoliigonAddon):
# self.thumbsDownloading = []
# @timer
def f_GetPageAssets(self, idx_page: int) -> Tuple[List[int], int]:
def f_GetPageAssets(self, idx_page: int) -> Tuple[Optional[List[int]], int]:
area = self.settings["area"]
search = self.vSearch[area]
num_per_page = self.settings["page"]
@@ -1189,10 +1206,16 @@ class c_Toolbox(PoliigonAddon):
self.logger.debug(f"f_GetPageAssets area={area}, search={search}, "
f"idx_page={idx_page}")
category_list = self.settings["category"][area]
category_list = self.settings["category"]
key = get_search_key(
tab=area, search=search, category_list=category_list)
if not self.all_assets_fetched:
return None, 0
if area != KEY_TAB_ONLINE and not self.are_user_assets_fetched():
return None, 0
asset_ids = self._asset_index.query(key_query=key,
chunk=IDX_PAGE_ACCUMULATED,
chunk_size=PAGE_SIZE_ACCUMULATED)
@@ -1249,6 +1272,26 @@ class c_Toolbox(PoliigonAddon):
self.logger.debug("f_GetAssetsSorted No Assets")
return []
def check_if_purchased(self, asset_id) -> bool:
"""Workaround to ensure only truly purchased assets are included here,
since asset_data.is_purchased actually means "purchased or downloads"
Old code: owned = bool(asset_data.is_purchased)
"""
query_key_all_user_assets = (
KEY_TAB_MY_ASSETS,
None,
None,
None,
IDX_PAGE_ACCUMULATED,
PAGE_SIZE_ACCUMULATED)
try:
asset_ids_my_assets = cTB._asset_index.cached_queries[
query_key_all_user_assets]
except KeyError:
asset_ids_my_assets = []
return asset_id in asset_ids_my_assets
def get_pref_size(self, asset_type: AssetType) -> str:
if asset_type == AssetType.TEXTURE:
return self.settings["res"]
@@ -1333,27 +1376,29 @@ class c_Toolbox(PoliigonAddon):
with self.lock_thumbs:
self.thumbs.clear()
self._asset_index.flush_queries_by_tab(tab=KEY_TAB_ONLINE)
self._asset_index.flush_queries_by_tab(tab=KEY_TAB_MY_ASSETS)
self._asset_index.flush_queries_by_tab(tab=KEY_TAB_IMPORTED)
self._asset_index.flush_is_local()
self._asset_index.flush()
if icons_only is False:
# Get updated account data, fresh "All Assets" data for both tabs
# and category counts
key_fetch_all = ((CATEGORY_ALL, ), "")
self.fetching_asset_data[KEY_TAB_MY_ASSETS][key_fetch_all] = True
self.fetching_asset_data[KEY_TAB_ONLINE][key_fetch_all] = True
self.fetching_user_data = True
self.api_rc.add_job_get_user_data(
self.user.user_name,
self.user.user_id,
callback_cancel=None,
callback_progress=None,
callback_done=self.callback_get_user_data_done,
force=True
)
if icons_only is True:
self.refresh_ui()
return
self.refresh_top_level_queries_flags()
# Get updated account data, fresh "All Assets" data for both tabs
# and category counts
key_fetch_all = ((CATEGORY_ALL, ), "")
self.fetching_asset_data[KEY_TAB_MY_ASSETS][key_fetch_all] = True
self.fetching_asset_data[KEY_TAB_ONLINE][key_fetch_all] = True
self.fetching_user_data = True
self.api_rc.add_job_get_user_data(
self.user.user_name,
self.user.user_id,
callback_cancel=None,
callback_progress=None,
do_fetch_asset_data=True,
callback_done=self.callback_get_user_data_done,
force=True
)
self.refresh_ui()
def get_accumulated_query_cache_key(self,
@@ -1409,53 +1454,62 @@ class c_Toolbox(PoliigonAddon):
pass
return asset_ids_in_scene
def f_GetSceneAssets(self):
self.logger.debug("f_GetSceneAssets")
# To the server "Imported" tab acts as if it was "My Assets" tab.
# And its responses will be stored as accumulated "My Assets" entries
# in query cache. These will then be used in here to create proper
# query cache entries for "Imported" tab, which will always be just
# filtered down versions of the "My Assets" requests.
def _get_tab_base_assets(self) -> List[int]:
# To the server "Imported" tab acts as if it was
# "My Assets" + "Downloads" tab.
# And its responses will be stored as accumulated "My Assets" and
# "Downloads"entries in query cache.
# These will then be used in here to create proper query cache entries
# for "Imported" tab, which will always be just filtered down versions
# of the "My Assets" requests.
#
# For unlimited users this needs to work a bit different, as local
# assets do not appear on My Assets tab, in this case the Online tab
# needs to be used as reference.
if self.is_unlimited_user():
tab_base = KEY_TAB_ONLINE
tab_base = [KEY_TAB_ONLINE]
else:
tab_base = KEY_TAB_MY_ASSETS
tab_base = [KEY_TAB_MY_ASSETS, KEY_TAB_RECENT_DOWNLOADS]
search_imported = self.vSearch[KEY_TAB_IMPORTED]
category_list_imported = self.settings["category"][KEY_TAB_IMPORTED]
query_base = self.get_accumulated_query_cache_key(
tab=tab_base,
search=search_imported,
category_list=category_list_imported)
query_key_imported = self.get_accumulated_query_cache_key(
tab=KEY_TAB_IMPORTED,
search=search_imported,
category_list=category_list_imported)
base_ids = []
for _tab in tab_base:
query_base = self.get_accumulated_query_cache_key(
tab=_tab,
search=self.vSearch[KEY_TAB_IMPORTED],
category_list=self.settings["category"])
if query_base not in self._asset_index.cached_queries:
return
if query_base not in self._asset_index.cached_queries:
continue
asset_ids_my_assets = self._asset_index.cached_queries[
query_base]
base_ids += self._asset_index.cached_queries[query_base]
return base_ids
asset_ids_in_scene = self._find_asset_ids_in_scene()
def f_GetSceneAssets(self) -> None:
self.logger.debug("f_GetSceneAssets")
user_asset_ids = self._get_tab_base_assets()
try:
asset_ids_in_scene = self._find_asset_ids_in_scene()
except AttributeError as e:
if "_RestrictData" in str(e):
# Blender still starting up, can't access local data yet
return
raise e
# Filter My Assets IDs by asset IDs in scene
# (so we maintain order and category/search filtering)
asset_ids_in_query = []
for _asset_id in asset_ids_my_assets:
for _asset_id in user_asset_ids:
if _asset_id not in asset_ids_in_scene:
continue
asset_ids_in_query.append(_asset_id)
# Finally store result in query cache
self._asset_index.cached_queries[
query_key_imported] = asset_ids_in_query
query_key_imported = self.get_accumulated_query_cache_key(
tab=KEY_TAB_IMPORTED,
search=self.vSearch[KEY_TAB_IMPORTED],
category_list=self.settings["category"])
self._asset_index.cached_queries[query_key_imported] = asset_ids_in_query
# TODO(Andreas): f_GetActiveData is in dire need for some love
def f_GetActiveData(self) -> None: