3162 lines
89 KiB
Python
3162 lines
89 KiB
Python
# Copyright (C) 2021 Victor Soupday
|
|
# This file is part of CC/iC Blender Tools <https://github.com/soupday/cc_blender_tools>
|
|
#
|
|
# CC/iC Blender Tools 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 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# CC/iC Blender Tools 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 CC/iC Blender Tools. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
import time
|
|
import difflib
|
|
import random
|
|
import re, json
|
|
import traceback
|
|
from mathutils import Vector, Quaternion, Matrix, Euler, Color
|
|
from hashlib import md5
|
|
import bpy
|
|
from typing import List
|
|
|
|
from . import vars
|
|
|
|
timer = 0
|
|
|
|
LOG_TIMER = {}
|
|
LOG_INDENT = 0
|
|
|
|
def log_indent():
|
|
global LOG_INDENT
|
|
LOG_INDENT += 3
|
|
|
|
|
|
def log_recess():
|
|
global LOG_INDENT
|
|
LOG_INDENT -= 3
|
|
|
|
|
|
def log_spacing():
|
|
return " " * LOG_INDENT
|
|
|
|
|
|
def log_detail(msg):
|
|
prefs = vars.prefs()
|
|
"""Log an info message to console."""
|
|
if prefs.log_level == "DETAILS":
|
|
print((" " * LOG_INDENT) + msg)
|
|
|
|
|
|
def log_info(msg):
|
|
prefs = vars.prefs()
|
|
"""Log an info message to console."""
|
|
if prefs.log_level == "ALL" or prefs.log_level == "DETAILS":
|
|
print((" " * LOG_INDENT) + msg)
|
|
|
|
|
|
def log_always(msg):
|
|
prefs = vars.prefs()
|
|
"""Log an info message to console."""
|
|
print((" " * LOG_INDENT) + msg)
|
|
|
|
|
|
def log_warn(msg):
|
|
prefs = vars.prefs()
|
|
"""Log a warning message to console."""
|
|
if prefs.log_level == "ALL" or prefs.log_level == "DETAILS" or prefs.log_level == "WARN":
|
|
print((" " * LOG_INDENT) + "Warning: " + msg)
|
|
|
|
|
|
def log_error(msg, e: Exception = None):
|
|
"""Log an error message to console and raise an exception."""
|
|
indent = LOG_INDENT
|
|
if indent > 1: indent -= 1
|
|
print("*" + (" " * indent) + "Error: " + msg)
|
|
if e is not None:
|
|
print(" -> " + getattr(e, 'message', repr(e)))
|
|
print("Stack Trace: ")
|
|
traceback.print_exc()
|
|
|
|
|
|
def start_timer(name="NONE"):
|
|
global LOG_TIMER
|
|
LOG_TIMER[name] = [time.perf_counter(), 0.0, 0]
|
|
|
|
|
|
def mark_timer(name="NONE"):
|
|
global LOG_TIMER
|
|
if name not in LOG_TIMER:
|
|
start_timer(name)
|
|
LOG_TIMER[name][0] = time.perf_counter()
|
|
|
|
|
|
def update_timer(name="NONE"):
|
|
global LOG_TIMER
|
|
if name not in LOG_TIMER:
|
|
start_timer(name)
|
|
pc = time.perf_counter()
|
|
duration = pc - LOG_TIMER[name][0]
|
|
LOG_TIMER[name][1] += duration
|
|
LOG_TIMER[name][0] = pc
|
|
LOG_TIMER[name][2] += 1
|
|
|
|
|
|
def log_timer(msg, unit = "s", name="NONE"):
|
|
global LOG_TIMER
|
|
prefs = vars.prefs()
|
|
if name not in LOG_TIMER:
|
|
start_timer(name)
|
|
if LOG_TIMER[name][2] == 0:
|
|
update_timer(name)
|
|
if prefs.log_level == "ALL" or prefs.log_level == "DETAILS":
|
|
total_duration = LOG_TIMER[name][1]
|
|
if unit == "ms":
|
|
total_duration *= 1000
|
|
elif unit == "us":
|
|
total_duration *= 1000000
|
|
elif unit == "ns":
|
|
total_duration *= 1000000000
|
|
print((" " * LOG_INDENT) + msg + ": " + str(total_duration) + " " + unit)
|
|
|
|
|
|
def message_box(message = "", title = "Info", icon = 'INFO'):
|
|
def draw(self, context):
|
|
self.layout.label(text = message)
|
|
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
|
|
|
|
|
|
def update_ui(context = None, area_type="VIEW_3D", region_type="UI", all=False):
|
|
for screen in bpy.data.screens:
|
|
for area in screen.areas:
|
|
if area.type == area_type or all:
|
|
for region in area.regions:
|
|
if region.type == region_type or all:
|
|
region.tag_redraw()
|
|
|
|
|
|
def report_multi(op, icon = 'INFO', messages = None):
|
|
if messages:
|
|
text = ""
|
|
for msg in messages:
|
|
text += msg + " \n"
|
|
if text:
|
|
op.report({icon}, text)
|
|
|
|
|
|
def message_box_multi(title = "Info", icon = 'INFO', messages = None):
|
|
def draw(self, context):
|
|
if messages:
|
|
for message in messages:
|
|
self.layout.label(text = message)
|
|
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
|
|
|
|
|
|
def unique_name(name, no_version = False):
|
|
"""Generate a unique name for the node or property to quickly
|
|
identify texture nodes or nodes with parameters."""
|
|
|
|
props = vars.props()
|
|
if no_version:
|
|
name = name + "_" + vars.NODE_PREFIX + str(props.node_id)
|
|
else:
|
|
name = vars.NODE_PREFIX + name + "_" + vars.VERSION_STRING + "_" + str(props.node_id)
|
|
props.node_id = props.node_id + 1
|
|
return name
|
|
|
|
|
|
def set_ccic_id(obj: bpy.types.Object):
|
|
props = vars.props()
|
|
obj["ccic_id"] = vars.VERSION_STRING + "_" + str(props.node_id)
|
|
props.node_id = props.node_id + 1
|
|
|
|
|
|
def has_ccic_id(obj: bpy.types.Object):
|
|
if "ccic_id" in obj:
|
|
return True
|
|
if vars.NODE_PREFIX in obj.name:
|
|
return True
|
|
return False
|
|
|
|
|
|
def deduplicate_names(names: list, func=None, replace: dict=None, partial=False):
|
|
totals = {}
|
|
name: str = None
|
|
for i, name in enumerate(names):
|
|
if replace:
|
|
if partial:
|
|
for r in replace:
|
|
if r in name:
|
|
name = name.replace(r, replace[r])
|
|
break
|
|
elif name in replace:
|
|
name = replace[name]
|
|
if func:
|
|
name = func(name)
|
|
if name in totals:
|
|
count = totals[name]
|
|
else:
|
|
count = 0
|
|
totals[name] = 0
|
|
names[i] = name if count == 0 else f"{name}.{count:03d}"
|
|
totals[name] += 1
|
|
return names
|
|
|
|
|
|
def obj_is_linked(obj):
|
|
try:
|
|
if obj.library is not None:
|
|
return True
|
|
except: ...
|
|
return False
|
|
|
|
|
|
def obj_is_override(obj):
|
|
try:
|
|
if obj.override_library is not None:
|
|
return True
|
|
except: ...
|
|
return False
|
|
|
|
|
|
def unique_material_name(name, mat=None, start_index=1):
|
|
name = strip_name(name)
|
|
index = start_index
|
|
if name in bpy.data.materials and bpy.data.materials[name] != mat:
|
|
while name + "_" + str(index).zfill(2) in bpy.data.materials:
|
|
index += 1
|
|
return name + "_" + str(index).zfill(2)
|
|
return name
|
|
|
|
|
|
def unique_image_name(name, image=None, start_index=1):
|
|
name = strip_name(name)
|
|
index = start_index
|
|
if name in bpy.data.images and bpy.data.images[name] != image:
|
|
while name + "_" + str(index).zfill(2) in bpy.data.images:
|
|
index += 1
|
|
return name + "_" + str(index).zfill(2)
|
|
return name
|
|
|
|
|
|
def unique_object_name(name, obj=None, capitalize=False, start_index=1, suffix=""):
|
|
name = strip_name(name)
|
|
if capitalize:
|
|
name = name.capitalize()
|
|
if suffix:
|
|
suffix = "_" + suffix
|
|
try_name = name + suffix
|
|
if try_name in bpy.data.objects and bpy.data.objects[try_name] != obj:
|
|
index = start_index
|
|
try_name = name + str(index).zfill(2) + suffix
|
|
while try_name in bpy.data.objects:
|
|
index += 1
|
|
try_name = name + str(index).zfill(2) + suffix
|
|
return try_name
|
|
|
|
|
|
def un_suffix_name(name):
|
|
"""Removes any combination of numerical suffixes from the end of a string"""
|
|
base_name = re.sub("([._+|/\,]\d+)*$", "", name)
|
|
return base_name
|
|
|
|
|
|
def is_same_path(pa, pb):
|
|
try:
|
|
if pa and pb:
|
|
return os.path.normpath(os.path.realpath(pa)) == os.path.normpath(os.path.realpath(pb))
|
|
else:
|
|
return False
|
|
except:
|
|
return False
|
|
|
|
|
|
def is_in_path(a, b):
|
|
"""Is path a in path b"""
|
|
try:
|
|
if a and b:
|
|
return os.path.normpath(os.path.realpath(a)) in os.path.normpath(os.path.realpath(b))
|
|
else:
|
|
return False
|
|
except:
|
|
return False
|
|
|
|
|
|
def path_is_parent(parent_path, child_path):
|
|
try:
|
|
parent_path = os.path.abspath(parent_path)
|
|
child_path = os.path.abspath(child_path)
|
|
return os.path.commonpath([parent_path]) == os.path.commonpath([parent_path, child_path])
|
|
except:
|
|
return False
|
|
|
|
|
|
def local_repath(path, original_start):
|
|
"""Takes the path relative to the original_start and makes
|
|
it relative to the blend file location instead.
|
|
Returns the full path."""
|
|
rel_path = relpath(path, original_start)
|
|
return os.path.normpath(bpy.path.abspath(f"//{rel_path}"))
|
|
|
|
|
|
def local_path(path = ""):
|
|
"""Get the full path of <path> relative to the blend file. Returns empty if no blend file path."""
|
|
if bpy.path.abspath("//"):
|
|
abs_path = bpy.path.abspath(f"//{path}")
|
|
return os.path.normpath(abs_path)
|
|
else:
|
|
return ""
|
|
|
|
|
|
def blend_file_name():
|
|
file_path = bpy.data.filepath
|
|
name = ""
|
|
if file_path:
|
|
folder, file = os.path.split(file_path)
|
|
name, ext = os.path.splitext(file)
|
|
return name
|
|
|
|
|
|
def relpath(path, start):
|
|
try:
|
|
return os.path.relpath(path, start)
|
|
except ValueError:
|
|
return os.path.abspath(path)
|
|
|
|
|
|
def search_up_path(path, folder):
|
|
path = os.path.normpath(path)
|
|
dir : str = os.path.dirname(path)
|
|
if dir == path or dir == "" or dir is None:
|
|
return ""
|
|
elif dir.lower().endswith(os.path.sep + folder.lower()):
|
|
return dir
|
|
return search_up_path(dir, folder)
|
|
|
|
|
|
def object_has_material(obj, name):
|
|
name = name.lower()
|
|
if obj.type == "MESH":
|
|
for mat in obj.data.materials:
|
|
if mat and name in mat.name.lower():
|
|
return True
|
|
return False
|
|
|
|
|
|
def object_exists_is_empty(obj):
|
|
"""Test if Object: obj still exists as an object in the scene, and is an empty."""
|
|
if obj is None:
|
|
return False
|
|
try:
|
|
name = obj.name
|
|
return len(obj.users_scene) > 0 and obj.type == "EMPTY"
|
|
except:
|
|
return False
|
|
|
|
|
|
def object_exists_is_mesh(obj):
|
|
"""Test if Object: obj still exists as an object in the scene, and is a mesh."""
|
|
if obj is None:
|
|
return False
|
|
try:
|
|
name = obj.name
|
|
return len(obj.users_scene) > 0 and obj.type == "MESH"
|
|
except:
|
|
return False
|
|
|
|
|
|
def object_exists_is_armature(obj) -> bool:
|
|
"""Test if Object: obj still exists as an object in the scene, and is an armature."""
|
|
if obj is None:
|
|
return False
|
|
try:
|
|
name = obj.name
|
|
return len(obj.users_scene) > 0 and obj.type == "ARMATURE"
|
|
except:
|
|
return False
|
|
|
|
|
|
def object_exists_is_light(obj):
|
|
"""Test if Object: obj still exists as an object in the scene, and is a light."""
|
|
if obj is None:
|
|
return False
|
|
try:
|
|
name = obj.name
|
|
return len(obj.users_scene) > 0 and obj.type == "LIGHT"
|
|
except:
|
|
return False
|
|
|
|
|
|
def object_exists_is_camera(obj):
|
|
"""Test if Object: obj still exists as an object in the scene, and is a camera."""
|
|
if obj is None:
|
|
return False
|
|
try:
|
|
name = obj.name
|
|
return len(obj.users_scene) > 0 and obj.type == "CAMERA"
|
|
except:
|
|
return False
|
|
|
|
|
|
def object_exists(obj: bpy.types.Object):
|
|
"""Test if Object: obj still exists as an object in the scene."""
|
|
if obj is None:
|
|
return False
|
|
try:
|
|
name = obj.name
|
|
return len(obj.users_scene) > 0
|
|
except:
|
|
return False
|
|
|
|
|
|
def action_exists(action: bpy.types.Action):
|
|
if action is None:
|
|
return False
|
|
try:
|
|
name = action.name
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def id_exists(item):
|
|
if item is None:
|
|
return False
|
|
try:
|
|
id = item.id_data
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def material_exists(mat: bpy.types.Material):
|
|
"""Test if material still exists."""
|
|
if mat is None:
|
|
return False
|
|
try:
|
|
name = mat.name
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def image_exists(img: bpy.types.Image):
|
|
"""Test if material still exists."""
|
|
if img is None:
|
|
return False
|
|
try:
|
|
name = img.name
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def purge_image(img: bpy.types.Image):
|
|
if image_exists(img):
|
|
users = img.users - (1 if img.use_extra_user else 0)
|
|
if users <= 0:
|
|
bpy.data.images.remove(img)
|
|
|
|
|
|
def get_selected_mesh():
|
|
if object_exists_is_mesh(get_active_object()):
|
|
return get_active_object()
|
|
elif bpy.context.selected_objects:
|
|
for obj in bpy.context.selected_objects:
|
|
if object_exists_is_mesh(obj):
|
|
return obj
|
|
return None
|
|
|
|
|
|
def get_selected_meshes(context = None):
|
|
"""Gets selected meshes and includes any current context mesh"""
|
|
objects = [ obj for obj in bpy.context.selected_objects if object_exists_is_mesh(obj) ]
|
|
if context and context.object:
|
|
if object_exists_is_mesh(context.object):
|
|
if context.object not in objects:
|
|
objects.append(context.object)
|
|
return objects
|
|
|
|
|
|
def get_selected_armatures(context = None):
|
|
"""Gets selected armatures and includes any current context armature"""
|
|
objects = [ obj for obj in bpy.context.selected_objects if object_exists_is_armature(obj) ]
|
|
if context and context.object:
|
|
if object_exists_is_armature(context.object):
|
|
if context.object not in objects:
|
|
objects.append(context.object)
|
|
return objects
|
|
|
|
|
|
def safe_remove(item, force = False):
|
|
|
|
if object_exists(item):
|
|
|
|
if type(item) == bpy.types.Armature:
|
|
if (item.use_fake_user and item.users == 1) or item.users == 0 or force:
|
|
log_info("Removing Armature: " + item.name)
|
|
bpy.data.armatures.remove(item)
|
|
else:
|
|
log_info("Armature: " + item.name + " still in use!")
|
|
|
|
elif type(item) == bpy.types.Mesh:
|
|
if (item.use_fake_user and item.users == 1) or item.users == 0 or force:
|
|
log_info("Removing Mesh: " + item.name)
|
|
bpy.data.meshes.remove(item)
|
|
else:
|
|
log_info("Mesh: " + item.name + " still in use!")
|
|
|
|
elif type(item) == bpy.types.Object:
|
|
if (item.use_fake_user and item.users == 1) or item.users == 0 or force:
|
|
log_info("Removing Object: " + item.name)
|
|
bpy.data.objects.remove(item)
|
|
else:
|
|
log_info("Object: " + item.name + " still in use!")
|
|
|
|
elif type(item) == bpy.types.Material:
|
|
if (item.use_fake_user and item.users == 1) or item.users == 0 or force:
|
|
log_info("Removing Material: " + item.name)
|
|
bpy.data.materials.remove(item)
|
|
else:
|
|
log_info("Material: " + item.name + " still in use!")
|
|
|
|
elif type(item) == bpy.types.Image:
|
|
if (item.use_fake_user and item.users == 1) or item.users == 0 or force:
|
|
log_info("Removing Image: " + item.name)
|
|
bpy.data.images.remove(item)
|
|
else:
|
|
log_info("Image: " + item.name + " still in use!")
|
|
|
|
elif type(item) == bpy.types.Texture:
|
|
if (item.use_fake_user and item.users == 1) or item.users == 0 or force:
|
|
log_info("Removing Texture: " + item.name)
|
|
bpy.data.textures.remove(item)
|
|
else:
|
|
log_info("Texture: " + item.name + " still in use!")
|
|
|
|
elif type(item) == bpy.types.Action:
|
|
if (item.use_fake_user and item.users == 1) or item.users == 0 or force:
|
|
log_info("Removing Action: " + item.name)
|
|
bpy.data.textures.remove(item)
|
|
else:
|
|
log_info("Action: " + item.name + " still in use!")
|
|
|
|
|
|
def clean_collection(collection, include_fake = False):
|
|
cleaned = False
|
|
for item in collection:
|
|
if (include_fake and item.use_fake_user and item.users == 1) or item.users == 0:
|
|
log_detail(f"Clean Collection Removing: {item}")
|
|
collection.remove(item)
|
|
cleaned = True
|
|
return cleaned
|
|
|
|
|
|
def clean_up_unused():
|
|
clean_collection(bpy.data.images)
|
|
clean_collection(bpy.data.materials)
|
|
clean_collection(bpy.data.textures)
|
|
clean_collection(bpy.data.meshes)
|
|
clean_collection(bpy.data.armatures)
|
|
# as some node_groups are nested...
|
|
while clean_collection(bpy.data.node_groups):
|
|
clean_collection(bpy.data.node_groups)
|
|
|
|
|
|
def same_sign(a, b):
|
|
if a < 0 and b < 0:
|
|
return True
|
|
if a > 0 and b > 0:
|
|
return True
|
|
return False
|
|
|
|
|
|
def sign(a):
|
|
if a >= 0:
|
|
return 1
|
|
return -1
|
|
|
|
|
|
def clamp(x, min = 0.0, max = 1.0):
|
|
if x < min:
|
|
x = min
|
|
if x > max:
|
|
x = max
|
|
return x
|
|
|
|
|
|
def smoothstep(edge0, edge1, x):
|
|
x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
|
|
return x * x * (3 - 2 * x)
|
|
|
|
|
|
def map_smoothstep(edge0, edge1, value0, value1, x):
|
|
if edge1 == edge0:
|
|
return value1
|
|
x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
|
|
t = x * x * (3 - 2 * x)
|
|
return value0 + (value1 - value0) * t
|
|
|
|
|
|
def saturate(x):
|
|
if x < 0.0:
|
|
x = 0.0
|
|
if x > 1.0:
|
|
x = 1.0
|
|
return x
|
|
|
|
|
|
def remap(from_min, from_max, to_min, to_max, x):
|
|
return to_min + ((x - from_min) * (to_max - to_min) / (from_max - from_min))
|
|
|
|
|
|
def lerp(v0, v1, t, clamp=True):
|
|
if clamp:
|
|
t = max(0, min(1, t))
|
|
l = v0 + (v1 - v0) * t
|
|
return l
|
|
|
|
|
|
def inverse_lerp(vmin, vmax, value):
|
|
return min(1.0, max(0.0, (value - vmin) / (vmax - vmin)))
|
|
|
|
|
|
def lerp_color(c0, c1, t):
|
|
if len(c0) == 4:
|
|
r = (lerp(c0[0], c1[0], t),
|
|
lerp(c0[1], c1[1], t),
|
|
lerp(c0[2], c1[2], t),
|
|
lerp(c0[3], c1[3], t))
|
|
else:
|
|
r = (lerp(c0[0], c1[0], t),
|
|
lerp(c0[1], c1[1], t),
|
|
lerp(c0[2], c1[2], t))
|
|
return r
|
|
|
|
|
|
def inverse_lerp_color(min, max, value):
|
|
return (inverse_lerp(min[0], max[0], value[0]),
|
|
inverse_lerp(min[1], max[1], value[1]),
|
|
inverse_lerp(min[2], max[2], value[2]),
|
|
inverse_lerp(min[3], max[3], value[3]))
|
|
|
|
|
|
def linear_to_srgbx(x):
|
|
if x < 0.0:
|
|
return 0.0
|
|
elif x < 0.0031308:
|
|
return x * 12.92
|
|
elif x < 1.0:
|
|
return 1.055 * pow(x, 1.0 / 2.4) - 0.055
|
|
else:
|
|
return pow(x, 5.0 / 11.0)
|
|
|
|
|
|
def linear_to_srgb(color):
|
|
if len(color) == 4:
|
|
return (linear_to_srgbx(color[0]),
|
|
linear_to_srgbx(color[1]),
|
|
linear_to_srgbx(color[2]),
|
|
color[3])
|
|
else:
|
|
return (linear_to_srgbx(color[0]),
|
|
linear_to_srgbx(color[1]),
|
|
linear_to_srgbx(color[2]))
|
|
|
|
def srgb_to_linearx(x):
|
|
if x <= 0.04045:
|
|
return x / 12.95
|
|
elif x < 1.0:
|
|
return pow((x + 0.055) / 1.055, 2.4)
|
|
else:
|
|
return pow(x, 2.2)
|
|
|
|
|
|
def srgb_to_linear(color):
|
|
if len(color) == 4:
|
|
return (srgb_to_linearx(color[0]),
|
|
srgb_to_linearx(color[1]),
|
|
srgb_to_linearx(color[2]),
|
|
color[3])
|
|
else:
|
|
return (srgb_to_linearx(color[0]),
|
|
srgb_to_linearx(color[1]),
|
|
srgb_to_linearx(color[2]))
|
|
|
|
|
|
def count_maps(*maps):
|
|
count = 0
|
|
for map in maps:
|
|
if map is not None:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
def key_count(obj: bpy.types.Object):
|
|
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
|
return len(obj.data.shape_keys.key_blocks)
|
|
return 0
|
|
|
|
|
|
def dimensions(x):
|
|
try:
|
|
l = len(x)
|
|
return l
|
|
except:
|
|
return 1
|
|
return 1
|
|
|
|
|
|
def mag(vector: tuple):
|
|
tot = 0
|
|
for v in vector:
|
|
tot += v*v
|
|
return pow(tot, 0.5)
|
|
|
|
|
|
def lum(col: tuple):
|
|
rgb: list = col[:3]
|
|
while len(rgb) < 3:
|
|
rgb.append(0)
|
|
a = 1.0 if len(col) < 4 else col[3]
|
|
return (0.2126*rgb[0] + 0.7152*rgb[1] + 0.0722*rgb[2]) * a
|
|
|
|
|
|
def match_dimensions(socket, value):
|
|
socket_dimensions = dimensions(socket)
|
|
value_dimensions = dimensions(value)
|
|
if socket_dimensions == 3 and value_dimensions == 1:
|
|
return (value, value, value)
|
|
elif socket_dimensions == 2 and value_dimensions == 1:
|
|
return (value, value)
|
|
else:
|
|
return value
|
|
|
|
|
|
def find_pose_bone(chr_cache, *name):
|
|
props = vars.props()
|
|
|
|
arm = chr_cache.get_armature()
|
|
for n in name:
|
|
if n in arm.pose.bones:
|
|
return arm.pose.bones[n]
|
|
return None
|
|
|
|
|
|
def find_pose_bone_in_armature(arm, *name):
|
|
if (arm.type == "ARMATURE"):
|
|
for n in name:
|
|
if n in arm.pose.bones:
|
|
return arm.pose.bones[n]
|
|
return None
|
|
|
|
|
|
def find_edit_bone_in_armature(arm, *name):
|
|
if (arm.type == "ARMATURE"):
|
|
for n in name:
|
|
if n in arm.data.edit_bones:
|
|
return arm.data.edit_bones[n]
|
|
return None
|
|
|
|
|
|
def get_active_object():
|
|
"""Return the actual active object and not the context reference."""
|
|
try:
|
|
if bpy.context.active_object:
|
|
return bpy.data.objects[bpy.context.active_object.name]
|
|
except:
|
|
pass
|
|
return None
|
|
|
|
|
|
def get_active_view_layer_object():
|
|
return bpy.context.view_layer.objects.active
|
|
|
|
|
|
def set_active_object(obj, deselect_all = False):
|
|
try:
|
|
if deselect_all:
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
obj.select_set(True)
|
|
bpy.context.view_layer.objects.active = obj
|
|
return (bpy.context.active_object == obj)
|
|
except:
|
|
return False
|
|
|
|
def set_only_active_object(obj):
|
|
return set_active_object(obj, True)
|
|
|
|
|
|
def set_mode(mode):
|
|
try:
|
|
if bpy.context.object == None:
|
|
if mode != "OBJECT":
|
|
log_error("No context object, unable to set any mode but OBJECT!")
|
|
return False
|
|
return True
|
|
else:
|
|
if bpy.context.object.mode != mode:
|
|
bpy.ops.object.mode_set(mode=mode)
|
|
if bpy.context.object.mode != mode:
|
|
log_error("Unable to set " + mode + " on object: " + bpy.context.object.name)
|
|
return False
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def get_mode():
|
|
try:
|
|
return bpy.context.object.mode
|
|
except:
|
|
return "OBJECT"
|
|
|
|
|
|
def is_selected_and_active(obj):
|
|
return get_active_object() == obj and obj in bpy.context.selected_objects
|
|
|
|
|
|
def is_only_selected_and_active(obj):
|
|
return (get_active_object() == obj and
|
|
obj in bpy.context.selected_objects and
|
|
len(bpy.context.selected_objects) == 1)
|
|
|
|
|
|
def edit_mode_to(obj, only_this = False):
|
|
if object_exists(obj):
|
|
if only_this and not is_only_selected_and_active(obj):
|
|
set_only_active_object(obj)
|
|
if is_selected_and_active(obj) and get_mode() == "EDIT":
|
|
return True
|
|
else:
|
|
if set_mode("OBJECT") and set_active_object(obj) and set_mode("EDIT"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def object_mode():
|
|
return set_mode("OBJECT")
|
|
|
|
|
|
def object_mode_to(obj):
|
|
if object_exists(obj):
|
|
if get_mode() == "OBJECT" and get_active_object() == obj:
|
|
return True
|
|
if set_mode("OBJECT"):
|
|
if try_select_object(obj):
|
|
if set_active_object(obj):
|
|
return True
|
|
return False
|
|
|
|
|
|
def pose_mode_to(arm):
|
|
if object_exists_is_armature(arm):
|
|
if get_mode() == "POSE" and get_active_object() == arm:
|
|
return True
|
|
if object_mode_to(arm):
|
|
if set_mode("POSE"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def duplicate_object(obj, duplicate_actions=False, keep_actions=False) -> bpy.types.Object:
|
|
if object_exists(obj) and set_mode("OBJECT"):
|
|
if try_select_object(obj, True) and set_active_object(obj):
|
|
|
|
obj_action = None
|
|
obj_slot = None
|
|
key_action = None
|
|
key_slot = None
|
|
|
|
# store existing actions
|
|
obj_action, obj_slot = safe_get_action_slot(obj)
|
|
if not duplicate_actions:
|
|
safe_set_action(obj, None, create=False)
|
|
if obj.type == "MESH":
|
|
key_action, key_slot = safe_get_action_slot(obj.data.shape_keys)
|
|
if not duplicate_actions:
|
|
safe_set_action(obj.data.shape_keys, None, create=False)
|
|
|
|
# duplicate object
|
|
bpy.ops.object.duplicate()
|
|
duplicate = get_active_object()
|
|
|
|
# restore non-duplicated actions
|
|
if not duplicate_actions:
|
|
if key_action:
|
|
safe_set_action(obj.data.shape_keys, key_action, slot=key_slot)
|
|
if obj_action:
|
|
safe_set_action(obj, obj_action, slot=obj_slot)
|
|
|
|
# restore actions on duplicate
|
|
if keep_actions:
|
|
if key_action:
|
|
safe_set_action(duplicate.data.shape_keys, key_action, slot=key_slot)
|
|
if obj_action:
|
|
safe_set_action(duplicate, obj_action, slot=obj_slot)
|
|
|
|
return duplicate
|
|
return None
|
|
|
|
|
|
def remove_all_shape_keys(obj: bpy.types.Object):
|
|
# Bugged in Blender 4.4 (Maybe other versions too) - reverting to operators
|
|
#if obj and obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
|
# key: bpy.types.ShapeKey = None
|
|
# keys = [key for key in obj.data.shape_keys.key_blocks]
|
|
# keys.reverse() # make sure basis is last to be removed...
|
|
# for key in keys:
|
|
# obj.shape_key_remove(key)
|
|
if obj and obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
|
sms = store_mode_selection_state()
|
|
set_active_object(obj, True)
|
|
bpy.ops.object.shape_key_remove(all=True, apply_mix=False)
|
|
restore_mode_selection_state(sms)
|
|
|
|
|
|
def force_object_name(obj, name):
|
|
if name in bpy.data.objects:
|
|
existing = bpy.data.objects[name]
|
|
if existing != obj:
|
|
old_name = obj.name
|
|
rnd_id = generate_random_id(10)
|
|
existing.name = existing.name + "_" + rnd_id
|
|
obj.name = name
|
|
existing.name = old_name
|
|
else:
|
|
obj.name = name
|
|
|
|
|
|
def force_mesh_name(mesh, name):
|
|
if name in bpy.data.meshes:
|
|
existing = bpy.data.meshes[name]
|
|
if existing != mesh:
|
|
old_name = mesh.name
|
|
rnd_id = generate_random_id(10)
|
|
existing.name = existing.name + "_" + rnd_id
|
|
mesh.name = name
|
|
existing.name = old_name
|
|
else:
|
|
mesh.name = name
|
|
|
|
|
|
def force_armature_name(arm, name):
|
|
if name in bpy.data.armatures:
|
|
existing = bpy.data.armatures[name]
|
|
if existing != arm:
|
|
old_name = arm.name
|
|
rnd_id = generate_random_id(10)
|
|
existing.name = existing.name + "_" + rnd_id
|
|
arm.name = name
|
|
existing.name = old_name
|
|
else:
|
|
arm.name = name
|
|
|
|
|
|
def force_material_name(mat, name):
|
|
if name in bpy.data.materials:
|
|
existing = bpy.data.materials[name]
|
|
if existing != mat:
|
|
old_name = mat.name
|
|
rnd_id = generate_random_id(10)
|
|
existing.name = existing.name + "_" + rnd_id
|
|
mat.name = name
|
|
existing.name = old_name
|
|
else:
|
|
mat.name = name
|
|
|
|
|
|
def force_action_name(action, name):
|
|
if name in bpy.data.actions:
|
|
existing = bpy.data.actions[name]
|
|
if existing != action:
|
|
old_name = action.name
|
|
rnd_id = generate_random_id(10)
|
|
existing.name = existing.name + "_" + rnd_id
|
|
action.name = name
|
|
existing.name = old_name
|
|
else:
|
|
action.name = name
|
|
|
|
|
|
def s2lin(x):
|
|
a = 0.055
|
|
if x <= 0.04045:
|
|
y = x * (1.0/12.92)
|
|
else:
|
|
y = pow((x + a)*(1.0/(1 + a)), 2.4)
|
|
return y
|
|
|
|
def lin2s(x):
|
|
a = 0.055
|
|
if x <= 0.0031308:
|
|
y = x * 12.92
|
|
else:
|
|
y = (1 + a)*pow(x, 1/2.4) - a
|
|
return y
|
|
|
|
|
|
def strip_name(name: str):
|
|
"""Remove any .001 from the material name"""
|
|
if len(name) >= 4:
|
|
if name[-4] == "." and name[-3:].isdigit():
|
|
name = name[:-4]
|
|
return name
|
|
|
|
|
|
def deduplicate_name(name: str):
|
|
"""Remove any _01 or _1 from the material name"""
|
|
if len(name) >= 3:
|
|
if name[-3] == "_" and name[-2:].isdigit():
|
|
name = name[:-3]
|
|
elif len(name) >= 2:
|
|
if name[-2] == "_" and name[-1:].isdigit():
|
|
name = name[:-2]
|
|
return name
|
|
|
|
|
|
def source_name(name):
|
|
return strip_name(deduplicate_name(name))
|
|
|
|
|
|
def is_same_source_name(name1, name2):
|
|
return source_name(name1) == source_name(name2)
|
|
|
|
|
|
def names_to_list(names: str, delim: str = "|") -> list:
|
|
name_list = None
|
|
if names:
|
|
split = names.strip().split(delim)
|
|
for s in split:
|
|
s = s.strip()
|
|
if s:
|
|
if name_list is None:
|
|
name_list = []
|
|
name_list.append(s)
|
|
return name_list
|
|
|
|
|
|
def get_auto_index_suffix(name):
|
|
auto_index = 0
|
|
try:
|
|
if type(name) is not str:
|
|
name = name.name
|
|
if name[-4] == "|" and name[-3:].isdigit():
|
|
auto_index = int(name[-3:])
|
|
elif name[-5] == "|" and name[-4:].isdigit():
|
|
auto_index = int(name[-4:])
|
|
except:
|
|
pass
|
|
return auto_index
|
|
|
|
|
|
def is_blender_duplicate(name):
|
|
if len(name) >= 4:
|
|
if (name[-1:].isdigit() and
|
|
name[-2:].isdigit() and
|
|
name[-3:].isdigit() and
|
|
name[-4] == "."):
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_duplication_suffix(name):
|
|
if len(name) >= 4:
|
|
if (name[-1:].isdigit() and
|
|
name[-2:].isdigit() and
|
|
name[-3:].isdigit() and
|
|
name[-4] == "."):
|
|
return int(name[-3:])
|
|
return 0
|
|
|
|
|
|
def make_unique_name_in(name, keys):
|
|
""""""
|
|
if name in keys:
|
|
i = 1
|
|
while name + "_" + str(i) in keys:
|
|
i += 1
|
|
return name + "_" + str(i)
|
|
return name
|
|
|
|
|
|
def partial_match(text, search, start = 0):
|
|
"""Action names can be truncated so sometimes we have to fall back on partial name matches."""
|
|
if text and search:
|
|
ls = len(search)
|
|
lt = len(text)
|
|
if start > -1 and lt > start:
|
|
if text[start:] == search:
|
|
return True
|
|
j = 0
|
|
for i in range(start, min(start + ls, lt)):
|
|
if text[i] != search[j]:
|
|
return False
|
|
j += 1
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_longest_alpha_match(a : str, b : str):
|
|
match = difflib.SequenceMatcher(lambda x: x in " 0123456789", a, b).find_longest_match()
|
|
if match[2] == 0:
|
|
return ""
|
|
else:
|
|
return a[match[0]:(match[0] + match[2])]
|
|
|
|
|
|
def get_common_name(names):
|
|
common_name = names[0]
|
|
for i in range(1, len(names)):
|
|
common_name = get_longest_alpha_match(common_name, names[i])
|
|
while common_name[-1] in "_0123456789":
|
|
common_name = common_name[:-1]
|
|
return common_name
|
|
|
|
|
|
def get_dot_file_ext(ext):
|
|
try:
|
|
if ext[0] == ".":
|
|
return ext.lower()
|
|
else:
|
|
return f".{ext}".lower()
|
|
except:
|
|
return ""
|
|
|
|
|
|
def get_file_ext(ext):
|
|
try:
|
|
if ext[0] == ".":
|
|
return ext[1:].lower()
|
|
else:
|
|
return ext.lower()
|
|
except:
|
|
return ""
|
|
|
|
|
|
def is_file_ext(test, ext):
|
|
try:
|
|
if ext[0] == ".":
|
|
ext = ext[1:]
|
|
if test[0] == ".":
|
|
test = test[1:]
|
|
return test.lower() == ext.lower()
|
|
except:
|
|
return False
|
|
|
|
|
|
def get_set(collection) -> set:
|
|
return set(collection)
|
|
|
|
|
|
def get_set_new(collection, old: set) -> list:
|
|
current = get_set(collection)
|
|
return list(current - old)
|
|
|
|
|
|
def tag_objects():
|
|
for obj in bpy.data.objects:
|
|
obj.tag = True
|
|
|
|
|
|
def untagged_objects():
|
|
untagged = []
|
|
for obj in bpy.data.objects:
|
|
if obj.tag == False:
|
|
untagged.append(obj)
|
|
obj.tag = False
|
|
return untagged
|
|
|
|
|
|
def tag_materials():
|
|
for mat in bpy.data.materials:
|
|
if mat:
|
|
mat.tag = True
|
|
|
|
|
|
def untagged_materials():
|
|
untagged = []
|
|
for mat in bpy.data.materials:
|
|
if mat and mat.tag == False:
|
|
untagged.append(mat)
|
|
mat.tag = False
|
|
return untagged
|
|
|
|
|
|
def tag_images():
|
|
for img in bpy.data.images:
|
|
img.tag = True
|
|
|
|
|
|
def untagged_images():
|
|
untagged = []
|
|
for img in bpy.data.images:
|
|
if img.tag == False:
|
|
untagged.append(img)
|
|
img.tag = False
|
|
return untagged
|
|
|
|
|
|
def tag_actions():
|
|
for action in bpy.data.actions:
|
|
action.tag = True
|
|
|
|
|
|
def untagged_actions():
|
|
untagged = []
|
|
for action in bpy.data.actions:
|
|
if action.tag == False:
|
|
untagged.append(action)
|
|
action.tag = False
|
|
return untagged
|
|
|
|
|
|
def try_select_child_objects(obj):
|
|
try:
|
|
if obj:
|
|
if obj.type == "ARMATURE" or obj.type == "MESH" or obj.type == "EMPTY":
|
|
obj.select_set(True)
|
|
result = True
|
|
for child in obj.children:
|
|
if not try_select_child_objects(child):
|
|
result = False
|
|
return result
|
|
else:
|
|
return False
|
|
except:
|
|
return False
|
|
|
|
|
|
def add_child_objects(obj, objects, follow_armatures=False, of_type=None):
|
|
for child in obj.children:
|
|
if child not in objects:
|
|
if child.type == "ARMATURE" and not follow_armatures:
|
|
continue
|
|
if not of_type or child.type == of_type:
|
|
objects.append(child)
|
|
if child.children:
|
|
add_child_objects(child, objects, follow_armatures, of_type)
|
|
|
|
|
|
def expand_with_child_objects(objects, follow_armatures=False, of_type=None):
|
|
for obj in objects:
|
|
if obj.type == "ARMATURE" and not follow_armatures:
|
|
continue
|
|
add_child_objects(obj, objects, follow_armatures, of_type)
|
|
|
|
|
|
def get_child_objects(obj, include_parent=False, follow_armatures=False, of_type=None):
|
|
objects = []
|
|
if include_parent:
|
|
if not of_type or obj.type == of_type:
|
|
objects.append(obj)
|
|
add_child_objects(obj, objects, follow_armatures, of_type)
|
|
return objects
|
|
|
|
|
|
def try_select_object(obj, clear_selection = False):
|
|
if clear_selection:
|
|
clear_selected_objects()
|
|
try:
|
|
obj.select_set(True)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def try_select_objects(objects, clear_selection = False, object_type = None, make_active = False):
|
|
if clear_selection:
|
|
clear_selected_objects()
|
|
result = True
|
|
for obj in objects:
|
|
if object_type and obj.type != object_type:
|
|
continue
|
|
if not try_select_object(obj):
|
|
result = False
|
|
else:
|
|
if make_active:
|
|
bpy.context.view_layer.objects.active = obj
|
|
return result
|
|
|
|
|
|
def clear_selected_objects():
|
|
try:
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def get_armature(name):
|
|
if (name in bpy.data.armatures and
|
|
name in bpy.data.objects and
|
|
bpy.data.objects[name].data == bpy.data.armatures[name] and
|
|
object_exists_is_armature(bpy.data.objects[name])):
|
|
return bpy.data.objects[name]
|
|
return None
|
|
|
|
|
|
def create_reuse_armature(name):
|
|
if name in bpy.data.armatures:
|
|
bpy.data.armatures.remove(bpy.data.armatures[name])
|
|
if name in bpy.data.objects:
|
|
bpy.data.objects.remove(bpy.data.armatures[name])
|
|
arm = bpy.data.armatures.new(name)
|
|
obj = bpy.data.objects.new(name, arm)
|
|
bpy.context.collection.objects.link(obj)
|
|
return obj
|
|
|
|
|
|
def get_armature_from_objects(objects):
|
|
armatures = get_armatures_from_objects(objects)
|
|
return get_topmost_object(armatures)
|
|
|
|
|
|
def get_armatures_from_objects(objects):
|
|
armatures = []
|
|
if objects:
|
|
for obj in objects:
|
|
arm = get_armature_from_object(obj)
|
|
if arm and arm not in armatures:
|
|
armatures.append(arm)
|
|
return armatures
|
|
|
|
|
|
def get_armature_from_object(obj):
|
|
arm = None
|
|
if obj.type == "ARMATURE":
|
|
arm = obj
|
|
elif obj.type == "MESH" and obj.parent and obj.parent.type == "ARMATURE":
|
|
arm = obj.parent
|
|
return arm
|
|
|
|
|
|
def is_child_of(obj, test):
|
|
"""Returns True if obj is a child or sub-child of test"""
|
|
while test.parent:
|
|
if test.parent == obj:
|
|
return True
|
|
test = test.parent
|
|
return False
|
|
|
|
|
|
def get_topmost_object(objects):
|
|
if not objects:
|
|
return None
|
|
if len(objects) == 1:
|
|
return objects[0]
|
|
else:
|
|
for obj in objects:
|
|
for test in objects:
|
|
if obj != test:
|
|
if not is_child_of(obj, test):
|
|
return obj
|
|
return None
|
|
|
|
|
|
def float_equals(a, b):
|
|
return abs(a - b) < 0.00001
|
|
|
|
|
|
def array_to_vector(arr):
|
|
if len(arr) == 3:
|
|
return Vector((arr[0], arr[1], arr[2]))
|
|
return Vector()
|
|
|
|
|
|
def array_to_color(arr, to_srgb=False, to_linear=False):
|
|
if len(arr) == 1:
|
|
r = g = b = arr[0]
|
|
a = 1
|
|
elif len(arr) == 2:
|
|
r = arr[0]
|
|
g = arr[1]
|
|
b = 0
|
|
a = 1
|
|
elif len(arr) == 3:
|
|
r = arr[0]
|
|
g = arr[1]
|
|
b = arr[2]
|
|
a = 1
|
|
elif len(arr) == 4:
|
|
r = arr[0]
|
|
g = arr[1]
|
|
b = arr[2]
|
|
a = arr[3]
|
|
if to_srgb:
|
|
return Color((linear_to_srgbx(r), linear_to_srgbx(g), linear_to_srgbx(b)))
|
|
elif to_linear:
|
|
return Color((srgb_to_linearx(r), srgb_to_linearx(g), srgb_to_linearx(b)))
|
|
else:
|
|
return Color((r,g,b))
|
|
|
|
|
|
def color_filter(color: Color, filter: Color):
|
|
cf = Color((color.r * filter.r, color.g * filter.g, color.b * filter.b))
|
|
if cf.v < 0.001:
|
|
return color
|
|
return cf * (color.v / cf.v)
|
|
|
|
|
|
def array_to_quaternion(arr):
|
|
if len(arr) == 4:
|
|
return Quaternion((arr[3], arr[0], arr[1], arr[2]))
|
|
return Quaternion()
|
|
|
|
|
|
def strip_cc_base_name(name):
|
|
obj_name = strip_name(name.strip())
|
|
if obj_name.startswith("CC_Base_") or obj_name.startswith("CC_Game_"):
|
|
obj_name = obj_name[8:]
|
|
return obj_name
|
|
|
|
|
|
def remove_from_collection(coll, item):
|
|
for i in range(0, len(coll)):
|
|
if coll[i] == item:
|
|
coll.remove(i)
|
|
return
|
|
|
|
|
|
def delete_armature_object(arm):
|
|
if object_exists_is_armature(arm):
|
|
data = arm.data
|
|
bpy.data.objects.remove(arm)
|
|
if data:
|
|
bpy.data.armatures.remove(data)
|
|
|
|
|
|
def delete_mesh_object(obj):
|
|
if object_exists_is_mesh(obj):
|
|
data = obj.data
|
|
bpy.data.objects.remove(obj)
|
|
if data:
|
|
bpy.data.meshes.remove(data)
|
|
|
|
|
|
def delete_light_object(obj):
|
|
if object_exists_is_light(obj):
|
|
data = obj.data
|
|
bpy.data.objects.remove(obj)
|
|
if data:
|
|
bpy.data.lights.remove(data)
|
|
|
|
|
|
def delete_objects(objects, log=False):
|
|
if objects:
|
|
for obj in objects:
|
|
if log:
|
|
log_info(f" - Deleting object: {obj.name}")
|
|
delete_object(obj)
|
|
|
|
|
|
def delete_object(obj):
|
|
if object_exists(obj):
|
|
try:
|
|
data = obj.data
|
|
except:
|
|
data = None
|
|
if data:
|
|
if obj.type == "MESH":
|
|
try:
|
|
bpy.data.meshes.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "ARMATURE":
|
|
try:
|
|
bpy.data.armatures.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "LIGHT":
|
|
try:
|
|
bpy.data.lights.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "CAMERA":
|
|
try:
|
|
bpy.data.camera.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "CURVE" or obj.type=="SURFACE" or obj.type == "FONT":
|
|
try:
|
|
bpy.data.curves.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "META":
|
|
try:
|
|
bpy.data.metaballs.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "VOLUME":
|
|
try:
|
|
bpy.data.volumes.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "GPENCIL":
|
|
try:
|
|
bpy.data.grease_pencils.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "LATICE":
|
|
try:
|
|
bpy.data.lattices.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "EMPTY":
|
|
try:
|
|
if obj.data:
|
|
if obj.data.type == "IMAGE":
|
|
bpy.data.images.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "LIGHT_PROBE":
|
|
try:
|
|
bpy.data.lightprobes.remove(data)
|
|
except:
|
|
pass
|
|
elif obj.type == "SPEAKER":
|
|
try:
|
|
bpy.data.speakers.remove(data)
|
|
except:
|
|
pass
|
|
try:
|
|
bpy.data.objects.remove(obj)
|
|
except:
|
|
pass
|
|
|
|
|
|
def delete_actions(actions):
|
|
for action in actions:
|
|
try:
|
|
bpy.data.actions.remove(action)
|
|
except: ...
|
|
|
|
|
|
def get_object_tree(obj, objects = None):
|
|
if objects is None:
|
|
objects = []
|
|
if object_exists(obj):
|
|
objects.append(obj)
|
|
for child_obj in obj.children:
|
|
get_object_tree(child_obj, objects)
|
|
return objects
|
|
|
|
|
|
def delete_object_tree(obj):
|
|
objects = get_object_tree(obj)
|
|
for obj in objects:
|
|
delete_object(obj)
|
|
|
|
|
|
def hide_tree(obj, hide=True, render=False):
|
|
objects = get_object_tree(obj)
|
|
for obj in objects:
|
|
try:
|
|
obj.hide_set(hide)
|
|
if render:
|
|
obj.hide_render = hide
|
|
except: ...
|
|
|
|
|
|
def show(obj: bpy.types.Object, show=True, render=False):
|
|
hide(obj, hide=not show, render=render)
|
|
|
|
|
|
def hide(obj: bpy.types.Object, hide=True, render=False):
|
|
try:
|
|
obj.hide_set(hide)
|
|
if render:
|
|
obj.hide_render = hide
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def unhide(obj):
|
|
# TODO expand this to force visible in tmp collection if unable to make visible with hide_set
|
|
# but will require something to remove tmp collection later...
|
|
return hide(obj, hide=False)
|
|
|
|
|
|
def get_context_area(context, area_type):
|
|
if context is None:
|
|
context = bpy.context
|
|
for area in context.screen.areas:
|
|
if area.type == area_type:
|
|
return area
|
|
return None
|
|
|
|
|
|
def get_context_mesh(context=None):
|
|
if context is None:
|
|
context = bpy.context
|
|
if object_exists_is_mesh(context.object):
|
|
return context.object
|
|
return None
|
|
|
|
|
|
def get_context_material(context=None):
|
|
if context is None:
|
|
context = bpy.context
|
|
try:
|
|
return context.object.material_slots[context.object.active_material_index].material
|
|
except:
|
|
return None
|
|
|
|
|
|
def get_context_armature(context):
|
|
if context.object:
|
|
if object_exists_is_armature(context.object):
|
|
return context.object
|
|
try:
|
|
arm = context.object.parent
|
|
if object_exists_is_armature(arm):
|
|
return arm
|
|
except:
|
|
pass
|
|
return None
|
|
|
|
|
|
def get_context_character(context, strict=False):
|
|
"""strict: selected must part of the character"""
|
|
props = vars.props()
|
|
if not context:
|
|
context = bpy.context
|
|
chr_cache = props.get_context_character_cache(context)
|
|
|
|
obj = context.object
|
|
mat = get_context_material(context)
|
|
obj_cache = None
|
|
mat_cache = None
|
|
|
|
if chr_cache:
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
mat_cache = chr_cache.get_material_cache(mat)
|
|
arm = chr_cache.get_armature()
|
|
|
|
# if the context object is an armature or child of armature that is not part of this chr_cache
|
|
# clear the chr_cache, as this is a separate generic character.
|
|
if obj and not obj_cache:
|
|
if not chr_cache.is_related_object(obj):
|
|
if obj.type == "ARMATURE" and obj != arm:
|
|
chr_cache = None
|
|
elif obj.type == "MESH" and obj.parent and obj.parent != arm:
|
|
chr_cache = None
|
|
|
|
# if strict only return chr_cache from valid object_cache context object
|
|
# otherwise it could return the first and only chr_cache
|
|
if strict and (not obj or not obj_cache):
|
|
chr_cache = None
|
|
|
|
return chr_cache, obj, mat, obj_cache, mat_cache
|
|
|
|
|
|
def get_current_tool_idname(context = None):
|
|
if context is None:
|
|
context = bpy.context
|
|
tool_idname = context.workspace.tools.from_space_view3d_mode(context.mode).idname
|
|
return tool_idname
|
|
|
|
|
|
def add_layer_collections(layer_collection: bpy.types.LayerCollection, layer_collections, search = None):
|
|
if search:
|
|
if type(search) is str and search in layer_collection.name:
|
|
layer_collections.append(layer_collection)
|
|
elif type(search) is bpy.types.Collection and layer_collection.collection == search:
|
|
layer_collections.append(layer_collection)
|
|
elif type(search) is bpy.types.LayerCollection and layer_collection == search:
|
|
layer_collections.append(layer_collection)
|
|
else:
|
|
layer_collections.append(layer_collection)
|
|
child_layer_collection : bpy.types.LayerCollection
|
|
for child_layer_collection in layer_collection.children:
|
|
add_layer_collections(child_layer_collection, layer_collections, search)
|
|
|
|
|
|
def get_view_layer_collections(search=None):
|
|
layer_collections = []
|
|
for view_layer in bpy.context.scene.view_layers:
|
|
for layer_collection in view_layer.layer_collection.children:
|
|
add_layer_collections(layer_collection, layer_collections, search)
|
|
return layer_collections
|
|
|
|
|
|
# C.scene.view_layers[0].layer_collection.children[0].exclude
|
|
def limit_view_layer_to_collection(collection_name, *items):
|
|
layer_collections = []
|
|
to_hide = []
|
|
# exclude all active layer collections
|
|
for view_layer in bpy.context.scene.view_layers:
|
|
layer_collection : bpy.types.LayerCollection
|
|
for layer_collection in view_layer.layer_collection.children:
|
|
if not layer_collection.exclude:
|
|
for obj in layer_collection.collection.objects:
|
|
if not obj.visible_get():
|
|
to_hide.append(obj)
|
|
layer_collections.append(layer_collection)
|
|
layer_collection.exclude = True
|
|
# add a new collection just for these items
|
|
tmp_collection = bpy.data.collections.new(collection_name)
|
|
bpy.context.scene.collection.children.link(tmp_collection)
|
|
for item in items:
|
|
if item:
|
|
if type(item) is list:
|
|
for sub_item in item:
|
|
tmp_collection.objects.link(sub_item)
|
|
unhide(sub_item)
|
|
else:
|
|
tmp_collection.objects.link(item)
|
|
unhide(item)
|
|
# return the temp collection and the layers exlcuded
|
|
return tmp_collection, layer_collections, to_hide
|
|
|
|
|
|
def create_collection(name, existing=True, parent_collection: bpy.types.Collection = None):
|
|
if name in bpy.data.collections and existing:
|
|
return bpy.data.collections[name]
|
|
else:
|
|
collection = bpy.data.collections.new(name)
|
|
if parent_collection:
|
|
parent_collection.children.link(collection)
|
|
else:
|
|
bpy.context.scene.collection.children.link(collection)
|
|
return collection
|
|
|
|
|
|
def restore_limited_view_layers(tmp_collection, layer_collections, to_hide):
|
|
objects = []
|
|
for obj in tmp_collection.objects:
|
|
objects.append(obj)
|
|
for obj in objects:
|
|
tmp_collection.objects.unlink(obj)
|
|
bpy.context.scene.collection.children.unlink(tmp_collection)
|
|
bpy.data.collections.remove(tmp_collection)
|
|
for layer_collection in layer_collections:
|
|
layer_collection.exclude = False
|
|
for obj in to_hide:
|
|
hide(obj)
|
|
|
|
|
|
def force_visible_in_scene(collection_name, *objects):
|
|
tmp_collection = bpy.data.collections.new(collection_name)
|
|
bpy.context.scene.collection.children.link(tmp_collection)
|
|
for obj in objects:
|
|
if not obj.visible_get():
|
|
log_info(f"Object: {obj.name} is not visible or in a hidden collection. Linking to temporary root collection and making visible.")
|
|
unhide(obj)
|
|
tmp_collection.objects.link(obj)
|
|
return tmp_collection
|
|
|
|
|
|
def restore_visible_in_scene(tmp_collection : bpy.types.Collection):
|
|
objects = []
|
|
for obj in tmp_collection.objects:
|
|
objects.append(obj)
|
|
for obj in objects:
|
|
log_info(f"Object: {obj.name} Unlinking from temporary root collection and hiding.")
|
|
hide(obj)
|
|
tmp_collection.objects.unlink(obj)
|
|
bpy.context.scene.collection.children.unlink(tmp_collection)
|
|
bpy.data.collections.remove(tmp_collection)
|
|
|
|
|
|
def get_object_scene_collections(obj, exclude_rbw = True):
|
|
collections = []
|
|
if obj.name in bpy.context.scene.collection.objects:
|
|
collections.append(bpy.context.scene.collection)
|
|
rbw = bpy.context.scene.rigidbody_world
|
|
for col in bpy.data.collections:
|
|
# exclude rigid body world collections
|
|
if exclude_rbw and rbw and (col == rbw.collection or col == rbw.constraints):
|
|
continue
|
|
if col != bpy.context.scene.collection and obj.name in col.objects:
|
|
collections.append(col)
|
|
return collections
|
|
|
|
|
|
def get_all_scene_collections(exclude_rbw = True):
|
|
collections = []
|
|
collections.append(bpy.context.scene.collection)
|
|
rbw = bpy.context.scene.rigidbody_world
|
|
for col in bpy.data.collections:
|
|
# exclude rigid body world collections
|
|
if exclude_rbw and rbw and (col == rbw.collection or col == rbw.constraints):
|
|
continue
|
|
if col != bpy.context.scene.collection:
|
|
collections.append(col)
|
|
return collections
|
|
|
|
|
|
def remove_from_scene_collections(obj, collections = None, exclude_rbw = True):
|
|
if collections is None:
|
|
collections = get_all_scene_collections()
|
|
rbw = bpy.context.scene.rigidbody_world
|
|
for col in collections:
|
|
# exclude rigid body world collections
|
|
if exclude_rbw and rbw and (col == rbw.collection or col == rbw.constraints):
|
|
continue
|
|
if obj.name in col.objects:
|
|
col.objects.unlink(obj)
|
|
|
|
|
|
def move_object_to_scene_collections(obj, collections, exclude_rbw = True):
|
|
remove_from_scene_collections(obj)
|
|
rbw = bpy.context.scene.rigidbody_world
|
|
for col in collections:
|
|
# exclude rigid body world collections
|
|
if exclude_rbw and rbw and (col == rbw.collection or col == rbw.constraints):
|
|
continue
|
|
if obj.name not in col.objects:
|
|
col.objects.link(obj)
|
|
|
|
|
|
def store_mode_selection_state():
|
|
mode = get_mode()
|
|
active = get_active_object()
|
|
selection = bpy.context.selected_objects.copy()
|
|
return [mode, active, selection,
|
|
(bpy.context.scene.frame_current, bpy.context.scene.frame_start, bpy.context.scene.frame_end)]
|
|
|
|
|
|
def restore_mode_selection_state(store, include_frames=True):
|
|
try:
|
|
set_mode("OBJECT")
|
|
try_select_objects(store[2], True)
|
|
set_active_object(store[1])
|
|
set_mode(store[0])
|
|
if include_frames:
|
|
bpy.context.scene.frame_current = store[3][0]
|
|
bpy.context.scene.frame_start = store[3][1]
|
|
bpy.context.scene.frame_end = store[3][2]
|
|
except:
|
|
pass
|
|
|
|
|
|
def store_render_visibility_state(objects=None):
|
|
rv = {}
|
|
obj : bpy.types.Object
|
|
if objects is None:
|
|
objects = bpy.data.objects
|
|
elif type(objects) is bpy.types.Object:
|
|
objects = [objects]
|
|
for obj in objects:
|
|
if object_exists(obj):
|
|
visible = obj.visible_get()
|
|
render = not obj.hide_render
|
|
rv[obj.name] = [visible, render]
|
|
return rv
|
|
|
|
|
|
def restore_render_visibility_state(rv):
|
|
obj : bpy.types.Object
|
|
for obj_name in rv:
|
|
if obj_name in bpy.data.objects:
|
|
obj = bpy.data.objects[obj_name]
|
|
if object_exists(obj):
|
|
visible, render = rv[obj.name]
|
|
try:
|
|
obj.hide_render = not render
|
|
hide(obj, not visible)
|
|
except:
|
|
pass
|
|
|
|
|
|
def set_only_render_visible(object):
|
|
obj : bpy.types.Object
|
|
for obj in bpy.data.objects:
|
|
if object_exists(obj):
|
|
visible = obj.visible_get()
|
|
render = not obj.hide_render
|
|
if obj == object:
|
|
try:
|
|
obj.hide_render = False
|
|
unhide(obj)
|
|
except:
|
|
pass
|
|
else:
|
|
try:
|
|
obj.hide_render = True
|
|
hide(obj)
|
|
except:
|
|
pass
|
|
|
|
|
|
def store_object_transform(obj: bpy.types.Object):
|
|
T = (obj.location.copy(),
|
|
obj.rotation_mode,
|
|
obj.rotation_quaternion.copy(),
|
|
[a for a in obj.rotation_axis_angle],
|
|
obj.rotation_euler.copy(),
|
|
obj.scale.copy())
|
|
return T
|
|
|
|
|
|
def restore_object_transform(obj: bpy.types.Object, T: tuple, ignore_scale=False):
|
|
if ignore_scale:
|
|
obj.location, obj.rotation_mode, obj.rotation_quaternion, obj.rotation_axis_angle, obj.rotation_euler, scale = T
|
|
else:
|
|
obj.location, obj.rotation_mode, obj.rotation_quaternion, obj.rotation_axis_angle, obj.rotation_euler, obj.scale = T
|
|
|
|
|
|
def reset_object_transform(obj: bpy.types.Object):
|
|
obj.location = Vector((0,0,0))
|
|
obj.rotation_quaternion = Quaternion((1.0, 0.0, 0.0, 0.0))
|
|
obj.rotation_euler = Euler((0.0, -0.0, 0.0), 'XYZ')
|
|
obj.rotation_axis_angle = [0,0,0,0]
|
|
|
|
|
|
def get_region_3d(context=None):
|
|
space = get_view_3d_space(context)
|
|
if space:
|
|
return space, space.region_3d
|
|
return None, None
|
|
|
|
|
|
def get_3d_regions(context=None):
|
|
spaces = get_view_3d_spaces(context)
|
|
if spaces:
|
|
return [ s.region_3d for s in spaces ]
|
|
return []
|
|
|
|
|
|
def get_view_3d_space(context=None) -> bpy.types.Space:
|
|
try:
|
|
if not context:
|
|
context = bpy.context
|
|
space_data = bpy.context.space_data
|
|
if space_data and space_data.tpye == "VIEW_3D":
|
|
return space_data
|
|
except: ...
|
|
try:
|
|
area = get_view_3d_area(context)
|
|
if area:
|
|
return area.spaces.active
|
|
except: ...
|
|
log_warn("Unable to get view 3d space!")
|
|
return None
|
|
|
|
|
|
def get_view_3d_spaces(context=None) -> bpy.types.Space:
|
|
try:
|
|
areas = get_view_3d_areas(context)
|
|
if areas:
|
|
return [ area.spaces.active for area in areas ]
|
|
except: ...
|
|
log_warn("Unable to get view 3d spaces!")
|
|
return []
|
|
|
|
|
|
def get_view_3d_shading(context=None) -> bpy.types.View3DShading:
|
|
try:
|
|
if not context:
|
|
context = bpy.context
|
|
space_data = context.space_data
|
|
if space_data and space_data.type == "VIEW_3D":
|
|
return space_data.shading
|
|
except: ...
|
|
try:
|
|
space_data = get_view_3d_space(context)
|
|
if space_data:
|
|
return space_data.shading
|
|
except: ...
|
|
log_warn("Unable to get view space shading!")
|
|
return None
|
|
|
|
|
|
def get_view_3d_override_context():
|
|
for window_manager in bpy.data.window_managers:
|
|
for window in window_manager.windows:
|
|
for area in window.screen.areas:
|
|
if area.type == 'VIEW_3D':
|
|
for region in area.regions:
|
|
if region.type == 'WINDOW':
|
|
return dict(window=window, area=area, region=region)
|
|
return None
|
|
|
|
|
|
def get_view_3d_area(context=None) -> bpy.types.Area:
|
|
try:
|
|
if not context:
|
|
context = bpy.context
|
|
for area in context.screen.areas:
|
|
if area.type == 'VIEW_3D':
|
|
return area
|
|
except: ...
|
|
return None
|
|
|
|
|
|
def get_view_3d_areas(context=None) -> bpy.types.Area:
|
|
areas = []
|
|
try:
|
|
if not context:
|
|
context = bpy.context
|
|
for area in context.screen.areas:
|
|
if area.type == 'VIEW_3D':
|
|
areas.append(area)
|
|
except:
|
|
areas = []
|
|
return areas
|
|
|
|
|
|
def align_object_to_view(obj, context):
|
|
if context is None:
|
|
context = bpy.context
|
|
area_3d = None
|
|
if context.area and context.area.type == 'VIEW_3D':
|
|
area_3d = bpy.context.area
|
|
else:
|
|
for area in context.screen.areas:
|
|
if area.type == 'VIEW_3D':
|
|
area_3d = area
|
|
if area_3d:
|
|
view_space = area_3d.spaces.active
|
|
r3d = view_space.region_3d
|
|
loc = r3d.view_location
|
|
rot = r3d.view_rotation
|
|
D = r3d.view_distance
|
|
v = Vector((0,0,1)) * D
|
|
|
|
obj.location = loc + rot @ v
|
|
set_transform_rotation(obj, rot)
|
|
|
|
|
|
def has_data_path(obj: bpy.types.Object, path: str):
|
|
try:
|
|
prop = obj.path_resolve(path)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def get_data_path_object_name(data_path: str):
|
|
if data_path.startswith("pose.bones["):
|
|
start = data_path.find('"', 0) + 1
|
|
end = data_path.find('"', start)
|
|
return data_path[start:end]
|
|
elif data_path.startswith("key_blocks["):
|
|
start = data_path.find('"', 0) + 1
|
|
end = data_path.find('"', start)
|
|
return data_path[start:end]
|
|
return ""
|
|
|
|
|
|
def copy_action(action: bpy.types.Action, new_name, reuse=False):
|
|
if reuse and new_name in bpy.data.actions:
|
|
bpy.data.actions.remove(bpy.data.actions[new_name])
|
|
new_action = action.copy()
|
|
new_action.name = new_name
|
|
return new_action
|
|
|
|
|
|
def make_action(name, reuse=False, slot_type=None, target_obj=None, slot_name=None, clear=False):
|
|
action = None
|
|
if reuse and name in bpy.data.actions:
|
|
action = bpy.data.actions[name]
|
|
if not action:
|
|
action = bpy.data.actions.new(name)
|
|
if clear:
|
|
clear_action(action)
|
|
if B440():
|
|
if target_obj:
|
|
if not slot_type:
|
|
slot_type = get_slot_type_for(target_obj)
|
|
if not slot_name:
|
|
slot_name = f"SLOT-{slot_type}"
|
|
make_action_slot(action, slot_type, slot_name)
|
|
return action
|
|
|
|
|
|
def make_action_slot(action, slot_type, slot_name):
|
|
if action and B440():
|
|
for slot in action.slots:
|
|
if slot.target_id_type == slot_type and strip_name(slot.name) == slot_name:
|
|
return slot
|
|
for slot in action.slots:
|
|
if slot.target_id_type == slot_type:
|
|
return slot
|
|
return action.slots.new(slot_type, slot_name)
|
|
return None
|
|
|
|
|
|
def get_action_slot(action, slot_type):
|
|
if action and B440():
|
|
for slot in action.slots:
|
|
if slot.target_id_type == slot_type:
|
|
return slot
|
|
return None
|
|
|
|
|
|
def find_action_slot(action, slot_type=None, slot_name=None, slot_id=None):
|
|
if action and B440():
|
|
for slot in action.slots:
|
|
if slot_type and slot_id:
|
|
if slot.target_id_type == slot_type and slot.identifier == slot_id:
|
|
return slot
|
|
elif slot_type and slot_name:
|
|
if slot.target_id_type == slot_type and slot.name_display == slot_name:
|
|
return slot
|
|
elif slot_id:
|
|
if slot.identifier == slot_id:
|
|
return slot
|
|
elif slot_name:
|
|
if slot.name_display == slot_name:
|
|
return slot
|
|
elif slot_type:
|
|
if slot.target_id_type == slot_type:
|
|
return slot
|
|
return None
|
|
|
|
|
|
def get_slot_type_for(obj):
|
|
T = type(obj)
|
|
slot_type = "OBJECT"
|
|
if T is bpy.types.Key:
|
|
slot_type = "KEY"
|
|
if (T is bpy.types.Light or
|
|
T is bpy.types.SpotLight or
|
|
T is bpy.types.SunLight or
|
|
T is bpy.types.AreaLight or
|
|
T is bpy.types.PointLight):
|
|
slot_type = "LIGHT"
|
|
if T is bpy.types.Camera:
|
|
slot_type = "CAMERA"
|
|
return slot_type
|
|
|
|
|
|
def set_action_slot(obj, action, slot=None):
|
|
"""Blender 4.4+ Only:
|
|
Set the obj.animation_data.action_slot to the supplied slot or
|
|
the first action slot with the matching slot_type"""
|
|
if obj and action and B440():
|
|
if slot:
|
|
try:
|
|
obj.animation_data.action_slot = slot
|
|
except Exception as e:
|
|
log_error(f"Unable to set action slot {action} / {slot}", e)
|
|
return True
|
|
else:
|
|
slot_type = get_slot_type_for(obj)
|
|
slot = get_action_slot(action, slot_type)
|
|
if slot:
|
|
try:
|
|
obj.animation_data.action_slot = slot
|
|
except:
|
|
log_error(f"Unable to set action slot by type: {slot_type} / {action} / {slot}")
|
|
return False
|
|
return True
|
|
|
|
|
|
def safe_get_action(obj) -> bpy.types.Action:
|
|
if obj:
|
|
try:
|
|
if obj.animation_data:
|
|
return obj.animation_data.action
|
|
except:
|
|
log_warn(f"Unable to get action from {obj.name}")
|
|
return None
|
|
|
|
|
|
def safe_get_action_slot(obj) -> bpy.types.Action:
|
|
if obj:
|
|
try:
|
|
if obj.animation_data:
|
|
if B440():
|
|
return obj.animation_data.action, obj.animation_data.action_slot
|
|
return obj.animation_data.action, None
|
|
except:
|
|
log_warn(f"Unable to get action from {obj.name}")
|
|
return None, None
|
|
|
|
|
|
def safe_set_action(obj, action, create=True, slot=None):
|
|
result = False
|
|
if obj:
|
|
try:
|
|
if create and not obj.animation_data:
|
|
obj.animation_data_create()
|
|
if obj.animation_data:
|
|
obj.animation_data.action = action
|
|
set_action_slot(obj, action, slot)
|
|
result = True
|
|
except Exception as e:
|
|
action_name = action.name if action else "None"
|
|
log_error(f"Unable to set action {action_name} to {obj.name}", e)
|
|
result = False
|
|
return result
|
|
|
|
|
|
def clear_action(action):
|
|
if action:
|
|
try:
|
|
if B440():
|
|
for layer in action.layers:
|
|
for strip in layer.strips:
|
|
for channelbag in strip.channelbags:
|
|
channelbag.fcurves.clear()
|
|
while action.slots:
|
|
action.slots.remove(action.slots[0])
|
|
else:
|
|
action.fcurves.clear()
|
|
return True
|
|
except:
|
|
log_error(f"Unable to clear action: {action}")
|
|
return False
|
|
|
|
|
|
def get_action_channelbags_list(action: bpy.types.Action):
|
|
channels = []
|
|
if action:
|
|
if B440():
|
|
channelbags = get_action_channelbags(action)
|
|
if channelbags:
|
|
for channelbag in channelbags:
|
|
channels.append(channelbag)
|
|
else:
|
|
channels.append(action)
|
|
return channels
|
|
|
|
|
|
def get_action_channelbags(action: bpy.types.Action):
|
|
if action:
|
|
if B440():
|
|
if not action.layers:
|
|
layer = action.layers.new("Layer")
|
|
else:
|
|
layer = action.layers[0]
|
|
if not layer.strips:
|
|
strip = layer.strips.new(type='KEYFRAME')
|
|
else:
|
|
strip = layer.strips[0]
|
|
return strip.channelbags
|
|
return None
|
|
|
|
|
|
def get_action_groups(action: bpy.types.Action) -> List[bpy.types.ActionGroup]:
|
|
groups = []
|
|
if B440():
|
|
channels = get_action_channelbags_list(action)
|
|
for channel in channels:
|
|
for group in channel.groups:
|
|
groups.append(group)
|
|
else:
|
|
for group in action.groups:
|
|
groups.append(group)
|
|
return groups
|
|
|
|
|
|
def get_action_fcurves(action: bpy.types.Action) -> List[bpy.types.FCurve]:
|
|
fcurves = []
|
|
if B440():
|
|
channels = get_action_channelbags_list(action)
|
|
for channel in channels:
|
|
for fcurve in channel.fcurves:
|
|
fcurves.append(fcurve)
|
|
else:
|
|
for fcurve in action.fcurves:
|
|
fcurves.append(fcurve)
|
|
return fcurves
|
|
|
|
|
|
def get_action_channelbag(action: bpy.types.Action, slot=None, slot_type=None):
|
|
if not action:
|
|
return None
|
|
if B440() and (slot or slot_type):
|
|
if not action.layers:
|
|
layer = action.layers.new("Layer")
|
|
else:
|
|
layer = action.layers[0]
|
|
if not layer.strips:
|
|
strip = layer.strips.new(type='KEYFRAME')
|
|
else:
|
|
strip = layer.strips[0]
|
|
if slot_type and not slot:
|
|
slot = get_action_slot(action, slot_type)
|
|
if slot:
|
|
channelbag = strip.channelbag(slot, ensure=True)
|
|
if channelbag:
|
|
return channelbag
|
|
if B500():
|
|
# actions do not have fcurves in B5
|
|
return None
|
|
else:
|
|
return action
|
|
|
|
|
|
def get_object_action_channelbag(obj):
|
|
action, slot = safe_get_action_slot(obj)
|
|
channels = get_action_channelbags_list(action)
|
|
for channel in channels:
|
|
if channel.slot == slot:
|
|
return channel
|
|
return None
|
|
|
|
|
|
def get_action_targets(obj):
|
|
targets = []
|
|
if object_exists_is_armature(obj):
|
|
targets.append(obj)
|
|
elif object_exists_is_mesh(obj):
|
|
targets.append(obj.data.shape_keys)
|
|
elif object_exists_is_light(obj) or object_exists_is_camera(obj):
|
|
targets.append(obj)
|
|
targets.append(obj.data)
|
|
return targets
|
|
|
|
|
|
def index_in_collection(item, collection):
|
|
for i, o in enumerate(collection):
|
|
if o == item:
|
|
return i
|
|
return -1
|
|
|
|
|
|
def collection_at_index(index, collection):
|
|
if index >= 0 and index < len(collection):
|
|
return collection[index]
|
|
return None
|
|
|
|
|
|
def set_active_layer_collection(layer_collection):
|
|
old = bpy.context.view_layer.active_layer_collection
|
|
bpy.context.view_layer.active_layer_collection = layer_collection
|
|
return old
|
|
|
|
|
|
def get_active_layer_collection():
|
|
return bpy.context.view_layer.active_layer_collection
|
|
|
|
|
|
def set_active_layer_collection_from(obj):
|
|
nlc = find_layer_collection_containing(obj)
|
|
return set_active_layer_collection(nlc)
|
|
|
|
|
|
def find_layer_collection_containing(obj, layer_collection = None):
|
|
if not layer_collection:
|
|
layer_collection = bpy.context.view_layer.layer_collection
|
|
if obj.name in layer_collection.collection.objects:
|
|
return layer_collection
|
|
for child in layer_collection.children:
|
|
found = find_layer_collection_containing(obj, child)
|
|
if found:
|
|
return found
|
|
return None
|
|
|
|
|
|
def find_layer_collection(name, layer_collection = None):
|
|
if not layer_collection:
|
|
layer_collection = bpy.context.view_layer.layer_collection
|
|
if layer_collection.name == name:
|
|
return layer_collection
|
|
for child in layer_collection.children:
|
|
found = find_layer_collection(name, child)
|
|
if found:
|
|
return found
|
|
return None
|
|
|
|
|
|
def clear_prop_collection(col):
|
|
try:
|
|
col.clear()
|
|
return True
|
|
except:
|
|
pass
|
|
try:
|
|
while col:
|
|
col.remove(col[0])
|
|
return True
|
|
except:
|
|
pass
|
|
log_error(f"Unable to clear property collection: {col}")
|
|
return False
|
|
|
|
|
|
def prop_to_list(prop):
|
|
L = len(prop)
|
|
result = [0]*L
|
|
prop.foreach_get(result)
|
|
return result
|
|
|
|
|
|
def B290():
|
|
return is_blender_version("2.90.0")
|
|
|
|
def B291():
|
|
return is_blender_version("2.91.0")
|
|
|
|
def B292():
|
|
return is_blender_version("2.92.0")
|
|
|
|
def B293():
|
|
return is_blender_version("2.93.0")
|
|
|
|
def B300():
|
|
return is_blender_version("3.0.0")
|
|
|
|
def B310():
|
|
return is_blender_version("3.1.0")
|
|
|
|
def B320():
|
|
return is_blender_version("3.2.0")
|
|
|
|
def B321():
|
|
return is_blender_version("3.2.1")
|
|
|
|
def B330():
|
|
return is_blender_version("3.3.0")
|
|
|
|
def B340():
|
|
return is_blender_version("3.4.0")
|
|
|
|
def B341():
|
|
return is_blender_version("3.4.1")
|
|
|
|
def B350():
|
|
return is_blender_version("3.5.0")
|
|
|
|
def B360():
|
|
return is_blender_version("3.6.0")
|
|
|
|
def B400():
|
|
return is_blender_version("4.0.0")
|
|
|
|
def B401():
|
|
return is_blender_version("4.0.1")
|
|
|
|
def B410():
|
|
return is_blender_version("4.1.0")
|
|
|
|
def B420():
|
|
return is_blender_version("4.2.0")
|
|
|
|
def B430():
|
|
return is_blender_version("4.3.0")
|
|
|
|
def B440():
|
|
return is_blender_version("4.4.0")
|
|
|
|
def B500():
|
|
return is_blender_version("5.0.0")
|
|
|
|
VER_CACHE = {}
|
|
def is_blender_version(version: str, test = "GTE"):
|
|
"""e.g. is_blender_version("3.0.0", "GTE")"""
|
|
|
|
global VER_CACHE
|
|
cid = (version, test)
|
|
if cid in VER_CACHE:
|
|
return VER_CACHE[cid]
|
|
|
|
major, minor, subversion = version.split(".")
|
|
blender_version = bpy.app.version
|
|
|
|
v_test = int(major) * 1000000 + int(minor) * 1000 + int(subversion)
|
|
v_blender = blender_version[0] * 1000000 + blender_version[1] * 1000 + blender_version[2]
|
|
|
|
result = False
|
|
if test == "GTE" and v_blender >= v_test:
|
|
result = True
|
|
elif test == "GT" and v_blender > v_test:
|
|
result = True
|
|
elif test == "LT" and v_blender < v_test:
|
|
result = True
|
|
elif test == "LTE" and v_blender <= v_test:
|
|
result = True
|
|
elif test == "EQ" and v_blender == v_test:
|
|
result = True
|
|
elif test == "NE" and v_blender != v_test:
|
|
result = True
|
|
VER_CACHE[cid] = result
|
|
return result
|
|
|
|
|
|
def is_addon_version(version: str, test = "GTE"):
|
|
"""e.g. is_addon_version("v1.1.8", "GTE")"""
|
|
major, minor, subversion = version[1:].split(".")
|
|
addon_version = vars.VERSION_STRING
|
|
addon_major, addon_minor, addon_subversion = addon_version[1:].split(".")
|
|
|
|
v_test = int(major) * 1000000 + int(minor) * 1000 + int(subversion)
|
|
v_addon = int(addon_major) * 1000000 + int(addon_minor) * 1000 + int(addon_subversion)
|
|
|
|
if test == "GTE" and v_addon >= v_test:
|
|
return True
|
|
elif test == "GT" and v_addon > v_test:
|
|
return True
|
|
elif test == "LT" and v_addon < v_test:
|
|
return True
|
|
elif test == "LTE" and v_addon <= v_test:
|
|
return True
|
|
elif test == "EQ" and v_addon == v_test:
|
|
return True
|
|
elif test == "NE" and v_addon != v_test:
|
|
return True
|
|
return False
|
|
|
|
|
|
def clear_reports():
|
|
win = bpy.context.window_manager.windows[0]
|
|
temp_area = True
|
|
info_area = win.screen.areas[0]
|
|
# try to find an existing info area
|
|
for area in win.screen.areas:
|
|
if info_area.type == "INFO":
|
|
info_area = area
|
|
temp_area = False
|
|
|
|
# other wise turn the first area into an info area temporarily
|
|
if temp_area:
|
|
area_type = info_area.type
|
|
info_area.type = "INFO"
|
|
|
|
context = bpy.context.copy()
|
|
context['window'] = win
|
|
context['screen'] = win.screen
|
|
context['area'] = win.screen.areas[0]
|
|
bpy.ops.info.select_all(context, action='SELECT')
|
|
bpy.ops.info.report_delete(context)
|
|
|
|
# restore the temp area
|
|
if temp_area:
|
|
info_area.type = area_type
|
|
|
|
|
|
def get_last_report():
|
|
win = bpy.context.window_manager.windows[0]
|
|
temp_area = True
|
|
info_area = win.screen.areas[0]
|
|
# try to find an existing info area
|
|
for area in win.screen.areas:
|
|
if info_area.type == "INFO":
|
|
info_area = area
|
|
temp_area = False
|
|
|
|
# other wise turn the first area into an info area temporarily
|
|
if temp_area:
|
|
area_type = info_area.type
|
|
info_area.type = "INFO"
|
|
|
|
context = bpy.context.copy()
|
|
context['window'] = win
|
|
context['screen'] = win.screen
|
|
context['area'] = win.screen.areas[0]
|
|
bpy.ops.info.select_all(context, action='SELECT')
|
|
bpy.ops.info.report_copy(context)
|
|
|
|
# restore the temp area
|
|
if temp_area:
|
|
info_area.type = area_type
|
|
|
|
# return the last line
|
|
clipboard = bpy.context.window_manager.clipboard
|
|
lines = clipboard.splitlines()
|
|
return lines[-1]
|
|
|
|
|
|
def match_wild(test: str, match_list: list) -> bool:
|
|
if test and match_list:
|
|
for match in match_list:
|
|
if test == match:
|
|
return True
|
|
elif match.startswith("*") and match.endswith("*"):
|
|
if match[1:-1] in test:
|
|
return True
|
|
elif match.startswith("*"):
|
|
if match[1:] in test:
|
|
return True
|
|
elif match.endswith("*"):
|
|
if match[:-1] in test:
|
|
return True
|
|
return False
|
|
|
|
|
|
def copy_collection_property(prop_a, prop_b, exclude: list = None):
|
|
prop_b.clear()
|
|
for prop in prop_a:
|
|
to_prop = prop_b.add()
|
|
copy_property_group(prop, to_prop, exclude=exclude)
|
|
|
|
|
|
def copy_property_group(group_a, group_b, exclude: list = None):
|
|
vars.block_property_update = True
|
|
# items() returns a list of properties that have been changed
|
|
# from the defaults, or been set, in the property group:
|
|
# returns a list of tuples [(prop, value), (prop, value)...]
|
|
items_a = group_a.items()
|
|
# get a list of all property names in group_a that have been altered
|
|
props_a = [ i[0] for i in items_a ]
|
|
# get a list of all properties in group_b
|
|
props_b = dir(group_b)
|
|
# get a list of all properties that have been changed in group_a and are present in group_b
|
|
props = [ p for p in props_a if p in props_b ]
|
|
for prop in props:
|
|
if exclude and match_wild(prop, exclude):
|
|
log_info(f" - excluding: {prop}")
|
|
continue
|
|
value = getattr(group_a, prop)
|
|
target = getattr(group_b, prop)
|
|
if type(target) == type(value) or value is None or target is None:
|
|
if issubclass(type(value), bpy.types.PropertyGroup):
|
|
# all property groups are subclasses of bpy.types.PropertyGroup
|
|
log_info(f" - copying property group: {prop}")
|
|
copy_property_group(value, target, exclude=exclude)
|
|
elif hasattr(value, "clear") and hasattr(value, "add"):
|
|
# collection properties have add() and clear() functions
|
|
log_info(f" - copying collection property: {prop}")
|
|
copy_collection_property(value, target, exclude=exclude)
|
|
else:
|
|
log_info(f" - setting: {prop} {value}")
|
|
setattr(group_b, prop, value)
|
|
else:
|
|
log_error(f"Properties are of different types: {prop} {type(value)} != {type(target)}")
|
|
vars.block_property_update = False
|
|
|
|
|
|
def stop_now():
|
|
raise Exception("STOP!")
|
|
|
|
|
|
def object_world_location(obj : bpy.types.Object, delta = None):
|
|
location = obj.location.copy()
|
|
if delta:
|
|
location += delta
|
|
if obj.parent:
|
|
return obj.parent.matrix_world @ location
|
|
else:
|
|
return location
|
|
|
|
|
|
def is_valid_icon(icon):
|
|
return icon in bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items.keys()
|
|
|
|
|
|
def check_icon(icon):
|
|
if B321():
|
|
if icon == "OUTLINER_OB_HAIR":
|
|
return "OUTLINER_OB_CURVES"
|
|
elif icon == "HAIR":
|
|
return "CURVES"
|
|
return icon
|
|
|
|
|
|
def md5sum(filename):
|
|
hash = md5()
|
|
with open(filename, "rb") as f:
|
|
for chunk in iter(lambda: f.read(128 * hash.block_size), b""):
|
|
hash.update(chunk)
|
|
return hash.hexdigest()
|
|
|
|
|
|
def store_object_state(objects=None):
|
|
"""Store object & mesh/armature and material names and slots."""
|
|
if objects is None:
|
|
objects = bpy.data.objects
|
|
obj_state = {}
|
|
for obj in objects:
|
|
if (object_exists_is_armature(obj) or object_exists_is_mesh(obj)) and obj not in obj_state:
|
|
obj_state[obj] = {
|
|
"names": [obj.name, obj.data.name],
|
|
"visible": obj.visible_get(),
|
|
}
|
|
if obj.type == "MESH":
|
|
obj_state[obj]["slots"] = [ slot.material for slot in obj.material_slots ]
|
|
for mat in obj.data.materials:
|
|
if material_exists(mat) and mat not in obj_state:
|
|
obj_state[mat] = { "name": mat.name }
|
|
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
|
obj_state[obj]["action"] = safe_get_action(obj.data.shape_keys)
|
|
if obj.type == "ARMATURE":
|
|
obj_state[obj]["action"] = safe_get_action(obj)
|
|
return obj_state
|
|
|
|
|
|
def restore_object_state(obj_state):
|
|
"""Restore object & mesh/armature and material names and slots."""
|
|
for item in obj_state:
|
|
state = obj_state[item]
|
|
if type(item) is bpy.types.Object:
|
|
obj: bpy.types.Object = item
|
|
restore_name = True
|
|
if "rl_do_not_restore_name" in obj:
|
|
restore_name = False
|
|
del obj["rl_do_not_restore_name"]
|
|
if object_exists(obj):
|
|
if restore_name:
|
|
force_object_name(obj, state["names"][0])
|
|
if obj.type == "MESH":
|
|
if restore_name:
|
|
force_mesh_name(obj.data, state["names"][1])
|
|
for i, mat in enumerate(state["slots"]):
|
|
if not material_exists(mat):
|
|
mat = None
|
|
if obj.material_slots[i].material != mat:
|
|
obj.material_slots[i].material = mat
|
|
if "action" in state:
|
|
safe_set_action(obj.data.shape_keys, state["action"])
|
|
elif obj.type == "ARMATURE":
|
|
if restore_name:
|
|
force_armature_name(obj.data, state["names"][1])
|
|
if "action" in state:
|
|
safe_set_action(obj, state["action"])
|
|
elif type(item) is bpy.types.Material:
|
|
mat: bpy.types.Material = item
|
|
if material_exists(mat):
|
|
force_material_name(mat, state["name"])
|
|
|
|
|
|
def reset_shape_keys(objects, exclude=None):
|
|
"""Unlock and reset object shape keys to zero."""
|
|
T = type(objects)
|
|
if T is not list and T is not tuple:
|
|
objects = [objects]
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
# disable shape key lock
|
|
obj.show_only_shape_key = False
|
|
# reset all shape keys to zero
|
|
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
|
for key in obj.data.shape_keys.key_blocks:
|
|
if exclude and key.name in exclude: continue
|
|
key.value = 0.0
|
|
|
|
|
|
INVALID_EXPORT_CHARACTERS = "`¬!\"£$%^&*()+-=[]{}:@~;'#<>?,./\| "
|
|
DIGITS = "0123456789"
|
|
|
|
|
|
def is_invalid_export_name(name, is_material = False):
|
|
for char in INVALID_EXPORT_CHARACTERS:
|
|
if char in name:
|
|
return True
|
|
if is_material:
|
|
if name[0] in DIGITS:
|
|
return True
|
|
return False
|
|
|
|
|
|
def safe_export_name(name, is_material = False, is_split=False):
|
|
if is_split:
|
|
if is_blender_duplicate(name):
|
|
num = get_duplication_suffix(name)
|
|
name = strip_name(name) + f"_S{num:02}"
|
|
for char in INVALID_EXPORT_CHARACTERS:
|
|
if char in name:
|
|
name = name.replace(char, "_")
|
|
if is_material:
|
|
if name[0] in DIGITS:
|
|
name = f"_{name}"
|
|
return name
|
|
|
|
|
|
def determine_object_export_name(chr_cache, obj, obj_cache = None):
|
|
"""Work out what the object should be named when exporting
|
|
by comparing the current name with the original name when imported.
|
|
"""
|
|
obj_name = obj.name
|
|
if not obj_cache:
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
source_changed = False
|
|
is_new_object = False
|
|
if obj_cache:
|
|
obj_expected_source_name = safe_export_name(strip_name(obj_name))
|
|
obj_source_name = obj_cache.source_name
|
|
source_changed = obj_expected_source_name != obj_source_name
|
|
if source_changed:
|
|
obj_safe_name = safe_export_name(obj_name)
|
|
else:
|
|
obj_safe_name = obj_source_name
|
|
else:
|
|
is_new_object = True
|
|
obj_safe_name = safe_export_name(obj_name)
|
|
obj_source_name = obj_safe_name
|
|
return obj_safe_name
|
|
|
|
|
|
def furthest_from(p0, *points):
|
|
most = 0
|
|
result = p0
|
|
for p in points:
|
|
dp = (p - p0).length
|
|
if dp > most:
|
|
most = dp
|
|
result = p
|
|
return result
|
|
|
|
|
|
def name_contains_distinct_keywords(name : str, *keywords : str):
|
|
"""Does the name contain the supplied keywords in distinct form:\n
|
|
i.e. capitalized "OneTwoThree"\n
|
|
or hungarian notation "oneTwoThree"\n
|
|
or surrouned by underscores "one_two_three"
|
|
"""
|
|
|
|
name_lower = name.lower()
|
|
name_length = len(name)
|
|
|
|
for k in keywords:
|
|
k_lower = k.lower()
|
|
k_length = len(k)
|
|
|
|
s = name_lower.find(k_lower)
|
|
e = s + k_length
|
|
|
|
if s >= 0:
|
|
|
|
# is keyword in name separated by underscores
|
|
if (name_lower.startswith(k_lower + "_") or
|
|
name_lower.endswith("_" + k_lower) or
|
|
"_" + k_lower + "_" in name_lower or
|
|
name_lower == k_lower):
|
|
return True
|
|
|
|
# match distinct keyword at start of name (any capitalization) or captitalized anywhere else
|
|
if s == 0 or name[s].isupper():
|
|
if e >= name_length or not name[e].islower():
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def is_name_or_duplication(a, b):
|
|
return strip_name(a) == strip_name(b)
|
|
|
|
|
|
def object_has_shape_keys(obj):
|
|
try:
|
|
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
|
return True
|
|
else:
|
|
return False
|
|
except:
|
|
return False
|
|
|
|
|
|
def object_has_shape_key(obj, key_name):
|
|
try:
|
|
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
|
return key_name in obj.data.shape_keys.key_blocks
|
|
except:
|
|
...
|
|
return False
|
|
|
|
|
|
def object_scale(obj):
|
|
try:
|
|
return (obj.scale[0] + obj.scale[1] + obj.scale[2]) / 3.0
|
|
except:
|
|
return 1.0
|
|
|
|
|
|
def make_transform_matrix(loc: Vector, rot: Quaternion, sca: Vector=Vector((1,1,1))):
|
|
return Matrix.Translation(loc) @ (rot.to_matrix().to_4x4()) @ Matrix.Diagonal(sca).to_4x4()
|
|
|
|
|
|
def set_transform_rotation(obj: bpy.types.Object, rotation: Quaternion):
|
|
if obj and rotation:
|
|
T = type(rotation)
|
|
if T is Euler:
|
|
rotation_quaternion = Quaternion(rotation)
|
|
elif T is tuple and len(rotation) == 2:
|
|
axis, angle = rotation
|
|
rotation_quaternion = axis_angle_to_quaternion(axis, angle)
|
|
elif T is Quaternion:
|
|
rotation_quaternion = rotation.copy()
|
|
else:
|
|
return
|
|
|
|
if obj.rotation_mode == "QUATERNION":
|
|
obj.rotation_quaternion = rotation_quaternion
|
|
elif obj.rotation_mode == "AXIS_ANGLE":
|
|
axis_angle = rotation_quaternion.to_axis_angle()
|
|
obj.rotation_axis_angle = axis_angle
|
|
else:
|
|
euler = rotation_quaternion.to_euler(obj.rotation_mode)
|
|
obj.rotation_euler = euler
|
|
|
|
|
|
def axis_angle_to_quaternion(axis: Vector, angle: float):
|
|
return Matrix.Rotation(angle, 4, axis).to_quaternion()
|
|
|
|
|
|
def get_transform_rotation(obj: bpy.types.Object) -> Quaternion:
|
|
if obj:
|
|
if obj.rotation_mode == "QUATERNION":
|
|
return obj.rotation_quaternion.copy()
|
|
elif obj.rotation_mode == "AXIS_ANGLE":
|
|
axis = obj.rotation_axis_angle[0:3]
|
|
angle = obj.rotation_axis_angle[3]
|
|
return axis_angle_to_quaternion(axis, angle)
|
|
else:
|
|
return obj.rotation_euler.to_quaternion()
|
|
return None
|
|
|
|
|
|
def is_local_view(context):
|
|
try:
|
|
return context.space_data.local_view is not None
|
|
except:
|
|
return False
|
|
|
|
|
|
def fix_local_view(context):
|
|
if is_local_view(context):
|
|
bpy.ops.view3d.localview()
|
|
|
|
|
|
def show_system_file_browser(path):
|
|
if platform.system() == "Windows":
|
|
try:
|
|
explorer_path = os.path.join(os.getenv("WINDIR"), "explorer.exe")
|
|
subprocess.Popen((explorer_path, "/select,", path))
|
|
except:
|
|
pass
|
|
|
|
|
|
def get_scene_frame_range():
|
|
scene = bpy.context.scene
|
|
if scene.use_preview_range:
|
|
return scene.frame_preview_start, scene.frame_preview_end
|
|
else:
|
|
return scene.frame_start, scene.frame_end
|
|
|
|
|
|
def set_scene_frame_range(start, end):
|
|
scene = bpy.context.scene
|
|
if scene.use_preview_range:
|
|
scene.frame_preview_start = start
|
|
scene.frame_preview_end = end
|
|
else:
|
|
scene.frame_start = start
|
|
scene.frame_end = end
|
|
|
|
|
|
def make_empty(name, loc=None, rot=None, scale=None, matrix=None):
|
|
ob = bpy.data.objects.new(name, None)
|
|
if matrix:
|
|
ob.matrix_world = matrix
|
|
else:
|
|
if loc:
|
|
ob.location = loc
|
|
if rot:
|
|
set_transform_rotation(ob, rot)
|
|
if scale:
|
|
ob.scale = scale
|
|
bpy.context.scene.collection.objects.link(ob)
|
|
|
|
|
|
def is_n_panel_sub_tabs():
|
|
for prefs in bpy.context.preferences.addons.keys():
|
|
if "n_panel_sub_tabs" in prefs:
|
|
return True
|
|
return False
|
|
|
|
|
|
def generate_random_id(length):
|
|
CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
id = ""
|
|
for i in range(0, length):
|
|
id += random.choice(CHARS)
|
|
return id
|
|
|
|
|
|
def set_prop(obj, prop_name, value):
|
|
try:
|
|
obj[prop_name] = value
|
|
return True
|
|
except:
|
|
try:
|
|
del(obj[prop_name])
|
|
obj[prop_name] = value
|
|
return True
|
|
except: ...
|
|
return False
|
|
|
|
|
|
def get_prop(obj, prop_name, default_value = None):
|
|
try:
|
|
return obj[prop_name]
|
|
except: ...
|
|
return default_value
|
|
|
|
|
|
def del_prop(obj, prop_name):
|
|
try:
|
|
del obj[prop_name]
|
|
except: ...
|
|
|
|
|
|
def set_rl_link_id(obj, link_id=None):
|
|
if link_id is None:
|
|
link_id = generate_random_id(20)
|
|
if obj:
|
|
set_prop(obj, "rl_link_id", link_id)
|
|
return link_id
|
|
|
|
|
|
def get_rl_link_id(obj):
|
|
if obj:
|
|
if "link_id" in obj:
|
|
link_id = obj["link_id"]
|
|
del(obj["link_id"])
|
|
set_rl_link_id(obj, link_id)
|
|
return link_id
|
|
elif "rl_link_id" in obj:
|
|
return obj["rl_link_id"]
|
|
return None
|
|
|
|
|
|
def set_rl_object_id(obj, new_id=None):
|
|
if new_id is None:
|
|
new_id = generate_random_id(20)
|
|
if obj:
|
|
if obj.type == "ARMATURE":
|
|
set_prop(obj, "rl_armature_id", new_id)
|
|
if "rl_object_id" in obj:
|
|
del(obj["rl_object_id"])
|
|
else:
|
|
set_prop(obj, "rl_object_id", new_id)
|
|
return new_id
|
|
|
|
|
|
def get_rl_object_id(obj):
|
|
if object_exists(obj):
|
|
if obj.type == "ARMATURE" and "rl_armature_id" in obj:
|
|
return obj["rl_armature_id"]
|
|
if "rl_object_id" in obj:
|
|
return obj["rl_object_id"]
|
|
return None
|
|
|
|
|
|
def merge(a: list, b: list):
|
|
c = a.copy()
|
|
for i in b:
|
|
if i not in c:
|
|
c.append(i)
|
|
return c
|
|
|
|
|
|
def fix_texture_rel_path(rel_path: str):
|
|
"""Fixes json texture relative path export bug in CC4 when exporting character directly
|
|
to the root folder of a drive"""
|
|
|
|
if rel_path.startswith(".textures"):
|
|
rel_path = rel_path[1:]
|
|
elif rel_path.startswith("./"):
|
|
rel_path = rel_path[2:]
|
|
return rel_path
|
|
|
|
|
|
def get_resource_path(folder, file):
|
|
addon_path = os.path.dirname(os.path.realpath(__file__))
|
|
resource_path = os.path.join(addon_path, folder, file)
|
|
return resource_path
|
|
|
|
|
|
def get_resource_folder(folder):
|
|
addon_path = os.path.dirname(os.path.realpath(__file__))
|
|
resource_folder = os.path.join(addon_path, folder)
|
|
return resource_folder
|
|
|
|
|
|
def get_unique_folder_path(parent_folder, folder_name, create=False, reuse=False):
|
|
suffix = 1
|
|
base_name = folder_name
|
|
folder_path = os.path.normpath(os.path.join(parent_folder, folder_name))
|
|
if not reuse:
|
|
while os.path.exists(folder_path):
|
|
folder_name = f"{base_name}_{str(suffix)}"
|
|
folder_path = os.path.normpath(os.path.join(parent_folder, folder_name))
|
|
suffix += 1
|
|
if create:
|
|
os.makedirs(folder_path, exist_ok=reuse)
|
|
return folder_path
|
|
|
|
|
|
def get_unique_file_path(parent_folder, file_name, reuse=False):
|
|
suffix = 1
|
|
base_name, ext = os.path.splitext(file_name)
|
|
file_path = os.path.normpath(os.path.join(parent_folder, file_name))
|
|
if not reuse:
|
|
while os.path.exists(file_path):
|
|
file_name = f"{base_name}_{str(suffix)}{ext}"
|
|
file_path = os.path.normpath(os.path.join(parent_folder, file_name))
|
|
suffix += 1
|
|
return file_path
|
|
|
|
|
|
def make_sub_folder(parent_folder, folder_name):
|
|
folder_path = os.path.normpath(os.path.join(parent_folder, folder_name))
|
|
os.makedirs(folder_path, exist_ok=True)
|
|
return folder_path
|
|
|
|
|
|
def timestampns():
|
|
return str(time.time_ns())
|
|
|
|
|
|
def datetimes():
|
|
return time.strftime("%Y%m%d%H%M%S")
|
|
|
|
|
|
def json_dumps(json_data, file_path=None, indent=4):
|
|
if file_path:
|
|
with open(file_path, "w") as f:
|
|
f.write(json.dumps(json_data, indent=indent))
|
|
else:
|
|
print(json.dumps(json_data, indent=indent))
|
|
|
|
|
|
def open_folder(folder_path):
|
|
os.startfile(folder_path)
|
|
|
|
|
|
def get_enum_prop_name(obj, prop_name, enum_value=None):
|
|
try:
|
|
prop = type(obj).bl_rna.properties[prop_name]
|
|
if enum_value is None:
|
|
enum_value = getattr(obj, prop_name)
|
|
return prop.enum_items[enum_value].name
|
|
except:
|
|
return prop_name
|
|
|
|
|
|
def largest_index(items: list, use_abs=False):
|
|
index = 0
|
|
largest_value = 0
|
|
for i, value in enumerate(items):
|
|
if use_abs:
|
|
if abs(value) > largest_value:
|
|
largest_value = abs(value)
|
|
index = i
|
|
else:
|
|
if value > largest_value:
|
|
largest_value = value
|
|
index = i
|
|
return index
|
|
|
|
|
|
def smallest_index(items: list):
|
|
index = 0
|
|
smallest_value = 0
|
|
for i, value in enumerate(items):
|
|
if value < smallest_value:
|
|
smallest_value = value
|
|
index = i
|
|
return index
|
|
|
|
|
|
def safe_free_bake(point_cache):
|
|
if B320():
|
|
with bpy.context.temp_override(point_cache=point_cache):
|
|
bpy.ops.ptcache.free_bake()
|
|
else:
|
|
context_override = bpy.context.copy()
|
|
context_override["point_cache"] = point_cache
|
|
bpy.ops.ptcache.free_bake(context_override) |