# Copyright (C) 2021 Victor Soupday # This file is part of CC/iC 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 . import os import platform import subprocess import time import difflib import random import re import traceback from mathutils import Vector, Quaternion, Matrix, Euler, Color from hashlib import md5 import bpy from . import vars timer = 0 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(): global timer timer = time.perf_counter() def log_timer(msg, unit = "s"): prefs = vars.prefs() global timer if prefs.log_level == "ALL": duration = time.perf_counter() - timer if unit == "ms": duration *= 1000 elif unit == "us": duration *= 1000000 elif unit == "ns": duration *= 1000000000 print(msg + ": " + str(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 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): name = strip_name(name) if capitalize: name = name.capitalize() if name in bpy.data.objects and bpy.data.objects[name] != obj: index = start_index while name + "_" + str(index).zfill(2) in bpy.data.objects: index += 1 return name + "_" + str(index).zfill(2) return 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 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): """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(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 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 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(edge0, edge1, min, max, x): return min + ((x - edge0) * (max - min) / (edge1 - edge0)) def lerp(v0, v1, t): 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): 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)) 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): return (linear_to_srgbx(color[0]), linear_to_srgbx(color[1]), linear_to_srgbx(color[2]), color[3]) 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): return (srgb_to_linearx(color[0]), srgb_to_linearx(color[1]), srgb_to_linearx(color[2]), color[3]) 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 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, include_action=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 shape_key_action = None # store existing actions obj_action = safe_get_action(obj) if not include_action: safe_set_action(obj, None, create=False) if obj.type == "MESH": shape_key_action = safe_get_action(obj.data.shape_keys) if not include_action: safe_set_action(obj.data.shape_keys, None, create=False) # duplicate object bpy.ops.object.duplicate() # restore non-duplicated actions if not include_action: if shape_key_action: safe_set_action(obj.data.shape_keys, shape_key_action) if obj_action: safe_set_action(obj, obj_action) return get_active_object() return None def remove_all_shape_keys(obj): if obj and obj.data.shape_keys and obj.data.shape_keys.key_blocks: 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) 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 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 # remove any .001 from the material name def strip_name(name: str): if len(name) >= 4: if name[-3:].isdigit() and name[-4] == ".": name = name[:-4] return name 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): for child in obj.children: if child not in objects: if child.type == "ARMATURE" and not follow_armatures: continue objects.append(child) if child.children: add_child_objects(child, objects, follow_armatures) def expand_with_child_objects(objects): for obj in objects: add_child_objects(obj, objects) def get_child_objects(obj, include_parent = False, follow_armatures = False): objects = [] if include_parent: objects.append(obj) add_child_objects(obj, objects, follow_armatures) 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 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): objects = get_object_tree(obj) for obj in objects: try: obj.hide_set(hide) except: ... def hide(obj, hide=True): try: obj.hide_set(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() 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 obj.type == "ARMATURE" and obj != arm and obj != chr_cache.rig_meta_rig: 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): try: set_mode("OBJECT") try_select_objects(store[2], True) set_active_object(store[1]) set_mode(store[0]) 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 for obj in objects: if object_exists(obj): visible = obj.visible_get() render = not obj.hide_render if render or visible: rv[obj.name] = [visible, render] return rv def restore_render_visibility_state(rv): obj : bpy.types.Object for obj in bpy.data.objects: if object_exists(obj): if obj.name in rv: visible, render = rv[obj.name] try: obj.hide_render = not render hide(obj, not visible) except: pass else: try: obj.hide_render = False hide(obj) 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_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_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_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 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 if obj.rotation_mode == "XYZ": obj.rotation_euler = rot.to_euler() elif obj.rotation_mode == "QUATERNION": obj.rotation_quaternion = rot.copy() def copy_action(action: bpy.types.Action, new_name): new_action = action.copy() new_action.name = new_name return new_action 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_set_action(obj, action, create=True): if obj: try: if create and not obj.animation_data: obj.animation_data_create() if obj.animation_data: obj.animation_data.action = action return True except: action_name = action.name if action else "None" log_warn(f"Unable to set action {action_name} to {obj.name}") return False def index_of_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 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 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 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 is_blender_version(version: str, test = "GTE"): """e.g. is_blender_version("3.0.0", "GTE")""" 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] if test == "GTE" and v_blender >= v_test: return True elif test == "GT" and v_blender > v_test: return True elif test == "LT" and v_blender < v_test: return True elif test == "LTE" and v_blender <= v_test: return True elif test == "EQ" and v_blender == v_test: return True elif test == "NE" and v_blender != v_test: return True return False 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): """Unlock and reset object shape keys to zero.""" 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: 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_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): return Matrix.Translation(loc) @ (rot.to_matrix().to_4x4()) @ Matrix.Diagonal(sca).to_4x4() 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: ob.rotation_mode = "QUATERNION" ob.rotation_quaternion = rot if scale: ob.scale = scale bpy.context.scene.collection.objects.link(ob) def generate_random_id(length): CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" id = "" for i in range(0, length): id += random.choice(CHARS) return id def set_rl_object_id(obj, new_id): if obj: if obj.type == "ARMATURE": obj["rl_armature_id"] = new_id if "rl_object_id" in obj: del(obj["rl_object_id"]) else: obj["rl_object_id"] = new_id def get_rl_object_id(obj): if 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 custom_prop(obj, prop_name, default=None): if prop_name in obj: return obj[prop_name] return default 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 open_folder(folder_path): os.startfile(folder_path)