283 lines
8.1 KiB
Python
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
|