2025-07-01
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# #### 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 #####
|
||||
@@ -0,0 +1,107 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_active(Operator):
|
||||
bl_idname = "poliigon.poliigon_active"
|
||||
bl_label = ""
|
||||
bl_description = _t("Set Active Asset")
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
mode: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
asset_type: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
data: 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
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
global cTB
|
||||
|
||||
if self.data == "":
|
||||
cTB.vActiveType = None
|
||||
cTB.vActiveAsset = None
|
||||
cTB.vActiveMat = None
|
||||
cTB.vActiveMode = None
|
||||
|
||||
elif self.mode == "asset":
|
||||
cTB.vActiveType = self.asset_type
|
||||
cTB.vActiveAsset = self.data
|
||||
if cTB.vActiveAsset in cTB.imported_assets["Textures"].keys():
|
||||
cTB.vActiveMat = cTB.imported_assets["Textures"][cTB.vActiveAsset][0].name
|
||||
context.scene.vEditMatName = cTB.vActiveMat
|
||||
else:
|
||||
cTB.vActiveMat = None
|
||||
cTB.vActiveMode = "asset"
|
||||
cTB.settings["show_active"] = 1
|
||||
|
||||
elif self.mode == "mat":
|
||||
# TODO(Andreas): This seems to be the only case used from this op.
|
||||
cTB.vActiveType = self.asset_type
|
||||
if "@" in self.data:
|
||||
cTB.vActiveAsset, cTB.vActiveMat = self.data.split("@")
|
||||
else:
|
||||
cTB.vActiveMat = self.data
|
||||
cTB.vActiveMode = "asset"
|
||||
|
||||
elif self.mode == "mixer":
|
||||
cTB.vActiveType = self.asset_type
|
||||
cTB.vActiveAsset = self.data
|
||||
context.scene.vEditMatName = cTB.vActiveAsset
|
||||
cTB.vActiveMat = self.data
|
||||
cTB.vActiveMode = "mixer"
|
||||
cTB.settings["show_active"] = 1
|
||||
|
||||
elif self.mode == "mix":
|
||||
cTB.vActiveMode = "mixer"
|
||||
cTB.vActiveMix = self.data
|
||||
|
||||
elif self.mode == "mixmat":
|
||||
cTB.vActiveMode = "mixer"
|
||||
cTB.vActiveMixMat = self.data
|
||||
|
||||
elif self.mode == "poliigon":
|
||||
cTB.vActiveMode = "poliigon"
|
||||
cTB.vActiveAsset = self.data
|
||||
|
||||
elif self.mode == "settings":
|
||||
# f_Settings()
|
||||
return {"FINISHED"}
|
||||
|
||||
cTB.f_GetActiveData()
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,114 @@
|
||||
# #### 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 EnumProperty
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..material_import_utils import load_poliigon_node_group
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
# These need to be global to work in _fill_node_drop_down()
|
||||
ENUM29 = (
|
||||
("Poliigon_Mixer",
|
||||
_t("Principled mixer"),
|
||||
_t("Principled mixer node")),
|
||||
("Mosaic_UV_Mapping",
|
||||
_t("Mosaic mapping"),
|
||||
_t("Poliigon Mosaic mapping node")),
|
||||
)
|
||||
ENUM28 = (
|
||||
("Poliigon_Mixer",
|
||||
_t("Principled mixer"),
|
||||
_t("Principled mixer node")),
|
||||
)
|
||||
|
||||
# Needs to be global,
|
||||
# as member variable can not be accessed in "items" function of EnumProperty
|
||||
view_screen_tracked_nodes = False
|
||||
|
||||
|
||||
class POLIIGON_OT_add_converter_node(Operator):
|
||||
bl_idname = "poliigon.add_converter_node"
|
||||
bl_label = _t("Converter node group")
|
||||
bl_description = _t("Adds a material converter node group")
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
@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 _fill_node_drop_down(self, context):
|
||||
"""Returns list of available nodes as EnumPropertyItems.
|
||||
While the enums are actually static, this function serves as a
|
||||
"draw detection" to track view screen.
|
||||
"""
|
||||
|
||||
# Called during class construction, we can not access a
|
||||
# member variable here
|
||||
global view_screen_tracked_nodes
|
||||
|
||||
if not view_screen_tracked_nodes:
|
||||
cTB.track_screen("blend_node_add")
|
||||
view_screen_tracked_nodes = True
|
||||
|
||||
if bpy.app.version >= (2, 90):
|
||||
return ENUM29
|
||||
else:
|
||||
return ENUM28
|
||||
|
||||
node_type: EnumProperty(items=_fill_node_drop_down)
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
if not self.node_type:
|
||||
self.report(
|
||||
{"Error"}, _t("No node_type specified to add"))
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not context.material:
|
||||
self.report(
|
||||
{"ERROR"}, _t("No active material selected to add nodegroup"))
|
||||
return {"CANCELLED"}
|
||||
|
||||
for node in context.material.node_tree.nodes:
|
||||
node.select = False
|
||||
|
||||
# TODO(Andreas): Not supposed to stay in legacy importer module
|
||||
node_group = load_poliigon_node_group(self.node_type)
|
||||
if node_group is None:
|
||||
self.report({"ERROR"}, _t("Failed to import nodegroup."))
|
||||
return {"CANCELLED"}
|
||||
|
||||
mat = context.material
|
||||
node_mosaic = mat.node_tree.nodes.new("ShaderNodeGroup")
|
||||
node_mosaic.node_tree = node_group
|
||||
node_mosaic.name = node_group.name
|
||||
node_mosaic.width = 200
|
||||
if not node_mosaic.node_tree:
|
||||
self.report({"ERROR"}, _t("Failed to load nodegroup."))
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Use this built in modal for moving the added node around
|
||||
return bpy.ops.node.translate_attach('INVOKE_DEFAULT')
|
||||
@@ -0,0 +1,196 @@
|
||||
# #### 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 #####
|
||||
|
||||
from typing import List
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
IntProperty,
|
||||
StringProperty)
|
||||
import bpy.utils.previews
|
||||
import bmesh
|
||||
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
SCALE_UNIT_FACTORS = {
|
||||
"KILOMETERS": 1000.0,
|
||||
"CENTIMETERS": 1.0 / 100.0,
|
||||
"MILLIMETERS": 1.0 / 1000.0,
|
||||
"MILES": 1.0 / 0.000621371,
|
||||
"FEET": 1.0 / 3.28084,
|
||||
"INCHES": 1.0 / 39.3701
|
||||
}
|
||||
|
||||
|
||||
class POLIIGON_OT_apply(Operator):
|
||||
bl_idname = "poliigon.poliigon_apply"
|
||||
bl_label = _t("Apply Material :")
|
||||
bl_description = _t("Apply Material to Selection")
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
|
||||
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
|
||||
name_material: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Runs once per operator call before drawing occurs."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.exec_count = 0
|
||||
|
||||
@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
|
||||
|
||||
@staticmethod
|
||||
def get_edit_objects(context) -> List[bpy.types.Object]:
|
||||
objs_selected = []
|
||||
|
||||
for obj in context.scene.objects:
|
||||
if obj.mode != "EDIT":
|
||||
continue
|
||||
mesh = obj.data
|
||||
b_mesh = bmesh.from_edit_mesh(mesh)
|
||||
for _face in b_mesh.faces:
|
||||
if not _face.select:
|
||||
continue
|
||||
objs_selected.append(obj)
|
||||
break
|
||||
return objs_selected
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
objs_selected = [_obj for _obj in context.selected_objects]
|
||||
if len(objs_selected) == 0:
|
||||
objs_selected = self.get_edit_objects(context)
|
||||
|
||||
objs_add_disp = [_obj
|
||||
for _obj in objs_selected
|
||||
if "Subdivision" not in _obj.modifiers]
|
||||
|
||||
asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
asset_name = asset_data.asset_name
|
||||
asset_type = asset_data.asset_type
|
||||
|
||||
mat = None
|
||||
if self.name_material != "":
|
||||
mat = bpy.data.materials[self.name_material]
|
||||
|
||||
elif asset_name in cTB.imported_assets["Textures"].keys():
|
||||
if len(cTB.imported_assets["Textures"][asset_name]) == 1:
|
||||
mat = cTB.imported_assets["Textures"][asset_name][0]
|
||||
if mat is None:
|
||||
# Unexpected need to download local, should avoid happening.
|
||||
reporting.capture_message(
|
||||
"triggered_popup_mid_apply", asset_name, level="info")
|
||||
return {"CANCELLED"}
|
||||
|
||||
do_subdiv = False
|
||||
if cTB.settings["use_disp"] and len(objs_add_disp) > 0:
|
||||
if cTB.prefs and cTB.prefs.mode_disp == "MICRO":
|
||||
do_subdiv = True
|
||||
|
||||
if do_subdiv or len(objs_selected) > len(objs_add_disp):
|
||||
bpy.context.scene.render.engine = "CYCLES"
|
||||
bpy.context.scene.cycles.feature_set = "EXPERIMENTAL"
|
||||
|
||||
for _node in mat.node_tree.nodes:
|
||||
if _node.type != "GROUP":
|
||||
continue
|
||||
for _input in _node.inputs:
|
||||
if _input.type != "VALUE":
|
||||
continue
|
||||
elif _input.name == "Displacement Strength":
|
||||
_node.inputs[_input.name].default_value = mat.poliigon_props.displacement
|
||||
|
||||
# TODO(Andreas): Not sure, what this was good for.
|
||||
# Seems to have done nothing.
|
||||
# faces_all = []
|
||||
# for _key in cTB.vActiveFaces.keys():
|
||||
# faces_all += cTB.vActiveFaces[_key]
|
||||
|
||||
valid_objects = 0
|
||||
for _obj in objs_selected:
|
||||
if hasattr(_obj.data, "materials"):
|
||||
valid_objects += 1
|
||||
else:
|
||||
continue
|
||||
|
||||
if _obj.mode != "EDIT":
|
||||
_obj.active_material = mat
|
||||
else:
|
||||
mats_obj = [_mat.material
|
||||
for _mat in _obj.material_slots
|
||||
if _mat is not None]
|
||||
if mat not in mats_obj:
|
||||
_obj.data.materials.append(mat)
|
||||
for idx in range(len(_obj.material_slots)):
|
||||
if _obj.material_slots[idx].material != mat:
|
||||
continue
|
||||
_obj.active_material_index = idx
|
||||
bpy.ops.object.material_slot_assign()
|
||||
|
||||
if do_subdiv and _obj in objs_add_disp:
|
||||
mod_subdiv = _obj.modifiers.new(
|
||||
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
|
||||
|
||||
# Scale ...........................................................
|
||||
# TODO(Andreas): What is this? Did this ever work?
|
||||
dimension = "?"
|
||||
if dimension != "?":
|
||||
scale_mult = bpy.context.scene.unit_settings.scale_length
|
||||
|
||||
unit = bpy.context.scene.unit_settings.length_unit
|
||||
scale_mult *= SCALE_UNIT_FACTORS[unit]
|
||||
vec_scale = (_obj.scale * scale_mult) / dimension
|
||||
|
||||
nodes_mat = mat.node_tree.nodes
|
||||
for _node in nodes_mat:
|
||||
if _node.type != "GROUP":
|
||||
continue
|
||||
for _input in _node.inputs:
|
||||
if _input.type != "VALUE":
|
||||
continue
|
||||
elif _input.name == "Scale":
|
||||
_node.inputs[_input.name].default_value = vec_scale[0]
|
||||
|
||||
cTB.vActiveType = asset_type.name
|
||||
cTB.vActiveAsset = asset_name
|
||||
cTB.vActiveMat = mat.name
|
||||
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,54 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import IntProperty
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_cancel_download(Operator):
|
||||
bl_idname = "poliigon.cancel_download"
|
||||
bl_label = _t("Cancel download")
|
||||
bl_description = _t("Cancel downloading this asset")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
asset_id: IntProperty(default=0, options={'SKIP_SAVE'}) # 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)
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
if self.asset_id <= 0:
|
||||
return {'CANCELLED'}
|
||||
|
||||
asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
if asset_data is None:
|
||||
return {'CANCELLED'}
|
||||
asset_data.state.dl.cancel()
|
||||
|
||||
cTB.logger.debug(f"Cancelled download {self.asset_id}")
|
||||
self.report({'WARNING'}, _t("Cancelling download"))
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,95 @@
|
||||
# #### 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 #####
|
||||
|
||||
from typing import List
|
||||
|
||||
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
|
||||
|
||||
|
||||
class POLIIGON_OT_category(Operator):
|
||||
bl_idname = "poliigon.poliigon_category"
|
||||
bl_label = _t("Select a Category")
|
||||
bl_description = _t("Select a Category")
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
data: 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
|
||||
|
||||
@staticmethod
|
||||
def _show_categories_menu(cTB, categories: List[str], index: str) -> 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)
|
||||
|
||||
for idx_category in range(len(categories)):
|
||||
if idx_category > 0 and idx_category % 15 == 0:
|
||||
col = row.column(align=True)
|
||||
|
||||
button = categories[idx_category]
|
||||
op = col.operator("poliigon.poliigon_setting", text=button)
|
||||
op.mode = f"category_{index}_{button}"
|
||||
op.tooltip = _t("Select {0}").format(button)
|
||||
|
||||
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):
|
||||
idx = self.data.split("@")[0]
|
||||
categories = self.data.split("@")[1:]
|
||||
|
||||
self._show_categories_menu(
|
||||
cTB,
|
||||
categories=categories,
|
||||
index=idx
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,49 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_check_update(Operator):
|
||||
bl_idname = "poliigon.check_update"
|
||||
bl_label = _t("Check for update")
|
||||
bl_description = _t("Check for any addon updates")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
@staticmethod
|
||||
def init_context(addon_version: str) -> None:
|
||||
"""Called from operators.py to init global addon context."""
|
||||
|
||||
global cTB
|
||||
cTB = get_context(addon_version)
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
cTB.logger.debug("Started check for update with "
|
||||
f"{cTB._updater.addon_version} "
|
||||
f"{cTB._updater.software_version}")
|
||||
cTB._updater.async_check_for_update(
|
||||
callback=cTB.check_update_callback, create_notifications=True)
|
||||
cTB.logger.debug("Update ready? "
|
||||
f"{cTB._updater.update_ready}"
|
||||
f"{cTB._updater.update_data}")
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,56 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import IntProperty
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_close_notification(Operator):
|
||||
bl_idname = "poliigon.close_notification"
|
||||
bl_label = ""
|
||||
bl_description = _t("Close notification")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
notification_index: IntProperty(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 _t("Close notification") # Avoids having an extra blank line.
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
notice = cTB.notify.get_top_notice(do_signal_view=False)
|
||||
if notice is None:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
_t("Could not dismiss notification, out of bounds.")
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
cTB.notify.dismiss_notice(notice)
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,60 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
# TODO(Andreas): currently not used at all?
|
||||
class POLIIGON_OT_detail(Operator):
|
||||
bl_idname = "poliigon.poliigon_detail"
|
||||
bl_label = ""
|
||||
bl_description = _t("Reset Property to Default")
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
data: 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 context.active_object is not None
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
if context.object.cycles.dicing_rate != context.scene.vDispDetail:
|
||||
context.object.cycles.dicing_rate = context.scene.vDispDetail
|
||||
context.object.modifiers["Subdivision"].subdivision_type = "SIMPLE"
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,851 @@
|
||||
# #### 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 #####
|
||||
|
||||
from enum import IntEnum
|
||||
from functools import partial
|
||||
import os
|
||||
from time import monotonic
|
||||
from typing import Dict, Optional
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
IntProperty,
|
||||
StringProperty)
|
||||
from bpy.types import Operator
|
||||
import bpy.utils.previews
|
||||
|
||||
from ..modules.poliigon_core.api_remote_control import ApiJob
|
||||
from ..modules.poliigon_core.assets import (
|
||||
AssetData,
|
||||
AssetType)
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..dialogs.utils_dlg import check_convention
|
||||
from ..dialogs.dlg_assets import (
|
||||
_asset_is_local,
|
||||
_determine_in_scene_sizes,
|
||||
_determine_thumb_width,
|
||||
_draw_button_download,
|
||||
_draw_button_hdri_local,
|
||||
_draw_button_model_local,
|
||||
_draw_button_purchase,
|
||||
_draw_button_quick_menu,
|
||||
_draw_button_texture_local,
|
||||
_draw_button_unsupported_convention,
|
||||
_draw_thumb_state_asset_downloading,
|
||||
_draw_thumb_state_asset_purchasing,
|
||||
_draw_thumb_state_cancelling_download,
|
||||
THUMB_SIZE_FACTOR)
|
||||
from ..toolbox import get_context
|
||||
from ..utils import load_image
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class MODE_SELECT(IntEnum):
|
||||
# Negative values, as zero and positive ones represent an index
|
||||
NEXT = -1
|
||||
PREVIOUS = -2
|
||||
|
||||
|
||||
class DetailViewState():
|
||||
"""Global state class used to transfer information between the three
|
||||
involved operators.
|
||||
"""
|
||||
|
||||
def __init__(self, num_previews: int):
|
||||
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 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()
|
||||
|
||||
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()
|
||||
|
||||
def init_preview_image_dict(self, num_previews: int) -> None:
|
||||
"""Prepares the image dictionary with 'downloading dummies'."""
|
||||
|
||||
self.imgs_preview = {}
|
||||
for _idx_preview in range(num_previews):
|
||||
self.imgs_preview[_idx_preview] = self.img_downloading
|
||||
|
||||
def set_image(self, idx_preview: int, img: bpy.types.Image) -> None:
|
||||
"""Stores the given image in previews dictionary."""
|
||||
|
||||
self.imgs_preview[idx_preview] = img
|
||||
|
||||
def set_error_image(self, idx_preview: int) -> None:
|
||||
"""Sets given preview index to error state (storing error image in
|
||||
preview dictionary).
|
||||
"""
|
||||
|
||||
self.imgs_preview[idx_preview] = self.img_error
|
||||
|
||||
def cleanup_images(self) -> None:
|
||||
"""Removes all images from blend data."""
|
||||
|
||||
for _img in self.imgs_preview.values():
|
||||
if _img.name in [".POLIIGON_PREVIEW_downloading",
|
||||
".POLIIGON_PREVIEW_error"]:
|
||||
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:
|
||||
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_downloading.user_clear()
|
||||
bpy.data.images.remove(self.img_downloading)
|
||||
|
||||
def next_index(self) -> None:
|
||||
self.idx_preview = (self.idx_preview + 1) % self.num_previews
|
||||
|
||||
def previous_index(self) -> None:
|
||||
self.idx_preview = (self.idx_preview - 1) % self.num_previews
|
||||
|
||||
def set_index(self, idx_preview: int) -> None:
|
||||
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
|
||||
|
||||
|
||||
class POLIIGON_OT_detail_view_select(Operator):
|
||||
bl_idname = "poliigon.detail_view_select"
|
||||
bl_label = ""
|
||||
bl_description = _t("Select another large thumb in detail view")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
select_preview: IntProperty(min=MODE_SELECT.PREVIOUS, 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):
|
||||
select_preview = properties.select_preview
|
||||
if select_preview == MODE_SELECT.NEXT:
|
||||
return _t("Next Preview")
|
||||
elif select_preview == MODE_SELECT.PREVIOUS:
|
||||
return _t("Previous Preview")
|
||||
elif 0 <= select_preview < MODE_SELECT.NEXT:
|
||||
return _t("Select Preview #{0}").format(select_preview)
|
||||
else:
|
||||
# Not reported, consequences are rather minimal
|
||||
cTB.logger.error(
|
||||
"Operator detail_view_select, tooltip unknown mode! "
|
||||
f"{select_preview}")
|
||||
return "" # Should not happen
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
global g_state
|
||||
|
||||
if self.select_preview == MODE_SELECT.NEXT:
|
||||
g_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)
|
||||
else:
|
||||
# Not reported, consequences are rather minimal
|
||||
cTB.logger.error(
|
||||
"Operator detail_view_select, execute unknown mode! "
|
||||
f"{self.select_preview}")
|
||||
|
||||
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
|
||||
|
||||
@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 cls.bl_description
|
||||
|
||||
@reporting.handle_invoke()
|
||||
def invoke(self, context, event):
|
||||
global g_open_request_s
|
||||
|
||||
g_open_request_s = None
|
||||
|
||||
self.asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
if self.asset_data is None:
|
||||
msg = (f"Asset ID {self.asset_id} not found in AssetIndex upon "
|
||||
"opening Detail View")
|
||||
cTB.logger.error(msg)
|
||||
reporting.capture_message("detail-view-no-asset-2", msg, "error")
|
||||
return {'CANCELLED'}
|
||||
|
||||
cTB.track_screen("large_preview")
|
||||
|
||||
return context.window_manager.invoke_props_dialog(
|
||||
self, width=450)
|
||||
|
||||
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
|
||||
|
||||
row_image = layout.row()
|
||||
col_image = row_image.column()
|
||||
col_image.scale_y = 1.125
|
||||
col_image.template_icon(
|
||||
icon_value=g_state.imgs_preview[idx_selected].preview.icon_id,
|
||||
scale=18.0)
|
||||
|
||||
def _add_select_previous_button(self, layout: bpy.types.UILayout) -> None:
|
||||
"""Adds the button to select the previous preview image to the popup
|
||||
dialog.
|
||||
"""
|
||||
|
||||
col_btn_prev = layout.column()
|
||||
col_btn_prev.alignment = "LEFT"
|
||||
op = col_btn_prev.operator(
|
||||
"poliigon.detail_view_select",
|
||||
text="",
|
||||
icon="TRIA_LEFT",
|
||||
emboss=False)
|
||||
op.select_preview = int(MODE_SELECT.PREVIOUS)
|
||||
|
||||
def _add_select_next_button(self, layout: bpy.types.UILayout) -> None:
|
||||
"""Adds the button to select the next preview image to the popup
|
||||
dialog.
|
||||
"""
|
||||
|
||||
col_btn_next = layout.column()
|
||||
col_btn_next.alignment = "RIGHT"
|
||||
op = col_btn_next.operator(
|
||||
"poliigon.detail_view_select",
|
||||
text="",
|
||||
icon="TRIA_RIGHT",
|
||||
emboss=False)
|
||||
op.select_preview = int(MODE_SELECT.NEXT)
|
||||
|
||||
def _add_select_index_buttons(
|
||||
self, layout: bpy.types.UILayout, idx_selected: int) -> None:
|
||||
"""Adds buttons to select a specific preview image to the popup
|
||||
dialog.
|
||||
"""
|
||||
|
||||
global g_state
|
||||
|
||||
for _idx in range(g_state.num_previews):
|
||||
if _idx == idx_selected:
|
||||
icon = "RADIOBUT_ON"
|
||||
else:
|
||||
icon = "RADIOBUT_OFF"
|
||||
op = layout.operator(
|
||||
"poliigon.detail_view_select",
|
||||
text="",
|
||||
icon=icon,
|
||||
emboss=False,
|
||||
depress=_idx == idx_selected)
|
||||
op.select_preview = _idx
|
||||
|
||||
def _add_selection_buttons(
|
||||
self, layout: bpy.types.UILayout, idx_selected: int) -> None:
|
||||
"""Adds a row with various buttons to select the preview image to
|
||||
display.
|
||||
"""
|
||||
|
||||
row_select = layout.row(align=True)
|
||||
self._add_select_previous_button(row_select)
|
||||
row_select.separator()
|
||||
row_select.label(text="") # Needed for dot buttons centered
|
||||
self._add_select_index_buttons(row_select, idx_selected)
|
||||
row_select.label(text="") # Needed for dot buttons centered
|
||||
row_select.separator()
|
||||
self._add_select_next_button(row_select)
|
||||
|
||||
def _add_meta_data_asset_name(self, layout: bpy.types.UILayout) -> None:
|
||||
"""Adds labels with the asset name to the popup dialog."""
|
||||
|
||||
col_asset_name = layout.column()
|
||||
col_asset_name.scale_y = 0.8
|
||||
col_asset_name.label(text=self.asset_data.display_name)
|
||||
col_asset_name.label(text=self.asset_data.asset_name)
|
||||
|
||||
def _add_action_buttons(self, layout: bpy.types.UILayout) -> None:
|
||||
"""Adds row with 'action buttons' to the popup dialog."""
|
||||
|
||||
# TODO(Andreas): No need to comment on this function. It currently is
|
||||
# a replica of what dlg_assets is doing.
|
||||
# Possible backlog task: Would like to restructure the
|
||||
# button implementation in dlg_assets (maybe introducing
|
||||
# a few helper functions in addon-core) so it can be
|
||||
# easily re-used here.
|
||||
|
||||
asset_data = self.asset_data
|
||||
api_convention = asset_data.get_convention()
|
||||
asset_id = asset_data.asset_id
|
||||
asset_type = asset_data.asset_type
|
||||
asset_type_data = asset_data.get_type_data()
|
||||
is_tex = asset_type == AssetType.TEXTURE
|
||||
is_purchased = asset_data.is_purchased
|
||||
is_local = asset_data.is_local
|
||||
is_downloaded = _asset_is_local(cTB, asset_data)
|
||||
is_unlimited = cTB.is_unlimited_user()
|
||||
is_selection = len(bpy.context.selected_objects) > 0
|
||||
is_purchase_in_progress = asset_data.state.purchase.is_in_progress()
|
||||
is_cancelled = asset_data.state.dl.is_cancelled()
|
||||
is_download_in_progress = asset_data.state.dl.is_in_progress()
|
||||
|
||||
thumb_size_factor = THUMB_SIZE_FACTOR[cTB.settings["thumbsize"]]
|
||||
thumb_width = _determine_thumb_width(cTB, thumb_size_factor)
|
||||
|
||||
size_pref = cTB.get_pref_size(asset_type)
|
||||
size_default = asset_type_data.get_size(
|
||||
size_pref,
|
||||
local_only=is_downloaded,
|
||||
addon_convention=cTB._asset_index.addon_convention,
|
||||
local_convention=self.asset_data.local_convention)
|
||||
sizes_in_scene, size_default = _determine_in_scene_sizes(
|
||||
cTB, asset_data, size_default)
|
||||
|
||||
size_default = asset_data.get_current_size(
|
||||
size_default,
|
||||
local_only=is_local,
|
||||
addon_convention=cTB.addon_convention)
|
||||
|
||||
if is_tex and api_convention >= 1:
|
||||
map_prefs = cTB.user.map_preferences
|
||||
is_downloaded = asset_type_data.all_expected_maps_local(
|
||||
map_prefs, size=size_default)
|
||||
|
||||
is_in_progress = is_purchase_in_progress or is_cancelled or is_download_in_progress
|
||||
have_folder_button = not is_in_progress and is_downloaded
|
||||
|
||||
row_action_buttons = layout.row()
|
||||
|
||||
if have_folder_button:
|
||||
split_action_buttons = row_action_buttons.split(factor=0.3333)
|
||||
row_main_action = split_action_buttons.row(align=True)
|
||||
split_other_buttons = split_action_buttons.split(factor=0.5)
|
||||
row_folder_button = split_other_buttons.row()
|
||||
row_link_button = split_other_buttons.row()
|
||||
else:
|
||||
split_action_buttons = row_action_buttons.split(factor=0.5)
|
||||
row_main_action = split_action_buttons.row(align=True)
|
||||
row_link_button = split_action_buttons.row()
|
||||
|
||||
if is_purchase_in_progress:
|
||||
_draw_thumb_state_asset_purchasing(row_main_action, asset_data)
|
||||
self._start_timer_periodic_redraw(asset_data)
|
||||
elif is_cancelled:
|
||||
_draw_thumb_state_cancelling_download(row_main_action, asset_data)
|
||||
self._start_timer_periodic_redraw(asset_data)
|
||||
elif is_download_in_progress:
|
||||
_draw_thumb_state_asset_downloading(
|
||||
row_main_action, asset_data, thumb_width)
|
||||
self._start_timer_periodic_redraw(asset_data)
|
||||
|
||||
elif is_purchased or is_unlimited:
|
||||
if is_downloaded:
|
||||
if asset_type == AssetType.MODEL:
|
||||
_draw_button_model_local(
|
||||
cTB, row_main_action, asset_data, error=None)
|
||||
elif asset_type == AssetType.TEXTURE:
|
||||
_draw_button_texture_local(
|
||||
cTB,
|
||||
row_main_action,
|
||||
asset_data,
|
||||
error=None,
|
||||
sizes_in_scene=sizes_in_scene,
|
||||
size_default=size_default,
|
||||
is_selection=is_selection)
|
||||
elif asset_type == AssetType.HDRI:
|
||||
_draw_button_hdri_local(
|
||||
cTB,
|
||||
row_main_action,
|
||||
asset_data,
|
||||
error=None,
|
||||
size_default=size_default)
|
||||
else:
|
||||
if not check_convention(asset_data):
|
||||
_draw_button_unsupported_convention(row_main_action)
|
||||
else:
|
||||
_draw_button_download(
|
||||
cTB,
|
||||
row_main_action, asset_data,
|
||||
error=None,
|
||||
size_default=size_default)
|
||||
else:
|
||||
if not check_convention(asset_data):
|
||||
_draw_button_unsupported_convention(row_main_action)
|
||||
else:
|
||||
_draw_button_purchase(
|
||||
cTB,
|
||||
row_main_action,
|
||||
asset_data,
|
||||
error=None,
|
||||
size_default=size_default)
|
||||
|
||||
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)
|
||||
|
||||
if have_folder_button:
|
||||
op = row_folder_button.operator(
|
||||
"poliigon.poliigon_folder",
|
||||
text=_t("Open folder location"),
|
||||
icon="FILE_FOLDER"
|
||||
)
|
||||
op.asset_id = asset_id
|
||||
|
||||
op = row_link_button.operator(
|
||||
"poliigon.poliigon_link",
|
||||
text=_t("View online"),
|
||||
icon_value=cTB.ui_icons["ICON_poliigon"].icon_id,
|
||||
)
|
||||
op.mode = str(asset_id)
|
||||
op.tooltip = _t("View on Poliigon.com")
|
||||
|
||||
def _add_meta_data_details_map_types(
|
||||
self,
|
||||
col_key: bpy.types.UILayout,
|
||||
col_value: bpy.types.UILayout,
|
||||
key: str,
|
||||
value: str
|
||||
) -> None:
|
||||
"""Map type details need to be wrapped into multiple lines."""
|
||||
|
||||
MAX_CHAR_PER_LINE = 40
|
||||
|
||||
map_type_names = value.split(", ")
|
||||
num_char_on_line = 0
|
||||
lines = []
|
||||
line_current = []
|
||||
for _map_type_name in map_type_names:
|
||||
num_char_type = len(_map_type_name)
|
||||
if num_char_on_line + num_char_type < MAX_CHAR_PER_LINE:
|
||||
line_current.append(_map_type_name)
|
||||
num_char_on_line += num_char_type
|
||||
else:
|
||||
line = ", ".join(line_current)
|
||||
lines.append(line)
|
||||
num_char_on_line = num_char_type
|
||||
line_current = [_map_type_name]
|
||||
if len(line_current) > 0:
|
||||
line = ", ".join(line_current)
|
||||
lines.append(line)
|
||||
|
||||
for _line in lines:
|
||||
col_key.label(text=key)
|
||||
col_value.label(text=_line)
|
||||
key = ""
|
||||
|
||||
def _add_meta_data_details(self, layout: bpy.types.UILayout) -> None:
|
||||
"""Adds a 'table' with asset's details to the popup dialog."""
|
||||
|
||||
row_infos = layout.split(factor=0.3)
|
||||
col_key = row_infos.column()
|
||||
col_key.scale_y = 0.8
|
||||
col_value = row_infos.column()
|
||||
col_value.scale_y = 0.8
|
||||
|
||||
details = self.asset_data.get_display_details_data()
|
||||
for _key, _value in details.items():
|
||||
if _key == "Maps":
|
||||
self._add_meta_data_details_map_types(
|
||||
col_key, col_value, _key, _value)
|
||||
else:
|
||||
col_key.label(text=_key)
|
||||
col_value.label(text=str(_value))
|
||||
|
||||
def _add_meta_data_section(self, layout: bpy.types.UILayout) -> None:
|
||||
"""Adds the meta data section to the popup dialog."""
|
||||
|
||||
col_meta_data = layout.column()
|
||||
self._add_meta_data_asset_name(col_meta_data)
|
||||
col_meta_data.separator()
|
||||
self._add_action_buttons(col_meta_data)
|
||||
col_meta_data.separator()
|
||||
self._add_meta_data_details(col_meta_data)
|
||||
|
||||
@reporting.handle_draw()
|
||||
def draw(self, context):
|
||||
global g_state
|
||||
|
||||
g_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)
|
||||
col_content.separator()
|
||||
self._add_meta_data_section(col_content)
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
"""Nothing to do, here."""
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def t_periodic_redraw(
|
||||
region_popup: Optional[bpy.types.Region],
|
||||
asset_data: AssetData
|
||||
) -> Optional[float]:
|
||||
"""Timer function to redraw/update the popup dialog.
|
||||
|
||||
Based on asset's state, the timer will either be scheduled to fire again or
|
||||
auto-disarm itself, if asset's state shows no more ongoing actions like for
|
||||
example 'downloading'.
|
||||
"""
|
||||
|
||||
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:
|
||||
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
|
||||
@@ -0,0 +1,82 @@
|
||||
# #### 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 bpy.types import Operator
|
||||
from bpy.props import StringProperty
|
||||
import bpy.utils.previews
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
# TODO(Andreas): Pull all lib dir handling into this, instead of having it
|
||||
# spread here and in two "modes" of operator_setting and operator_library!!!
|
||||
|
||||
class POLIIGON_OT_directory(Operator):
|
||||
bl_idname = "poliigon.poliigon_directory"
|
||||
bl_label = _t("Add Additional Directory")
|
||||
bl_description = _t("Add Additional Directory to search for assets")
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
directory: StringProperty(subtype="DIR_PATH") # 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
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
global cTB
|
||||
|
||||
directory = self.directory.replace("\\", "/")
|
||||
cTB.logger.debug(f"POLIIGON_OT_directory execute: {directory}")
|
||||
|
||||
if not os.path.exists(directory):
|
||||
return {"FINISHED"}
|
||||
|
||||
if directory in cTB.settings["disabled_dirs"]:
|
||||
cTB.settings["disabled_dirs"].remove(directory)
|
||||
|
||||
cTB.add_library_path(
|
||||
directory, primary=False, update_local_assets=True)
|
||||
|
||||
cTB.refresh_ui()
|
||||
|
||||
# TODO(Andreas): What is this?
|
||||
# poliigon_setting has no invoke function...
|
||||
bpy.ops.poliigon.poliigon_setting("INVOKE_DEFAULT")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
cTB.logger.debug(f"POLIIGON_OT_directory invoke: {self.directory}")
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
@@ -0,0 +1,253 @@
|
||||
# #### 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 #####
|
||||
|
||||
from threading import Event
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
StringProperty)
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..modules.poliigon_core.api_remote_control import ApiJob
|
||||
from ..modules.poliigon_core.assets import AssetData
|
||||
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 .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_download(Operator):
|
||||
bl_idname = "poliigon.poliigon_download"
|
||||
bl_label = ""
|
||||
bl_description = _t("(Download Asset from Poliigon.com)")
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
|
||||
mode: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
size: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
do_synchronous: 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 _callback_done_sync(self, job: ApiJob) -> None:
|
||||
cTB.callback_asset_update_ui(job)
|
||||
self.event_sync.set()
|
||||
|
||||
def create_auto_download_job(self, asset_data: AssetData) -> ApiJob:
|
||||
"""Create the follow up download job to attach to purchase job."""
|
||||
|
||||
# NOTE: Free assets are implicitly always "auto-download"
|
||||
credits = 0 if asset_data.credits is None else asset_data.credits
|
||||
if not cTB.settings["auto_download"] and credits > 0:
|
||||
return None
|
||||
|
||||
name_renderer = "Cycles"
|
||||
|
||||
fbx_only = False
|
||||
if fbx_only:
|
||||
native_mesh = False
|
||||
else:
|
||||
native_mesh = bool(cTB.settings["download_prefer_blend"])
|
||||
if native_mesh:
|
||||
download_lods = False
|
||||
else:
|
||||
download_lods = bool(cTB.settings["download_lods"])
|
||||
|
||||
if self.do_synchronous:
|
||||
self.event_sync = Event()
|
||||
callback_done = self._callback_done_sync
|
||||
else:
|
||||
callback_done = cTB.callback_asset_update_ui
|
||||
|
||||
job_download = cTB.api_rc.create_job_download_asset(
|
||||
asset_data,
|
||||
size=self.size,
|
||||
size_bg=self.size,
|
||||
type_bg=None,
|
||||
lod="NONE",
|
||||
variant="",
|
||||
download_lods=download_lods,
|
||||
native_mesh=native_mesh,
|
||||
renderer=name_renderer,
|
||||
callback_progress=cTB.callback_asset_update_ui,
|
||||
callback_done=callback_done
|
||||
)
|
||||
return job_download
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
|
||||
asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
|
||||
if self.mode == "download":
|
||||
if ";" in self.size:
|
||||
# Presumed previously supported multi size downloads using ;
|
||||
# as a separator. No longer allowed.
|
||||
reporting.capture_message(
|
||||
"reached_legacy_multi_vsize_for_dl",
|
||||
f"size contained unexpected `;` {self.size}")
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
_t("Failed to download, multiple sizes specified")
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
size = None
|
||||
if self.size != "":
|
||||
size = self.size
|
||||
|
||||
cTB.logger.debug("POLIIGON_OT_download Queue download asset "
|
||||
f"{self.asset_id}")
|
||||
|
||||
name_renderer = "Cycles"
|
||||
|
||||
fbx_only = False
|
||||
if fbx_only:
|
||||
native_mesh = False
|
||||
download_lods = cTB.settings["download_lods"]
|
||||
else:
|
||||
native_mesh = bool(cTB.settings["download_prefer_blend"])
|
||||
if native_mesh:
|
||||
download_lods = False
|
||||
else:
|
||||
download_lods = bool(cTB.settings["download_lods"])
|
||||
|
||||
if self.do_synchronous:
|
||||
self.event_sync = Event()
|
||||
callback_done = self._callback_done_sync
|
||||
else:
|
||||
callback_done = cTB.callback_asset_update_ui
|
||||
|
||||
cTB.api_rc.add_job_download_asset(
|
||||
asset_data,
|
||||
size=size,
|
||||
size_bg="",
|
||||
type_bg="EXR",
|
||||
lod="NONE",
|
||||
variant=None,
|
||||
download_lods=download_lods,
|
||||
native_mesh=native_mesh,
|
||||
renderer=name_renderer,
|
||||
callback_progress=cTB.callback_asset_update_ui,
|
||||
callback_done=callback_done
|
||||
)
|
||||
elif self.mode == "purchase":
|
||||
cTB.logger.debug("POLIIGON_OT_download Purchase asset "
|
||||
f"{self.asset_id}")
|
||||
|
||||
job_download = self.create_auto_download_job(asset_data)
|
||||
|
||||
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
|
||||
else:
|
||||
callback_done = cTB.callback_asset_update_ui
|
||||
|
||||
cTB.api_rc.add_job_purchase_asset(
|
||||
asset_data,
|
||||
cTB.settings["category"][area],
|
||||
search,
|
||||
job_download=job_download,
|
||||
callback_done=callback_done,
|
||||
force=True
|
||||
)
|
||||
|
||||
if self.do_synchronous:
|
||||
self.event_sync.wait(30.0)
|
||||
|
||||
cTB.refresh_ui()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class POLIIGON_OT_popup_purchase(Operator):
|
||||
bl_idname = "poliigon.popup_purchase"
|
||||
bl_label = _t("Purchase Confirmation")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
|
||||
mode: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
size: 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
|
||||
|
||||
def invoke(self, context, event):
|
||||
cTB.signal_popup(popup="CONFIRM_PURCHASE")
|
||||
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)
|
||||
|
||||
col_content = self.layout.column()
|
||||
wrapped_label(
|
||||
cTB,
|
||||
width=label_width,
|
||||
text=_t("Would you like to confirm purchase of this asset?"),
|
||||
container=col_content,
|
||||
add_padding_bottom=True)
|
||||
wrapped_label(
|
||||
cTB,
|
||||
width=label_width,
|
||||
text=_t("You can turn this reminder off in preferences by "
|
||||
"unchecking Show Purchase Confirmation"),
|
||||
container=col_content)
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
cTB.signal_popup(popup="CONFIRM_PURCHASE", click="CONFIRM_PURCHASE")
|
||||
bpy.ops.poliigon.poliigon_download(
|
||||
asset_id=self.asset_id,
|
||||
mode=self.mode,
|
||||
size=self.size)
|
||||
cTB.refresh_ui()
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,65 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import IntProperty
|
||||
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from ..utils import open_dir
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_folder(Operator):
|
||||
bl_idname = "poliigon.poliigon_folder"
|
||||
bl_label = _t("Open Asset Folder")
|
||||
bl_description = _t("Open Asset Folder in system browser")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
asset_id: IntProperty(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)
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
asset_name = asset_data.asset_name
|
||||
|
||||
path_asset = asset_data.get_asset_directory()
|
||||
|
||||
if path_asset is None:
|
||||
msg = _t("No asset path to open for {0}").format(asset_name)
|
||||
self.report({"ERROR"}, msg)
|
||||
reporting.capture_message(
|
||||
f"open_folder_failed: {msg}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
did_open = open_dir(path_asset)
|
||||
if not did_open:
|
||||
reporting.capture_message("open_folder_failed", path_asset)
|
||||
self.report(
|
||||
{"ERROR"}, _t("Open folder here: {0}").format(path_asset))
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,514 @@
|
||||
# #### 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 #####
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
import mathutils
|
||||
import os
|
||||
import re
|
||||
from math import pi
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
)
|
||||
import bpy.utils.previews
|
||||
|
||||
from ..modules.poliigon_core.assets import AssetData
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_hdri(Operator):
|
||||
bl_idname = "poliigon.poliigon_hdri"
|
||||
bl_label = _t("HDRI Import")
|
||||
bl_description = _t("Import HDRI")
|
||||
bl_options = {"GRAB_CURSOR", "BLOCKING", "REGISTER", "INTERNAL", "UNDO"}
|
||||
|
||||
def _fill_light_size_drop_down(
|
||||
self, context) -> List[Tuple[str, str, str]]:
|
||||
asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
asset_type_data = asset_data.get_type_data()
|
||||
|
||||
# Get list of locally available sizes
|
||||
asset_files = {}
|
||||
asset_type_data.get_files(asset_files)
|
||||
asset_files = list(asset_files.keys())
|
||||
|
||||
# Populate dropdown items
|
||||
local_exr_sizes = []
|
||||
for path_asset in asset_files:
|
||||
filename = os.path.basename(path_asset)
|
||||
if not filename.endswith(".exr"):
|
||||
continue
|
||||
match_object = re.search(r"_(\d+K)[_\.]", filename)
|
||||
if match_object:
|
||||
local_exr_sizes.append(match_object.group(1))
|
||||
# Sort by comparing integer size without "K"
|
||||
local_exr_sizes.sort(key=lambda s: int(s[:-1]))
|
||||
items_size = []
|
||||
for size in local_exr_sizes:
|
||||
# Tuple: (id, name, description, icon, enum value)
|
||||
items_size.append((size, f"{size} EXR", f"{size} EXR"))
|
||||
return items_size
|
||||
|
||||
def _fill_bg_size_drop_down(self, context) -> List[Tuple[str, str, str]]:
|
||||
asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
asset_type_data = asset_data.get_type_data()
|
||||
|
||||
# Get list of locally available sizes
|
||||
asset_files = {}
|
||||
asset_type_data.get_files(asset_files)
|
||||
asset_files = list(asset_files.keys())
|
||||
|
||||
# Populate dropdown items
|
||||
local_exr_sizes = []
|
||||
local_jpg_sizes = []
|
||||
for path_asset in asset_files:
|
||||
filename = os.path.basename(path_asset)
|
||||
is_exr = filename.endswith(".exr")
|
||||
is_jpg = filename.lower().endswith(".jpg")
|
||||
is_jpg &= "_JPG" in filename
|
||||
if not is_exr and not is_jpg:
|
||||
continue
|
||||
match_object = re.search(r"_(\d+K)[_\.]", filename)
|
||||
if not match_object:
|
||||
continue
|
||||
local_size = match_object.group(1)
|
||||
if is_exr:
|
||||
local_exr_sizes.append(f"{local_size}_EXR")
|
||||
elif is_jpg:
|
||||
local_jpg_sizes.append(f"{local_size}_JPG")
|
||||
|
||||
local_sizes = local_exr_sizes + local_jpg_sizes
|
||||
# Sort by comparing integer size without "K_JPG" or "K_EXR"
|
||||
local_sizes.sort(key=lambda s: int(s[:-5]))
|
||||
items_size = []
|
||||
for size in local_sizes:
|
||||
# Tuple: (id, name, description, icon, enum value)
|
||||
label = size.replace("_", " ")
|
||||
items_size.append((size, label, label))
|
||||
return items_size
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
|
||||
# If do_apply is set True, the sizes are ignored and set internally
|
||||
do_apply: BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
|
||||
size: EnumProperty(
|
||||
name=_t("Light Texture"), # noqa F722
|
||||
items=_fill_light_size_drop_down,
|
||||
description=_t("Change size of light texture.")) # noqa F722
|
||||
# This is not a pure size, but is a string like "4K_JPG"
|
||||
size_bg: EnumProperty(
|
||||
name=_t("Background Texture"), # noqa F722
|
||||
items=_fill_bg_size_drop_down,
|
||||
description=_t("Change size of background texture.")) # noqa F722
|
||||
hdr_strength: FloatProperty(
|
||||
name=_t("HDR Strength"), # noqa F722
|
||||
description=_t("Strength of Light and Background textures"), # noqa F722
|
||||
soft_min=0.0,
|
||||
step=10,
|
||||
default=1.0)
|
||||
rotation: FloatProperty(
|
||||
name=_t("Z-Rotation"), # noqa: F821
|
||||
description=_t("Z-Rotation"), # noqa: F821
|
||||
unit="ROTATION", # noqa: F821
|
||||
soft_min=-2.0 * pi,
|
||||
soft_max=2.0 * pi,
|
||||
# precision needed here, otherwise Redo Last and node show different values
|
||||
precision=3,
|
||||
step=10,
|
||||
default=0.0)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Runs once per operator call before drawing occurs."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.exec_count = 0
|
||||
|
||||
@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
|
||||
|
||||
@staticmethod
|
||||
def get_imported_hdri(asset_id: int) -> Optional[bpy.types.Image]:
|
||||
"""Returns the imported HDRI image (if any)."""
|
||||
|
||||
img_hdri = None
|
||||
for _img in bpy.data.images:
|
||||
try:
|
||||
asset_id_img = _img.poliigon_props.asset_id
|
||||
if asset_id_img != asset_id:
|
||||
continue
|
||||
img_hdri = _img
|
||||
break
|
||||
except BaseException:
|
||||
# skip non-Poliigon images (no props)
|
||||
continue
|
||||
return img_hdri
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
asset_type_data = asset_data.get_type_data()
|
||||
|
||||
asset_name = asset_data.asset_name
|
||||
local_convention = asset_data.get_convention(local=True)
|
||||
addon_convention = cTB.addon_convention
|
||||
|
||||
name_light = f"{asset_name}_Light"
|
||||
name_bg = f"{asset_name}_Background"
|
||||
|
||||
try:
|
||||
if "_" not in self.size_bg:
|
||||
raise ValueError
|
||||
size_bg_eff, filetype_bg = self.size_bg.split("_")
|
||||
except Exception:
|
||||
msg = ("POLIIGON_OT_hdri: Wrong size_bg format "
|
||||
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}")
|
||||
|
||||
existing = self.get_imported_hdri(self.asset_id)
|
||||
|
||||
# Whenever an HDR is loaded, it fully replaces the prior loaded
|
||||
# images/resolutions. Thus, if we are "applying" an already imported
|
||||
# one, we don't need to worry about resolution selection.
|
||||
|
||||
if not self.do_apply or not existing:
|
||||
# Remove existing images to force loading this resolution.
|
||||
if name_light in bpy.data.images.keys():
|
||||
bpy.data.images.remove(bpy.data.images[name_light])
|
||||
|
||||
if name_bg in bpy.data.images.keys():
|
||||
bpy.data.images.remove(bpy.data.images[name_bg])
|
||||
elif self.do_apply:
|
||||
if name_light in bpy.data.images.keys():
|
||||
path_light = bpy.data.images[name_light].filepath
|
||||
filename = os.path.basename(path_light)
|
||||
match_object = re.search(r"_(\d+K)[_\.]", filename)
|
||||
size_light = match_object.group(1) if match_object else cTB.settings["hdri"]
|
||||
self.size = size_light
|
||||
if name_bg in bpy.data.images.keys():
|
||||
path_bg = bpy.data.images[name_bg].filepath
|
||||
filename = os.path.basename(path_bg)
|
||||
file_type = "JPG" if "_JPG" in filename else "EXR"
|
||||
match_object = re.search(r"_(\d+K)[_\.]", filename)
|
||||
# TODO(Andreas): should next line use cTB.settings["hdribg"] ?
|
||||
size_bg = match_object.group(1) if match_object else cTB.settings["hdri"]
|
||||
self.size_bg = f"{size_bg}_{file_type}"
|
||||
size_bg_eff = size_bg
|
||||
filetype_bg = file_type
|
||||
|
||||
light_exists = name_light in bpy.data.images.keys()
|
||||
bg_exists = name_bg in bpy.data.images.keys()
|
||||
if not light_exists or not bg_exists:
|
||||
if not self.size or self.do_apply:
|
||||
# Edge case that shouldn't occur as the resolution should be
|
||||
# explicitly set, or just applying a local tex already,
|
||||
# but fallback if needed.
|
||||
self.size = cTB.settings["hdri"]
|
||||
|
||||
size_light = asset_type_data.get_size(
|
||||
self.size,
|
||||
local_only=True,
|
||||
addon_convention=addon_convention,
|
||||
local_convention=local_convention)
|
||||
|
||||
files = {}
|
||||
asset_type_data.get_files(files)
|
||||
files = list(files.keys())
|
||||
|
||||
files_tex_exr = [_file for _file in files
|
||||
if size_light in os.path.basename(_file) and _file.lower().endswith(".exr")]
|
||||
|
||||
if len(files_tex_exr) == 0:
|
||||
# TODO(Andreas): Shouldn't be needed anymore with AssetIndex
|
||||
# cTB.f_GetLocalAssets() # Refresh local assets data structure.
|
||||
msg = (f"Unable to locate image {name_light} with size {size_light}, "
|
||||
f"try downloading {asset_name} again.")
|
||||
reporting.capture_message(
|
||||
"failed_load_light_hdri", msg, "error")
|
||||
msg = _t(
|
||||
"Unable to locate image {0} with size {1}, try downloading {2} again."
|
||||
).format(name_light, size_light, asset_name)
|
||||
self.report({"ERROR"}, msg)
|
||||
return {"CANCELLED"}
|
||||
file_tex_light = files_tex_exr[0]
|
||||
|
||||
if cTB.settings["hdri_use_jpg_bg"] and filetype_bg == "JPG":
|
||||
size_bg = asset_type_data.get_size(
|
||||
size_bg_eff,
|
||||
local_only=True,
|
||||
addon_convention=addon_convention,
|
||||
local_convention=local_convention)
|
||||
|
||||
files_tex_jpg = [_file for _file in files
|
||||
if size_bg in os.path.basename(_file) and _file.lower().endswith(".jpg")]
|
||||
|
||||
if len(files_tex_jpg) == 0:
|
||||
# TODO(Andreas): Shouldn't be needed anymore with AssetIndex
|
||||
# cTB.f_GetLocalAssets() # Refresh local assets data structure.
|
||||
msg = (f"Unable to locate image {name_bg} with size {size_bg} (JPG), "
|
||||
f"try downloading {asset_name} again.")
|
||||
reporting.capture_message(
|
||||
"failed_load_bg_jpg", msg, "error")
|
||||
msg = _t(
|
||||
"Unable to locate image {0} with size {1} (JPG), try downloading {2} again."
|
||||
).format(name_bg, size_bg, asset_name)
|
||||
self.report({"ERROR"}, msg)
|
||||
return {"CANCELLED"}
|
||||
file_tex_bg = files_tex_jpg[0]
|
||||
elif size_light != size_bg_eff:
|
||||
size_bg = size_bg_eff
|
||||
files_tex_exr = [_file for _file in files
|
||||
if size_bg_eff in os.path.basename(_file) and _file.lower().endswith(".exr")]
|
||||
if len(files_tex_exr) == 0:
|
||||
# TODO(Andreas): Shouldn't be needed anymore with AssetIndex
|
||||
# cTB.f_GetLocalAssets() # Refresh local assets data structure.
|
||||
msg = (f"Unable to locate image {name_bg} with size {size_bg} (EXR), "
|
||||
f"try downloading {asset_name} again")
|
||||
reporting.capture_message(
|
||||
"failed_load_bg_hdri", msg, "error")
|
||||
msg = _t(
|
||||
"Unable to locate image {0} with size {1} (EXR), try downloading {2} again"
|
||||
).format(name_bg, size_bg, asset_name)
|
||||
self.report({"ERROR"}, msg)
|
||||
return {"CANCELLED"}
|
||||
file_tex_bg = files_tex_exr[0]
|
||||
else:
|
||||
size_bg = size_light
|
||||
file_tex_bg = file_tex_light
|
||||
|
||||
# Reset apply for Redo Last menu to work properly
|
||||
self.do_apply = False
|
||||
|
||||
# ...............................................................................................
|
||||
|
||||
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]
|
||||
|
||||
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.set_poliigon_props_world(context, asset_data)
|
||||
|
||||
cTB.f_GetSceneAssets()
|
||||
|
||||
if self.exec_count == 0:
|
||||
cTB.signal_import_asset(asset_id=self.asset_id)
|
||||
self.exec_count += 1
|
||||
self.report({"INFO"}, _t("HDRI Imported : {0}").format(asset_name))
|
||||
return {"FINISHED"}
|
||||
|
||||
def set_poliigon_props_image(
|
||||
self, img: bpy.types.Image, asset_data: AssetData) -> None:
|
||||
"""Sets Poliigon property of an imported image."""
|
||||
|
||||
img.poliigon_props.asset_name = asset_data.asset_name
|
||||
img.poliigon_props.asset_id = self.asset_id
|
||||
img.poliigon_props.asset_type = asset_data.asset_type.name
|
||||
img.poliigon_props.size = self.size
|
||||
img.poliigon_props.size_bg = self.size_bg
|
||||
img.poliigon_props.hdr_strength = self.hdr_strength
|
||||
img.poliigon_props.rotation = self.rotation
|
||||
|
||||
def set_poliigon_props_world(self, context, asset_data: AssetData) -> None:
|
||||
"""Sets Poliigon property of world."""
|
||||
|
||||
context_world = context.scene.world
|
||||
context_world.poliigon_props.asset_name = asset_data.asset_name
|
||||
context_world.poliigon_props.asset_id = self.asset_id
|
||||
context_world.poliigon_props.asset_type = asset_data.asset_type.name
|
||||
context_world.poliigon_props.size = self.size
|
||||
context_world.poliigon_props.size_bg = self.size_bg
|
||||
context_world.poliigon_props.hdr_strength = self.hdr_strength
|
||||
context_world.poliigon_props.rotation = self.rotation
|
||||
@@ -0,0 +1,94 @@
|
||||
# #### 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 (
|
||||
EnumProperty,
|
||||
StringProperty)
|
||||
import bpy.utils.previews
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..asset_browser.asset_browser import create_poliigon_library
|
||||
from ..toolbox import get_context
|
||||
from ..toolbox_settings import save_settings
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_library(Operator):
|
||||
bl_idname = "poliigon.poliigon_library"
|
||||
bl_label = _t("Poliigon Library")
|
||||
bl_description = _t("(Set Poliigon Library Location)")
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
_enum_items = [
|
||||
("set_library", "set_library", _t("Set path on first load")),
|
||||
("update_library", "update_library", _t("Update path from preferences"))
|
||||
]
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
directory: StringProperty(subtype="DIR_PATH") # noqa: F821
|
||||
mode: EnumProperty(items=_enum_items, 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
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
global cTB
|
||||
|
||||
directory = self.directory.replace("\\", "/")
|
||||
|
||||
if self.mode == "set_library":
|
||||
# Stage for confirmation on startup (or after deleted)
|
||||
cTB.settings["set_library"] = directory
|
||||
else:
|
||||
# Update_library, from user preferences
|
||||
|
||||
if bpy.app.version >= (3, 0):
|
||||
create_poliigon_library(force=True)
|
||||
|
||||
path_old = cTB.get_library_path(primary=True)
|
||||
cTB.replace_library_path(
|
||||
path_old=path_old,
|
||||
path_new=directory,
|
||||
primary=True,
|
||||
update_local_assets=True)
|
||||
|
||||
cTB.refresh_ui()
|
||||
|
||||
save_settings(cTB)
|
||||
|
||||
# if os.path.exists(vDir):
|
||||
# bpy.ops.poliigon.poliigon_setting("INVOKE_DEFAULT")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
@@ -0,0 +1,104 @@
|
||||
# #### 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 webbrowser
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
IntProperty,
|
||||
StringProperty)
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..modules.poliigon_core.notifications import (
|
||||
NOTICE_ID_SURVEY_FREE,
|
||||
NOTICE_ID_SURVEY_ACTIVE,
|
||||
NOTICE_ID_UPDATE)
|
||||
from ..notifications import get_datetime_now
|
||||
from ..toolbox import get_context
|
||||
from ..toolbox_settings import save_settings
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_link(Operator):
|
||||
bl_idname = "poliigon.poliigon_link"
|
||||
bl_label = ""
|
||||
bl_description = _t("(Find asset on Poliigon.com in your default browser)")
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
|
||||
mode: 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
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
global cTB
|
||||
|
||||
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)
|
||||
|
||||
open_free_survey = notice.id_notice == NOTICE_ID_SURVEY_FREE
|
||||
open_paying_survey = notice.id_notice == NOTICE_ID_SURVEY_ACTIVE
|
||||
update_clicked = notice.id_notice == NOTICE_ID_UPDATE
|
||||
if open_free_survey or open_paying_survey:
|
||||
time_now = get_datetime_now()
|
||||
cTB.settings["last_nps_open"] = time_now.timestamp()
|
||||
save_settings(cTB)
|
||||
webbrowser.open(notice.url)
|
||||
elif update_clicked:
|
||||
# TODO(patrick): Remove reliance on the @struc, and _api itself
|
||||
if "@Logs" in self.mode:
|
||||
cTB._api.open_poliigon_link("changelog")
|
||||
elif "@Update" in self.mode:
|
||||
webbrowser.open(notice.download_url)
|
||||
elif hasattr(notice, "url"):
|
||||
webbrowser.open(notice.url)
|
||||
else:
|
||||
reporting.capture_message(
|
||||
"invalid_notify_url", str(notice), "error")
|
||||
elif self.mode == "survey" and notice is not None:
|
||||
cTB._api.open_poliigon_link(self.mode, env_name=cTB._env.env_name)
|
||||
cTB.notify.clicked_notice(notice)
|
||||
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 in cTB._api._url_paths:
|
||||
cTB._api.open_poliigon_link(self.mode, env_name=cTB._env.env_name)
|
||||
elif self.mode.startswith("https:"):
|
||||
webbrowser.open(self.mode)
|
||||
else:
|
||||
# Assume passed in asset id, open asset page.
|
||||
asset_id = int(self.mode)
|
||||
cTB.open_asset_url(asset_id)
|
||||
|
||||
if notice is not None:
|
||||
cTB.notify.dismiss_notice(notice)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,152 @@
|
||||
# #### 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 #####
|
||||
|
||||
from typing import List, Tuple
|
||||
import os
|
||||
import re
|
||||
|
||||
from bpy.props import (IntProperty,
|
||||
StringProperty)
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..modules.poliigon_core.assets import SIZES
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_load_asset_size_from_list(Operator):
|
||||
# NOTE: This operator is considered internal and therefore not translated
|
||||
|
||||
bl_idname = "poliigon.load_asset_size_from_list"
|
||||
bl_label = "Import list of files as an asset"
|
||||
bl_description = "Import an asset from a list of files"
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
|
||||
# Use negative asset IDs for, now,
|
||||
# caller to keep track to keep unique within session.
|
||||
asset_id: IntProperty(default=-1, options={'SKIP_SAVE'}) # noqa: F821
|
||||
asset_name: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
asset_type: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
file_list_json: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
size: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
lod: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
convention: IntProperty(default=1, options={'SKIP_SAVE'}) # 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 _validate_properties(self) -> None:
|
||||
"""Validates received parameters.
|
||||
|
||||
Raise ValueError, if validation failed.
|
||||
"""
|
||||
|
||||
if self.asset_id >= 0:
|
||||
msg = f"Only negative asset IDs allowed, for now (not {self.asset_id})"
|
||||
self.report({"ERROR"}, msg)
|
||||
raise ValueError(msg)
|
||||
if len(self.asset_name) == 0:
|
||||
msg = "Please specify an asset name"
|
||||
self.report({"ERROR"}, msg)
|
||||
raise ValueError(msg)
|
||||
if self.asset_type not in ["HDRIs", "Models", "Textures"]:
|
||||
msg = (f"Unknown asset type: {self.asset_type}\n"
|
||||
"Known types: HDRIs, Models, Textures")
|
||||
self.report({"ERROR"}, msg)
|
||||
raise ValueError(msg)
|
||||
if len(self.size) > 0:
|
||||
try:
|
||||
if self.size[-1] == "K":
|
||||
int(self.size[:-1])
|
||||
else:
|
||||
int(self.size)
|
||||
except ValueError:
|
||||
msg = (f"Unknown size string: '{self.size}'\n"
|
||||
"Expected something like: '256', '2K' or '16K'")
|
||||
raise ValueError(msg)
|
||||
if len(self.lod) > 0 and not self.lod.startswith("LOD"):
|
||||
msg = (f"Unknown LOD string format: {self.lod}\n"
|
||||
"Expected something like: 'LOD0'")
|
||||
self.report({"ERROR"}, msg)
|
||||
raise ValueError(msg)
|
||||
if self.convention < 0:
|
||||
msg = (f"Invalid asset convention: {self.convention}\n"
|
||||
"Expected values: convention >= 0")
|
||||
self.report({"ERROR"}, msg)
|
||||
raise ValueError(msg)
|
||||
# TODO(Andreas): Any additional validation needed?
|
||||
# E.g. test if file types in file_list_json actually
|
||||
# match file tags?`Like an "xyz.fbx" for COL channel?
|
||||
|
||||
def _derive_properties_from_files(
|
||||
self,
|
||||
tex_maps: List[str]
|
||||
) -> Tuple[List[str], List[str], List[str]]:
|
||||
"""Derives workflows, sizes and LODs from filenames."""
|
||||
|
||||
workflows = []
|
||||
sizes = []
|
||||
lods = []
|
||||
for path in tex_maps:
|
||||
dir_parent = os.path.basename(os.path.dirname(path))
|
||||
filename = os.path.basename(path)
|
||||
filename_no_ext, _ = os.path.splitext(filename)
|
||||
filename_parts = filename_no_ext.split("_")
|
||||
for part in filename_parts:
|
||||
match_size = re.search(r"(\d+K)", part)
|
||||
match_lod = re.search(r"(LOD\d)", part)
|
||||
|
||||
if part in ["METALNESS", "SPECULAR", "REGULAR"]:
|
||||
workflows.append(part)
|
||||
elif match_size is not None:
|
||||
sizes.append(part)
|
||||
elif match_lod is not None:
|
||||
lods.append(part)
|
||||
elif dir_parent in SIZES:
|
||||
sizes.append(dir_parent)
|
||||
|
||||
workflows = list(set(workflows))
|
||||
sizes = list(set(sizes))
|
||||
lods = list(set(lods))
|
||||
|
||||
return workflows, sizes, lods
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
# Deliberately not catching ValueError, here.
|
||||
# Scripts using this operator are supposed to fail.
|
||||
self._validate_properties()
|
||||
|
||||
cTB._asset_index.load_asset_from_list(
|
||||
self.asset_id,
|
||||
self.asset_name,
|
||||
self.asset_type,
|
||||
self.size,
|
||||
self.lod,
|
||||
"METALNESS",
|
||||
self.file_list_json,
|
||||
convention=self.convention,
|
||||
# "my_assets",
|
||||
# -1,
|
||||
# 1000000
|
||||
)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,143 @@
|
||||
# #### 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 time
|
||||
from threading import Event
|
||||
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty)
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..modules.poliigon_core.api_remote_control import ApiJob
|
||||
from ..modules.poliigon_core.api_remote_control_params import (
|
||||
CATEGORY_ALL,
|
||||
KEY_TAB_MY_ASSETS,
|
||||
KEY_TAB_ONLINE)
|
||||
from ..constants import ASSET_ID_ALL
|
||||
from ..toolbox import get_context
|
||||
|
||||
|
||||
class POLIIGON_OT_get_local_asset_sync(Operator):
|
||||
bl_idname = "poliigon.get_local_asset_sync"
|
||||
bl_label = "For internal testing, only"
|
||||
bl_description = "For internal testing, only"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
timeout: FloatProperty(options={"HIDDEN"}, default=60.0) # noqa: F821
|
||||
asset_id: IntProperty(options={"HIDDEN"}, default=ASSET_ID_ALL) # noqa: F821
|
||||
await_startup_poliigon: BoolProperty(options={"HIDDEN"}, default=True) # noqa: F821
|
||||
await_startup_my_assets: BoolProperty(options={"HIDDEN"}, default=True) # noqa: F821
|
||||
get_poliigon: BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
|
||||
get_my_assets: BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
|
||||
abort_ongoing_jobs: 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 _callback_get_asset_done(self, job: ApiJob) -> None:
|
||||
"""Calls the standard done callback and sets local event afterwards."""
|
||||
|
||||
cTB.callback_get_asset_done(job)
|
||||
|
||||
last_page = job.params.idx_page >= job.result.body.get("last_page", -1)
|
||||
if last_page:
|
||||
self.ev_done.set()
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
if await_my_assets:
|
||||
fetching_my_assets = cTB.fetching_asset_data[KEY_TAB_MY_ASSETS]
|
||||
startup_done &= len(fetching_my_assets) == 0
|
||||
return startup_done
|
||||
|
||||
def _await_end_of_startup(self) -> None:
|
||||
to_wait_s = int(self.timeout)
|
||||
startup_done = False
|
||||
while to_wait_s > 0:
|
||||
startup_done = self._is_startup_done(
|
||||
await_poliigon=self.await_startup_poliigon,
|
||||
await_my_assets=self.await_startup_my_assets)
|
||||
if startup_done:
|
||||
break
|
||||
time.sleep(1)
|
||||
to_wait_s -= 1
|
||||
if not startup_done:
|
||||
print("FAILED TO AWAIT STARTUP END")
|
||||
|
||||
def _do_sync_get_assets(self) -> bool:
|
||||
tabs = []
|
||||
if self.get_poliigon:
|
||||
tabs.append(KEY_TAB_ONLINE)
|
||||
if self.get_my_assets:
|
||||
tabs.append(KEY_TAB_MY_ASSETS)
|
||||
|
||||
search = None
|
||||
if self.asset_id != ASSET_ID_ALL:
|
||||
asset_ids = cTB._asset_index.get_asset_id_list()
|
||||
if self.asset_id in asset_ids:
|
||||
return True
|
||||
|
||||
search = str(self.asset_id)
|
||||
|
||||
self.ev_done = Event()
|
||||
|
||||
for _tab in tabs:
|
||||
cTB.f_GetAssets(
|
||||
area=_tab,
|
||||
categories=[CATEGORY_ALL],
|
||||
search=search,
|
||||
force=True,
|
||||
callback_done=self._callback_get_asset_done
|
||||
)
|
||||
if not self.ev_done.wait(self.timeout):
|
||||
msg = ("POLIIGON_OT_get_local_asset_sync: Failed to get assets, "
|
||||
f"tab: {_tab}")
|
||||
cTB.logger.error(msg)
|
||||
return False
|
||||
|
||||
self.ev_done.clear()
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
if self.abort_ongoing_jobs:
|
||||
cTB.api_rc.wait_for_all(do_wait=False)
|
||||
|
||||
self._await_end_of_startup()
|
||||
|
||||
if not self.get_poliigon and not self.get_my_assets:
|
||||
return {"FINISHED"}
|
||||
|
||||
result = self._do_sync_get_assets()
|
||||
if not result:
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,407 @@
|
||||
# #### 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 #####
|
||||
|
||||
from typing import List
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
StringProperty)
|
||||
import bpy.utils.previews
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..material_import_utils import find_identical_material
|
||||
from .utils_operator import fill_size_drop_down
|
||||
from ..toolbox import get_context
|
||||
from ..utils import (
|
||||
is_cycles,
|
||||
is_eevee_next)
|
||||
from .. import reporting
|
||||
|
||||
|
||||
UV_OPTIONS = [("UV", "UV", "UV"),
|
||||
("MOSAIC", "Mosaic", "Poliigon Mosaic"),
|
||||
("FLAT", "Flat", "Flat"),
|
||||
("BOX", "Box", "Box"),
|
||||
("SPHERE", "Sphere", "Sphere"),
|
||||
("TUBE", "Tube", "Tube")]
|
||||
|
||||
|
||||
def set_op_mat_disp_strength(ops, asset_name: str, mode_disp: str) -> None:
|
||||
if asset_name.startswith("Poliigon_"):
|
||||
ops.displacement = 0.2
|
||||
elif mode_disp == "MICRO":
|
||||
ops.displacement = 0.05
|
||||
else:
|
||||
ops.displacement = 0.0
|
||||
|
||||
|
||||
class POLIIGON_OT_material(Operator):
|
||||
bl_idname = "poliigon.poliigon_material"
|
||||
bl_label = _t("Poliigon Material Import")
|
||||
bl_description = _t("Create Material")
|
||||
bl_options = {"GRAB_CURSOR", "BLOCKING", "REGISTER", "INTERNAL", "UNDO"}
|
||||
|
||||
def _get_dispopts(self, context):
|
||||
options = [
|
||||
("NORMAL", "Normal Only", "Use the Normal Map for surface details")
|
||||
]
|
||||
|
||||
if is_cycles() or is_eevee_next():
|
||||
# Only cycles and 4.2's eevee next support displacement
|
||||
options.append(
|
||||
("BUMP",
|
||||
"Bump Only",
|
||||
("Use the displacement map for surface details without "
|
||||
"displacement"))
|
||||
)
|
||||
options.append(
|
||||
("DISP",
|
||||
"Displacement and Bump",
|
||||
("Use the displacement map for surface details and physical "
|
||||
"displacement"))
|
||||
)
|
||||
|
||||
if is_cycles():
|
||||
# While Eevee next in blender 4.2 could have the above options,
|
||||
# below is still only relevnat in cycles.
|
||||
options.append(
|
||||
("MICRO",
|
||||
"Adaptive Displacement Only",
|
||||
("Use the displacement map for physical displacement with "
|
||||
"adaptive geometry subdivisions\n"
|
||||
"Note: This has a high render performance cost!"))
|
||||
)
|
||||
return options
|
||||
|
||||
def _fill_size_drop_down(self, context):
|
||||
return fill_size_drop_down(cTB, self.asset_id)
|
||||
|
||||
def _update_displacement_options(self, context):
|
||||
# We can not access self.asset_data, here!
|
||||
asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
if asset_data.local_convention == 1:
|
||||
self.displacement = 0.2
|
||||
else:
|
||||
self.displacement = 0.05
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
|
||||
size: EnumProperty(
|
||||
name=_t("Texture"), # noqa: F821
|
||||
items=_fill_size_drop_down,
|
||||
description=_t("Change size of assigned textures.") # noqa: F722
|
||||
)
|
||||
mapping: EnumProperty(
|
||||
name=_t("Mapping"), # noqa: F821
|
||||
items=UV_OPTIONS,
|
||||
default="UV" # noqa: F821
|
||||
)
|
||||
scale: FloatProperty(
|
||||
name=_t("Scale"), # noqa: F821
|
||||
default=1.0
|
||||
)
|
||||
if bpy.app.version >= (3, 0):
|
||||
mode_disp: EnumProperty(name="Displacement", # noqa: F821
|
||||
items=_get_dispopts,
|
||||
default=0, # noqa: F821
|
||||
update=_update_displacement_options)
|
||||
else:
|
||||
mode_disp: EnumProperty(name="Displacement", # noqa: F821
|
||||
items=_get_dispopts,
|
||||
update=_update_displacement_options)
|
||||
|
||||
displacement: FloatProperty(
|
||||
name=_t("Displacement Strength"), # noqa: F722
|
||||
default=0.0
|
||||
)
|
||||
use_16bit: BoolProperty(
|
||||
name=_t("16-Bit Textures (if any)"), # noqa: F722
|
||||
default=False
|
||||
)
|
||||
reuse_material: BoolProperty(
|
||||
name=_t("Reuse Material"), # noqa: F722
|
||||
default=True
|
||||
)
|
||||
keep_unused_tex_nodes: BoolProperty(
|
||||
name=_t("Keep Additional Texture Nodes"), # noqa: F722
|
||||
default=True
|
||||
)
|
||||
# TODO(Andreas): Can do_rename be removed, seems not in use at all?
|
||||
do_rename: BoolProperty(options={"HIDDEN"}) # noqa: F821
|
||||
do_apply: BoolProperty(options={"HIDDEN"}, default=True) # noqa: F821
|
||||
ignore_map_prefs: BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Runs once per operator call before drawing occurs."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Note: During property update handlers (like e.g.
|
||||
# _update_displacement_options()) we can not rely on on these
|
||||
# members to be defined!
|
||||
self.asset_data = None
|
||||
self.exec_count = 0
|
||||
|
||||
@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 draw(self, context):
|
||||
if self.asset_data is None:
|
||||
self.asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
|
||||
is_backplate = self.asset_data.is_backplate()
|
||||
|
||||
col = self.layout.column()
|
||||
col.prop(self, "size")
|
||||
row = col.row()
|
||||
row.prop(self, "mapping")
|
||||
row.enabled = not is_backplate
|
||||
if is_cycles() or is_eevee_next():
|
||||
row = col.row()
|
||||
row.prop(self, "mode_disp")
|
||||
row.enabled = not is_backplate
|
||||
if self.mode_disp != "NORMAL" and (is_cycles() or is_eevee_next()):
|
||||
row = col.row()
|
||||
row.prop(self, "displacement")
|
||||
row.enabled = not is_backplate
|
||||
row = col.row()
|
||||
row.prop(self, "scale") # TODO(Andreas): implement for backplate?
|
||||
row.enabled = not is_backplate
|
||||
row = col.row()
|
||||
row.prop(self, "use_16bit")
|
||||
row.enabled = not is_backplate
|
||||
col.prop(self, "reuse_material")
|
||||
row = col.row()
|
||||
row.prop(self, "keep_unused_tex_nodes")
|
||||
row.enabled = not self.reuse_material
|
||||
|
||||
def evaluate_displacement_method_property(self) -> None:
|
||||
"""Tries to set dislacement method from prefs, if not set already."""
|
||||
|
||||
if self.properties.is_property_set("mode_disp"):
|
||||
return
|
||||
|
||||
try:
|
||||
self.mode_disp = cTB.prefs.mode_disp
|
||||
except TypeError:
|
||||
# Could be Eevee active while assigning a displacement value
|
||||
self.mode_disp = "NORMAL"
|
||||
except AttributeError as e:
|
||||
reporting.capture_exception(e)
|
||||
self.mode_disp = "NORMAL"
|
||||
|
||||
def determine_objects_needing_subdiv(
|
||||
self, context) -> List[bpy.types.Object]:
|
||||
"""Returns a list of selected objects, which still need a Subdivision
|
||||
modifier for microdisplacement.
|
||||
"""
|
||||
|
||||
if not is_cycles() or self.mode_disp != "MICRO":
|
||||
return []
|
||||
|
||||
objs_selected = [_obj for _obj in context.selected_objects]
|
||||
objs_add_subdiv = [_obj
|
||||
for _obj in objs_selected
|
||||
if "Subdivision" not in _obj.modifiers]
|
||||
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
|
||||
modifier = _obj.modifiers.new("Subdivision", "SUBSURF")
|
||||
if modifier is None:
|
||||
# TODO(Andreas): Would we want to report failure to create modifier?
|
||||
continue
|
||||
modifier.subdivision_type = "SIMPLE"
|
||||
modifier.levels = 0 # Don't do subdiv in viewport
|
||||
|
||||
def check_material_reuse(self) -> bool:
|
||||
"""Optionally re-uses an already imported identical material."""
|
||||
|
||||
if not self.reuse_material:
|
||||
return False
|
||||
|
||||
identical_mat = find_identical_material(
|
||||
self.asset_data,
|
||||
self.size,
|
||||
self.mapping,
|
||||
self.scale,
|
||||
self.displacement,
|
||||
self.use_16bit,
|
||||
self.mode_disp
|
||||
)
|
||||
|
||||
if identical_mat is None:
|
||||
return False
|
||||
|
||||
# Prevent duplicate materials from being created unintentionally
|
||||
# but we probably want to provide an option for that at some point.
|
||||
cTB.logger.debug("POLIIGON_OT_material Applying existing material: "
|
||||
f"{identical_mat.name}")
|
||||
self.report({"WARNING"}, _t("Applying existing material"))
|
||||
|
||||
result = bpy.ops.poliigon.poliigon_apply(
|
||||
"INVOKE_DEFAULT",
|
||||
asset_id=self.asset_id,
|
||||
name_material=identical_mat.name
|
||||
)
|
||||
if result == {"CANCELLED"}:
|
||||
self.report(
|
||||
{"WARNING"}, _t("Could not apply materials to selection"))
|
||||
else:
|
||||
self.signal_import()
|
||||
return True
|
||||
|
||||
def signal_import(self) -> None:
|
||||
if self.exec_count == 0:
|
||||
cTB.signal_import_asset(asset_id=self.asset_id)
|
||||
self.exec_count += 1
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
self.asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
|
||||
# TODO(Andreas): Can do_rename be removed, seems not in use at all?
|
||||
if self.do_rename:
|
||||
mat = bpy.data.materials[cTB.vActiveMat]
|
||||
cTB.vActiveMat = bpy.context.scene.vEditMatName
|
||||
mat.name = cTB.vActiveMat
|
||||
return {"FINISHED"}
|
||||
|
||||
asset_type_data = self.asset_data.get_type_data()
|
||||
asset_name = self.asset_data.asset_name
|
||||
asset_type = self.asset_data.asset_type
|
||||
|
||||
workflow = asset_type_data.get_workflow("METALNESS")
|
||||
self.size = asset_type_data.get_size(
|
||||
self.size,
|
||||
local_only=True,
|
||||
addon_convention=cTB.addon_convention,
|
||||
local_convention=self.asset_data.local_convention
|
||||
)
|
||||
|
||||
self.evaluate_displacement_method_property()
|
||||
|
||||
objs_add_disp = self.determine_objects_needing_subdiv(context)
|
||||
|
||||
# TODO(Andreas): Not sure what this was supposed to be good for:
|
||||
# msg_error_local = None
|
||||
# msg_error_my_assets = None
|
||||
# if msg_error_local is not None:
|
||||
# self.report({"ERROR"}, msg_error_local)
|
||||
# return {'CANCELLED'}
|
||||
# if msg_error_my_assets is not None:
|
||||
# cTB.print_debug_(
|
||||
# 0, "apply_mat_size_not_local", asset_name, self.size)
|
||||
# self.report({"ERROR"}, msg_error_my_assets)
|
||||
# reporting.capture_message("apply_mat_size_not_local", asset_name)
|
||||
# return {"CANCELLED"}
|
||||
|
||||
cTB.logger.debug(f"POLIIGON_OT_material Size: {self.size}")
|
||||
|
||||
did_reuse = self.check_material_reuse()
|
||||
if did_reuse:
|
||||
return {"FINISHED"}
|
||||
|
||||
tex_maps = asset_type_data.get_maps(
|
||||
workflow=workflow,
|
||||
size=self.size,
|
||||
prefer_16_bit=self.use_16bit,
|
||||
variant=None)
|
||||
if len(tex_maps) == 0:
|
||||
self.report({"WARNING"}, _t("No Textures found."))
|
||||
reporting.capture_message("apply_mat_tex_not_found", asset_name)
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.ignore_map_prefs:
|
||||
map_prefs = None
|
||||
else:
|
||||
map_prefs = cTB.user.map_preferences
|
||||
|
||||
mat = cTB.mat_import.import_material(
|
||||
asset_data=self.asset_data,
|
||||
do_apply=False,
|
||||
workflow="METALNESS",
|
||||
size=self.size,
|
||||
lod="",
|
||||
projection=self.mapping,
|
||||
use_16bit=self.use_16bit,
|
||||
mode_disp=self.mode_disp,
|
||||
translate_x=0.0,
|
||||
translate_y=0.0,
|
||||
scale=self.scale,
|
||||
global_rotation=0.0,
|
||||
aspect_ratio=1.0,
|
||||
displacement=self.displacement,
|
||||
keep_unused_tex_nodes=self.keep_unused_tex_nodes,
|
||||
reuse_existing=False, # we already checked for reusable mats before
|
||||
map_prefs=map_prefs
|
||||
)
|
||||
|
||||
if mat is None:
|
||||
reporting.capture_message(
|
||||
"could_not_create_mat", asset_name, "error")
|
||||
self.report({"ERROR"}, _t("Material could not be created."))
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.add_subdiv_to_objects(objs_add_disp)
|
||||
|
||||
cTB.f_GetSceneAssets()
|
||||
|
||||
# TODO(Andreas): check what it is used for
|
||||
# TODO(Andreas): Even more strange, op.poliigon_active does the
|
||||
# same again...
|
||||
# TODO(Andreas): Even more more strange, op.poliigon_apply then does
|
||||
# this again and also calls op.poliigon_active, again?
|
||||
cTB.vActiveType = asset_type.name # self.vType
|
||||
cTB.active_asset_id = self.asset_id
|
||||
cTB.vActiveMat = mat.name
|
||||
|
||||
bpy.ops.poliigon.poliigon_active(
|
||||
mode="mat", asset_type=cTB.vActiveType, data=cTB.vActiveMat
|
||||
)
|
||||
|
||||
self.signal_import()
|
||||
|
||||
if not self.do_apply:
|
||||
return {"FINISHED"}
|
||||
|
||||
rtn = bpy.ops.poliigon.poliigon_apply(
|
||||
"INVOKE_DEFAULT",
|
||||
asset_id=self.asset_id,
|
||||
name_material=mat.name
|
||||
)
|
||||
if rtn == {"CANCELLED"}:
|
||||
self.report(
|
||||
{"WARNING"}, _t("Could not apply materials to selection"))
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,894 @@
|
||||
# #### 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 #####
|
||||
|
||||
from typing import List, Tuple
|
||||
import os
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
)
|
||||
|
||||
from ..modules.poliigon_core.assets import (
|
||||
AssetData,
|
||||
LODS,
|
||||
MAP_EXT_LOWER,
|
||||
ModelType,
|
||||
VARIANTS)
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..material_import_utils import replace_tex_size
|
||||
from .utils_operator import fill_size_drop_down
|
||||
from ..toolbox import get_context
|
||||
from ..utils import (
|
||||
construct_model_name,
|
||||
f_Ex,
|
||||
f_FExt,
|
||||
f_FName)
|
||||
from .. import reporting
|
||||
|
||||
|
||||
LOD_DESCS = {
|
||||
"NONE": _t("med. poly"),
|
||||
"LOD0": _t("high poly"),
|
||||
"LOD1": _t("med. poly"),
|
||||
"LOD2": _t("low poly"),
|
||||
"LOD3": _t("lower poly"),
|
||||
"LOD4": _t("min. poly"),
|
||||
}
|
||||
LOD_NAME = "{0} ({1})"
|
||||
LOD_DESCRIPTION_FBX = _t("Import the {0} level of detail (LOD) FBX file")
|
||||
|
||||
|
||||
class POLIIGON_OT_model(Operator):
|
||||
bl_idname = "poliigon.poliigon_model"
|
||||
bl_label = _t("Import model")
|
||||
bl_description = _t("Import Model")
|
||||
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
|
||||
|
||||
def _fill_size_drop_down(self, context):
|
||||
return fill_size_drop_down(cTB, self.asset_id)
|
||||
|
||||
def _fill_lod_drop_down(self, context):
|
||||
# Get list of locally available sizes
|
||||
local_lods = []
|
||||
|
||||
local_lods = cTB._asset_index.check_asset_local_lods(
|
||||
self.asset_id, ModelType.FBX)
|
||||
|
||||
items_lod = [
|
||||
("NONE",
|
||||
LOD_NAME.format("NONE", LOD_DESCS["NONE"]),
|
||||
_t("Import the med. poly level of detail (LOD) .blend file"))
|
||||
]
|
||||
for _lod, is_local in local_lods.items():
|
||||
if not is_local:
|
||||
continue
|
||||
# Tuple: (id, name, description[, icon, [enum value]])
|
||||
lod_tuple = (_lod,
|
||||
LOD_NAME.format(_lod, LOD_DESCS[_lod]),
|
||||
LOD_DESCRIPTION_FBX.format(LOD_DESCS[_lod]))
|
||||
# Note: Usually we rather do a list(set()) afterwards,
|
||||
# but in this case order is important!
|
||||
if lod_tuple not in items_lod:
|
||||
items_lod.append(lod_tuple)
|
||||
|
||||
return items_lod
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
|
||||
do_use_collection: BoolProperty(
|
||||
name=_t("Import as collection"), # noqa: F722
|
||||
description=_t("Instance model from a reusable collection"), # noqa: F722
|
||||
default=False
|
||||
)
|
||||
do_reuse_materials: BoolProperty(
|
||||
name=_t("Reuse materials"), # noqa: F722
|
||||
description=_t("Reuse already imported materials to avoid duplicates"), # noqa: F722
|
||||
default=False)
|
||||
do_link_blend: BoolProperty(
|
||||
name=_t("Link .blend file"), # noqa: F722
|
||||
description=_t("Link the .blend file instead of appending"), # noqa: F722
|
||||
default=False)
|
||||
size: EnumProperty(
|
||||
name=_t("Texture"), # noqa: F821
|
||||
items=_fill_size_drop_down,
|
||||
description=_t("Change size of assigned textures.")) # noqa: F722
|
||||
lod: EnumProperty(
|
||||
name=_t("LOD"), # noqa: F821
|
||||
items=_fill_lod_drop_down,
|
||||
description=_t("Change LOD of the Model.")) # noqa: F722
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Runs once per operator call before drawing occurs."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Infer the default value on each press from the cached session
|
||||
# setting, which will be updated by redo last but not saved to prefs.
|
||||
self.do_link_blend = cTB.link_blend_session
|
||||
|
||||
self.blend_exists = False
|
||||
self.lod_import = False
|
||||
|
||||
self.asset_data = None
|
||||
self.exec_count = 0
|
||||
|
||||
@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 draw(self, context):
|
||||
prefer_blend = cTB.settings["download_prefer_blend"] and self.blend_exists
|
||||
if not self.blend_exists:
|
||||
label = _t("No local .blend file :")
|
||||
row_link_enabled = False
|
||||
row_sizes_enabled = True
|
||||
elif not prefer_blend:
|
||||
label = _t("Enable preference 'Download + Import .blend' :")
|
||||
row_link_enabled = False
|
||||
row_sizes_enabled = True
|
||||
elif self.lod_import:
|
||||
label = _t("Set 'LOD' to 'NONE' to load .blend :")
|
||||
row_link_enabled = False
|
||||
row_sizes_enabled = True
|
||||
else:
|
||||
label = None
|
||||
row_link_enabled = True
|
||||
row_sizes_enabled = not self.do_link_blend
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(self, "lod")
|
||||
row.enabled = True
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(self, "size")
|
||||
row.enabled = row_sizes_enabled
|
||||
|
||||
self.layout.prop(self, "do_use_collection")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(self, "do_reuse_materials")
|
||||
row.enabled = not (row_link_enabled and self.do_link_blend)
|
||||
|
||||
if label is not None:
|
||||
self.layout.label(text=label)
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(self, "do_link_blend")
|
||||
row.enabled = row_link_enabled
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
"""Runs at least once before first draw call occurs."""
|
||||
|
||||
self.asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
asset_name = self.asset_data.asset_name
|
||||
|
||||
# Save any updated preference (to link or not), if changed via the
|
||||
# redo last menu (without changing the saved preferences value).
|
||||
cTB.link_blend_session = self.do_link_blend
|
||||
|
||||
project_files, textures, size, lod = self.get_model_data(
|
||||
self.asset_data)
|
||||
|
||||
asset_name = construct_model_name(asset_name, size, lod)
|
||||
did_fresh_import = False
|
||||
inst = None
|
||||
new_objs = []
|
||||
|
||||
# Import the model.
|
||||
blend_import = False
|
||||
coll_exists = bpy.data.collections.get(asset_name) is not None
|
||||
if self.do_use_collection is False or not coll_exists:
|
||||
ok, new_objs, blend_import, fbx_fail = self.run_fresh_import(
|
||||
context,
|
||||
project_files,
|
||||
textures,
|
||||
size,
|
||||
lod)
|
||||
if fbx_fail:
|
||||
return {'CANCELLED'}
|
||||
did_fresh_import = True
|
||||
|
||||
# If imported, perform these steps regardless of collection or not.
|
||||
if did_fresh_import:
|
||||
empty = self.setup_empty_parent(context, new_objs, asset_name)
|
||||
if blend_import and not self.do_use_collection:
|
||||
empty.location = bpy.context.scene.cursor.location
|
||||
else:
|
||||
empty = None
|
||||
|
||||
if self.do_use_collection is True:
|
||||
# Move the objects into a subcollection, and place in scene.
|
||||
if did_fresh_import:
|
||||
# Always create a new collection if did a fresh import.
|
||||
cache_coll = bpy.data.collections.new(asset_name)
|
||||
# Ensure all objects are only part of this new collection.
|
||||
for _obj in [empty] + new_objs:
|
||||
for _collection in _obj.users_collection:
|
||||
_collection.objects.unlink(_obj)
|
||||
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:
|
||||
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:
|
||||
err = _t("Failed to get new collection to instance")
|
||||
self.report({"ERROR"}, err)
|
||||
return {"CANCELLED"}
|
||||
|
||||
elif not blend_import:
|
||||
# Make sure the objects imported are all part of the same coll.
|
||||
self.append_cleanup(context, empty)
|
||||
|
||||
# Final notifications and reporting.
|
||||
cTB.f_GetSceneAssets()
|
||||
|
||||
if blend_import:
|
||||
# Fix import info message containing LOD info
|
||||
asset_name = construct_model_name(asset_name, size, "")
|
||||
|
||||
if did_fresh_import is True:
|
||||
fmt = "blend" if blend_import else "FBX"
|
||||
self.report(
|
||||
{"INFO"},
|
||||
_t("Model Imported ({0}) : {1}").format(fmt, asset_name))
|
||||
elif self.do_use_collection and inst is not None:
|
||||
self.report(
|
||||
{"INFO"}, _t("Instance created : ").format(inst.name))
|
||||
else:
|
||||
err = _t(
|
||||
"Failed to import model correctly: {0}").format(asset_name)
|
||||
self.report({"ERROR"}, err)
|
||||
err = f"Failed to import model correctly: {asset_name}"
|
||||
reporting.capture_message("import-model-failed", err, "error")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.exec_count == 0:
|
||||
cTB.signal_import_asset(asset_id=self.asset_id)
|
||||
self.exec_count += 1
|
||||
return {"FINISHED"}
|
||||
|
||||
def _filter_lod_fbxs(self,
|
||||
file_list: List[str]
|
||||
) -> List[str]:
|
||||
"""Returns a list with all FBX files with LOD tag in filename"""
|
||||
|
||||
all_lod_fbxs = []
|
||||
for asset_path in file_list:
|
||||
if f_FExt(asset_path) != ".fbx":
|
||||
continue
|
||||
filename_parts = f_FName(asset_path).split("_")
|
||||
for lod_level in LODS:
|
||||
if lod_level not in filename_parts:
|
||||
continue
|
||||
all_lod_fbxs.append(asset_path)
|
||||
break
|
||||
return all_lod_fbxs
|
||||
|
||||
def get_model_data(self, asset_data: AssetData):
|
||||
asset_type_data = asset_data.get_type_data()
|
||||
|
||||
# Get the intended material size and LOD import to use.
|
||||
size_desired = self.size if self.size is not None else cTB.settings["mres"]
|
||||
size = asset_type_data.get_size(
|
||||
size_desired,
|
||||
local_only=True,
|
||||
addon_convention=cTB._asset_index.addon_convention,
|
||||
local_convention=asset_data.get_convention(local=True))
|
||||
|
||||
prefer_blend = cTB.settings["download_prefer_blend"]
|
||||
|
||||
lod = self.lod
|
||||
if lod == "NONE":
|
||||
if not prefer_blend:
|
||||
lod = "LOD1"
|
||||
else:
|
||||
lod = cTB.settings["lod"]
|
||||
lod = asset_type_data.get_lod(lod)
|
||||
if lod == "NONE":
|
||||
lod = None
|
||||
|
||||
files = {}
|
||||
asset_type_data.get_files(files)
|
||||
files = list(files.keys())
|
||||
all_lod_fbxs = self._filter_lod_fbxs(files)
|
||||
|
||||
files_lod_fbx = [
|
||||
_file for _file in all_lod_fbxs
|
||||
if f_FExt(_file) == ".fbx" and str(lod) in f_FName(_file).split("_")
|
||||
]
|
||||
# Most likely redundant, but rather safe than sorry
|
||||
files_lod_fbx = list(set(files_lod_fbx))
|
||||
|
||||
files_fbx = []
|
||||
files_blend = []
|
||||
for _file in files:
|
||||
filename_ext = f_FExt(_file)
|
||||
is_fbx = filename_ext == ".fbx"
|
||||
is_blend = filename_ext == ".blend" and "_LIB.blend" not in _file
|
||||
|
||||
if not is_fbx and not is_blend:
|
||||
continue
|
||||
|
||||
if is_fbx and lod is not None and len(files_lod_fbx):
|
||||
if _file not in files_lod_fbx:
|
||||
continue
|
||||
|
||||
if is_fbx and lod is None and _file in all_lod_fbxs:
|
||||
continue
|
||||
|
||||
if is_fbx:
|
||||
files_fbx.append(_file)
|
||||
elif is_blend:
|
||||
files_blend.append(_file)
|
||||
|
||||
cTB.logger.debug("POLIIGON_OT_model get_model_data "
|
||||
f"{os.path.basename(_file)}")
|
||||
|
||||
self.blend_exists = len(files_blend) > 0
|
||||
self.lod_import = lod is not None and self.lod != "NONE"
|
||||
|
||||
if prefer_blend and self.blend_exists and not self.lod_import:
|
||||
files_project = files_blend
|
||||
elif not prefer_blend and not files_fbx and self.blend_exists:
|
||||
# Settings specify to import fbx files, but only blend exists.
|
||||
# Needed for asset browser imports and right-click
|
||||
# TODO(Patrick): Migrate to using a use_blend operator arg, so it
|
||||
# can also be a backdoor option.
|
||||
files_project = files_blend
|
||||
else:
|
||||
files_project = files_fbx
|
||||
|
||||
files_tex = []
|
||||
for _file in files:
|
||||
if f_FExt(_file) not in MAP_EXT_LOWER:
|
||||
continue
|
||||
|
||||
if size not in f_FName(_file).split("_"):
|
||||
continue
|
||||
|
||||
if any(
|
||||
_lod in f_FName(_file).split("_") for _lod in LODS
|
||||
) and lod not in f_FName(_file).split("_"):
|
||||
continue
|
||||
|
||||
files_tex.append(_file)
|
||||
|
||||
return files_project, files_tex, size, lod
|
||||
|
||||
def _load_blend(self, path_proj: str):
|
||||
"""Loads all objects from a .blend file."""
|
||||
|
||||
path_proj_norm = os.path.normpath(path_proj)
|
||||
with bpy.data.libraries.load(path_proj_norm,
|
||||
link=self.do_link_blend
|
||||
) as (data_from,
|
||||
data_to):
|
||||
data_to.objects = data_from.objects
|
||||
return data_to.objects
|
||||
|
||||
def _cut_identity_counter(self, s: str) -> str:
|
||||
"""Reduces strings like 'walter.042' to 'walter'"""
|
||||
|
||||
splits = s.rsplit(".", maxsplit=1)
|
||||
if len(splits) > 1 and splits[1].isdecimal():
|
||||
s = splits[0]
|
||||
return s
|
||||
|
||||
def _reuse_materials(self, imported_objs: List, imported_mats: List):
|
||||
"""Re-uses previously imported materials after a .blend import"""
|
||||
|
||||
if not self.do_reuse_materials or self.do_link_blend:
|
||||
return
|
||||
|
||||
# Mark all materials from this import
|
||||
PROP_FRESH_IMPORT = "poliigon_fresh_import"
|
||||
for mat in imported_mats:
|
||||
mat[PROP_FRESH_IMPORT] = True
|
||||
# Find any previously imported materials with same name
|
||||
# and make the objects use those
|
||||
mats_remap = [] # list of tuples (from_mat, to_mat)
|
||||
for obj in imported_objs:
|
||||
mat_on_obj = obj.active_material
|
||||
if mat_on_obj is None:
|
||||
continue
|
||||
materials = reversed(sorted(list(bpy.data.materials.keys())))
|
||||
for name_mat in materials:
|
||||
# Unfortunately we seem to have little control,
|
||||
# where and when Blender adds counter suffixes for
|
||||
# identically named materials.
|
||||
# Therefore we compare names without any counter suffix.
|
||||
name_on_obj_cmp = self._cut_identity_counter(mat_on_obj.name)
|
||||
name_mat_cmp = self._cut_identity_counter(name_mat)
|
||||
if name_on_obj_cmp != name_mat_cmp:
|
||||
continue
|
||||
mat_reuse = bpy.data.materials[name_mat]
|
||||
is_fresh = mat_reuse.get(PROP_FRESH_IMPORT, False)
|
||||
if is_fresh:
|
||||
continue
|
||||
if (mat_on_obj, mat_reuse) not in mats_remap:
|
||||
mats_remap.append((mat_on_obj, mat_reuse))
|
||||
break
|
||||
# Remove previously added marker
|
||||
for mat in imported_mats:
|
||||
if PROP_FRESH_IMPORT in mat.keys():
|
||||
del mat[PROP_FRESH_IMPORT]
|
||||
# Finally remap the materials and remove those freshly imported ones
|
||||
did_send_sentry = False
|
||||
for from_mat, to_mat in mats_remap:
|
||||
from_mat.user_remap(to_mat)
|
||||
from_mat.user_clear()
|
||||
if from_mat in imported_mats:
|
||||
imported_mats.remove(from_mat)
|
||||
if from_mat.users != 0 and not did_send_sentry:
|
||||
msg = ("User count not zero on material replaced by reuse: "
|
||||
f"Asset: {self.asset_id}, Material: {from_mat.name}")
|
||||
reporting.capture_message(
|
||||
"import_model_mat_reuse_user_count",
|
||||
msg,
|
||||
"info")
|
||||
did_send_sentry = True
|
||||
continue
|
||||
try:
|
||||
bpy.data.materials.remove(from_mat)
|
||||
except Exception as e:
|
||||
reporting.capture_exception(e)
|
||||
self.report({"WARNING"},
|
||||
_t("Failed to remove material after reuse."))
|
||||
|
||||
# TODO(Andreas): replace usage with AssetIndex/AssetData alternative
|
||||
@staticmethod
|
||||
def f_GetVar(name: str) -> str:
|
||||
for _variant in VARIANTS:
|
||||
if _variant in name:
|
||||
return _variant
|
||||
return None
|
||||
|
||||
def run_fresh_import(self,
|
||||
context,
|
||||
project_files: List[str],
|
||||
files_tex: List[str],
|
||||
size: str,
|
||||
lod: str
|
||||
) -> Tuple[bool,
|
||||
List[bpy.types.Object],
|
||||
bool,
|
||||
bool]:
|
||||
"""Performs a fresh import of the whole model.
|
||||
|
||||
There can be multiple FBX models, therefore we want to import all of
|
||||
them and verify each one was properly imported.
|
||||
|
||||
Return values:
|
||||
Tuple[0] - True, if all FBX files have been imported successfully
|
||||
Tuple[1] - List of all imported mesh objects
|
||||
Tuple[2] - True, if it was a .blend import instead of an FBX import
|
||||
Tuple[3] - True, if an error occurred during FBX loading
|
||||
"""
|
||||
|
||||
PROP_LIBRARY_LINKED = "poliigon_linked"
|
||||
|
||||
asset_name = self.asset_data.asset_name
|
||||
|
||||
meshes_all = []
|
||||
dict_mats_imported = {}
|
||||
imported_proj = []
|
||||
blend_import = False
|
||||
for path_proj in project_files:
|
||||
filename_base = f_FName(path_proj)
|
||||
|
||||
if not f_Ex(path_proj):
|
||||
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}"
|
||||
reporting.capture_message("model_fbx_missing", err, "info")
|
||||
continue
|
||||
|
||||
list_objs_before = list(context.scene.objects)
|
||||
|
||||
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)
|
||||
|
||||
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))
|
||||
did_full_import = False
|
||||
meshes_all = []
|
||||
blend_import = False
|
||||
fbx_error = True
|
||||
return (did_full_import,
|
||||
meshes_all,
|
||||
blend_import,
|
||||
fbx_error)
|
||||
|
||||
imported_proj.append(path_proj)
|
||||
vMeshes = [_obj for _obj in list(context.scene.objects)
|
||||
if _obj not in list_objs_before]
|
||||
|
||||
meshes_all += vMeshes
|
||||
|
||||
if ext_proj == ".blend":
|
||||
for _mesh in vMeshes:
|
||||
# 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)
|
||||
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)
|
||||
|
||||
# There could have been multiple FBXs, consider fully imported
|
||||
# for user-popup reporting if all FBX files imported.
|
||||
did_full_import = len(imported_proj) == len(project_files)
|
||||
|
||||
return did_full_import, meshes_all, blend_import, False
|
||||
|
||||
def set_poliigon_props_model(
|
||||
self, obj: bpy.types.Object, lod: str) -> None:
|
||||
"""Sets Poliigon properties on an imported object."""
|
||||
|
||||
obj.poliigon_props.asset_id = self.asset_data.asset_id
|
||||
obj.poliigon_props.asset_name = self.asset_data.asset_name
|
||||
obj.poliigon_props.asset_type = self.asset_data.asset_type.name
|
||||
obj.poliigon_props.size = self.size
|
||||
if lod is not None:
|
||||
obj.poliigon_props.lod = lod
|
||||
obj.poliigon_props.use_collection = self.do_use_collection
|
||||
obj.poliigon_props.link_blend = self.do_link_blend
|
||||
|
||||
def object_has_children(self, obj_parent: bpy.types.Object) -> bool:
|
||||
"""Returns True, if obj_parent has children."""
|
||||
|
||||
for _obj in bpy.data.objects:
|
||||
if _obj.parent == obj_parent:
|
||||
return True
|
||||
return False
|
||||
|
||||
def setup_empty_parent(self,
|
||||
context,
|
||||
new_meshes: List[bpy.types.Object],
|
||||
asset_name: str
|
||||
) -> bpy.types.Object:
|
||||
"""Parents newly imported objects to a central empty object."""
|
||||
|
||||
radius = 0
|
||||
for _mesh in new_meshes:
|
||||
dimensions = _mesh.dimensions
|
||||
if dimensions.x > radius:
|
||||
radius = dimensions.x
|
||||
if dimensions.y > radius:
|
||||
radius = dimensions.y
|
||||
|
||||
empty = bpy.data.objects.new(
|
||||
name=f"{asset_name}_Empty", object_data=None)
|
||||
if self.do_use_collection:
|
||||
empty.empty_display_size = 0.01
|
||||
else:
|
||||
empty.empty_display_size = radius * 0.5
|
||||
|
||||
layer = context.view_layer.active_layer_collection
|
||||
layer.collection.objects.link(empty)
|
||||
|
||||
for _mesh in new_meshes.copy():
|
||||
if _mesh.type == "EMPTY" and not self.object_has_children(_mesh):
|
||||
bpy.data.objects.remove(_mesh, do_unlink=True)
|
||||
new_meshes.remove(_mesh)
|
||||
continue
|
||||
_mesh.parent = empty
|
||||
|
||||
return empty
|
||||
|
||||
def create_instance(self, context, coll, size, lod):
|
||||
"""Creates an instance of an existing collection int he active view."""
|
||||
|
||||
asset_name = self.asset_data.asset_name
|
||||
|
||||
inst_name = construct_model_name(asset_name, size, lod) + "_Instance"
|
||||
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)
|
||||
inst.location = context.scene.cursor.location
|
||||
inst.empty_display_size = 0.01
|
||||
|
||||
# Set selection and active object.
|
||||
for _obj in context.scene.collection.all_objects:
|
||||
_obj.select_set(False)
|
||||
inst.select_set(True)
|
||||
context.view_layer.objects.active = inst
|
||||
return inst
|
||||
|
||||
def append_cleanup(self, context, root_empty):
|
||||
"""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")
|
||||
return
|
||||
|
||||
# Set empty location
|
||||
root_empty.location = context.scene.cursor.location
|
||||
|
||||
# Deselect all others in scene (faster than using operator call).
|
||||
for _obj in context.scene.collection.all_objects:
|
||||
_obj.select_set(False)
|
||||
|
||||
# Make empty active and select it + children.
|
||||
root_empty.select_set(True)
|
||||
context.view_layer.objects.active = root_empty
|
||||
for _obj in root_empty.children:
|
||||
_obj.select_set(True)
|
||||
@@ -0,0 +1,73 @@
|
||||
# #### 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 ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_notice_operator(Operator):
|
||||
bl_idname = "poliigon.notice_operator"
|
||||
bl_label = ""
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
notice_id: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
ops_name: 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
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
# Execute the operator via breaking into parts e.g. "wm.quit_blender"
|
||||
atr = self.ops_name.split(".")
|
||||
if len(atr) != 2:
|
||||
reporting.capture_message("bad_notice_operator", self.ops_name)
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Safeguard to avoid injection.
|
||||
if self.ops_name not in ("wm.quit_blender"):
|
||||
cTB.logger.error("POLIIGON_OT_notice_operator Unsupported "
|
||||
f"operation: {self.ops_name}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
notice = cTB.notify.get_top_notice(do_signal_view=False)
|
||||
if notice is not None:
|
||||
cTB.notify.clicked_notice(notice)
|
||||
|
||||
cTB.logger.debug(
|
||||
f"POLIIGON_OT_notice_operator Running {self.ops_name}")
|
||||
|
||||
# Using invoke acts like in the interface, so any "save?" dialogue
|
||||
# will pick up, for instance if a "quit" operator.
|
||||
getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT')
|
||||
if notice is not None:
|
||||
cTB.notify.dismiss_notice(notice)
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,96 @@
|
||||
# #### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import os
|
||||
import re
|
||||
import webbrowser
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import IntProperty, StringProperty
|
||||
|
||||
from ..modules.poliigon_core.assets import AssetType
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from ..utils import open_dir
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_options(Operator):
|
||||
bl_idname = "poliigon.poliigon_asset_options"
|
||||
bl_label = ""
|
||||
bl_description = _t("Asset Options")
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
mode: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
asset_id: IntProperty(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
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
global cTB
|
||||
|
||||
asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
asset_name = asset_data.asset_name
|
||||
asset_type_data = asset_data.get_type_data()
|
||||
dict_asset_files = {}
|
||||
asset_type_data.get_files(dict_asset_files)
|
||||
asset_files = list(dict_asset_files.keys())
|
||||
|
||||
if self.mode == "dir":
|
||||
directories = sorted(
|
||||
list(set([os.path.dirname(_file)
|
||||
for _file in asset_files]))
|
||||
)
|
||||
for idx_dir in range(len(directories)):
|
||||
if asset_name in directories[idx_dir]:
|
||||
directories[idx_dir] = directories[idx_dir].split(
|
||||
asset_name)[0] + asset_name
|
||||
directories = sorted(list(set(directories)))
|
||||
|
||||
for _dir in directories:
|
||||
open_dir(_dir)
|
||||
|
||||
elif self.mode == "link":
|
||||
asset_type = asset_data.asset_type
|
||||
url_name = asset_name
|
||||
url_name = re.sub(r"(?<=[a-z])(?=[A-Z])", " ", url_name)
|
||||
url_name = (
|
||||
(re.sub(r"(?<=[a-z])(?=[0-9])", " ", url_name))
|
||||
.lower()
|
||||
.replace(" ", "-")
|
||||
)
|
||||
if asset_type == AssetType.TEXTURE:
|
||||
url = f"https://www.poliigon.com/texture/{url_name}"
|
||||
elif asset_type == AssetType.MODEL:
|
||||
url = f"https://www.poliigon.com/model/{url_name}"
|
||||
webbrowser.open(url)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,295 @@
|
||||
# #### 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.props import StringProperty
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..modules.poliigon_core.api import TIMEOUT
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..modules.poliigon_core.upgrade_content import UpgradeContent
|
||||
from ..dialogs.utils_dlg import get_ui_scale, wrapped_label
|
||||
from ..constants import POPUP_WIDTH_LABEL
|
||||
from ..toolbox import (
|
||||
get_context,
|
||||
t_check_change_plan_response)
|
||||
from .. import reporting
|
||||
|
||||
|
||||
# A bit longer than API timeouts
|
||||
TIMEOUT_CHANGE_PLAN = TIMEOUT + 1.0
|
||||
|
||||
|
||||
class POLIIGON_OT_popup_change_plan(Operator):
|
||||
"""This operator provides the 'subscription plan update' details popup
|
||||
dialog.
|
||||
"""
|
||||
|
||||
bl_idname = "poliigon.popup_change_plan"
|
||||
bl_label = _t("Change Plan")
|
||||
bl_options = {"INTERNAL", "REGISTER", "UNDO"}
|
||||
|
||||
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
|
||||
|
||||
def invoke(self, context, event):
|
||||
cTB.plan_upgrade_in_progress = False
|
||||
cTB.plan_upgrade_finished = False
|
||||
cTB.msg_plan_upgrade_finished = None
|
||||
cTB.upgrade_manager.emit_signal(clicked=True)
|
||||
return context.window_manager.invoke_props_popup(self, event)
|
||||
|
||||
def _draw_plan_details(
|
||||
self,
|
||||
layout: bpy.types.UILayout,
|
||||
upgrade_content: UpgradeContent
|
||||
) -> None:
|
||||
"""Draws the plan details table."""
|
||||
|
||||
upgrade_popup_table = upgrade_content.upgrade_popup_table
|
||||
upgrade_popup_key_value = upgrade_content.upgrade_popup_key_value
|
||||
if upgrade_popup_table is None and upgrade_popup_key_value is None:
|
||||
return
|
||||
|
||||
row = layout.row()
|
||||
col_left = row.column(align=True)
|
||||
col_right = row.column(align=True)
|
||||
|
||||
if upgrade_popup_table is not None:
|
||||
for _label, _value in upgrade_popup_table.items():
|
||||
col_left.label(text=_label)
|
||||
col_right.label(text=_value)
|
||||
|
||||
col_left.label(text=_t(" "))
|
||||
col_right.label(text=_t(" "))
|
||||
|
||||
if upgrade_popup_key_value is not None:
|
||||
for _label, _value in upgrade_popup_key_value.items():
|
||||
col_left.label(text=_label)
|
||||
col_right.label(text=_value)
|
||||
|
||||
layout.separator()
|
||||
|
||||
def _draw_note(
|
||||
self,
|
||||
layout: bpy.types.UILayout,
|
||||
label_width: float,
|
||||
upgrade_content: UpgradeContent
|
||||
) -> None:
|
||||
"""Draws a text note (e.g. tax advice...)."""
|
||||
|
||||
text = upgrade_content.upgrade_popup_text
|
||||
if text is None:
|
||||
return
|
||||
|
||||
row = layout.row()
|
||||
col = row.column(align=True)
|
||||
wrapped_label(
|
||||
cTB,
|
||||
width=label_width,
|
||||
text=text,
|
||||
container=col,
|
||||
add_padding_bottom=True)
|
||||
|
||||
def _draw_button_change_plan(
|
||||
self,
|
||||
layout: bpy.types.UILayout,
|
||||
upgrade_content: UpgradeContent
|
||||
) -> None:
|
||||
"""Draws the confirmation button to actually change the subscription
|
||||
plan.
|
||||
"""
|
||||
|
||||
label = upgrade_content.upgrade_popup_confirm_button
|
||||
|
||||
row_button = layout.row()
|
||||
row_button.operator_context = 'INVOKE_DEFAULT'
|
||||
op = row_button.operator(
|
||||
"poliigon.change_plan", text=label)
|
||||
op.tooltip = _t("Confirm the change of your subscription plan")
|
||||
|
||||
def _draw_legal(
|
||||
self,
|
||||
layout: bpy.types.UILayout,
|
||||
upgrade_content: UpgradeContent
|
||||
) -> None:
|
||||
"""Draws legal web links."""
|
||||
|
||||
row = layout.row()
|
||||
col = row.column(align=True)
|
||||
|
||||
if upgrade_content.upgrade_popup_pricing_button is not None:
|
||||
label = upgrade_content.upgrade_popup_pricing_button
|
||||
op_pricing = col.operator("poliigon.poliigon_link",
|
||||
text=label,
|
||||
emboss=False)
|
||||
op_pricing.tooltip = _t("View all pricing details online")
|
||||
op_pricing.mode = "subscribe"
|
||||
|
||||
if upgrade_content.upgrade_popup_terms_button is not None:
|
||||
label = upgrade_content.upgrade_popup_terms_button
|
||||
op_terms = col.operator("poliigon.poliigon_link",
|
||||
text=label,
|
||||
emboss=False)
|
||||
op_terms.tooltip = _t("View our terms & policy documents online")
|
||||
op_terms.mode = "terms_policy"
|
||||
|
||||
@reporting.handle_draw()
|
||||
def draw(self, context):
|
||||
label_width = POPUP_WIDTH_LABEL * get_ui_scale(cTB)
|
||||
# Accounting for the left+right border columns (eyeballing):
|
||||
if bpy.app.version >= (4, 0):
|
||||
label_width -= 10.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)
|
||||
else:
|
||||
label_width -= 10.0
|
||||
|
||||
col_content = self.layout.column()
|
||||
|
||||
upgrade_content = cTB.upgrade_manager.content
|
||||
|
||||
self._draw_plan_details(col_content, upgrade_content)
|
||||
|
||||
self._draw_note(col_content, label_width, upgrade_content)
|
||||
|
||||
self._draw_button_change_plan(col_content, upgrade_content)
|
||||
|
||||
self._draw_legal(col_content, upgrade_content)
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POLIIGON_OT_banner_change_plan_dismiss(Operator):
|
||||
"""This operator provides the functionality to dismiss any
|
||||
'subscription plan update' banner (if the banner type allows dismissal).
|
||||
"""
|
||||
|
||||
bl_idname = "poliigon.popup_change_plan_dismiss"
|
||||
bl_label = _t("Dismiss upgrade notice")
|
||||
bl_options = {"INTERNAL", "REGISTER"}
|
||||
bl_description = _t("Dismiss upgrade notice")
|
||||
|
||||
@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 cls.bl_description
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
cTB.upgrade_manager.dismiss_upgrade()
|
||||
cTB.refresh_ui()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POLIIGON_OT_banner_finish_dismiss(Operator):
|
||||
"""This operator provides the functionality to dismiss the final
|
||||
success/error banner of a 'subscription plan update'.
|
||||
"""
|
||||
|
||||
bl_idname = "poliigon.banner_finish_dismiss"
|
||||
bl_label = _t("Dismiss this message")
|
||||
bl_options = {"INTERNAL", "REGISTER"}
|
||||
bl_description = _t("Dismiss this message")
|
||||
|
||||
@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 cls.bl_description
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
cTB.plan_upgrade_in_progress = False
|
||||
cTB.plan_upgrade_finished = False
|
||||
cTB.msg_plan_upgrade_finished = None
|
||||
cTB.refresh_ui()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POLIIGON_OT_change_plan(Operator):
|
||||
"""This operator (usually triggered by button in 'change plan' popup)
|
||||
executes the actual change of the subscription plan.
|
||||
"""
|
||||
|
||||
bl_idname = "poliigon.change_plan"
|
||||
bl_label = _t("Change Plan")
|
||||
bl_options = {"INTERNAL", "REGISTER"}
|
||||
|
||||
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
|
||||
|
||||
@reporting.handle_draw()
|
||||
def draw(self, context):
|
||||
return
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
# Close the 'change plan' popup
|
||||
# (yeah, I know, but it indeed works...)
|
||||
# From: https://blender.stackexchange.com/questions/202550/close-a-popup-with-an-op-call
|
||||
context.window.screen = context.window.screen
|
||||
|
||||
cTB.plan_upgrade_in_progress = True
|
||||
cTB.upgrade_manager.finish_upgrade_plan()
|
||||
|
||||
# Just to make sure, the progress banner does not stay on,
|
||||
# even if hell freezes over.
|
||||
if bpy.app.timers.is_registered(t_check_change_plan_response):
|
||||
bpy.app.timers.unregister(t_check_change_plan_response)
|
||||
bpy.app.timers.register(
|
||||
t_check_change_plan_response,
|
||||
first_interval=TIMEOUT_CHANGE_PLAN,
|
||||
persistent=True)
|
||||
|
||||
cTB.refresh_ui()
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,79 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..dialogs.utils_dlg import (
|
||||
get_ui_scale,
|
||||
wrapped_label)
|
||||
from ..constants import (
|
||||
POPUP_WIDTH_LABEL_WIDE,
|
||||
POPUP_WIDTH_WIDE)
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_popup_download_limit(Operator):
|
||||
bl_idname = "poliigon.popup_download_limit"
|
||||
bl_label = _t("Unlimited Fair Use Exceeded")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
msg: StringProperty(options={"HIDDEN"}, default="") # noqa: F821, F722
|
||||
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 not cTB.is_unlimited_user():
|
||||
# We shouldn't even be here. Just a safety to never show this
|
||||
# to non-unlimited users.
|
||||
return {'FINISHED'}
|
||||
|
||||
cTB.signal_popup(popup="DOWNLOAD_RATE_LIMITED")
|
||||
return context.window_manager.invoke_props_dialog(
|
||||
self, width=POPUP_WIDTH_WIDE)
|
||||
|
||||
@reporting.handle_draw()
|
||||
def draw(self, context):
|
||||
label_width = POPUP_WIDTH_LABEL_WIDE * get_ui_scale(cTB)
|
||||
|
||||
col_content = self.layout.column()
|
||||
wrapped_label(
|
||||
cTB,
|
||||
width=label_width,
|
||||
text=self.msg,
|
||||
container=col_content,
|
||||
add_padding_bottom=True)
|
||||
|
||||
op = col_content.operator(
|
||||
"poliigon.poliigon_link", text=_t("Learn More"))
|
||||
op.mode = "unlimited_plan_help"
|
||||
op.tooltip = _t("See more info about Unlimited Fair Use policy on our "
|
||||
"website.")
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
cTB.refresh_ui()
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,79 @@
|
||||
# #### 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 ..dialogs.utils_dlg import (
|
||||
get_ui_scale,
|
||||
wrapped_label)
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_popup_message(Operator):
|
||||
bl_idname = "poliigon.popup_message"
|
||||
bl_label = ""
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
message_body: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
message_url: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
notice_id: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
|
||||
target_width = 400
|
||||
|
||||
@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 invoke(self, context, event):
|
||||
width = self.target_width # Blender handles scaling to ui.
|
||||
|
||||
notice = cTB.notify.get_top_notice(do_signal_view=False)
|
||||
if notice is not None:
|
||||
cTB.notify.clicked_notice(notice)
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=width)
|
||||
|
||||
@reporting.handle_draw()
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
target_wrap = self.target_width * get_ui_scale(cTB)
|
||||
target_wrap -= 25 * get_ui_scale(cTB)
|
||||
wrapped_label(cTB, target_wrap, self.message_body, layout)
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
if self.message_url:
|
||||
bpy.ops.wm.url_open(url=self.message_url)
|
||||
|
||||
notice = cTB.notify.get_top_notice(do_signal_view=False)
|
||||
if notice is not None:
|
||||
cTB.notify.dismiss_notice(notice)
|
||||
|
||||
cTB.refresh_ui()
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,176 @@
|
||||
# #### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import os
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
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
|
||||
from ..toolbox_settings import save_settings
|
||||
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")
|
||||
bl_options = {"INTERNAL", "REGISTER", "UNDO"}
|
||||
|
||||
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 _load_images(self) -> None:
|
||||
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'}
|
||||
|
||||
self._load_images()
|
||||
cTB.settings["popup_welcome"] = 1
|
||||
save_settings(cTB)
|
||||
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 (eyeballing):
|
||||
if bpy.app.version >= (4, 0):
|
||||
label_width -= 10.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)
|
||||
else:
|
||||
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()
|
||||
col_right_gap = row_text.column()
|
||||
col_right_gap.alignment = "RIGHT"
|
||||
col_right_gap.label(text=" ")
|
||||
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('Preview any texture by clicking the "eye" icon.'),
|
||||
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)
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
cTB.refresh_ui()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POLIIGON_OT_popup_first_download(Operator):
|
||||
bl_idname = "poliigon.popup_first_download"
|
||||
bl_label = _t("Purchased, download started")
|
||||
bl_options = {"INTERNAL", "REGISTER", "UNDO"}
|
||||
|
||||
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'}
|
||||
@@ -0,0 +1,362 @@
|
||||
# #### 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}")
|
||||
|
||||
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
|
||||
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'}
|
||||
@@ -0,0 +1,54 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.props import StringProperty
|
||||
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):
|
||||
bl_idname = "poliigon.refresh_data"
|
||||
bl_label = _t("Refresh data")
|
||||
bl_description = _t("Refresh thumbnails and reload data")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(
|
||||
options={"HIDDEN"}, # noqa: F821
|
||||
default=_t("Refresh thumbnails and reload data")) # noqa: F722
|
||||
|
||||
@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(silent=True)
|
||||
def execute(self, context):
|
||||
check_and_report_detail_view_not_opening()
|
||||
|
||||
cTB.refresh_data()
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,92 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty
|
||||
import bpy.utils.previews
|
||||
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..dialogs.utils_dlg import (
|
||||
get_ui_scale,
|
||||
wrapped_label)
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
USER_COMMENT_LENGTH = 512 # Max length for user submitted error messages.
|
||||
|
||||
|
||||
class POLIIGON_OT_report_error(Operator):
|
||||
bl_idname = "poliigon.report_error"
|
||||
bl_label = _t("Report error")
|
||||
bl_description = _t("Report an error to the developers")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
error_report: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
user_message: StringProperty(
|
||||
default="", # noqa: F722
|
||||
maxlen=USER_COMMENT_LENGTH,
|
||||
options={'SKIP_SAVE'}) # noqa: F821
|
||||
|
||||
target_width = 600
|
||||
|
||||
@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):
|
||||
width = self.target_width # Blender handles scaling to ui.
|
||||
return context.window_manager.invoke_props_dialog(self, width=width)
|
||||
|
||||
@reporting.handle_draw()
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
# Display the error message (no word wrapping in case too long)
|
||||
box = layout.box()
|
||||
box.scale_y = 0.5
|
||||
box_wrap = self.target_width * get_ui_scale(cTB)
|
||||
box_wrap -= 20 * get_ui_scale(cTB)
|
||||
lines = self.error_report.split("\n")
|
||||
if len(lines) > 10: # Prefer the last few lines.
|
||||
lines = lines[-10:]
|
||||
for ln in lines:
|
||||
if not ln:
|
||||
continue
|
||||
box.label(text=ln)
|
||||
|
||||
# Display instructions to submit a comment.
|
||||
label_txt = _t("(Optional) What were you doing when this error occurred?")
|
||||
target_wrap = self.target_width * get_ui_scale(cTB)
|
||||
target_wrap -= 10 * get_ui_scale(cTB)
|
||||
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.
|
||||
return {'CANCELLED'}
|
||||
reporting.user_report(self.error_report, self.user_message)
|
||||
self.report({"INFO"}, _t("Thanks for sharing this report"))
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,68 @@
|
||||
# #### 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 #####
|
||||
|
||||
from threading import Event
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty
|
||||
|
||||
from ..modules.poliigon_core.api_remote_control import ApiJob
|
||||
from ..preferences_map_prefs_util import update_map_prefs_properties
|
||||
from ..toolbox import get_context
|
||||
from ..toolbox_settings import save_settings
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_reset_map_prefs(Operator):
|
||||
bl_idname = "poliigon.reset_map_prefs"
|
||||
bl_label = ""
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
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
|
||||
|
||||
def _callback_done(self, job: ApiJob) -> None:
|
||||
self.event.set()
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
|
||||
self.event = Event()
|
||||
|
||||
cTB.api_rc.add_job_get_download_prefs(
|
||||
callback_cancel=None,
|
||||
callback_progress=None,
|
||||
callback_done=self._callback_done,
|
||||
force=True)
|
||||
|
||||
self.event.wait(10.0)
|
||||
|
||||
update_map_prefs_properties(cTB)
|
||||
save_settings(cTB)
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,170 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty
|
||||
import bpy.utils.previews
|
||||
import bmesh
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_select(Operator):
|
||||
bl_idname = "poliigon.poliigon_select"
|
||||
bl_label = ""
|
||||
bl_description = _t("Select Model")
|
||||
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
mode: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
data: 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
|
||||
|
||||
def select_faces(self, context) -> None:
|
||||
"""Selects all faces which have imported material assigned."""
|
||||
|
||||
self.deselect(context)
|
||||
|
||||
obj = context.active_object
|
||||
idx_mat = int(self.data)
|
||||
# #### vMat = obj.material_slots[i].material
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
mesh = obj.data
|
||||
bm = bmesh.from_edit_mesh(mesh)
|
||||
for _face in bm.faces:
|
||||
_face.select = 0
|
||||
|
||||
obj.active_material_index = idx_mat
|
||||
bpy.ops.object.material_slot_select()
|
||||
|
||||
def select_object(self, context) -> None:
|
||||
"""Selects an object by name."""
|
||||
|
||||
self.deselect(context)
|
||||
obj = context.scene.objects[self.data]
|
||||
try:
|
||||
obj.select_set(True)
|
||||
except RuntimeError:
|
||||
pass # Might not be in view layer
|
||||
|
||||
def select_sets(self, context) -> None:
|
||||
"""TODO(Andreas): Really not sure what this is.
|
||||
What does it do? And for what purpose?
|
||||
"""
|
||||
|
||||
parts_data = self.data.split("@")
|
||||
self.deselect(context)
|
||||
try:
|
||||
context.scene.objects[parts_data[1]].select_set(1)
|
||||
except RuntimeError:
|
||||
pass # Might not be in view layer
|
||||
|
||||
def select_model(self, context) -> None:
|
||||
"""Selects all objects belonging to an imported Model asset."""
|
||||
|
||||
if not context.mode == "OBJECT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
self.deselect(context)
|
||||
objs_to_select = []
|
||||
|
||||
key = self.data
|
||||
for obj in context.scene.objects:
|
||||
split = obj.name.rsplit("_")[0]
|
||||
|
||||
# For instance collections
|
||||
if split == key and obj.instance_type == "COLLECTION":
|
||||
objs_to_select.append(obj)
|
||||
# For empty parents
|
||||
ref_key = key + "_empty"
|
||||
if obj.name.lower().startswith(ref_key.lower()):
|
||||
objs_to_select.append(obj)
|
||||
# For the objects within the empty tree
|
||||
if key in obj.poliigon.split(";")[-1]:
|
||||
objs_to_select.append(obj)
|
||||
|
||||
for _obj in objs_to_select:
|
||||
try:
|
||||
_obj.select_set(True)
|
||||
except RuntimeError:
|
||||
pass # Might not be in view layer
|
||||
|
||||
def select_objects_by_texture(self, context) -> None:
|
||||
"""Selects all objects which have a given Texture asset assigned."""
|
||||
|
||||
mat = bpy.data.materials[self.data]
|
||||
objs = [_obj
|
||||
for _obj in context.scene.objects
|
||||
if _obj.active_material == mat]
|
||||
|
||||
if len(objs) == 1:
|
||||
self.deselect(context)
|
||||
try:
|
||||
objs[0].select_set(True)
|
||||
except RuntimeError:
|
||||
pass # Might not be in view layer
|
||||
|
||||
else:
|
||||
# TODO(Andreas): Looks like this branch is never used.
|
||||
# This operator seems to be only used in "model"
|
||||
# mode.
|
||||
# For sure the name access on our object list is a
|
||||
# bug.
|
||||
reporting.capture_message(
|
||||
"reached_legacy_f_DropdownSelect", objs.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
# TODO(Andreas): addon seems to use "mode == model", only
|
||||
if self.mode == "faces":
|
||||
self.select_faces(context)
|
||||
elif self.mode == "object":
|
||||
self.select_object(context)
|
||||
# TODO(Andreas): Seems odd to have this one data comparison in between
|
||||
# the mode ones...
|
||||
elif "@" in self.data:
|
||||
self.select_sets(context)
|
||||
elif self.mode == "model":
|
||||
self.select_model(context)
|
||||
elif self.mode == "mat_objs":
|
||||
self.select_objects_by_texture(context)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def deselect(self, context):
|
||||
"""Deselects objects in a lower api, faster, context-invaraint way."""
|
||||
for obj in context.scene.collection.all_objects:
|
||||
try:
|
||||
obj.select_set(False)
|
||||
except RuntimeError:
|
||||
pass # Might not be in view layer
|
||||
@@ -0,0 +1,447 @@
|
||||
# #### 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 #####
|
||||
|
||||
from enum import IntEnum
|
||||
from typing import Tuple
|
||||
|
||||
from bpy.types import Operator
|
||||
import bpy.utils.previews
|
||||
|
||||
from ..modules.poliigon_core.api_remote_control_params import (
|
||||
CATEGORY_FREE,
|
||||
KEY_TAB_IMPORTED,
|
||||
KEY_TAB_MY_ASSETS,
|
||||
KEY_TAB_ONLINE)
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..asset_browser.asset_browser import create_poliigon_library
|
||||
from ..dialogs.utils_dlg import get_ui_scale
|
||||
from ..constants import HDRI_RESOLUTIONS
|
||||
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):
|
||||
NO_UPDATE = 0
|
||||
# Any update includes UI refresh
|
||||
IMPORTED_AND_GET_ASSETS_AND_PAGE_1 = 1
|
||||
IMPORTED_AND_GET_ASSETS = 2
|
||||
IMPORTED_ONLY = 3
|
||||
|
||||
|
||||
class POLIIGON_OT_setting(Operator):
|
||||
bl_idname = "poliigon.poliigon_setting"
|
||||
bl_label = ""
|
||||
bl_description = _t("Edit Poliigon Addon Settings")
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
tooltip: bpy.props.StringProperty(default="", options={"HIDDEN"}) # noqa: F722, F821
|
||||
mode: bpy.props.StringProperty(default="", options={"HIDDEN"}) # noqa: F722, 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
|
||||
|
||||
@staticmethod
|
||||
def _set_library() -> int:
|
||||
path_library_new = cTB.settings["set_library"]
|
||||
path_library_old = cTB.get_library_path(primary=True)
|
||||
f_MDir(path_library_new)
|
||||
|
||||
cTB.replace_library_path(
|
||||
path_library_old,
|
||||
path_library_new,
|
||||
primary=True,
|
||||
update_local_assets=True)
|
||||
|
||||
if bpy.app.version >= (3, 0):
|
||||
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:
|
||||
with cTB.lock_thumbs:
|
||||
cTB.thumbs.clear()
|
||||
area = self.mode.replace("area_", "")
|
||||
cTB.settings["area"] = area
|
||||
|
||||
# This caused a delay when switching between Poliigon/My Assets
|
||||
# vClearCache = 1
|
||||
|
||||
cTB.settings["show_settings"] = 0
|
||||
cTB.settings["show_user"] = 0
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _show_my_account() -> None:
|
||||
cTB.settings["show_settings"] = 0
|
||||
cTB.settings["show_user"] = 1
|
||||
cTB.vActiveAsset = None
|
||||
cTB.refresh_ui()
|
||||
check_and_report_detail_view_not_opening()
|
||||
|
||||
@staticmethod
|
||||
def _show_settings() -> None:
|
||||
# TODO(Andreas): Does this code still have a purpose?
|
||||
cTB.settings["show_settings"] = 1
|
||||
cTB.settings["show_user"] = 0
|
||||
cTB.vActiveAsset = None
|
||||
cTB.refresh_ui()
|
||||
|
||||
def _set_category(self) -> int:
|
||||
props = bpy.context.window_manager.poliigon_props
|
||||
area = cTB.settings["area"]
|
||||
|
||||
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
|
||||
else:
|
||||
cTB.settings["category"][area].append(button)
|
||||
cTB.settings["category"][area] = cTB.settings[
|
||||
"category"][area][: idx_category + 1]
|
||||
|
||||
categories = cTB.settings["category"][area]
|
||||
if len(categories) > 1 and categories[-1].startswith("All "):
|
||||
categories = categories[:-1]
|
||||
cTB.settings["category"][area] = 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.search_free = False
|
||||
|
||||
check_and_report_detail_view_not_opening()
|
||||
|
||||
# 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()
|
||||
|
||||
area = cTB.settings["area"]
|
||||
idx_page = self.mode.split("_")[-1]
|
||||
|
||||
if cTB.vPage[area] == idx_page:
|
||||
return {"FINISHED"}
|
||||
elif idx_page == "-":
|
||||
if cTB.vPage[area] > 0:
|
||||
cTB.vPage[area] -= 1
|
||||
elif idx_page == "+":
|
||||
if cTB.vPage[area] < cTB.vPages[area]:
|
||||
cTB.vPage[area] += 1
|
||||
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
|
||||
|
||||
def _set_page_size(self) -> Tuple[int, bool]:
|
||||
per_page = int(self.mode.split("@")[1])
|
||||
if cTB.settings["page"] == per_page:
|
||||
return 0, False
|
||||
|
||||
cTB.settings["page"] = per_page
|
||||
# do_update and clear_cache
|
||||
# do_update: update and switch to page 1
|
||||
return ModeUpdate.IMPORTED_AND_GET_ASSETS_AND_PAGE_1, True
|
||||
|
||||
def _clear_search(self) -> None:
|
||||
props = bpy.context.window_manager.poliigon_props
|
||||
if self.mode.endswith(KEY_TAB_ONLINE):
|
||||
props.search_poliigon = ""
|
||||
elif self.mode.endswith(KEY_TAB_MY_ASSETS):
|
||||
props.search_my_assets = ""
|
||||
elif self.mode.endswith(KEY_TAB_IMPORTED):
|
||||
props.search_imported = ""
|
||||
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:
|
||||
return
|
||||
cTB.settings["thumbsize"] = size
|
||||
cTB.refresh_ui()
|
||||
|
||||
def _set_mode(self) -> int:
|
||||
cTB.settings[self.mode] = not cTB.settings[self.mode]
|
||||
|
||||
# Update the session reference of this setting too.
|
||||
do_update = ModeUpdate.NO_UPDATE
|
||||
if self.mode == "download_link_blend":
|
||||
cTB.link_blend_session = cTB.settings[self.mode]
|
||||
elif self.mode == "download_prefer_blend":
|
||||
cTB._asset_index.flush_is_local()
|
||||
cTB._asset_index.update_all_local_assets(
|
||||
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
|
||||
return do_update
|
||||
|
||||
def _set_default(self) -> int:
|
||||
key = self.mode.split("_")[1]
|
||||
value = self.mode.split("_")[2]
|
||||
cTB.settings[key] = value
|
||||
|
||||
if not self.mode.startswith("default_hdri"):
|
||||
cTB.refresh_ui()
|
||||
return 0 # do_update: no update
|
||||
|
||||
idx_size_exr = HDRI_RESOLUTIONS.index(
|
||||
cTB.settings["hdri"])
|
||||
idx_size_jpg = HDRI_RESOLUTIONS.index(
|
||||
cTB.settings["hdrib"])
|
||||
if idx_size_jpg <= idx_size_exr:
|
||||
idx_size_jpg_new = min(idx_size_exr + 1,
|
||||
len(HDRI_RESOLUTIONS) - 1)
|
||||
cTB.settings["hdrib"] = HDRI_RESOLUTIONS[idx_size_jpg_new]
|
||||
# do_update: update, but skip switching to page 1
|
||||
return ModeUpdate.IMPORTED_AND_GET_ASSETS
|
||||
|
||||
def _disable_library_directory(self) -> None:
|
||||
directory = self.mode.replace("disable_dir_", "")
|
||||
if directory in cTB.settings["disabled_dirs"]:
|
||||
cTB.settings["disabled_dirs"].remove(directory)
|
||||
cTB.logger.info(f"Enabled directory: {directory}")
|
||||
cTB.add_library_path(
|
||||
directory, primary=False, update_local_assets=True)
|
||||
else:
|
||||
cTB.settings["disabled_dirs"].append(directory)
|
||||
cTB.logger.info(f"Disabled directory: {directory}")
|
||||
cTB.remove_library_path(
|
||||
directory, update_local_assets=True)
|
||||
cTB.refresh_ui()
|
||||
|
||||
def _forget_library_directory(self) -> None:
|
||||
directory = self.mode.replace("del_dir_", "")
|
||||
cTB.remove_library_path(
|
||||
directory, update_local_assets=True)
|
||||
cTB.refresh_ui()
|
||||
|
||||
def _toggle_material_property(self) -> None:
|
||||
prop = self.mode.split("@")[1]
|
||||
if prop in cTB.settings["mat_props"]:
|
||||
cTB.settings["mat_props"].remove(prop)
|
||||
else:
|
||||
cTB.settings["mat_props"].append(prop)
|
||||
|
||||
@staticmethod
|
||||
def _view_more() -> int:
|
||||
props = bpy.context.window_manager.poliigon_props
|
||||
area = cTB.settings["area"]
|
||||
|
||||
prev_area = area
|
||||
area = KEY_TAB_ONLINE
|
||||
|
||||
cTB.settings["area"] = area
|
||||
cat_area = cTB.settings["category"][prev_area]
|
||||
cTB.settings["category"][area] = 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]
|
||||
cTB.vActiveAsset = None
|
||||
# do_update: update, but skip switching to page 1
|
||||
return ModeUpdate.IMPORTED_AND_GET_ASSETS
|
||||
|
||||
@staticmethod
|
||||
def _do_clear_cache(clear_cache: bool) -> None:
|
||||
if not clear_cache:
|
||||
return
|
||||
# TODO(Andreas): I assume somebody planned some cTB.thumbs refresh
|
||||
# or something?
|
||||
|
||||
@staticmethod
|
||||
def _do_update(do_update: int) -> None:
|
||||
if not do_update:
|
||||
return
|
||||
|
||||
area = cTB.settings["area"]
|
||||
cTB.flush_thumb_prefetch_queue()
|
||||
|
||||
if do_update == ModeUpdate.IMPORTED_AND_GET_ASSETS_AND_PAGE_1:
|
||||
cTB.vPage[area] = 0
|
||||
cTB.vPages[area] = 1
|
||||
|
||||
# Not setting cursor as it can lead to being stuck on "wait".
|
||||
# bpy.context.window.cursor_set("WAIT")
|
||||
|
||||
# TODO(SOFT-762): refactor to cache raw API request, also validate
|
||||
# if this needs re-requesting (has calls to f_GetCategoryChildren).
|
||||
|
||||
# TODO(Andreas): disabled, when going addon-core
|
||||
# cTB.f_GetCategories()
|
||||
|
||||
# TODO(Andreas): redundant?
|
||||
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)
|
||||
|
||||
cTB.refresh_ui()
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
cTB.logger.debug(f"POLIIGON_OT_setting: mode='{self.mode}'")
|
||||
|
||||
get_ui_scale(cTB) # Force update DPI check for scale.
|
||||
do_update = ModeUpdate.NO_UPDATE
|
||||
clear_cache = False
|
||||
|
||||
if self.mode in ["none", ""]:
|
||||
return {"FINISHED"}
|
||||
elif self.mode == "set_library":
|
||||
do_update = self._set_library()
|
||||
elif self.mode.startswith("area_"):
|
||||
do_update = self._set_area()
|
||||
elif self.mode == "my_account":
|
||||
self._show_my_account()
|
||||
return {"FINISHED"}
|
||||
elif self.mode == "settings":
|
||||
self._show_settings()
|
||||
return {"FINISHED"}
|
||||
elif self.mode.startswith("category_"):
|
||||
do_update = self._set_category()
|
||||
elif self.mode.startswith("page_"):
|
||||
do_update = self._set_page()
|
||||
elif self.mode.startswith("page@"):
|
||||
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 [
|
||||
"apply_subdiv",
|
||||
"auto_download",
|
||||
"download_lods",
|
||||
"download_prefer_blend",
|
||||
"download_link_blend",
|
||||
"hdri_use_jpg_bg",
|
||||
"mat_props_edit",
|
||||
"new_top",
|
||||
"one_click_purchase",
|
||||
"show_active",
|
||||
"show_add_dir",
|
||||
"show_asset_info",
|
||||
"show_credits",
|
||||
"show_default_prefs",
|
||||
"show_display_prefs",
|
||||
"show_import_prefs",
|
||||
"show_asset_browser_prefs",
|
||||
"show_mat_ops",
|
||||
"show_mat_props",
|
||||
"show_mat_texs",
|
||||
"show_plan",
|
||||
"show_feedback",
|
||||
"show_settings",
|
||||
"show_user",
|
||||
"use_16"
|
||||
]:
|
||||
do_update = self._set_mode()
|
||||
elif self.mode.startswith("default_"):
|
||||
do_update = self._set_default()
|
||||
elif self.mode.startswith("disable_dir_"):
|
||||
self._disable_library_directory()
|
||||
elif self.mode.startswith("del_dir_"):
|
||||
self._forget_library_directory()
|
||||
elif self.mode.startswith("prop@"):
|
||||
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()
|
||||
else:
|
||||
reporting.capture_message("invalid_setting_mode", self.mode)
|
||||
self.report(
|
||||
{"WARNING"}, _t("Invalid setting mode {0}").format(self.mode))
|
||||
return {'CANCELLED'}
|
||||
|
||||
self._do_clear_cache(clear_cache)
|
||||
self._do_update(do_update)
|
||||
save_settings(cTB)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,124 @@
|
||||
# #### 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 addon_utils
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import EnumProperty
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
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):
|
||||
"""Open user preferences and display Poliigon settings"""
|
||||
bl_idname = "poliigon.open_preferences"
|
||||
bl_label = _t("Show Poliigon preferences")
|
||||
|
||||
_options = (
|
||||
("skip",
|
||||
_t("Skip"),
|
||||
_t("Open user preferences as-is without changing visible areas")),
|
||||
("all",
|
||||
_t("All"),
|
||||
_t("Expand all sections of user preferences")),
|
||||
("show_add_dir",
|
||||
_t("Additional library"),
|
||||
_t("Show additional library directory preferences")),
|
||||
("show_display_prefs",
|
||||
_t("Display"),
|
||||
_t("Show display preferences")),
|
||||
("show_default_prefs",
|
||||
_t("Asset prefs"),
|
||||
_t("Show asset preferences"))
|
||||
)
|
||||
|
||||
set_focus: EnumProperty(items=_options, 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)
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
prefs = get_prefs()
|
||||
if self.set_focus == "all":
|
||||
cTB.settings["show_add_dir"] = True
|
||||
cTB.settings["show_display_prefs"] = True
|
||||
cTB.settings["show_default_prefs"] = True
|
||||
cTB.settings["show_updater_prefs"] = True
|
||||
if prefs:
|
||||
prefs.show_updater_prefs = True
|
||||
elif self.set_focus != "skip":
|
||||
show_add_dir = self.set_focus == "show_add_dir"
|
||||
cTB.settings["show_add_dir"] = show_add_dir
|
||||
show_display_prefs = self.set_focus == "show_display_prefs"
|
||||
cTB.settings["show_display_prefs"] = show_display_prefs
|
||||
show_default_prefs = self.set_focus == "show_default_prefs"
|
||||
cTB.settings["show_default_prefs"] = show_default_prefs
|
||||
if prefs:
|
||||
prefs.show_updater_prefs = False
|
||||
|
||||
bpy.ops.screen.userpref_show('INVOKE_AREA')
|
||||
bpy.data.window_managers["WinMan"].addon_search = "Poliigon"
|
||||
prefs = context.preferences
|
||||
try:
|
||||
prefs.active_section = "ADDONS"
|
||||
except TypeError as err:
|
||||
reporting.capture_message(
|
||||
"assign_preferences_tab", str(err), "error")
|
||||
|
||||
# __spec__.parent since __package__ got deprecated
|
||||
# Since this module moved into operators,
|
||||
# we need to split off .operators
|
||||
spec_parent = __spec__.parent
|
||||
spec_parent = spec_parent.split(".")[0]
|
||||
|
||||
addons_ids = [
|
||||
mod for mod in addon_utils.modules(refresh=False)
|
||||
if mod.__name__ == spec_parent]
|
||||
if not addons_ids:
|
||||
msg = "Failed to directly load and open Poliigon preferences"
|
||||
reporting.capture_message(
|
||||
"preferences_open_no_id", msg, "error")
|
||||
return {'CANCELLED'}
|
||||
|
||||
addon_blinfo = addon_utils.module_bl_info(addons_ids[0])
|
||||
if not addon_blinfo["show_expanded"]:
|
||||
has_prefs = hasattr(bpy.ops, "preferences")
|
||||
has_prefs = has_prefs and hasattr(bpy.ops.preferences,
|
||||
"addon_expand")
|
||||
|
||||
if has_prefs: # later 2.8 buids
|
||||
bpy.ops.preferences.addon_expand(module=spec_parent)
|
||||
else:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
_t("Search for and expand the Poliigon addon in preferences")
|
||||
)
|
||||
|
||||
check_and_report_detail_view_not_opening()
|
||||
cTB.track_screen("settings")
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,63 @@
|
||||
# #### 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 (BoolProperty,
|
||||
IntProperty,
|
||||
StringProperty)
|
||||
import bpy.utils.previews
|
||||
|
||||
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):
|
||||
bl_idname = "poliigon.show_quick_menu"
|
||||
bl_label = ""
|
||||
bl_description = _t("Show quick menu")
|
||||
|
||||
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
|
||||
hide_detail_view: BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
|
||||
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
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
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)
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,64 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.props import StringProperty
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from ..dialogs.dlg_popup import open_popup
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_unsupported_convention(Operator):
|
||||
bl_idname = "poliigon.unsupported_convention"
|
||||
bl_label = _t("Unsupported")
|
||||
bl_description = _t(
|
||||
"Addon does not support asset convention, try updating the addon")
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
|
||||
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
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
# TODO(Andreas): Does this work for translation?
|
||||
msg = _t(
|
||||
"Asset not supported, please update plugin. "
|
||||
"This asset is published with newer conventions to improve "
|
||||
"render outputs."
|
||||
)
|
||||
open_popup(
|
||||
cTB,
|
||||
title=_t("Addon Update Needed"),
|
||||
msg=msg,
|
||||
buttons=[_t("OK"), _t("Update")],
|
||||
commands=["check_update", "open_p4b_url"],
|
||||
mode=None,
|
||||
w_limit=210)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,172 @@
|
||||
# #### 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 datetime
|
||||
from threading import Event
|
||||
import time
|
||||
|
||||
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
|
||||
|
||||
|
||||
class POLIIGON_OT_user(Operator):
|
||||
bl_idname = "poliigon.poliigon_user"
|
||||
bl_label = ""
|
||||
bl_description = ""
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
mode: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
do_synchronous: 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 _login_determine_elapsed(self) -> None:
|
||||
"""Calculates the time between addon enable and login.
|
||||
|
||||
This is included in the initiate login or direct email/pwd login only
|
||||
if this is the first time install+login. This value gets included in
|
||||
the initiate/login request which will treat as an addon install event.
|
||||
"""
|
||||
|
||||
cTB.login_elapsed_s = None
|
||||
if not cTB.settings["first_enabled_time"]:
|
||||
return
|
||||
|
||||
now = datetime.datetime.now()
|
||||
install_tstr = cTB.settings["first_enabled_time"]
|
||||
install_t = datetime.datetime.strptime(
|
||||
install_tstr, "%Y-%m-%d %H:%M:%S")
|
||||
elapsed = now - install_t
|
||||
cTB.login_elapsed_s = int(elapsed.total_seconds())
|
||||
if cTB.login_elapsed_s <= 0:
|
||||
cTB.logger.debug(
|
||||
"POLIIGON_OT_user Throwing out negative elapsed time")
|
||||
cTB.login_elapsed_s = None
|
||||
|
||||
def _callback_login_done_sync(self, job: ApiJob) -> None:
|
||||
cTB.callback_login_done(job)
|
||||
self.event_sync.set()
|
||||
|
||||
def _callback_logout_done_sync(self, job: ApiJob) -> None:
|
||||
cTB.callback_logout_done(job)
|
||||
self.event_sync.set()
|
||||
|
||||
def _do_login(self, cTB) -> None:
|
||||
cTB.logger.debug(
|
||||
"POLIIGON_OT_user Sending login with website request")
|
||||
|
||||
cTB.login_cancelled = False
|
||||
cTB.login_res = None
|
||||
cTB.login_time_start = time.time()
|
||||
|
||||
cTB.login_in_progress = True
|
||||
cTB.last_login_error = None
|
||||
|
||||
if self.do_synchronous:
|
||||
self.event_sync = Event()
|
||||
callback_login_done = self._callback_login_done_sync
|
||||
callback_logout_done = self._callback_logout_done_sync
|
||||
else:
|
||||
callback_login_done = cTB.callback_login_done
|
||||
callback_logout_done = cTB.callback_logout_done
|
||||
|
||||
if self.mode == "login_with_website":
|
||||
mode = CmdLoginMode.LOGIN_BROWSER
|
||||
callback_cancel = cTB.callback_login_cancel
|
||||
callback_done = callback_login_done
|
||||
email = None
|
||||
pwd = None
|
||||
login_elapsed_s = cTB.login_elapsed_s
|
||||
elif self.mode == "login":
|
||||
mode = CmdLoginMode.LOGIN_CREDENTIALS
|
||||
callback_cancel = None
|
||||
callback_done = callback_login_done
|
||||
email = bpy.context.window_manager.poliigon_props.vEmail
|
||||
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
|
||||
email = None
|
||||
pwd = None
|
||||
login_elapsed_s = None
|
||||
|
||||
cTB.api_rc.add_job_login(
|
||||
mode=mode,
|
||||
email=email,
|
||||
pwd=pwd,
|
||||
time_since_enable=login_elapsed_s,
|
||||
callback_cancel=callback_cancel,
|
||||
callback_done=callback_done,
|
||||
force=True
|
||||
)
|
||||
|
||||
if self.do_synchronous:
|
||||
self.event_sync.wait(30.0)
|
||||
|
||||
@reporting.handle_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":
|
||||
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}")
|
||||
|
||||
cTB.refresh_ui()
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,253 @@
|
||||
# #### 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 Tuple
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
IntProperty,
|
||||
StringProperty)
|
||||
import bpy.utils.previews
|
||||
|
||||
from ..modules.poliigon_core.assets import AssetData
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_view_thumbnail(Operator):
|
||||
bl_idname = "poliigon.view_thumbnail"
|
||||
bl_label = ""
|
||||
bl_description = _t("View larger thumbnail")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
|
||||
thumbnail_index: IntProperty(min=0, options={"HIDDEN"}) # noqa: F821
|
||||
resolution: IntProperty(min=300, default=900, 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
|
||||
|
||||
@reporting.handle_operator()
|
||||
def execute(self, context):
|
||||
asset_data = cTB._asset_index.get_asset(self.asset_id)
|
||||
|
||||
op_dims_backup = self._backup_show_render_op_dimensions(context)
|
||||
|
||||
# Don't modify by get_ui_scale(), as it uses physical pixel size. Also
|
||||
# making it smaller than 1024 to minimize margins around image.
|
||||
pixels = int(self.resolution + 100)
|
||||
pixels = min(pixels, 1000)
|
||||
|
||||
res = {'CANCELLED'}
|
||||
try:
|
||||
# Modify render settings to force new window to appear.
|
||||
self._set_show_render_op_dimensions(context, pixels, pixels)
|
||||
|
||||
# Main loading steps
|
||||
area = self._create_window(asset_data)
|
||||
download_ok = self._download_preview(asset_data)
|
||||
if download_ok:
|
||||
res = self._load_preview(area, asset_data)
|
||||
|
||||
except Exception as e:
|
||||
# If exception occurs, will run after the finally block below.
|
||||
raise e
|
||||
|
||||
finally:
|
||||
# Ensure we always restore render settings and preferences.
|
||||
self._restore_show_render_op_dimensions(context, op_dims_backup)
|
||||
|
||||
cTB.track_screen("large_preview")
|
||||
return res
|
||||
|
||||
def _backup_show_render_op_dimensions(
|
||||
self, context) -> Tuple[int, int, str]:
|
||||
"""Stores/backups dimensions of 'show render' operator."""
|
||||
|
||||
# We use the show render operator to hack setting an explicit size,
|
||||
# be sure to capture the resolution to revert back.
|
||||
render = bpy.context.scene.render
|
||||
init_res_x = render.resolution_x
|
||||
init_res_y = render.resolution_y
|
||||
if hasattr(context.preferences.view, "render_display_type"):
|
||||
init_display = bpy.context.preferences.view.render_display_type
|
||||
else:
|
||||
init_display = context.scene.render.display_mode
|
||||
return (init_res_x, init_res_y, init_display)
|
||||
|
||||
def _restore_show_render_op_dimensions(
|
||||
self, context, dims_backup: Tuple[int, int, str]) -> None:
|
||||
"""Restores dimensions of 'show render' operator."""
|
||||
init_res_x, init_res_y, init_display = dims_backup
|
||||
|
||||
render = bpy.context.scene.render
|
||||
render.resolution_x = init_res_x
|
||||
render.resolution_y = init_res_y
|
||||
if hasattr(context.preferences.view, "render_display_type"):
|
||||
context.preferences.view.render_display_type = init_display
|
||||
else:
|
||||
context.scene.render.display_mode = init_display
|
||||
|
||||
def _set_show_render_op_dimensions(
|
||||
self, context, width: int, height: int) -> None:
|
||||
"""Sets dimensions of 'show render' operator."""
|
||||
|
||||
render = bpy.context.scene.render
|
||||
render.resolution_x = width
|
||||
render.resolution_y = height
|
||||
if hasattr(context.preferences.view, "render_display_type"):
|
||||
context.preferences.view.render_display_type = "WINDOW"
|
||||
else:
|
||||
context.scene.render.display_mode = "WINDOW"
|
||||
|
||||
def _get_thumb_path_and_url(self) -> Tuple[str, str]:
|
||||
path_thumb, url_thumb = cTB._asset_index.get_cf_thumbnail_info(
|
||||
self.asset_id,
|
||||
resolution=self.resolution,
|
||||
index=self.thumbnail_index
|
||||
)
|
||||
if path_thumb is None or url_thumb is None:
|
||||
# Issue already reported inside get_cf_thumbnail_info()
|
||||
return None, None
|
||||
|
||||
path, ext = os.path.splitext(path_thumb)
|
||||
path_thumb = f"{path}_{self.resolution}px{ext}"
|
||||
|
||||
return path_thumb, url_thumb
|
||||
|
||||
@staticmethod
|
||||
def _delete_temp_file(path_thumb_dl: str) -> None:
|
||||
"""Delete temp file from previous download attempts."""
|
||||
|
||||
if not os.path.exists(path_thumb_dl):
|
||||
return
|
||||
|
||||
try:
|
||||
os.remove(path_thumb_dl)
|
||||
except Exception:
|
||||
# TODO(Andreas): Not sure, what to do now.
|
||||
# Would we want to report this?
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _rename_successful_download(
|
||||
path_thumb_dl: str, path_thumb: str) -> None:
|
||||
if not os.path.isfile(path_thumb_dl):
|
||||
return
|
||||
|
||||
try:
|
||||
os.rename(path_thumb_dl, path_thumb)
|
||||
except Exception:
|
||||
POLIIGON_OT_view_thumbnail._delete_temp_file(path_thumb_dl)
|
||||
# TODO(Andreas): Not sure, we would want to report this?
|
||||
|
||||
def _download_preview_exec(self, asset_data: AssetData) -> bool:
|
||||
path_thumb, url_thumb = self._get_thumb_path_and_url()
|
||||
if path_thumb is None or url_thumb is None:
|
||||
return False
|
||||
|
||||
if os.path.isfile(path_thumb):
|
||||
return True # already local
|
||||
|
||||
asset_name = asset_data.asset_name
|
||||
path_thumb_dl = f"{path_thumb}_dl"
|
||||
self._delete_temp_file(path_thumb_dl)
|
||||
|
||||
resp = cTB._api.download_preview(url_thumb, path_thumb_dl, asset_name)
|
||||
if not resp.ok:
|
||||
# Issue already reported inside download_preview()
|
||||
self._delete_temp_file(path_thumb_dl)
|
||||
return False # Failed download
|
||||
|
||||
self._rename_successful_download(path_thumb_dl, path_thumb)
|
||||
return True
|
||||
|
||||
def _download_preview(
|
||||
self, asset_data: AssetData) -> bool:
|
||||
"""Download the target thumbnail if not local, no threading."""
|
||||
|
||||
cTB.logger.debug("POLIIGON_OT_view_thumbnail Download thumbnail index "
|
||||
f"{self.thumbnail_index}")
|
||||
|
||||
bpy.context.window.cursor_set("WAIT")
|
||||
result = self._download_preview_exec(asset_data)
|
||||
bpy.context.window.cursor_set("DEFAULT")
|
||||
return result
|
||||
|
||||
def _create_window(self, asset_data: AssetData):
|
||||
# Call image editor window
|
||||
asset_name = asset_data.asset_name
|
||||
bpy.ops.render.view_show("INVOKE_DEFAULT")
|
||||
|
||||
# Set up the window as needed.
|
||||
area = None
|
||||
for _window in bpy.context.window_manager.windows:
|
||||
this_area = _window.screen.areas[0]
|
||||
if this_area.type == "IMAGE_EDITOR":
|
||||
area = this_area
|
||||
break
|
||||
if not area:
|
||||
return None
|
||||
|
||||
dispaly_details = asset_data.get_display_details_data()
|
||||
details = dispaly_details.get("Physical Size", "")
|
||||
rwsize = f" ({details})" if details else ""
|
||||
|
||||
area.header_text_set(_t("Asset thumbnail: {0} {1}").format(
|
||||
asset_name, rwsize))
|
||||
area.show_menus = False
|
||||
return area
|
||||
|
||||
def _load_preview(self, area, asset_data: AssetData):
|
||||
"""Load in the image preview based on the area."""
|
||||
|
||||
asset_name = asset_data.asset_name
|
||||
path_thumb, _ = self._get_thumb_path_and_url()
|
||||
|
||||
if not os.path.isfile(path_thumb):
|
||||
self.report({"ERROR"}, _t("Could not find image preview"))
|
||||
msg = f"{asset_name}: Could not find image preview {path_thumb}"
|
||||
reporting.capture_message("thumbnail_file_missing", msg, "error")
|
||||
return {'CANCELLED'}
|
||||
|
||||
thumbnail = bpy.data.images.load(path_thumb)
|
||||
|
||||
if area:
|
||||
area.spaces[0].image = thumbnail
|
||||
else:
|
||||
msg = _t("Open the image now loaded in an image viewer")
|
||||
self.report({"ERROR"}, msg)
|
||||
err = "Failed to open window for preview"
|
||||
reporting.capture_message("img_window_failed_open", err, "info")
|
||||
|
||||
# Tag this image with a property, could be used to trigger UI draws in
|
||||
# the viewer in the future.
|
||||
thumbnail["poliigon_thumbnail"] = True
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,172 @@
|
||||
# #### 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.utils.previews
|
||||
|
||||
from .operator_active import POLIIGON_OT_active
|
||||
from .operator_add_converter_node import POLIIGON_OT_add_converter_node
|
||||
from .operator_apply import POLIIGON_OT_apply
|
||||
from .operator_cancel_download import POLIIGON_OT_cancel_download
|
||||
from .operator_category import POLIIGON_OT_category
|
||||
from .operator_check_update import POLIIGON_OT_check_update
|
||||
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 (
|
||||
POLIIGON_OT_download,
|
||||
POLIIGON_OT_popup_purchase)
|
||||
from .operator_folder import POLIIGON_OT_folder
|
||||
from .operator_hdri import POLIIGON_OT_hdri
|
||||
from .operator_library import POLIIGON_OT_library
|
||||
from .operator_link import POLIIGON_OT_link
|
||||
from .operator_local_asset_sync import POLIIGON_OT_get_local_asset_sync
|
||||
from .operator_load_asset_from_list import (
|
||||
POLIIGON_OT_load_asset_size_from_list)
|
||||
from .operator_material import POLIIGON_OT_material
|
||||
from .operator_model import POLIIGON_OT_model
|
||||
from .operator_options import POLIIGON_OT_options
|
||||
from .operator_notice import POLIIGON_OT_notice_operator
|
||||
from .operator_popup_download_limit import POLIIGON_OT_popup_download_limit
|
||||
from .operator_popup_message import POLIIGON_OT_popup_message
|
||||
from .operator_popup_change_plan import (
|
||||
POLIIGON_OT_banner_change_plan_dismiss,
|
||||
POLIIGON_OT_banner_finish_dismiss,
|
||||
POLIIGON_OT_change_plan,
|
||||
POLIIGON_OT_popup_change_plan)
|
||||
from .operator_popups_onboarding import (
|
||||
POLIIGON_OT_popup_welcome,
|
||||
POLIIGON_OT_popup_first_download)
|
||||
from .operator_preview import (
|
||||
POLIIGON_OT_popup_first_preview,
|
||||
POLIIGON_OT_preview)
|
||||
from .operator_refresh_data import POLIIGON_OT_refresh_data
|
||||
from .operator_report_error import POLIIGON_OT_report_error
|
||||
from .operator_reset_map_prefs import POLIIGON_OT_reset_map_prefs
|
||||
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
|
||||
try:
|
||||
from .operator_unit_test_helper import (
|
||||
POLIIGON_OT_unit_test_helper,
|
||||
UnitTestProperties)
|
||||
HAVE_UNIT_TEST_HELPER = True
|
||||
except ImportError:
|
||||
# It is fine, to not have this. We only use it in uit tests.
|
||||
HAVE_UNIT_TEST_HELPER = False
|
||||
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 ..toolbox import get_context
|
||||
|
||||
|
||||
classes = (
|
||||
POLIIGON_OT_active,
|
||||
POLIIGON_OT_add_converter_node,
|
||||
POLIIGON_OT_apply,
|
||||
POLIIGON_OT_banner_change_plan_dismiss,
|
||||
POLIIGON_OT_banner_finish_dismiss,
|
||||
POLIIGON_OT_cancel_download,
|
||||
POLIIGON_OT_category,
|
||||
POLIIGON_OT_change_plan,
|
||||
POLIIGON_OT_check_update,
|
||||
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,
|
||||
POLIIGON_OT_folder,
|
||||
POLIIGON_OT_get_local_asset_sync,
|
||||
POLIIGON_OT_hdri,
|
||||
POLIIGON_OT_library,
|
||||
POLIIGON_OT_link,
|
||||
POLIIGON_OT_load_asset_size_from_list,
|
||||
POLIIGON_OT_material,
|
||||
POLIIGON_OT_model,
|
||||
POLIIGON_OT_notice_operator,
|
||||
POLIIGON_OT_options,
|
||||
POLIIGON_OT_popup_change_plan,
|
||||
POLIIGON_OT_popup_download_limit,
|
||||
POLIIGON_OT_popup_first_download,
|
||||
POLIIGON_OT_popup_first_preview,
|
||||
POLIIGON_OT_popup_message,
|
||||
POLIIGON_OT_popup_purchase,
|
||||
POLIIGON_OT_popup_welcome,
|
||||
POLIIGON_OT_preview,
|
||||
POLIIGON_OT_refresh_data,
|
||||
POLIIGON_OT_report_error,
|
||||
POLIIGON_OT_reset_map_prefs,
|
||||
POLIIGON_OT_select,
|
||||
POLIIGON_OT_setting,
|
||||
POLIIGON_OT_show_preferences,
|
||||
POLIIGON_OT_show_quick_menu,
|
||||
POLIIGON_OT_unsupported_convention,
|
||||
POLIIGON_OT_user,
|
||||
POLIIGON_OT_view_thumbnail
|
||||
)
|
||||
|
||||
|
||||
cTB = None
|
||||
|
||||
|
||||
def register_unit_test_helper() -> None:
|
||||
global classes
|
||||
|
||||
if not HAVE_UNIT_TEST_HELPER:
|
||||
return
|
||||
|
||||
if POLIIGON_OT_unit_test_helper not in classes:
|
||||
# Have operator un-/registered like all others
|
||||
classes = (*classes, POLIIGON_OT_unit_test_helper)
|
||||
|
||||
bpy.utils.register_class(UnitTestProperties)
|
||||
bpy.types.WindowManager.polligon_unit_test = bpy.props.PointerProperty(
|
||||
type=UnitTestProperties)
|
||||
|
||||
|
||||
def unregister_unit_test_helper() -> None:
|
||||
if not HAVE_UNIT_TEST_HELPER:
|
||||
return
|
||||
|
||||
del bpy.types.WindowManager.polligon_unit_test
|
||||
bpy.utils.unregister_class(UnitTestProperties)
|
||||
|
||||
|
||||
def register(addon_version: str) -> None:
|
||||
global cTB
|
||||
|
||||
register_unit_test_helper()
|
||||
|
||||
cTB = get_context(addon_version)
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
cls.init_context(addon_version)
|
||||
|
||||
|
||||
def unregister() -> None:
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
unregister_unit_test_helper()
|
||||
@@ -0,0 +1,37 @@
|
||||
# #### 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 #####
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
def fill_size_drop_down(addon, asset_id: int) -> List[Tuple[str, str, str]]:
|
||||
"""Returns a list of enum items with locally available sizes."""
|
||||
|
||||
asset_data = addon._asset_index.get_asset(asset_id)
|
||||
asset_type_data = asset_data.get_type_data()
|
||||
|
||||
local_sizes = asset_type_data.get_size_list(
|
||||
local_only=True,
|
||||
addon_convention=addon._asset_index.addon_convention,
|
||||
local_convention=asset_data.get_convention(local=True))
|
||||
# Populate dropdown items
|
||||
items_size = []
|
||||
for size in local_sizes:
|
||||
# Tuple: (id, name, description, icon, enum value)
|
||||
items_size.append((size, size, size))
|
||||
return items_size
|
||||
Reference in New Issue
Block a user