Files
2026-03-17 14:58:51 -06:00

944 lines
35 KiB
Python

# #### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
from typing import Dict, 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")
PROP_LIBRARY_LINKED = "poliigon_linked"
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:
# 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_coll = context.view_layer.active_layer_collection
layer_coll.collection.children.link(cache_coll)
for _child in layer_coll.children:
if _child.collection == cache_coll:
_child.exclude = True
else:
cache_coll = bpy.data.collections.get(asset_name)
if cache_coll is None:
err = _t("Failed to get new collection to instance")
self.report({"ERROR"}, err)
return {"CANCELLED"}
# Now finally add the instance to the scene.
inst = self.create_instance(context, cache_coll, size, lod)
else:
# Make sure the objects imported are all part of the same coll.
self.append_cleanup(context, empty)
# 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)
cTB.set_first_local_asset()
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
) -> Tuple[List[str], List[str], str, str]:
"""Returns details for Model import from a Model asset."""
asset_type_data = asset_data.get_type_data()
# Get the intended material size and LOD import to use.
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) -> List[bpy.types.Object]:
"""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) -> None:
"""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_blend(
self,
context,
path_proj: str,
filename_base: str,
size: str
) -> None:
"""Imports from blend files."""
cTB.logger.debug("POLIIGON_OT_model BLEND IMPORT")
filename = filename_base + ".blend"
if self.do_link_blend and filename in bpy.data.libraries.keys():
lib = bpy.data.libraries[filename]
if lib[PROP_LIBRARY_LINKED]:
linked_objs = []
for obj in bpy.data.objects:
if obj.library == lib:
linked_objs.append(obj)
imported_objs = []
for obj in linked_objs:
imported_objs.append(obj.copy())
else:
imported_objs = self._load_blend(path_proj)
else:
imported_objs = self._load_blend(path_proj)
if filename in bpy.data.libraries.keys():
lib = bpy.data.libraries[filename]
lib[PROP_LIBRARY_LINKED] = self.do_link_blend
for obj in context.view_layer.objects:
obj.select_set(False)
layer = context.view_layer.active_layer_collection
imported_mats = []
for obj in imported_objs:
if obj is None:
continue
obj_copy = obj.copy()
layer.collection.objects.link(obj_copy)
obj_copy.select_set(True)
if obj_copy.active_material is None:
pass
elif obj_copy.active_material not in imported_mats:
imported_mats.append(obj_copy.active_material)
files_dict = {}
asset_type_data = self.asset_data.get_type_data()
asset_type_data.get_files(files_dict)
asset_files = list(files_dict.keys())
replace_tex_size(
imported_mats,
asset_files,
size,
self.do_link_blend
)
self._reuse_materials(imported_objs, imported_mats)
def _run_fresh_import_fbx(self, path_proj: str) -> bool:
"""Imports from FBX files."""
cTB.logger.debug("POLIIGON_OT_model FBX IMPORT")
if "fbx" not in dir(bpy.ops.import_scene):
try:
bpy.ops.preferences.addon_enable(module="io_scene_fbx")
self.report(
{"INFO"},
_t("FBX importer addon enabled for import")
)
except RuntimeError:
self.report(
{"ERROR"},
_t("Built-in FBX importer could not be found, "
"check Blender install")
)
return False
try:
# Note on use_custom_normals parameter:
# It always defaulted to True. But in Blender 4.0+
# it started to cause issues.
# Mateusz sync'ed with Stephen and recommended to turn it
# off regardless of Blender version.
bpy.ops.import_scene.fbx(filepath=path_proj,
axis_up="-Z",
use_custom_normals=False)
except Exception as e:
self.report({"ERROR"},
_t("FBX importer exception:") + str(e))
return False
return True
def _run_fresh_import_create_materials(
self,
*,
path_proj: str,
filename_base: str,
imported_proj: List[str],
files_tex: List[str],
objs_from_file: List[bpy.types.Object],
dict_mats_imported: Dict[str, bpy.types.Material],
size: str,
lod: str
) -> None:
"""Creates materials after successful FBX import."""
asset_name = self.asset_data.asset_name
for _mesh in objs_from_file:
if _mesh.type == "EMPTY":
continue
name_mat_imported = ""
if _mesh.active_material is not None:
name_mat_imported = _mesh.active_material.name
# Note: Of course the check if "_mat" is contained could be
# written in one line. But I wouldn't consider "_mat"
# unlikely in arbitrary filenames. Thus I chose to
# explicitly compare for "_mat" at the end and
# additionally check if "_mat_" is contained, in order
# to at least reduce the chance of false positives a bit.
name_mat_imported_lower = name_mat_imported.lower()
name_tex_remastered = ""
name_tex_on_obj = ""
ends_remastered = name_mat_imported_lower.endswith("_mat")
contains_remastered = "_mat_" in name_mat_imported_lower
if ends_remastered or contains_remastered:
pos_remastered = name_mat_imported_lower.rfind("_mat", 1)
name_tex_remastered = name_mat_imported[:pos_remastered]
else:
name_tex_on_obj = name_mat_imported.split("_")[0]
name_mesh_base = _mesh.name.split(".")[0].split("_")[0]
variant_mesh = self.f_GetVar(_mesh.name)
variant_name = variant_mesh
if variant_mesh is None:
# This is a fallback for models,
# where the object name does not contain a variant indicator.
# As None is covered explicitly in the loop below,
# this should do no harm.
variant_mesh = "VAR1"
variant_name = ""
name_mat = name_mesh_base
files_tex_filtered = []
if len(name_tex_remastered) > 0:
# Remastered textures
files_tex_filtered = [
_file
for _file in files_tex
if os.path.basename(_file).startswith(
name_tex_remastered)
]
name_mat = name_mat_imported
else:
for vCheck in [name_mesh_base,
filename_base.split("_")[0],
asset_name,
name_tex_on_obj]:
files_tex_filtered = [
_file
for _file in files_tex
if os.path.basename(_file).startswith(vCheck)
if self.f_GetVar(f_FName(_file)) in [None,
variant_mesh]
]
name_mat = vCheck
if len(files_tex_filtered) > 0:
break
if len(files_tex_filtered) == 0:
err = f"No Textures found for: {self.asset_id} {_mesh.name}"
reporting.capture_message(
"model_texture_missing", err, "info")
continue
name_mat += f"_{size}"
if size not in name_mat:
name_mat += f"_{size}"
if variant_name != "":
name_mat += f"_{variant_name}"
# TODO(Andreas): Not sure, why these lines are commented out.
# Looks reasonable to me.
# if name_mat in bpy.data.materials and self.do_reuse_materials:
# mat = bpy.data.materials[name_mat]
# el
# TODO(Andreas): Not sure, this should also be depending on
# self.do_reuse_materials?
if name_mat in dict_mats_imported.keys():
# Already built in previous iteration
mat = dict_mats_imported[name_mat]
else:
mat = cTB.mat_import.import_material(
asset_data=self.asset_data,
do_apply=False,
workflow="METALNESS",
size=size,
size_bg=None,
lod=lod,
variant=variant_name if variant_name != "" else None,
name_material=name_mat,
name_mesh=name_mesh_base,
ref_objs=None,
projection="UV",
use_16bit=True,
mode_disp="NORMAL", # We never want model dispalcement
translate_x=0.0,
translate_y=0.0,
scale=1.0,
global_rotation=0.0,
aspect_ratio=1.0,
displacement=0.0,
keep_unused_tex_nodes=False,
reuse_existing=self.do_reuse_materials
)
if mat is None:
msg = f"{self.asset_id}: Failed to build matrial: {name_mat}"
reporting.capture_message(
"could_not_create_fbx_mat", msg, "error")
self.report(
{"ERROR"}, _t("Material could not be created."))
imported_proj.remove(path_proj)
break
dict_mats_imported[name_mat] = mat
# This sequence is important!
# 1) Setting the material slot to None
# 2) Changing the link mode
# 3) Assigning our generated material
# Any other order of these statements will get us into trouble
# one way or another.
_mesh.active_material = None
if len(_mesh.material_slots) > 0:
_mesh.material_slots[0].link = "OBJECT"
_mesh.active_material = mat
if variant_mesh is not None:
if len(_mesh.material_slots) == 0:
_mesh.data.materials.append(mat)
else:
_mesh.material_slots[0].link = "OBJECT"
_mesh.material_slots[0].material = mat
_mesh.material_slots[0].link = "OBJECT"
# Ensure we can identify the mesh & LOD even on name change.
# TODO(Andreas): Remove old properties?
_mesh.poliigon = f"Models;{asset_name}"
if lod is not None:
_mesh.poliigon_lod = lod
self.set_poliigon_props_model(_mesh, lod)
# Finally try to remove the originally imported materials
if name_mat_imported not in bpy.data.materials:
continue
mat_imported = bpy.data.materials[name_mat_imported]
if mat_imported.users == 0:
mat_imported.user_clear()
bpy.data.materials.remove(mat_imported)
def run_fresh_import(self,
context,
project_files: List[str],
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
"""
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 = ("Couldn't load project file: "
f"{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":
self._run_fresh_import_blend(
context, path_proj, filename_base, size)
blend_import = True
else:
import_ok = self._run_fresh_import_fbx(path_proj)
if not import_ok:
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)
objs_from_file = [_obj for _obj in list(context.scene.objects)
if _obj not in list_objs_before]
meshes_all += objs_from_file
if ext_proj == ".blend":
for _mesh in objs_from_file:
# Ensure we can identify the mesh & LOD even on name change
# TODO(Andreas): Remove old properties?
_mesh.poliigon = f"Models;{asset_name}"
if lod is not None:
_mesh.poliigon_lod = lod
self.set_poliigon_props_model(_mesh, lod)
continue
self._run_fresh_import_create_materials(
path_proj=path_proj,
filename_base=filename_base,
imported_proj=imported_proj,
files_tex=files_tex,
objs_from_file=objs_from_file,
dict_mats_imported=dict_mats_imported,
size=size,
lod=lod)
# There could have been multiple FBXs, consider fully imported
# for user-popup reporting if all FBX files imported.
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: str, lod: str) -> bpy.types.Object:
"""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"
layer_coll = context.view_layer.active_layer_collection
layer_coll.collection.objects.link(inst)
inst.location = context.scene.cursor.location
inst.empty_display_size = 0.01
# 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: bpy.types.Object) -> None:
"""Performs selection and placement cleanup after an import/append."""
if root_empty is None:
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)