Files
blender-portable-repo/scripts/addons/poliigon-addon-blender/operators/operator_preview.py
T
2026-03-17 14:58:51 -06:00

369 lines
13 KiB
Python

# #### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
import os
from typing import List
import threading
import time
import bpy
from bpy.types import Operator
from bpy.props import (
BoolProperty,
IntProperty,
StringProperty)
import bpy.utils.previews
import bmesh
from ..modules.poliigon_core.api_remote_control import ApiJob
from ..modules.poliigon_core.multilingual import _t
from ..dialogs.utils_dlg import get_ui_scale, wrapped_label
from ..constants import POPUP_WIDTH_NARROW, POPUP_WIDTH_LABEL_NARROW
from ..toolbox import get_context
from ..toolbox_settings import save_settings
from ..utils import load_image
from .. import reporting
class POLIIGON_OT_preview(Operator):
"""Download and apply a watermarked version of this texture for previewing."""
bl_idname = "poliigon.poliigon_preview"
bl_label = _t("Texture Preview")
bl_description = _t("Download and apply a watermarked version of this texture for previewing")
bl_options = {"GRAB_CURSOR", "BLOCKING", "REGISTER", "INTERNAL", "UNDO"}
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
def __init__(self, *args, **kwargs):
"""Runs once per operator call before drawing occurs."""
super().__init__(*args, **kwargs)
self.asset_data = None
@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
@reporting.handle_operator()
def execute(self, context):
self.asset_data = cTB._asset_index.get_asset(self.asset_id)
asset_name = self.asset_data.asset_name
t_start = time.time()
files = self.download_preview(context)
t_downloaded = time.time()
if len(files) == 0:
self.report({'ERROR'}, _t("Failed to download preview files"))
struc_str = f"asset_name: {asset_name}"
reporting.capture_message(
"quick_preview_download_failed", struc_str, "error")
return {'CANCELLED'}
# Warn user on viewport change before post process, so that any later
# warnings will popup and take priority to be visible to user. Blender
# shows the last "self.report" message only (but all print to console).
self.report_viewport(context)
cTB.refresh_ui()
res = self.post_process_material(context, files)
t_post_processed = time.time()
total_time = t_post_processed - t_start
download_time = t_downloaded - t_start
debug_str = (f"Preview Total time: {total_time}, "
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
def _callback_download_wm_preview_done(self, job: ApiJob) -> None:
self.ev_download_done.set()
def download_preview(self, context) -> List[str]:
"""Download a preview and return expected files."""
asset_name = self.asset_data.asset_name
asset_type_data = self.asset_data.get_type_data()
self._name = f"PREVIEW_{asset_name}"
if len(asset_type_data.watermarked_urls):
bpy.context.window.cursor_set("WAIT")
self.ev_download_done = threading.Event()
cTB.api_rc.add_job_download_wm_preview(
self.asset_data,
renderer="Cycles", # TODO(Andreas)
callback_done=self._callback_download_wm_preview_done
)
self.ev_download_done.wait()
self.ev_download_done = None
workflow = asset_type_data.get_workflow("METALNESS")
tex_maps = asset_type_data.get_maps(workflow=workflow, size="WM")
files = [_tex_map.get_path() for _tex_map in tex_maps]
return files
else:
return []
@staticmethod
def create_plane(context,
scale: float = 5.0,
aspect_ratio: float = 1.0,
name: str = "Preview Plane",
do_select: bool = True
) -> bpy.types.Object:
"""Creates a new primitive plane object."""
mesh = bpy.data.meshes.new(name)
uv_layer = mesh.uv_layers.new()
mesh.uv_layers.active = uv_layer
obj = bpy.data.objects.new(name, mesh)
bpy.context.collection.objects.link(obj)
bm = bmesh.new()
bm.from_mesh(mesh)
x = (scale / 2.0) * aspect_ratio
y = scale / 2.0
bm.verts.new((x, y, 0))
bm.verts.new((x, -y, 0))
bm.verts.new((-x, -y, 0))
bm.verts.new((-x, y, 0))
bm.verts.ensure_lookup_table()
# Have inverted vertex order for our normal to point upward
face = bm.faces.new(
(bm.verts[3], bm.verts[2], bm.verts[1], bm.verts[0]))
bm.faces.ensure_lookup_table()
uv_layer = bm.loops.layers.uv.verify()
face.loops[0][uv_layer].uv = (1.0, 1.0)
face.loops[1][uv_layer].uv = (1.0, -1.0)
face.loops[2][uv_layer].uv = (-1.0, -1.0)
face.loops[3][uv_layer].uv = (-1.0, 1.0)
bmesh.ops.contextual_create(bm, geom=bm.verts)
bm.to_mesh(mesh)
bm.free()
obj.location = context.scene.cursor.location
if do_select:
obj.select_set(True)
return obj
def post_process_material(self, context, files):
"""Run after the download of WM textures has completed."""
asset_name = self.asset_data.asset_name
asset_type_data = self.asset_data.get_type_data()
workflow = asset_type_data.get_workflow("METALNESS")
bpy.context.window.cursor_set("DEFAULT")
mat = cTB.mat_import.import_material(
asset_data=self.asset_data,
do_apply=False,
workflow=workflow,
size="WM",
size_bg=None,
lod="",
variant=None,
name_material=None,
name_mesh=None,
ref_objs=None,
projection="UV",
use_16bit=True,
mode_disp="NORMAL",
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=True,
map_prefs=None # Previews ignore map prefs
)
if mat is None:
self.report({"ERROR"}, _t("Material could not be created."))
reporting.capture_message(
"could_not_create_preview_mat", asset_name, "error")
return {"CANCELLED"}
objs_selected = [_obj
for _obj in context.scene.objects
if _obj.select_get()]
if len(objs_selected) == 0:
img_preview = None
for _img in bpy.data.images:
if _img.filepath in files:
img_preview = _img
# TODO(Andreas): Added this break as it seemed appropriate.
# Correct???
# Consequence is we take AR from first
# instead of last image in sequence
break
if img_preview is not None:
aspect_ratio = img_preview.size[0] / img_preview.size[1]
else:
aspect_ratio = 1.0
self.create_plane(
context, aspect_ratio=aspect_ratio, do_select=True)
result = bpy.ops.poliigon.poliigon_apply(
"INVOKE_DEFAULT",
asset_id=self.asset_id,
name_material=mat.name
)
if result == {"CANCELLED"}:
self.report(
{"WARNING"}, _t("Could not apply materials to selection"))
bpy.context.window.cursor_set("DEFAULT")
return {"FINISHED"}
def report_viewport(self, context):
"""Send the appropriate report based on the current shading mode."""
any_mat_or_render = False
for vA in context.screen.areas:
if vA.type != "VIEW_3D":
continue
for vSpace in vA.spaces:
if vSpace.type != "VIEW_3D":
continue
if vSpace.shading.type in ["MATERIAL", "RENDERED"]:
any_mat_or_render = True
if not any_mat_or_render:
msg = _t(
"Enter material or rendered mode to view applied quick preview"
)
self.report({'WARNING'}, msg)
class POLIIGON_OT_popup_first_preview(Operator):
bl_idname = "poliigon.popup_first_preview"
bl_label = _t("Texture Preview")
bl_description = _t("Download and apply a watermarked version of this texture for previewing")
bl_options = {"INTERNAL"}
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
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)
@classmethod
def description(cls, context, properties):
return properties.tooltip
def _load_images(self) -> None:
path = os.path.join(cTB.dir_script, "onboarding_watermarked.png")
self.img_welcome = load_image("POPUP_watermarked", path)
def invoke(self, context, event):
if cTB.settings["popup_preview"] and not self.force:
return {'FINISHED'}
self._load_images()
save_settings(cTB)
cTB.signal_popup(popup="ONBOARD_WMPREVIEW")
return context.window_manager.invoke_props_dialog(
self, width=POPUP_WIDTH_NARROW)
@reporting.handle_draw()
def draw(self, context):
label_width = POPUP_WIDTH_LABEL_NARROW * get_ui_scale(cTB)
# Accounting for the left+right border columns:
label_width -= 10.0
col_content = self.layout.column()
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)
row_text = col_content.row()
if bpy.app.version >= (3, 0):
col_left_gap = row_text.column()
col_left_gap.alignment = "LEFT"
col_left_gap.label(text=" ")
col_text = row_text.column()
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
# way too larger border gap.
col_text = row_text.column()
col_text.alignment = "CENTER"
wrapped_label(
cTB,
width=label_width,
text=_t("Previews are watermarked 1K resolution textures."),
container=col_text,
add_padding=True)
wrapped_label(
cTB,
width=label_width,
text=_t("Subscribe to download high-resolution textures up to 8K "
"without watermarks."),
container=col_text,
add_padding_bottom=True)
@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)
cTB.refresh_ui()
return {'FINISHED'}