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

283 lines
8.1 KiB
Python

# #### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
import os
import subprocess
import time
from typing import List
import bpy
def f_Ex(vPath):
return os.path.exists(vPath)
def f_FName(vPath):
return os.path.splitext(os.path.basename(vPath))[0]
def f_FExt(vPath):
return os.path.splitext(os.path.basename(vPath))[1].lower()
def f_FNameExt(vPath):
vSplit = list(os.path.splitext(os.path.basename(vPath)))
vSplit[1] = vSplit[1].lower()
return vSplit
def f_FSplit(vPath):
vSplit = list(os.path.splitext(vPath))
vSplit[1] = vSplit[1].lower()
return vSplit
def f_MDir(vPath):
if f_Ex(vPath):
return
try:
os.makedirs(vPath)
except Exception as e:
print("Failed to create directory: ", e)
def timer(fn):
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = fn(*args, **kwargs)
end_time = time.perf_counter()
duration = round(end_time - start_time, 2)
if duration > 60:
msec = str(duration - int(duration)).split('.')[1]
duration = f"{time.strftime('%M:%S', time.gmtime(duration))}.{msec}"
print(f"{fn.__name__} : {duration}s")
return result
return wrapper
def construct_model_name(asset_name, size, lod):
"""Constructs the model name from the given inputs."""
if lod:
model_name = f"{asset_name}_{size}_{lod}"
else:
model_name = f"{asset_name}_{size}"
return model_name
def shorten_label(label: str, max_len: int) -> str:
"""Shortens the given label by replacing the center part with '...'"""
if len(label) <= max_len:
return label
overlen = (len(label) - (max_len - 3)) // 2
center = len(label) // 2
return label[:center - overlen] + "..." + label[center + overlen:]
def open_dir(directory: str) -> bool:
"""Attempts to open a directory in a cross-platform way."""
directory_norm = os.path.normpath(directory)
did_open = False
try:
os.startfile(directory_norm)
did_open = True
except Exception:
# TODO(Andreas): I somehow doubt, the following is really necessary.
# Neither P4Max nor P4Cinema jump through such hoops.
try:
subprocess.Popen(("open", directory_norm))
did_open = True
except Exception:
try:
subprocess.Popen(("xdg-open", directory_norm))
did_open = True
except Exception:
pass
return did_open
def is_cycles() -> bool:
"""Returns True, if Cycles render engine is enabled."""
return bpy.context.scene.render.engine == "CYCLES"
def is_eevee() -> bool:
"""Returns True, if Eevee render engine is enabled."""
return bpy.context.scene.render.engine == "BLENDER_EEVEE"
def is_eevee_next() -> bool:
"""Returns True, if Eevee render engine is enabled."""
# Blender 4.2's Eevee Next supports for example displacement
# (but not adaptive)
return bpy.context.scene.render.engine == "BLENDER_EEVEE_NEXT"
def set_colorspace(img: bpy.types.Image, colorspace: str = "") -> None:
"""Sets an image's colorspace.
It does so in a try statement, because some people might actually
replace the default colorspace settings, and it literally can't be
guessed what these people use, even if it will mostly be the filmic addon.
"""
try:
if colorspace == "":
colorspace = guess_colorspace()
if colorspace == "Non-Color":
img.colorspace_settings.is_data = True
else:
img.colorspace_settings.name = colorspace
except Exception as e:
print(f"Colorspace {colorspace} not found: {e}")
def guess_colorspace() -> str:
display_device = bpy.context.scene.display_settings.display_device
if display_device == "sRGB":
return "sRGB"
if display_device == "ACES":
return "aces"
def img_to_preview(img: bpy.types.Image, copy_original: bool = False) -> None:
"""Ensures an image's preview is identical to the image."""
if bpy.app.version[0] >= 3:
img.preview_ensure()
if not copy_original:
return
if img.preview.image_size != img.size:
img.preview.image_size = (img.size[0], img.size[1])
img.preview.image_pixels_float = img.pixels[:]
def remove_alpha(img: bpy.types.Image, color_bg: List[float]) -> None:
"""Replaces transparent parts of an image with a new background color."""
pixels = list(img.pixels)
for _idx in range(0, len(pixels), 4):
idx_alpha = _idx + 3
alpha = pixels[idx_alpha]
if alpha < 0.95:
r = min(color_bg[0] * (1 - alpha) + pixels[_idx] * alpha, 1.0)
g = min(color_bg[1] * (1 - alpha) + pixels[_idx + 1] * alpha, 1.0)
b = min(color_bg[2] * (1 - alpha) + pixels[_idx + 2] * alpha, 1.0)
pixels[_idx:_idx + 3] = [r, g, b]
pixels[idx_alpha] = 1.0 # no transparency
img.pixels = pixels
img.update()
def load_image(
name: str,
path: str,
*,
hidden: bool = True,
do_identical_preview: bool = True,
do_set_colorspace: bool = True,
do_remove_alpha: bool = False,
color_bg: List[float] = [0.0, 0.0, 0.0, 0.0],
force: bool = False
) -> bpy.types.Image:
"""Loads an image from disk.
Arguments:
hidden: Mark the image data block as hidden (prepending . to its name)
do_identical_preview: Copies the original image into image's preview.
do_set_colorspace: Sets color space of image to sRGB or ACES, if none.
do_remove_alpha: Replaces transparent parts with a new background color.
force: Do load the image, even if a data block with that name already
exists.
"""
if hidden and not name.startswith("."):
name = f".{name}" # hidden
img = bpy.data.images.get(name)
if img is not None and not force:
return img
img = bpy.data.images.load(path, check_existing=True)
if img is None:
return None
img.name = name
if do_set_colorspace:
set_colorspace(img)
if do_remove_alpha:
remove_alpha(img, color_bg)
if do_identical_preview:
img_to_preview(img, copy_original=True)
return img
def copy_simple_property_group(
source: bpy.types.PropertyGroup,
target: bpy.types.PropertyGroup
) -> None:
"""Copies property values from one PropertyGroup to another.
From: https://blenderartists.org/t/duplicating-pointerproperty-propertygroup-and-collectionproperty/1419096
"""
if not hasattr(source, "__annotations__"):
return
for prop_name in source.__annotations__.keys():
try:
setattr(target, prop_name, getattr(source, prop_name))
except (AttributeError, TypeError):
pass
def compare_simple_property_group(
group_a: bpy.types.PropertyGroup,
group_b: bpy.types.PropertyGroup
) -> bool:
"""Compares property values of two PropertyGroups and
returns True if equal.
"""
if not hasattr(group_a, "__annotations__"):
return False
if not hasattr(group_b, "__annotations__"):
return False
for prop_name in group_a.__annotations__.keys():
try:
val_a = getattr(group_a, prop_name)
val_b = getattr(group_b, prop_name)
if val_a != val_b:
return False
except (AttributeError, TypeError):
return False
return True