2025-12-01
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user