c31d25dd01
You can choose between errors, warning, info or full debug, errors will always log to ensure we don't have silent failures with debug on or off.
335 lines
11 KiB
Python
335 lines
11 KiB
Python
# -*- 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 logging
|
|
import os
|
|
import re
|
|
from typing import Callable, Optional, Set
|
|
|
|
import bpy
|
|
|
|
from .bpyutils import FnContext
|
|
|
|
|
|
## 指定したオブジェクトのみを選択状態かつアクティブにする
|
|
def selectAObject(obj):
|
|
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):
|
|
selectAObject(obj)
|
|
if obj.mode != "EDIT":
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
|
|
|
|
|
def setParentToBone(obj, parent, bone_name):
|
|
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, armature, bone_name, reset_pose=False):
|
|
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: bpy.types.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, use_underscore=False):
|
|
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<lr>(?P<separator>[._])[lL])(?P<after>($|(?P=separator)))")
|
|
__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))")
|
|
|
|
|
|
def convertLRToName(name):
|
|
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, src_vertex_group_name, dest_vertex_group_name):
|
|
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: bpy.types.Object):
|
|
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():
|
|
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):
|
|
# 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, base, width=0):
|
|
"""
|
|
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, start, strategy="inside"):
|
|
"""
|
|
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, index):
|
|
if 0 <= index < len(items):
|
|
return items[index]
|
|
return None
|
|
|
|
@staticmethod
|
|
def resize(items: bpy.types.bpy_prop_collection, length: int):
|
|
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, index):
|
|
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, index, move_type, index_min=0, index_max=None):
|
|
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):
|
|
"""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):
|
|
def _inner_wrapper(*args, **kwargs):
|
|
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.
|
|
"""
|
|
logging.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,
|
|
)
|
|
|
|
# import warnings # pylint: disable=import-outside-toplevel
|
|
|
|
# warnings.warn(f"""{function_name}is deprecated{f" since {deprecated_in}" if deprecated_in else ""}{f": {details}" if details else ""}""", category=DeprecationWarning, stacklevel=2)
|