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
@@ -70,7 +70,7 @@ class POLIIGON_OT_add_converter_node(Operator):
global view_screen_tracked_nodes
if not view_screen_tracked_nodes:
cTB.track_screen("blend_node_add")
cTB.signal_view_screen("blend_node_add")
view_screen_tracked_nodes = True
if bpy.app.version >= (2, 90):
@@ -118,7 +118,9 @@ class POLIIGON_OT_apply(Operator):
if do_subdiv or len(objs_selected) > len(objs_add_disp):
bpy.context.scene.render.engine = "CYCLES"
bpy.context.scene.cycles.feature_set = "EXPERIMENTAL"
if bpy.app.version < (5, 0):
# Blender 5.0 no longer has feature sets, adaptive is included
bpy.context.scene.cycles.feature_set = "EXPERIMENTAL"
for _node in mat.node_tree.nodes:
if _node.type != "GROUP":
@@ -161,7 +163,9 @@ class POLIIGON_OT_apply(Operator):
name="Subdivision", type="SUBSURF")
mod_subdiv.subdivision_type = "SIMPLE"
mod_subdiv.levels = 0 # Don't do subdiv in viewport
_obj.cycles.use_adaptive_subdivision = 1
if bpy.app.version < (5, 0):
# In blender 5.0, there's no special settings for this.
_obj.cycles.use_adaptive_subdivision = True
# Scale ...........................................................
# TODO(Andreas): What is this? Did this ever work?
@@ -189,8 +193,4 @@ class POLIIGON_OT_apply(Operator):
bpy.ops.poliigon.poliigon_active(
mode="mat", asset_type=asset_type.name, data=cTB.vActiveMat
)
if self.exec_count == 0:
cTB.signal_import_asset(asset_id=self.asset_id)
self.exec_count += 1
return {"FINISHED"}
@@ -0,0 +1,77 @@
# #### 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 bpy
from bpy.types import Operator
from bpy.props import StringProperty
from ..modules.poliigon_core.multilingual import _t
from ..modules.poliigon_core.asset_filters import FilterOptions
from ..toolbox import get_context
from .. import reporting
class POLIIGON_OT_asset_filter(Operator):
bl_idname = "poliigon.asset_filter"
bl_label = _t(FilterOptions.ALL_ASSETS.map_to_short_value())
bl_description = _t("Filter Assets")
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
@staticmethod
def init_context(addon_version: str) -> None:
"""Called from operators.py to init global addon context."""
global cTB
cTB = get_context(addon_version)
@classmethod
def description(cls, context, properties):
return properties.tooltip
@classmethod
def poll(cls, context):
return cTB.all_assets_fetched
@staticmethod
def _show_filters_menu(cTB) -> None:
"""Generates the popup menu to display category selection options."""
@reporting.handle_draw()
def draw(self, context):
layout = self.layout
row = layout.row()
col = row.column(align=True)
if cTB.user is None:
filter_options = FilterOptions.get_list()
else:
filter_options = FilterOptions.get_list_for_user(cTB.user)
for _filter_data in filter_options:
button_text = _t(_filter_data.map_to_short_value())
op = col.operator("poliigon.poliigon_setting", text=button_text)
op.mode = f"asset_filter_{_filter_data.map_to_query()}"
op.tooltip = _t("Filter {0} Assets").format(button_text)
bpy.context.window_manager.popup_menu(draw)
def execute(self, context):
self._show_filters_menu(cTB)
return {"FINISHED"}
@@ -23,7 +23,6 @@ import bpy
from bpy.types import Operator
from bpy.props import StringProperty
from ..modules.poliigon_core.api_remote_control_params import KEY_TAB_ONLINE
from ..modules.poliigon_core.multilingual import _t
from ..toolbox import get_context
from .. import reporting
@@ -71,15 +70,6 @@ class POLIIGON_OT_category(Operator):
if idx_category == 0:
col.separator()
area = cTB.settings["area"]
if area == KEY_TAB_ONLINE and index == "0":
col.separator()
tooltip = _t("Search for Free Assets")
op = col.operator("poliigon.poliigon_setting", text=_t("Free"))
op.mode = "search_free"
op.tooltip = tooltip
bpy.context.window_manager.popup_menu(draw)
def execute(self, context):
@@ -52,5 +52,6 @@ class POLIIGON_OT_close_notification(Operator):
_t("Could not dismiss notification, out of bounds.")
)
return {'CANCELLED'}
cTB.notify.dismiss_notice(notice)
return {'FINISHED'}
@@ -19,13 +19,15 @@
from enum import IntEnum
from functools import partial
import os
from time import monotonic
from typing import Dict, Optional
from threading import Event
import time
from typing import Callable, Dict, List, Optional
import bpy
from bpy.app.handlers import persistent
from bpy.props import (
IntProperty,
StringProperty)
FloatProperty,
IntProperty)
from bpy.types import Operator
import bpy.utils.previews
@@ -55,6 +57,10 @@ from ..utils import load_image
from .. import reporting
MAX_NUM_PREVIEWS = 10000
TIMEOUT_THUMB_DOWNLOAD = 15.0 # per single thumb download
class MODE_SELECT(IntEnum):
# Negative values, as zero and positive ones represent an index
NEXT = -1
@@ -66,44 +72,58 @@ class DetailViewState():
involved operators.
"""
def __init__(self, num_previews: int):
def __init__(
self,
num_previews: int,
ts: float,
context: bpy.types.Context
):
self.ts: float = ts
self.context: bpy.types.Context = context
self.partial_periodic_redraw: Optional[Callable] = None
self.partials_load_and_redraw: List[Callable] = []
self.num_previews: int = num_previews
self.idx_preview: int = 0
self.img_downloading: Optional[bpy.types.Image] = None
self.img_error: Optional[bpy.types.Image] = None
self.imgs_preview: Dict[int, bpy.types.Image] = {}
self.region_popup: Optional[bpy.types.Region] = None
self.open_retries: int = 3
self.popup_closed: bool = False # new state, popup about to open
self.load_dummy_images()
self.init_preview_image_dict(num_previews)
def __del__(self):
self.cleanup_images()
self.popup_closed = True
def load_dummy_images(self) -> None:
"""Loads the dummy images for 'downloading' and 'download error'."""
theme = bpy.context.preferences.themes[0]
color_bg = theme.user_interface.wcol_menu_back.inner
if ".POLIIGON_PREVIEW_downloading" not in bpy.data.images:
path = os.path.join(cTB.dir_script, "get_preview_600px.png")
self.img_downloading = load_image(
"POLIIGON_PREVIEW_downloading",
path,
do_remove_alpha=False,
color_bg=color_bg)
# We do not want this image saved into the blend file
self.img_downloading.user_clear()
name_downloading = f".POLIIGON_PREVIEW_downloading_{self.ts:.4f}"
name_error = f".POLIIGON_PREVIEW_error_{self.ts:.4f}"
if ".POLIIGON_PREVIEW_error" not in bpy.data.images:
path = os.path.join(cTB.dir_script, "icon_nopreview_600px.png")
self.img_error = load_image(
"POLIIGON_PREVIEW_error",
path,
do_remove_alpha=False,
color_bg=color_bg)
# We do not want this image saved into the blend file
self.img_error.user_clear()
path = os.path.join(cTB.dir_script, "get_preview_600px.png")
self.img_downloading = load_image(
name_downloading,
path,
do_remove_alpha=False,
color_bg=color_bg)
# We do not want this image saved into the blend file
self.img_downloading.user_clear()
path = os.path.join(cTB.dir_script, "icon_nopreview_600px.png")
self.img_error = load_image(
name_error,
path,
do_remove_alpha=False,
color_bg=color_bg)
# We do not want this image saved into the blend file
self.img_error.user_clear()
def init_preview_image_dict(self, num_previews: int) -> None:
"""Prepares the image dictionary with 'downloading dummies'."""
@@ -127,35 +147,49 @@ class DetailViewState():
def cleanup_images(self) -> None:
"""Removes all images from blend data."""
name_downloading = f".POLIIGON_PREVIEW_downloading_{self.ts:.4f}"
name_error = f".POLIIGON_PREVIEW_error_{self.ts:.4f}"
for _img in self.imgs_preview.values():
if _img.name in [".POLIIGON_PREVIEW_downloading",
".POLIIGON_PREVIEW_error"]:
if _img.name in [name_downloading,
name_error]:
# Skip any dummy images (thumbs with error or not yet
# downloaded), the actual dummy images get free'd below.
continue
_img.user_clear()
bpy.data.images.remove(_img)
self.imgs_preview = {}
if self.img_error is not None and ".POLIIGON_PREVIEW_error" in bpy.data.images:
if self.img_error is not None and name_error in bpy.data.images:
self.img_error.user_clear()
bpy.data.images.remove(self.img_error)
if self.img_downloading is not None and ".POLIIGON_PREVIEW_downloading" in bpy.data.images:
self.img_error = None
if self.img_downloading is not None and name_downloading in bpy.data.images:
self.img_downloading.user_clear()
bpy.data.images.remove(self.img_downloading)
self.img_downloading = None
def next_index(self) -> None:
"""Select the next preview image."""
self.idx_preview = (self.idx_preview + 1) % self.num_previews
def previous_index(self) -> None:
"""Select the previous preview image."""
self.idx_preview = (self.idx_preview - 1) % self.num_previews
def set_index(self, idx_preview: int) -> None:
"""Select a specific preview image by index."""
self.idx_preview = idx_preview
# Global state, only valid between opening and closing the popup.
g_state: Optional[DetailViewState] = None
g_open_request_s: Optional[float] = None # stores a monotonic timestamp
# Global state(s), states found in here are only valid between opening
# and closing the popup.
# Definition is down here for the type hinting purposes.
g_states: Dict[int, DetailViewState] = {}
class POLIIGON_OT_detail_view_select(Operator):
@@ -165,6 +199,7 @@ class POLIIGON_OT_detail_view_select(Operator):
bl_options = {"INTERNAL"}
select_preview: IntProperty(min=MODE_SELECT.PREVIOUS, options={"HIDDEN"}) # noqa: F821
ts: FloatProperty(options={"HIDDEN"}) # noqa: F821
@staticmethod
def init_context(addon_version: str) -> None:
@@ -180,7 +215,7 @@ class POLIIGON_OT_detail_view_select(Operator):
return _t("Next Preview")
elif select_preview == MODE_SELECT.PREVIOUS:
return _t("Previous Preview")
elif 0 <= select_preview < MODE_SELECT.NEXT:
elif 0 <= select_preview < MAX_NUM_PREVIEWS:
return _t("Select Preview #{0}").format(select_preview)
else:
# Not reported, consequences are rather minimal
@@ -191,14 +226,18 @@ class POLIIGON_OT_detail_view_select(Operator):
@reporting.handle_operator()
def execute(self, context):
global g_state
global g_states
state = g_states.get(self.ts, None)
if state is None:
return {'CANCELLED'}
if self.select_preview == MODE_SELECT.NEXT:
g_state.next_index()
state.next_index()
elif self.select_preview == MODE_SELECT.PREVIOUS:
g_state.previous_index()
elif 0 <= self.select_preview < 10000:
g_state.set_index(self.select_preview)
state.previous_index()
elif 0 <= self.select_preview < MAX_NUM_PREVIEWS:
state.set_index(self.select_preview)
else:
# Not reported, consequences are rather minimal
cTB.logger.error(
@@ -208,83 +247,15 @@ class POLIIGON_OT_detail_view_select(Operator):
return {'FINISHED'}
def _start_timer_load_and_redraw(
idx_preview: int,
path_preview: str,
do_load_image: bool = True
) -> None:
"""Starts a one-shot timer, which will (optionally) load a preview
image and afterwards tag the popup for redraw.
Reason is, API RC's done callback (see _callback_thumb_done()) is
running in threaded context and we can not reliably load an image into
a Blender data block, if not on main thread.
"""
global g_state
partial_load_and_redraw = partial(
t_load_and_redraw_preview,
region_popup=g_state.region_popup,
dict_imgs=g_state.imgs_preview,
idx_preview=idx_preview,
path_preview=path_preview,
img_error=g_state.img_error,
do_load_image=do_load_image
)
bpy.app.timers.register(
partial_load_and_redraw, first_interval=0, persistent=False)
def load_thumb_image(
idx_preview: int,
path_preview: str,
img_error: bpy.types.Image
) -> bpy.types.Image:
"""Loads in a preview image, returning the error dummy on failure."""
theme = bpy.context.preferences.themes[0]
color_bg = theme.user_interface.wcol_menu_back.inner
img_preview = load_image(
f"POLIIGON_PREVIEW_{idx_preview}",
path_preview,
# With some (NOT all) preview images, setting colorspace fails
do_set_colorspace=False,
# Index 0 preview (identical to the thumbnail image) comes with an
# alpha channel, confusing our template_icon widget. So, we'll replace
# the transparent parts with a new background color.
do_remove_alpha=idx_preview == 0,
color_bg=color_bg,
force=True)
if img_preview is None:
img_preview = img_error
return img_preview
class POLIIGON_OT_detail_view(Operator):
bl_idname = "poliigon.detail_view"
bl_label = _t("Asset Details")
bl_description = _t("View larger thumbnails and asset details")
bl_options = {"INTERNAL"}
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
def __del__(self):
global g_state
# Docs say, there would be super().__del__(), but seems, there is not.
# super().__del__()
if bpy.app.timers.is_registered(t_periodic_redraw):
bpy.app.timers.unregister(t_periodic_redraw)
if g_state is None:
return
g_state.cleanup_images()
g_state.popup_closed = True
resolution_dl: IntProperty(min=300, default=600, options={"HIDDEN"}) # noqa: F821
ts: FloatProperty(options={"HIDDEN"}) # noqa: F821
@staticmethod
def init_context(addon_version: str) -> None:
@@ -299,9 +270,7 @@ class POLIIGON_OT_detail_view(Operator):
@reporting.handle_invoke()
def invoke(self, context, event):
global g_open_request_s
g_open_request_s = None
global g_states
self.asset_data = cTB._asset_index.get_asset(self.asset_id)
if self.asset_data is None:
@@ -311,22 +280,191 @@ class POLIIGON_OT_detail_view(Operator):
reporting.capture_message("detail-view-no-asset-2", msg, "error")
return {'CANCELLED'}
cTB.track_screen("large_preview")
num_previews = len(self.asset_data.cloudflare_thumb_urls)
return context.window_manager.invoke_props_dialog(
self, width=450)
self.ts = time.monotonic()
state = DetailViewState(num_previews, self.ts, context)
partial_periodic_redraw = partial(
t_periodic_redraw,
ts=self.ts,
asset_data=self.asset_data)
state.partial_periodic_redraw = partial_periodic_redraw
g_states[self.ts] = state
self._get_all_thumbs(num_previews)
cTB.signal_view_screen("large_preview")
return context.window_manager.invoke_props_dialog(self, width=450)
def _start_timer_periodic_redraw(self, asset_data: AssetData) -> None:
"""Starts a timer to periodically upate/redraw the popup.
For longer running actions like asset download, we want regular popup
updates, so it can display progress bars and switch to different
buttons after the action has finished. Therefore we start a timer,
which will do the trick and which will auto-disarm itself, once the
asset's state signals no more ongoing actions in background.
"""
global g_states
state = g_states.get(self.ts, None)
if state is None:
return
if bpy.app.timers.is_registered(state.partial_periodic_redraw):
return
bpy.app.timers.register(
state.partial_periodic_redraw,
first_interval=0,
persistent=False)
def _callback_thumb_done(self, event: Event, job: ApiJob) -> None:
"""DCC specific finalization of 'download thumb' job."""
event.set()
def _load_thumb_image(
self,
idx_preview: int,
path_preview: str,
img_error: bpy.types.Image
) -> bpy.types.Image:
"""Loads in a preview image, returning the img_error upon failure."""
theme = bpy.context.preferences.themes[0]
color_bg = theme.user_interface.wcol_menu_back.inner
img_preview = load_image(
f"POLIIGON_PREVIEW_{self.ts:.4f}_{idx_preview}",
path_preview,
# With some (NOT all) preview images, setting colorspace fails
do_set_colorspace=False,
# Index 0 preview (identical to the thumbnail image) comes with an
# alpha channel, confusing our template_icon widget. So, we'll replace
# the transparent parts with a new background color.
do_remove_alpha=idx_preview == 0,
color_bg=color_bg,
force=True)
if img_preview is None:
img_preview = img_error
return img_preview
def _get_thumb(
self,
state: DetailViewState,
events_download: List[Event],
idx_preview: Optional[int] = None,
) -> None:
"""Either loads a large preview (if file exists) or
starts a job to download it.
"""
if idx_preview is None:
idx_preview = state.idx_preview
path_preview, url_preview = cTB._asset_index.get_cf_thumbnail_info(
self.asset_id, self.resolution_dl, idx_preview)
if path_preview is None:
msg = (
f"Asset ID {self.asset_id}: "
f"Encountered preview index {idx_preview} returning no path")
cTB.logger.warning(msg)
reporting.capture_message("detail-view-thumb-index", msg, "error")
state.set_error_image(idx_preview)
return
if os.path.isfile(path_preview):
img_preview = self._load_thumb_image(
idx_preview, path_preview, state.img_error)
img_preview.user_clear()
state.set_image(idx_preview, img_preview)
else:
ev_download = Event()
events_download.append((idx_preview, path_preview, ev_download))
partial_callback_done = partial(
self._callback_thumb_done,
event=ev_download)
cTB.api_rc.add_job_download_thumb(
asset_id=self.asset_id,
url=url_preview,
path=path_preview,
idx_thumb=idx_preview,
callback_done=partial_callback_done
)
def _await_all_thumb_downloads(
self,
state: DetailViewState,
events_download: List[Event]
) -> None:
"""Waits dor all events in events_download and loads thumbs
which finished download.
"""
for _idx_preview, _path_preview, _ev_download in events_download:
is_event_set = _ev_download.wait(TIMEOUT_THUMB_DOWNLOAD)
if not is_event_set:
msg = (f"Thumb download timeout ({self.asset_id}, "
f"{_idx_preview})!")
reporting.capture_message(
"detail-view-thumb-timeout", msg, "error")
# Continue nevertheless, if the file appeared on disc we are
# still fine
if os.path.isfile(_path_preview):
img_preview = self._load_thumb_image(
_idx_preview, _path_preview, state.img_error)
img_preview.user_clear()
state.imgs_preview[_idx_preview] = img_preview
else:
state.set_error_image(_idx_preview)
def _get_all_thumbs(self, num_previews: int) -> None:
"""Requests all large previews for the asset."""
global g_state
state = g_states.get(self.ts, None)
if state is None:
return
events_download = []
for _idx_preview in range(num_previews):
self._get_thumb(state, events_download, _idx_preview)
self._await_all_thumb_downloads(state, events_download)
def _add_preview_image(
self, layout: bpy.types.UILayout, idx_selected: int) -> None:
"""Adds the actual preview image to the popup dialog."""
global g_state
global g_states
row_image = layout.row()
col_image = row_image.column()
col_image.scale_y = 1.125
state = g_states.get(self.ts, None)
if state is None:
return
imgs_preview = state.imgs_preview
if idx_selected in imgs_preview and imgs_preview[idx_selected] is not None:
id_icon = imgs_preview[idx_selected].preview.icon_id
else:
id_icon = state.img_error.preview.icon_id
col_image.template_icon(
icon_value=g_state.imgs_preview[idx_selected].preview.icon_id,
icon_value=id_icon,
scale=18.0)
def _add_select_previous_button(self, layout: bpy.types.UILayout) -> None:
@@ -342,6 +480,7 @@ class POLIIGON_OT_detail_view(Operator):
icon="TRIA_LEFT",
emboss=False)
op.select_preview = int(MODE_SELECT.PREVIOUS)
op.ts = self.ts
def _add_select_next_button(self, layout: bpy.types.UILayout) -> None:
"""Adds the button to select the next preview image to the popup
@@ -356,6 +495,7 @@ class POLIIGON_OT_detail_view(Operator):
icon="TRIA_RIGHT",
emboss=False)
op.select_preview = int(MODE_SELECT.NEXT)
op.ts = self.ts
def _add_select_index_buttons(
self, layout: bpy.types.UILayout, idx_selected: int) -> None:
@@ -363,9 +503,12 @@ class POLIIGON_OT_detail_view(Operator):
dialog.
"""
global g_state
global g_states
for _idx in range(g_state.num_previews):
state = g_states.get(self.ts, None)
if state is None:
return
for _idx in range(state.num_previews):
if _idx == idx_selected:
icon = "RADIOBUT_ON"
else:
@@ -377,6 +520,7 @@ class POLIIGON_OT_detail_view(Operator):
emboss=False,
depress=_idx == idx_selected)
op.select_preview = _idx
op.ts = self.ts
def _add_selection_buttons(
self, layout: bpy.types.UILayout, idx_selected: int) -> None:
@@ -516,7 +660,8 @@ class POLIIGON_OT_detail_view(Operator):
error=None,
size_default=size_default)
have_quickmenu = is_downloaded or check_convention(asset_data, is_local)
have_quickmenu = is_downloaded or check_convention(
asset_data, is_local)
if have_quickmenu and not is_in_progress:
_draw_button_quick_menu(
row_main_action, asset_data, hide_detail_view=True)
@@ -603,48 +748,62 @@ class POLIIGON_OT_detail_view(Operator):
def draw(self, context):
global g_state
g_state.region_popup = context.region_popup
state = g_states.get(self.ts, None)
if state is None:
return
state.context = context
state.region_popup = context.region_popup
col_content = self.layout.column()
self._add_preview_image(col_content, g_state.idx_preview)
self._add_selection_buttons(col_content, g_state.idx_preview)
self._add_preview_image(col_content, state.idx_preview)
self._add_selection_buttons(col_content, state.idx_preview)
col_content.separator()
self._add_meta_data_section(col_content)
def _cleanup(self) -> None:
"""Cleans up behind ourselves. When the popup closes, we need to get
rid of any timers, we registered and all images we loaded.
"""
global g_states
state = g_states.get(self.ts, None)
if state is None:
return
if bpy.app.timers.is_registered(state.partial_periodic_redraw):
bpy.app.timers.unregister(state.partial_periodic_redraw)
for _partial in state.partials_load_and_redraw:
if bpy.app.timers.is_registered(_partial):
bpy.app.timers.unregister(_partial)
# Important, so state instances get actuelly free'd
self.partial_periodic_redraw = None
self.partials_load_and_redraw = []
del g_states[self.ts]
@reporting.handle_operator()
def execute(self, context):
"""Nothing to do, here."""
"""The real action happens in invoke() and draw(), execute() only runs
after user clicks 'Ok' button, then we need to clean up here.
"""
self._cleanup()
return {'FINISHED'}
def cancel(self, context):
"""Upon user clicking 'Cancel' or popup loosing focus we'll cleanup
here.
"""
def t_load_and_redraw_preview(
region_popup: Optional[bpy.types.Region],
dict_imgs: Dict[int, bpy.types.Image],
idx_preview: int,
path_preview: str,
img_error: bpy.types.Image,
do_load_image: bool = False
) -> Optional[float]:
"""One-shot timer function to redraw/update the popup dialog.
Optionally loads in a freshly downloaded preview image.
"""
if do_load_image:
img_preview = load_thumb_image(idx_preview, path_preview, img_error)
dict_imgs[idx_preview] = img_preview
if region_popup is not None:
# region_popup.tag_redraw() does not seem to do the trick.
# Not sure, what it is actually supposed to do?
# Luckily tag_refresh_ui() works for our pourposes.
region_popup.tag_refresh_ui()
return None
self._cleanup()
@persistent
def t_periodic_redraw(
region_popup: Optional[bpy.types.Region],
ts: float,
asset_data: AssetData
) -> Optional[float]:
"""Timer function to redraw/update the popup dialog.
@@ -654,198 +813,21 @@ def t_periodic_redraw(
example 'downloading'.
"""
global g_states
state = g_states.get(ts, None)
if state is None:
return None
region_popup = state.region_popup
next_update_s = None # Auto-disarm, if none of the states below
is_purchasing = asset_data.state.purchase.is_in_progress()
is_downloading = asset_data.state.dl.is_in_progress()
is_cancelling = asset_data.state.dl.is_cancelled()
if is_purchasing or is_downloading or is_cancelling:
next_update_s = 0.250
if region_popup is not None:
has_region_popup = region_popup is not None
if has_region_popup and region_popup.type == "TEMPORARY":
region_popup.tag_refresh_ui()
return next_update_s
def t_open_detail_view(asset_id: int) -> Optional[float]:
"""Opens our asset detail view popup.
Called on by blender timer handlers to allow execution on main thread.
The returned value signifies how long until the next execution.
"""
global g_state
if bpy.context.window_manager.is_interface_locked:
if g_state.open_retries > 0:
msg = ("UI locked upon opening Detail View, will retry "
f"{g_state.open_retries} times.")
cTB.logger.warning(msg)
g_state.open_retries -= 1
return 0.05
else:
msg = "Gave up opening Detail View after three retries."
cTB.logger.critical(msg)
reporting.capture_message("dateil-view-ui-locked", msg, "error")
return None
bpy.ops.poliigon.detail_view("INVOKE_DEFAULT", asset_id=asset_id)
return None # Auto disarm, one-shot timer
class POLIIGON_OT_detail_view_open(Operator):
"""Helper operator to allow opening the detail view popup from quickmenu
(by calling the actual detail view operator from a one-shot timer on main
thread).
"""
bl_idname = "poliigon.detail_view_open"
bl_label = _t("Open Asset Details")
bl_description = _t("Opens the detail view with larger thumbnails and "
"asset details")
bl_options = {"INTERNAL"}
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
resolution_dl: IntProperty(min=300, default=600, options={"HIDDEN"}) # noqa: F821
@staticmethod
def init_context(addon_version: str) -> None:
"""Called from operators.py to init global addon context."""
global cTB
cTB = get_context(addon_version)
def _start_timer_periodic_redraw(self, asset_data: AssetData) -> None:
"""Starts a timer to periodically upate/redraw the popup.
For longer running actions like asset download, we want regular popup
updates, so it can display progress bars and switch to different
buttons after the action has finished. Therefore we start a timer,
which will do the trick and which will auto-disarm itself, once the
asset's state signals no more ongoing actions in background.
"""
global g_state
if bpy.app.timers.is_registered(t_periodic_redraw):
return
partial_periodic_redraw = partial(
t_periodic_redraw,
region_popup=g_state.region_popup,
asset_data=asset_data)
bpy.app.timers.register(
partial_periodic_redraw,
first_interval=0,
persistent=False)
def _callback_thumb_done(self, job: ApiJob) -> None:
"""DCC specific finalization of 'download thumb' job."""
global g_state
if g_state.popup_closed:
return
idx_preview = job.params.idx_thumb
path_preview = job.params.path
if not os.path.isfile(path_preview):
g_state.set_error_image(idx_preview)
do_load_image = False
else:
do_load_image = True
_start_timer_load_and_redraw(
idx_preview, path_preview, do_load_image)
def _get_thumb(self, idx_preview: Optional[int] = None) -> None:
"""Either loads a large preview (if file exists) or
starts a job to download it.
"""
global g_state
if idx_preview is None:
idx_preview = g_state.idx_preview
path_preview, url_preview = cTB._asset_index.get_cf_thumbnail_info(
self.asset_id, self.resolution_dl, idx_preview)
if path_preview is None:
msg = (
f"Asset ID {self.asset_id}: "
f"Encountered preview index {idx_preview} returning no path")
cTB.logger.warning(msg)
reporting.capture_message("detail-view-thumb-index", msg, "error")
g_state.set_error_image(idx_preview)
return
if os.path.isfile(path_preview):
img_preview = load_thumb_image(
idx_preview, path_preview, g_state.img_error)
img_preview.user_clear()
g_state.set_image(idx_preview, img_preview)
else:
cTB.api_rc.add_job_download_thumb(
asset_id=self.asset_id,
url=url_preview,
path=path_preview,
idx_thumb=idx_preview,
callback_done=self._callback_thumb_done,
)
def _get_all_thumbs(self, num_previews: int) -> None:
"""Requests all large previews for the asset."""
for _idx_preview in range(num_previews):
self._get_thumb(_idx_preview)
@classmethod
def description(cls, context, properties):
return cls.bl_description
@reporting.handle_operator()
def execute(self, context):
"""Registers a one shot timer, which then in turn calls the detail view
operator.
"""
global g_state
global g_open_request_s
asset_data = cTB._asset_index.get_asset(self.asset_id)
if asset_data is None:
msg = (f"Asset ID {self.asset_id} not found in AssetIndex before "
"opening Detail View")
cTB.logger.error(msg)
reporting.capture_message("detail-view-no-asset-1", msg, "error")
return {'CANCELLED'}
g_open_request_s = monotonic()
num_previews = len(asset_data.cloudflare_thumb_urls)
g_state = DetailViewState(num_previews)
partial_open_detail_view = partial(
t_open_detail_view, asset_id=self.asset_id)
bpy.app.timers.register(
partial_open_detail_view, first_interval=0, persistent=False)
self._get_all_thumbs(g_state.num_previews)
return {'FINISHED'}
def check_and_report_detail_view_not_opening() -> None:
global g_open_request_s
if g_open_request_s is None:
return
diff = monotonic() - g_open_request_s
if diff < 1.0:
return
msg = (f"Detail Viewer did not open (since {diff:.03} s)")
cTB.logger.error(msg)
reporting.capture_message("detail-view-not-opened", msg, "error")
g_open_request_s = None
@@ -17,6 +17,7 @@
# ##### END GPL LICENSE BLOCK #####
from threading import Event
from typing import Optional
import bpy
from bpy.props import (
@@ -63,7 +64,7 @@ class POLIIGON_OT_download(Operator):
cTB.callback_asset_update_ui(job)
self.event_sync.set()
def create_auto_download_job(self, asset_data: AssetData) -> ApiJob:
def create_auto_download_job(self, asset_data: AssetData) -> Optional[ApiJob]:
"""Create the follow up download job to attach to purchase job."""
# NOTE: Free assets are implicitly always "auto-download"
@@ -161,6 +162,8 @@ class POLIIGON_OT_download(Operator):
callback_progress=cTB.callback_asset_update_ui,
callback_done=callback_done
)
bpy.ops.poliigon.popup_first_download("INVOKE_DEFAULT")
elif self.mode == "purchase":
cTB.logger.debug("POLIIGON_OT_download Purchase asset "
f"{self.asset_id}")
@@ -170,12 +173,6 @@ class POLIIGON_OT_download(Operator):
area = cTB.settings["area"]
search = cTB.vSearch[area]
one_click_purchase = cTB.settings["one_click_purchase"]
user_unlimited = cTB.is_unlimited_user()
credits = 0 if asset_data.credits is None else asset_data.credits
if credits > 0 and one_click_purchase and not user_unlimited:
bpy.ops.poliigon.popup_first_download("INVOKE_DEFAULT")
if self.do_synchronous and job_download is None:
self.event_sync = Event()
callback_done = self._callback_done_sync
@@ -184,12 +181,15 @@ class POLIIGON_OT_download(Operator):
cTB.api_rc.add_job_purchase_asset(
asset_data,
cTB.settings["category"][area],
cTB.settings["category"],
search,
job_download=job_download,
callback_done=callback_done,
force=True
)
one_click_purchase = cTB.settings["one_click_purchase"]
if one_click_purchase:
bpy.ops.poliigon.popup_first_download("INVOKE_DEFAULT")
if self.do_synchronous:
self.event_sync.wait(30.0)
@@ -144,6 +144,15 @@ class POLIIGON_OT_hdri(Operator):
"""Runs once per operator call before drawing occurs."""
super().__init__(*args, **kwargs)
self.exec_count = 0
self.node_output_world = None
self.node_tex_coord = None
self.node_mapping = None
self.node_tex_env_light = None
self.node_background_light = None
self.node_tex_env_bg = None
self.node_background_bg = None
self.node_mix_shader = None
self.node_light_path = None
@staticmethod
def init_context(addon_version: str) -> None:
@@ -194,9 +203,6 @@ class POLIIGON_OT_hdri(Operator):
f"({self.size_bg}), expected '4K_JPG' or '1K_EXR'")
raise ValueError(msg)
if self.size == size_bg_eff:
name_bg = name_light
cTB.logger.debug("POLIIGON_OT_hdri "
f"{asset_name}, {name_light}, {name_bg}")
@@ -267,7 +273,7 @@ class POLIIGON_OT_hdri(Operator):
return {"CANCELLED"}
file_tex_light = files_tex_exr[0]
if cTB.settings["hdri_use_jpg_bg"] and filetype_bg == "JPG":
if filetype_bg == "JPG":
size_bg = asset_type_data.get_size(
size_bg_eff,
local_only=True,
@@ -314,170 +320,23 @@ class POLIIGON_OT_hdri(Operator):
# Reset apply for Redo Last menu to work properly
self.do_apply = False
# Check if same texture is used for both lighting and background
use_simple_layout = file_tex_bg == file_tex_light
# Use same name for both light and background only when using same file (simple layout)
if use_simple_layout:
name_bg = name_light
# ...............................................................................................
node_tex_coord = None
node_mapping = None
node_tex_env_light = None
node_background_light = None
node_tex_env_bg = None
node_background_bg = None
node_mix_shader = None
node_light_path = None
node_output_world = None
if not bpy.context.scene.world:
bpy.ops.world.new()
bpy.context.scene.world = bpy.data.worlds[-1]
context.scene.world.use_nodes = True
nodes_world = context.scene.world.node_tree.nodes
links_world = context.scene.world.node_tree.links
for _node in nodes_world:
if _node.type == "TEX_COORD":
if _node.label == "Mapping":
node_tex_coord = _node
elif _node.type == "MAPPING":
if _node.label == "Mapping":
node_mapping = _node
elif _node.type == "TEX_ENVIRONMENT":
if _node.label == "Lighting":
node_tex_env_light = _node
elif _node.label == "Background":
node_tex_env_bg = _node
elif _node.type == "BACKGROUND":
if _node.label == "Lighting":
node_background_light = _node
elif _node.label == "Background":
node_background_bg = _node
elif len(nodes_world) == 2:
node_background_light = _node
node_background_light.label = "Lighting"
node_background_light.location = mathutils.Vector(
(-110, 200))
elif _node.type == "MIX_SHADER":
node_mix_shader = _node
elif _node.type == "LIGHT_PATH":
node_light_path = _node
elif _node.type == "OUTPUT_WORLD":
node_output_world = _node
if node_tex_coord is None:
node_tex_coord = nodes_world.new("ShaderNodeTexCoord")
node_tex_coord.label = "Mapping"
node_tex_coord.location = mathutils.Vector((-1080, 420))
if node_mapping is None:
node_mapping = nodes_world.new("ShaderNodeMapping")
node_mapping.label = "Mapping"
node_mapping.location = mathutils.Vector((-870, 420))
if node_tex_env_light is None:
node_tex_env_light = nodes_world.new("ShaderNodeTexEnvironment")
node_tex_env_light.label = "Lighting"
node_tex_env_light.location = mathutils.Vector((-470, 420))
if node_tex_env_bg is None:
node_tex_env_bg = nodes_world.new("ShaderNodeTexEnvironment")
node_tex_env_bg.label = "Background"
node_tex_env_bg.location = mathutils.Vector((-470, 100))
if node_background_light is None:
node_background_light = nodes_world.new("ShaderNodeBackground")
node_background_light.label = "Lighting"
node_background_light.location = mathutils.Vector((-110, 200))
if node_background_bg is None:
node_background_bg = nodes_world.new("ShaderNodeBackground")
node_background_bg.label = "Background"
node_background_bg.location = mathutils.Vector((-110, 70))
if node_mix_shader is None:
node_mix_shader = nodes_world.new("ShaderNodeMixShader")
node_mix_shader.location = mathutils.Vector((110, 300))
if node_light_path is None:
node_light_path = nodes_world.new("ShaderNodeLightPath")
node_light_path.location = mathutils.Vector((-110, 550))
if node_output_world is None:
node_output_world = nodes_world.new("ShaderNodeOutputWorld")
node_output_world.location = mathutils.Vector((370, 300))
links_world.new(
node_tex_coord.outputs["Generated"],
node_mapping.inputs["Vector"])
links_world.new(
node_mapping.outputs["Vector"],
node_tex_env_light.inputs["Vector"])
links_world.new(
node_tex_env_light.outputs["Color"],
node_background_light.inputs["Color"])
links_world.new(
node_background_light.outputs[0],
node_mix_shader.inputs[1])
links_world.new(
node_tex_coord.outputs["Generated"],
node_mapping.inputs["Vector"])
links_world.new(
node_mapping.outputs["Vector"],
node_tex_env_bg.inputs["Vector"])
links_world.new(
node_tex_env_bg.outputs["Color"],
node_background_bg.inputs["Color"])
links_world.new(
node_background_bg.outputs[0],
node_mix_shader.inputs[2])
links_world.new(
node_light_path.outputs[0],
node_mix_shader.inputs[0])
links_world.new(
node_mix_shader.outputs[0],
node_output_world.inputs[0])
if name_light in bpy.data.images.keys():
img_light = bpy.data.images[name_light]
if use_simple_layout:
self._simple_layout(name_light,
file_tex_light,
asset_data)
else:
file_tex_light_norm = os.path.normpath(file_tex_light)
img_light = bpy.data.images.load(file_tex_light_norm)
img_light.name = name_light
img_light.poliigon = "HDRIs;" + asset_name
self.set_poliigon_props_image(img_light, asset_data)
if name_bg in bpy.data.images.keys():
img_bg = bpy.data.images[name_bg]
else:
file_tex_bg_norm = os.path.normpath(file_tex_bg)
img_bg = bpy.data.images.load(file_tex_bg_norm)
img_bg.name = name_bg
self.set_poliigon_props_image(img_bg, asset_data)
if "Rotation" in node_mapping.inputs:
node_mapping.inputs["Rotation"].default_value[2] = self.rotation
else:
node_mapping.rotation[2] = self.rotation
node_tex_env_light.image = img_light
node_background_light.inputs["Strength"].default_value = self.hdr_strength
node_tex_env_bg.image = img_bg
node_background_bg.inputs["Strength"].default_value = self.hdr_strength
self._complex_layout(name_light,
file_tex_light,
name_bg,
file_tex_bg,
asset_data)
self.set_poliigon_props_world(context, asset_data)
@@ -485,10 +344,391 @@ class POLIIGON_OT_hdri(Operator):
if self.exec_count == 0:
cTB.signal_import_asset(asset_id=self.asset_id)
cTB.set_first_local_asset()
self.exec_count += 1
self.report({"INFO"}, _t("HDRI Imported : {0}").format(asset_name))
return {"FINISHED"}
def _get_poliigon_hdri_nodes(self, nodes_world) -> dict:
"""Identify existing Poliigon HDRI nodes by their labels and types.
Returns a dictionary mapping node roles to existing nodes, or None if not found.
This allows us to preserve user-authored nodes while only replacing Poliigon ones.
"""
poliigon_nodes = {
'tex_coord': None,
'mapping': None,
'tex_env_light': None,
'tex_env_bg': None,
'background_light': None,
'background_bg': None,
'mix_shader': None,
'light_path': None,
'output_world': None
}
for node in nodes_world:
try:
# Skip invalid nodes
if not hasattr(node, 'type') or not hasattr(node, 'label'):
continue
if node.type == "TEX_COORD" and node.label == "Mapping":
poliigon_nodes['tex_coord'] = node
elif node.type == "MAPPING" and node.label == "Mapping":
poliigon_nodes['mapping'] = node
elif node.type == "TEX_ENVIRONMENT":
if node.label == "Lighting":
poliigon_nodes['tex_env_light'] = node
elif node.label == "Background":
poliigon_nodes['tex_env_bg'] = node
elif node.type == "BACKGROUND":
if node.label == "Lighting":
poliigon_nodes['background_light'] = node
elif node.label == "Background":
poliigon_nodes['background_bg'] = node
elif poliigon_nodes['background_light'] is None:
# If background_light isn't assigned yet, assign the
# first one we identify. This avoids clutter in the
# default blender world layout by reusing both nodes.
poliigon_nodes['background_light'] = node
elif node.type == "MIX_SHADER":
# Mix shader nodes created by Poliigon typically don't have custom labels
# but we only want to replace if it's connected to our setup
if self._is_poliigon_mix_shader(node):
poliigon_nodes['mix_shader'] = node
elif node.type == "LIGHT_PATH":
# Similar to mix shader - identify by connection pattern
if self._is_poliigon_light_path(node):
poliigon_nodes['light_path'] = node
elif node.type == "OUTPUT_WORLD":
poliigon_nodes['output_world'] = node
except Exception as e:
# Skip problematic nodes instead of crashing
cTB.logger.warning(f"Skipping problematic node during identification: {str(e)}")
continue
return poliigon_nodes
def _is_poliigon_mix_shader(self, node) -> bool:
"""Check if a MixShader node is part of the Poliigon HDRI setup."""
try:
# Validate node has required attributes
if not hasattr(node, 'inputs') or len(node.inputs) < 3:
return False
# Check if it's connected to background nodes with Poliigon labels
input_1 = node.inputs[1]
input_2 = node.inputs[2]
if hasattr(input_1, 'is_linked') and input_1.is_linked:
if hasattr(input_1, 'links') and input_1.links:
linked_node = input_1.links[0].from_node
if (hasattr(linked_node, 'type') and linked_node.type == "BACKGROUND" and
hasattr(linked_node, 'label') and linked_node.label == "Lighting"):
return True
if hasattr(input_2, 'is_linked') and input_2.is_linked:
if hasattr(input_2, 'links') and input_2.links:
linked_node = input_2.links[0].from_node
if (hasattr(linked_node, 'type') and linked_node.type == "BACKGROUND" and
hasattr(linked_node, 'label') and linked_node.label == "Background"):
return True
except Exception:
pass
return False
def _is_poliigon_light_path(self, node) -> bool:
"""Check if a LightPath node is part of the Poliigon HDRI setup."""
try:
# Validate node has required attributes
if not hasattr(node, 'outputs'):
return False
# Check if it's connected to a mix shader that's part of Poliigon setup
for output in node.outputs:
if hasattr(output, 'is_linked') and output.is_linked:
if hasattr(output, 'links'):
for link in output.links:
if hasattr(link, 'to_node'):
linked_node = link.to_node
if (hasattr(linked_node, 'type') and linked_node.type == "MIX_SHADER" and
self._is_poliigon_mix_shader(linked_node)):
return True
except Exception:
pass
return False
def _remove_poliigon_nodes(self, nodes_world, poliigon_nodes: dict) -> None:
"""Safely remove only the identified Poliigon HDRI nodes."""
nodes_to_remove = []
for node_role, node in poliigon_nodes.items():
if node is not None and node_role != 'output_world': # Never remove output_world
nodes_to_remove.append(node)
# Remove nodes in a separate loop to avoid modifying collection during iteration
for node in nodes_to_remove:
try:
# Additional validation before removal
if hasattr(node, 'name') and node.name in nodes_world:
nodes_world.remove(node)
except Exception as e:
cTB.logger.warning(f"Failed to remove node {getattr(node, 'name', 'unnamed')}: {str(e)}")
continue
def _create_or_reuse_output_world(self, nodes_world, poliigon_nodes: dict) -> object:
"""Create or reuse the output world node."""
output_world = poliigon_nodes.get('output_world')
# Validate that the existing output world is still valid
if output_world is not None:
try:
# Test if the node is still accessible and valid
_ = output_world.type
_ = output_world.location
return output_world
except Exception:
# Node is corrupted or no longer accessible
cTB.logger.warning("Existing output world node is corrupted, creating new one")
output_world = None
if output_world is None:
# Create new output world node
output_world = nodes_world.new("ShaderNodeOutputWorld")
output_world.location = mathutils.Vector((250, 225.0))
return output_world
def _validate_node_setup(self) -> bool:
"""Validate that all required nodes were created successfully."""
required_nodes = ['node_output_world', 'node_tex_coord', 'node_mapping', 'node_tex_env_light', 'node_background_light']
for node_attr in required_nodes:
if not hasattr(self, node_attr) or getattr(self, node_attr) is None:
cTB.logger.error(f"Required node {node_attr} was not created")
return False
return True
def _simple_layout(self,
name_light: str,
file_tex_light: str,
asset_data: AssetData
) -> None:
if not bpy.context.scene.world:
bpy.ops.world.new()
bpy.context.scene.world = bpy.data.worlds[-1]
bpy.context.scene.world.use_nodes = True
nodes_world = bpy.context.scene.world.node_tree.nodes
links_world = bpy.context.scene.world.node_tree.links
# Use improved selective node management
try:
poliigon_nodes = self._get_poliigon_hdri_nodes(nodes_world)
self._remove_poliigon_nodes(nodes_world, poliigon_nodes)
self.node_output_world = self._create_or_reuse_output_world(nodes_world, poliigon_nodes)
except Exception as e:
cTB.logger.error(f"Failed during node identification/removal: {str(e)}")
# Fallback to more aggressive approach if needed
self.node_output_world = None
for node in nodes_world:
if node.type == "OUTPUT_WORLD":
self.node_output_world = node
break
if self.node_output_world is None:
self.node_output_world = nodes_world.new("ShaderNodeOutputWorld")
self.node_output_world.location = mathutils.Vector((320, 300))
# Create new Poliigon nodes
try:
self.node_tex_coord = nodes_world.new("ShaderNodeTexCoord")
self.node_tex_coord.label = "Mapping"
self.node_tex_coord.location = mathutils.Vector((-720, 300))
self.node_mapping = nodes_world.new("ShaderNodeMapping")
self.node_mapping.label = "Mapping"
self.node_mapping.location = mathutils.Vector((-470, 300))
self.node_tex_env_light = nodes_world.new("ShaderNodeTexEnvironment")
self.node_tex_env_light.label = "Lighting"
self.node_tex_env_light.location = mathutils.Vector((-220, 300))
self.node_background_light = nodes_world.new("ShaderNodeBackground")
self.node_background_light.label = "Lighting"
self.node_background_light.location = mathutils.Vector((120, 300))
# Validate node creation
if not self._validate_node_setup():
raise Exception("Failed to create required nodes")
except Exception as e:
cTB.logger.error(f"Failed to create nodes in simple layout: {str(e)}")
raise
# Create connections with error handling
try:
links_world.new(self.node_tex_coord.outputs["Generated"], self.node_mapping.inputs["Vector"])
links_world.new(self.node_mapping.outputs["Vector"], self.node_tex_env_light.inputs["Vector"])
links_world.new(self.node_tex_env_light.outputs["Color"], self.node_background_light.inputs["Color"])
links_world.new(self.node_background_light.outputs[0], self.node_output_world.inputs["Surface"])
except Exception as e:
cTB.logger.error(f"Failed to create node links in simple layout: {str(e)}")
raise
# Load images
try:
if name_light in bpy.data.images.keys():
img_light = bpy.data.images[name_light]
else:
file_tex_light_norm = os.path.normpath(file_tex_light)
img_light = bpy.data.images.load(file_tex_light_norm)
img_light.name = name_light
img_light.poliigon = "HDRIs;" + asset_data.asset_name
self.set_poliigon_props_image(img_light, asset_data)
except Exception as e:
cTB.logger.error(f"Failed to load images in simple layout: {str(e)}")
raise
# Set node properties
try:
if "Rotation" in self.node_mapping.inputs:
self.node_mapping.inputs["Rotation"].default_value[2] = self.rotation
else:
self.node_mapping.rotation[2] = self.rotation
self.node_tex_env_light.image = img_light
self.node_background_light.inputs["Strength"].default_value = self.hdr_strength
except Exception as e:
cTB.logger.error(f"Failed to set node properties in simple layout: {str(e)}")
raise
def _complex_layout(self,
name_light: str,
file_tex_light: str,
name_bg: str,
file_tex_bg: str,
asset_data: AssetData
) -> None:
if not bpy.context.scene.world:
bpy.ops.world.new()
bpy.context.scene.world = bpy.data.worlds[-1]
bpy.context.scene.world.use_nodes = True
nodes_world = bpy.context.scene.world.node_tree.nodes
links_world = bpy.context.scene.world.node_tree.links
# Use improved selective node management
try:
poliigon_nodes = self._get_poliigon_hdri_nodes(nodes_world)
self._remove_poliigon_nodes(nodes_world, poliigon_nodes)
self.node_output_world = self._create_or_reuse_output_world(nodes_world, poliigon_nodes)
except Exception as e:
cTB.logger.error(f"Failed during node identification/removal: {str(e)}")
# Fallback to more aggressive approach if needed
self.node_output_world = None
for node in nodes_world:
if node.type == "OUTPUT_WORLD":
self.node_output_world = node
break
if self.node_output_world is None:
self.node_output_world = nodes_world.new("ShaderNodeOutputWorld")
self.node_output_world.location = mathutils.Vector((250, 225.0))
# Create all required nodes
try:
self.node_tex_coord = nodes_world.new("ShaderNodeTexCoord")
self.node_tex_coord.label = "Mapping"
self.node_tex_coord.location = mathutils.Vector((-950, 225))
self.node_mapping = nodes_world.new("ShaderNodeMapping")
self.node_mapping.label = "Mapping"
self.node_mapping.location = mathutils.Vector((-750, 225))
self.node_tex_env_light = nodes_world.new("ShaderNodeTexEnvironment")
self.node_tex_env_light.label = "Lighting"
self.node_tex_env_light.location = mathutils.Vector((-550, 350))
self.node_tex_env_bg = nodes_world.new("ShaderNodeTexEnvironment")
self.node_tex_env_bg.label = "Background"
self.node_tex_env_bg.location = mathutils.Vector((-550, 100))
self.node_background_light = nodes_world.new("ShaderNodeBackground")
self.node_background_light.label = "Lighting"
self.node_background_light.location = mathutils.Vector((-250, 100))
self.node_background_bg = nodes_world.new("ShaderNodeBackground")
self.node_background_bg.label = "Background"
self.node_background_bg.location = mathutils.Vector((-250, -50))
self.node_mix_shader = nodes_world.new("ShaderNodeMixShader")
self.node_mix_shader.location = mathutils.Vector((0, 225))
self.node_light_path = nodes_world.new("ShaderNodeLightPath")
self.node_light_path.location = mathutils.Vector((-250, 440))
# Validate critical nodes for complex layout
critical_nodes = ['node_output_world', 'node_tex_coord', 'node_mapping',
'node_tex_env_light', 'node_background_light', 'node_tex_env_bg',
'node_background_bg', 'node_mix_shader', 'node_light_path']
for node_attr in critical_nodes:
if not hasattr(self, node_attr) or getattr(self, node_attr) is None:
raise Exception(f"Required node {node_attr} was not created")
except Exception as e:
cTB.logger.error(f"Failed to create nodes in complex layout: {str(e)}")
raise
# Create all links after ensuring nodes exist
try:
links_world.new(self.node_tex_coord.outputs["Generated"], self.node_mapping.inputs["Vector"])
links_world.new(self.node_mapping.outputs["Vector"], self.node_tex_env_light.inputs["Vector"])
links_world.new(self.node_tex_env_light.outputs["Color"], self.node_background_light.inputs["Color"])
links_world.new(self.node_background_light.outputs[0], self.node_mix_shader.inputs[1])
links_world.new(self.node_mapping.outputs["Vector"], self.node_tex_env_bg.inputs["Vector"])
links_world.new(self.node_tex_env_bg.outputs["Color"], self.node_background_bg.inputs["Color"])
links_world.new(self.node_background_bg.outputs[0], self.node_mix_shader.inputs[2])
links_world.new(self.node_light_path.outputs[0], self.node_mix_shader.inputs[0])
links_world.new(self.node_mix_shader.outputs[0], self.node_output_world.inputs[0])
except Exception as e:
cTB.logger.error(f"Failed to create node links: {str(e)}")
raise
# Load images safely
try:
# Load light image
if name_light in bpy.data.images.keys():
img_light = bpy.data.images[name_light]
else:
file_tex_light_norm = os.path.normpath(file_tex_light)
img_light = bpy.data.images.load(file_tex_light_norm)
img_light.name = name_light
img_light.poliigon = "HDRIs;" + asset_data.asset_name
self.set_poliigon_props_image(img_light, asset_data)
# Load background image
if name_bg in bpy.data.images.keys():
img_bg = bpy.data.images[name_bg]
else:
file_tex_bg_norm = os.path.normpath(file_tex_bg)
img_bg = bpy.data.images.load(file_tex_bg_norm)
img_bg.name = name_bg
self.set_poliigon_props_image(img_bg, asset_data)
# Set node properties safely
if "Rotation" in self.node_mapping.inputs:
self.node_mapping.inputs["Rotation"].default_value[2] = self.rotation
else:
self.node_mapping.rotation[2] = self.rotation
self.node_tex_env_light.image = img_light
self.node_background_light.inputs["Strength"].default_value = self.hdr_strength
self.node_tex_env_bg.image = img_bg
self.node_background_bg.inputs["Strength"].default_value = self.hdr_strength
except Exception as e:
cTB.logger.error(f"Failed to load or setup images: {str(e)}")
raise
def set_poliigon_props_image(
self, img: bpy.types.Image, asset_data: AssetData) -> None:
"""Sets Poliigon property of an imported image."""
@@ -70,7 +70,7 @@ class POLIIGON_OT_library(Operator):
else:
# Update_library, from user preferences
if bpy.app.version >= (3, 0):
if bpy.app.version >= (3, 0) and cTB.user_legacy_own_assets():
create_poliigon_library(force=True)
path_old = cTB.get_library_path(primary=True)
@@ -17,10 +17,12 @@
# ##### END GPL LICENSE BLOCK #####
import webbrowser
import bpy
from bpy.types import Operator
from bpy.props import (
IntProperty,
BoolProperty,
StringProperty)
from ..modules.poliigon_core.multilingual import _t
@@ -45,6 +47,7 @@ class POLIIGON_OT_link(Operator):
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
mode: StringProperty(options={"HIDDEN"}) # noqa: F821
confirm_popup: BoolProperty(options={"HIDDEN"}) # noqa: F821
@staticmethod
def init_context(addon_version: str) -> None:
@@ -61,6 +64,11 @@ class POLIIGON_OT_link(Operator):
def execute(self, context):
global cTB
if self.confirm_popup:
self._confirmation_popup()
self.confirm_popup = False
return {"FINISHED"}
notice = cTB.notify.get_top_notice(do_signal_view=False)
if self.mode.startswith("notify") and notice is not None:
cTB.notify.clicked_notice(notice)
@@ -90,6 +98,9 @@ class POLIIGON_OT_link(Operator):
elif self.mode == "subscribe_banner":
cTB.upgrade_manager.emit_signal(clicked=True)
cTB._api.open_poliigon_link("subscribe", env_name=cTB._env.env_name)
elif self.mode == "signup":
# Open the signup URL but don't update UI or set login_in_progress flag
cTB._api.open_poliigon_link(self.mode, env_name=cTB._env.env_name)
elif self.mode in cTB._api._url_paths:
cTB._api.open_poliigon_link(self.mode, env_name=cTB._env.env_name)
elif self.mode.startswith("https:"):
@@ -102,3 +113,8 @@ class POLIIGON_OT_link(Operator):
if notice is not None:
cTB.notify.dismiss_notice(notice)
return {"FINISHED"}
def _confirmation_popup(self):
bpy.ops.poliigon.popup_message("INVOKE_DEFAULT",
message_body=_t("Open Addon Help in browser?"),
message_url=self.mode)
@@ -68,9 +68,9 @@ class POLIIGON_OT_load_asset_size_from_list(Operator):
msg = "Please specify an asset name"
self.report({"ERROR"}, msg)
raise ValueError(msg)
if self.asset_type not in ["HDRIs", "Models", "Textures"]:
if self.asset_type not in ["HDRIS", "Models", "Textures"]:
msg = (f"Unknown asset type: {self.asset_type}\n"
"Known types: HDRIs, Models, Textures")
"Known types: HDRIS, Models, Textures")
self.report({"ERROR"}, msg)
raise ValueError(msg)
if len(self.size) > 0:
@@ -68,14 +68,11 @@ class POLIIGON_OT_get_local_asset_sync(Operator):
def _is_startup_done(
*, await_poliigon: bool, await_my_assets: bool) -> bool:
"""Returns True, if the addon is fetching no more asset data."""
startup_done = True
if await_poliigon:
fetching_poliigon = cTB.fetching_asset_data[KEY_TAB_ONLINE]
startup_done = len(fetching_poliigon) == 0
startup_done = cTB.all_assets_fetched
if await_my_assets:
fetching_my_assets = cTB.fetching_asset_data[KEY_TAB_MY_ASSETS]
startup_done &= len(fetching_my_assets) == 0
startup_done &= cTB.are_user_assets_fetched()
return startup_done
def _await_end_of_startup(self) -> None:
@@ -115,6 +112,9 @@ class POLIIGON_OT_get_local_asset_sync(Operator):
categories=[CATEGORY_ALL],
search=search,
force=True,
# Not only force the job to be added, but also force the actual
# request, we want fresh data!
force_request=True,
callback_done=self._callback_get_asset_done
)
if not self.ev_done.wait(self.timeout):
@@ -229,14 +229,18 @@ class POLIIGON_OT_material(Operator):
objs_add_subdiv = [_obj
for _obj in objs_selected
if "Subdivision" not in _obj.modifiers]
bpy.context.scene.cycles.feature_set = "EXPERIMENTAL"
if bpy.app.version < (5, 0):
# Blender 5.0 no longer has feature sets, adaptive is included
bpy.context.scene.cycles.feature_set = "EXPERIMENTAL"
return objs_add_subdiv
def add_subdiv_to_objects(self, obj_list: List[bpy.types.Object]) -> None:
"""Adds a Subdivision modifier to all objects in list."""
for _obj in obj_list:
_obj.cycles.use_adaptive_subdivision = True
if bpy.app.version < (5, 0):
# In blender 5.0, there's no special settings for this.
_obj.cycles.use_adaptive_subdivision = True
modifier = _obj.modifiers.new("Subdivision", "SUBSURF")
if modifier is None:
# TODO(Andreas): Would we want to report failure to create modifier?
@@ -284,6 +288,7 @@ class POLIIGON_OT_material(Operator):
def signal_import(self) -> None:
if self.exec_count == 0:
cTB.signal_import_asset(asset_id=self.asset_id)
cTB.set_first_local_asset()
self.exec_count += 1
@reporting.handle_operator()
@@ -330,6 +335,7 @@ class POLIIGON_OT_material(Operator):
did_reuse = self.check_material_reuse()
if did_reuse:
self.signal_import()
return {"FINISHED"}
tex_maps = asset_type_data.get_maps(
@@ -16,7 +16,7 @@
#
# ##### END GPL LICENSE BLOCK #####
from typing import List, Tuple
from typing import Dict, List, Tuple
import os
import bpy
@@ -57,6 +57,8 @@ LOD_DESCS = {
LOD_NAME = "{0} ({1})"
LOD_DESCRIPTION_FBX = _t("Import the {0} level of detail (LOD) FBX file")
PROP_LIBRARY_LINKED = "poliigon_linked"
class POLIIGON_OT_model(Operator):
bl_idname = "poliigon.poliigon_model"
@@ -223,7 +225,7 @@ class POLIIGON_OT_model(Operator):
else:
empty = None
if self.do_use_collection is True:
if self.do_use_collection:
# Move the objects into a subcollection, and place in scene.
if did_fresh_import:
# Always create a new collection if did a fresh import.
@@ -235,25 +237,22 @@ class POLIIGON_OT_model(Operator):
cache_coll.objects.link(_obj)
# Now add the cache collection to the layer, but unchecked.
layer = context.view_layer.active_layer_collection
layer.collection.children.link(cache_coll)
for _child in layer.children:
layer_coll = context.view_layer.active_layer_collection
layer_coll.collection.children.link(cache_coll)
for _child in layer_coll.children:
if _child.collection == cache_coll:
_child.exclude = True
else:
cache_coll = bpy.data.collections.get(asset_name)
# Now finally add the instance to the scene.
if cache_coll:
inst = self.create_instance(context, cache_coll, size, lod)
# layer = context.view_layer.active_layer_collection
# layer.collection.objects.link(inst)
else:
if cache_coll is None:
err = _t("Failed to get new collection to instance")
self.report({"ERROR"}, err)
return {"CANCELLED"}
elif not blend_import:
# Now finally add the instance to the scene.
inst = self.create_instance(context, cache_coll, size, lod)
else:
# Make sure the objects imported are all part of the same coll.
self.append_cleanup(context, empty)
@@ -282,6 +281,7 @@ class POLIIGON_OT_model(Operator):
if self.exec_count == 0:
cTB.signal_import_asset(asset_id=self.asset_id)
cTB.set_first_local_asset()
self.exec_count += 1
return {"FINISHED"}
@@ -302,7 +302,12 @@ class POLIIGON_OT_model(Operator):
break
return all_lod_fbxs
def get_model_data(self, asset_data: AssetData):
def get_model_data(
self,
asset_data: AssetData
) -> Tuple[List[str], List[str], str, str]:
"""Returns details for Model import from a Model asset."""
asset_type_data = asset_data.get_type_data()
# Get the intended material size and LOD import to use.
@@ -393,7 +398,7 @@ class POLIIGON_OT_model(Operator):
return files_project, files_tex, size, lod
def _load_blend(self, path_proj: str):
def _load_blend(self, path_proj: str) -> List[bpy.types.Object]:
"""Loads all objects from a .blend file."""
path_proj_norm = os.path.normpath(path_proj)
@@ -412,7 +417,8 @@ class POLIIGON_OT_model(Operator):
s = splits[0]
return s
def _reuse_materials(self, imported_objs: List, imported_mats: List):
def _reuse_materials(
self, imported_objs: List, imported_mats: List) -> None:
"""Re-uses previously imported materials after a .blend import"""
if not self.do_reuse_materials or self.do_link_blend:
@@ -481,6 +487,272 @@ class POLIIGON_OT_model(Operator):
return _variant
return None
def _run_fresh_import_blend(
self,
context,
path_proj: str,
filename_base: str,
size: str
) -> None:
"""Imports from blend files."""
cTB.logger.debug("POLIIGON_OT_model BLEND IMPORT")
filename = filename_base + ".blend"
if self.do_link_blend and filename in bpy.data.libraries.keys():
lib = bpy.data.libraries[filename]
if lib[PROP_LIBRARY_LINKED]:
linked_objs = []
for obj in bpy.data.objects:
if obj.library == lib:
linked_objs.append(obj)
imported_objs = []
for obj in linked_objs:
imported_objs.append(obj.copy())
else:
imported_objs = self._load_blend(path_proj)
else:
imported_objs = self._load_blend(path_proj)
if filename in bpy.data.libraries.keys():
lib = bpy.data.libraries[filename]
lib[PROP_LIBRARY_LINKED] = self.do_link_blend
for obj in context.view_layer.objects:
obj.select_set(False)
layer = context.view_layer.active_layer_collection
imported_mats = []
for obj in imported_objs:
if obj is None:
continue
obj_copy = obj.copy()
layer.collection.objects.link(obj_copy)
obj_copy.select_set(True)
if obj_copy.active_material is None:
pass
elif obj_copy.active_material not in imported_mats:
imported_mats.append(obj_copy.active_material)
files_dict = {}
asset_type_data = self.asset_data.get_type_data()
asset_type_data.get_files(files_dict)
asset_files = list(files_dict.keys())
replace_tex_size(
imported_mats,
asset_files,
size,
self.do_link_blend
)
self._reuse_materials(imported_objs, imported_mats)
def _run_fresh_import_fbx(self, path_proj: str) -> bool:
"""Imports from FBX files."""
cTB.logger.debug("POLIIGON_OT_model FBX IMPORT")
if "fbx" not in dir(bpy.ops.import_scene):
try:
bpy.ops.preferences.addon_enable(module="io_scene_fbx")
self.report(
{"INFO"},
_t("FBX importer addon enabled for import")
)
except RuntimeError:
self.report(
{"ERROR"},
_t("Built-in FBX importer could not be found, "
"check Blender install")
)
return False
try:
# Note on use_custom_normals parameter:
# It always defaulted to True. But in Blender 4.0+
# it started to cause issues.
# Mateusz sync'ed with Stephen and recommended to turn it
# off regardless of Blender version.
bpy.ops.import_scene.fbx(filepath=path_proj,
axis_up="-Z",
use_custom_normals=False)
except Exception as e:
self.report({"ERROR"},
_t("FBX importer exception:") + str(e))
return False
return True
def _run_fresh_import_create_materials(
self,
*,
path_proj: str,
filename_base: str,
imported_proj: List[str],
files_tex: List[str],
objs_from_file: List[bpy.types.Object],
dict_mats_imported: Dict[str, bpy.types.Material],
size: str,
lod: str
) -> None:
"""Creates materials after successful FBX import."""
asset_name = self.asset_data.asset_name
for _mesh in objs_from_file:
if _mesh.type == "EMPTY":
continue
name_mat_imported = ""
if _mesh.active_material is not None:
name_mat_imported = _mesh.active_material.name
# Note: Of course the check if "_mat" is contained could be
# written in one line. But I wouldn't consider "_mat"
# unlikely in arbitrary filenames. Thus I chose to
# explicitly compare for "_mat" at the end and
# additionally check if "_mat_" is contained, in order
# to at least reduce the chance of false positives a bit.
name_mat_imported_lower = name_mat_imported.lower()
name_tex_remastered = ""
name_tex_on_obj = ""
ends_remastered = name_mat_imported_lower.endswith("_mat")
contains_remastered = "_mat_" in name_mat_imported_lower
if ends_remastered or contains_remastered:
pos_remastered = name_mat_imported_lower.rfind("_mat", 1)
name_tex_remastered = name_mat_imported[:pos_remastered]
else:
name_tex_on_obj = name_mat_imported.split("_")[0]
name_mesh_base = _mesh.name.split(".")[0].split("_")[0]
variant_mesh = self.f_GetVar(_mesh.name)
variant_name = variant_mesh
if variant_mesh is None:
# This is a fallback for models,
# where the object name does not contain a variant indicator.
# As None is covered explicitly in the loop below,
# this should do no harm.
variant_mesh = "VAR1"
variant_name = ""
name_mat = name_mesh_base
files_tex_filtered = []
if len(name_tex_remastered) > 0:
# Remastered textures
files_tex_filtered = [
_file
for _file in files_tex
if os.path.basename(_file).startswith(
name_tex_remastered)
]
name_mat = name_mat_imported
else:
for vCheck in [name_mesh_base,
filename_base.split("_")[0],
asset_name,
name_tex_on_obj]:
files_tex_filtered = [
_file
for _file in files_tex
if os.path.basename(_file).startswith(vCheck)
if self.f_GetVar(f_FName(_file)) in [None,
variant_mesh]
]
name_mat = vCheck
if len(files_tex_filtered) > 0:
break
if len(files_tex_filtered) == 0:
err = f"No Textures found for: {self.asset_id} {_mesh.name}"
reporting.capture_message(
"model_texture_missing", err, "info")
continue
name_mat += f"_{size}"
if size not in name_mat:
name_mat += f"_{size}"
if variant_name != "":
name_mat += f"_{variant_name}"
# TODO(Andreas): Not sure, why these lines are commented out.
# Looks reasonable to me.
# if name_mat in bpy.data.materials and self.do_reuse_materials:
# mat = bpy.data.materials[name_mat]
# el
# TODO(Andreas): Not sure, this should also be depending on
# self.do_reuse_materials?
if name_mat in dict_mats_imported.keys():
# Already built in previous iteration
mat = dict_mats_imported[name_mat]
else:
mat = cTB.mat_import.import_material(
asset_data=self.asset_data,
do_apply=False,
workflow="METALNESS",
size=size,
size_bg=None,
lod=lod,
variant=variant_name if variant_name != "" else None,
name_material=name_mat,
name_mesh=name_mesh_base,
ref_objs=None,
projection="UV",
use_16bit=True,
mode_disp="NORMAL", # We never want model dispalcement
translate_x=0.0,
translate_y=0.0,
scale=1.0,
global_rotation=0.0,
aspect_ratio=1.0,
displacement=0.0,
keep_unused_tex_nodes=False,
reuse_existing=self.do_reuse_materials
)
if mat is None:
msg = f"{self.asset_id}: Failed to build matrial: {name_mat}"
reporting.capture_message(
"could_not_create_fbx_mat", msg, "error")
self.report(
{"ERROR"}, _t("Material could not be created."))
imported_proj.remove(path_proj)
break
dict_mats_imported[name_mat] = mat
# This sequence is important!
# 1) Setting the material slot to None
# 2) Changing the link mode
# 3) Assigning our generated material
# Any other order of these statements will get us into trouble
# one way or another.
_mesh.active_material = None
if len(_mesh.material_slots) > 0:
_mesh.material_slots[0].link = "OBJECT"
_mesh.active_material = mat
if variant_mesh is not None:
if len(_mesh.material_slots) == 0:
_mesh.data.materials.append(mat)
else:
_mesh.material_slots[0].link = "OBJECT"
_mesh.material_slots[0].material = mat
_mesh.material_slots[0].link = "OBJECT"
# Ensure we can identify the mesh & LOD even on name change.
# TODO(Andreas): Remove old properties?
_mesh.poliigon = f"Models;{asset_name}"
if lod is not None:
_mesh.poliigon_lod = lod
self.set_poliigon_props_model(_mesh, lod)
# Finally try to remove the originally imported materials
if name_mat_imported not in bpy.data.materials:
continue
mat_imported = bpy.data.materials[name_mat_imported]
if mat_imported.users == 0:
mat_imported.user_clear()
bpy.data.materials.remove(mat_imported)
def run_fresh_import(self,
context,
project_files: List[str],
@@ -503,8 +775,6 @@ class POLIIGON_OT_model(Operator):
Tuple[3] - True, if an error occurred during FBX loading
"""
PROP_LIBRARY_LINKED = "poliigon_linked"
asset_name = self.asset_data.asset_name
meshes_all = []
@@ -518,7 +788,8 @@ class POLIIGON_OT_model(Operator):
err = _t("Couldn't load project file: {0} {1}").format(
asset_name, path_proj)
self.report({"ERROR"}, err)
err = f"Couldn't load project file: {self.asset_id} {path_proj}"
err = ("Couldn't load project file: "
f"{self.asset_id} {path_proj}")
reporting.capture_message("model_fbx_missing", err, "info")
continue
@@ -526,91 +797,12 @@ class POLIIGON_OT_model(Operator):
ext_proj = f_FExt(path_proj)
if ext_proj == ".blend":
cTB.logger.debug("POLIIGON_OT_model BLEND IMPORT")
filename = filename_base + ".blend"
if self.do_link_blend and filename in bpy.data.libraries.keys():
lib = bpy.data.libraries[filename]
if lib[PROP_LIBRARY_LINKED]:
linked_objs = []
for obj in bpy.data.objects:
if obj.library == lib:
linked_objs.append(obj)
imported_objs = []
for obj in linked_objs:
imported_objs.append(obj.copy())
else:
imported_objs = self._load_blend(path_proj)
else:
imported_objs = self._load_blend(path_proj)
if filename in bpy.data.libraries.keys():
lib = bpy.data.libraries[filename]
lib[PROP_LIBRARY_LINKED] = self.do_link_blend
for obj in context.view_layer.objects:
obj.select_set(False)
layer = context.view_layer.active_layer_collection
imported_mats = []
for obj in imported_objs:
if obj is None:
continue
obj_copy = obj.copy()
layer.collection.objects.link(obj_copy)
obj_copy.select_set(True)
if obj_copy.active_material is None:
pass
elif obj_copy.active_material not in imported_mats:
imported_mats.append(obj_copy.active_material)
files_dict = {}
asset_type_data = self.asset_data.get_type_data()
asset_type_data.get_files(files_dict)
asset_files = list(files_dict.keys())
replace_tex_size(
imported_mats,
asset_files,
size,
self.do_link_blend
)
self._reuse_materials(imported_objs, imported_mats)
self._run_fresh_import_blend(
context, path_proj, filename_base, size)
blend_import = True
else:
cTB.logger.debug("POLIIGON_OT_model FBX IMPORT")
if "fbx" not in dir(bpy.ops.import_scene):
try:
bpy.ops.preferences.addon_enable(module="io_scene_fbx")
self.report(
{"INFO"},
_t("FBX importer addon enabled for import")
)
except RuntimeError:
self.report(
{"ERROR"},
_t("Built-in FBX importer could not be found, check Blender install")
)
did_full_import = False
meshes_all = []
blend_import = False
fbx_error = True
return (did_full_import,
meshes_all,
blend_import,
fbx_error)
try:
# Note on use_custom_normals parameter:
# It always defaulted to True. But in Blender 4.0+
# it started to cause issues.
# Mateusz sync'ed with Stephen and recommended to turn it
# off regardless of Blender version.
bpy.ops.import_scene.fbx(filepath=path_proj,
axis_up="-Z",
use_custom_normals=False)
except Exception as e:
self.report({"ERROR"},
_t("FBX importer exception:") + str(e))
import_ok = self._run_fresh_import_fbx(path_proj)
if not import_ok:
did_full_import = False
meshes_all = []
blend_import = False
@@ -621,13 +813,13 @@ class POLIIGON_OT_model(Operator):
fbx_error)
imported_proj.append(path_proj)
vMeshes = [_obj for _obj in list(context.scene.objects)
if _obj not in list_objs_before]
objs_from_file = [_obj for _obj in list(context.scene.objects)
if _obj not in list_objs_before]
meshes_all += vMeshes
meshes_all += objs_from_file
if ext_proj == ".blend":
for _mesh in vMeshes:
for _mesh in objs_from_file:
# Ensure we can identify the mesh & LOD even on name change
# TODO(Andreas): Remove old properties?
_mesh.poliigon = f"Models;{asset_name}"
@@ -636,160 +828,15 @@ class POLIIGON_OT_model(Operator):
self.set_poliigon_props_model(_mesh, lod)
continue
for _mesh in vMeshes:
if _mesh.type == "EMPTY":
continue
name_mat_imported = ""
if _mesh.active_material is not None:
name_mat_imported = _mesh.active_material.name
# Note: Of course the check if "_mat" is contained could be
# written in one line. But I wouldn't consider "_mat"
# unlikely in arbitrary filenames. Thus I chose to
# explicitly compare for "_mat" at the end and
# additionally check if "_mat_" is contained, in order
# to at least reduce the chance of false positives a bit.
name_mat_imported_lower = name_mat_imported.lower()
name_tex_remastered = ""
name_tex_on_obj = ""
ends_remastered = name_mat_imported_lower.endswith("_mat")
contains_remastered = "_mat_" in name_mat_imported_lower
if ends_remastered or contains_remastered:
pos_remastered = name_mat_imported_lower.rfind("_mat", 1)
name_tex_remastered = name_mat_imported[:pos_remastered]
else:
name_tex_on_obj = name_mat_imported.split("_")[0]
name_mesh_base = _mesh.name.split(".")[0].split("_")[0]
variant_mesh = self.f_GetVar(_mesh.name)
variant_name = variant_mesh
if variant_mesh is None:
# This is a fallback for models,
# where the object name does not contain a variant indicator.
# As None is covered explicitly in the loop below,
# this should do no harm.
variant_mesh = "VAR1"
variant_name = ""
name_mat = name_mesh_base
files_tex_filtered = []
if len(name_tex_remastered) > 0:
# Remastered textures
files_tex_filtered = [
_file
for _file in files_tex
if os.path.basename(_file).startswith(
name_tex_remastered)
]
name_mat = name_mat_imported
else:
for vCheck in [name_mesh_base,
filename_base.split("_")[0],
asset_name,
name_tex_on_obj]:
files_tex_filtered = [
_file
for _file in files_tex
if os.path.basename(_file).startswith(vCheck)
if self.f_GetVar(f_FName(_file)) in [None,
variant_mesh]
]
name_mat = vCheck
if len(files_tex_filtered) > 0:
break
if len(files_tex_filtered) == 0:
err = f"No Textures found for: {self.asset_id} {_mesh.name}"
reporting.capture_message(
"model_texture_missing", err, "info")
continue
name_mat += f"_{size}"
if size not in name_mat:
name_mat += f"_{size}"
if variant_name != "":
name_mat += f"_{variant_name}"
# TODO(Andreas): Not sure, why these lines are commented out.
# Looks reasonable to me.
# if name_mat in bpy.data.materials and self.do_reuse_materials:
# mat = bpy.data.materials[name_mat]
# el
# TODO(Andreas): Not sure, this should also be dependening on self.do_reuse_materials?
if name_mat in dict_mats_imported.keys():
# Already built in previous iteration
mat = dict_mats_imported[name_mat]
else:
mat = cTB.mat_import.import_material(
asset_data=self.asset_data,
do_apply=False,
workflow="METALNESS",
size=size,
size_bg=None,
lod=lod,
variant=variant_name if variant_name != "" else None,
name_material=name_mat,
name_mesh=name_mesh_base,
ref_objs=None,
projection="UV",
use_16bit=True,
mode_disp="NORMAL", # We never want model dispalcement
translate_x=0.0,
translate_y=0.0,
scale=1.0,
global_rotation=0.0,
aspect_ratio=1.0,
displacement=0.0,
keep_unused_tex_nodes=False,
reuse_existing=self.do_reuse_materials
)
if mat is None:
msg = f"{self.asset_id}: Failed to build matrial: {name_mat}"
reporting.capture_message(
"could_not_create_fbx_mat", msg, "error")
self.report(
{"ERROR"}, _t("Material could not be created."))
imported_proj.remove(path_proj)
break
dict_mats_imported[name_mat] = mat
# This sequence is important!
# 1) Setting the material slot to None
# 2) Changing the link mode
# 3) Assigning our generated material
# Any other order of these statements will get us into trouble
# one way or another.
_mesh.active_material = None
if len(_mesh.material_slots) > 0:
_mesh.material_slots[0].link = "OBJECT"
_mesh.active_material = mat
if variant_mesh is not None:
if len(_mesh.material_slots) == 0:
_mesh.data.materials.append(mat)
else:
_mesh.material_slots[0].link = "OBJECT"
_mesh.material_slots[0].material = mat
_mesh.material_slots[0].link = "OBJECT"
# Ensure we can identify the mesh & LOD even on name change.
# TODO(Andreas): Remove old properties?
_mesh.poliigon = f"Models;{asset_name}"
if lod is not None:
_mesh.poliigon_lod = lod
self.set_poliigon_props_model(_mesh, lod)
# Finally try to remove the originally imported materials
if name_mat_imported in bpy.data.materials:
mat_imported = bpy.data.materials[name_mat_imported]
if mat_imported.users == 0:
mat_imported.user_clear()
bpy.data.materials.remove(mat_imported)
self._run_fresh_import_create_materials(
path_proj=path_proj,
filename_base=filename_base,
imported_proj=imported_proj,
files_tex=files_tex,
objs_from_file=objs_from_file,
dict_mats_imported=dict_mats_imported,
size=size,
lod=lod)
# There could have been multiple FBXs, consider fully imported
# for user-popup reporting if all FBX files imported.
@@ -852,7 +899,8 @@ class POLIIGON_OT_model(Operator):
return empty
def create_instance(self, context, coll, size, lod):
def create_instance(
self, context, coll, size: str, lod: str) -> bpy.types.Object:
"""Creates an instance of an existing collection int he active view."""
asset_name = self.asset_data.asset_name
@@ -861,8 +909,8 @@ class POLIIGON_OT_model(Operator):
inst = bpy.data.objects.new(name=inst_name, object_data=None)
inst.instance_collection = coll
inst.instance_type = "COLLECTION"
lc = context.view_layer.active_layer_collection
lc.collection.objects.link(inst)
layer_coll = context.view_layer.active_layer_collection
layer_coll.collection.objects.link(inst)
inst.location = context.scene.cursor.location
inst.empty_display_size = 0.01
@@ -873,11 +921,12 @@ class POLIIGON_OT_model(Operator):
context.view_layer.objects.active = inst
return inst
def append_cleanup(self, context, root_empty):
def append_cleanup(self, context, root_empty: bpy.types.Object) -> None:
"""Performs selection and placement cleanup after an import/append."""
if not root_empty:
cTB.logger.error("root_empty was not a valid object, exiting cleanup")
if root_empty is None:
cTB.logger.error(
"root_empty was not a valid object, exiting cleanup")
return
# Set empty location
@@ -0,0 +1,101 @@
# #### 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 bpy
from bpy.types import Operator
from bpy.props import StringProperty
from ..modules.poliigon_core.multilingual import _t
from ..modules.poliigon_core.user import PoliigonUserProfiles
from ..toolbox import get_context
from ..toolbox_settings import save_settings
from .. import reporting
class POLIIGON_OT_onboarding_survey(Operator):
bl_idname = "poliigon.poliigon_onboarding_survey"
bl_label = _t("Onboarding Survey")
bl_description = _t("Handle onboarding survey interactions")
bl_options = {"REGISTER", "INTERNAL"}
user_type: StringProperty(options={"HIDDEN"}) # noqa: F821
@staticmethod
def init_context(addon_version: str) -> None:
"""Called from operators.py to init global addon context."""
global cTB
cTB = get_context(addon_version)
@classmethod
def description(cls, context, properties):
"""Return description based on user type."""
# Default description
desc = cls.bl_description
# Get specific descriptions based on user_type
if properties.user_type == "As a student":
desc = _t("As a student")
elif properties.user_type == "As a teacher":
desc = _t("As a teacher")
elif properties.user_type == "As a hobbyist":
desc = _t("As a hobbyist")
elif properties.user_type == "As a professional (individual)":
desc = _t("As a professional (individual)")
elif properties.user_type == "As a professional (studio/team)":
desc = _t("As a professional (studio/team)")
return desc
def _submit_onboarding_survey(self, vProps) -> None:
"""Submit the onboarding survey"""
user_type_enum = PoliigonUserProfiles.from_string(self.user_type)
email_optin = vProps.onboarding_email_preference
if not user_type_enum:
err = f"Invalid user type selected: {user_type_enum}"
cTB.logger.error(err)
reporting.capture_message("invalid_user_type", err, "error")
return
cTB.user.user_profile = user_type_enum
cTB.user.email_preference = email_optin
cTB.update_user_use()
@reporting.handle_operator()
def execute(self, context):
"""Handle onboarding survey interactions."""
vProps = context.window_manager.poliigon_props
cTB.logger.debug(f"Selected user type: {self.user_type}")
# Clear the onboarding survey flag to hide the dialog immediately
cTB.did_show_profile_survey = True
# Persist the selection
save_settings(cTB)
self._submit_onboarding_survey(vProps)
cTB.refresh_ui()
return {'FINISHED'}
def register():
bpy.utils.register_class(POLIIGON_OT_onboarding_survey)
def unregister():
bpy.utils.unregister_class(POLIIGON_OT_onboarding_survey)
@@ -69,7 +69,11 @@ class POLIIGON_OT_popup_message(Operator):
@reporting.handle_operator(silent=True)
def execute(self, context):
if self.message_url:
bpy.ops.wm.url_open(url=self.message_url)
if self.message_url in cTB._api._url_paths:
cTB._api.open_poliigon_link(self.message_url,
env_name=cTB._env.env_name)
else:
bpy.ops.wm.url_open(url=self.message_url)
notice = cTB.notify.get_top_notice(do_signal_view=False)
if notice is not None:
@@ -25,7 +25,6 @@ from bpy.props import BoolProperty
from ..modules.poliigon_core.multilingual import _t
from ..dialogs.utils_dlg import get_ui_scale, wrapped_label
from ..constants import (
POPUP_WIDTH_LABEL,
POPUP_WIDTH_NARROW,
POPUP_WIDTH_LABEL_NARROW)
from ..toolbox import get_context
@@ -34,12 +33,15 @@ from ..utils import load_image
from .. import reporting
class POLIIGON_OT_popup_welcome(Operator):
bl_idname = "poliigon.popup_welcome"
bl_label = _t("Welcome to the Poliigon Addon")
MODE_DOWNLOADING = "downloading"
MODE_ONBOARDING = "onboarding"
class BaseOnboardingOperator(Operator):
bl_options = {"INTERNAL", "REGISTER", "UNDO"}
force: BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
force = BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
mode = ""
@staticmethod
def init_context(addon_version: str) -> None:
@@ -48,19 +50,48 @@ class POLIIGON_OT_popup_welcome(Operator):
global cTB
cTB = get_context(addon_version)
def _call_signal(self) -> None:
# After discussion both onboarding popups will signal as "onboarding"
cTB.signal_popup(popup="ONBOARDING")
def _get_label_texts(self) -> None:
self.label1 = _t("Preview any texture by clicking the \"eye\" icon.")
self.label2 = _t("Download, import and apply in just two clicks.")
if self.mode != MODE_DOWNLOADING:
return
self.label1 = _t("Find assets you previously downloaded "
"from the filter dropdown.")
dl_label = _t("You can turn off one-click purchase in addon preferences.")
self.label2 = dl_label if not cTB.is_unlimited_user() else ""
def _load_images(self) -> None:
path = os.path.join(cTB.dir_script, "onboarding_welcome.png")
self.img_welcome = load_image("POPUP_welcome", path)
if self.mode == MODE_DOWNLOADING:
path = os.path.join(cTB.dir_script, "onboarding_welcome_unlimited.png")
self.img_download = load_image("POPUP_download", path)
else:
path = os.path.join(cTB.dir_script, "onboarding_welcome.png")
self.img_welcome = load_image("POPUP_welcome", path)
def invoke(self, context, event):
if cTB.is_unlimited_user():
return {'FINISHED'}
if cTB.settings["popup_welcome"] and not self.force:
return {'FINISHED'}
if self.force is True:
pass
elif self.mode == MODE_ONBOARDING:
if cTB.is_unlimited_user():
return {'FINISHED'}
elif self.mode == MODE_DOWNLOADING:
if cTB.is_addon_first_onboarding_done():
return {'FINISHED'}
self._load_images()
cTB.settings["popup_welcome"] = 1
save_settings(cTB)
self._get_label_texts()
self._call_signal()
if self.mode == MODE_DOWNLOADING:
cTB.set_addon_first_onboarding_done()
else:
cTB.settings["popup_welcome"] = 1
save_settings(cTB)
return context.window_manager.invoke_props_dialog(
self, width=POPUP_WIDTH_NARROW)
@@ -69,7 +100,7 @@ class POLIIGON_OT_popup_welcome(Operator):
label_width = POPUP_WIDTH_LABEL_NARROW * get_ui_scale(cTB)
# Accounting for the left+right border columns (eyeballing):
if bpy.app.version >= (4, 0):
label_width -= 10.0
label_width -= 18.0
elif bpy.app.version >= (3, 0):
# TODO(Andreas): Scale for other versions and in other popups, too?
label_width -= 25.0 * get_ui_scale(cTB)
@@ -80,9 +111,15 @@ class POLIIGON_OT_popup_welcome(Operator):
col_image = col_content.column()
col_image.scale_y = 0.5
col_image.template_icon(
icon_value=self.img_welcome.preview.icon_id,
scale=18.0)
if self.mode == MODE_DOWNLOADING:
col_image.template_icon(
icon_value=self.img_download.preview.icon_id,
scale=18.0)
else:
col_image.template_icon(
icon_value=self.img_welcome.preview.icon_id,
scale=18.0)
row_text = col_content.row()
if bpy.app.version >= (3, 0):
@@ -96,7 +133,7 @@ class POLIIGON_OT_popup_welcome(Operator):
else:
col_left_gap = row_text.column()
col_left_gap.alignment = "LEFT"
# Note, here no label in left column. Otherwise we'd end up with a
# Note, here no label in left column. Otherwise, we'd end up with a
# way too larger border gap.
col_text = row_text.column()
col_text.alignment = "CENTER"
@@ -104,73 +141,33 @@ class POLIIGON_OT_popup_welcome(Operator):
wrapped_label(
cTB,
width=label_width,
text=_t('Preview any texture by clicking the "eye" icon.'),
text=self.label1,
container=col_text,
add_padding=True)
wrapped_label(
cTB,
width=label_width,
text=_t("Download, import and apply in just two clicks."),
container=col_text,
add_padding_bottom=True)
if self.label2:
wrapped_label(
cTB,
width=label_width,
text=self.label2,
container=col_text,
add_padding_bottom=True)
@reporting.handle_operator(silent=True)
def execute(self, context):
cTB.refresh_ui()
return {'FINISHED'}
return {'CANCELLED'} # Avoids showing in redo-last
class POLIIGON_OT_popup_first_download(Operator):
class POLIIGON_OT_popup_welcome(BaseOnboardingOperator):
bl_idname = "poliigon.popup_welcome"
bl_label = _t("Welcome to the Poliigon Addon")
mode = MODE_ONBOARDING
class POLIIGON_OT_popup_first_download(BaseOnboardingOperator):
bl_idname = "poliigon.popup_first_download"
bl_label = _t("Purchased, download started")
bl_options = {"INTERNAL", "REGISTER", "UNDO"}
bl_label = _t("Download started")
force: BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
@staticmethod
def init_context(addon_version: str) -> None:
"""Called from operators.py to init global addon context."""
global cTB
cTB = get_context(addon_version)
def invoke(self, context, event):
if cTB.is_unlimited_user():
return {'FINISHED'}
if cTB.settings["popup_download"] and not self.force:
return {'FINISHED'}
cTB.settings["popup_download"] = 1
cTB.signal_popup(popup="ONBOARD_PURCHASE")
save_settings(cTB)
return context.window_manager.invoke_props_popup(self, event)
@reporting.handle_draw()
def draw(self, context):
label_width = POPUP_WIDTH_LABEL * get_ui_scale(cTB)
col_content = self.layout.column()
wrapped_label(
cTB,
width=label_width,
text=_t("Thanks for purchasing your first Poliigon Asset!"),
container=col_content)
wrapped_label(
cTB,
width=label_width,
text=_t("By default Show Purchase Confirmation is disabled. You "
"can adjust this as well as your default download "
"settings in preferences."),
container=col_content,
add_padding=True)
op = col_content.operator(
"poliigon.open_preferences",
text="View Preferences"
)
op.set_focus = "show_default_prefs"
@reporting.handle_operator(silent=True)
def execute(self, context):
cTB.refresh_ui()
return {'FINISHED'}
mode = MODE_DOWNLOADING
@@ -100,6 +100,10 @@ class POLIIGON_OT_preview(Operator):
f"download: {download_time}")
cTB.logger.debug(f"POLIIGON_OT_preview {debug_str}")
# Set the flag that user has used watermarked preview
cTB.set_onboarding_wm_preview_done()
save_settings(cTB)
cTB.signal_preview_asset(asset_id=self.asset_id)
return res
@@ -355,6 +359,8 @@ class POLIIGON_OT_popup_first_preview(Operator):
@reporting.handle_operator(silent=True)
def execute(self, context):
cTB.settings["popup_preview"] = 1
# Set the flag that user has used watermarked preview
cTB.set_onboarding_wm_preview_done()
save_settings(cTB)
cTB.signal_popup(popup="ONBOARD_WMPREVIEW", click="ONBOARD_WMPREVIEW")
bpy.ops.poliigon.poliigon_preview(asset_id=self.asset_id)
@@ -22,7 +22,6 @@ from bpy.types import Operator
from ..modules.poliigon_core.multilingual import _t
from ..toolbox import get_context
from .. import reporting
from .operator_detail_view import check_and_report_detail_view_not_opening
class POLIIGON_OT_refresh_data(Operator):
@@ -48,7 +47,5 @@ class POLIIGON_OT_refresh_data(Operator):
@reporting.handle_operator(silent=True)
def execute(self, context):
check_and_report_detail_view_not_opening()
cTB.refresh_data()
return {'FINISHED'}
@@ -81,8 +81,6 @@ class POLIIGON_OT_report_error(Operator):
wrapped_label(cTB, target_wrap, label_txt, layout)
layout.prop(self, "user_message", text="")
wrapped_label(cTB, target_wrap, _t("Press OK to send report"), layout)
@reporting.handle_operator(silent=True)
def execute(self, context):
if bpy.app.background: # No user to give feedback anyways.
@@ -17,16 +17,18 @@
# ##### END GPL LICENSE BLOCK #####
from enum import IntEnum
from typing import Tuple
from typing import Tuple, Optional
from bpy.types import Operator
import bpy.utils.previews
from ..modules.poliigon_core.api_remote_control_params import (
CATEGORY_FREE,
CATEGORY_ALL,
KEY_TAB_IMPORTED,
KEY_TAB_MY_ASSETS,
KEY_TAB_ONLINE)
KEY_TAB_RECENT_DOWNLOADS,
KEY_TAB_ONLINE,
KEY_TAB_LOCAL)
from ..modules.poliigon_core.multilingual import _t
from ..asset_browser.asset_browser import create_poliigon_library
from ..dialogs.utils_dlg import get_ui_scale
@@ -35,7 +37,6 @@ from ..toolbox import get_context
from ..toolbox_settings import save_settings
from ..utils import f_MDir
from .. import reporting
from .operator_detail_view import check_and_report_detail_view_not_opening
class ModeUpdate(IntEnum):
@@ -80,19 +81,20 @@ class POLIIGON_OT_setting(Operator):
primary=True,
update_local_assets=True)
if bpy.app.version >= (3, 0):
if bpy.app.version >= (3, 0) and cTB.user_legacy_own_assets():
create_poliigon_library(force=True)
cTB.signal_popup(popup="ONBOARD_WELCOME")
bpy.ops.poliigon.popup_welcome("INVOKE_DEFAULT")
# do_update: update and switch to page 1
return ModeUpdate.IMPORTED_AND_GET_ASSETS_AND_PAGE_1
def _set_area(self) -> int:
def _set_area(self, area: Optional[str] = None) -> int:
with cTB.lock_thumbs:
cTB.thumbs.clear()
area = self.mode.replace("area_", "")
if area is None:
area = self.mode.replace("asset_filter_", "").replace("area_", "")
cTB.settings["area"] = area
# This caused a delay when switching between Poliigon/My Assets
@@ -103,9 +105,19 @@ class POLIIGON_OT_setting(Operator):
cTB.vActiveAsset = None
cTB.track_screen_from_area()
check_and_report_detail_view_not_opening()
# do_update: update, but skip switching to page 1 and getting assets
return ModeUpdate.IMPORTED_ONLY
update = ModeUpdate.IMPORTED_ONLY
# If local assets, always run get_assets in api_rc (no api call involved)
if area == KEY_TAB_LOCAL:
update = ModeUpdate.IMPORTED_AND_GET_ASSETS
# When changing filters we are always resetting to page 1
if "asset_filter" in self.mode:
update = ModeUpdate.IMPORTED_AND_GET_ASSETS_AND_PAGE_1
return update
@staticmethod
def _show_my_account() -> None:
@@ -113,7 +125,6 @@ class POLIIGON_OT_setting(Operator):
cTB.settings["show_user"] = 1
cTB.vActiveAsset = None
cTB.refresh_ui()
check_and_report_detail_view_not_opening()
@staticmethod
def _show_settings() -> None:
@@ -130,41 +141,37 @@ class POLIIGON_OT_setting(Operator):
mode_parts = self.mode.split("_")
idx_category = int(mode_parts[1])
button = mode_parts[2]
if idx_category < len(cTB.settings["category"][area]):
cTB.settings["category"][area][idx_category] = button
if idx_category < len(cTB.settings["category"]):
cTB.settings["category"][idx_category] = button
else:
cTB.settings["category"][area].append(button)
cTB.settings["category"][area] = cTB.settings[
"category"][area][: idx_category + 1]
cTB.settings["category"].append(button)
cTB.settings["category"] = cTB.settings[
"category"][: idx_category + 1]
categories = cTB.settings["category"][area]
categories = cTB.settings["category"]
if len(categories) > 1 and categories[-1].startswith("All "):
categories = categories[:-1]
cTB.settings["category"][area] = categories
cTB.settings["category"] = categories
if categories != [CATEGORY_ALL]:
cTB.signal_category_filter(" > ".join(categories))
cTB.vActiveAsset = None
cTB.vActiveMat = None
cTB.vActiveMode = None
# Do we want to clear searches when switching between areas?
cTB.vSearch[KEY_TAB_ONLINE] = props.search_poliigon
cTB.vSearch[KEY_TAB_MY_ASSETS] = props.search_my_assets
cTB.vSearch[KEY_TAB_IMPORTED] = props.search_imported
cTB.vSearch[KEY_TAB_ONLINE] = props.search
cTB.vSearch[KEY_TAB_MY_ASSETS] = props.search
cTB.vSearch[KEY_TAB_RECENT_DOWNLOADS] = props.search
cTB.vSearch[KEY_TAB_IMPORTED] = props.search
cTB.vSearch[KEY_TAB_LOCAL] = props.search
cTB.search_free = False
check_and_report_detail_view_not_opening()
self._clear_search_tab(tab=area)
self._set_area(KEY_TAB_ONLINE)
# do_update: update and switch to page 1
return ModeUpdate.IMPORTED_AND_GET_ASSETS_AND_PAGE_1
def _set_free_search(self) -> int:
cTB.search_free = True
cTB.settings["category"][KEY_TAB_ONLINE] = [CATEGORY_FREE]
cTB.vActiveAsset = None
cTB.vActiveMat = None
cTB.vActiveMode = None
return ModeUpdate.IMPORTED_AND_GET_ASSETS_AND_PAGE_1
def _set_page(self) -> int:
with cTB.lock_thumbs:
cTB.thumbs.clear()
@@ -183,8 +190,6 @@ class POLIIGON_OT_setting(Operator):
else:
cTB.vPage[area] = int(idx_page)
check_and_report_detail_view_not_opening()
# do_update: update, but skip switching to page 1 and getting assets
return ModeUpdate.IMPORTED_ONLY
@@ -198,40 +203,26 @@ class POLIIGON_OT_setting(Operator):
# do_update: update and switch to page 1
return ModeUpdate.IMPORTED_AND_GET_ASSETS_AND_PAGE_1, True
def _clear_search_tab(self, tab: str) -> None:
props = bpy.context.window_manager.poliigon_props
props.search = ""
cTB.vLastSearch[tab] = ""
cTB.vSearch[tab] = ""
def _clear_search(self) -> None:
props = bpy.context.window_manager.poliigon_props
if self.mode.endswith(KEY_TAB_ONLINE):
props.search_poliigon = ""
self._clear_search_tab(tab=KEY_TAB_ONLINE)
elif self.mode.endswith(KEY_TAB_MY_ASSETS):
props.search_my_assets = ""
self._clear_search_tab(tab=KEY_TAB_MY_ASSETS)
elif self.mode.endswith(KEY_TAB_IMPORTED):
props.search_imported = ""
self._clear_search_tab(tab=KEY_TAB_IMPORTED)
elif self.mode.endswith(KEY_TAB_RECENT_DOWNLOADS):
self._clear_search_tab(tab=KEY_TAB_RECENT_DOWNLOADS)
elif self.mode.endswith(KEY_TAB_LOCAL):
self._clear_search_tab(tab=KEY_TAB_LOCAL)
cTB.flush_thumb_prefetch_queue()
check_and_report_detail_view_not_opening()
@staticmethod
def _clear_email() -> None:
props = bpy.context.window_manager.poliigon_props
props.vEmail = ""
@staticmethod
def _clear_password() -> None:
props = bpy.context.window_manager.poliigon_props
props.vPassHide = ""
props.vPassShow = ""
@staticmethod
def _show_password() -> None:
# Can be removed if we're not going use the "show password" button
props = bpy.context.window_manager.poliigon_props
# TODO(Andreas): strange toggle logic?
if cTB.settings["show_pass"]:
props.vPassHide = (props.vPassShow)
else:
props.vPassShow = (props.vPassHide)
cTB.settings["show_pass"] = not cTB.settings["show_pass"]
def _set_thumb_size(self) -> None:
size = self.mode.split("@")[1]
if cTB.settings["thumbsize"] == size:
@@ -252,8 +243,7 @@ class POLIIGON_OT_setting(Operator):
library_dirs=cTB.get_library_paths())
cTB.refresh_ui()
elif self.mode == "hdri_use_jpg_bg":
# do_update: update, but skip switching to page 1
do_update = ModeUpdate.IMPORTED_AND_GET_ASSETS
do_update = ModeUpdate.IMPORTED_ONLY
return do_update
def _set_default(self) -> int:
@@ -312,12 +302,12 @@ class POLIIGON_OT_setting(Operator):
area = KEY_TAB_ONLINE
cTB.settings["area"] = area
cat_area = cTB.settings["category"][prev_area]
cTB.settings["category"][area] = cat_area
cat_area = cTB.settings["category"]
cTB.settings["category"] = cat_area
cTB.settings["show_settings"] = 0
cTB.settings["show_user"] = 0
cTB.vSearch[KEY_TAB_ONLINE] = cTB.vSearch[prev_area]
props.search_poliigon = cTB.vSearch[prev_area]
props.search = cTB.vSearch[prev_area]
cTB.vActiveAsset = None
# do_update: update, but skip switching to page 1
return ModeUpdate.IMPORTED_AND_GET_ASSETS
@@ -354,8 +344,13 @@ class POLIIGON_OT_setting(Operator):
cTB.f_GetSceneAssets()
if do_update < ModeUpdate.IMPORTED_ONLY:
if area in [KEY_TAB_ONLINE, KEY_TAB_MY_ASSETS, KEY_TAB_IMPORTED]:
cTB.f_GetAssets(area=area)
if area in [KEY_TAB_ONLINE,
KEY_TAB_MY_ASSETS,
KEY_TAB_RECENT_DOWNLOADS,
KEY_TAB_IMPORTED,
KEY_TAB_LOCAL]:
cTB.f_GetAssets(area=area,
force_request=area == KEY_TAB_LOCAL)
cTB.refresh_ui()
@@ -387,12 +382,6 @@ class POLIIGON_OT_setting(Operator):
do_update, clear_cache = self._set_page_size()
elif self.mode.startswith("clear_search_"):
self._clear_search()
elif self.mode == "clear_email":
self._clear_email()
elif self.mode == "clear_pass":
self._clear_password()
elif self.mode == "show_pass":
self._show_password()
elif self.mode.startswith("thumbsize@"):
self._set_thumb_size()
elif self.mode in [
@@ -433,8 +422,8 @@ class POLIIGON_OT_setting(Operator):
self._toggle_material_property()
elif self.mode == "view_more":
do_update = self._view_more()
elif self.mode == "search_free":
do_update = self._set_free_search()
elif self.mode.startswith("asset_filter_"):
do_update = self._set_area()
else:
reporting.capture_message("invalid_setting_mode", self.mode)
self.report(
@@ -26,7 +26,6 @@ from ..toolbox import (
get_context,
get_prefs)
from .. import reporting
from .operator_detail_view import check_and_report_detail_view_not_opening
class POLIIGON_OT_show_preferences(Operator):
@@ -119,6 +118,5 @@ class POLIIGON_OT_show_preferences(Operator):
_t("Search for and expand the Poliigon addon in preferences")
)
check_and_report_detail_view_not_opening()
cTB.track_screen("settings")
cTB.signal_view_screen("settings")
return {'FINISHED'}
@@ -27,7 +27,6 @@ from ..modules.poliigon_core.multilingual import _t
from ..toolbox import get_context
from ..dialogs.dlg_quickmenu import show_quick_menu
from .. import reporting
from .operator_detail_view import check_and_report_detail_view_not_opening
class POLIIGON_OT_show_quick_menu(Operator):
@@ -55,8 +54,6 @@ class POLIIGON_OT_show_quick_menu(Operator):
if bpy.app.background:
return {'CANCELLED'} # Don't popup menus when running headless.
check_and_report_detail_view_not_opening()
asset_data = cTB._asset_index.get_asset(self.asset_id)
show_quick_menu(
cTB, asset_data=asset_data, hide_detail_view=self.hide_detail_view)
@@ -20,13 +20,12 @@ import datetime
from threading import Event
import time
import bpy
from bpy.types import Operator
from bpy.props import (BoolProperty, StringProperty)
import bpy
from ..modules.poliigon_core.api_remote_control import ApiJob
from ..modules.poliigon_core.api_remote_control_params import CmdLoginMode
from ..dialogs.dlg_login import ERR_CREDS_FORMAT
from ..toolbox import get_context
from .. import reporting
@@ -111,7 +110,8 @@ class POLIIGON_OT_user(Operator):
email = None
pwd = None
login_elapsed_s = cTB.login_elapsed_s
elif self.mode == "login":
cTB.login_is_signup = False # Flag to indicate login was clicked
elif self.mode == "login": # Now only used in tests
mode = CmdLoginMode.LOGIN_CREDENTIALS
callback_cancel = None
callback_done = callback_login_done
@@ -119,8 +119,6 @@ class POLIIGON_OT_user(Operator):
pwd = bpy.context.window_manager.poliigon_props.vPassHide
login_elapsed_s = cTB.login_elapsed_s
elif self.mode == "logout":
bpy.ops.poliigon.poliigon_setting(mode="clear_email")
bpy.ops.poliigon.poliigon_setting(mode="clear_pass")
mode = CmdLoginMode.LOGOUT
callback_cancel = None
callback_done = callback_logout_done
@@ -145,25 +143,16 @@ class POLIIGON_OT_user(Operator):
def execute(self, context):
global cTB
props = bpy.context.window_manager.poliigon_props
if self.mode == "login":
if "@" not in props.vEmail or len(props.vPassHide) < 6:
cTB.clear_user_invalidated()
cTB.last_login_error = ERR_CREDS_FORMAT
return {"CANCELLED"}
self._login_determine_elapsed()
if self.mode in ["login", "login_with_website", "logout"]:
self._do_login(cTB)
elif self.mode == "login_cancel":
cTB.login_cancelled = True
elif self.mode == "login_switch_to_email":
# Reset the login_in_progress flag to return UI to normal state
cTB.login_in_progress = False
cTB.login_is_signup = False # Reset signup flag when cancelling
cTB.last_login_error = None
cTB.login_mode_browser = False
elif self.mode == "login_switch_to_browser":
cTB.login_mode_browser = True
else:
cTB.logger.error(
f"POLIIGON_OT_user UNKNOWN LOGIN COMMAND {self.mode}")
@@ -84,7 +84,7 @@ class POLIIGON_OT_view_thumbnail(Operator):
# Ensure we always restore render settings and preferences.
self._restore_show_render_op_dimensions(context, op_dims_backup)
cTB.track_screen("large_preview")
cTB.signal_view_screen("large_preview")
return res
def _backup_show_render_op_dimensions(
@@ -28,7 +28,6 @@ from .operator_close_notification import POLIIGON_OT_close_notification
from .operator_detail import POLIIGON_OT_detail
from .operator_detail_view import (
POLIIGON_OT_detail_view,
POLIIGON_OT_detail_view_open,
POLIIGON_OT_detail_view_select)
from .operator_directory import POLIIGON_OT_directory
from .operator_download import (
@@ -65,6 +64,8 @@ from .operator_show_preferences import POLIIGON_OT_show_preferences
from .operator_select import POLIIGON_OT_select
from .operator_setting import POLIIGON_OT_setting
from .operator_show_quick_menu import POLIIGON_OT_show_quick_menu
from .operator_asset_filter import POLIIGON_OT_asset_filter
try:
from .operator_unit_test_helper import (
POLIIGON_OT_unit_test_helper,
@@ -76,6 +77,7 @@ except ImportError:
from .operator_unsupported_convention import POLIIGON_OT_unsupported_convention
from .operator_user import POLIIGON_OT_user
from .operator_view_thumbnail import POLIIGON_OT_view_thumbnail
from .operator_onboarding_survey import POLIIGON_OT_onboarding_survey
from ..toolbox import get_context
@@ -92,7 +94,6 @@ classes = (
POLIIGON_OT_close_notification,
POLIIGON_OT_detail,
POLIIGON_OT_detail_view,
POLIIGON_OT_detail_view_open,
POLIIGON_OT_detail_view_select,
POLIIGON_OT_directory,
POLIIGON_OT_download,
@@ -105,6 +106,7 @@ classes = (
POLIIGON_OT_material,
POLIIGON_OT_model,
POLIIGON_OT_notice_operator,
POLIIGON_OT_onboarding_survey,
POLIIGON_OT_options,
POLIIGON_OT_popup_change_plan,
POLIIGON_OT_popup_download_limit,
@@ -123,7 +125,8 @@ classes = (
POLIIGON_OT_show_quick_menu,
POLIIGON_OT_unsupported_convention,
POLIIGON_OT_user,
POLIIGON_OT_view_thumbnail
POLIIGON_OT_view_thumbnail,
POLIIGON_OT_asset_filter
)