# -*- coding: utf-8 -*- # Copyright 2014 MMD Tools authors # This file was originally part of the MMD Tools add-on for Blender # You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import os import re from typing import Callable, Dict, List, Optional, Set, Tuple, Union, Any import bpy from bpy.types import Object, Bone, PoseBone, Mesh, VertexGroup from ..logging_setup import logger from .bpyutils import FnContext ## 指定したオブジェクトのみを選択状態かつアクティブにする def selectAObject(obj: Object) -> None: try: bpy.ops.object.mode_set(mode="OBJECT") except Exception: pass bpy.ops.object.select_all(action="DESELECT") FnContext.select_object(FnContext.ensure_context(), obj) FnContext.set_active_object(FnContext.ensure_context(), obj) ## 現在のモードを指定したオブジェクトのEdit Modeに変更する def enterEditMode(obj: Object) -> None: selectAObject(obj) if obj.mode != "EDIT": bpy.ops.object.mode_set(mode="EDIT") def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None: selectAObject(obj) FnContext.set_active_object(FnContext.ensure_context(), parent) bpy.ops.object.mode_set(mode="POSE") parent.data.bones.active = parent.data.bones[bone_name] bpy.ops.object.parent_set(type="BONE", xmirror=False, keep_transform=False) bpy.ops.object.mode_set(mode="OBJECT") def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None: try: bpy.ops.object.mode_set(mode="OBJECT") except: pass for i in context.selected_objects: i.select_set(False) FnContext.set_active_object(context, armature) bpy.ops.object.mode_set(mode="POSE") if reset_pose: for p_bone in armature.pose.bones: p_bone.matrix_basis.identity() armature_bones: bpy.types.ArmatureBones = armature.data.bones i: Bone for i in armature_bones: i.select = i.name == bone_name i.select_head = i.select_tail = i.select if i.select: armature_bones.active = i i.hide = False __CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$") __CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$") ## 日本語で左右を命名されている名前をblender方式のL(R)に変更する def convertNameToLR(name: str, use_underscore: bool = False) -> str: m = __CONVERT_NAME_TO_L_REGEXP.match(name) delimiter = "_" if use_underscore else "." if m: name = m.group(1) + m.group(2) + delimiter + "L" m = __CONVERT_NAME_TO_R_REGEXP.match(name) if m: name = m.group(1) + m.group(2) + delimiter + "R" return name __CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P(?P[._])[lL])(?P($|(?P=separator)))") __CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P(?P[._])[rR])(?P($|(?P=separator)))") def convertLRToName(name: str) -> str: match = __CONVERT_L_TO_NAME_REGEXP.search(name) if match: return f"左{name[0:match.start()]}{match['after']}{name[match.end():]}" match = __CONVERT_R_TO_NAME_REGEXP.search(name) if match: return f"右{name[0:match.start()]}{match['after']}{name[match.end():]}" return name ## src_vertex_groupのWeightをdest_vertex_groupにaddする def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None: mesh = meshObj.data src_vertex_group = meshObj.vertex_groups[src_vertex_group_name] dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name] vtxIndex = src_vertex_group.index for v in mesh.vertices: try: gi = [i.group for i in v.groups].index(vtxIndex) dest_vertex_group.add([v.index], v.groups[gi].weight, "ADD") except ValueError: pass def separateByMaterials(meshObj: Object) -> None: if len(meshObj.data.materials) < 2: selectAObject(meshObj) return matrix_parent_inverse = meshObj.matrix_parent_inverse.copy() prev_parent = meshObj.parent dummy_parent = bpy.data.objects.new(name="tmp", object_data=None) meshObj.parent = dummy_parent meshObj.active_shape_key_index = 0 try: enterEditMode(meshObj) bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.separate(type="MATERIAL") finally: bpy.ops.object.mode_set(mode="OBJECT") for i in dummy_parent.children: materials = i.data.materials i.name = getattr(materials[0], "name", "None") if len(materials) else "None" i.parent = prev_parent i.matrix_parent_inverse = matrix_parent_inverse bpy.data.objects.remove(dummy_parent) def clearUnusedMeshes() -> None: meshes_to_delete = [] for mesh in bpy.data.meshes: if mesh.users == 0: meshes_to_delete.append(mesh) for mesh in meshes_to_delete: bpy.data.meshes.remove(mesh) ## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を # それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成 def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]: # Maintain backward compatibility with mmd_tools v0.4.x or older. return {(i.mmd_bone.name_j or i.get("mmd_bone_name_j", i.get("name_j", i.name))): i for i in armObj.pose.bones} __REMOVE_PREFIX_DIGITS_REGEXP = re.compile(r"\.\d{1,}$") def unique_name(name: str, used_names: Set[str]) -> str: """Helper function for storing unique names. This function is a limited and simplified version of bpy_extras.io_utils.unique_name. Args: name (str): The name to make unique. used_names (Set[str]): A set of names that are already used. Returns: str: The unique name, formatted as "{name}.{number:03d}". """ if name not in used_names: return name count = 1 new_name = orig_name = __REMOVE_PREFIX_DIGITS_REGEXP.sub("", name) while new_name in used_names: new_name = f"{orig_name}.{count:03d}" count += 1 return new_name def int2base(x: int, base: int, width: int = 0) -> str: """ Method to convert an int to a base Source: http://stackoverflow.com/questions/2267362 """ import string digs = string.digits + string.ascii_uppercase assert 2 <= base <= len(digs) digits, negtive = "", False if x <= 0: if x == 0: return "0" * max(1, width) x, negtive, width = -x, True, width - 1 while x: digits = digs[x % base] + digits x //= base digits = "0" * (width - len(digits)) + digits if negtive: digits = "-" + digits return digits def saferelpath(path: str, start: str, strategy: str = "inside") -> str: """ On Windows relpath will raise a ValueError when trying to calculate the relative path to a different drive. This method will behave different depending on the strategy choosen to handle the different drive issue. Strategies: - inside: this will just return the basename of the path given - outside: this will prepend '..' to the basename - absolute: this will return the absolute path instead of a relative. See http://bugs.python.org/issue7195 """ if strategy == "inside": return os.path.basename(path) if strategy == "absolute": return os.path.abspath(path) if strategy == "outside" and os.name == "nt": d1, _ = os.path.splitdrive(path) d2, _ = os.path.splitdrive(start) if d1 != d2: return ".." + os.sep + os.path.basename(path) return os.path.relpath(path, start) class ItemOp: @staticmethod def get_by_index(items: bpy.types.bpy_prop_collection, index: int) -> Optional[Any]: if 0 <= index < len(items): return items[index] return None @staticmethod def resize(items: bpy.types.bpy_prop_collection, length: int) -> None: count = length - len(items) if count > 0: for i in range(count): items.add() elif count < 0: for i in range(-count): items.remove(length) @staticmethod def add_after(items: bpy.types.bpy_prop_collection, index: int) -> Tuple[Any, int]: index_end = len(items) index = max(0, min(index_end, index + 1)) items.add() items.move(index_end, index) return items[index], index class ItemMoveOp: type: bpy.props.EnumProperty( name="Type", description="Move type", items=[ ("UP", "Up", "", 0), ("DOWN", "Down", "", 1), ("TOP", "Top", "", 2), ("BOTTOM", "Bottom", "", 3), ], default="UP", ) @staticmethod def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str, index_min: int = 0, index_max: Optional[int] = None) -> int: if index_max is None: index_max = len(items) - 1 else: index_max = min(index_max, len(items) - 1) index_min = min(index_min, index_max) if index < index_min: items.move(index, index_min) return index_min elif index > index_max: items.move(index, index_max) return index_max index_new = index if move_type == "UP": index_new = max(index_min, index - 1) elif move_type == "DOWN": index_new = min(index + 1, index_max) elif move_type == "TOP": index_new = index_min elif move_type == "BOTTOM": index_new = index_max if index_new != index: items.move(index, index_new) return index_new def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None) -> Callable: """Decorator to mark a function as deprecated. Args: deprecated_in (Optional[str]): Version in which the function was deprecated. details (Optional[str]): Additional details about the deprecation. Returns: Callable: The decorated function. """ def _function_wrapper(function: Callable) -> Callable: def _inner_wrapper(*args: Any, **kwargs: Any) -> Any: warn_deprecation(function.__name__, deprecated_in, details) return function(*args, **kwargs) return _inner_wrapper return _function_wrapper def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, details: Optional[str] = None) -> None: """Reports a deprecation warning. Args: function_name (str): Name of the deprecated function. deprecated_in (Optional[str]): Version in which the function was deprecated. details (Optional[str]): Additional details about the deprecation. """ logger.warning( "%s is deprecated%s%s", function_name, f" since {deprecated_in}" if deprecated_in else "", f": {details}" if details else "", stack_info=True, stacklevel=4, )