Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b7f6632ea | |||
| 24b489f7a2 | |||
| 1e734a518e | |||
| 4b538cb8b2 | |||
| d85231b62b | |||
| b13ca15ece | |||
| 84bacca923 | |||
| 53d2ac10b7 | |||
| 95cb726485 | |||
| cb5b891d0d | |||
| aedd83e078 | |||
| 5719a55ae5 | |||
| 659f3eb91e | |||
| ff19a895dc | |||
| e6e5a98e58 | |||
| 3fe00da569 | |||
| 108f9d3bc8 | |||
| 1847628dc8 | |||
| 25a43afdbc | |||
| baaf4049f6 | |||
| 7ef86b68fa | |||
| 27e18b5656 | |||
| b61283b9d5 | |||
| fbcf709ffc | |||
| 299800e5c2 | |||
| f6197ccbbf | |||
| fd01c39cf9 | |||
| 117ce4f41d | |||
| f11e9d35fb | |||
| 7f1decc644 | |||
| a929f68ad4 | |||
| f0bda259d3 | |||
| f4d93a8180 | |||
| 303707adf7 | |||
| ef84478af7 | |||
| 56005c5d37 | |||
| fe122f9f13 | |||
| 17fb0fcadd | |||
| 1d9c186613 | |||
| 49f5bf7063 | |||
| daef1298d4 | |||
| 86406efc6b | |||
| 734d5fe401 | |||
| 5029ba8724 | |||
| 3545951fae | |||
| 0b5bff9222 | |||
| 862849c032 | |||
| e060186716 | |||
| 07c4dd501f | |||
| e80c0c034d | |||
| f40b2faacb | |||
| d2b98716ff | |||
| e4f3cdbf17 | |||
| 1d34ac2dd8 | |||
| 3bb533ff64 | |||
| 69cae02160 | |||
| 5496078a39 | |||
| dbf2fb77f9 | |||
| 3de600cf64 | |||
| ba9d579176 | |||
| 35458f9aed | |||
| d2c30caef5 | |||
| 54a1dff122 | |||
| 7dc74964e8 | |||
| 00a015a8d3 | |||
| b9f7a4acd0 | |||
| e626bdc5c5 | |||
| da2bfeb2fc | |||
| 2b53146e83 | |||
| 444554528d | |||
| cae6ce4301 | |||
| 74716b187f | |||
| fbb4569e99 | |||
| 56967fc9a9 | |||
| 5881180e69 | |||
| 4ba594d712 | |||
| 031b78ee7b | |||
| 61c77cf756 | |||
| e19dd78557 | |||
| d820edfc64 | |||
| b39e20e647 | |||
| 929cadd596 | |||
| f90efb549a | |||
| 3e8ab41ab9 | |||
| c28cfe1d1d | |||
| 15ce911256 | |||
| 2f3b8ab0ee | |||
| 7b58f25913 | |||
| d25543d95b | |||
| ba9a7a8af3 | |||
| 408d3f24f7 | |||
| bd33efe7ae | |||
| c50f275b1b | |||
| 1ddda1336a | |||
| 634563afb3 | |||
| 543869218c | |||
| e5e09e2cf3 | |||
| 29f728442a | |||
| 8c2c52f882 | |||
| 6f5e7a394d | |||
| 6eb253be17 | |||
| 5276aa0fe0 | |||
| c830938dce | |||
| 60ba1b363f | |||
| e3052d867d | |||
| 08082501c9 | |||
| a8482a87f3 | |||
| 482fe1b593 |
@@ -1,4 +1,5 @@
|
|||||||
# Avatar Toolkit
|
# Avatar Toolkit
|
||||||
|
We are aware the wiki is down and are working on a new one, please don't report this.
|
||||||
|
|
||||||
## Avatar Toolkit is in Alpha, There will be issues, please ensure you report them!. If using a Alpha plugin isn't your fancy you can find Cats Blender Plugin [HERE](https://github.com/unofficalcats/Cats-Blender-Plugin-Unofficial-)!
|
## Avatar Toolkit is in Alpha, There will be issues, please ensure you report them!. If using a Alpha plugin isn't your fancy you can find Cats Blender Plugin [HERE](https://github.com/unofficalcats/Cats-Blender-Plugin-Unofficial-)!
|
||||||
#### Avatar Toolkit is in Alpha and will contain issues, please ensure you report them!
|
#### Avatar Toolkit is in Alpha and will contain issues, please ensure you report them!
|
||||||
@@ -32,9 +33,8 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/lega
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
1) Blender Version
|
1) Blender Version
|
||||||
- Blender 4.4 or newer is required
|
- Blender 4.5 or newer is required
|
||||||
- Blender 4.4 is the current recommended version
|
- Blender 4.5 is the current recommended version
|
||||||
|
|
||||||
|
|
||||||
2) Python Requirements
|
2) Python Requirements
|
||||||
- If using a custom Python installation with Blender, ensure NumPy is installed
|
- If using a custom Python installation with Blender, ensure NumPy is installed
|
||||||
@@ -42,7 +42,17 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/lega
|
|||||||
|
|
||||||
3) Recommended Setup
|
3) Recommended Setup
|
||||||
- Download Blender directly from https://blender.org
|
- Download Blender directly from https://blender.org
|
||||||
- Use Blender 4.4 for the best experience
|
- Use Blender 4.5 for the best experience
|
||||||
|
|
||||||
|
#### Unfortunately, due to the increased number of people complaining to me (yes, we get DMs about this) that AT or CATS is broken when it's not, we are going to have to be a bit more strict about which Blender releases we will provide support for.
|
||||||
|
|
||||||
|
#### We only support the following Blender releases:
|
||||||
|
- Steam release
|
||||||
|
- The Blender website releases (there are downloads for Linux, Mac, and Windows)
|
||||||
|
|
||||||
|
#### We do not support the following what so ever and we will not give help if your running the following.
|
||||||
|
- We do not support the Windows Store due to it causing issues, and we also don't support the Snap Store for Linux.
|
||||||
|
- We do not support package manager releases on Linux. This is because package managers are normally run by the distro, and a lot of the time the distro will build Blender themselves and make their own changes which are not sanctioned by Blender (for example, bundling a newer version of Python which tends to break plugins). If you report a bug from anything apart from the Blender versions we support, you will be told we can't help you from now on.
|
||||||
|
|
||||||
#### Additional Plugins Requirements.
|
#### Additional Plugins Requirements.
|
||||||
Currently None.
|
Currently None.
|
||||||
|
|||||||
@@ -3,23 +3,23 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "avatar_toolkit"
|
id = "avatar_toolkit"
|
||||||
version = "0.3.0"
|
version = "0.6.0"
|
||||||
name = "Avatar Toolkit"
|
name = "Avatar Toolkit"
|
||||||
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
||||||
maintainer = "Team NekoNeo"
|
maintainer = "Team NekoNeo"
|
||||||
type = "add-on"
|
type = "add-on"
|
||||||
|
|
||||||
blender_version_min = "4.4.0"
|
blender_version_min = "5.0.0"
|
||||||
|
|
||||||
license = [
|
license = [
|
||||||
"SPDX:GPL-3.0-or-later",
|
"SPDX:GPL-3.0-or-later",
|
||||||
]
|
]
|
||||||
|
|
||||||
wheels = [
|
wheels = [
|
||||||
"./wheels/lz4-4.4.3-cp311-cp311-macosx_11_0_arm64.whl",
|
"./wheels/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl",
|
||||||
"./wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl",
|
"./wheels/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl",
|
||||||
"./wheels/lz4-4.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
"./wheels/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
|
||||||
"./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl"
|
"./wheels/lz4-4.4.5-cp311-cp311-win_amd64.whl"
|
||||||
]
|
]
|
||||||
|
|
||||||
[permissions]
|
[permissions]
|
||||||
|
|||||||
@@ -63,6 +63,6 @@ def get_addon_preferences(context):
|
|||||||
# Initialize preferences if the file doesn't exist
|
# Initialize preferences if the file doesn't exist
|
||||||
if not os.path.exists(PREFERENCES_FILE):
|
if not os.path.exists(PREFERENCES_FILE):
|
||||||
save_preference("language", 0) # Set default language to 0 (auto)
|
save_preference("language", 0) # Set default language to 0 (auto)
|
||||||
save_preference("validation_mode", "STRICT") # Set default validation mode
|
save_preference("validation_mode", "NONE") # Set default validation mode to NONE (off by default)
|
||||||
save_preference("enable_logging", False) # Set default logging mode
|
save_preference("enable_logging", False) # Set default logging mode
|
||||||
save_preference("highlight_problem_bones", True) # Set default bone highlighting
|
save_preference("highlight_problem_bones", True) # Set default bone highlighting
|
||||||
|
|||||||
+281
-27
@@ -10,25 +10,45 @@ from ..core.dictionaries import (
|
|||||||
bone_hierarchy,
|
bone_hierarchy,
|
||||||
finger_hierarchy,
|
finger_hierarchy,
|
||||||
acceptable_bone_hierarchy,
|
acceptable_bone_hierarchy,
|
||||||
acceptable_bone_names
|
acceptable_bone_names,
|
||||||
|
simplify_bonename
|
||||||
)
|
)
|
||||||
from ..core.logging_setup import logger
|
from ..core.logging_setup import logger
|
||||||
|
|
||||||
def validate_armature(armature: Object, detailed_messages: bool = False) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]:
|
def is_pmx_model(armature: Object) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the armature is a PMX/MMD model.
|
||||||
|
PMX models have an mmd_type attribute set to 'ROOT' on the root object.
|
||||||
|
"""
|
||||||
|
if not armature:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if armature itself has mmd_type set to ROOT
|
||||||
|
if hasattr(armature, 'mmd_type') and armature.mmd_type == 'ROOT':
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if parent has mmd_type set to ROOT (parent container model)
|
||||||
|
if hasattr(armature, 'parent') and armature.parent:
|
||||||
|
parent = armature.parent
|
||||||
|
if hasattr(parent, 'mmd_type') and parent.mmd_type == 'ROOT':
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def validate_armature(armature: Object, detailed_messages: bool = False, override_mode: Optional[str] = None) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]:
|
||||||
"""
|
"""
|
||||||
Validates armature and returns validation results
|
Validates armature and returns validation results
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Validating armature: {armature.name if armature else 'None'}")
|
logger.debug(f"Validating armature: {armature.name if armature else 'None'}")
|
||||||
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
|
validation_mode = override_mode if override_mode else bpy.context.scene.avatar_toolkit.validation_mode
|
||||||
messages: List[str] = []
|
messages: List[str] = []
|
||||||
hierarchy_messages: List[str] = []
|
hierarchy_messages: List[str] = []
|
||||||
non_standard_messages: List[str] = []
|
non_standard_messages: List[str] = []
|
||||||
scale_messages: List[str] = []
|
scale_messages: List[str] = []
|
||||||
|
|
||||||
# Check if this is a PMX model
|
# Check if this is a PMX model
|
||||||
is_pmx_model = False
|
pmx_model = is_pmx_model(armature)
|
||||||
if armature and hasattr(armature, 'mmd_type') or (hasattr(armature, 'parent') and armature.parent and hasattr(armature.parent, 'mmd_type')):
|
if pmx_model:
|
||||||
is_pmx_model = True
|
|
||||||
logger.debug("Detected PMX model, using specialized validation")
|
logger.debug("Detected PMX model, using specialized validation")
|
||||||
|
|
||||||
if validation_mode == 'NONE':
|
if validation_mode == 'NONE':
|
||||||
@@ -104,17 +124,41 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
|
|||||||
|
|
||||||
# Non-standard bones check
|
# Non-standard bones check
|
||||||
non_standard_bones = []
|
non_standard_bones = []
|
||||||
required_patterns = [
|
|
||||||
'Hips', 'Spine', 'Chest', 'Neck', 'Head',
|
# Bones to ignore
|
||||||
'Upper', 'Lower', 'Hand', 'Foot', 'Toe',
|
ignore_patterns = [
|
||||||
'Thumb', 'Index', 'Middle', 'Ring', 'Pinky',
|
'tail', 'skirt', 'dress', 'hair', 'ribbon', 'bow', 'hat', 'cap',
|
||||||
'Eye'
|
'butt', 'breast', 'boob', 'chest_', 'belly', 'stomach',
|
||||||
|
'wing', 'fin', 'horn', 'ear_', 'accessory', 'extra',
|
||||||
|
'cloth', 'fabric', 'cape', 'coat', 'jacket', 'shirt',
|
||||||
|
'pants', 'shoe', 'boot', 'sock', 'glove', 'mitten',
|
||||||
|
'belt', 'strap', 'buckle', 'button', 'zipper',
|
||||||
|
'jewel', 'gem', 'ring', 'necklace', 'earring',
|
||||||
|
'flower', 'leaf', 'feather', 'fur', 'scale',
|
||||||
|
'bangs', 'sideburn', 'bell', 'leash', 'ears', 'chain',
|
||||||
|
'headband', 'necklace', 'necktie', 'strapNeck', 'ring',
|
||||||
|
'pin', 'hair',
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Create normalized lookup sets for faster comparison
|
||||||
|
normalized_standard_bones = {simplify_bonename(name) for name in standard_bones.values()}
|
||||||
|
normalized_acceptable_bones = set()
|
||||||
|
for names in acceptable_bone_names.values():
|
||||||
|
normalized_acceptable_bones.update(simplify_bonename(name) for name in names)
|
||||||
|
|
||||||
for bone_name in found_bones:
|
for bone_name in found_bones:
|
||||||
if any(pattern in bone_name for pattern in required_patterns):
|
# Normalize bone name for comparison
|
||||||
is_standard = bone_name in standard_bones.values()
|
normalized_bone_name = simplify_bonename(bone_name)
|
||||||
is_acceptable_bone = any(bone_name in names for names in acceptable_bone_names.values())
|
|
||||||
|
# Check if bone should be ignored (accessory bone)
|
||||||
|
is_ignored = any(pattern in normalized_bone_name for pattern in ignore_patterns)
|
||||||
|
|
||||||
|
if not is_ignored:
|
||||||
|
# Check if bone is in standard or acceptable lists
|
||||||
|
is_standard = normalized_bone_name in normalized_standard_bones
|
||||||
|
is_acceptable_bone = normalized_bone_name in normalized_acceptable_bones
|
||||||
|
|
||||||
if not (is_standard or is_acceptable_bone):
|
if not (is_standard or is_acceptable_bone):
|
||||||
non_standard_bones.append(bone_name)
|
non_standard_bones.append(bone_name)
|
||||||
|
|
||||||
@@ -132,7 +176,7 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
|
|||||||
non_standard_messages.append(t("Armature.validation.standardize_note.line3"))
|
non_standard_messages.append(t("Armature.validation.standardize_note.line3"))
|
||||||
|
|
||||||
# Special handling for PMX models
|
# Special handling for PMX models
|
||||||
if is_pmx_model:
|
if pmx_model:
|
||||||
logger.info("PMX model detected, applying specialized validation")
|
logger.info("PMX model detected, applying specialized validation")
|
||||||
# For PMX models, we'll be more lenient with validation
|
# For PMX models, we'll be more lenient with validation
|
||||||
# and provide specific guidance for these models
|
# and provide specific guidance for these models
|
||||||
@@ -186,31 +230,188 @@ def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name
|
|||||||
return False
|
return False
|
||||||
return bones[child_name].parent == bones[parent_name]
|
return bones[child_name].parent == bones[parent_name]
|
||||||
|
|
||||||
|
def extract_bone_side_info(bone_name: str) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Extract base bone name and side indicator from a bone name.
|
||||||
|
Returns (base_name, side) where side is 'L', 'R', or ''
|
||||||
|
"""
|
||||||
|
normalized = simplify_bonename(bone_name)
|
||||||
|
original = bone_name
|
||||||
|
|
||||||
|
# Common left/right patterns to check
|
||||||
|
left_patterns = [
|
||||||
|
'left', 'l', 'lft', 'lt',
|
||||||
|
'.l', '_l', '-l', ' l',
|
||||||
|
'左', 'ひだり'
|
||||||
|
]
|
||||||
|
|
||||||
|
right_patterns = [
|
||||||
|
'right', 'r', 'rgt', 'rt',
|
||||||
|
'.r', '_r', '-r', ' r',
|
||||||
|
'右', 'みぎ'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check for left patterns
|
||||||
|
for pattern in left_patterns:
|
||||||
|
pattern_norm = simplify_bonename(pattern)
|
||||||
|
if normalized.startswith(pattern_norm):
|
||||||
|
base = normalized[len(pattern_norm):]
|
||||||
|
if base: # Make sure there's something left
|
||||||
|
return base, 'L'
|
||||||
|
elif normalized.endswith(pattern_norm):
|
||||||
|
base = normalized[:-len(pattern_norm)]
|
||||||
|
if base:
|
||||||
|
return base, 'L'
|
||||||
|
elif pattern_norm in normalized:
|
||||||
|
# Handle cases like ArmLeft
|
||||||
|
parts = normalized.split(pattern_norm)
|
||||||
|
if len(parts) == 2:
|
||||||
|
base = parts[0] + parts[1]
|
||||||
|
if base:
|
||||||
|
return base, 'L'
|
||||||
|
|
||||||
|
# Check for right patterns
|
||||||
|
for pattern in right_patterns:
|
||||||
|
pattern_norm = simplify_bonename(pattern)
|
||||||
|
if normalized.startswith(pattern_norm):
|
||||||
|
base = normalized[len(pattern_norm):]
|
||||||
|
if base:
|
||||||
|
return base, 'R'
|
||||||
|
elif normalized.endswith(pattern_norm):
|
||||||
|
base = normalized[:-len(pattern_norm)]
|
||||||
|
if base:
|
||||||
|
return base, 'R'
|
||||||
|
elif pattern_norm in normalized:
|
||||||
|
parts = normalized.split(pattern_norm)
|
||||||
|
if len(parts) == 2:
|
||||||
|
base = parts[0] + parts[1]
|
||||||
|
if base:
|
||||||
|
return base, 'R'
|
||||||
|
|
||||||
|
return normalized, ''
|
||||||
|
|
||||||
|
def find_symmetric_bone_pairs(bones: Dict[str, Bone]) -> Dict[str, Tuple[List[str], List[str]]]:
|
||||||
|
"""
|
||||||
|
Automatically find symmetric bone pairs in the armature.
|
||||||
|
Returns dict mapping base_name to (left_bones, right_bones)
|
||||||
|
"""
|
||||||
|
bone_groups = {}
|
||||||
|
|
||||||
|
for bone_name in bones.keys():
|
||||||
|
base, side = extract_bone_side_info(bone_name)
|
||||||
|
|
||||||
|
if side:
|
||||||
|
if base not in bone_groups:
|
||||||
|
bone_groups[base] = {'L': [], 'R': []}
|
||||||
|
bone_groups[base][side].append(bone_name)
|
||||||
|
|
||||||
|
symmetric_pairs = {}
|
||||||
|
for base, sides in bone_groups.items():
|
||||||
|
if sides['L'] and sides['R']:
|
||||||
|
symmetric_pairs[base] = (sides['L'], sides['R'])
|
||||||
|
|
||||||
|
return symmetric_pairs
|
||||||
|
|
||||||
|
def validate_armature_symmetry(armature: Object) -> Tuple[bool, List[str]]:
|
||||||
|
"""
|
||||||
|
Comprehensive symmetry validation that provides detailed feedback
|
||||||
|
"""
|
||||||
|
if not armature or armature.type != 'ARMATURE':
|
||||||
|
return False, ["Invalid armature"]
|
||||||
|
|
||||||
|
bones = {bone.name: bone for bone in armature.data.bones}
|
||||||
|
symmetric_pairs = find_symmetric_bone_pairs(bones)
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
is_symmetric = True
|
||||||
|
|
||||||
|
if symmetric_pairs:
|
||||||
|
messages.append("Found symmetric bone pairs:")
|
||||||
|
for base, (left_bones, right_bones) in symmetric_pairs.items():
|
||||||
|
left_count = len(left_bones)
|
||||||
|
right_count = len(right_bones)
|
||||||
|
|
||||||
|
if left_count == right_count:
|
||||||
|
messages.append(f" ✓ {base}: {left_count} bones on each side")
|
||||||
|
for l_bone, r_bone in zip(sorted(left_bones), sorted(right_bones)):
|
||||||
|
messages.append(f" {l_bone} ↔ {r_bone}")
|
||||||
|
else:
|
||||||
|
is_symmetric = False
|
||||||
|
messages.append(f" ✗ {base}: {left_count} left, {right_count} right bones")
|
||||||
|
messages.append(f" Left: {', '.join(sorted(left_bones))}")
|
||||||
|
messages.append(f" Right: {', '.join(sorted(right_bones))}")
|
||||||
|
else:
|
||||||
|
messages.append("No symmetric bone pairs detected")
|
||||||
|
is_symmetric = False
|
||||||
|
|
||||||
|
return is_symmetric, messages
|
||||||
|
|
||||||
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool:
|
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool:
|
||||||
"""Validate if matching left and right bones exist for a given base bone name"""
|
"""Validate if matching left and right bones exist for a given base bone name"""
|
||||||
# Extract left and right bone names from both hierarchies
|
# First try the new intelligent detection
|
||||||
|
symmetric_pairs = find_symmetric_bone_pairs(bones)
|
||||||
|
|
||||||
|
# Look for bones that match the requested base type
|
||||||
|
matching_left_bones = []
|
||||||
|
matching_right_bones = []
|
||||||
|
|
||||||
|
# Check each detected symmetric pair
|
||||||
|
for pair_base, (left_bones, right_bones) in symmetric_pairs.items():
|
||||||
|
if base.lower() in pair_base.lower() or pair_base.lower() in base.lower():
|
||||||
|
matching_left_bones.extend(left_bones)
|
||||||
|
matching_right_bones.extend(right_bones)
|
||||||
|
|
||||||
|
if matching_left_bones or matching_right_bones:
|
||||||
|
left_bases = {}
|
||||||
|
right_bases = {}
|
||||||
|
|
||||||
|
for bone_name in matching_left_bones:
|
||||||
|
bone_base, side = extract_bone_side_info(bone_name)
|
||||||
|
if bone_base not in left_bases:
|
||||||
|
left_bases[bone_base] = []
|
||||||
|
left_bases[bone_base].append(bone_name)
|
||||||
|
|
||||||
|
for bone_name in matching_right_bones:
|
||||||
|
bone_base, side = extract_bone_side_info(bone_name)
|
||||||
|
if bone_base not in right_bases:
|
||||||
|
right_bases[bone_base] = []
|
||||||
|
right_bases[bone_base].append(bone_name)
|
||||||
|
|
||||||
|
all_bases = set(left_bases.keys()) | set(right_bases.keys())
|
||||||
|
for bone_base in all_bases:
|
||||||
|
left_count = len(left_bases.get(bone_base, []))
|
||||||
|
right_count = len(right_bases.get(bone_base, []))
|
||||||
|
if left_count != right_count:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return len(all_bases) > 0
|
||||||
|
|
||||||
|
# Fallback to original dictionary-based method
|
||||||
left_bone_names = set()
|
left_bone_names = set()
|
||||||
right_bone_names = set()
|
right_bone_names = set()
|
||||||
|
|
||||||
|
# Normalize bone names in the bones dict for comparison
|
||||||
|
normalized_bones = {simplify_bonename(name): name for name in bones.keys()}
|
||||||
|
|
||||||
# Add standard bones
|
# Add standard bones
|
||||||
for key, value in standard_bones.items():
|
for key, value in standard_bones.items():
|
||||||
if base in key.lower():
|
if base in key.lower():
|
||||||
if '_l' in key.lower():
|
if '_l' in key.lower():
|
||||||
left_bone_names.add(value)
|
left_bone_names.add(simplify_bonename(value))
|
||||||
elif '_r' in key.lower():
|
elif '_r' in key.lower():
|
||||||
right_bone_names.add(value)
|
right_bone_names.add(simplify_bonename(value))
|
||||||
|
|
||||||
# Add acceptable bones
|
# Add acceptable bones
|
||||||
for key, names in acceptable_bone_names.items():
|
for key, names in acceptable_bone_names.items():
|
||||||
if base in key.lower():
|
if base in key.lower():
|
||||||
if '_l' in key.lower():
|
if '_l' in key.lower():
|
||||||
left_bone_names.update(names)
|
left_bone_names.update(simplify_bonename(name) for name in names)
|
||||||
elif '_r' in key.lower():
|
elif '_r' in key.lower():
|
||||||
right_bone_names.update(names)
|
right_bone_names.update(simplify_bonename(name) for name in names)
|
||||||
|
|
||||||
# Check if at least one pair exists and matches
|
# Check if at least one pair exists and matches
|
||||||
left_exists = any(name in bones for name in left_bone_names)
|
left_exists = any(name in normalized_bones for name in left_bone_names)
|
||||||
right_exists = any(name in bones for name in right_bone_names)
|
right_exists = any(name in normalized_bones for name in right_bone_names)
|
||||||
|
|
||||||
return left_exists == right_exists
|
return left_exists == right_exists
|
||||||
|
|
||||||
@@ -224,22 +425,34 @@ def validate_finger_chain(bones: Dict[str, Bone], chain: Tuple[str, ...]) -> boo
|
|||||||
def check_acceptable_standards(bones: Dict[str, Bone]) -> bool:
|
def check_acceptable_standards(bones: Dict[str, Bone]) -> bool:
|
||||||
"""Check if armature matches acceptable non-standard hierarchy"""
|
"""Check if armature matches acceptable non-standard hierarchy"""
|
||||||
logger.debug("Checking for acceptable standards")
|
logger.debug("Checking for acceptable standards")
|
||||||
|
|
||||||
|
# Create normalized lookup for existing bones
|
||||||
|
normalized_bones = {simplify_bonename(name): name for name in bones.keys()}
|
||||||
|
|
||||||
# Check if bones exist in acceptable list
|
# Check if bones exist in acceptable list
|
||||||
for bone_category, acceptable_names in acceptable_bone_names.items():
|
for bone_category, acceptable_names in acceptable_bone_names.items():
|
||||||
found = False
|
found = False
|
||||||
for name in acceptable_names:
|
for name in acceptable_names:
|
||||||
if name in bones:
|
normalized_name = simplify_bonename(name)
|
||||||
|
if normalized_name in normalized_bones:
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
logger.debug(f"Missing acceptable bone for category: {bone_category}")
|
logger.debug(f"Missing acceptable bone for category: {bone_category}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Validate acceptable hierarchy
|
# Validate acceptable hierarchy using normalized names
|
||||||
for parent, child in acceptable_bone_hierarchy:
|
for parent, child in acceptable_bone_hierarchy:
|
||||||
if parent in bones and child in bones:
|
parent_normalized = simplify_bonename(parent)
|
||||||
if not validate_bone_hierarchy(bones, parent, child):
|
child_normalized = simplify_bonename(child)
|
||||||
logger.debug(f"Invalid acceptable hierarchy: {parent} -> {child}")
|
|
||||||
|
# Find actual bone names from normalized names
|
||||||
|
actual_parent = normalized_bones.get(parent_normalized)
|
||||||
|
actual_child = normalized_bones.get(child_normalized)
|
||||||
|
|
||||||
|
if actual_parent and actual_child:
|
||||||
|
if not validate_bone_hierarchy(bones, actual_parent, actual_child):
|
||||||
|
logger.debug(f"Invalid acceptable hierarchy: {actual_parent} -> {actual_child}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.debug("Armature meets acceptable standards")
|
logger.debug("Armature meets acceptable standards")
|
||||||
@@ -589,3 +802,44 @@ class AvatarToolkit_OT_ClearBoneHighlighting(Operator):
|
|||||||
logger.info("Bone highlighting cleared")
|
logger.info("Bone highlighting cleared")
|
||||||
self.report({'INFO'}, t("Validation.highlighting_cleared"))
|
self.report({'INFO'}, t("Validation.highlighting_cleared"))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ValidateArmatureManual(Operator):
|
||||||
|
"""Manually validate armature and show results"""
|
||||||
|
bl_idname = "avatar_toolkit.validate_armature_manual"
|
||||||
|
bl_label = t("Validation.validate_now", "Validate Armature Now")
|
||||||
|
bl_description = t("Validation.validate_now_desc", "Run armature validation and display detailed results")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return get_active_armature(context) is not None
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
logger.warning("No active armature found for validation")
|
||||||
|
self.report({'ERROR'}, t("Validation.no_armature"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
logger.info(f"Running manual validation for armature: {armature.name}")
|
||||||
|
|
||||||
|
# Clear the validation cache to force a refresh
|
||||||
|
from ..ui.quick_access_panel import clear_armature_caches
|
||||||
|
clear_armature_caches()
|
||||||
|
|
||||||
|
# Toggle the show_validation_results flag to display results
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
props.show_validation_results = True
|
||||||
|
|
||||||
|
# Run validation
|
||||||
|
is_valid, messages, is_acceptable = validate_armature(armature, detailed_messages=False)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
if is_acceptable:
|
||||||
|
self.report({'INFO'}, t("Armature.validation.acceptable_standard.success"))
|
||||||
|
else:
|
||||||
|
self.report({'INFO'}, t("QuickAccess.valid_armature"))
|
||||||
|
else:
|
||||||
|
self.report({'WARNING'}, t("Validation.status.failed"))
|
||||||
|
|
||||||
|
logger.info("Manual validation complete")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|||||||
+130
-10
@@ -92,23 +92,132 @@ class ProgressTracker:
|
|||||||
|
|
||||||
def get_active_armature(context: Context) -> Optional[Object]:
|
def get_active_armature(context: Context) -> Optional[Object]:
|
||||||
"""Get the currently selected armature from Avatar Toolkit properties"""
|
"""Get the currently selected armature from Avatar Toolkit properties"""
|
||||||
armature_name = str(context.scene.avatar_toolkit.active_armature)
|
try:
|
||||||
if armature_name and armature_name != 'NONE':
|
# Get the safe identifier from the enum property
|
||||||
return bpy.data.objects.get(armature_name)
|
armature_id = context.scene.avatar_toolkit.active_armature
|
||||||
|
|
||||||
|
if not armature_id or armature_id == 'NONE':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# The identifier format is "ARM_{pointer_value}"
|
||||||
|
if armature_id.startswith('ARM_'):
|
||||||
|
try:
|
||||||
|
pointer_str = armature_id[4:]
|
||||||
|
pointer_value = int(pointer_str)
|
||||||
|
|
||||||
|
# Find the armature with this pointer value
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
if obj.type == 'ARMATURE' and obj.as_pointer() == pointer_value:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
logger.warning(f"Armature with pointer {pointer_value} not found")
|
||||||
|
except (ValueError, AttributeError) as e:
|
||||||
|
logger.error(f"Failed to parse armature identifier: {e}")
|
||||||
|
|
||||||
|
# Fallback for old-style identifiers (direct name)
|
||||||
|
# This handles backward compatibility
|
||||||
|
return bpy.data.objects.get(armature_id)
|
||||||
|
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError, AttributeError) as e:
|
||||||
|
# Handle encoding issues as a last resort
|
||||||
|
logger.warning(f"Encoding issue with active_armature property: {e}")
|
||||||
|
|
||||||
|
# Final fallback: return active object if it's an armature, or first armature found
|
||||||
|
if context.view_layer.objects.active and context.view_layer.objects.active.type == 'ARMATURE':
|
||||||
|
return context.view_layer.objects.active
|
||||||
|
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
if obj.type == 'ARMATURE':
|
||||||
|
logger.info(f"Falling back to first armature found: {obj.name}")
|
||||||
|
return obj
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_active_armature(context: Context, armature: Object) -> None:
|
def set_active_armature(context: Context, armature: Object) -> None:
|
||||||
"""Set the active armature for Avatar Toolkit operations"""
|
"""Set the active armature for Avatar Toolkit operations using safe identifier"""
|
||||||
context.scene.avatar_toolkit.active_armature = armature
|
if armature and armature.type == 'ARMATURE':
|
||||||
|
# Use the same safe identifier format as get_armature_list
|
||||||
|
safe_id = f"ARM_{armature.as_pointer()}"
|
||||||
|
context.scene.avatar_toolkit.active_armature = safe_id
|
||||||
|
else:
|
||||||
|
context.scene.avatar_toolkit.active_armature = 'NONE'
|
||||||
|
|
||||||
|
def get_mesh_from_identifier(mesh_id: str) -> Optional[Object]:
|
||||||
|
"""Get mesh object from safe identifier
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mesh_id: Safe identifier in format "MESH_{pointer}" or direct object name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mesh object or None if not found
|
||||||
|
"""
|
||||||
|
if not mesh_id or mesh_id == 'NONE':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Handle new-style identifiers (MESH_{pointer})
|
||||||
|
if mesh_id.startswith('MESH_'):
|
||||||
|
try:
|
||||||
|
pointer_str = mesh_id[5:] # Remove "MESH_" prefix
|
||||||
|
target_pointer = int(pointer_str)
|
||||||
|
|
||||||
|
# Search for object with matching pointer
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if obj.type == 'MESH' and obj.as_pointer() == target_pointer:
|
||||||
|
return obj
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback for old-style identifiers (direct name)
|
||||||
|
return bpy.data.objects.get(mesh_id)
|
||||||
|
|
||||||
|
def clear_enum_caches() -> None:
|
||||||
|
"""Clear all enum property caches to force refresh of dropdown lists"""
|
||||||
|
if hasattr(get_armature_list, '_cache_key'):
|
||||||
|
delattr(get_armature_list, '_cache_key')
|
||||||
|
if hasattr(get_armature_list, '_cached_items'):
|
||||||
|
delattr(get_armature_list, '_cached_items')
|
||||||
|
|
||||||
def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]:
|
def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]:
|
||||||
"""Get list of all armature objects in the scene"""
|
"""Get list of all armature objects in the scene
|
||||||
|
|
||||||
|
Returns tuples of (identifier, display_name, description) where:
|
||||||
|
- identifier: ASCII-safe unique ID (uses object's memory address)
|
||||||
|
- display_name: The actual object name (can contain Japanese characters)
|
||||||
|
- description: Empty string
|
||||||
|
|
||||||
|
Uses caching to prevent encoding issues with Blender's EnumProperty system
|
||||||
|
"""
|
||||||
if context is None:
|
if context is None:
|
||||||
context = bpy.context
|
context = bpy.context
|
||||||
armatures = [(obj.name, obj.name, "") for obj in context.scene.objects if obj.type == 'ARMATURE']
|
|
||||||
|
# Create a cache key based on armature objects in scene
|
||||||
|
armature_objects = [obj for obj in context.scene.objects if obj.type == 'ARMATURE']
|
||||||
|
cache_key = tuple((obj.name, obj.as_pointer()) for obj in armature_objects)
|
||||||
|
|
||||||
|
# Check if we have a cached result
|
||||||
|
if hasattr(get_armature_list, '_cache_key') and get_armature_list._cache_key == cache_key:
|
||||||
|
if hasattr(get_armature_list, '_cached_items'):
|
||||||
|
return get_armature_list._cached_items
|
||||||
|
|
||||||
|
# Build the list
|
||||||
|
armatures = []
|
||||||
|
for obj in armature_objects:
|
||||||
|
# Create a safe ASCII identifier using the object pointer
|
||||||
|
safe_id = f"ARM_{obj.as_pointer()}"
|
||||||
|
# Use the name directly - Blender should handle Unicode in display names
|
||||||
|
display_name = obj.name
|
||||||
|
armatures.append((safe_id, display_name, ""))
|
||||||
|
|
||||||
if not armatures:
|
if not armatures:
|
||||||
return [('NONE', t("Armature.validation.no_armature"), '')]
|
result = [('NONE', t("Armature.validation.no_armature"), '')]
|
||||||
return armatures
|
else:
|
||||||
|
result = armatures
|
||||||
|
|
||||||
|
# Cache the result
|
||||||
|
get_armature_list._cache_key = cache_key
|
||||||
|
get_armature_list._cached_items = result
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def auto_select_single_armature(context: Context) -> None:
|
def auto_select_single_armature(context: Context) -> None:
|
||||||
"""Automatically select armature if only one exists in scene"""
|
"""Automatically select armature if only one exists in scene"""
|
||||||
@@ -140,6 +249,12 @@ def get_all_meshes(context: Context) -> List[Object]:
|
|||||||
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_meshes_for_armature(armature: Object) -> List[Object]:
|
||||||
|
"""Get all mesh objects parented to a specific armature"""
|
||||||
|
if armature and armature.type == 'ARMATURE':
|
||||||
|
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
||||||
|
return []
|
||||||
|
|
||||||
def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]:
|
def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]:
|
||||||
"""Validate mesh object for pose operations"""
|
"""Validate mesh object for pose operations"""
|
||||||
if not mesh_obj.data:
|
if not mesh_obj.data:
|
||||||
@@ -650,9 +765,14 @@ class ArmatureData(Tuple[bool,bool]):
|
|||||||
|
|
||||||
def store_breaking_settings_armature(armature: bpy.types.Object) -> ArmatureData:
|
def store_breaking_settings_armature(armature: bpy.types.Object) -> ArmatureData:
|
||||||
armature_data: bpy.types.Armature = armature.data
|
armature_data: bpy.types.Armature = armature.data
|
||||||
return (armature_data.use_mirror_x, armature.pose.use_mirror_x)
|
data: ArmatureData = (armature_data.use_mirror_x, armature.pose.use_mirror_x)
|
||||||
|
armature_data.use_mirror_x, armature.pose.use_mirror_x = (False, False)
|
||||||
|
return data
|
||||||
|
|
||||||
def restore_breaking_settings_armature(armature: bpy.types.Object, data: ArmatureData) -> None:
|
def restore_breaking_settings_armature(armature: bpy.types.Object, data: ArmatureData) -> None:
|
||||||
|
# Check if armature object is still valid (not removed)
|
||||||
|
if not armature or armature.name not in bpy.data.objects:
|
||||||
|
return
|
||||||
armature_data: bpy.types.Armature = armature.data
|
armature_data: bpy.types.Armature = armature.data
|
||||||
armature_data.use_mirror_x, armature.pose.use_mirror_x = data
|
armature_data.use_mirror_x, armature.pose.use_mirror_x = data
|
||||||
|
|
||||||
|
|||||||
+304
-153
@@ -255,30 +255,115 @@ bone_names = {
|
|||||||
"right_eye": [
|
"right_eye": [
|
||||||
"eyeright", "righteye", "eyer", "reye", "右目", "ik_右目"
|
"eyeright", "righteye", "eyer", "reye", "右目", "ik_右目"
|
||||||
],
|
],
|
||||||
|
"breast_1_l": [
|
||||||
|
"j_sec_l_bust1", "breast1_l", "leftbreast1", "lbreast1", "bust1_l"
|
||||||
|
],
|
||||||
|
"breast_2_l": [
|
||||||
|
"j_sec_l_bust2", "breast2_l", "leftbreast2", "lbreast2", "bust2_l"
|
||||||
|
],
|
||||||
|
"breast_3_l": [
|
||||||
|
"j_sec_l_bust3", "breast3_l", "leftbreast3", "lbreast3", "bust3_l"
|
||||||
|
],
|
||||||
|
"breast_1_r": [
|
||||||
|
"j_sec_r_bust1", "breast1_r", "rightbreast1", "rbreast1", "bust1_r"
|
||||||
|
],
|
||||||
|
"breast_2_r": [
|
||||||
|
"j_sec_r_bust2", "breast2_r", "rightbreast2", "rbreast2", "bust2_r"
|
||||||
|
],
|
||||||
|
"breast_3_r": [
|
||||||
|
"j_sec_r_bust3", "breast3_r", "rightbreast3", "rbreast3", "bust3_r"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add VRM bone name variations
|
# Add VRM bone name variations
|
||||||
bone_names.update({
|
bone_names.update({
|
||||||
'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips'],
|
'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips', 'leftupperleg', 'rightupperleg'],
|
||||||
'spine': bone_names['spine'] + ['jbipcspine', 'jspine', 'vrmspine'],
|
'spine': bone_names['spine'] + ['jbipcspine', 'jspine', 'vrmspine'],
|
||||||
'chest': bone_names['chest'] + ['jbipcchest', 'jchest', 'vrmchest'],
|
'chest': bone_names['chest'] + ['jbipcchest', 'jchest', 'vrmchest', 'upperchest'],
|
||||||
'upper_chest': bone_names['upper_chest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest'],
|
'upper_chest': bone_names['upper_chest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest', 'upperchest'],
|
||||||
'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'],
|
'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'],
|
||||||
'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead'],
|
'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead', 'lefteye', 'righteye'],
|
||||||
|
|
||||||
# VRM specific finger naming
|
# VRM arms - both simplified patterns
|
||||||
'thumb_0_l': bone_names['thumb_0_l'] + ['thumbmetacarpall', 'jthumb1l'],
|
'left_shoulder': bone_names['left_shoulder'] + ['jbipllshoulder', 'jlshoulder', 'jbiplshoulder', 'leftshoulder', 'jbipllclavicle'],
|
||||||
'index_0_l': bone_names['index_0_l'] + ['indexmetacarpall', 'jindex1l'],
|
'left_arm': bone_names['left_arm'] + ['jbiplupperarm', 'jlupperarm', 'leftupperarm'],
|
||||||
'middle_0_l': bone_names['middle_0_l'] + ['middlemetacarpall', 'jmiddle1l'],
|
'left_elbow': bone_names['left_elbow'] + ['jbipllforearm', 'jlforearm', 'jbipllowerarm', 'leftlowerarm'],
|
||||||
'ring_0_l': bone_names['ring_0_l'] + ['ringmetacarpall', 'jring1l'],
|
'left_wrist': bone_names['left_wrist'] + ['jbipllhand', 'jlhand', 'jbiplhand', 'lefthand'],
|
||||||
'pinkie_0_l': bone_names['pinkie_0_l'] + ['littlemetacarpall', 'jlittle1l'],
|
|
||||||
|
|
||||||
# Mirror for right side
|
'right_shoulder': bone_names['right_shoulder'] + ['jbiprlshoulder', 'jrshoulder', 'jbiprshoulder', 'rightshoulder', 'jbiprrclavicle'],
|
||||||
'thumb_0_r': bone_names['thumb_0_r'] + ['thumbmetacarpalr', 'jthumb1r'],
|
'right_arm': bone_names['right_arm'] + ['jbiprrupperarm', 'jrupperarm', 'jbiprupperarm', 'rightupperarm'],
|
||||||
'index_0_r': bone_names['index_0_r'] + ['indexmetacarpalr', 'jindex1r'],
|
'right_elbow': bone_names['right_elbow'] + ['jbiprrforearm', 'jrforearm', 'jbiprforearm', 'jbiprlowerarm', 'rightlowerarm'],
|
||||||
'middle_0_r': bone_names['middle_0_r'] + ['middlemetacarpalr', 'jmiddle1r'],
|
'right_wrist': bone_names['right_wrist'] + ['jbiprrhand', 'jrhand', 'jbiprhand', 'righthand'],
|
||||||
'ring_0_r': bone_names['ring_0_r'] + ['ringmetacarpalr', 'jring1r'],
|
|
||||||
'pinkie_0_r': bone_names['pinkie_0_r'] + ['littlemetacarpalr', 'jlittle1r']
|
# VRM legs - both simplified patterns
|
||||||
|
'left_leg': bone_names['left_leg'] + ['jbiplupperleg', 'jlupperleg', 'leftupperleg'],
|
||||||
|
'left_knee': bone_names['left_knee'] + ['jbipllowerleg', 'jllowerleg', 'leftlowerleg'],
|
||||||
|
'left_ankle': bone_names['left_ankle'] + ['jbipllfoot', 'jlfoot', 'jbiplfoot', 'leftfoot'],
|
||||||
|
'left_toe': bone_names['left_toe'] + ['jbiplltoe', 'jltoe', 'jbipltoebase', 'lefttoes'],
|
||||||
|
|
||||||
|
'right_leg': bone_names['right_leg'] + ['jbiprrupperleg', 'jrupperleg', 'jbiprupperleg', 'rightupperleg'],
|
||||||
|
'right_knee': bone_names['right_knee'] + ['jbiprrlowerleg', 'jrlowerleg', 'jbiprlowerleg', 'rightlowerleg'],
|
||||||
|
'right_ankle': bone_names['right_ankle'] + ['jbiprrfoot', 'jrfoot', 'jbiprfoot', 'rightfoot'],
|
||||||
|
'right_toe': bone_names['right_toe'] + ['jbiprrtoe', 'jrtoe', 'jbiprtoebase', 'righttoes'],
|
||||||
|
|
||||||
|
# VRM eyes
|
||||||
|
'left_eye': bone_names['left_eye'] + ['jbipcleye', 'jleye', 'jadjlfaceeye'],
|
||||||
|
'right_eye': bone_names['right_eye'] + ['jbipcreye', 'jreye', 'jadjrfaceeye'],
|
||||||
|
|
||||||
|
# VRM jaw
|
||||||
|
'jaw': ['jaw', 'mandible', 'lowerjaw', 'chin', 'あご', 'ik_あご'],
|
||||||
|
|
||||||
|
# Breast bones
|
||||||
|
'breast_1_l': bone_names['breast_1_l'] + ['jbipcbreast1l', 'jlbreast1', 'jseclbust1'],
|
||||||
|
'breast_2_l': bone_names['breast_2_l'] + ['jbipcbreast2l', 'jlbreast2', 'jseclbust2'],
|
||||||
|
'breast_3_l': bone_names['breast_3_l'] + ['jbipcbreast3l', 'jlbreast3', 'jseclbust3'],
|
||||||
|
'breast_1_r': bone_names['breast_1_r'] + ['jbipcbreast1r', 'jrbreast1', 'jsecrbust1'],
|
||||||
|
'breast_2_r': bone_names['breast_2_r'] + ['jbipcbreast2r', 'jrbreast2', 'jsecrbust2'],
|
||||||
|
'breast_3_r': bone_names['breast_3_r'] + ['jbipcbreast3r', 'jrbreast3', 'jsecrbust3'],
|
||||||
|
|
||||||
|
# VRM fingers - Left (including Little finger variations)
|
||||||
|
'thumb_0_l': bone_names['thumb_0_l'] + ['jbipllthumb0', 'jlthumb0', 'jbipllthumbmetacarpal', 'jlthumbmetacarpal', 'leftthumbmetacarpal'],
|
||||||
|
'thumb_1_l': bone_names['thumb_1_l'] + ['jbipllthumb1', 'jlthumb1', 'jbiplthumb1', 'leftthumbproximal'],
|
||||||
|
'thumb_2_l': bone_names['thumb_2_l'] + ['jbipllthumb2', 'jlthumb2', 'jbiplthumb2', 'leftthumbintermediate'],
|
||||||
|
'thumb_3_l': bone_names['thumb_3_l'] + ['jbipllthumb3', 'jlthumb3', 'jbiplthumb3', 'leftthumbdistal'],
|
||||||
|
|
||||||
|
'index_1_l': bone_names['index_1_l'] + ['jbipllindex1', 'jlindex1', 'jbiplindex1', 'leftindexproximal'],
|
||||||
|
'index_2_l': bone_names['index_2_l'] + ['jbipllindex2', 'jlindex2', 'jbiplindex2', 'leftindexintermediate'],
|
||||||
|
'index_3_l': bone_names['index_3_l'] + ['jbipllindex3', 'jlindex3', 'jbiplindex3', 'leftindexdistal'],
|
||||||
|
|
||||||
|
'middle_1_l': bone_names['middle_1_l'] + ['jbipllmiddle1', 'jlmiddle1', 'jbiplmiddle1', 'leftmiddleproximal'],
|
||||||
|
'middle_2_l': bone_names['middle_2_l'] + ['jbipllmiddle2', 'jlmiddle2', 'jbiplmiddle2', 'leftmiddleintermediate'],
|
||||||
|
'middle_3_l': bone_names['middle_3_l'] + ['jbipllmiddle3', 'jlmiddle3', 'jbiplmiddle3', 'leftmiddledistal'],
|
||||||
|
|
||||||
|
'ring_1_l': bone_names['ring_1_l'] + ['jbipllring1', 'jlring1', 'jbiplring1', 'leftringproximal'],
|
||||||
|
'ring_2_l': bone_names['ring_2_l'] + ['jbipllring2', 'jlring2', 'jbiplring2', 'leftringintermediate'],
|
||||||
|
'ring_3_l': bone_names['ring_3_l'] + ['jbipllring3', 'jlring3', 'jbiplring3', 'leftringdistal'],
|
||||||
|
|
||||||
|
'pinkie_1_l': bone_names['pinkie_1_l'] + ['jbipllpinky1', 'jlpinky1', 'jbipllittle1', 'jbipllpinkie1', 'leftlittleproximal'],
|
||||||
|
'pinkie_2_l': bone_names['pinkie_2_l'] + ['jbipllpinky2', 'jlpinky2', 'jbipllittle2', 'jbipllpinkie2', 'leftlittleintermediate'],
|
||||||
|
'pinkie_3_l': bone_names['pinkie_3_l'] + ['jbipllpinky3', 'jlpinky3', 'jbipllittle3', 'jbipllpinkie3', 'leftlittledistal'],
|
||||||
|
|
||||||
|
# VRM fingers - Right (including Little finger variations)
|
||||||
|
'thumb_0_r': bone_names['thumb_0_r'] + ['jbiprthumb0', 'jrthumb0', 'jbiprthumbmetacarpal', 'jrthumbmetacarpal', 'rightthumbmetacarpal'],
|
||||||
|
'thumb_1_r': bone_names['thumb_1_r'] + ['jbiprthumb1', 'jrthumb1', 'jbiprrrthumb1', 'rightthumbproximal'],
|
||||||
|
'thumb_2_r': bone_names['thumb_2_r'] + ['jbiprthumb2', 'jrthumb2', 'jbiprrrthumb2', 'rightthumbintermediate'],
|
||||||
|
'thumb_3_r': bone_names['thumb_3_r'] + ['jbiprthumb3', 'jrthumb3', 'jbiprrrthumb3', 'rightthumbdistal'],
|
||||||
|
|
||||||
|
'index_1_r': bone_names['index_1_r'] + ['jbiprindex1', 'jrindex1', 'jbiprrrindex1', 'rightindexproximal'],
|
||||||
|
'index_2_r': bone_names['index_2_r'] + ['jbiprindex2', 'jrindex2', 'jbiprrrindex2', 'rightindexintermediate'],
|
||||||
|
'index_3_r': bone_names['index_3_r'] + ['jbiprindex3', 'jrindex3', 'jbiprrrindex3', 'rightindexdistal'],
|
||||||
|
|
||||||
|
'middle_1_r': bone_names['middle_1_r'] + ['jbiprmiddle1', 'jrmiddle1', 'jbiprrmiddle1', 'rightmiddleproximal'],
|
||||||
|
'middle_2_r': bone_names['middle_2_r'] + ['jbiprmiddle2', 'jrmiddle2', 'jbiprrmiddle2', 'rightmiddleintermediate'],
|
||||||
|
'middle_3_r': bone_names['middle_3_r'] + ['jbiprmiddle3', 'jrmiddle3', 'jbiprrmiddle3', 'rightmiddledistal'],
|
||||||
|
|
||||||
|
'ring_1_r': bone_names['ring_1_r'] + ['jbiprring1', 'jrring1', 'jbiprrrring1', 'rightringproximal'],
|
||||||
|
'ring_2_r': bone_names['ring_2_r'] + ['jbiprring2', 'jrring2', 'jbiprrrring2', 'rightringintermediate'],
|
||||||
|
'ring_3_r': bone_names['ring_3_r'] + ['jbiprring3', 'jrring3', 'jbiprrrring3', 'rightringdistal'],
|
||||||
|
|
||||||
|
'pinkie_1_r': bone_names['pinkie_1_r'] + ['jbiprpinky1', 'jrpinky1', 'jbiprlittle1', 'jbiprrrpinky1', 'rightlittleproximal'],
|
||||||
|
'pinkie_2_r': bone_names['pinkie_2_r'] + ['jbiprpinky2', 'jrpinky2', 'jbiprlittle2', 'jbiprrrpinky2', 'rightlittleintermediate'],
|
||||||
|
'pinkie_3_r': bone_names['pinkie_3_r'] + ['jbiprpinky3', 'jrpinky3', 'jbiprlittle3', 'jbiprrrpinky3', 'rightlittledistal']
|
||||||
})
|
})
|
||||||
|
|
||||||
# array taken from cats
|
# array taken from cats
|
||||||
@@ -367,113 +452,125 @@ standard_bones = {
|
|||||||
'hips': 'Hips',
|
'hips': 'Hips',
|
||||||
'spine': 'Spine',
|
'spine': 'Spine',
|
||||||
'chest': 'Chest',
|
'chest': 'Chest',
|
||||||
'upper_chest': 'Chest.Up',
|
'upper_chest': 'UpperChest',
|
||||||
'neck': 'Neck',
|
'neck': 'Neck',
|
||||||
'head': 'Head',
|
'head': 'Head',
|
||||||
|
|
||||||
# Arms
|
# Arms
|
||||||
'left_arm': 'UpperArm.L',
|
'left_shoulder': 'Shoulder_L',
|
||||||
'left_elbow': 'LowerArm.L',
|
'left_arm': 'UpperArm_L',
|
||||||
'left_wrist': 'Hand.L',
|
'left_elbow': 'LowerArm_L',
|
||||||
'right_arm': 'UpperArm.R',
|
'left_wrist': 'Hand_L',
|
||||||
'right_elbow': 'LowerArm.R',
|
'right_shoulder': 'Shoulder_R',
|
||||||
'right_wrist': 'Hand.R',
|
'right_arm': 'UpperArm_R',
|
||||||
|
'right_elbow': 'LowerArm_R',
|
||||||
|
'right_wrist': 'Hand_R',
|
||||||
|
|
||||||
# Legs
|
# Legs
|
||||||
'left_leg': 'UpperLeg.L',
|
'left_leg': 'UpperLeg_L',
|
||||||
'left_knee': 'LowerLeg.L',
|
'left_knee': 'LowerLeg_L',
|
||||||
'left_ankle': 'Foot.L',
|
'left_ankle': 'Foot_L',
|
||||||
'left_toe': 'Toes.L',
|
'left_toe': 'Toe_L',
|
||||||
'right_leg': 'UpperLeg.R',
|
'right_leg': 'UpperLeg_R',
|
||||||
'right_knee': 'LowerLeg.R',
|
'right_knee': 'LowerLeg_R',
|
||||||
'right_ankle': 'Foot.R',
|
'right_ankle': 'Foot_R',
|
||||||
'right_toe': 'Toes.R',
|
'right_toe': 'Toe_R',
|
||||||
|
|
||||||
# Fingers Left
|
# Fingers Left
|
||||||
'thumb_1_l': 'Thumb1.L',
|
'thumb_1_l': 'Thumb_L',
|
||||||
'thumb_2_l': 'Thumb2.L',
|
'thumb_2_l': 'Thumb_L.001',
|
||||||
'thumb_3_l': 'Thumb3.L',
|
'thumb_3_l': 'Thumb_L.002',
|
||||||
'index_1_l': 'Index1.L',
|
'index_1_l': 'Index_L',
|
||||||
'index_2_l': 'Index2.L',
|
'index_2_l': 'Index_L.001',
|
||||||
'index_3_l': 'Index3.L',
|
'index_3_l': 'Index_L.002',
|
||||||
'middle_1_l': 'Middle1.L',
|
'middle_1_l': 'Middle_L',
|
||||||
'middle_2_l': 'Middle2.L',
|
'middle_2_l': 'Middle_L.001',
|
||||||
'middle_3_l': 'Middle3.L',
|
'middle_3_l': 'Middle_L.002',
|
||||||
'ring_1_l': 'Ring1.L',
|
'ring_1_l': 'Ring_L',
|
||||||
'ring_2_l': 'Ring2.L',
|
'ring_2_l': 'Ring_L.001',
|
||||||
'ring_3_l': 'Ring3.L',
|
'ring_3_l': 'Ring_L.002',
|
||||||
'pinkie_1_l': 'Pinky1.L',
|
'pinkie_1_l': 'Pinky_L',
|
||||||
'pinkie_2_l': 'Pinky2.L',
|
'pinkie_2_l': 'Pinky_L.001',
|
||||||
'pinkie_3_l': 'Pinky3.L',
|
'pinkie_3_l': 'Pinky_L.002',
|
||||||
|
|
||||||
# Fingers Right
|
# Fingers Right
|
||||||
'thumb_1_r': 'Thumb1.R',
|
'thumb_1_r': 'Thumb_R',
|
||||||
'thumb_2_r': 'Thumb2.R',
|
'thumb_2_r': 'Thumb_R.001',
|
||||||
'thumb_3_r': 'Thumb3.R',
|
'thumb_3_r': 'Thumb_R.002',
|
||||||
'index_1_r': 'Index1.R',
|
'index_1_r': 'Index_R',
|
||||||
'index_2_r': 'Index2.R',
|
'index_2_r': 'Index_R.001',
|
||||||
'index_3_r': 'Index3.R',
|
'index_3_r': 'Index_R.002',
|
||||||
'middle_1_r': 'Middle1.R',
|
'middle_1_r': 'Middle_R',
|
||||||
'middle_2_r': 'Middle2.R',
|
'middle_2_r': 'Middle_R.001',
|
||||||
'middle_3_r': 'Middle3.R',
|
'middle_3_r': 'Middle_R.002',
|
||||||
'ring_1_r': 'Ring1.R',
|
'ring_1_r': 'Ring_R',
|
||||||
'ring_2_r': 'Ring2.R',
|
'ring_2_r': 'Ring_R.001',
|
||||||
'ring_3_r': 'Ring3.R',
|
'ring_3_r': 'Ring_R.002',
|
||||||
'pinkie_1_r': 'Pinky1.R',
|
'pinkie_1_r': 'Pinky_R',
|
||||||
'pinkie_2_r': 'Pinky2.R',
|
'pinkie_2_r': 'Pinky_R.001',
|
||||||
'pinkie_3_r': 'Pinky3.R',
|
'pinkie_3_r': 'Pinky_R.002',
|
||||||
|
|
||||||
# Eyes
|
# Eyes
|
||||||
'left_eye': 'Eye.L',
|
'left_eye': 'Eye_L',
|
||||||
'right_eye': 'Eye.R'
|
'right_eye': 'Eye_R',
|
||||||
|
|
||||||
|
# Breast bones
|
||||||
|
'breast_1_l': 'Breast1_L',
|
||||||
|
'breast_2_l': 'Breast2_L',
|
||||||
|
'breast_3_l': 'Breast3_L',
|
||||||
|
'breast_1_r': 'Breast1_R',
|
||||||
|
'breast_2_r': 'Breast2_R',
|
||||||
|
'breast_3_r': 'Breast3_R'
|
||||||
}
|
}
|
||||||
|
|
||||||
bone_hierarchy = [
|
bone_hierarchy = [
|
||||||
('Hips', 'Spine'),
|
('Hips', 'Spine'),
|
||||||
('Spine', 'Chest'),
|
('Spine', 'Chest'),
|
||||||
('Chest', 'Chest.Up'),
|
('Chest', 'UpperChest'),
|
||||||
('Chest.Up', 'Neck'),
|
('UpperChest', 'Neck'),
|
||||||
('Neck', 'Head'),
|
('Neck', 'Head'),
|
||||||
('Head', 'Eye.L'),
|
('Head', 'Eye_L'),
|
||||||
('Head', 'Eye.R'),
|
('Head', 'Eye_R'),
|
||||||
|
|
||||||
# Left Arm Chain
|
# Left Arm Chain
|
||||||
('Chest.Up', 'UpperArm.L'),
|
('UpperChest', 'Shoulder_L'),
|
||||||
('UpperArm.L', 'LowerArm.L'),
|
('Shoulder_L', 'UpperArm_L'),
|
||||||
('LowerArm.L', 'Hand.L'),
|
('UpperArm_L', 'LowerArm_L'),
|
||||||
|
('LowerArm_L', 'Hand_L'),
|
||||||
|
|
||||||
# Right Arm Chain
|
# Right Arm Chain
|
||||||
('Chest.Up', 'UpperArm.R'),
|
('UpperChest', 'Shoulder_R'),
|
||||||
('UpperArm.R', 'LowerArm.R'),
|
('Shoulder_R', 'UpperArm_R'),
|
||||||
('LowerArm.R', 'Hand.R'),
|
('UpperArm_R', 'LowerArm_R'),
|
||||||
|
('LowerArm_R', 'Hand_R'),
|
||||||
|
|
||||||
# Left Leg Chain
|
# Left Leg Chain
|
||||||
('Hips', 'UpperLeg.L'),
|
('Hips', 'UpperLeg_L'),
|
||||||
('UpperLeg.L', 'LowerLeg.L'),
|
('UpperLeg_L', 'LowerLeg_L'),
|
||||||
('LowerLeg.L', 'Foot.L'),
|
('LowerLeg_L', 'Foot_L'),
|
||||||
('Foot.L', 'Toes.L'),
|
('Foot_L', 'Toe_L'),
|
||||||
|
|
||||||
# Right Leg Chain
|
# Right Leg Chain
|
||||||
('Hips', 'UpperLeg.R'),
|
('Hips', 'UpperLeg_R'),
|
||||||
('UpperLeg.R', 'LowerLeg.R'),
|
('UpperLeg_R', 'LowerLeg_R'),
|
||||||
('LowerLeg.R', 'Foot.R'),
|
('LowerLeg_R', 'Foot_R'),
|
||||||
('Foot.R', 'Toes.R')
|
('Foot_R', 'Toe_R')
|
||||||
]
|
]
|
||||||
|
|
||||||
finger_hierarchy = {
|
finger_hierarchy = {
|
||||||
'left': [
|
'left': [
|
||||||
('Hand.L', 'Thumb1.L', 'Thumb2.L', 'Thumb3.L'),
|
('Hand_L', 'Thumb_L', 'Thumb_L.001', 'Thumb_L.002'),
|
||||||
('Hand.L', 'Index1.L', 'Index2.L', 'Index3.L'),
|
('Hand_L', 'Index_L', 'Index_L.001', 'Index_L.002'),
|
||||||
('Hand.L', 'Middle1.L', 'Middle2.L', 'Middle3.L'),
|
('Hand_L', 'Middle_L', 'Middle_L.001', 'Middle_L.002'),
|
||||||
('Hand.L', 'Ring1.L', 'Ring2.L', 'Ring3.L'),
|
('Hand_L', 'Ring_L', 'Ring_L.001', 'Ring_L.002'),
|
||||||
('Hand.L', 'Pinky1.L', 'Pinky2.L', 'Pinky3.L')
|
('Hand_L', 'Pinky_L', 'Pinky_L.001', 'Pinky_L.002')
|
||||||
],
|
],
|
||||||
'right': [
|
'right': [
|
||||||
('Hand.R', 'Thumb1.R', 'Thumb2.R', 'Thumb3.R'),
|
('Hand_R', 'Thumb_R', 'Thumb_R.001', 'Thumb_R.002'),
|
||||||
('Hand.R', 'Index1.R', 'Index2.R', 'Index3.R'),
|
('Hand_R', 'Index_R', 'Index_R.001', 'Index_R.002'),
|
||||||
('Hand.R', 'Middle1.R', 'Middle2.R', 'Middle3.R'),
|
('Hand_R', 'Middle_R', 'Middle_R.001', 'Middle_R.002'),
|
||||||
('Hand.R', 'Ring1.R', 'Ring2.R', 'Ring3.R'),
|
('Hand_R', 'Ring_R', 'Ring_R.001', 'Ring_R.002'),
|
||||||
('Hand.R', 'Pinky1.R', 'Pinky2.R', 'Pinky3.R')
|
('Hand_R', 'Pinky_R', 'Pinky_R.001', 'Pinky_R.002')
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,6 +603,8 @@ acceptable_bone_hierarchy = [
|
|||||||
('Head', 'Eye_R'),
|
('Head', 'Eye_R'),
|
||||||
('Head', 'LeftEye'),
|
('Head', 'LeftEye'),
|
||||||
('Head', 'RightEye'),
|
('Head', 'RightEye'),
|
||||||
|
('Head', 'Eye.L'),
|
||||||
|
('Head', 'Eye.R'),
|
||||||
|
|
||||||
# Unity humanoid naming
|
# Unity humanoid naming
|
||||||
('Hips', 'Spine'),
|
('Hips', 'Spine'),
|
||||||
@@ -516,6 +615,40 @@ acceptable_bone_hierarchy = [
|
|||||||
('Head', 'LeftEye'),
|
('Head', 'LeftEye'),
|
||||||
('Head', 'RightEye'),
|
('Head', 'RightEye'),
|
||||||
|
|
||||||
|
# Old standard bone hierarchy patterns
|
||||||
|
('UpperChest', 'UpperArm.L'),
|
||||||
|
('UpperArm.L', 'LowerArm.L'),
|
||||||
|
('LowerArm.L', 'Hand.L'),
|
||||||
|
('UpperChest', 'UpperArm.R'),
|
||||||
|
('UpperArm.R', 'LowerArm.R'),
|
||||||
|
('LowerArm.R', 'Hand.R'),
|
||||||
|
('Hips', 'UpperLeg.L'),
|
||||||
|
('UpperLeg.L', 'LowerLeg.L'),
|
||||||
|
('LowerLeg.L', 'Foot.L'),
|
||||||
|
('Foot.L', 'Toes.L'),
|
||||||
|
('Hips', 'UpperLeg.R'),
|
||||||
|
('UpperLeg.R', 'LowerLeg.R'),
|
||||||
|
('LowerLeg.R', 'Foot.R'),
|
||||||
|
('Foot.R', 'Toes.R'),
|
||||||
|
|
||||||
|
# New standard bone hierarchy patterns (with shoulders)
|
||||||
|
('UpperChest', 'Shoulder_L'),
|
||||||
|
('Shoulder_L', 'UpperArm_L'),
|
||||||
|
('UpperArm_L', 'LowerArm_L'),
|
||||||
|
('LowerArm_L', 'Hand_L'),
|
||||||
|
('UpperChest', 'Shoulder_R'),
|
||||||
|
('Shoulder_R', 'UpperArm_R'),
|
||||||
|
('UpperArm_R', 'LowerArm_R'),
|
||||||
|
('LowerArm_R', 'Hand_R'),
|
||||||
|
('Hips', 'UpperLeg_L'),
|
||||||
|
('UpperLeg_L', 'LowerLeg_L'),
|
||||||
|
('LowerLeg_L', 'Foot_L'),
|
||||||
|
('Foot_L', 'Toe_L'),
|
||||||
|
('Hips', 'UpperLeg_R'),
|
||||||
|
('UpperLeg_R', 'LowerLeg_R'),
|
||||||
|
('LowerLeg_R', 'Foot_R'),
|
||||||
|
('Foot_R', 'Toe_R'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
acceptable_bone_names = {
|
acceptable_bone_names = {
|
||||||
@@ -523,59 +656,75 @@ acceptable_bone_names = {
|
|||||||
'chest': ['Chest', 'spine1', 'Spine1', 'spine_01', 'SPINE1', 'Spine01'],
|
'chest': ['Chest', 'spine1', 'Spine1', 'spine_01', 'SPINE1', 'Spine01'],
|
||||||
'neck': ['Neck', 'neck_01', 'Neck01'],
|
'neck': ['Neck', 'neck_01', 'Neck01'],
|
||||||
'head': ['Head', 'head_01', 'Head01'],
|
'head': ['Head', 'head_01', 'Head01'],
|
||||||
'eye_l': ['Eye_L', 'LeftEye', 'lefteye', 'eye_left', 'EyeLeft'],
|
'eye_l': ['Eye_L', 'LeftEye', 'lefteye', 'eye_left', 'EyeLeft', 'Eye.L'],
|
||||||
'eye_r': ['Eye_R', 'RightEye', 'righteye', 'eye_right', 'EyeRight'],
|
'eye_r': ['Eye_R', 'RightEye', 'righteye', 'eye_right', 'EyeRight', 'Eye.R'],
|
||||||
|
|
||||||
'shoulder_r': ['Shoulder.R', 'clavicle_r', 'ClavicleRight', 'RightShoulder'],
|
'shoulder_r': ['Shoulder.R', 'clavicle_r', 'ClavicleRight', 'RightShoulder', 'Shoulder_R'],
|
||||||
'arm_r': ['Arm.R', 'upperarm_r', 'UpperArmRight', 'RightArm'],
|
'arm_r': ['Arm.R', 'upperarm_r', 'UpperArmRight', 'RightArm', 'UpperArm.R', 'UpperArm_R'],
|
||||||
'elbow_r': ['Elbow.R', 'lowerarm_r', 'ForearmRight', 'RightForeArm'],
|
'elbow_r': ['Elbow.R', 'lowerarm_r', 'ForearmRight', 'RightForeArm', 'LowerArm.R', 'LowerArm_R'],
|
||||||
'wrist_r': ['Wrist.R', 'hand_r', 'HandRight', 'RightHand'],
|
'wrist_r': ['Wrist.R', 'hand_r', 'HandRight', 'RightHand', 'Hand.R', 'Hand_R'],
|
||||||
'leg_r': ['Leg.R', 'thigh_r', 'ThighRight', 'RightLeg', 'RightUpLeg'],
|
'leg_r': ['Leg.R', 'thigh_r', 'ThighRight', 'RightLeg', 'RightUpLeg', 'UpperLeg.R', 'UpperLeg_R'],
|
||||||
'knee_r': ['Knee.R', 'calf_r', 'CalfRight', 'RightShin', 'RightLowerLeg'],
|
'knee_r': ['Knee.R', 'calf_r', 'CalfRight', 'RightShin', 'RightLowerLeg', 'LowerLeg.R', 'LowerLeg_R'],
|
||||||
'foot_r': ['Foot.R', 'foot_r', 'FootRight', 'RightFoot'],
|
'foot_r': ['Foot.R', 'foot_r', 'FootRight', 'RightFoot', 'Foot_R'],
|
||||||
'toes_r': ['Toes.R', 'ball_r', 'ToeRight', 'RightToeBase'],
|
'toes_r': ['Toes.R', 'ball_r', 'ToeRight', 'RightToeBase', 'Toe_R'],
|
||||||
|
|
||||||
'shoulder_l': ['Shoulder.L', 'clavicle_l', 'ClavicleLeft', 'LeftShoulder'],
|
'shoulder_l': ['Shoulder.L', 'clavicle_l', 'ClavicleLeft', 'LeftShoulder', 'Shoulder_L'],
|
||||||
'arm_l': ['Arm.L', 'upperarm_l', 'UpperArmLeft', 'LeftArm'],
|
'arm_l': ['Arm.L', 'upperarm_l', 'UpperArmLeft', 'LeftArm', 'UpperArm.L', 'UpperArm_L'],
|
||||||
'elbow_l': ['Elbow.L', 'lowerarm_l', 'ForearmLeft', 'LeftForeArm'],
|
'elbow_l': ['Elbow.L', 'lowerarm_l', 'ForearmLeft', 'LeftForeArm', 'LowerArm.L', 'LowerArm_L'],
|
||||||
'wrist_l': ['Wrist.L', 'hand_l', 'HandLeft', 'LeftHand'],
|
'wrist_l': ['Wrist.L', 'hand_l', 'HandLeft', 'LeftHand', 'Hand.L', 'Hand_L'],
|
||||||
'leg_l': ['Leg.L', 'thigh_l', 'ThighLeft', 'LeftLeg', 'LeftUpLeg'],
|
'leg_l': ['Leg.L', 'thigh_l', 'ThighLeft', 'LeftLeg', 'LeftUpLeg', 'UpperLeg.L', 'UpperLeg_L'],
|
||||||
'knee_l': ['Knee.L', 'calf_l', 'CalfLeft', 'LeftShin', 'LeftLowerLeg'],
|
'knee_l': ['Knee.L', 'calf_l', 'CalfLeft', 'LeftShin', 'LeftLowerLeg', 'LowerLeg.L', 'LowerLeg_L'],
|
||||||
'foot_l': ['Foot.L', 'foot_l', 'FootLeft', 'LeftFoot'],
|
'foot_l': ['Foot.L', 'foot_l', 'FootLeft', 'LeftFoot', 'Foot_L'],
|
||||||
'toes_l': ['Toes.L', 'ball_l', 'ToeLeft', 'LeftToeBase'],
|
'toes_l': ['Toes.L', 'ball_l', 'ToeLeft', 'LeftToeBase', 'Toe_L'],
|
||||||
|
|
||||||
# Add finger bones for left hand
|
# Add finger bones for left hand
|
||||||
'thumb_0_l': ['Thumb0_L'],
|
'thumb_0_l': ['Thumb0_L', 'Thumb0.L'],
|
||||||
'thumb_1_l': ['Thumb1_L'],
|
'thumb_1_l': ['Thumb1_L', 'Thumb1.L', 'Thumb_L'],
|
||||||
'thumb_2_l': ['Thumb2_L'],
|
'thumb_2_l': ['Thumb2_L', 'Thumb2.L', 'Thumb_L.001'],
|
||||||
'index_1_l': ['IndexFinger1_L'],
|
'thumb_3_l': ['Thumb3_L', 'Thumb3.L', 'Thumb_L.002'],
|
||||||
'index_2_l': ['IndexFinger2_L'],
|
'index_1_l': ['IndexFinger1_L', 'IndexFinger1.L', 'Index1.L', 'Index_L'],
|
||||||
'index_3_l': ['IndexFinger3_L'],
|
'index_2_l': ['IndexFinger2_L', 'IndexFinger2.L', 'Index2.L', 'Index_L.001'],
|
||||||
'middle_1_l': ['MiddleFinger1_L'],
|
'index_3_l': ['IndexFinger3_L', 'IndexFinger3.L', 'Index3.L', 'Index_L.002'],
|
||||||
'middle_2_l': ['MiddleFinger2_L'],
|
'middle_1_l': ['MiddleFinger1_L', 'MiddleFinger1.L', 'Middle1.L', 'Middle_L'],
|
||||||
'middle_3_l': ['MiddleFinger3_L'],
|
'middle_2_l': ['MiddleFinger2_L', 'MiddleFinger2.L', 'Middle2.L', 'Middle_L.001'],
|
||||||
'ring_1_l': ['RingFinger1_L'],
|
'middle_3_l': ['MiddleFinger3_L', 'MiddleFinger3.L', 'Middle3.L', 'Middle_L.002'],
|
||||||
'ring_2_l': ['RingFinger2_L'],
|
'ring_1_l': ['RingFinger1_L', 'RingFinger1.L', 'Ring1.L', 'Ring_L'],
|
||||||
'ring_3_l': ['RingFinger3_L'],
|
'ring_2_l': ['RingFinger2_L', 'RingFinger2.L', 'Ring2.L', 'Ring_L.001'],
|
||||||
|
'ring_3_l': ['RingFinger3_L', 'RingFinger3.L', 'Ring3.L', 'Ring_L.002'],
|
||||||
|
'pinky_1_l': ['Pinky1_L', 'Pinky1.L', 'Pinky_L'],
|
||||||
|
'pinky_2_l': ['Pinky2_L', 'Pinky2.L', 'Pinky_L.001'],
|
||||||
|
'pinky_3_l': ['Pinky3_L', 'Pinky3.L', 'Pinky_L.002'],
|
||||||
|
|
||||||
# Add finger bones for right hand
|
# Add finger bones for right hand
|
||||||
'thumb_0_r': ['Thumb0_R', 'ThumbO_R'],
|
'thumb_0_r': ['Thumb0_R', 'Thumb0.R', 'ThumbO_R'],
|
||||||
'thumb_1_r': ['Thumb1_R'],
|
'thumb_1_r': ['Thumb1_R', 'Thumb1.R', 'Thumb_R'],
|
||||||
'thumb_2_r': ['Thumb2_R'],
|
'thumb_2_r': ['Thumb2_R', 'Thumb2.R', 'Thumb_R.001'],
|
||||||
'index_1_r': ['IndexFinger1_R'],
|
'thumb_3_r': ['Thumb3_R', 'Thumb3.R', 'Thumb_R.002'],
|
||||||
'index_2_r': ['IndexFinger2_R'],
|
'index_1_r': ['IndexFinger1_R', 'IndexFinger1.R', 'Index1.R', 'Index_R'],
|
||||||
'index_3_r': ['IndexFinger3_R'],
|
'index_2_r': ['IndexFinger2_R', 'IndexFinger2.R', 'Index2.R', 'Index_R.001'],
|
||||||
'middle_1_r': ['MiddleFinger1_R'],
|
'index_3_r': ['IndexFinger3_R', 'IndexFinger3.R', 'Index3.R', 'Index_R.002'],
|
||||||
'middle_2_r': ['MiddleFinger2_R'],
|
'middle_1_r': ['MiddleFinger1_R', 'MiddleFinger1.R', 'Middle1.R', 'Middle_R'],
|
||||||
'middle_3_r': ['MiddleFinger3_R'],
|
'middle_2_r': ['MiddleFinger2_R', 'MiddleFinger2.R', 'Middle2.R', 'Middle_R.001'],
|
||||||
'ring_1_r': ['RingFinger1_R'],
|
'middle_3_r': ['MiddleFinger3_R', 'MiddleFinger3.R', 'Middle3.R', 'Middle_R.002'],
|
||||||
'ring_2_r': ['RingFinger2_R'],
|
'ring_1_r': ['RingFinger1_R', 'RingFinger1.R', 'Ring1.R', 'Ring_R'],
|
||||||
'ring_3_r': ['RingFinger3_R'],
|
'ring_2_r': ['RingFinger2_R', 'RingFinger2.R', 'Ring2.R', 'Ring_R.001'],
|
||||||
|
'ring_3_r': ['RingFinger3_R', 'RingFinger3.R', 'Ring3.R', 'Ring_R.002'],
|
||||||
|
'pinky_1_r': ['Pinky1_R', 'Pinky1.R', 'Pinky_R'],
|
||||||
|
'pinky_2_r': ['Pinky2_R', 'Pinky2.R', 'Pinky_R.001'],
|
||||||
|
'pinky_3_r': ['Pinky3_R', 'Pinky3.R', 'Pinky_R.002'],
|
||||||
|
|
||||||
'breast_upper_1_l': ['BreastUpper1_L'],
|
'breast_upper_1_l': ['BreastUpper1_L', 'BreastUpper1.L'],
|
||||||
'breast_upper_2_l': ['BreastUpper2_L'],
|
'breast_upper_2_l': ['BreastUpper2_L', 'BreastUpper2.L'],
|
||||||
'breast_upper_1_r': ['BreastUpper1_R'],
|
'breast_upper_1_r': ['BreastUpper1_R', 'BreastUpper1.R'],
|
||||||
'breast_upper_2_r': ['BreastUpper2_R'],
|
'breast_upper_2_r': ['BreastUpper2_R', 'BreastUpper2.R'],
|
||||||
|
|
||||||
|
# Little finger bones
|
||||||
|
'little_finger_1_l': ['LittleFinger1_L', 'LittleFinger1.L'],
|
||||||
|
'little_finger_2_l': ['LittleFinger2_L', 'LittleFinger2.L'],
|
||||||
|
'little_finger_3_l': ['LittleFinger3_L', 'LittleFinger3.L'],
|
||||||
|
'little_finger_1_r': ['LittleFinger1_R', 'LittleFinger1.R'],
|
||||||
|
'little_finger_2_r': ['LittleFinger2_R', 'LittleFinger2.R'],
|
||||||
|
'little_finger_3_r': ['LittleFinger3_R', 'LittleFinger3.R'],
|
||||||
|
|
||||||
'ear_upper_l': ['UpperEar.L', 'Upper Ear.L', 'Upper Ear_L'],
|
'ear_upper_l': ['UpperEar.L', 'Upper Ear.L', 'Upper Ear_L'],
|
||||||
'ear_upper_r': ['UpperEar.R', 'Upper Ear.R', 'Upper Ear_R'],
|
'ear_upper_r': ['UpperEar.R', 'Upper Ear.R', 'Upper Ear_R'],
|
||||||
@@ -695,17 +844,17 @@ non_standard_mappings = {
|
|||||||
'left_arm': [
|
'left_arm': [
|
||||||
'mixamorig:LeftArm', 'mixamorig_LeftArm',
|
'mixamorig:LeftArm', 'mixamorig_LeftArm',
|
||||||
'ORG-upper_arm.L', 'upper_arm.L',
|
'ORG-upper_arm.L', 'upper_arm.L',
|
||||||
'lShldrBend', 'lShldrTwist', 'lArm'
|
'lShldrBend', 'lShldrTwist', 'lArm', 'UpperArm.L'
|
||||||
],
|
],
|
||||||
'left_elbow': [
|
'left_elbow': [
|
||||||
'mixamorig:LeftForeArm', 'mixamorig_LeftForeArm',
|
'mixamorig:LeftForeArm', 'mixamorig_LeftForeArm',
|
||||||
'ORG-forearm.L', 'forearm.L',
|
'ORG-forearm.L', 'forearm.L',
|
||||||
'lForearmBend', 'lElbow', 'lForeArm'
|
'lForearmBend', 'lElbow', 'lForeArm', 'LowerArm.L'
|
||||||
],
|
],
|
||||||
'left_wrist': [
|
'left_wrist': [
|
||||||
'mixamorig:LeftHand', 'mixamorig_LeftHand',
|
'mixamorig:LeftHand', 'mixamorig_LeftHand',
|
||||||
'ORG-hand.L', 'hand.L',
|
'ORG-hand.L', 'hand.L',
|
||||||
'lHand', 'lWrist'
|
'lHand', 'lWrist', 'Hand.L'
|
||||||
],
|
],
|
||||||
|
|
||||||
'right_shoulder': [
|
'right_shoulder': [
|
||||||
@@ -716,59 +865,61 @@ non_standard_mappings = {
|
|||||||
'right_arm': [
|
'right_arm': [
|
||||||
'mixamorig:RightArm', 'mixamorig_RightArm',
|
'mixamorig:RightArm', 'mixamorig_RightArm',
|
||||||
'ORG-upper_arm.R', 'upper_arm.R',
|
'ORG-upper_arm.R', 'upper_arm.R',
|
||||||
'rShldrBend', 'rShldrTwist', 'rArm'
|
'rShldrBend', 'rShldrTwist', 'rArm', 'UpperArm.R'
|
||||||
],
|
],
|
||||||
'right_elbow': [
|
'right_elbow': [
|
||||||
'mixamorig:RightForeArm', 'mixamorig_RightForeArm',
|
'mixamorig:RightForeArm', 'mixamorig_RightForeArm',
|
||||||
'ORG-forearm.R', 'forearm.R',
|
'ORG-forearm.R', 'forearm.R',
|
||||||
'rForearmBend', 'rElbow', 'rForeArm'
|
'rForearmBend', 'rElbow', 'rForeArm', 'LowerArm.R'
|
||||||
],
|
],
|
||||||
'right_wrist': [
|
'right_wrist': [
|
||||||
'mixamorig:RightHand', 'mixamorig_RightHand',
|
'mixamorig:RightHand', 'mixamorig_RightHand',
|
||||||
'ORG-hand.R', 'hand.R',
|
'ORG-hand.R', 'hand.R',
|
||||||
'rHand', 'rWrist'
|
'rHand', 'rWrist', 'Hand.R'
|
||||||
],
|
],
|
||||||
|
|
||||||
'left_leg': [
|
'left_leg': [
|
||||||
'mixamorig:LeftUpLeg', 'mixamorig_LeftUpLeg',
|
'mixamorig:LeftUpLeg', 'mixamorig_LeftUpLeg',
|
||||||
'ORG-thigh.L', 'thigh.L',
|
'ORG-thigh.L', 'thigh.L',
|
||||||
'lThighBend', 'lThigh'
|
'lThighBend', 'lThigh', 'UpperLeg.L',
|
||||||
|
'LeftUpperLeg'
|
||||||
],
|
],
|
||||||
'left_knee': [
|
'left_knee': [
|
||||||
'mixamorig:LeftLeg', 'mixamorig_LeftLeg',
|
'mixamorig:LeftLeg', 'mixamorig_LeftLeg',
|
||||||
'ORG-shin.L', 'shin.L',
|
'ORG-shin.L', 'shin.L',
|
||||||
'lShin', 'lKnee', 'lLeg'
|
'lShin', 'lKnee', 'lLeg', 'LowerLeg.L'
|
||||||
],
|
],
|
||||||
'left_ankle': [
|
'left_ankle': [
|
||||||
'mixamorig:LeftFoot', 'mixamorig_LeftFoot',
|
'mixamorig:LeftFoot', 'mixamorig_LeftFoot',
|
||||||
'ORG-foot.L', 'foot.L',
|
'ORG-foot.L', 'foot.L',
|
||||||
'lFoot', 'lAnkle'
|
'lFoot', 'lAnkle', 'Foot.L'
|
||||||
],
|
],
|
||||||
'left_toe': [
|
'left_toe': [
|
||||||
'mixamorig:LeftToeBase', 'mixamorig_LeftToeBase',
|
'mixamorig:LeftToeBase', 'mixamorig_LeftToeBase',
|
||||||
'ORG-toe.L', 'toe.L',
|
'ORG-toe.L', 'toe.L',
|
||||||
'lToe'
|
'lToe', 'Toes.L', 'LeftToeBase'
|
||||||
],
|
],
|
||||||
|
|
||||||
'right_leg': [
|
'right_leg': [
|
||||||
'mixamorig:RightUpLeg', 'mixamorig_RightUpLeg',
|
'mixamorig:RightUpLeg', 'mixamorig_RightUpLeg',
|
||||||
'ORG-thigh.R', 'thigh.R',
|
'ORG-thigh.R', 'thigh.R',
|
||||||
'rThighBend', 'rThigh'
|
'rThighBend', 'rThigh', 'UpperLeg.R',
|
||||||
|
'RightUpperLeg'
|
||||||
],
|
],
|
||||||
'right_knee': [
|
'right_knee': [
|
||||||
'mixamorig:RightLeg', 'mixamorig_RightLeg',
|
'mixamorig:RightLeg', 'mixamorig_RightLeg',
|
||||||
'ORG-shin.R', 'shin.R',
|
'ORG-shin.R', 'shin.R',
|
||||||
'rShin', 'rKnee', 'rLeg'
|
'rShin', 'rKnee', 'rLeg', 'LowerLeg.R'
|
||||||
],
|
],
|
||||||
'right_ankle': [
|
'right_ankle': [
|
||||||
'mixamorig:RightFoot', 'mixamorig_RightFoot',
|
'mixamorig:RightFoot', 'mixamorig_RightFoot',
|
||||||
'ORG-foot.R', 'foot.R',
|
'ORG-foot.R', 'foot.R',
|
||||||
'rFoot', 'rAnkle'
|
'rFoot', 'rAnkle', 'Foot.R'
|
||||||
],
|
],
|
||||||
'right_toe': [
|
'right_toe': [
|
||||||
'mixamorig:RightToeBase', 'mixamorig_RightToeBase',
|
'mixamorig:RightToeBase', 'mixamorig_RightToeBase',
|
||||||
'ORG-toe.R', 'toe.R',
|
'ORG-toe.R', 'toe.R',
|
||||||
'rToe'
|
'rToe', 'Toes.R', 'RightToeBase'
|
||||||
],
|
],
|
||||||
|
|
||||||
'thumb_1_l': [
|
'thumb_1_l': [
|
||||||
@@ -934,12 +1085,12 @@ non_standard_mappings = {
|
|||||||
'left_eye': [
|
'left_eye': [
|
||||||
'mixamorig:LeftEye', 'mixamorig_LeftEye',
|
'mixamorig:LeftEye', 'mixamorig_LeftEye',
|
||||||
'ORG-eye.L', 'eye.L',
|
'ORG-eye.L', 'eye.L',
|
||||||
'lEye'
|
'lEye', 'Eye.L'
|
||||||
],
|
],
|
||||||
'right_eye': [
|
'right_eye': [
|
||||||
'mixamorig:RightEye', 'mixamorig_RightEye',
|
'mixamorig:RightEye', 'mixamorig_RightEye',
|
||||||
'ORG-eye.R', 'eye.R',
|
'ORG-eye.R', 'eye.R',
|
||||||
'rEye'
|
'rEye', 'Eye.R'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,496 @@
|
|||||||
|
# GPL License
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional, Set, Tuple
|
||||||
|
from .dictionaries import bone_names, reverse_bone_lookup, simplify_bonename
|
||||||
|
from .logging_setup import logger
|
||||||
|
|
||||||
|
# Enhanced dictionaries for comprehensive translation support
|
||||||
|
|
||||||
|
# Shapekey/Morph name translations (Japanese to English)
|
||||||
|
shapekey_names: Dict[str, List[str]] = {
|
||||||
|
# Basic facial expressions
|
||||||
|
"neutral": ["ニュートラル", "中立", "通常", "普通", "デフォルト", "basis"],
|
||||||
|
"smile": ["笑顔", "スマイル", "えがお", "笑い", "にこり", "ほほえみ", "smile", "happy"],
|
||||||
|
"angry": ["怒り", "怒る", "アングリー", "いかり", "おこり", "むかつき", "angry", "mad"],
|
||||||
|
"sad": ["悲しい", "かなしい", "悲哀", "サッド", "sad", "sorrow"],
|
||||||
|
"surprised": ["驚き", "びっくり", "おどろき", "サプライズ", "surprised", "shock"],
|
||||||
|
"disgusted": ["嫌悪", "いやがり", "きもち悪い", "disgusted"],
|
||||||
|
"fearful": ["恐怖", "怖い", "こわい", "恐れ", "fearful", "scared"],
|
||||||
|
"blink": ["瞬き", "まばたき", "ブリンク", "目閉じ", "blink", "eyeclose"],
|
||||||
|
"wink_left": ["ウィンク左", "左目ウィンク", "ひだりめうぃんく", "winkleft", "wink_l"],
|
||||||
|
"wink_right": ["ウィンク右", "右目ウィンク", "みぎめうぃんく", "winkright", "wink_r"],
|
||||||
|
"eye_close": ["目閉じ", "目を閉じる", "めとじ", "eyeclose", "closedeyes"],
|
||||||
|
"eye_wide": ["目見開き", "目を見開く", "びっくり目", "eyewide", "wideeyes"],
|
||||||
|
"eye_narrow": ["細目", "目細め", "ほそめ", "eyenarrow", "narroweyes"],
|
||||||
|
"mouth_open": ["口開け", "口を開ける", "くちあけ", "mouthopen", "openmouth"],
|
||||||
|
"mouth_smile": ["口角上げ", "口笑顔", "くちえがお", "mouthsmile"],
|
||||||
|
"mouth_frown": ["口角下げ", "への字口", "くちしかめ", "mouthfrown"],
|
||||||
|
"mouth_pout": ["すぼめ口", "とがらせ口", "mouthpout"],
|
||||||
|
"eyebrow_up": ["眉上げ", "眉毛上げ", "まゆあげ", "eyebrowup", "raiseeyebrow"],
|
||||||
|
"eyebrow_down": ["眉下げ", "眉寄せ", "まゆさげ", "eyebrowdown", "lowereyebrow"],
|
||||||
|
"eyebrow_angry": ["怒り眉", "眉怒り", "まゆいかり", "angrybrow"],
|
||||||
|
"cheek_puff": ["頬膨らまし", "ほほふくらまし", "cheekpuff"],
|
||||||
|
"cheek_suck": ["頬すぼめ", "ほほすぼめ", "cheeksuck"],
|
||||||
|
"joy": ["喜び", "よろこび", "ジョイ", "joy", "happiness"],
|
||||||
|
"contempt": ["軽蔑", "けいべつ", "contempt"],
|
||||||
|
"confusion": ["困惑", "こんわく", "confusion", "confused"],
|
||||||
|
"concentration": ["集中", "しゅうちゅう", "concentration", "focused"],
|
||||||
|
|
||||||
|
# VRC Visemes
|
||||||
|
"viseme_sil": ["無音", "むおん", "サイレンス", "silence", "sil"],
|
||||||
|
"viseme_aa": ["あ", "aa", "mouth_a"],
|
||||||
|
"viseme_ih": ["い", "ih", "mouth_i"],
|
||||||
|
"viseme_ou": ["う", "ou", "mouth_u"],
|
||||||
|
"viseme_e": ["え", "e", "mouth_e"],
|
||||||
|
"viseme_oh": ["お", "oh", "mouth_o"],
|
||||||
|
"viseme_ch": ["ち", "ch"],
|
||||||
|
"viseme_dd": ["だ", "dd"],
|
||||||
|
"viseme_ff": ["ふ", "ff"],
|
||||||
|
"viseme_kk": ["か", "kk"],
|
||||||
|
"viseme_nn": ["ん", "nn"],
|
||||||
|
"viseme_pp": ["ぱ", "pp"],
|
||||||
|
"viseme_rr": ["ら", "rr"],
|
||||||
|
"viseme_ss": ["さ", "ss"],
|
||||||
|
"viseme_th": ["た", "th"],
|
||||||
|
|
||||||
|
"basis": ["基本", "きほん", "ベース", "base", "basis", "default"],
|
||||||
|
"reset": ["リセット", "初期化", "しょきか", "reset", "clear"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Material name translations (Japanese to English)
|
||||||
|
material_names: Dict[str, List[str]] = {
|
||||||
|
# Basic materials
|
||||||
|
"skin": ["肌", "はだ", "皮膚", "ひふ", "スキン", "skin", "flesh"],
|
||||||
|
"hair": ["髪", "かみ", "毛髪", "もうはつ", "ヘア", "hair"],
|
||||||
|
"eyes": ["目", "め", "眼", "がん", "アイ", "eye", "iris"],
|
||||||
|
"eyebrow": ["眉", "まゆ", "眉毛", "まゆげ", "eyebrow", "brow"],
|
||||||
|
"eyelash": ["まつ毛", "まつげ", "睫毛", "eyelash", "lash"],
|
||||||
|
"teeth": ["歯", "は", "歯列", "しれつ", "tooth", "teeth"],
|
||||||
|
"tongue": ["舌", "した", "tongue"],
|
||||||
|
"nails": ["爪", "つめ", "nail", "nails"],
|
||||||
|
"shirt": ["シャツ", "上着", "うわぎ", "shirt", "top"],
|
||||||
|
"pants": ["パンツ", "ズボン", "下着", "したぎ", "pants", "trousers"],
|
||||||
|
"skirt": ["スカート", "skirt"],
|
||||||
|
"dress": ["ドレス", "ワンピース", "dress"],
|
||||||
|
"shoes": ["靴", "くつ", "シューズ", "shoe", "shoes"],
|
||||||
|
"socks": ["靴下", "くつした", "ソックス", "sock", "socks"],
|
||||||
|
"gloves": ["手袋", "てぶくろ", "グローブ", "glove", "gloves"],
|
||||||
|
"hat": ["帽子", "ぼうし", "ハット", "hat", "cap"],
|
||||||
|
"jacket": ["ジャケット", "上着", "うわぎ", "jacket", "coat"],
|
||||||
|
"underwear": ["下着", "したぎ", "パンティー", "underwear", "panties"],
|
||||||
|
"bra": ["ブラ", "ブラジャー", "胸当て", "bra", "brassiere"],
|
||||||
|
"glasses": ["眼鏡", "めがね", "メガネ", "glasses", "spectacles"],
|
||||||
|
"earring": ["イヤリング", "耳飾り", "みみかざり", "earring"],
|
||||||
|
"necklace": ["ネックレス", "首飾り", "くびかざり", "necklace"],
|
||||||
|
"bracelet": ["ブレスレット", "腕輪", "うでわ", "bracelet"],
|
||||||
|
"ring": ["指輪", "ゆびわ", "リング", "ring"],
|
||||||
|
"watch": ["時計", "とけい", "ウォッチ", "watch"],
|
||||||
|
"bag": ["鞄", "かばん", "バッグ", "bag", "purse"],
|
||||||
|
"belt": ["ベルト", "帯", "おび", "belt"],
|
||||||
|
"transparent": ["透明", "とうめい", "クリア", "transparent", "clear"],
|
||||||
|
"metal": ["金属", "きんぞく", "メタル", "metal"],
|
||||||
|
"fabric": ["布", "ぬの", "生地", "きじ", "fabric", "cloth"],
|
||||||
|
"leather": ["革", "かわ", "皮", "ひ", "レザー", "leather"],
|
||||||
|
"plastic": ["プラスチック", "プラ", "plastic"],
|
||||||
|
"glass": ["ガラス", "硝子", "glass"],
|
||||||
|
"rubber": ["ゴム", "ラバー", "rubber"],
|
||||||
|
"wood": ["木", "き", "木材", "もくざい", "wood", "wooden"],
|
||||||
|
"diffuse": ["ディフューズ", "基本色", "きほんしょく", "diffuse", "albedo"],
|
||||||
|
"normal": ["ノーマル", "法線", "ほうせん", "normal", "bump"],
|
||||||
|
"specular": ["スペキュラー", "反射", "はんしゃ", "specular", "reflection"],
|
||||||
|
"emission": ["発光", "はっこう", "エミッション", "emission", "glow"],
|
||||||
|
"roughness": ["粗さ", "あらさ", "ラフネス", "roughness"],
|
||||||
|
"metallic": ["メタリック", "金属性", "きんぞくせい", "metallic"],
|
||||||
|
"subsurface": ["表面下散乱", "サブサーフェス", "subsurface", "sss"],
|
||||||
|
|
||||||
|
# Common naming patterns
|
||||||
|
"main": ["メイン", "主要", "しゅよう", "main", "primary"],
|
||||||
|
"sub": ["サブ", "副", "ふく", "sub", "secondary"],
|
||||||
|
"detail": ["詳細", "しょうさい", "ディテール", "detail"],
|
||||||
|
"shadow": ["影", "かげ", "シャドウ", "shadow"],
|
||||||
|
"highlight": ["ハイライト", "強調", "きょうちょう", "highlight"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Object name translations (Japanese to English)
|
||||||
|
object_names: Dict[str, List[str]] = {
|
||||||
|
|
||||||
|
"body": ["体", "からだ", "身体", "しんたい", "ボディ", "body", "torso"],
|
||||||
|
"head": ["頭", "あたま", "ヘッド", "head"],
|
||||||
|
"face": ["顔", "かお", "フェイス", "face"],
|
||||||
|
"neck": ["首", "くび", "ネック", "neck"],
|
||||||
|
"chest": ["胸", "むね", "チェスト", "chest", "breast"],
|
||||||
|
"back": ["背中", "せなか", "バック", "back"],
|
||||||
|
"waist": ["腰", "こし", "ウエスト", "waist"],
|
||||||
|
"hip": ["腰", "こし", "ヒップ", "hip"],
|
||||||
|
"arm": ["腕", "うで", "アーム", "arm"],
|
||||||
|
"hand": ["手", "て", "ハンド", "hand"],
|
||||||
|
"finger": ["指", "ゆび", "フィンガー", "finger"],
|
||||||
|
"leg": ["足", "あし", "脚", "レッグ", "leg"],
|
||||||
|
"foot": ["足", "あし", "フット", "foot"],
|
||||||
|
"toe": ["つま先", "つまさき", "トゥ", "toe"],
|
||||||
|
"clothing": ["服", "ふく", "衣服", "いふく", "クロージング", "clothing", "clothes"],
|
||||||
|
"outfit": ["服装", "ふくそう", "アウトフィット", "outfit"],
|
||||||
|
"accessory": ["アクセサリー", "装身具", "そうしんぐ", "accessory"],
|
||||||
|
"decoration": ["装飾", "そうしょく", "デコレーション", "decoration"],
|
||||||
|
"hair_front": ["前髪", "まえがみ", "フロント髪", "hairfront"],
|
||||||
|
"hair_back": ["後ろ髪", "うしろがみ", "バック髪", "hairback"],
|
||||||
|
"hair_side": ["横髪", "よこがみ", "サイド髪", "hairside"],
|
||||||
|
"ponytail": ["ポニーテール", "一つ結び", "ひとつむすび", "ponytail"],
|
||||||
|
"twintail": ["ツインテール", "二つ結び", "ふたつむすび", "twintail"],
|
||||||
|
"ahoge": ["あほ毛", "アホ毛", "はね毛", "ahoge", "antenna"],
|
||||||
|
"eyeball": ["眼球", "がんきゅう", "目玉", "めだま", "eyeball"],
|
||||||
|
"pupil": ["瞳", "ひとみ", "瞳孔", "どうこう", "pupil"],
|
||||||
|
"iris": ["虹彩", "こうさい", "アイリス", "iris"],
|
||||||
|
"eyelid": ["まぶた", "眼瞼", "がんけん", "eyelid"],
|
||||||
|
"nose": ["鼻", "はな", "ノーズ", "nose"],
|
||||||
|
"mouth": ["口", "くち", "マウス", "mouth"],
|
||||||
|
"lip": ["唇", "くちびる", "リップ", "lip"],
|
||||||
|
"ear": ["耳", "みみ", "イヤー", "ear"],
|
||||||
|
|
||||||
|
# Common object suffixes
|
||||||
|
"left": ["左", "ひだり", "レフト", "left", "l"],
|
||||||
|
"right": ["右", "みぎ", "ライト", "right", "r"],
|
||||||
|
"upper": ["上", "うえ", "アッパー", "upper", "top"],
|
||||||
|
"lower": ["下", "した", "ロワー", "lower", "bottom"],
|
||||||
|
"inner": ["内", "うち", "インナー", "inner", "inside"],
|
||||||
|
"outer": ["外", "そと", "アウター", "outer", "outside"],
|
||||||
|
"front": ["前", "まえ", "フロント", "front"],
|
||||||
|
"back": ["後ろ", "うしろ", "バック", "back", "rear"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Physics object names (for MMD rigid bodies and joints)
|
||||||
|
physics_names: Dict[str, List[str]] = {
|
||||||
|
# Rigid body types
|
||||||
|
"rigidbody": ["剛体", "ごうたい", "リジッドボディ", "rigidbody", "rigid"],
|
||||||
|
"joint": ["ジョイント", "関節", "かんせつ", "joint", "constraint"],
|
||||||
|
"collision": ["当たり判定", "あたりはんてい", "コリジョン", "collision"],
|
||||||
|
"hair_physics": ["髪物理", "かみぶつり", "ヘアフィジックス", "hairphys"],
|
||||||
|
"hair_root": ["髪根元", "かみねもと", "ヘアルート", "hairroot"],
|
||||||
|
"hair_tip": ["髪先", "かみさき", "ヘアティップ", "hairtip"],
|
||||||
|
"cloth_physics": ["布物理", "ぬのぶつり", "クロスフィジックス", "clothphys"],
|
||||||
|
"skirt_physics": ["スカート物理", "スカートフィジックス", "skirtphys"],
|
||||||
|
"breast_physics": ["胸物理", "むねぶつり", "ブレストフィジックス", "breastphys"],
|
||||||
|
"breast_root": ["胸根元", "むねねもと", "ブレストルート", "breastroot"],
|
||||||
|
"breast_tip": ["胸先", "むねさき", "ブレストティップ", "breasttip"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# MMD bone name patterns (for detection)
|
||||||
|
mmd_bone_patterns: List[str] = [
|
||||||
|
# Japanese bone names
|
||||||
|
'全ての親', 'センター', '上半身', '下半身', '首', '頭',
|
||||||
|
'右腕', '左腕', '右ひじ', '左ひじ', '右手首', '左手首',
|
||||||
|
'右足', '左足', '右ひざ', '左ひざ', '右足首', '左足首',
|
||||||
|
'両目', '左目', '右目', '右肩', '左肩',
|
||||||
|
# English bone names (common in MMD exports)
|
||||||
|
'center', 'groove', 'waist', 'upperbody', 'upperbody2', 'lowerbody',
|
||||||
|
'neck', 'head',
|
||||||
|
'shoulder_r', 'shoulder_l', 'arm_r', 'arm_l',
|
||||||
|
'elbow_r', 'elbow_l', 'wrist_r', 'wrist_l',
|
||||||
|
'leg_r', 'leg_l', 'knee_r', 'knee_l',
|
||||||
|
'ankle_r', 'ankle_l', 'toe_r', 'toe_l',
|
||||||
|
# Mixed/Romanized patterns
|
||||||
|
'센터', 'グルーブ', 'ウエスト',
|
||||||
|
# Common MMD suffixes
|
||||||
|
'_r', '_l', '.r', '.l'
|
||||||
|
]
|
||||||
|
|
||||||
|
# MMD to Unity bone mapping
|
||||||
|
# Maps MMD bone names (after English translation) to Unity humanoid bone names
|
||||||
|
mmd_to_unity_bone_map: Dict[str, Optional[str]] = {
|
||||||
|
# Root and core
|
||||||
|
"ParentNode": None, # Remove this
|
||||||
|
"Center": "Hips",
|
||||||
|
"センター": "Hips",
|
||||||
|
"Groove": None, # Remove this
|
||||||
|
"グルーブ": None,
|
||||||
|
"Waist": None, # Will be merged into Hips
|
||||||
|
|
||||||
|
# Spine chain
|
||||||
|
"LowerBody": "Hips",
|
||||||
|
"下半身": "Hips",
|
||||||
|
"UpperBody": "Spine",
|
||||||
|
"上半身": "Spine",
|
||||||
|
"UpperBody2": "Chest",
|
||||||
|
"上半身2": "Chest",
|
||||||
|
"Neck": "Neck",
|
||||||
|
"首": "Neck",
|
||||||
|
"Head": "Head",
|
||||||
|
"頭": "Head",
|
||||||
|
|
||||||
|
# Right leg
|
||||||
|
"RightLeg": "Right leg",
|
||||||
|
"右足": "Right leg",
|
||||||
|
"RightLegD": None, # Remove D variant
|
||||||
|
"RightKnee": "Right knee",
|
||||||
|
"右ひざ": "Right knee",
|
||||||
|
"RightAnkle": "Right ankle",
|
||||||
|
"右足首": "Right ankle",
|
||||||
|
"RightToe": "Right toe",
|
||||||
|
"右つま先": "Right toe",
|
||||||
|
|
||||||
|
# Left leg
|
||||||
|
"LeftLeg": "Left leg",
|
||||||
|
"左足": "Left leg",
|
||||||
|
"LeftLegD": None, # Remove D variant
|
||||||
|
"LeftKnee": "Left knee",
|
||||||
|
"左ひざ": "Left knee",
|
||||||
|
"LeftAnkle": "Left ankle",
|
||||||
|
"左足首": "Left ankle",
|
||||||
|
"LeftToe": "Left toe",
|
||||||
|
"左つま先": "Left toe",
|
||||||
|
|
||||||
|
# Right arm
|
||||||
|
"RightShoulder": "Right shoulder",
|
||||||
|
"右肩": "Right shoulder",
|
||||||
|
"RightArm": "Right arm",
|
||||||
|
"右腕": "Right arm",
|
||||||
|
"RightElbow": "Right elbow",
|
||||||
|
"右ひじ": "Right elbow",
|
||||||
|
"RightWrist": "Right wrist",
|
||||||
|
"右手首": "Right wrist",
|
||||||
|
|
||||||
|
# Left arm
|
||||||
|
"LeftShoulder": "Left shoulder",
|
||||||
|
"左肩": "Left shoulder",
|
||||||
|
"LeftArm": "Left arm",
|
||||||
|
"左腕": "Left arm",
|
||||||
|
"LeftElbow": "Left elbow",
|
||||||
|
"左ひじ": "Left elbow",
|
||||||
|
"LeftWrist": "Left wrist",
|
||||||
|
"左手首": "Left wrist",
|
||||||
|
|
||||||
|
# Cancel/Helper bones (remove these)
|
||||||
|
"WaistCancelRight": None,
|
||||||
|
"WaistCancelLeft": None,
|
||||||
|
"LegIKParentRight": None,
|
||||||
|
"LegIKParentLeft": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Unity humanoid bone hierarchy
|
||||||
|
# Defines parent-child relationships for Unity standard
|
||||||
|
unity_bone_hierarchy: Dict[str, Optional[str]] = {
|
||||||
|
"Hips": None, # Root bone
|
||||||
|
"Spine": "Hips",
|
||||||
|
"Chest": "Spine",
|
||||||
|
"Neck": "Chest",
|
||||||
|
"Head": "Neck",
|
||||||
|
|
||||||
|
# Arms
|
||||||
|
"Left shoulder": "Chest",
|
||||||
|
"Left arm": "Left shoulder",
|
||||||
|
"Left elbow": "Left arm",
|
||||||
|
"Left wrist": "Left elbow",
|
||||||
|
|
||||||
|
"Right shoulder": "Chest",
|
||||||
|
"Right arm": "Right shoulder",
|
||||||
|
"Right elbow": "Right arm",
|
||||||
|
"Right wrist": "Right elbow",
|
||||||
|
|
||||||
|
# Legs
|
||||||
|
"Left leg": "Hips",
|
||||||
|
"Left knee": "Left leg",
|
||||||
|
"Left ankle": "Left knee",
|
||||||
|
"Left toe": "Left ankle",
|
||||||
|
|
||||||
|
"Right leg": "Hips",
|
||||||
|
"Right knee": "Right leg",
|
||||||
|
"Right ankle": "Right knee",
|
||||||
|
"Right toe": "Right ankle",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create reverse lookup dictionaries
|
||||||
|
reverse_shapekey_lookup: Dict[str, str] = {}
|
||||||
|
reverse_material_lookup: Dict[str, str] = {}
|
||||||
|
reverse_object_lookup: Dict[str, str] = {}
|
||||||
|
reverse_physics_lookup: Dict[str, str] = {}
|
||||||
|
|
||||||
|
def _build_reverse_lookups():
|
||||||
|
"""Build reverse lookup dictionaries for fast translation"""
|
||||||
|
global reverse_shapekey_lookup, reverse_material_lookup, reverse_object_lookup, reverse_physics_lookup
|
||||||
|
|
||||||
|
for standard_name, variations in shapekey_names.items():
|
||||||
|
for variation in variations:
|
||||||
|
simplified = simplify_bonename(variation)
|
||||||
|
reverse_shapekey_lookup[simplified] = standard_name
|
||||||
|
|
||||||
|
for standard_name, variations in material_names.items():
|
||||||
|
for variation in variations:
|
||||||
|
simplified = simplify_bonename(variation)
|
||||||
|
reverse_material_lookup[simplified] = standard_name
|
||||||
|
|
||||||
|
for standard_name, variations in object_names.items():
|
||||||
|
for variation in variations:
|
||||||
|
simplified = simplify_bonename(variation)
|
||||||
|
reverse_object_lookup[simplified] = standard_name
|
||||||
|
|
||||||
|
for standard_name, variations in physics_names.items():
|
||||||
|
for variation in variations:
|
||||||
|
simplified = simplify_bonename(variation)
|
||||||
|
reverse_physics_lookup[simplified] = standard_name
|
||||||
|
|
||||||
|
_build_reverse_lookups()
|
||||||
|
|
||||||
|
|
||||||
|
class EnhancedDictionaryTranslator:
|
||||||
|
"""Enhanced dictionary translator with support for bones, shapekeys, materials, and objects"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.translation_stats = {
|
||||||
|
'bones': 0,
|
||||||
|
'shapekeys': 0,
|
||||||
|
'materials': 0,
|
||||||
|
'objects': 0,
|
||||||
|
'physics': 0,
|
||||||
|
'total': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def translate_bone_name(self, name: str) -> Optional[str]:
|
||||||
|
"""Translate bone name using existing bone dictionary"""
|
||||||
|
simplified = simplify_bonename(name)
|
||||||
|
if simplified in reverse_bone_lookup:
|
||||||
|
self.translation_stats['bones'] += 1
|
||||||
|
self.translation_stats['total'] += 1
|
||||||
|
return reverse_bone_lookup[simplified]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def translate_shapekey_name(self, name: str) -> Optional[str]:
|
||||||
|
"""Translate shapekey/morph name using shapekey dictionary"""
|
||||||
|
simplified = simplify_bonename(name)
|
||||||
|
if simplified in reverse_shapekey_lookup:
|
||||||
|
self.translation_stats['shapekeys'] += 1
|
||||||
|
self.translation_stats['total'] += 1
|
||||||
|
return reverse_shapekey_lookup[simplified]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def translate_material_name(self, name: str) -> Optional[str]:
|
||||||
|
"""Translate material name using material dictionary"""
|
||||||
|
simplified = simplify_bonename(name)
|
||||||
|
if simplified in reverse_material_lookup:
|
||||||
|
self.translation_stats['materials'] += 1
|
||||||
|
self.translation_stats['total'] += 1
|
||||||
|
return reverse_material_lookup[simplified]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def translate_object_name(self, name: str) -> Optional[str]:
|
||||||
|
"""Translate object name using object dictionary"""
|
||||||
|
simplified = simplify_bonename(name)
|
||||||
|
if simplified in reverse_object_lookup:
|
||||||
|
self.translation_stats['objects'] += 1
|
||||||
|
self.translation_stats['total'] += 1
|
||||||
|
return reverse_object_lookup[simplified]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def translate_physics_name(self, name: str) -> Optional[str]:
|
||||||
|
"""Translate physics object name using physics dictionary"""
|
||||||
|
simplified = simplify_bonename(name)
|
||||||
|
if simplified in reverse_physics_lookup:
|
||||||
|
self.translation_stats['physics'] += 1
|
||||||
|
self.translation_stats['total'] += 1
|
||||||
|
return reverse_physics_lookup[simplified]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def translate_name(self, name: str, category: str = "auto") -> Tuple[Optional[str], str]:
|
||||||
|
"""
|
||||||
|
Translate name with automatic category detection or specified category
|
||||||
|
Returns (translated_name, detected_category)
|
||||||
|
"""
|
||||||
|
if not name or not name.strip():
|
||||||
|
return None, "none"
|
||||||
|
|
||||||
|
if category == "bones":
|
||||||
|
result = self.translate_bone_name(name)
|
||||||
|
return (result, "bones") if result else (None, "unknown")
|
||||||
|
elif category == "shapekeys":
|
||||||
|
result = self.translate_shapekey_name(name)
|
||||||
|
return (result, "shapekeys") if result else (None, "unknown")
|
||||||
|
elif category == "materials":
|
||||||
|
result = self.translate_material_name(name)
|
||||||
|
return (result, "materials") if result else (None, "unknown")
|
||||||
|
elif category == "objects":
|
||||||
|
result = self.translate_object_name(name)
|
||||||
|
return (result, "objects") if result else (None, "unknown")
|
||||||
|
elif category == "physics":
|
||||||
|
result = self.translate_physics_name(name)
|
||||||
|
return (result, "physics") if result else (None, "unknown")
|
||||||
|
elif category == "auto":
|
||||||
|
# Try all categories in order of likelihood
|
||||||
|
for cat_name, translate_func in [
|
||||||
|
("bones", self.translate_bone_name),
|
||||||
|
("shapekeys", self.translate_shapekey_name),
|
||||||
|
("materials", self.translate_material_name),
|
||||||
|
("objects", self.translate_object_name),
|
||||||
|
("physics", self.translate_physics_name)
|
||||||
|
]:
|
||||||
|
result = translate_func(name)
|
||||||
|
if result:
|
||||||
|
return result, cat_name
|
||||||
|
return None, "unknown"
|
||||||
|
else:
|
||||||
|
return None, "invalid_category"
|
||||||
|
|
||||||
|
def get_statistics(self) -> Dict[str, int]:
|
||||||
|
"""Get translation statistics"""
|
||||||
|
return self.translation_stats.copy()
|
||||||
|
|
||||||
|
def reset_statistics(self) -> None:
|
||||||
|
"""Reset translation statistics"""
|
||||||
|
for key in self.translation_stats:
|
||||||
|
self.translation_stats[key] = 0
|
||||||
|
|
||||||
|
|
||||||
|
# Global enhanced dictionary translator instance
|
||||||
|
_enhanced_translator: Optional[EnhancedDictionaryTranslator] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_enhanced_translator() -> EnhancedDictionaryTranslator:
|
||||||
|
"""Get the global enhanced dictionary translator"""
|
||||||
|
global _enhanced_translator
|
||||||
|
if _enhanced_translator is None:
|
||||||
|
_enhanced_translator = EnhancedDictionaryTranslator()
|
||||||
|
return _enhanced_translator
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_dictionary_names() -> Dict[str, Dict[str, List[str]]]:
|
||||||
|
"""Get all dictionary names for reference"""
|
||||||
|
return {
|
||||||
|
"bones": bone_names,
|
||||||
|
"shapekeys": shapekey_names,
|
||||||
|
"materials": material_names,
|
||||||
|
"objects": object_names,
|
||||||
|
"physics": physics_names
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_custom_translation(category: str, standard_name: str, variations: List[str]) -> bool:
|
||||||
|
"""Add custom translation to the dictionaries"""
|
||||||
|
try:
|
||||||
|
if category == "bones":
|
||||||
|
if standard_name not in bone_names:
|
||||||
|
bone_names[standard_name] = []
|
||||||
|
bone_names[standard_name].extend(variations)
|
||||||
|
elif category == "shapekeys":
|
||||||
|
if standard_name not in shapekey_names:
|
||||||
|
shapekey_names[standard_name] = []
|
||||||
|
shapekey_names[standard_name].extend(variations)
|
||||||
|
elif category == "materials":
|
||||||
|
if standard_name not in material_names:
|
||||||
|
material_names[standard_name] = []
|
||||||
|
material_names[standard_name].extend(variations)
|
||||||
|
elif category == "objects":
|
||||||
|
if standard_name not in object_names:
|
||||||
|
object_names[standard_name] = []
|
||||||
|
object_names[standard_name].extend(variations)
|
||||||
|
elif category == "physics":
|
||||||
|
if standard_name not in physics_names:
|
||||||
|
physics_names[standard_name] = []
|
||||||
|
physics_names[standard_name].extend(variations)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
_build_reverse_lookups()
|
||||||
|
logger.info(f"Added custom translation for {category}: {standard_name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to add custom translation: {e}")
|
||||||
|
return False
|
||||||
+20
-24
@@ -8,7 +8,6 @@ from bpy_extras.io_utils import ImportHelper
|
|||||||
from typing import Optional, Callable, Dict, List, Union, Set
|
from typing import Optional, Callable, Dict, List, Union, Set
|
||||||
from ..common import clear_default_objects
|
from ..common import clear_default_objects
|
||||||
from ..translations import t
|
from ..translations import t
|
||||||
from ..mmd.core.pmx.importer import PMXImporter
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@@ -203,34 +202,31 @@ class AvatarToolKit_OT_Import(Operator, ImportHelper):
|
|||||||
|
|
||||||
def import_pmx_file(filepath: str) -> None:
|
def import_pmx_file(filepath: str) -> None:
|
||||||
"""
|
"""
|
||||||
Import a PMX file using the MMD Tools PMXImporter
|
Import a PMX file using the MMD Tools import operator
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filepath: Path to the PMX file
|
filepath: Path to the PMX file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Default import settings
|
# Use the MMD Tools operator to import PMX files (CATS-compatible)
|
||||||
import_settings = {
|
# Must pass files + directory like CATS does, not just filepath
|
||||||
"filepath": filepath,
|
|
||||||
"scale": 0.08,
|
|
||||||
"types": {"MESH", "ARMATURE", "MORPHS", "DISPLAY"},
|
|
||||||
"clean_model": True,
|
|
||||||
"remove_doubles": False,
|
|
||||||
"fix_IK_links": True,
|
|
||||||
"ik_loop_factor": 3,
|
|
||||||
"use_mipmap": True,
|
|
||||||
"sph_blend_factor": 1.0,
|
|
||||||
"spa_blend_factor": 1.0,
|
|
||||||
"rename_LR_bones": False,
|
|
||||||
"use_underscore": False,
|
|
||||||
"apply_bone_fixed_axis": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create and execute the importer
|
|
||||||
importer = PMXImporter()
|
|
||||||
try:
|
try:
|
||||||
importer.execute(**import_settings)
|
directory = os.path.dirname(filepath)
|
||||||
|
filename = os.path.basename(filepath)
|
||||||
|
|
||||||
|
bpy.ops.mmd_tools.import_model('EXEC_DEFAULT',
|
||||||
|
files=[{'name': filename}],
|
||||||
|
directory=directory,
|
||||||
|
scale=0.08,
|
||||||
|
types={'MESH', 'ARMATURE', 'MORPHS', 'DISPLAY'},
|
||||||
|
clean_model=False, # Disable cleaning to preserve morph indices
|
||||||
|
remove_doubles=False,
|
||||||
|
fix_ik_links=False,
|
||||||
|
ik_loop_factor=5,
|
||||||
|
apply_bone_fixed_axis=False,
|
||||||
|
rename_bones=False,
|
||||||
|
use_underscore=False)
|
||||||
logger.info(f"Successfully imported PMX file: {filepath}")
|
logger.info(f"Successfully imported PMX file: {filepath}")
|
||||||
except Exception:
|
except (AttributeError, TypeError, ValueError) as e:
|
||||||
logger.error(f"Failed to import PMX file: {traceback.format_exc()}", exc_info=True)
|
logger.error(f"Failed to import PMX file: {e}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ def configure_logging(enabled: bool = False, level: str = "WARNING") -> None:
|
|||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
def error_with_traceback(msg, *args, **kwargs):
|
def error_with_traceback(msg, *args, **kwargs):
|
||||||
if isinstance(kwargs.get('exception', None), Exception):
|
# If exc_info is True, include traceback in the message
|
||||||
|
if kwargs.get('exc_info', False):
|
||||||
full_msg = f"{msg}\n{traceback.format_exc()}"
|
full_msg = f"{msg}\n{traceback.format_exc()}"
|
||||||
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False})
|
_original_error(full_msg, *args, **{k: v for k, v in kwargs.items() if k != 'exc_info'})
|
||||||
else:
|
else:
|
||||||
_original_error(msg, *args, **kwargs)
|
_original_error(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
+101
-105
@@ -1,18 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2013 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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 contextlib
|
import contextlib
|
||||||
from typing import Generator, List, Optional, TypeVar, Any, Set, Tuple, Dict, Union
|
import math
|
||||||
|
from typing import Generator, List, Optional, TypeVar
|
||||||
|
|
||||||
|
import bmesh
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Object, Context, ID, Key, ShapeKey, FCurve, LayerCollection, Collection
|
from mathutils import Matrix
|
||||||
from bpy.types import AddonPreferences, Addon, WindowManager, Area, Region, Window
|
|
||||||
|
|
||||||
from ..logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
class Props: # For API changes of only name changed properties
|
class Props: # For API changes of only name changed properties
|
||||||
@@ -24,7 +19,7 @@ class Props: # For API changes of only name changed properties
|
|||||||
|
|
||||||
|
|
||||||
class __EditMode:
|
class __EditMode:
|
||||||
def __init__(self, obj: Object):
|
def __init__(self, obj):
|
||||||
if not isinstance(obj, bpy.types.Object):
|
if not isinstance(obj, bpy.types.Object):
|
||||||
raise ValueError
|
raise ValueError
|
||||||
self.__prevMode = obj.mode
|
self.__prevMode = obj.mode
|
||||||
@@ -34,10 +29,10 @@ class __EditMode:
|
|||||||
if obj.mode != "EDIT":
|
if obj.mode != "EDIT":
|
||||||
bpy.ops.object.mode_set(mode="EDIT")
|
bpy.ops.object.mode_set(mode="EDIT")
|
||||||
|
|
||||||
def __enter__(self) -> Any:
|
def __enter__(self):
|
||||||
return self.__obj.data
|
return self.__obj.data
|
||||||
|
|
||||||
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
if self.__prevMode == "EDIT":
|
if self.__prevMode == "EDIT":
|
||||||
bpy.ops.object.mode_set(mode="OBJECT") # update edited data
|
bpy.ops.object.mode_set(mode="OBJECT") # update edited data
|
||||||
bpy.ops.object.mode_set(mode=self.__prevMode)
|
bpy.ops.object.mode_set(mode=self.__prevMode)
|
||||||
@@ -45,43 +40,46 @@ class __EditMode:
|
|||||||
|
|
||||||
|
|
||||||
class __SelectObjects:
|
class __SelectObjects:
|
||||||
def __init__(self, active_object: Object, selected_objects: Optional[List[Object]] = None):
|
def __init__(self, active_object: bpy.types.Object, selected_objects: Optional[List[bpy.types.Object]] = None):
|
||||||
if not isinstance(active_object, bpy.types.Object):
|
if not isinstance(active_object, bpy.types.Object):
|
||||||
raise ValueError
|
raise ValueError
|
||||||
try:
|
try:
|
||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("Failed to set object mode")
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
context = FnContext.ensure_context()
|
contenxt = FnContext.ensure_context()
|
||||||
|
|
||||||
for i in context.selected_objects:
|
for i in contenxt.selected_objects:
|
||||||
i.select_set(False)
|
i.select_set(False)
|
||||||
|
|
||||||
self.__active_object = active_object
|
self.__active_object = active_object
|
||||||
self.__selected_objects = tuple(set(selected_objects) | set([active_object])) if selected_objects else (active_object,)
|
self.__selected_objects = tuple(set(selected_objects) | {active_object}) if selected_objects else (active_object,)
|
||||||
|
|
||||||
self.__hides: List[bool] = []
|
self.__hides: List[bool] = []
|
||||||
for i in self.__selected_objects:
|
for i in self.__selected_objects:
|
||||||
self.__hides.append(i.hide_get())
|
self.__hides.append(i.hide_get())
|
||||||
FnContext.select_object(context, i)
|
FnContext.select_object(contenxt, i)
|
||||||
FnContext.set_active_object(context, active_object)
|
FnContext.set_active_object(contenxt, active_object)
|
||||||
|
|
||||||
def __enter__(self) -> Object:
|
def __enter__(self) -> bpy.types.Object:
|
||||||
return self.__active_object
|
return self.__active_object
|
||||||
|
|
||||||
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
for i, j in zip(self.__selected_objects, self.__hides):
|
for i, j in zip(self.__selected_objects, self.__hides, strict=False):
|
||||||
|
try:
|
||||||
i.hide_set(j)
|
i.hide_set(j)
|
||||||
|
except ReferenceError:
|
||||||
|
# Object may no longer exist, so skip restoring hidden state.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def setParent(obj: Object, parent: Object) -> None:
|
def setParent(obj, parent):
|
||||||
with select_object(parent, objects=[parent, obj]):
|
with select_object(parent, objects=[parent, obj]):
|
||||||
bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False)
|
bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False)
|
||||||
|
|
||||||
|
|
||||||
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
|
def setParentToBone(obj, parent, bone_name):
|
||||||
with select_object(parent, objects=[parent, obj]):
|
with select_object(parent, objects=[parent, obj]):
|
||||||
bpy.ops.object.mode_set(mode="POSE")
|
bpy.ops.object.mode_set(mode="POSE")
|
||||||
parent.data.bones.active = parent.data.bones[bone_name]
|
parent.data.bones.active = parent.data.bones[bone_name]
|
||||||
@@ -89,7 +87,7 @@ def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
|
|||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
|
||||||
|
|
||||||
def edit_object(obj: Object) -> __EditMode:
|
def edit_object(obj):
|
||||||
"""Set the object interaction mode to 'EDIT'
|
"""Set the object interaction mode to 'EDIT'
|
||||||
|
|
||||||
It is recommended to use 'edit_object' with 'with' statement like the following code.
|
It is recommended to use 'edit_object' with 'with' statement like the following code.
|
||||||
@@ -100,7 +98,7 @@ def edit_object(obj: Object) -> __EditMode:
|
|||||||
return __EditMode(obj)
|
return __EditMode(obj)
|
||||||
|
|
||||||
|
|
||||||
def select_object(obj: Object, objects: Optional[List[Object]] = None) -> __SelectObjects:
|
def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object]] = None):
|
||||||
"""Select objects.
|
"""Select objects.
|
||||||
|
|
||||||
It is recommended to use 'select_object' with 'with' statement like the following code.
|
It is recommended to use 'select_object' with 'with' statement like the following code.
|
||||||
@@ -109,27 +107,26 @@ def select_object(obj: Object, objects: Optional[List[Object]] = None) -> __Sele
|
|||||||
with select_object(obj):
|
with select_object(obj):
|
||||||
some functions...
|
some functions...
|
||||||
"""
|
"""
|
||||||
# TODO: Reimplement with bpy.context.temp_override (If it ain't broke, don't fix it.)
|
# TODO: Consider reimplementing with bpy.context.temp_override,
|
||||||
|
# but note that Blender's new API has stability issues.
|
||||||
|
# temp_override is prone to crashes, making the current approach safer.
|
||||||
|
# If it ain't broke, don't fix it.
|
||||||
return __SelectObjects(obj, objects)
|
return __SelectObjects(obj, objects)
|
||||||
|
|
||||||
|
|
||||||
def duplicateObject(obj: Object, total_len: int) -> List[Object]:
|
def duplicateObject(obj, total_len):
|
||||||
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
|
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
|
||||||
|
|
||||||
|
|
||||||
def createObject(name: str = "Object", object_data: Optional[ID] = None, target_scene: Optional[bpy.types.Scene] = None) -> Object:
|
def createObject(name="Object", object_data=None, target_scene=None):
|
||||||
context = FnContext.ensure_context(target_scene)
|
context = FnContext.ensure_context(target_scene)
|
||||||
return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data))
|
return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data))
|
||||||
|
|
||||||
|
|
||||||
def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, target_object: Optional[Object] = None) -> Object:
|
def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None):
|
||||||
import bmesh
|
|
||||||
|
|
||||||
if target_object is None:
|
if target_object is None:
|
||||||
target_object = createObject(name="Sphere")
|
mesh_data = bpy.data.meshes.new("Sphere")
|
||||||
logger.debug(f"Created new sphere object: {target_object.name}")
|
target_object = createObject(name="Sphere", object_data=mesh_data)
|
||||||
else:
|
|
||||||
logger.debug(f"Using existing object for sphere: {target_object.name}")
|
|
||||||
|
|
||||||
mesh = target_object.data
|
mesh = target_object.data
|
||||||
bm = bmesh.new()
|
bm = bmesh.new()
|
||||||
@@ -146,15 +143,10 @@ def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, targe
|
|||||||
return target_object
|
return target_object
|
||||||
|
|
||||||
|
|
||||||
def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object:
|
def makeBox(size=(1, 1, 1), target_object=None):
|
||||||
import bmesh
|
|
||||||
from mathutils import Matrix
|
|
||||||
|
|
||||||
if target_object is None:
|
if target_object is None:
|
||||||
target_object = createObject(name="Box")
|
mesh_data = bpy.data.meshes.new("Box")
|
||||||
logger.debug(f"Created new box object: {target_object.name}")
|
target_object = createObject(name="Box", object_data=mesh_data)
|
||||||
else:
|
|
||||||
logger.debug(f"Using existing object for box: {target_object.name}")
|
|
||||||
|
|
||||||
mesh = target_object.data
|
mesh = target_object.data
|
||||||
bm = bmesh.new()
|
bm = bmesh.new()
|
||||||
@@ -170,16 +162,10 @@ def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optiona
|
|||||||
return target_object
|
return target_object
|
||||||
|
|
||||||
|
|
||||||
def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, height: float = 1.0, target_object: Optional[Object] = None) -> Object:
|
def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=None):
|
||||||
import math
|
|
||||||
import bmesh
|
|
||||||
|
|
||||||
if target_object is None:
|
if target_object is None:
|
||||||
target_object = createObject(name="Capsule")
|
mesh_data = bpy.data.meshes.new("Capsule")
|
||||||
logger.debug(f"Created new capsule object: {target_object.name}")
|
target_object = createObject(name="Capsule", object_data=mesh_data)
|
||||||
else:
|
|
||||||
logger.debug(f"Using existing object for capsule: {target_object.name}")
|
|
||||||
|
|
||||||
height = max(height, 1e-3)
|
height = max(height, 1e-3)
|
||||||
|
|
||||||
mesh = target_object.data
|
mesh = target_object.data
|
||||||
@@ -188,8 +174,11 @@ def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, heig
|
|||||||
top = (0, 0, height / 2 + radius)
|
top = (0, 0, height / 2 + radius)
|
||||||
verts.new(top)
|
verts.new(top)
|
||||||
|
|
||||||
# f = lambda i: radius*i/ring_count
|
# def f(i):
|
||||||
f = lambda i: radius * math.sin(0.5 * math.pi * i / ring_count)
|
# return radius * i / ring_count
|
||||||
|
def f(i):
|
||||||
|
return radius * math.sin(0.5 * math.pi * i / ring_count)
|
||||||
|
|
||||||
for i in range(ring_count, 0, -1):
|
for i in range(ring_count, 0, -1):
|
||||||
z = f(i - 1)
|
z = f(i - 1)
|
||||||
t = math.sqrt(radius**2 - z**2)
|
t = math.sqrt(radius**2 - z**2)
|
||||||
@@ -238,10 +227,10 @@ def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, heig
|
|||||||
|
|
||||||
|
|
||||||
class TransformConstraintOp:
|
class TransformConstraintOp:
|
||||||
__MIN_MAX_MAP: Dict[Union[str, Tuple[str, str]], Union[str, Tuple[str, ...]]] = {"ROTATION": "_rot", "SCALE": "_scale"}
|
__MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(constraints: bpy.types.ObjectConstraints, name: str, map_type: str) -> bpy.types.TransformConstraint:
|
def create(constraints, name, map_type):
|
||||||
c = constraints.get(name, None)
|
c = constraints.get(name, None)
|
||||||
if c and c.type != "TRANSFORM":
|
if c and c.type != "TRANSFORM":
|
||||||
constraints.remove(c)
|
constraints.remove(c)
|
||||||
@@ -259,7 +248,7 @@ class TransformConstraintOp:
|
|||||||
return c
|
return c
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def min_max_attributes(cls, map_type: str, name_id: str = "") -> Tuple[str, ...]:
|
def min_max_attributes(cls, map_type, name_id=""):
|
||||||
key = (map_type, name_id)
|
key = (map_type, name_id)
|
||||||
ret = cls.__MIN_MAX_MAP.get(key, None)
|
ret = cls.__MIN_MAX_MAP.get(key, None)
|
||||||
if ret is None:
|
if ret is None:
|
||||||
@@ -269,7 +258,7 @@ class TransformConstraintOp:
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_min_max(cls, constraint: bpy.types.TransformConstraint, value: float, influence: Optional[float] = 1) -> None:
|
def update_min_max(cls, constraint, value, influence=1):
|
||||||
c = constraint
|
c = constraint
|
||||||
if not c or c.type != "TRANSFORM":
|
if not c or c.type != "TRANSFORM":
|
||||||
return
|
return
|
||||||
@@ -293,14 +282,14 @@ class FnObject:
|
|||||||
raise NotImplementedError("This class is not expected to be instantiated.")
|
raise NotImplementedError("This class is not expected to be instantiated.")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mesh_remove_shape_key(mesh_object: Object, shape_key: ShapeKey) -> None:
|
def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey):
|
||||||
assert isinstance(mesh_object.data, bpy.types.Mesh)
|
assert isinstance(mesh_object.data, bpy.types.Mesh)
|
||||||
|
|
||||||
key: Key = shape_key.id_data
|
key: bpy.types.Key = shape_key.id_data
|
||||||
assert key == mesh_object.data.shape_keys
|
assert key == mesh_object.data.shape_keys
|
||||||
|
|
||||||
if mesh_object.animation_data is not None:
|
if mesh_object.animation_data is not None:
|
||||||
fc_curve: FCurve
|
fc_curve: bpy.types.FCurve
|
||||||
for fc_curve in mesh_object.animation_data.drivers:
|
for fc_curve in mesh_object.animation_data.drivers:
|
||||||
if not fc_curve.data_path.startswith(shape_key.path_from_id()):
|
if not fc_curve.data_path.startswith(shape_key.path_from_id()):
|
||||||
continue
|
continue
|
||||||
@@ -324,35 +313,43 @@ class FnContext:
|
|||||||
raise NotImplementedError("This class is not expected to be instantiated.")
|
raise NotImplementedError("This class is not expected to be instantiated.")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def ensure_context(context: Optional[Context] = None) -> Context:
|
def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context:
|
||||||
return context or bpy.context
|
return context or bpy.context
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_active_object(context: Context) -> Optional[Object]:
|
def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]:
|
||||||
|
# Added defensive programming for get methods
|
||||||
|
# Related to: https://github.com/MMD-Blender/blender_mmd_tools_local/issues/176
|
||||||
|
if context is None or not hasattr(context, "active_object"):
|
||||||
|
return None
|
||||||
return context.active_object
|
return context.active_object
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_active_object(context: Context, obj: Object) -> Object:
|
def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||||
context.view_layer.objects.active = obj
|
context.view_layer.objects.active = obj
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_active_and_select_single_object(context: Context, obj: Object) -> Object:
|
def set_active_and_select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||||
return FnContext.set_active_object(context, FnContext.select_single_object(context, obj))
|
return FnContext.set_active_object(context, FnContext.select_single_object(context, obj))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_scene_objects(context: Context) -> bpy.types.SceneObjects:
|
def get_scene_objects(context: bpy.types.Context) -> bpy.types.SceneObjects:
|
||||||
|
# Added defensive programming for get methods
|
||||||
|
# Added for consistency with get_active_object
|
||||||
|
if context is None or not hasattr(context, "scene") or not hasattr(context.scene, "objects"):
|
||||||
|
return []
|
||||||
return context.scene.objects
|
return context.scene.objects
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def ensure_selectable(context: Context, obj: Object) -> Object:
|
def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||||
obj.hide_viewport = False
|
obj.hide_viewport = False
|
||||||
obj.hide_select = False
|
obj.hide_select = False
|
||||||
obj.hide_set(False)
|
obj.hide_set(False)
|
||||||
|
|
||||||
if obj not in context.selectable_objects:
|
if obj not in context.selectable_objects:
|
||||||
|
|
||||||
def __layer_check(layer_collection: LayerCollection) -> bool:
|
def __layer_check(layer_collection: bpy.types.LayerCollection) -> bool:
|
||||||
for lc in layer_collection.children:
|
for lc in layer_collection.children:
|
||||||
if __layer_check(lc):
|
if __layer_check(lc):
|
||||||
lc.hide_viewport = False
|
lc.hide_viewport = False
|
||||||
@@ -374,44 +371,44 @@ class FnContext:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def select_object(context: Context, obj: Object) -> Object:
|
def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||||
FnContext.ensure_selectable(context, obj).select_set(True)
|
FnContext.ensure_selectable(context, obj).select_set(True)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def select_objects(context: Context, *objects: Object) -> List[Object]:
|
def select_objects(context: bpy.types.Context, *objects: bpy.types.Object) -> List[bpy.types.Object]:
|
||||||
return [FnContext.select_object(context, obj) for obj in objects]
|
return [FnContext.select_object(context, obj) for obj in objects]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def select_single_object(context: Context, obj: Object) -> Object:
|
def select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||||
for i in context.selected_objects:
|
for i in context.selected_objects:
|
||||||
if i != obj:
|
if i != obj:
|
||||||
i.select_set(False)
|
i.select_set(False)
|
||||||
return FnContext.select_object(context, obj)
|
return FnContext.select_object(context, obj)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def link_object(context: Context, obj: Object) -> Object:
|
def link_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||||
context.collection.objects.link(obj)
|
context.collection.objects.link(obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def new_and_link_object(context: Context, name: str, object_data: Optional[ID]) -> Object:
|
def new_and_link_object(context: bpy.types.Context, name: str, object_data: Optional[bpy.types.ID]) -> bpy.types.Object:
|
||||||
return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data))
|
return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def duplicate_object(context: Context, object_to_duplicate: Object, target_count: int) -> List[Object]:
|
def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]:
|
||||||
"""
|
"""
|
||||||
Duplicate object.
|
Duplicate object.
|
||||||
|
|
||||||
This function duplicates the given object and returns a list of duplicated objects.
|
This function duplicates the given object and returns a list of duplicated objects.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
context (Context): The context in which the duplication is performed.
|
context (bpy.types.Context): The context in which the duplication is performed.
|
||||||
object_to_duplicate (Object): The object to be duplicated.
|
object_to_duplicate (bpy.types.Object): The object to be duplicated.
|
||||||
target_count (int): The desired count of duplicated objects.
|
target_count (int): The desired count of duplicated objects.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Object]: A list of duplicated objects.
|
List[bpy.types.Object]: A list of duplicated objects.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
AssertionError: If the number of selected objects in the context is not equal to 1 or if the selected object is not the same as the object to be duplicated.
|
AssertionError: If the number of selected objects in the context is not equal to 1 or if the selected object is not the same as the object to be duplicated.
|
||||||
@@ -435,28 +432,27 @@ class FnContext:
|
|||||||
last_selected_objects[i].select_set(True)
|
last_selected_objects[i].select_set(True)
|
||||||
last_selected_objects = context.selected_objects
|
last_selected_objects = context.selected_objects
|
||||||
assert len(result_objects) == target_count
|
assert len(result_objects) == target_count
|
||||||
logger.debug(f"Duplicated object {object_to_duplicate.name} to create {target_count} objects")
|
|
||||||
return result_objects
|
return result_objects
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_user_layer_collection_by_object(context: Context, target_object: Object) -> Optional[LayerCollection]:
|
def find_user_layer_collection_by_object(context: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]:
|
||||||
"""
|
"""
|
||||||
Finds the layer collection that contains the given target_object in the user's collections.
|
Find the layer collection that contains the given target_object in the user's collections.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
context (Context): The Blender context.
|
context (bpy.types.Context): The Blender context.
|
||||||
target_object (Object): The target object to find the layer collection for.
|
target_object (bpy.types.Object): The target object to find the layer collection for.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[LayerCollection]: The layer collection that contains the target_object, or None if not found.
|
Optional[bpy.types.LayerCollection]: The layer collection that contains the target_object, or None if not found.
|
||||||
"""
|
"""
|
||||||
scene_layer_collection: LayerCollection = context.view_layer.layer_collection
|
scene_layer_collection: bpy.types.LayerCollection = context.view_layer.layer_collection
|
||||||
|
|
||||||
def find_layer_collection_by_name(layer_collection: LayerCollection, name: str) -> Optional[LayerCollection]:
|
def find_layer_collection_by_name(layer_collection: bpy.types.LayerCollection, name: str) -> Optional[bpy.types.LayerCollection]:
|
||||||
if layer_collection.name == name:
|
if layer_collection.name == name:
|
||||||
return layer_collection
|
return layer_collection
|
||||||
|
|
||||||
child_layer_collection: LayerCollection
|
child_layer_collection: bpy.types.LayerCollection
|
||||||
for child_layer_collection in layer_collection.children:
|
for child_layer_collection in layer_collection.children:
|
||||||
found = find_layer_collection_by_name(child_layer_collection, name)
|
found = find_layer_collection_by_name(child_layer_collection, name)
|
||||||
if found is not None:
|
if found is not None:
|
||||||
@@ -464,7 +460,7 @@ class FnContext:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user_collection: Collection
|
user_collection: bpy.types.Collection
|
||||||
for user_collection in target_object.users_collection:
|
for user_collection in target_object.users_collection:
|
||||||
found = find_layer_collection_by_name(scene_layer_collection, user_collection.name)
|
found = find_layer_collection_by_name(scene_layer_collection, user_collection.name)
|
||||||
if found is not None:
|
if found is not None:
|
||||||
@@ -474,7 +470,7 @@ class FnContext:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def temp_override_active_layer_collection(context: Context, target_object: Object) -> Generator[Context, None, None]:
|
def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]:
|
||||||
"""
|
"""
|
||||||
Context manager to temporarily override the active_layer_collection that contains the target object.
|
Context manager to temporarily override the active_layer_collection that contains the target object.
|
||||||
|
|
||||||
@@ -482,11 +478,11 @@ class FnContext:
|
|||||||
It ensures that the original active_layer_collection is restored after the context is exited.
|
It ensures that the original active_layer_collection is restored after the context is exited.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
context (Context): The context in which the active_layer_collection will be overridden.
|
context (bpy.types.Context): The context in which the active_layer_collection will be overridden.
|
||||||
target_object (Object): The target object whose layer collection will be set as the active_layer_collection.
|
target_object (bpy.types.Object): The target object whose layer collection will be set as the active_layer_collection.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
Context: The modified context with the active_layer_collection overridden.
|
bpy.types.Context: The modified context with the active_layer_collection overridden.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
with FnContext.temp_override_active_layer_collection(context, target_object):
|
with FnContext.temp_override_active_layer_collection(context, target_object):
|
||||||
@@ -507,24 +503,24 @@ class FnContext:
|
|||||||
context.view_layer.active_layer_collection = original_layer_collection
|
context.view_layer.active_layer_collection = original_layer_collection
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_addon_preferences(context: Context) -> Optional[AddonPreferences]:
|
def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]:
|
||||||
addon: Addon = context.preferences.addons.get(__package__, None)
|
addon: bpy.types.Addon = context.preferences.addons.get(__package__, None)
|
||||||
return addon.preferences if addon else None
|
return addon.preferences if addon else None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_addon_preferences_attribute(context: Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE:
|
def get_addon_preferences_attribute(context: bpy.types.Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE:
|
||||||
return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value)
|
return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def temp_override_objects(
|
def temp_override_objects(
|
||||||
context: Context,
|
context: bpy.types.Context,
|
||||||
window: Optional[Window] = None,
|
window: Optional[bpy.types.Window] = None,
|
||||||
area: Optional[Area] = None,
|
area: Optional[bpy.types.Area] = None,
|
||||||
region: Optional[Region] = None,
|
region: Optional[bpy.types.Region] = None,
|
||||||
active_object: Optional[Object] = None,
|
active_object: Optional[bpy.types.Object] = None,
|
||||||
selected_objects: Optional[List[Object]] = None,
|
selected_objects: Optional[List[bpy.types.Object]] = None,
|
||||||
**keywords: Any,
|
**keywords,
|
||||||
) -> Generator[Context, None, None]:
|
) -> Generator[bpy.types.Context, None, None]:
|
||||||
if active_object is not None:
|
if active_object is not None:
|
||||||
keywords["active_object"] = active_object
|
keywords["active_object"] = active_object
|
||||||
keywords["object"] = active_object
|
keywords["object"] = active_object
|
||||||
|
|||||||
+95
-185
@@ -1,44 +1,37 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2015 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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 math
|
import math
|
||||||
from typing import TYPE_CHECKING, Iterable, Optional, Set, List, Dict, Tuple, Any, Union, cast
|
from typing import TYPE_CHECKING, Iterable, Optional, Set
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
from bpy.types import Object, EditBone, PoseBone, Constraint, Armature, BoneCollection
|
|
||||||
|
|
||||||
from .. import bpyutils
|
from .. import bpyutils
|
||||||
from ..bpyutils import TransformConstraintOp
|
from ..bpyutils import TransformConstraintOp
|
||||||
from ..utils import ItemOp
|
from ..utils import ItemOp
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..properties.root import MMDRoot, MMDDisplayItemFrame
|
|
||||||
from ..properties.pose_bone import MMDBone
|
from ..properties.pose_bone import MMDBone
|
||||||
|
from ..properties.root import MMDDisplayItemFrame, MMDRoot
|
||||||
|
|
||||||
|
|
||||||
def remove_constraint(constraints: Any, name: str) -> bool:
|
def remove_constraint(constraints, name):
|
||||||
"""Remove a constraint by name if it exists"""
|
|
||||||
c = constraints.get(name, None)
|
c = constraints.get(name, None)
|
||||||
if c:
|
if c:
|
||||||
constraints.remove(c)
|
constraints.remove(c)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remove_edit_bones(edit_bones: bpy.types.ArmatureEditBones, bone_names: List[str]) -> None:
|
|
||||||
"""Remove edit bones by name"""
|
def remove_edit_bones(edit_bones, bone_names):
|
||||||
for name in bone_names:
|
for name in bone_names:
|
||||||
b = edit_bones.get(name, None)
|
b = edit_bones.get(name, None)
|
||||||
if b:
|
if b:
|
||||||
edit_bones.remove(b)
|
edit_bones.remove(b)
|
||||||
|
|
||||||
|
|
||||||
BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools"
|
BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools_local"
|
||||||
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection"
|
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection"
|
||||||
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection"
|
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection"
|
||||||
BONE_COLLECTION_NAME_SHADOW = "mmd_shadow"
|
BONE_COLLECTION_NAME_SHADOW = "mmd_shadow"
|
||||||
@@ -48,39 +41,33 @@ SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NA
|
|||||||
|
|
||||||
|
|
||||||
class FnBone:
|
class FnBone:
|
||||||
AUTO_LOCAL_AXIS_ARMS: Tuple[str, ...] = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
|
AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
|
||||||
AUTO_LOCAL_AXIS_FINGERS: Tuple[str, ...] = ("親指", "人指", "中指", "薬指", "小指")
|
AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指")
|
||||||
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: Tuple[str, ...] = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
|
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self):
|
||||||
raise NotImplementedError("This class cannot be instantiated.")
|
raise NotImplementedError("This class cannot be instantiated.")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_pose_bone_by_bone_id(armature_object: Object, bone_id: int) -> Optional[PoseBone]:
|
def find_pose_bone_by_bone_id(armature_object: bpy.types.Object, bone_id: int) -> Optional[bpy.types.PoseBone]:
|
||||||
"""Find a pose bone by its bone ID"""
|
|
||||||
for bone in armature_object.pose.bones:
|
for bone in armature_object.pose.bones:
|
||||||
if bone.mmd_bone.bone_id != bone_id:
|
if bone.mmd_bone.bone_id != bone_id:
|
||||||
continue
|
continue
|
||||||
return bone
|
return bone
|
||||||
logger.debug(f"Bone with ID {bone_id} not found in armature {armature_object.name}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __new_bone_id(armature_object: Object) -> int:
|
def __new_bone_id(armature_object: bpy.types.Object) -> int:
|
||||||
"""Generate a new unique bone ID"""
|
|
||||||
return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1
|
return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_or_assign_bone_id(pose_bone: PoseBone) -> int:
|
def get_or_assign_bone_id(pose_bone: bpy.types.PoseBone) -> int:
|
||||||
"""Get the bone ID or assign a new one if not set"""
|
|
||||||
if pose_bone.mmd_bone.bone_id < 0:
|
if pose_bone.mmd_bone.bone_id < 0:
|
||||||
pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data)
|
pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data)
|
||||||
logger.debug(f"Assigned new bone ID {pose_bone.mmd_bone.bone_id} to bone {pose_bone.name}")
|
|
||||||
return pose_bone.mmd_bone.bone_id
|
return pose_bone.mmd_bone.bone_id
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_selected_pose_bones(armature_object: Object) -> Iterable[PoseBone]:
|
def __get_selected_pose_bones(armature_object: bpy.types.Object) -> Iterable[bpy.types.PoseBone]:
|
||||||
"""Get selected pose bones from the armature"""
|
|
||||||
if armature_object.mode == "EDIT":
|
if armature_object.mode == "EDIT":
|
||||||
bpy.ops.object.mode_set(mode="OBJECT") # update selected bones
|
bpy.ops.object.mode_set(mode="OBJECT") # update selected bones
|
||||||
bpy.ops.object.mode_set(mode="EDIT") # back to edit mode
|
bpy.ops.object.mode_set(mode="EDIT") # back to edit mode
|
||||||
@@ -89,11 +76,9 @@ class FnBone:
|
|||||||
return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone)
|
return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_bone_fixed_axis(armature_object: Object, enable: bool = True) -> None:
|
def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True):
|
||||||
"""Load fixed axis settings for selected bones"""
|
|
||||||
logger.debug(f"Loading bone fixed axis (enable={enable}) for {armature_object.name}")
|
|
||||||
for b in FnBone.__get_selected_pose_bones(armature_object):
|
for b in FnBone.__get_selected_pose_bones(armature_object):
|
||||||
mmd_bone = b.mmd_bone
|
mmd_bone: MMDBone = b.mmd_bone
|
||||||
mmd_bone.enabled_fixed_axis = enable
|
mmd_bone.enabled_fixed_axis = enable
|
||||||
lock_rotation = b.lock_rotation[:]
|
lock_rotation = b.lock_rotation[:]
|
||||||
if enable:
|
if enable:
|
||||||
@@ -108,91 +93,72 @@ class FnBone:
|
|||||||
b.lock_location = b.lock_scale = (False, False, False)
|
b.lock_location = b.lock_scale = (False, False, False)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setup_special_bone_collections(armature_object: Object) -> Object:
|
def setup_special_bone_collections(armature_object: bpy.types.Object) -> bpy.types.Object:
|
||||||
"""Set up special bone collections for MMD"""
|
armature: bpy.types.Armature = armature_object.data
|
||||||
armature = cast(Armature, armature_object.data)
|
|
||||||
bone_collections = armature.collections
|
bone_collections = armature.collections
|
||||||
for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES:
|
for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES:
|
||||||
if bone_collection_name in bone_collections:
|
if bone_collection_name in bone_collections:
|
||||||
continue
|
continue
|
||||||
bone_collection = bone_collections.new(bone_collection_name)
|
bone_collection = bone_collections.new(bone_collection_name)
|
||||||
FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False)
|
FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False)
|
||||||
logger.debug(f"Created special bone collection: {bone_collection_name}")
|
|
||||||
return armature_object
|
return armature_object
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __is_mmd_tools_bone_collection(bone_collection: BoneCollection) -> bool:
|
def __is_mmd_tools_local_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
|
||||||
"""Check if a bone collection is an MMD Tools collection"""
|
|
||||||
return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection
|
return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __is_special_bone_collection(bone_collection: BoneCollection) -> bool:
|
def __is_special_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
|
||||||
"""Check if a bone collection is a special MMD collection"""
|
return bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) == BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
|
||||||
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __set_bone_collection_to_special(bone_collection: BoneCollection, is_visible: bool) -> None:
|
def __set_bone_collection_to_special(bone_collection: bpy.types.BoneCollection, is_visible: bool):
|
||||||
"""Mark a bone collection as special"""
|
|
||||||
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
|
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
|
||||||
bone_collection.is_visible = is_visible
|
bone_collection.is_visible = is_visible
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __is_normal_bone_collection(bone_collection: BoneCollection) -> bool:
|
def __is_normal_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
|
||||||
"""Check if a bone collection is a normal MMD collection"""
|
return bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) == BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
|
||||||
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __set_bone_collection_to_normal(bone_collection: BoneCollection) -> None:
|
def __set_bone_collection_to_normal(bone_collection: bpy.types.BoneCollection):
|
||||||
"""Mark a bone collection as normal"""
|
|
||||||
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
|
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __set_edit_bone_to_special(edit_bone: EditBone, bone_collection_name: str) -> EditBone:
|
def __set_edit_bone_to_special(edit_bone: bpy.types.EditBone, bone_collection_name: str) -> bpy.types.EditBone:
|
||||||
"""Set an edit bone to a special collection"""
|
|
||||||
edit_bone.id_data.collections[bone_collection_name].assign(edit_bone)
|
edit_bone.id_data.collections[bone_collection_name].assign(edit_bone)
|
||||||
edit_bone.use_deform = False
|
edit_bone.use_deform = False
|
||||||
return edit_bone
|
return edit_bone
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_edit_bone_to_dummy(edit_bone: EditBone) -> EditBone:
|
def set_edit_bone_to_dummy(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
|
||||||
"""Set an edit bone as a dummy bone"""
|
|
||||||
logger.debug(f"Setting bone {edit_bone.name} as dummy bone")
|
|
||||||
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY)
|
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_edit_bone_to_shadow(edit_bone: EditBone) -> EditBone:
|
def set_edit_bone_to_shadow(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
|
||||||
"""Set an edit bone as a shadow bone"""
|
|
||||||
logger.debug(f"Setting bone {edit_bone.name} as shadow bone")
|
|
||||||
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW)
|
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __unassign_mmd_tools_bone_collections(edit_bone: EditBone) -> EditBone:
|
def __unassign_mmd_tools_local_bone_collections(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
|
||||||
"""Unassign an edit bone from all MMD Tools collections"""
|
|
||||||
for bone_collection in edit_bone.collections:
|
for bone_collection in edit_bone.collections:
|
||||||
if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
|
if not FnBone.__is_mmd_tools_local_bone_collection(bone_collection):
|
||||||
continue
|
continue
|
||||||
bone_collection.unassign(edit_bone)
|
bone_collection.unassign(edit_bone)
|
||||||
return edit_bone
|
return edit_bone
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sync_bone_collections_from_display_item_frames(armature_object: Object) -> None:
|
def sync_bone_collections_from_display_item_frames(armature_object: bpy.types.Object):
|
||||||
"""Synchronize bone collections from display item frames"""
|
armature: bpy.types.Armature = armature_object.data
|
||||||
logger.info(f"Syncing bone collections from display item frames for {armature_object.name}")
|
|
||||||
armature = cast(Armature, armature_object.data)
|
|
||||||
bone_collections = armature.collections
|
bone_collections = armature.collections
|
||||||
|
|
||||||
from .model import FnModel
|
from .model import FnModel
|
||||||
|
|
||||||
root_object = FnModel.find_root_object(armature_object)
|
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
|
||||||
if not root_object:
|
mmd_root: MMDRoot = root_object.mmd_root
|
||||||
logger.error(f"No root object found for armature {armature_object.name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
mmd_root = root_object.mmd_root
|
|
||||||
|
|
||||||
bones = armature.bones
|
bones = armature.bones
|
||||||
used_groups: Set[str] = set()
|
used_groups = set()
|
||||||
unassigned_bone_names: Set[str] = {b.name for b in bones}
|
unassigned_bone_names = {b.name for b in bones}
|
||||||
|
|
||||||
for frame in mmd_root.display_item_frames:
|
for frame in mmd_root.display_item_frames:
|
||||||
for item in frame.data:
|
for item in frame.data:
|
||||||
@@ -204,12 +170,11 @@ class FnBone:
|
|||||||
if bone_collection is None:
|
if bone_collection is None:
|
||||||
bone_collection = bone_collections.new(name=group_name)
|
bone_collection = bone_collections.new(name=group_name)
|
||||||
FnBone.__set_bone_collection_to_normal(bone_collection)
|
FnBone.__set_bone_collection_to_normal(bone_collection)
|
||||||
logger.debug(f"Created new bone collection: {group_name}")
|
|
||||||
bone_collection.assign(bones[item.name])
|
bone_collection.assign(bones[item.name])
|
||||||
|
|
||||||
for name in unassigned_bone_names:
|
for name in unassigned_bone_names:
|
||||||
for bc in bones[name].collections:
|
for bc in bones[name].collections:
|
||||||
if not FnBone.__is_mmd_tools_bone_collection(bc):
|
if not FnBone.__is_mmd_tools_local_bone_collection(bc):
|
||||||
continue
|
continue
|
||||||
if not FnBone.__is_normal_bone_collection(bc):
|
if not FnBone.__is_normal_bone_collection(bc):
|
||||||
continue
|
continue
|
||||||
@@ -219,48 +184,40 @@ class FnBone:
|
|||||||
for bone_collection in bone_collections.values():
|
for bone_collection in bone_collections.values():
|
||||||
if bone_collection.name in used_groups:
|
if bone_collection.name in used_groups:
|
||||||
continue
|
continue
|
||||||
if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
|
if not FnBone.__is_mmd_tools_local_bone_collection(bone_collection):
|
||||||
continue
|
continue
|
||||||
if not FnBone.__is_normal_bone_collection(bone_collection):
|
if not FnBone.__is_normal_bone_collection(bone_collection):
|
||||||
continue
|
continue
|
||||||
logger.debug(f"Removing unused bone collection: {bone_collection.name}")
|
|
||||||
bone_collections.remove(bone_collection)
|
bone_collections.remove(bone_collection)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sync_display_item_frames_from_bone_collections(armature_object: Object) -> None:
|
def sync_display_item_frames_from_bone_collections(armature_object: bpy.types.Object):
|
||||||
"""Synchronize display item frames from bone collections"""
|
armature: bpy.types.Armature = armature_object.data
|
||||||
logger.info(f"Syncing display item frames from bone collections for {armature_object.name}")
|
bone_collections: bpy.types.BoneCollections = armature.collections
|
||||||
armature = cast(Armature, armature_object.data)
|
|
||||||
bone_collections = armature.collections
|
|
||||||
|
|
||||||
from .model import FnModel
|
from .model import FnModel
|
||||||
|
|
||||||
root_object = FnModel.find_root_object(armature_object)
|
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
|
||||||
if not root_object:
|
mmd_root: MMDRoot = root_object.mmd_root
|
||||||
logger.error(f"No root object found for armature {armature_object.name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
mmd_root = root_object.mmd_root
|
|
||||||
display_item_frames = mmd_root.display_item_frames
|
display_item_frames = mmd_root.display_item_frames
|
||||||
|
|
||||||
used_frame_index: Set[int] = set()
|
used_frame_index: Set[int] = set()
|
||||||
|
|
||||||
bone_collection: BoneCollection
|
bone_collection: bpy.types.BoneCollection
|
||||||
for bone_collection in bone_collections:
|
for bone_collection in bone_collections:
|
||||||
if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection):
|
if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
bone_collection_name = bone_collection.name
|
bone_collection_name = bone_collection.name
|
||||||
display_item_frame = display_item_frames.get(bone_collection_name)
|
display_item_frame: Optional[MMDDisplayItemFrame] = display_item_frames.get(bone_collection_name)
|
||||||
if display_item_frame is None:
|
if display_item_frame is None:
|
||||||
display_item_frame = display_item_frames.add()
|
display_item_frame = display_item_frames.add()
|
||||||
display_item_frame.name = bone_collection_name
|
display_item_frame.name = bone_collection_name
|
||||||
display_item_frame.name_e = bone_collection_name
|
display_item_frame.name_e = bone_collection_name
|
||||||
logger.debug(f"Created new display item frame: {bone_collection_name}")
|
|
||||||
used_frame_index.add(display_item_frames.find(bone_collection_name))
|
used_frame_index.add(display_item_frames.find(bone_collection_name))
|
||||||
|
|
||||||
ItemOp.resize(display_item_frame.data, len(bone_collection.bones))
|
ItemOp.resize(display_item_frame.data, len(bone_collection.bones))
|
||||||
for display_item, bone in zip(display_item_frame.data, bone_collection.bones):
|
for display_item, bone in zip(display_item_frame.data, bone_collection.bones, strict=False):
|
||||||
display_item.type = "BONE"
|
display_item.type = "BONE"
|
||||||
display_item.name = bone.name
|
display_item.name = bone.name
|
||||||
|
|
||||||
@@ -271,27 +228,23 @@ class FnBone:
|
|||||||
if display_item_frame.is_special:
|
if display_item_frame.is_special:
|
||||||
if display_item_frame.name != "表情":
|
if display_item_frame.name != "表情":
|
||||||
display_item_frame.data.clear()
|
display_item_frame.data.clear()
|
||||||
logger.debug(f"Cleared special display item frame: {display_item_frame.name}")
|
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Removing unused display item frame: {display_item_frames[i].name}")
|
|
||||||
display_item_frames.remove(i)
|
display_item_frames.remove(i)
|
||||||
mmd_root.active_display_item_frame = 0
|
mmd_root.active_display_item_frame = 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_bone_fixed_axis(armature_object: Object) -> None:
|
def apply_bone_fixed_axis(armature_object: bpy.types.Object):
|
||||||
"""Apply fixed axis to bones"""
|
bone_map = {}
|
||||||
logger.info(f"Applying bone fixed axis for {armature_object.name}")
|
|
||||||
bone_map: Dict[str, Tuple[Vector, bool, bool]] = {}
|
|
||||||
for b in armature_object.pose.bones:
|
for b in armature_object.pose.bones:
|
||||||
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
|
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
|
||||||
continue
|
continue
|
||||||
mmd_bone = b.mmd_bone
|
mmd_bone: MMDBone = b.mmd_bone
|
||||||
parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip
|
parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip
|
||||||
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
|
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
|
||||||
|
|
||||||
force_align = True
|
force_align = True
|
||||||
with bpyutils.edit_object(armature_object) as data:
|
with bpyutils.edit_object(armature_object) as data:
|
||||||
bone: EditBone
|
bone: bpy.types.EditBone
|
||||||
for bone in data.edit_bones:
|
for bone in data.edit_bones:
|
||||||
if bone.name not in bone_map:
|
if bone.name not in bone_map:
|
||||||
bone.select = False
|
bone.select = False
|
||||||
@@ -322,7 +275,6 @@ class FnBone:
|
|||||||
else:
|
else:
|
||||||
bone_map[bone.name] = (True, True, True)
|
bone_map[bone.name] = (True, True, True)
|
||||||
bone.select = True
|
bone.select = True
|
||||||
logger.debug(f"Applied fixed axis to bone: {bone.name}")
|
|
||||||
|
|
||||||
for bone_name, locks in bone_map.items():
|
for bone_name, locks in bone_map.items():
|
||||||
b = armature_object.pose.bones[bone_name]
|
b = armature_object.pose.bones[bone_name]
|
||||||
@@ -330,11 +282,9 @@ class FnBone:
|
|||||||
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks
|
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_bone_local_axes(armature_object: Object, enable: bool = True) -> None:
|
def load_bone_local_axes(armature_object: bpy.types.Object, enable=True):
|
||||||
"""Load local axes for selected bones"""
|
|
||||||
logger.debug(f"Loading bone local axes (enable={enable}) for {armature_object.name}")
|
|
||||||
for b in FnBone.__get_selected_pose_bones(armature_object):
|
for b in FnBone.__get_selected_pose_bones(armature_object):
|
||||||
mmd_bone = b.mmd_bone
|
mmd_bone: MMDBone = b.mmd_bone
|
||||||
mmd_bone.enabled_local_axes = enable
|
mmd_bone.enabled_local_axes = enable
|
||||||
if enable:
|
if enable:
|
||||||
axes = b.bone.matrix_local.to_3x3().transposed()
|
axes = b.bone.matrix_local.to_3x3().transposed()
|
||||||
@@ -342,18 +292,16 @@ class FnBone:
|
|||||||
mmd_bone.local_axis_z = axes[2].xzy
|
mmd_bone.local_axis_z = axes[2].xzy
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_bone_local_axes(armature_object: Object) -> None:
|
def apply_bone_local_axes(armature_object: bpy.types.Object):
|
||||||
"""Apply local axes to bones"""
|
bone_map = {}
|
||||||
logger.info(f"Applying bone local axes for {armature_object.name}")
|
|
||||||
bone_map: Dict[str, Tuple[Vector, Vector]] = {}
|
|
||||||
for b in armature_object.pose.bones:
|
for b in armature_object.pose.bones:
|
||||||
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
|
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
|
||||||
continue
|
continue
|
||||||
mmd_bone = b.mmd_bone
|
mmd_bone: MMDBone = b.mmd_bone
|
||||||
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
|
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
|
||||||
|
|
||||||
with bpyutils.edit_object(armature_object) as data:
|
with bpyutils.edit_object(armature_object) as data:
|
||||||
bone: EditBone
|
bone: bpy.types.EditBone
|
||||||
for bone in data.edit_bones:
|
for bone in data.edit_bones:
|
||||||
if bone.name not in bone_map:
|
if bone.name not in bone_map:
|
||||||
bone.select = False
|
bone.select = False
|
||||||
@@ -361,18 +309,15 @@ class FnBone:
|
|||||||
local_axis_x, local_axis_z = bone_map[bone.name]
|
local_axis_x, local_axis_z = bone_map[bone.name]
|
||||||
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
|
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
|
||||||
bone.select = True
|
bone.select = True
|
||||||
logger.debug(f"Applied local axes to bone: {bone.name}")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_bone_roll(edit_bone: EditBone, mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> None:
|
def update_bone_roll(edit_bone: bpy.types.EditBone, mmd_local_axis_x, mmd_local_axis_z):
|
||||||
"""Update bone roll based on local axes"""
|
|
||||||
axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z)
|
axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z)
|
||||||
idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
|
idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
|
||||||
edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3])
|
edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_axes(mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> Tuple[Vector, Vector, Vector]:
|
def get_axes(mmd_local_axis_x, mmd_local_axis_z):
|
||||||
"""Get axes from local axis vectors"""
|
|
||||||
x_axis = Vector(mmd_local_axis_x).normalized().xzy
|
x_axis = Vector(mmd_local_axis_x).normalized().xzy
|
||||||
z_axis = Vector(mmd_local_axis_z).normalized().xzy
|
z_axis = Vector(mmd_local_axis_z).normalized().xzy
|
||||||
y_axis = z_axis.cross(x_axis).normalized()
|
y_axis = z_axis.cross(x_axis).normalized()
|
||||||
@@ -380,25 +325,18 @@ class FnBone:
|
|||||||
return (x_axis, y_axis, z_axis)
|
return (x_axis, y_axis, z_axis)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_auto_bone_roll(armature: Object) -> None:
|
def apply_auto_bone_roll(armature):
|
||||||
"""Apply automatic bone roll to appropriate bones"""
|
bone_names = [b.name for b in armature.pose.bones if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j)]
|
||||||
logger.info(f"Applying auto bone roll for {armature.name}")
|
|
||||||
bone_names: List[str] = []
|
|
||||||
for b in armature.pose.bones:
|
|
||||||
if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j):
|
|
||||||
bone_names.append(b.name)
|
|
||||||
with bpyutils.edit_object(armature) as data:
|
with bpyutils.edit_object(armature) as data:
|
||||||
bone: EditBone
|
bone: bpy.types.EditBone
|
||||||
for bone in data.edit_bones:
|
for bone in data.edit_bones:
|
||||||
if bone.name not in bone_names:
|
if bone.name not in bone_names:
|
||||||
continue
|
continue
|
||||||
FnBone.update_auto_bone_roll(bone)
|
FnBone.update_auto_bone_roll(bone)
|
||||||
bone.select = True
|
bone.select = True
|
||||||
logger.debug(f"Applied auto bone roll to bone: {bone.name}")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_auto_bone_roll(edit_bone: EditBone) -> None:
|
def update_auto_bone_roll(edit_bone):
|
||||||
"""Update bone roll automatically"""
|
|
||||||
# make a triangle face (p1,p2,p3)
|
# make a triangle face (p1,p2,p3)
|
||||||
p1 = edit_bone.head.copy()
|
p1 = edit_bone.head.copy()
|
||||||
p2 = edit_bone.tail.copy()
|
p2 = edit_bone.tail.copy()
|
||||||
@@ -419,8 +357,7 @@ class FnBone:
|
|||||||
FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy)
|
FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_auto_local_axis(name_j: str) -> bool:
|
def has_auto_local_axis(name_j):
|
||||||
"""Check if a bone should have automatic local axis"""
|
|
||||||
if name_j:
|
if name_j:
|
||||||
if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS:
|
if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS:
|
||||||
return True
|
return True
|
||||||
@@ -430,11 +367,12 @@ class FnBone:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_additional_transformation(armature_object: Object) -> None:
|
def clean_additional_transformation(armature_object: bpy.types.Object):
|
||||||
"""Clean additional transformation constraints and bones"""
|
if armature_object.type != "ARMATURE" or armature_object.pose is None:
|
||||||
logger.info(f"Cleaning additional transformations for {armature_object.name}")
|
return
|
||||||
|
|
||||||
# clean constraints
|
# clean constraints
|
||||||
p_bone: PoseBone
|
p_bone: bpy.types.PoseBone
|
||||||
for p_bone in armature_object.pose.bones:
|
for p_bone in armature_object.pose.bones:
|
||||||
p_bone.mmd_bone.is_additional_transform_dirty = True
|
p_bone.mmd_bone.is_additional_transform_dirty = True
|
||||||
constraints = p_bone.constraints
|
constraints = p_bone.constraints
|
||||||
@@ -450,21 +388,17 @@ class FnBone:
|
|||||||
"ADDITIONAL_TRANSFORM_INVERT",
|
"ADDITIONAL_TRANSFORM_INVERT",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __is_at_shadow_bone(b: PoseBone) -> bool:
|
def __is_at_shadow_bone(b):
|
||||||
return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types
|
return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types
|
||||||
|
|
||||||
shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)]
|
shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)]
|
||||||
if len(shadow_bone_names) > 0:
|
if len(shadow_bone_names) > 0:
|
||||||
logger.debug(f"Removing {len(shadow_bone_names)} shadow bones")
|
|
||||||
with bpyutils.edit_object(armature_object) as data:
|
with bpyutils.edit_object(armature_object) as data:
|
||||||
remove_edit_bones(data.edit_bones, shadow_bone_names)
|
remove_edit_bones(data.edit_bones, shadow_bone_names)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_additional_transformation(armature_object: Object) -> None:
|
def apply_additional_transformation(armature_object: bpy.types.Object):
|
||||||
"""Apply additional transformation to bones"""
|
def __is_dirty_bone(b):
|
||||||
logger.info(f"Applying additional transformations for {armature_object.name}")
|
|
||||||
|
|
||||||
def __is_dirty_bone(b: PoseBone) -> bool:
|
|
||||||
if b.is_mmd_shadow_bone:
|
if b.is_mmd_shadow_bone:
|
||||||
return False
|
return False
|
||||||
mmd_bone = b.mmd_bone
|
mmd_bone = b.mmd_bone
|
||||||
@@ -473,10 +407,9 @@ class FnBone:
|
|||||||
return mmd_bone.is_additional_transform_dirty
|
return mmd_bone.is_additional_transform_dirty
|
||||||
|
|
||||||
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
|
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
|
||||||
logger.debug(f"Found {len(dirty_bones)} dirty bones to process")
|
|
||||||
|
|
||||||
# setup constraints
|
# setup constraints
|
||||||
shadow_bone_pool: List[Union[_AT_ShadowBoneRemove, _AT_ShadowBoneCreate]] = []
|
shadow_bone_pool = []
|
||||||
for p_bone in dirty_bones:
|
for p_bone in dirty_bones:
|
||||||
sb = FnBone.__setup_constraints(p_bone)
|
sb = FnBone.__setup_constraints(p_bone)
|
||||||
if sb:
|
if sb:
|
||||||
@@ -497,8 +430,7 @@ class FnBone:
|
|||||||
p_bone.mmd_bone.is_additional_transform_dirty = False
|
p_bone.mmd_bone.is_additional_transform_dirty = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __setup_constraints(p_bone: PoseBone) -> Optional[Union['_AT_ShadowBoneRemove', '_AT_ShadowBoneCreate']]:
|
def __setup_constraints(p_bone):
|
||||||
"""Set up constraints for additional transformation"""
|
|
||||||
bone_name = p_bone.name
|
bone_name = p_bone.name
|
||||||
mmd_bone = p_bone.mmd_bone
|
mmd_bone = p_bone.mmd_bone
|
||||||
influence = mmd_bone.additional_transform_influence
|
influence = mmd_bone.additional_transform_influence
|
||||||
@@ -511,18 +443,21 @@ class FnBone:
|
|||||||
rot = remove_constraint(constraints, "mmd_additional_rotation")
|
rot = remove_constraint(constraints, "mmd_additional_rotation")
|
||||||
loc = remove_constraint(constraints, "mmd_additional_location")
|
loc = remove_constraint(constraints, "mmd_additional_location")
|
||||||
if rot or loc:
|
if rot or loc:
|
||||||
logger.debug(f"Removing additional transform constraints for bone: {bone_name}")
|
|
||||||
return _AT_ShadowBoneRemove(bone_name)
|
return _AT_ShadowBoneRemove(bone_name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.debug(f"Setting up additional transform for bone: {bone_name} targeting {target_bone}")
|
|
||||||
shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone)
|
shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone)
|
||||||
|
|
||||||
def __config(name: str, mute: bool, map_type: str, value: float) -> None:
|
def __config(name, mute, map_type, value):
|
||||||
if mute:
|
if mute:
|
||||||
remove_constraint(constraints, name)
|
remove_constraint(constraints, name)
|
||||||
return
|
return
|
||||||
c = TransformConstraintOp.create(constraints, name, map_type)
|
c = TransformConstraintOp.create(constraints, name, map_type)
|
||||||
|
# FIXME: Some bones require specific rotation modes to match MMD behavior.
|
||||||
|
# Currently using hardcoded bone names as a temporary solution.
|
||||||
|
# See https://github.com/MMD-Blender/blender_mmd_tools_local/issues/242
|
||||||
|
if bone_name in {"左肩C", "右肩C", "肩C.L", "肩C.R", "肩C_L", "肩C_R"}:
|
||||||
|
c.from_rotation_mode = "ZYX" # Best matches MMD behavior for shoulder bones
|
||||||
c.target = p_bone.id_data
|
c.target = p_bone.id_data
|
||||||
shadow_bone.add_constraint(c)
|
shadow_bone.add_constraint(c)
|
||||||
TransformConstraintOp.update_min_max(c, value, influence)
|
TransformConstraintOp.update_min_max(c, value, influence)
|
||||||
@@ -533,81 +468,62 @@ class FnBone:
|
|||||||
return shadow_bone
|
return shadow_bone
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_additional_transform_influence(pose_bone: PoseBone) -> None:
|
def update_additional_transform_influence(pose_bone: bpy.types.PoseBone):
|
||||||
"""Update the influence of additional transform constraints"""
|
|
||||||
influence = pose_bone.mmd_bone.additional_transform_influence
|
influence = pose_bone.mmd_bone.additional_transform_influence
|
||||||
constraints = pose_bone.constraints
|
constraints = pose_bone.constraints
|
||||||
c = constraints.get("mmd_additional_rotation", None)
|
c = constraints.get("mmd_additional_rotation", None)
|
||||||
TransformConstraintOp.update_min_max(c, math.pi, influence)
|
TransformConstraintOp.update_min_max(c, math.pi, influence)
|
||||||
c = constraints.get("mmd_additional_location", None)
|
c = constraints.get("mmd_additional_location", None)
|
||||||
TransformConstraintOp.update_min_max(c, 100, influence)
|
TransformConstraintOp.update_min_max(c, 100, influence)
|
||||||
logger.debug(f"Updated additional transform influence for bone: {pose_bone.name} to {influence}")
|
|
||||||
|
|
||||||
|
|
||||||
class MigrationFnBone:
|
class MigrationFnBone:
|
||||||
"""Migration Functions for old MMD models broken by bugs or issues"""
|
"""Migration Functions for old MMD models broken by bugs or issues"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fix_mmd_ik_limit_override(armature_object: Object) -> None:
|
def fix_mmd_ik_limit_override(armature_object: bpy.types.Object):
|
||||||
"""Fix IK limit override constraints in old MMD models"""
|
pose_bone: bpy.types.PoseBone
|
||||||
logger.info(f"Fixing MMD IK limit overrides for {armature_object.name}")
|
|
||||||
pose_bone: PoseBone
|
|
||||||
for pose_bone in armature_object.pose.bones:
|
for pose_bone in armature_object.pose.bones:
|
||||||
constraint: Constraint
|
constraint: bpy.types.Constraint
|
||||||
for constraint in pose_bone.constraints:
|
for constraint in pose_bone.constraints:
|
||||||
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
|
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
|
||||||
constraint.owner_space = "LOCAL"
|
constraint.owner_space = "LOCAL"
|
||||||
logger.debug(f"Fixed IK limit override for bone: {pose_bone.name}")
|
|
||||||
|
|
||||||
|
|
||||||
class _AT_ShadowBoneRemove:
|
class _AT_ShadowBoneRemove:
|
||||||
"""Handler for removing shadow bones"""
|
def __init__(self, bone_name):
|
||||||
|
|
||||||
def __init__(self, bone_name: str) -> None:
|
|
||||||
"""Initialize with bone name"""
|
|
||||||
self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name)
|
self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name)
|
||||||
|
|
||||||
def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
|
def update_edit_bones(self, edit_bones):
|
||||||
"""Update edit bones by removing shadow bones"""
|
|
||||||
remove_edit_bones(edit_bones, self.__shadow_bone_names)
|
remove_edit_bones(edit_bones, self.__shadow_bone_names)
|
||||||
logger.debug(f"Removed shadow bones: {self.__shadow_bone_names}")
|
|
||||||
|
|
||||||
def update_pose_bones(self, pose_bones: Any) -> None:
|
def update_pose_bones(self, pose_bones):
|
||||||
"""Update pose bones (no-op for removal)"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class _AT_ShadowBoneCreate:
|
class _AT_ShadowBoneCreate:
|
||||||
"""Handler for creating shadow bones"""
|
def __init__(self, bone_name, target_bone_name):
|
||||||
|
|
||||||
def __init__(self, bone_name: str, target_bone_name: str) -> None:
|
|
||||||
"""Initialize with bone names"""
|
|
||||||
self.__dummy_bone_name = "_dummy_" + bone_name
|
self.__dummy_bone_name = "_dummy_" + bone_name
|
||||||
self.__shadow_bone_name = "_shadow_" + bone_name
|
self.__shadow_bone_name = "_shadow_" + bone_name
|
||||||
self.__bone_name = bone_name
|
self.__bone_name = bone_name
|
||||||
self.__target_bone_name = target_bone_name
|
self.__target_bone_name = target_bone_name
|
||||||
self.__constraint_pool: List[Constraint] = []
|
self.__constraint_pool = []
|
||||||
|
|
||||||
def __is_well_aligned(self, bone0: EditBone, bone1: EditBone) -> bool:
|
def __is_well_aligned(self, bone0, bone1):
|
||||||
"""Check if two bones are well aligned"""
|
|
||||||
return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99
|
return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99
|
||||||
|
|
||||||
def __update_constraints(self, use_shadow: bool = True) -> None:
|
def __update_constraints(self, use_shadow=True):
|
||||||
"""Update constraints to use shadow or target bone"""
|
|
||||||
subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name
|
subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name
|
||||||
for c in self.__constraint_pool:
|
for c in self.__constraint_pool:
|
||||||
c.subtarget = subtarget
|
c.subtarget = subtarget
|
||||||
|
|
||||||
def add_constraint(self, constraint: Constraint) -> None:
|
def add_constraint(self, constraint):
|
||||||
"""Add a constraint to the pool"""
|
|
||||||
self.__constraint_pool.append(constraint)
|
self.__constraint_pool.append(constraint)
|
||||||
|
|
||||||
def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
|
def update_edit_bones(self, edit_bones):
|
||||||
"""Update edit bones by creating shadow bones"""
|
|
||||||
bone = edit_bones[self.__bone_name]
|
bone = edit_bones[self.__bone_name]
|
||||||
target_bone = edit_bones[self.__target_bone_name]
|
target_bone = edit_bones[self.__target_bone_name]
|
||||||
if bone != target_bone and self.__is_well_aligned(bone, target_bone):
|
if bone != target_bone and self.__is_well_aligned(bone, target_bone):
|
||||||
logger.debug(f"Bones are well aligned, removing shadow bones for {self.__bone_name}")
|
|
||||||
_AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones)
|
_AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -617,7 +533,6 @@ class _AT_ShadowBoneCreate:
|
|||||||
dummy.head = target_bone.head
|
dummy.head = target_bone.head
|
||||||
dummy.tail = dummy.head + bone.tail - bone.head
|
dummy.tail = dummy.head + bone.tail - bone.head
|
||||||
dummy.roll = bone.roll
|
dummy.roll = bone.roll
|
||||||
logger.debug(f"Created/updated dummy bone: {dummy_bone_name}")
|
|
||||||
|
|
||||||
shadow_bone_name = self.__shadow_bone_name
|
shadow_bone_name = self.__shadow_bone_name
|
||||||
shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name))
|
shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name))
|
||||||
@@ -625,12 +540,9 @@ class _AT_ShadowBoneCreate:
|
|||||||
shadow.head = dummy.head
|
shadow.head = dummy.head
|
||||||
shadow.tail = dummy.tail
|
shadow.tail = dummy.tail
|
||||||
shadow.roll = bone.roll
|
shadow.roll = bone.roll
|
||||||
logger.debug(f"Created/updated shadow bone: {shadow_bone_name}")
|
|
||||||
|
|
||||||
def update_pose_bones(self, pose_bones: Any) -> None:
|
def update_pose_bones(self, pose_bones):
|
||||||
"""Update pose bones by setting up shadow bone properties"""
|
|
||||||
if self.__shadow_bone_name not in pose_bones:
|
if self.__shadow_bone_name not in pose_bones:
|
||||||
logger.debug(f"Shadow bone {self.__shadow_bone_name} not found, using target bone directly")
|
|
||||||
self.__update_constraints(use_shadow=False)
|
self.__update_constraints(use_shadow=False)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -649,7 +561,5 @@ class _AT_ShadowBoneCreate:
|
|||||||
c.subtarget = dummy_p_bone.name
|
c.subtarget = dummy_p_bone.name
|
||||||
c.target_space = "POSE"
|
c.target_space = "POSE"
|
||||||
c.owner_space = "POSE"
|
c.owner_space = "POSE"
|
||||||
logger.debug(f"Created copy transforms constraint for shadow bone: {self.__shadow_bone_name}")
|
|
||||||
|
|
||||||
self.__update_constraints()
|
self.__update_constraints()
|
||||||
logger.debug(f"Updated constraints for shadow bone: {self.__shadow_bone_name}")
|
|
||||||
|
|||||||
+39
-114
@@ -1,25 +1,18 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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 math
|
import math
|
||||||
from typing import Optional, List, Tuple, Callable, Any, Union
|
from typing import Optional
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Object, ID, Camera, Context
|
from mathutils import Matrix, Vector
|
||||||
from mathutils import Vector, Matrix, Euler
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from ..bpyutils import FnContext, Props
|
from ..bpyutils import FnContext, Props
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
class FnCamera:
|
class FnCamera:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_root(obj: Optional[Object]) -> Optional[Object]:
|
def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
|
||||||
"""Find the root object of an MMD camera setup."""
|
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return None
|
return None
|
||||||
if FnCamera.is_mmd_camera_root(obj):
|
if FnCamera.is_mmd_camera_root(obj):
|
||||||
@@ -29,22 +22,16 @@ class FnCamera:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_mmd_camera(obj: Object) -> bool:
|
def is_mmd_camera(obj: bpy.types.Object) -> bool:
|
||||||
"""Check if an object is an MMD camera."""
|
|
||||||
return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None
|
return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_mmd_camera_root(obj: Object) -> bool:
|
def is_mmd_camera_root(obj: bpy.types.Object) -> bool:
|
||||||
"""Check if an object is an MMD camera root."""
|
|
||||||
return obj.type == "EMPTY" and obj.mmd_type == "CAMERA"
|
return obj.type == "EMPTY" and obj.mmd_type == "CAMERA"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_drivers(camera_object: Object) -> None:
|
def add_drivers(camera_object: bpy.types.Object):
|
||||||
"""Add drivers to the camera object for MMD camera functionality."""
|
def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index: int = -1):
|
||||||
logger.debug(f"Adding drivers to camera: {camera_object.name}")
|
|
||||||
|
|
||||||
def __add_driver(id_data: ID, data_path: str, expression: str, index: int = -1) -> None:
|
|
||||||
"""Add a driver to the specified ID data."""
|
|
||||||
d = id_data.driver_add(data_path, index).driver
|
d = id_data.driver_add(data_path, index).driver
|
||||||
d.type = "SCRIPTED"
|
d.type = "SCRIPTED"
|
||||||
if "$empty_distance" in expression:
|
if "$empty_distance" in expression:
|
||||||
@@ -72,46 +59,31 @@ class FnCamera:
|
|||||||
v.targets[0].data_path = "mmd_camera.angle"
|
v.targets[0].data_path = "mmd_camera.angle"
|
||||||
expression = expression.replace("$angle", v.name)
|
expression = expression.replace("$angle", v.name)
|
||||||
if "$sensor_height" in expression:
|
if "$sensor_height" in expression:
|
||||||
v = d.variables.new()
|
# Use fixed sensor_height instead of dynamic reference.
|
||||||
v.name = "sensor_height"
|
# When controlled by MMD angle, sensor_height shouldn't change.
|
||||||
v.type = "SINGLE_PROP"
|
# This avoids unnecessary dependency cycles.
|
||||||
v.targets[0].id_type = "CAMERA"
|
# Reference: https://github.com/MMD-Blender/blender_mmd_tools_local/issues/227
|
||||||
v.targets[0].id = camera_object.data
|
current_sensor_height = camera_object.data.sensor_height
|
||||||
v.targets[0].data_path = "sensor_height"
|
expression = expression.replace("$sensor_height", str(current_sensor_height))
|
||||||
expression = expression.replace("$sensor_height", v.name)
|
|
||||||
|
|
||||||
d.expression = expression
|
d.expression = expression
|
||||||
|
|
||||||
try:
|
|
||||||
__add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45")
|
__add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45")
|
||||||
__add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1)
|
__add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1)
|
||||||
__add_driver(camera_object.data, "type", "not $is_perspective")
|
__add_driver(camera_object.data, "type", "not $is_perspective")
|
||||||
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
|
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
|
||||||
logger.debug(f"Successfully added drivers to camera: {camera_object.name}")
|
|
||||||
except Exception:
|
|
||||||
logger.error(f"Failed to add drivers to camera {camera_object.name}: {traceback.format_exc()}")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def remove_drivers(camera_object: Object) -> None:
|
def remove_drivers(camera_object: bpy.types.Object):
|
||||||
"""Remove drivers from the camera object."""
|
|
||||||
logger.debug(f"Removing drivers from camera: {camera_object.name}")
|
|
||||||
try:
|
|
||||||
camera_object.data.driver_remove("ortho_scale")
|
camera_object.data.driver_remove("ortho_scale")
|
||||||
camera_object.driver_remove("rotation_euler")
|
camera_object.driver_remove("rotation_euler")
|
||||||
camera_object.data.driver_remove("ortho_scale")
|
camera_object.data.driver_remove("type")
|
||||||
camera_object.data.driver_remove("lens")
|
camera_object.data.driver_remove("lens")
|
||||||
logger.debug(f"Successfully removed drivers from camera: {camera_object.name}")
|
|
||||||
except Exception:
|
|
||||||
logger.error(f"Failed to remove drivers from camera {camera_object.name}: {traceback.format_exc()}")
|
|
||||||
|
|
||||||
|
|
||||||
class MigrationFnCamera:
|
class MigrationFnCamera:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_mmd_camera() -> None:
|
def update_mmd_camera():
|
||||||
"""Update all MMD cameras in the scene."""
|
|
||||||
logger.info("Updating all MMD cameras in the scene")
|
|
||||||
updated_count = 0
|
|
||||||
|
|
||||||
for camera_object in bpy.data.objects:
|
for camera_object in bpy.data.objects:
|
||||||
if camera_object.type != "CAMERA":
|
if camera_object.type != "CAMERA":
|
||||||
continue
|
continue
|
||||||
@@ -121,57 +93,39 @@ class MigrationFnCamera:
|
|||||||
# It's not a MMD Camera
|
# It's not a MMD Camera
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
|
||||||
FnCamera.remove_drivers(camera_object)
|
FnCamera.remove_drivers(camera_object)
|
||||||
FnCamera.add_drivers(camera_object)
|
FnCamera.add_drivers(camera_object)
|
||||||
updated_count += 1
|
|
||||||
except Exception:
|
|
||||||
logger.error(f"Failed to update MMD camera {camera_object.name}: {traceback.format_exc()}")
|
|
||||||
|
|
||||||
logger.info(f"Updated {updated_count} MMD cameras")
|
|
||||||
|
|
||||||
|
|
||||||
class MMDCamera:
|
class MMDCamera:
|
||||||
def __init__(self, obj: Object):
|
def __init__(self, obj):
|
||||||
"""Initialize an MMD camera."""
|
|
||||||
root_object = FnCamera.find_root(obj)
|
root_object = FnCamera.find_root(obj)
|
||||||
if root_object is None:
|
if root_object is None:
|
||||||
logger.error(f"Object {obj.name} is not an MMD camera")
|
raise ValueError(f"{str(obj)} is not MMDCamera")
|
||||||
raise ValueError(f"{obj.name} is not an MMD camera")
|
|
||||||
|
|
||||||
self.__emptyObj = getattr(root_object, "original", obj)
|
self.__emptyObj = getattr(root_object, "original", obj)
|
||||||
logger.debug(f"Initialized MMD camera with root: {self.__emptyObj.name}")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def isMMDCamera(obj: Object) -> bool:
|
def isMMDCamera(obj: bpy.types.Object) -> bool:
|
||||||
"""Check if an object is an MMD camera."""
|
|
||||||
return FnCamera.find_root(obj) is not None
|
return FnCamera.find_root(obj) is not None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def addDrivers(cameraObj: Object) -> None:
|
def addDrivers(cameraObj: bpy.types.Object):
|
||||||
"""Add drivers to the camera object."""
|
|
||||||
FnCamera.add_drivers(cameraObj)
|
FnCamera.add_drivers(cameraObj)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def removeDrivers(cameraObj: Object) -> None:
|
def removeDrivers(cameraObj: bpy.types.Object):
|
||||||
"""Remove drivers from the camera object. """
|
|
||||||
if cameraObj.type != "CAMERA":
|
if cameraObj.type != "CAMERA":
|
||||||
return
|
return
|
||||||
FnCamera.remove_drivers(cameraObj)
|
FnCamera.remove_drivers(cameraObj)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def convertToMMDCamera(cameraObj: Object, scale: float = 1.0) -> 'MMDCamera':
|
def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0):
|
||||||
"""Convert a camera to an MMD camera."""
|
|
||||||
logger.info(f"Converting camera {cameraObj.name} to MMD camera with scale {scale}")
|
|
||||||
|
|
||||||
if FnCamera.is_mmd_camera(cameraObj):
|
if FnCamera.is_mmd_camera(cameraObj):
|
||||||
logger.debug(f"Camera {cameraObj.name} is already an MMD camera")
|
|
||||||
return MMDCamera(cameraObj)
|
return MMDCamera(cameraObj)
|
||||||
|
|
||||||
try:
|
|
||||||
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
|
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
|
||||||
context = FnContext.ensure_context()
|
FnContext.link_object(FnContext.ensure_context(), empty)
|
||||||
FnContext.link_object(context, empty)
|
|
||||||
|
|
||||||
cameraObj.parent = empty
|
cameraObj.parent = empty
|
||||||
cameraObj.data.sensor_fit = "VERTICAL"
|
cameraObj.data.sensor_fit = "VERTICAL"
|
||||||
@@ -195,50 +149,34 @@ class MMDCamera:
|
|||||||
empty.mmd_type = "CAMERA"
|
empty.mmd_type = "CAMERA"
|
||||||
empty.mmd_camera.angle = math.radians(30)
|
empty.mmd_camera.angle = math.radians(30)
|
||||||
empty.mmd_camera.persp = True
|
empty.mmd_camera.persp = True
|
||||||
|
|
||||||
logger.info(f"Successfully converted {cameraObj.name} to MMD camera")
|
|
||||||
return MMDCamera(empty)
|
return MMDCamera(empty)
|
||||||
except Exception:
|
|
||||||
logger.error(f"Failed to convert camera {cameraObj.name} to MMD camera: {traceback.format_exc()}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def newMMDCameraAnimation(
|
def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1):
|
||||||
cameraObj: Optional[Object],
|
|
||||||
cameraTarget: Optional[Object] = None,
|
|
||||||
scale: float = 1.0,
|
|
||||||
min_distance: float = 0.1
|
|
||||||
) -> 'MMDCamera':
|
|
||||||
"""Create a new MMD camera animation."""
|
|
||||||
logger.info(f"Creating new MMD camera animation with scale {scale}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
scene = bpy.context.scene
|
scene = bpy.context.scene
|
||||||
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
|
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
|
||||||
FnContext.link_object(FnContext.ensure_context(), mmd_cam)
|
FnContext.link_object(FnContext.ensure_context(), mmd_cam)
|
||||||
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
|
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
|
||||||
mmd_cam_root = mmd_cam.parent
|
mmd_cam_root = mmd_cam.parent
|
||||||
|
|
||||||
_camera_override_func: Optional[Callable[[], Object]] = None
|
_camera_override_func = None
|
||||||
if cameraObj is None:
|
if cameraObj is None:
|
||||||
if scene.camera is None:
|
if scene.camera is None:
|
||||||
scene.camera = mmd_cam
|
scene.camera = mmd_cam
|
||||||
logger.debug("Set scene camera to new MMD camera")
|
|
||||||
return MMDCamera(mmd_cam_root)
|
return MMDCamera(mmd_cam_root)
|
||||||
_camera_override_func = lambda: scene.camera
|
def _camera_override_func():
|
||||||
|
return scene.camera
|
||||||
|
|
||||||
_target_override_func: Optional[Callable[[Object], Object]] = None
|
_target_override_func = None
|
||||||
if cameraTarget is None:
|
if cameraTarget is None:
|
||||||
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj
|
def _target_override_func(camObj):
|
||||||
|
return camObj.data.dof.focus_object or camObj
|
||||||
|
|
||||||
action_name = mmd_cam_root.name
|
action_name = mmd_cam_root.name
|
||||||
parent_action = bpy.data.actions.new(name=action_name)
|
parent_action = bpy.data.actions.new(name=action_name)
|
||||||
distance_action = bpy.data.actions.new(name=action_name + "_dis")
|
distance_action = bpy.data.actions.new(name=action_name + "_dis")
|
||||||
FnCamera.remove_drivers(mmd_cam)
|
FnCamera.remove_drivers(mmd_cam)
|
||||||
|
|
||||||
from math import atan
|
|
||||||
from mathutils import Matrix, Vector
|
|
||||||
|
|
||||||
render = scene.render
|
render = scene.render
|
||||||
factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x)
|
factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x)
|
||||||
matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]))
|
matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]))
|
||||||
@@ -247,19 +185,15 @@ class MMDCamera:
|
|||||||
frame_count = frame_end - frame_start
|
frame_count = frame_end - frame_start
|
||||||
frames = range(frame_start, frame_end)
|
frames = range(frame_start, frame_end)
|
||||||
|
|
||||||
fcurves = []
|
fcurves = [parent_action.fcurves.new(data_path="location", index=i) for i in range(3)] # x, y, z
|
||||||
for i in range(3):
|
fcurves.extend(parent_action.fcurves.new(data_path="rotation_euler", index=i) for i in range(3)) # rx, ry, rz
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
|
|
||||||
for i in range(3):
|
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
|
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
|
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
|
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
|
||||||
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
|
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
|
||||||
for c in fcurves:
|
for c in fcurves:
|
||||||
c.keyframe_points.add(frame_count)
|
c.keyframe_points.add(frame_count)
|
||||||
|
|
||||||
logger.debug(f"Processing {frame_count} frames for camera animation")
|
for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves), strict=False):
|
||||||
for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves)):
|
|
||||||
scene.frame_set(f)
|
scene.frame_set(f)
|
||||||
if _camera_override_func:
|
if _camera_override_func:
|
||||||
cameraObj = _camera_override_func()
|
cameraObj = _camera_override_func()
|
||||||
@@ -292,7 +226,7 @@ class MMDCamera:
|
|||||||
x.co, y.co, z.co = ((f, i) for i in cam_target_loc)
|
x.co, y.co, z.co = ((f, i) for i in cam_target_loc)
|
||||||
rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation)
|
rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation)
|
||||||
dis.co = (f, cam_dis)
|
dis.co = (f, cam_dis)
|
||||||
fov.co = (f, 2 * atan(tan_val))
|
fov.co = (f, 2 * math.atan(tan_val))
|
||||||
persp.co = (f, cameraObj.data.type != "ORTHO")
|
persp.co = (f, cameraObj.data.type != "ORTHO")
|
||||||
persp.interpolation = "CONSTANT"
|
persp.interpolation = "CONSTANT"
|
||||||
for kp in (x, y, z, rx, ry, rz, fov, dis):
|
for kp in (x, y, z, rx, ry, rz, fov, dis):
|
||||||
@@ -302,22 +236,13 @@ class MMDCamera:
|
|||||||
mmd_cam_root.animation_data_create().action = parent_action
|
mmd_cam_root.animation_data_create().action = parent_action
|
||||||
mmd_cam.animation_data_create().action = distance_action
|
mmd_cam.animation_data_create().action = distance_action
|
||||||
scene.frame_set(frame_current)
|
scene.frame_set(frame_current)
|
||||||
|
|
||||||
logger.info(f"Successfully created MMD camera animation with {frame_count} frames")
|
|
||||||
return MMDCamera(mmd_cam_root)
|
return MMDCamera(mmd_cam_root)
|
||||||
|
|
||||||
except Exception:
|
def object(self):
|
||||||
logger.error(f"Failed to create MMD camera animation: {traceback.format_exc()}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def object(self) -> Object:
|
|
||||||
"""Get the root object of the MMD camera."""
|
|
||||||
return self.__emptyObj
|
return self.__emptyObj
|
||||||
|
|
||||||
def camera(self) -> Object:
|
def camera(self):
|
||||||
"""Get the camera object of the MMD camera."""
|
|
||||||
for i in self.__emptyObj.children:
|
for i in self.__emptyObj.children:
|
||||||
if i.type == "CAMERA":
|
if i.type == "CAMERA":
|
||||||
return i
|
return i
|
||||||
logger.error(f"No camera found for MMD camera root {self.__emptyObj.name}")
|
raise KeyError
|
||||||
raise KeyError(f"No camera found for MMD camera root {self.__emptyObj.name}")
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2016 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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
|
# Module for custom exceptions
|
||||||
# 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.
|
|
||||||
|
|
||||||
|
|
||||||
class MaterialNotFoundError(KeyError):
|
class MaterialNotFoundError(KeyError):
|
||||||
"""Exception raised when a material is not found in the scene"""
|
"""Exception raised when a material is not found in the scene"""
|
||||||
|
|
||||||
def __init__(self, *args: object) -> None:
|
def __init__(self, *args: object) -> None:
|
||||||
"""Constructor for MaterialNotFoundError"""
|
"""Initialize MaterialNotFoundError"""
|
||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
|
|||||||
+14
-35
@@ -1,53 +1,37 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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 bpy
|
import bpy
|
||||||
from typing import Optional, Union, Any, List, Tuple
|
|
||||||
from bpy.types import Object, Context
|
|
||||||
|
|
||||||
from ..bpyutils import FnContext, Props
|
from ..bpyutils import FnContext, Props
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
class MMDLamp:
|
class MMDLamp:
|
||||||
def __init__(self, obj: Object) -> None:
|
def __init__(self, obj):
|
||||||
if MMDLamp.isLamp(obj):
|
if MMDLamp.isLamp(obj):
|
||||||
obj = obj.parent
|
obj = obj.parent
|
||||||
if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT":
|
if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT":
|
||||||
self.__emptyObj: Object = obj
|
self.__emptyObj = obj
|
||||||
else:
|
else:
|
||||||
error_msg = f"{str(obj)} is not MMDLamp"
|
raise ValueError(f"{str(obj)} is not MMDLamp")
|
||||||
logger.error(error_msg)
|
|
||||||
raise ValueError(error_msg)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def isLamp(obj: Optional[Object]) -> bool:
|
def isLamp(obj):
|
||||||
"""Check if the object is a lamp/light object"""
|
return obj and obj.type in {"LIGHT", "LAMP"}
|
||||||
return obj is not None and obj.type in {"LIGHT", "LAMP"}
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def isMMDLamp(obj: Optional[Object]) -> bool:
|
def isMMDLamp(obj):
|
||||||
"""Check if the object is an MMD lamp"""
|
|
||||||
if MMDLamp.isLamp(obj):
|
if MMDLamp.isLamp(obj):
|
||||||
obj = obj.parent
|
obj = obj.parent
|
||||||
return obj is not None and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
|
return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def convertToMMDLamp(lampObj: Object, scale: float = 1.0) -> 'MMDLamp':
|
def convertToMMDLamp(lampObj, scale=1.0):
|
||||||
"""Convert a regular lamp to an MMD lamp"""
|
|
||||||
if MMDLamp.isMMDLamp(lampObj):
|
if MMDLamp.isMMDLamp(lampObj):
|
||||||
logger.debug(f"Object {lampObj.name} is already an MMD lamp")
|
|
||||||
return MMDLamp(lampObj)
|
return MMDLamp(lampObj)
|
||||||
|
|
||||||
logger.info(f"Converting {lampObj.name} to MMD lamp with scale {scale}")
|
empty = bpy.data.objects.new(name="MMD_Light", object_data=None)
|
||||||
|
FnContext.link_object(FnContext.ensure_context(), empty)
|
||||||
empty: Object = bpy.data.objects.new(name="MMD_Light", object_data=None)
|
|
||||||
context = FnContext.ensure_context()
|
|
||||||
FnContext.link_object(context, empty)
|
|
||||||
|
|
||||||
empty.rotation_mode = "XYZ"
|
empty.rotation_mode = "XYZ"
|
||||||
empty.lock_rotation = (True, True, True)
|
empty.lock_rotation = (True, True, True)
|
||||||
@@ -69,18 +53,13 @@ class MMDLamp:
|
|||||||
constraint.track_axis = "TRACK_NEGATIVE_Z"
|
constraint.track_axis = "TRACK_NEGATIVE_Z"
|
||||||
constraint.up_axis = "UP_Y"
|
constraint.up_axis = "UP_Y"
|
||||||
|
|
||||||
logger.debug(f"Successfully created MMD lamp from {lampObj.name}")
|
|
||||||
return MMDLamp(empty)
|
return MMDLamp(empty)
|
||||||
|
|
||||||
def object(self) -> Object:
|
def object(self):
|
||||||
"""Get the empty object that represents this MMD lamp"""
|
|
||||||
return self.__emptyObj
|
return self.__emptyObj
|
||||||
|
|
||||||
def lamp(self) -> Object:
|
def lamp(self):
|
||||||
"""Get the actual lamp/light object"""
|
|
||||||
for i in self.__emptyObj.children:
|
for i in self.__emptyObj.children:
|
||||||
if MMDLamp.isLamp(i):
|
if MMDLamp.isLamp(i):
|
||||||
return i
|
return i
|
||||||
error_msg = f"No lamp found in MMD lamp {self.__emptyObj.name}"
|
raise KeyError
|
||||||
logger.error(error_msg)
|
|
||||||
raise KeyError(error_msg)
|
|
||||||
|
|||||||
+89
-154
@@ -1,13 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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
|
from ....core.logging_setup import logger
|
||||||
import os
|
import os
|
||||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast, Dict, List, Any, Union, Set
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
@@ -15,7 +12,6 @@ from mathutils import Vector
|
|||||||
from ..bpyutils import FnContext
|
from ..bpyutils import FnContext
|
||||||
from .exceptions import MaterialNotFoundError
|
from .exceptions import MaterialNotFoundError
|
||||||
from .shader import _NodeGroupUtils
|
from .shader import _NodeGroupUtils
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..properties.material import MMDMaterial
|
from ..properties.material import MMDMaterial
|
||||||
@@ -28,55 +24,51 @@ SPHERE_MODE_SUBTEX = 3
|
|||||||
|
|
||||||
|
|
||||||
class _DummyTexture:
|
class _DummyTexture:
|
||||||
def __init__(self, image: bpy.types.Image):
|
def __init__(self, image):
|
||||||
self.type: str = "IMAGE"
|
self.type = "IMAGE"
|
||||||
self.image: bpy.types.Image = image
|
self.image = image
|
||||||
self.use_mipmap: bool = True
|
self.use_mipmap = True
|
||||||
|
|
||||||
|
|
||||||
class _DummyTextureSlot:
|
class _DummyTextureSlot:
|
||||||
def __init__(self, image: bpy.types.Image):
|
def __init__(self, image):
|
||||||
self.diffuse_color_factor: float = 1
|
self.diffuse_color_factor = 1
|
||||||
self.uv_layer: str = ""
|
self.uv_layer = ""
|
||||||
self.texture: _DummyTexture = _DummyTexture(image)
|
self.texture = _DummyTexture(image)
|
||||||
|
|
||||||
|
|
||||||
class FnMaterial:
|
class FnMaterial:
|
||||||
__NODES_ARE_READONLY: bool = False
|
__NODES_ARE_READONLY: bool = False
|
||||||
|
|
||||||
def __init__(self, material: bpy.types.Material):
|
def __init__(self, material: bpy.types.Material):
|
||||||
self.__material: bpy.types.Material = material
|
self.__material = material
|
||||||
self._nodes_are_readonly: bool = FnMaterial.__NODES_ARE_READONLY
|
self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_nodes_are_readonly(nodes_are_readonly: bool) -> None:
|
def set_nodes_are_readonly(nodes_are_readonly: bool):
|
||||||
FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly
|
FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_material_id(cls, material_id: str) -> Optional['FnMaterial']:
|
def from_material_id(cls, material_id: int):
|
||||||
for material in bpy.data.materials:
|
for material in bpy.data.materials:
|
||||||
if material.mmd_material.material_id == material_id:
|
if material.mmd_material.material_id == material_id:
|
||||||
return cls(material)
|
return cls(material)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_materials(obj: bpy.types.Object, can_remove: Callable[[bpy.types.Material], bool]) -> None:
|
def clean_materials(obj, can_remove: Callable[[bpy.types.Material], bool]):
|
||||||
materials = obj.data.materials
|
materials = obj.data.materials
|
||||||
materials_pop = materials.pop
|
materials_pop = materials.pop
|
||||||
removed_count = 0
|
|
||||||
for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True):
|
for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True):
|
||||||
m = materials_pop(index=i)
|
m = materials_pop(index=i)
|
||||||
removed_count += 1
|
|
||||||
if m.users < 1:
|
if m.users < 1:
|
||||||
bpy.data.materials.remove(m)
|
bpy.data.materials.remove(m)
|
||||||
|
|
||||||
if removed_count > 0:
|
|
||||||
logger.debug(f"Removed {removed_count} materials from {obj.name}")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def swap_materials(mesh_object: bpy.types.Object, mat1_ref: Union[str, int], mat2_ref: Union[str, int], reverse: bool = False, swap_slots: bool = False) -> Tuple[bpy.types.Material, bpy.types.Material]:
|
def swap_materials(mesh_object: bpy.types.Object, mat1_ref: str | int, mat2_ref: str | int, reverse=False, swap_slots=False) -> Tuple[bpy.types.Material, bpy.types.Material]:
|
||||||
"""
|
"""
|
||||||
This method will assign the polygons of mat1 to mat2.
|
Assign the polygons of mat1 to mat2.
|
||||||
|
|
||||||
If reverse is True it will also swap the polygons assigned to mat2 to mat1.
|
If reverse is True it will also swap the polygons assigned to mat2 to mat1.
|
||||||
The reference to materials can be indexes or names
|
The reference to materials can be indexes or names
|
||||||
Finally it will also swap the material slots if the option is given.
|
Finally it will also swap the material slots if the option is given.
|
||||||
@@ -94,22 +86,18 @@ class FnMaterial:
|
|||||||
Raises:
|
Raises:
|
||||||
MaterialNotFoundError: If one of the materials is not found
|
MaterialNotFoundError: If one of the materials is not found
|
||||||
"""
|
"""
|
||||||
mesh = cast(bpy.types.Mesh, mesh_object.data)
|
mesh = cast("bpy.types.Mesh", mesh_object.data)
|
||||||
try:
|
try:
|
||||||
# Try to find the materials
|
# Try to find the materials
|
||||||
mat1 = mesh.materials[mat1_ref]
|
mat1 = mesh.materials[mat1_ref]
|
||||||
mat2 = mesh.materials[mat2_ref]
|
mat2 = mesh.materials[mat2_ref]
|
||||||
if None in (mat1, mat2):
|
if None in {mat1, mat2}:
|
||||||
raise MaterialNotFoundError()
|
raise MaterialNotFoundError
|
||||||
except (KeyError, IndexError) as exc:
|
except (KeyError, IndexError) as exc:
|
||||||
# Wrap exceptions within our custom ones
|
# Wrap exceptions within our custom ones
|
||||||
raise MaterialNotFoundError() from exc
|
raise MaterialNotFoundError from exc
|
||||||
|
|
||||||
mat1_idx = mesh.materials.find(mat1.name)
|
mat1_idx = mesh.materials.find(mat1.name)
|
||||||
mat2_idx = mesh.materials.find(mat2.name)
|
mat2_idx = mesh.materials.find(mat2.name)
|
||||||
|
|
||||||
logger.debug(f"Swapping materials: {mat1.name} (idx:{mat1_idx}) <-> {mat2.name} (idx:{mat2_idx}) in {mesh_object.name}")
|
|
||||||
|
|
||||||
# Swap polygons
|
# Swap polygons
|
||||||
for poly in mesh.polygons:
|
for poly in mesh.polygons:
|
||||||
if poly.material_index == mat1_idx:
|
if poly.material_index == mat1_idx:
|
||||||
@@ -123,37 +111,31 @@ class FnMaterial:
|
|||||||
return mat1, mat2
|
return mat1, mat2
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]) -> None:
|
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]):
|
||||||
"""
|
"""Fix the material order which is lost after joining meshes."""
|
||||||
This method will fix the material order. Which is lost after joining meshes.
|
materials = cast("bpy.types.Mesh", meshObj.data).materials
|
||||||
"""
|
|
||||||
materials = cast(bpy.types.Mesh, meshObj.data).materials
|
|
||||||
logger.debug(f"Fixing material order for {meshObj.name}")
|
|
||||||
|
|
||||||
for new_idx, mat in enumerate(material_names):
|
for new_idx, mat in enumerate(material_names):
|
||||||
# Get the material that is currently on this index
|
# Get the material that is currently on this index
|
||||||
other_mat = materials[new_idx]
|
other_mat = materials[new_idx]
|
||||||
if other_mat.name == mat:
|
if other_mat.name == mat:
|
||||||
continue # This is already in place
|
continue # This is already in place
|
||||||
logger.debug(f"Moving material {mat} to index {new_idx}")
|
|
||||||
FnMaterial.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True)
|
FnMaterial.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def material_id(self) -> int:
|
def material_id(self):
|
||||||
mmd_mat: 'MMDMaterial' = self.__material.mmd_material
|
mmd_mat: MMDMaterial = self.__material.mmd_material
|
||||||
if mmd_mat.material_id < 0:
|
if mmd_mat.material_id < 0:
|
||||||
max_id = -1
|
max_id = -1
|
||||||
for mat in bpy.data.materials:
|
for mat in bpy.data.materials:
|
||||||
max_id = max(max_id, mat.mmd_material.material_id)
|
max_id = max(max_id, mat.mmd_material.material_id)
|
||||||
mmd_mat.material_id = max_id + 1
|
mmd_mat.material_id = max_id + 1
|
||||||
logger.debug(f"Assigned new material ID {mmd_mat.material_id} to {self.__material.name}")
|
|
||||||
return mmd_mat.material_id
|
return mmd_mat.material_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def material(self) -> bpy.types.Material:
|
def material(self):
|
||||||
return self.__material
|
return self.__material
|
||||||
|
|
||||||
def __same_image_file(self, image: Optional[bpy.types.Image], filepath: str) -> bool:
|
def __same_image_file(self, image, filepath):
|
||||||
if image and image.source == "FILE":
|
if image and image.source == "FILE":
|
||||||
# pylint: disable=assignment-from-no-return
|
# pylint: disable=assignment-from-no-return
|
||||||
img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user()
|
img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user()
|
||||||
@@ -162,19 +144,18 @@ class FnMaterial:
|
|||||||
# pylint: disable=bare-except
|
# pylint: disable=bare-except
|
||||||
try:
|
try:
|
||||||
return os.path.samefile(img_filepath, filepath)
|
return os.path.samefile(img_filepath, filepath)
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.warning(f"Failed to compare files '{img_filepath}' and '{filepath}': {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _load_image(self, filepath: str) -> bpy.types.Image:
|
def _load_image(self, filepath):
|
||||||
img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None)
|
img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None)
|
||||||
if img is None:
|
if img is None:
|
||||||
# pylint: disable=bare-except
|
# pylint: disable=bare-except
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Loading image: {filepath}")
|
|
||||||
img = bpy.data.images.load(filepath)
|
img = bpy.data.images.load(filepath)
|
||||||
except:
|
except Exception:
|
||||||
logger.warning(f"Cannot create a texture for {filepath}. No such file.")
|
logger.warning("Cannot create a texture for %s. No such file.", filepath)
|
||||||
img = bpy.data.images.new(os.path.basename(filepath), 1, 1)
|
img = bpy.data.images.new(os.path.basename(filepath), 1, 1)
|
||||||
img.source = "FILE"
|
img.source = "FILE"
|
||||||
img.filepath = filepath
|
img.filepath = filepath
|
||||||
@@ -185,46 +166,43 @@ class FnMaterial:
|
|||||||
img.alpha_mode = "NONE"
|
img.alpha_mode = "NONE"
|
||||||
return img
|
return img
|
||||||
|
|
||||||
def update_toon_texture(self) -> None:
|
def update_toon_texture(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
mmd_mat: 'MMDMaterial' = self.__material.mmd_material
|
mmd_mat: MMDMaterial = self.__material.mmd_material
|
||||||
if mmd_mat.is_shared_toon_texture:
|
if mmd_mat.is_shared_toon_texture:
|
||||||
shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "")
|
shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "")
|
||||||
toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1))
|
toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1))
|
||||||
logger.debug(f"Using shared toon texture: {toon_path}")
|
self.create_toon_texture(str(Path(toon_path).resolve()))
|
||||||
self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path))
|
|
||||||
elif mmd_mat.toon_texture != "":
|
elif mmd_mat.toon_texture != "":
|
||||||
logger.debug(f"Using custom toon texture: {mmd_mat.toon_texture}")
|
|
||||||
self.create_toon_texture(mmd_mat.toon_texture)
|
self.create_toon_texture(mmd_mat.toon_texture)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Removing toon texture from {self.__material.name}")
|
|
||||||
self.remove_toon_texture()
|
self.remove_toon_texture()
|
||||||
|
|
||||||
def _mix_diffuse_and_ambient(self, mmd_mat: 'MMDMaterial') -> List[float]:
|
def _mix_diffuse_and_ambient(self, mmd_mat):
|
||||||
r, g, b = mmd_mat.diffuse_color
|
r, g, b = mmd_mat.diffuse_color
|
||||||
ar, ag, ab = mmd_mat.ambient_color
|
ar, ag, ab = mmd_mat.ambient_color
|
||||||
return [min(1.0, 0.5 * r + ar), min(1.0, 0.5 * g + ag), min(1.0, 0.5 * b + ab)]
|
return [min(1.0, 0.5 * r + ar), min(1.0, 0.5 * g + ag), min(1.0, 0.5 * b + ab)]
|
||||||
|
|
||||||
def update_drop_shadow(self) -> None:
|
def update_drop_shadow(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update_enabled_toon_edge(self) -> None:
|
def update_enabled_toon_edge(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
self.update_edge_color()
|
self.update_edge_color()
|
||||||
|
|
||||||
def update_edge_color(self) -> None:
|
def update_edge_color(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
mat = self.__material
|
mat = self.__material
|
||||||
mmd_mat: 'MMDMaterial' = mat.mmd_material
|
mmd_mat: MMDMaterial = mat.mmd_material
|
||||||
color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3]
|
color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3]
|
||||||
line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),)
|
line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),)
|
||||||
if hasattr(mat, "line_color"): # freestyle line color
|
if hasattr(mat, "line_color"): # freestyle line color
|
||||||
mat.line_color = line_color
|
mat.line_color = line_color
|
||||||
|
|
||||||
mat_edge: Optional[bpy.types.Material] = bpy.data.materials.get("mmd_edge." + mat.name, None)
|
mat_edge: bpy.types.Material = bpy.data.materials.get("mmd_edge." + mat.name, None)
|
||||||
if mat_edge:
|
if mat_edge:
|
||||||
mat_edge.mmd_material.edge_color = line_color
|
mat_edge.mmd_material.edge_color = line_color
|
||||||
|
|
||||||
@@ -236,51 +214,44 @@ class FnMaterial:
|
|||||||
if node_shader and "Alpha" in node_shader.inputs:
|
if node_shader and "Alpha" in node_shader.inputs:
|
||||||
node_shader.inputs["Alpha"].default_value = alpha
|
node_shader.inputs["Alpha"].default_value = alpha
|
||||||
|
|
||||||
logger.debug(f"Updated edge color for {mat.name}")
|
def update_edge_weight(self):
|
||||||
|
|
||||||
def update_edge_weight(self) -> None:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_texture(self) -> Optional[_DummyTexture]:
|
def get_texture(self):
|
||||||
return self.__get_texture_node("mmd_base_tex", use_dummy=True)
|
return self.__get_texture_node("mmd_base_tex", use_dummy=True)
|
||||||
|
|
||||||
def create_texture(self, filepath: str) -> _DummyTextureSlot:
|
def create_texture(self, filepath):
|
||||||
texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1))
|
texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1))
|
||||||
logger.debug(f"Created base texture for {self.__material.name}: {filepath}")
|
|
||||||
return _DummyTextureSlot(texture.image)
|
return _DummyTextureSlot(texture.image)
|
||||||
|
|
||||||
def remove_texture(self) -> None:
|
def remove_texture(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
logger.debug(f"Removing base texture from {self.__material.name}")
|
|
||||||
self.__remove_texture_node("mmd_base_tex")
|
self.__remove_texture_node("mmd_base_tex")
|
||||||
|
|
||||||
def get_sphere_texture(self) -> Optional[_DummyTexture]:
|
def get_sphere_texture(self):
|
||||||
return self.__get_texture_node("mmd_sphere_tex", use_dummy=True)
|
return self.__get_texture_node("mmd_sphere_tex", use_dummy=True)
|
||||||
|
|
||||||
def use_sphere_texture(self, use_sphere: bool, obj: Optional[bpy.types.Object] = None) -> None:
|
def use_sphere_texture(self, use_sphere, obj=None):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
if use_sphere:
|
if use_sphere:
|
||||||
logger.debug(f"Enabling sphere texture for {self.__material.name}")
|
|
||||||
self.update_sphere_texture_type(obj)
|
self.update_sphere_texture_type(obj)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Disabling sphere texture for {self.__material.name}")
|
|
||||||
self.__update_shader_input("Sphere Tex Fac", 0)
|
self.__update_shader_input("Sphere Tex Fac", 0)
|
||||||
|
|
||||||
def create_sphere_texture(self, filepath: str, obj: Optional[bpy.types.Object] = None) -> _DummyTextureSlot:
|
def create_sphere_texture(self, filepath, obj=None):
|
||||||
texture = self.__create_texture_node("mmd_sphere_tex", filepath, (-2, -2))
|
texture = self.__create_texture_node("mmd_sphere_tex", filepath, (-2, -2))
|
||||||
logger.debug(f"Created sphere texture for {self.__material.name}: {filepath}")
|
|
||||||
self.update_sphere_texture_type(obj)
|
self.update_sphere_texture_type(obj)
|
||||||
return _DummyTextureSlot(texture.image)
|
return _DummyTextureSlot(texture.image)
|
||||||
|
|
||||||
def update_sphere_texture_type(self, obj: Optional[bpy.types.Object] = None) -> None:
|
def update_sphere_texture_type(self, obj=None):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
sphere_texture_type = int(self.material.mmd_material.sphere_texture_type)
|
sphere_texture_type = int(self.material.mmd_material.sphere_texture_type)
|
||||||
is_sph_add = sphere_texture_type == 2
|
is_sph_add = sphere_texture_type == 2
|
||||||
|
|
||||||
if sphere_texture_type not in (1, 2, 3):
|
if sphere_texture_type not in {1, 2, 3}:
|
||||||
self.__update_shader_input("Sphere Tex Fac", 0)
|
self.__update_shader_input("Sphere Tex Fac", 0)
|
||||||
else:
|
else:
|
||||||
self.__update_shader_input("Sphere Tex Fac", 1)
|
self.__update_shader_input("Sphere Tex Fac", 1)
|
||||||
@@ -298,62 +269,54 @@ class FnMaterial:
|
|||||||
nodes, links = mat.node_tree.nodes, mat.node_tree.links
|
nodes, links = mat.node_tree.nodes, mat.node_tree.links
|
||||||
if sphere_texture_type == 3:
|
if sphere_texture_type == 3:
|
||||||
if obj and obj.type == "MESH" and mat in tuple(obj.data.materials):
|
if obj and obj.type == "MESH" and mat in tuple(obj.data.materials):
|
||||||
uv_layers = (l for l in obj.data.uv_layers if not l.name.startswith("_"))
|
uv_layers = (layer for layer in obj.data.uv_layers if not layer.name.startswith("_"))
|
||||||
next(uv_layers, None) # skip base UV
|
next(uv_layers, None) # skip base UV
|
||||||
subtex_uv = getattr(next(uv_layers, None), "name", "")
|
subtex_uv = getattr(next(uv_layers, None), "name", "")
|
||||||
if subtex_uv != "UV1":
|
if subtex_uv != "UV1":
|
||||||
logger.info(f'Material({mat.name}): object "{obj.name}" use UV "{subtex_uv}" for SubTex')
|
logger.info(' * material(%s): object "%s" use UV "%s" for SubTex', mat.name, obj.name, subtex_uv)
|
||||||
links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"])
|
links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"])
|
||||||
else:
|
else:
|
||||||
links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"])
|
links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"])
|
||||||
|
|
||||||
logger.debug(f"Updated sphere texture type for {self.material.name}: {sphere_texture_type}")
|
def remove_sphere_texture(self):
|
||||||
|
|
||||||
def remove_sphere_texture(self) -> None:
|
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
logger.debug(f"Removing sphere texture from {self.__material.name}")
|
|
||||||
self.__remove_texture_node("mmd_sphere_tex")
|
self.__remove_texture_node("mmd_sphere_tex")
|
||||||
|
|
||||||
def get_toon_texture(self) -> Optional[_DummyTexture]:
|
def get_toon_texture(self):
|
||||||
return self.__get_texture_node("mmd_toon_tex", use_dummy=True)
|
return self.__get_texture_node("mmd_toon_tex", use_dummy=True)
|
||||||
|
|
||||||
def use_toon_texture(self, use_toon: bool) -> None:
|
def use_toon_texture(self, use_toon):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
logger.debug(f"{'Enabling' if use_toon else 'Disabling'} toon texture for {self.__material.name}")
|
|
||||||
self.__update_shader_input("Toon Tex Fac", use_toon)
|
self.__update_shader_input("Toon Tex Fac", use_toon)
|
||||||
|
|
||||||
def create_toon_texture(self, filepath: str) -> _DummyTextureSlot:
|
def create_toon_texture(self, filepath):
|
||||||
texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5))
|
texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5))
|
||||||
logger.debug(f"Created toon texture for {self.__material.name}: {filepath}")
|
|
||||||
return _DummyTextureSlot(texture.image)
|
return _DummyTextureSlot(texture.image)
|
||||||
|
|
||||||
def remove_toon_texture(self) -> None:
|
def remove_toon_texture(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
logger.debug(f"Removing toon texture from {self.__material.name}")
|
|
||||||
self.__remove_texture_node("mmd_toon_tex")
|
self.__remove_texture_node("mmd_toon_tex")
|
||||||
|
|
||||||
def __get_texture_node(self, node_name: str, use_dummy: bool = False) -> Optional[Union[bpy.types.ShaderNodeTexImage, _DummyTexture]]:
|
def __get_texture_node(self, node_name, use_dummy=False):
|
||||||
mat = self.material
|
mat = self.material
|
||||||
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
|
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
|
||||||
if isinstance(texture, bpy.types.ShaderNodeTexImage):
|
if isinstance(texture, bpy.types.ShaderNodeTexImage):
|
||||||
return _DummyTexture(texture.image) if use_dummy else texture
|
return _DummyTexture(texture.image) if use_dummy else texture
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __remove_texture_node(self, node_name: str) -> None:
|
def __remove_texture_node(self, node_name):
|
||||||
mat = self.material
|
mat = self.material
|
||||||
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
|
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
|
||||||
if isinstance(texture, bpy.types.ShaderNodeTexImage):
|
if isinstance(texture, bpy.types.ShaderNodeTexImage):
|
||||||
mat.node_tree.nodes.remove(texture)
|
mat.node_tree.nodes.remove(texture)
|
||||||
mat.update_tag()
|
mat.update_tag()
|
||||||
|
|
||||||
def __create_texture_node(self, node_name: str, filepath: str, pos: Tuple[float, float]) -> bpy.types.ShaderNodeTexImage:
|
def __create_texture_node(self, node_name, filepath, pos):
|
||||||
texture = self.__get_texture_node(node_name)
|
texture = self.__get_texture_node(node_name)
|
||||||
if texture is None:
|
if texture is None:
|
||||||
from mathutils import Vector
|
|
||||||
|
|
||||||
self.__update_shader_nodes()
|
self.__update_shader_nodes()
|
||||||
nodes = self.material.node_tree.nodes
|
nodes = self.material.node_tree.nodes
|
||||||
texture = nodes.new("ShaderNodeTexImage")
|
texture = nodes.new("ShaderNodeTexImage")
|
||||||
@@ -365,25 +328,23 @@ class FnMaterial:
|
|||||||
self.__update_shader_nodes()
|
self.__update_shader_nodes()
|
||||||
return texture
|
return texture
|
||||||
|
|
||||||
def update_ambient_color(self) -> None:
|
def update_ambient_color(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
mat = self.material
|
mat = self.material
|
||||||
mmd_mat = mat.mmd_material
|
mmd_mat = mat.mmd_material
|
||||||
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
|
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
|
||||||
self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,))
|
self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,))
|
||||||
logger.debug(f"Updated ambient color for {mat.name}")
|
|
||||||
|
|
||||||
def update_diffuse_color(self) -> None:
|
def update_diffuse_color(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
mat = self.material
|
mat = self.material
|
||||||
mmd_mat = mat.mmd_material
|
mmd_mat = mat.mmd_material
|
||||||
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
|
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
|
||||||
self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,))
|
self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,))
|
||||||
logger.debug(f"Updated diffuse color for {mat.name}")
|
|
||||||
|
|
||||||
def update_alpha(self) -> None:
|
def update_alpha(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
mat = self.material
|
mat = self.material
|
||||||
@@ -401,31 +362,28 @@ class FnMaterial:
|
|||||||
mat.diffuse_color[3] = mmd_mat.alpha
|
mat.diffuse_color[3] = mmd_mat.alpha
|
||||||
self.__update_shader_input("Alpha", mmd_mat.alpha)
|
self.__update_shader_input("Alpha", mmd_mat.alpha)
|
||||||
self.update_self_shadow_map()
|
self.update_self_shadow_map()
|
||||||
logger.debug(f"Updated alpha for {mat.name}: {mmd_mat.alpha}")
|
|
||||||
|
|
||||||
def update_specular_color(self) -> None:
|
def update_specular_color(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
mat = self.material
|
mat = self.material
|
||||||
mmd_mat = mat.mmd_material
|
mmd_mat = mat.mmd_material
|
||||||
mat.specular_color = mmd_mat.specular_color
|
mat.specular_color = mmd_mat.specular_color
|
||||||
self.__update_shader_input("Specular Color", mmd_mat.specular_color[:] + (1,))
|
self.__update_shader_input("Specular Color", mmd_mat.specular_color[:] + (1,))
|
||||||
logger.debug(f"Updated specular color for {mat.name}")
|
|
||||||
|
|
||||||
def update_shininess(self) -> None:
|
def update_shininess(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
mat = self.material
|
mat = self.material
|
||||||
mmd_mat = mat.mmd_material
|
mmd_mat = mat.mmd_material
|
||||||
mat.roughness = 1 / pow(max(mmd_mat.shininess, 1), 0.37)
|
mat.roughness = 1 / pow(max(mmd_mat.shininess, 1), 0.37)
|
||||||
if hasattr(mat, "metallic"):
|
if hasattr(mat, "metallic"):
|
||||||
mat.metallic = pow(1 - mat.roughness, 2.7)
|
mat.metallic = 0.0
|
||||||
if hasattr(mat, "specular_hardness"):
|
if hasattr(mat, "specular_hardness"):
|
||||||
mat.specular_hardness = mmd_mat.shininess
|
mat.specular_hardness = mmd_mat.shininess
|
||||||
self.__update_shader_input("Reflect", mmd_mat.shininess)
|
self.__update_shader_input("Reflect", mmd_mat.shininess)
|
||||||
logger.debug(f"Updated shininess for {mat.name}: {mmd_mat.shininess}")
|
|
||||||
|
|
||||||
def update_is_double_sided(self) -> None:
|
def update_is_double_sided(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
mat = self.material
|
mat = self.material
|
||||||
@@ -435,9 +393,8 @@ class FnMaterial:
|
|||||||
elif hasattr(mat, "use_backface_culling"):
|
elif hasattr(mat, "use_backface_culling"):
|
||||||
mat.use_backface_culling = not mmd_mat.is_double_sided
|
mat.use_backface_culling = not mmd_mat.is_double_sided
|
||||||
self.__update_shader_input("Double Sided", mmd_mat.is_double_sided)
|
self.__update_shader_input("Double Sided", mmd_mat.is_double_sided)
|
||||||
logger.debug(f"Updated double-sided setting for {mat.name}: {mmd_mat.is_double_sided}")
|
|
||||||
|
|
||||||
def update_self_shadow_map(self) -> None:
|
def update_self_shadow_map(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
mat = self.material
|
mat = self.material
|
||||||
@@ -445,24 +402,21 @@ class FnMaterial:
|
|||||||
cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False
|
cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False
|
||||||
if hasattr(mat, "shadow_method"):
|
if hasattr(mat, "shadow_method"):
|
||||||
mat.shadow_method = "HASHED" if cast_shadows else "NONE"
|
mat.shadow_method = "HASHED" if cast_shadows else "NONE"
|
||||||
logger.debug(f"Updated self shadow map for {mat.name}: {cast_shadows}")
|
|
||||||
|
|
||||||
def update_self_shadow(self) -> None:
|
def update_self_shadow(self):
|
||||||
if self._nodes_are_readonly:
|
if self._nodes_are_readonly:
|
||||||
return
|
return
|
||||||
mat = self.material
|
mat = self.material
|
||||||
mmd_mat = mat.mmd_material
|
mmd_mat = mat.mmd_material
|
||||||
self.__update_shader_input("Self Shadow", mmd_mat.enabled_self_shadow)
|
self.__update_shader_input("Self Shadow", mmd_mat.enabled_self_shadow)
|
||||||
logger.debug(f"Updated self shadow for {mat.name}: {mmd_mat.enabled_self_shadow}")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def convert_to_mmd_material(material: bpy.types.Material, context: bpy.types.Context = bpy.context) -> None:
|
def convert_to_mmd_material(material, context=bpy.context):
|
||||||
m, mmd_material = material, material.mmd_material
|
m, mmd_material = material, material.mmd_material
|
||||||
logger.debug(f"Converting material to MMD material: {material.name}")
|
|
||||||
|
|
||||||
if m.use_nodes and next((n for n in m.node_tree.nodes if n.name.startswith("mmd_")), None) is None:
|
if m.use_nodes and next((n for n in m.node_tree.nodes if n.name.startswith("mmd_")), None) is None:
|
||||||
|
|
||||||
def search_tex_image_node(node: bpy.types.ShaderNode) -> Optional[bpy.types.ShaderNodeTexImage]:
|
def search_tex_image_node(node: bpy.types.ShaderNode):
|
||||||
if node.type == "TEX_IMAGE":
|
if node.type == "TEX_IMAGE":
|
||||||
return node
|
return node
|
||||||
for node_input in node.inputs:
|
for node_input in node.inputs:
|
||||||
@@ -481,7 +435,8 @@ class FnMaterial:
|
|||||||
|
|
||||||
preferred_output_node_target = {
|
preferred_output_node_target = {
|
||||||
"CYCLES": "CYCLES",
|
"CYCLES": "CYCLES",
|
||||||
"BLENDER_EEVEE_NEXT": "EEVEE",
|
"BLENDER_EEVEE": "EEVEE",
|
||||||
|
"BLENDER_EEVEE_NEXT": "EEVEE", # Keep for backwards compatibility with 4.x
|
||||||
}.get(active_render_engine, "ALL")
|
}.get(active_render_engine, "ALL")
|
||||||
|
|
||||||
tex_node = None
|
tex_node = None
|
||||||
@@ -499,15 +454,13 @@ class FnMaterial:
|
|||||||
if tex_node is None:
|
if tex_node is None:
|
||||||
tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None)
|
tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None)
|
||||||
if tex_node:
|
if tex_node:
|
||||||
logger.debug(f"Found texture node for {material.name}: {tex_node.name}")
|
|
||||||
tex_node.name = "mmd_base_tex"
|
tex_node.name = "mmd_base_tex"
|
||||||
else:
|
else:
|
||||||
# Take the Base Color from BSDF if there's no texture
|
# Take the Base Color from BSDF if there's no texture
|
||||||
bsdf_node = next((n for n in m.node_tree.nodes if n.type.startswith('BSDF_')), None)
|
bsdf_node = next((n for n in m.node_tree.nodes if n.type.startswith("BSDF_")), None)
|
||||||
if bsdf_node:
|
if bsdf_node:
|
||||||
base_color_input = bsdf_node.inputs.get('Base Color') or bsdf_node.inputs.get('Color')
|
base_color_input = bsdf_node.inputs.get("Base Color") or bsdf_node.inputs.get("Color")
|
||||||
if base_color_input:
|
if base_color_input:
|
||||||
logger.debug(f"Using BSDF base color for {material.name}")
|
|
||||||
mmd_material.diffuse_color = base_color_input.default_value[:3]
|
mmd_material.diffuse_color = base_color_input.default_value[:3]
|
||||||
# ambient should be half the diffuse
|
# ambient should be half the diffuse
|
||||||
mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color]
|
mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color]
|
||||||
@@ -538,12 +491,11 @@ class FnMaterial:
|
|||||||
|
|
||||||
# delete bsdf node if it's there
|
# delete bsdf node if it's there
|
||||||
if m.use_nodes:
|
if m.use_nodes:
|
||||||
nodes_to_remove = [n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED' or n.type.startswith('BSDF_')]
|
nodes_to_remove = [n for n in m.node_tree.nodes if n.type == "BSDF_PRINCIPLED" or n.type.startswith("BSDF_")]
|
||||||
for n in nodes_to_remove:
|
for n in nodes_to_remove:
|
||||||
logger.debug(f"Removing BSDF node from {material.name}: {n.name}")
|
|
||||||
m.node_tree.nodes.remove(n)
|
m.node_tree.nodes.remove(n)
|
||||||
|
|
||||||
def __update_shader_input(self, name: str, val: Any) -> None:
|
def __update_shader_input(self, name, val):
|
||||||
mat = self.material
|
mat = self.material
|
||||||
if mat.name.startswith("mmd_"): # skip mmd_edge.*
|
if mat.name.startswith("mmd_"): # skip mmd_edge.*
|
||||||
return
|
return
|
||||||
@@ -555,29 +507,26 @@ class FnMaterial:
|
|||||||
val = min(max(val, interface_socket.min_value), interface_socket.max_value)
|
val = min(max(val, interface_socket.min_value), interface_socket.max_value)
|
||||||
shader.inputs[name].default_value = val
|
shader.inputs[name].default_value = val
|
||||||
|
|
||||||
def __update_shader_nodes(self) -> None:
|
def __update_shader_nodes(self):
|
||||||
mat = self.material
|
mat = self.material
|
||||||
if mat.node_tree is None:
|
if mat.node_tree is None:
|
||||||
logger.debug(f"Creating node tree for {mat.name}")
|
|
||||||
mat.use_nodes = True
|
mat.use_nodes = True
|
||||||
mat.node_tree.nodes.clear()
|
mat.node_tree.nodes.clear()
|
||||||
|
|
||||||
nodes, links = mat.node_tree.nodes, mat.node_tree.links
|
nodes, links = mat.node_tree.nodes, mat.node_tree.links
|
||||||
|
|
||||||
class _Dummy:
|
class _Dummy:
|
||||||
default_value: Any = None
|
default_value, is_linked = None, True
|
||||||
is_linked: bool = True
|
|
||||||
|
|
||||||
node_shader = nodes.get("mmd_shader", None)
|
node_shader = nodes.get("mmd_shader", None)
|
||||||
if node_shader is None:
|
if node_shader is None:
|
||||||
logger.debug(f"Creating MMD shader node for {mat.name}")
|
|
||||||
node_shader: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
|
node_shader: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
|
||||||
node_shader.name = "mmd_shader"
|
node_shader.name = "mmd_shader"
|
||||||
node_shader.location = (0, 1500)
|
node_shader.location = (0, 300)
|
||||||
node_shader.width = 200
|
node_shader.width = 200
|
||||||
node_shader.node_tree = self.__get_shader()
|
node_shader.node_tree = self.__get_shader()
|
||||||
|
|
||||||
mmd_mat: 'MMDMaterial' = mat.mmd_material
|
mmd_mat: MMDMaterial = mat.mmd_material
|
||||||
node_shader.inputs.get("Ambient Color", _Dummy).default_value = mmd_mat.ambient_color[:] + (1,)
|
node_shader.inputs.get("Ambient Color", _Dummy).default_value = mmd_mat.ambient_color[:] + (1,)
|
||||||
node_shader.inputs.get("Diffuse Color", _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,)
|
node_shader.inputs.get("Diffuse Color", _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,)
|
||||||
node_shader.inputs.get("Specular Color", _Dummy).default_value = mmd_mat.specular_color[:] + (1,)
|
node_shader.inputs.get("Specular Color", _Dummy).default_value = mmd_mat.specular_color[:] + (1,)
|
||||||
@@ -589,7 +538,6 @@ class FnMaterial:
|
|||||||
|
|
||||||
node_uv = nodes.get("mmd_tex_uv", None)
|
node_uv = nodes.get("mmd_tex_uv", None)
|
||||||
if node_uv is None:
|
if node_uv is None:
|
||||||
logger.debug(f"Creating MMD UV node for {mat.name}")
|
|
||||||
node_uv: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
|
node_uv: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
|
||||||
node_uv.name = "mmd_tex_uv"
|
node_uv.name = "mmd_tex_uv"
|
||||||
node_uv.location = node_shader.location + Vector((-5 * 210, -2.5 * 220))
|
node_uv.location = node_shader.location + Vector((-5 * 210, -2.5 * 220))
|
||||||
@@ -604,7 +552,7 @@ class FnMaterial:
|
|||||||
links.new(node_shader.outputs["Shader"], node_output.inputs["Surface"])
|
links.new(node_shader.outputs["Shader"], node_output.inputs["Surface"])
|
||||||
|
|
||||||
for name_id in ("Base", "Toon", "Sphere"):
|
for name_id in ("Base", "Toon", "Sphere"):
|
||||||
texture = self.__get_texture_node("mmd_%s_tex" % name_id.lower())
|
texture = self.__get_texture_node(f"mmd_{name_id.lower()}_tex")
|
||||||
if texture:
|
if texture:
|
||||||
name_tex_in, name_alpha_in, name_uv_out = (name_id + x for x in (" Tex", " Alpha", " UV"))
|
name_tex_in, name_alpha_in, name_uv_out = (name_id + x for x in (" Tex", " Alpha", " UV"))
|
||||||
if not node_shader.inputs.get(name_tex_in, _Dummy).is_linked:
|
if not node_shader.inputs.get(name_tex_in, _Dummy).is_linked:
|
||||||
@@ -614,13 +562,12 @@ class FnMaterial:
|
|||||||
if not texture.inputs["Vector"].is_linked:
|
if not texture.inputs["Vector"].is_linked:
|
||||||
links.new(node_uv.outputs[name_uv_out], texture.inputs["Vector"])
|
links.new(node_uv.outputs[name_uv_out], texture.inputs["Vector"])
|
||||||
|
|
||||||
def __get_shader_uv(self) -> bpy.types.ShaderNodeTree:
|
def __get_shader_uv(self):
|
||||||
group_name = "MMDTexUV"
|
group_name = "MMDTexUV"
|
||||||
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||||
if len(shader.nodes):
|
if len(shader.nodes):
|
||||||
return shader
|
return shader
|
||||||
|
|
||||||
logger.debug(f"Creating MMD UV shader node group")
|
|
||||||
ng = _NodeGroupUtils(shader)
|
ng = _NodeGroupUtils(shader)
|
||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
@@ -652,13 +599,12 @@ class FnMaterial:
|
|||||||
|
|
||||||
return shader
|
return shader
|
||||||
|
|
||||||
def __get_shader(self) -> bpy.types.ShaderNodeTree:
|
def __get_shader(self):
|
||||||
group_name = "MMDShaderDev"
|
group_name = "MMDShaderDev"
|
||||||
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||||
if len(shader.nodes):
|
if len(shader.nodes):
|
||||||
return shader
|
return shader
|
||||||
|
|
||||||
logger.debug(f"Creating MMD shader node group")
|
|
||||||
ng = _NodeGroupUtils(shader)
|
ng = _NodeGroupUtils(shader)
|
||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
@@ -748,18 +694,15 @@ class FnMaterial:
|
|||||||
|
|
||||||
class MigrationFnMaterial:
|
class MigrationFnMaterial:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_mmd_shader() -> None:
|
def update_mmd_shader():
|
||||||
mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev")
|
mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev")
|
||||||
if mmd_shader_node_tree is None:
|
if mmd_shader_node_tree is None:
|
||||||
logger.debug("No MMD shader node tree found, skipping update")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
ng = _NodeGroupUtils(mmd_shader_node_tree)
|
ng = _NodeGroupUtils(mmd_shader_node_tree)
|
||||||
if "Color" in ng.node_output.inputs:
|
if "Color" in ng.node_output.inputs:
|
||||||
logger.debug("MMD shader already has Color output, skipping update")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info("Updating MMD shader node tree")
|
|
||||||
shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0]
|
shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0]
|
||||||
node_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs["Color"].links[0].from_node
|
node_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs["Color"].links[0].from_node
|
||||||
node_output: bpy.types.NodeGroupOutput = ng.node_output
|
node_output: bpy.types.NodeGroupOutput = ng.node_output
|
||||||
@@ -768,11 +711,3 @@ class MigrationFnMaterial:
|
|||||||
|
|
||||||
ng.new_output_socket("Color", node_sphere.outputs["Color"])
|
ng.new_output_socket("Color", node_sphere.outputs["Color"])
|
||||||
ng.new_output_socket("Alpha", node_alpha.outputs["Value"])
|
ng.new_output_socket("Alpha", node_alpha.outputs["Value"])
|
||||||
logger.info("MMD shader node tree updated successfully")
|
|
||||||
|
|
||||||
# Add Self Shadow input if it doesn't exist
|
|
||||||
if "Self Shadow" not in ng.node_input.outputs:
|
|
||||||
logger.info("Adding Self Shadow input to MMD shader")
|
|
||||||
# Find shader_base_mix node to connect Self Shadow
|
|
||||||
shader_base_mix = shader_alpha_mix.inputs[2].links[0].from_node
|
|
||||||
ng.new_input_socket("Self Shadow", shader_base_mix.inputs["Fac"], 0, min_max=(0, 1))
|
|
||||||
|
|||||||
+595
-302
File diff suppressed because it is too large
Load Diff
+93
-103
@@ -1,39 +1,34 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2016 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
|
||||||
|
|
||||||
|
from ....core.logging_setup import logger
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING, Tuple, cast, List, Dict, Optional, Set, Any, Union, Iterator
|
from typing import TYPE_CHECKING, Tuple, cast
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
|
||||||
from bpy.types import Object, ShapeKey, Material, Mesh, Armature, PoseBone, Constraint
|
|
||||||
|
|
||||||
from .. import bpyutils, utils
|
from .. import bpyutils, utils
|
||||||
from ..bpyutils import FnContext, FnObject, TransformConstraintOp
|
from ..bpyutils import FnContext, FnObject, TransformConstraintOp
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .model import Model
|
from .model import Model
|
||||||
|
|
||||||
|
|
||||||
class FnMorph:
|
class FnMorph:
|
||||||
def __init__(self, morph: Any, model: "Model"):
|
def __init__(self, morph, model: "Model"):
|
||||||
self.__morph = morph
|
self.__morph = morph
|
||||||
self.__rig = model
|
self.__rig = model
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def storeShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None:
|
def storeShapeKeyOrder(cls, obj, shape_key_names):
|
||||||
if len(shape_key_names) < 1:
|
if len(shape_key_names) < 1:
|
||||||
return
|
return
|
||||||
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
|
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
|
||||||
if obj.data.shape_keys is None:
|
if obj.data.shape_keys is None:
|
||||||
bpy.ops.object.shape_key_add()
|
bpy.ops.object.shape_key_add()
|
||||||
|
|
||||||
def __move_to_bottom(key_blocks: bpy.types.bpy_prop_collection, name: str) -> None:
|
def __move_to_bottom(key_blocks, name):
|
||||||
obj.active_shape_key_index = key_blocks.find(name)
|
obj.active_shape_key_index = key_blocks.find(name)
|
||||||
bpy.ops.object.shape_key_move(type="BOTTOM")
|
bpy.ops.object.shape_key_move(type="BOTTOM")
|
||||||
|
|
||||||
@@ -45,7 +40,7 @@ class FnMorph:
|
|||||||
__move_to_bottom(key_blocks, name)
|
__move_to_bottom(key_blocks, name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fixShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None:
|
def fixShapeKeyOrder(cls, obj, shape_key_names):
|
||||||
if len(shape_key_names) < 1:
|
if len(shape_key_names) < 1:
|
||||||
return
|
return
|
||||||
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
|
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
|
||||||
@@ -60,11 +55,11 @@ class FnMorph:
|
|||||||
bpy.ops.object.shape_key_move(type="BOTTOM")
|
bpy.ops.object.shape_key_move(type="BOTTOM")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_morph_slider(rig: "Model") -> "_MorphSlider":
|
def get_morph_slider(rig):
|
||||||
return _MorphSlider(rig)
|
return _MorphSlider(rig)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def category_guess(morph: Any) -> None:
|
def category_guess(morph):
|
||||||
name_lower = morph.name.lower()
|
name_lower = morph.name.lower()
|
||||||
if "mouth" in name_lower:
|
if "mouth" in name_lower:
|
||||||
morph.category = "MOUTH"
|
morph.category = "MOUTH"
|
||||||
@@ -75,7 +70,7 @@ class FnMorph:
|
|||||||
morph.category = "EYE"
|
morph.category = "EYE"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_morphs(cls, rig: "Model") -> None:
|
def load_morphs(cls, rig):
|
||||||
mmd_root = rig.rootObject().mmd_root
|
mmd_root = rig.rootObject().mmd_root
|
||||||
vertex_morphs = mmd_root.vertex_morphs
|
vertex_morphs = mmd_root.vertex_morphs
|
||||||
uv_morphs = mmd_root.uv_morphs
|
uv_morphs = mmd_root.uv_morphs
|
||||||
@@ -94,7 +89,7 @@ class FnMorph:
|
|||||||
cls.category_guess(item)
|
cls.category_guess(item)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def remove_shape_key(mesh_object: Object, shape_key_name: str) -> None:
|
def remove_shape_key(mesh_object: bpy.types.Object, shape_key_name: str):
|
||||||
assert isinstance(mesh_object.data, bpy.types.Mesh)
|
assert isinstance(mesh_object.data, bpy.types.Mesh)
|
||||||
|
|
||||||
shape_keys = mesh_object.data.shape_keys
|
shape_keys = mesh_object.data.shape_keys
|
||||||
@@ -106,7 +101,7 @@ class FnMorph:
|
|||||||
FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name])
|
FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def copy_shape_key(mesh_object: Object, src_name: str, dest_name: str) -> None:
|
def copy_shape_key(mesh_object: bpy.types.Object, src_name: str, dest_name: str):
|
||||||
assert isinstance(mesh_object.data, bpy.types.Mesh)
|
assert isinstance(mesh_object.data, bpy.types.Mesh)
|
||||||
|
|
||||||
shape_keys = mesh_object.data.shape_keys
|
shape_keys = mesh_object.data.shape_keys
|
||||||
@@ -128,13 +123,13 @@ class FnMorph:
|
|||||||
mesh_object.active_shape_key_index = key_blocks.find(dest_name)
|
mesh_object.active_shape_key_index = key_blocks.find(dest_name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_uv_morph_vertex_groups(obj: Object, morph_name: Optional[str] = None, offset_axes: str = "XYZW") -> Iterator[Tuple[bpy.types.VertexGroup, str, str]]:
|
def get_uv_morph_vertex_groups(obj, morph_name=None, offset_axes="XYZW"):
|
||||||
pattern = "UV_%s[+-][%s]$" % (morph_name or ".{1,}", offset_axes or "XYZW")
|
pattern = "UV_%s[+-][%s]$" % (morph_name or ".{1,}", offset_axes or "XYZW")
|
||||||
# yield (vertex_group, morph_name, axis),...
|
# yield (vertex_group, morph_name, axis),...
|
||||||
return ((g, g.name[3:-2], g.name[-2:]) for g in obj.vertex_groups if re.match(pattern, g.name))
|
return ((g, g.name[3:-2], g.name[-2:]) for g in obj.vertex_groups if re.match(pattern, g.name))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def copy_uv_morph_vertex_groups(obj: Object, src_name: str, dest_name: str) -> None:
|
def copy_uv_morph_vertex_groups(obj, src_name, dest_name):
|
||||||
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name):
|
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name):
|
||||||
obj.vertex_groups.remove(vg)
|
obj.vertex_groups.remove(vg)
|
||||||
|
|
||||||
@@ -145,12 +140,12 @@ class FnMorph:
|
|||||||
obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name)
|
obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def overwrite_bone_morphs_from_action_pose(armature_object: Object) -> None:
|
def overwrite_bone_morphs_from_action_pose(armature_object):
|
||||||
armature = armature_object.id_data
|
armature = armature_object.id_data
|
||||||
|
|
||||||
# Use animation_data and action instead of action_pose
|
# Use animation_data and action instead of action_pose
|
||||||
if armature.animation_data is None or armature.animation_data.action is None:
|
if armature.animation_data is None or armature.animation_data.action is None:
|
||||||
logger.warning('Armature "%s" has no animation data or action', armature_object.name)
|
logger.warning('[WARNING] armature "%s" has no animation data or action', armature_object.name)
|
||||||
return
|
return
|
||||||
|
|
||||||
action = armature.animation_data.action
|
action = armature.animation_data.action
|
||||||
@@ -164,7 +159,7 @@ class FnMorph:
|
|||||||
bone_morphs = mmd_root.bone_morphs
|
bone_morphs = mmd_root.bone_morphs
|
||||||
|
|
||||||
utils.selectAObject(armature_object)
|
utils.selectAObject(armature_object)
|
||||||
original_mode = bpy.context.object.mode
|
original_mode = bpy.context.active_object.mode
|
||||||
bpy.ops.object.mode_set(mode="POSE")
|
bpy.ops.object.mode_set(mode="POSE")
|
||||||
try:
|
try:
|
||||||
for index, pose_marker in enumerate(pose_markers):
|
for index, pose_marker in enumerate(pose_markers):
|
||||||
@@ -189,9 +184,9 @@ class FnMorph:
|
|||||||
utils.selectAObject(root)
|
utils.selectAObject(root)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_uv_morph_vertex_groups(obj: Object) -> None:
|
def clean_uv_morph_vertex_groups(obj):
|
||||||
# remove empty vertex groups of uv morphs
|
# remove empty vertex groups of uv morphs
|
||||||
vg_indices: Set[int] = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
|
vg_indices = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
|
||||||
vertex_groups = obj.vertex_groups
|
vertex_groups = obj.vertex_groups
|
||||||
for v in obj.data.vertices:
|
for v in obj.data.vertices:
|
||||||
for x in v.groups:
|
for x in v.groups:
|
||||||
@@ -205,8 +200,8 @@ class FnMorph:
|
|||||||
vertex_groups.remove(vg)
|
vertex_groups.remove(vg)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_uv_morph_offset_map(obj: Object, morph: Any) -> Dict[int, List[float]]:
|
def get_uv_morph_offset_map(obj, morph):
|
||||||
offset_map: Dict[int, List[float]] = {} # offset_map[vertex_index] = offset_xyzw
|
offset_map = {} # offset_map[vertex_index] = offset_xyzw
|
||||||
if morph.data_type == "VERTEX_GROUP":
|
if morph.data_type == "VERTEX_GROUP":
|
||||||
scale = morph.vertex_group_scale
|
scale = morph.vertex_group_scale
|
||||||
axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)}
|
axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)}
|
||||||
@@ -221,13 +216,13 @@ class FnMorph:
|
|||||||
for val in morph.data:
|
for val in morph.data:
|
||||||
i = val.index
|
i = val.index
|
||||||
if i in offset_map:
|
if i in offset_map:
|
||||||
offset_map[i] = [a + b for a, b in zip(offset_map[i], val.offset)]
|
offset_map[i] = [a + b for a, b in zip(offset_map[i], val.offset, strict=False)]
|
||||||
else:
|
else:
|
||||||
offset_map[i] = val.offset
|
offset_map[i] = val.offset
|
||||||
return offset_map
|
return offset_map
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def store_uv_morph_data(obj: Object, morph: Any, offsets: Optional[List[Any]] = None, offset_axes: str = "XYZW") -> None:
|
def store_uv_morph_data(obj, morph, offsets=None, offset_axes="XYZW"):
|
||||||
vertex_groups = obj.vertex_groups
|
vertex_groups = obj.vertex_groups
|
||||||
morph_name = getattr(morph, "name", None)
|
morph_name = getattr(morph, "name", None)
|
||||||
if offset_axes:
|
if offset_axes:
|
||||||
@@ -246,13 +241,13 @@ class FnMorph:
|
|||||||
max_value = max(max(abs(x) for x in v) for v in offset_map.values() or ([0],))
|
max_value = max(max(abs(x) for x in v) for v in offset_map.values() or ([0],))
|
||||||
scale = morph.vertex_group_scale = max(abs(morph.vertex_group_scale), max_value)
|
scale = morph.vertex_group_scale = max(abs(morph.vertex_group_scale), max_value)
|
||||||
for idx, offset in offset_map.items():
|
for idx, offset in offset_map.items():
|
||||||
for val, axis in zip(offset, "XYZW"):
|
for val, axis in zip(offset, "XYZW", strict=False):
|
||||||
if abs(val) > 1e-4:
|
if abs(val) > 1e-4:
|
||||||
vg_name = "UV_{0}{1}{2}".format(morph_name, "-" if val < 0 else "+", axis)
|
vg_name = f"UV_{morph_name}{'-' if val < 0 else '+'}{axis}"
|
||||||
vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name)
|
vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name)
|
||||||
vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE")
|
vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE")
|
||||||
|
|
||||||
def update_mat_related_mesh(self, new_mesh: Optional[Object] = None) -> None:
|
def update_mat_related_mesh(self, new_mesh=None):
|
||||||
for offset in self.__morph.data:
|
for offset in self.__morph.data:
|
||||||
# Use the new_mesh if provided
|
# Use the new_mesh if provided
|
||||||
meshObj = new_mesh
|
meshObj = new_mesh
|
||||||
@@ -272,28 +267,28 @@ class FnMorph:
|
|||||||
offset.related_mesh = meshObj.data.name
|
offset.related_mesh = meshObj.data.name
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_duplicated_material_morphs(mmd_root_object: Object) -> None:
|
def clean_duplicated_material_morphs(mmd_root_object: bpy.types.Object):
|
||||||
"""Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]"""
|
"""Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]"""
|
||||||
mmd_root = mmd_root_object.mmd_root
|
mmd_root = mmd_root_object.mmd_root
|
||||||
|
|
||||||
def morph_data_equals(l: Any, r: Any) -> bool:
|
def morph_data_equals(left, right) -> bool:
|
||||||
return (
|
return (
|
||||||
l.related_mesh_data == r.related_mesh_data
|
left.related_mesh_data == right.related_mesh_data
|
||||||
and l.offset_type == r.offset_type
|
and left.offset_type == right.offset_type
|
||||||
and l.material == r.material
|
and left.material == right.material
|
||||||
and all(a == b for a, b in zip(l.diffuse_color, r.diffuse_color))
|
and all(a == b for a, b in zip(left.diffuse_color, right.diffuse_color, strict=False))
|
||||||
and all(a == b for a, b in zip(l.specular_color, r.specular_color))
|
and all(a == b for a, b in zip(left.specular_color, right.specular_color, strict=False))
|
||||||
and l.shininess == r.shininess
|
and left.shininess == right.shininess
|
||||||
and all(a == b for a, b in zip(l.ambient_color, r.ambient_color))
|
and all(a == b for a, b in zip(left.ambient_color, right.ambient_color, strict=False))
|
||||||
and all(a == b for a, b in zip(l.edge_color, r.edge_color))
|
and all(a == b for a, b in zip(left.edge_color, right.edge_color, strict=False))
|
||||||
and l.edge_weight == r.edge_weight
|
and left.edge_weight == right.edge_weight
|
||||||
and all(a == b for a, b in zip(l.texture_factor, r.texture_factor))
|
and all(a == b for a, b in zip(left.texture_factor, right.texture_factor, strict=False))
|
||||||
and all(a == b for a, b in zip(l.sphere_texture_factor, r.sphere_texture_factor))
|
and all(a == b for a, b in zip(left.sphere_texture_factor, right.sphere_texture_factor, strict=False))
|
||||||
and all(a == b for a, b in zip(l.toon_texture_factor, r.toon_texture_factor))
|
and all(a == b for a, b in zip(left.toon_texture_factor, right.toon_texture_factor, strict=False))
|
||||||
)
|
)
|
||||||
|
|
||||||
def morph_equals(l: Any, r: Any) -> bool:
|
def morph_equals(left, right) -> bool:
|
||||||
return len(l.data) == len(r.data) and all(morph_data_equals(a, b) for a, b in zip(l.data, r.data))
|
return len(left.data) == len(right.data) and all(morph_data_equals(a, b) for a, b in zip(left.data, right.data, strict=False))
|
||||||
|
|
||||||
# Remove duplicated mmd_root.material_morphs.data[]
|
# Remove duplicated mmd_root.material_morphs.data[]
|
||||||
for material_morph in mmd_root.material_morphs:
|
for material_morph in mmd_root.material_morphs:
|
||||||
@@ -327,7 +322,7 @@ class _MorphSlider:
|
|||||||
def __init__(self, model: "Model"):
|
def __init__(self, model: "Model"):
|
||||||
self.__rig = model
|
self.__rig = model
|
||||||
|
|
||||||
def placeholder(self, create: bool = False, binded: bool = False) -> Optional[Object]:
|
def placeholder(self, create=False, binded=False):
|
||||||
rig = self.__rig
|
rig = self.__rig
|
||||||
root = rig.rootObject()
|
root = rig.rootObject()
|
||||||
obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None)
|
obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None)
|
||||||
@@ -345,11 +340,11 @@ class _MorphSlider:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dummy_armature(self) -> Optional[Object]:
|
def dummy_armature(self):
|
||||||
obj = self.placeholder()
|
obj = self.placeholder()
|
||||||
return self.__dummy_armature(obj) if obj else None
|
return self.__dummy_armature(obj) if obj else None
|
||||||
|
|
||||||
def __dummy_armature(self, obj: Object, create: bool = False) -> Optional[Object]:
|
def __dummy_armature(self, obj, create=False):
|
||||||
arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None)
|
arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None)
|
||||||
if create and arm is None:
|
if create and arm is None:
|
||||||
arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature"))
|
arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature"))
|
||||||
@@ -362,7 +357,7 @@ class _MorphSlider:
|
|||||||
FnBone.setup_special_bone_collections(arm)
|
FnBone.setup_special_bone_collections(arm)
|
||||||
return arm
|
return arm
|
||||||
|
|
||||||
def get(self, morph_name: str) -> Optional[ShapeKey]:
|
def get(self, morph_name):
|
||||||
obj = self.placeholder()
|
obj = self.placeholder()
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return None
|
return None
|
||||||
@@ -371,13 +366,13 @@ class _MorphSlider:
|
|||||||
return None
|
return None
|
||||||
return key_blocks.get(morph_name, None)
|
return key_blocks.get(morph_name, None)
|
||||||
|
|
||||||
def create(self) -> Object:
|
def create(self):
|
||||||
self.__rig.loadMorphs()
|
self.__rig.loadMorphs()
|
||||||
obj = self.placeholder(create=True)
|
obj = self.placeholder(create=True)
|
||||||
self.__load(obj, self.__rig.rootObject().mmd_root)
|
self.__load(obj, self.__rig.rootObject().mmd_root)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def __load(self, obj: Object, mmd_root: Any) -> None:
|
def __load(self, obj, mmd_root):
|
||||||
attr_list = ("group", "vertex", "bone", "uv", "material")
|
attr_list = ("group", "vertex", "bone", "uv", "material")
|
||||||
morph_sliders = obj.data.shape_keys.key_blocks
|
morph_sliders = obj.data.shape_keys.key_blocks
|
||||||
for m in (x for attr in attr_list for x in getattr(mmd_root, attr + "_morphs", ())):
|
for m in (x for attr in attr_list for x in getattr(mmd_root, attr + "_morphs", ())):
|
||||||
@@ -388,15 +383,15 @@ class _MorphSlider:
|
|||||||
obj.shape_key_add(name=name, from_mix=False)
|
obj.shape_key_add(name=name, from_mix=False)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __driver_variables(id_data: Any, path: str, index: int = -1) -> Tuple[Any, Any]:
|
def __driver_variables(id_data, path, index=-1):
|
||||||
d = id_data.driver_add(path, index)
|
d = id_data.driver_add(path, index)
|
||||||
variables = d.driver.variables
|
variables = d.driver.variables
|
||||||
for x in variables:
|
for x in reversed(variables):
|
||||||
variables.remove(x)
|
variables.remove(x)
|
||||||
return d.driver, variables
|
return d.driver, variables
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __add_single_prop(variables: Any, id_obj: Object, data_path: str, prefix: str) -> Any:
|
def __add_single_prop(variables, id_obj, data_path, prefix):
|
||||||
var = variables.new()
|
var = variables.new()
|
||||||
var.name = f"{prefix}{len(variables)}"
|
var.name = f"{prefix}{len(variables)}"
|
||||||
var.type = "SINGLE_PROP"
|
var.type = "SINGLE_PROP"
|
||||||
@@ -407,7 +402,7 @@ class _MorphSlider:
|
|||||||
return var
|
return var
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __shape_key_driver_check(key_block: ShapeKey, resolve_path: bool = False) -> bool:
|
def __shape_key_driver_check(key_block, resolve_path=False):
|
||||||
if resolve_path:
|
if resolve_path:
|
||||||
try:
|
try:
|
||||||
key_block.id_data.path_resolve(key_block.path_from_id())
|
key_block.id_data.path_resolve(key_block.path_from_id())
|
||||||
@@ -421,22 +416,20 @@ class _MorphSlider:
|
|||||||
d = next((i for i in key_block.id_data.animation_data.drivers if i.data_path == data_path), None)
|
d = next((i for i in key_block.id_data.animation_data.drivers if i.data_path == data_path), None)
|
||||||
return not d or d.driver.expression == "".join(("*w", "+g", "v")[-1 if i < 1 else i % 2] + str(i + 1) for i in range(len(d.driver.variables)))
|
return not d or d.driver.expression == "".join(("*w", "+g", "v")[-1 if i < 1 else i % 2] + str(i + 1) for i in range(len(d.driver.variables)))
|
||||||
|
|
||||||
def __cleanup(self, names_in_use: Optional[Dict[str, Any]] = None) -> None:
|
def __cleanup(self, names_in_use=None):
|
||||||
from math import ceil, floor
|
|
||||||
|
|
||||||
names_in_use = names_in_use or {}
|
names_in_use = names_in_use or {}
|
||||||
rig = self.__rig
|
rig = self.__rig
|
||||||
morph_sliders = self.placeholder()
|
morph_sliders = self.placeholder()
|
||||||
morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {}
|
morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {}
|
||||||
for mesh_object in rig.meshes():
|
for mesh_object in rig.meshes():
|
||||||
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[ShapeKey], ())):
|
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast("Tuple[bpy.types.ShapeKey]", ())):
|
||||||
if kb.name in names_in_use:
|
if kb.name in names_in_use:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if kb.name.startswith("mmd_bind"):
|
if kb.name.startswith("mmd_bind"):
|
||||||
kb.driver_remove("value")
|
kb.driver_remove("value")
|
||||||
ms = morph_sliders[kb.relative_key.name]
|
ms = morph_sliders[kb.relative_key.name]
|
||||||
kb.relative_key.slider_min, kb.relative_key.slider_max = min(ms.slider_min, floor(ms.value)), max(ms.slider_max, ceil(ms.value))
|
kb.relative_key.slider_min, kb.relative_key.slider_max = min(ms.slider_min, math.floor(ms.value)), max(ms.slider_max, math.ceil(ms.value))
|
||||||
kb.relative_key.value = ms.value
|
kb.relative_key.value = ms.value
|
||||||
kb.relative_key.mute = False
|
kb.relative_key.mute = False
|
||||||
FnObject.mesh_remove_shape_key(mesh_object, kb)
|
FnObject.mesh_remove_shape_key(mesh_object, kb)
|
||||||
@@ -444,9 +437,9 @@ class _MorphSlider:
|
|||||||
elif kb.name in morph_sliders and self.__shape_key_driver_check(kb):
|
elif kb.name in morph_sliders and self.__shape_key_driver_check(kb):
|
||||||
ms = morph_sliders[kb.name]
|
ms = morph_sliders[kb.name]
|
||||||
kb.driver_remove("value")
|
kb.driver_remove("value")
|
||||||
kb.slider_min, kb.slider_max = min(ms.slider_min, floor(kb.value)), max(ms.slider_max, ceil(kb.value))
|
kb.slider_min, kb.slider_max = min(ms.slider_min, math.floor(kb.value)), max(ms.slider_max, math.ceil(kb.value))
|
||||||
|
|
||||||
for m in mesh_object.modifiers: # uv morph
|
for m in reversed(mesh_object.modifiers): # uv morph
|
||||||
if m.name.startswith("mmd_bind") and m.name not in names_in_use:
|
if m.name.startswith("mmd_bind") and m.name not in names_in_use:
|
||||||
mesh_object.modifiers.remove(m)
|
mesh_object.modifiers.remove(m)
|
||||||
|
|
||||||
@@ -461,13 +454,13 @@ class _MorphSlider:
|
|||||||
attributes = set(TransformConstraintOp.min_max_attributes("LOCATION", "to"))
|
attributes = set(TransformConstraintOp.min_max_attributes("LOCATION", "to"))
|
||||||
attributes |= set(TransformConstraintOp.min_max_attributes("ROTATION", "to"))
|
attributes |= set(TransformConstraintOp.min_max_attributes("ROTATION", "to"))
|
||||||
for b in rig.armature().pose.bones:
|
for b in rig.armature().pose.bones:
|
||||||
for c in b.constraints:
|
for c in reversed(b.constraints):
|
||||||
if c.name.startswith("mmd_bind") and c.name[:-4] not in names_in_use:
|
if c.name.startswith("mmd_bind") and c.name[:-4] not in names_in_use:
|
||||||
for attr in attributes:
|
for attr in attributes:
|
||||||
c.driver_remove(attr)
|
c.driver_remove(attr)
|
||||||
b.constraints.remove(c)
|
b.constraints.remove(c)
|
||||||
|
|
||||||
def unbind(self) -> None:
|
def unbind(self):
|
||||||
mmd_root = self.__rig.rootObject().mmd_root
|
mmd_root = self.__rig.rootObject().mmd_root
|
||||||
|
|
||||||
# after unbind, the weird lag problem will disappear.
|
# after unbind, the weird lag problem will disappear.
|
||||||
@@ -490,7 +483,7 @@ class _MorphSlider:
|
|||||||
b.driver_remove("rotation_quaternion")
|
b.driver_remove("rotation_quaternion")
|
||||||
self.__cleanup()
|
self.__cleanup()
|
||||||
|
|
||||||
def bind(self) -> None:
|
def bind(self):
|
||||||
rig = self.__rig
|
rig = self.__rig
|
||||||
root = rig.rootObject()
|
root = rig.rootObject()
|
||||||
armObj = rig.armature()
|
armObj = rig.armature()
|
||||||
@@ -504,10 +497,10 @@ class _MorphSlider:
|
|||||||
morph_sliders = obj.data.shape_keys.key_blocks
|
morph_sliders = obj.data.shape_keys.key_blocks
|
||||||
|
|
||||||
# data gathering
|
# data gathering
|
||||||
group_map: Dict[Tuple[str, str], List[List[Any]]] = {}
|
group_map = {}
|
||||||
|
|
||||||
shape_key_map: Dict[str, List[Tuple[ShapeKey, str, List[Any]]]] = {}
|
shape_key_map = {}
|
||||||
uv_morph_map: Dict[str, List[Tuple[str, str, str, List[Any]]]] = {}
|
uv_morph_map = {}
|
||||||
for mesh_object in rig.meshes():
|
for mesh_object in rig.meshes():
|
||||||
mesh_object.show_only_shape_key = False
|
mesh_object.show_only_shape_key = False
|
||||||
key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ())
|
key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ())
|
||||||
@@ -528,11 +521,11 @@ class _MorphSlider:
|
|||||||
kb_bind.slider_max = 10
|
kb_bind.slider_max = 10
|
||||||
|
|
||||||
data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"')
|
data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"')
|
||||||
groups: List[Any] = []
|
groups = []
|
||||||
shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups))
|
shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups))
|
||||||
group_map.setdefault(("vertex_morphs", kb_name), []).append(groups)
|
group_map.setdefault(("vertex_morphs", kb_name), []).append(groups)
|
||||||
|
|
||||||
uv_layers = [l.name for l in mesh_object.data.uv_layers if not l.name.startswith("_")]
|
uv_layers = [layer.name for layer in mesh_object.data.uv_layers if not layer.name.startswith("_")]
|
||||||
uv_layers += [""] * (5 - len(uv_layers))
|
uv_layers += [""] * (5 - len(uv_layers))
|
||||||
for vg, morph_name, axis in FnMorph.get_uv_morph_vertex_groups(mesh_object):
|
for vg, morph_name, axis in FnMorph.get_uv_morph_vertex_groups(mesh_object):
|
||||||
morph = mmd_root.uv_morphs.get(morph_name, None)
|
morph = mmd_root.uv_morphs.get(morph_name, None)
|
||||||
@@ -544,7 +537,7 @@ class _MorphSlider:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
name_bind = "mmd_bind%s" % hash(vg.name)
|
name_bind = "mmd_bind%s" % hash(vg.name)
|
||||||
uv_morph_map.setdefault(name_bind, [])
|
uv_morph_map.setdefault(name_bind, ())
|
||||||
mod = mesh_object.modifiers.get(name_bind, None) or mesh_object.modifiers.new(name=name_bind, type="UV_WARP")
|
mod = mesh_object.modifiers.get(name_bind, None) or mesh_object.modifiers.new(name=name_bind, type="UV_WARP")
|
||||||
mod.show_expanded = False
|
mod.show_expanded = False
|
||||||
mod.vertex_group = vg.name
|
mod.vertex_group = vg.name
|
||||||
@@ -557,13 +550,13 @@ class _MorphSlider:
|
|||||||
else:
|
else:
|
||||||
mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base"
|
mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base"
|
||||||
|
|
||||||
bone_offset_map: Dict[str, Tuple[str, Any, str, str, List[Any]]] = {}
|
bone_offset_map = {}
|
||||||
with bpyutils.edit_object(arm) as data:
|
with bpyutils.edit_object(arm) as data:
|
||||||
from .bone import FnBone
|
from .bone import FnBone
|
||||||
|
|
||||||
edit_bones = data.edit_bones
|
edit_bones = data.edit_bones
|
||||||
|
|
||||||
def __get_bone(name: str, parent: Optional[bpy.types.EditBone]) -> bpy.types.EditBone:
|
def __get_bone(name, parent):
|
||||||
b = edit_bones.get(name, None) or edit_bones.new(name=name)
|
b = edit_bones.get(name, None) or edit_bones.new(name=name)
|
||||||
b.head = (0, 0, 0)
|
b.head = (0, 0, 0)
|
||||||
b.tail = (0, 0, 1)
|
b.tail = (0, 0, 1)
|
||||||
@@ -580,7 +573,7 @@ class _MorphSlider:
|
|||||||
continue
|
continue
|
||||||
d.name = name_bind = f"mmd_bind{hash(d)}"
|
d.name = name_bind = f"mmd_bind{hash(d)}"
|
||||||
b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None))
|
b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None))
|
||||||
groups: List[Any] = []
|
groups = []
|
||||||
bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups)
|
bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups)
|
||||||
group_map.setdefault(("bone_morphs", m.name), []).append(groups)
|
group_map.setdefault(("bone_morphs", m.name), []).append(groups)
|
||||||
|
|
||||||
@@ -591,21 +584,21 @@ class _MorphSlider:
|
|||||||
scale_path = f'mmd_root.uv_morphs["{morph_name}"].vertex_group_scale'
|
scale_path = f'mmd_root.uv_morphs["{morph_name}"].vertex_group_scale'
|
||||||
name_bind = f"mmd_bind{hash(m.name)}"
|
name_bind = f"mmd_bind{hash(m.name)}"
|
||||||
b = FnBone.set_edit_bone_to_dummy(__get_bone(name_bind, ctrl_base))
|
b = FnBone.set_edit_bone_to_dummy(__get_bone(name_bind, ctrl_base))
|
||||||
groups: List[Any] = []
|
groups = []
|
||||||
uv_morph_map.setdefault(name_bind, []).append((b.name, data_path, scale_path, groups))
|
uv_morph_map.setdefault(name_bind, []).append((b.name, data_path, scale_path, groups))
|
||||||
group_map.setdefault(("uv_morphs", m.name), []).append(groups)
|
group_map.setdefault(("uv_morphs", m.name), []).append(groups)
|
||||||
|
|
||||||
used_bone_names: Set[str] = set(bone_offset_map.keys()) | set(uv_morph_map.keys())
|
used_bone_names = bone_offset_map.keys() | uv_morph_map.keys()
|
||||||
used_bone_names.add(ctrl_base.name)
|
used_bone_names.add(ctrl_base.name)
|
||||||
for b in edit_bones: # cleanup
|
for b in reversed(edit_bones): # cleanup
|
||||||
if b.name.startswith("mmd_bind") and b.name not in used_bone_names:
|
if b.name.startswith("mmd_bind") and b.name not in used_bone_names:
|
||||||
edit_bones.remove(b)
|
edit_bones.remove(b)
|
||||||
|
|
||||||
material_offset_map: Dict[str, Any] = {}
|
material_offset_map = {}
|
||||||
for m in mmd_root.material_morphs:
|
for m in mmd_root.material_morphs:
|
||||||
morph_name = m.name.replace('"', '\\"')
|
morph_name = m.name.replace('"', '\\"')
|
||||||
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
|
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
|
||||||
groups: List[Any] = []
|
groups = []
|
||||||
group_map.setdefault(("material_morphs", m.name), []).append(groups)
|
group_map.setdefault(("material_morphs", m.name), []).append(groups)
|
||||||
material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups)
|
material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups)
|
||||||
for d in m.data:
|
for d in m.data:
|
||||||
@@ -616,7 +609,7 @@ class _MorphSlider:
|
|||||||
|
|
||||||
for m in mmd_root.group_morphs:
|
for m in mmd_root.group_morphs:
|
||||||
if len(m.data) != len(set(m.data.keys())):
|
if len(m.data) != len(set(m.data.keys())):
|
||||||
logger.warning('Found duplicated morph data in Group Morph "%s"', m.name)
|
logger.warning(' * Found duplicated morph data in Group Morph "%s"', m.name)
|
||||||
morph_name = m.name.replace('"', '\\"')
|
morph_name = m.name.replace('"', '\\"')
|
||||||
morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
|
morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
|
||||||
for d in m.data:
|
for d in m.data:
|
||||||
@@ -627,7 +620,7 @@ class _MorphSlider:
|
|||||||
|
|
||||||
self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys())
|
self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys())
|
||||||
|
|
||||||
def __config_groups(variables: Any, expression: str, groups: List[Any]) -> str:
|
def __config_groups(variables, expression, groups):
|
||||||
for g_name, morph_path, factor_path in groups:
|
for g_name, morph_path, factor_path in groups:
|
||||||
var = self.__add_single_prop(variables, obj, morph_path, "g")
|
var = self.__add_single_prop(variables, obj, morph_path, "g")
|
||||||
fvar = self.__add_single_prop(variables, root, factor_path, "w")
|
fvar = self.__add_single_prop(variables, root, factor_path, "w")
|
||||||
@@ -635,7 +628,7 @@ class _MorphSlider:
|
|||||||
return expression
|
return expression
|
||||||
|
|
||||||
# vertex morphs
|
# vertex morphs
|
||||||
for kb_bind, morph_data_path, groups in (i for l in shape_key_map.values() for i in l):
|
for kb_bind, morph_data_path, groups in (i for value_list in shape_key_map.values() for i in value_list):
|
||||||
driver, variables = self.__driver_variables(kb_bind, "value")
|
driver, variables = self.__driver_variables(kb_bind, "value")
|
||||||
var = self.__add_single_prop(variables, obj, morph_data_path, "v")
|
var = self.__add_single_prop(variables, obj, morph_data_path, "v")
|
||||||
if kb_bind.name.startswith("mmd_bind"):
|
if kb_bind.name.startswith("mmd_bind"):
|
||||||
@@ -646,7 +639,7 @@ class _MorphSlider:
|
|||||||
kb_bind.mute = False
|
kb_bind.mute = False
|
||||||
|
|
||||||
# bone morphs
|
# bone morphs
|
||||||
def __config_bone_morph(constraints: bpy.types.ArmatureConstraints, map_type: str, attributes: Set[str], val: float, val_str: str) -> None:
|
def __config_bone_morph(constraints, map_type, attributes, val, val_str):
|
||||||
c_name = f"mmd_bind{hash(data)}.{map_type[:3]}"
|
c_name = f"mmd_bind{hash(data)}.{map_type[:3]}"
|
||||||
c = TransformConstraintOp.create(constraints, c_name, map_type)
|
c = TransformConstraintOp.create(constraints, c_name, map_type)
|
||||||
TransformConstraintOp.update_min_max(c, val, None)
|
TransformConstraintOp.update_min_max(c, val, None)
|
||||||
@@ -660,8 +653,6 @@ class _MorphSlider:
|
|||||||
sign = "-" if attr.startswith("to_min") else ""
|
sign = "-" if attr.startswith("to_min") else ""
|
||||||
driver.expression = f"{sign}{val_str}*({expression})"
|
driver.expression = f"{sign}{val_str}*({expression})"
|
||||||
|
|
||||||
from math import pi
|
|
||||||
|
|
||||||
attributes_rot = TransformConstraintOp.min_max_attributes("ROTATION", "to")
|
attributes_rot = TransformConstraintOp.min_max_attributes("ROTATION", "to")
|
||||||
attributes_loc = TransformConstraintOp.min_max_attributes("LOCATION", "to")
|
attributes_loc = TransformConstraintOp.min_max_attributes("LOCATION", "to")
|
||||||
for morph_name, data, bname, morph_data_path, groups in bone_offset_map.values():
|
for morph_name, data, bname, morph_data_path, groups in bone_offset_map.values():
|
||||||
@@ -671,7 +662,7 @@ class _MorphSlider:
|
|||||||
b.is_mmd_shadow_bone = True
|
b.is_mmd_shadow_bone = True
|
||||||
b.mmd_shadow_bone_type = "BIND"
|
b.mmd_shadow_bone_type = "BIND"
|
||||||
pb = armObj.pose.bones[data.bone]
|
pb = armObj.pose.bones[data.bone]
|
||||||
__config_bone_morph(pb.constraints, "ROTATION", attributes_rot, pi, "pi")
|
__config_bone_morph(pb.constraints, "ROTATION", attributes_rot, math.pi, "pi")
|
||||||
__config_bone_morph(pb.constraints, "LOCATION", attributes_loc, 100, "100")
|
__config_bone_morph(pb.constraints, "LOCATION", attributes_loc, 100, "100")
|
||||||
|
|
||||||
# uv morphs
|
# uv morphs
|
||||||
@@ -680,7 +671,7 @@ class _MorphSlider:
|
|||||||
b = arm.pose.bones["mmd_bind_ctrl_base"]
|
b = arm.pose.bones["mmd_bind_ctrl_base"]
|
||||||
b.is_mmd_shadow_bone = True
|
b.is_mmd_shadow_bone = True
|
||||||
b.mmd_shadow_bone_type = "BIND"
|
b.mmd_shadow_bone_type = "BIND"
|
||||||
for bname, data_path, scale_path, groups in (i for l in uv_morph_map.values() for i in l):
|
for bname, data_path, scale_path, groups in (i for value_list in uv_morph_map.values() for i in value_list):
|
||||||
b = arm.pose.bones[bname]
|
b = arm.pose.bones[bname]
|
||||||
b.is_mmd_shadow_bone = True
|
b.is_mmd_shadow_bone = True
|
||||||
b.mmd_shadow_bone_type = "BIND"
|
b.mmd_shadow_bone_type = "BIND"
|
||||||
@@ -694,9 +685,9 @@ class _MorphSlider:
|
|||||||
|
|
||||||
group_dict = material_offset_map.get("group_dict", {})
|
group_dict = material_offset_map.get("group_dict", {})
|
||||||
|
|
||||||
def __config_material_morph(mat: Material, morph_list: List[Tuple[str, Any, str]]) -> None:
|
def __config_material_morph(mat, morph_list):
|
||||||
nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list))
|
nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list))
|
||||||
for (morph_name, data, name_bind), node in zip(morph_list, nodes):
|
for (morph_name, data, name_bind), node in zip(morph_list, nodes, strict=False):
|
||||||
node.label, node.name = morph_name, name_bind
|
node.label, node.name = morph_name, name_bind
|
||||||
data_path, groups = group_dict[morph_name]
|
data_path, groups = group_dict[morph_name]
|
||||||
driver, variables = self.__driver_variables(mat.node_tree, node.inputs[0].path_from_id("default_value"))
|
driver, variables = self.__driver_variables(mat.node_tree, node.inputs[0].path_from_id("default_value"))
|
||||||
@@ -706,7 +697,7 @@ class _MorphSlider:
|
|||||||
for mat in (m for m in rig.materials() if m and m.use_nodes and not m.name.startswith("mmd_")):
|
for mat in (m for m in rig.materials() if m and m.use_nodes and not m.name.startswith("mmd_")):
|
||||||
mul_all, add_all = material_offset_map.get("#", ([], []))
|
mul_all, add_all = material_offset_map.get("#", ([], []))
|
||||||
if mat.name == "":
|
if mat.name == "":
|
||||||
logger.warning("Oh no. The material name should never be empty.")
|
logger.warning("Oh no. The material name should never empty.")
|
||||||
mul_list, add_list = [], []
|
mul_list, add_list = [], []
|
||||||
else:
|
else:
|
||||||
mat_name = "#" + mat.name
|
mat_name = "#" + mat.name
|
||||||
@@ -722,7 +713,7 @@ class _MorphSlider:
|
|||||||
|
|
||||||
class MigrationFnMorph:
|
class MigrationFnMorph:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_mmd_morph() -> None:
|
def update_mmd_morph():
|
||||||
from .material import FnMaterial
|
from .material import FnMaterial
|
||||||
|
|
||||||
for root in bpy.data.objects:
|
for root in bpy.data.objects:
|
||||||
@@ -733,7 +724,7 @@ class MigrationFnMorph:
|
|||||||
for morph_data in mat_morph.data:
|
for morph_data in mat_morph.data:
|
||||||
if morph_data.material_data is not None:
|
if morph_data.material_data is not None:
|
||||||
# SUPPORT_UNTIL: 5 LTS
|
# SUPPORT_UNTIL: 5 LTS
|
||||||
# The material_id is also no longer used, but for compatibility with older version mmd_tools, keep it.
|
# The material_id is also no longer used, but for compatibility with older version mmd_tools_local, keep it.
|
||||||
if "material_id" not in morph_data.material_data.mmd_material or "material_id" not in morph_data or morph_data.material_data.mmd_material["material_id"] == morph_data["material_id"]:
|
if "material_id" not in morph_data.material_data.mmd_material or "material_id" not in morph_data or morph_data.material_data.mmd_material["material_id"] == morph_data["material_id"]:
|
||||||
# In the new version, the related_mesh property is no longer used.
|
# In the new version, the related_mesh property is no longer used.
|
||||||
# Explicitly remove this property to avoid misuse.
|
# Explicitly remove this property to avoid misuse.
|
||||||
@@ -741,15 +732,14 @@ class MigrationFnMorph:
|
|||||||
del morph_data["related_mesh"]
|
del morph_data["related_mesh"]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
# Compat case. The new version mmd_tools_local saved. And old version mmd_tools_local edit. Then new version mmd_tools_local load again.
|
||||||
# Compat case. The new version mmd_tools saved. And old version mmd_tools edit. Then new version mmd_tools load again.
|
|
||||||
# Go update path.
|
# Go update path.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
morph_data.material_data = None
|
morph_data.material_data = None
|
||||||
if "material_id" in morph_data:
|
if "material_id" in morph_data:
|
||||||
mat_id = morph_data["material_id"]
|
mat_id = morph_data["material_id"]
|
||||||
if mat_id != -1:
|
if mat_id >= 0:
|
||||||
fnMat = FnMaterial.from_material_id(mat_id)
|
fnMat = FnMaterial.from_material_id(mat_id)
|
||||||
if fnMat:
|
if fnMat:
|
||||||
morph_data.material_data = fnMat.material
|
morph_data.material_data = fnMat.material
|
||||||
@@ -764,11 +754,11 @@ class MigrationFnMorph:
|
|||||||
morph_data.related_mesh_data = bpy.data.meshes[related_mesh]
|
morph_data.related_mesh_data = bpy.data.meshes[related_mesh]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def ensure_material_id_not_conflict() -> None:
|
def ensure_material_id_not_conflict():
|
||||||
mat_ids_set: Set[int] = set()
|
mat_ids_set = set()
|
||||||
|
|
||||||
# The reference library properties cannot be modified and bypassed in advance.
|
# The reference library properties cannot be modified and bypassed in advance.
|
||||||
need_update_mat: List[Material] = []
|
need_update_mat = []
|
||||||
for mat in bpy.data.materials:
|
for mat in bpy.data.materials:
|
||||||
if mat.mmd_material.material_id < 0:
|
if mat.mmd_material.material_id < 0:
|
||||||
continue
|
continue
|
||||||
@@ -783,7 +773,7 @@ class MigrationFnMorph:
|
|||||||
mat_ids_set.add(mat.mmd_material.material_id)
|
mat_ids_set.add(mat.mmd_material.material_id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compatible_with_old_version_mmd_tools() -> None:
|
def compatible_with_old_version_mmd_tools_local():
|
||||||
MigrationFnMorph.ensure_material_id_not_conflict()
|
MigrationFnMorph.ensure_material_id_not_conflict()
|
||||||
|
|
||||||
for root in bpy.data.objects:
|
for root in bpy.data.objects:
|
||||||
|
|||||||
+177
-177
@@ -5,7 +5,7 @@
|
|||||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
# 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.
|
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||||
|
|
||||||
import logging
|
from .....core.logging_setup import logger
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ class FileStream:
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self.__file_obj is not None:
|
if self.__file_obj is not None:
|
||||||
logging.debug('close the file("%s")', self.__path)
|
logger.debug('close the file("%s")', self.__path)
|
||||||
self.__file_obj.close()
|
self.__file_obj.close()
|
||||||
self.__file_obj = None
|
self.__file_obj = None
|
||||||
|
|
||||||
@@ -260,20 +260,20 @@ class Header:
|
|||||||
return 4
|
return 4
|
||||||
|
|
||||||
def load(self, fs):
|
def load(self, fs):
|
||||||
logging.info('loading pmx header information...')
|
logger.info('loading pmx header information...')
|
||||||
self.sign = fs.readBytes(4)
|
self.sign = fs.readBytes(4)
|
||||||
logging.debug('File signature is %s', self.sign)
|
logger.debug('File signature is %s', self.sign)
|
||||||
if self.sign[:3] != self.PMX_SIGN[:3]:
|
if self.sign[:3] != self.PMX_SIGN[:3]:
|
||||||
logging.info('File signature is invalid')
|
logger.info('File signature is invalid')
|
||||||
logging.error('This file is unsupported format, or corrupt file.')
|
logger.error('This file is unsupported format, or corrupt file.')
|
||||||
raise InvalidFileError('File signature is invalid.')
|
raise InvalidFileError('File signature is invalid.')
|
||||||
self.version = fs.readFloat()
|
self.version = fs.readFloat()
|
||||||
logging.info('pmx format version: %f', self.version)
|
logger.info('pmx format version: %f', self.version)
|
||||||
if self.version != self.VERSION:
|
if self.version != self.VERSION:
|
||||||
logging.error('PMX version %.1f is unsupported', self.version)
|
logger.error('PMX version %.1f is unsupported', self.version)
|
||||||
raise UnsupportedVersionError('unsupported PMX version: %.1f'%self.version)
|
raise UnsupportedVersionError('unsupported PMX version: %.1f'%self.version)
|
||||||
if fs.readByte() != 8 or self.sign[3] != self.PMX_SIGN[3]:
|
if fs.readByte() != 8 or self.sign[3] != self.PMX_SIGN[3]:
|
||||||
logging.warning(' * This file might be corrupted.')
|
logger.warning(' * This file might be corrupted.')
|
||||||
self.encoding = Encoding(fs.readByte())
|
self.encoding = Encoding(fs.readByte())
|
||||||
self.additional_uvs = fs.readByte()
|
self.additional_uvs = fs.readByte()
|
||||||
self.vertex_index_size = fs.readByte()
|
self.vertex_index_size = fs.readByte()
|
||||||
@@ -283,19 +283,19 @@ class Header:
|
|||||||
self.morph_index_size = fs.readByte()
|
self.morph_index_size = fs.readByte()
|
||||||
self.rigid_index_size = fs.readByte()
|
self.rigid_index_size = fs.readByte()
|
||||||
|
|
||||||
logging.info('----------------------------')
|
logger.info('----------------------------')
|
||||||
logging.info('pmx header information')
|
logger.info('pmx header information')
|
||||||
logging.info('----------------------------')
|
logger.info('----------------------------')
|
||||||
logging.info('pmx version: %.1f', self.version)
|
logger.info('pmx version: %.1f', self.version)
|
||||||
logging.info('encoding: %s', str(self.encoding))
|
logger.info('encoding: %s', str(self.encoding))
|
||||||
logging.info('number of uvs: %d', self.additional_uvs)
|
logger.info('number of uvs: %d', self.additional_uvs)
|
||||||
logging.info('vertex index size: %d byte(s)', self.vertex_index_size)
|
logger.info('vertex index size: %d byte(s)', self.vertex_index_size)
|
||||||
logging.info('texture index: %d byte(s)', self.texture_index_size)
|
logger.info('texture index: %d byte(s)', self.texture_index_size)
|
||||||
logging.info('material index: %d byte(s)', self.material_index_size)
|
logger.info('material index: %d byte(s)', self.material_index_size)
|
||||||
logging.info('bone index: %d byte(s)', self.bone_index_size)
|
logger.info('bone index: %d byte(s)', self.bone_index_size)
|
||||||
logging.info('morph index: %d byte(s)', self.morph_index_size)
|
logger.info('morph index: %d byte(s)', self.morph_index_size)
|
||||||
logging.info('rigid index: %d byte(s)', self.rigid_index_size)
|
logger.info('rigid index: %d byte(s)', self.rigid_index_size)
|
||||||
logging.info('----------------------------')
|
logger.info('----------------------------')
|
||||||
|
|
||||||
def save(self, fs):
|
def save(self, fs):
|
||||||
fs.writeBytes(self.PMX_SIGN)
|
fs.writeBytes(self.PMX_SIGN)
|
||||||
@@ -364,27 +364,27 @@ class Model:
|
|||||||
self.comment = fs.readStr()
|
self.comment = fs.readStr()
|
||||||
self.comment_e = fs.readStr()
|
self.comment_e = fs.readStr()
|
||||||
|
|
||||||
logging.info('Model name: %s', self.name)
|
logger.info('Model name: %s', self.name)
|
||||||
logging.info('Model name(english): %s', self.name_e)
|
logger.info('Model name(english): %s', self.name_e)
|
||||||
logging.info('Comment:%s', self.comment)
|
logger.info('Comment:%s', self.comment)
|
||||||
logging.info('Comment(english):%s', self.comment_e)
|
logger.info('Comment(english):%s', self.comment_e)
|
||||||
|
|
||||||
logging.info('')
|
logger.info('')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
logging.info('Load Vertices')
|
logger.info('Load Vertices')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
num_vertices = fs.readInt()
|
num_vertices = fs.readInt()
|
||||||
self.vertices = []
|
self.vertices = []
|
||||||
for i in range(num_vertices):
|
for i in range(num_vertices):
|
||||||
v = Vertex()
|
v = Vertex()
|
||||||
v.load(fs)
|
v.load(fs)
|
||||||
self.vertices.append(v)
|
self.vertices.append(v)
|
||||||
logging.info('----- Loaded %d vertices', len(self.vertices))
|
logger.info('----- Loaded %d vertices', len(self.vertices))
|
||||||
|
|
||||||
logging.info('')
|
logger.info('')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
logging.info(' Load Faces')
|
logger.info(' Load Faces')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
num_faces = fs.readInt()
|
num_faces = fs.readInt()
|
||||||
self.faces = []
|
self.faces = []
|
||||||
for i in range(int(num_faces/3)):
|
for i in range(int(num_faces/3)):
|
||||||
@@ -392,25 +392,25 @@ class Model:
|
|||||||
f2 = fs.readVertexIndex()
|
f2 = fs.readVertexIndex()
|
||||||
f3 = fs.readVertexIndex()
|
f3 = fs.readVertexIndex()
|
||||||
self.faces.append((f3, f2, f1))
|
self.faces.append((f3, f2, f1))
|
||||||
logging.info(' Load %d faces', len(self.faces))
|
logger.info(' Load %d faces', len(self.faces))
|
||||||
|
|
||||||
logging.info('')
|
logger.info('')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
logging.info(' Load Textures')
|
logger.info(' Load Textures')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
num_textures = fs.readInt()
|
num_textures = fs.readInt()
|
||||||
self.textures = []
|
self.textures = []
|
||||||
for i in range(num_textures):
|
for i in range(num_textures):
|
||||||
t = Texture()
|
t = Texture()
|
||||||
t.load(fs)
|
t.load(fs)
|
||||||
self.textures.append(t)
|
self.textures.append(t)
|
||||||
logging.info('Texture %d: %s', i, t.path)
|
logger.info('Texture %d: %s', i, t.path)
|
||||||
logging.info(' ----- Loaded %d textures', len(self.textures))
|
logger.info(' ----- Loaded %d textures', len(self.textures))
|
||||||
|
|
||||||
logging.info('')
|
logger.info('')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
logging.info(' Load Materials')
|
logger.info(' Load Materials')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
num_materials = fs.readInt()
|
num_materials = fs.readInt()
|
||||||
self.materials = []
|
self.materials = []
|
||||||
for i in range(num_materials):
|
for i in range(num_materials):
|
||||||
@@ -418,38 +418,38 @@ class Model:
|
|||||||
m.load(fs, num_textures)
|
m.load(fs, num_textures)
|
||||||
self.materials.append(m)
|
self.materials.append(m)
|
||||||
|
|
||||||
logging.info('Material %d: %s', i, m.name)
|
logger.info('Material %d: %s', i, m.name)
|
||||||
logging.debug(' Name(english): %s', m.name_e)
|
logger.debug(' Name(english): %s', m.name_e)
|
||||||
logging.debug(' Comment: %s', m.comment)
|
logger.debug(' Comment: %s', m.comment)
|
||||||
logging.debug(' Vertex Count: %d', m.vertex_count)
|
logger.debug(' Vertex Count: %d', m.vertex_count)
|
||||||
logging.debug(' Diffuse: (%.2f, %.2f, %.2f, %.2f)', *m.diffuse)
|
logger.debug(' Diffuse: (%.2f, %.2f, %.2f, %.2f)', *m.diffuse)
|
||||||
logging.debug(' Specular: (%.2f, %.2f, %.2f)', *m.specular)
|
logger.debug(' Specular: (%.2f, %.2f, %.2f)', *m.specular)
|
||||||
logging.debug(' Shininess: %f', m.shininess)
|
logger.debug(' Shininess: %f', m.shininess)
|
||||||
logging.debug(' Ambient: (%.2f, %.2f, %.2f)', *m.ambient)
|
logger.debug(' Ambient: (%.2f, %.2f, %.2f)', *m.ambient)
|
||||||
logging.debug(' Double Sided: %s', str(m.is_double_sided))
|
logger.debug(' Double Sided: %s', str(m.is_double_sided))
|
||||||
logging.debug(' Drop Shadow: %s', str(m.enabled_drop_shadow))
|
logger.debug(' Drop Shadow: %s', str(m.enabled_drop_shadow))
|
||||||
logging.debug(' Self Shadow: %s', str(m.enabled_self_shadow))
|
logger.debug(' Self Shadow: %s', str(m.enabled_self_shadow))
|
||||||
logging.debug(' Self Shadow Map: %s', str(m.enabled_self_shadow_map))
|
logger.debug(' Self Shadow Map: %s', str(m.enabled_self_shadow_map))
|
||||||
logging.debug(' Edge: %s', str(m.enabled_toon_edge))
|
logger.debug(' Edge: %s', str(m.enabled_toon_edge))
|
||||||
logging.debug(' Edge Color: (%.2f, %.2f, %.2f, %.2f)', *m.edge_color)
|
logger.debug(' Edge Color: (%.2f, %.2f, %.2f, %.2f)', *m.edge_color)
|
||||||
logging.debug(' Edge Size: %.2f', m.edge_size)
|
logger.debug(' Edge Size: %.2f', m.edge_size)
|
||||||
if m.texture != -1:
|
if m.texture != -1:
|
||||||
logging.debug(' Texture Index: %d', m.texture)
|
logger.debug(' Texture Index: %d', m.texture)
|
||||||
else:
|
else:
|
||||||
logging.debug(' Texture: None')
|
logger.debug(' Texture: None')
|
||||||
if m.sphere_texture != -1:
|
if m.sphere_texture != -1:
|
||||||
logging.debug(' Sphere Texture Index: %d', m.sphere_texture)
|
logger.debug(' Sphere Texture Index: %d', m.sphere_texture)
|
||||||
logging.debug(' Sphere Texture Mode: %d', m.sphere_texture_mode)
|
logger.debug(' Sphere Texture Mode: %d', m.sphere_texture_mode)
|
||||||
else:
|
else:
|
||||||
logging.debug(' Sphere Texture: None')
|
logger.debug(' Sphere Texture: None')
|
||||||
logging.debug('')
|
logger.debug('')
|
||||||
|
|
||||||
logging.info('----- Loaded %d materials.', len(self.materials))
|
logger.info('----- Loaded %d materials.', len(self.materials))
|
||||||
|
|
||||||
logging.info('')
|
logger.info('')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
logging.info(' Load Bones')
|
logger.info(' Load Bones')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
num_bones = fs.readInt()
|
num_bones = fs.readInt()
|
||||||
self.bones = []
|
self.bones = []
|
||||||
for i in range(num_bones):
|
for i in range(num_bones):
|
||||||
@@ -457,33 +457,33 @@ class Model:
|
|||||||
b.load(fs)
|
b.load(fs)
|
||||||
self.bones.append(b)
|
self.bones.append(b)
|
||||||
|
|
||||||
logging.info('Bone %d: %s', i, b.name)
|
logger.info('Bone %d: %s', i, b.name)
|
||||||
logging.debug(' Name(english): %s', b.name_e)
|
logger.debug(' Name(english): %s', b.name_e)
|
||||||
logging.debug(' Location: (%f, %f, %f)', *b.location)
|
logger.debug(' Location: (%f, %f, %f)', *b.location)
|
||||||
logging.debug(' displayConnection: %s', str(b.displayConnection))
|
logger.debug(' displayConnection: %s', str(b.displayConnection))
|
||||||
logging.debug(' Parent: %s', str(b.parent))
|
logger.debug(' Parent: %s', str(b.parent))
|
||||||
logging.debug(' Transform Order: %s', str(b.transform_order))
|
logger.debug(' Transform Order: %s', str(b.transform_order))
|
||||||
logging.debug(' Rotatable: %s', str(b.isRotatable))
|
logger.debug(' Rotatable: %s', str(b.isRotatable))
|
||||||
logging.debug(' Movable: %s', str(b.isMovable))
|
logger.debug(' Movable: %s', str(b.isMovable))
|
||||||
logging.debug(' Visible: %s', str(b.visible))
|
logger.debug(' Visible: %s', str(b.visible))
|
||||||
logging.debug(' Controllable: %s', str(b.isControllable))
|
logger.debug(' Controllable: %s', str(b.isControllable))
|
||||||
logging.debug(' Additional Location: %s', str(b.hasAdditionalLocation))
|
logger.debug(' Additional Location: %s', str(b.hasAdditionalLocation))
|
||||||
logging.debug(' Additional Rotation: %s', str(b.hasAdditionalRotate))
|
logger.debug(' Additional Rotation: %s', str(b.hasAdditionalRotate))
|
||||||
if b.additionalTransform is not None:
|
if b.additionalTransform is not None:
|
||||||
logging.debug(' Additional Transform: Bone:%d, influence: %f', *b.additionalTransform)
|
logger.debug(' Additional Transform: Bone:%d, influence: %f', *b.additionalTransform)
|
||||||
logging.debug(' IK: %s', str(b.isIK))
|
logger.debug(' IK: %s', str(b.isIK))
|
||||||
if b.isIK:
|
if b.isIK:
|
||||||
logging.debug(' Unit Angle: %f', b.rotationConstraint)
|
logger.debug(' Unit Angle: %f', b.rotationConstraint)
|
||||||
logging.debug(' Target: %d', b.target)
|
logger.debug(' Target: %d', b.target)
|
||||||
for j, link in enumerate(b.ik_links):
|
for j, link in enumerate(b.ik_links):
|
||||||
logging.debug(' IK Link %d: %d, %s - %s', j, link.target, str(link.minimumAngle), str(link.maximumAngle))
|
logger.debug(' IK Link %d: %d, %s - %s', j, link.target, str(link.minimumAngle), str(link.maximumAngle))
|
||||||
logging.debug('')
|
logger.debug('')
|
||||||
logging.info('----- Loaded %d bones.', len(self.bones))
|
logger.info('----- Loaded %d bones.', len(self.bones))
|
||||||
|
|
||||||
logging.info('')
|
logger.info('')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
logging.info(' Load Morphs')
|
logger.info(' Load Morphs')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
num_morph = fs.readInt()
|
num_morph = fs.readInt()
|
||||||
self.morphs = []
|
self.morphs = []
|
||||||
display_categories = {0: 'System', 1: 'Eyebrow', 2: 'Eye', 3: 'Mouth', 4: 'Other'}
|
display_categories = {0: 'System', 1: 'Eyebrow', 2: 'Eye', 3: 'Mouth', 4: 'Other'}
|
||||||
@@ -491,16 +491,16 @@ class Model:
|
|||||||
m = Morph.create(fs)
|
m = Morph.create(fs)
|
||||||
self.morphs.append(m)
|
self.morphs.append(m)
|
||||||
|
|
||||||
logging.info('%s %d: %s', m.__class__.__name__, i, m.name)
|
logger.info('%s %d: %s', m.__class__.__name__, i, m.name)
|
||||||
logging.debug(' Name(english): %s', m.name_e)
|
logger.debug(' Name(english): %s', m.name_e)
|
||||||
logging.debug(' Category: %s (%d)', display_categories.get(m.category, '#Invalid'), m.category)
|
logger.debug(' Category: %s (%d)', display_categories.get(m.category, '#Invalid'), m.category)
|
||||||
logging.debug('')
|
logger.debug('')
|
||||||
logging.info('----- Loaded %d morphs.', len(self.morphs))
|
logger.info('----- Loaded %d morphs.', len(self.morphs))
|
||||||
|
|
||||||
logging.info('')
|
logger.info('')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
logging.info(' Load Display Items')
|
logger.info(' Load Display Items')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
num_disp = fs.readInt()
|
num_disp = fs.readInt()
|
||||||
self.display = []
|
self.display = []
|
||||||
for i in range(num_disp):
|
for i in range(num_disp):
|
||||||
@@ -508,15 +508,15 @@ class Model:
|
|||||||
d.load(fs)
|
d.load(fs)
|
||||||
self.display.append(d)
|
self.display.append(d)
|
||||||
|
|
||||||
logging.info('Display Item %d: %s', i, d.name)
|
logger.info('Display Item %d: %s', i, d.name)
|
||||||
logging.debug(' Name(english): %s', d.name_e)
|
logger.debug(' Name(english): %s', d.name_e)
|
||||||
logging.debug('')
|
logger.debug('')
|
||||||
logging.info('----- Loaded %d display items.', len(self.display))
|
logger.info('----- Loaded %d display items.', len(self.display))
|
||||||
|
|
||||||
logging.info('')
|
logger.info('')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
logging.info(' Load Rigid Bodies')
|
logger.info(' Load Rigid Bodies')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
num_rigid = fs.readInt()
|
num_rigid = fs.readInt()
|
||||||
self.rigids = []
|
self.rigids = []
|
||||||
rigid_types = {0: 'Sphere', 1: 'Box', 2: 'Capsule'}
|
rigid_types = {0: 'Sphere', 1: 'Box', 2: 'Capsule'}
|
||||||
@@ -525,27 +525,27 @@ class Model:
|
|||||||
r = Rigid()
|
r = Rigid()
|
||||||
r.load(fs)
|
r.load(fs)
|
||||||
self.rigids.append(r)
|
self.rigids.append(r)
|
||||||
logging.info('Rigid Body %d: %s', i, r.name)
|
logger.info('Rigid Body %d: %s', i, r.name)
|
||||||
logging.debug(' Name(english): %s', r.name_e)
|
logger.debug(' Name(english): %s', r.name_e)
|
||||||
logging.debug(' Type: %s', rigid_types[r.type])
|
logger.debug(' Type: %s', rigid_types[r.type])
|
||||||
logging.debug(' Mode: %s (%d)', rigid_modes.get(r.mode, '#Invalid'), r.mode)
|
logger.debug(' Mode: %s (%d)', rigid_modes.get(r.mode, '#Invalid'), r.mode)
|
||||||
logging.debug(' Related bone: %s', r.bone)
|
logger.debug(' Related bone: %s', r.bone)
|
||||||
logging.debug(' Collision group: %d', r.collision_group_number)
|
logger.debug(' Collision group: %d', r.collision_group_number)
|
||||||
logging.debug(' Collision group mask: 0x%x', r.collision_group_mask)
|
logger.debug(' Collision group mask: 0x%x', r.collision_group_mask)
|
||||||
logging.debug(' Size: (%f, %f, %f)', *r.size)
|
logger.debug(' Size: (%f, %f, %f)', *r.size)
|
||||||
logging.debug(' Location: (%f, %f, %f)', *r.location)
|
logger.debug(' Location: (%f, %f, %f)', *r.location)
|
||||||
logging.debug(' Rotation: (%f, %f, %f)', *r.rotation)
|
logger.debug(' Rotation: (%f, %f, %f)', *r.rotation)
|
||||||
logging.debug(' Mass: %f', r.mass)
|
logger.debug(' Mass: %f', r.mass)
|
||||||
logging.debug(' Bounce: %f', r.bounce)
|
logger.debug(' Bounce: %f', r.bounce)
|
||||||
logging.debug(' Friction: %f', r.friction)
|
logger.debug(' Friction: %f', r.friction)
|
||||||
logging.debug('')
|
logger.debug('')
|
||||||
|
|
||||||
logging.info('----- Loaded %d rigid bodies.', len(self.rigids))
|
logger.info('----- Loaded %d rigid bodies.', len(self.rigids))
|
||||||
|
|
||||||
logging.info('')
|
logger.info('')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
logging.info(' Load Joints')
|
logger.info(' Load Joints')
|
||||||
logging.info('------------------------------')
|
logger.info('------------------------------')
|
||||||
num_joints = fs.readInt()
|
num_joints = fs.readInt()
|
||||||
self.joints = []
|
self.joints = []
|
||||||
for i in range(num_joints):
|
for i in range(num_joints):
|
||||||
@@ -553,19 +553,19 @@ class Model:
|
|||||||
j.load(fs)
|
j.load(fs)
|
||||||
self.joints.append(j)
|
self.joints.append(j)
|
||||||
|
|
||||||
logging.info('Joint %d: %s', i, j.name)
|
logger.info('Joint %d: %s', i, j.name)
|
||||||
logging.debug(' Name(english): %s', j.name_e)
|
logger.debug(' Name(english): %s', j.name_e)
|
||||||
logging.debug(' Rigid A: %s', j.src_rigid)
|
logger.debug(' Rigid A: %s', j.src_rigid)
|
||||||
logging.debug(' Rigid B: %s', j.dest_rigid)
|
logger.debug(' Rigid B: %s', j.dest_rigid)
|
||||||
logging.debug(' Location: (%f, %f, %f)', *j.location)
|
logger.debug(' Location: (%f, %f, %f)', *j.location)
|
||||||
logging.debug(' Rotation: (%f, %f, %f)', *j.rotation)
|
logger.debug(' Rotation: (%f, %f, %f)', *j.rotation)
|
||||||
logging.debug(' Location Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_location + j.maximum_location))
|
logger.debug(' Location Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_location + j.maximum_location))
|
||||||
logging.debug(' Rotation Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_rotation + j.maximum_rotation))
|
logger.debug(' Rotation Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_rotation + j.maximum_rotation))
|
||||||
logging.debug(' Spring: (%f, %f, %f)', *j.spring_constant)
|
logger.debug(' Spring: (%f, %f, %f)', *j.spring_constant)
|
||||||
logging.debug(' Spring(rotation): (%f, %f, %f)', *j.spring_rotation_constant)
|
logger.debug(' Spring(rotation): (%f, %f, %f)', *j.spring_rotation_constant)
|
||||||
logging.debug('')
|
logger.debug('')
|
||||||
|
|
||||||
logging.info('----- Loaded %d joints.', len(self.joints))
|
logger.info('----- Loaded %d joints.', len(self.joints))
|
||||||
|
|
||||||
def save(self, fs):
|
def save(self, fs):
|
||||||
fs.writeStr(self.name)
|
fs.writeStr(self.name)
|
||||||
@@ -574,7 +574,7 @@ class Model:
|
|||||||
fs.writeStr(self.comment)
|
fs.writeStr(self.comment)
|
||||||
fs.writeStr(self.comment_e)
|
fs.writeStr(self.comment_e)
|
||||||
|
|
||||||
logging.info('''exportings pmx model data...
|
logger.info('''exportings pmx model data...
|
||||||
name: %s
|
name: %s
|
||||||
name(english): %s
|
name(english): %s
|
||||||
comment:
|
comment:
|
||||||
@@ -583,62 +583,62 @@ comment(english):
|
|||||||
%s
|
%s
|
||||||
''', self.name, self.name_e, self.comment, self.comment_e)
|
''', self.name, self.name_e, self.comment, self.comment_e)
|
||||||
|
|
||||||
logging.info('exporting vertices... %d', len(self.vertices))
|
logger.info('exporting vertices... %d', len(self.vertices))
|
||||||
fs.writeInt(len(self.vertices))
|
fs.writeInt(len(self.vertices))
|
||||||
for i in self.vertices:
|
for i in self.vertices:
|
||||||
i.save(fs)
|
i.save(fs)
|
||||||
logging.info('finished exporting vertices.')
|
logger.info('finished exporting vertices.')
|
||||||
|
|
||||||
logging.info('exporting faces... %d', len(self.faces))
|
logger.info('exporting faces... %d', len(self.faces))
|
||||||
fs.writeInt(len(self.faces)*3)
|
fs.writeInt(len(self.faces)*3)
|
||||||
for f3, f2, f1 in self.faces:
|
for f3, f2, f1 in self.faces:
|
||||||
fs.writeVertexIndex(f1)
|
fs.writeVertexIndex(f1)
|
||||||
fs.writeVertexIndex(f2)
|
fs.writeVertexIndex(f2)
|
||||||
fs.writeVertexIndex(f3)
|
fs.writeVertexIndex(f3)
|
||||||
logging.info('finished exporting faces.')
|
logger.info('finished exporting faces.')
|
||||||
|
|
||||||
logging.info('exporting textures... %d', len(self.textures))
|
logger.info('exporting textures... %d', len(self.textures))
|
||||||
fs.writeInt(len(self.textures))
|
fs.writeInt(len(self.textures))
|
||||||
for i in self.textures:
|
for i in self.textures:
|
||||||
i.save(fs)
|
i.save(fs)
|
||||||
logging.info('finished exporting textures.')
|
logger.info('finished exporting textures.')
|
||||||
|
|
||||||
logging.info('exporting materials... %d', len(self.materials))
|
logger.info('exporting materials... %d', len(self.materials))
|
||||||
fs.writeInt(len(self.materials))
|
fs.writeInt(len(self.materials))
|
||||||
for i in self.materials:
|
for i in self.materials:
|
||||||
i.save(fs)
|
i.save(fs)
|
||||||
logging.info('finished exporting materials.')
|
logger.info('finished exporting materials.')
|
||||||
|
|
||||||
logging.info('exporting bones... %d', len(self.bones))
|
logger.info('exporting bones... %d', len(self.bones))
|
||||||
fs.writeInt(len(self.bones))
|
fs.writeInt(len(self.bones))
|
||||||
for i in self.bones:
|
for i in self.bones:
|
||||||
i.save(fs)
|
i.save(fs)
|
||||||
logging.info('finished exporting bones.')
|
logger.info('finished exporting bones.')
|
||||||
|
|
||||||
logging.info('exporting morphs... %d', len(self.morphs))
|
logger.info('exporting morphs... %d', len(self.morphs))
|
||||||
fs.writeInt(len(self.morphs))
|
fs.writeInt(len(self.morphs))
|
||||||
for i in self.morphs:
|
for i in self.morphs:
|
||||||
i.save(fs)
|
i.save(fs)
|
||||||
logging.info('finished exporting morphs.')
|
logger.info('finished exporting morphs.')
|
||||||
|
|
||||||
logging.info('exporting display items... %d', len(self.display))
|
logger.info('exporting display items... %d', len(self.display))
|
||||||
fs.writeInt(len(self.display))
|
fs.writeInt(len(self.display))
|
||||||
for i in self.display:
|
for i in self.display:
|
||||||
i.save(fs)
|
i.save(fs)
|
||||||
logging.info('finished exporting display items.')
|
logger.info('finished exporting display items.')
|
||||||
|
|
||||||
logging.info('exporting rigid bodies... %d', len(self.rigids))
|
logger.info('exporting rigid bodies... %d', len(self.rigids))
|
||||||
fs.writeInt(len(self.rigids))
|
fs.writeInt(len(self.rigids))
|
||||||
for i in self.rigids:
|
for i in self.rigids:
|
||||||
i.save(fs)
|
i.save(fs)
|
||||||
logging.info('finished exporting rigid bodies.')
|
logger.info('finished exporting rigid bodies.')
|
||||||
|
|
||||||
logging.info('exporting joints... %d', len(self.joints))
|
logger.info('exporting joints... %d', len(self.joints))
|
||||||
fs.writeInt(len(self.joints))
|
fs.writeInt(len(self.joints))
|
||||||
for i in self.joints:
|
for i in self.joints:
|
||||||
i.save(fs)
|
i.save(fs)
|
||||||
logging.info('finished exporting joints.')
|
logger.info('finished exporting joints.')
|
||||||
logging.info('finished exporting the model.')
|
logger.info('finished exporting the model.')
|
||||||
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -803,7 +803,7 @@ class Texture:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
relPath = self.path
|
relPath = self.path
|
||||||
relPath = relPath.replace(os.path.sep, '\\') # always save using windows path conventions
|
relPath = relPath.replace(os.path.sep, '\\') # always save using windows path conventions
|
||||||
logging.info('writing to pmx file the relative texture path: %s', relPath)
|
logger.info('writing to pmx file the relative texture path: %s', relPath)
|
||||||
fs.writeStr(relPath)
|
fs.writeStr(relPath)
|
||||||
|
|
||||||
class SharedTexture(Texture):
|
class SharedTexture(Texture):
|
||||||
@@ -1170,7 +1170,7 @@ class Morph:
|
|||||||
|
|
||||||
name = fs.readStr()
|
name = fs.readStr()
|
||||||
name_e = fs.readStr()
|
name_e = fs.readStr()
|
||||||
logging.debug('morph: %s', name)
|
logger.debug('morph: %s', name)
|
||||||
category = fs.readSignedByte()
|
category = fs.readSignedByte()
|
||||||
typeIndex = fs.readSignedByte()
|
typeIndex = fs.readSignedByte()
|
||||||
ret = _CLASSES[typeIndex](name, name_e, category, type_index = typeIndex)
|
ret = _CLASSES[typeIndex](name, name_e, category, type_index = typeIndex)
|
||||||
@@ -1399,7 +1399,7 @@ class Display:
|
|||||||
else:
|
else:
|
||||||
raise Exception('invalid value.')
|
raise Exception('invalid value.')
|
||||||
self.data.append((disp_type, index))
|
self.data.append((disp_type, index))
|
||||||
logging.debug('the number of display elements: %d', len(self.data))
|
logger.debug('the number of display elements: %d', len(self.data))
|
||||||
|
|
||||||
def save(self, fs):
|
def save(self, fs):
|
||||||
fs.writeStr(self.name)
|
fs.writeStr(self.name)
|
||||||
@@ -1595,12 +1595,12 @@ class Joint:
|
|||||||
|
|
||||||
def load(path):
|
def load(path):
|
||||||
with FileReadStream(path) as fs:
|
with FileReadStream(path) as fs:
|
||||||
logging.info('****************************************')
|
logger.info('****************************************')
|
||||||
logging.info(' mmd_tools.pmx module')
|
logger.info(' mmd_tools.pmx module')
|
||||||
logging.info('----------------------------------------')
|
logger.info('----------------------------------------')
|
||||||
logging.info(' Start to load model data form a pmx file')
|
logger.info(' Start to load model data form a pmx file')
|
||||||
logging.info(' by the mmd_tools.pmx modlue.')
|
logger.info(' by the mmd_tools.pmx modlue.')
|
||||||
logging.info('')
|
logger.info('')
|
||||||
header = Header()
|
header = Header()
|
||||||
header.load(fs)
|
header.load(fs)
|
||||||
fs.setHeader(header)
|
fs.setHeader(header)
|
||||||
@@ -1608,12 +1608,12 @@ def load(path):
|
|||||||
try:
|
try:
|
||||||
model.load(fs)
|
model.load(fs)
|
||||||
except struct.error as e:
|
except struct.error as e:
|
||||||
logging.error(' * Corrupted file: %s', e)
|
logger.error(' * Corrupted file: %s', e)
|
||||||
#raise
|
#raise
|
||||||
logging.info(' Finished loading.')
|
logger.info(' Finished loading.')
|
||||||
logging.info('----------------------------------------')
|
logger.info('----------------------------------------')
|
||||||
logging.info(' mmd_tools.pmx module')
|
logger.info(' mmd_tools.pmx module')
|
||||||
logging.info('****************************************')
|
logger.info('****************************************')
|
||||||
return model
|
return model
|
||||||
|
|
||||||
def save(path, model, add_uv_count=0):
|
def save(path, model, add_uv_count=0):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, List, Optional, Dict, Tuple, Set, Callable, Any, Union, FrozenSet, Iterator
|
from typing import TYPE_CHECKING, List, Optional, Dict, Tuple, Set, Callable, Any, Union, FrozenSet, Iterator
|
||||||
@@ -103,7 +104,7 @@ class PMXImporter:
|
|||||||
obj_name = self.__safe_name(bpy.path.display_name(pmxModel.filepath), max_length=54)
|
obj_name = self.__safe_name(bpy.path.display_name(pmxModel.filepath), max_length=54)
|
||||||
logger.info(f"Creating objects for model: {obj_name}")
|
logger.info(f"Creating objects for model: {obj_name}")
|
||||||
|
|
||||||
self.__rig = Model.create(pmxModel.name, pmxModel.name_e, self.__scale or 1.0, obj_name)
|
self.__rig = Model.create(pmxModel.name, pmxModel.name_e, self.__scale, obj_name)
|
||||||
root = self.__rig.rootObject()
|
root = self.__rig.rootObject()
|
||||||
mmd_root: 'MMDRoot' = root.mmd_root
|
mmd_root: 'MMDRoot' = root.mmd_root
|
||||||
self.__root = root
|
self.__root = root
|
||||||
@@ -192,7 +193,7 @@ class PMXImporter:
|
|||||||
|
|
||||||
mesh: Mesh = self.__meshObj.data
|
mesh: Mesh = self.__meshObj.data
|
||||||
mesh.vertices.add(count=vertex_count)
|
mesh.vertices.add(count=vertex_count)
|
||||||
mesh.vertices.foreach_set("co", tuple(i for pv in pmx_vertices for i in (Vector(pv.co).xzy * (self.__scale or 1.0))))
|
mesh.vertices.foreach_set("co", tuple(i for pv in pmx_vertices for i in (Vector(pv.co).xzy * self.__scale)))
|
||||||
|
|
||||||
vertex_group_table = self.__vertexGroupTable
|
vertex_group_table = self.__vertexGroupTable
|
||||||
if not vertex_group_table:
|
if not vertex_group_table:
|
||||||
@@ -249,9 +250,9 @@ class PMXImporter:
|
|||||||
|
|
||||||
for i, pv in self.__sdefVertices.items():
|
for i, pv in self.__sdefVertices.items():
|
||||||
w = pv.weight.weights
|
w = pv.weight.weights
|
||||||
sdefC.data[i].co = Vector(w.c).xzy * (self.__scale or 1.0)
|
sdefC.data[i].co = Vector(w.c).xzy * self.__scale
|
||||||
sdefR0.data[i].co = Vector(w.r0).xzy * (self.__scale or 1.0)
|
sdefR0.data[i].co = Vector(w.r0).xzy * self.__scale
|
||||||
sdefR1.data[i].co = Vector(w.r1).xzy * (self.__scale or 1.0)
|
sdefR1.data[i].co = Vector(w.r1).xzy * self.__scale
|
||||||
|
|
||||||
logger.debug(f"Stored {len(self.__sdefVertices)} SDEF vertices in shape keys")
|
logger.debug(f"Stored {len(self.__sdefVertices)} SDEF vertices in shape keys")
|
||||||
|
|
||||||
@@ -290,13 +291,13 @@ class PMXImporter:
|
|||||||
# Create bones
|
# Create bones
|
||||||
for i in pmx_bones:
|
for i in pmx_bones:
|
||||||
bone = data.edit_bones.new(name=i.name)
|
bone = data.edit_bones.new(name=i.name)
|
||||||
loc = _VectorXZY(i.location) * (self.__scale or 1.0)
|
loc = _VectorXZY(i.location) * self.__scale
|
||||||
bone.head = loc
|
bone.head = loc
|
||||||
editBoneTable.append(bone)
|
editBoneTable.append(bone)
|
||||||
nameTable.append(bone.name)
|
nameTable.append(bone.name)
|
||||||
|
|
||||||
# Set parent relationships
|
# Set parent relationships
|
||||||
for i, (b_bone, m_bone) in enumerate(zip(editBoneTable, pmx_bones)):
|
for i, (b_bone, m_bone) in enumerate(zip(editBoneTable, pmx_bones, strict=False)):
|
||||||
if m_bone.parent != -1:
|
if m_bone.parent != -1:
|
||||||
if i not in dependency_cycle_ik_bones:
|
if i not in dependency_cycle_ik_bones:
|
||||||
b_bone.parent = editBoneTable[m_bone.parent]
|
b_bone.parent = editBoneTable[m_bone.parent]
|
||||||
@@ -304,18 +305,18 @@ class PMXImporter:
|
|||||||
b_bone.parent = editBoneTable[m_bone.parent].parent
|
b_bone.parent = editBoneTable[m_bone.parent].parent
|
||||||
|
|
||||||
# Set tail positions
|
# Set tail positions
|
||||||
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
|
for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
|
||||||
if isinstance(m_bone.displayConnection, int):
|
if isinstance(m_bone.displayConnection, int):
|
||||||
if m_bone.displayConnection != -1:
|
if m_bone.displayConnection != -1:
|
||||||
b_bone.tail = editBoneTable[m_bone.displayConnection].head
|
b_bone.tail = editBoneTable[m_bone.displayConnection].head
|
||||||
else:
|
else:
|
||||||
b_bone.tail = b_bone.head
|
b_bone.tail = b_bone.head
|
||||||
else:
|
else:
|
||||||
loc = _VectorXZY(m_bone.displayConnection) * (self.__scale or 1.0)
|
loc = _VectorXZY(m_bone.displayConnection) * self.__scale
|
||||||
b_bone.tail = b_bone.head + loc
|
b_bone.tail = b_bone.head + loc
|
||||||
|
|
||||||
# Check and fix IK links
|
# Check and fix IK links
|
||||||
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
|
for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
|
||||||
if m_bone.isIK and m_bone.target != -1:
|
if m_bone.isIK and m_bone.target != -1:
|
||||||
logger.debug(f"Checking IK links of {b_bone.name}")
|
logger.debug(f"Checking IK links of {b_bone.name}")
|
||||||
b_target = editBoneTable[m_bone.target]
|
b_target = editBoneTable[m_bone.target]
|
||||||
@@ -333,30 +334,30 @@ class PMXImporter:
|
|||||||
b_bone_link.tail = b_bone_link.head + loc
|
b_bone_link.tail = b_bone_link.head + loc
|
||||||
|
|
||||||
# Fix too short bones
|
# Fix too short bones
|
||||||
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
|
for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
|
||||||
# Set the length of too short bones to 1 because Blender delete them.
|
# Set the length of too short bones to 1 because Blender delete them.
|
||||||
if b_bone.length < 0.001:
|
if b_bone.length < 0.001:
|
||||||
if not self.__apply_bone_fixed_axis and m_bone.axis is not None:
|
if not self.__apply_bone_fixed_axis and m_bone.axis is not None:
|
||||||
fixed_axis = Vector(m_bone.axis)
|
fixed_axis = Vector(m_bone.axis)
|
||||||
if fixed_axis.length:
|
if fixed_axis.length:
|
||||||
b_bone.tail = b_bone.head + fixed_axis.xzy.normalized() * (self.__scale or 1.0)
|
b_bone.tail = b_bone.head + fixed_axis.xzy.normalized() * self.__scale
|
||||||
else:
|
else:
|
||||||
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * (self.__scale or 1.0)
|
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale
|
||||||
else:
|
else:
|
||||||
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * (self.__scale or 1.0)
|
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale
|
||||||
if m_bone.displayConnection != -1 and m_bone.displayConnection != [0.0, 0.0, 0.0]:
|
if m_bone.displayConnection != -1 and m_bone.displayConnection != [0.0, 0.0, 0.0]:
|
||||||
logger.debug(f"Special tip bone {b_bone.name}, display {str(m_bone.displayConnection)}")
|
logger.debug(f"Special tip bone {b_bone.name}, display {str(m_bone.displayConnection)}")
|
||||||
specialTipBones.append(b_bone.name)
|
specialTipBones.append(b_bone.name)
|
||||||
|
|
||||||
# Update bone roll
|
# Update bone roll
|
||||||
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
|
for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
|
||||||
if m_bone.localCoordinate is not None:
|
if m_bone.localCoordinate is not None:
|
||||||
FnBone.update_bone_roll(b_bone, m_bone.localCoordinate.x_axis, m_bone.localCoordinate.z_axis)
|
FnBone.update_bone_roll(b_bone, m_bone.localCoordinate.x_axis, m_bone.localCoordinate.z_axis)
|
||||||
elif FnBone.has_auto_local_axis(m_bone.name):
|
elif FnBone.has_auto_local_axis(m_bone.name):
|
||||||
FnBone.update_auto_bone_roll(b_bone)
|
FnBone.update_auto_bone_roll(b_bone)
|
||||||
|
|
||||||
# Set bone connections
|
# Set bone connections
|
||||||
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
|
for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
|
||||||
if isinstance(m_bone.displayConnection, int) and m_bone.displayConnection >= 0:
|
if isinstance(m_bone.displayConnection, int) and m_bone.displayConnection >= 0:
|
||||||
t = editBoneTable[m_bone.displayConnection]
|
t = editBoneTable[m_bone.displayConnection]
|
||||||
if t.parent is None or t.parent != b_bone:
|
if t.parent is None or t.parent != b_bone:
|
||||||
@@ -534,7 +535,8 @@ class PMXImporter:
|
|||||||
elif b_bone.name in specialTipBones:
|
elif b_bone.name in specialTipBones:
|
||||||
mmd_bone.is_tip = True
|
mmd_bone.is_tip = True
|
||||||
|
|
||||||
b_bone.bone.hide = not pmx_bone.visible # or mmd_bone.is_tip
|
# Blender 5.0: use pose bone hide for Pose/Object mode visibility
|
||||||
|
b_bone.hide = not pmx_bone.visible # or mmd_bone.is_tip
|
||||||
|
|
||||||
if not pmx_bone.isRotatable:
|
if not pmx_bone.isRotatable:
|
||||||
b_bone.lock_rotation = [True, True, True]
|
b_bone.lock_rotation = [True, True, True]
|
||||||
@@ -589,7 +591,7 @@ class PMXImporter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for i, (rigid, rigid_obj) in enumerate(zip(self.__model.rigids, rigid_pool)):
|
for i, (rigid, rigid_obj) in enumerate(zip(self.__model.rigids, rigid_pool)):
|
||||||
loc = Vector(rigid.location).xzy * (self.__scale or 1.0)
|
loc = Vector(rigid.location).xzy * self.__scale
|
||||||
rot = Vector(rigid.rotation).xzy * -1
|
rot = Vector(rigid.rotation).xzy * -1
|
||||||
size = Vector(rigid.size).xzy if rigid.type == pmx.Rigid.TYPE_BOX else Vector(rigid.size)
|
size = Vector(rigid.size).xzy if rigid.type == pmx.Rigid.TYPE_BOX else Vector(rigid.size)
|
||||||
|
|
||||||
@@ -598,7 +600,7 @@ class PMXImporter:
|
|||||||
shape_type=rigid.type,
|
shape_type=rigid.type,
|
||||||
location=loc,
|
location=loc,
|
||||||
rotation=rot,
|
rotation=rot,
|
||||||
size=size * (self.__scale or 1.0),
|
size=size * self.__scale,
|
||||||
dynamics_type=rigid.mode,
|
dynamics_type=rigid.mode,
|
||||||
name=rigid.name,
|
name=rigid.name,
|
||||||
name_e=rigid.name_e,
|
name_e=rigid.name_e,
|
||||||
@@ -636,7 +638,7 @@ class PMXImporter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for i, (joint, joint_obj) in enumerate(zip(self.__model.joints, joint_pool)):
|
for i, (joint, joint_obj) in enumerate(zip(self.__model.joints, joint_pool)):
|
||||||
loc = Vector(joint.location).xzy * (self.__scale or 1.0)
|
loc = Vector(joint.location).xzy * self.__scale
|
||||||
rot = Vector(joint.rotation).xzy * -1
|
rot = Vector(joint.rotation).xzy * -1
|
||||||
|
|
||||||
obj = FnRigidBody.setup_joint_object(
|
obj = FnRigidBody.setup_joint_object(
|
||||||
@@ -647,8 +649,8 @@ class PMXImporter:
|
|||||||
rotation=rot,
|
rotation=rot,
|
||||||
rigid_a=self.__rigidTable.get(joint.src_rigid, None),
|
rigid_a=self.__rigidTable.get(joint.src_rigid, None),
|
||||||
rigid_b=self.__rigidTable.get(joint.dest_rigid, None),
|
rigid_b=self.__rigidTable.get(joint.dest_rigid, None),
|
||||||
maximum_location=Vector(joint.maximum_location).xzy * (self.__scale or 1.0),
|
maximum_location=Vector(joint.maximum_location).xzy * self.__scale,
|
||||||
minimum_location=Vector(joint.minimum_location).xzy * (self.__scale or 1.0),
|
minimum_location=Vector(joint.minimum_location).xzy * self.__scale,
|
||||||
maximum_rotation=Vector(joint.minimum_rotation).xzy * -1,
|
maximum_rotation=Vector(joint.minimum_rotation).xzy * -1,
|
||||||
minimum_rotation=Vector(joint.maximum_rotation).xzy * -1,
|
minimum_rotation=Vector(joint.maximum_rotation).xzy * -1,
|
||||||
spring_linear=Vector(joint.spring_constant).xzy,
|
spring_linear=Vector(joint.spring_constant).xzy,
|
||||||
@@ -752,7 +754,7 @@ class PMXImporter:
|
|||||||
uv_layer.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in uv_table[i]))
|
uv_layer.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in uv_table[i]))
|
||||||
|
|
||||||
if hasattr(mesh, "uv_textures"):
|
if hasattr(mesh, "uv_textures"):
|
||||||
for bf, mi in zip(uv_tex.data, material_indices):
|
for bf, mi in zip(uv_tex.data, material_indices, strict=False):
|
||||||
bf.image = self.__imageTable.get(mi, None)
|
bf.image = self.__imageTable.get(mi, None)
|
||||||
|
|
||||||
if pmxModel.header and pmxModel.header.additional_uvs:
|
if pmxModel.header and pmxModel.header.additional_uvs:
|
||||||
@@ -829,14 +831,18 @@ class PMXImporter:
|
|||||||
logger.debug(f"Found {len(vertex_morphs)} vertex morphs")
|
logger.debug(f"Found {len(vertex_morphs)} vertex morphs")
|
||||||
|
|
||||||
for morph in vertex_morphs:
|
for morph in vertex_morphs:
|
||||||
shapeKey = self.__meshObj.shape_key_add(name=morph.name)
|
shapeKey = self.__meshObj.shape_key_add(name=morph.name, from_mix=False)
|
||||||
|
shapeKey.value = 0.0 # Set shape key value to 0 (inactive) on import
|
||||||
vtx_morph = mmd_root.vertex_morphs.add()
|
vtx_morph = mmd_root.vertex_morphs.add()
|
||||||
vtx_morph.name = morph.name
|
vtx_morph.name = morph.name
|
||||||
vtx_morph.name_e = morph.name_e
|
vtx_morph.name_e = morph.name_e
|
||||||
vtx_morph.category = categories.get(morph.category, "OTHER")
|
vtx_morph.category = categories.get(morph.category, "OTHER")
|
||||||
for md in morph.offsets:
|
for md in morph.offsets:
|
||||||
|
if md.index < len(shapeKey.data):
|
||||||
shapeKeyPoint = shapeKey.data[md.index]
|
shapeKeyPoint = shapeKey.data[md.index]
|
||||||
shapeKeyPoint.co += Vector(md.offset).xzy * (self.__scale or 1.0)
|
shapeKeyPoint.co += Vector(md.offset).xzy * self.__scale
|
||||||
|
else:
|
||||||
|
logger.warning(f"Morph {morph.name} has out-of-range vertex index: {md.index}")
|
||||||
logger.debug(f"Imported vertex morph: {morph.name} with {len(morph.offsets)} offsets")
|
logger.debug(f"Imported vertex morph: {morph.name} with {len(morph.offsets)} offsets")
|
||||||
|
|
||||||
def __importMaterialMorphs(self) -> None:
|
def __importMaterialMorphs(self) -> None:
|
||||||
@@ -897,7 +903,7 @@ class PMXImporter:
|
|||||||
data = bone_morph.data.add()
|
data = bone_morph.data.add()
|
||||||
bl_bone = self.__boneTable[morph_data.index]
|
bl_bone = self.__boneTable[morph_data.index]
|
||||||
data.bone = bl_bone.name
|
data.bone = bl_bone.name
|
||||||
converter = BoneConverter(bl_bone, self.__scale or 1.0)
|
converter = BoneConverter(bl_bone, self.__scale)
|
||||||
data.location = converter.convert_location(morph_data.location_offset)
|
data.location = converter.convert_location(morph_data.location_offset)
|
||||||
data.rotation = converter.convert_rotation(morph_data.rotation_offset)
|
data.rotation = converter.convert_rotation(morph_data.rotation_offset)
|
||||||
valid_offsets += 1
|
valid_offsets += 1
|
||||||
@@ -1000,12 +1006,19 @@ class PMXImporter:
|
|||||||
armModifier = meshObj.modifiers.new(name="Armature", type="ARMATURE")
|
armModifier = meshObj.modifiers.new(name="Armature", type="ARMATURE")
|
||||||
armModifier.object = armObj
|
armModifier.object = armObj
|
||||||
armModifier.use_vertex_groups = True
|
armModifier.use_vertex_groups = True
|
||||||
armModifier.name = "mmd_bone_order_override"
|
armModifier.name = "mmd_armature"
|
||||||
armModifier.show_render = armModifier.show_viewport = len(meshObj.data.vertices) > 0
|
|
||||||
logger.debug("Armature modifier added")
|
logger.debug("Armature modifier added")
|
||||||
|
|
||||||
def __assignCustomNormals(self) -> None:
|
def __assignCustomNormals(self) -> None:
|
||||||
"""Assign custom normals to the mesh"""
|
"""Assign custom normals to the mesh"""
|
||||||
|
# NOTE: This uses the older Blender API instead of the newer mesh.attributes approach
|
||||||
|
# because it requires "INT16_2D" format for proper functionality.
|
||||||
|
# Manual calculation of normals in INT16_2D format is overly complex.
|
||||||
|
# The newer implementation was removed in commit [ad47b9a] due to these issues.
|
||||||
|
# The current implementation uses normals_split_custom_set() with 179-degree sharp edge
|
||||||
|
# marking as a workaround. While not ideal, this remains the most practical solution
|
||||||
|
# for preserving custom normals in most cases.
|
||||||
|
|
||||||
if not self.__meshObj or not self.__model:
|
if not self.__meshObj or not self.__model:
|
||||||
logger.error("Mesh object or model not created")
|
logger.error("Mesh object or model not created")
|
||||||
return
|
return
|
||||||
@@ -1013,17 +1026,41 @@ class PMXImporter:
|
|||||||
mesh: Mesh = self.__meshObj.data
|
mesh: Mesh = self.__meshObj.data
|
||||||
logger.info("Setting custom normals...")
|
logger.info("Setting custom normals...")
|
||||||
|
|
||||||
|
# CRITICAL: Mark sharp edges (based on angle) BEFORE setting custom normals
|
||||||
|
# For mesh.normals_split_custom_set() to work as expected, two conditions must be met:
|
||||||
|
# 1. The normal vectors must be non-zero (mentioned in Blender documentation)
|
||||||
|
# 2. Some edges must be marked as sharp (NOT mentioned in Blender documentation)
|
||||||
|
# An angle of 179 degrees is confirmed to be sufficient to preserve all custom normals.
|
||||||
|
# 180 degrees does not work because it misses some sharp edges required for normals_split_custom_set to work 100% correctly.
|
||||||
|
current_mode = bpy.context.active_object.mode if bpy.context.active_object else 'OBJECT'
|
||||||
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
bpy.context.view_layer.objects.active = self.__meshObj
|
||||||
|
|
||||||
|
# Mark sharp edges
|
||||||
|
bpy.ops.object.mode_set(mode="EDIT")
|
||||||
|
bpy.ops.mesh.select_all(action="DESELECT")
|
||||||
|
bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(179))
|
||||||
|
bpy.ops.mesh.mark_sharp()
|
||||||
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
total_edges = len(mesh.edges)
|
||||||
|
sharp_edges = sum(1 for edge in mesh.edges if edge.use_edge_sharp)
|
||||||
|
percentage = (sharp_edges / total_edges) * 100 if total_edges > 0 else 0
|
||||||
|
logger.info(f" - Marked {sharp_edges}/{total_edges} ({percentage:.2f}%) sharp edges with angle: 179 degrees")
|
||||||
|
|
||||||
if self.__vertex_map:
|
if self.__vertex_map:
|
||||||
verts, faces = self.__model.vertices, self.__model.faces
|
verts, faces = self.__model.vertices, self.__model.faces
|
||||||
custom_normals = [(Vector(verts[i].normal).xzy).normalized() for f in faces for i in f]
|
custom_normals = [(Vector(verts[i].normal).xzy).normalized() for f in faces for i in f]
|
||||||
mesh.normals_split_custom_set(custom_normals)
|
mesh.normals_split_custom_set(custom_normals)
|
||||||
logger.debug(f"Set {len(custom_normals)} custom normals using face data")
|
|
||||||
else:
|
else:
|
||||||
custom_normals = [(Vector(v.normal).xzy).normalized() for v in self.__model.vertices]
|
custom_normals = [(Vector(v.normal).xzy).normalized() for v in self.__model.vertices]
|
||||||
mesh.normals_split_custom_set_from_vertices(custom_normals)
|
mesh.normals_split_custom_set_from_vertices(custom_normals)
|
||||||
logger.debug(f"Set {len(custom_normals)} custom normals from vertices")
|
|
||||||
|
|
||||||
logger.info("Custom normals set successfully")
|
bpy.ops.object.mode_set(mode=current_mode)
|
||||||
|
logger.info(" - Done!!")
|
||||||
|
# Continue without custom normals - mesh will use auto-calculated normals
|
||||||
|
|
||||||
def __renameLRBones(self, use_underscore: bool) -> None:
|
def __renameLRBones(self, use_underscore: bool) -> None:
|
||||||
"""Rename bones with left/right naming convention"""
|
"""Rename bones with left/right naming convention"""
|
||||||
|
|||||||
+19
-44
@@ -1,17 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
|
||||||
|
|
||||||
from typing import List, Optional, Tuple, Union, Dict, Any, Set, cast
|
from ....core.logging_setup import logger
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from mathutils import Euler, Vector, Matrix
|
from mathutils import Euler, Vector
|
||||||
|
|
||||||
from ..bpyutils import FnContext, Props
|
from ..bpyutils import FnContext, Props
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
SHAPE_SPHERE = 0
|
SHAPE_SPHERE = 0
|
||||||
SHAPE_BOX = 1
|
SHAPE_BOX = 1
|
||||||
@@ -22,30 +18,25 @@ MODE_DYNAMIC = 1
|
|||||||
MODE_DYNAMIC_BONE = 2
|
MODE_DYNAMIC_BONE = 2
|
||||||
|
|
||||||
|
|
||||||
def shapeType(collision_shape: str) -> int:
|
def shapeType(collision_shape):
|
||||||
"""Convert collision shape name to type index"""
|
|
||||||
return ("SPHERE", "BOX", "CAPSULE").index(collision_shape)
|
return ("SPHERE", "BOX", "CAPSULE").index(collision_shape)
|
||||||
|
|
||||||
|
|
||||||
def collisionShape(shape_type: int) -> str:
|
def collisionShape(shape_type):
|
||||||
"""Convert shape type index to collision shape name"""
|
|
||||||
return ("SPHERE", "BOX", "CAPSULE")[shape_type]
|
return ("SPHERE", "BOX", "CAPSULE")[shape_type]
|
||||||
|
|
||||||
|
|
||||||
def setRigidBodyWorldEnabled(enable: bool) -> bool:
|
def setRigidBodyWorldEnabled(enable):
|
||||||
"""Enable or disable the rigid body world and return previous state"""
|
|
||||||
if bpy.ops.rigidbody.world_add.poll():
|
if bpy.ops.rigidbody.world_add.poll():
|
||||||
logger.debug("Creating rigid body world")
|
|
||||||
bpy.ops.rigidbody.world_add()
|
bpy.ops.rigidbody.world_add()
|
||||||
rigidbody_world = bpy.context.scene.rigidbody_world
|
rigidbody_world = bpy.context.scene.rigidbody_world
|
||||||
enabled = rigidbody_world.enabled
|
enabled = rigidbody_world.enabled
|
||||||
rigidbody_world.enabled = enable
|
rigidbody_world.enabled = enable
|
||||||
logger.debug(f"Rigid body world enabled: {enable} (was: {enabled})")
|
|
||||||
return enabled
|
return enabled
|
||||||
|
|
||||||
|
|
||||||
class RigidBodyMaterial:
|
class RigidBodyMaterial:
|
||||||
COLORS: List[int] = [
|
COLORS = [
|
||||||
0x7FDDD4,
|
0x7FDDD4,
|
||||||
0xF0E68C,
|
0xF0E68C,
|
||||||
0xEE82EE,
|
0xEE82EE,
|
||||||
@@ -65,12 +56,10 @@ class RigidBodyMaterial:
|
|||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getMaterial(cls, number: int) -> bpy.types.Material:
|
def getMaterial(cls, number):
|
||||||
"""Get or create a material for rigid bodies with the specified number"""
|
|
||||||
number = int(number)
|
number = int(number)
|
||||||
material_name = f"mmd_tools_rigid_{number}"
|
material_name = "mmd_tools_rigid_%d" % (number)
|
||||||
if material_name not in bpy.data.materials:
|
if material_name not in bpy.data.materials:
|
||||||
logger.debug(f"Creating rigid body material: {material_name}")
|
|
||||||
mat = bpy.data.materials.new(material_name)
|
mat = bpy.data.materials.new(material_name)
|
||||||
color = cls.COLORS[number]
|
color = cls.COLORS[number]
|
||||||
mat.diffuse_color[:3] = [((0xFF0000 & color) >> 16) / float(255), ((0x00FF00 & color) >> 8) / float(255), (0x0000FF & color) / float(255)]
|
mat.diffuse_color[:3] = [((0xFF0000 & color) >> 16) / float(255), ((0x00FF00 & color) >> 8) / float(255), (0x0000FF & color) / float(255)]
|
||||||
@@ -97,11 +86,9 @@ class RigidBodyMaterial:
|
|||||||
class FnRigidBody:
|
class FnRigidBody:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]:
|
def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]:
|
||||||
"""Create multiple rigid body objects parented to the specified object"""
|
|
||||||
if count < 1:
|
if count < 1:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
logger.debug(f"Creating {count} rigid body objects parented to {parent_object.name}")
|
|
||||||
obj = FnRigidBody.new_rigid_body_object(context, parent_object)
|
obj = FnRigidBody.new_rigid_body_object(context, parent_object)
|
||||||
|
|
||||||
if count == 1:
|
if count == 1:
|
||||||
@@ -111,8 +98,6 @@ class FnRigidBody:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object:
|
def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object:
|
||||||
"""Create a new rigid body object parented to the specified object"""
|
|
||||||
logger.debug(f"Creating new rigid body object parented to {parent_object.name}")
|
|
||||||
obj = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody"))
|
obj = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody"))
|
||||||
obj.parent = parent_object
|
obj.parent = parent_object
|
||||||
obj.mmd_type = "RIGID_BODY"
|
obj.mmd_type = "RIGID_BODY"
|
||||||
@@ -130,11 +115,11 @@ class FnRigidBody:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def setup_rigid_body_object(
|
def setup_rigid_body_object(
|
||||||
obj: bpy.types.Object,
|
obj: bpy.types.Object,
|
||||||
shape_type: int,
|
shape_type: str,
|
||||||
location: Vector,
|
location: Vector,
|
||||||
rotation: Euler,
|
rotation: Euler,
|
||||||
size: Vector,
|
size: Vector,
|
||||||
dynamics_type: int,
|
dynamics_type: str,
|
||||||
collision_group_number: Optional[int] = None,
|
collision_group_number: Optional[int] = None,
|
||||||
collision_group_mask: Optional[List[bool]] = None,
|
collision_group_mask: Optional[List[bool]] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
@@ -146,8 +131,6 @@ class FnRigidBody:
|
|||||||
linear_damping: Optional[float] = None,
|
linear_damping: Optional[float] = None,
|
||||||
bounce: Optional[float] = None,
|
bounce: Optional[float] = None,
|
||||||
) -> bpy.types.Object:
|
) -> bpy.types.Object:
|
||||||
"""Set up a rigid body object with the specified parameters"""
|
|
||||||
logger.debug(f"Setting up rigid body object: {obj.name}")
|
|
||||||
obj.location = location
|
obj.location = location
|
||||||
obj.rotation_euler = rotation
|
obj.rotation_euler = rotation
|
||||||
|
|
||||||
@@ -189,35 +172,31 @@ class FnRigidBody:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_rigid_body_size(obj: bpy.types.Object) -> Tuple[float, float, float]:
|
def get_rigid_body_size(obj: bpy.types.Object):
|
||||||
"""Get the size of a rigid body object based on its shape type"""
|
|
||||||
assert obj.mmd_type == "RIGID_BODY"
|
assert obj.mmd_type == "RIGID_BODY"
|
||||||
|
|
||||||
x0, y0, z0 = obj.bound_box[0]
|
x0, y0, z0 = obj.bound_box[0]
|
||||||
x1, y1, z1 = obj.bound_box[6]
|
x1, y1, z1 = obj.bound_box[6]
|
||||||
assert x1 >= x0 and y1 >= y0 and z1 >= z0
|
if not (x1 >= x0 and y1 >= y0 and z1 >= z0):
|
||||||
|
logger.warning(f"Rigid body '{obj.name}' has invalid bounding box coordinates, using default size")
|
||||||
|
return (1.0, 1.0, 1.0)
|
||||||
|
|
||||||
shape = obj.mmd_rigid.shape
|
shape = obj.mmd_rigid.shape
|
||||||
if shape == "SPHERE":
|
if shape == "SPHERE":
|
||||||
radius = (z1 - z0) / 2
|
radius = (z1 - z0) / 2
|
||||||
return (radius, 0.0, 0.0)
|
return (radius, 0.0, 0.0)
|
||||||
elif shape == "BOX":
|
if shape == "BOX":
|
||||||
x, y, z = (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2
|
x, y, z = (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2
|
||||||
return (x, y, z)
|
return (x, y, z)
|
||||||
elif shape == "CAPSULE":
|
if shape == "CAPSULE":
|
||||||
diameter = x1 - x0
|
diameter = x1 - x0
|
||||||
radius = diameter / 2
|
radius = diameter / 2
|
||||||
height = abs((z1 - z0) - diameter)
|
height = abs((z1 - z0) - diameter)
|
||||||
return (radius, height, 0.0)
|
return (radius, height, 0.0)
|
||||||
else:
|
raise ValueError(f"Invalid shape type: {shape}")
|
||||||
error_msg = f"Invalid shape type: {shape}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
raise ValueError(error_msg)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object:
|
def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object:
|
||||||
"""Create a new joint object parented to the specified object"""
|
|
||||||
logger.debug(f"Creating new joint object parented to {parent_object.name}")
|
|
||||||
obj = FnContext.new_and_link_object(context, name="Joint", object_data=None)
|
obj = FnContext.new_and_link_object(context, name="Joint", object_data=None)
|
||||||
obj.parent = parent_object
|
obj.parent = parent_object
|
||||||
obj.mmd_type = "JOINT"
|
obj.mmd_type = "JOINT"
|
||||||
@@ -249,11 +228,9 @@ class FnRigidBody:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]:
|
def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]:
|
||||||
"""Create multiple joint objects parented to the specified object"""
|
|
||||||
if count < 1:
|
if count < 1:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
logger.debug(f"Creating {count} joint objects parented to {parent_object.name}")
|
|
||||||
obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size)
|
obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size)
|
||||||
|
|
||||||
if count == 1:
|
if count == 1:
|
||||||
@@ -277,8 +254,6 @@ class FnRigidBody:
|
|||||||
name: str,
|
name: str,
|
||||||
name_e: Optional[str] = None,
|
name_e: Optional[str] = None,
|
||||||
) -> bpy.types.Object:
|
) -> bpy.types.Object:
|
||||||
"""Set up a joint object with the specified parameters"""
|
|
||||||
logger.debug(f"Setting up joint object: {obj.name} with name {name}")
|
|
||||||
obj.name = f"J.{name}"
|
obj.name = f"J.{name}"
|
||||||
|
|
||||||
obj.location = location
|
obj.location = location
|
||||||
|
|||||||
+54
-79
@@ -1,52 +1,42 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2018 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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
|
from ....core.logging_setup import logger
|
||||||
import time
|
import time
|
||||||
from typing import Dict, List, Tuple, Set, Optional, Any, Union, cast, TypeVar, Callable
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from mathutils import Matrix, Vector, Quaternion, Euler
|
from mathutils import Matrix, Vector
|
||||||
from bpy.types import Object, PoseBone, Pose, ShapeKey, Modifier, VertexGroup
|
|
||||||
|
|
||||||
from ..bpyutils import FnObject
|
from ..bpyutils import FnObject
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
|
||||||
|
|
||||||
def _hash(v: Union[Object, PoseBone, Pose]) -> int:
|
def _hash(v):
|
||||||
if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)):
|
if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)):
|
||||||
return hash(type(v).__name__ + v.name)
|
return hash(type(v).__name__ + v.name)
|
||||||
elif isinstance(v, bpy.types.Pose):
|
if isinstance(v, bpy.types.Pose):
|
||||||
return hash(type(v).__name__ + v.id_data.name)
|
return hash(type(v).__name__ + v.id_data.name)
|
||||||
else:
|
|
||||||
raise NotImplementedError("hash")
|
raise NotImplementedError("hash")
|
||||||
|
|
||||||
|
|
||||||
class FnSDEF:
|
class FnSDEF:
|
||||||
g_verts: Dict[int, Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]] = {} # global cache
|
g_verts = {} # global cache
|
||||||
g_shapekey_data: Dict[int, Optional[np.ndarray]] = {}
|
g_shapekey_data = {}
|
||||||
g_bone_check: Dict[int, Dict[Union[Tuple[int, int], str], Union[Tuple[Matrix, Matrix], bool]]] = {}
|
g_bone_check = {}
|
||||||
__g_armature_check: Dict[int, Optional[int]] = {}
|
__g_armature_check = {}
|
||||||
SHAPEKEY_NAME: str = "mmd_sdef_skinning"
|
SHAPEKEY_NAME = "mmd_sdef_skinning"
|
||||||
MASK_NAME: str = "mmd_sdef_mask"
|
MASK_NAME = "mmd_sdef_mask"
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self):
|
||||||
raise NotImplementedError("not allowed")
|
raise NotImplementedError("not allowed")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init_cache(cls, obj: Object, shapekey: ShapeKey) -> bool:
|
def __init_cache(cls, obj, shapekey):
|
||||||
key = _hash(obj)
|
key = _hash(obj)
|
||||||
obj = getattr(obj, "original", obj)
|
obj = getattr(obj, "original", obj)
|
||||||
mod = obj.modifiers.get("mmd_bone_order_override")
|
mod = obj.modifiers.get("mmd_armature")
|
||||||
key_armature = _hash(mod.object.pose) if mod and mod.type == "ARMATURE" and mod.object else None
|
key_armature = _hash(mod.object.pose) if mod and mod.type == "ARMATURE" and mod.object else None
|
||||||
if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature:
|
if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature:
|
||||||
logger.debug(f"Initializing SDEF cache for {obj.name}")
|
|
||||||
cls.g_verts[key] = cls.__find_vertices(obj)
|
cls.g_verts[key] = cls.__find_vertices(obj)
|
||||||
cls.g_bone_check[key] = {}
|
cls.g_bone_check[key] = {}
|
||||||
cls.__g_armature_check[key] = key_armature
|
cls.__g_armature_check[key] = key_armature
|
||||||
@@ -55,7 +45,7 @@ class FnSDEF:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __check_bone_update(cls, obj: Object, bone0: PoseBone, bone1: PoseBone) -> bool:
|
def __check_bone_update(cls, obj, bone0, bone1):
|
||||||
check = cls.g_bone_check[_hash(obj)]
|
check = cls.g_bone_check[_hash(obj)]
|
||||||
key = (_hash(bone0), _hash(bone1))
|
key = (_hash(bone0), _hash(bone1))
|
||||||
if key not in check or (bone0.matrix, bone1.matrix) != check[key]:
|
if key not in check or (bone0.matrix, bone1.matrix) != check[key]:
|
||||||
@@ -64,21 +54,20 @@ class FnSDEF:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mute_sdef_set(cls, obj: Object, mute: bool) -> None:
|
def mute_sdef_set(cls, obj, mute):
|
||||||
key_blocks = getattr(obj.data.shape_keys, "key_blocks", ())
|
key_blocks = getattr(obj.data.shape_keys, "key_blocks", ())
|
||||||
if cls.SHAPEKEY_NAME in key_blocks:
|
if cls.SHAPEKEY_NAME in key_blocks:
|
||||||
shapekey = key_blocks[cls.SHAPEKEY_NAME]
|
shapekey = key_blocks[cls.SHAPEKEY_NAME]
|
||||||
shapekey.mute = mute
|
shapekey.mute = mute
|
||||||
if cls.has_sdef_data(obj):
|
if cls.has_sdef_data(obj):
|
||||||
logger.debug(f"Setting SDEF mute state to {mute} for {obj.name}")
|
|
||||||
cls.__init_cache(obj, shapekey)
|
cls.__init_cache(obj, shapekey)
|
||||||
cls.__sdef_muted(obj, shapekey)
|
cls.__sdef_muted(obj, shapekey)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __sdef_muted(cls, obj: Object, shapekey: ShapeKey) -> bool:
|
def __sdef_muted(cls, obj, shapekey):
|
||||||
mute = shapekey.mute
|
mute = shapekey.mute
|
||||||
if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"):
|
if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"):
|
||||||
mod = obj.modifiers.get("mmd_bone_order_override")
|
mod = obj.modifiers.get("mmd_armature")
|
||||||
if mod and mod.type == "ARMATURE":
|
if mod and mod.type == "ARMATURE":
|
||||||
if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != "EDIT":
|
if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != "EDIT":
|
||||||
mask = tuple(i for v in cls.g_verts[_hash(obj)].values() for i in v[3])
|
mask = tuple(i for v in cls.g_verts[_hash(obj)].values() for i in v[3])
|
||||||
@@ -87,33 +76,32 @@ class FnSDEF:
|
|||||||
mod.invert_vertex_group = True
|
mod.invert_vertex_group = True
|
||||||
shapekey.vertex_group = cls.MASK_NAME
|
shapekey.vertex_group = cls.MASK_NAME
|
||||||
cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute
|
cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute
|
||||||
logger.debug(f"SDEF mute state updated to {mute} for {obj.name}")
|
|
||||||
return mute
|
return mute
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_sdef_data(obj: Object) -> bool:
|
def has_sdef_data(obj):
|
||||||
mod = obj.modifiers.get("mmd_bone_order_override")
|
if obj is None or not hasattr(obj, "modifiers") or not hasattr(obj, "data") or obj.data is None:
|
||||||
|
return False
|
||||||
|
mod = obj.modifiers.get("mmd_armature")
|
||||||
if mod and mod.type == "ARMATURE" and mod.object:
|
if mod and mod.type == "ARMATURE" and mod.object:
|
||||||
kb = getattr(obj.data.shape_keys, "key_blocks", None)
|
kb = getattr(obj.data.shape_keys, "key_blocks", None)
|
||||||
return kb and "mmd_sdef_c" in kb and "mmd_sdef_r0" in kb and "mmd_sdef_r1" in kb
|
return kb and "mmd_sdef_c" in kb and "mmd_sdef_r0" in kb and "mmd_sdef_r1" in kb
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __find_vertices(cls, obj: Object) -> Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]:
|
def __find_vertices(cls, obj):
|
||||||
if not cls.has_sdef_data(obj):
|
if not cls.has_sdef_data(obj):
|
||||||
|
logger.debug(f"SDEF vertex search skipped for '{obj.name}': No SDEF data found")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
vertices: Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]] = {}
|
vertices = {}
|
||||||
pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones
|
pose_bones = obj.modifiers.get("mmd_armature").object.pose.bones
|
||||||
bone_map: Dict[int, PoseBone] = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones}
|
bone_map = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones}
|
||||||
sdef_c = obj.data.shape_keys.key_blocks["mmd_sdef_c"].data
|
sdef_c = obj.data.shape_keys.key_blocks["mmd_sdef_c"].data
|
||||||
sdef_r0 = obj.data.shape_keys.key_blocks["mmd_sdef_r0"].data
|
sdef_r0 = obj.data.shape_keys.key_blocks["mmd_sdef_r0"].data
|
||||||
sdef_r1 = obj.data.shape_keys.key_blocks["mmd_sdef_r1"].data
|
sdef_r1 = obj.data.shape_keys.key_blocks["mmd_sdef_r1"].data
|
||||||
vd = obj.data.vertices
|
vd = obj.data.vertices
|
||||||
|
|
||||||
logger.debug(f"Finding SDEF vertices for {obj.name}")
|
|
||||||
vertex_count = 0
|
|
||||||
|
|
||||||
for i in range(len(sdef_c)):
|
for i in range(len(sdef_c)):
|
||||||
if vd[i].co != sdef_c[i].co:
|
if vd[i].co != sdef_c[i].co:
|
||||||
bgs = [g for g in vd[i].groups if g.group in bone_map and g.weight] # bone groups
|
bgs = [g for g in vd[i].groups if g.group in bone_map and g.weight] # bone groups
|
||||||
@@ -122,7 +110,7 @@ class FnSDEF:
|
|||||||
# preprocessing
|
# preprocessing
|
||||||
w0, w1 = bgs[0].weight, bgs[1].weight
|
w0, w1 = bgs[0].weight, bgs[1].weight
|
||||||
# w0 + w1 == 1
|
# w0 + w1 == 1
|
||||||
w0 = w0 / (w0 + w1)
|
w0 /= (w0 + w1)
|
||||||
w1 = 1 - w0
|
w1 = 1 - w0
|
||||||
|
|
||||||
c, r0, r1 = sdef_c[i].co, sdef_r0[i].co, sdef_r1[i].co
|
c, r0, r1 = sdef_c[i].co, sdef_r0[i].co, sdef_r1[i].co
|
||||||
@@ -136,19 +124,22 @@ class FnSDEF:
|
|||||||
vertices[key] = (bone_map[bgs[0].group], bone_map[bgs[1].group], [], [])
|
vertices[key] = (bone_map[bgs[0].group], bone_map[bgs[1].group], [], [])
|
||||||
vertices[key][2].append((i, w0, w1, vd[i].co - c, (c + r0) / 2, (c + r1) / 2))
|
vertices[key][2].append((i, w0, w1, vd[i].co - c, (c + r0) / 2, (c + r1) / 2))
|
||||||
vertices[key][3].append(i)
|
vertices[key][3].append(i)
|
||||||
vertex_count += 1
|
|
||||||
|
|
||||||
logger.debug(f"Found {vertex_count} SDEF vertices in {obj.name}")
|
|
||||||
return vertices
|
return vertices
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def driver_function_wrap(cls, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float:
|
def driver_function_wrap(cls, obj_name, bulk_update, use_skip, use_scale):
|
||||||
|
if obj_name not in bpy.data.objects:
|
||||||
|
logger.warning(f"SDEF driver wrap: Object '{obj_name}' not found")
|
||||||
|
return 0.0
|
||||||
obj = bpy.data.objects[obj_name]
|
obj = bpy.data.objects[obj_name]
|
||||||
shapekey = obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]
|
shapekey = obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]
|
||||||
return cls.driver_function(shapekey, obj_name, bulk_update, use_skip, use_scale)
|
return cls.driver_function(shapekey, obj_name, bulk_update, use_skip, use_scale)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def driver_function(cls, shapekey: ShapeKey, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float:
|
def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale):
|
||||||
|
if obj_name not in bpy.data.objects:
|
||||||
|
logger.warning(f"SDEF driver: Object '{obj_name}' not found, driver will be inactive")
|
||||||
|
return 0.0
|
||||||
obj = bpy.data.objects[obj_name]
|
obj = bpy.data.objects[obj_name]
|
||||||
if getattr(shapekey.id_data, "is_evaluated", False):
|
if getattr(shapekey.id_data, "is_evaluated", False):
|
||||||
# For Blender 2.8x, we should use evaluated object, and the only reference is the "obj" variable of SDEF driver
|
# For Blender 2.8x, we should use evaluated object, and the only reference is the "obj" variable of SDEF driver
|
||||||
@@ -159,7 +150,7 @@ class FnSDEF:
|
|||||||
if cls.__sdef_muted(obj, shapekey):
|
if cls.__sdef_muted(obj, shapekey):
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones
|
pose_bones = obj.modifiers.get("mmd_armature").object.pose.bones
|
||||||
if not bulk_update:
|
if not bulk_update:
|
||||||
shapekey_data = shapekey.data
|
shapekey_data = shapekey.data
|
||||||
if use_scale:
|
if use_scale:
|
||||||
@@ -200,8 +191,6 @@ class FnSDEF:
|
|||||||
else: # bulk update
|
else: # bulk update
|
||||||
shapekey_data = cls.g_shapekey_data[_hash(obj)]
|
shapekey_data = cls.g_shapekey_data[_hash(obj)]
|
||||||
if shapekey_data is None:
|
if shapekey_data is None:
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
shapekey_data = np.zeros(len(shapekey.data) * 3, dtype=np.float32)
|
shapekey_data = np.zeros(len(shapekey.data) * 3, dtype=np.float32)
|
||||||
shapekey.data.foreach_get("co", shapekey_data)
|
shapekey.data.foreach_get("co", shapekey_data)
|
||||||
shapekey_data = cls.g_shapekey_data[_hash(obj)] = shapekey_data.reshape(len(shapekey.data), 3)
|
shapekey_data = cls.g_shapekey_data[_hash(obj)] = shapekey_data.reshape(len(shapekey.data), 3)
|
||||||
@@ -220,15 +209,15 @@ class FnSDEF:
|
|||||||
rot1 = -rot1
|
rot1 = -rot1
|
||||||
s0, s1 = mat0.to_scale(), mat1.to_scale()
|
s0, s1 = mat0.to_scale(), mat1.to_scale()
|
||||||
|
|
||||||
def scale(mat_rot: Matrix, w0: float, w1: float) -> Matrix:
|
def scale(mat_rot, w0, w1, s0, s1):
|
||||||
s = s0 * w0 + s1 * w1
|
s = s0 * w0 + s1 * w1
|
||||||
return mat_rot @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])])
|
return mat_rot @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])])
|
||||||
|
|
||||||
def offset(mat_rot: Matrix, pos_c: Vector, vid: int) -> Vector:
|
def offset(mat_rot, pos_c, vid):
|
||||||
delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = ''
|
delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = ''
|
||||||
return (mat_rot @ (pos_c + delta)) - delta
|
return (mat_rot @ (pos_c + delta)) - delta
|
||||||
|
|
||||||
shapekey_data[vids] = [offset(scale((rot0 * w0 + rot1 * w1).normalized().to_matrix(), w0, w1), pos_c, vid) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data]
|
shapekey_data[vids] = [offset(scale((rot0 * w0 + rot1 * w1).normalized().to_matrix(), w0, w1, s0, s1), pos_c, vid) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data]
|
||||||
else:
|
else:
|
||||||
# bulk update
|
# bulk update
|
||||||
for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values():
|
for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values():
|
||||||
@@ -247,19 +236,16 @@ class FnSDEF:
|
|||||||
return 1.0 # shapkey value
|
return 1.0 # shapkey value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_driver_function(cls) -> None:
|
def register_driver_function(cls):
|
||||||
"""Register driver functions in Blender's driver namespace."""
|
|
||||||
if "mmd_sdef_driver" not in bpy.app.driver_namespace:
|
if "mmd_sdef_driver" not in bpy.app.driver_namespace:
|
||||||
logger.debug("Registering SDEF driver function")
|
|
||||||
bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function
|
bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function
|
||||||
if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace:
|
if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace:
|
||||||
logger.debug("Registering SDEF driver wrapper function")
|
|
||||||
bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap
|
bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap
|
||||||
|
|
||||||
BENCH_LOOP: int = 10
|
BENCH_LOOP = 10
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __get_benchmark_result(cls, obj: Object, shapkey: ShapeKey, use_scale: bool, use_skip: bool) -> bool:
|
def __get_benchmark_result(cls, obj, shapkey, use_scale, use_skip):
|
||||||
# warmed up
|
# warmed up
|
||||||
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
|
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
|
||||||
cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale)
|
cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale)
|
||||||
@@ -273,15 +259,15 @@ class FnSDEF:
|
|||||||
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
|
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
|
||||||
bulk_time = time.time() - t
|
bulk_time = time.time() - t
|
||||||
result = default_time > bulk_time
|
result = default_time > bulk_time
|
||||||
logger.info(f"SDEF benchmark for {obj.name}: default {default_time:.4f}s vs bulk_update {bulk_time:.4f}s => bulk_update={result}")
|
logger.info("FnSDEF:benchmark: default %.4f vs bulk_update %.4f => bulk_update=%s", default_time, bulk_time, result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def bind(cls, obj: Object, bulk_update: Optional[bool] = None, use_skip: bool = True, use_scale: bool = False) -> bool:
|
def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False):
|
||||||
# Unbind first
|
# Unbind first
|
||||||
cls.unbind(obj)
|
cls.unbind(obj)
|
||||||
if not cls.has_sdef_data(obj):
|
if not cls.has_sdef_data(obj):
|
||||||
logger.debug(f"Object {obj.name} does not have SDEF data")
|
logger.debug(f"SDEF bind skipped for '{obj.name}': No SDEF data found")
|
||||||
return False
|
return False
|
||||||
# Create the shapekey for the driver
|
# Create the shapekey for the driver
|
||||||
shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False)
|
shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False)
|
||||||
@@ -300,50 +286,41 @@ class FnSDEF:
|
|||||||
ov.type = "SINGLE_PROP"
|
ov.type = "SINGLE_PROP"
|
||||||
ov.targets[0].id = obj
|
ov.targets[0].id = obj
|
||||||
ov.targets[0].data_path = "name"
|
ov.targets[0].data_path = "name"
|
||||||
if not bulk_update and use_skip: # FIXME: force disable use_skip=True for bulk_update=False on 2.8
|
mod = obj.modifiers.get("mmd_armature")
|
||||||
use_skip = False
|
|
||||||
mod = obj.modifiers.get("mmd_bone_order_override")
|
|
||||||
variables = f.driver.variables
|
variables = f.driver.variables
|
||||||
for name in set(data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)): # add required bones for dependency graph
|
for name in {data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)}: # add required bones for dependency graph
|
||||||
var = variables.new()
|
var = variables.new()
|
||||||
var.type = "TRANSFORMS"
|
var.type = "TRANSFORMS"
|
||||||
var.targets[0].id = mod.object
|
var.targets[0].id = mod.object
|
||||||
var.targets[0].bone_target = name
|
var.targets[0].bone_target = name
|
||||||
f.driver.use_self = True
|
f.driver.use_self = True
|
||||||
param = (bulk_update, use_skip, use_scale)
|
f.driver.expression = f"mmd_sdef_driver(self, obj, bulk_update={bulk_update}, use_skip={use_skip}, use_scale={use_scale})"
|
||||||
f.driver.expression = "mmd_sdef_driver(self, obj, bulk_update={}, use_skip={}, use_scale={})".format(*param)
|
|
||||||
logger.info(f"Successfully bound SDEF to {obj.name} with bulk_update={bulk_update}, use_skip={use_skip}, use_scale={use_scale}")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def unbind(cls, obj: Object) -> None:
|
def unbind(cls, obj):
|
||||||
if obj.data.shape_keys:
|
if obj.data.shape_keys:
|
||||||
if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks:
|
if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks:
|
||||||
logger.debug(f"Removing SDEF shape key from {obj.name}")
|
|
||||||
FnObject.mesh_remove_shape_key(obj, obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME])
|
FnObject.mesh_remove_shape_key(obj, obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME])
|
||||||
for mod in obj.modifiers:
|
for mod in obj.modifiers:
|
||||||
if mod.type == "ARMATURE" and mod.vertex_group == cls.MASK_NAME:
|
if mod.type == "ARMATURE" and mod.vertex_group == cls.MASK_NAME:
|
||||||
logger.debug(f"Clearing SDEF vertex group from modifier in {obj.name}")
|
|
||||||
mod.vertex_group = ""
|
mod.vertex_group = ""
|
||||||
mod.invert_vertex_group = False
|
mod.invert_vertex_group = False
|
||||||
break
|
break
|
||||||
if cls.MASK_NAME in obj.vertex_groups:
|
if cls.MASK_NAME in obj.vertex_groups:
|
||||||
logger.debug(f"Removing SDEF vertex group from {obj.name}")
|
|
||||||
obj.vertex_groups.remove(obj.vertex_groups[cls.MASK_NAME])
|
obj.vertex_groups.remove(obj.vertex_groups[cls.MASK_NAME])
|
||||||
cls.clear_cache(obj)
|
cls.clear_cache(obj)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clear_cache(cls, obj: Optional[Object] = None, unused_only: bool = False) -> None:
|
def clear_cache(cls, obj=None, unused_only=False):
|
||||||
if unused_only:
|
if unused_only:
|
||||||
valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj)
|
valid_keys = {_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj}
|
||||||
removed_keys = cls.g_verts.keys() - valid_keys
|
for key in cls.g_verts.keys() - valid_keys:
|
||||||
for key in removed_keys:
|
|
||||||
del cls.g_verts[key]
|
del cls.g_verts[key]
|
||||||
for key in cls.g_shapekey_data.keys() - cls.g_verts.keys():
|
for key in cls.g_shapekey_data.keys() - cls.g_verts.keys():
|
||||||
del cls.g_shapekey_data[key]
|
del cls.g_shapekey_data[key]
|
||||||
for key in cls.g_bone_check.keys() - cls.g_verts.keys():
|
for key in cls.g_bone_check.keys() - cls.g_verts.keys():
|
||||||
del cls.g_bone_check[key]
|
del cls.g_bone_check[key]
|
||||||
logger.debug(f"Cleared {len(removed_keys)} unused SDEF cache entries")
|
|
||||||
elif obj:
|
elif obj:
|
||||||
key = _hash(obj)
|
key = _hash(obj)
|
||||||
if key in cls.g_verts:
|
if key in cls.g_verts:
|
||||||
@@ -352,9 +329,7 @@ class FnSDEF:
|
|||||||
del cls.g_shapekey_data[key]
|
del cls.g_shapekey_data[key]
|
||||||
if key in cls.g_bone_check:
|
if key in cls.g_bone_check:
|
||||||
del cls.g_bone_check[key]
|
del cls.g_bone_check[key]
|
||||||
logger.debug(f"Cleared SDEF cache for {obj.name}")
|
|
||||||
else:
|
else:
|
||||||
logger.debug("Cleared all SDEF cache")
|
|
||||||
cls.g_verts = {}
|
cls.g_verts = {}
|
||||||
cls.g_bone_check = {}
|
cls.g_bone_check = {}
|
||||||
cls.g_shapekey_data = {}
|
cls.g_shapekey_data = {}
|
||||||
|
|||||||
+43
-66
@@ -1,37 +1,26 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2019 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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
|
from typing import Optional, Tuple, cast
|
||||||
# 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.
|
|
||||||
|
|
||||||
from typing import Optional, Tuple, cast, List, Dict, Any, Union
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import (
|
|
||||||
ShaderNodeTree,
|
|
||||||
ShaderNode,
|
|
||||||
NodeGroupInput,
|
|
||||||
NodeGroupOutput,
|
|
||||||
Material
|
|
||||||
)
|
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
class _NodeTreeUtils:
|
class _NodeTreeUtils:
|
||||||
def __init__(self, shader: ShaderNodeTree):
|
def __init__(self, shader: bpy.types.ShaderNodeTree):
|
||||||
self.shader = shader
|
self.shader = shader
|
||||||
self.nodes: bpy.types.bpy_prop_collection[ShaderNode] = shader.nodes # type: ignore
|
self.nodes: bpy.types.bpy_prop_collection[bpy.types.ShaderNode] = shader.nodes # type: ignore[assignment]
|
||||||
self.links = shader.links
|
self.links = shader.links
|
||||||
|
|
||||||
def _find_node(self, node_type: str) -> Optional[ShaderNode]:
|
def _find_node(self, node_type: str) -> Optional[bpy.types.ShaderNode]:
|
||||||
return next((n for n in self.nodes if n.bl_idname == node_type), None)
|
return next((n for n in self.nodes if n.bl_idname == node_type), None)
|
||||||
|
|
||||||
def new_node(self, idname: str, pos: Tuple[int, int]) -> ShaderNode:
|
def new_node(self, idname: str, pos: Tuple[int, int]) -> bpy.types.ShaderNode:
|
||||||
node: ShaderNode = self.nodes.new(idname)
|
node: bpy.types.ShaderNode = self.nodes.new(idname)
|
||||||
node.location = (pos[0] * 210, pos[1] * 220)
|
node.location = (pos[0] * 210, pos[1] * 220)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
def new_math_node(self, operation: str, pos: Tuple[int, int], value1: Optional[float] = None, value2: Optional[float] = None) -> ShaderNode:
|
def new_math_node(self, operation, pos, value1=None, value2=None):
|
||||||
node = self.new_node("ShaderNodeMath", pos)
|
node = self.new_node("ShaderNodeMath", pos)
|
||||||
node.operation = operation
|
node.operation = operation
|
||||||
if value1 is not None:
|
if value1 is not None:
|
||||||
@@ -40,7 +29,7 @@ class _NodeTreeUtils:
|
|||||||
node.inputs[1].default_value = value2
|
node.inputs[1].default_value = value2
|
||||||
return node
|
return node
|
||||||
|
|
||||||
def new_vector_math_node(self, operation: str, pos: Tuple[int, int], vector1: Optional[Tuple[float, float, float, float]] = None, vector2: Optional[Tuple[float, float, float, float]] = None) -> ShaderNode:
|
def new_vector_math_node(self, operation, pos, vector1=None, vector2=None):
|
||||||
node = self.new_node("ShaderNodeVectorMath", pos)
|
node = self.new_node("ShaderNodeVectorMath", pos)
|
||||||
node.operation = operation
|
node.operation = operation
|
||||||
if vector1 is not None:
|
if vector1 is not None:
|
||||||
@@ -49,7 +38,7 @@ class _NodeTreeUtils:
|
|||||||
node.inputs[1].default_value = vector2
|
node.inputs[1].default_value = vector2
|
||||||
return node
|
return node
|
||||||
|
|
||||||
def new_mix_node(self, blend_type: str, pos: Tuple[int, int], fac: Optional[float] = None, color1: Optional[Tuple[float, float, float, float]] = None, color2: Optional[Tuple[float, float, float, float]] = None) -> ShaderNode:
|
def new_mix_node(self, blend_type, pos, fac=None, color1=None, color2=None):
|
||||||
node = self.new_node("ShaderNodeMixRGB", pos)
|
node = self.new_node("ShaderNodeMixRGB", pos)
|
||||||
node.blend_type = blend_type
|
node.blend_type = blend_type
|
||||||
if fac is not None:
|
if fac is not None:
|
||||||
@@ -61,30 +50,30 @@ class _NodeTreeUtils:
|
|||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
||||||
SOCKET_TYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "NodeSocketFloat"}
|
SOCKET_TYPE_MAPPING = {"NodeSocketFloatFactor": "NodeSocketFloat"}
|
||||||
|
|
||||||
SOCKET_SUBTYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "FACTOR"}
|
SOCKET_SUBTYPE_MAPPING = {"NodeSocketFloatFactor": "FACTOR"}
|
||||||
|
|
||||||
|
|
||||||
class _NodeGroupUtils(_NodeTreeUtils):
|
class _NodeGroupUtils(_NodeTreeUtils):
|
||||||
def __init__(self, shader: ShaderNodeTree):
|
def __init__(self, shader: bpy.types.ShaderNodeTree):
|
||||||
super().__init__(shader)
|
super().__init__(shader)
|
||||||
self.__node_input: Optional[NodeGroupInput] = None
|
self.__node_input: Optional[bpy.types.NodeGroupInput] = None
|
||||||
self.__node_output: Optional[NodeGroupOutput] = None
|
self.__node_output: Optional[bpy.types.NodeGroupOutput] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def node_input(self) -> NodeGroupInput:
|
def node_input(self) -> bpy.types.NodeGroupInput:
|
||||||
if not self.__node_input:
|
if not self.__node_input:
|
||||||
self.__node_input = cast(NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
|
self.__node_input = cast("bpy.types.NodeGroupInput", self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
|
||||||
return self.__node_input
|
return self.__node_input
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def node_output(self) -> NodeGroupOutput:
|
def node_output(self) -> bpy.types.NodeGroupOutput:
|
||||||
if not self.__node_output:
|
if not self.__node_output:
|
||||||
self.__node_output = cast(NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
|
self.__node_output = cast("bpy.types.NodeGroupOutput", self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
|
||||||
return self.__node_output
|
return self.__node_output
|
||||||
|
|
||||||
def hide_nodes(self, hide_sockets: bool = True) -> None:
|
def hide_nodes(self, hide_sockets=True):
|
||||||
skip_nodes = {self.__node_input, self.__node_output}
|
skip_nodes = {self.__node_input, self.__node_output}
|
||||||
for n in (x for x in self.nodes if x not in skip_nodes):
|
for n in (x for x in self.nodes if x not in skip_nodes):
|
||||||
n.hide = True
|
n.hide = True
|
||||||
@@ -95,22 +84,22 @@ class _NodeGroupUtils(_NodeTreeUtils):
|
|||||||
for s in n.outputs:
|
for s in n.outputs:
|
||||||
s.hide = not s.is_linked
|
s.hide = not s.is_linked
|
||||||
|
|
||||||
def new_input_socket(self, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
|
def new_input_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
|
||||||
self.__new_io("INPUT", self.node_input.outputs, io_name, socket, default_val, min_max, socket_type)
|
self.__new_io("INPUT", self.node_input.outputs, io_name, socket, default_val, min_max, socket_type)
|
||||||
|
|
||||||
def new_output_socket(self, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
|
def new_output_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
|
||||||
self.__new_io("OUTPUT", self.node_output.inputs, io_name, socket, default_val, min_max, socket_type)
|
self.__new_io("OUTPUT", self.node_output.inputs, io_name, socket, default_val, min_max, socket_type)
|
||||||
|
|
||||||
def __new_io(self, in_out: str, io_sockets: bpy.types.bpy_prop_collection, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
|
def __new_io(self, in_out, io_sockets, io_name, socket, default_val=None, min_max=None, socket_type=None):
|
||||||
if io_name not in io_sockets:
|
if io_name not in io_sockets:
|
||||||
idname = socket_type or (socket.bl_idname if socket else "NodeSocketFloat")
|
idname = socket_type or socket.bl_idname
|
||||||
interface_socket = self.shader.interface.new_socket(name=io_name, in_out=in_out, socket_type=SOCKET_TYPE_MAPPING.get(idname, idname))
|
interface_socket = self.shader.interface.new_socket(name=io_name, in_out=in_out, socket_type=SOCKET_TYPE_MAPPING.get(idname, idname))
|
||||||
if idname in SOCKET_SUBTYPE_MAPPING:
|
if idname in SOCKET_SUBTYPE_MAPPING:
|
||||||
interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "")
|
interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "")
|
||||||
if not min_max:
|
if not min_max:
|
||||||
if idname.endswith("Factor") or io_name.endswith("Alpha"):
|
if idname.endswith("Factor") or io_name.endswith("Alpha"):
|
||||||
interface_socket.min_value, interface_socket.max_value = 0, 1
|
interface_socket.min_value, interface_socket.max_value = 0, 1
|
||||||
elif idname.endswith("Float") or idname.endswith("Vector"):
|
elif idname.endswith(("Float", "Vector")):
|
||||||
interface_socket.min_value, interface_socket.max_value = -10, 10
|
interface_socket.min_value, interface_socket.max_value = -10, 10
|
||||||
if socket is not None:
|
if socket is not None:
|
||||||
self.links.new(io_sockets[io_name], socket)
|
self.links.new(io_sockets[io_name], socket)
|
||||||
@@ -122,18 +111,14 @@ class _NodeGroupUtils(_NodeTreeUtils):
|
|||||||
|
|
||||||
class _MaterialMorph:
|
class _MaterialMorph:
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_morph_inputs(cls, material: Optional[Material], morph: Any) -> None:
|
def update_morph_inputs(cls, material, morph):
|
||||||
"""Update material morph inputs based on morph data"""
|
|
||||||
if material and material.node_tree and morph.name in material.node_tree.nodes:
|
if material and material.node_tree and morph.name in material.node_tree.nodes:
|
||||||
logger.debug(f"Updating morph inputs for {morph.name} in {material.name}")
|
|
||||||
cls.__update_node_inputs(material.node_tree.nodes[morph.name], morph)
|
cls.__update_node_inputs(material.node_tree.nodes[morph.name], morph)
|
||||||
cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph)
|
cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_morph_nodes(cls, material: Material, morphs: List[Any]) -> List[ShaderNode]:
|
def setup_morph_nodes(cls, material, morphs):
|
||||||
"""Set up morph nodes for a material"""
|
|
||||||
node, nodes = None, []
|
node, nodes = None, []
|
||||||
logger.debug(f"Setting up {len(morphs)} morph nodes for {material.name}")
|
|
||||||
for m in morphs:
|
for m in morphs:
|
||||||
node = cls.__morph_node_add(material, m, node)
|
node = cls.__morph_node_add(material, m, node)
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
@@ -149,25 +134,23 @@ class _MaterialMorph:
|
|||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def reset_morph_links(cls, node: ShaderNode) -> None:
|
def reset_morph_links(cls, node):
|
||||||
"""Reset morph links for a node"""
|
|
||||||
logger.debug(f"Resetting morph links for {node.name}")
|
|
||||||
cls.__update_morph_links(node, reset=True)
|
cls.__update_morph_links(node, reset=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __update_morph_links(cls, node: ShaderNode, reset: bool = False) -> None:
|
def __update_morph_links(cls, node, reset=False):
|
||||||
nodes, links = node.id_data.nodes, node.id_data.links
|
nodes, links = node.id_data.nodes, node.id_data.links
|
||||||
if reset:
|
if reset:
|
||||||
if any(l.from_node.name.startswith("mmd_bind") for i in node.inputs for l in i.links):
|
if any(link.from_node.name.startswith("mmd_bind") for i in node.inputs for link in i.links):
|
||||||
return
|
return
|
||||||
|
|
||||||
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None:
|
def __init_link(socket_morph, socket_shader):
|
||||||
if socket_shader and socket_morph.is_linked:
|
if socket_shader and socket_morph.is_linked:
|
||||||
links.new(socket_morph.links[0].from_socket, socket_shader)
|
links.new(socket_morph.links[0].from_socket, socket_shader)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None:
|
def __init_link(socket_morph, socket_shader):
|
||||||
if socket_shader:
|
if socket_shader:
|
||||||
if socket_shader.is_linked:
|
if socket_shader.is_linked:
|
||||||
links.new(socket_shader.links[0].from_socket, socket_morph)
|
links.new(socket_shader.links[0].from_socket, socket_morph)
|
||||||
@@ -192,8 +175,7 @@ class _MaterialMorph:
|
|||||||
__init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"])
|
__init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __update_node_inputs(cls, node: ShaderNode, morph: Any) -> None:
|
def __update_node_inputs(cls, node, morph):
|
||||||
"""Update node inputs based on morph data"""
|
|
||||||
node.inputs["Ambient2"].default_value[:3] = morph.ambient_color[:3]
|
node.inputs["Ambient2"].default_value[:3] = morph.ambient_color[:3]
|
||||||
node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3]
|
node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3]
|
||||||
node.inputs["Specular2"].default_value[:3] = morph.specular_color[:3]
|
node.inputs["Specular2"].default_value[:3] = morph.specular_color[:3]
|
||||||
@@ -211,8 +193,7 @@ class _MaterialMorph:
|
|||||||
node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3]
|
node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __morph_node_add(cls, material: Material, morph: Optional[Any], prev_node: Optional[ShaderNode]) -> Optional[ShaderNode]:
|
def __morph_node_add(cls, material, morph, prev_node):
|
||||||
"""Add a morph node to a material"""
|
|
||||||
nodes, links = material.node_tree.nodes, material.node_tree.links
|
nodes, links = material.node_tree.nodes, material.node_tree.links
|
||||||
|
|
||||||
shader = nodes.get("mmd_shader", None)
|
shader = nodes.get("mmd_shader", None)
|
||||||
@@ -237,9 +218,8 @@ class _MaterialMorph:
|
|||||||
return node
|
return node
|
||||||
# connect last node to shader
|
# connect last node to shader
|
||||||
if shader:
|
if shader:
|
||||||
logger.debug(f"Connecting last node to shader for {material.name}")
|
|
||||||
|
|
||||||
def __soft_link(socket_out: Optional[bpy.types.NodeSocket], socket_in: Optional[bpy.types.NodeSocket]) -> None:
|
def __soft_link(socket_out, socket_in):
|
||||||
if socket_out and socket_in:
|
if socket_out and socket_in:
|
||||||
links.new(socket_out, socket_in)
|
links.new(socket_out, socket_in)
|
||||||
|
|
||||||
@@ -261,14 +241,12 @@ class _MaterialMorph:
|
|||||||
return shader
|
return shader
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __get_shader(cls, morph_type: str) -> ShaderNodeTree:
|
def __get_shader(cls, morph_type):
|
||||||
"""Get or create a shader node group for the specified morph type"""
|
|
||||||
group_name = "MMDMorph" + morph_type
|
group_name = "MMDMorph" + morph_type
|
||||||
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||||
if len(shader.nodes):
|
if len(shader.nodes):
|
||||||
return shader
|
return shader
|
||||||
|
|
||||||
logger.info(f"Creating new shader node group: {group_name}")
|
|
||||||
ng = _NodeGroupUtils(shader)
|
ng = _NodeGroupUtils(shader)
|
||||||
links = ng.links
|
links = ng.links
|
||||||
|
|
||||||
@@ -279,18 +257,18 @@ class _MaterialMorph:
|
|||||||
ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat")
|
ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat")
|
||||||
ng.new_node("NodeGroupOutput", (3, 0))
|
ng.new_node("NodeGroupOutput", (3, 0))
|
||||||
|
|
||||||
def __blend_color_add(id_name: str, pos: Tuple[int, int], tag: str = "") -> ShaderNode:
|
def __blend_color_add(id_name, pos, tag=""):
|
||||||
# MA_RAMP_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac))
|
# MA_RAMP_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac))
|
||||||
# MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2
|
# MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2
|
||||||
# https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400
|
# https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400
|
||||||
node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos[0] + 1, pos[1]))
|
node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos[0] + 1, pos[1]))
|
||||||
links.new(node_input.outputs["Fac"], node_mix.inputs["Fac"])
|
links.new(node_input.outputs["Fac"], node_mix.inputs["Fac"])
|
||||||
ng.new_input_socket("%s1" % id_name + tag, node_mix.inputs["Color1"])
|
ng.new_input_socket(f"{id_name}1" + tag, node_mix.inputs["Color1"])
|
||||||
ng.new_input_socket("%s2" % id_name + tag, node_mix.inputs["Color2"], socket_type="NodeSocketVector")
|
ng.new_input_socket(f"{id_name}2" + tag, node_mix.inputs["Color2"], socket_type="NodeSocketVector")
|
||||||
ng.new_output_socket(id_name + tag, node_mix.outputs["Color"])
|
ng.new_output_socket(id_name + tag, node_mix.outputs["Color"])
|
||||||
return node_mix
|
return node_mix
|
||||||
|
|
||||||
def __blend_tex_color(id_name: str, pos: Tuple[int, int], node_tex_rgb: ShaderNode, node_tex_a_output: bpy.types.NodeSocket) -> None:
|
def __blend_tex_color(id_name, pos, node_tex_rgb, node_tex_a_output):
|
||||||
# Tex Color = tex_rgb * tex_a + (1 - tex_a)
|
# Tex Color = tex_rgb * tex_a + (1 - tex_a)
|
||||||
# : tex_rgb = TexRGB * ColorMul + ColorAdd
|
# : tex_rgb = TexRGB * ColorMul + ColorAdd
|
||||||
# : tex_a = TexA * ValueMul + ValueAdd
|
# : tex_a = TexA * ValueMul + ValueAdd
|
||||||
@@ -313,7 +291,7 @@ class _MaterialMorph:
|
|||||||
ng.new_output_socket(id_name + " Tex", node_add.outputs[0], socket_type="NodeSocketColor")
|
ng.new_output_socket(id_name + " Tex", node_add.outputs[0], socket_type="NodeSocketColor")
|
||||||
ng.new_output_socket(id_name + " Tex Add", node_scale.outputs[0], socket_type="NodeSocketColor")
|
ng.new_output_socket(id_name + " Tex Add", node_scale.outputs[0], socket_type="NodeSocketColor")
|
||||||
|
|
||||||
def __add_sockets(id_name: str, input1: bpy.types.NodeSocket, input2: bpy.types.NodeSocket, output: bpy.types.NodeSocket, tag: str = "") -> None:
|
def __add_sockets(id_name, input1, input2, output, tag=""):
|
||||||
ng.new_input_socket(f"{id_name}1{tag}", input1, use_mul)
|
ng.new_input_socket(f"{id_name}1{tag}", input1, use_mul)
|
||||||
ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul)
|
ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul)
|
||||||
ng.new_output_socket(f"{id_name}{tag}", output)
|
ng.new_output_socket(f"{id_name}{tag}", output)
|
||||||
@@ -362,5 +340,4 @@ class _MaterialMorph:
|
|||||||
__blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2])
|
__blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2])
|
||||||
|
|
||||||
ng.hide_nodes()
|
ng.hide_nodes()
|
||||||
logger.debug(f"Shader node group {group_name} created successfully")
|
|
||||||
return ng.shader
|
return ng.shader
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2021 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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 itertools
|
import itertools
|
||||||
import re
|
import re
|
||||||
@@ -33,11 +29,7 @@ class MMDTranslationElementType(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class MMDDataHandlerABC(ABC):
|
class MMDDataHandlerABC(ABC):
|
||||||
@classmethod
|
type_name: str
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def type_name(cls) -> str:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -67,7 +59,8 @@ class MMDDataHandlerABC(ABC):
|
|||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
|
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
|
||||||
"""Returns (name, name_j, name_e)"""
|
"""Return (name, name_j, name_e)"""
|
||||||
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_restorable(cls, mmd_translation_element: "MMDTranslationElement") -> bool:
|
def is_restorable(cls, mmd_translation_element: "MMDTranslationElement") -> bool:
|
||||||
@@ -75,7 +68,7 @@ class MMDDataHandlerABC(ABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_data_visible(cls, filter_selected: bool, filter_visible: bool, select: bool, hide: bool) -> bool:
|
def check_data_visible(cls, filter_selected: bool, filter_visible: bool, select: bool, hide: bool) -> bool:
|
||||||
return filter_selected and not select or filter_visible and hide
|
return (filter_selected and not select) or (filter_visible and hide)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def prop_restorable(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str, original_value: str, index: int):
|
def prop_restorable(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str, original_value: str, index: int):
|
||||||
@@ -86,7 +79,7 @@ class MMDDataHandlerABC(ABC):
|
|||||||
row.label(text="", icon="BLANK1")
|
row.label(text="", icon="BLANK1")
|
||||||
return
|
return
|
||||||
|
|
||||||
op = row.operator("mmd_tools.restore_mmd_translation_element_name", text="", icon="FILE_REFRESH")
|
op = row.operator("mmd_tools_local.restore_mmd_translation_element_name", text="", icon="FILE_REFRESH")
|
||||||
op.index = index
|
op.index = index
|
||||||
op.prop_name = prop_name
|
op.prop_name = prop_name
|
||||||
op.restore_value = original_value
|
op.restore_value = original_value
|
||||||
@@ -100,10 +93,7 @@ class MMDDataHandlerABC(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class MMDBoneHandler(MMDDataHandlerABC):
|
class MMDBoneHandler(MMDDataHandlerABC):
|
||||||
@classmethod
|
type_name = MMDTranslationElementType.BONE.name
|
||||||
@property
|
|
||||||
def type_name(cls) -> str:
|
|
||||||
return MMDTranslationElementType.BONE.name
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
|
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
|
||||||
@@ -114,18 +104,18 @@ class MMDBoneHandler(MMDDataHandlerABC):
|
|||||||
cls.prop_restorable(prop_row, mmd_translation_element, "name", pose_bone.name, index)
|
cls.prop_restorable(prop_row, mmd_translation_element, "name", pose_bone.name, index)
|
||||||
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", pose_bone.mmd_bone.name_j, index)
|
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", pose_bone.mmd_bone.name_j, index)
|
||||||
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", pose_bone.mmd_bone.name_e, index)
|
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", pose_bone.mmd_bone.name_e, index)
|
||||||
row.prop(pose_bone.bone, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if pose_bone.bone.select else "RESTRICT_SELECT_ON")
|
row.prop(pose_bone.bone, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if pose_bone.select else "RESTRICT_SELECT_ON")
|
||||||
row.prop(pose_bone.bone, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if pose_bone.bone.hide else "HIDE_OFF")
|
row.prop(pose_bone.bone, "hide", text="", emboss=False, icon_only=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def collect_data(cls, mmd_translation: "MMDTranslation"):
|
def collect_data(cls, mmd_translation: "MMDTranslation"):
|
||||||
armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data)
|
armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data)
|
||||||
pose_bone: bpy.types.PoseBone
|
pose_bone: bpy.types.PoseBone
|
||||||
for index, pose_bone in enumerate(armature_object.pose.bones):
|
for index, pose_bone in enumerate(armature_object.pose.bones):
|
||||||
if not any(c.is_visible for c in pose_bone.bone.collections):
|
if pose_bone.bone.hide or (pose_bone.bone.collections and not any(c.is_visible for c in pose_bone.bone.collections)):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
|
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
|
||||||
mmd_translation_element.type = MMDTranslationElementType.BONE.name
|
mmd_translation_element.type = MMDTranslationElementType.BONE.name
|
||||||
mmd_translation_element.object = armature_object
|
mmd_translation_element.object = armature_object
|
||||||
mmd_translation_element.data_path = f"pose.bones[{index}]"
|
mmd_translation_element.data_path = f"pose.bones[{index}]"
|
||||||
@@ -140,14 +130,14 @@ class MMDBoneHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
||||||
mmd_translation_element: "MMDTranslationElement"
|
mmd_translation_element: MMDTranslationElement
|
||||||
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
||||||
if mmd_translation_element.type != MMDTranslationElementType.BONE.name:
|
if mmd_translation_element.type != MMDTranslationElementType.BONE.name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
|
pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
|
||||||
|
|
||||||
if cls.check_data_visible(filter_selected, filter_visible, pose_bone.bone.select, pose_bone.bone.hide):
|
if cls.check_data_visible(filter_selected, filter_visible, pose_bone.select, pose_bone.bone.hide):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if check_blank_name(mmd_translation_element.name_j, mmd_translation_element.name_e):
|
if check_blank_name(mmd_translation_element.name_j, mmd_translation_element.name_e):
|
||||||
@@ -156,7 +146,7 @@ class MMDBoneHandler(MMDDataHandlerABC):
|
|||||||
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
|
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
|
||||||
mmd_translation_element_index.value = index
|
mmd_translation_element_index.value = index
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -176,14 +166,11 @@ class MMDBoneHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
|
|
||||||
class MMDMorphHandler(MMDDataHandlerABC):
|
class MMDMorphHandler(MMDDataHandlerABC):
|
||||||
@classmethod
|
type_name = MMDTranslationElementType.MORPH.name
|
||||||
@property
|
|
||||||
def type_name(cls) -> str:
|
|
||||||
return MMDTranslationElementType.MORPH.name
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
|
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
|
||||||
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
|
morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.label(text="", icon="SHAPEKEY_DATA")
|
row.label(text="", icon="SHAPEKEY_DATA")
|
||||||
prop_row = row.row()
|
prop_row = row.row()
|
||||||
@@ -198,7 +185,7 @@ class MMDMorphHandler(MMDDataHandlerABC):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def collect_data(cls, mmd_translation: "MMDTranslation"):
|
def collect_data(cls, mmd_translation: "MMDTranslation"):
|
||||||
root_object: bpy.types.Object = mmd_translation.id_data
|
root_object: bpy.types.Object = mmd_translation.id_data
|
||||||
mmd_root: "MMDRoot" = root_object.mmd_root
|
mmd_root: MMDRoot = root_object.mmd_root
|
||||||
|
|
||||||
for morphs_name, morphs in {
|
for morphs_name, morphs in {
|
||||||
"material_morphs": mmd_root.material_morphs,
|
"material_morphs": mmd_root.material_morphs,
|
||||||
@@ -207,9 +194,9 @@ class MMDMorphHandler(MMDDataHandlerABC):
|
|||||||
"vertex_morphs": mmd_root.vertex_morphs,
|
"vertex_morphs": mmd_root.vertex_morphs,
|
||||||
"group_morphs": mmd_root.group_morphs,
|
"group_morphs": mmd_root.group_morphs,
|
||||||
}.items():
|
}.items():
|
||||||
morph: "_MorphBase"
|
morph: _MorphBase
|
||||||
for index, morph in enumerate(morphs):
|
for index, morph in enumerate(morphs):
|
||||||
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
|
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
|
||||||
mmd_translation_element.type = MMDTranslationElementType.MORPH.name
|
mmd_translation_element.type = MMDTranslationElementType.MORPH.name
|
||||||
mmd_translation_element.object = root_object
|
mmd_translation_element.object = root_object
|
||||||
mmd_translation_element.data_path = f"mmd_root.{morphs_name}[{index}]"
|
mmd_translation_element.data_path = f"mmd_root.{morphs_name}[{index}]"
|
||||||
@@ -228,24 +215,24 @@ class MMDMorphHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
||||||
mmd_translation_element: "MMDTranslationElement"
|
mmd_translation_element: MMDTranslationElement
|
||||||
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
||||||
if mmd_translation_element.type != MMDTranslationElementType.MORPH.name:
|
if mmd_translation_element.type != MMDTranslationElementType.MORPH.name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
|
morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
|
||||||
if check_blank_name(morph.name, morph.name_e):
|
if check_blank_name(morph.name, morph.name_e):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
|
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
|
||||||
mmd_translation_element_index.value = index
|
mmd_translation_element_index.value = index
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
|
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
|
||||||
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
|
morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
|
||||||
if name is not None:
|
if name is not None:
|
||||||
morph.name = name
|
morph.name = name
|
||||||
if name_e is not None:
|
if name_e is not None:
|
||||||
@@ -253,15 +240,12 @@ class MMDMorphHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
|
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
|
||||||
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
|
morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
|
||||||
return (morph.name, "", morph.name_e)
|
return (morph.name, "", morph.name_e)
|
||||||
|
|
||||||
|
|
||||||
class MMDMaterialHandler(MMDDataHandlerABC):
|
class MMDMaterialHandler(MMDDataHandlerABC):
|
||||||
@classmethod
|
type_name = MMDTranslationElementType.MATERIAL.name
|
||||||
@property
|
|
||||||
def type_name(cls) -> str:
|
|
||||||
return MMDTranslationElementType.MATERIAL.name
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
|
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
|
||||||
@@ -274,7 +258,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
|
|||||||
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", material.mmd_material.name_j, index)
|
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", material.mmd_material.name_j, index)
|
||||||
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", material.mmd_material.name_e, index)
|
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", material.mmd_material.name_e, index)
|
||||||
row.prop(mesh_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mesh_object.select_get() else "RESTRICT_SELECT_ON")
|
row.prop(mesh_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mesh_object.select_get() else "RESTRICT_SELECT_ON")
|
||||||
row.prop(mesh_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mesh_object.hide_get() else "HIDE_OFF")
|
row.prop(mesh_object, "hide", text="", emboss=False, icon_only=True)
|
||||||
|
|
||||||
MATERIAL_DATA_PATH_EXTRACT = re.compile(r"data\.materials\[(?P<index>\d*)\]")
|
MATERIAL_DATA_PATH_EXTRACT = re.compile(r"data\.materials\[(?P<index>\d*)\]")
|
||||||
|
|
||||||
@@ -293,7 +277,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
|
|||||||
if not hasattr(material, "mmd_material"):
|
if not hasattr(material, "mmd_material"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
|
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
|
||||||
mmd_translation_element.type = MMDTranslationElementType.MATERIAL.name
|
mmd_translation_element.type = MMDTranslationElementType.MATERIAL.name
|
||||||
mmd_translation_element.object = mesh_object
|
mmd_translation_element.object = mesh_object
|
||||||
mmd_translation_element.data_path = f"data.materials[{index}]"
|
mmd_translation_element.data_path = f"data.materials[{index}]"
|
||||||
@@ -314,7 +298,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
||||||
mmd_translation_element: "MMDTranslationElement"
|
mmd_translation_element: MMDTranslationElement
|
||||||
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
||||||
if mmd_translation_element.type != MMDTranslationElementType.MATERIAL.name:
|
if mmd_translation_element.type != MMDTranslationElementType.MATERIAL.name:
|
||||||
continue
|
continue
|
||||||
@@ -330,7 +314,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
|
|||||||
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
|
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
|
||||||
mmd_translation_element_index.value = index
|
mmd_translation_element_index.value = index
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -350,10 +334,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
|
|
||||||
class MMDDisplayHandler(MMDDataHandlerABC):
|
class MMDDisplayHandler(MMDDataHandlerABC):
|
||||||
@classmethod
|
type_name = MMDTranslationElementType.DISPLAY.name
|
||||||
@property
|
|
||||||
def type_name(cls) -> str:
|
|
||||||
return MMDTranslationElementType.DISPLAY.name
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
|
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
|
||||||
@@ -366,7 +347,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
|
|||||||
cls.prop_disabled(prop_row, mmd_translation_element, "name")
|
cls.prop_disabled(prop_row, mmd_translation_element, "name")
|
||||||
cls.prop_disabled(prop_row, mmd_translation_element, "name_e")
|
cls.prop_disabled(prop_row, mmd_translation_element, "name_e")
|
||||||
row.prop(mmd_translation_element.object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mmd_translation_element.object.select_get() else "RESTRICT_SELECT_ON")
|
row.prop(mmd_translation_element.object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mmd_translation_element.object.select_get() else "RESTRICT_SELECT_ON")
|
||||||
row.prop(mmd_translation_element.object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mmd_translation_element.object.hide_get() else "HIDE_OFF")
|
row.prop(mmd_translation_element.object, "hide", text="", emboss=False, icon_only=True)
|
||||||
|
|
||||||
DISPLAY_DATA_PATH_EXTRACT = re.compile(r"data\.collections\[(?P<index>\d*)\]")
|
DISPLAY_DATA_PATH_EXTRACT = re.compile(r"data\.collections\[(?P<index>\d*)\]")
|
||||||
|
|
||||||
@@ -375,7 +356,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
|
|||||||
armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data)
|
armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data)
|
||||||
bone_collection: bpy.types.BoneCollection
|
bone_collection: bpy.types.BoneCollection
|
||||||
for index, bone_collection in enumerate(armature_object.data.collections):
|
for index, bone_collection in enumerate(armature_object.data.collections):
|
||||||
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
|
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
|
||||||
mmd_translation_element.type = MMDTranslationElementType.DISPLAY.name
|
mmd_translation_element.type = MMDTranslationElementType.DISPLAY.name
|
||||||
mmd_translation_element.object = armature_object
|
mmd_translation_element.object = armature_object
|
||||||
mmd_translation_element.data_path = f"data.collections[{index}]"
|
mmd_translation_element.data_path = f"data.collections[{index}]"
|
||||||
@@ -396,7 +377,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
||||||
mmd_translation_element: "MMDTranslationElement"
|
mmd_translation_element: MMDTranslationElement
|
||||||
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
||||||
if mmd_translation_element.type != MMDTranslationElementType.DISPLAY.name:
|
if mmd_translation_element.type != MMDTranslationElementType.DISPLAY.name:
|
||||||
continue
|
continue
|
||||||
@@ -412,7 +393,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
|
|||||||
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
|
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
|
||||||
mmd_translation_element_index.value = index
|
mmd_translation_element_index.value = index
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -428,10 +409,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
|
|
||||||
class MMDPhysicsHandler(MMDDataHandlerABC):
|
class MMDPhysicsHandler(MMDDataHandlerABC):
|
||||||
@classmethod
|
type_name = MMDTranslationElementType.PHYSICS.name
|
||||||
@property
|
|
||||||
def type_name(cls) -> str:
|
|
||||||
return MMDTranslationElementType.PHYSICS.name
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
|
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
|
||||||
@@ -451,7 +429,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
|
|||||||
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", mmd_object.name_j, index)
|
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", mmd_object.name_j, index)
|
||||||
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", mmd_object.name_e, index)
|
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", mmd_object.name_e, index)
|
||||||
row.prop(obj, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if obj.select_get() else "RESTRICT_SELECT_ON")
|
row.prop(obj, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if obj.select_get() else "RESTRICT_SELECT_ON")
|
||||||
row.prop(obj, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if obj.hide_get() else "HIDE_OFF")
|
row.prop(obj, "hide", text="", emboss=False, icon_only=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def collect_data(cls, mmd_translation: "MMDTranslation"):
|
def collect_data(cls, mmd_translation: "MMDTranslation"):
|
||||||
@@ -460,7 +438,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
obj: bpy.types.Object
|
obj: bpy.types.Object
|
||||||
for obj in model.rigidBodies():
|
for obj in model.rigidBodies():
|
||||||
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
|
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
|
||||||
mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name
|
mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name
|
||||||
mmd_translation_element.object = obj
|
mmd_translation_element.object = obj
|
||||||
mmd_translation_element.data_path = "mmd_rigid"
|
mmd_translation_element.data_path = "mmd_rigid"
|
||||||
@@ -470,7 +448,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
obj: bpy.types.Object
|
obj: bpy.types.Object
|
||||||
for obj in model.joints():
|
for obj in model.joints():
|
||||||
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
|
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
|
||||||
mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name
|
mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name
|
||||||
mmd_translation_element.object = obj
|
mmd_translation_element.object = obj
|
||||||
mmd_translation_element.data_path = "mmd_joint"
|
mmd_translation_element.data_path = "mmd_joint"
|
||||||
@@ -484,7 +462,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
||||||
mmd_translation_element: "MMDTranslationElement"
|
mmd_translation_element: MMDTranslationElement
|
||||||
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
||||||
if mmd_translation_element.type != MMDTranslationElementType.PHYSICS.name:
|
if mmd_translation_element.type != MMDTranslationElementType.PHYSICS.name:
|
||||||
continue
|
continue
|
||||||
@@ -504,7 +482,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
|
|||||||
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
|
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
|
||||||
mmd_translation_element_index.value = index
|
mmd_translation_element_index.value = index
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -536,10 +514,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
|
|
||||||
class MMDInfoHandler(MMDDataHandlerABC):
|
class MMDInfoHandler(MMDDataHandlerABC):
|
||||||
@classmethod
|
type_name = MMDTranslationElementType.INFO.name
|
||||||
@property
|
|
||||||
def type_name(cls) -> str:
|
|
||||||
return MMDTranslationElementType.INFO.name
|
|
||||||
|
|
||||||
TYPE_TO_ICONS = {
|
TYPE_TO_ICONS = {
|
||||||
"EMPTY": "EMPTY_DATA",
|
"EMPTY": "EMPTY_DATA",
|
||||||
@@ -557,7 +532,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
|
|||||||
cls.prop_disabled(prop_row, mmd_translation_element, "name")
|
cls.prop_disabled(prop_row, mmd_translation_element, "name")
|
||||||
cls.prop_disabled(prop_row, mmd_translation_element, "name_e")
|
cls.prop_disabled(prop_row, mmd_translation_element, "name_e")
|
||||||
row.prop(info_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if info_object.select_get() else "RESTRICT_SELECT_ON")
|
row.prop(info_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if info_object.select_get() else "RESTRICT_SELECT_ON")
|
||||||
row.prop(info_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if info_object.hide_get() else "HIDE_OFF")
|
row.prop(info_object, "hide", text="", emboss=False, icon_only=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def collect_data(cls, mmd_translation: "MMDTranslation"):
|
def collect_data(cls, mmd_translation: "MMDTranslation"):
|
||||||
@@ -568,7 +543,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
|
|||||||
info_objects.append(armature_object)
|
info_objects.append(armature_object)
|
||||||
|
|
||||||
for info_object in itertools.chain(info_objects, FnModel.iterate_mesh_objects(root_object)):
|
for info_object in itertools.chain(info_objects, FnModel.iterate_mesh_objects(root_object)):
|
||||||
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
|
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
|
||||||
mmd_translation_element.type = MMDTranslationElementType.INFO.name
|
mmd_translation_element.type = MMDTranslationElementType.INFO.name
|
||||||
mmd_translation_element.object = info_object
|
mmd_translation_element.object = info_object
|
||||||
mmd_translation_element.data_path = ""
|
mmd_translation_element.data_path = ""
|
||||||
@@ -582,7 +557,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
|
||||||
mmd_translation_element: "MMDTranslationElement"
|
mmd_translation_element: MMDTranslationElement
|
||||||
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
|
||||||
if mmd_translation_element.type != MMDTranslationElementType.INFO.name:
|
if mmd_translation_element.type != MMDTranslationElementType.INFO.name:
|
||||||
continue
|
continue
|
||||||
@@ -597,7 +572,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
|
|||||||
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
|
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
|
||||||
mmd_translation_element_index.value = index
|
mmd_translation_element_index.value = index
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -627,10 +602,10 @@ MMD_DATA_TYPE_TO_HANDLERS: Dict[str, MMDDataHandlerABC] = {h.type_name: h for h
|
|||||||
class FnTranslations:
|
class FnTranslations:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_translations(root_object: bpy.types.Object):
|
def apply_translations(root_object: bpy.types.Object):
|
||||||
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
|
mmd_translation: MMDTranslation = root_object.mmd_root.translation
|
||||||
mmd_translation_element_index: "MMDTranslationElementIndex"
|
mmd_translation_element_index: MMDTranslationElementIndex
|
||||||
for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices:
|
for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices:
|
||||||
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value]
|
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements[mmd_translation_element_index.value]
|
||||||
handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type]
|
handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type]
|
||||||
name, name_j, name_e = handler.get_names(mmd_translation_element)
|
name, name_j, name_e = handler.get_names(mmd_translation_element)
|
||||||
handler.set_names(
|
handler.set_names(
|
||||||
@@ -642,7 +617,7 @@ class FnTranslations:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def execute_translation_batch(root_object: bpy.types.Object) -> Tuple[Dict[str, str], Optional[bpy.types.Text]]:
|
def execute_translation_batch(root_object: bpy.types.Object) -> Tuple[Dict[str, str], Optional[bpy.types.Text]]:
|
||||||
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
|
mmd_translation: MMDTranslation = root_object.mmd_root.translation
|
||||||
batch_operation_script = mmd_translation.batch_operation_script
|
batch_operation_script = mmd_translation.batch_operation_script
|
||||||
if not batch_operation_script:
|
if not batch_operation_script:
|
||||||
return ({}, None)
|
return ({}, None)
|
||||||
@@ -657,9 +632,9 @@ class FnTranslations:
|
|||||||
batch_operation_script_ast = compile(mmd_translation.batch_operation_script, "<string>", "eval")
|
batch_operation_script_ast = compile(mmd_translation.batch_operation_script, "<string>", "eval")
|
||||||
batch_operation_target: str = mmd_translation.batch_operation_target
|
batch_operation_target: str = mmd_translation.batch_operation_target
|
||||||
|
|
||||||
mmd_translation_element_index: "MMDTranslationElementIndex"
|
mmd_translation_element_index: MMDTranslationElementIndex
|
||||||
for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices:
|
for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices:
|
||||||
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value]
|
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements[mmd_translation_element_index.value]
|
||||||
|
|
||||||
handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type]
|
handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type]
|
||||||
|
|
||||||
@@ -684,7 +659,7 @@ class FnTranslations:
|
|||||||
"org_name_j": org_name_j,
|
"org_name_j": org_name_j,
|
||||||
"org_name_e": org_name_e,
|
"org_name_e": org_name_e,
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if batch_operation_target == "BLENDER":
|
if batch_operation_target == "BLENDER":
|
||||||
@@ -701,8 +676,8 @@ class FnTranslations:
|
|||||||
if mmd_translation.filtered_translation_element_indices_active_index < 0:
|
if mmd_translation.filtered_translation_element_indices_active_index < 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices[mmd_translation.filtered_translation_element_indices_active_index]
|
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices[mmd_translation.filtered_translation_element_indices_active_index]
|
||||||
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value]
|
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements[mmd_translation_element_index.value]
|
||||||
|
|
||||||
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].update_index(mmd_translation_element)
|
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].update_index(mmd_translation_element)
|
||||||
|
|
||||||
@@ -724,7 +699,7 @@ class FnTranslations:
|
|||||||
filter_visible: bool = mmd_translation.filter_visible
|
filter_visible: bool = mmd_translation.filter_visible
|
||||||
|
|
||||||
def check_blank_name(name_j: str, name_e: str) -> bool:
|
def check_blank_name(name_j: str, name_e: str) -> bool:
|
||||||
return filter_japanese_blank and name_j or filter_english_blank and name_e
|
return (filter_japanese_blank and name_j) or (filter_english_blank and name_e)
|
||||||
|
|
||||||
for handler in MMD_DATA_HANDLERS:
|
for handler in MMD_DATA_HANDLERS:
|
||||||
if handler.type_name in mmd_translation.filter_types:
|
if handler.type_name in mmd_translation.filter_types:
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
# 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.
|
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||||
|
|
||||||
import logging
|
from .....core.logging_setup import logger
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
from bpy_extras import anim_utils
|
||||||
from mathutils import Quaternion, Vector
|
from mathutils import Quaternion, Vector
|
||||||
|
|
||||||
from ... import utils
|
from ... import utils
|
||||||
@@ -260,7 +261,7 @@ class VMDImporter:
|
|||||||
def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, convert_mmd_camera=True, convert_mmd_lamp=True, frame_margin=5, use_mirror=False, use_NLA=False):
|
def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, convert_mmd_camera=True, convert_mmd_lamp=True, frame_margin=5, use_mirror=False, use_NLA=False):
|
||||||
self.__vmdFile = vmd.File()
|
self.__vmdFile = vmd.File()
|
||||||
self.__vmdFile.load(filepath=filepath)
|
self.__vmdFile.load(filepath=filepath)
|
||||||
logging.debug(str(self.__vmdFile.header))
|
logger.debug(str(self.__vmdFile.header))
|
||||||
self.__scale = scale
|
self.__scale = scale
|
||||||
self.__convert_mmd_camera = convert_mmd_camera
|
self.__convert_mmd_camera = convert_mmd_camera
|
||||||
self.__convert_mmd_lamp = convert_mmd_lamp
|
self.__convert_mmd_lamp = convert_mmd_lamp
|
||||||
@@ -300,21 +301,31 @@ class VMDImporter:
|
|||||||
kp.handle_right = kp.co + Vector((1, 0))
|
kp.handle_right = kp.co + Vector((1, 0))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __keyframe_insert_inner(fcurves: bpy.types.ActionFCurves, path: str, index: int, frame: float, value: float):
|
def __get_channelbag(action: bpy.types.Action, target_id=None):
|
||||||
fcurve = fcurves.find(path, index=index)
|
"""Get or create channelbag for action using Blender 5.0 API."""
|
||||||
|
if not action.slots:
|
||||||
|
slot = action.slots.new(for_id=target_id)
|
||||||
|
else:
|
||||||
|
slot = action.slots[0]
|
||||||
|
return anim_utils.action_ensure_channelbag_for_slot(action, slot)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __keyframe_insert_inner(action: bpy.types.Action, path: str, index: int, frame: float, value: float, target_id=None, group_name=None):
|
||||||
|
channelbag = VMDImporter.__get_channelbag(action, target_id)
|
||||||
|
fcurve = channelbag.fcurves.find(path, index=index)
|
||||||
if fcurve is None:
|
if fcurve is None:
|
||||||
fcurve = fcurves.new(path, index=index)
|
fcurve = channelbag.fcurves.new(path, index=index, group_name=group_name)
|
||||||
fcurve.keyframe_points.insert(frame, value, options={"FAST"})
|
fcurve.keyframe_points.insert(frame, value, options={"FAST"})
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __keyframe_insert(fcurves: bpy.types.ActionFCurves, path: str, frame: float, value: Union[int, float, Vector]):
|
def __keyframe_insert(action: bpy.types.Action, path: str, frame: float, value: Union[int, float, Vector], target_id=None, group_name=None):
|
||||||
if isinstance(value, (int, float)):
|
if isinstance(value, (int, float)):
|
||||||
VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value)
|
VMDImporter.__keyframe_insert_inner(action, path, 0, frame, value, target_id, group_name)
|
||||||
|
|
||||||
elif isinstance(value, Vector):
|
elif isinstance(value, Vector):
|
||||||
VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value[0])
|
VMDImporter.__keyframe_insert_inner(action, path, 0, frame, value[0], target_id, group_name)
|
||||||
VMDImporter.__keyframe_insert_inner(fcurves, path, 1, frame, value[1])
|
VMDImporter.__keyframe_insert_inner(action, path, 1, frame, value[1], target_id, group_name)
|
||||||
VMDImporter.__keyframe_insert_inner(fcurves, path, 2, frame, value[2])
|
VMDImporter.__keyframe_insert_inner(action, path, 2, frame, value[2], target_id, group_name)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise TypeError("Unsupported type: {0}".format(type(value)))
|
raise TypeError("Unsupported type: {0}".format(type(value)))
|
||||||
@@ -370,7 +381,7 @@ class VMDImporter:
|
|||||||
|
|
||||||
def __assignToArmature(self, armObj, action_name=None):
|
def __assignToArmature(self, armObj, action_name=None):
|
||||||
boneAnim = self.__vmdFile.boneAnimation
|
boneAnim = self.__vmdFile.boneAnimation
|
||||||
logging.info("---- bone animations:%5d target: %s", len(boneAnim), armObj.name)
|
logger.info("---- bone animations:%5d target: %s", len(boneAnim), armObj.name)
|
||||||
if len(boneAnim) < 1:
|
if len(boneAnim) < 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -401,22 +412,25 @@ class VMDImporter:
|
|||||||
continue
|
continue
|
||||||
bone = pose_bones.get(name, None)
|
bone = pose_bones.get(name, None)
|
||||||
if bone is None:
|
if bone is None:
|
||||||
logging.warning("WARNING: not found bone %s (%d frames)", name, len(keyFrames))
|
logger.warning("WARNING: not found bone %s (%d frames)", name, len(keyFrames))
|
||||||
continue
|
continue
|
||||||
logging.info("(bone) frames:%5d name: %s", len(keyFrames), name)
|
logger.info("(bone) frames:%5d name: %s", len(keyFrames), name)
|
||||||
assert bone_name_table.get(bone.name, name) == name
|
assert bone_name_table.get(bone.name, name) == name
|
||||||
bone_name_table[bone.name] = name
|
bone_name_table[bone.name] = name
|
||||||
|
|
||||||
|
# Get channelbag for this action
|
||||||
|
channelbag = self.__get_channelbag(action, armObj.data)
|
||||||
|
|
||||||
fcurves = [dummy_keyframe_points] * 7 # x, y, z, r0, r1, r2, (r3)
|
fcurves = [dummy_keyframe_points] * 7 # x, y, z, r0, r1, r2, (r3)
|
||||||
data_path_rot = prop_rot_map.get(bone.rotation_mode, "rotation_euler")
|
data_path_rot = prop_rot_map.get(bone.rotation_mode, "rotation_euler")
|
||||||
bone_rotation = getattr(bone, data_path_rot)
|
bone_rotation = getattr(bone, data_path_rot)
|
||||||
default_values = list(bone.location) + list(bone_rotation)
|
default_values = list(bone.location) + list(bone_rotation)
|
||||||
data_path = 'pose.bones["%s"].location' % bone.name
|
data_path = 'pose.bones["%s"].location' % bone.name
|
||||||
for axis_i in range(3):
|
for axis_i in range(3):
|
||||||
fcurves[axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name)
|
fcurves[axis_i] = channelbag.fcurves.new(data_path=data_path, index=axis_i, group_name=bone.name)
|
||||||
data_path = 'pose.bones["%s"].%s' % (bone.name, data_path_rot)
|
data_path = 'pose.bones["%s"].%s' % (bone.name, data_path_rot)
|
||||||
for axis_i in range(len(bone_rotation)):
|
for axis_i in range(len(bone_rotation)):
|
||||||
fcurves[3 + axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name)
|
fcurves[3 + axis_i] = channelbag.fcurves.new(data_path=data_path, index=axis_i, group_name=bone.name)
|
||||||
|
|
||||||
for i in range(len(default_values)):
|
for i in range(len(default_values)):
|
||||||
c = fcurves[i]
|
c = fcurves[i]
|
||||||
@@ -458,15 +472,17 @@ class VMDImporter:
|
|||||||
self.__setInterpolation(interp[idx : idx + 16 : 4], prev_kp, kp)
|
self.__setInterpolation(interp[idx : idx + 16 : 4], prev_kp, kp)
|
||||||
prev_kps = curr_kps
|
prev_kps = curr_kps
|
||||||
|
|
||||||
for c in action.fcurves:
|
# Get channelbag to iterate fcurves
|
||||||
|
channelbag = self.__get_channelbag(action, armObj.data)
|
||||||
|
for c in channelbag.fcurves:
|
||||||
self.__fixFcurveHandles(c)
|
self.__fixFcurveHandles(c)
|
||||||
|
|
||||||
# property animation
|
# property animation
|
||||||
propertyAnim = self.__vmdFile.propertyAnimation
|
propertyAnim = self.__vmdFile.propertyAnimation
|
||||||
if len(propertyAnim) > 0:
|
if len(propertyAnim) > 0:
|
||||||
logging.info("---- IK animations:%5d target: %s", len(propertyAnim), armObj.name)
|
logger.info("---- IK animations:%5d target: %s", len(propertyAnim), armObj.name)
|
||||||
for keyFrame in propertyAnim:
|
for keyFrame in propertyAnim:
|
||||||
logging.debug("(IK) frame:%5d list: %s", keyFrame.frame_number, keyFrame.ik_states)
|
logger.debug("(IK) frame:%5d list: %s", keyFrame.frame_number, keyFrame.ik_states)
|
||||||
frame = keyFrame.frame_number + self.__frame_margin
|
frame = keyFrame.frame_number + self.__frame_margin
|
||||||
for ikName, enable in keyFrame.ik_states:
|
for ikName, enable in keyFrame.ik_states:
|
||||||
bone = pose_bones.get(ikName, None)
|
bone = pose_bones.get(ikName, None)
|
||||||
@@ -500,7 +516,7 @@ class VMDImporter:
|
|||||||
|
|
||||||
def __assignToMesh(self, meshObj, action_name=None):
|
def __assignToMesh(self, meshObj, action_name=None):
|
||||||
shapeKeyAnim = self.__vmdFile.shapeKeyAnimation
|
shapeKeyAnim = self.__vmdFile.shapeKeyAnimation
|
||||||
logging.info("---- morph animations:%5d target: %s", len(shapeKeyAnim), meshObj.name)
|
logger.info("---- morph animations:%5d target: %s", len(shapeKeyAnim), meshObj.name)
|
||||||
if len(shapeKeyAnim) < 1:
|
if len(shapeKeyAnim) < 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -514,11 +530,12 @@ class VMDImporter:
|
|||||||
|
|
||||||
for name, keyFrames in shapeKeyAnim.items():
|
for name, keyFrames in shapeKeyAnim.items():
|
||||||
if name not in shapeKeyDict:
|
if name not in shapeKeyDict:
|
||||||
logging.warning("WARNING: not found shape key %s (%d frames)", name, len(keyFrames))
|
logger.warning("WARNING: not found shape key %s (%d frames)", name, len(keyFrames))
|
||||||
continue
|
continue
|
||||||
logging.info("(mesh) frames:%5d name: %s", len(keyFrames), name)
|
logger.info("(mesh) frames:%5d name: %s", len(keyFrames), name)
|
||||||
shapeKey = shapeKeyDict[name]
|
shapeKey = shapeKeyDict[name]
|
||||||
fcurve = action.fcurves.new(data_path='key_blocks["%s"].value' % shapeKey.name)
|
channelbag = self.__get_channelbag(action, meshObj.data.shape_keys)
|
||||||
|
fcurve = channelbag.fcurves.new(data_path='key_blocks["%s"].value' % shapeKey.name)
|
||||||
fcurve.keyframe_points.add(len(keyFrames))
|
fcurve.keyframe_points.add(len(keyFrames))
|
||||||
keyFrames.sort(key=lambda x: x.frame_number)
|
keyFrames.sort(key=lambda x: x.frame_number)
|
||||||
for k, v in zip(keyFrames, fcurve.keyframe_points):
|
for k, v in zip(keyFrames, fcurve.keyframe_points):
|
||||||
@@ -532,16 +549,16 @@ class VMDImporter:
|
|||||||
|
|
||||||
def __assignToRoot(self, rootObj, action_name=None):
|
def __assignToRoot(self, rootObj, action_name=None):
|
||||||
propertyAnim = self.__vmdFile.propertyAnimation
|
propertyAnim = self.__vmdFile.propertyAnimation
|
||||||
logging.info("---- display animations:%5d target: %s", len(propertyAnim), rootObj.name)
|
logger.info("---- display animations:%5d target: %s", len(propertyAnim), rootObj.name)
|
||||||
if len(propertyAnim) < 1:
|
if len(propertyAnim) < 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
action_name = action_name or rootObj.name
|
action_name = action_name or rootObj.name
|
||||||
action = bpy.data.actions.new(name=action_name)
|
action = bpy.data.actions.new(name=action_name)
|
||||||
|
|
||||||
logging.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim])
|
logger.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim])
|
||||||
for keyFrame in propertyAnim:
|
for keyFrame in propertyAnim:
|
||||||
self.__keyframe_insert(action.fcurves, "mmd_root.show_meshes", keyFrame.frame_number + self.__frame_margin, float(keyFrame.visible))
|
self.__keyframe_insert(action, "mmd_root.show_meshes", keyFrame.frame_number + self.__frame_margin, float(keyFrame.visible), rootObj)
|
||||||
|
|
||||||
self.__assign_action(rootObj, action)
|
self.__assign_action(rootObj, action)
|
||||||
|
|
||||||
@@ -562,7 +579,7 @@ class VMDImporter:
|
|||||||
cameraObj = mmdCameraInstance.camera()
|
cameraObj = mmdCameraInstance.camera()
|
||||||
|
|
||||||
cameraAnim = self.__vmdFile.cameraAnimation
|
cameraAnim = self.__vmdFile.cameraAnimation
|
||||||
logging.info("(camera) frames:%5d name: %s", len(cameraAnim), mmdCamera.name)
|
logger.info("(camera) frames:%5d name: %s", len(cameraAnim), mmdCamera.name)
|
||||||
if len(cameraAnim) < 1:
|
if len(cameraAnim) < 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -574,14 +591,18 @@ class VMDImporter:
|
|||||||
if self.__mirror:
|
if self.__mirror:
|
||||||
_loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation3
|
_loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation3
|
||||||
|
|
||||||
|
# Get channelbags for camera actions
|
||||||
|
parent_channelbag = self.__get_channelbag(parent_action, mmdCamera.parent)
|
||||||
|
distance_channelbag = self.__get_channelbag(distance_action, mmdCamera.distance)
|
||||||
|
|
||||||
fcurves = []
|
fcurves = []
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
|
fcurves.append(parent_channelbag.fcurves.new(data_path="location", index=i)) # x, y, z
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
|
fcurves.append(parent_channelbag.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
|
fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.angle")) # fov
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
|
fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
|
||||||
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
|
fcurves.append(distance_channelbag.fcurves.new(data_path="location", index=1)) # dis
|
||||||
for c in fcurves:
|
for c in fcurves:
|
||||||
c.keyframe_points.add(len(cameraAnim))
|
c.keyframe_points.add(len(cameraAnim))
|
||||||
|
|
||||||
@@ -629,7 +650,7 @@ class VMDImporter:
|
|||||||
lampObj = mmdLampInstance.lamp()
|
lampObj = mmdLampInstance.lamp()
|
||||||
|
|
||||||
lampAnim = self.__vmdFile.lampAnimation
|
lampAnim = self.__vmdFile.lampAnimation
|
||||||
logging.info("(lamp) frames:%5d name: %s", len(lampAnim), mmdLamp.name)
|
logger.info("(lamp) frames:%5d name: %s", len(lampAnim), mmdLamp.name)
|
||||||
if len(lampAnim) < 1:
|
if len(lampAnim) < 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -640,10 +661,11 @@ class VMDImporter:
|
|||||||
_loc = _MirrorMapper.get_location if self.__mirror else lambda i: i
|
_loc = _MirrorMapper.get_location if self.__mirror else lambda i: i
|
||||||
for keyFrame in lampAnim:
|
for keyFrame in lampAnim:
|
||||||
frame = keyFrame.frame_number + self.__frame_margin
|
frame = keyFrame.frame_number + self.__frame_margin
|
||||||
self.__keyframe_insert(color_action.fcurves, "color", frame, Vector(keyFrame.color))
|
self.__keyframe_insert(color_action, "color", frame, Vector(keyFrame.color), lampObj)
|
||||||
self.__keyframe_insert(location_action.fcurves, "location", frame, Vector(_loc(keyFrame.direction)).xzy * -1)
|
self.__keyframe_insert(location_action, "location", frame, Vector(_loc(keyFrame.direction)).xzy * -1, mmdLamp)
|
||||||
|
|
||||||
for fcurve in location_action.fcurves:
|
location_channelbag = self.__get_channelbag(location_action, mmdLamp)
|
||||||
|
for fcurve in location_channelbag.fcurves:
|
||||||
self.detectLampChange(fcurve)
|
self.detectLampChange(fcurve)
|
||||||
|
|
||||||
self.__assign_action(lampObj.data, color_action)
|
self.__assign_action(lampObj.data, color_action)
|
||||||
|
|||||||
@@ -1,48 +1,39 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2012 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
|
||||||
|
|
||||||
from typing import Iterable, Optional, Any, List, Tuple, Union
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Material, NodeTree, Node, NodeSocket, ShaderNodeGroup, ShaderNodeOutputMaterial, NodeLink
|
|
||||||
|
|
||||||
from ..logging_setup import logger
|
|
||||||
from .core.shader import _NodeGroupUtils
|
|
||||||
from .core.material import FnMaterial
|
from .core.material import FnMaterial
|
||||||
|
from .core.shader import _NodeGroupUtils
|
||||||
|
|
||||||
|
|
||||||
def __switchToCyclesRenderEngine() -> None:
|
def __switchToCyclesRenderEngine():
|
||||||
if bpy.context.scene.render.engine != "CYCLES":
|
if bpy.context.scene.render.engine != "CYCLES":
|
||||||
logger.debug("Switching render engine to Cycles")
|
|
||||||
bpy.context.scene.render.engine = "CYCLES"
|
bpy.context.scene.render.engine = "CYCLES"
|
||||||
|
|
||||||
|
|
||||||
def __exposeNodeTreeInput(in_socket: NodeSocket, name: str, default_value: Any, node_input: Node, shader: NodeTree) -> None:
|
def __exposeNodeTreeInput(in_socket, name, default_value, node_input, shader):
|
||||||
_NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value)
|
_NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value)
|
||||||
|
|
||||||
|
|
||||||
def __exposeNodeTreeOutput(out_socket: NodeSocket, name: str, node_output: Node, shader: NodeTree) -> None:
|
def __exposeNodeTreeOutput(out_socket, name, node_output, shader):
|
||||||
_NodeGroupUtils(shader).new_output_socket(name, out_socket)
|
_NodeGroupUtils(shader).new_output_socket(name, out_socket)
|
||||||
|
|
||||||
|
|
||||||
def __getMaterialOutput(nodes: bpy.types.Nodes, bl_idname: str) -> Node:
|
def __getMaterialOutput(nodes, bl_idname):
|
||||||
o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname)
|
o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname)
|
||||||
o.is_active_output = True
|
o.is_active_output = True
|
||||||
return o
|
return o
|
||||||
|
|
||||||
|
|
||||||
def create_MMDAlphaShader() -> NodeTree:
|
def create_MMDAlphaShader():
|
||||||
__switchToCyclesRenderEngine()
|
__switchToCyclesRenderEngine()
|
||||||
|
|
||||||
if "MMDAlphaShader" in bpy.data.node_groups:
|
if "MMDAlphaShader" in bpy.data.node_groups:
|
||||||
logger.debug("Using existing MMDAlphaShader node group")
|
|
||||||
return bpy.data.node_groups["MMDAlphaShader"]
|
return bpy.data.node_groups["MMDAlphaShader"]
|
||||||
|
|
||||||
logger.info("Creating new MMDAlphaShader node group")
|
|
||||||
shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree")
|
shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree")
|
||||||
|
|
||||||
node_input = shader.nodes.new("NodeGroupInput")
|
node_input = shader.nodes.new("NodeGroupInput")
|
||||||
@@ -64,28 +55,26 @@ def create_MMDAlphaShader() -> NodeTree:
|
|||||||
return shader
|
return shader
|
||||||
|
|
||||||
|
|
||||||
def create_MMDBasicShader() -> NodeTree:
|
def create_MMDBasicShader():
|
||||||
__switchToCyclesRenderEngine()
|
__switchToCyclesRenderEngine()
|
||||||
|
|
||||||
if "MMDBasicShader" in bpy.data.node_groups:
|
if "MMDBasicShader" in bpy.data.node_groups:
|
||||||
logger.debug("Using existing MMDBasicShader node group")
|
|
||||||
return bpy.data.node_groups["MMDBasicShader"]
|
return bpy.data.node_groups["MMDBasicShader"]
|
||||||
|
|
||||||
logger.info("Creating new MMDBasicShader node group")
|
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree")
|
||||||
shader: NodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree")
|
|
||||||
|
|
||||||
node_input: Node = shader.nodes.new("NodeGroupInput")
|
node_input: bpy.types.NodeGroupInput = shader.nodes.new("NodeGroupInput")
|
||||||
node_output: Node = shader.nodes.new("NodeGroupOutput")
|
node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput")
|
||||||
node_output.location.x += 250
|
node_output.location.x += 250
|
||||||
node_input.location.x -= 500
|
node_input.location.x -= 500
|
||||||
|
|
||||||
dif: Node = shader.nodes.new("ShaderNodeBsdfDiffuse")
|
dif: bpy.types.ShaderNodeBsdfDiffuse = shader.nodes.new("ShaderNodeBsdfDiffuse")
|
||||||
dif.location.x -= 250
|
dif.location.x -= 250
|
||||||
dif.location.y += 150
|
dif.location.y += 150
|
||||||
glo: Node = shader.nodes.new("ShaderNodeBsdfAnisotropic")
|
glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic")
|
||||||
glo.location.x -= 250
|
glo.location.x -= 250
|
||||||
glo.location.y -= 150
|
glo.location.y -= 150
|
||||||
mix: Node = shader.nodes.new("ShaderNodeMixShader")
|
mix: bpy.types.ShaderNodeMixShader = shader.nodes.new("ShaderNodeMixShader")
|
||||||
shader.links.new(mix.inputs[1], dif.outputs["BSDF"])
|
shader.links.new(mix.inputs[1], dif.outputs["BSDF"])
|
||||||
shader.links.new(mix.inputs[2], glo.outputs["BSDF"])
|
shader.links.new(mix.inputs[2], glo.outputs["BSDF"])
|
||||||
|
|
||||||
@@ -98,65 +87,62 @@ def create_MMDBasicShader() -> NodeTree:
|
|||||||
return shader
|
return shader
|
||||||
|
|
||||||
|
|
||||||
def __enum_linked_nodes(node: Node) -> Iterable[Node]:
|
def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]:
|
||||||
yield node
|
yield node
|
||||||
if node.parent:
|
if node.parent:
|
||||||
yield node.parent
|
yield node.parent
|
||||||
for n in set(l.from_node for i in node.inputs for l in i.links):
|
for n in {link.from_node for i in node.inputs for link in i.links}:
|
||||||
yield from __enum_linked_nodes(n)
|
yield from __enum_linked_nodes(n)
|
||||||
|
|
||||||
|
|
||||||
def __cleanNodeTree(material: Material) -> None:
|
def __cleanNodeTree(material: bpy.types.Material):
|
||||||
logger.debug(f"Cleaning node tree for material: {material.name}")
|
|
||||||
nodes = material.node_tree.nodes
|
nodes = material.node_tree.nodes
|
||||||
node_names = set(n.name for n in nodes)
|
node_names = {n.name for n in nodes}
|
||||||
for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}):
|
for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}):
|
||||||
if any(i.is_linked for i in o.inputs):
|
if any(i.is_linked for i in o.inputs):
|
||||||
node_names -= set(linked.name for linked in __enum_linked_nodes(o))
|
node_names -= {linked.name for linked in __enum_linked_nodes(o)}
|
||||||
for name in node_names:
|
for name in node_names:
|
||||||
nodes.remove(nodes[name])
|
nodes.remove(nodes[name])
|
||||||
|
|
||||||
|
|
||||||
def convertToCyclesShader(obj: bpy.types.Object, use_principled: bool = False, clean_nodes: bool = False, subsurface: float = 0.001) -> None:
|
def convertToCyclesShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001):
|
||||||
logger.info(f"Converting {obj.name} to Cycles shader (use_principled={use_principled}, clean_nodes={clean_nodes})")
|
|
||||||
__switchToCyclesRenderEngine()
|
__switchToCyclesRenderEngine()
|
||||||
convertToBlenderShader(obj, use_principled, clean_nodes, subsurface)
|
convertToBlenderShader(obj, use_principled, clean_nodes, subsurface)
|
||||||
|
|
||||||
|
|
||||||
def convertToBlenderShader(obj: bpy.types.Object, use_principled: bool = False, clean_nodes: bool = False, subsurface: float = 0.001) -> None:
|
def convertToBlenderShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001):
|
||||||
for i in obj.material_slots:
|
for i in obj.material_slots:
|
||||||
if not i.material:
|
if not i.material:
|
||||||
continue
|
continue
|
||||||
|
# use_nodes is deprecated in 5.0 but always returns True and setting it is safe
|
||||||
if not i.material.use_nodes:
|
if not i.material.use_nodes:
|
||||||
logger.debug(f"Enabling nodes for material: {i.material.name}")
|
|
||||||
i.material.use_nodes = True
|
i.material.use_nodes = True
|
||||||
__convertToMMDBasicShader(i.material)
|
__convertToMMDBasicShader(i.material)
|
||||||
if use_principled:
|
if use_principled:
|
||||||
logger.debug(f"Converting material to Principled BSDF: {i.material.name}")
|
|
||||||
__convertToPrincipledBsdf(i.material, subsurface)
|
__convertToPrincipledBsdf(i.material, subsurface)
|
||||||
if clean_nodes:
|
if clean_nodes:
|
||||||
__cleanNodeTree(i.material)
|
__cleanNodeTree(i.material)
|
||||||
|
|
||||||
def convertToMMDShader(obj: bpy.types.Object) -> None:
|
|
||||||
|
def convertToMMDShader(obj):
|
||||||
"""BSDF -> MMDShaderDev conversion."""
|
"""BSDF -> MMDShaderDev conversion."""
|
||||||
logger.info(f"Converting {obj.name} to MMD shader")
|
|
||||||
for i in obj.material_slots:
|
for i in obj.material_slots:
|
||||||
if not i.material:
|
if not i.material:
|
||||||
continue
|
continue
|
||||||
|
# use_nodes is deprecated in 5.0 but always returns True and setting it is safe
|
||||||
if not i.material.use_nodes:
|
if not i.material.use_nodes:
|
||||||
logger.debug(f"Enabling nodes for material: {i.material.name}")
|
|
||||||
i.material.use_nodes = True
|
i.material.use_nodes = True
|
||||||
FnMaterial.convert_to_mmd_material(i.material)
|
FnMaterial.convert_to_mmd_material(i.material)
|
||||||
|
|
||||||
def __convertToMMDBasicShader(material: Material) -> None:
|
|
||||||
logger.debug(f"Converting material to MMD Basic Shader: {material.name}")
|
def __convertToMMDBasicShader(material: bpy.types.Material):
|
||||||
# TODO: test me
|
# TODO: test me
|
||||||
mmd_basic_shader_grp = create_MMDBasicShader()
|
mmd_basic_shader_grp = create_MMDBasicShader()
|
||||||
mmd_alpha_shader_grp = create_MMDAlphaShader()
|
mmd_alpha_shader_grp = create_MMDAlphaShader()
|
||||||
|
|
||||||
if not any(filter(lambda x: isinstance(x, ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)):
|
if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)):
|
||||||
# Add nodes for Cycles Render
|
# Add nodes for Cycles Render
|
||||||
shader: ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
|
shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
|
||||||
shader.node_tree = mmd_basic_shader_grp
|
shader.node_tree = mmd_basic_shader_grp
|
||||||
shader.inputs[0].default_value[:3] = material.diffuse_color[:3]
|
shader.inputs[0].default_value[:3] = material.diffuse_color[:3]
|
||||||
shader.inputs[1].default_value[:3] = material.specular_color[:3]
|
shader.inputs[1].default_value[:3] = material.specular_color[:3]
|
||||||
@@ -171,8 +157,7 @@ def __convertToMMDBasicShader(material: Material) -> None:
|
|||||||
alpha_value = material.diffuse_color[3]
|
alpha_value = material.diffuse_color[3]
|
||||||
|
|
||||||
if alpha_value < 1.0:
|
if alpha_value < 1.0:
|
||||||
logger.debug(f"Material has alpha: {material.name}, alpha={alpha_value}")
|
alpha_shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
|
||||||
alpha_shader: ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
|
|
||||||
alpha_shader.location.x = shader.location.x + 250
|
alpha_shader.location.x = shader.location.x + 250
|
||||||
alpha_shader.location.y = shader.location.y - 150
|
alpha_shader.location.y = shader.location.y - 150
|
||||||
alpha_shader.node_tree = mmd_alpha_shader_grp
|
alpha_shader.node_tree = mmd_alpha_shader_grp
|
||||||
@@ -180,22 +165,21 @@ def __convertToMMDBasicShader(material: Material) -> None:
|
|||||||
material.node_tree.links.new(alpha_shader.inputs[0], outplug)
|
material.node_tree.links.new(alpha_shader.inputs[0], outplug)
|
||||||
outplug = alpha_shader.outputs[0]
|
outplug = alpha_shader.outputs[0]
|
||||||
|
|
||||||
material_output: ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial")
|
material_output: bpy.types.ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial")
|
||||||
material.node_tree.links.new(material_output.inputs["Surface"], outplug)
|
material.node_tree.links.new(material_output.inputs["Surface"], outplug)
|
||||||
material_output.location.x = shader.location.x + 500
|
material_output.location.x = shader.location.x + 500
|
||||||
material_output.location.y = shader.location.y - 150
|
material_output.location.y = shader.location.y - 150
|
||||||
|
|
||||||
|
|
||||||
def __convertToPrincipledBsdf(material: Material, subsurface: float) -> None:
|
def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float):
|
||||||
logger.debug(f"Converting material to Principled BSDF: {material.name}")
|
|
||||||
node_names = set()
|
node_names = set()
|
||||||
for s in (n for n in material.node_tree.nodes if isinstance(n, ShaderNodeGroup)):
|
for s in (n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)):
|
||||||
if s.node_tree.name == "MMDBasicShader":
|
if s.node_tree.name == "MMDBasicShader":
|
||||||
l: NodeLink
|
link: bpy.types.NodeLink
|
||||||
for l in s.outputs[0].links:
|
for link in s.outputs[0].links:
|
||||||
to_node = l.to_node
|
to_node = link.to_node
|
||||||
# assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader
|
# assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader
|
||||||
if isinstance(to_node, ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader":
|
if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader":
|
||||||
__switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node)
|
__switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node)
|
||||||
node_names.add(to_node.name)
|
node_names.add(to_node.name)
|
||||||
else:
|
else:
|
||||||
@@ -210,9 +194,8 @@ def __convertToPrincipledBsdf(material: Material, subsurface: float) -> None:
|
|||||||
nodes.remove(nodes[name])
|
nodes.remove(nodes[name])
|
||||||
|
|
||||||
|
|
||||||
def __switchToPrincipledBsdf(node_tree: NodeTree, node_basic: ShaderNodeGroup, subsurface: float, node_alpha: Optional[ShaderNodeGroup] = None) -> None:
|
def __switchToPrincipledBsdf(node_tree: bpy.types.NodeTree, node_basic: bpy.types.ShaderNodeGroup, subsurface: float, node_alpha: Optional[bpy.types.ShaderNodeGroup] = None):
|
||||||
logger.debug(f"Switching to Principled BSDF: {node_basic.name}")
|
shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled")
|
||||||
shader: Node = node_tree.nodes.new("ShaderNodeBsdfPrincipled")
|
|
||||||
shader.parent = node_basic.parent
|
shader.parent = node_basic.parent
|
||||||
shader.location.x = node_basic.location.x
|
shader.location.x = node_basic.location.x
|
||||||
shader.location.y = node_basic.location.y
|
shader.location.y = node_basic.location.y
|
||||||
@@ -240,7 +223,7 @@ def __switchToPrincipledBsdf(node_tree: NodeTree, node_basic: ShaderNodeGroup, s
|
|||||||
|
|
||||||
if alpha_socket_name in node_alpha.inputs:
|
if alpha_socket_name in node_alpha.inputs:
|
||||||
if "Alpha" in shader.inputs:
|
if "Alpha" in shader.inputs:
|
||||||
shader.inputs["Alpha"].default_value = node_alpha.inputs[alpha_socket_name].default_value
|
shader.inputs["Alpha"].default_value = node_alpha.inputs["Alpha"].default_value
|
||||||
if node_alpha.inputs[alpha_socket_name].is_linked:
|
if node_alpha.inputs[alpha_socket_name].is_linked:
|
||||||
node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs["Alpha"])
|
node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs["Alpha"])
|
||||||
else:
|
else:
|
||||||
@@ -256,5 +239,5 @@ def __switchToPrincipledBsdf(node_tree: NodeTree, node_basic: ShaderNodeGroup, s
|
|||||||
node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, node_invert.inputs[1])
|
node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, node_invert.inputs[1])
|
||||||
node_tree.links.new(node_invert.outputs[0], shader.inputs["Transmission"])
|
node_tree.links.new(node_invert.outputs[0], shader.inputs["Transmission"])
|
||||||
|
|
||||||
for l in output_links:
|
for link in output_links:
|
||||||
node_tree.links.new(shader.outputs[0], l.to_socket)
|
node_tree.links.new(shader.outputs[0], link.to_socket)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+142
-98
@@ -1,22 +1,16 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
from collections import defaultdict
|
||||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import BoolProperty, StringProperty, FloatProperty
|
from bpy.props import BoolProperty, StringProperty
|
||||||
from bpy.types import Operator, Context, Object, Material
|
from bpy.types import Operator
|
||||||
|
|
||||||
from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast
|
|
||||||
|
|
||||||
from .. import cycles_converter
|
from .. import cycles_converter
|
||||||
from ..core.exceptions import MaterialNotFoundError
|
from ..core.exceptions import MaterialNotFoundError
|
||||||
from ..core.material import FnMaterial
|
from ..core.material import FnMaterial
|
||||||
from ..core.shader import _NodeGroupUtils
|
from ..core.shader import _NodeGroupUtils
|
||||||
from ....core.logging_setup import logger
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
class ConvertMaterialsForCycles(Operator):
|
class ConvertMaterialsForCycles(Operator):
|
||||||
@@ -25,14 +19,14 @@ class ConvertMaterialsForCycles(Operator):
|
|||||||
bl_description = "Convert materials of selected objects for Cycles."
|
bl_description = "Convert materials of selected objects for Cycles."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
use_principled: BoolProperty(
|
use_principled: bpy.props.BoolProperty(
|
||||||
name="Convert to Principled BSDF",
|
name="Convert to Principled BSDF",
|
||||||
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
|
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
|
||||||
default=False,
|
default=False,
|
||||||
options={"SKIP_SAVE"},
|
options={"SKIP_SAVE"},
|
||||||
)
|
)
|
||||||
|
|
||||||
clean_nodes: BoolProperty(
|
clean_nodes: bpy.props.BoolProperty(
|
||||||
name="Clean Nodes",
|
name="Clean Nodes",
|
||||||
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
||||||
default=False,
|
default=False,
|
||||||
@@ -40,27 +34,22 @@ class ConvertMaterialsForCycles(Operator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context):
|
||||||
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
|
return any(x.type == "MESH" for x in context.selected_objects)
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.prop(self, "use_principled")
|
layout.prop(self, "use_principled")
|
||||||
layout.prop(self, "clean_nodes")
|
layout.prop(self, "clean_nodes")
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
try:
|
try:
|
||||||
context.scene.render.engine = "CYCLES"
|
context.scene.render.engine = "CYCLES"
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error(f"Failed to change to Cycles render engine: {traceback.format_exc()}")
|
|
||||||
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
|
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
logger.info(f"Converting materials for Cycles with principled={self.use_principled}, clean_nodes={self.clean_nodes}")
|
|
||||||
for obj in (x for x in context.selected_objects if x.type == "MESH"):
|
for obj in (x for x in context.selected_objects if x.type == "MESH"):
|
||||||
logger.debug(f"Converting materials for object: {obj.name}")
|
|
||||||
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
|
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -70,21 +59,21 @@ class ConvertMaterials(Operator):
|
|||||||
bl_description = "Convert materials of selected objects."
|
bl_description = "Convert materials of selected objects."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
use_principled: BoolProperty(
|
use_principled: bpy.props.BoolProperty(
|
||||||
name="Convert to Principled BSDF",
|
name="Convert to Principled BSDF",
|
||||||
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
|
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
|
||||||
default=True,
|
default=True,
|
||||||
options={"SKIP_SAVE"},
|
options={"SKIP_SAVE"},
|
||||||
)
|
)
|
||||||
|
|
||||||
clean_nodes: BoolProperty(
|
clean_nodes: bpy.props.BoolProperty(
|
||||||
name="Clean Nodes",
|
name="Clean Nodes",
|
||||||
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
||||||
default=True,
|
default=True,
|
||||||
options={"SKIP_SAVE"},
|
options={"SKIP_SAVE"},
|
||||||
)
|
)
|
||||||
|
|
||||||
subsurface: FloatProperty(
|
subsurface: bpy.props.FloatProperty(
|
||||||
name="Subsurface",
|
name="Subsurface",
|
||||||
default=0.001,
|
default=0.001,
|
||||||
soft_min=0.000,
|
soft_min=0.000,
|
||||||
@@ -94,41 +83,130 @@ class ConvertMaterials(Operator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context):
|
||||||
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
|
return any(x.type == "MESH" for x in context.selected_objects)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
logger.info(f"Converting materials with principled={self.use_principled}, clean_nodes={self.clean_nodes}, subsurface={self.subsurface}")
|
|
||||||
for obj in context.selected_objects:
|
for obj in context.selected_objects:
|
||||||
if obj.type != "MESH":
|
if obj.type != "MESH":
|
||||||
continue
|
continue
|
||||||
logger.debug(f"Converting materials for object: {obj.name}")
|
|
||||||
cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
|
cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
class ConvertBSDFMaterials(Operator):
|
|
||||||
bl_idname = 'mmd_tools.convert_bsdf_materials'
|
class MergeMaterials(Operator):
|
||||||
bl_label = 'Convert Blender Materials'
|
bl_idname = "mmd_tools.merge_materials"
|
||||||
bl_description = 'Convert materials of selected objects.'
|
bl_label = "Merge Materials"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_description = "Merge materials with the same texture in selected objects. Only merges materials with exactly one texture node. Materials with no texture or with multiple textures are not merged. Please convert to Blender materials first."
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context):
|
||||||
return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None
|
return any(x.type == "MESH" for x in context.selected_objects)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
logger.info("Converting BSDF materials to MMD shader")
|
# Process all selected mesh objects
|
||||||
for obj in context.selected_objects:
|
for obj in context.selected_objects:
|
||||||
if obj.type != 'MESH':
|
if obj.type != "MESH":
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.merge_materials_for_object(context, obj)
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
def merge_materials_for_object(self, context, obj):
|
||||||
|
"""Merge materials with same texture for a single object"""
|
||||||
|
if not obj.data.materials:
|
||||||
|
self.report({"INFO"}, f"Object '{obj.name}' has no materials")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Map texture paths to material indices and names
|
||||||
|
texture_to_materials = defaultdict(list)
|
||||||
|
|
||||||
|
# Check each material
|
||||||
|
for i, material in enumerate(obj.data.materials):
|
||||||
|
# use_nodes is deprecated in 5.0 but always returns True, so check is safe
|
||||||
|
if not material or not material.use_nodes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 1. Check texture node count (must be exactly 1)
|
||||||
|
texture_nodes = [node for node in material.node_tree.nodes if node.type == "TEX_IMAGE"]
|
||||||
|
if len(texture_nodes) != 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2. Record texture path and material info
|
||||||
|
texture_node = texture_nodes[0]
|
||||||
|
if texture_node.image:
|
||||||
|
texture_path = bpy.path.abspath(texture_node.image.filepath)
|
||||||
|
texture_to_materials[texture_path].append({"index": i, "name": material.name})
|
||||||
|
|
||||||
|
# Find material groups that need merging
|
||||||
|
materials_to_merge = {path: materials for path, materials in texture_to_materials.items() if len(materials) > 1}
|
||||||
|
|
||||||
|
if not materials_to_merge:
|
||||||
|
self.report({"INFO"}, f"No materials to merge in object '{obj.name}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process each texture group
|
||||||
|
context.view_layer.objects.active = obj
|
||||||
|
bpy.ops.object.mode_set(mode="EDIT")
|
||||||
|
merge_details = []
|
||||||
|
for texture_path, materials in materials_to_merge.items():
|
||||||
|
# Use first material as target
|
||||||
|
target_material = materials[0]
|
||||||
|
target_index = target_material["index"]
|
||||||
|
target_name = target_material["name"]
|
||||||
|
|
||||||
|
source_materials = []
|
||||||
|
|
||||||
|
# Reassign faces from other materials to target material
|
||||||
|
for source_material in materials[1:]:
|
||||||
|
source_index = source_material["index"]
|
||||||
|
source_name = source_material["name"]
|
||||||
|
source_materials.append(source_name)
|
||||||
|
|
||||||
|
bpy.ops.mesh.select_all(action="DESELECT")
|
||||||
|
obj.active_material_index = source_index
|
||||||
|
bpy.ops.object.material_slot_select()
|
||||||
|
obj.active_material_index = target_index
|
||||||
|
bpy.ops.object.material_slot_assign()
|
||||||
|
|
||||||
|
# Record merge details
|
||||||
|
texture_name = bpy.path.basename(texture_path)
|
||||||
|
merge_details.append({"texture": texture_name, "target": target_name, "sources": source_materials})
|
||||||
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
bpy.ops.object.material_slot_remove_unused()
|
||||||
|
|
||||||
|
merged_count = sum(len(details["sources"]) for details in merge_details)
|
||||||
|
self.report({"INFO"}, f"Object '{obj.name}': Merged {merged_count} materials")
|
||||||
|
|
||||||
|
for details in merge_details:
|
||||||
|
sources_text = ", ".join(details["sources"])
|
||||||
|
self.report({"INFO"}, f"Same Texture '{details['texture']}': Merged materials [{sources_text}] into '{details['target']}'")
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertBSDFMaterials(Operator):
|
||||||
|
bl_idname = "mmd_tools.convert_bsdf_materials"
|
||||||
|
bl_label = "Convert Blender Materials"
|
||||||
|
bl_description = "Convert materials of selected objects."
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return any(x.type == "MESH" for x in context.selected_objects)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
for obj in context.selected_objects:
|
||||||
|
if obj.type != "MESH":
|
||||||
continue
|
continue
|
||||||
logger.debug(f"Converting BSDF materials for object: {obj.name}")
|
|
||||||
cycles_converter.convertToMMDShader(obj)
|
cycles_converter.convertToMMDShader(obj)
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class _OpenTextureBase:
|
class _OpenTextureBase:
|
||||||
"""Create a texture for mmd model material."""
|
"""Create a texture for mmd model material."""
|
||||||
|
|
||||||
bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
filepath: StringProperty(
|
filepath: StringProperty(
|
||||||
name="File Path",
|
name="File Path",
|
||||||
@@ -142,7 +220,7 @@ class _OpenTextureBase:
|
|||||||
options={"HIDDEN"},
|
options={"HIDDEN"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
def invoke(self, context, event):
|
||||||
context.window_manager.fileselect_add(self)
|
context.window_manager.fileselect_add(self)
|
||||||
return {"RUNNING_MODAL"}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
@@ -152,13 +230,8 @@ class OpenTexture(Operator, _OpenTextureBase):
|
|||||||
bl_label = "Open Texture"
|
bl_label = "Open Texture"
|
||||||
bl_description = "Create main texture of active material"
|
bl_description = "Create main texture of active material"
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
mat = context.active_object.active_material
|
mat = context.active_object.active_material
|
||||||
if not mat:
|
|
||||||
logger.error("No active material found")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
logger.info(f"Creating texture for material: {mat.name} from {self.filepath}")
|
|
||||||
fnMat = FnMaterial(mat)
|
fnMat = FnMaterial(mat)
|
||||||
fnMat.create_texture(self.filepath)
|
fnMat.create_texture(self.filepath)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -172,13 +245,8 @@ class RemoveTexture(Operator):
|
|||||||
bl_description = "Remove main texture of active material"
|
bl_description = "Remove main texture of active material"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
mat = context.active_object.active_material
|
mat = context.active_object.active_material
|
||||||
if not mat:
|
|
||||||
logger.error("No active material found")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
logger.info(f"Removing texture from material: {mat.name}")
|
|
||||||
fnMat = FnMaterial(mat)
|
fnMat = FnMaterial(mat)
|
||||||
fnMat.remove_texture()
|
fnMat.remove_texture()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -191,13 +259,8 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase):
|
|||||||
bl_label = "Open Sphere Texture"
|
bl_label = "Open Sphere Texture"
|
||||||
bl_description = "Create sphere texture of active material"
|
bl_description = "Create sphere texture of active material"
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
mat = context.active_object.active_material
|
mat = context.active_object.active_material
|
||||||
if not mat:
|
|
||||||
logger.error("No active material found")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
logger.info(f"Creating sphere texture for material: {mat.name} from {self.filepath}")
|
|
||||||
fnMat = FnMaterial(mat)
|
fnMat = FnMaterial(mat)
|
||||||
fnMat.create_sphere_texture(self.filepath, context.active_object)
|
fnMat.create_sphere_texture(self.filepath, context.active_object)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -211,13 +274,8 @@ class RemoveSphereTexture(Operator):
|
|||||||
bl_description = "Remove sphere texture of active material"
|
bl_description = "Remove sphere texture of active material"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
mat = context.active_object.active_material
|
mat = context.active_object.active_material
|
||||||
if not mat:
|
|
||||||
logger.error("No active material found")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
logger.info(f"Removing sphere texture from material: {mat.name}")
|
|
||||||
fnMat = FnMaterial(mat)
|
fnMat = FnMaterial(mat)
|
||||||
fnMat.remove_sphere_texture()
|
fnMat.remove_sphere_texture()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -230,21 +288,17 @@ class MoveMaterialUp(Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
|
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" and obj.active_material_index > 0
|
||||||
return bool(valid_mesh and obj.active_material_index > 0)
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
current_idx = obj.active_material_index
|
current_idx = obj.active_material_index
|
||||||
prev_index = current_idx - 1
|
prev_index = current_idx - 1
|
||||||
|
|
||||||
logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}")
|
|
||||||
try:
|
try:
|
||||||
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
|
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
|
||||||
except MaterialNotFoundError:
|
except MaterialNotFoundError:
|
||||||
logger.error(f"Materials not found for indices {current_idx} and {prev_index}")
|
|
||||||
self.report({"ERROR"}, "Materials not found")
|
self.report({"ERROR"}, "Materials not found")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
obj.active_material_index = prev_index
|
obj.active_material_index = prev_index
|
||||||
@@ -259,21 +313,17 @@ class MoveMaterialDown(Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
|
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" and obj.active_material_index < len(obj.material_slots) - 1
|
||||||
return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1)
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
current_idx = obj.active_material_index
|
current_idx = obj.active_material_index
|
||||||
next_index = current_idx + 1
|
next_index = current_idx + 1
|
||||||
|
|
||||||
logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}")
|
|
||||||
try:
|
try:
|
||||||
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
|
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
|
||||||
except MaterialNotFoundError:
|
except MaterialNotFoundError:
|
||||||
logger.error(f"Materials not found for indices {current_idx} and {next_index}")
|
|
||||||
self.report({"ERROR"}, "Materials not found")
|
self.report({"ERROR"}, "Materials not found")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
obj.active_material_index = next_index
|
obj.active_material_index = next_index
|
||||||
@@ -296,31 +346,26 @@ class EdgePreviewSetup(Operator):
|
|||||||
default="CREATE",
|
default="CREATE",
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
from ..core.model import FnModel
|
from ..core.model import FnModel
|
||||||
|
|
||||||
root = FnModel.find_root_object(context.active_object)
|
root = FnModel.find_root_object(context.active_object)
|
||||||
if root is None:
|
if root is None:
|
||||||
logger.error("No MMD model root found")
|
|
||||||
self.report({"ERROR"}, "Select a MMD model")
|
self.report({"ERROR"}, "Select a MMD model")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
if self.action == "CLEAN":
|
if self.action == "CLEAN":
|
||||||
logger.info(f"Cleaning toon edge for model: {root.name}")
|
|
||||||
for obj in FnModel.iterate_mesh_objects(root):
|
for obj in FnModel.iterate_mesh_objects(root):
|
||||||
self.__clean_toon_edge(obj)
|
self.__clean_toon_edge(obj)
|
||||||
else:
|
else:
|
||||||
from ..bpyutils import Props
|
from ..bpyutils import Props
|
||||||
|
|
||||||
logger.info(f"Creating toon edge for model: {root.name}")
|
|
||||||
scale = 0.2 * getattr(root, Props.empty_display_size)
|
scale = 0.2 * getattr(root, Props.empty_display_size)
|
||||||
counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root))
|
counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root))
|
||||||
logger.info(f"Created {counts} toon edge(s)")
|
|
||||||
self.report({"INFO"}, "Created %d toon edge(s)" % counts)
|
self.report({"INFO"}, "Created %d toon edge(s)" % counts)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def __clean_toon_edge(self, obj: Object) -> None:
|
def __clean_toon_edge(self, obj):
|
||||||
logger.debug(f"Cleaning toon edge for object: {obj.name}")
|
|
||||||
if "mmd_edge_preview" in obj.modifiers:
|
if "mmd_edge_preview" in obj.modifiers:
|
||||||
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
|
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
|
||||||
|
|
||||||
@@ -329,8 +374,7 @@ class EdgePreviewSetup(Operator):
|
|||||||
|
|
||||||
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
|
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
|
||||||
|
|
||||||
def __create_toon_edge(self, obj: Object, scale: float = 1.0) -> int:
|
def __create_toon_edge(self, obj, scale=1.0):
|
||||||
logger.debug(f"Creating toon edge for object: {obj.name} with scale {scale}")
|
|
||||||
self.__clean_toon_edge(obj)
|
self.__clean_toon_edge(obj)
|
||||||
materials = obj.data.materials
|
materials = obj.data.materials
|
||||||
material_offset = len(materials)
|
material_offset = len(materials)
|
||||||
@@ -355,10 +399,10 @@ class EdgePreviewSetup(Operator):
|
|||||||
mod.vertex_group = "mmd_edge_preview"
|
mod.vertex_group = "mmd_edge_preview"
|
||||||
return len(materials) - material_offset
|
return len(materials) - material_offset
|
||||||
|
|
||||||
def __create_edge_preview_group(self, obj: Object) -> None:
|
def __create_edge_preview_group(self, obj):
|
||||||
vertices, materials = obj.data.vertices, obj.data.materials
|
vertices, materials = obj.data.vertices, obj.data.materials
|
||||||
weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m}
|
weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m}
|
||||||
scale_map: Dict[int, float] = {}
|
scale_map = {}
|
||||||
vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
|
vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
|
||||||
if vg_scale_index >= 0:
|
if vg_scale_index >= 0:
|
||||||
scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index}
|
scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index}
|
||||||
@@ -367,7 +411,7 @@ class EdgePreviewSetup(Operator):
|
|||||||
weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02
|
weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02
|
||||||
vg_edge_preview.add(index=[i], weight=weight, type="REPLACE")
|
vg_edge_preview.add(index=[i], weight=weight, type="REPLACE")
|
||||||
|
|
||||||
def __get_edge_material(self, mat_name: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material:
|
def __get_edge_material(self, mat_name, edge_color, materials):
|
||||||
if mat_name in materials:
|
if mat_name in materials:
|
||||||
return materials[mat_name]
|
return materials[mat_name]
|
||||||
mat = bpy.data.materials.get(mat_name, None)
|
mat = bpy.data.materials.get(mat_name, None)
|
||||||
@@ -385,7 +429,7 @@ class EdgePreviewSetup(Operator):
|
|||||||
self.__make_shader(mat)
|
self.__make_shader(mat)
|
||||||
return mat
|
return mat
|
||||||
|
|
||||||
def __make_shader(self, m: Material) -> None:
|
def __make_shader(self, m):
|
||||||
m.use_nodes = True
|
m.use_nodes = True
|
||||||
nodes, links = m.node_tree.nodes, m.node_tree.links
|
nodes, links = m.node_tree.nodes, m.node_tree.links
|
||||||
|
|
||||||
@@ -406,7 +450,7 @@ class EdgePreviewSetup(Operator):
|
|||||||
node_shader.inputs["Color"].default_value = m.mmd_material.edge_color
|
node_shader.inputs["Color"].default_value = m.mmd_material.edge_color
|
||||||
node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3]
|
node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3]
|
||||||
|
|
||||||
def __get_edge_preview_shader(self) -> bpy.types.NodeTree:
|
def __get_edge_preview_shader(self):
|
||||||
group_name = "MMDEdgePreview"
|
group_name = "MMDEdgePreview"
|
||||||
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||||
if len(shader.nodes):
|
if len(shader.nodes):
|
||||||
@@ -414,8 +458,8 @@ class EdgePreviewSetup(Operator):
|
|||||||
|
|
||||||
ng = _NodeGroupUtils(shader)
|
ng = _NodeGroupUtils(shader)
|
||||||
|
|
||||||
node_input = ng.new_node("NodeGroupInput", (-5, 0))
|
ng.new_node("NodeGroupInput", (-5, 0))
|
||||||
node_output = ng.new_node("NodeGroupOutput", (3, 0))
|
ng.new_node("NodeGroupOutput", (3, 0))
|
||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5))
|
node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5))
|
||||||
|
|||||||
+47
-68
@@ -1,22 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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 re
|
import re
|
||||||
from typing import List, Dict, Any, Set, Optional, Tuple, Union, Type
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Context, Object, Operator, ShapeKey
|
|
||||||
|
|
||||||
from .. import utils
|
from .. import utils
|
||||||
from ..bpyutils import FnContext, FnObject
|
from ..bpyutils import FnContext, FnObject
|
||||||
from ..core.bone import FnBone
|
from ..core.bone import FnBone
|
||||||
from ..core.model import FnModel, Model
|
from ..core.model import FnModel, Model
|
||||||
from ..core.morph import FnMorph
|
from ..core.morph import FnMorph
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
class SelectObject(bpy.types.Operator):
|
class SelectObject(bpy.types.Operator):
|
||||||
@@ -32,8 +25,7 @@ class SelectObject(bpy.types.Operator):
|
|||||||
options={"HIDDEN", "SKIP_SAVE"},
|
options={"HIDDEN", "SKIP_SAVE"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
logger.debug(f"Selecting object: {self.name}")
|
|
||||||
utils.selectAObject(context.scene.objects[self.name])
|
utils.selectAObject(context.scene.objects[self.name])
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -47,43 +39,41 @@ class MoveObject(bpy.types.Operator, utils.ItemMoveOp):
|
|||||||
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
|
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_index(cls, obj: Object, index: int) -> None:
|
def set_index(cls, obj, index):
|
||||||
m = cls.__PREFIX_REGEXP.match(obj.name)
|
m = cls.__PREFIX_REGEXP.match(obj.name)
|
||||||
name = m.group("name") if m else obj.name
|
name = m.group("name") if m else obj.name
|
||||||
obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name)
|
obj.name = f"{utils.int2base(index, 36, 3)}_{name}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str:
|
def get_name(cls, obj, prefix=None):
|
||||||
m = cls.__PREFIX_REGEXP.match(obj.name)
|
m = cls.__PREFIX_REGEXP.match(obj.name)
|
||||||
name = m.group("name") if m else obj.name
|
name = m.group("name") if m else obj.name
|
||||||
return name[len(prefix) :] if prefix and name.startswith(prefix) else name
|
return name[len(prefix) :] if prefix and name.startswith(prefix) else name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def normalize_indices(cls, objects: List[Object]) -> None:
|
def normalize_indices(cls, objects):
|
||||||
for i, x in enumerate(objects):
|
for i, x in enumerate(objects):
|
||||||
cls.set_index(x, i)
|
cls.set_index(x, i)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context):
|
||||||
return context.active_object is not None
|
return context.active_object is not None
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
objects = self.__get_objects(obj)
|
objects = self.__get_objects(obj)
|
||||||
if obj not in objects:
|
if obj not in objects:
|
||||||
logger.error(f'Cannot move object "{obj.name}"')
|
|
||||||
self.report({"ERROR"}, f'Can not move object "{obj.name}"')
|
self.report({"ERROR"}, f'Can not move object "{obj.name}"')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
objects.sort(key=lambda x: x.name)
|
objects.sort(key=lambda x: x.name)
|
||||||
logger.debug(f"Moving object {obj.name} {self.type}")
|
|
||||||
self.move(objects, objects.index(obj), self.type)
|
self.move(objects, objects.index(obj), self.type)
|
||||||
self.normalize_indices(objects)
|
self.normalize_indices(objects)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def __get_objects(self, obj: Object) -> Any:
|
def __get_objects(self, obj):
|
||||||
class __MovableList(list):
|
class __MovableList(list):
|
||||||
def move(self, index_old: int, index_new: int) -> None:
|
def move(self, index_old, index_new):
|
||||||
item = self[index_old]
|
item = self[index_old]
|
||||||
self.remove(item)
|
self.remove(item)
|
||||||
self.insert(index_new, item)
|
self.insert(index_new, item)
|
||||||
@@ -108,43 +98,40 @@ class CleanShapeKeys(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context):
|
||||||
return any(o.type == "MESH" for o in context.selected_objects)
|
return any(o.type == "MESH" for o in context.selected_objects)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __can_remove(key_block: ShapeKey) -> bool:
|
def __can_remove(key_block):
|
||||||
if key_block.relative_key == key_block:
|
if key_block.relative_key == key_block:
|
||||||
return False # Basis
|
return False # Basis
|
||||||
for v0, v1 in zip(key_block.relative_key.data, key_block.data):
|
for v0, v1 in zip(key_block.relative_key.data, key_block.data, strict=False):
|
||||||
if v0.co != v1.co:
|
if v0.co != v1.co:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None:
|
def __shape_key_clean(self, obj, key_blocks):
|
||||||
for kb in key_blocks:
|
for kb in key_blocks:
|
||||||
if self.__can_remove(kb):
|
if self.__can_remove(kb):
|
||||||
logger.debug(f"Removing unused shape key: {kb.name} from {obj.name}")
|
|
||||||
FnObject.mesh_remove_shape_key(obj, kb)
|
FnObject.mesh_remove_shape_key(obj, kb)
|
||||||
if len(key_blocks) == 1:
|
if len(key_blocks) == 1:
|
||||||
logger.debug(f"Removing single shape key: {key_blocks[0].name} from {obj.name}")
|
|
||||||
FnObject.mesh_remove_shape_key(obj, key_blocks[0])
|
FnObject.mesh_remove_shape_key(obj, key_blocks[0])
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
logger.info("Cleaning shape keys for selected objects")
|
obj: bpy.types.Object
|
||||||
obj: Object
|
|
||||||
for obj in context.selected_objects:
|
for obj in context.selected_objects:
|
||||||
if obj.type != "MESH" or obj.data.shape_keys is None:
|
if obj.type != "MESH" or obj.data.shape_keys is None:
|
||||||
continue
|
continue
|
||||||
if not obj.data.shape_keys.use_relative:
|
if not obj.data.shape_keys.use_relative:
|
||||||
continue # not be considered yet
|
continue # not be considered yet
|
||||||
logger.debug(f"Processing shape keys for {obj.name}")
|
|
||||||
self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
|
self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class SeparateByMaterials(bpy.types.Operator):
|
class SeparateByMaterials(bpy.types.Operator):
|
||||||
bl_idname = "mmd_tools.separate_by_materials"
|
bl_idname = "mmd_tools.separate_by_materials"
|
||||||
bl_label = "Separate By Materials"
|
bl_label = "Sep by Mat(High Risk)"
|
||||||
|
bl_description = "Separate by Materials (High Risk)\nSeparate the mesh into multiple objects based on materials.\nHIGH RISK & BUGGY: This operation is not reversible and may cause various issues. It splits adjacent geometry by material, and merging later will not reconnect shared edges.\nKnown issues include potential mesh corruption, UV mapping problems, and other unpredictable behaviors. Use with extreme caution and backup your work first."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
clean_shape_keys: bpy.props.BoolProperty(
|
clean_shape_keys: bpy.props.BoolProperty(
|
||||||
@@ -153,26 +140,32 @@ class SeparateByMaterials(bpy.types.Operator):
|
|||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
keep_normals: bpy.props.BoolProperty(
|
||||||
def poll(cls, context: Context) -> bool:
|
name="Keep Normals",
|
||||||
obj = context.active_object
|
default=True,
|
||||||
return obj and obj.type == "MESH"
|
)
|
||||||
|
|
||||||
def __separate_by_materials(self, obj: Object) -> None:
|
@classmethod
|
||||||
logger.info(f"Separating {obj.name} by materials")
|
def poll(cls, context):
|
||||||
utils.separateByMaterials(obj)
|
obj = context.active_object
|
||||||
|
return obj is not None and obj.type == "MESH"
|
||||||
|
|
||||||
|
def __separate_by_materials(self, obj):
|
||||||
|
utils.separateByMaterials(obj, self.keep_normals)
|
||||||
if self.clean_shape_keys:
|
if self.clean_shape_keys:
|
||||||
logger.debug("Cleaning shape keys after separation")
|
|
||||||
bpy.ops.mmd_tools.clean_shape_keys()
|
bpy.ops.mmd_tools.clean_shape_keys()
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
|
|
||||||
|
# Sep by Mat crashes Blender if used after morph assembly
|
||||||
|
rig = Model(root)
|
||||||
|
rig.morph_slider.unbind()
|
||||||
|
|
||||||
if root is None:
|
if root is None:
|
||||||
logger.debug("No root object found, separating single object")
|
|
||||||
self.__separate_by_materials(obj)
|
self.__separate_by_materials(obj)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Root object found: {root.name}, preparing for separation")
|
|
||||||
bpy.ops.mmd_tools.clear_temp_materials()
|
bpy.ops.mmd_tools.clear_temp_materials()
|
||||||
bpy.ops.mmd_tools.clear_uv_morph_view()
|
bpy.ops.mmd_tools.clear_uv_morph_view()
|
||||||
|
|
||||||
@@ -185,11 +178,9 @@ class SeparateByMaterials(bpy.types.Operator):
|
|||||||
if len(mesh.data.materials) > 0:
|
if len(mesh.data.materials) > 0:
|
||||||
mat = mesh.data.materials[0]
|
mat = mesh.data.materials[0]
|
||||||
idx = mat_names.index(getattr(mat, "name", None))
|
idx = mat_names.index(getattr(mat, "name", None))
|
||||||
logger.debug(f"Setting index {idx} for mesh {mesh.name}")
|
|
||||||
MoveObject.set_index(mesh, idx)
|
MoveObject.set_index(mesh, idx)
|
||||||
|
|
||||||
for morph in root.mmd_root.material_morphs:
|
for morph in root.mmd_root.material_morphs:
|
||||||
logger.debug(f"Updating material morph: {morph.name}")
|
|
||||||
FnMorph(morph, rig).update_mat_related_mesh()
|
FnMorph(morph, rig).update_mat_related_mesh()
|
||||||
utils.clearUnusedMeshes()
|
utils.clearUnusedMeshes()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -207,15 +198,13 @@ class JoinMeshes(bpy.types.Operator):
|
|||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
if root is None:
|
if root is None:
|
||||||
logger.error("No MMD model found")
|
|
||||||
self.report({"ERROR"}, "Select a MMD model")
|
self.report({"ERROR"}, "Select a MMD model")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
logger.info(f"Joining meshes for model: {root.name}")
|
|
||||||
bpy.ops.mmd_tools.clear_temp_materials()
|
bpy.ops.mmd_tools.clear_temp_materials()
|
||||||
bpy.ops.mmd_tools.clear_uv_morph_view()
|
bpy.ops.mmd_tools.clear_uv_morph_view()
|
||||||
|
|
||||||
@@ -223,11 +212,9 @@ class JoinMeshes(bpy.types.Operator):
|
|||||||
rig = Model(root)
|
rig = Model(root)
|
||||||
meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
|
meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
|
||||||
if not meshes_list:
|
if not meshes_list:
|
||||||
logger.error("No meshes found in the model")
|
|
||||||
self.report({"ERROR"}, "The model does not have any meshes")
|
self.report({"ERROR"}, "The model does not have any meshes")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
active_mesh = meshes_list[0]
|
active_mesh = meshes_list[0]
|
||||||
logger.debug(f"Found {len(meshes_list)} meshes, using {active_mesh.name} as active")
|
|
||||||
|
|
||||||
FnContext.select_objects(context, *meshes_list)
|
FnContext.select_objects(context, *meshes_list)
|
||||||
FnContext.set_active_object(context, active_mesh)
|
FnContext.set_active_object(context, active_mesh)
|
||||||
@@ -236,19 +223,15 @@ class JoinMeshes(bpy.types.Operator):
|
|||||||
for m in meshes_list[1:]:
|
for m in meshes_list[1:]:
|
||||||
for mat in m.data.materials:
|
for mat in m.data.materials:
|
||||||
if mat not in active_mesh.data.materials[:]:
|
if mat not in active_mesh.data.materials[:]:
|
||||||
logger.debug(f"Adding material {mat.name} to active mesh")
|
|
||||||
active_mesh.data.materials.append(mat)
|
active_mesh.data.materials.append(mat)
|
||||||
|
|
||||||
# Join selected meshes
|
# Join selected meshes
|
||||||
logger.debug("Joining meshes")
|
|
||||||
bpy.ops.object.join()
|
bpy.ops.object.join()
|
||||||
|
|
||||||
if self.sort_shape_keys:
|
if self.sort_shape_keys:
|
||||||
logger.debug("Sorting shape keys")
|
|
||||||
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
|
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
|
||||||
active_mesh.active_shape_key_index = 0
|
active_mesh.active_shape_key_index = 0
|
||||||
for morph in root.mmd_root.material_morphs:
|
for morph in root.mmd_root.material_morphs:
|
||||||
logger.debug(f"Updating material morph: {morph.name}")
|
|
||||||
FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
|
FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
|
||||||
utils.clearUnusedMeshes()
|
utils.clearUnusedMeshes()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -262,20 +245,17 @@ class AttachMeshesToMMD(bpy.types.Operator):
|
|||||||
|
|
||||||
add_armature_modifier: bpy.props.BoolProperty(default=True)
|
add_armature_modifier: bpy.props.BoolProperty(default=True)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: bpy.types.Context):
|
||||||
root = FnModel.find_root_object(context.active_object)
|
root = FnModel.find_root_object(context.active_object)
|
||||||
if root is None:
|
if root is None:
|
||||||
logger.error("No MMD model found")
|
|
||||||
self.report({"ERROR"}, "Select a MMD model")
|
self.report({"ERROR"}, "Select a MMD model")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
armObj = FnModel.find_armature_object(root)
|
armObj = FnModel.find_armature_object(root)
|
||||||
if armObj is None:
|
if armObj is None:
|
||||||
logger.error("Model armature not found")
|
|
||||||
self.report({"ERROR"}, "Model Armature not found")
|
self.report({"ERROR"}, "Model Armature not found")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
logger.info(f"Attaching meshes to model: {root.name}")
|
|
||||||
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
|
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -295,18 +275,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context):
|
||||||
return FnModel.find_root_object(context.active_object) is not None
|
root = FnModel.find_root_object(context.active_object)
|
||||||
|
return root is not None
|
||||||
|
|
||||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
def invoke(self, context, event):
|
||||||
root_object = FnModel.find_root_object(context.active_object)
|
root_object = FnModel.find_root_object(context.active_object)
|
||||||
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
|
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
|
||||||
vm = context.window_manager
|
vm = context.window_manager
|
||||||
return vm.invoke_props_dialog(self)
|
return vm.invoke_props_dialog(self)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
root_object = FnModel.find_root_object(context.active_object)
|
root_object = FnModel.find_root_object(context.active_object)
|
||||||
logger.info(f"Changing IK loop factor to {self.mmd_ik_loop_factor} for model: {root_object.name}")
|
|
||||||
FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
|
FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -318,22 +298,21 @@ class RecalculateBoneRoll(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
return obj and obj.type == "ARMATURE"
|
return obj is not None and obj.type == "ARMATURE"
|
||||||
|
|
||||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
def invoke(self, context, event):
|
||||||
vm = context.window_manager
|
vm = context.window_manager
|
||||||
return vm.invoke_props_dialog(self)
|
return vm.invoke_props_dialog(self)
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
c = layout.column()
|
c = layout.column()
|
||||||
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
|
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
|
||||||
c.label(text="Click [OK] to run the operation.")
|
c.label(text="Click [OK] to run the operation.")
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
arm = context.active_object
|
arm = context.active_object
|
||||||
logger.info(f"Recalculating bone roll for armature: {arm.name}")
|
|
||||||
FnBone.apply_auto_bone_roll(arm)
|
FnBone.apply_auto_bone_roll(arm)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|||||||
+232
-131
@@ -1,32 +1,27 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2022 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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 itertools
|
import itertools
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Dict, List, Optional, Set, Tuple, Any
|
from typing import Dict, List, Optional, Set
|
||||||
|
|
||||||
import bmesh
|
import bmesh
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import numpy.typing as npt
|
from mathutils import Matrix
|
||||||
from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature
|
|
||||||
|
|
||||||
from ..bpyutils import FnContext
|
from ..bpyutils import FnContext, select_object
|
||||||
from ..core.model import FnModel, Model
|
from ..core.model import FnModel, Model
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
class MessageException(Exception):
|
class NoModelSelectedError(Exception):
|
||||||
"""Class for error with message."""
|
"""Raised when no MMD model is selected."""
|
||||||
|
|
||||||
|
|
||||||
class ModelJoinByBonesOperator(bpy.types.Operator):
|
class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||||
bl_idname = "mmd_tools.model_join_by_bones"
|
bl_idname = "mmd_tools.model_join_by_bones"
|
||||||
bl_label = "Model Join by Bones"
|
bl_label = "Model Join by Bones"
|
||||||
|
bl_description = "Join multiple MMD models into one.\n\nWARNING: To align models before joining, only adjust the root (cross under the model) transformation. Do not move armatures, meshes, rigid bodies, or joints directly as they will not move together.\n\nIMPORTANT: Don't use any of the 'Assembly' functions before using this function. This function requires the models to be in a clean state."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
join_type: bpy.props.EnumProperty(
|
join_type: bpy.props.EnumProperty(
|
||||||
@@ -39,8 +34,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: bpy.types.Context):
|
||||||
active_object: Optional[Object] = context.active_object
|
active_object: Optional[bpy.types.Object] = context.active_object
|
||||||
|
|
||||||
if context.mode != "POSE":
|
if context.mode != "POSE":
|
||||||
return False
|
return False
|
||||||
@@ -56,22 +51,19 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
return len(context.selected_pose_bones) > 0
|
return len(context.selected_pose_bones) > 0
|
||||||
|
|
||||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
def invoke(self, context, event):
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: bpy.types.Context):
|
||||||
try:
|
try:
|
||||||
logger.info("Starting model join by bones operation")
|
|
||||||
self.join(context)
|
self.join(context)
|
||||||
logger.info("Model join by bones completed successfully")
|
except NoModelSelectedError as ex:
|
||||||
except MessageException as ex:
|
|
||||||
logger.error(f"Model join by bones failed: {str(ex)}")
|
|
||||||
self.report(type={"ERROR"}, message=str(ex))
|
self.report(type={"ERROR"}, message=str(ex))
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def join(self, context: Context) -> None:
|
def join(self, context: bpy.types.Context):
|
||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
|
||||||
parent_root_object = FnModel.find_root_object(context.active_object)
|
parent_root_object = FnModel.find_root_object(context.active_object)
|
||||||
@@ -79,23 +71,35 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
|||||||
child_root_objects.remove(parent_root_object)
|
child_root_objects.remove(parent_root_object)
|
||||||
|
|
||||||
if parent_root_object is None or len(child_root_objects) == 0:
|
if parent_root_object is None or len(child_root_objects) == 0:
|
||||||
raise MessageException("No MMD Models selected")
|
raise NoModelSelectedError("No MMD Models selected")
|
||||||
|
|
||||||
logger.debug(f"Joining {len(child_root_objects)} models into parent model: {parent_root_object.name}")
|
# Save original active_layer_collection
|
||||||
with FnContext.temp_override_active_layer_collection(context, parent_root_object):
|
orig_active_layer_collection = context.view_layer.active_layer_collection
|
||||||
|
|
||||||
|
# Find layer collection containing parent_root_object and set it as active
|
||||||
|
layer_collection = FnContext.find_user_layer_collection_by_object(context, parent_root_object)
|
||||||
|
if layer_collection:
|
||||||
|
context.view_layer.active_layer_collection = layer_collection
|
||||||
|
|
||||||
|
# Execute the join operation
|
||||||
FnModel.join_models(parent_root_object, child_root_objects)
|
FnModel.join_models(parent_root_object, child_root_objects)
|
||||||
|
|
||||||
|
# Restore original active_layer_collection
|
||||||
|
context.view_layer.active_layer_collection = orig_active_layer_collection
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
parent_armature_object = FnModel.find_armature_object(parent_root_object)
|
||||||
|
FnContext.set_active_and_select_single_object(context, parent_armature_object)
|
||||||
bpy.ops.object.mode_set(mode="EDIT")
|
bpy.ops.object.mode_set(mode="EDIT")
|
||||||
bpy.ops.armature.parent_set(type="OFFSET")
|
bpy.ops.armature.parent_set(type="OFFSET")
|
||||||
|
|
||||||
# Connect child bones
|
# Connect child bones
|
||||||
if self.join_type == "CONNECTED":
|
if self.join_type == "CONNECTED":
|
||||||
parent_edit_bone: EditBone = context.active_bone
|
parent_edit_bone: bpy.types.EditBone = context.active_bone
|
||||||
child_edit_bones: Set[EditBone] = set(context.selected_bones)
|
child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
|
||||||
child_edit_bones.remove(parent_edit_bone)
|
child_edit_bones.remove(parent_edit_bone)
|
||||||
|
|
||||||
logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}")
|
child_edit_bone: bpy.types.EditBone
|
||||||
child_edit_bone: EditBone
|
|
||||||
for child_edit_bone in child_edit_bones:
|
for child_edit_bone in child_edit_bones:
|
||||||
child_edit_bone.use_connect = True
|
child_edit_bone.use_connect = True
|
||||||
|
|
||||||
@@ -105,6 +109,7 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
|||||||
class ModelSeparateByBonesOperator(bpy.types.Operator):
|
class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||||
bl_idname = "mmd_tools.model_separate_by_bones"
|
bl_idname = "mmd_tools.model_separate_by_bones"
|
||||||
bl_label = "Model Separate by Bones"
|
bl_label = "Model Separate by Bones"
|
||||||
|
bl_description = "Separate MMD model into multiple models based on selected bones.\n\nWARNING: This operation will split meshes, armatures, rigid bodies and joints. To move models before separating, only adjust the root (cross under the model) transformation. Do not move armatures, meshes, rigid bodies, or joints directly before separating as they will not move together.\n\nIMPORTANT: Don't use any of the 'Assembly' functions before using this function. This function requires the model to be in a clean state."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True)
|
separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True)
|
||||||
@@ -120,8 +125,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: bpy.types.Context):
|
||||||
active_object: Optional[Object] = context.active_object
|
active_object: Optional[bpy.types.Object] = context.active_object
|
||||||
|
|
||||||
if context.mode != "POSE":
|
if context.mode != "POSE":
|
||||||
return False
|
return False
|
||||||
@@ -137,155 +142,183 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
return len(context.selected_pose_bones) > 0
|
return len(context.selected_pose_bones) > 0
|
||||||
|
|
||||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
def invoke(self, context, event):
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: bpy.types.Context):
|
||||||
try:
|
try:
|
||||||
logger.info("Starting model separate by bones operation")
|
|
||||||
self.separate(context)
|
self.separate(context)
|
||||||
logger.info("Model separate by bones completed successfully")
|
except NoModelSelectedError as ex:
|
||||||
except MessageException as ex:
|
|
||||||
logger.error(f"Model separate by bones failed: {str(ex)}")
|
|
||||||
self.report(type={"ERROR"}, message=str(ex))
|
self.report(type={"ERROR"}, message=str(ex))
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def separate(self, context: Context) -> None:
|
def separate(self, context: bpy.types.Context):
|
||||||
weight_threshold: float = self.weight_threshold
|
weight_threshold: float = self.weight_threshold
|
||||||
mmd_scale = 0.08
|
mmd_scale = 0.08
|
||||||
|
|
||||||
target_armature_object: Object = context.active_object
|
target_armature_object: bpy.types.Object = context.active_object
|
||||||
logger.debug(f"Target armature: {target_armature_object.name}")
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode="EDIT")
|
bpy.ops.object.mode_set(mode="EDIT")
|
||||||
root_bones: Set[EditBone] = set(context.selected_bones)
|
root_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
|
||||||
logger.debug(f"Selected root bones: {len(root_bones)}")
|
|
||||||
|
|
||||||
if self.include_descendant_bones:
|
if self.include_descendant_bones:
|
||||||
logger.debug("Including descendant bones")
|
original_active_bone = context.active_bone
|
||||||
for edit_bone in root_bones:
|
for edit_bone in root_bones:
|
||||||
with context.temp_override(active_bone=edit_bone):
|
context.active_object.data.edit_bones.active = edit_bone
|
||||||
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
|
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
|
||||||
|
self._select_related_ik_bones(target_armature_object)
|
||||||
|
if original_active_bone:
|
||||||
|
context.active_object.data.edit_bones.active = original_active_bone
|
||||||
|
|
||||||
separate_bones: Dict[str, EditBone] = {b.name: b for b in context.selected_bones}
|
separate_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in context.selected_bones}
|
||||||
deform_bones: Dict[str, EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
|
deform_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
|
||||||
logger.debug(f"Total bones to separate: {len(separate_bones)}")
|
mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object)
|
||||||
|
|
||||||
mmd_root_object: Object = FnModel.find_root_object(context.active_object)
|
|
||||||
mmd_model = Model(mmd_root_object)
|
mmd_model = Model(mmd_root_object)
|
||||||
mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes())
|
mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes())
|
||||||
logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model")
|
mmd_model_mesh_objects = list(self._select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys())
|
||||||
|
|
||||||
mesh_selection_result = self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold)
|
|
||||||
mmd_model_mesh_objects = list(mesh_selection_result.keys())
|
|
||||||
logger.debug(f"Selected {len(mmd_model_mesh_objects)} mesh objects with weighted vertices")
|
|
||||||
|
|
||||||
# separate armature bones
|
|
||||||
separate_armature_object: Optional[Object]
|
|
||||||
if self.separate_armature:
|
|
||||||
logger.debug("Separating armature")
|
|
||||||
target_armature_object.select_set(True)
|
|
||||||
bpy.ops.armature.separate()
|
|
||||||
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None)
|
|
||||||
if separate_armature_object:
|
|
||||||
logger.debug(f"Created separate armature: {separate_armature_object.name}")
|
|
||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
|
||||||
# collect separate rigid bodies
|
# Store original transform matrix for root object
|
||||||
separate_rigid_bodies: Set[Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
|
original_matrix_world = mmd_root_object.matrix_world.copy()
|
||||||
logger.debug(f"Found {len(separate_rigid_bodies)} rigid bodies to separate")
|
mmd_root_object.matrix_world = Matrix.Identity(4)
|
||||||
|
|
||||||
|
# Reset object visibility
|
||||||
|
FnContext.set_active_and_select_single_object(context, mmd_root_object)
|
||||||
|
bpy.ops.mmd_tools.reset_object_visibility()
|
||||||
|
|
||||||
|
# Clean additional transform
|
||||||
|
FnContext.set_active_and_select_single_object(context, mmd_root_object)
|
||||||
|
bpy.ops.mmd_tools.clean_additional_transform()
|
||||||
|
|
||||||
|
# Create new separate model first
|
||||||
|
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, obj_name=mmd_root_object.name, add_root_bone=False)
|
||||||
|
separate_model.initialDisplayFrames()
|
||||||
|
separate_root_object = separate_model.rootObject()
|
||||||
|
separate_root_object.matrix_world = mmd_root_object.matrix_world
|
||||||
|
separate_model_armature_object = separate_model.armature()
|
||||||
|
|
||||||
|
# Now separate armature bones from original model
|
||||||
|
separate_armature_object: Optional[bpy.types.Object] = None
|
||||||
|
if self.separate_armature:
|
||||||
|
FnContext.set_active_and_select_single_object(context, target_armature_object)
|
||||||
|
bpy.ops.object.mode_set(mode="EDIT")
|
||||||
|
|
||||||
|
# Re-select the bones that should be separated (they might have been deselected)
|
||||||
|
for bone_name in separate_bones.keys():
|
||||||
|
if bone_name in target_armature_object.data.edit_bones:
|
||||||
|
target_armature_object.data.edit_bones[bone_name].select = True
|
||||||
|
|
||||||
|
bpy.ops.armature.separate()
|
||||||
|
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object and a.type == "ARMATURE"]), None)
|
||||||
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
|
||||||
|
# Collect separate rigid bodies
|
||||||
|
separate_rigid_bodies: Set[bpy.types.Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
|
||||||
|
|
||||||
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
|
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
|
||||||
|
|
||||||
# collect separate joints
|
# Collect separate joints
|
||||||
separate_joints: Set[Object] = {
|
separate_joints: Set[bpy.types.Object] = {
|
||||||
joint_object
|
joint_object
|
||||||
for joint_object in mmd_model.joints()
|
for joint_object in mmd_model.joints()
|
||||||
if boundary_joint_owner_condition(
|
if boundary_joint_owner_condition(
|
||||||
[
|
[
|
||||||
joint_object.rigid_body_constraint.object1 in separate_rigid_bodies,
|
joint_object.rigid_body_constraint.object1 in separate_rigid_bodies,
|
||||||
joint_object.rigid_body_constraint.object2 in separate_rigid_bodies,
|
joint_object.rigid_body_constraint.object2 in separate_rigid_bodies,
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
logger.debug(f"Found {len(separate_joints)} joints to separate")
|
|
||||||
|
|
||||||
separate_mesh_objects: Set[Object]
|
separate_mesh_objects: List[bpy.types.Object] = []
|
||||||
model2separate_mesh_objects: Dict[Object, Object]
|
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object] = {}
|
||||||
if len(mmd_model_mesh_objects) == 0:
|
if len(mmd_model_mesh_objects) > 0:
|
||||||
logger.debug("No mesh objects to separate")
|
# Find a single unique attribute name that doesn't conflict with any existing attributes.
|
||||||
separate_mesh_objects = set()
|
all_attribute_names = {attr.name for obj in mmd_model_mesh_objects for attr in obj.data.attributes}
|
||||||
model2separate_mesh_objects = dict()
|
temp_normal_name = "mmd_temp_normal"
|
||||||
|
i = 0
|
||||||
|
while temp_normal_name in all_attribute_names:
|
||||||
|
temp_normal_name = f"mmd_temp_normal.{i:03d}"
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Backup custom normals to the unique temporary attribute.
|
||||||
|
for mesh_obj in mmd_model_mesh_objects:
|
||||||
|
mesh_data = mesh_obj.data
|
||||||
|
existing_custom_normal = mesh_data.attributes.get("custom_normal")
|
||||||
|
if not existing_custom_normal:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if existing_custom_normal.data_type == "INT16_2D":
|
||||||
|
normals_data = np.empty(len(mesh_data.loops) * 2, dtype=np.int16)
|
||||||
|
existing_custom_normal.data.foreach_get("value", normals_data)
|
||||||
|
temp_normal_attr = mesh_data.attributes.new(temp_normal_name, "INT16_2D", "CORNER")
|
||||||
|
temp_normal_attr.data.foreach_set("value", normals_data)
|
||||||
else:
|
else:
|
||||||
# select meshes
|
raise TypeError(f"Unsupported custom_normal data type: '{existing_custom_normal.data_type}'. Supported types: 'INT16_2D'")
|
||||||
logger.debug("Selecting meshes for separation")
|
|
||||||
obj: Object
|
# Select meshes
|
||||||
|
obj: bpy.types.Object
|
||||||
for obj in context.view_layer.objects:
|
for obj in context.view_layer.objects:
|
||||||
obj.select_set(obj in mmd_model_mesh_objects)
|
obj.select_set(obj in mmd_model_mesh_objects)
|
||||||
context.view_layer.objects.active = mmd_model_mesh_objects[0]
|
context.view_layer.objects.active = mmd_model_mesh_objects[0]
|
||||||
|
|
||||||
# separate mesh by selected vertices
|
# Separate mesh by selected vertices
|
||||||
logger.debug("Separating meshes by selected vertices")
|
|
||||||
bpy.ops.object.mode_set(mode="EDIT")
|
bpy.ops.object.mode_set(mode="EDIT")
|
||||||
bpy.ops.mesh.separate(type="SELECTED")
|
bpy.ops.mesh.separate(type="SELECTED")
|
||||||
separate_mesh_objects: List[Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
|
separate_mesh_objects = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
|
||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
logger.debug(f"Created {len(separate_mesh_objects)} separate mesh objects")
|
|
||||||
|
|
||||||
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects))
|
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects, strict=False))
|
||||||
|
|
||||||
logger.debug(f"Creating new model with scale {mmd_scale}")
|
# Restore normal data for all meshes (original and separated)
|
||||||
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False)
|
all_mesh_objects = list(mmd_model_mesh_objects) + list(separate_mesh_objects)
|
||||||
|
for mesh_obj in all_mesh_objects:
|
||||||
|
mesh_data = mesh_obj.data
|
||||||
|
temp_normal_attr = mesh_data.attributes.get(temp_normal_name)
|
||||||
|
if not temp_normal_attr:
|
||||||
|
continue
|
||||||
|
|
||||||
separate_model.initialDisplayFrames()
|
try:
|
||||||
separate_root_object = separate_model.rootObject()
|
if temp_normal_attr.data_type == "INT16_2D":
|
||||||
separate_root_object.matrix_world = mmd_root_object.matrix_world
|
normals_data = np.empty(len(mesh_data.loops) * 2, dtype=np.int16)
|
||||||
separate_model_armature_object = separate_model.armature()
|
temp_normal_attr.data.foreach_get("value", normals_data)
|
||||||
logger.debug(f"Created separate model with root: {separate_root_object.name}")
|
custom_normal_attr = mesh_data.attributes.get("custom_normal")
|
||||||
|
if not custom_normal_attr:
|
||||||
|
custom_normal_attr = mesh_data.attributes.new("custom_normal", "INT16_2D", "CORNER")
|
||||||
|
custom_normal_attr.data.foreach_set("value", normals_data)
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Unsupported custom_normal data type: '{temp_normal_attr.data_type}'. Supported types: 'INT16_2D'")
|
||||||
|
finally:
|
||||||
|
mesh_data.attributes.remove(temp_normal_attr)
|
||||||
|
|
||||||
if self.separate_armature:
|
if self.separate_armature and separate_armature_object:
|
||||||
logger.debug("Joining separate armature to new model")
|
separate_armature_data = separate_armature_object.data
|
||||||
with context.temp_override(
|
with select_object(separate_model_armature_object, objects=[separate_model_armature_object, separate_armature_object]):
|
||||||
active_object=separate_model_armature_object,
|
|
||||||
selected_editable_objects=[separate_model_armature_object, separate_armature_object],
|
|
||||||
):
|
|
||||||
bpy.ops.object.join()
|
bpy.ops.object.join()
|
||||||
|
if separate_armature_data.users == 0:
|
||||||
|
bpy.data.armatures.remove(separate_armature_data)
|
||||||
|
|
||||||
# add mesh
|
if separate_mesh_objects:
|
||||||
logger.debug("Parenting separate mesh objects to new model")
|
with select_object(separate_model_armature_object, objects=[separate_model_armature_object] + separate_mesh_objects):
|
||||||
with context.temp_override(
|
|
||||||
object=separate_model_armature_object,
|
|
||||||
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
|
|
||||||
):
|
|
||||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||||
|
|
||||||
# replace mesh armature modifier.object
|
# Replace mesh armature modifier.object
|
||||||
logger.debug("Updating armature modifiers on separate meshes")
|
|
||||||
for separate_mesh in separate_mesh_objects:
|
for separate_mesh in separate_mesh_objects:
|
||||||
armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None)
|
armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None)
|
||||||
if armature_modifier is None:
|
if armature_modifier is None:
|
||||||
logger.debug(f"Creating new armature modifier for {separate_mesh.name}")
|
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_armature", "ARMATURE")
|
||||||
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
|
|
||||||
|
|
||||||
armature_modifier.object = separate_model_armature_object
|
armature_modifier.object = separate_model_armature_object
|
||||||
|
|
||||||
logger.debug("Parenting rigid bodies to new model")
|
if separate_rigid_bodies:
|
||||||
with context.temp_override(
|
with select_object(separate_model.rigidGroupObject(), objects=[separate_model.rigidGroupObject()] + list(separate_rigid_bodies)):
|
||||||
object=separate_model.rigidGroupObject(),
|
|
||||||
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
|
|
||||||
):
|
|
||||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||||
|
|
||||||
logger.debug("Parenting joints to new model")
|
if separate_joints:
|
||||||
with context.temp_override(
|
with select_object(separate_model.jointGroupObject(), objects=[separate_model.jointGroupObject()] + list(separate_joints)):
|
||||||
object=separate_model.jointGroupObject(),
|
|
||||||
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
|
|
||||||
):
|
|
||||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||||
|
|
||||||
# move separate objects to new collection
|
# Move separate objects to new collection
|
||||||
mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object)
|
mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object)
|
||||||
assert mmd_layer_collection is not None
|
assert mmd_layer_collection is not None
|
||||||
|
|
||||||
@@ -293,31 +326,42 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
|||||||
assert separate_layer_collection is not None
|
assert separate_layer_collection is not None
|
||||||
|
|
||||||
if mmd_layer_collection.name != separate_layer_collection.name:
|
if mmd_layer_collection.name != separate_layer_collection.name:
|
||||||
logger.debug(f"Moving objects from collection {mmd_layer_collection.name} to {separate_layer_collection.name}")
|
|
||||||
for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
|
for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
|
||||||
|
if separate_object.name not in separate_layer_collection.collection.objects:
|
||||||
separate_layer_collection.collection.objects.link(separate_object)
|
separate_layer_collection.collection.objects.link(separate_object)
|
||||||
|
if separate_object.name in mmd_layer_collection.collection.objects:
|
||||||
mmd_layer_collection.collection.objects.unlink(separate_object)
|
mmd_layer_collection.collection.objects.unlink(separate_object)
|
||||||
|
|
||||||
logger.debug("Copying MMD root properties")
|
|
||||||
FnModel.copy_mmd_root(
|
FnModel.copy_mmd_root(
|
||||||
separate_root_object,
|
separate_root_object,
|
||||||
mmd_root_object,
|
mmd_root_object,
|
||||||
overwrite=True,
|
overwrite=True,
|
||||||
replace_name2values={
|
replace_name2values={
|
||||||
# replace related_mesh property values
|
# Replace related_mesh property values
|
||||||
"related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()}
|
"related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def select_weighted_vertices(self, mmd_model_mesh_objects: List[Object], separate_bones: Dict[str, EditBone], deform_bones: Dict[str, EditBone], weight_threshold: float) -> Dict[Object, int]:
|
# Apply additional transform
|
||||||
"""Select vertices weighted to the bones to be separated"""
|
FnContext.set_active_and_select_single_object(context, mmd_root_object)
|
||||||
logger.debug(f"Selecting vertices weighted to {len(separate_bones)} bones with threshold {weight_threshold}")
|
bpy.ops.mmd_tools.apply_additional_transform()
|
||||||
mesh2selected_vertex_count: Dict[Object, int] = dict()
|
FnContext.set_active_and_select_single_object(context, separate_root_object)
|
||||||
|
bpy.ops.mmd_tools.apply_additional_transform()
|
||||||
|
|
||||||
|
# Restore original transform matrix for root object
|
||||||
|
mmd_root_object.matrix_world = original_matrix_world
|
||||||
|
separate_root_object.matrix_world = original_matrix_world
|
||||||
|
|
||||||
|
# End state
|
||||||
|
FnContext.set_active_and_select_single_object(context, separate_root_object)
|
||||||
|
|
||||||
|
def _select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object], separate_bones: Dict[str, bpy.types.EditBone], deform_bones: Dict[str, bpy.types.EditBone], weight_threshold: float) -> Dict[bpy.types.Object, int]:
|
||||||
|
mesh2selected_vertex_count: Dict[bpy.types.Object, int] = {}
|
||||||
target_bmesh: bmesh.types.BMesh = bmesh.new()
|
target_bmesh: bmesh.types.BMesh = bmesh.new()
|
||||||
for mesh_object in mmd_model_mesh_objects:
|
for mesh_object in mmd_model_mesh_objects:
|
||||||
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
|
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
|
||||||
|
|
||||||
mesh: Mesh = mesh_object.data
|
mesh: bpy.types.Mesh = mesh_object.data
|
||||||
target_bmesh.from_mesh(mesh, face_normals=False)
|
target_bmesh.from_mesh(mesh, face_normals=False)
|
||||||
target_bmesh.select_mode |= {"VERT"}
|
target_bmesh.select_mode |= {"VERT"}
|
||||||
deform_layer = target_bmesh.verts.layers.deform.verify()
|
deform_layer = target_bmesh.verts.layers.deform.verify()
|
||||||
@@ -344,7 +388,6 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
|||||||
vert.select_set(True)
|
vert.select_set(True)
|
||||||
|
|
||||||
if selected_vertex_count > 0:
|
if selected_vertex_count > 0:
|
||||||
logger.debug(f"Selected {selected_vertex_count} vertices in mesh {mesh_object.name}")
|
|
||||||
mesh2selected_vertex_count[mesh_object] = selected_vertex_count
|
mesh2selected_vertex_count[mesh_object] = selected_vertex_count
|
||||||
target_bmesh.select_flush_mode()
|
target_bmesh.select_flush_mode()
|
||||||
target_bmesh.to_mesh(mesh)
|
target_bmesh.to_mesh(mesh)
|
||||||
@@ -352,3 +395,61 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
|||||||
target_bmesh.clear()
|
target_bmesh.clear()
|
||||||
|
|
||||||
return mesh2selected_vertex_count
|
return mesh2selected_vertex_count
|
||||||
|
|
||||||
|
def _select_related_ik_bones(self, armature_object: bpy.types.Object) -> None:
|
||||||
|
"""
|
||||||
|
Expand the current selection to include any full IK systems that are
|
||||||
|
partially selected. An IK system includes the chain bones, the IK
|
||||||
|
target bone, and the pole target bone.
|
||||||
|
|
||||||
|
NOTE: This method operates entirely in EDIT mode and avoids mode switching
|
||||||
|
to prevent segmentation faults.
|
||||||
|
"""
|
||||||
|
edit_bones = armature_object.data.edit_bones
|
||||||
|
initial_selection_names = {b.name for b in edit_bones if b.select}
|
||||||
|
|
||||||
|
# Access pose bones constraints directly without mode switching
|
||||||
|
pose_bones = armature_object.pose.bones
|
||||||
|
|
||||||
|
# Find all complete IK systems
|
||||||
|
ik_systems = []
|
||||||
|
|
||||||
|
for pose_bone in pose_bones:
|
||||||
|
for constraint in pose_bone.constraints:
|
||||||
|
if constraint.type == "IK":
|
||||||
|
# Build the set of bones in this IK system
|
||||||
|
system_bones = {pose_bone.name}
|
||||||
|
|
||||||
|
# Add the main IK Target bone
|
||||||
|
if constraint.target and constraint.subtarget:
|
||||||
|
system_bones.add(constraint.subtarget)
|
||||||
|
|
||||||
|
# Add the Pole Target bone
|
||||||
|
if constraint.pole_target and constraint.pole_subtarget:
|
||||||
|
system_bones.add(constraint.pole_subtarget)
|
||||||
|
|
||||||
|
# Add all other bones in the IK chain
|
||||||
|
current_bone_name = pose_bone.name
|
||||||
|
chain_count = constraint.chain_count
|
||||||
|
|
||||||
|
# Walk up the parent chain
|
||||||
|
for _ in range(chain_count - 1):
|
||||||
|
if current_bone_name not in edit_bones:
|
||||||
|
break
|
||||||
|
current_bone = edit_bones[current_bone_name]
|
||||||
|
if not current_bone.parent:
|
||||||
|
break
|
||||||
|
current_bone_name = current_bone.parent.name
|
||||||
|
system_bones.add(current_bone_name)
|
||||||
|
|
||||||
|
ik_systems.append(system_bones)
|
||||||
|
|
||||||
|
# Expand selection to include any related, full IK systems
|
||||||
|
final_selection_names = set(initial_selection_names)
|
||||||
|
for system in ik_systems:
|
||||||
|
if not system.isdisjoint(initial_selection_names):
|
||||||
|
final_selection_names.update(system)
|
||||||
|
|
||||||
|
# Apply the final selection
|
||||||
|
for bone in edit_bones:
|
||||||
|
bone.select = bone.name in final_selection_names
|
||||||
|
|||||||
+399
-106
@@ -1,30 +1,26 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2015 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
|
||||||
|
|
||||||
from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union
|
from collections import namedtuple
|
||||||
|
from typing import Optional, cast
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from mathutils import Quaternion, Vector
|
from mathutils import Quaternion, Vector
|
||||||
|
|
||||||
from ..core.model import FnModel
|
|
||||||
from .. import bpyutils, utils
|
from .. import bpyutils, utils
|
||||||
from ..core.exceptions import MaterialNotFoundError
|
from ..core.exceptions import MaterialNotFoundError
|
||||||
from ..core.material import FnMaterial
|
from ..core.material import FnMaterial
|
||||||
|
from ..core.model import FnModel
|
||||||
from ..core.morph import FnMorph
|
from ..core.morph import FnMorph
|
||||||
from ..utils import ItemMoveOp, ItemOp
|
from ..utils import ItemMoveOp, ItemOp
|
||||||
from ....logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
# Util functions
|
# Util functions
|
||||||
def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
|
def divide_vector_components(vec1, vec2):
|
||||||
if len(vec1) != len(vec2):
|
if len(vec1) != len(vec2):
|
||||||
raise ValueError("Vectors should have the same number of components")
|
raise ValueError("Vectors should have the same number of components")
|
||||||
result = []
|
result = []
|
||||||
for v1, v2 in zip(vec1, vec2):
|
for v1, v2 in zip(vec1, vec2, strict=False):
|
||||||
if v2 == 0:
|
if v2 == 0:
|
||||||
if v1 == 0:
|
if v1 == 0:
|
||||||
v2 = 1 # If we have a 0/0 case we change the divisor to 1
|
v2 = 1 # If we have a 0/0 case we change the divisor to 1
|
||||||
@@ -34,17 +30,17 @@ def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def multiply_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
|
def multiply_vector_components(vec1, vec2):
|
||||||
if len(vec1) != len(vec2):
|
if len(vec1) != len(vec2):
|
||||||
raise ValueError("Vectors should have the same number of components")
|
raise ValueError("Vectors should have the same number of components")
|
||||||
result = []
|
result = []
|
||||||
for v1, v2 in zip(vec1, vec2):
|
for v1, v2 in zip(vec1, vec2, strict=False):
|
||||||
result.append(v1 * v2)
|
result.append(v1 * v2)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def special_division(n1: float, n2: float) -> float:
|
def special_division(n1, n2):
|
||||||
"""This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
|
"""Return 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
|
||||||
if n2 == 0:
|
if n2 == 0:
|
||||||
if n1 == 0:
|
if n1 == 0:
|
||||||
n2 = 1
|
n2 = 1
|
||||||
@@ -59,7 +55,7 @@ class AddMorph(bpy.types.Operator):
|
|||||||
bl_description = "Add a morph item to active morph list"
|
bl_description = "Add a morph item to active morph list"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
mmd_root = root.mmd_root
|
mmd_root = root.mmd_root
|
||||||
@@ -69,7 +65,6 @@ class AddMorph(bpy.types.Operator):
|
|||||||
morph.name = "New Morph"
|
morph.name = "New Morph"
|
||||||
if morph_type.startswith("uv"):
|
if morph_type.startswith("uv"):
|
||||||
morph.data_type = "VERTEX_GROUP"
|
morph.data_type = "VERTEX_GROUP"
|
||||||
logger.debug(f"Added new morph of type {morph_type}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +81,7 @@ class RemoveMorph(bpy.types.Operator):
|
|||||||
options={"SKIP_SAVE"},
|
options={"SKIP_SAVE"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
mmd_root = root.mmd_root
|
mmd_root = root.mmd_root
|
||||||
@@ -101,21 +96,19 @@ class RemoveMorph(bpy.types.Operator):
|
|||||||
if self.all:
|
if self.all:
|
||||||
morphs.clear()
|
morphs.clear()
|
||||||
mmd_root.active_morph = 0
|
mmd_root.active_morph = 0
|
||||||
logger.debug(f"Removed all morphs of type {morph_type}")
|
|
||||||
else:
|
else:
|
||||||
morphs.remove(mmd_root.active_morph)
|
morphs.remove(mmd_root.active_morph)
|
||||||
mmd_root.active_morph = max(0, mmd_root.active_morph - 1)
|
mmd_root.active_morph = max(0, mmd_root.active_morph - 1)
|
||||||
logger.debug(f"Removed morph at index {mmd_root.active_morph} of type {morph_type}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class MoveMorph(bpy.types.Operator, ItemMoveOp):
|
class MoveMorph(bpy.types.Operator, ItemMoveOp):
|
||||||
bl_idname = "mmd_tools.morph_move"
|
bl_idname = "mmd_tools.morph_move"
|
||||||
bl_label = "Move Morph"
|
bl_label = "Move Morph"
|
||||||
bl_description = "Move active morph item up/down in the list"
|
bl_description = "Move active morph item up/down in the list. This will not affect the morph order in exported PMX files (use Display Panel order instead)."
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
mmd_root = root.mmd_root
|
mmd_root = root.mmd_root
|
||||||
@@ -124,7 +117,6 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
|
|||||||
mmd_root.active_morph,
|
mmd_root.active_morph,
|
||||||
self.type,
|
self.type,
|
||||||
)
|
)
|
||||||
logger.debug(f"Moved morph to index {mmd_root.active_morph}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -134,7 +126,7 @@ class CopyMorph(bpy.types.Operator):
|
|||||||
bl_description = "Make a copy of active morph in the list"
|
bl_description = "Make a copy of active morph in the list"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
@@ -146,7 +138,7 @@ class CopyMorph(bpy.types.Operator):
|
|||||||
if morph is None:
|
if morph is None:
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
name_orig, name_tmp = morph.name, "_tmp%s" % str(morph.as_pointer())
|
name_orig, name_tmp = morph.name, f"_tmp{str(morph.as_pointer())}"
|
||||||
|
|
||||||
if morph_type.startswith("vertex"):
|
if morph_type.startswith("vertex"):
|
||||||
for obj in FnModel.iterate_mesh_objects(root):
|
for obj in FnModel.iterate_mesh_objects(root):
|
||||||
@@ -161,7 +153,6 @@ class CopyMorph(bpy.types.Operator):
|
|||||||
for k, v in morph.items():
|
for k, v in morph.items():
|
||||||
morph_new[k] = v if k != "name" else name_tmp
|
morph_new[k] = v if k != "name" else name_tmp
|
||||||
morph_new.name = name_orig + "_copy" # trigger name check
|
morph_new.name = name_orig + "_copy" # trigger name check
|
||||||
logger.debug(f"Copied morph {name_orig} to {morph_new.name}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -171,17 +162,14 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: bpy.types.Context) -> bool:
|
def poll(cls, context):
|
||||||
root = FnModel.find_root_object(context.active_object)
|
root = FnModel.find_root_object(context.active_object)
|
||||||
if root is None:
|
return root is not None and root.mmd_root.active_morph_type == "bone_morphs"
|
||||||
return False
|
|
||||||
|
|
||||||
return root.mmd_root.active_morph_type == "bone_morphs"
|
def execute(self, context):
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
|
||||||
root = FnModel.find_root_object(context.active_object)
|
root = FnModel.find_root_object(context.active_object)
|
||||||
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
|
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
|
||||||
logger.info("Overwrote bone morphs from active action pose")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -191,7 +179,7 @@ class AddMorphOffset(bpy.types.Operator):
|
|||||||
bl_description = "Add a morph offset item to the list"
|
bl_description = "Add a morph offset item to the list"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
mmd_root = root.mmd_root
|
mmd_root = root.mmd_root
|
||||||
@@ -216,7 +204,6 @@ class AddMorphOffset(bpy.types.Operator):
|
|||||||
item.location = pose_bone.location
|
item.location = pose_bone.location
|
||||||
item.rotation = pose_bone.rotation_quaternion
|
item.rotation = pose_bone.rotation_quaternion
|
||||||
|
|
||||||
logger.debug(f"Added morph offset to {morph_type}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -233,7 +220,7 @@ class RemoveMorphOffset(bpy.types.Operator):
|
|||||||
options={"SKIP_SAVE"},
|
options={"SKIP_SAVE"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
@@ -250,21 +237,17 @@ class RemoveMorphOffset(bpy.types.Operator):
|
|||||||
if morph_type.startswith("vertex"):
|
if morph_type.startswith("vertex"):
|
||||||
for obj in FnModel.iterate_mesh_objects(root):
|
for obj in FnModel.iterate_mesh_objects(root):
|
||||||
FnMorph.remove_shape_key(obj, morph.name)
|
FnMorph.remove_shape_key(obj, morph.name)
|
||||||
logger.debug(f"Removed all vertex morph offsets for {morph.name}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
elif morph_type.startswith("uv"):
|
if morph_type.startswith("uv"):
|
||||||
if morph.data_type == "VERTEX_GROUP":
|
if morph.data_type == "VERTEX_GROUP":
|
||||||
for obj in FnModel.iterate_mesh_objects(root):
|
for obj in FnModel.iterate_mesh_objects(root):
|
||||||
FnMorph.store_uv_morph_data(obj, morph)
|
FnMorph.store_uv_morph_data(obj, morph)
|
||||||
logger.debug(f"Removed all UV morph offsets for {morph.name}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
morph.data.clear()
|
morph.data.clear()
|
||||||
morph.active_data = 0
|
morph.active_data = 0
|
||||||
logger.debug(f"Cleared all morph offsets for {morph.name}")
|
|
||||||
else:
|
else:
|
||||||
morph.data.remove(morph.active_data)
|
morph.data.remove(morph.active_data)
|
||||||
morph.active_data = max(0, morph.active_data - 1)
|
morph.active_data = max(0, morph.active_data - 1)
|
||||||
logger.debug(f"Removed morph offset at index {morph.active_data}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -280,7 +263,7 @@ class InitMaterialOffset(bpy.types.Operator):
|
|||||||
default=0,
|
default=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
mmd_root = root.mmd_root
|
mmd_root = root.mmd_root
|
||||||
@@ -292,7 +275,6 @@ class InitMaterialOffset(bpy.types.Operator):
|
|||||||
mat_data.specular_color = mat_data.ambient_color = (val,) * 3
|
mat_data.specular_color = mat_data.ambient_color = (val,) * 3
|
||||||
mat_data.shininess = mat_data.edge_weight = val
|
mat_data.shininess = mat_data.edge_weight = val
|
||||||
mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4
|
mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4
|
||||||
logger.debug(f"Initialized material offset with value {val}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -302,7 +284,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
|||||||
bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
|
bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
mmd_root = root.mmd_root
|
mmd_root = root.mmd_root
|
||||||
@@ -340,7 +322,6 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
|||||||
|
|
||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD
|
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD
|
||||||
logger.warning("Zero division detected, switching to ADD offset type")
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.report({"ERROR"}, "An unexpected error happened")
|
self.report({"ERROR"}, "An unexpected error happened")
|
||||||
# We should stop on our tracks and re-raise the exception
|
# We should stop on our tracks and re-raise the exception
|
||||||
@@ -358,7 +339,6 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
|||||||
mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight
|
mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight
|
||||||
|
|
||||||
FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat)
|
FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat)
|
||||||
logger.info(f"Applied material offset for {mat_data.material}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -368,7 +348,7 @@ class CreateWorkMaterial(bpy.types.Operator):
|
|||||||
bl_description = "Creates a temporary material to edit this offset"
|
bl_description = "Creates a temporary material to edit this offset"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
mmd_root = root.mmd_root
|
mmd_root = root.mmd_root
|
||||||
@@ -385,12 +365,12 @@ class CreateWorkMaterial(bpy.types.Operator):
|
|||||||
|
|
||||||
base_mat = meshObj.data.materials.get(mat_data.material, None)
|
base_mat = meshObj.data.materials.get(mat_data.material, None)
|
||||||
if base_mat is None:
|
if base_mat is None:
|
||||||
self.report({"ERROR"}, 'Material "%s" not found' % mat_data.material)
|
self.report({"ERROR"}, f'Material "{mat_data.material}" not found')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
work_mat_name = base_mat.name + "_temp"
|
work_mat_name = base_mat.name + "_temp"
|
||||||
if work_mat_name in bpy.data.materials:
|
if work_mat_name in bpy.data.materials:
|
||||||
self.report({"ERROR"}, 'Temporary material "%s" is in use' % work_mat_name)
|
self.report({"ERROR"}, f'Temporary material "{work_mat_name}" is in use')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
work_mat = base_mat.copy()
|
work_mat = base_mat.copy()
|
||||||
@@ -427,7 +407,6 @@ class CreateWorkMaterial(bpy.types.Operator):
|
|||||||
work_mmd_mat.edge_color = list(edge_offset)
|
work_mmd_mat.edge_color = list(edge_offset)
|
||||||
work_mmd_mat.edge_weight += mat_data.edge_weight
|
work_mmd_mat.edge_weight += mat_data.edge_weight
|
||||||
|
|
||||||
logger.info(f"Created work material {work_mat_name}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -437,24 +416,23 @@ class ClearTempMaterials(bpy.types.Operator):
|
|||||||
bl_description = "Clears all the temporary materials"
|
bl_description = "Clears all the temporary materials"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
for meshObj in FnModel.iterate_mesh_objects(root):
|
for meshObj in FnModel.iterate_mesh_objects(root):
|
||||||
|
|
||||||
def __pre_remove(m: Optional[bpy.types.Material]) -> bool:
|
def __pre_remove(m, meshObj=meshObj):
|
||||||
if m and "_temp" in m.name:
|
if m and "_temp" in m.name:
|
||||||
base_mat_name = m.name.split("_temp")[0]
|
base_mat_name = m.name.split("_temp")[0]
|
||||||
try:
|
try:
|
||||||
FnMaterial.swap_materials(meshObj, m.name, base_mat_name)
|
FnMaterial.swap_materials(meshObj, m.name, base_mat_name)
|
||||||
return True
|
return True
|
||||||
except MaterialNotFoundError:
|
except MaterialNotFoundError:
|
||||||
self.report({"WARNING"}, "Base material for %s was not found" % m.name)
|
self.report({"WARNING"}, f"Base material for {m.name} was not found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
|
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
|
||||||
logger.info("Cleared all temporary materials")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -464,7 +442,7 @@ class ViewBoneMorph(bpy.types.Operator):
|
|||||||
bl_description = "View the result of active bone morph"
|
bl_description = "View the result of active bone morph"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
@@ -475,11 +453,10 @@ class ViewBoneMorph(bpy.types.Operator):
|
|||||||
for morph_data in morph.data:
|
for morph_data in morph.data:
|
||||||
p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None)
|
p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None)
|
||||||
if p_bone:
|
if p_bone:
|
||||||
p_bone.bone.select = True
|
p_bone.select = True
|
||||||
mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4()
|
mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4()
|
||||||
mtx.translation = p_bone.location + morph_data.location
|
mtx.translation = p_bone.location + morph_data.location
|
||||||
p_bone.matrix_basis = mtx
|
p_bone.matrix_basis = mtx
|
||||||
logger.info(f"Viewing bone morph: {morph.name}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -489,14 +466,13 @@ class ClearBoneMorphView(bpy.types.Operator):
|
|||||||
bl_description = "Reset transforms of all bones to their default values"
|
bl_description = "Reset transforms of all bones to their default values"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
armature = FnModel.find_armature_object(root)
|
armature = FnModel.find_armature_object(root)
|
||||||
for p_bone in armature.pose.bones:
|
for p_bone in armature.pose.bones:
|
||||||
p_bone.matrix_basis.identity()
|
p_bone.matrix_basis.identity()
|
||||||
logger.info("Cleared bone morph view")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -506,7 +482,7 @@ class ApplyBoneMorph(bpy.types.Operator):
|
|||||||
bl_description = "Apply current pose to active bone morph"
|
bl_description = "Apply current pose to active bone morph"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
@@ -521,10 +497,9 @@ class ApplyBoneMorph(bpy.types.Operator):
|
|||||||
item.bone = p_bone.name
|
item.bone = p_bone.name
|
||||||
item.location = p_bone.location
|
item.location = p_bone.location
|
||||||
item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
|
item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
|
||||||
p_bone.bone.select = True
|
p_bone.select = True
|
||||||
else:
|
else:
|
||||||
p_bone.bone.select = False
|
p_bone.select = False
|
||||||
logger.info(f"Applied current pose to bone morph: {morph.name}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -534,7 +509,7 @@ class SelectRelatedBone(bpy.types.Operator):
|
|||||||
bl_description = "Select the bone assigned to this offset in the armature"
|
bl_description = "Select the bone assigned to this offset in the armature"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
@@ -543,7 +518,6 @@ class SelectRelatedBone(bpy.types.Operator):
|
|||||||
morph = mmd_root.bone_morphs[mmd_root.active_morph]
|
morph = mmd_root.bone_morphs[mmd_root.active_morph]
|
||||||
morph_data = morph.data[morph.active_data]
|
morph_data = morph.data[morph.active_data]
|
||||||
utils.selectSingleBone(context, armature, morph_data.bone)
|
utils.selectSingleBone(context, armature, morph_data.bone)
|
||||||
logger.debug(f"Selected bone: {morph_data.bone}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -553,7 +527,7 @@ class EditBoneOffset(bpy.types.Operator):
|
|||||||
bl_description = "Applies the location and rotation of this offset to the bone"
|
bl_description = "Applies the location and rotation of this offset to the bone"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
@@ -566,7 +540,6 @@ class EditBoneOffset(bpy.types.Operator):
|
|||||||
mtx.translation = morph_data.location
|
mtx.translation = morph_data.location
|
||||||
p_bone.matrix_basis = mtx
|
p_bone.matrix_basis = mtx
|
||||||
utils.selectSingleBone(context, armature, p_bone.name)
|
utils.selectSingleBone(context, armature, p_bone.name)
|
||||||
logger.debug(f"Edited bone offset for {p_bone.name}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -576,7 +549,7 @@ class ApplyBoneOffset(bpy.types.Operator):
|
|||||||
bl_description = "Stores the current bone location and rotation into this offset"
|
bl_description = "Stores the current bone location and rotation into this offset"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
@@ -588,7 +561,6 @@ class ApplyBoneOffset(bpy.types.Operator):
|
|||||||
p_bone = armature.pose.bones[morph_data.bone]
|
p_bone = armature.pose.bones[morph_data.bone]
|
||||||
morph_data.location = p_bone.location
|
morph_data.location = p_bone.location
|
||||||
morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
|
morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
|
||||||
logger.debug(f"Applied bone offset for {p_bone.name}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -598,7 +570,7 @@ class ViewUVMorph(bpy.types.Operator):
|
|||||||
bl_description = "View the result of active UV morph on current mesh object"
|
bl_description = "View the result of active UV morph on current mesh object"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
@@ -616,11 +588,11 @@ class ViewUVMorph(bpy.types.Operator):
|
|||||||
|
|
||||||
selected = meshObj.select_get()
|
selected = meshObj.select_get()
|
||||||
with bpyutils.select_object(meshObj):
|
with bpyutils.select_object(meshObj):
|
||||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
mesh = cast("bpy.types.Mesh", meshObj.data)
|
||||||
morph = mmd_root.uv_morphs[mmd_root.active_morph]
|
morph = mmd_root.uv_morphs[mmd_root.active_morph]
|
||||||
uv_textures = mesh.uv_layers
|
uv_textures = mesh.uv_layers
|
||||||
|
|
||||||
base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith("_")]
|
base_uv_layers = [layer for layer in mesh.uv_layers if not layer.name.startswith("_")]
|
||||||
if morph.uv_index >= len(base_uv_layers):
|
if morph.uv_index >= len(base_uv_layers):
|
||||||
self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index)
|
self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index)
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
@@ -630,7 +602,7 @@ class ViewUVMorph(bpy.types.Operator):
|
|||||||
uv_textures.active = uv_textures[uv_layer_name]
|
uv_textures.active = uv_textures[uv_layer_name]
|
||||||
|
|
||||||
uv_layer_name = uv_textures.active.name
|
uv_layer_name = uv_textures.active.name
|
||||||
uv_tex = uv_textures.new(name="__uv.%s" % uv_layer_name)
|
uv_tex = uv_textures.new(name=f"__uv.{uv_layer_name}")
|
||||||
if uv_tex is None:
|
if uv_tex is None:
|
||||||
self.report({"ERROR"}, "Failed to create a temporary uv layer")
|
self.report({"ERROR"}, "Failed to create a temporary uv layer")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
@@ -640,16 +612,15 @@ class ViewUVMorph(bpy.types.Operator):
|
|||||||
if len(offsets) > 0:
|
if len(offsets) > 0:
|
||||||
base_uv_data = mesh.uv_layers.active.data
|
base_uv_data = mesh.uv_layers.active.data
|
||||||
temp_uv_data = mesh.uv_layers[uv_tex.name].data
|
temp_uv_data = mesh.uv_layers[uv_tex.name].data
|
||||||
for i, l in enumerate(mesh.loops):
|
for i, loop in enumerate(mesh.loops):
|
||||||
select = temp_uv_data[i].select = l.vertex_index in offsets
|
select = temp_uv_data[i].select = loop.vertex_index in offsets
|
||||||
if select:
|
if select:
|
||||||
temp_uv_data[i].uv = base_uv_data[i].uv + offsets[l.vertex_index]
|
temp_uv_data[i].uv = base_uv_data[i].uv + offsets[loop.vertex_index]
|
||||||
|
|
||||||
uv_textures.active = uv_tex
|
uv_textures.active = uv_tex
|
||||||
uv_tex.active_render = True
|
uv_tex.active_render = True
|
||||||
meshObj.hide_set(False)
|
meshObj.hide_set(False)
|
||||||
meshObj.select_set(selected)
|
meshObj.select_set(selected)
|
||||||
logger.info(f"Viewing UV morph: {morph.name}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -659,14 +630,14 @@ class ClearUVMorphView(bpy.types.Operator):
|
|||||||
bl_description = "Clear all temporary data of UV morphs"
|
bl_description = "Clear all temporary data of UV morphs"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
assert root is not None
|
assert root is not None
|
||||||
for m in FnModel.iterate_mesh_objects(root):
|
for m in FnModel.iterate_mesh_objects(root):
|
||||||
mesh = m.data
|
mesh = m.data
|
||||||
uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers)
|
uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers)
|
||||||
for t in uv_textures:
|
for t in reversed(uv_textures):
|
||||||
if t.name.startswith("__uv."):
|
if t.name.startswith("__uv."):
|
||||||
uv_textures.remove(t)
|
uv_textures.remove(t)
|
||||||
if len(uv_textures) > 0:
|
if len(uv_textures) > 0:
|
||||||
@@ -676,7 +647,7 @@ class ClearUVMorphView(bpy.types.Operator):
|
|||||||
animation_data = mesh.animation_data
|
animation_data = mesh.animation_data
|
||||||
if animation_data:
|
if animation_data:
|
||||||
nla_tracks = animation_data.nla_tracks
|
nla_tracks = animation_data.nla_tracks
|
||||||
for t in nla_tracks:
|
for t in reversed(nla_tracks):
|
||||||
if t.name.startswith("__uv."):
|
if t.name.startswith("__uv."):
|
||||||
nla_tracks.remove(t)
|
nla_tracks.remove(t)
|
||||||
if animation_data.action and animation_data.action.name.startswith("__uv."):
|
if animation_data.action and animation_data.action.name.startswith("__uv."):
|
||||||
@@ -684,10 +655,9 @@ class ClearUVMorphView(bpy.types.Operator):
|
|||||||
if animation_data.action is None and len(nla_tracks) == 0:
|
if animation_data.action is None and len(nla_tracks) == 0:
|
||||||
mesh.animation_data_clear()
|
mesh.animation_data_clear()
|
||||||
|
|
||||||
for act in bpy.data.actions:
|
for act in reversed(bpy.data.actions):
|
||||||
if act.name.startswith("__uv.") and act.users < 1:
|
if act.name.startswith("__uv.") and act.users < 1:
|
||||||
bpy.data.actions.remove(act)
|
bpy.data.actions.remove(act)
|
||||||
logger.info("Cleared UV morph view")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -698,20 +668,20 @@ class EditUVMorph(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: bpy.types.Context) -> bool:
|
def poll(cls, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
if obj.type != "MESH":
|
if obj is None or obj.type != "MESH":
|
||||||
return False
|
return False
|
||||||
active_uv_layer = obj.data.uv_layers.active
|
active_uv_layer = obj.data.uv_layers.active
|
||||||
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.")
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
meshObj = obj
|
meshObj = obj
|
||||||
|
|
||||||
selected = meshObj.select_get()
|
selected = meshObj.select_get()
|
||||||
with bpyutils.select_object(meshObj):
|
with bpyutils.select_object(meshObj):
|
||||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
mesh = cast("bpy.types.Mesh", meshObj.data)
|
||||||
bpy.ops.object.mode_set(mode="EDIT")
|
bpy.ops.object.mode_set(mode="EDIT")
|
||||||
bpy.ops.mesh.select_mode(type="VERT", action="ENABLE")
|
bpy.ops.mesh.select_mode(type="VERT", action="ENABLE")
|
||||||
bpy.ops.mesh.reveal() # unhide all vertices
|
bpy.ops.mesh.reveal() # unhide all vertices
|
||||||
@@ -719,16 +689,15 @@ class EditUVMorph(bpy.types.Operator):
|
|||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
|
||||||
vertices = mesh.vertices
|
vertices = mesh.vertices
|
||||||
for l, d in zip(mesh.loops, mesh.uv_layers.active.data):
|
for loop, d in zip(mesh.loops, mesh.uv_layers.active.data, strict=False):
|
||||||
if d.select:
|
if d.select:
|
||||||
vertices[l.vertex_index].select = True
|
vertices[loop.vertex_index].select = True
|
||||||
|
|
||||||
polygons = mesh.polygons
|
polygons = mesh.polygons
|
||||||
polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active)
|
polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active)
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode="EDIT")
|
bpy.ops.object.mode_set(mode="EDIT")
|
||||||
meshObj.select_set(selected)
|
meshObj.select_set(selected)
|
||||||
logger.info("Editing UV morph")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -739,14 +708,14 @@ class ApplyUVMorph(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: bpy.types.Context) -> bool:
|
def poll(cls, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
if obj.type != "MESH":
|
if obj is None or obj.type != "MESH":
|
||||||
return False
|
return False
|
||||||
active_uv_layer = obj.data.uv_layers.active
|
active_uv_layer = obj.data.uv_layers.active
|
||||||
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.")
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
mmd_root = root.mmd_root
|
mmd_root = root.mmd_root
|
||||||
@@ -754,34 +723,31 @@ class ApplyUVMorph(bpy.types.Operator):
|
|||||||
|
|
||||||
selected = meshObj.select_get()
|
selected = meshObj.select_get()
|
||||||
with bpyutils.select_object(meshObj):
|
with bpyutils.select_object(meshObj):
|
||||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
mesh = cast("bpy.types.Mesh", meshObj.data)
|
||||||
morph = mmd_root.uv_morphs[mmd_root.active_morph]
|
morph = mmd_root.uv_morphs[mmd_root.active_morph]
|
||||||
|
|
||||||
base_uv_name = mesh.uv_layers.active.name[5:]
|
base_uv_name = mesh.uv_layers.active.name[5:]
|
||||||
if base_uv_name not in mesh.uv_layers:
|
if base_uv_name not in mesh.uv_layers:
|
||||||
self.report({"ERROR"}, ' * UV map "%s" not found' % base_uv_name)
|
self.report({"ERROR"}, f' * UV map "{base_uv_name}" not found')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
base_uv_data = mesh.uv_layers[base_uv_name].data
|
base_uv_data = mesh.uv_layers[base_uv_name].data
|
||||||
temp_uv_data = mesh.uv_layers.active.data
|
temp_uv_data = mesh.uv_layers.active.data
|
||||||
axis_type = "ZW" if base_uv_name.startswith("_") else "XY"
|
axis_type = "ZW" if base_uv_name.startswith("_") else "XY"
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
__OffsetData = namedtuple("OffsetData", "index, offset")
|
__OffsetData = namedtuple("OffsetData", "index, offset")
|
||||||
offsets = {}
|
offsets = {}
|
||||||
vertices = mesh.vertices
|
vertices = mesh.vertices
|
||||||
for l, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data):
|
for loop, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data, strict=False):
|
||||||
if vertices[l.vertex_index].select and l.vertex_index not in offsets:
|
if vertices[loop.vertex_index].select and loop.vertex_index not in offsets:
|
||||||
dx, dy = i1.uv - i0.uv
|
dx, dy = i1.uv - i0.uv
|
||||||
if abs(dx) > 0.0001 or abs(dy) > 0.0001:
|
if abs(dx) > 0.0001 or abs(dy) > 0.0001:
|
||||||
offsets[l.vertex_index] = __OffsetData(l.vertex_index, (dx, dy, dx, dy))
|
offsets[loop.vertex_index] = __OffsetData(loop.vertex_index, (dx, dy, dx, dy))
|
||||||
|
|
||||||
FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type)
|
FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type)
|
||||||
morph.data_type = "VERTEX_GROUP"
|
morph.data_type = "VERTEX_GROUP"
|
||||||
|
|
||||||
meshObj.select_set(selected)
|
meshObj.select_set(selected)
|
||||||
logger.info(f"Applied UV morph: {morph.name}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -792,12 +758,339 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: bpy.types.Context) -> bool:
|
def poll(cls, context):
|
||||||
return FnModel.find_root_object(context.active_object) is not None
|
root = FnModel.find_root_object(context.active_object)
|
||||||
|
return root is not None
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context: bpy.types.Context):
|
||||||
mmd_root_object = FnModel.find_root_object(context.active_object)
|
mmd_root_object = FnModel.find_root_object(context.active_object)
|
||||||
FnMorph.clean_duplicated_material_morphs(mmd_root_object)
|
FnMorph.clean_duplicated_material_morphs(mmd_root_object)
|
||||||
logger.info("Cleaned duplicated material morphs")
|
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertBoneMorphToVertexMorph(bpy.types.Operator):
|
||||||
|
bl_idname = "mmd_tools.convert_bone_morph_to_vertex_morph"
|
||||||
|
bl_label = "Convert To Vertex Morph"
|
||||||
|
bl_description = "Convert a bone morph into a single vertex morph by applying the bone transformations.\nIf a corresponding vertex morph already exists, it will be updated."
|
||||||
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
root = FnModel.find_root_object(context.active_object)
|
||||||
|
if root is None:
|
||||||
|
return False
|
||||||
|
mmd_root = root.mmd_root
|
||||||
|
if mmd_root.active_morph_type != "bone_morphs":
|
||||||
|
return False
|
||||||
|
morph = ItemOp.get_by_index(mmd_root.bone_morphs, mmd_root.active_morph)
|
||||||
|
return morph is not None and len(morph.data) > 0
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
obj = context.active_object
|
||||||
|
root = FnModel.find_root_object(obj)
|
||||||
|
mmd_root = root.mmd_root
|
||||||
|
|
||||||
|
# Get the active bone morph
|
||||||
|
bone_morph = ItemOp.get_by_index(mmd_root.bone_morphs, mmd_root.active_morph)
|
||||||
|
if bone_morph is None:
|
||||||
|
self.report({"ERROR"}, "No active bone morph")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
original_name = bone_morph.name
|
||||||
|
target_name = original_name
|
||||||
|
|
||||||
|
# Add 'B' suffix if necessary
|
||||||
|
if not original_name.endswith("B"):
|
||||||
|
bone_morph.name = original_name + "B"
|
||||||
|
target_name = original_name
|
||||||
|
else:
|
||||||
|
# If already has B suffix, use name without B
|
||||||
|
target_name = original_name[:-1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 1: import
|
||||||
|
from ..core.model import Model
|
||||||
|
|
||||||
|
rig = Model(root)
|
||||||
|
|
||||||
|
# Ensure morph slider is bound
|
||||||
|
bpy.ops.mmd_tools.morph_slider_setup(type="BIND")
|
||||||
|
|
||||||
|
# Re-obtain placeholder object
|
||||||
|
placeholder_obj = rig.morph_slider.placeholder()
|
||||||
|
if placeholder_obj is None or placeholder_obj.data.shape_keys is None:
|
||||||
|
self.report({"ERROR"}, "Failed to create morph slider system")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
shape_keys = placeholder_obj.data.shape_keys
|
||||||
|
key_blocks = shape_keys.key_blocks
|
||||||
|
|
||||||
|
# Step 2: Check if target bone morph exists
|
||||||
|
current_morph_name = bone_morph.name
|
||||||
|
if current_morph_name not in key_blocks:
|
||||||
|
self.report({"ERROR"}, f"Bone morph '{current_morph_name}' not found in morph sliders")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
# Step 3: Save all current morph values
|
||||||
|
original_values = {}
|
||||||
|
for key_block in key_blocks:
|
||||||
|
if key_block.name != "--- morph sliders ---":
|
||||||
|
original_values[key_block.name] = key_block.value
|
||||||
|
|
||||||
|
# Step 4: Set all morphs to 0
|
||||||
|
for key_block in key_blocks:
|
||||||
|
if key_block.name != "--- morph sliders ---":
|
||||||
|
key_block.value = 0
|
||||||
|
|
||||||
|
# Step 5: Set target bone morph to 1.0
|
||||||
|
key_blocks[current_morph_name].value = 1.0
|
||||||
|
|
||||||
|
# Step 6: Use Armature Modifier's "Apply as Shape Key" functionality
|
||||||
|
created_shape_keys = []
|
||||||
|
for mesh_obj in FnModel.iterate_mesh_objects(root):
|
||||||
|
# Switch to this mesh object
|
||||||
|
context.view_layer.objects.active = mesh_obj
|
||||||
|
|
||||||
|
# Ensure mesh object has shape keys
|
||||||
|
if mesh_obj.data.shape_keys is None:
|
||||||
|
mesh_obj.shape_key_add(name="Basis", from_mix=False)
|
||||||
|
|
||||||
|
# Delete existing shape key with same name
|
||||||
|
if target_name in mesh_obj.data.shape_keys.key_blocks:
|
||||||
|
idx = mesh_obj.data.shape_keys.key_blocks.find(target_name)
|
||||||
|
if idx >= 0:
|
||||||
|
mesh_obj.active_shape_key_index = idx
|
||||||
|
bpy.ops.object.shape_key_remove()
|
||||||
|
|
||||||
|
# Find armature modifier
|
||||||
|
armature_modifier = None
|
||||||
|
for modifier in mesh_obj.modifiers:
|
||||||
|
if modifier.type == "ARMATURE":
|
||||||
|
armature_modifier = modifier
|
||||||
|
break
|
||||||
|
|
||||||
|
if armature_modifier is None:
|
||||||
|
self.report({"WARNING"}, f"No armature modifier found on mesh '{mesh_obj.name}'")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Use Apply as Shape Key functionality, keeping the modifier
|
||||||
|
bpy.ops.object.modifier_apply_as_shapekey(modifier=armature_modifier.name, keep_modifier=True)
|
||||||
|
|
||||||
|
# Rename the newly created shape key to target name
|
||||||
|
shape_key_blocks = mesh_obj.data.shape_keys.key_blocks
|
||||||
|
new_shape_key = shape_key_blocks[-1] # Latest created shape key
|
||||||
|
new_shape_key.name = target_name
|
||||||
|
new_shape_key.value = 0.0 # Set to 0 to avoid double effect
|
||||||
|
|
||||||
|
created_shape_keys.append((mesh_obj.name, target_name))
|
||||||
|
self.report({"INFO"}, f"Created shape key '{target_name}' on mesh '{mesh_obj.name}'")
|
||||||
|
|
||||||
|
# Step 7: Restore all original morph values
|
||||||
|
for key_name, original_value in original_values.items():
|
||||||
|
if key_name in key_blocks:
|
||||||
|
key_blocks[key_name].value = original_value
|
||||||
|
|
||||||
|
# Step 8: Create or update vertex morph entry
|
||||||
|
vertex_morph_exists = False
|
||||||
|
for i, morph in enumerate(mmd_root.vertex_morphs):
|
||||||
|
if morph.name == target_name:
|
||||||
|
vertex_morph_exists = True
|
||||||
|
mmd_root.active_morph_type = "vertex_morphs"
|
||||||
|
mmd_root.active_morph = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if not vertex_morph_exists:
|
||||||
|
mmd_root.active_morph_type = "vertex_morphs"
|
||||||
|
morph, mmd_root.active_morph = ItemOp.add_after(mmd_root.vertex_morphs, mmd_root.active_morph)
|
||||||
|
morph.name = target_name
|
||||||
|
|
||||||
|
# Step 9: Add to facial expression display frame
|
||||||
|
facial_frame = None
|
||||||
|
for frame in mmd_root.display_item_frames:
|
||||||
|
if frame.name == "表情":
|
||||||
|
facial_frame = frame
|
||||||
|
break
|
||||||
|
|
||||||
|
if facial_frame:
|
||||||
|
morph_exists_in_frame = False
|
||||||
|
for item in facial_frame.data:
|
||||||
|
if item.type == "MORPH" and item.name == target_name and item.morph_type == "vertex_morphs":
|
||||||
|
morph_exists_in_frame = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not morph_exists_in_frame:
|
||||||
|
new_item = facial_frame.data.add()
|
||||||
|
new_item.type = "MORPH"
|
||||||
|
new_item.morph_type = "vertex_morphs"
|
||||||
|
new_item.name = target_name
|
||||||
|
|
||||||
|
facial_frame.active_item = len(facial_frame.data) - 1
|
||||||
|
|
||||||
|
for i, frame in enumerate(mmd_root.display_item_frames):
|
||||||
|
if frame.name == "表情":
|
||||||
|
mmd_root.active_display_item_frame = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# UNBIND
|
||||||
|
bpy.ops.mmd_tools.morph_slider_setup(type="UNBIND")
|
||||||
|
|
||||||
|
# Success message
|
||||||
|
shape_key_info = ", ".join([f"{mesh}:{key}" for mesh, key in created_shape_keys])
|
||||||
|
self.report({"INFO"}, f"Successfully converted bone morph '{original_name}' to vertex morph '{target_name}'. Created shape keys: {shape_key_info}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.report({"ERROR"}, f"Error during conversion: {str(e)}")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertGroupMorphToVertexMorph(bpy.types.Operator):
|
||||||
|
bl_idname = "mmd_tools.convert_group_morph_to_vertex_morph"
|
||||||
|
bl_label = "Convert To Vertex Morph"
|
||||||
|
bl_description = "Convert a group morph into a single vertex morph by merging only the vertex morphs within the group.\nIf a corresponding vertex morph already exists, it will be updated."
|
||||||
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
root = FnModel.find_root_object(context.active_object)
|
||||||
|
if root is None:
|
||||||
|
return False
|
||||||
|
mmd_root = root.mmd_root
|
||||||
|
if mmd_root.active_morph_type != "group_morphs":
|
||||||
|
return False
|
||||||
|
morph = ItemOp.get_by_index(mmd_root.group_morphs, mmd_root.active_morph)
|
||||||
|
return morph is not None and len(morph.data) > 0
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
bpy.ops.mmd_tools.morph_slider_setup(type="UNBIND")
|
||||||
|
|
||||||
|
obj = context.active_object
|
||||||
|
root = FnModel.find_root_object(obj)
|
||||||
|
mmd_root = root.mmd_root
|
||||||
|
|
||||||
|
# Get the active group morph
|
||||||
|
group_morph = ItemOp.get_by_index(mmd_root.group_morphs, mmd_root.active_morph)
|
||||||
|
if group_morph is None:
|
||||||
|
self.report({"ERROR"}, "No active group morph")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
# Check if the group morph contains any vertex morphs to convert
|
||||||
|
has_vertex_morphs = False
|
||||||
|
for offset in group_morph.data:
|
||||||
|
if offset.morph_type == "vertex_morphs":
|
||||||
|
has_vertex_morphs = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not has_vertex_morphs:
|
||||||
|
self.report({"ERROR"}, "The group morph does not contain any vertex morphs to convert")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
original_name = group_morph.name
|
||||||
|
target_name = original_name
|
||||||
|
|
||||||
|
# Add 'G' suffix if necessary
|
||||||
|
if not original_name.endswith("G"):
|
||||||
|
group_morph.name = original_name + "G"
|
||||||
|
target_name = original_name
|
||||||
|
else:
|
||||||
|
# If already has G suffix, use name without G
|
||||||
|
target_name = original_name[:-1]
|
||||||
|
|
||||||
|
# First, reset all shape keys to zero
|
||||||
|
for obj in FnModel.iterate_mesh_objects(root):
|
||||||
|
if obj.data.shape_keys:
|
||||||
|
for kb in obj.data.shape_keys.key_blocks:
|
||||||
|
kb.value = 0
|
||||||
|
|
||||||
|
# Apply only the vertex morphs from the group morph
|
||||||
|
for offset in group_morph.data:
|
||||||
|
if offset.morph_type == "vertex_morphs":
|
||||||
|
# Find the vertex morph by name
|
||||||
|
vertex_morph = getattr(root.mmd_root, offset.morph_type).get(offset.name)
|
||||||
|
if vertex_morph:
|
||||||
|
# Apply this morph at the specified factor
|
||||||
|
for obj in FnModel.iterate_mesh_objects(root):
|
||||||
|
if obj.data.shape_keys:
|
||||||
|
kb = obj.data.shape_keys.key_blocks.get(offset.name)
|
||||||
|
if kb:
|
||||||
|
kb.value = offset.factor
|
||||||
|
|
||||||
|
# Now add a new shape key from mix for each mesh
|
||||||
|
for obj in FnModel.iterate_mesh_objects(root):
|
||||||
|
if obj.data.shape_keys:
|
||||||
|
# Make this the active object
|
||||||
|
context.view_layer.objects.active = obj
|
||||||
|
|
||||||
|
# Remove existing shape key if it exists
|
||||||
|
if target_name in obj.data.shape_keys.key_blocks:
|
||||||
|
idx = obj.data.shape_keys.key_blocks.find(target_name)
|
||||||
|
if idx >= 0:
|
||||||
|
obj.active_shape_key_index = idx
|
||||||
|
bpy.ops.object.shape_key_remove()
|
||||||
|
|
||||||
|
# Add shape key from mix
|
||||||
|
bpy.ops.object.shape_key_add(from_mix=True)
|
||||||
|
|
||||||
|
# Rename the newly created shape key
|
||||||
|
new_key = obj.data.shape_keys.key_blocks[-1]
|
||||||
|
new_key.name = target_name
|
||||||
|
|
||||||
|
# Check if a vertex morph with the target name already exists
|
||||||
|
vertex_morph_exists = False
|
||||||
|
for i, morph in enumerate(mmd_root.vertex_morphs):
|
||||||
|
if morph.name == target_name:
|
||||||
|
vertex_morph_exists = True
|
||||||
|
mmd_root.active_morph_type = "vertex_morphs"
|
||||||
|
mmd_root.active_morph = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# If not, create a new vertex morph
|
||||||
|
if not vertex_morph_exists:
|
||||||
|
# Switch to vertex morphs panel
|
||||||
|
mmd_root.active_morph_type = "vertex_morphs"
|
||||||
|
|
||||||
|
# Add new vertex morph
|
||||||
|
morph, mmd_root.active_morph = ItemOp.add_after(mmd_root.vertex_morphs, mmd_root.active_morph)
|
||||||
|
morph.name = target_name
|
||||||
|
|
||||||
|
# Add the new vertex morph to the facial display frame
|
||||||
|
facial_frame = None
|
||||||
|
for frame in mmd_root.display_item_frames:
|
||||||
|
if frame.name == "表情": # This is the facial display frame
|
||||||
|
facial_frame = frame
|
||||||
|
break
|
||||||
|
|
||||||
|
if facial_frame:
|
||||||
|
# Check if this morph is already in the facial frame
|
||||||
|
morph_exists_in_frame = False
|
||||||
|
for item in facial_frame.data:
|
||||||
|
if item.type == "MORPH" and item.name == target_name and item.morph_type == "vertex_morphs":
|
||||||
|
morph_exists_in_frame = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# If not, add it
|
||||||
|
if not morph_exists_in_frame:
|
||||||
|
new_item = facial_frame.data.add()
|
||||||
|
new_item.type = "MORPH"
|
||||||
|
new_item.morph_type = "vertex_morphs"
|
||||||
|
new_item.name = target_name
|
||||||
|
|
||||||
|
# Make this the active item in the facial frame
|
||||||
|
facial_frame.active_item = len(facial_frame.data) - 1
|
||||||
|
|
||||||
|
# Set the facial frame as active
|
||||||
|
for i, frame in enumerate(mmd_root.display_item_frames):
|
||||||
|
if frame.name == "表情":
|
||||||
|
mmd_root.active_display_item_frame = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Reset all shape keys
|
||||||
|
for obj in FnModel.iterate_mesh_objects(root):
|
||||||
|
if obj.data.shape_keys:
|
||||||
|
for kb in obj.data.shape_keys.key_blocks:
|
||||||
|
kb.value = 0
|
||||||
|
|
||||||
|
self.report({"INFO"}, f"Successfully converted vertex morphs in group to vertex morph '{target_name}' and added to facial display frame")
|
||||||
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2015 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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 math
|
import math
|
||||||
from typing import Dict, Optional, Tuple, cast, Set, List, Any, Union, Generator
|
from typing import Dict, Optional, Tuple, cast
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from mathutils import Euler, Vector
|
from mathutils import Euler, Vector
|
||||||
@@ -16,7 +12,6 @@ from ..bpyutils import FnContext, Props
|
|||||||
from ..core import rigid_body
|
from ..core import rigid_body
|
||||||
from ..core.model import FnModel, Model
|
from ..core.model import FnModel, Model
|
||||||
from ..core.rigid_body import FnRigidBody
|
from ..core.rigid_body import FnRigidBody
|
||||||
from ...logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
class SelectRigidBody(bpy.types.Operator):
|
class SelectRigidBody(bpy.types.Operator):
|
||||||
@@ -44,15 +39,15 @@ class SelectRigidBody(bpy.types.Operator):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
def invoke(self, context, event):
|
||||||
vm = context.window_manager
|
vm = context.window_manager
|
||||||
return vm.invoke_props_dialog(self)
|
return vm.invoke_props_dialog(self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: bpy.types.Context) -> bool:
|
def poll(cls, context):
|
||||||
return FnModel.is_rigid_body_object(context.active_object)
|
return FnModel.is_rigid_body_object(context.active_object)
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
if root is None:
|
if root is None:
|
||||||
@@ -174,7 +169,7 @@ class AddRigidBody(bpy.types.Operator):
|
|||||||
default=0.1,
|
default=0.1,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None) -> bpy.types.Object:
|
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None):
|
||||||
name_j: str = self.name_j
|
name_j: str = self.name_j
|
||||||
name_e: str = self.name_e
|
name_e: str = self.name_e
|
||||||
size = self.size.copy()
|
size = self.size.copy()
|
||||||
@@ -227,7 +222,7 @@ class AddRigidBody(bpy.types.Operator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: bpy.types.Context) -> bool:
|
def poll(cls, context):
|
||||||
root_object = FnModel.find_root_object(context.active_object)
|
root_object = FnModel.find_root_object(context.active_object)
|
||||||
if root_object is None:
|
if root_object is None:
|
||||||
return False
|
return False
|
||||||
@@ -238,11 +233,11 @@ class AddRigidBody(bpy.types.Operator):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
active_object = context.active_object
|
active_object = context.active_object
|
||||||
|
|
||||||
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
|
root_object = cast("bpy.types.Object", FnModel.find_root_object(active_object))
|
||||||
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object))
|
armature_object = cast("bpy.types.Object", FnModel.find_armature_object(root_object))
|
||||||
|
|
||||||
if active_object != armature_object:
|
if active_object != armature_object:
|
||||||
FnContext.select_single_object(context, root_object).select_set(False)
|
FnContext.select_single_object(context, root_object).select_set(False)
|
||||||
@@ -255,17 +250,15 @@ class AddRigidBody(bpy.types.Operator):
|
|||||||
|
|
||||||
armature_object.select_set(False)
|
armature_object.select_set(False)
|
||||||
if len(selected_pose_bones) > 0:
|
if len(selected_pose_bones) > 0:
|
||||||
logger.info(f"Adding rigid bodies to {len(selected_pose_bones)} selected bones")
|
|
||||||
for pose_bone in selected_pose_bones:
|
for pose_bone in selected_pose_bones:
|
||||||
rigid = self.__add_rigid_body(context, root_object, pose_bone)
|
rigid = self.__add_rigid_body(context, root_object, pose_bone)
|
||||||
rigid.select_set(True)
|
rigid.select_set(True)
|
||||||
else:
|
else:
|
||||||
logger.info("Adding a single rigid body without bone attachment")
|
|
||||||
rigid = self.__add_rigid_body(context, root_object)
|
rigid = self.__add_rigid_body(context, root_object)
|
||||||
rigid.select_set(True)
|
rigid.select_set(True)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
def invoke(self, context, event):
|
||||||
no_bone = True
|
no_bone = True
|
||||||
if context.selected_bones and len(context.selected_bones) > 0:
|
if context.selected_bones and len(context.selected_bones) > 0:
|
||||||
no_bone = False
|
no_bone = False
|
||||||
@@ -291,13 +284,12 @@ class RemoveRigidBody(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: bpy.types.Context) -> bool:
|
def poll(cls, context):
|
||||||
return FnModel.is_rigid_body_object(context.active_object)
|
return FnModel.is_rigid_body_object(context.active_object)
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
logger.info(f"Removing rigid body: {obj.name}")
|
|
||||||
utils.selectAObject(obj) # ensure this is the only one object select
|
utils.selectAObject(obj) # ensure this is the only one object select
|
||||||
bpy.ops.object.delete(use_global=True)
|
bpy.ops.object.delete(use_global=True)
|
||||||
if root:
|
if root:
|
||||||
@@ -310,8 +302,7 @@ class RigidBodyBake(bpy.types.Operator):
|
|||||||
bl_label = "Bake"
|
bl_label = "Bake"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context: bpy.types.Context):
|
||||||
logger.info("Baking rigid body simulation")
|
|
||||||
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
|
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
|
||||||
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
|
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
|
||||||
|
|
||||||
@@ -323,8 +314,7 @@ class RigidBodyDeleteBake(bpy.types.Operator):
|
|||||||
bl_label = "Delete Bake"
|
bl_label = "Delete Bake"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context: bpy.types.Context):
|
||||||
logger.info("Deleting rigid body simulation bake")
|
|
||||||
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
|
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
|
||||||
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
|
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
|
||||||
|
|
||||||
@@ -387,7 +377,7 @@ class AddJoint(bpy.types.Operator):
|
|||||||
min=0,
|
min=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> Generator[Tuple[bpy.types.Object, bpy.types.Object], None, None]:
|
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]):
|
||||||
obj_seq = tuple(bone_map.keys())
|
obj_seq = tuple(bone_map.keys())
|
||||||
for rigid_a, bone_a in bone_map.items():
|
for rigid_a, bone_a in bone_map.items():
|
||||||
for rigid_b, bone_b in bone_map.items():
|
for rigid_b, bone_b in bone_map.items():
|
||||||
@@ -400,7 +390,7 @@ class AddJoint(bpy.types.Operator):
|
|||||||
else:
|
else:
|
||||||
yield obj_seq
|
yield obj_seq
|
||||||
|
|
||||||
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> bpy.types.Object:
|
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map):
|
||||||
loc: Optional[Vector] = None
|
loc: Optional[Vector] = None
|
||||||
rot = Euler((0.0, 0.0, 0.0))
|
rot = Euler((0.0, 0.0, 0.0))
|
||||||
rigid_a, rigid_b = rigid_pair
|
rigid_a, rigid_b = rigid_pair
|
||||||
@@ -438,7 +428,7 @@ class AddJoint(bpy.types.Operator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: bpy.types.Context) -> bool:
|
def poll(cls, context):
|
||||||
root_object = FnModel.find_root_object(context.active_object)
|
root_object = FnModel.find_root_object(context.active_object)
|
||||||
if root_object is None:
|
if root_object is None:
|
||||||
return False
|
return False
|
||||||
@@ -449,11 +439,11 @@ class AddJoint(bpy.types.Operator):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
active_object = context.active_object
|
active_object = context.active_object
|
||||||
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
|
root_object = cast("bpy.types.Object", FnModel.find_root_object(active_object))
|
||||||
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object))
|
armature_object = cast("bpy.types.Object", FnModel.find_armature_object(root_object))
|
||||||
bones = cast(bpy.types.Armature, armature_object.data).bones
|
bones = cast("bpy.types.Armature", armature_object.data).bones
|
||||||
bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]] = {r: bones.get(r.mmd_rigid.bone, None) for r in FnModel.iterate_rigid_body_objects(root_object) if r.select_get()}
|
bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]] = {r: bones.get(r.mmd_rigid.bone, None) for r in FnModel.iterate_rigid_body_objects(root_object) if r.select_get()}
|
||||||
|
|
||||||
if len(bone_map) < 2:
|
if len(bone_map) < 2:
|
||||||
@@ -462,19 +452,15 @@ class AddJoint(bpy.types.Operator):
|
|||||||
|
|
||||||
FnContext.select_single_object(context, root_object).select_set(False)
|
FnContext.select_single_object(context, root_object).select_set(False)
|
||||||
if context.scene.rigidbody_world is None:
|
if context.scene.rigidbody_world is None:
|
||||||
logger.info("Creating rigid body world")
|
|
||||||
bpy.ops.rigidbody.world_add()
|
bpy.ops.rigidbody.world_add()
|
||||||
|
|
||||||
joint_count = 0
|
|
||||||
for pair in self.__enumerate_rigid_pair(bone_map):
|
for pair in self.__enumerate_rigid_pair(bone_map):
|
||||||
joint = self.__add_joint(context, root_object, pair, bone_map)
|
joint = self.__add_joint(context, root_object, pair, bone_map)
|
||||||
joint.select_set(True)
|
joint.select_set(True)
|
||||||
joint_count += 1
|
|
||||||
|
|
||||||
logger.info(f"Added {joint_count} joints between rigid bodies")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
def invoke(self, context, event):
|
||||||
vm = context.window_manager
|
vm = context.window_manager
|
||||||
return vm.invoke_props_dialog(self)
|
return vm.invoke_props_dialog(self)
|
||||||
|
|
||||||
@@ -486,13 +472,12 @@ class RemoveJoint(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: bpy.types.Context) -> bool:
|
def poll(cls, context):
|
||||||
return FnModel.is_joint_object(context.active_object)
|
return FnModel.is_joint_object(context.active_object)
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
root = FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
logger.info(f"Removing joint: {obj.name}")
|
|
||||||
utils.selectAObject(obj) # ensure this is the only one object select
|
utils.selectAObject(obj) # ensure this is the only one object select
|
||||||
bpy.ops.object.delete(use_global=True)
|
bpy.ops.object.delete(use_global=True)
|
||||||
if root:
|
if root:
|
||||||
@@ -507,7 +492,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_rigid_body_world_objects() -> Tuple[bpy.types.Collection, bpy.types.Collection]:
|
def __get_rigid_body_world_objects():
|
||||||
rigid_body.setRigidBodyWorldEnabled(True)
|
rigid_body.setRigidBodyWorldEnabled(True)
|
||||||
rbw = bpy.context.scene.rigidbody_world
|
rbw = bpy.context.scene.rigidbody_world
|
||||||
if not rbw.collection:
|
if not rbw.collection:
|
||||||
@@ -522,21 +507,21 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
|||||||
|
|
||||||
return rbw.collection.objects, rbw.constraints.objects
|
return rbw.collection.objects, rbw.constraints.objects
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context):
|
||||||
scene = context.scene
|
scene = context.scene
|
||||||
scene_objs = set(scene.objects)
|
scene_objs = set(scene.objects)
|
||||||
scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects)
|
scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects)
|
||||||
|
|
||||||
def _update_group(obj: bpy.types.Object, group: bpy.types.Collection) -> bool:
|
def _update_group(obj, group):
|
||||||
if obj in scene_objs:
|
if obj in scene_objs:
|
||||||
if obj not in group.values():
|
if obj not in group.values():
|
||||||
group.link(obj)
|
group.link(obj)
|
||||||
return True
|
return True
|
||||||
elif obj in group.values():
|
if obj in group.values():
|
||||||
group.unlink(obj)
|
group.unlink(obj)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _references(obj: bpy.types.Object) -> Generator[bpy.types.Object, None, None]:
|
def _references(obj):
|
||||||
yield obj
|
yield obj
|
||||||
if getattr(obj, "proxy", None):
|
if getattr(obj, "proxy", None):
|
||||||
yield from _references(obj.proxy)
|
yield from _references(obj.proxy)
|
||||||
@@ -553,7 +538,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
|||||||
# Object.rigid_body are removed,
|
# Object.rigid_body are removed,
|
||||||
# but Object.rigid_body_constraint are retained.
|
# but Object.rigid_body_constraint are retained.
|
||||||
# Therefore, it must be checked with Object.mmd_type.
|
# Therefore, it must be checked with Object.mmd_type.
|
||||||
logger.info("Updating rigid body world objects")
|
|
||||||
for i in (x for x in objects if x.mmd_type == "RIGID_BODY"):
|
for i in (x for x in objects if x.mmd_type == "RIGID_BODY"):
|
||||||
if not _update_group(i, rb_objs):
|
if not _update_group(i, rb_objs):
|
||||||
continue
|
continue
|
||||||
@@ -568,7 +552,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
|||||||
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
|
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
|
||||||
# mass, friction, restitution, linear_dumping, angular_dumping
|
# mass, friction, restitution, linear_dumping, angular_dumping
|
||||||
|
|
||||||
logger.info("Updating rigid body constraints")
|
|
||||||
for i in (x for x in objects if x.rigid_body_constraint):
|
for i in (x for x in objects if x.rigid_body_constraint):
|
||||||
if not _update_group(i, rbc_objs):
|
if not _update_group(i, rbc_objs):
|
||||||
continue
|
continue
|
||||||
@@ -579,7 +562,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
|||||||
rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
|
rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
|
||||||
|
|
||||||
if need_rebuild_physics:
|
if need_rebuild_physics:
|
||||||
logger.info("Rebuilding physics for models")
|
|
||||||
for root_object in scene.objects:
|
for root_object in scene.objects:
|
||||||
if root_object.mmd_type != "ROOT":
|
if root_object.mmd_type != "ROOT":
|
||||||
continue
|
continue
|
||||||
|
|||||||
+13
-23
@@ -1,23 +1,18 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2018 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
|
||||||
|
|
||||||
from typing import Set, Tuple
|
from typing import Set
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Operator, Context, Object
|
from bpy.types import Operator
|
||||||
|
|
||||||
from ..core.model import FnModel
|
from ..core.model import FnModel
|
||||||
from ..core.sdef import FnSDEF
|
from ..core.sdef import FnSDEF
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
def _get_target_objects(context: Context) -> Tuple[Set[Object], Set[Object]]:
|
def _get_target_objects(context):
|
||||||
root_objects: Set[Object] = set()
|
root_objects: Set[bpy.types.Object] = set()
|
||||||
selected_objects: Set[Object] = set()
|
selected_objects: Set[bpy.types.Object] = set()
|
||||||
for i in context.selected_objects:
|
for i in context.selected_objects:
|
||||||
if i.type == "MESH":
|
if i.type == "MESH":
|
||||||
selected_objects.add(i)
|
selected_objects.add(i)
|
||||||
@@ -41,13 +36,11 @@ class ResetSDEFCache(Operator):
|
|||||||
bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
|
bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context):
|
||||||
target_meshes, _ = _get_target_objects(context)
|
target_meshes, _ = _get_target_objects(context)
|
||||||
logger.info(f"Resetting SDEF cache for {len(target_meshes)} objects")
|
|
||||||
for i in target_meshes:
|
for i in target_meshes:
|
||||||
FnSDEF.clear_cache(i)
|
FnSDEF.clear_cache(i)
|
||||||
FnSDEF.clear_cache(unused_only=True)
|
FnSDEF.clear_cache(unused_only=True)
|
||||||
logger.debug("SDEF cache reset completed")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -78,20 +71,19 @@ class BindSDEF(Operator):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]:
|
def invoke(self, context, event):
|
||||||
vm = context.window_manager
|
vm = context.window_manager
|
||||||
return vm.invoke_props_dialog(self)
|
return vm.invoke_props_dialog(self)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
# TODO: Utility Functionalize
|
||||||
|
def execute(self, context):
|
||||||
target_meshes, root_objects = _get_target_objects(context)
|
target_meshes, root_objects = _get_target_objects(context)
|
||||||
logger.info(f"Binding SDEF for {len(target_meshes)} objects with mode={self.mode}, skip={self.use_skip}, scale={self.use_scale}")
|
|
||||||
|
|
||||||
for r in root_objects:
|
for r in root_objects:
|
||||||
r.mmd_root.use_sdef = True
|
r.mmd_root.use_sdef = True
|
||||||
|
|
||||||
param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale)
|
param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale)
|
||||||
count = sum(FnSDEF.bind(i, *param) for i in target_meshes)
|
count = sum(FnSDEF.bind(i, *param) for i in target_meshes)
|
||||||
logger.info(f"Successfully bound SDEF for {count} of {len(target_meshes)} meshes")
|
|
||||||
self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)")
|
self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)")
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -102,15 +94,13 @@ class UnbindSDEF(Operator):
|
|||||||
bl_description = "Unbind MMD SDEF data of selected objects"
|
bl_description = "Unbind MMD SDEF data of selected objects"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
# TODO: Utility Functionalize
|
||||||
|
def execute(self, context):
|
||||||
target_meshes, root_objects = _get_target_objects(context)
|
target_meshes, root_objects = _get_target_objects(context)
|
||||||
logger.info(f"Unbinding SDEF for {len(target_meshes)} objects")
|
|
||||||
|
|
||||||
for i in target_meshes:
|
for i in target_meshes:
|
||||||
FnSDEF.unbind(i)
|
FnSDEF.unbind(i)
|
||||||
|
|
||||||
for r in root_objects:
|
for r in root_objects:
|
||||||
r.mmd_root.use_sdef = False
|
r.mmd_root.use_sdef = False
|
||||||
|
|
||||||
logger.debug("SDEF unbinding completed")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2021 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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 csv
|
||||||
|
import os
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
@@ -14,7 +12,11 @@ from ..core.translations import MMD_DATA_TYPE_TO_HANDLERS, FnTranslations
|
|||||||
from ..translations import DictionaryEnum
|
from ..translations import DictionaryEnum
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex
|
from ..properties.translations import (
|
||||||
|
MMDTranslation,
|
||||||
|
MMDTranslationElement,
|
||||||
|
MMDTranslationElementIndex,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TranslateMMDModel(bpy.types.Operator):
|
class TranslateMMDModel(bpy.types.Operator):
|
||||||
@@ -77,7 +79,8 @@ class TranslateMMDModel(bpy.types.Operator):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
return obj in context.selected_objects and FnModel.find_root_object(obj)
|
root = FnModel.find_root_object(obj)
|
||||||
|
return obj is not None and obj in context.selected_objects and root is not None
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
vm = context.window_manager
|
vm = context.window_manager
|
||||||
@@ -87,7 +90,7 @@ class TranslateMMDModel(bpy.types.Operator):
|
|||||||
try:
|
try:
|
||||||
self.__translator = DictionaryEnum.get_translator(self.dictionary)
|
self.__translator = DictionaryEnum.get_translator(self.dictionary)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({"ERROR"}, "Failed to load dictionary: %s" % e)
|
self.report({"ERROR"}, f"Failed to load dictionary: {e}")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
@@ -96,7 +99,7 @@ class TranslateMMDModel(bpy.types.Operator):
|
|||||||
|
|
||||||
if "MMD" in self.modes:
|
if "MMD" in self.modes:
|
||||||
for i in self.types:
|
for i in self.types:
|
||||||
getattr(self, "translate_%s" % i.lower())(rig)
|
getattr(self, f"translate_{i.lower()}")(rig)
|
||||||
|
|
||||||
if "BLENDER" in self.modes:
|
if "BLENDER" in self.modes:
|
||||||
self.translate_blender_names(rig)
|
self.translate_blender_names(rig)
|
||||||
@@ -104,7 +107,11 @@ class TranslateMMDModel(bpy.types.Operator):
|
|||||||
translator = self.__translator
|
translator = self.__translator
|
||||||
txt = translator.save_fails()
|
txt = translator.save_fails()
|
||||||
if translator.fails:
|
if translator.fails:
|
||||||
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(translator.fails), txt.name))
|
self.report(
|
||||||
|
{"WARNING"},
|
||||||
|
"Failed to translate %d names, see '%s' in text editor"
|
||||||
|
% (len(translator.fails), txt.name),
|
||||||
|
)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def translate(self, name_j, name_e):
|
def translate(self, name_j, name_e):
|
||||||
@@ -130,7 +137,7 @@ class TranslateMMDModel(bpy.types.Operator):
|
|||||||
|
|
||||||
if "DISPLAY" in self.types:
|
if "DISPLAY" in self.types:
|
||||||
g: bpy.types.BoneCollection
|
g: bpy.types.BoneCollection
|
||||||
for g in cast(bpy.types.Armature, rig.armature().data).collections:
|
for g in cast("bpy.types.Armature", rig.armature().data).collections:
|
||||||
g.name = self.translate(g.name, g.name)
|
g.name = self.translate(g.name, g.name)
|
||||||
|
|
||||||
if "PHYSICS" in self.types:
|
if "PHYSICS" in self.types:
|
||||||
@@ -153,7 +160,9 @@ class TranslateMMDModel(bpy.types.Operator):
|
|||||||
comment_text = bpy.data.texts.get(mmd_root.comment_text, None)
|
comment_text = bpy.data.texts.get(mmd_root.comment_text, None)
|
||||||
comment_e_text = bpy.data.texts.get(mmd_root.comment_e_text, None)
|
comment_e_text = bpy.data.texts.get(mmd_root.comment_e_text, None)
|
||||||
if comment_text and comment_e_text:
|
if comment_text and comment_e_text:
|
||||||
comment_e = self.translate(comment_text.as_string(), comment_e_text.as_string())
|
comment_e = self.translate(
|
||||||
|
comment_text.as_string(), comment_e_text.as_string(),
|
||||||
|
)
|
||||||
comment_e_text.from_string(comment_e)
|
comment_e_text.from_string(comment_e)
|
||||||
|
|
||||||
def translate_bone(self, rig):
|
def translate_bone(self, rig):
|
||||||
@@ -167,7 +176,7 @@ class TranslateMMDModel(bpy.types.Operator):
|
|||||||
mmd_root = rig.rootObject().mmd_root
|
mmd_root = rig.rootObject().mmd_root
|
||||||
attr_list = ("group", "vertex", "bone", "uv", "material")
|
attr_list = ("group", "vertex", "bone", "uv", "material")
|
||||||
prefix_list = ("G_", "", "B_", "UV_", "M_")
|
prefix_list = ("G_", "", "B_", "UV_", "M_")
|
||||||
for attr, prefix in zip(attr_list, prefix_list):
|
for attr, prefix in zip(attr_list, prefix_list, strict=False):
|
||||||
for m in getattr(mmd_root, attr + "_morphs", []):
|
for m in getattr(mmd_root, attr + "_morphs", []):
|
||||||
m.name_e = self.translate(m.name, m.name_e)
|
m.name_e = self.translate(m.name, m.name_e)
|
||||||
if not prefix:
|
if not prefix:
|
||||||
@@ -182,7 +191,9 @@ class TranslateMMDModel(bpy.types.Operator):
|
|||||||
for m in rig.materials():
|
for m in rig.materials():
|
||||||
if m is None:
|
if m is None:
|
||||||
continue
|
continue
|
||||||
m.mmd_material.name_e = self.translate(m.mmd_material.name_j, m.mmd_material.name_e)
|
m.mmd_material.name_e = self.translate(
|
||||||
|
m.mmd_material.name_j, m.mmd_material.name_e,
|
||||||
|
)
|
||||||
|
|
||||||
def translate_display(self, rig):
|
def translate_display(self, rig):
|
||||||
mmd_root = rig.rootObject().mmd_root
|
mmd_root = rig.rootObject().mmd_root
|
||||||
@@ -200,10 +211,24 @@ class TranslateMMDModel(bpy.types.Operator):
|
|||||||
DEFAULT_SHOW_ROW_COUNT = 20
|
DEFAULT_SHOW_ROW_COUNT = 20
|
||||||
|
|
||||||
|
|
||||||
class MMD_TOOLS_UL_MMDTranslationElementIndex(bpy.types.UIList):
|
class MMD_TOOLS_LOCAL_UL_MMDTranslationElementIndex(bpy.types.UIList):
|
||||||
def draw_item(self, context, layout: bpy.types.UILayout, data, mmd_translation_element_index: "MMDTranslationElementIndex", icon, active_data, active_propname, index: int):
|
def draw_item(
|
||||||
mmd_translation_element: "MMDTranslationElement" = data.translation_elements[mmd_translation_element_index.value]
|
self,
|
||||||
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(layout, mmd_translation_element, index)
|
context,
|
||||||
|
layout: bpy.types.UILayout,
|
||||||
|
data,
|
||||||
|
mmd_translation_element_index: "MMDTranslationElementIndex",
|
||||||
|
icon,
|
||||||
|
active_data,
|
||||||
|
active_propname,
|
||||||
|
index: int,
|
||||||
|
):
|
||||||
|
mmd_translation_element: MMDTranslationElement = data.translation_elements[
|
||||||
|
mmd_translation_element_index.value
|
||||||
|
]
|
||||||
|
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(
|
||||||
|
layout, mmd_translation_element, index,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RestoreMMDDataReferenceOperator(bpy.types.Operator):
|
class RestoreMMDDataReferenceOperator(bpy.types.Operator):
|
||||||
@@ -216,9 +241,15 @@ class RestoreMMDDataReferenceOperator(bpy.types.Operator):
|
|||||||
restore_value: bpy.props.StringProperty()
|
restore_value: bpy.props.StringProperty()
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context):
|
def execute(self, context: bpy.types.Context):
|
||||||
root_object = FnModel.find_root_object(context.object)
|
root_object = FnModel.find_root_object(context.active_object)
|
||||||
mmd_translation_element_index = root_object.mmd_root.translation.filtered_translation_element_indices[self.index].value
|
mmd_translation_element_index = (
|
||||||
mmd_translation_element = root_object.mmd_root.translation.translation_elements[mmd_translation_element_index]
|
root_object.mmd_root.translation.filtered_translation_element_indices[
|
||||||
|
self.index
|
||||||
|
].value
|
||||||
|
)
|
||||||
|
mmd_translation_element = root_object.mmd_root.translation.translation_elements[
|
||||||
|
mmd_translation_element_index
|
||||||
|
]
|
||||||
setattr(mmd_translation_element, self.prop_name, self.restore_value)
|
setattr(mmd_translation_element, self.prop_name, self.restore_value)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -231,7 +262,8 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return FnModel.find_root_object(context.object) is not None
|
root = FnModel.find_root_object(context.active_object)
|
||||||
|
return root is not None
|
||||||
|
|
||||||
def draw(self, _context):
|
def draw(self, _context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
@@ -244,13 +276,33 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
|||||||
|
|
||||||
group = row.row(align=True, heading="is Blank:")
|
group = row.row(align=True, heading="is Blank:")
|
||||||
group.alignment = "RIGHT"
|
group.alignment = "RIGHT"
|
||||||
group.prop(mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese")
|
group.prop(
|
||||||
|
mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese",
|
||||||
|
)
|
||||||
group.prop(mmd_translation, "filter_english_blank", toggle=True, text="English")
|
group.prop(mmd_translation, "filter_english_blank", toggle=True, text="English")
|
||||||
|
|
||||||
group = row.row(align=True)
|
group = row.row(align=True)
|
||||||
group.prop(mmd_translation, "filter_restorable", toggle=True, icon="FILE_REFRESH", icon_only=True)
|
group.prop(
|
||||||
group.prop(mmd_translation, "filter_selected", toggle=True, icon="RESTRICT_SELECT_OFF", icon_only=True)
|
mmd_translation,
|
||||||
group.prop(mmd_translation, "filter_visible", toggle=True, icon="HIDE_OFF", icon_only=True)
|
"filter_restorable",
|
||||||
|
toggle=True,
|
||||||
|
icon="FILE_REFRESH",
|
||||||
|
icon_only=True,
|
||||||
|
)
|
||||||
|
group.prop(
|
||||||
|
mmd_translation,
|
||||||
|
"filter_selected",
|
||||||
|
toggle=True,
|
||||||
|
icon="RESTRICT_SELECT_OFF",
|
||||||
|
icon_only=True,
|
||||||
|
)
|
||||||
|
group.prop(
|
||||||
|
mmd_translation,
|
||||||
|
"filter_visible",
|
||||||
|
toggle=True,
|
||||||
|
icon="HIDE_OFF",
|
||||||
|
icon_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
box = col.box().column(align=True)
|
box = col.box().column(align=True)
|
||||||
@@ -262,11 +314,14 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
|||||||
row.label(text="", icon="RESTRICT_SELECT_OFF")
|
row.label(text="", icon="RESTRICT_SELECT_OFF")
|
||||||
row.label(text="", icon="HIDE_OFF")
|
row.label(text="", icon="HIDE_OFF")
|
||||||
|
|
||||||
if len(mmd_translation.filtered_translation_element_indices) > DEFAULT_SHOW_ROW_COUNT:
|
if (
|
||||||
|
len(mmd_translation.filtered_translation_element_indices)
|
||||||
|
> DEFAULT_SHOW_ROW_COUNT
|
||||||
|
):
|
||||||
row.label(text="", icon="BLANK1")
|
row.label(text="", icon="BLANK1")
|
||||||
|
|
||||||
col.template_list(
|
col.template_list(
|
||||||
"MMD_TOOLS_UL_MMDTranslationElementIndex",
|
"mmd_tools_UL_MMDTranslationElementIndex",
|
||||||
"",
|
"",
|
||||||
mmd_translation,
|
mmd_translation,
|
||||||
"filtered_translation_element_indices",
|
"filtered_translation_element_indices",
|
||||||
@@ -281,7 +336,12 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
|||||||
|
|
||||||
box.separator()
|
box.separator()
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.prop(mmd_translation, "batch_operation_script_preset", text="Preset", icon="CON_TRANSFORM_CACHE")
|
row.prop(
|
||||||
|
mmd_translation,
|
||||||
|
"batch_operation_script_preset",
|
||||||
|
text="Preset",
|
||||||
|
icon="CON_TRANSFORM_CACHE",
|
||||||
|
)
|
||||||
row.operator(ExecuteTranslationBatchOperator.bl_idname, text="Execute")
|
row.operator(ExecuteTranslationBatchOperator.bl_idname, text="Execute")
|
||||||
|
|
||||||
box.separator()
|
box.separator()
|
||||||
@@ -289,18 +349,25 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
|||||||
translation_box.label(text="Dictionaries:", icon="HELP")
|
translation_box.label(text="Dictionaries:", icon="HELP")
|
||||||
row = translation_box.row()
|
row = translation_box.row()
|
||||||
row.prop(mmd_translation, "dictionary", text="to_english")
|
row.prop(mmd_translation, "dictionary", text="to_english")
|
||||||
# row.operator(ExecuteTranslationScriptOperator.bl_idname, text='Write to .csv')
|
|
||||||
|
|
||||||
translation_box.separator()
|
translation_box.separator()
|
||||||
row = translation_box.row()
|
row = translation_box.row()
|
||||||
row.prop(mmd_translation, "dictionary", text="replace")
|
row.prop(mmd_translation, "dictionary", text="replace")
|
||||||
|
|
||||||
|
# CSV import/export
|
||||||
|
box.separator()
|
||||||
|
translation_box = box.box().column(align=True)
|
||||||
|
translation_box.label(text="CSV:", icon="FILE_TEXT")
|
||||||
|
row = translation_box.row()
|
||||||
|
row.operator(ImportTranslationCSVOperator.bl_idname, text="Import CSV")
|
||||||
|
row.operator(ExportTranslationCSVOperator.bl_idname, text="Export CSV")
|
||||||
|
|
||||||
def invoke(self, context: bpy.types.Context, _event):
|
def invoke(self, context: bpy.types.Context, _event):
|
||||||
root_object = FnModel.find_root_object(context.object)
|
root_object = FnModel.find_root_object(context.active_object)
|
||||||
if root_object is None:
|
if root_object is None:
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
|
mmd_translation: MMDTranslation = root_object.mmd_root.translation
|
||||||
self._mmd_translation = mmd_translation
|
self._mmd_translation = mmd_translation
|
||||||
FnTranslations.clear_data(mmd_translation)
|
FnTranslations.clear_data(mmd_translation)
|
||||||
FnTranslations.collect_data(mmd_translation)
|
FnTranslations.collect_data(mmd_translation)
|
||||||
@@ -309,7 +376,7 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
|||||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
root_object = FnModel.find_root_object(context.object)
|
root_object = FnModel.find_root_object(context.active_object)
|
||||||
if root_object is None:
|
if root_object is None:
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
@@ -325,12 +392,175 @@ class ExecuteTranslationBatchOperator(bpy.types.Operator):
|
|||||||
bl_options = {"INTERNAL"}
|
bl_options = {"INTERNAL"}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context):
|
def execute(self, context: bpy.types.Context):
|
||||||
root = FnModel.find_root_object(context.object)
|
root = FnModel.find_root_object(context.active_object)
|
||||||
if root is None:
|
if root is None:
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
fails, text = FnTranslations.execute_translation_batch(root)
|
fails, text = FnTranslations.execute_translation_batch(root)
|
||||||
if fails:
|
if fails:
|
||||||
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(fails), text.name))
|
self.report(
|
||||||
|
{"WARNING"},
|
||||||
|
"Failed to translate %d names, see '%s' in text editor"
|
||||||
|
% (len(fails), text.name),
|
||||||
|
)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTranslationCSVOperator(bpy.types.Operator):
|
||||||
|
bl_idname = "mmd_tools.export_translation_csv"
|
||||||
|
bl_description = "Export CSV for external translation."
|
||||||
|
bl_label = "Export Translation CSV"
|
||||||
|
|
||||||
|
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||||
|
filename_ext = ".csv"
|
||||||
|
filepath: bpy.props.StringProperty(
|
||||||
|
name="File Path",
|
||||||
|
description="Path to save the translation CSV",
|
||||||
|
subtype="FILE_PATH",
|
||||||
|
default="mmd_translation.csv",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _ensure_csv_extension(self):
|
||||||
|
"""Ensure the file path ends with a .csv extension (case-insensitive)."""
|
||||||
|
if not self.filepath.lower().endswith(".csv"):
|
||||||
|
self.filepath = bpy.path.ensure_ext(self.filepath, ".csv")
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self._ensure_csv_extension()
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
self._ensure_csv_extension()
|
||||||
|
root_object = FnModel.find_root_object(context.active_object)
|
||||||
|
if root_object is None:
|
||||||
|
self.report({"ERROR"}, "Root object not found")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
mmd_translation = root_object.mmd_root.translation
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.filepath, "w", newline="", encoding="utf-8") as csvfile:
|
||||||
|
writer = csv.writer(csvfile)
|
||||||
|
writer.writerow(["type", "blender", "japanese", "english"])
|
||||||
|
for idx in mmd_translation.filtered_translation_element_indices:
|
||||||
|
element = mmd_translation.translation_elements[idx.value]
|
||||||
|
writer.writerow(
|
||||||
|
[element.type, element.name, element.name_j, element.name_e],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.report({"ERROR"}, f"Failed to write CSV: {e}")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
self.report({"INFO"}, f"Exported to {os.path.basename(self.filepath)}")
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class ImportTranslationCSVOperator(bpy.types.Operator):
|
||||||
|
bl_idname = "mmd_tools.import_translation_csv"
|
||||||
|
bl_description = "Import translated CSV."
|
||||||
|
bl_label = "Import Translation CSV"
|
||||||
|
|
||||||
|
only_update_english_name: bpy.props.BoolProperty(
|
||||||
|
name="Only Update English Name",
|
||||||
|
description="(Enabled by default) Only update English name (name_e). otherwise, update all names when different",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||||
|
filepath: bpy.props.StringProperty(
|
||||||
|
name="File Path",
|
||||||
|
description="Path to import the translation CSV",
|
||||||
|
subtype="FILE_PATH",
|
||||||
|
default="*.csv",
|
||||||
|
)
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
root_object = FnModel.find_root_object(context.active_object)
|
||||||
|
if root_object is None:
|
||||||
|
self.report({"ERROR"}, "Root object not found")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
mmd_translation = root_object.mmd_root.translation
|
||||||
|
updated_count = 0
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.filepath, encoding="utf-8") as csvfile:
|
||||||
|
reader = csv.DictReader(csvfile)
|
||||||
|
required_headers = {"blender", "japanese", "english"}
|
||||||
|
if not required_headers.issubset(set(reader.fieldnames or [])):
|
||||||
|
missing = required_headers - set(reader.fieldnames or [])
|
||||||
|
self.report(
|
||||||
|
{"ERROR"},
|
||||||
|
f"Missing required headers in CSV: {', '.join(missing)}",
|
||||||
|
)
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
visible_indices = [
|
||||||
|
i.value
|
||||||
|
for i in mmd_translation.filtered_translation_element_indices
|
||||||
|
]
|
||||||
|
translation_elements_list = list(mmd_translation.translation_elements)
|
||||||
|
row_count = 0
|
||||||
|
|
||||||
|
for row in reader:
|
||||||
|
if row_count >= len(visible_indices):
|
||||||
|
row_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
element = translation_elements_list[visible_indices[row_count]]
|
||||||
|
|
||||||
|
b_name = row.get("blender", "").strip()
|
||||||
|
j_name = row.get("japanese", "").strip()
|
||||||
|
e_name = row.get("english", "").strip()
|
||||||
|
|
||||||
|
updated = False
|
||||||
|
if self.only_update_english_name:
|
||||||
|
if element.name_e != e_name:
|
||||||
|
element.name_e = e_name
|
||||||
|
updated = True
|
||||||
|
else:
|
||||||
|
if element.name != b_name:
|
||||||
|
element.name = b_name
|
||||||
|
updated = True
|
||||||
|
if element.name_j != j_name:
|
||||||
|
element.name_j = j_name
|
||||||
|
updated = True
|
||||||
|
if element.name_e != e_name:
|
||||||
|
element.name_e = e_name
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
if updated:
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
row_count += 1
|
||||||
|
|
||||||
|
# Output warnings
|
||||||
|
if row_count > len(visible_indices):
|
||||||
|
warnings.append(
|
||||||
|
f"{row_count - len(visible_indices)} extra lines in CSV! (ignored)",
|
||||||
|
)
|
||||||
|
elif row_count < len(visible_indices):
|
||||||
|
warnings.append(
|
||||||
|
f"{len(visible_indices) - row_count} missing lines in CSV! (aborted translation)",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.report({"ERROR"}, f"Failed to read CSV: {e}")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
FnTranslations.update_query(mmd_translation)
|
||||||
|
|
||||||
|
msg = f"Imported {updated_count} entries from CSV"
|
||||||
|
if warnings:
|
||||||
|
for w in warnings:
|
||||||
|
self.report({"WARNING"}, w)
|
||||||
|
msg += " with warnings"
|
||||||
|
|
||||||
|
self.report({"INFO"}, msg)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|||||||
+39
-50
@@ -1,49 +1,43 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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 re
|
import re
|
||||||
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type, Iterator
|
|
||||||
|
|
||||||
from bpy.types import Operator, Context
|
from bpy.types import Operator
|
||||||
from mathutils import Matrix, Vector, Quaternion
|
from mathutils import Matrix, Quaternion
|
||||||
|
|
||||||
from ...logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
class _SetShadingBase:
|
class _SetShadingBase:
|
||||||
bl_options: Set[str] = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_view3d_spaces(context: Context) -> Iterator[Any]:
|
def _get_view3d_spaces(context):
|
||||||
if getattr(context.area, "type", None) == "VIEW_3D":
|
if getattr(context.area, "type", None) == "VIEW_3D":
|
||||||
return (context.area.spaces[0],)
|
return (context.area.spaces[0],)
|
||||||
return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D")
|
return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _reset_color_management(context: Context, use_display_device: bool = True) -> None:
|
def _reset_color_management(context, use_display_device=True):
|
||||||
try:
|
try:
|
||||||
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
|
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None:
|
def _reset_material_shading(context, use_shadeless=False):
|
||||||
for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"):
|
for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"):
|
||||||
for s in i.material_slots:
|
for s in i.material_slots:
|
||||||
if s.material is None:
|
if s.material is None:
|
||||||
continue
|
continue
|
||||||
|
# use_nodes is deprecated in 5.0 but harmless to set
|
||||||
s.material.use_nodes = False
|
s.material.use_nodes = False
|
||||||
s.material.use_shadeless = use_shadeless
|
s.material.use_shadeless = use_shadeless
|
||||||
|
|
||||||
def execute(self, context: Context) -> Dict[str, str]:
|
def execute(self, context):
|
||||||
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
|
# Changed from BLENDER_EEVEE_NEXT to BLENDER_EEVEE for Blender 5.0
|
||||||
logger.debug(f"Setting render engine to BLENDER_EEVEE_NEXT")
|
context.scene.render.engine = "BLENDER_EEVEE"
|
||||||
|
|
||||||
shading_mode: Optional[str] = getattr(self, "_shading_mode", None)
|
shading_mode = getattr(self, "_shading_mode", None)
|
||||||
for space in self._get_view3d_spaces(context):
|
for space in self._get_view3d_spaces(context):
|
||||||
shading = space.shading
|
shading = space.shading
|
||||||
shading.type = "SOLID"
|
shading.type = "SOLID"
|
||||||
@@ -51,40 +45,39 @@ class _SetShadingBase:
|
|||||||
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
|
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
|
||||||
shading.show_object_outline = False
|
shading.show_object_outline = False
|
||||||
shading.show_backface_culling = False
|
shading.show_backface_culling = False
|
||||||
logger.debug(f"Applied shading mode: {shading_mode or 'DEFAULT'}")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class SetGLSLShading(Operator, _SetShadingBase):
|
class SetGLSLShading(Operator, _SetShadingBase):
|
||||||
bl_idname: str = "mmd_tools.set_glsl_shading"
|
bl_idname = "mmd_tools.set_glsl_shading"
|
||||||
bl_label: str = "GLSL View"
|
bl_label = "GLSL View"
|
||||||
bl_description: str = "Use GLSL shading with additional lighting"
|
bl_description = "Use GLSL shading with additional lighting"
|
||||||
|
|
||||||
_shading_mode: str = "GLSL"
|
_shading_mode = "GLSL"
|
||||||
|
|
||||||
|
|
||||||
class SetShadelessGLSLShading(Operator, _SetShadingBase):
|
class SetShadelessGLSLShading(Operator, _SetShadingBase):
|
||||||
bl_idname: str = "mmd_tools.set_shadeless_glsl_shading"
|
bl_idname = "mmd_tools.set_shadeless_glsl_shading"
|
||||||
bl_label: str = "Shadeless GLSL View"
|
bl_label = "Shadeless GLSL View"
|
||||||
bl_description: str = "Use only toon shading"
|
bl_description = "Use only toon shading"
|
||||||
|
|
||||||
_shading_mode: str = "SHADELESS"
|
_shading_mode = "SHADELESS"
|
||||||
|
|
||||||
|
|
||||||
class ResetShading(Operator, _SetShadingBase):
|
class ResetShading(Operator, _SetShadingBase):
|
||||||
bl_idname: str = "mmd_tools.reset_shading"
|
bl_idname = "mmd_tools.reset_shading"
|
||||||
bl_label: str = "Reset View"
|
bl_label = "Reset View"
|
||||||
bl_description: str = "Reset to default Blender shading"
|
bl_description = "Reset to default Blender shading"
|
||||||
|
|
||||||
|
|
||||||
class FlipPose(Operator):
|
class FlipPose(Operator):
|
||||||
bl_idname: str = "mmd_tools.flip_pose"
|
bl_idname = "mmd_tools.flip_pose"
|
||||||
bl_label: str = "Flip Pose"
|
bl_label = "Flip Pose"
|
||||||
bl_description: str = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
|
bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
|
||||||
bl_options: Set[str] = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
|
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
|
||||||
__LR_REGEX: List[Dict[str, Any]] = [
|
__LR_REGEX = [
|
||||||
{"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1},
|
{"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1},
|
||||||
{"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2},
|
{"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2},
|
||||||
{"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0},
|
{"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0},
|
||||||
@@ -92,7 +85,7 @@ class FlipPose(Operator):
|
|||||||
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
|
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
|
||||||
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
|
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
|
||||||
]
|
]
|
||||||
__LR_MAP: Dict[str, str] = {
|
__LR_MAP = {
|
||||||
"RIGHT": "LEFT",
|
"RIGHT": "LEFT",
|
||||||
"Right": "Left",
|
"Right": "Left",
|
||||||
"right": "left",
|
"right": "left",
|
||||||
@@ -108,7 +101,7 @@ class FlipPose(Operator):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def flip_name(cls, name: str) -> str:
|
def flip_name(cls, name):
|
||||||
for regex in cls.__LR_REGEX:
|
for regex in cls.__LR_REGEX:
|
||||||
match = regex["re"].match(name)
|
match = regex["re"].match(name)
|
||||||
if match:
|
if match:
|
||||||
@@ -126,15 +119,15 @@ class FlipPose(Operator):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __cmul(vec1: Union[Vector, Quaternion], vec2: Tuple[float, float, float, float]) -> Union[Vector, Quaternion]:
|
def __cmul(vec1, vec2):
|
||||||
return type(vec1)([x * y for x, y in zip(vec1, vec2)])
|
return type(vec1)([x * y for x, y in zip(vec1, vec2, strict=False)])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __matrix_compose(loc: Vector, rot: Quaternion, scale: Vector) -> Matrix:
|
def __matrix_compose(loc, rot, scale):
|
||||||
return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)])
|
return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __flip_pose(cls, matrix_basis: Matrix, bone_src: Any, bone_dest: Any) -> None:
|
def __flip_pose(cls, matrix_basis, bone_src, bone_dest):
|
||||||
m = bone_dest.bone.matrix_local.to_3x3().transposed()
|
m = bone_dest.bone.matrix_local.to_3x3().transposed()
|
||||||
mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted()
|
mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted()
|
||||||
loc, rot, scale = matrix_basis.decompose()
|
loc, rot, scale = matrix_basis.decompose()
|
||||||
@@ -143,16 +136,12 @@ class FlipPose(Operator):
|
|||||||
bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
|
bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context):
|
||||||
return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE"
|
obj = context.active_object
|
||||||
|
return obj is not None and obj.type == "ARMATURE" and obj.mode == "POSE"
|
||||||
|
|
||||||
def execute(self, context: Context) -> Dict[str, str]:
|
def execute(self, context):
|
||||||
logger.info("Executing flip pose operation")
|
|
||||||
pose_bones = context.active_object.pose.bones
|
pose_bones = context.active_object.pose.bones
|
||||||
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]:
|
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]:
|
||||||
flip_name = self.flip_name(b.name)
|
self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b))
|
||||||
target_bone = pose_bones.get(flip_name, b)
|
|
||||||
logger.debug(f"Flipping pose from {b.name} to {target_bone.name}")
|
|
||||||
self.__flip_pose(mat, b, target_bone)
|
|
||||||
logger.info("Flip pose operation completed")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -1,90 +1,82 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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 bpy
|
import bpy
|
||||||
from typing import Optional, Set, Dict, Any, List, Tuple, Union, Type
|
|
||||||
|
|
||||||
from .. import utils
|
from .. import utils
|
||||||
from ..core import material
|
from ..core import material
|
||||||
from ..core.material import FnMaterial
|
from ..core.material import FnMaterial
|
||||||
from ..core.model import FnModel
|
from ..core.model import FnModel
|
||||||
from . import patch_library_overridable
|
from . import patch_library_overridable
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_ambient_color()
|
FnMaterial(prop.id_data).update_ambient_color()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_diffuse_color()
|
FnMaterial(prop.id_data).update_diffuse_color()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_alpha(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_alpha(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_alpha()
|
FnMaterial(prop.id_data).update_alpha()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_specular_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_specular_color(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_specular_color()
|
FnMaterial(prop.id_data).update_specular_color()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_shininess(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_shininess(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_shininess()
|
FnMaterial(prop.id_data).update_shininess()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_is_double_sided()
|
FnMaterial(prop.id_data).update_is_double_sided()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context: bpy.types.Context) -> None:
|
def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context):
|
||||||
FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object)
|
FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object)
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_toon_texture()
|
FnMaterial(prop.id_data).update_toon_texture()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_drop_shadow()
|
FnMaterial(prop.id_data).update_drop_shadow()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_self_shadow_map()
|
FnMaterial(prop.id_data).update_self_shadow_map()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_self_shadow()
|
FnMaterial(prop.id_data).update_self_shadow()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_enabled_toon_edge()
|
FnMaterial(prop.id_data).update_enabled_toon_edge()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_edge_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_edge_color(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_edge_color()
|
FnMaterial(prop.id_data).update_edge_color()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context):
|
||||||
FnMaterial(prop.id_data).update_edge_weight()
|
FnMaterial(prop.id_data).update_edge_weight()
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_get_name_j(prop: "MMDMaterial") -> str:
|
def _mmd_material_get_name_j(prop: "MMDMaterial"):
|
||||||
return prop.get("name_j", "")
|
return prop.get("name_j", "")
|
||||||
|
|
||||||
|
|
||||||
def _mmd_material_set_name_j(prop: "MMDMaterial", value: str) -> None:
|
def _mmd_material_set_name_j(prop: "MMDMaterial", value: str):
|
||||||
prop_value = value
|
prop_value = value
|
||||||
if prop_value and prop_value != prop.get("name_j"):
|
if prop_value and prop_value != prop.get("name_j"):
|
||||||
root = FnModel.find_root_object(bpy.context.active_object)
|
root = FnModel.find_root_object(bpy.context.active_object)
|
||||||
if root is None:
|
if root is None:
|
||||||
logger.debug(f"No root object found, using unique name for material: {value}")
|
|
||||||
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials})
|
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials})
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Root object found, using unique name for material within model: {value}")
|
|
||||||
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)})
|
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)})
|
||||||
|
|
||||||
prop["name_j"] = prop_value
|
prop["name_j"] = prop_value
|
||||||
@@ -279,15 +271,13 @@ class MMDMaterial(bpy.types.PropertyGroup):
|
|||||||
description="Comment",
|
description="Comment",
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_id_unique(self) -> bool:
|
def is_id_unique(self):
|
||||||
return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None)
|
return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register() -> None:
|
def register():
|
||||||
logger.debug("Registering MMD material properties")
|
|
||||||
bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial))
|
bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unregister() -> None:
|
def unregister():
|
||||||
logger.debug("Unregistering MMD material properties")
|
|
||||||
del bpy.types.Material.mmd_material
|
del bpy.types.Material.mmd_material
|
||||||
|
|||||||
@@ -1,38 +1,34 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2015 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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 bpy
|
import bpy
|
||||||
from typing import Optional, List, Dict, Any, Set, Tuple, Union, TypeVar, Type
|
|
||||||
from bpy.types import PropertyGroup, Object, ShapeKey
|
|
||||||
|
|
||||||
from .. import utils
|
from .. import utils
|
||||||
from ..core.bone import FnBone
|
from ..core.bone import FnBone
|
||||||
from ..core.material import FnMaterial
|
from ..core.material import FnMaterial
|
||||||
from ..core.model import FnModel, Model
|
from ..core.model import FnModel, Model
|
||||||
from ..core.morph import FnMorph
|
from ..core.morph import FnMorph
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
def _morph_base_get_name(prop: "_MorphBase") -> str:
|
def _morph_base_get_name(prop: "_MorphBase") -> str:
|
||||||
return prop.get("name", "")
|
return prop.get("name", "")
|
||||||
|
|
||||||
|
|
||||||
def _morph_base_set_name(prop: "_MorphBase", value: str) -> None:
|
def _morph_base_set_name(prop: "_MorphBase", value: str):
|
||||||
mmd_root = prop.id_data.mmd_root
|
mmd_root = prop.id_data.mmd_root
|
||||||
morph_type = "%s_morphs" % prop.bl_rna.identifier[:-5].lower()
|
# morph_type = mmd_root.active_morph_type
|
||||||
|
morph_type = f"{prop.bl_rna.identifier[:-5].lower()}_morphs"
|
||||||
|
# assert(prop.bl_rna.identifier.endswith('Morph'))
|
||||||
|
# logging.debug('_set_name: %s %s %s', prop, value, morph_type)
|
||||||
prop_name = prop.get("name", None)
|
prop_name = prop.get("name", None)
|
||||||
if prop_name == value:
|
if prop_name == value:
|
||||||
return
|
return
|
||||||
|
|
||||||
used_names: Set[str] = {x.name for x in getattr(mmd_root, morph_type) if x != prop}
|
used_names = {x.name for x in getattr(mmd_root, morph_type) if x != prop}
|
||||||
value = utils.unique_name(value, used_names)
|
value = utils.unique_name(value, used_names)
|
||||||
if prop_name is not None:
|
if prop_name is not None:
|
||||||
if morph_type == "vertex_morphs":
|
if morph_type == "vertex_morphs":
|
||||||
kb_list: Dict[str, List[ShapeKey]] = {}
|
kb_list = {}
|
||||||
for mesh in FnModel.iterate_mesh_objects(prop.id_data):
|
for mesh in FnModel.iterate_mesh_objects(prop.id_data):
|
||||||
for kb in getattr(mesh.data.shape_keys, "key_blocks", ()):
|
for kb in getattr(mesh.data.shape_keys, "key_blocks", ()):
|
||||||
kb_list.setdefault(kb.name, []).append(kb)
|
kb_list.setdefault(kb.name, []).append(kb)
|
||||||
@@ -43,7 +39,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str) -> None:
|
|||||||
kb.name = value
|
kb.name = value
|
||||||
|
|
||||||
elif morph_type == "uv_morphs":
|
elif morph_type == "uv_morphs":
|
||||||
vg_list: Dict[str, List[Any]] = {}
|
vg_list = {}
|
||||||
for mesh in FnModel.iterate_mesh_objects(prop.id_data):
|
for mesh in FnModel.iterate_mesh_objects(prop.id_data):
|
||||||
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh):
|
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh):
|
||||||
vg_list.setdefault(n, []).append(vg)
|
vg_list.setdefault(n, []).append(vg)
|
||||||
@@ -72,7 +68,6 @@ def _morph_base_set_name(prop: "_MorphBase", value: str) -> None:
|
|||||||
kb.name = value
|
kb.name = value
|
||||||
|
|
||||||
prop["name"] = value
|
prop["name"] = value
|
||||||
logger.debug(f"Renamed morph from '{prop_name}' to '{value}'")
|
|
||||||
|
|
||||||
|
|
||||||
class _MorphBase:
|
class _MorphBase:
|
||||||
@@ -101,12 +96,16 @@ class _MorphBase:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _bone_morph_data_update_bone_id(prop: "BoneMorphData", context: bpy.types.Context):
|
||||||
|
pass # Empty function is sufficient to trigger UI update
|
||||||
|
|
||||||
|
|
||||||
def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
|
def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
|
||||||
bone_id: int = prop.get("bone_id", -1)
|
bone_id = prop.get("bone_id", -1)
|
||||||
if bone_id < 0:
|
if bone_id < 0:
|
||||||
return ""
|
return ""
|
||||||
root_object: Object = prop.id_data
|
root_object = prop.id_data
|
||||||
armature_object: Optional[Object] = FnModel.find_armature_object(root_object)
|
armature_object = FnModel.find_armature_object(root_object)
|
||||||
if armature_object is None:
|
if armature_object is None:
|
||||||
return ""
|
return ""
|
||||||
pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id)
|
pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id)
|
||||||
@@ -115,9 +114,9 @@ def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
|
|||||||
return pose_bone.name
|
return pose_bone.name
|
||||||
|
|
||||||
|
|
||||||
def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str) -> None:
|
def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str):
|
||||||
root: Object = prop.id_data
|
root = prop.id_data
|
||||||
arm: Optional[Object] = FnModel.find_armature_object(root)
|
arm = FnModel.find_armature_object(root)
|
||||||
|
|
||||||
# Load the library_override file. This function is triggered when loading, but the arm obj cannot be found.
|
# Load the library_override file. This function is triggered when loading, but the arm obj cannot be found.
|
||||||
# The arm obj is exist, but the relative relationship has not yet been established.
|
# The arm obj is exist, but the relative relationship has not yet been established.
|
||||||
@@ -125,14 +124,13 @@ def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if value not in arm.pose.bones.keys():
|
if value not in arm.pose.bones.keys():
|
||||||
prop["bone_id"] = -1
|
prop.bone_id = -1
|
||||||
return
|
return
|
||||||
pose_bone = arm.pose.bones[value]
|
pose_bone = arm.pose.bones[value]
|
||||||
prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
|
prop.bone_id = FnBone.get_or_assign_bone_id(pose_bone)
|
||||||
logger.debug(f"Set bone morph data bone to '{value}' with ID {prop['bone_id']}")
|
|
||||||
|
|
||||||
|
|
||||||
def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context: bpy.types.Context) -> None:
|
def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context):
|
||||||
if not prop.name.startswith("mmd_bind"):
|
if not prop.name.startswith("mmd_bind"):
|
||||||
return
|
return
|
||||||
arm = FnModel(prop.id_data).morph_slider.dummy_armature
|
arm = FnModel(prop.id_data).morph_slider.dummy_armature
|
||||||
@@ -141,12 +139,9 @@ def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context
|
|||||||
if bone:
|
if bone:
|
||||||
bone.location = prop.location
|
bone.location = prop.location
|
||||||
bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency
|
bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency
|
||||||
logger.debug(f"Updated bone morph data location/rotation for '{prop.name}'")
|
|
||||||
|
|
||||||
|
|
||||||
class BoneMorphData(bpy.types.PropertyGroup):
|
class BoneMorphData(bpy.types.PropertyGroup):
|
||||||
""" """
|
|
||||||
|
|
||||||
bone: bpy.props.StringProperty(
|
bone: bpy.props.StringProperty(
|
||||||
name="Bone",
|
name="Bone",
|
||||||
description="Target bone",
|
description="Target bone",
|
||||||
@@ -156,6 +151,7 @@ class BoneMorphData(bpy.types.PropertyGroup):
|
|||||||
|
|
||||||
bone_id: bpy.props.IntProperty(
|
bone_id: bpy.props.IntProperty(
|
||||||
name="Bone ID",
|
name="Bone ID",
|
||||||
|
update=_bone_morph_data_update_bone_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
location: bpy.props.FloatVectorProperty(
|
location: bpy.props.FloatVectorProperty(
|
||||||
@@ -191,61 +187,53 @@ class BoneMorph(_MorphBase, bpy.types.PropertyGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _material_morph_data_get_material(prop: "MaterialMorphData") -> str:
|
def _material_morph_data_get_material(prop: "MaterialMorphData"):
|
||||||
mat_p = prop.get("material_data", None)
|
mat_data = prop.get("material_data", None)
|
||||||
if mat_p is not None:
|
if mat_data is not None:
|
||||||
return mat_p.name
|
return mat_data.name
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _material_morph_data_set_material(prop: "MaterialMorphData", value: str) -> None:
|
def _material_morph_data_set_material(prop: "MaterialMorphData", value: str):
|
||||||
if value not in bpy.data.materials:
|
if value not in bpy.data.materials:
|
||||||
prop["material_data"] = None
|
prop.material_data = None
|
||||||
prop["material_id"] = -1
|
prop.material_id = -1
|
||||||
logger.debug(f"Material '{value}' not found, setting material_data to None")
|
|
||||||
else:
|
else:
|
||||||
mat = bpy.data.materials[value]
|
mat = bpy.data.materials[value]
|
||||||
fnMat = FnMaterial(mat)
|
fnMat = FnMaterial(mat)
|
||||||
prop["material_data"] = mat
|
prop.material_data = mat
|
||||||
prop["material_id"] = fnMat.material_id
|
prop.material_id = fnMat.material_id
|
||||||
logger.debug(f"Set material morph data material to '{value}' with ID {fnMat.material_id}")
|
|
||||||
|
|
||||||
|
|
||||||
def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str) -> None:
|
def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str):
|
||||||
mesh = FnModel.find_mesh_object_by_name(prop.id_data, value)
|
mesh = FnModel.find_mesh_object_by_name(prop.id_data, value)
|
||||||
if mesh is not None:
|
if mesh is not None:
|
||||||
prop["related_mesh_data"] = mesh.data
|
prop.related_mesh_data = mesh.data
|
||||||
logger.debug(f"Set material morph data related mesh to '{value}'")
|
|
||||||
else:
|
else:
|
||||||
prop["related_mesh_data"] = None
|
prop.related_mesh_data = None
|
||||||
logger.debug(f"Mesh '{value}' not found, setting related_mesh_data to None")
|
|
||||||
|
|
||||||
|
|
||||||
def _material_morph_data_get_related_mesh(prop: "MaterialMorphData") -> str:
|
def _material_morph_data_get_related_mesh(prop):
|
||||||
mesh_p = prop.get("related_mesh_data", None)
|
mesh_data = prop.get("related_mesh_data", None)
|
||||||
if mesh_p is not None:
|
if mesh_data is not None:
|
||||||
return mesh_p.name
|
return mesh_data.name
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context: bpy.types.Context) -> None:
|
def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context):
|
||||||
if not prop.name.startswith("mmd_bind"):
|
if not prop.name.startswith("mmd_bind"):
|
||||||
return
|
return
|
||||||
from ..core.shader import _MaterialMorph
|
from ..core.shader import _MaterialMorph
|
||||||
|
|
||||||
mat = prop["material_data"]
|
mat_data = prop.get("material_data", None)
|
||||||
if mat is not None:
|
if mat_data is not None:
|
||||||
_MaterialMorph.update_morph_inputs(mat, prop)
|
_MaterialMorph.update_morph_inputs(mat_data, prop)
|
||||||
logger.debug(f"Updated material morph modifiable values for '{prop.name}'")
|
|
||||||
else:
|
else:
|
||||||
for mat in FnModel(prop.id_data).materials():
|
for mat_data in FnModel(prop.id_data).materials():
|
||||||
_MaterialMorph.update_morph_inputs(mat, prop)
|
_MaterialMorph.update_morph_inputs(mat_data, prop)
|
||||||
logger.debug(f"Updated material morph modifiable values for all materials")
|
|
||||||
|
|
||||||
|
|
||||||
class MaterialMorphData(bpy.types.PropertyGroup):
|
class MaterialMorphData(bpy.types.PropertyGroup):
|
||||||
""" """
|
|
||||||
|
|
||||||
related_mesh: bpy.props.StringProperty(
|
related_mesh: bpy.props.StringProperty(
|
||||||
name="Related Mesh",
|
name="Related Mesh",
|
||||||
description="Stores a reference to the mesh where this morph data belongs to",
|
description="Stores a reference to the mesh where this morph data belongs to",
|
||||||
@@ -416,6 +404,9 @@ class UVMorphOffset(bpy.types.PropertyGroup):
|
|||||||
name="UV Offset",
|
name="UV Offset",
|
||||||
description="UV offset",
|
description="UV offset",
|
||||||
size=4,
|
size=4,
|
||||||
|
# min=-1,
|
||||||
|
# max=1,
|
||||||
|
# precision=3,
|
||||||
step=0.1,
|
step=0.1,
|
||||||
default=[0, 0, 0, 0],
|
default=[0, 0, 0, 0],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,37 +1,31 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
from typing import cast
|
||||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
|
||||||
|
|
||||||
from typing import cast, Optional, Any, Union
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Context, PropertyGroup, PoseBone, Object, Armature
|
|
||||||
|
|
||||||
from ..core.bone import FnBone
|
from ..core.bone import FnBone
|
||||||
from . import patch_library_overridable
|
from . import patch_library_overridable
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
def _mmd_bone_update_additional_transform(prop: "MMDBone", context: Context) -> None:
|
def _mmd_bone_update_additional_transform(prop: "MMDBone", context: bpy.types.Context):
|
||||||
prop["is_additional_transform_dirty"] = True
|
prop.is_additional_transform_dirty = True
|
||||||
|
# Apply additional transform (Assembly -> Bone button) (Very Slow)
|
||||||
p_bone = context.active_pose_bone
|
p_bone = context.active_pose_bone
|
||||||
if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer():
|
if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer():
|
||||||
logger.debug(f"Applying additional transformation for {p_bone.name}")
|
|
||||||
FnBone.apply_additional_transformation(prop.id_data)
|
FnBone.apply_additional_transformation(prop.id_data)
|
||||||
|
|
||||||
|
|
||||||
def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: Context) -> None:
|
def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: bpy.types.Context):
|
||||||
pose_bone = context.active_pose_bone
|
pose_bone = context.active_pose_bone
|
||||||
if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer():
|
if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer():
|
||||||
logger.debug(f"Updating additional transform influence for {pose_bone.name}")
|
|
||||||
FnBone.update_additional_transform_influence(pose_bone)
|
FnBone.update_additional_transform_influence(pose_bone)
|
||||||
else:
|
else:
|
||||||
prop["is_additional_transform_dirty"] = True
|
prop.is_additional_transform_dirty = True
|
||||||
|
|
||||||
|
|
||||||
def _mmd_bone_get_additional_transform_bone(prop: "MMDBone") -> str:
|
def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"):
|
||||||
arm = prop.id_data
|
arm = prop.id_data
|
||||||
bone_id = prop.get("additional_transform_bone_id", -1)
|
bone_id = prop.get("additional_transform_bone_id", -1)
|
||||||
if bone_id < 0:
|
if bone_id < 0:
|
||||||
@@ -42,17 +36,57 @@ def _mmd_bone_get_additional_transform_bone(prop: "MMDBone") -> str:
|
|||||||
return pose_bone.name
|
return pose_bone.name
|
||||||
|
|
||||||
|
|
||||||
def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str) -> None:
|
def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str):
|
||||||
arm = prop.id_data
|
arm = prop.id_data
|
||||||
prop["is_additional_transform_dirty"] = True
|
prop.is_additional_transform_dirty = True
|
||||||
|
|
||||||
if value not in arm.pose.bones.keys():
|
if value not in arm.pose.bones.keys():
|
||||||
prop["additional_transform_bone_id"] = -1
|
prop.additional_transform_bone_id = -1
|
||||||
return
|
return
|
||||||
|
|
||||||
pose_bone = arm.pose.bones[value]
|
pose_bone = arm.pose.bones[value]
|
||||||
prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
|
target_bone_id = FnBone.get_or_assign_bone_id(pose_bone)
|
||||||
|
|
||||||
|
if prop.bone_id == target_bone_id:
|
||||||
|
prop.additional_transform_bone_id = -1
|
||||||
|
return
|
||||||
|
|
||||||
|
prop.additional_transform_bone_id = target_bone_id
|
||||||
|
|
||||||
|
|
||||||
class MMDBone(PropertyGroup):
|
def _mmd_bone_update_display_connection(prop: "MMDBone", context: bpy.types.Context):
|
||||||
|
pass # Empty function is sufficient to trigger UI update
|
||||||
|
|
||||||
|
|
||||||
|
def _mmd_bone_get_display_connection_bone(prop: "MMDBone"):
|
||||||
|
arm = prop.id_data
|
||||||
|
bone_id = prop.get("display_connection_bone_id", -1)
|
||||||
|
if bone_id < 0:
|
||||||
|
return ""
|
||||||
|
pose_bone = FnBone.find_pose_bone_by_bone_id(arm, bone_id)
|
||||||
|
if pose_bone is None:
|
||||||
|
return ""
|
||||||
|
return pose_bone.name
|
||||||
|
|
||||||
|
|
||||||
|
def _mmd_bone_set_display_connection_bone(prop: "MMDBone", value: str):
|
||||||
|
arm = prop.id_data
|
||||||
|
|
||||||
|
if value not in arm.pose.bones.keys():
|
||||||
|
prop.display_connection_bone_id = -1
|
||||||
|
return
|
||||||
|
|
||||||
|
pose_bone = arm.pose.bones[value]
|
||||||
|
target_bone_id = FnBone.get_or_assign_bone_id(pose_bone)
|
||||||
|
|
||||||
|
if prop.bone_id == target_bone_id:
|
||||||
|
prop.display_connection_bone_id = -1
|
||||||
|
return
|
||||||
|
|
||||||
|
prop.display_connection_bone_id = target_bone_id
|
||||||
|
|
||||||
|
|
||||||
|
class MMDBone(bpy.types.PropertyGroup):
|
||||||
name_j: bpy.props.StringProperty(
|
name_j: bpy.props.StringProperty(
|
||||||
name="Name",
|
name="Name",
|
||||||
description="Japanese Name",
|
description="Japanese Name",
|
||||||
@@ -188,12 +222,35 @@ class MMDBone(PropertyGroup):
|
|||||||
|
|
||||||
is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True)
|
is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True)
|
||||||
|
|
||||||
def is_id_unique(self) -> bool:
|
display_connection_bone: bpy.props.StringProperty(
|
||||||
|
name="Display Connection Bone",
|
||||||
|
description="Target bone for display connection",
|
||||||
|
set=_mmd_bone_set_display_connection_bone,
|
||||||
|
get=_mmd_bone_get_display_connection_bone,
|
||||||
|
)
|
||||||
|
|
||||||
|
display_connection_bone_id: bpy.props.IntProperty(
|
||||||
|
name="Display Connection Bone ID",
|
||||||
|
description="Bone ID for display connection (PMX displayConnection)",
|
||||||
|
default=-1,
|
||||||
|
update=_mmd_bone_update_display_connection,
|
||||||
|
)
|
||||||
|
|
||||||
|
display_connection_type: bpy.props.EnumProperty(
|
||||||
|
name="Display Connection Type",
|
||||||
|
description="Type of display connection",
|
||||||
|
items=[
|
||||||
|
("BONE", "Bone", "Connected to a bone"),
|
||||||
|
("OFFSET", "Offset", "Connected to an offset position"),
|
||||||
|
],
|
||||||
|
default="OFFSET",
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_id_unique(self):
|
||||||
return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None)
|
return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register() -> None:
|
def register():
|
||||||
logger.debug("Registering MMDBone properties")
|
|
||||||
bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone))
|
bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone))
|
||||||
bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False))
|
bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False))
|
||||||
bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type"))
|
bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type"))
|
||||||
@@ -203,25 +260,24 @@ class MMDBone(PropertyGroup):
|
|||||||
description="MMD IK toggle is used to import/export animation of IK on-off",
|
description="MMD IK toggle is used to import/export animation of IK on-off",
|
||||||
update=_pose_bone_update_mmd_ik_toggle,
|
update=_pose_bone_update_mmd_ik_toggle,
|
||||||
default=True,
|
default=True,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unregister() -> None:
|
def unregister():
|
||||||
logger.debug("Unregistering MMDBone properties")
|
|
||||||
del bpy.types.PoseBone.mmd_ik_toggle
|
del bpy.types.PoseBone.mmd_ik_toggle
|
||||||
del bpy.types.PoseBone.mmd_shadow_bone_type
|
del bpy.types.PoseBone.mmd_shadow_bone_type
|
||||||
del bpy.types.PoseBone.is_mmd_shadow_bone
|
del bpy.types.PoseBone.is_mmd_shadow_bone
|
||||||
del bpy.types.PoseBone.mmd_bone
|
del bpy.types.PoseBone.mmd_bone
|
||||||
|
|
||||||
|
|
||||||
def _pose_bone_update_mmd_ik_toggle(prop: PoseBone, _context: Any) -> None:
|
def _pose_bone_update_mmd_ik_toggle(prop: bpy.types.PoseBone, _context):
|
||||||
v = prop.mmd_ik_toggle
|
v = prop.mmd_ik_toggle
|
||||||
armature_object = cast(Object, prop.id_data)
|
armature_object = cast("bpy.types.Object", prop.id_data)
|
||||||
for b in armature_object.pose.bones:
|
for b in armature_object.pose.bones:
|
||||||
for c in b.constraints:
|
for c in b.constraints:
|
||||||
if c.type == "IK" and c.subtarget == prop.name:
|
if c.type == "IK" and c.subtarget == prop.name:
|
||||||
logger.debug(f"Updating IK toggle for {b.name} {c.name}")
|
# logging.debug(' %s %s', b.name, c.name)
|
||||||
c.influence = v
|
c.influence = v
|
||||||
b = b if c.use_tail else b.parent
|
b = b if c.use_tail else b.parent
|
||||||
for b in ([b] + b.parent_recursive)[: c.chain_count]:
|
for b in ([b] + b.parent_recursive)[: c.chain_count]:
|
||||||
|
|||||||
@@ -1,42 +1,35 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Properties for rigid bodies and joints"""
|
"""Properties for rigid bodies and joints"""
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from typing import Optional, Any, Set, List, Dict, Tuple, Union
|
|
||||||
from bpy.types import Context, Object, PropertyGroup, Material
|
|
||||||
|
|
||||||
from .. import bpyutils
|
from .. import bpyutils
|
||||||
from ..core import rigid_body
|
from ..core import rigid_body
|
||||||
from ..core.rigid_body import RigidBodyMaterial, FnRigidBody
|
|
||||||
from ..core.model import FnModel
|
from ..core.model import FnModel
|
||||||
|
from ..core.rigid_body import FnRigidBody, RigidBodyMaterial
|
||||||
from . import patch_library_overridable
|
from . import patch_library_overridable
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
|
||||||
|
|
||||||
def _updateCollisionGroup(prop: PropertyGroup, _context: Context) -> None:
|
def _updateCollisionGroup(prop, _context):
|
||||||
obj: Object = prop.id_data
|
obj = prop.id_data
|
||||||
materials: List[Material] = obj.data.materials
|
materials = obj.data.materials
|
||||||
if len(materials) == 0:
|
if len(materials) == 0:
|
||||||
materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number))
|
materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number))
|
||||||
else:
|
else:
|
||||||
obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number)
|
obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number)
|
||||||
|
|
||||||
|
|
||||||
def _updateType(prop: PropertyGroup, _context: Context) -> None:
|
def _updateType(prop, _context):
|
||||||
obj: Object = prop.id_data
|
obj = prop.id_data
|
||||||
rb = obj.rigid_body
|
rb = obj.rigid_body
|
||||||
if rb:
|
if rb:
|
||||||
rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC
|
rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC
|
||||||
|
|
||||||
|
|
||||||
def _updateShape(prop: PropertyGroup, _context: Context) -> None:
|
def _updateShape(prop, _context):
|
||||||
obj: Object = prop.id_data
|
obj = prop.id_data
|
||||||
|
|
||||||
if len(obj.data.vertices) > 0:
|
if len(obj.data.vertices) > 0:
|
||||||
size = prop.size
|
size = prop.size
|
||||||
@@ -47,8 +40,8 @@ def _updateShape(prop: PropertyGroup, _context: Context) -> None:
|
|||||||
rb.collision_shape = prop.shape
|
rb.collision_shape = prop.shape
|
||||||
|
|
||||||
|
|
||||||
def _get_bone(prop: PropertyGroup) -> str:
|
def _get_bone(prop):
|
||||||
obj: Object = prop.id_data
|
obj = prop.id_data
|
||||||
relation = obj.constraints.get("mmd_tools_rigid_parent", None)
|
relation = obj.constraints.get("mmd_tools_rigid_parent", None)
|
||||||
if relation:
|
if relation:
|
||||||
arm = relation.target
|
arm = relation.target
|
||||||
@@ -58,9 +51,9 @@ def _get_bone(prop: PropertyGroup) -> str:
|
|||||||
return prop.get("bone", "")
|
return prop.get("bone", "")
|
||||||
|
|
||||||
|
|
||||||
def _set_bone(prop: PropertyGroup, value: str) -> None:
|
def _set_bone(prop, value):
|
||||||
bone_name: str = value
|
bone_name = value
|
||||||
obj: Object = prop.id_data
|
obj = prop.id_data
|
||||||
relation = obj.constraints.get("mmd_tools_rigid_parent", None)
|
relation = obj.constraints.get("mmd_tools_rigid_parent", None)
|
||||||
if relation is None:
|
if relation is None:
|
||||||
relation = obj.constraints.new("CHILD_OF")
|
relation = obj.constraints.new("CHILD_OF")
|
||||||
@@ -81,21 +74,24 @@ def _set_bone(prop: PropertyGroup, value: str) -> None:
|
|||||||
prop["bone"] = bone_name
|
prop["bone"] = bone_name
|
||||||
|
|
||||||
|
|
||||||
def _get_size(prop: PropertyGroup) -> Tuple[float, float, float]:
|
def _get_size(prop):
|
||||||
if prop.id_data.mmd_type != "RIGID_BODY":
|
if prop.id_data.mmd_type != "RIGID_BODY":
|
||||||
return (0, 0, 0)
|
return (0, 0, 0)
|
||||||
return FnRigidBody.get_rigid_body_size(prop.id_data)
|
return FnRigidBody.get_rigid_body_size(prop.id_data)
|
||||||
|
|
||||||
|
|
||||||
def _set_size(prop: PropertyGroup, value: Tuple[float, float, float]) -> None:
|
def _set_size(prop, value):
|
||||||
obj: Object = prop.id_data
|
obj = prop.id_data
|
||||||
assert obj.mode == "OBJECT" # not support other mode yet
|
assert obj.mode == "OBJECT" # not support other mode yet
|
||||||
shape: str = prop.shape
|
shape = prop.shape
|
||||||
|
|
||||||
mesh = obj.data
|
mesh = obj.data
|
||||||
rb = obj.rigid_body
|
rb = obj.rigid_body
|
||||||
|
|
||||||
if len(mesh.vertices) == 0 or rb is None or rb.collision_shape != shape:
|
current_size = FnRigidBody.get_rigid_body_size(obj)
|
||||||
|
is_zero_size = all(abs(s) < 1e-6 for s in current_size)
|
||||||
|
|
||||||
|
if len(mesh.vertices) == 0 or rb is None or rb.collision_shape != shape or is_zero_size:
|
||||||
if shape == "SPHERE":
|
if shape == "SPHERE":
|
||||||
bpyutils.makeSphere(
|
bpyutils.makeSphere(
|
||||||
radius=value[0],
|
radius=value[0],
|
||||||
@@ -149,15 +145,15 @@ def _set_size(prop: PropertyGroup, value: Tuple[float, float, float]) -> None:
|
|||||||
mesh.update()
|
mesh.update()
|
||||||
|
|
||||||
|
|
||||||
def _get_rigid_name(prop: PropertyGroup) -> str:
|
def _get_rigid_name(prop):
|
||||||
return prop.get("name", "")
|
return prop.get("name", "")
|
||||||
|
|
||||||
|
|
||||||
def _set_rigid_name(prop: PropertyGroup, value: str) -> None:
|
def _set_rigid_name(prop, value):
|
||||||
prop["name"] = value
|
prop["name"] = value
|
||||||
|
|
||||||
|
|
||||||
class MMDRigidBody(PropertyGroup):
|
class MMDRigidBody(bpy.types.PropertyGroup):
|
||||||
name_j: bpy.props.StringProperty(
|
name_j: bpy.props.StringProperty(
|
||||||
name="Name",
|
name="Name",
|
||||||
description="Japanese Name",
|
description="Japanese Name",
|
||||||
@@ -230,18 +226,16 @@ class MMDRigidBody(PropertyGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register() -> None:
|
def register():
|
||||||
logger.debug("Registering MMDRigidBody property")
|
|
||||||
bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody))
|
bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unregister() -> None:
|
def unregister():
|
||||||
logger.debug("Unregistering MMDRigidBody property")
|
|
||||||
del bpy.types.Object.mmd_rigid
|
del bpy.types.Object.mmd_rigid
|
||||||
|
|
||||||
|
|
||||||
def _updateSpringLinear(prop: PropertyGroup, context: Context) -> None:
|
def _updateSpringLinear(prop, context):
|
||||||
obj: Object = prop.id_data
|
obj = prop.id_data
|
||||||
rbc = obj.rigid_body_constraint
|
rbc = obj.rigid_body_constraint
|
||||||
if rbc:
|
if rbc:
|
||||||
rbc.spring_stiffness_x = prop.spring_linear[0]
|
rbc.spring_stiffness_x = prop.spring_linear[0]
|
||||||
@@ -249,8 +243,8 @@ def _updateSpringLinear(prop: PropertyGroup, context: Context) -> None:
|
|||||||
rbc.spring_stiffness_z = prop.spring_linear[2]
|
rbc.spring_stiffness_z = prop.spring_linear[2]
|
||||||
|
|
||||||
|
|
||||||
def _updateSpringAngular(prop: PropertyGroup, context: Context) -> None:
|
def _updateSpringAngular(prop, context):
|
||||||
obj: Object = prop.id_data
|
obj = prop.id_data
|
||||||
rbc = obj.rigid_body_constraint
|
rbc = obj.rigid_body_constraint
|
||||||
if rbc and hasattr(rbc, "use_spring_ang_x"):
|
if rbc and hasattr(rbc, "use_spring_ang_x"):
|
||||||
rbc.spring_stiffness_ang_x = prop.spring_angular[0]
|
rbc.spring_stiffness_ang_x = prop.spring_angular[0]
|
||||||
@@ -258,7 +252,7 @@ def _updateSpringAngular(prop: PropertyGroup, context: Context) -> None:
|
|||||||
rbc.spring_stiffness_ang_z = prop.spring_angular[2]
|
rbc.spring_stiffness_ang_z = prop.spring_angular[2]
|
||||||
|
|
||||||
|
|
||||||
class MMDJoint(PropertyGroup):
|
class MMDJoint(bpy.types.PropertyGroup):
|
||||||
name_j: bpy.props.StringProperty(
|
name_j: bpy.props.StringProperty(
|
||||||
name="Name",
|
name="Name",
|
||||||
description="Japanese Name",
|
description="Japanese Name",
|
||||||
@@ -292,12 +286,9 @@ class MMDJoint(PropertyGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register() -> None:
|
def register():
|
||||||
logger.debug("Registering MMDJoint property")
|
|
||||||
bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint))
|
bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unregister() -> None:
|
def unregister():
|
||||||
logger.debug("Unregistering MMDJoint property")
|
|
||||||
del bpy.types.Object.mmd_joint
|
del bpy.types.Object.mmd_joint
|
||||||
|
|
||||||
|
|||||||
+86
-55
@@ -1,16 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 MMD Tools authors
|
# Copyright 2014 MMD Tools authors
|
||||||
# This file was originally part of the MMD Tools add-on for Blender
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Properties for MMD model root object"""
|
"""Properties for MMD model root object"""
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from typing import Optional, List, Dict, Any, Set, Tuple, Union, Type, TypeVar, cast
|
|
||||||
|
|
||||||
from .. import utils
|
|
||||||
from ..bpyutils import FnContext
|
from ..bpyutils import FnContext
|
||||||
from ..core.material import FnMaterial
|
from ..core.material import FnMaterial
|
||||||
from ..core.model import FnModel
|
from ..core.model import FnModel
|
||||||
@@ -18,18 +12,19 @@ from ..core.sdef import FnSDEF
|
|||||||
from . import patch_library_overridable
|
from . import patch_library_overridable
|
||||||
from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph
|
from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph
|
||||||
from .translations import MMDTranslation
|
from .translations import MMDTranslation
|
||||||
from ....core.logging_setup import logger
|
|
||||||
|
IS_BLENDER_50_UP = bpy.app.version >= (5, 0)
|
||||||
|
|
||||||
|
|
||||||
def __driver_variables(constraint: bpy.types.Constraint, path: str, index: int = -1) -> Tuple[bpy.types.Driver, Any]:
|
def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
|
||||||
d = constraint.driver_add(path, index)
|
d = constraint.driver_add(path, index)
|
||||||
variables = d.driver.variables
|
variables = d.driver.variables
|
||||||
for x in variables:
|
for x in reversed(variables):
|
||||||
variables.remove(x)
|
variables.remove(x)
|
||||||
return d.driver, variables
|
return d.driver, variables
|
||||||
|
|
||||||
|
|
||||||
def __add_single_prop(variables: Any, id_obj: bpy.types.Object, data_path: str, prefix: str) -> Any:
|
def __add_single_prop(variables, id_obj, data_path, prefix):
|
||||||
var = variables.new()
|
var = variables.new()
|
||||||
var.name = prefix + str(len(variables))
|
var.name = prefix + str(len(variables))
|
||||||
var.type = "SINGLE_PROP"
|
var.type = "SINGLE_PROP"
|
||||||
@@ -40,18 +35,17 @@ def __add_single_prop(variables: Any, id_obj: bpy.types.Object, data_path: str,
|
|||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
def _toggleUsePropertyDriver(self: "MMDRoot", _context):
|
||||||
root_object: bpy.types.Object = self.id_data
|
root_object: bpy.types.Object = self.id_data
|
||||||
armature_object = FnModel.find_armature_object(root_object)
|
armature_object = FnModel.find_armature_object(root_object)
|
||||||
|
|
||||||
if armature_object is None:
|
if armature_object is None:
|
||||||
ik_map: Dict[Any, Tuple[Any, Any]] = {}
|
ik_map = {}
|
||||||
else:
|
else:
|
||||||
bones = armature_object.pose.bones
|
bones = armature_object.pose.bones
|
||||||
ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones}
|
ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones}
|
||||||
|
|
||||||
if self.use_property_driver:
|
if self.use_property_driver:
|
||||||
logger.debug("Enabling property drivers for %s", root_object.name)
|
|
||||||
for ik, (b, c) in ik_map.items():
|
for ik, (b, c) in ik_map.items():
|
||||||
driver, variables = __driver_variables(c, "influence")
|
driver, variables = __driver_variables(c, "influence")
|
||||||
driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
|
driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
|
||||||
@@ -66,7 +60,6 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> No
|
|||||||
driver, variables = __driver_variables(i, prop_hide)
|
driver, variables = __driver_variables(i, prop_hide)
|
||||||
driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name
|
driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name
|
||||||
else:
|
else:
|
||||||
logger.debug("Disabling property drivers for %s", root_object.name)
|
|
||||||
for ik, (b, c) in ik_map.items():
|
for ik, (b, c) in ik_map.items():
|
||||||
c.driver_remove("influence")
|
c.driver_remove("influence")
|
||||||
b = b if c.use_tail else b.parent
|
b = b if c.use_tail else b.parent
|
||||||
@@ -84,35 +77,31 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> No
|
|||||||
# ===========================================
|
# ===========================================
|
||||||
|
|
||||||
|
|
||||||
def _toggleUseToonTexture(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
def _toggleUseToonTexture(self: "MMDRoot", _context):
|
||||||
use_toon = self.use_toon_texture
|
use_toon = self.use_toon_texture
|
||||||
logger.debug("Toggling toon texture to %s for %s", use_toon, self.id_data.name)
|
|
||||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||||
for m in i.data.materials:
|
for m in i.data.materials:
|
||||||
if m:
|
if m:
|
||||||
FnMaterial(m).use_toon_texture(use_toon)
|
FnMaterial(m).use_toon_texture(use_toon)
|
||||||
|
|
||||||
|
|
||||||
def _toggleUseSphereTexture(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
def _toggleUseSphereTexture(self: "MMDRoot", _context):
|
||||||
use_sphere = self.use_sphere_texture
|
use_sphere = self.use_sphere_texture
|
||||||
logger.debug("Toggling sphere texture to %s for %s", use_sphere, self.id_data.name)
|
|
||||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||||
for m in i.data.materials:
|
for m in i.data.materials:
|
||||||
if m:
|
if m:
|
||||||
FnMaterial(m).use_sphere_texture(use_sphere, i)
|
FnMaterial(m).use_sphere_texture(use_sphere, i)
|
||||||
|
|
||||||
|
|
||||||
def _toggleUseSDEF(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
def _toggleUseSDEF(self: "MMDRoot", _context):
|
||||||
mute_sdef = not self.use_sdef
|
mute_sdef = not self.use_sdef
|
||||||
logger.debug("Toggling SDEF to %s for %s", not mute_sdef, self.id_data.name)
|
|
||||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||||
FnSDEF.mute_sdef_set(i, mute_sdef)
|
FnSDEF.mute_sdef_set(i, mute_sdef)
|
||||||
|
|
||||||
|
|
||||||
def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context) -> None:
|
def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
|
||||||
root = self.id_data
|
root = self.id_data
|
||||||
hide = not self.show_meshes
|
hide = not self.show_meshes
|
||||||
logger.debug("Toggling mesh visibility to %s for %s", not hide, root.name)
|
|
||||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||||
i.hide_set(hide)
|
i.hide_set(hide)
|
||||||
i.hide_render = hide
|
i.hide_render = hide
|
||||||
@@ -120,30 +109,27 @@ def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context) -> No
|
|||||||
FnContext.set_active_object(context, root)
|
FnContext.set_active_object(context, root)
|
||||||
|
|
||||||
|
|
||||||
def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context) -> None:
|
def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context):
|
||||||
root = self.id_data
|
root = self.id_data
|
||||||
hide = not self.show_rigid_bodies
|
hide = not self.show_rigid_bodies
|
||||||
logger.debug("Toggling rigid body visibility to %s for %s", not hide, root.name)
|
|
||||||
for i in FnModel.iterate_rigid_body_objects(root):
|
for i in FnModel.iterate_rigid_body_objects(root):
|
||||||
i.hide_set(hide)
|
i.hide_set(hide)
|
||||||
if hide and context.active_object is None:
|
if hide and context.active_object is None:
|
||||||
FnContext.set_active_object(context, root)
|
FnContext.set_active_object(context, root)
|
||||||
|
|
||||||
|
|
||||||
def _toggleVisibilityOfJoints(self: "MMDRoot", context: bpy.types.Context) -> None:
|
def _toggleVisibilityOfJoints(self: "MMDRoot", context):
|
||||||
root_object = self.id_data
|
root_object = self.id_data
|
||||||
hide = not self.show_joints
|
hide = not self.show_joints
|
||||||
logger.debug("Toggling joint visibility to %s for %s", not hide, root_object.name)
|
|
||||||
for i in FnModel.iterate_joint_objects(root_object):
|
for i in FnModel.iterate_joint_objects(root_object):
|
||||||
i.hide_set(hide)
|
i.hide_set(hide)
|
||||||
if hide and context.active_object is None:
|
if hide and context.active_object is None:
|
||||||
FnContext.set_active_object(context, root_object)
|
FnContext.set_active_object(context, root_object)
|
||||||
|
|
||||||
|
|
||||||
def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context) -> None:
|
def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context):
|
||||||
root_object: bpy.types.Object = self.id_data
|
root_object: bpy.types.Object = self.id_data
|
||||||
hide = not self.show_temporary_objects
|
hide = not self.show_temporary_objects
|
||||||
logger.debug("Toggling temporary object visibility to %s for %s", not hide, root_object.name)
|
|
||||||
with FnContext.temp_override_active_layer_collection(context, root_object):
|
with FnContext.temp_override_active_layer_collection(context, root_object):
|
||||||
for i in FnModel.iterate_temporary_objects(root_object):
|
for i in FnModel.iterate_temporary_objects(root_object):
|
||||||
i.hide_set(hide)
|
i.hide_set(hide)
|
||||||
@@ -151,48 +137,45 @@ def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Cont
|
|||||||
FnContext.set_active_object(context, root_object)
|
FnContext.set_active_object(context, root_object)
|
||||||
|
|
||||||
|
|
||||||
def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context):
|
||||||
root = self.id_data
|
root = self.id_data
|
||||||
show_names = root.mmd_root.show_names_of_rigid_bodies
|
show_names = root.mmd_root.show_names_of_rigid_bodies
|
||||||
logger.debug("Toggling rigid body names to %s for %s", show_names, root.name)
|
|
||||||
for i in FnModel.iterate_rigid_body_objects(root):
|
for i in FnModel.iterate_rigid_body_objects(root):
|
||||||
i.show_name = show_names
|
i.show_name = show_names
|
||||||
|
|
||||||
|
|
||||||
def _toggleShowNamesOfJoints(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
def _toggleShowNamesOfJoints(self: "MMDRoot", _context):
|
||||||
root = self.id_data
|
root = self.id_data
|
||||||
show_names = root.mmd_root.show_names_of_joints
|
show_names = root.mmd_root.show_names_of_joints
|
||||||
logger.debug("Toggling joint names to %s for %s", show_names, root.name)
|
|
||||||
for i in FnModel.iterate_joint_objects(root):
|
for i in FnModel.iterate_joint_objects(root):
|
||||||
i.show_name = show_names
|
i.show_name = show_names
|
||||||
|
|
||||||
|
|
||||||
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool) -> None:
|
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool):
|
||||||
root = prop.id_data
|
root = prop.id_data
|
||||||
arm = FnModel.find_armature_object(root)
|
arm = FnModel.find_armature_object(root)
|
||||||
if arm is None:
|
if arm is None:
|
||||||
return
|
return
|
||||||
if not v and bpy.context.active_object == arm:
|
if not v and bpy.context.active_object == arm:
|
||||||
FnContext.set_active_object(bpy.context, root)
|
FnContext.set_active_object(bpy.context, root)
|
||||||
logger.debug("Setting armature visibility to %s for %s", v, root.name)
|
|
||||||
arm.hide_set(not v)
|
arm.hide_set(not v)
|
||||||
|
|
||||||
|
|
||||||
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot") -> bool:
|
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"):
|
||||||
if prop.id_data.mmd_type != "ROOT":
|
if prop.id_data.mmd_type != "ROOT":
|
||||||
return False
|
return False
|
||||||
arm = FnModel.find_armature_object(prop.id_data)
|
arm = FnModel.find_armature_object(prop.id_data)
|
||||||
return arm and not arm.hide_get()
|
return arm is not None and not arm.hide_get()
|
||||||
|
|
||||||
|
|
||||||
def _setActiveRigidbodyObject(prop: "MMDRoot", v: int) -> None:
|
def _setActiveRigidbodyObject(prop: "MMDRoot", v: int):
|
||||||
obj = FnContext.get_scene_objects(bpy.context)[v]
|
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||||
if FnModel.is_rigid_body_object(obj):
|
if FnModel.is_rigid_body_object(obj):
|
||||||
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||||
prop["active_rigidbody_object_index"] = v
|
prop["active_rigidbody_object_index"] = v
|
||||||
|
|
||||||
|
|
||||||
def _getActiveRigidbodyObject(prop: "MMDRoot") -> int:
|
def _getActiveRigidbodyObject(prop: "MMDRoot"):
|
||||||
context = bpy.context
|
context = bpy.context
|
||||||
active_obj = FnContext.get_active_object(context)
|
active_obj = FnContext.get_active_object(context)
|
||||||
if FnModel.is_rigid_body_object(active_obj):
|
if FnModel.is_rigid_body_object(active_obj):
|
||||||
@@ -200,14 +183,14 @@ def _getActiveRigidbodyObject(prop: "MMDRoot") -> int:
|
|||||||
return prop.get("active_rigidbody_object_index", 0)
|
return prop.get("active_rigidbody_object_index", 0)
|
||||||
|
|
||||||
|
|
||||||
def _setActiveJointObject(prop: "MMDRoot", v: int) -> None:
|
def _setActiveJointObject(prop: "MMDRoot", v: int):
|
||||||
obj = FnContext.get_scene_objects(bpy.context)[v]
|
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||||
if FnModel.is_joint_object(obj):
|
if FnModel.is_joint_object(obj):
|
||||||
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||||
prop["active_joint_object_index"] = v
|
prop["active_joint_object_index"] = v
|
||||||
|
|
||||||
|
|
||||||
def _getActiveJointObject(prop: "MMDRoot") -> int:
|
def _getActiveJointObject(prop: "MMDRoot"):
|
||||||
context = bpy.context
|
context = bpy.context
|
||||||
active_obj = FnContext.get_active_object(context)
|
active_obj = FnContext.get_active_object(context)
|
||||||
if FnModel.is_joint_object(active_obj):
|
if FnModel.is_joint_object(active_obj):
|
||||||
@@ -215,26 +198,26 @@ def _getActiveJointObject(prop: "MMDRoot") -> int:
|
|||||||
return prop.get("active_joint_object_index", 0)
|
return prop.get("active_joint_object_index", 0)
|
||||||
|
|
||||||
|
|
||||||
def _setActiveMorph(prop: "MMDRoot", v: bool) -> None:
|
def _setActiveMorph(prop: "MMDRoot", v: bool):
|
||||||
if "active_morph_indices" not in prop:
|
if "active_morph_indices" not in prop:
|
||||||
prop["active_morph_indices"] = [0] * 5
|
prop["active_morph_indices"] = [0] * 5
|
||||||
prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v
|
prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v
|
||||||
|
|
||||||
|
|
||||||
def _getActiveMorph(prop: "MMDRoot") -> int:
|
def _getActiveMorph(prop: "MMDRoot"):
|
||||||
if "active_morph_indices" in prop:
|
if "active_morph_indices" in prop:
|
||||||
return prop["active_morph_indices"][prop.get("active_morph_type", 3)]
|
return prop["active_morph_indices"][prop.get("active_morph_type", 3)]
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _setActiveMeshObject(prop: "MMDRoot", v: int) -> None:
|
def _setActiveMeshObject(prop: "MMDRoot", v: int):
|
||||||
obj = FnContext.get_scene_objects(bpy.context)[v]
|
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||||
if FnModel.is_mesh_object(obj):
|
if FnModel.is_mesh_object(obj):
|
||||||
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||||
prop["active_mesh_index"] = v
|
prop["active_mesh_index"] = v
|
||||||
|
|
||||||
|
|
||||||
def _getActiveMeshObject(prop: "MMDRoot") -> int:
|
def _getActiveMeshObject(prop: "MMDRoot"):
|
||||||
context = bpy.context
|
context = bpy.context
|
||||||
active_obj = FnContext.get_active_object(context)
|
active_obj = FnContext.get_active_object(context)
|
||||||
if FnModel.is_mesh_object(active_obj):
|
if FnModel.is_mesh_object(active_obj):
|
||||||
@@ -393,6 +376,18 @@ class MMDRoot(bpy.types.PropertyGroup):
|
|||||||
update=_toggleShowNamesOfJoints,
|
update=_toggleShowNamesOfJoints,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
show_japanese_name: bpy.props.BoolProperty(
|
||||||
|
name="Japanese name",
|
||||||
|
description="Toggle Japanese name display",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
show_english_name: bpy.props.BoolProperty(
|
||||||
|
name="English name",
|
||||||
|
description="Toggle English name display",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
use_toon_texture: bpy.props.BoolProperty(
|
use_toon_texture: bpy.props.BoolProperty(
|
||||||
name="Use Toon Texture",
|
name="Use Toon Texture",
|
||||||
description="Use toon texture",
|
description="Use toon texture",
|
||||||
@@ -453,6 +448,15 @@ class MMDRoot(bpy.types.PropertyGroup):
|
|||||||
default=0,
|
default=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# *************************
|
||||||
|
# Bone
|
||||||
|
# *************************
|
||||||
|
active_bone_index: bpy.props.IntProperty(
|
||||||
|
name="Active Bone Index",
|
||||||
|
description="Index of the active bone in the armature",
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
# *************************
|
# *************************
|
||||||
# Morph
|
# Morph
|
||||||
# *************************
|
# *************************
|
||||||
@@ -513,29 +517,40 @@ class MMDRoot(bpy.types.PropertyGroup):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_select(prop: bpy.types.Object) -> bool:
|
def __get_select(prop: bpy.types.Object) -> bool:
|
||||||
utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead")
|
# TODO: Object.select is deprecated since v4.0.0, use Object.select_get() method instead
|
||||||
|
# utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead")
|
||||||
return prop.select_get()
|
return prop.select_get()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __set_select(prop: bpy.types.Object, value: bool) -> None:
|
def __set_select(prop: bpy.types.Object, value: bool) -> None:
|
||||||
utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead")
|
# TODO: Object.select is deprecated since v4.0.0, use Object.select_set() method instead
|
||||||
|
# utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead")
|
||||||
prop.select_set(value)
|
prop.select_set(value)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_hide(prop: bpy.types.Object) -> bool:
|
def __get_hide(prop: bpy.types.Object) -> bool:
|
||||||
utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead")
|
# TODO: Object.hide is deprecated since v4.0.0, use Object.hide_get() method instead
|
||||||
|
# utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead")
|
||||||
return prop.hide_get()
|
return prop.hide_get()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __set_hide(prop: bpy.types.Object, value: bool) -> None:
|
def __set_hide(prop: bpy.types.Object, value: bool) -> None:
|
||||||
utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead")
|
# TODO: Object.hide is deprecated since v4.0.0, use Object.hide_set() method instead
|
||||||
|
# utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead")
|
||||||
prop.hide_set(value)
|
prop.hide_set(value)
|
||||||
if prop.hide_viewport != value:
|
if prop.hide_viewport != value:
|
||||||
prop.hide_viewport = value
|
prop.hide_viewport = value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register() -> None:
|
def __get_pose_bone_select(prop: bpy.types.PoseBone) -> bool:
|
||||||
logger.debug("Registering MMDRoot property group")
|
return prop.bone.select
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __set_pose_bone_select(prop: bpy.types.PoseBone, value: bool) -> None:
|
||||||
|
prop.bone.select = value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def register():
|
||||||
bpy.types.Object.mmd_type = patch_library_overridable(
|
bpy.types.Object.mmd_type = patch_library_overridable(
|
||||||
bpy.props.EnumProperty(
|
bpy.props.EnumProperty(
|
||||||
name="Type",
|
name="Type",
|
||||||
@@ -557,7 +572,7 @@ class MMDRoot(bpy.types.PropertyGroup):
|
|||||||
("SPRING_CONSTRAINT", "Spring Constraint", "", 53),
|
("SPRING_CONSTRAINT", "Spring Constraint", "", 53),
|
||||||
("SPRING_GOAL", "Spring Goal", "", 54),
|
("SPRING_GOAL", "Spring Goal", "", 54),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
bpy.types.Object.mmd_root = patch_library_overridable(bpy.props.PointerProperty(type=MMDRoot))
|
bpy.types.Object.mmd_root = patch_library_overridable(bpy.props.PointerProperty(type=MMDRoot))
|
||||||
|
|
||||||
@@ -570,7 +585,7 @@ class MMDRoot(bpy.types.PropertyGroup):
|
|||||||
"ANIMATABLE",
|
"ANIMATABLE",
|
||||||
"LIBRARY_EDITABLE",
|
"LIBRARY_EDITABLE",
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
bpy.types.Object.hide = patch_library_overridable(
|
bpy.types.Object.hide = patch_library_overridable(
|
||||||
bpy.props.BoolProperty(
|
bpy.props.BoolProperty(
|
||||||
@@ -581,13 +596,29 @@ class MMDRoot(bpy.types.PropertyGroup):
|
|||||||
"ANIMATABLE",
|
"ANIMATABLE",
|
||||||
"LIBRARY_EDITABLE",
|
"LIBRARY_EDITABLE",
|
||||||
},
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not IS_BLENDER_50_UP:
|
||||||
|
bpy.types.PoseBone.select = patch_library_overridable(
|
||||||
|
bpy.props.BoolProperty(
|
||||||
|
name="Select",
|
||||||
|
description="Pose bone selection state (compatibility layer for Blender 4.x, forwards to bone.select)",
|
||||||
|
get=MMDRoot.__get_pose_bone_select,
|
||||||
|
set=MMDRoot.__set_pose_bone_select,
|
||||||
|
options={
|
||||||
|
"SKIP_SAVE",
|
||||||
|
"ANIMATABLE",
|
||||||
|
"LIBRARY_EDITABLE",
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unregister() -> None:
|
def unregister():
|
||||||
logger.debug("Unregistering MMDRoot property group")
|
|
||||||
del bpy.types.Object.hide
|
del bpy.types.Object.hide
|
||||||
del bpy.types.Object.select
|
del bpy.types.Object.select
|
||||||
del bpy.types.Object.mmd_root
|
del bpy.types.Object.mmd_root
|
||||||
del bpy.types.Object.mmd_type
|
del bpy.types.Object.mmd_type
|
||||||
|
if not IS_BLENDER_50_UP:
|
||||||
|
del bpy.types.PoseBone.select
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2021 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
|
||||||
|
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
|||||||
+42
-103
@@ -1,25 +1,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2016 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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 csv
|
import csv
|
||||||
|
from ...core.logging_setup import logger
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
from typing import List, Tuple, Dict, Optional, Any, Generator, Union, TextIO, Iterator, Set
|
from collections import OrderedDict
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Text, Context
|
|
||||||
|
|
||||||
from .bpyutils import FnContext
|
from .bpyutils import FnContext
|
||||||
from ..logging_setup import logger
|
|
||||||
|
|
||||||
# Type definitions for translation tuples
|
jp_half_to_full_tuples = (
|
||||||
TranslationTuple = Tuple[str, str]
|
|
||||||
TranslationList = List[TranslationTuple]
|
|
||||||
|
|
||||||
jp_half_to_full_tuples: TranslationList = (
|
|
||||||
("ヴ", "ヴ"),
|
("ヴ", "ヴ"),
|
||||||
("ガ", "ガ"),
|
("ガ", "ガ"),
|
||||||
("ギ", "ギ"),
|
("ギ", "ギ"),
|
||||||
@@ -109,7 +101,7 @@ jp_half_to_full_tuples: TranslationList = (
|
|||||||
("ン", "ン"),
|
("ン", "ン"),
|
||||||
)
|
)
|
||||||
|
|
||||||
jp_to_en_tuples: TranslationList = [
|
jp_to_en_tuples = [
|
||||||
("全ての親", "ParentNode"),
|
("全ての親", "ParentNode"),
|
||||||
("操作中心", "ControlNode"),
|
("操作中心", "ControlNode"),
|
||||||
("センター", "Center"),
|
("センター", "Center"),
|
||||||
@@ -299,30 +291,22 @@ jp_to_en_tuples: TranslationList = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def translateFromJp(name: str) -> str:
|
def translateFromJp(name):
|
||||||
"""Translate a Japanese name to English using the translation tuples."""
|
for t in jp_to_en_tuples:
|
||||||
logger.debug(f"Translating from Japanese: {name}")
|
if t[0] in name:
|
||||||
for tuple in jp_to_en_tuples:
|
name = name.replace(t[0], t[1])
|
||||||
if tuple[0] in name:
|
|
||||||
name = name.replace(tuple[0], tuple[1])
|
|
||||||
logger.debug(f"Translation result: {name}")
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
def getTranslator(csvfile: Union[str, Dict[str, str], Text] = "", keep_order: bool = False) -> 'MMDTranslator':
|
def getTranslator(csvfile="", keep_order=False):
|
||||||
"""Get a translator instance with the specified CSV file."""
|
|
||||||
translator = MMDTranslator()
|
translator = MMDTranslator()
|
||||||
if isinstance(csvfile, bpy.types.Text):
|
if isinstance(csvfile, bpy.types.Text):
|
||||||
logger.debug(f"Loading translator from Text object: {csvfile.name}")
|
|
||||||
translator.load_from_stream(csvfile)
|
translator.load_from_stream(csvfile)
|
||||||
elif isinstance(csvfile, dict):
|
elif isinstance(csvfile, dict):
|
||||||
logger.debug(f"Loading translator from dictionary with {len(csvfile)} entries")
|
|
||||||
translator.csv_tuples.extend(csvfile.items())
|
translator.csv_tuples.extend(csvfile.items())
|
||||||
elif csvfile in bpy.data.texts.keys():
|
elif csvfile in bpy.data.texts.keys():
|
||||||
logger.debug(f"Loading translator from text data: {csvfile}")
|
|
||||||
translator.load_from_stream(bpy.data.texts[csvfile])
|
translator.load_from_stream(bpy.data.texts[csvfile])
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Loading translator from file: {csvfile}")
|
|
||||||
translator.load(csvfile)
|
translator.load(csvfile)
|
||||||
|
|
||||||
if not keep_order:
|
if not keep_order:
|
||||||
@@ -332,20 +316,16 @@ def getTranslator(csvfile: Union[str, Dict[str, str], Text] = "", keep_order: bo
|
|||||||
|
|
||||||
|
|
||||||
class MMDTranslator:
|
class MMDTranslator:
|
||||||
"""Handles translation of Japanese text to English for MMD models."""
|
def __init__(self):
|
||||||
|
self.__csv_tuples = []
|
||||||
def __init__(self) -> None:
|
self.__fails = {}
|
||||||
self.__csv_tuples: List[Tuple[str, str]] = []
|
|
||||||
self.__fails: Dict[str, str] = {}
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def default_csv_filepath() -> str:
|
def default_csv_filepath():
|
||||||
"""Get the default CSV filepath for translations."""
|
|
||||||
return __file__[:-3] + ".csv"
|
return __file__[:-3] + ".csv"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_csv_text(text_name: Optional[str] = None) -> Text:
|
def get_csv_text(text_name=None):
|
||||||
"""Get or create a Text object for CSV data."""
|
|
||||||
text_name = text_name or bpy.path.basename(MMDTranslator.default_csv_filepath())
|
text_name = text_name or bpy.path.basename(MMDTranslator.default_csv_filepath())
|
||||||
csv_text = bpy.data.texts.get(text_name, None)
|
csv_text = bpy.data.texts.get(text_name, None)
|
||||||
if csv_text is None:
|
if csv_text is None:
|
||||||
@@ -353,88 +333,67 @@ class MMDTranslator:
|
|||||||
return csv_text
|
return csv_text
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def replace_from_tuples(name: str, tuples: List[Tuple[str, str]]) -> str:
|
def replace_from_tuples(name, tuples):
|
||||||
"""Replace parts of a string based on translation tuples."""
|
|
||||||
for pair in tuples:
|
for pair in tuples:
|
||||||
if pair[0] in name:
|
if pair[0] in name:
|
||||||
name = name.replace(pair[0], pair[1])
|
name = name.replace(pair[0], pair[1])
|
||||||
return name
|
return name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def csv_tuples(self) -> List[Tuple[str, str]]:
|
def csv_tuples(self):
|
||||||
"""Get the CSV tuples."""
|
|
||||||
return self.__csv_tuples
|
return self.__csv_tuples
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fails(self) -> Dict[str, str]:
|
def fails(self):
|
||||||
"""Get the failed translations."""
|
|
||||||
return self.__fails
|
return self.__fails
|
||||||
|
|
||||||
def sort(self) -> None:
|
def sort(self):
|
||||||
"""Sort the CSV tuples by length (longest first) and then alphabetically."""
|
|
||||||
logger.debug("Sorting translation tuples")
|
|
||||||
self.__csv_tuples.sort(key=lambda row: (-len(row[0]), row))
|
self.__csv_tuples.sort(key=lambda row: (-len(row[0]), row))
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self):
|
||||||
"""Update the CSV tuples, removing duplicates."""
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
count_old = len(self.__csv_tuples)
|
count_old = len(self.__csv_tuples)
|
||||||
tuples_dict = OrderedDict((row[0], row) for row in self.__csv_tuples if len(row) >= 2 and row[0])
|
tuples_dict = OrderedDict((row[0], row) for row in self.__csv_tuples if len(row) >= 2 and row[0])
|
||||||
self.__csv_tuples.clear()
|
self.__csv_tuples.clear()
|
||||||
self.__csv_tuples.extend(tuples_dict.values())
|
self.__csv_tuples.extend(tuples_dict.values())
|
||||||
logger.info("Translation update - removed items: %d (of %d)", count_old - len(self.__csv_tuples), count_old)
|
logger.info(" - removed items:\t%d\t(of %d)", count_old - len(self.__csv_tuples), count_old)
|
||||||
|
|
||||||
def half_to_full(self, name: str) -> str:
|
def half_to_full(self, name):
|
||||||
"""Convert half-width Japanese characters to full-width."""
|
|
||||||
return self.replace_from_tuples(name, jp_half_to_full_tuples)
|
return self.replace_from_tuples(name, jp_half_to_full_tuples)
|
||||||
|
|
||||||
def is_translated(self, name: str) -> bool:
|
def is_translated(self, name):
|
||||||
"""Check if a string is already translated (contains only ASCII characters)."""
|
|
||||||
try:
|
try:
|
||||||
name.encode("ascii", errors="strict")
|
name.encode("ascii", errors="strict")
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def translate(self, name: str, default: Optional[str] = None, from_full_width: bool = True) -> str:
|
def translate(self, name, default=None, from_full_width=True):
|
||||||
"""Translate a string from Japanese to English."""
|
|
||||||
logger.debug(f"Translating: {name}")
|
|
||||||
if from_full_width:
|
if from_full_width:
|
||||||
name = self.half_to_full(name)
|
name = self.half_to_full(name)
|
||||||
name_new = self.replace_from_tuples(name, self.__csv_tuples)
|
name_new = self.replace_from_tuples(name, self.__csv_tuples)
|
||||||
if default is not None and not self.is_translated(name_new):
|
if default is not None and not self.is_translated(name_new):
|
||||||
logger.warning(f"Translation failed for: {name}")
|
|
||||||
self.__fails[name] = name_new
|
self.__fails[name] = name_new
|
||||||
return default
|
return default
|
||||||
return name_new
|
return name_new
|
||||||
|
|
||||||
def save_fails(self, text_name: Optional[str] = None) -> Text:
|
def save_fails(self, text_name=None):
|
||||||
"""Save failed translations to a Text object."""
|
|
||||||
text_name = text_name or (__name__ + ".fails")
|
text_name = text_name or (__name__ + ".fails")
|
||||||
txt = self.get_csv_text(text_name)
|
txt = self.get_csv_text(text_name)
|
||||||
fmt = '"%s","%s"'
|
fmt = '"%s","%s"'
|
||||||
items = sorted(self.__fails.items(), key=lambda row: (-len(row[0]), row))
|
items = sorted(self.__fails.items(), key=lambda row: (-len(row[0]), row))
|
||||||
txt.from_string("\n".join(fmt % (k, v) for k, v in items))
|
txt.from_string("\n".join(fmt % (k, v) for k, v in items))
|
||||||
logger.info(f"Saved {len(items)} failed translations to {text_name}")
|
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def load_from_stream(self, csvfile: Union[Text, Iterator[str]] = None) -> None:
|
def load_from_stream(self, csvfile=None):
|
||||||
"""Load translations from a stream."""
|
|
||||||
csvfile = csvfile or self.get_csv_text()
|
csvfile = csvfile or self.get_csv_text()
|
||||||
if isinstance(csvfile, bpy.types.Text):
|
if isinstance(csvfile, bpy.types.Text):
|
||||||
csvfile = (l.body + "\n" for l in csvfile.lines)
|
csvfile = (line.body + "\n" for line in csvfile.lines)
|
||||||
spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True)
|
spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True)
|
||||||
csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2]
|
csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2]
|
||||||
self.__csv_tuples = csv_tuples
|
self.__csv_tuples = csv_tuples
|
||||||
logger.info("Loaded %d translation items", len(self.__csv_tuples))
|
logger.info(" - load items:\t%d", len(self.__csv_tuples))
|
||||||
|
|
||||||
def save_to_stream(self, csvfile: Union[Text, TextIO] = None) -> None:
|
def save_to_stream(self, csvfile=None):
|
||||||
"""Save translations to a stream.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
csvfile: The CSV file or stream to save to
|
|
||||||
"""
|
|
||||||
csvfile = csvfile or self.get_csv_text()
|
csvfile = csvfile or self.get_csv_text()
|
||||||
lineterminator = "\r\n"
|
lineterminator = "\r\n"
|
||||||
if isinstance(csvfile, bpy.types.Text):
|
if isinstance(csvfile, bpy.types.Text):
|
||||||
@@ -442,38 +401,27 @@ class MMDTranslator:
|
|||||||
lineterminator = "\n"
|
lineterminator = "\n"
|
||||||
spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL)
|
spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL)
|
||||||
spamwriter.writerows(self.__csv_tuples)
|
spamwriter.writerows(self.__csv_tuples)
|
||||||
logger.info("Saved %d translation items", len(self.__csv_tuples))
|
logger.info(" - save items:\t%d", len(self.__csv_tuples))
|
||||||
|
|
||||||
def load(self, filepath: Optional[str] = None) -> None:
|
def load(self, filepath=None):
|
||||||
"""Load translations from a file."""
|
|
||||||
filepath = filepath or self.default_csv_filepath()
|
filepath = filepath or self.default_csv_filepath()
|
||||||
logger.info("Loading CSV file: %s", filepath)
|
logger.info("Loading csv file:\t%s", filepath)
|
||||||
try:
|
with open(filepath, encoding="utf-8", newline="") as csvfile:
|
||||||
with open(filepath, "rt", encoding="utf-8", newline="") as csvfile:
|
|
||||||
self.load_from_stream(csvfile)
|
self.load_from_stream(csvfile)
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to load CSV file: {e}")
|
|
||||||
|
|
||||||
def save(self, filepath: Optional[str] = None) -> None:
|
def save(self, filepath=None):
|
||||||
"""Save translations to a file."""
|
|
||||||
filepath = filepath or self.default_csv_filepath()
|
filepath = filepath or self.default_csv_filepath()
|
||||||
logger.info("Saving CSV file: %s", filepath)
|
logger.info("Saving csv file:\t%s", filepath)
|
||||||
try:
|
with open(filepath, "w", encoding="utf-8", newline="") as csvfile:
|
||||||
with open(filepath, "wt", encoding="utf-8", newline="") as csvfile:
|
|
||||||
self.save_to_stream(csvfile)
|
self.save_to_stream(csvfile)
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to save CSV file: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
class DictionaryEnum:
|
class DictionaryEnum:
|
||||||
"""Handles dictionary enumeration for UI."""
|
__items_ttl = 0.0
|
||||||
|
__items_cache = None
|
||||||
__items_ttl: float = 0.0
|
|
||||||
__items_cache: Optional[List[Tuple[str, str, str, int]]] = None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_dictionary_items(prop: Any, context: Context) -> List[Tuple[str, str, str, Union[int, str], int]]:
|
def get_dictionary_items(prop, context):
|
||||||
"""Get dictionary items for UI enumeration."""
|
|
||||||
if DictionaryEnum.__items_ttl > time.time():
|
if DictionaryEnum.__items_ttl > time.time():
|
||||||
return DictionaryEnum.__items_cache
|
return DictionaryEnum.__items_cache
|
||||||
|
|
||||||
@@ -487,8 +435,6 @@ class DictionaryEnum:
|
|||||||
for txt_name in sorted(x.name for x in bpy.data.texts if x.name.lower().endswith(".csv")):
|
for txt_name in sorted(x.name for x in bpy.data.texts if x.name.lower().endswith(".csv")):
|
||||||
items.append((txt_name, txt_name, f"bpy.data.texts['{txt_name}']", "TEXT", len(items)))
|
items.append((txt_name, txt_name, f"bpy.data.texts['{txt_name}']", "TEXT", len(items)))
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
folder = FnContext.get_addon_preferences_attribute(context, "dictionary_folder", "")
|
folder = FnContext.get_addon_preferences_attribute(context, "dictionary_folder", "")
|
||||||
if os.path.isdir(folder):
|
if os.path.isdir(folder):
|
||||||
for filename in sorted(x for x in os.listdir(folder) if x.lower().endswith(".csv")):
|
for filename in sorted(x for x in os.listdir(folder) if x.lower().endswith(".csv")):
|
||||||
@@ -498,19 +444,12 @@ class DictionaryEnum:
|
|||||||
|
|
||||||
if "dictionary" in prop:
|
if "dictionary" in prop:
|
||||||
prop["dictionary"] = min(prop["dictionary"], len(items) - 1)
|
prop["dictionary"] = min(prop["dictionary"], len(items) - 1)
|
||||||
|
|
||||||
logger.debug(f"Found {len(items)} dictionary items")
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_translator(dictionary: str) -> Optional[MMDTranslator]:
|
def get_translator(dictionary):
|
||||||
"""Get a translator for the specified dictionary."""
|
|
||||||
if dictionary == "DISABLED":
|
if dictionary == "DISABLED":
|
||||||
logger.debug("Translation disabled")
|
|
||||||
return None
|
return None
|
||||||
if dictionary == "INTERNAL":
|
if dictionary == "INTERNAL":
|
||||||
logger.debug("Using internal dictionary")
|
|
||||||
return getTranslator(dict(jp_to_en_tuples))
|
return getTranslator(dict(jp_to_en_tuples))
|
||||||
|
|
||||||
logger.debug(f"Using dictionary: {dictionary}")
|
|
||||||
return getTranslator(dictionary)
|
return getTranslator(dictionary)
|
||||||
|
|||||||
+89
-61
@@ -1,23 +1,20 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2012 MMD Tools authors
|
||||||
# Copyright 2014 MMD Tools authors
|
# This file is part of MMD Tools.
|
||||||
# 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.
|
|
||||||
|
|
||||||
|
from ...core.logging_setup import logger
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Callable, Dict, List, Optional, Set, Tuple, Union, Any
|
import string
|
||||||
|
from typing import Callable, Optional, Set
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Object, Bone, PoseBone, Mesh, VertexGroup
|
import numpy as np
|
||||||
|
|
||||||
from ..logging_setup import logger
|
|
||||||
from .bpyutils import FnContext
|
from .bpyutils import FnContext
|
||||||
|
|
||||||
|
|
||||||
## 指定したオブジェクトのみを選択状態かつアクティブにする
|
# 指定したオブジェクトのみを選択状態かつアクティブにする
|
||||||
def selectAObject(obj: Object) -> None:
|
def selectAObject(obj):
|
||||||
try:
|
try:
|
||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -27,14 +24,14 @@ def selectAObject(obj: Object) -> None:
|
|||||||
FnContext.set_active_object(FnContext.ensure_context(), obj)
|
FnContext.set_active_object(FnContext.ensure_context(), obj)
|
||||||
|
|
||||||
|
|
||||||
## 現在のモードを指定したオブジェクトのEdit Modeに変更する
|
# 現在のモードを指定したオブジェクトのEdit Modeに変更する
|
||||||
def enterEditMode(obj: Object) -> None:
|
def enterEditMode(obj):
|
||||||
selectAObject(obj)
|
selectAObject(obj)
|
||||||
if obj.mode != "EDIT":
|
if obj.mode != "EDIT":
|
||||||
bpy.ops.object.mode_set(mode="EDIT")
|
bpy.ops.object.mode_set(mode="EDIT")
|
||||||
|
|
||||||
|
|
||||||
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
|
def setParentToBone(obj, parent, bone_name):
|
||||||
selectAObject(obj)
|
selectAObject(obj)
|
||||||
FnContext.set_active_object(FnContext.ensure_context(), parent)
|
FnContext.set_active_object(FnContext.ensure_context(), parent)
|
||||||
bpy.ops.object.mode_set(mode="POSE")
|
bpy.ops.object.mode_set(mode="POSE")
|
||||||
@@ -43,11 +40,11 @@ def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
|
|||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
|
||||||
|
|
||||||
def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None:
|
def selectSingleBone(context, armature, bone_name, reset_pose=False):
|
||||||
try:
|
try:
|
||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.warning(f"Failed to set object mode: {e}")
|
||||||
for i in context.selected_objects:
|
for i in context.selected_objects:
|
||||||
i.select_set(False)
|
i.select_set(False)
|
||||||
FnContext.set_active_object(context, armature)
|
FnContext.set_active_object(context, armature)
|
||||||
@@ -55,22 +52,21 @@ def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: st
|
|||||||
if reset_pose:
|
if reset_pose:
|
||||||
for p_bone in armature.pose.bones:
|
for p_bone in armature.pose.bones:
|
||||||
p_bone.matrix_basis.identity()
|
p_bone.matrix_basis.identity()
|
||||||
armature_bones: bpy.types.ArmatureBones = armature.data.bones
|
|
||||||
i: Bone
|
for p_bone in armature.pose.bones:
|
||||||
for i in armature_bones:
|
is_target = p_bone.name == bone_name
|
||||||
i.select = i.name == bone_name
|
p_bone.select = is_target
|
||||||
i.select_head = i.select_tail = i.select
|
if is_target:
|
||||||
if i.select:
|
armature.data.bones.active = p_bone.bone
|
||||||
armature_bones.active = i
|
p_bone.bone.hide = False
|
||||||
i.hide = False
|
|
||||||
|
|
||||||
|
|
||||||
__CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$")
|
__CONVERT_NAME_TO_L_REGEXP = re.compile(r"^(.*)左(.*)$")
|
||||||
__CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$")
|
__CONVERT_NAME_TO_R_REGEXP = re.compile(r"^(.*)右(.*)$")
|
||||||
|
|
||||||
|
|
||||||
## 日本語で左右を命名されている名前をblender方式のL(R)に変更する
|
# 日本語で左右を命名されている名前をblender方式のL(R)に変更する
|
||||||
def convertNameToLR(name: str, use_underscore: bool = False) -> str:
|
def convertNameToLR(name, use_underscore=False):
|
||||||
m = __CONVERT_NAME_TO_L_REGEXP.match(name)
|
m = __CONVERT_NAME_TO_L_REGEXP.match(name)
|
||||||
delimiter = "_" if use_underscore else "."
|
delimiter = "_" if use_underscore else "."
|
||||||
if m:
|
if m:
|
||||||
@@ -85,7 +81,7 @@ __CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[lL])(?P<aft
|
|||||||
__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))")
|
__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))")
|
||||||
|
|
||||||
|
|
||||||
def convertLRToName(name: str) -> str:
|
def convertLRToName(name):
|
||||||
match = __CONVERT_L_TO_NAME_REGEXP.search(name)
|
match = __CONVERT_L_TO_NAME_REGEXP.search(name)
|
||||||
if match:
|
if match:
|
||||||
return f"左{name[0:match.start()]}{match['after']}{name[match.end():]}"
|
return f"左{name[0:match.start()]}{match['after']}{name[match.end():]}"
|
||||||
@@ -97,8 +93,8 @@ def convertLRToName(name: str) -> str:
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
## src_vertex_groupのWeightをdest_vertex_groupにaddする
|
# src_vertex_groupのWeightをdest_vertex_groupにaddする
|
||||||
def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None:
|
def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name):
|
||||||
mesh = meshObj.data
|
mesh = meshObj.data
|
||||||
src_vertex_group = meshObj.vertex_groups[src_vertex_group_name]
|
src_vertex_group = meshObj.vertex_groups[src_vertex_group_name]
|
||||||
dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name]
|
dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name]
|
||||||
@@ -112,43 +108,73 @@ def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_gr
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def separateByMaterials(meshObj: Object) -> None:
|
def separateByMaterials(meshObj: bpy.types.Object, keep_normals: bool = False):
|
||||||
if len(meshObj.data.materials) < 2:
|
meshData = meshObj.data
|
||||||
|
if len(meshData.materials) < 2:
|
||||||
selectAObject(meshObj)
|
selectAObject(meshObj)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
dummy_parent = None
|
||||||
|
try:
|
||||||
|
dummy_parent = bpy.data.objects.new(name="tmp", object_data=None)
|
||||||
matrix_parent_inverse = meshObj.matrix_parent_inverse.copy()
|
matrix_parent_inverse = meshObj.matrix_parent_inverse.copy()
|
||||||
prev_parent = meshObj.parent
|
prev_parent = meshObj.parent
|
||||||
dummy_parent = bpy.data.objects.new(name="tmp", object_data=None)
|
|
||||||
meshObj.parent = dummy_parent
|
meshObj.parent = dummy_parent
|
||||||
meshObj.active_shape_key_index = 0
|
meshObj.active_shape_key_index = 0
|
||||||
|
mmd_normal_name = None # To avoid conflict ("mmd_normal.001", etc.)
|
||||||
|
if keep_normals:
|
||||||
|
existing_custom_normal = meshData.attributes.get("custom_normal")
|
||||||
|
if existing_custom_normal:
|
||||||
|
if existing_custom_normal.data_type == "INT16_2D":
|
||||||
|
normals_data = np.empty(len(meshData.loops) * 2, dtype=np.int16)
|
||||||
|
existing_custom_normal.data.foreach_get("value", normals_data)
|
||||||
|
mmd_normal = meshData.attributes.new("mmd_normal", "INT16_2D", "CORNER")
|
||||||
|
mmd_normal_name = mmd_normal.name
|
||||||
|
mmd_normal.data.foreach_set("value", normals_data)
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Unsupported custom_normal data type: '{existing_custom_normal.data_type}'. Supported types: 'INT16_2D'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
enterEditMode(meshObj)
|
enterEditMode(meshObj)
|
||||||
bpy.ops.mesh.select_all(action="SELECT")
|
|
||||||
bpy.ops.mesh.separate(type="MATERIAL")
|
bpy.ops.mesh.separate(type="MATERIAL")
|
||||||
finally:
|
finally:
|
||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
|
||||||
for i in dummy_parent.children:
|
for i in dummy_parent.children:
|
||||||
materials = i.data.materials
|
materials = i.data.materials
|
||||||
i.name = getattr(materials[0], "name", "None") if len(materials) else "None"
|
i.name = getattr(materials[0], "name", "None") if len(materials) else "None"
|
||||||
i.parent = prev_parent
|
i.parent = prev_parent
|
||||||
i.matrix_parent_inverse = matrix_parent_inverse
|
i.matrix_parent_inverse = matrix_parent_inverse
|
||||||
|
|
||||||
|
if keep_normals and mmd_normal_name:
|
||||||
|
mmd_normal = i.data.attributes.get(mmd_normal_name)
|
||||||
|
if mmd_normal:
|
||||||
|
if mmd_normal.data_type == "INT16_2D":
|
||||||
|
normals_data = np.empty(len(i.data.loops) * 2, dtype=np.int16)
|
||||||
|
mmd_normal.data.foreach_get("value", normals_data)
|
||||||
|
custom_normal_attr = i.data.attributes.get("custom_normal")
|
||||||
|
if not custom_normal_attr:
|
||||||
|
custom_normal_attr = i.data.attributes.new("custom_normal", "INT16_2D", "CORNER")
|
||||||
|
custom_normal_attr.data.foreach_set("value", normals_data)
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Unsupported custom_normal data type: '{mmd_normal.data_type}'. Supported types: 'INT16_2D'")
|
||||||
|
i.data.attributes.remove(mmd_normal)
|
||||||
|
finally:
|
||||||
|
if dummy_parent and dummy_parent.name in bpy.data.objects:
|
||||||
bpy.data.objects.remove(dummy_parent)
|
bpy.data.objects.remove(dummy_parent)
|
||||||
|
|
||||||
|
|
||||||
def clearUnusedMeshes() -> None:
|
def clearUnusedMeshes():
|
||||||
meshes_to_delete = []
|
meshes_to_delete = [mesh for mesh in bpy.data.meshes if mesh.users == 0]
|
||||||
for mesh in bpy.data.meshes:
|
|
||||||
if mesh.users == 0:
|
|
||||||
meshes_to_delete.append(mesh)
|
|
||||||
|
|
||||||
for mesh in meshes_to_delete:
|
for mesh in meshes_to_delete:
|
||||||
bpy.data.meshes.remove(mesh)
|
bpy.data.meshes.remove(mesh)
|
||||||
|
|
||||||
|
|
||||||
## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を
|
# Boneのカスタムプロパティにname_jが存在する場合、name_jの値を
|
||||||
# それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成
|
# それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成
|
||||||
def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]:
|
def makePmxBoneMap(armObj):
|
||||||
# Maintain backward compatibility with mmd_tools v0.4.x or older.
|
# Maintain backward compatibility with mmd_tools_local 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}
|
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}
|
||||||
|
|
||||||
|
|
||||||
@@ -156,7 +182,7 @@ __REMOVE_PREFIX_DIGITS_REGEXP = re.compile(r"\.\d{1,}$")
|
|||||||
|
|
||||||
|
|
||||||
def unique_name(name: str, used_names: Set[str]) -> str:
|
def unique_name(name: str, used_names: Set[str]) -> str:
|
||||||
"""Helper function for storing unique names.
|
"""Generate a unique name from the given name.
|
||||||
This function is a limited and simplified version of bpy_extras.io_utils.unique_name.
|
This function is a limited and simplified version of bpy_extras.io_utils.unique_name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -176,13 +202,11 @@ def unique_name(name: str, used_names: Set[str]) -> str:
|
|||||||
return new_name
|
return new_name
|
||||||
|
|
||||||
|
|
||||||
def int2base(x: int, base: int, width: int = 0) -> str:
|
def int2base(x, base, width=0):
|
||||||
"""
|
"""
|
||||||
Method to convert an int to a base
|
Convert an int to a base
|
||||||
Source: http://stackoverflow.com/questions/2267362
|
Source: http://stackoverflow.com/questions/2267362
|
||||||
"""
|
"""
|
||||||
import string
|
|
||||||
|
|
||||||
digs = string.digits + string.ascii_uppercase
|
digs = string.digits + string.ascii_uppercase
|
||||||
assert 2 <= base <= len(digs)
|
assert 2 <= base <= len(digs)
|
||||||
digits, negtive = "", False
|
digits, negtive = "", False
|
||||||
@@ -199,7 +223,7 @@ def int2base(x: int, base: int, width: int = 0) -> str:
|
|||||||
return digits
|
return digits
|
||||||
|
|
||||||
|
|
||||||
def saferelpath(path: str, start: str, strategy: str = "inside") -> str:
|
def saferelpath(path, start, strategy="inside"):
|
||||||
"""
|
"""
|
||||||
On Windows relpath will raise a ValueError
|
On Windows relpath will raise a ValueError
|
||||||
when trying to calculate the relative path to a
|
when trying to calculate the relative path to a
|
||||||
@@ -226,15 +250,16 @@ def saferelpath(path: str, start: str, strategy: str = "inside") -> str:
|
|||||||
|
|
||||||
return os.path.relpath(path, start)
|
return os.path.relpath(path, start)
|
||||||
|
|
||||||
|
|
||||||
class ItemOp:
|
class ItemOp:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_by_index(items: bpy.types.bpy_prop_collection, index: int) -> Optional[Any]:
|
def get_by_index(items, index):
|
||||||
if 0 <= index < len(items):
|
if 0 <= index < len(items):
|
||||||
return items[index]
|
return items[index]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resize(items: bpy.types.bpy_prop_collection, length: int) -> None:
|
def resize(items: bpy.types.bpy_prop_collection, length: int):
|
||||||
count = length - len(items)
|
count = length - len(items)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
@@ -244,7 +269,7 @@ class ItemOp:
|
|||||||
items.remove(length)
|
items.remove(length)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_after(items: bpy.types.bpy_prop_collection, index: int) -> Tuple[Any, int]:
|
def add_after(items, index):
|
||||||
index_end = len(items)
|
index_end = len(items)
|
||||||
index = max(0, min(index_end, index + 1))
|
index = max(0, min(index_end, index + 1))
|
||||||
items.add()
|
items.add()
|
||||||
@@ -266,8 +291,7 @@ class ItemMoveOp:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str,
|
def move(items, index, move_type, index_min=0, index_max=None):
|
||||||
index_min: int = 0, index_max: Optional[int] = None) -> int:
|
|
||||||
if index_max is None:
|
if index_max is None:
|
||||||
index_max = len(items) - 1
|
index_max = len(items) - 1
|
||||||
else:
|
else:
|
||||||
@@ -277,7 +301,7 @@ class ItemMoveOp:
|
|||||||
if index < index_min:
|
if index < index_min:
|
||||||
items.move(index, index_min)
|
items.move(index, index_min)
|
||||||
return index_min
|
return index_min
|
||||||
elif index > index_max:
|
if index > index_max:
|
||||||
items.move(index, index_max)
|
items.move(index, index_max)
|
||||||
return index_max
|
return index_max
|
||||||
|
|
||||||
@@ -296,8 +320,8 @@ class ItemMoveOp:
|
|||||||
return index_new
|
return index_new
|
||||||
|
|
||||||
|
|
||||||
def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None) -> Callable:
|
def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None):
|
||||||
"""Decorator to mark a function as deprecated.
|
"""Mark a function as deprecated.
|
||||||
Args:
|
Args:
|
||||||
deprecated_in (Optional[str]): Version in which the function was deprecated.
|
deprecated_in (Optional[str]): Version in which the function was deprecated.
|
||||||
details (Optional[str]): Additional details about the deprecation.
|
details (Optional[str]): Additional details about the deprecation.
|
||||||
@@ -305,8 +329,8 @@ def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = Non
|
|||||||
Callable: The decorated function.
|
Callable: The decorated function.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _function_wrapper(function: Callable) -> Callable:
|
def _function_wrapper(function: Callable):
|
||||||
def _inner_wrapper(*args: Any, **kwargs: Any) -> Any:
|
def _inner_wrapper(*args, **kwargs):
|
||||||
warn_deprecation(function.__name__, deprecated_in, details)
|
warn_deprecation(function.__name__, deprecated_in, details)
|
||||||
return function(*args, **kwargs)
|
return function(*args, **kwargs)
|
||||||
|
|
||||||
@@ -316,7 +340,7 @@ def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = Non
|
|||||||
|
|
||||||
|
|
||||||
def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, details: Optional[str] = None) -> None:
|
def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, details: Optional[str] = None) -> None:
|
||||||
"""Reports a deprecation warning.
|
"""Report a deprecation warning.
|
||||||
Args:
|
Args:
|
||||||
function_name (str): Name of the deprecated function.
|
function_name (str): Name of the deprecated function.
|
||||||
deprecated_in (Optional[str]): Version in which the function was deprecated.
|
deprecated_in (Optional[str]): Version in which the function was deprecated.
|
||||||
@@ -330,3 +354,7 @@ def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, de
|
|||||||
stack_info=True,
|
stack_info=True,
|
||||||
stacklevel=4,
|
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)
|
||||||
|
|||||||
+349
-6
@@ -35,6 +35,11 @@ def update_validation_mode(self: PropertyGroup, context: Context) -> None:
|
|||||||
logger.info(f"Updating validation mode to: {self.validation_mode}")
|
logger.info(f"Updating validation mode to: {self.validation_mode}")
|
||||||
save_preference("validation_mode", self.validation_mode)
|
save_preference("validation_mode", self.validation_mode)
|
||||||
|
|
||||||
|
# Hide validation results if mode is set to NONE
|
||||||
|
if self.validation_mode == 'NONE':
|
||||||
|
self.show_validation_results = False
|
||||||
|
logger.debug("Validation mode set to NONE, hiding validation results")
|
||||||
|
|
||||||
|
|
||||||
def update_logging_state(self: PropertyGroup, context: Context) -> None:
|
def update_logging_state(self: PropertyGroup, context: Context) -> None:
|
||||||
"""Updates logging state and configures logging"""
|
"""Updates logging state and configures logging"""
|
||||||
@@ -62,10 +67,110 @@ def highlight_problem_bones(self: PropertyGroup, context: Context) -> None:
|
|||||||
save_preference("highlight_problem_bones", self.highlight_problem_bones)
|
save_preference("highlight_problem_bones", self.highlight_problem_bones)
|
||||||
|
|
||||||
def get_mesh_objects(self, context):
|
def get_mesh_objects(self, context):
|
||||||
meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH']
|
"""Get list of all mesh objects with ASCII-safe identifiers
|
||||||
|
|
||||||
|
Returns tuples of (identifier, display_name, description) where:
|
||||||
|
- identifier: ASCII-safe unique ID (uses object's memory address)
|
||||||
|
- display_name: The actual object name (can contain Japanese/non-ASCII characters)
|
||||||
|
- description: Empty string
|
||||||
|
|
||||||
|
Uses caching to prevent encoding issues with Blender's EnumProperty system
|
||||||
|
"""
|
||||||
|
# Create a cache key based on mesh objects
|
||||||
|
mesh_objects = [obj for obj in bpy.data.objects if obj.type == 'MESH']
|
||||||
|
cache_key = tuple((obj.name, obj.as_pointer()) for obj in mesh_objects)
|
||||||
|
|
||||||
|
# Check if we have a cached result
|
||||||
|
if hasattr(get_mesh_objects, '_cache_key') and get_mesh_objects._cache_key == cache_key:
|
||||||
|
if hasattr(get_mesh_objects, '_cached_items'):
|
||||||
|
return get_mesh_objects._cached_items
|
||||||
|
|
||||||
|
# Build the list
|
||||||
|
meshes = []
|
||||||
|
for obj in mesh_objects:
|
||||||
|
safe_id = f"MESH_{obj.as_pointer()}"
|
||||||
|
# Use the name directly - Blender should handle Unicode in display names
|
||||||
|
display_name = obj.name
|
||||||
|
meshes.append((safe_id, display_name, ""))
|
||||||
|
|
||||||
if not meshes:
|
if not meshes:
|
||||||
return [('NONE', t("Visemes.no_meshes"), '')]
|
result = [('NONE', t("Visemes.no_meshes"), '')]
|
||||||
return meshes
|
else:
|
||||||
|
result = meshes
|
||||||
|
|
||||||
|
# Cache the result
|
||||||
|
get_mesh_objects._cache_key = cache_key
|
||||||
|
get_mesh_objects._cached_items = result
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def auto_populate_merge_armatures(context: Context) -> None:
|
||||||
|
"""Auto-populate merge armature fields when there are 2+ armatures"""
|
||||||
|
armatures = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE']
|
||||||
|
|
||||||
|
if len(armatures) >= 2:
|
||||||
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
|
if not toolkit.merge_armature_into and not toolkit.merge_armature:
|
||||||
|
toolkit.merge_armature_into = armatures[0].name
|
||||||
|
toolkit.merge_armature = armatures[1].name
|
||||||
|
logger.debug(f"Auto-populated merge armatures: {armatures[0].name} <- {armatures[1].name}")
|
||||||
|
|
||||||
|
elif toolkit.merge_armature_into and not toolkit.merge_armature:
|
||||||
|
for armature in armatures:
|
||||||
|
if armature.name != toolkit.merge_armature_into:
|
||||||
|
toolkit.merge_armature = armature.name
|
||||||
|
logger.debug(f"Auto-populated merge_armature: {armature.name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
elif not toolkit.merge_armature_into and toolkit.merge_armature:
|
||||||
|
for armature in armatures:
|
||||||
|
if armature.name != toolkit.merge_armature:
|
||||||
|
toolkit.merge_armature_into = armature.name
|
||||||
|
logger.debug(f"Auto-populated merge_armature_into: {armature.name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
def update_merge_armature_into(self: PropertyGroup, context: Context) -> None:
|
||||||
|
"""Update function for merge_armature_into property"""
|
||||||
|
auto_populate_merge_armatures(context)
|
||||||
|
|
||||||
|
def update_merge_armature(self: PropertyGroup, context: Context) -> None:
|
||||||
|
"""Update function for merge_armature property"""
|
||||||
|
auto_populate_merge_armatures(context)
|
||||||
|
|
||||||
|
@bpy.app.handlers.persistent
|
||||||
|
def depsgraph_update_handler(scene: Scene, depsgraph) -> None:
|
||||||
|
"""Handler to auto-populate merge armatures when objects change"""
|
||||||
|
# Check for any armature-related updates
|
||||||
|
armature_updated = False
|
||||||
|
for update in depsgraph.updates:
|
||||||
|
if hasattr(update, 'id') and update.id and hasattr(update.id, 'type'):
|
||||||
|
if update.id.type == 'ARMATURE':
|
||||||
|
armature_updated = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if armature_updated:
|
||||||
|
# Use a timer to defer the update to avoid context issues
|
||||||
|
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=0.1)
|
||||||
|
|
||||||
|
def auto_populate_safe() -> None:
|
||||||
|
"""Safe auto-populate function that can be called from timer"""
|
||||||
|
try:
|
||||||
|
if bpy.context and hasattr(bpy.context, 'scene') and hasattr(bpy.context.scene, 'avatar_toolkit'):
|
||||||
|
auto_populate_merge_armatures(bpy.context)
|
||||||
|
except (AttributeError, ReferenceError):
|
||||||
|
pass
|
||||||
|
return None # Don't repeat the timer
|
||||||
|
|
||||||
|
@bpy.app.handlers.persistent
|
||||||
|
def undo_post_handler(scene: Scene) -> None:
|
||||||
|
"""Handler for undo operations that might add/remove armatures"""
|
||||||
|
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=0.1)
|
||||||
|
|
||||||
|
@bpy.app.handlers.persistent
|
||||||
|
def redo_post_handler(scene: Scene) -> None:
|
||||||
|
"""Handler for redo operations that might add/remove armatures"""
|
||||||
|
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=0.1)
|
||||||
|
|
||||||
class AvatarToolkitSceneProperties(PropertyGroup):
|
class AvatarToolkitSceneProperties(PropertyGroup):
|
||||||
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
||||||
@@ -85,6 +190,12 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
show_validation_results: BoolProperty(
|
||||||
|
name="Show Validation Results",
|
||||||
|
default=False,
|
||||||
|
description="Show the validation results section"
|
||||||
|
)
|
||||||
|
|
||||||
material_search_filter: StringProperty(
|
material_search_filter: StringProperty(
|
||||||
name=t("TextureAtlas.search_materials"),
|
name=t("TextureAtlas.search_materials"),
|
||||||
description=t("TextureAtlas.search_materials_desc"),
|
description=t("TextureAtlas.search_materials_desc"),
|
||||||
@@ -197,6 +308,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
items=get_armature_list,
|
items=get_armature_list,
|
||||||
name=t("QuickAccess.select_armature"),
|
name=t("QuickAccess.select_armature"),
|
||||||
description=t("QuickAccess.select_armature"),
|
description=t("QuickAccess.select_armature"),
|
||||||
|
update=lambda self, context: update_active_armature(self, context)
|
||||||
)
|
)
|
||||||
|
|
||||||
language: EnumProperty(
|
language: EnumProperty(
|
||||||
@@ -214,7 +326,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
('BASIC', t("Settings.validation_mode.basic"), t("Settings.validation_mode.basic_desc")),
|
('BASIC', t("Settings.validation_mode.basic"), t("Settings.validation_mode.basic_desc")),
|
||||||
('NONE', t("Settings.validation_mode.none"), t("Settings.validation_mode.none_desc"))
|
('NONE', t("Settings.validation_mode.none"), t("Settings.validation_mode.none_desc"))
|
||||||
],
|
],
|
||||||
default=get_preference("validation_mode", "STRICT"),
|
default=get_preference("validation_mode", "NONE"),
|
||||||
update=update_validation_mode
|
update=update_validation_mode
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -465,13 +577,15 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
merge_armature_into: StringProperty(
|
merge_armature_into: StringProperty(
|
||||||
name=t('MergeArmature.into'),
|
name=t('MergeArmature.into'),
|
||||||
description=t('MergeArmature.into_desc'),
|
description=t('MergeArmature.into_desc'),
|
||||||
default=""
|
default="",
|
||||||
|
update=update_merge_armature_into
|
||||||
)
|
)
|
||||||
|
|
||||||
merge_armature: StringProperty(
|
merge_armature: StringProperty(
|
||||||
name=t('MergeArmature.from'),
|
name=t('MergeArmature.from'),
|
||||||
description=t('MergeArmature.from_desc'),
|
description=t('MergeArmature.from_desc'),
|
||||||
default=""
|
default="",
|
||||||
|
update=update_merge_armature
|
||||||
)
|
)
|
||||||
|
|
||||||
attach_mesh: StringProperty(
|
attach_mesh: StringProperty(
|
||||||
@@ -608,12 +722,233 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
update=update_log_level
|
update=update_log_level
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# VRM Conversion Properties
|
||||||
|
vrm_remove_colliders: BoolProperty(
|
||||||
|
name=t("VRM.remove_colliders"),
|
||||||
|
description=t("VRM.remove_colliders_desc"),
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
vrm_remove_root: BoolProperty(
|
||||||
|
name=t("VRM.remove_root"),
|
||||||
|
description=t("VRM.remove_root_desc"),
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# MMD Conversion Properties
|
||||||
|
mmd_make_parent: BoolProperty(
|
||||||
|
name=t("MMD.make_armature_parent"),
|
||||||
|
description="Remove parent Empty object and make armature the main parent",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_rename_armature: BoolProperty(
|
||||||
|
name=t("MMD.rename_to_armature"),
|
||||||
|
description="Rename the armature object to 'Armature'",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_translate_names: BoolProperty(
|
||||||
|
name=t("MMD.translate_names"),
|
||||||
|
description="Translate Japanese names to English using MMD dictionary and translation services",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_translate_bones: BoolProperty(
|
||||||
|
name=t("MMD.translate_bones"),
|
||||||
|
description="Translate bone names",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_translate_materials: BoolProperty(
|
||||||
|
name=t("MMD.translate_materials"),
|
||||||
|
description="Translate material names",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_translate_shapekeys: BoolProperty(
|
||||||
|
name=t("MMD.translate_shapekeys"),
|
||||||
|
description="Translate shape key names",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_translate_objects: BoolProperty(
|
||||||
|
name=t("MMD.translate_objects"),
|
||||||
|
description="Translate object names",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_restructure_bones: BoolProperty(
|
||||||
|
name=t("MMD.restructure_bones"),
|
||||||
|
description="Restructure bone hierarchy to Unity humanoid format (Hips, Spine, Chest, etc.)",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_remove_twist_bones: BoolProperty(
|
||||||
|
name=t("MMD.remove_twist_bones"),
|
||||||
|
description="Remove twist bones",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_remove_zero_weight_bones: BoolProperty(
|
||||||
|
name=t("MMD.remove_zero_weight_bones"),
|
||||||
|
description="Remove bones with zero or near-zero vertex weights",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Translation System Properties
|
||||||
|
translation_service: EnumProperty(
|
||||||
|
name=t("Translation.service"),
|
||||||
|
description=t("Translation.service_desc"),
|
||||||
|
items=[
|
||||||
|
('mymemory', t("Translation.service.mymemory"), t("Translation.service.mymemory_desc")),
|
||||||
|
('libretranslate', t("Translation.service.libretranslate"), t("Translation.service.libretranslate_desc")),
|
||||||
|
('deepl', t("Translation.service.deepl"), t("Translation.service.deepl_desc"))
|
||||||
|
],
|
||||||
|
default=get_preference("translation_service", "mymemory"),
|
||||||
|
update=lambda self, context: update_translation_service(self, context)
|
||||||
|
)
|
||||||
|
|
||||||
|
translation_mode: EnumProperty(
|
||||||
|
name=t("Translation.mode"),
|
||||||
|
description=t("Translation.mode_desc"),
|
||||||
|
items=[
|
||||||
|
('hybrid', t("Translation.mode.hybrid"), t("Translation.mode.hybrid_desc")),
|
||||||
|
('dictionary_only', t("Translation.mode.dictionary_only"), t("Translation.mode.dictionary_only_desc")),
|
||||||
|
('api_only', t("Translation.mode.api_only"), t("Translation.mode.api_only_desc"))
|
||||||
|
],
|
||||||
|
default=get_preference("translation_mode", "hybrid"),
|
||||||
|
update=lambda self, context: update_translation_mode(self, context)
|
||||||
|
)
|
||||||
|
|
||||||
|
translation_expand: BoolProperty(
|
||||||
|
name="Translation Settings Expanded",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
translation_target_language: EnumProperty(
|
||||||
|
name=t("Translation.target_language"),
|
||||||
|
description=t("Translation.target_language_desc"),
|
||||||
|
items=[
|
||||||
|
('en', 'English', 'Translate to English'),
|
||||||
|
('ja', 'Japanese', 'Translate to Japanese'),
|
||||||
|
('ko', 'Korean', 'Translate to Korean'),
|
||||||
|
('zh', 'Chinese', 'Translate to Chinese'),
|
||||||
|
('es', 'Spanish', 'Translate to Spanish'),
|
||||||
|
('fr', 'French', 'Translate to French'),
|
||||||
|
('de', 'German', 'Translate to German')
|
||||||
|
],
|
||||||
|
default='en'
|
||||||
|
)
|
||||||
|
|
||||||
|
translation_source_language: EnumProperty(
|
||||||
|
name=t("Translation.source_language"),
|
||||||
|
description=t("Translation.source_language_desc"),
|
||||||
|
items=[
|
||||||
|
('auto', 'Auto-detect', 'Automatically detect source language'),
|
||||||
|
('ja', 'Japanese', 'Source is Japanese'),
|
||||||
|
('en', 'English', 'Source is English'),
|
||||||
|
('ko', 'Korean', 'Source is Korean'),
|
||||||
|
('zh', 'Chinese', 'Source is Chinese')
|
||||||
|
],
|
||||||
|
default='ja'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_translation_service(self: PropertyGroup, context: Context) -> None:
|
||||||
|
"""Update translation service preference"""
|
||||||
|
logger.info(f"Updating translation service to: {self.translation_service}")
|
||||||
|
save_preference("translation_service", self.translation_service)
|
||||||
|
|
||||||
|
# Clear module-level translation caches when service changes
|
||||||
|
try:
|
||||||
|
from ..ui.translation_panel import _ui_cache
|
||||||
|
_ui_cache['deepl_config'].clear()
|
||||||
|
_ui_cache['libretranslate_config'].clear()
|
||||||
|
_ui_cache['translation_status'].clear()
|
||||||
|
if 'batch_info' in _ui_cache:
|
||||||
|
del _ui_cache['batch_info'] # Clear batch info cache when service changes
|
||||||
|
except ImportError:
|
||||||
|
pass # UI module might not be loaded yet
|
||||||
|
|
||||||
|
# Set the primary service
|
||||||
|
try:
|
||||||
|
from .translation_manager import get_avatar_translation_manager
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
manager.service_manager.set_primary_service(self.translation_service)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update translation service: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def update_translation_mode(self: PropertyGroup, context: Context) -> None:
|
||||||
|
"""Update translation mode preference"""
|
||||||
|
logger.info(f"Updating translation mode to: {self.translation_mode}")
|
||||||
|
save_preference("translation_mode", self.translation_mode)
|
||||||
|
|
||||||
|
# Clear module-level translation status cache when mode changes
|
||||||
|
try:
|
||||||
|
from ..ui.translation_panel import _ui_cache
|
||||||
|
_ui_cache['translation_status'].clear()
|
||||||
|
if 'batch_info' in _ui_cache:
|
||||||
|
del _ui_cache['batch_info'] # Clear batch info cache when mode changes
|
||||||
|
except ImportError:
|
||||||
|
pass # UI module might not be loaded yet
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .translation_manager import get_avatar_translation_manager, TranslationMode
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
manager.set_translation_mode(TranslationMode(self.translation_mode))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update translation mode: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def update_active_armature(self: PropertyGroup, context: Context) -> None:
|
||||||
|
"""Update the active armature when selection changes"""
|
||||||
|
if self.active_armature and self.active_armature != 'NONE':
|
||||||
|
# Get the actual armature object from the identifier
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
|
||||||
|
if armature:
|
||||||
|
logger.info(f"Active armature set to: {armature.name}")
|
||||||
|
# Deselect all objects first
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
# Select and make active the chosen armature
|
||||||
|
armature.select_set(True)
|
||||||
|
context.view_layer.objects.active = armature
|
||||||
|
logger.info(f"Selected and activated armature: {armature.name}")
|
||||||
|
|
||||||
|
# Clear armature caches when armature changes to ensure fresh validation
|
||||||
|
try:
|
||||||
|
from ..ui.quick_access_panel import clear_armature_caches
|
||||||
|
clear_armature_caches()
|
||||||
|
except ImportError:
|
||||||
|
pass # UI module might not be loaded yet
|
||||||
|
else:
|
||||||
|
logger.warning("Failed to get armature object from identifier")
|
||||||
|
else:
|
||||||
|
logger.info("No armature selected")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def register() -> None:
|
def register() -> None:
|
||||||
"""Register the Avatar Toolkit property group"""
|
"""Register the Avatar Toolkit property group"""
|
||||||
logger.info("Registering Avatar Toolkit properties")
|
logger.info("Registering Avatar Toolkit properties")
|
||||||
|
|
||||||
# Only register the property, not the classes (auto_load will handle that)
|
# Only register the property, not the classes (auto_load will handle that)
|
||||||
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
|
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
|
||||||
|
|
||||||
|
# Register handlers for auto-populating merge armatures
|
||||||
|
bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_handler)
|
||||||
|
bpy.app.handlers.undo_post.append(undo_post_handler)
|
||||||
|
bpy.app.handlers.redo_post.append(redo_post_handler)
|
||||||
|
|
||||||
|
# Initial auto-populate
|
||||||
|
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=1.0)
|
||||||
|
|
||||||
logger.debug("Properties registered successfully")
|
logger.debug("Properties registered successfully")
|
||||||
|
|
||||||
|
|
||||||
@@ -621,6 +956,14 @@ def unregister() -> None:
|
|||||||
"""Unregister the Avatar Toolkit property group"""
|
"""Unregister the Avatar Toolkit property group"""
|
||||||
logger.info("Unregistering Avatar Toolkit properties")
|
logger.info("Unregistering Avatar Toolkit properties")
|
||||||
|
|
||||||
|
# Remove handlers
|
||||||
|
if depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
|
||||||
|
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_handler)
|
||||||
|
if undo_post_handler in bpy.app.handlers.undo_post:
|
||||||
|
bpy.app.handlers.undo_post.remove(undo_post_handler)
|
||||||
|
if redo_post_handler in bpy.app.handlers.redo_post:
|
||||||
|
bpy.app.handlers.redo_post.remove(redo_post_handler)
|
||||||
|
|
||||||
# Remove the property
|
# Remove the property
|
||||||
if hasattr(bpy.types.Scene, "avatar_toolkit"):
|
if hasattr(bpy.types.Scene, "avatar_toolkit"):
|
||||||
try:
|
try:
|
||||||
|
|||||||
+26
-4
@@ -2,6 +2,7 @@ import traceback
|
|||||||
from types import FrameType
|
from types import FrameType
|
||||||
import bpy
|
import bpy
|
||||||
import bpy_extras
|
import bpy_extras
|
||||||
|
from bpy_extras import anim_utils
|
||||||
from numpy import double
|
from numpy import double
|
||||||
from typing import Set, Dict
|
from typing import Set, Dict
|
||||||
import re
|
import re
|
||||||
@@ -116,11 +117,32 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
|
|||||||
|
|
||||||
|
|
||||||
def makeorexistingfcurve(action: bpy.types.Action, data_path: str, action_group: str, index=0) -> bpy.types.FCurve:
|
def makeorexistingfcurve(action: bpy.types.Action, data_path: str, action_group: str, index=0) -> bpy.types.FCurve:
|
||||||
fcurve = action.fcurves.find(data_path=data_path,index=index)
|
"""Get or create an F-Curve using Blender 5.0 channelbag system.
|
||||||
if fcurve == None:
|
|
||||||
return action.fcurves.new(data_path,action_group=action_group,index=index)
|
Blender 5.0 Breaking Change: The legacy action.fcurves API has been removed.
|
||||||
|
F-Curves are now accessed through channelbags. Each slot of an Action can have a channelbag.
|
||||||
|
This function has been migrated to use bpy_extras.anim_utils.action_ensure_channelbag_for_slot().
|
||||||
|
"""
|
||||||
|
# Get the action slot (assumes single slot for now - armature actions typically use first slot)
|
||||||
|
if not action.slots:
|
||||||
|
slot = action.slots.new(for_id=bpy.context.object.data if bpy.context.object and bpy.context.object.type == 'ARMATURE' else None)
|
||||||
else:
|
else:
|
||||||
print("fcurve with data \""+data_path+"\" already exists")
|
slot = action.slots[0]
|
||||||
|
|
||||||
|
# Get or create channelbag for this slot
|
||||||
|
channelbag = anim_utils.action_ensure_channelbag_for_slot(action, slot)
|
||||||
|
|
||||||
|
# Use ensure() to get existing or create new F-Curve
|
||||||
|
fcurve = channelbag.fcurves.ensure(data_path, index=index, group_name=action_group)
|
||||||
|
|
||||||
|
if fcurve:
|
||||||
|
return fcurve
|
||||||
|
else:
|
||||||
|
print(f"fcurve with data \"{data_path}\" creation failed")
|
||||||
|
# Fallback: try to find or create manually
|
||||||
|
fcurve = channelbag.fcurves.find(data_path, index=index)
|
||||||
|
if fcurve is None:
|
||||||
|
fcurve = channelbag.fcurves.new(data_path, index=index, group_name=action_group)
|
||||||
return fcurve
|
return fcurve
|
||||||
|
|
||||||
class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper):
|
class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper):
|
||||||
|
|||||||
@@ -0,0 +1,657 @@
|
|||||||
|
# GPL License
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from typing import Dict, List, Optional, Tuple, Set, Any, Callable
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Object, Material, ShapeKey
|
||||||
|
|
||||||
|
from .translation_service import get_translation_manager, TranslationServiceManager
|
||||||
|
from .enhanced_dictionaries import get_enhanced_translator, EnhancedDictionaryTranslator
|
||||||
|
from .logging_setup import logger
|
||||||
|
from .addon_preferences import get_preference, save_preference
|
||||||
|
from .translations import t
|
||||||
|
|
||||||
|
|
||||||
|
class TranslationMode(Enum):
|
||||||
|
"""Translation modes for different approaches"""
|
||||||
|
DICTIONARY_ONLY = "dictionary_only"
|
||||||
|
API_ONLY = "api_only"
|
||||||
|
HYBRID = "hybrid" # Default: Dictionary first, then API fallback
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TranslationJob:
|
||||||
|
"""Represents a translation job for batch processing"""
|
||||||
|
name: str
|
||||||
|
category: str
|
||||||
|
source_lang: str = "ja"
|
||||||
|
target_lang: str = "en"
|
||||||
|
object_ref: Optional[Any] = None
|
||||||
|
property_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TranslationResult:
|
||||||
|
"""Result of a translation operation"""
|
||||||
|
original: str
|
||||||
|
translated: str
|
||||||
|
method: str # "dictionary", "api", "failed"
|
||||||
|
service: Optional[str] = None
|
||||||
|
category: str = "unknown"
|
||||||
|
confidence: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
class TranslationCache:
|
||||||
|
"""Persistent translation cache with file storage"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._cache: Dict[str, Dict[str, str]] = {}
|
||||||
|
self._cache_file = self._get_cache_file_path()
|
||||||
|
self._cache_lock = threading.Lock()
|
||||||
|
self._load_cache()
|
||||||
|
|
||||||
|
def _get_cache_file_path(self) -> str:
|
||||||
|
"""Get the cache file path in user preferences directory"""
|
||||||
|
user_path = bpy.utils.resource_path('USER')
|
||||||
|
cache_dir = os.path.join(user_path, "config", "avatar_toolkit_prefs")
|
||||||
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
|
return os.path.join(cache_dir, "translation_cache.json")
|
||||||
|
|
||||||
|
def _load_cache(self) -> None:
|
||||||
|
"""Load cache from file"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(self._cache_file):
|
||||||
|
# Try UTF-8 first, fallback to other encodings
|
||||||
|
try:
|
||||||
|
with open(self._cache_file, 'r', encoding='utf-8') as f:
|
||||||
|
self._cache = json.load(f)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# Try with UTF-8 error handling
|
||||||
|
with open(self._cache_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
self._cache = json.load(f)
|
||||||
|
logger.debug(f"Loaded translation cache with {len(self._cache)} entries")
|
||||||
|
else:
|
||||||
|
self._cache = {}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load translation cache: {e}")
|
||||||
|
self._cache = {}
|
||||||
|
|
||||||
|
def _save_cache(self) -> None:
|
||||||
|
"""Save cache to file"""
|
||||||
|
try:
|
||||||
|
with open(self._cache_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self._cache, f, indent=2, ensure_ascii=False)
|
||||||
|
logger.debug(f"Saved translation cache with {len(self._cache)} entries")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save translation cache: {e}")
|
||||||
|
|
||||||
|
def get(self, text: str, source_lang: str, target_lang: str) -> Optional[str]:
|
||||||
|
"""Get cached translation"""
|
||||||
|
cache_key = f"{source_lang}_{target_lang}"
|
||||||
|
with self._cache_lock:
|
||||||
|
if cache_key in self._cache and text in self._cache[cache_key]:
|
||||||
|
return self._cache[cache_key][text]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def put(self, text: str, translation: str, source_lang: str, target_lang: str) -> None:
|
||||||
|
"""Store translation in cache"""
|
||||||
|
cache_key = f"{source_lang}_{target_lang}"
|
||||||
|
with self._cache_lock:
|
||||||
|
if cache_key not in self._cache:
|
||||||
|
self._cache[cache_key] = {}
|
||||||
|
self._cache[cache_key][text] = translation
|
||||||
|
|
||||||
|
# Save cache periodically (every 10 new entries)
|
||||||
|
if len(self._cache.get(cache_key, {})) % 10 == 0:
|
||||||
|
self._save_cache()
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Clear all cached translations"""
|
||||||
|
with self._cache_lock:
|
||||||
|
self._cache.clear()
|
||||||
|
self._save_cache()
|
||||||
|
logger.info("Translation cache cleared")
|
||||||
|
|
||||||
|
def get_stats(self) -> Dict[str, int]:
|
||||||
|
"""Get cache statistics"""
|
||||||
|
with self._cache_lock:
|
||||||
|
total_entries = sum(len(lang_cache) for lang_cache in self._cache.values())
|
||||||
|
return {
|
||||||
|
"language_pairs": len(self._cache),
|
||||||
|
"total_entries": total_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolkitTranslationManager:
|
||||||
|
"""Main translation manager for Avatar Toolkit"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.service_manager: TranslationServiceManager = get_translation_manager()
|
||||||
|
self.dictionary_translator: EnhancedDictionaryTranslator = get_enhanced_translator()
|
||||||
|
self.cache: TranslationCache = TranslationCache()
|
||||||
|
self.translation_mode: TranslationMode = TranslationMode(
|
||||||
|
get_preference("translation_mode", "hybrid")
|
||||||
|
)
|
||||||
|
self._progress_callback: Optional[Callable[[int, int, str], None]] = None
|
||||||
|
|
||||||
|
def set_translation_mode(self, mode: TranslationMode) -> None:
|
||||||
|
"""Set the translation mode"""
|
||||||
|
self.translation_mode = mode
|
||||||
|
save_preference("translation_mode", mode.value)
|
||||||
|
logger.info(f"Translation mode set to: {mode.value}")
|
||||||
|
|
||||||
|
def set_progress_callback(self, callback: Optional[Callable[[int, int, str], None]]) -> None:
|
||||||
|
"""Set progress callback for batch operations"""
|
||||||
|
self._progress_callback = callback
|
||||||
|
|
||||||
|
def translate_single(self, name: str, category: str = "auto",
|
||||||
|
source_lang: str = "ja", target_lang: str = "en") -> TranslationResult:
|
||||||
|
"""Translate a single name with comprehensive fallback logic"""
|
||||||
|
# Import safe_decode_text from translation_service
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
|
# Ensure name is properly encoded
|
||||||
|
try:
|
||||||
|
name = safe_decode_text(name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode name: {e}")
|
||||||
|
|
||||||
|
if not name or not name.strip():
|
||||||
|
return TranslationResult(name, name, "skipped")
|
||||||
|
|
||||||
|
original_name = name.strip()
|
||||||
|
|
||||||
|
# Check cache first
|
||||||
|
cached_result = self.cache.get(original_name, source_lang, target_lang)
|
||||||
|
if cached_result:
|
||||||
|
return TranslationResult(original_name, cached_result, "cache", category=category)
|
||||||
|
|
||||||
|
# Dictionary translation (always try first in hybrid mode)
|
||||||
|
if self.translation_mode in [TranslationMode.DICTIONARY_ONLY, TranslationMode.HYBRID]:
|
||||||
|
dict_result, detected_category = self.dictionary_translator.translate_name(original_name, category)
|
||||||
|
if dict_result:
|
||||||
|
self.cache.put(original_name, dict_result, source_lang, target_lang)
|
||||||
|
return TranslationResult(original_name, dict_result, "dictionary",
|
||||||
|
category=detected_category, confidence=1.0)
|
||||||
|
|
||||||
|
if self.translation_mode in [TranslationMode.API_ONLY, TranslationMode.HYBRID]:
|
||||||
|
try:
|
||||||
|
api_result, service_name = self.service_manager.translate_with_fallback(
|
||||||
|
original_name, source_lang, target_lang
|
||||||
|
)
|
||||||
|
if api_result != original_name: # Translation succeeded
|
||||||
|
self.cache.put(original_name, api_result, source_lang, target_lang)
|
||||||
|
return TranslationResult(original_name, api_result, "api",
|
||||||
|
service=service_name, category=category, confidence=0.8)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"API translation failed for '{original_name}': {e}")
|
||||||
|
|
||||||
|
# No translation available
|
||||||
|
return TranslationResult(original_name, original_name, "failed", category=category)
|
||||||
|
|
||||||
|
def translate_batch(self, jobs: List[TranslationJob],
|
||||||
|
apply_results: bool = True) -> List[TranslationResult]:
|
||||||
|
"""Translate multiple items in batch with progress reporting and interruption handling"""
|
||||||
|
results = []
|
||||||
|
total_jobs = len(jobs)
|
||||||
|
|
||||||
|
logger.info(f"Starting batch translation of {total_jobs} items")
|
||||||
|
|
||||||
|
# Group jobs by category for more efficient processing
|
||||||
|
jobs_by_category: Dict[str, List[TranslationJob]] = {}
|
||||||
|
for job in jobs:
|
||||||
|
if job.category not in jobs_by_category:
|
||||||
|
jobs_by_category[job.category] = []
|
||||||
|
jobs_by_category[job.category].append(job)
|
||||||
|
|
||||||
|
completed = 0
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
for category, category_jobs in jobs_by_category.items():
|
||||||
|
logger.debug(f"Processing {len(category_jobs)} {category} translations")
|
||||||
|
|
||||||
|
# Check if we can use optimized batch translation for API calls
|
||||||
|
can_use_api_batch = (self.translation_mode in [TranslationMode.API_ONLY, TranslationMode.HYBRID] and
|
||||||
|
len(category_jobs) > 3)
|
||||||
|
|
||||||
|
if can_use_api_batch:
|
||||||
|
# Try optimized batch translation with API
|
||||||
|
batch_results = self._process_category_batch_optimized(category_jobs, completed, total_jobs, start_time)
|
||||||
|
if batch_results:
|
||||||
|
# Apply results to Blender objects if requested
|
||||||
|
for i, (job, result) in enumerate(zip(category_jobs, batch_results)):
|
||||||
|
if apply_results and result.method != "failed" and job.object_ref:
|
||||||
|
try:
|
||||||
|
self._apply_translation_to_object(job, result)
|
||||||
|
logger.debug(f"Successfully applied translation: {job.name} -> {result.translated}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to apply translation to object {job.name}: {e}")
|
||||||
|
result.method = "apply_failed"
|
||||||
|
result.translated = job.name
|
||||||
|
|
||||||
|
results.extend(batch_results)
|
||||||
|
completed += len(category_jobs)
|
||||||
|
|
||||||
|
progress_percent = (completed / total_jobs) * 100
|
||||||
|
logger.info(f"Batch translation progress: {completed}/{total_jobs} ({progress_percent:.1f}%) - completed {category} batch")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fallback to individual processing
|
||||||
|
for job in category_jobs:
|
||||||
|
# Check if we should continue (for potential cancellation support)
|
||||||
|
current_time = time.time()
|
||||||
|
elapsed_time = current_time - start_time
|
||||||
|
|
||||||
|
# Progress callback with detailed status
|
||||||
|
if self._progress_callback:
|
||||||
|
avg_time_per_item = elapsed_time / max(completed, 1)
|
||||||
|
remaining_items = total_jobs - completed
|
||||||
|
estimated_remaining = avg_time_per_item * remaining_items
|
||||||
|
|
||||||
|
status_msg = f"Translating {job.name}"
|
||||||
|
if completed > 0:
|
||||||
|
status_msg += f" (ETA: {estimated_remaining:.1f}s)"
|
||||||
|
|
||||||
|
self._progress_callback(completed, total_jobs, status_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug(f"Translating job {completed + 1}/{total_jobs}: {job.name} ({job.category})")
|
||||||
|
|
||||||
|
result = self.translate_single(job.name, job.category,
|
||||||
|
job.source_lang, job.target_lang)
|
||||||
|
|
||||||
|
if apply_results and result.method != "failed" and job.object_ref:
|
||||||
|
try:
|
||||||
|
self._apply_translation_to_object(job, result)
|
||||||
|
logger.debug(f"Successfully applied translation: {job.name} -> {result.translated}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to apply translation to object {job.name}: {e}")
|
||||||
|
result.method = "apply_failed"
|
||||||
|
result.translated = job.name
|
||||||
|
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Translation failed for job {job.name}: {e}")
|
||||||
|
# Create a failed result
|
||||||
|
failed_result = TranslationResult(
|
||||||
|
original=job.name,
|
||||||
|
translated=job.name,
|
||||||
|
method="failed",
|
||||||
|
category=job.category
|
||||||
|
)
|
||||||
|
results.append(failed_result)
|
||||||
|
|
||||||
|
completed += 1
|
||||||
|
|
||||||
|
# Log progress periodically
|
||||||
|
if completed % 10 == 0 or completed == total_jobs:
|
||||||
|
progress_percent = (completed / total_jobs) * 100
|
||||||
|
logger.info(f"Batch translation progress: {completed}/{total_jobs} ({progress_percent:.1f}%)")
|
||||||
|
|
||||||
|
if self._progress_callback:
|
||||||
|
total_time = time.time() - start_time
|
||||||
|
self._progress_callback(total_jobs, total_jobs, f"Translation complete ({total_time:.1f}s)")
|
||||||
|
|
||||||
|
successful = sum(1 for r in results if r.method not in ["failed", "skipped", "apply_failed"])
|
||||||
|
failed = sum(1 for r in results if r.method in ["failed", "apply_failed"])
|
||||||
|
skipped = sum(1 for r in results if r.method == "skipped")
|
||||||
|
|
||||||
|
dictionary_count = sum(1 for r in results if r.method == "dictionary")
|
||||||
|
api_count = sum(1 for r in results if r.method == "api")
|
||||||
|
cache_count = sum(1 for r in results if r.method == "cache")
|
||||||
|
|
||||||
|
logger.info(f"Batch translation complete: {successful}/{total_jobs} successful, {failed} failed, {skipped} skipped")
|
||||||
|
logger.info(f"Translation methods used: Dictionary: {dictionary_count}, API: {api_count}, Cache: {cache_count}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _process_category_batch_optimized(self, category_jobs: List[TranslationJob],
|
||||||
|
completed: int, total_jobs: int, start_time: float) -> Optional[List[TranslationResult]]:
|
||||||
|
"""Process a batch of jobs from the same category using optimized API batch translation"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
|
if not category_jobs:
|
||||||
|
return []
|
||||||
|
|
||||||
|
logger.info(f"Starting optimized batch translation for {len(category_jobs)} {category_jobs[0].category} items")
|
||||||
|
|
||||||
|
api_batch_jobs = []
|
||||||
|
api_batch_texts = []
|
||||||
|
results = [None] * len(category_jobs)
|
||||||
|
|
||||||
|
# First pass: try dictionary translations and collect API candidates
|
||||||
|
for i, job in enumerate(category_jobs):
|
||||||
|
if not job.name or not job.name.strip():
|
||||||
|
results[i] = TranslationResult(job.name, job.name, "skipped", category=job.category)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ensure name is properly encoded
|
||||||
|
try:
|
||||||
|
original_name = safe_decode_text(job.name.strip())
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode job name: {e}")
|
||||||
|
original_name = job.name.strip()
|
||||||
|
continue
|
||||||
|
|
||||||
|
original_name = job.name.strip()
|
||||||
|
|
||||||
|
# Check cache first
|
||||||
|
cached_result = self.cache.get(original_name, job.source_lang, job.target_lang)
|
||||||
|
if cached_result:
|
||||||
|
results[i] = TranslationResult(original_name, cached_result, "cache", category=job.category)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Try dictionary translation first (if in hybrid mode)
|
||||||
|
if self.translation_mode == TranslationMode.HYBRID:
|
||||||
|
dict_result, detected_category = self.dictionary_translator.translate_name(original_name, job.category)
|
||||||
|
if dict_result:
|
||||||
|
self.cache.put(original_name, dict_result, job.source_lang, job.target_lang)
|
||||||
|
results[i] = TranslationResult(original_name, dict_result, "dictionary",
|
||||||
|
category=detected_category, confidence=1.0)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add to API batch candidates
|
||||||
|
api_batch_jobs.append((i, job))
|
||||||
|
api_batch_texts.append(original_name)
|
||||||
|
|
||||||
|
# Process API batch if we have candidates
|
||||||
|
if api_batch_texts:
|
||||||
|
logger.info(f"Sending {len(api_batch_texts)} items to API batch translation")
|
||||||
|
|
||||||
|
if self._progress_callback:
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
avg_time_per_item = elapsed_time / max(completed, 1) if completed > 0 else 1.0
|
||||||
|
remaining_items = total_jobs - completed
|
||||||
|
estimated_remaining = avg_time_per_item * remaining_items
|
||||||
|
|
||||||
|
status_msg = f"Batch translating {len(api_batch_texts)} {category_jobs[0].category} items"
|
||||||
|
if completed > 0:
|
||||||
|
status_msg += f" (ETA: {estimated_remaining:.1f}s)"
|
||||||
|
|
||||||
|
self._progress_callback(completed, total_jobs, status_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use the service manager's optimized batch translation
|
||||||
|
if len(set(job.source_lang for _, job in api_batch_jobs)) == 1 and len(set(job.target_lang for _, job in api_batch_jobs)) == 1:
|
||||||
|
source_lang = api_batch_jobs[0][1].source_lang
|
||||||
|
target_lang = api_batch_jobs[0][1].target_lang
|
||||||
|
|
||||||
|
batch_results = self.service_manager.batch_translate_with_fallback(
|
||||||
|
api_batch_texts, source_lang, target_lang
|
||||||
|
)
|
||||||
|
|
||||||
|
for j, (result_idx, job) in enumerate(api_batch_jobs):
|
||||||
|
if j < len(batch_results):
|
||||||
|
translated_text, service_name = batch_results[j]
|
||||||
|
|
||||||
|
# Cache successful translations
|
||||||
|
if translated_text != job.name:
|
||||||
|
self.cache.put(job.name.strip(), translated_text, job.source_lang, job.target_lang)
|
||||||
|
|
||||||
|
results[result_idx] = TranslationResult(
|
||||||
|
original=job.name.strip(),
|
||||||
|
translated=translated_text,
|
||||||
|
method="api" if translated_text != job.name else "failed",
|
||||||
|
service=service_name,
|
||||||
|
category=job.category,
|
||||||
|
confidence=0.8
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Fallback for missing results
|
||||||
|
results[result_idx] = TranslationResult(job.name, job.name, "failed", category=job.category)
|
||||||
|
else:
|
||||||
|
# Mixed language pairs - fallback to individual translations
|
||||||
|
logger.info("Mixed language pairs detected, falling back to individual API translations")
|
||||||
|
for result_idx, job in api_batch_jobs:
|
||||||
|
try:
|
||||||
|
result = self.translate_single(job.name, job.category, job.source_lang, job.target_lang)
|
||||||
|
results[result_idx] = result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Individual API translation failed for {job.name}: {e}")
|
||||||
|
results[result_idx] = TranslationResult(job.name, job.name, "failed", category=job.category)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Batch API translation failed: {e}")
|
||||||
|
# Fallback to individual translations
|
||||||
|
for result_idx, job in api_batch_jobs:
|
||||||
|
try:
|
||||||
|
result = self.translate_single(job.name, job.category, job.source_lang, job.target_lang)
|
||||||
|
results[result_idx] = result
|
||||||
|
except Exception as individual_e:
|
||||||
|
logger.error(f"Individual fallback translation failed for {job.name}: {individual_e}")
|
||||||
|
results[result_idx] = TranslationResult(job.name, job.name, "failed", category=job.category)
|
||||||
|
|
||||||
|
for i, result in enumerate(results):
|
||||||
|
if result is None:
|
||||||
|
results[i] = TranslationResult(category_jobs[i].name, category_jobs[i].name, "failed", category=category_jobs[i].category)
|
||||||
|
|
||||||
|
successful_batch = sum(1 for r in results if r.method not in ["failed", "skipped"])
|
||||||
|
logger.info(f"Optimized batch complete: {successful_batch}/{len(category_jobs)} successful")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _apply_translation_to_object(self, job: TranslationJob, result: TranslationResult) -> None:
|
||||||
|
"""Apply translation result to a Blender object"""
|
||||||
|
if not job.object_ref or not job.property_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
setattr(job.object_ref, job.property_name, result.translated)
|
||||||
|
logger.debug(f"Applied translation: {job.object_ref.name}.{job.property_name} = '{result.translated}'")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to set property {job.property_name}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def translate_armature_bones(self, armature: Object, apply_results: bool = True) -> List[TranslationResult]:
|
||||||
|
"""Translate all bone names in an armature"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
|
if not armature or armature.type != 'ARMATURE':
|
||||||
|
return []
|
||||||
|
|
||||||
|
jobs = []
|
||||||
|
for bone in armature.data.bones:
|
||||||
|
try:
|
||||||
|
bone_name = safe_decode_text(bone.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode bone name, using as-is: {e}")
|
||||||
|
bone_name = bone.name
|
||||||
|
|
||||||
|
jobs.append(TranslationJob(
|
||||||
|
name=bone_name,
|
||||||
|
category="bones",
|
||||||
|
object_ref=bone,
|
||||||
|
property_name="name"
|
||||||
|
))
|
||||||
|
|
||||||
|
return self.translate_batch(jobs, apply_results)
|
||||||
|
|
||||||
|
def translate_object_shapekeys(self, mesh_obj: Object, apply_results: bool = True) -> List[TranslationResult]:
|
||||||
|
"""Translate all shape key names in a mesh object"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
|
if not mesh_obj or mesh_obj.type != 'MESH' or not mesh_obj.data.shape_keys:
|
||||||
|
return []
|
||||||
|
|
||||||
|
jobs = []
|
||||||
|
for shape_key in mesh_obj.data.shape_keys.key_blocks:
|
||||||
|
try:
|
||||||
|
sk_name = safe_decode_text(shape_key.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode shape key name, using as-is: {e}")
|
||||||
|
sk_name = shape_key.name
|
||||||
|
|
||||||
|
jobs.append(TranslationJob(
|
||||||
|
name=sk_name,
|
||||||
|
category="shapekeys",
|
||||||
|
object_ref=shape_key,
|
||||||
|
property_name="name"
|
||||||
|
))
|
||||||
|
|
||||||
|
return self.translate_batch(jobs, apply_results)
|
||||||
|
|
||||||
|
def translate_scene_materials(self, apply_results: bool = True) -> List[TranslationResult]:
|
||||||
|
"""Translate all material names in the scene"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
|
jobs = []
|
||||||
|
processed_materials: Set[str] = set()
|
||||||
|
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if obj.type == 'MESH' and obj.data.materials:
|
||||||
|
for material in obj.data.materials:
|
||||||
|
if material and material.name not in processed_materials:
|
||||||
|
try:
|
||||||
|
mat_name = safe_decode_text(material.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode material name, using as-is: {e}")
|
||||||
|
mat_name = material.name
|
||||||
|
|
||||||
|
jobs.append(TranslationJob(
|
||||||
|
name=mat_name,
|
||||||
|
category="materials",
|
||||||
|
object_ref=material,
|
||||||
|
property_name="name"
|
||||||
|
))
|
||||||
|
processed_materials.add(material.name)
|
||||||
|
|
||||||
|
return self.translate_batch(jobs, apply_results)
|
||||||
|
|
||||||
|
def translate_scene_objects(self, object_types: Optional[Set[str]] = None,
|
||||||
|
apply_results: bool = True) -> List[TranslationResult]:
|
||||||
|
"""Translate all object names in the scene"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
|
if object_types is None:
|
||||||
|
object_types = {'MESH', 'ARMATURE', 'EMPTY'}
|
||||||
|
|
||||||
|
jobs = []
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if obj.type in object_types:
|
||||||
|
try:
|
||||||
|
obj_name = safe_decode_text(obj.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode object name, using as-is: {e}")
|
||||||
|
obj_name = obj.name
|
||||||
|
|
||||||
|
jobs.append(TranslationJob(
|
||||||
|
name=obj_name,
|
||||||
|
category="objects",
|
||||||
|
object_ref=obj,
|
||||||
|
property_name="name"
|
||||||
|
))
|
||||||
|
|
||||||
|
return self.translate_batch(jobs, apply_results)
|
||||||
|
|
||||||
|
def get_translation_stats(self) -> Dict[str, Any]:
|
||||||
|
"""Get comprehensive translation statistics"""
|
||||||
|
dict_stats = self.dictionary_translator.get_statistics()
|
||||||
|
cache_stats = self.cache.get_stats()
|
||||||
|
available_services = self.service_manager.get_available_services()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"dictionary_translations": dict_stats,
|
||||||
|
"cache_stats": cache_stats,
|
||||||
|
"available_services": available_services,
|
||||||
|
"current_mode": self.translation_mode.value,
|
||||||
|
"primary_service": get_preference("translation_service", "microsoft")
|
||||||
|
}
|
||||||
|
|
||||||
|
def clear_all_caches(self) -> None:
|
||||||
|
"""Clear all translation caches"""
|
||||||
|
self.cache.clear()
|
||||||
|
for service_id, service in self.service_manager._services.items():
|
||||||
|
service.clear_cache()
|
||||||
|
logger.info("All translation caches cleared")
|
||||||
|
|
||||||
|
|
||||||
|
_translation_manager: Optional[AvatarToolkitTranslationManager] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_avatar_translation_manager() -> AvatarToolkitTranslationManager:
|
||||||
|
"""Get the global Avatar Toolkit translation manager"""
|
||||||
|
global _translation_manager
|
||||||
|
if _translation_manager is None:
|
||||||
|
_translation_manager = AvatarToolkitTranslationManager()
|
||||||
|
return _translation_manager
|
||||||
|
|
||||||
|
|
||||||
|
def translate_name_simple(name: str, category: str = "auto") -> str:
|
||||||
|
"""Simple translation function for quick use"""
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
result = manager.translate_single(name, category)
|
||||||
|
return result.translated
|
||||||
|
|
||||||
|
|
||||||
|
def is_translation_service_available(service_name: str) -> bool:
|
||||||
|
"""Check if a specific translation service is available"""
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
available_services = manager.service_manager.get_available_services()
|
||||||
|
return any(service_id == service_name for service_id, _ in available_services)
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_translation_services() -> List[Tuple[str, str]]:
|
||||||
|
"""Get list of available translation services"""
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
return manager.service_manager.get_available_services()
|
||||||
|
|
||||||
|
|
||||||
|
def get_batch_translation_info() -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""Get information about batch translation capabilities of available services"""
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
batch_info = {}
|
||||||
|
|
||||||
|
for service_id, service_name in manager.service_manager.get_available_services():
|
||||||
|
service = manager.service_manager.get_service(service_id)
|
||||||
|
if service:
|
||||||
|
batch_info[service_id] = {
|
||||||
|
'name': service_name,
|
||||||
|
'supports_batch': service.supports_batch_translation(),
|
||||||
|
'batch_type': 'native' if service_id == 'deepl' else 'concurrent' if service_id in ['libretranslate', 'mymemory'] else 'individual'
|
||||||
|
}
|
||||||
|
|
||||||
|
return batch_info
|
||||||
|
|
||||||
|
|
||||||
|
def configure_translation_service(service_id: str, **config) -> bool:
|
||||||
|
"""Configure a translation service with the provided settings (now with batch support)"""
|
||||||
|
try:
|
||||||
|
success = False
|
||||||
|
if service_id == "deepl":
|
||||||
|
from .translation_service import configure_deepl_translator
|
||||||
|
success = configure_deepl_translator(
|
||||||
|
config.get("api_key", ""),
|
||||||
|
config.get("use_free_api", True)
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
logger.info("DeepL configured with native batch translation support (up to 50 texts per request)")
|
||||||
|
elif service_id == "libretranslate":
|
||||||
|
from .translation_service import configure_libretranslate_server
|
||||||
|
success = configure_libretranslate_server(
|
||||||
|
config.get("server_url", "https://libretranslate.com"),
|
||||||
|
config.get("api_key", None)
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
logger.info("LibreTranslate configured with concurrent batch processing (3x faster)")
|
||||||
|
elif service_id == "microsoft":
|
||||||
|
from .translation_service import configure_microsoft_translator
|
||||||
|
success = configure_microsoft_translator(
|
||||||
|
config.get("api_key", ""),
|
||||||
|
config.get("region", "global")
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error(f"Unknown translation service: {service_id}")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return success
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to configure translation service {service_id}: {e}")
|
||||||
|
return False
|
||||||
@@ -0,0 +1,993 @@
|
|||||||
|
# GPL License
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict, List, Optional, Tuple, Any, Set
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from .logging_setup import logger
|
||||||
|
from .addon_preferences import save_preference, get_preference
|
||||||
|
|
||||||
|
|
||||||
|
def safe_decode_text(text: str) -> str:
|
||||||
|
"""Safely decode text that might be in various encodings (UTF-8, Shift-JIS, etc.)"""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
|
||||||
|
# If it's already a proper string, return it
|
||||||
|
if isinstance(text, str):
|
||||||
|
try:
|
||||||
|
# Test if it's valid UTF-8
|
||||||
|
text.encode('utf-8')
|
||||||
|
return text
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try common encodings for Japanese text
|
||||||
|
encodings = ['utf-8', 'shift-jis', 'cp932', 'euc-jp', 'iso-2022-jp']
|
||||||
|
|
||||||
|
for encoding in encodings:
|
||||||
|
try:
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
return text.decode(encoding)
|
||||||
|
else:
|
||||||
|
# Try to re-encode and decode
|
||||||
|
return text.encode('latin-1', errors='ignore').decode(encoding, errors='ignore')
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError, AttributeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fallback: replace problematic characters
|
||||||
|
try:
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
return text.decode('utf-8', errors='replace')
|
||||||
|
else:
|
||||||
|
return str(text).encode('utf-8', errors='replace').decode('utf-8')
|
||||||
|
except:
|
||||||
|
return str(text)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TranslationRequest:
|
||||||
|
"""Represents a translation request"""
|
||||||
|
text: str
|
||||||
|
source_lang: str = "ja"
|
||||||
|
target_lang: str = "en"
|
||||||
|
category: str = "general"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TranslationResult:
|
||||||
|
"""Represents a translation result"""
|
||||||
|
original: str
|
||||||
|
translated: str
|
||||||
|
service: str
|
||||||
|
confidence: float = 1.0
|
||||||
|
cached: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TranslationError(Exception):
|
||||||
|
"""Custom exception for translation errors"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TranslationService(ABC):
|
||||||
|
"""Abstract base class for translation services"""
|
||||||
|
|
||||||
|
def __init__(self, name: str):
|
||||||
|
self.name = name
|
||||||
|
self._cache: Dict[str, str] = {}
|
||||||
|
self._rate_limit_lock = threading.Lock()
|
||||||
|
self._last_request_time = 0.0
|
||||||
|
self._request_count = 0
|
||||||
|
self._rate_limit_per_second = 10 # Default rate limit
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
||||||
|
"""Translate a single text string"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""Check if the service is available"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_supported_languages(self) -> List[Tuple[str, str]]:
|
||||||
|
"""Get list of supported language pairs (code, name)"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def batch_translate(self, texts: List[str], source_lang: str = "ja", target_lang: str = "en") -> List[str]:
|
||||||
|
"""Translate multiple texts with rate limiting - base implementation for services without native batch support"""
|
||||||
|
results = []
|
||||||
|
for text in texts:
|
||||||
|
# Check cache first
|
||||||
|
cache_key = f"{source_lang}_{target_lang}_{text}"
|
||||||
|
if cache_key in self._cache:
|
||||||
|
results.append(self._cache[cache_key])
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
with self._rate_limit_lock:
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self._last_request_time < (1.0 / self._rate_limit_per_second):
|
||||||
|
time.sleep((1.0 / self._rate_limit_per_second) - (current_time - self._last_request_time))
|
||||||
|
|
||||||
|
try:
|
||||||
|
translated = self.translate_text(text, source_lang, target_lang)
|
||||||
|
self._cache[cache_key] = translated
|
||||||
|
results.append(translated)
|
||||||
|
self._last_request_time = time.time()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Translation failed for '{text}': {e}")
|
||||||
|
results.append(text)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def supports_batch_translation(self) -> bool:
|
||||||
|
"""Check if service supports native batch translation"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear_cache(self) -> None:
|
||||||
|
"""Clear the translation cache"""
|
||||||
|
self._cache.clear()
|
||||||
|
logger.info(f"Cleared cache for {self.name}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DeepLService(TranslationService):
|
||||||
|
"""DeepL translation service - requires API key"""
|
||||||
|
|
||||||
|
def __init__(self, api_key: str = "", use_free_api: bool = True):
|
||||||
|
super().__init__("DeepL" + (" (Free)" if use_free_api else " (Pro)"))
|
||||||
|
self.api_key = api_key
|
||||||
|
self.use_free_api = use_free_api
|
||||||
|
self._rate_limit_per_second = 5 # DeepL allows more requests
|
||||||
|
self._base_url = "https://api-free.deepl.com" if use_free_api else "https://api.deepl.com"
|
||||||
|
|
||||||
|
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
||||||
|
"""Translate text using DeepL API"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
|
logger.info(f"DeepL: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
|
if not text or not text.strip():
|
||||||
|
logger.debug("Empty text provided, returning as-is")
|
||||||
|
return text
|
||||||
|
|
||||||
|
if not self.api_key:
|
||||||
|
raise TranslationError("DeepL API key is required")
|
||||||
|
|
||||||
|
# DeepL language codes mapping
|
||||||
|
lang_map = {
|
||||||
|
"ja": "JA", "en": "EN", "ko": "KO", "zh": "ZH",
|
||||||
|
"es": "ES", "fr": "FR", "de": "DE", "it": "IT",
|
||||||
|
"pt": "PT", "ru": "RU", "nl": "NL", "pl": "PL"
|
||||||
|
}
|
||||||
|
source_lang = lang_map.get(source_lang, source_lang.upper())
|
||||||
|
target_lang = lang_map.get(target_lang, target_lang.upper())
|
||||||
|
|
||||||
|
endpoint = f"{self._base_url}/v2/translate"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"DeepL-Auth-Key {self.api_key}",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"text": text,
|
||||||
|
"source_lang": source_lang,
|
||||||
|
"target_lang": target_lang
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug(f"Making request to DeepL API: {endpoint}")
|
||||||
|
response = requests.post(endpoint, headers=headers, data=data, timeout=15)
|
||||||
|
logger.debug(f"DeepL response status: {response.status_code}")
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
logger.debug(f"DeepL response: {result}")
|
||||||
|
|
||||||
|
if "translations" in result and len(result["translations"]) > 0:
|
||||||
|
translated_text = result["translations"][0]["text"]
|
||||||
|
logger.info(f"DeepL SUCCESS: '{text}' -> '{translated_text}'")
|
||||||
|
return translated_text
|
||||||
|
else:
|
||||||
|
raise TranslationError("DeepL API returned no translations")
|
||||||
|
|
||||||
|
except requests.HTTPError as e:
|
||||||
|
if e.response.status_code == 401:
|
||||||
|
raise TranslationError("DeepL API key is invalid")
|
||||||
|
elif e.response.status_code == 403:
|
||||||
|
raise TranslationError("DeepL API key access denied or quota exceeded")
|
||||||
|
elif e.response.status_code == 456:
|
||||||
|
raise TranslationError("DeepL quota exceeded")
|
||||||
|
else:
|
||||||
|
logger.error(f"DeepL HTTP error: {e}")
|
||||||
|
raise TranslationError(f"DeepL API error: {e}")
|
||||||
|
except requests.Timeout:
|
||||||
|
logger.error("DeepL request timed out")
|
||||||
|
raise TranslationError("DeepL request timed out after 15 seconds")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(f"DeepL API request failed: {e}")
|
||||||
|
raise TranslationError(f"DeepL API request failed: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in DeepL: {e}")
|
||||||
|
raise TranslationError(f"Unexpected error: {e}")
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""Check if DeepL service is available"""
|
||||||
|
if not self.api_key:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
headers = {"Authorization": f"DeepL-Auth-Key {self.api_key}"}
|
||||||
|
response = requests.get(f"{self._base_url}/v2/usage", headers=headers, timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_supported_languages(self) -> List[Tuple[str, str]]:
|
||||||
|
"""Get supported languages for DeepL"""
|
||||||
|
return [
|
||||||
|
("ja", "Japanese"),
|
||||||
|
("en", "English"),
|
||||||
|
("ko", "Korean"),
|
||||||
|
("zh", "Chinese"),
|
||||||
|
("es", "Spanish"),
|
||||||
|
("fr", "French"),
|
||||||
|
("de", "German"),
|
||||||
|
("it", "Italian"),
|
||||||
|
("pt", "Portuguese"),
|
||||||
|
("ru", "Russian"),
|
||||||
|
("nl", "Dutch"),
|
||||||
|
("pl", "Polish")
|
||||||
|
]
|
||||||
|
|
||||||
|
def supports_batch_translation(self) -> bool:
|
||||||
|
"""DeepL supports native batch translation"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def batch_translate(self, texts: List[str], source_lang: str = "ja", target_lang: str = "en") -> List[str]:
|
||||||
|
"""Translate multiple texts using DeepL batch API"""
|
||||||
|
if not texts:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Ensure all texts are properly encoded
|
||||||
|
texts = [safe_decode_text(text) for text in texts]
|
||||||
|
logger.info(f"DeepL: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
|
results = [None] * len(texts)
|
||||||
|
uncached_indices = []
|
||||||
|
uncached_texts = []
|
||||||
|
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
if not text or not text.strip():
|
||||||
|
results[i] = text
|
||||||
|
continue
|
||||||
|
|
||||||
|
cache_key = f"{source_lang}_{target_lang}_{text}"
|
||||||
|
if cache_key in self._cache:
|
||||||
|
results[i] = self._cache[cache_key]
|
||||||
|
continue
|
||||||
|
|
||||||
|
uncached_indices.append(i)
|
||||||
|
uncached_texts.append(text)
|
||||||
|
|
||||||
|
if not uncached_texts:
|
||||||
|
logger.info(f"DeepL: All {len(texts)} texts found in cache")
|
||||||
|
return results
|
||||||
|
|
||||||
|
logger.info(f"DeepL: Translating {len(uncached_texts)} uncached texts")
|
||||||
|
|
||||||
|
if not self.api_key:
|
||||||
|
logger.error("DeepL API key is required for batch translation")
|
||||||
|
for i, idx in enumerate(uncached_indices):
|
||||||
|
results[idx] = texts[idx]
|
||||||
|
return results
|
||||||
|
|
||||||
|
# DeepL language codes mapping
|
||||||
|
lang_map = {
|
||||||
|
"ja": "JA", "en": "EN", "ko": "KO", "zh": "ZH",
|
||||||
|
"es": "ES", "fr": "FR", "de": "DE", "it": "IT",
|
||||||
|
"pt": "PT", "ru": "RU", "nl": "NL", "pl": "PL"
|
||||||
|
}
|
||||||
|
source_lang_code = lang_map.get(source_lang, source_lang.upper())
|
||||||
|
target_lang_code = lang_map.get(target_lang, target_lang.upper())
|
||||||
|
|
||||||
|
# Batch size limit for DeepL
|
||||||
|
batch_size = 50
|
||||||
|
|
||||||
|
for batch_start in range(0, len(uncached_texts), batch_size):
|
||||||
|
batch_end = min(batch_start + batch_size, len(uncached_texts))
|
||||||
|
batch_texts = uncached_texts[batch_start:batch_end]
|
||||||
|
batch_indices = uncached_indices[batch_start:batch_end]
|
||||||
|
|
||||||
|
logger.debug(f"DeepL batch {batch_start//batch_size + 1}: Processing {len(batch_texts)} texts")
|
||||||
|
|
||||||
|
endpoint = f"{self._base_url}/v2/translate"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"DeepL-Auth-Key {self.api_key}",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build form data with multiple text parameters (DeepL supports multiple 'text' params)
|
||||||
|
form_data = [
|
||||||
|
('source_lang', source_lang_code),
|
||||||
|
('target_lang', target_lang_code)
|
||||||
|
]
|
||||||
|
for text in batch_texts:
|
||||||
|
form_data.append(('text', text))
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug(f"Making batch request to DeepL API: {endpoint}")
|
||||||
|
|
||||||
|
import requests
|
||||||
|
response = requests.post(endpoint, headers=headers, data=form_data, timeout=30)
|
||||||
|
logger.debug(f"DeepL batch response status: {response.status_code}")
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
logger.debug(f"DeepL batch response: {result}")
|
||||||
|
|
||||||
|
if "translations" in result and len(result["translations"]) == len(batch_texts):
|
||||||
|
for i, translation_data in enumerate(result["translations"]):
|
||||||
|
original_text = batch_texts[i]
|
||||||
|
translated_text = translation_data["text"]
|
||||||
|
original_idx = batch_indices[i]
|
||||||
|
|
||||||
|
cache_key = f"{source_lang}_{target_lang}_{original_text}"
|
||||||
|
self._cache[cache_key] = translated_text
|
||||||
|
|
||||||
|
results[original_idx] = translated_text
|
||||||
|
logger.debug(f"DeepL batch SUCCESS: '{original_text}' -> '{translated_text}'")
|
||||||
|
else:
|
||||||
|
logger.error(f"DeepL batch API returned unexpected response: {result}")
|
||||||
|
for i, idx in enumerate(batch_indices):
|
||||||
|
results[idx] = batch_texts[i]
|
||||||
|
|
||||||
|
# Rate limiting between batches
|
||||||
|
if batch_end < len(uncached_texts):
|
||||||
|
time.sleep(1.0 / self._rate_limit_per_second)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"DeepL batch translation failed: {e}")
|
||||||
|
for i, idx in enumerate(batch_indices):
|
||||||
|
results[idx] = batch_texts[i]
|
||||||
|
|
||||||
|
# Ensure all results are filled
|
||||||
|
for i, result in enumerate(results):
|
||||||
|
if result is None:
|
||||||
|
results[i] = texts[i]
|
||||||
|
|
||||||
|
successful_translations = sum(1 for i, result in enumerate(results) if result != texts[i])
|
||||||
|
logger.info(f"DeepL batch translation complete: {successful_translations}/{len(texts)} successfully translated")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class MyMemoryService(TranslationService):
|
||||||
|
"""MyMemory free translation service - no API key required"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("MyMemory (Free)")
|
||||||
|
self._rate_limit_per_second = 1 # Conservative rate limiting for free service
|
||||||
|
self._base_url = "https://api.mymemory.translated.net"
|
||||||
|
|
||||||
|
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
||||||
|
"""Translate text using MyMemory free API"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
|
logger.info(f"MyMemory: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
|
if not text or not text.strip():
|
||||||
|
logger.debug("Empty text provided, returning as-is")
|
||||||
|
return text
|
||||||
|
|
||||||
|
# MyMemory uses different language codes
|
||||||
|
lang_map = {"ja": "ja", "en": "en", "ko": "ko", "zh": "zh", "es": "es", "fr": "fr", "de": "de"}
|
||||||
|
source_lang = lang_map.get(source_lang, source_lang)
|
||||||
|
target_lang = lang_map.get(target_lang, target_lang)
|
||||||
|
|
||||||
|
endpoint = f"{self._base_url}/get"
|
||||||
|
params = {
|
||||||
|
'q': text,
|
||||||
|
'langpair': f"{source_lang}|{target_lang}",
|
||||||
|
'de': 'neoneko@avatartoolkit.com' # Optional email for higher quotas
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug(f"Making request to MyMemory API: {endpoint} with params: {params}")
|
||||||
|
response = requests.get(endpoint, params=params, timeout=15) # Increased timeout
|
||||||
|
logger.debug(f"MyMemory response status: {response.status_code}")
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
logger.debug(f"MyMemory response: {result}")
|
||||||
|
|
||||||
|
if result.get('responseStatus') == 200 and 'responseData' in result:
|
||||||
|
translated_text = result['responseData']['translatedText']
|
||||||
|
matches = result.get('matches', [])
|
||||||
|
if matches and len(matches) > 0:
|
||||||
|
match_quality = matches[0].get('quality', '0')
|
||||||
|
logger.debug(f"MyMemory translation quality: {match_quality}")
|
||||||
|
|
||||||
|
logger.info(f"MyMemory SUCCESS: '{text}' -> '{translated_text}'")
|
||||||
|
return translated_text
|
||||||
|
else:
|
||||||
|
error_msg = result.get('responseDetails', 'Unknown error')
|
||||||
|
logger.error(f"MyMemory API error: {error_msg}")
|
||||||
|
|
||||||
|
if 'QUOTA_EXCEEDED' in error_msg:
|
||||||
|
raise TranslationError(f"MyMemory daily quota (1000 requests) exceeded. Try again tomorrow or switch to another service.")
|
||||||
|
else:
|
||||||
|
raise TranslationError(f"MyMemory API error: {error_msg}")
|
||||||
|
|
||||||
|
except requests.Timeout as e:
|
||||||
|
logger.error(f"MyMemory request timed out: {e}")
|
||||||
|
raise TranslationError(f"MyMemory request timed out after 15 seconds")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(f"MyMemory API request failed: {e}")
|
||||||
|
raise TranslationError(f"MyMemory API request failed: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in MyMemory: {e}")
|
||||||
|
raise TranslationError(f"Unexpected error: {e}")
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""Check if MyMemory service is available"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self._base_url}/get",
|
||||||
|
params={'q': 'test', 'langpair': 'en|en'},
|
||||||
|
timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_supported_languages(self) -> List[Tuple[str, str]]:
|
||||||
|
"""Get supported languages for MyMemory"""
|
||||||
|
return [
|
||||||
|
("ja", "Japanese"),
|
||||||
|
("en", "English"),
|
||||||
|
("ko", "Korean"),
|
||||||
|
("zh", "Chinese"),
|
||||||
|
("es", "Spanish"),
|
||||||
|
("fr", "French"),
|
||||||
|
("de", "German"),
|
||||||
|
("it", "Italian"),
|
||||||
|
("pt", "Portuguese"),
|
||||||
|
("ru", "Russian")
|
||||||
|
]
|
||||||
|
|
||||||
|
def supports_batch_translation(self) -> bool:
|
||||||
|
"""MyMemory optimized batch processing"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def batch_translate(self, texts: List[str], source_lang: str = "ja", target_lang: str = "en") -> List[str]:
|
||||||
|
"""Translate multiple texts using MyMemory with optimized batching and caching"""
|
||||||
|
if not texts:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Ensure all texts are properly encoded
|
||||||
|
texts = [safe_decode_text(text) for text in texts]
|
||||||
|
logger.info(f"MyMemory: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
|
results = [None] * len(texts)
|
||||||
|
uncached_indices = []
|
||||||
|
uncached_texts = []
|
||||||
|
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
if not text or not text.strip():
|
||||||
|
results[i] = text
|
||||||
|
continue
|
||||||
|
|
||||||
|
cache_key = f"{source_lang}_{target_lang}_{text}"
|
||||||
|
if cache_key in self._cache:
|
||||||
|
results[i] = self._cache[cache_key]
|
||||||
|
continue
|
||||||
|
|
||||||
|
uncached_indices.append(i)
|
||||||
|
uncached_texts.append(text)
|
||||||
|
|
||||||
|
if not uncached_texts:
|
||||||
|
logger.info(f"MyMemory: All {len(texts)} texts found in cache")
|
||||||
|
return results
|
||||||
|
|
||||||
|
logger.info(f"MyMemory: Translating {len(uncached_texts)} uncached texts using concurrent processing")
|
||||||
|
|
||||||
|
# Use concurrent processing for MyMemory to speed up translations
|
||||||
|
import concurrent.futures
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import threading
|
||||||
|
|
||||||
|
def translate_single_text(text_info):
|
||||||
|
idx, text = text_info
|
||||||
|
try:
|
||||||
|
with self._rate_limit_lock:
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self._last_request_time < (1.0 / self._rate_limit_per_second):
|
||||||
|
sleep_time = (1.0 / self._rate_limit_per_second) - (current_time - self._last_request_time)
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
self._last_request_time = time.time()
|
||||||
|
|
||||||
|
translated = self.translate_text(text, source_lang, target_lang)
|
||||||
|
|
||||||
|
cache_key = f"{source_lang}_{target_lang}_{text}"
|
||||||
|
self._cache[cache_key] = translated
|
||||||
|
|
||||||
|
return idx, translated, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"MyMemory concurrent translation failed for '{text}': {e}")
|
||||||
|
return idx, text, e
|
||||||
|
|
||||||
|
# Use conservative concurrent processing (2 workers max for free service)
|
||||||
|
max_workers = min(len(uncached_texts), 2)
|
||||||
|
batch_size = 8
|
||||||
|
|
||||||
|
for batch_start in range(0, len(uncached_texts), batch_size):
|
||||||
|
batch_end = min(batch_start + batch_size, len(uncached_texts))
|
||||||
|
batch_texts = uncached_texts[batch_start:batch_end]
|
||||||
|
batch_indices = uncached_indices[batch_start:batch_end]
|
||||||
|
|
||||||
|
text_info_batch = [(batch_indices[i], text) for i, text in enumerate(batch_texts)]
|
||||||
|
|
||||||
|
logger.debug(f"MyMemory concurrent batch {batch_start//batch_size + 1}: Processing {len(batch_texts)} texts with {max_workers} workers")
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
future_to_text = {executor.submit(translate_single_text, text_info): text_info for text_info in text_info_batch}
|
||||||
|
|
||||||
|
for future in concurrent.futures.as_completed(future_to_text):
|
||||||
|
try:
|
||||||
|
original_idx, translated_text, error = future.result(timeout=25)
|
||||||
|
results[original_idx] = translated_text
|
||||||
|
|
||||||
|
if error is None:
|
||||||
|
logger.debug(f"MyMemory concurrent SUCCESS: -> '{translated_text}'")
|
||||||
|
else:
|
||||||
|
logger.debug(f"MyMemory concurrent FAILED: {error}")
|
||||||
|
|
||||||
|
except concurrent.futures.TimeoutError:
|
||||||
|
text_info = future_to_text[future]
|
||||||
|
original_idx, original_text = text_info
|
||||||
|
results[original_idx] = original_text
|
||||||
|
logger.warning(f"MyMemory concurrent timeout for text: '{original_text}'")
|
||||||
|
except Exception as e:
|
||||||
|
text_info = future_to_text[future]
|
||||||
|
original_idx, original_text = text_info
|
||||||
|
results[original_idx] = original_text
|
||||||
|
logger.error(f"MyMemory concurrent thread error for '{original_text}': {e}")
|
||||||
|
|
||||||
|
# Shorter pause between batches since we're not hammering the API
|
||||||
|
if batch_end < len(uncached_texts):
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
for i, result in enumerate(results):
|
||||||
|
if result is None:
|
||||||
|
results[i] = texts[i]
|
||||||
|
|
||||||
|
successful_translations = sum(1 for i, result in enumerate(results) if result != texts[i])
|
||||||
|
logger.info(f"MyMemory concurrent batch translation complete: {successful_translations}/{len(texts)} successfully translated")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class LibreTranslateService(TranslationService):
|
||||||
|
"""LibreTranslate translation service with configurable server"""
|
||||||
|
|
||||||
|
def __init__(self, api_url: str = "https://libretranslate.com", api_key: str = None):
|
||||||
|
super().__init__("LibreTranslate")
|
||||||
|
# Ensure URL has trailing slash like official implementation
|
||||||
|
self.api_url = api_url.rstrip('/') + '/'
|
||||||
|
self.api_key = api_key
|
||||||
|
self._rate_limit_per_second = 2 # Conservative rate limiting
|
||||||
|
self._is_paid_service = "libretranslate.com" in api_url.lower()
|
||||||
|
|
||||||
|
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
||||||
|
"""Translate text using LibreTranslate API"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
|
logger.info(f"LibreTranslate: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
|
if not text or not text.strip():
|
||||||
|
logger.debug("Empty text provided, returning as-is")
|
||||||
|
return text
|
||||||
|
|
||||||
|
lang_map = {"ja": "ja", "en": "en", "ko": "ko", "zh": "zh", "es": "es", "fr": "fr", "de": "de", "it": "it", "pt": "pt", "ru": "ru"}
|
||||||
|
source_lang = lang_map.get(source_lang, source_lang)
|
||||||
|
target_lang = lang_map.get(target_lang, target_lang)
|
||||||
|
|
||||||
|
endpoint = f"{self.api_url}translate"
|
||||||
|
data = {
|
||||||
|
"q": text,
|
||||||
|
"source": source_lang,
|
||||||
|
"target": target_lang
|
||||||
|
}
|
||||||
|
# Add API key if available (required for libretranslate.com, optional for self-hosted)
|
||||||
|
if self.api_key:
|
||||||
|
data["api_key"] = self.api_key
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug(f"Making request to LibreTranslate API: {endpoint}")
|
||||||
|
# Use JSON format like official API documentation
|
||||||
|
response = requests.post(endpoint, json=data, headers=headers, timeout=15)
|
||||||
|
logger.debug(f"LibreTranslate response status: {response.status_code}")
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
logger.debug(f"LibreTranslate response: {result}")
|
||||||
|
|
||||||
|
if "translatedText" in result:
|
||||||
|
translated_text = result["translatedText"]
|
||||||
|
logger.info(f"LibreTranslate SUCCESS: '{text}' -> '{translated_text}'")
|
||||||
|
return translated_text
|
||||||
|
else:
|
||||||
|
raise TranslationError("LibreTranslate API returned no translation")
|
||||||
|
|
||||||
|
except requests.HTTPError as e:
|
||||||
|
if e.response.status_code == 429:
|
||||||
|
raise TranslationError("LibreTranslate rate limit exceeded")
|
||||||
|
elif e.response.status_code == 400:
|
||||||
|
raise TranslationError("LibreTranslate: Invalid language pair or text")
|
||||||
|
else:
|
||||||
|
logger.error(f"LibreTranslate HTTP error: {e}")
|
||||||
|
raise TranslationError(f"LibreTranslate API error: {e}")
|
||||||
|
except requests.Timeout:
|
||||||
|
logger.error("LibreTranslate request timed out")
|
||||||
|
raise TranslationError("LibreTranslate request timed out after 15 seconds")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(f"LibreTranslate API request failed: {e}")
|
||||||
|
raise TranslationError(f"LibreTranslate API request failed: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in LibreTranslate: {e}")
|
||||||
|
raise TranslationError(f"Unexpected error: {e}")
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""Check if LibreTranslate service is available"""
|
||||||
|
try:
|
||||||
|
endpoint = f"{self.api_url}languages"
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
if self.api_key:
|
||||||
|
params["api_key"] = self.api_key
|
||||||
|
|
||||||
|
response = requests.get(endpoint, params=params if params else None, timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_supported_languages(self) -> List[Tuple[str, str]]:
|
||||||
|
"""Get supported languages for LibreTranslate"""
|
||||||
|
try:
|
||||||
|
endpoint = f"{self.api_url}languages"
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
if self.api_key:
|
||||||
|
params["api_key"] = self.api_key
|
||||||
|
|
||||||
|
response = requests.get(endpoint, params=params if params else None, timeout=5)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
languages = response.json()
|
||||||
|
return [(lang["code"], lang["name"]) for lang in languages]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback to common languages
|
||||||
|
return [
|
||||||
|
("ja", "Japanese"),
|
||||||
|
("en", "English"),
|
||||||
|
("ko", "Korean"),
|
||||||
|
("zh", "Chinese"),
|
||||||
|
("es", "Spanish"),
|
||||||
|
("fr", "French"),
|
||||||
|
("de", "German"),
|
||||||
|
("it", "Italian"),
|
||||||
|
("pt", "Portuguese"),
|
||||||
|
("ru", "Russian")
|
||||||
|
]
|
||||||
|
|
||||||
|
def supports_batch_translation(self) -> bool:
|
||||||
|
"""LibreTranslate optimized batch processing (concurrent requests)"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def batch_translate(self, texts: List[str], source_lang: str = "ja", target_lang: str = "en") -> List[str]:
|
||||||
|
"""Translate multiple texts using LibreTranslate with optimized concurrent requests"""
|
||||||
|
if not texts:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Ensure all texts are properly encoded
|
||||||
|
texts = [safe_decode_text(text) for text in texts]
|
||||||
|
logger.info(f"LibreTranslate: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
|
# Check cache and separate cached vs uncached texts
|
||||||
|
results = [None] * len(texts)
|
||||||
|
uncached_indices = []
|
||||||
|
uncached_texts = []
|
||||||
|
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
if not text or not text.strip():
|
||||||
|
results[i] = text
|
||||||
|
continue
|
||||||
|
|
||||||
|
cache_key = f"{source_lang}_{target_lang}_{text}"
|
||||||
|
if cache_key in self._cache:
|
||||||
|
results[i] = self._cache[cache_key]
|
||||||
|
continue
|
||||||
|
|
||||||
|
uncached_indices.append(i)
|
||||||
|
uncached_texts.append(text)
|
||||||
|
|
||||||
|
if not uncached_texts:
|
||||||
|
logger.info(f"LibreTranslate: All {len(texts)} texts found in cache")
|
||||||
|
return results
|
||||||
|
|
||||||
|
logger.info(f"LibreTranslate: Translating {len(uncached_texts)} uncached texts")
|
||||||
|
|
||||||
|
# LibreTranslate language mapping
|
||||||
|
lang_map = {"ja": "ja", "en": "en", "ko": "ko", "zh": "zh", "es": "es", "fr": "fr", "de": "de", "it": "it", "pt": "pt", "ru": "ru"}
|
||||||
|
source_lang_code = lang_map.get(source_lang, source_lang)
|
||||||
|
target_lang_code = lang_map.get(target_lang, target_lang)
|
||||||
|
|
||||||
|
# Batch process in groups to avoid overwhelming the server
|
||||||
|
import concurrent.futures
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
def translate_single_text(text_info):
|
||||||
|
idx, text = text_info
|
||||||
|
try:
|
||||||
|
translated = self.translate_text(text, source_lang, target_lang)
|
||||||
|
cache_key = f"{source_lang}_{target_lang}_{text}"
|
||||||
|
self._cache[cache_key] = translated
|
||||||
|
return idx, translated, None
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"LibreTranslate translation failed for '{text}': {e}")
|
||||||
|
return idx, text, e
|
||||||
|
|
||||||
|
# Use thread pool for concurrent requests (limited to avoid server overload)
|
||||||
|
max_workers = min(len(uncached_texts), 3)
|
||||||
|
batch_size = 10 # Process in smaller batches
|
||||||
|
|
||||||
|
for batch_start in range(0, len(uncached_texts), batch_size):
|
||||||
|
batch_end = min(batch_start + batch_size, len(uncached_texts))
|
||||||
|
batch_texts = uncached_texts[batch_start:batch_end]
|
||||||
|
batch_indices = uncached_indices[batch_start:batch_end]
|
||||||
|
|
||||||
|
text_info_batch = [(batch_indices[i], text) for i, text in enumerate(batch_texts)]
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
future_to_text = {executor.submit(translate_single_text, text_info): text_info for text_info in text_info_batch}
|
||||||
|
|
||||||
|
for future in concurrent.futures.as_completed(future_to_text):
|
||||||
|
try:
|
||||||
|
original_idx, translated_text, error = future.result(timeout=30)
|
||||||
|
results[original_idx] = translated_text
|
||||||
|
|
||||||
|
if error is None:
|
||||||
|
logger.debug(f"LibreTranslate SUCCESS: -> '{translated_text}'")
|
||||||
|
else:
|
||||||
|
logger.debug(f"LibreTranslate FAILED: {error}")
|
||||||
|
|
||||||
|
except concurrent.futures.TimeoutError:
|
||||||
|
text_info = future_to_text[future]
|
||||||
|
original_idx, original_text = text_info
|
||||||
|
results[original_idx] = original_text
|
||||||
|
logger.warning(f"LibreTranslate timeout for text: '{original_text}'")
|
||||||
|
except Exception as e:
|
||||||
|
text_info = future_to_text[future]
|
||||||
|
original_idx, original_text = text_info
|
||||||
|
results[original_idx] = original_text
|
||||||
|
logger.error(f"LibreTranslate thread error for '{original_text}': {e}")
|
||||||
|
|
||||||
|
if batch_end < len(uncached_texts):
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
for i, result in enumerate(results):
|
||||||
|
if result is None:
|
||||||
|
results[i] = texts[i]
|
||||||
|
|
||||||
|
successful_translations = sum(1 for i, result in enumerate(results) if result != texts[i])
|
||||||
|
logger.info(f"LibreTranslate batch translation complete: {successful_translations}/{len(texts)} successfully translated")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class TranslationServiceManager:
|
||||||
|
"""Manages multiple translation services with fallback logic"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._services: Dict[str, TranslationService] = {}
|
||||||
|
self._primary_service: Optional[str] = None
|
||||||
|
self._initialize_services()
|
||||||
|
|
||||||
|
def _initialize_services(self):
|
||||||
|
"""Initialize available translation services"""
|
||||||
|
mymemory = MyMemoryService()
|
||||||
|
self._services["mymemory"] = mymemory
|
||||||
|
|
||||||
|
libretranslate_url = get_preference("libretranslate_url", "https://libretranslate.com")
|
||||||
|
libretranslate_api_key = get_preference("libretranslate_api_key", "")
|
||||||
|
libretranslate = LibreTranslateService(api_url=libretranslate_url, api_key=libretranslate_api_key if libretranslate_api_key else None)
|
||||||
|
self._services["libretranslate"] = libretranslate
|
||||||
|
|
||||||
|
deepl_api_key = get_preference("deepl_api_key", "")
|
||||||
|
if deepl_api_key:
|
||||||
|
deepl = DeepLService(api_key=deepl_api_key, use_free_api=True)
|
||||||
|
self._services["deepl"] = deepl
|
||||||
|
|
||||||
|
# Set primary service from preferences (default to free service)
|
||||||
|
self._primary_service = get_preference("translation_service", "mymemory")
|
||||||
|
|
||||||
|
logger.info(f"Initialized translation services: {list(self._services.keys())}")
|
||||||
|
logger.info(f"Primary service: {self._primary_service}")
|
||||||
|
|
||||||
|
def get_available_services(self) -> List[Tuple[str, str]]:
|
||||||
|
"""Get list of available translation services"""
|
||||||
|
available = []
|
||||||
|
for service_id, service in self._services.items():
|
||||||
|
if service.is_available():
|
||||||
|
available.append((service_id, service.name))
|
||||||
|
else:
|
||||||
|
logger.debug(f"Service {service.name} is not available")
|
||||||
|
return available
|
||||||
|
|
||||||
|
def set_primary_service(self, service_id: str) -> bool:
|
||||||
|
"""Set the primary translation service"""
|
||||||
|
if service_id in self._services:
|
||||||
|
self._primary_service = service_id
|
||||||
|
save_preference("translation_service", service_id)
|
||||||
|
logger.info(f"Set primary translation service to: {service_id}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_service(self, service_id: Optional[str] = None) -> Optional[TranslationService]:
|
||||||
|
"""Get a translation service by ID"""
|
||||||
|
if service_id is None:
|
||||||
|
service_id = self._primary_service
|
||||||
|
|
||||||
|
if service_id and service_id in self._services:
|
||||||
|
service = self._services[service_id]
|
||||||
|
if service.is_available():
|
||||||
|
return service
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def translate_with_fallback(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> Tuple[str, str]:
|
||||||
|
"""Translate text with automatic fallback to other services"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
|
if not text or not text.strip():
|
||||||
|
return text, "none"
|
||||||
|
|
||||||
|
# Try primary service first
|
||||||
|
primary_service = self.get_service()
|
||||||
|
if primary_service:
|
||||||
|
try:
|
||||||
|
result = primary_service.translate_text(text, source_lang, target_lang)
|
||||||
|
return result, primary_service.name
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Primary service {primary_service.name} failed: {e}")
|
||||||
|
|
||||||
|
for service_id, service in self._services.items():
|
||||||
|
if service_id == self._primary_service:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if service.is_available():
|
||||||
|
try:
|
||||||
|
result = service.translate_text(text, source_lang, target_lang)
|
||||||
|
logger.info(f"Fallback to {service.name} successful")
|
||||||
|
return result, service.name
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Fallback service {service.name} failed: {e}")
|
||||||
|
|
||||||
|
logger.error(f"All translation services failed for: {text}")
|
||||||
|
return text, "failed"
|
||||||
|
|
||||||
|
def batch_translate_with_fallback(self, texts: List[str], source_lang: str = "ja", target_lang: str = "en") -> List[Tuple[str, str]]:
|
||||||
|
"""Batch translate with fallback - uses optimized batch processing when available"""
|
||||||
|
if not texts:
|
||||||
|
return []
|
||||||
|
|
||||||
|
logger.info(f"Starting batch translation of {len(texts)} texts using service manager")
|
||||||
|
|
||||||
|
primary_service = self.get_service()
|
||||||
|
if primary_service:
|
||||||
|
try:
|
||||||
|
if primary_service.supports_batch_translation():
|
||||||
|
logger.info(f"Using native batch translation with {primary_service.name}")
|
||||||
|
translations = primary_service.batch_translate(texts, source_lang, target_lang)
|
||||||
|
return [(translation, primary_service.name) for translation in translations]
|
||||||
|
else:
|
||||||
|
logger.info(f"Service {primary_service.name} does not support batch translation, using individual requests")
|
||||||
|
# Use the base implementation for services without batch support
|
||||||
|
translations = []
|
||||||
|
for text in texts:
|
||||||
|
translated = primary_service.translate_text(text, source_lang, target_lang)
|
||||||
|
translations.append(translated)
|
||||||
|
return [(translation, primary_service.name) for translation in translations]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Batch translation failed with {primary_service.name}: {e}")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for text in texts:
|
||||||
|
translation, service_name = self.translate_with_fallback(text, source_lang, target_lang)
|
||||||
|
results.append((translation, service_name))
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# Global translation service manager instance
|
||||||
|
_translation_manager: Optional[TranslationServiceManager] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_translation_manager() -> TranslationServiceManager:
|
||||||
|
"""Get the global translation service manager"""
|
||||||
|
global _translation_manager
|
||||||
|
if _translation_manager is None:
|
||||||
|
_translation_manager = TranslationServiceManager()
|
||||||
|
return _translation_manager
|
||||||
|
|
||||||
|
|
||||||
|
def configure_deepl_translator(api_key: str, use_free_api: bool = True) -> bool:
|
||||||
|
"""Configure DeepL translation service"""
|
||||||
|
try:
|
||||||
|
save_preference("deepl_api_key", api_key)
|
||||||
|
save_preference("deepl_use_free_api", use_free_api)
|
||||||
|
|
||||||
|
# Test the API key
|
||||||
|
deepl = DeepLService(api_key=api_key, use_free_api=use_free_api)
|
||||||
|
if deepl.is_available():
|
||||||
|
# Re-initialize the global manager to pick up new service
|
||||||
|
global _translation_manager
|
||||||
|
_translation_manager = None
|
||||||
|
logger.info("DeepL translator configured successfully")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("DeepL API key test failed")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to configure DeepL translator: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def configure_libretranslate_server(server_url: str, api_key: str = None) -> bool:
|
||||||
|
"""Configure LibreTranslate server URL and optional API key"""
|
||||||
|
try:
|
||||||
|
if not server_url.strip():
|
||||||
|
server_url = "https://libretranslate.com"
|
||||||
|
|
||||||
|
# Ensure proper URL format
|
||||||
|
if not server_url.startswith(('http://', 'https://')):
|
||||||
|
server_url = 'https://' + server_url
|
||||||
|
|
||||||
|
save_preference("libretranslate_url", server_url)
|
||||||
|
save_preference("libretranslate_api_key", api_key if api_key else "")
|
||||||
|
|
||||||
|
# Test the server
|
||||||
|
libretranslate = LibreTranslateService(api_url=server_url, api_key=api_key)
|
||||||
|
if libretranslate.is_available():
|
||||||
|
# Re-initialize the global manager to pick up new service
|
||||||
|
global _translation_manager
|
||||||
|
_translation_manager = None
|
||||||
|
logger.info(f"LibreTranslate server configured successfully: {server_url}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"LibreTranslate server test failed: {server_url}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to configure LibreTranslate server: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+2
-2
@@ -19,8 +19,8 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
|
|||||||
|
|
||||||
# Define which version series this installation can update to
|
# Define which version series this installation can update to
|
||||||
# For example: ["0.1"] means only look for 0.1.x updates
|
# For example: ["0.1"] means only look for 0.1.x updates
|
||||||
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates
|
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x
|
||||||
ALLOWED_VERSION_SERIES = ["0.3"]
|
ALLOWED_VERSION_SERIES = ["0.6"]
|
||||||
|
|
||||||
is_checking_for_update: bool = False
|
is_checking_for_update: bool = False
|
||||||
update_needed: bool = False
|
update_needed: bool = False
|
||||||
|
|||||||
@@ -0,0 +1,486 @@
|
|||||||
|
import bpy
|
||||||
|
from typing import Dict, List, Optional, Tuple, Set
|
||||||
|
from bpy.types import Object, Bone
|
||||||
|
from .common import get_active_armature
|
||||||
|
from .dictionaries import simplify_bonename, standard_bones, bone_hierarchy, reverse_bone_lookup
|
||||||
|
from .logging_setup import logger
|
||||||
|
from .translations import t
|
||||||
|
|
||||||
|
|
||||||
|
def detect_vrm_armature(armature: Object) -> bool:
|
||||||
|
"""
|
||||||
|
Detect if armature uses VRM bone naming conventions
|
||||||
|
"""
|
||||||
|
if not armature or armature.type != 'ARMATURE':
|
||||||
|
return False
|
||||||
|
|
||||||
|
vrm_patterns = [
|
||||||
|
'jbipchips', 'jbipcspine', 'jbipcchest', 'jbipcneck', 'jbipchead',
|
||||||
|
# Right arm patterns (both single and double R)
|
||||||
|
'jbiprlshoulder', 'jbiprshoulder', 'jbiprupperarm', 'jbiprforearm', 'jbiprhand', 'jbiprlowerarm',
|
||||||
|
'jbiprrupperarm', 'jbiprrforearm', 'jbiprrhand',
|
||||||
|
# Left arm patterns
|
||||||
|
'jbipllshoulder', 'jbiplshoulder', 'jbiplupperarm', 'jbipllforearm', 'jbipllhand', 'jbipllowerarm', 'jbiplhand',
|
||||||
|
# Right leg patterns (both single and double R)
|
||||||
|
'jbiprupperleg', 'jbiprlowerleg', 'jbiprfoot', 'jbiprtoe', 'jbiprtoebase',
|
||||||
|
'jbiprrupperleg', 'jbiprrlowerleg', 'jbiprrfoot', 'jbiprrtoe',
|
||||||
|
# Left leg patterns
|
||||||
|
'jbiplupperleg', 'jbipllowerleg', 'jbipllfoot', 'jbiplfoot', 'jbiplltoe', 'jbipltoebase',
|
||||||
|
# Finger patterns
|
||||||
|
'jbipllittle1', 'jbiprlittle1',
|
||||||
|
'jbiplthumb1', 'jbiplthumb2', 'jbiplthumb3',
|
||||||
|
'jbiplindex1', 'jbiplindex2', 'jbiplindex3',
|
||||||
|
'jbiplmiddle1', 'jbiplmiddle2', 'jbiplmiddle3',
|
||||||
|
'jbiplring1', 'jbiplring2', 'jbiplring3',
|
||||||
|
# Face eye patterns
|
||||||
|
'jadjlfaceeye', 'jadjrfaceeye',
|
||||||
|
# Breast patterns
|
||||||
|
'jseclbust1', 'jseclbust2', 'jseclbust3',
|
||||||
|
'jsecrbust1', 'jsecrbust2', 'jsecrbust3',
|
||||||
|
'jbipc', 'jbipr', 'jbipl'
|
||||||
|
]
|
||||||
|
|
||||||
|
found_vrm_bones = 0
|
||||||
|
for bone_name in armature.data.bones.keys():
|
||||||
|
simplified_name = simplify_bonename(bone_name)
|
||||||
|
if simplified_name.startswith('jbip') or any(pattern in simplified_name for pattern in vrm_patterns):
|
||||||
|
found_vrm_bones += 1
|
||||||
|
|
||||||
|
# Consider it VRM if we find at least 5 VRM bones
|
||||||
|
logger.debug(f"Found {found_vrm_bones} VRM bones in armature {armature.name}")
|
||||||
|
return found_vrm_bones >= 5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def find_vrm_bones_in_armature(armature: Object) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Find VRM bones in armature and return mapping to their actual names using dictionary lookup
|
||||||
|
"""
|
||||||
|
found_bones = {}
|
||||||
|
|
||||||
|
for bone_name in armature.data.bones.keys():
|
||||||
|
simplified_name = simplify_bonename(bone_name)
|
||||||
|
|
||||||
|
# Check if this bone exists in our reverse lookup dictionary
|
||||||
|
if simplified_name in reverse_bone_lookup:
|
||||||
|
standard_bone_key = reverse_bone_lookup[simplified_name]
|
||||||
|
|
||||||
|
# Get the Unity name from standard_bones
|
||||||
|
if standard_bone_key in standard_bones:
|
||||||
|
unity_name = standard_bones[standard_bone_key]
|
||||||
|
found_bones[bone_name] = unity_name
|
||||||
|
logger.debug(f"Found VRM bone via dictionary: {bone_name} -> {unity_name}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Standard bone key '{standard_bone_key}' not found in standard_bones for bone '{bone_name}'")
|
||||||
|
|
||||||
|
# Fallback for unrecognized VRM bones that start with 'jbip'
|
||||||
|
elif simplified_name.startswith('jbip') and bone_name not in found_bones:
|
||||||
|
unity_equivalent = guess_unity_name_from_vrm(simplified_name)
|
||||||
|
if unity_equivalent:
|
||||||
|
found_bones[bone_name] = unity_equivalent
|
||||||
|
logger.debug(f"Guessed VRM bone mapping: {bone_name} -> {unity_equivalent}")
|
||||||
|
|
||||||
|
return found_bones
|
||||||
|
|
||||||
|
|
||||||
|
def guess_unity_name_from_vrm(vrm_simplified: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Attempt to guess Unity bone name from VRM simplified name using dictionary lookup
|
||||||
|
"""
|
||||||
|
if vrm_simplified in reverse_bone_lookup:
|
||||||
|
standard_bone_key = reverse_bone_lookup[vrm_simplified]
|
||||||
|
|
||||||
|
if standard_bone_key in standard_bones:
|
||||||
|
return standard_bones[standard_bone_key]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_vrm_collider_object(obj_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Test if an object name represents a VRM collider
|
||||||
|
"""
|
||||||
|
obj_name_lower = obj_name.lower()
|
||||||
|
collider_patterns = ['collider', 'collision', 'dynamic', 'spring', 'physics', 'secondary']
|
||||||
|
|
||||||
|
# Must contain a collider pattern
|
||||||
|
contains_collider = any(pattern in obj_name_lower for pattern in collider_patterns)
|
||||||
|
if not contains_collider:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Must be VRM-related (multiple detection methods)
|
||||||
|
is_vrm = (
|
||||||
|
'j_bip' in obj_name_lower or
|
||||||
|
'jbip' in simplify_bonename(obj_name) or
|
||||||
|
any(vrm_part in obj_name_lower for vrm_part in ['j_bip_c_', 'j_bip_l_', 'j_bip_r_'])
|
||||||
|
)
|
||||||
|
|
||||||
|
return is_vrm
|
||||||
|
|
||||||
|
|
||||||
|
def remove_collection_from_hierarchy(collection_to_remove) -> bool:
|
||||||
|
"""
|
||||||
|
Recursively remove a collection from all parent collections in the hierarchy
|
||||||
|
"""
|
||||||
|
removed_from_any_parent = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check scene collection
|
||||||
|
scene_collection = bpy.context.scene.collection
|
||||||
|
if collection_to_remove in scene_collection.children:
|
||||||
|
scene_collection.children.unlink(collection_to_remove)
|
||||||
|
logger.debug(f" Unlinked '{collection_to_remove.name}' from scene collection")
|
||||||
|
removed_from_any_parent = True
|
||||||
|
|
||||||
|
# Check all other collections recursively
|
||||||
|
for parent_collection in list(bpy.data.collections):
|
||||||
|
if parent_collection != collection_to_remove and collection_to_remove in parent_collection.children:
|
||||||
|
try:
|
||||||
|
parent_collection.children.unlink(collection_to_remove)
|
||||||
|
logger.debug(f" Unlinked '{collection_to_remove.name}' from parent '{parent_collection.name}'")
|
||||||
|
removed_from_any_parent = True
|
||||||
|
except Exception as unlink_error:
|
||||||
|
logger.warning(f" Failed to unlink '{collection_to_remove.name}' from '{parent_collection.name}': {str(unlink_error)}")
|
||||||
|
|
||||||
|
return removed_from_any_parent
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error removing collection '{collection_to_remove.name}' from hierarchy: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def remove_vrm_colliders(armature: Object = None) -> Tuple[int, List[str], int]:
|
||||||
|
"""
|
||||||
|
Simple approach: Remove ALL objects with 'collider' in their name and clean up empty collections
|
||||||
|
"""
|
||||||
|
objects_to_remove = []
|
||||||
|
removed_names = []
|
||||||
|
collections_to_check = set()
|
||||||
|
|
||||||
|
# Store the current mode and active object
|
||||||
|
current_mode = bpy.context.mode
|
||||||
|
original_active = bpy.context.view_layer.objects.active
|
||||||
|
|
||||||
|
if current_mode != 'OBJECT':
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("Starting simple collider removal - removing ALL objects with 'collider' in name")
|
||||||
|
|
||||||
|
collider_object_names = []
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if 'collider' in obj.name.lower():
|
||||||
|
collider_object_names.append(obj.name)
|
||||||
|
# Track collections this object is in
|
||||||
|
for collection in obj.users_collection:
|
||||||
|
collections_to_check.add(collection)
|
||||||
|
logger.info(f"Found collider object: {obj.name}")
|
||||||
|
|
||||||
|
logger.info(f"Found {len(collider_object_names)} collider objects to remove")
|
||||||
|
|
||||||
|
# Remove collider objects by name
|
||||||
|
removed_count = 0
|
||||||
|
for obj_name in collider_object_names:
|
||||||
|
try:
|
||||||
|
# Check if object still exists
|
||||||
|
if obj_name in bpy.data.objects:
|
||||||
|
obj = bpy.data.objects[obj_name]
|
||||||
|
logger.info(f"Removing collider object: {obj_name}")
|
||||||
|
|
||||||
|
# Remove from all collections first
|
||||||
|
for collection in list(obj.users_collection):
|
||||||
|
collection.objects.unlink(obj)
|
||||||
|
logger.debug(f" Unlinked from collection: {collection.name}")
|
||||||
|
|
||||||
|
bpy.data.objects.remove(obj, do_unlink=True)
|
||||||
|
removed_count += 1
|
||||||
|
removed_names.append(obj_name)
|
||||||
|
logger.info(f" Successfully removed: {obj_name}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Object {obj_name} already removed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to remove collider object {obj_name}: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
logger.info(f"Successfully removed {removed_count} collider objects")
|
||||||
|
|
||||||
|
# Clean up empty collections (prioritize collider-related collections)
|
||||||
|
empty_collections_removed = 0
|
||||||
|
|
||||||
|
# Also check all collections in the scene for collider-related names
|
||||||
|
all_collections_to_check = set(collections_to_check)
|
||||||
|
for collection in bpy.data.collections:
|
||||||
|
collection_name_lower = collection.name.lower()
|
||||||
|
if any(pattern in collection_name_lower for pattern in ['collider', 'collision', 'physics', 'dynamic']):
|
||||||
|
all_collections_to_check.add(collection)
|
||||||
|
logger.debug(f"Found collider-related collection to check: {collection.name}")
|
||||||
|
|
||||||
|
for collection in list(all_collections_to_check):
|
||||||
|
try:
|
||||||
|
# Check if collection exists and is empty
|
||||||
|
if collection.name not in bpy.data.collections:
|
||||||
|
logger.debug(f"Collection {collection.name} already removed")
|
||||||
|
continue
|
||||||
|
|
||||||
|
collection_name_lower = collection.name.lower()
|
||||||
|
is_collider_collection = any(pattern in collection_name_lower for pattern in ['collider', 'collision', 'physics', 'dynamic'])
|
||||||
|
is_empty = len(collection.objects) == 0 and len(collection.children) == 0
|
||||||
|
is_protected = collection.name in ["Collection", "Master Collection"]
|
||||||
|
|
||||||
|
# Remove if empty and (was used by colliders OR has collider-related name)
|
||||||
|
if is_empty and not is_protected and (collection in collections_to_check or is_collider_collection):
|
||||||
|
logger.info(f"Removing empty {'collider-related ' if is_collider_collection else ''}collection: {collection.name}")
|
||||||
|
|
||||||
|
# Use helper function to remove from all parent collections
|
||||||
|
removed_from_parents = remove_collection_from_hierarchy(collection)
|
||||||
|
|
||||||
|
if not removed_from_parents:
|
||||||
|
logger.debug(f" Collection {collection.name} was not found in any parent collections")
|
||||||
|
|
||||||
|
# Remove the collection data
|
||||||
|
try:
|
||||||
|
bpy.data.collections.remove(collection)
|
||||||
|
empty_collections_removed += 1
|
||||||
|
logger.info(f" Successfully removed collection: {collection.name}")
|
||||||
|
except Exception as remove_error:
|
||||||
|
logger.warning(f" Failed to remove collection {collection.name}: {str(remove_error)}")
|
||||||
|
# Continue with other collections even if this one fails
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to remove empty collection {collection.name}: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
logger.debug(f"Collection removal traceback: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
if empty_collections_removed > 0:
|
||||||
|
logger.info(f"Cleaned up {empty_collections_removed} empty collections")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during collider removal: {str(e)}")
|
||||||
|
return 0, [], 0
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if original_active and original_active.name in bpy.data.objects:
|
||||||
|
bpy.context.view_layer.objects.active = original_active
|
||||||
|
|
||||||
|
if current_mode != 'OBJECT':
|
||||||
|
try:
|
||||||
|
bpy.ops.object.mode_set(mode=current_mode)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info(f"Collider removal complete. Removed {len(removed_names)} objects and {empty_collections_removed} collections")
|
||||||
|
return len(removed_names), removed_names, empty_collections_removed
|
||||||
|
|
||||||
|
|
||||||
|
def remove_vrm_root_bone(armature: Object) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Remove unnecessary VRM root bone and make Hips the root bone
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not armature or armature.type != 'ARMATURE':
|
||||||
|
return False, "No valid armature provided"
|
||||||
|
|
||||||
|
# Look for potential root bones and Hips bone
|
||||||
|
potential_roots = []
|
||||||
|
hips_bone = None
|
||||||
|
|
||||||
|
for bone in armature.data.edit_bones:
|
||||||
|
bone_name_lower = bone.name.lower()
|
||||||
|
|
||||||
|
# Check if this could be Hips (various naming conventions)
|
||||||
|
if any(hips_name in bone_name_lower for hips_name in ['hips', 'hip', 'pelvis', 'jbipchips']):
|
||||||
|
hips_bone = bone
|
||||||
|
logger.debug(f"Found Hips bone: {bone.name}")
|
||||||
|
|
||||||
|
# Check if this could be a root bone
|
||||||
|
if bone.parent is None and len(bone.children) > 0:
|
||||||
|
# Common VRM root bone names
|
||||||
|
if any(root_name in bone_name_lower for root_name in ['root', 'vrm', 'armature', 'rig']):
|
||||||
|
potential_roots.append(bone)
|
||||||
|
logger.debug(f"Found potential root bone: {bone.name}")
|
||||||
|
|
||||||
|
if not hips_bone:
|
||||||
|
return False, "Could not find Hips bone to promote as root"
|
||||||
|
|
||||||
|
if not potential_roots:
|
||||||
|
logger.info("No unnecessary root bone found - Hips may already be root")
|
||||||
|
return True, "No root bone removal needed"
|
||||||
|
|
||||||
|
# Find the root bone that is the parent of Hips
|
||||||
|
root_to_remove = None
|
||||||
|
for root_bone in potential_roots:
|
||||||
|
if hips_bone.parent == root_bone:
|
||||||
|
root_to_remove = root_bone
|
||||||
|
break
|
||||||
|
|
||||||
|
if not root_to_remove:
|
||||||
|
# Check if Hips is already parentless (already root)
|
||||||
|
if hips_bone.parent is None:
|
||||||
|
logger.info("Hips bone is already the root bone")
|
||||||
|
return True, "Hips is already root - no changes needed"
|
||||||
|
else:
|
||||||
|
logger.warning(f"Hips bone has parent '{hips_bone.parent.name}' but no matching root found")
|
||||||
|
return False, "Could not identify safe root bone to remove"
|
||||||
|
|
||||||
|
root_name = root_to_remove.name
|
||||||
|
logger.info(f"Removing root bone '{root_name}' and promoting Hips to root")
|
||||||
|
|
||||||
|
# Reparent all children of the root bone (except Hips) to Hips
|
||||||
|
children_to_reparent = []
|
||||||
|
for child in root_to_remove.children:
|
||||||
|
if child != hips_bone:
|
||||||
|
children_to_reparent.append(child)
|
||||||
|
|
||||||
|
hips_bone.parent = None
|
||||||
|
|
||||||
|
for child in children_to_reparent:
|
||||||
|
child.parent = hips_bone
|
||||||
|
logger.debug(f"Reparented {child.name} from {root_name} to {hips_bone.name}")
|
||||||
|
|
||||||
|
armature.data.edit_bones.remove(root_to_remove)
|
||||||
|
|
||||||
|
message = f"Removed root bone '{root_name}' - Hips is now the root bone"
|
||||||
|
logger.info(message)
|
||||||
|
return True, message
|
||||||
|
|
||||||
|
|
||||||
|
def convert_vrm_to_unity(armature: Object, remove_colliders: bool = True, remove_root: bool = True) -> Tuple[bool, List[str], int]:
|
||||||
|
"""
|
||||||
|
Convert VRM armature bone names to Unity humanoid format
|
||||||
|
"""
|
||||||
|
if not armature or armature.type != 'ARMATURE':
|
||||||
|
return False, ["No valid armature selected"], 0
|
||||||
|
|
||||||
|
logger.info(f"Starting VRM to Unity conversion for armature: {armature.name}")
|
||||||
|
|
||||||
|
# Check if this is a VRM armature
|
||||||
|
if not detect_vrm_armature(armature):
|
||||||
|
return False, ["Selected armature does not appear to be a VRM armature"], 0
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
converted_count = 0
|
||||||
|
failed_conversions = []
|
||||||
|
collider_count = 0
|
||||||
|
|
||||||
|
current_mode = bpy.context.mode
|
||||||
|
if current_mode != 'EDIT':
|
||||||
|
bpy.context.view_layer.objects.active = armature
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# First, remove collider objects and bones if requested
|
||||||
|
if remove_colliders:
|
||||||
|
collider_count, removed_colliders, collections_removed = remove_vrm_colliders(armature)
|
||||||
|
if collider_count > 0 or collections_removed > 0:
|
||||||
|
if collections_removed > 0:
|
||||||
|
messages.append(f"Removed {collider_count} VRM collider objects and {collections_removed} empty collections")
|
||||||
|
else:
|
||||||
|
messages.append(f"Removed {collider_count} VRM collider objects")
|
||||||
|
logger.info(f"Removed {collider_count} VRM colliders: {removed_colliders}")
|
||||||
|
|
||||||
|
vrm_bones = find_vrm_bones_in_armature(armature)
|
||||||
|
|
||||||
|
if not vrm_bones:
|
||||||
|
if remove_colliders and (collider_count > 0 or collections_removed > 0):
|
||||||
|
messages.append("No VRM bones found to convert (colliders were removed)")
|
||||||
|
return True, messages, 0
|
||||||
|
else:
|
||||||
|
return False, ["No VRM bones found in armature"], 0
|
||||||
|
|
||||||
|
if bpy.context.mode != 'EDIT':
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
# Remove unnecessary root bone if requested
|
||||||
|
if remove_root:
|
||||||
|
root_success, root_message = remove_vrm_root_bone(armature)
|
||||||
|
messages.append(root_message)
|
||||||
|
if not root_success:
|
||||||
|
logger.warning(f"Root bone removal failed: {root_message}")
|
||||||
|
|
||||||
|
# Rename bones
|
||||||
|
for vrm_bone_name, unity_name in vrm_bones.items():
|
||||||
|
if vrm_bone_name in armature.data.edit_bones:
|
||||||
|
bone = armature.data.edit_bones[vrm_bone_name]
|
||||||
|
|
||||||
|
# Check if target name already exists
|
||||||
|
if unity_name in armature.data.edit_bones and unity_name != vrm_bone_name:
|
||||||
|
failed_conversions.append(f"{vrm_bone_name} -> {unity_name} (name conflict)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Rename the bone
|
||||||
|
bone.name = unity_name
|
||||||
|
converted_count += 1
|
||||||
|
logger.debug(f"Renamed bone: {vrm_bone_name} -> {unity_name}")
|
||||||
|
|
||||||
|
messages.append(f"Successfully converted {converted_count} VRM bones to Unity format")
|
||||||
|
|
||||||
|
if failed_conversions:
|
||||||
|
messages.append("Failed conversions due to name conflicts:")
|
||||||
|
messages.extend(failed_conversions)
|
||||||
|
|
||||||
|
logger.info(f"VRM to Unity conversion completed. Converted {converted_count} bones")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during VRM conversion: {str(e)}")
|
||||||
|
messages.append(f"Error during conversion: {str(e)}")
|
||||||
|
return False, messages, converted_count
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Restore original mode
|
||||||
|
if current_mode != 'EDIT':
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
return converted_count > 0 or (remove_colliders and collider_count > 0), messages, converted_count
|
||||||
|
|
||||||
|
|
||||||
|
def validate_unity_hierarchy(armature: Object) -> Tuple[bool, List[str]]:
|
||||||
|
"""
|
||||||
|
Validate that the converted armature has proper Unity humanoid hierarchy
|
||||||
|
"""
|
||||||
|
if not armature or armature.type != 'ARMATURE':
|
||||||
|
return False, ["No valid armature to validate"]
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
is_valid = True
|
||||||
|
|
||||||
|
# Check for essential Unity bones
|
||||||
|
essential_unity_bones = [
|
||||||
|
standard_bones['hips'],
|
||||||
|
standard_bones['spine'],
|
||||||
|
standard_bones['chest'],
|
||||||
|
standard_bones['neck'],
|
||||||
|
standard_bones['head']
|
||||||
|
]
|
||||||
|
|
||||||
|
missing_bones = []
|
||||||
|
for bone_name in essential_unity_bones:
|
||||||
|
if bone_name not in armature.data.bones:
|
||||||
|
missing_bones.append(bone_name)
|
||||||
|
|
||||||
|
if missing_bones:
|
||||||
|
is_valid = False
|
||||||
|
messages.append("Missing essential Unity bones:")
|
||||||
|
messages.extend([f"- {bone}" for bone in missing_bones])
|
||||||
|
|
||||||
|
# Validate basic hierarchy
|
||||||
|
hierarchy_issues = []
|
||||||
|
for parent_name, child_name in bone_hierarchy:
|
||||||
|
if parent_name in armature.data.bones and child_name in armature.data.bones:
|
||||||
|
parent_bone = armature.data.bones[parent_name]
|
||||||
|
child_bone = armature.data.bones[child_name]
|
||||||
|
|
||||||
|
if child_bone.parent != parent_bone:
|
||||||
|
hierarchy_issues.append(f"{parent_name} -> {child_name}")
|
||||||
|
|
||||||
|
if hierarchy_issues:
|
||||||
|
is_valid = False
|
||||||
|
messages.append("Hierarchy issues found:")
|
||||||
|
messages.extend([f"- {issue}" for issue in hierarchy_issues])
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
messages.append("Unity hierarchy validation passed")
|
||||||
|
|
||||||
|
return is_valid, messages
|
||||||
@@ -28,7 +28,15 @@ def scale_images_to_largest(images: List[Image]) -> tuple[int, int]:
|
|||||||
x: int = 0
|
x: int = 0
|
||||||
y: int = 0
|
y: int = 0
|
||||||
|
|
||||||
valid_images = [img for img in images if img and img.has_data]
|
valid_images = []
|
||||||
|
for img in images:
|
||||||
|
if img:
|
||||||
|
try:
|
||||||
|
if img.has_data:
|
||||||
|
valid_images.append(img)
|
||||||
|
except ReferenceError:
|
||||||
|
# Image has been removed from Blender's memory
|
||||||
|
pass
|
||||||
|
|
||||||
if not valid_images:
|
if not valid_images:
|
||||||
return 0, 0
|
return 0, 0
|
||||||
@@ -66,50 +74,56 @@ def get_material_images_from_scene(context: Context) -> list[MaterialImageList]:
|
|||||||
new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo]
|
new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo]
|
||||||
except Exception:
|
except Exception:
|
||||||
name = mat_slot.material.name + "_albedo_replacement"
|
name = mat_slot.material.name + "_albedo_replacement"
|
||||||
if name in bpy.data.images:
|
if name not in bpy.data.images:
|
||||||
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
|
||||||
new_mat_image_item.albedo = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
new_mat_image_item.albedo = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
|
new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
|
||||||
|
else:
|
||||||
|
new_mat_image_item.albedo = bpy.data.images[name]
|
||||||
try:
|
try:
|
||||||
new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal]
|
new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal]
|
||||||
except Exception:
|
except Exception:
|
||||||
name = mat_slot.material.name + "_normal_replacement"
|
name = mat_slot.material.name + "_normal_replacement"
|
||||||
if name in bpy.data.images:
|
if name not in bpy.data.images:
|
||||||
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
|
||||||
new_mat_image_item.normal = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
new_mat_image_item.normal = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32)
|
new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32)
|
||||||
|
else:
|
||||||
|
new_mat_image_item.normal = bpy.data.images[name]
|
||||||
try:
|
try:
|
||||||
new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission]
|
new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission]
|
||||||
except Exception:
|
except Exception:
|
||||||
name = mat_slot.material.name + "_emission_replacement"
|
name = mat_slot.material.name + "_emission_replacement"
|
||||||
if name in bpy.data.images:
|
if name not in bpy.data.images:
|
||||||
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
|
||||||
new_mat_image_item.emission = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
new_mat_image_item.emission = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
|
new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
|
||||||
|
else:
|
||||||
|
new_mat_image_item.emission = bpy.data.images[name]
|
||||||
try:
|
try:
|
||||||
new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion]
|
new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion]
|
||||||
except Exception:
|
except Exception:
|
||||||
name = mat_slot.material.name + "_ambient_occlusion_replacement"
|
name = mat_slot.material.name + "_ambient_occlusion_replacement"
|
||||||
if name in bpy.data.images:
|
if name not in bpy.data.images:
|
||||||
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
|
||||||
new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32)
|
new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32)
|
||||||
|
else:
|
||||||
|
new_mat_image_item.ambient_occlusion = bpy.data.images[name]
|
||||||
try:
|
try:
|
||||||
new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height]
|
new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height]
|
||||||
except Exception:
|
except Exception:
|
||||||
name = mat_slot.material.name + "_height_replacement"
|
name = mat_slot.material.name + "_height_replacement"
|
||||||
if name in bpy.data.images:
|
if name not in bpy.data.images:
|
||||||
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
|
||||||
new_mat_image_item.height = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
new_mat_image_item.height = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32)
|
new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32)
|
||||||
|
else:
|
||||||
|
new_mat_image_item.height = bpy.data.images[name]
|
||||||
try:
|
try:
|
||||||
new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness]
|
new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness]
|
||||||
except Exception:
|
except Exception:
|
||||||
name = mat_slot.material.name + "_roughness_replacement"
|
name = mat_slot.material.name + "_roughness_replacement"
|
||||||
if name in bpy.data.images:
|
if name not in bpy.data.images:
|
||||||
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
|
||||||
new_mat_image_item.roughness = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
new_mat_image_item.roughness = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32)
|
new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32)
|
||||||
|
else:
|
||||||
|
new_mat_image_item.roughness = bpy.data.images[name]
|
||||||
|
|
||||||
new_mat_image_item.material = mat_slot.material
|
new_mat_image_item.material = mat_slot.material
|
||||||
new_mat_image_item.parent_mesh = obj
|
new_mat_image_item.parent_mesh = obj
|
||||||
@@ -227,7 +241,7 @@ class AvatarToolKit_OT_AtlasMaterials(Operator):
|
|||||||
# Create material nodes
|
# Create material nodes
|
||||||
atlased_mat.material = bpy.data.materials.new(
|
atlased_mat.material = bpy.data.materials.new(
|
||||||
name=f"Atlas_Final_{context.scene.name}_{Path(bpy.data.filepath).stem}")
|
name=f"Atlas_Final_{context.scene.name}_{Path(bpy.data.filepath).stem}")
|
||||||
atlased_mat.material.use_nodes = True
|
# Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes
|
||||||
atlased_mat.material.node_tree.nodes.clear()
|
atlased_mat.material.node_tree.nodes.clear()
|
||||||
|
|
||||||
principled_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
|
principled_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from ...core.translations import t
|
|||||||
import traceback
|
import traceback
|
||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
|
get_meshes_for_armature,
|
||||||
fix_zero_length_bones,
|
fix_zero_length_bones,
|
||||||
remove_unused_vertex_groups,
|
remove_unused_vertex_groups,
|
||||||
clear_unused_data_blocks,
|
clear_unused_data_blocks,
|
||||||
@@ -28,10 +29,32 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
return len(get_all_meshes(context)) > 1
|
# Check if we have valid armature selections for merging
|
||||||
|
base_armature_name: str = context.scene.avatar_toolkit.merge_armature_into
|
||||||
|
merge_armature_name: str = context.scene.avatar_toolkit.merge_armature
|
||||||
|
|
||||||
|
if not base_armature_name or not merge_armature_name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
|
||||||
|
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
|
||||||
|
|
||||||
|
return (base_armature is not None and
|
||||||
|
merge_armature is not None and
|
||||||
|
base_armature.type == 'ARMATURE' and
|
||||||
|
merge_armature.type == 'ARMATURE' and
|
||||||
|
base_armature != merge_armature)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
try:
|
try:
|
||||||
|
# Store original mode to restore later
|
||||||
|
original_mode: str = context.mode
|
||||||
|
logger.debug(f"Original mode: {original_mode}")
|
||||||
|
|
||||||
|
# Switch to object mode if not already
|
||||||
|
if context.mode != 'OBJECT':
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
wm.progress_begin(0, 100)
|
wm.progress_begin(0, 100)
|
||||||
|
|
||||||
@@ -49,6 +72,9 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
|||||||
data_breaking_base = store_breaking_settings_armature(base_armature)
|
data_breaking_base = store_breaking_settings_armature(base_armature)
|
||||||
data_breaking_merge = store_breaking_settings_armature(merge_armature)
|
data_breaking_merge = store_breaking_settings_armature(merge_armature)
|
||||||
|
|
||||||
|
# Store the merge armature name before it gets removed during join
|
||||||
|
merge_armature_name_stored = merge_armature.name
|
||||||
|
|
||||||
# Remove Rigid Bodies and Joints
|
# Remove Rigid Bodies and Joints
|
||||||
delete_rigidbodies_and_joints(base_armature)
|
delete_rigidbodies_and_joints(base_armature)
|
||||||
delete_rigidbodies_and_joints(merge_armature)
|
delete_rigidbodies_and_joints(merge_armature)
|
||||||
@@ -76,16 +102,43 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
|||||||
wm.progress_update(100)
|
wm.progress_update(100)
|
||||||
wm.progress_end()
|
wm.progress_end()
|
||||||
|
|
||||||
|
# Restore settings only for the base armature since merge_armature is removed during join
|
||||||
restore_breaking_settings_armature(base_armature, data_breaking_base)
|
restore_breaking_settings_armature(base_armature, data_breaking_base)
|
||||||
restore_breaking_settings_armature(merge_armature, data_breaking_merge)
|
if merge_armature_name_stored in bpy.data.objects:
|
||||||
|
merge_armature_obj = bpy.data.objects[merge_armature_name_stored]
|
||||||
|
restore_breaking_settings_armature(merge_armature_obj, data_breaking_merge)
|
||||||
|
|
||||||
|
# Restore original mode if it wasn't OBJECT
|
||||||
|
try:
|
||||||
|
if original_mode == 'EDIT_ARMATURE':
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
elif original_mode == 'POSE':
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
elif original_mode != 'OBJECT':
|
||||||
|
logger.debug(f"Restoring to original mode: {original_mode}")
|
||||||
|
# For other modes, stay in object mode as it's safest
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"Could not restore original mode: {original_mode}")
|
||||||
|
|
||||||
self.report({'INFO'}, t('MergeArmature.success'))
|
self.report({'INFO'}, t('MergeArmature.success'))
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error merging armatures:", exception=e)
|
errormessage: str = traceback.format_exc()
|
||||||
self.report({'ERROR'}, traceback.format_exc())
|
logger.error(f"Error merging armatures: {str(e)}\n{errormessage}")
|
||||||
|
self.report({'ERROR'}, f"Error merging armatures: {errormessage}")
|
||||||
|
|
||||||
|
# Try to restore original mode even on error
|
||||||
|
try:
|
||||||
|
if 'original_mode' in locals() and original_mode != 'OBJECT':
|
||||||
|
if original_mode == 'EDIT_ARMATURE':
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
elif original_mode == 'POSE':
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Could not restore mode after error")
|
||||||
|
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
def delete_rigidbodies_and_joints(armature: Object) -> None:
|
def delete_rigidbodies_and_joints(armature: Object) -> None:
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class AvatarToolkit_OT_StopPoseMode(Operator):
|
|||||||
self.report({'ERROR'}, t("PoseMode.error.stop", error=traceback.format_exc()))
|
self.report({'ERROR'}, t("PoseMode.error.stop", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
|
||||||
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
|
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
|
||||||
bl_label = t("QuickAccess.apply_pose_as_shapekey.label")
|
bl_label = t("QuickAccess.apply_pose_as_shapekey.label")
|
||||||
bl_description = t("QuickAccess.apply_pose_as_shapekey.desc")
|
bl_description = t("QuickAccess.apply_pose_as_shapekey.desc")
|
||||||
@@ -136,7 +136,7 @@ class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
|||||||
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=traceback.format_exc()))
|
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
|
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
||||||
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
|
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
|
||||||
bl_label = t("QuickAccess.apply_pose_as_rest.label")
|
bl_label = t("QuickAccess.apply_pose_as_rest.label")
|
||||||
bl_description = t("QuickAccess.apply_pose_as_rest.desc")
|
bl_description = t("QuickAccess.apply_pose_as_rest.desc")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import traceback
|
import traceback
|
||||||
import bpy
|
import bpy
|
||||||
|
import bpy_extras
|
||||||
|
from bpy_extras import anim_utils
|
||||||
import re
|
import re
|
||||||
from bpy.types import Operator, Context, EditBone, Object, Armature, Mesh
|
from bpy.types import Operator, Context, EditBone, Object, Armature, Mesh
|
||||||
from typing import Optional, Dict, Any, List, Tuple
|
from typing import Optional, Dict, Any, List, Tuple
|
||||||
@@ -186,7 +188,6 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
if not armature:
|
if not armature:
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
# Store initial transforms
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
initial_transforms: Dict[str, Dict[str, Any]] = {}
|
initial_transforms: Dict[str, Dict[str, Any]] = {}
|
||||||
data_breaking = store_breaking_settings_armature(armature)
|
data_breaking = store_breaking_settings_armature(armature)
|
||||||
@@ -200,56 +201,61 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
'parent': bone.parent.name if bone.parent else None
|
'parent': bone.parent.name if bone.parent else None
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get weighted bones
|
# Get bones with any weight
|
||||||
weighted_bones: List[str] = []
|
weighted_bones: List[str] = []
|
||||||
meshes = get_all_meshes(context)
|
meshes = get_all_meshes(context)
|
||||||
zero_weight_bones: List[str] = []
|
|
||||||
|
|
||||||
for mesh in meshes:
|
for mesh in meshes:
|
||||||
mesh_data: Mesh = mesh.data
|
for vertex in mesh.data.vertices:
|
||||||
for vertex in mesh_data.vertices:
|
|
||||||
for group in vertex.groups:
|
for group in vertex.groups:
|
||||||
if group.weight > context.scene.avatar_toolkit.merge_weights_threshold:
|
if group.weight > context.scene.avatar_toolkit.merge_weights_threshold:
|
||||||
weighted_bones.append(mesh.vertex_groups[group.group].name)
|
vg = mesh.vertex_groups[group.group]
|
||||||
|
if vg.name not in weighted_bones:
|
||||||
|
weighted_bones.append(vg.name)
|
||||||
|
|
||||||
# Process bone removal
|
armature_data = armature.data
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
armature_data: Armature = armature.data
|
|
||||||
removed_count = 0
|
removed_count = 0
|
||||||
|
zero_weight_bones: List[str] = []
|
||||||
|
|
||||||
for bone in armature_data.edit_bones[:]: # Create a copy of the list
|
def is_zero_weight_chain(bone, weighted_bones, preserve_check_fn):
|
||||||
if (bone.name not in weighted_bones and
|
if bone.name in weighted_bones or preserve_check_fn(bone.name, context):
|
||||||
not self.should_preserve_bone(bone.name, context)):
|
return False
|
||||||
|
return all(is_zero_weight_chain(child, weighted_bones, preserve_check_fn) for child in bone.children)
|
||||||
|
|
||||||
|
for bone in armature_data.edit_bones[:]:
|
||||||
|
if bone.name in weighted_bones or self.should_preserve_bone(bone.name, context):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not is_zero_weight_chain(bone, weighted_bones, self.should_preserve_bone):
|
||||||
|
continue
|
||||||
|
|
||||||
if context.scene.avatar_toolkit.list_only_mode:
|
if context.scene.avatar_toolkit.list_only_mode:
|
||||||
zero_weight_bones.append(bone.name)
|
zero_weight_bones.append(bone.name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Store children data
|
# Traverse and collect the full empty chain
|
||||||
children = bone.children
|
stack = [bone]
|
||||||
children_data = {child.name: initial_transforms[child.name] for child in children}
|
chain = []
|
||||||
|
|
||||||
# Reparent children
|
while stack:
|
||||||
for child in children:
|
b = stack.pop()
|
||||||
|
chain.append(b)
|
||||||
|
stack.extend(b.children)
|
||||||
|
|
||||||
|
for b in reversed(chain): # Remove children before parents
|
||||||
|
for child in b.children:
|
||||||
child.use_connect = False
|
child.use_connect = False
|
||||||
if bone.parent:
|
if b.parent:
|
||||||
child.parent = bone.parent
|
child.parent = b.parent
|
||||||
|
if b.name in armature_data.edit_bones:
|
||||||
# Remove bone
|
armature_data.edit_bones.remove(b)
|
||||||
armature_data.edit_bones.remove(bone)
|
|
||||||
removed_count += 1
|
removed_count += 1
|
||||||
|
|
||||||
# Restore children positions
|
|
||||||
for child_name, data in children_data.items():
|
|
||||||
if child_name in armature_data.edit_bones:
|
|
||||||
child = armature_data.edit_bones[child_name]
|
|
||||||
restore_bone_transforms(child, data)
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
if context.scene.avatar_toolkit.list_only_mode:
|
if context.scene.avatar_toolkit.list_only_mode:
|
||||||
self.populate_bone_list(context, zero_weight_bones)
|
self.populate_bone_list(context, zero_weight_bones)
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
restore_breaking_settings_armature(armature, data_breaking)
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
@@ -343,10 +349,17 @@ class AvatarToolKit_OT_FlipCurrentKeyFrames(Operator):
|
|||||||
armature_data.bones.foreach_set("select", [False] * len(armature_data.bones))
|
armature_data.bones.foreach_set("select", [False] * len(armature_data.bones))
|
||||||
|
|
||||||
|
|
||||||
|
# Get channelbag for the action using Blender 5.0 API
|
||||||
|
action = armature.animation_data.action
|
||||||
|
if not action.slots:
|
||||||
|
slot = action.slots.new(for_id=armature.data)
|
||||||
|
else:
|
||||||
|
slot = action.slots[0]
|
||||||
|
channelbag = anim_utils.action_ensure_channelbag_for_slot(action, slot)
|
||||||
|
|
||||||
#create a set for every frame time where we need to key a keyframe for the flipped pose
|
#create a set for every frame time where we need to key a keyframe for the flipped pose
|
||||||
times: Dict[float,list[bpy.types.FCurve]] = {}
|
times: Dict[float,list[bpy.types.FCurve]] = {}
|
||||||
for curve in armature.animation_data.action.fcurves:
|
for curve in channelbag.fcurves:
|
||||||
if not curve.data_path.startswith("pose"):
|
if not curve.data_path.startswith("pose"):
|
||||||
continue
|
continue
|
||||||
for point in curve.keyframe_points:
|
for point in curve.keyframe_points:
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
from ...core.logging_setup import logger
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_RemoveAllColliders(Operator):
|
||||||
|
"""Remove all objects with 'collider' in their name"""
|
||||||
|
bl_idname = "avatar_toolkit.remove_all_colliders"
|
||||||
|
bl_label = "Remove All Colliders"
|
||||||
|
bl_description = "Remove all objects that have 'collider' in their name"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
logger.info("Starting standalone collider removal")
|
||||||
|
|
||||||
|
# Store current mode and active object
|
||||||
|
current_mode = bpy.context.mode
|
||||||
|
original_active = bpy.context.view_layer.objects.active
|
||||||
|
|
||||||
|
# Switch to object mode
|
||||||
|
if current_mode != 'OBJECT':
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find all collider objects
|
||||||
|
collider_names = []
|
||||||
|
all_objects = list(bpy.data.objects)
|
||||||
|
|
||||||
|
logger.info(f"Scanning {len(all_objects)} objects for colliders")
|
||||||
|
|
||||||
|
for obj in all_objects:
|
||||||
|
if 'collider' in obj.name.lower():
|
||||||
|
collider_names.append(obj.name)
|
||||||
|
logger.info(f"Found collider: {obj.name}")
|
||||||
|
|
||||||
|
if not collider_names:
|
||||||
|
self.report({'INFO'}, "No collider objects found")
|
||||||
|
logger.info("No collider objects found")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
logger.info(f"Found {len(collider_names)} collider objects to remove")
|
||||||
|
self.report({'INFO'}, f"Found {len(collider_names)} collider objects")
|
||||||
|
|
||||||
|
# Remove each collider
|
||||||
|
removed_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
|
||||||
|
for obj_name in collider_names:
|
||||||
|
try:
|
||||||
|
if obj_name in bpy.data.objects:
|
||||||
|
obj = bpy.data.objects[obj_name]
|
||||||
|
|
||||||
|
# Deselect all objects first
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
|
||||||
|
# Select and make active
|
||||||
|
obj.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = obj
|
||||||
|
|
||||||
|
# Delete the object
|
||||||
|
bpy.ops.object.delete(use_global=False)
|
||||||
|
|
||||||
|
removed_count += 1
|
||||||
|
logger.info(f"Removed collider: {obj_name}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug(f"Object {obj_name} no longer exists")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
failed_count += 1
|
||||||
|
logger.error(f"Failed to remove {obj_name}: {str(e)}")
|
||||||
|
self.report({'WARNING'}, f"Failed to remove {obj_name}: {str(e)}")
|
||||||
|
|
||||||
|
# Report results
|
||||||
|
if removed_count > 0:
|
||||||
|
success_msg = f"Successfully removed {removed_count} collider objects"
|
||||||
|
logger.info(success_msg)
|
||||||
|
self.report({'INFO'}, success_msg)
|
||||||
|
|
||||||
|
if failed_count > 0:
|
||||||
|
failure_msg = f"Failed to remove {failed_count} collider objects"
|
||||||
|
logger.warning(failure_msg)
|
||||||
|
self.report({'WARNING'}, failure_msg)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error during collider removal: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
self.report({'ERROR'}, error_msg)
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Restore original state
|
||||||
|
try:
|
||||||
|
if original_active and original_active.name in bpy.data.objects:
|
||||||
|
bpy.context.view_layer.objects.active = original_active
|
||||||
|
|
||||||
|
if current_mode != 'OBJECT':
|
||||||
|
bpy.ops.object.mode_set(mode=current_mode)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
@@ -119,8 +119,10 @@ class AvatarToolkit_OT_ExplodeMesh(Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
|
active_obj = context.view_layer.objects.active
|
||||||
return context.view_layer.objects.active.type == "MESH" and len(context.view_layer.objects.selected) == 1
|
return (active_obj is not None and
|
||||||
|
active_obj.type == "MESH" and
|
||||||
|
len(context.view_layer.objects.selected) == 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import traceback
|
|||||||
import bpy
|
import bpy
|
||||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||||
from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint
|
from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint
|
||||||
from ...core.common import get_active_armature
|
from ...core.common import get_active_armature, transfer_vertex_weights, get_all_meshes
|
||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
|
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
|
||||||
@@ -69,19 +69,50 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
|||||||
|
|
||||||
# Set armature as active object before mode switch
|
# Set armature as active object before mode switch
|
||||||
bpy.context.view_layer.objects.active = armature
|
bpy.context.view_layer.objects.active = armature
|
||||||
|
|
||||||
|
# Get all meshes for weight transfer
|
||||||
|
meshes = get_all_meshes(bpy.context)
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
bones_to_remove: List[str] = []
|
bones_to_remove: List[str] = []
|
||||||
for bone in armature.data.edit_bones:
|
for bone in armature.data.edit_bones:
|
||||||
if any(pattern in bone.name.lower() for pattern in rigify_unnecessary_bones):
|
bone_name_lower = bone.name.lower()
|
||||||
|
if any(bone_name_lower.startswith(pattern) or bone_name_lower == pattern
|
||||||
|
for pattern in rigify_unnecessary_bones):
|
||||||
bones_to_remove.append(bone.name)
|
bones_to_remove.append(bone.name)
|
||||||
|
|
||||||
|
# Check for neck bones that need merging
|
||||||
|
merge_neck_bones = 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
# Transfer weights from bones being removed
|
||||||
|
for bone_name in bones_to_remove:
|
||||||
|
if bone_name in armature.data.bones:
|
||||||
|
logger.debug(f"Transferring weights from bone: {bone_name}")
|
||||||
|
for mesh in meshes:
|
||||||
|
if bone_name in mesh.vertex_groups:
|
||||||
|
# Remove the vertex group since we don't need the weights
|
||||||
|
mesh.vertex_groups.remove(mesh.vertex_groups[bone_name])
|
||||||
|
|
||||||
|
# Transfer weights for neck bone merging
|
||||||
|
if merge_neck_bones:
|
||||||
|
logger.debug("Transferring weights from spine.005 to spine.004")
|
||||||
|
for mesh in meshes:
|
||||||
|
if 'spine.005' in mesh.vertex_groups:
|
||||||
|
transfer_vertex_weights(mesh, 'spine.005', 'spine.004')
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
# Remove unnecessary bones
|
||||||
for bone_name in bones_to_remove:
|
for bone_name in bones_to_remove:
|
||||||
if bone_name in armature.data.edit_bones:
|
if bone_name in armature.data.edit_bones:
|
||||||
logger.debug(f"Removing bone: {bone_name}")
|
logger.debug(f"Removing bone: {bone_name}")
|
||||||
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
|
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
|
||||||
|
|
||||||
if 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones:
|
# Merge neck bones
|
||||||
|
if merge_neck_bones:
|
||||||
logger.debug("Merging neck bones")
|
logger.debug("Merging neck bones")
|
||||||
neck_start = armature.data.edit_bones['spine.004']
|
neck_start = armature.data.edit_bones['spine.004']
|
||||||
neck_end = armature.data.edit_bones['spine.005']
|
neck_end = armature.data.edit_bones['spine.005']
|
||||||
@@ -89,6 +120,7 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
|||||||
armature.data.edit_bones.remove(neck_end)
|
armature.data.edit_bones.remove(neck_end)
|
||||||
neck_start.name = "Neck"
|
neck_start.name = "Neck"
|
||||||
|
|
||||||
|
# Rename head bone
|
||||||
if 'spine.006' in armature.data.edit_bones:
|
if 'spine.006' in armature.data.edit_bones:
|
||||||
logger.debug("Renaming head bone")
|
logger.debug("Renaming head bone")
|
||||||
head_bone = armature.data.edit_bones['spine.006']
|
head_bone = armature.data.edit_bones['spine.006']
|
||||||
@@ -137,6 +169,22 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
|||||||
if bone_name in armature.data.bones:
|
if bone_name in armature.data.bones:
|
||||||
armature.data.bones[bone_name].use_deform = False
|
armature.data.bones[bone_name].use_deform = False
|
||||||
|
|
||||||
|
# Get all meshes for weight transfer
|
||||||
|
meshes = get_all_meshes(bpy.context)
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
for bone_name in remove_bones_in_chain:
|
||||||
|
if bone_name in armature.data.bones:
|
||||||
|
parent_name = armature.data.bones[bone_name].parent.name if armature.data.bones[bone_name].parent else None
|
||||||
|
if parent_name:
|
||||||
|
logger.debug(f"Transferring weights from {bone_name} to {parent_name}")
|
||||||
|
for mesh in meshes:
|
||||||
|
if bone_name in mesh.vertex_groups and parent_name in mesh.vertex_groups:
|
||||||
|
transfer_vertex_weights(mesh, bone_name, parent_name)
|
||||||
|
elif bone_name in mesh.vertex_groups:
|
||||||
|
# Remove weights if no parent to merge to
|
||||||
|
mesh.vertex_groups.remove(mesh.vertex_groups[bone_name])
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
for bone_name in remove_bones_in_chain:
|
for bone_name in remove_bones_in_chain:
|
||||||
if bone_name in armature.data.bones:
|
if bone_name in armature.data.bones:
|
||||||
@@ -190,6 +238,17 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
|||||||
("DEF-thigh_twist.R", "DEF-thigh.R")
|
("DEF-thigh_twist.R", "DEF-thigh.R")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Get all meshes for weight transfer
|
||||||
|
meshes = get_all_meshes(bpy.context)
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
for twist_bone, parent_bone in twist_bones:
|
||||||
|
if twist_bone in armature.data.bones and parent_bone in armature.data.bones:
|
||||||
|
logger.debug(f"Transferring weights from {twist_bone} to {parent_bone}")
|
||||||
|
for mesh in meshes:
|
||||||
|
if twist_bone in mesh.vertex_groups:
|
||||||
|
transfer_vertex_weights(mesh, twist_bone, parent_bone)
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
for twist_bone, parent_bone in twist_bones:
|
for twist_bone, parent_bone in twist_bones:
|
||||||
if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones:
|
if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones:
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ from ...core.dictionaries import (
|
|||||||
bone_hierarchy,
|
bone_hierarchy,
|
||||||
acceptable_bone_names,
|
acceptable_bone_names,
|
||||||
acceptable_bone_hierarchy,
|
acceptable_bone_hierarchy,
|
||||||
non_standard_mappings
|
non_standard_mappings,
|
||||||
|
reverse_bone_lookup,
|
||||||
|
simplify_bonename
|
||||||
)
|
)
|
||||||
|
|
||||||
class AvatarToolkit_OT_StandardizeArmature(Operator):
|
class AvatarToolkit_OT_StandardizeArmature(Operator):
|
||||||
@@ -53,12 +55,6 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
|||||||
|
|
||||||
logger.info(f"Starting armature standardization for {armature.name}")
|
logger.info(f"Starting armature standardization for {armature.name}")
|
||||||
|
|
||||||
is_valid, _, _ = validate_armature(armature)
|
|
||||||
if is_valid:
|
|
||||||
logger.info("Armature already meets standards, no changes needed")
|
|
||||||
self.report({'INFO'}, t("Tools.standardize_already_valid"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
original_mode: str = context.mode
|
original_mode: str = context.mode
|
||||||
logger.debug(f"Original mode: {original_mode}")
|
logger.debug(f"Original mode: {original_mode}")
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
@@ -88,7 +84,7 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
|||||||
logger.info(f"Fixed {fixed_scale} scale issues")
|
logger.info(f"Fixed {fixed_scale} scale issues")
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
is_valid, messages, _ = validate_armature(armature)
|
is_valid, messages, _ = validate_armature(armature, override_mode='STRICT')
|
||||||
|
|
||||||
if is_valid:
|
if is_valid:
|
||||||
logger.info("Armature successfully standardized")
|
logger.info("Armature successfully standardized")
|
||||||
@@ -134,17 +130,14 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
|||||||
existing_standard_bones.add(bone.name)
|
existing_standard_bones.add(bone.name)
|
||||||
logger.debug(f"Found existing standard bone: {bone.name}")
|
logger.debug(f"Found existing standard bone: {bone.name}")
|
||||||
|
|
||||||
# Build a mapping of non-standard bone names to standard names
|
# Use the reverse bone lookup that's already built and simplified
|
||||||
name_mapping: Dict[str, str] = {}
|
name_mapping: Dict[str, str] = {}
|
||||||
for category, standard_name in standard_bones.items():
|
for simplified_name, category in reverse_bone_lookup.items():
|
||||||
|
if category in standard_bones:
|
||||||
|
standard_name = standard_bones[category]
|
||||||
# Skip if this standard bone already exists
|
# Skip if this standard bone already exists
|
||||||
if standard_name in existing_standard_bones:
|
if standard_name not in existing_standard_bones:
|
||||||
continue
|
name_mapping[simplified_name] = standard_name
|
||||||
|
|
||||||
# Get all variants for this category
|
|
||||||
if category in non_standard_mappings:
|
|
||||||
for variant in non_standard_mappings[category]:
|
|
||||||
name_mapping[variant.lower()] = standard_name
|
|
||||||
|
|
||||||
# First pass: identify bones to rename
|
# First pass: identify bones to rename
|
||||||
bones_to_rename: Dict[str, str] = {}
|
bones_to_rename: Dict[str, str] = {}
|
||||||
@@ -155,20 +148,14 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
|||||||
if original_name in standard_bones.values():
|
if original_name in standard_bones.values():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
simplified_name: str = original_name.lower().replace(' ', '').replace('_', '').replace('.', '')
|
simplified_name: str = simplify_bonename(original_name)
|
||||||
|
|
||||||
# Check if this bone matches any known pattern
|
|
||||||
for variant, standard_name in name_mapping.items():
|
|
||||||
# More precise matching - exact match or with common separators
|
|
||||||
if (variant == simplified_name or
|
|
||||||
variant == original_name.lower() or
|
|
||||||
f"{variant}_" in simplified_name or
|
|
||||||
f"{variant}." in simplified_name):
|
|
||||||
|
|
||||||
|
# Check if this simplified bone name has a standard mapping
|
||||||
|
if simplified_name in name_mapping:
|
||||||
|
standard_name = name_mapping[simplified_name]
|
||||||
if original_name != standard_name:
|
if original_name != standard_name:
|
||||||
bones_to_rename[original_name] = standard_name
|
bones_to_rename[original_name] = standard_name
|
||||||
logger.debug(f"Identified bone to rename: {original_name} -> {standard_name}")
|
logger.debug(f"Identified bone to rename: {original_name} -> {standard_name}")
|
||||||
break
|
|
||||||
|
|
||||||
# Special case for spine/chest hierarchy
|
# Special case for spine/chest hierarchy
|
||||||
# If we don't have an upper chest, don't rename chest to upper chest because it will break hierarchy
|
# If we don't have an upper chest, don't rename chest to upper chest because it will break hierarchy
|
||||||
|
|||||||
@@ -8,6 +8,26 @@ from ...core.translations import t
|
|||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
def get_uv_vertex_selection(mesh: Mesh) -> List[bool]:
|
||||||
|
"""
|
||||||
|
Get UV vertex selection state for Blender 5.0.
|
||||||
|
UV selection is stored in mesh attributes (.uv_select_vert).
|
||||||
|
"""
|
||||||
|
uv_select_attr = mesh.attributes['.uv_select_vert']
|
||||||
|
selection = [False] * len(mesh.loops)
|
||||||
|
uv_select_attr.data.foreach_get('value', selection)
|
||||||
|
return selection
|
||||||
|
|
||||||
|
|
||||||
|
def set_uv_vertex_selection(mesh: Mesh, loop_index: int, value: bool) -> None:
|
||||||
|
"""
|
||||||
|
Set UV vertex selection state for Blender 5.0.
|
||||||
|
UV selection is stored in mesh attributes (.uv_select_vert).
|
||||||
|
"""
|
||||||
|
uv_select_attr = mesh.attributes['.uv_select_vert']
|
||||||
|
uv_select_attr.data[loop_index].value = value
|
||||||
|
|
||||||
class GenerateLoopTreeResult(TypedDict):
|
class GenerateLoopTreeResult(TypedDict):
|
||||||
tree: Dict[str, Set[str]]
|
tree: Dict[str, Set[str]]
|
||||||
selected_loops: Dict[str, List[int]]
|
selected_loops: Dict[str, List[int]]
|
||||||
@@ -78,8 +98,9 @@ class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
|
|||||||
# that two vertices share the same face loop, and therefore are connected.
|
# that two vertices share the same face loop, and therefore are connected.
|
||||||
|
|
||||||
#hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate
|
#hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate
|
||||||
for k,i in enumerate(uv_lay.vertex_selection):
|
uv_selection = get_uv_vertex_selection(me)
|
||||||
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False):
|
for k, is_selected in enumerate(uv_selection):
|
||||||
|
if (is_selected == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False):
|
||||||
key = np.array(uv_lay.uv[k].vector[:])
|
key = np.array(uv_lay.uv[k].vector[:])
|
||||||
key = key.round(decimals=5)
|
key = key.round(decimals=5)
|
||||||
|
|
||||||
@@ -140,7 +161,7 @@ class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
|
|||||||
uv_lay = me.uv_layers.active
|
uv_lay = me.uv_layers.active
|
||||||
for uvcoordstr in vert_target_loops:
|
for uvcoordstr in vert_target_loops:
|
||||||
for loop in vert_target_loops[uvcoordstr]:
|
for loop in vert_target_loops[uvcoordstr]:
|
||||||
uv_lay.vertex_selection[loop].value = True
|
set_uv_vertex_selection(me, loop, True)
|
||||||
|
|
||||||
bm.free()
|
bm.free()
|
||||||
me.validate()
|
me.validate()
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
from ...core.common import get_active_armature
|
||||||
|
from ...core.translations import t
|
||||||
|
from ...core.vrm_unity_converter import convert_vrm_to_unity, validate_unity_hierarchy
|
||||||
|
from ...core.logging_setup import logger
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ConvertVRMToUnity(Operator):
|
||||||
|
"""Convert VRM armature bone names to Unity humanoid format"""
|
||||||
|
bl_idname = "avatar_toolkit.convert_vrm_to_unity"
|
||||||
|
bl_label = t("VRM.convert_to_unity.label")
|
||||||
|
bl_description = t("VRM.convert_to_unity.desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
return armature is not None
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
logger.warning("No active armature found for VRM conversion")
|
||||||
|
self.report({'ERROR'}, t("VRM.no_armature_selected"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
logger.info(f"Starting VRM to Unity conversion for armature: {armature.name}")
|
||||||
|
|
||||||
|
# Get conversion settings
|
||||||
|
remove_colliders = context.scene.avatar_toolkit.vrm_remove_colliders
|
||||||
|
remove_root = context.scene.avatar_toolkit.vrm_remove_root
|
||||||
|
logger.info(f"Collider removal setting: {remove_colliders}")
|
||||||
|
logger.info(f"Root bone removal setting: {remove_root}")
|
||||||
|
|
||||||
|
# Log all objects with 'collider' in name for debugging
|
||||||
|
collider_objects = [obj.name for obj in bpy.data.objects if 'collider' in obj.name.lower()]
|
||||||
|
if collider_objects:
|
||||||
|
logger.info(f"Found {len(collider_objects)} objects with 'collider' in name:")
|
||||||
|
for obj_name in collider_objects:
|
||||||
|
logger.info(f" - {obj_name}")
|
||||||
|
|
||||||
|
success, messages, converted_count = convert_vrm_to_unity(armature, remove_colliders, remove_root)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
logger.warning(f"VRM conversion failed: {messages}")
|
||||||
|
for msg in messages:
|
||||||
|
self.report({'WARNING'}, msg)
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
logger.info(f"VRM conversion completed successfully. Converted {converted_count} bones")
|
||||||
|
for msg in messages:
|
||||||
|
self.report({'INFO'}, msg)
|
||||||
|
|
||||||
|
# Validate the converted armature
|
||||||
|
try:
|
||||||
|
is_valid, validation_messages = validate_unity_hierarchy(armature)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
logger.info("Unity hierarchy validation passed")
|
||||||
|
self.report({'INFO'}, t("VRM.validation.hierarchy_passed"))
|
||||||
|
else:
|
||||||
|
logger.warning("Unity hierarchy validation found issues")
|
||||||
|
self.report({'WARNING'}, t("VRM.validation.hierarchy_issues"))
|
||||||
|
for msg in validation_messages:
|
||||||
|
self.report({'WARNING'}, msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
armature_valid, armature_messages, _ = validate_armature(armature)
|
||||||
|
if armature_valid:
|
||||||
|
logger.info("Full armature validation passed")
|
||||||
|
self.report({'INFO'}, t("VRM.validation.armature_passed"))
|
||||||
|
else:
|
||||||
|
logger.info("Full armature validation found minor issues")
|
||||||
|
# Don't report these as errors since the conversion was successful
|
||||||
|
# Just log them for debugging
|
||||||
|
for msg in armature_messages[:3]:
|
||||||
|
logger.debug(f"Armature validation: {msg}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error during full armature validation: {str(e)}")
|
||||||
|
# Don't fail the operation for validation errors
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during hierarchy validation: {str(e)}")
|
||||||
|
self.report({'WARNING'}, t("VRM.validation.failed", error=str(e)))
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
@@ -137,15 +137,17 @@ class AvatarToolkit_OT_PreviewVisemes(Operator):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Get mesh from UI selection
|
# Get mesh from UI selection
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
mesh_obj = get_mesh_from_identifier(props.viseme_mesh)
|
||||||
|
|
||||||
# Validate mesh
|
# Validate mesh
|
||||||
return mesh_obj and mesh_obj.type == 'MESH'
|
return mesh_obj and mesh_obj.type == 'MESH'
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
mesh = bpy.data.objects.get(props.viseme_mesh)
|
mesh = get_mesh_from_identifier(props.viseme_mesh)
|
||||||
|
|
||||||
if props.viseme_preview_mode:
|
if props.viseme_preview_mode:
|
||||||
VisemePreview.end_preview(mesh)
|
VisemePreview.end_preview(mesh)
|
||||||
@@ -191,15 +193,17 @@ class AvatarToolkit_OT_CreateVisemes(Operator):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Get mesh from UI selection
|
# Get mesh from UI selection
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
mesh_obj = get_mesh_from_identifier(props.viseme_mesh)
|
||||||
|
|
||||||
# Validate mesh
|
# Validate mesh
|
||||||
return mesh_obj and mesh_obj.type == 'MESH'
|
return mesh_obj and mesh_obj.type == 'MESH'
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
mesh = bpy.data.objects.get(props.viseme_mesh) # Changed from context.active_object
|
mesh = get_mesh_from_identifier(props.viseme_mesh)
|
||||||
|
|
||||||
if not mesh or not mesh.data.shape_keys:
|
if not mesh or not mesh.data.shape_keys:
|
||||||
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.3.0)",
|
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.6.0)",
|
||||||
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
|
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
|
||||||
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
|
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
|
||||||
"AvatarToolkit.desc3": "please report it on our Github.",
|
"AvatarToolkit.desc3": "please report it on our Github.",
|
||||||
@@ -117,6 +117,15 @@
|
|||||||
"Validation.clear_bone_highlighting": "Clear Bone Highlighting",
|
"Validation.clear_bone_highlighting": "Clear Bone Highlighting",
|
||||||
"Validation.clear_bone_highlighting_desc": "Remove bone highlighting and reset bone colors to default",
|
"Validation.clear_bone_highlighting_desc": "Remove bone highlighting and reset bone colors to default",
|
||||||
"Validation.highlighting_cleared": "Bone highlighting cleared successfully",
|
"Validation.highlighting_cleared": "Bone highlighting cleared successfully",
|
||||||
|
"Validation.label": "Armature Validation",
|
||||||
|
"Validation.validate_now": "Validate Armature Now",
|
||||||
|
"Validation.validate_now_desc": "Run armature validation and display detailed results",
|
||||||
|
"Validation.results": "Validation Results",
|
||||||
|
"Validation.tpose.validate_now": "Validate T-Pose Now",
|
||||||
|
|
||||||
|
"Armature.validation.acceptable_standard.success": "Armature meets acceptable standards",
|
||||||
|
"Armature.validation.acceptable_standard.note": "This is a valid armature format that is compatible with most avatar systems",
|
||||||
|
"Armature.validation.acceptable_standard.option": "You can standardize the armature if desired",
|
||||||
|
|
||||||
"Mesh.validation.no_data": "No mesh data",
|
"Mesh.validation.no_data": "No mesh data",
|
||||||
"Mesh.validation.no_vertex_groups": "No vertex groups found",
|
"Mesh.validation.no_vertex_groups": "No vertex groups found",
|
||||||
@@ -191,6 +200,7 @@
|
|||||||
"Tools.digitigrade_error": "Failed to create digitigrade legs: {error}",
|
"Tools.digitigrade_error": "Failed to create digitigrade legs: {error}",
|
||||||
"Tools.digitigrade_success": "Successfully created digitigrade leg setup",
|
"Tools.digitigrade_success": "Successfully created digitigrade leg setup",
|
||||||
"Tools.processing_leg": "Processing leg bone: {bone}",
|
"Tools.processing_leg": "Processing leg bone: {bone}",
|
||||||
|
"Tools.weight_title": "Weight Tools",
|
||||||
"Tools.merge_twist_bones": "Keep Twist Bones",
|
"Tools.merge_twist_bones": "Keep Twist Bones",
|
||||||
"Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight",
|
"Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight",
|
||||||
"Tools.clean_weights": "Remove Zero Weight Bones",
|
"Tools.clean_weights": "Remove Zero Weight Bones",
|
||||||
@@ -325,6 +335,7 @@
|
|||||||
"Visemes.success": "Visemes created successfully",
|
"Visemes.success": "Visemes created successfully",
|
||||||
"Visemes.mesh_select": "Select Mesh",
|
"Visemes.mesh_select": "Select Mesh",
|
||||||
"Visemes.mesh_select_desc": "Select the mesh to create visemes on",
|
"Visemes.mesh_select_desc": "Select the mesh to create visemes on",
|
||||||
|
"Visemes.no_meshes": "No meshes found",
|
||||||
|
|
||||||
"EyeTracking.label": "Eye Tracking",
|
"EyeTracking.label": "Eye Tracking",
|
||||||
"EyeTracking.setup": "Eye Tracking Setup",
|
"EyeTracking.setup": "Eye Tracking Setup",
|
||||||
@@ -517,6 +528,8 @@
|
|||||||
"TextureAtlas.save_file_instructions": "Use File > Save As... or click the button below:",
|
"TextureAtlas.save_file_instructions": "Use File > Save As... or click the button below:",
|
||||||
"TextureAtlas.save_file_button": "Save Blender File",
|
"TextureAtlas.save_file_button": "Save Blender File",
|
||||||
"TextureAtlas.save_file_required": "Save File Required",
|
"TextureAtlas.save_file_required": "Save File Required",
|
||||||
|
"TextureAtlas.search_materials": "Search Materials",
|
||||||
|
"TextureAtlas.search_materials_desc": "Filter materials by name",
|
||||||
|
|
||||||
"Settings.label": "Settings",
|
"Settings.label": "Settings",
|
||||||
"Settings.language": "Language",
|
"Settings.language": "Language",
|
||||||
@@ -554,6 +567,171 @@
|
|||||||
"Language.ko_KR": "Korean",
|
"Language.ko_KR": "Korean",
|
||||||
"Language.changed.title": "Language Changed",
|
"Language.changed.title": "Language Changed",
|
||||||
"Language.changed.success": "Language changed successfully!",
|
"Language.changed.success": "Language changed successfully!",
|
||||||
"Language.changed.restart": "Some UI elements may require restarting Blender"
|
"Language.changed.restart": "Some UI elements may require restarting Blender",
|
||||||
|
|
||||||
|
"VRM.panel.label": "VRM to Unity",
|
||||||
|
"VRM.converter.title": "VRM Converter",
|
||||||
|
"VRM.no_armature_selected": "No armature selected",
|
||||||
|
"VRM.select_armature_to_convert": "Select an armature to convert",
|
||||||
|
"VRM.armature_name": "Armature: {name}",
|
||||||
|
"VRM.armature_detected": "VRM armature detected",
|
||||||
|
"VRM.no_vrm_bones_detected": "No VRM bones detected",
|
||||||
|
"VRM.remove_colliders": "Remove Colliders",
|
||||||
|
"VRM.remove_root_bone": "Remove Root Bone",
|
||||||
|
"VRM.convert_to_unity_format": "Convert to Unity Format",
|
||||||
|
"VRM.convert_to_unity.label": "Convert VRM to Unity",
|
||||||
|
"VRM.convert_to_unity.desc": "Convert VRM armature bone names to Unity humanoid naming convention",
|
||||||
|
"VRM.conversion_info.title": "Conversion Info:",
|
||||||
|
"VRM.conversion_info.renames_bones": "• Renames VRM bones to Unity format",
|
||||||
|
"VRM.conversion_info.removes_colliders": "• Removes collider bones (optional)",
|
||||||
|
"VRM.conversion_info.removes_root": "• Removes root bone, makes Hips root (optional)",
|
||||||
|
"VRM.conversion_info.maintains_hierarchy": "• Maintains bone hierarchy",
|
||||||
|
"VRM.conversion_info.validates_results": "• Validates conversion results",
|
||||||
|
"VRM.conversion_info.preserves_animations": "• Preserves all animations",
|
||||||
|
"VRM.detection_failed.title": "VRM Detection Failed:",
|
||||||
|
"VRM.detection_failed.not_vrm_format": "• Selected armature is not VRM format",
|
||||||
|
"VRM.detection_failed.bones_start_with": "• VRM bones start with 'J_Bip_C_'",
|
||||||
|
"VRM.detection_failed.need_five_bones": "• Need at least 5 VRM bones detected",
|
||||||
|
"VRM.detection_failed.check_bone_names": "• Check armature bone names",
|
||||||
|
"VRM.validation.hierarchy_passed": "Unity hierarchy validation passed",
|
||||||
|
"VRM.validation.hierarchy_issues": "Conversion completed but hierarchy validation found issues:",
|
||||||
|
"VRM.validation.armature_passed": "Armature passes standard validation",
|
||||||
|
"VRM.validation.failed": "Conversion completed but validation failed: {error}",
|
||||||
|
"VRM.remove_colliders_desc": "Remove VRM collider bones during conversion",
|
||||||
|
"VRM.remove_root": "Remove Root Bone",
|
||||||
|
"VRM.remove_root_desc": "Remove unnecessary VRM root bone and make Hips the root bone",
|
||||||
|
|
||||||
|
"MMD.panel.label": "MMD Converter",
|
||||||
|
"MMD.converter.title": "MMD Armature Converter",
|
||||||
|
"MMD.no_armature_selected": "No armature selected",
|
||||||
|
"MMD.select_armature_to_convert": "Select an armature to convert",
|
||||||
|
"MMD.armature_name": "Armature: {name}",
|
||||||
|
"MMD.armature_detected": "MMD armature detected",
|
||||||
|
"MMD.no_mmd_bones_detected": "No MMD bones detected",
|
||||||
|
"MMD.not_mmd_armature": "Selected armature does not appear to be MMD format",
|
||||||
|
"MMD.make_armature_parent": "Make Armature Main Parent",
|
||||||
|
"MMD.rename_to_armature": "Rename to 'Armature'",
|
||||||
|
"MMD.translate_names": "Translate Names to English",
|
||||||
|
"MMD.translate_bones": "Bones",
|
||||||
|
"MMD.translate_materials": "Materials",
|
||||||
|
"MMD.translate_shapekeys": "Shape Keys",
|
||||||
|
"MMD.translate_objects": "Objects",
|
||||||
|
"MMD.restructure_bones": "Restructure to Unity Format",
|
||||||
|
"MMD.bone_cleanup": "Bone Cleanup Options:",
|
||||||
|
"MMD.remove_ik_bones": "Remove IK Bones",
|
||||||
|
"MMD.remove_twist_bones": "Remove Twist Bones",
|
||||||
|
"MMD.remove_zero_weight_bones": "Remove Zero Weight Bones",
|
||||||
|
"MMD.translation_options": "Translation Options:",
|
||||||
|
"MMD.convert_armature_button": "Convert MMD Armature",
|
||||||
|
"MMD.convert_armature.label": "Convert MMD Armature",
|
||||||
|
"MMD.convert_armature.desc": "Convert MMD armature to standard Blender format",
|
||||||
|
"MMD.conversion_info.title": "Conversion Info:",
|
||||||
|
"MMD.conversion_info.removes_parent": "• Removes parent Empty object",
|
||||||
|
"MMD.conversion_info.renames_armature": "• Renames armature to 'Armature'",
|
||||||
|
"MMD.conversion_info.restructures_bones": "• Converts to Unity bone structure (Hips/Spine/Chest)",
|
||||||
|
"MMD.conversion_info.removes_ik_bones": "• Removes IK (Inverse Kinematics) bones",
|
||||||
|
"MMD.conversion_info.removes_twist_bones": "• Removes twist bones",
|
||||||
|
"MMD.conversion_info.removes_zero_weight_bones": "• Removes bones with zero vertex weights",
|
||||||
|
"MMD.conversion_info.maintains_hierarchy": "• Maintains object hierarchy",
|
||||||
|
"MMD.conversion_info.translates_names": "• Translates Japanese names to English",
|
||||||
|
"MMD.detection_failed.title": "MMD Detection Failed:",
|
||||||
|
"MMD.detection_failed.not_mmd_format": "• Selected armature is not MMD format",
|
||||||
|
"MMD.detection_failed.need_mmd_bones": "• Need at least 5 MMD bones detected",
|
||||||
|
"MMD.detection_failed.check_bone_names": "• Check armature bone names",
|
||||||
|
"MMD.error.invalid_armature": "Invalid armature object",
|
||||||
|
"MMD.error.not_mmd_armature": "Armature does not appear to be MMD format",
|
||||||
|
"MMD.error.rename_failed": "Failed to rename armature: {error}",
|
||||||
|
"MMD.armature_already_root": "Armature already has no parent",
|
||||||
|
"MMD.armature_already_named": "Armature is already named 'Armature'",
|
||||||
|
"MMD.parent_removed_and_reparented": "Removed parent '{parent_name}' and reparented {count} objects to armature",
|
||||||
|
"MMD.parent_unlinked_and_reparented": "Unlinked from parent '{parent_name}' and reparented {count} objects",
|
||||||
|
"MMD.parent_unlinked": "Unlinked armature from parent '{parent_name}'",
|
||||||
|
"MMD.armature_renamed": "Renamed armature from '{old_name}' to '{new_name}'",
|
||||||
|
"MMD.armature_renamed_with_suffix": "Renamed armature from '{old_name}' to '{new_name}' (name collision)",
|
||||||
|
"MMD.conversion_complete": "MMD armature conversion completed successfully",
|
||||||
|
"MMD.translation_starting": "Starting name translation...",
|
||||||
|
"MMD.bones_translated": "Translated {count} bones",
|
||||||
|
"MMD.bones_failed": "Failed to translate {count} bones",
|
||||||
|
"MMD.materials_translated": "Translated {count} materials",
|
||||||
|
"MMD.materials_failed": "Failed to translate {count} materials",
|
||||||
|
"MMD.shapekeys_translated": "Translated {count} shape keys",
|
||||||
|
"MMD.shapekeys_failed": "Failed to translate {count} shape keys",
|
||||||
|
"MMD.objects_translated": "Translated {count} objects",
|
||||||
|
"MMD.objects_failed": "Failed to translate {count} objects",
|
||||||
|
"MMD.translation_complete": "Translation complete: {total} items translated",
|
||||||
|
"MMD.restructure_starting": "Restructuring bones to Unity format...",
|
||||||
|
"MMD.bones_restructured": "Restructured {count} bones to Unity format",
|
||||||
|
"MMD.bones_removed": "Removed {count} unnecessary bones",
|
||||||
|
"MMD.bones_reparented": "Reparented {count} bones",
|
||||||
|
"MMD.restructure_failed": "Bone restructuring failed: {error}",
|
||||||
|
"MMD.ik_bones_removed": "Removed {count} IK bones",
|
||||||
|
"MMD.no_ik_bones_found": "No IK bones found to remove",
|
||||||
|
"MMD.ik_removal_failed": "IK bone removal failed: {error}",
|
||||||
|
"MMD.twist_bones_removed": "Removed {count} twist bones",
|
||||||
|
"MMD.no_twist_bones_found": "No twist bones found to remove",
|
||||||
|
"MMD.twist_removal_failed": "Twist bone removal failed: {error}",
|
||||||
|
"MMD.zero_weight_bones_removed": "Removed {count} zero weight bones",
|
||||||
|
"MMD.no_zero_weight_bones_found": "No zero weight bones found to remove",
|
||||||
|
"MMD.zero_weight_removal_failed": "Zero weight bone removal failed: {error}",
|
||||||
|
|
||||||
|
"Translation.label": "Translation",
|
||||||
|
"Translation.service": "Translation Service",
|
||||||
|
"Translation.service_desc": "Choose the translation service to use",
|
||||||
|
"Translation.mode": "Translation Mode",
|
||||||
|
"Translation.mode_desc": "Select how translation should work",
|
||||||
|
"Translation.mode.hybrid": "Hybrid (Dictionary + API)",
|
||||||
|
"Translation.mode.hybrid_desc": "Try dictionary first, then use API service as fallback",
|
||||||
|
"Translation.mode.dictionary_only": "Dictionary Only",
|
||||||
|
"Translation.mode.dictionary_only_desc": "Only use built-in dictionaries for translation",
|
||||||
|
"Translation.mode.api_only": "API Only",
|
||||||
|
"Translation.mode.api_only_desc": "Only use online translation services",
|
||||||
|
"Translation.service_settings": "Translation Service",
|
||||||
|
"Translation.language_settings": "Language Settings",
|
||||||
|
"Translation.quick_actions": "Quick Actions",
|
||||||
|
"Translation.utilities": "Utilities",
|
||||||
|
"Translation.advanced_settings": "Advanced Settings",
|
||||||
|
"Translation.source_language": "Source Language",
|
||||||
|
"Translation.source_language_desc": "Language to translate from",
|
||||||
|
"Translation.target_language": "Target Language",
|
||||||
|
"Translation.target_language_desc": "Language to translate to",
|
||||||
|
"Translation.translate_names": "Translate Names",
|
||||||
|
"Translation.translate_names_desc": "Translate names using the selected service and settings",
|
||||||
|
"Translation.test_service": "Test Service",
|
||||||
|
"Translation.test_service_desc": "Test the currently selected translation service",
|
||||||
|
"Translation.clear_cache": "Clear Cache",
|
||||||
|
"Translation.clear_cache_desc": "Clear all cached translations",
|
||||||
|
"Translation.show_stats": "Show Statistics",
|
||||||
|
"Translation.show_stats_desc": "Show translation statistics and information",
|
||||||
|
"Translation.no_armature": "No armature selected",
|
||||||
|
"Translation.test_failed": "Translation service test failed - check configuration",
|
||||||
|
"Translation.cache_cleared": "Translation cache cleared successfully",
|
||||||
|
"Translation.mymemory_info": "MyMemory is completely free with no API key required. Provides 1000 translations per day.",
|
||||||
|
"Translation.service.mymemory": "MyMemory (Free)",
|
||||||
|
"Translation.service.mymemory_desc": "Completely free service - no API key needed!",
|
||||||
|
"Translation.service.libretranslate": "LibreTranslate",
|
||||||
|
"Translation.service.libretranslate_desc": "Configurable server - can be self-hosted",
|
||||||
|
"Translation.service.deepl": "DeepL",
|
||||||
|
"Translation.service.deepl_desc": "High-quality translations - API key required",
|
||||||
|
"Translation.type.bones": "Bones",
|
||||||
|
"Translation.type.bones_desc": "Translate bone names",
|
||||||
|
"Translation.type.shapekeys": "Shape Keys",
|
||||||
|
"Translation.type.shapekeys_desc": "Translate shape key names",
|
||||||
|
"Translation.type.materials": "Materials",
|
||||||
|
"Translation.type.materials_desc": "Translate material names",
|
||||||
|
"Translation.type.objects": "Objects",
|
||||||
|
"Translation.type.objects_desc": "Translate object names",
|
||||||
|
"Translation.type.all": "All",
|
||||||
|
"Translation.type.all_desc": "Translate all supported types",
|
||||||
|
"Translation.configure_deepl": "Configure DeepL API",
|
||||||
|
"Translation.configure_deepl_desc": "Configure DeepL translation service API key",
|
||||||
|
"Translation.deepl_api_key": "DeepL API Key",
|
||||||
|
"Translation.deepl_api_key_desc": "Your DeepL API key (get free key at deepl.com/pro)",
|
||||||
|
"Translation.configure_libretranslate": "Configure LibreTranslate Server",
|
||||||
|
"Translation.configure_libretranslate_desc": "Configure LibreTranslate translation service server URL",
|
||||||
|
"Translation.server_url": "Server URL",
|
||||||
|
"Translation.server_url_desc": "LibreTranslate server URL (e.g., https://your-server.com)",
|
||||||
|
"Translation.api_key": "API Key",
|
||||||
|
"Translation.api_key_desc": "API key for LibreTranslate server (optional for some servers)"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "アバターツールキット (アルファ 0.3.0)",
|
"AvatarToolkit.label": "アバターツールキット (アルファ 0.6.0)",
|
||||||
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
|
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
|
||||||
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
||||||
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
||||||
@@ -117,6 +117,15 @@
|
|||||||
"Validation.clear_bone_highlighting": "ボーンの強調表示をクリア",
|
"Validation.clear_bone_highlighting": "ボーンの強調表示をクリア",
|
||||||
"Validation.clear_bone_highlighting_desc": "ボーンの強調表示を削除し、ボーンの色をデフォルトにリセット",
|
"Validation.clear_bone_highlighting_desc": "ボーンの強調表示を削除し、ボーンの色をデフォルトにリセット",
|
||||||
"Validation.highlighting_cleared": "ボーンの強調表示が正常にクリアされました",
|
"Validation.highlighting_cleared": "ボーンの強調表示が正常にクリアされました",
|
||||||
|
"Validation.label": "アーマチュア検証",
|
||||||
|
"Validation.validate_now": "アーマチュアを検証する",
|
||||||
|
"Validation.validate_now_desc": "アーマチュア検証を実行し、詳細な結果を表示",
|
||||||
|
"Validation.results": "検証結果",
|
||||||
|
"Validation.tpose.validate_now": "T-ポーズを検証する",
|
||||||
|
|
||||||
|
"Armature.validation.acceptable_standard.success": "アーマチュアが許容可能な標準を満たしています",
|
||||||
|
"Armature.validation.acceptable_standard.note": "これは、ほとんどのアバターシステムと互換性のある有効なアーマチュア形式です",
|
||||||
|
"Armature.validation.acceptable_standard.option": "必要に応じてアーマチュアを標準化できます",
|
||||||
|
|
||||||
"Mesh.validation.no_data": "メッシュデータがありません",
|
"Mesh.validation.no_data": "メッシュデータがありません",
|
||||||
"Mesh.validation.no_vertex_groups": "頂点グループが見つかりません",
|
"Mesh.validation.no_vertex_groups": "頂点グループが見つかりません",
|
||||||
@@ -194,6 +203,7 @@
|
|||||||
"Tools.digitigrade_error": "デジティグレード脚の作成に失敗: {error}",
|
"Tools.digitigrade_error": "デジティグレード脚の作成に失敗: {error}",
|
||||||
"Tools.digitigrade_success": "デジティグレード脚の設定が正常に作成されました",
|
"Tools.digitigrade_success": "デジティグレード脚の設定が正常に作成されました",
|
||||||
"Tools.processing_leg": "脚のボーンを処理中: {bone}",
|
"Tools.processing_leg": "脚のボーンを処理中: {bone}",
|
||||||
|
"Tools.weight_title": "ウェイトツール",
|
||||||
"Tools.merge_twist_bones": "ツイストボーンを保持",
|
"Tools.merge_twist_bones": "ツイストボーンを保持",
|
||||||
"Tools.merge_twist_bones_desc": "チェックすると、ウェイトがゼロでもツイストボーンが保持されます",
|
"Tools.merge_twist_bones_desc": "チェックすると、ウェイトがゼロでもツイストボーンが保持されます",
|
||||||
"Tools.clean_weights": "ゼロウェイトボーンを削除",
|
"Tools.clean_weights": "ゼロウェイトボーンを削除",
|
||||||
@@ -308,6 +318,7 @@
|
|||||||
"Visemes.success": "口形素が正常に作成されました",
|
"Visemes.success": "口形素が正常に作成されました",
|
||||||
"Visemes.mesh_select": "メッシュを選択",
|
"Visemes.mesh_select": "メッシュを選択",
|
||||||
"Visemes.mesh_select_desc": "口形素を作成するメッシュを選択",
|
"Visemes.mesh_select_desc": "口形素を作成するメッシュを選択",
|
||||||
|
"Visemes.no_meshes": "メッシュが見つかりません",
|
||||||
|
|
||||||
"EyeTracking.label": "アイトラッキング",
|
"EyeTracking.label": "アイトラッキング",
|
||||||
"EyeTracking.setup": "アイトラッキング設定",
|
"EyeTracking.setup": "アイトラッキング設定",
|
||||||
@@ -500,6 +511,8 @@
|
|||||||
"TextureAtlas.save_file_instructions": "ファイル > 名前を付けて保存... を使用するか、下のボタンをクリックしてください:",
|
"TextureAtlas.save_file_instructions": "ファイル > 名前を付けて保存... を使用するか、下のボタンをクリックしてください:",
|
||||||
"TextureAtlas.save_file_button": "Blenderファイルを保存",
|
"TextureAtlas.save_file_button": "Blenderファイルを保存",
|
||||||
"TextureAtlas.save_file_required": "ファイルの保存が必要です",
|
"TextureAtlas.save_file_required": "ファイルの保存が必要です",
|
||||||
|
"TextureAtlas.search_materials": "マテリアルを検索",
|
||||||
|
"TextureAtlas.search_materials_desc": "名前でマテリアルをフィルタリング",
|
||||||
|
|
||||||
"Settings.label": "設定",
|
"Settings.label": "設定",
|
||||||
"Settings.language": "言語",
|
"Settings.language": "言語",
|
||||||
@@ -537,6 +550,97 @@
|
|||||||
"Language.ko_KR": "韓国語",
|
"Language.ko_KR": "韓国語",
|
||||||
"Language.changed.title": "言語が変更されました",
|
"Language.changed.title": "言語が変更されました",
|
||||||
"Language.changed.success": "言語が正常に変更されました!",
|
"Language.changed.success": "言語が正常に変更されました!",
|
||||||
"Language.changed.restart": "一部のUI要素はBlenderの再起動が必要な場合があります"
|
"Language.changed.restart": "一部のUI要素はBlenderの再起動が必要な場合があります",
|
||||||
|
|
||||||
|
"VRM.panel.label": "VRMからUnityへ",
|
||||||
|
"VRM.converter.title": "VRMコンバーター",
|
||||||
|
"VRM.no_armature_selected": "アーマチュアが選択されていません",
|
||||||
|
"VRM.select_armature_to_convert": "変換するアーマチュアを選択してください",
|
||||||
|
"VRM.armature_name": "アーマチュア: {name}",
|
||||||
|
"VRM.armature_detected": "VRMアーマチュアが検出されました",
|
||||||
|
"VRM.no_vrm_bones_detected": "VRMボーンが検出されませんでした",
|
||||||
|
"VRM.remove_root_bone": "ルートボーンを削除",
|
||||||
|
"VRM.convert_to_unity_format": "Unity形式に変換",
|
||||||
|
"VRM.convert_to_unity.label": "VRMをUnityに変換",
|
||||||
|
"VRM.convert_to_unity.desc": "VRMアーマチュアのボーン名をUnityヒューマノイド命名規則に変換",
|
||||||
|
"VRM.conversion_info.title": "変換情報:",
|
||||||
|
"VRM.conversion_info.renames_bones": "• VRMボーンをUnity形式にリネーム",
|
||||||
|
"VRM.conversion_info.removes_colliders": "• コライダーボーンを削除(オプション)",
|
||||||
|
"VRM.conversion_info.removes_root": "• ルートボーンを削除し、Hipsをルートにする(オプション)",
|
||||||
|
"VRM.conversion_info.maintains_hierarchy": "• ボーン階層を維持",
|
||||||
|
"VRM.conversion_info.validates_results": "• 変換結果を検証",
|
||||||
|
"VRM.conversion_info.preserves_animations": "• すべてのアニメーションを保持",
|
||||||
|
"VRM.detection_failed.title": "VRM検出失敗:",
|
||||||
|
"VRM.detection_failed.not_vrm_format": "• 選択されたアーマチュアはVRM形式ではありません",
|
||||||
|
"VRM.detection_failed.bones_start_with": "• VRMボーンは'J_Bip_C_'で始まります",
|
||||||
|
"VRM.detection_failed.need_five_bones": "• 少なくとも5つのVRMボーンが検出される必要があります",
|
||||||
|
"VRM.detection_failed.check_bone_names": "• アーマチュアのボーン名を確認してください",
|
||||||
|
"VRM.validation.hierarchy_passed": "Unity階層検証に合格しました",
|
||||||
|
"VRM.validation.hierarchy_issues": "変換は完了しましたが、階層検証で問題が見つかりました:",
|
||||||
|
"VRM.validation.armature_passed": "アーマチュアは標準検証に合格しました",
|
||||||
|
"VRM.validation.failed": "変換は完了しましたが、検証に失敗しました: {error}",
|
||||||
|
"VRM.remove_colliders": "コライダーを削除",
|
||||||
|
"VRM.remove_colliders_desc": "変換中にVRMコライダーボーンを削除",
|
||||||
|
"VRM.remove_root": "ルートボーンを削除",
|
||||||
|
"VRM.remove_root_desc": "不要なVRMルートボーンを削除し、ヒップをルートボーンにする",
|
||||||
|
|
||||||
|
"Translation.label": "翻訳",
|
||||||
|
"Translation.service": "翻訳サービス",
|
||||||
|
"Translation.service_desc": "使用する翻訳サービスを選択",
|
||||||
|
"Translation.mode": "翻訳モード",
|
||||||
|
"Translation.mode_desc": "翻訳の動作方法を選択",
|
||||||
|
"Translation.mode.hybrid": "ハイブリッド(辞書 + API)",
|
||||||
|
"Translation.mode.hybrid_desc": "まず辞書を試し、その後APIサービスをフォールバックとして使用",
|
||||||
|
"Translation.mode.dictionary_only": "辞書のみ",
|
||||||
|
"Translation.mode.dictionary_only_desc": "翻訳には組み込み辞書のみを使用",
|
||||||
|
"Translation.mode.api_only": "APIのみ",
|
||||||
|
"Translation.mode.api_only_desc": "オンライン翻訳サービスのみを使用",
|
||||||
|
"Translation.service_settings": "翻訳サービス",
|
||||||
|
"Translation.language_settings": "言語設定",
|
||||||
|
"Translation.quick_actions": "クイックアクション",
|
||||||
|
"Translation.utilities": "ユーティリティ",
|
||||||
|
"Translation.advanced_settings": "詳細設定",
|
||||||
|
"Translation.source_language": "ソース言語",
|
||||||
|
"Translation.source_language_desc": "翻訳元の言語",
|
||||||
|
"Translation.target_language": "ターゲット言語",
|
||||||
|
"Translation.target_language_desc": "翻訳先の言語",
|
||||||
|
"Translation.translate_names": "名前を翻訳",
|
||||||
|
"Translation.translate_names_desc": "選択したサービスと設定を使用して名前を翻訳",
|
||||||
|
"Translation.test_service": "サービスをテスト",
|
||||||
|
"Translation.test_service_desc": "現在選択されている翻訳サービスをテスト",
|
||||||
|
"Translation.clear_cache": "キャッシュをクリア",
|
||||||
|
"Translation.clear_cache_desc": "すべてのキャッシュされた翻訳をクリア",
|
||||||
|
"Translation.show_stats": "統計を表示",
|
||||||
|
"Translation.show_stats_desc": "翻訳統計と情報を表示",
|
||||||
|
"Translation.no_armature": "アーマチュアが選択されていません",
|
||||||
|
"Translation.test_failed": "翻訳サービステストが失敗しました - 設定を確認してください",
|
||||||
|
"Translation.cache_cleared": "翻訳キャッシュが正常にクリアされました",
|
||||||
|
"Translation.mymemory_info": "MyMemoryは完全に無料でAPIキー不要です。1日1000回の翻訳を提供します。",
|
||||||
|
"Translation.service.mymemory": "MyMemory(無料)",
|
||||||
|
"Translation.service.mymemory_desc": "完全に無料のサービス - APIキー不要!",
|
||||||
|
"Translation.service.libretranslate": "LibreTranslate",
|
||||||
|
"Translation.service.libretranslate_desc": "設定可能なサーバー - セルフホスト可能",
|
||||||
|
"Translation.service.deepl": "DeepL",
|
||||||
|
"Translation.service.deepl_desc": "高品質な翻訳 - APIキーが必要",
|
||||||
|
"Translation.type.bones": "ボーン",
|
||||||
|
"Translation.type.bones_desc": "ボーン名を翻訳",
|
||||||
|
"Translation.type.shapekeys": "シェイプキー",
|
||||||
|
"Translation.type.shapekeys_desc": "シェイプキー名を翻訳",
|
||||||
|
"Translation.type.materials": "マテリアル",
|
||||||
|
"Translation.type.materials_desc": "マテリアル名を翻訳",
|
||||||
|
"Translation.type.objects": "オブジェクト",
|
||||||
|
"Translation.type.objects_desc": "オブジェクト名を翻訳",
|
||||||
|
"Translation.type.all": "すべて",
|
||||||
|
"Translation.type.all_desc": "サポートされているすべてのタイプを翻訳",
|
||||||
|
"Translation.configure_deepl": "DeepL APIを設定",
|
||||||
|
"Translation.configure_deepl_desc": "DeepL翻訳サービスAPIキーを設定",
|
||||||
|
"Translation.deepl_api_key": "DeepL APIキー",
|
||||||
|
"Translation.deepl_api_key_desc": "あなたのDeepL APIキー(deepl.com/proで無料キーを取得)",
|
||||||
|
"Translation.configure_libretranslate": "LibreTranslateサーバーを設定",
|
||||||
|
"Translation.configure_libretranslate_desc": "LibreTranslate翻訳サービスサーバーURLを設定",
|
||||||
|
"Translation.server_url": "サーバーURL",
|
||||||
|
"Translation.server_url_desc": "LibreTranslateサーバーURL(例:https://your-server.com)",
|
||||||
|
"Translation.api_key": "APIキー",
|
||||||
|
"Translation.api_key_desc": "LibreTranslateサーバー用のAPIキー(一部のサーバーでは任意)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "아바타 툴킷 (알파 0.3.0)",
|
"AvatarToolkit.label": "아바타 툴킷 (알파 0.6.0)",
|
||||||
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
||||||
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
||||||
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
||||||
@@ -117,6 +117,15 @@
|
|||||||
"Validation.clear_bone_highlighting": "본 강조 표시 지우기",
|
"Validation.clear_bone_highlighting": "본 강조 표시 지우기",
|
||||||
"Validation.clear_bone_highlighting_desc": "본 강조 표시를 제거하고 본 색상을 기본값으로 재설정",
|
"Validation.clear_bone_highlighting_desc": "본 강조 표시를 제거하고 본 색상을 기본값으로 재설정",
|
||||||
"Validation.highlighting_cleared": "본 강조 표시 지우기 성공",
|
"Validation.highlighting_cleared": "본 강조 표시 지우기 성공",
|
||||||
|
"Validation.label": "아마추어 검증",
|
||||||
|
"Validation.validate_now": "지금 아마추어 검증",
|
||||||
|
"Validation.validate_now_desc": "아마추어 검증을 실행하고 자세한 결과 표시",
|
||||||
|
"Validation.results": "검증 결과",
|
||||||
|
"Validation.tpose.validate_now": "지금 T-포즈 검증",
|
||||||
|
|
||||||
|
"Armature.validation.acceptable_standard.success": "아마추어가 허용 가능한 표준을 충족합니다",
|
||||||
|
"Armature.validation.acceptable_standard.note": "이것은 대부분의 아바타 시스템과 호환되는 유효한 아마추어 형식입니다",
|
||||||
|
"Armature.validation.acceptable_standard.option": "필요한 경우 아마추어를 표준화할 수 있습니다",
|
||||||
|
|
||||||
"Mesh.validation.no_data": "메시 데이터 없음",
|
"Mesh.validation.no_data": "메시 데이터 없음",
|
||||||
"Mesh.validation.no_vertex_groups": "버텍스 그룹을 찾을 수 없음",
|
"Mesh.validation.no_vertex_groups": "버텍스 그룹을 찾을 수 없음",
|
||||||
@@ -194,6 +203,7 @@
|
|||||||
"Tools.digitigrade_error": "디지티그레이드 다리 생성 실패: {error}",
|
"Tools.digitigrade_error": "디지티그레이드 다리 생성 실패: {error}",
|
||||||
"Tools.digitigrade_success": "디지티그레이드 다리 설정 생성 성공",
|
"Tools.digitigrade_success": "디지티그레이드 다리 설정 생성 성공",
|
||||||
"Tools.processing_leg": "다리 본 처리 중: {bone}",
|
"Tools.processing_leg": "다리 본 처리 중: {bone}",
|
||||||
|
"Tools.weight_title": "가중치 도구",
|
||||||
"Tools.merge_twist_bones": "트위스트 본 유지",
|
"Tools.merge_twist_bones": "트위스트 본 유지",
|
||||||
"Tools.merge_twist_bones_desc": "체크하면 가중치가 0이더라도 트위스트 본이 유지됩니다",
|
"Tools.merge_twist_bones_desc": "체크하면 가중치가 0이더라도 트위스트 본이 유지됩니다",
|
||||||
"Tools.clean_weights": "가중치 0인 본 제거",
|
"Tools.clean_weights": "가중치 0인 본 제거",
|
||||||
@@ -308,6 +318,7 @@
|
|||||||
"Visemes.success": "비셈 생성 성공",
|
"Visemes.success": "비셈 생성 성공",
|
||||||
"Visemes.mesh_select": "메시 선택",
|
"Visemes.mesh_select": "메시 선택",
|
||||||
"Visemes.mesh_select_desc": "비셈을 생성할 메시 선택",
|
"Visemes.mesh_select_desc": "비셈을 생성할 메시 선택",
|
||||||
|
"Visemes.no_meshes": "메시를 찾을 수 없음",
|
||||||
|
|
||||||
"EyeTracking.label": "시선 추적",
|
"EyeTracking.label": "시선 추적",
|
||||||
"EyeTracking.setup": "시선 추적 설정",
|
"EyeTracking.setup": "시선 추적 설정",
|
||||||
@@ -500,6 +511,8 @@
|
|||||||
"TextureAtlas.save_file_instructions": "파일 > 다른 이름으로 저장... 을 사용하거나 아래 버튼을 클릭하세요:",
|
"TextureAtlas.save_file_instructions": "파일 > 다른 이름으로 저장... 을 사용하거나 아래 버튼을 클릭하세요:",
|
||||||
"TextureAtlas.save_file_button": "Blender 파일 저장",
|
"TextureAtlas.save_file_button": "Blender 파일 저장",
|
||||||
"TextureAtlas.save_file_required": "파일 저장 필요",
|
"TextureAtlas.save_file_required": "파일 저장 필요",
|
||||||
|
"TextureAtlas.search_materials": "재질 검색",
|
||||||
|
"TextureAtlas.search_materials_desc": "이름으로 재질 필터링",
|
||||||
|
|
||||||
"Settings.label": "설정",
|
"Settings.label": "설정",
|
||||||
"Settings.language": "언어",
|
"Settings.language": "언어",
|
||||||
@@ -537,6 +550,97 @@
|
|||||||
"Language.ko_KR": "한국어",
|
"Language.ko_KR": "한국어",
|
||||||
"Language.changed.title": "언어 변경됨",
|
"Language.changed.title": "언어 변경됨",
|
||||||
"Language.changed.success": "언어가 성공적으로 변경되었습니다!",
|
"Language.changed.success": "언어가 성공적으로 변경되었습니다!",
|
||||||
"Language.changed.restart": "일부 UI 요소는 블렌더를 다시 시작해야 할 수 있습니다"
|
"Language.changed.restart": "일부 UI 요소는 블렌더를 다시 시작해야 할 수 있습니다",
|
||||||
|
|
||||||
|
"VRM.panel.label": "VRM에서 Unity로",
|
||||||
|
"VRM.converter.title": "VRM 변환기",
|
||||||
|
"VRM.no_armature_selected": "선택된 아마추어 없음",
|
||||||
|
"VRM.select_armature_to_convert": "변환할 아마추어를 선택하세요",
|
||||||
|
"VRM.armature_name": "아마추어: {name}",
|
||||||
|
"VRM.armature_detected": "VRM 아마추어 감지됨",
|
||||||
|
"VRM.no_vrm_bones_detected": "VRM 본이 감지되지 않음",
|
||||||
|
"VRM.remove_root_bone": "루트 본 제거",
|
||||||
|
"VRM.convert_to_unity_format": "Unity 형식으로 변환",
|
||||||
|
"VRM.convert_to_unity.label": "VRM을 Unity로 변환",
|
||||||
|
"VRM.convert_to_unity.desc": "VRM 아마추어 본 이름을 Unity 휴머노이드 명명 규칙으로 변환",
|
||||||
|
"VRM.conversion_info.title": "변환 정보:",
|
||||||
|
"VRM.conversion_info.renames_bones": "• VRM 본을 Unity 형식으로 이름 변경",
|
||||||
|
"VRM.conversion_info.removes_colliders": "• 콜라이더 본 제거 (선택사항)",
|
||||||
|
"VRM.conversion_info.removes_root": "• 루트 본 제거, Hips를 루트로 설정 (선택사항)",
|
||||||
|
"VRM.conversion_info.maintains_hierarchy": "• 본 계층 구조 유지",
|
||||||
|
"VRM.conversion_info.validates_results": "• 변환 결과 검증",
|
||||||
|
"VRM.conversion_info.preserves_animations": "• 모든 애니메이션 보존",
|
||||||
|
"VRM.detection_failed.title": "VRM 감지 실패:",
|
||||||
|
"VRM.detection_failed.not_vrm_format": "• 선택된 아마추어가 VRM 형식이 아님",
|
||||||
|
"VRM.detection_failed.bones_start_with": "• VRM 본은 'J_Bip_C_'로 시작함",
|
||||||
|
"VRM.detection_failed.need_five_bones": "• 최소 5개의 VRM 본이 감지되어야 함",
|
||||||
|
"VRM.detection_failed.check_bone_names": "• 아마추어 본 이름을 확인하세요",
|
||||||
|
"VRM.validation.hierarchy_passed": "Unity 계층 구조 검증 통과",
|
||||||
|
"VRM.validation.hierarchy_issues": "변환은 완료되었지만 계층 구조 검증에서 문제를 발견했습니다:",
|
||||||
|
"VRM.validation.armature_passed": "아마추어가 표준 검증을 통과했습니다",
|
||||||
|
"VRM.validation.failed": "변환은 완료되었지만 검증에 실패했습니다: {error}",
|
||||||
|
"VRM.remove_colliders": "콜라이더 제거",
|
||||||
|
"VRM.remove_colliders_desc": "변환 중 VRM 콜라이더 본 제거",
|
||||||
|
"VRM.remove_root": "루트 본 제거",
|
||||||
|
"VRM.remove_root_desc": "불필요한 VRM 루트 본을 제거하고 힙을 루트 본으로 설정",
|
||||||
|
|
||||||
|
"Translation.label": "번역",
|
||||||
|
"Translation.service": "번역 서비스",
|
||||||
|
"Translation.service_desc": "사용할 번역 서비스 선택",
|
||||||
|
"Translation.mode": "번역 모드",
|
||||||
|
"Translation.mode_desc": "번역 동작 방식 선택",
|
||||||
|
"Translation.mode.hybrid": "하이브리드 (사전 + API)",
|
||||||
|
"Translation.mode.hybrid_desc": "먼저 사전을 시도하고, 그 다음 API 서비스를 폴백으로 사용",
|
||||||
|
"Translation.mode.dictionary_only": "사전만",
|
||||||
|
"Translation.mode.dictionary_only_desc": "번역에 내장 사전만 사용",
|
||||||
|
"Translation.mode.api_only": "API만",
|
||||||
|
"Translation.mode.api_only_desc": "온라인 번역 서비스만 사용",
|
||||||
|
"Translation.service_settings": "번역 서비스",
|
||||||
|
"Translation.language_settings": "언어 설정",
|
||||||
|
"Translation.quick_actions": "빠른 작업",
|
||||||
|
"Translation.utilities": "유틸리티",
|
||||||
|
"Translation.advanced_settings": "고급 설정",
|
||||||
|
"Translation.source_language": "소스 언어",
|
||||||
|
"Translation.source_language_desc": "번역할 원본 언어",
|
||||||
|
"Translation.target_language": "대상 언어",
|
||||||
|
"Translation.target_language_desc": "번역할 대상 언어",
|
||||||
|
"Translation.translate_names": "이름 번역",
|
||||||
|
"Translation.translate_names_desc": "선택한 서비스와 설정을 사용하여 이름 번역",
|
||||||
|
"Translation.test_service": "서비스 테스트",
|
||||||
|
"Translation.test_service_desc": "현재 선택된 번역 서비스 테스트",
|
||||||
|
"Translation.clear_cache": "캐시 지우기",
|
||||||
|
"Translation.clear_cache_desc": "모든 캐시된 번역 지우기",
|
||||||
|
"Translation.show_stats": "통계 표시",
|
||||||
|
"Translation.show_stats_desc": "번역 통계 및 정보 표시",
|
||||||
|
"Translation.no_armature": "선택된 아마추어 없음",
|
||||||
|
"Translation.test_failed": "번역 서비스 테스트 실패 - 구성을 확인하세요",
|
||||||
|
"Translation.cache_cleared": "번역 캐시가 성공적으로 지워졌습니다",
|
||||||
|
"Translation.mymemory_info": "MyMemory는 API 키 없이 완전히 무료입니다. 하루 1000회 번역을 제공합니다.",
|
||||||
|
"Translation.service.mymemory": "MyMemory (무료)",
|
||||||
|
"Translation.service.mymemory_desc": "완전히 무료 서비스 - API 키 불필요!",
|
||||||
|
"Translation.service.libretranslate": "LibreTranslate",
|
||||||
|
"Translation.service.libretranslate_desc": "구성 가능한 서버 - 셀프 호스팅 가능",
|
||||||
|
"Translation.service.deepl": "DeepL",
|
||||||
|
"Translation.service.deepl_desc": "고품질 번역 - API 키 필요",
|
||||||
|
"Translation.type.bones": "본",
|
||||||
|
"Translation.type.bones_desc": "본 이름 번역",
|
||||||
|
"Translation.type.shapekeys": "쉐이프 키",
|
||||||
|
"Translation.type.shapekeys_desc": "쉐이프 키 이름 번역",
|
||||||
|
"Translation.type.materials": "재질",
|
||||||
|
"Translation.type.materials_desc": "재질 이름 번역",
|
||||||
|
"Translation.type.objects": "객체",
|
||||||
|
"Translation.type.objects_desc": "객체 이름 번역",
|
||||||
|
"Translation.type.all": "모두",
|
||||||
|
"Translation.type.all_desc": "지원되는 모든 유형 번역",
|
||||||
|
"Translation.configure_deepl": "DeepL API 구성",
|
||||||
|
"Translation.configure_deepl_desc": "DeepL 번역 서비스 API 키 구성",
|
||||||
|
"Translation.deepl_api_key": "DeepL API 키",
|
||||||
|
"Translation.deepl_api_key_desc": "당신의 DeepL API 키 (deepl.com/pro에서 무료 키 획득)",
|
||||||
|
"Translation.configure_libretranslate": "LibreTranslate 서버 구성",
|
||||||
|
"Translation.configure_libretranslate_desc": "LibreTranslate 번역 서비스 서버 URL 구성",
|
||||||
|
"Translation.server_url": "서버 URL",
|
||||||
|
"Translation.server_url_desc": "LibreTranslate 서버 URL (예: https://your-server.com)",
|
||||||
|
"Translation.api_key": "API 키",
|
||||||
|
"Translation.api_key_desc": "LibreTranslate 서버용 API 키 (일부 서버는 선택사항)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from bpy.types import UIList, Panel, UILayout, Object, Context, Material, Operat
|
|||||||
import bpy
|
import bpy
|
||||||
from math import sqrt
|
from math import sqrt
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from .panel_layout import get_panel_order, should_open_by_default
|
||||||
from ..core.common import SceneMatClass, MaterialListBool, get_active_armature
|
from ..core.common import SceneMatClass, MaterialListBool, get_active_armature
|
||||||
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
|
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
@@ -214,7 +215,8 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel):
|
|||||||
bl_region_type = 'UI'
|
bl_region_type = 'UI'
|
||||||
bl_category = CATEGORY_NAME
|
bl_category = CATEGORY_NAME
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order = 7
|
bl_order = get_panel_order('texture_atlas')
|
||||||
|
bl_options = set() if not should_open_by_default('TEXTURE_ATLAS') else {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context):
|
def draw(self, context: Context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import bpy
|
|||||||
from typing import Set, List, Tuple, Any
|
from typing import Set, List, Tuple, Any
|
||||||
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
|
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from .panel_layout import get_panel_order, should_open_by_default
|
||||||
from ..functions.custom_tools.mesh_attachment import AvatarToolkit_OT_AttachMesh
|
from ..functions.custom_tools.mesh_attachment import AvatarToolkit_OT_AttachMesh
|
||||||
from ..functions.custom_tools.armature_merging import AvatarToolkit_OT_MergeArmature
|
from ..functions.custom_tools.armature_merging import AvatarToolkit_OT_MergeArmature
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
@@ -112,8 +113,8 @@ class AvatarToolKit_PT_CustomPanel(Panel):
|
|||||||
bl_region_type: str = 'UI'
|
bl_region_type: str = 'UI'
|
||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 4
|
bl_order: int = get_panel_order('custom_avatar')
|
||||||
bl_options: Set[str] = {'DEFAULT_CLOSED'}
|
bl_options: Set[str] = set() if not should_open_by_default('CUSTOM_AVATAR') else {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the custom avatar panel UI"""
|
"""Draw the custom avatar panel UI"""
|
||||||
|
|||||||
+43
-72
@@ -2,6 +2,8 @@ import bpy
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
|
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from .ui_utils import UIStyle, draw_section_header, wrap_text_label
|
||||||
|
from .panel_layout import get_panel_order, should_open_by_default
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.common import get_active_armature, get_all_meshes
|
from ..core.common import get_active_armature, get_all_meshes
|
||||||
from ..functions.eye_tracking import (
|
from ..functions.eye_tracking import (
|
||||||
@@ -26,38 +28,37 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
bl_region_type: str = 'UI'
|
bl_region_type: str = 'UI'
|
||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 6
|
bl_order: int = get_panel_order('eye_tracking')
|
||||||
bl_options: Set[str] = {'DEFAULT_CLOSED'}
|
bl_options: Set[str] = set() if not should_open_by_default('EYE_TRACKING') else {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the eye tracking panel interface"""
|
"""Draw the eye tracking panel interface"""
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# SDK Version Selection Box
|
# SDK Version Selection
|
||||||
sdk_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.sdk_version"), icon='PRESET')
|
||||||
col: UILayout = sdk_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.sdk_version"), icon='PRESET')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.prop(toolkit, "eye_tracking_type", expand=True)
|
row.prop(toolkit, "eye_tracking_type", expand=True)
|
||||||
|
|
||||||
if toolkit.eye_tracking_type == 'SDK2':
|
if toolkit.eye_tracking_type == 'SDK2':
|
||||||
# SDK2 Warning Box
|
# SDK2 Warning
|
||||||
warning_box: UILayout = layout.box()
|
warning_box: UILayout = layout.box()
|
||||||
col: UILayout = warning_box.column(align=True)
|
col: UILayout = warning_box.column(align=True)
|
||||||
col.label(text=t("EyeTracking.sdk2_warning"), icon='INFO')
|
col.alert = True
|
||||||
col.separator(factor=0.5)
|
col.label(text=t("EyeTracking.sdk2_warning"), icon='ERROR')
|
||||||
col.label(text=t("EyeTracking.sdk2_warning_detail1"))
|
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
|
||||||
col.label(text=t("EyeTracking.sdk2_warning_detail2"))
|
|
||||||
col.label(text=t("EyeTracking.sdk2_warning_detail3"))
|
|
||||||
col.label(text=t("EyeTracking.sdk2_warning_detail4"))
|
|
||||||
|
|
||||||
# Mode Selection Box
|
warning_text = "\n".join([
|
||||||
mode_box: UILayout = layout.box()
|
t("EyeTracking.sdk2_warning_detail1"),
|
||||||
col: UILayout = mode_box.column(align=True)
|
t("EyeTracking.sdk2_warning_detail2"),
|
||||||
col.label(text=t("EyeTracking.setup"), icon='TOOL_SETTINGS')
|
t("EyeTracking.sdk2_warning_detail3"),
|
||||||
col.separator(factor=0.5)
|
t("EyeTracking.sdk2_warning_detail4")
|
||||||
|
])
|
||||||
|
wrap_text_label(col, warning_text, max_length=45)
|
||||||
|
|
||||||
|
# Mode Selection
|
||||||
|
col = draw_section_header(layout, t("EyeTracking.setup"), icon='TOOL_SETTINGS')
|
||||||
col.prop(toolkit, "eye_mode", expand=True)
|
col.prop(toolkit, "eye_mode", expand=True)
|
||||||
|
|
||||||
if toolkit.eye_mode == 'CREATION':
|
if toolkit.eye_mode == 'CREATION':
|
||||||
@@ -72,11 +73,9 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
"""Draw the AV3 eye tracking setup interface"""
|
"""Draw the AV3 eye tracking setup interface"""
|
||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Bone Setup Box
|
# Bone Setup
|
||||||
bone_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.bone_setup"), icon='BONE_DATA')
|
||||||
col: UILayout = bone_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if armature:
|
if armature:
|
||||||
@@ -86,21 +85,16 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
else:
|
else:
|
||||||
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
|
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
|
||||||
|
|
||||||
# Create Button
|
|
||||||
row: UILayout = layout.row(align=True)
|
row: UILayout = layout.row(align=True)
|
||||||
row.scale_y = 1.5
|
row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
|
||||||
row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY')
|
row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY')
|
||||||
|
|
||||||
def draw_creation_mode(self, context: Context, layout: UILayout) -> None:
|
def draw_creation_mode(self, context: Context, layout: UILayout) -> None:
|
||||||
"""Draw the eye tracking creation mode interface"""
|
"""Draw the eye tracking creation mode interface"""
|
||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Bone Setup Box
|
# Bone Setup
|
||||||
bone_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.bone_setup"), icon='BONE_DATA')
|
||||||
col: UILayout = bone_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if armature:
|
if armature:
|
||||||
col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone"))
|
col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone"))
|
||||||
@@ -109,19 +103,12 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
else:
|
else:
|
||||||
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
|
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
|
||||||
|
|
||||||
# Mesh Setup Box
|
# Mesh Setup
|
||||||
mesh_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.mesh_setup"), icon='MESH_DATA')
|
||||||
col: UILayout = mesh_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="")
|
col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="")
|
||||||
|
|
||||||
# Shape Key Setup Box
|
# Shape Key Setup
|
||||||
shape_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
|
||||||
col: UILayout = shape_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
|
mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
|
||||||
if mesh and mesh.data.shape_keys:
|
if mesh and mesh.data.shape_keys:
|
||||||
col.prop_search(toolkit, "wink_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_left"))
|
col.prop_search(toolkit, "wink_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_left"))
|
||||||
@@ -131,19 +118,15 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
else:
|
else:
|
||||||
col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR')
|
col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR')
|
||||||
|
|
||||||
# Options Box
|
# Options
|
||||||
options_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.options"), icon='SETTINGS')
|
||||||
col: UILayout = options_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.options"), icon='SETTINGS')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.prop(toolkit, "disable_eye_blinking")
|
col.prop(toolkit, "disable_eye_blinking")
|
||||||
col.prop(toolkit, "disable_eye_movement")
|
col.prop(toolkit, "disable_eye_movement")
|
||||||
if not toolkit.disable_eye_movement:
|
if not toolkit.disable_eye_movement:
|
||||||
col.prop(toolkit, "eye_distance")
|
col.prop(toolkit, "eye_distance")
|
||||||
|
|
||||||
# Create Button
|
|
||||||
row: UILayout = layout.row(align=True)
|
row: UILayout = layout.row(align=True)
|
||||||
row.scale_y = 1.5
|
row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
|
||||||
row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY')
|
row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY')
|
||||||
|
|
||||||
def draw_testing_mode(self, context: Context, layout: UILayout) -> None:
|
def draw_testing_mode(self, context: Context, layout: UILayout) -> None:
|
||||||
@@ -151,37 +134,25 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
if context.mode != 'POSE':
|
if context.mode != 'POSE':
|
||||||
# Testing Start Box
|
# Testing Start
|
||||||
test_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.testing"), icon='PLAY')
|
||||||
col: UILayout = test_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.testing"), icon='PLAY')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.scale_y = 1.5
|
row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
|
||||||
row.operator(StartTestingButton.bl_idname, icon='PLAY')
|
row.operator(StartTestingButton.bl_idname, icon='PLAY')
|
||||||
else:
|
else:
|
||||||
# Eye Rotation Box
|
# Eye Rotation
|
||||||
rotation_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE')
|
||||||
col: UILayout = rotation_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x"))
|
col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x"))
|
||||||
col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y"))
|
col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y"))
|
||||||
col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK')
|
col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK')
|
||||||
|
|
||||||
# Eye Adjustment Box
|
# Eye Adjustment
|
||||||
adjust_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.adjustments"), icon='MODIFIER')
|
||||||
col: UILayout = adjust_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.prop(toolkit, "eye_distance")
|
col.prop(toolkit, "eye_distance")
|
||||||
col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO')
|
col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO')
|
||||||
|
|
||||||
# Blinking Test Box
|
# Blinking Test
|
||||||
blink_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.blink_testing"), icon='HIDE_OFF')
|
||||||
col: UILayout = blink_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.prop(toolkit, "eye_blink_shape")
|
row.prop(toolkit, "eye_blink_shape")
|
||||||
row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF')
|
row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF')
|
||||||
@@ -192,7 +163,7 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
|
|
||||||
# Stop Testing Button
|
# Stop Testing Button
|
||||||
row: UILayout = layout.row(align=True)
|
row: UILayout = layout.row(align=True)
|
||||||
row.scale_y = 1.5
|
row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
|
||||||
row.operator(StopTestingButton.bl_idname, icon='PAUSE')
|
row.operator(StopTestingButton.bl_idname, icon='PAUSE')
|
||||||
|
|
||||||
# Reset Button
|
# Reset Button
|
||||||
|
|||||||
+9
-7
@@ -1,6 +1,7 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from typing import Optional, Set
|
from typing import Optional, Set
|
||||||
from bpy.types import Panel, Context, UILayout
|
from bpy.types import Panel, Context, UILayout
|
||||||
|
from .ui_utils import UIStyle, wrap_text_label
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
|
|
||||||
CATEGORY_NAME: str = "Avatar Toolkit"
|
CATEGORY_NAME: str = "Avatar Toolkit"
|
||||||
@@ -16,13 +17,14 @@ def draw_title(self: Panel) -> None:
|
|||||||
row.scale_y: float = 1.2
|
row.scale_y: float = 1.2
|
||||||
row.label(text=t("AvatarToolkit.label"), icon='ARMATURE_DATA')
|
row.label(text=t("AvatarToolkit.label"), icon='ARMATURE_DATA')
|
||||||
|
|
||||||
# Description as a flowing paragraph
|
# Description
|
||||||
desc_col: UILayout = col.column()
|
col.separator(factor=UIStyle.SECTION_SEPARATOR_FACTOR)
|
||||||
desc_col.scale_y: float = 0.6
|
description = " ".join([
|
||||||
desc_col.label(text=t("AvatarToolkit.desc1"))
|
t("AvatarToolkit.desc1"),
|
||||||
desc_col.label(text=t("AvatarToolkit.desc2"))
|
t("AvatarToolkit.desc2"),
|
||||||
desc_col.label(text=t("AvatarToolkit.desc3"))
|
t("AvatarToolkit.desc3")
|
||||||
col.separator()
|
])
|
||||||
|
wrap_text_label(col, description, max_length=50)
|
||||||
|
|
||||||
class AvatarToolKit_PT_AvatarToolkitPanel(Panel):
|
class AvatarToolKit_PT_AvatarToolkitPanel(Panel):
|
||||||
"""Main panel for Avatar Toolkit containing general information and settings"""
|
"""Main panel for Avatar Toolkit containing general information and settings"""
|
||||||
|
|||||||
+15
-28
@@ -2,6 +2,8 @@ import bpy
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
from bpy.types import Panel, Context, UILayout, Operator
|
from bpy.types import Panel, Context, UILayout, Operator
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from .ui_utils import UIStyle, draw_section_header, draw_operator_row
|
||||||
|
from .panel_layout import get_panel_order, should_open_by_default
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..functions.optimization.materials_tools import AvatarToolkit_OT_CombineMaterials
|
from ..functions.optimization.materials_tools import AvatarToolkit_OT_CombineMaterials
|
||||||
from ..functions.optimization.remove_doubles import AvatarToolkit_OT_RemoveDoubles
|
from ..functions.optimization.remove_doubles import AvatarToolkit_OT_RemoveDoubles
|
||||||
@@ -15,39 +17,24 @@ class AvatarToolKit_PT_OptimizationPanel(Panel):
|
|||||||
bl_region_type: str = 'UI'
|
bl_region_type: str = 'UI'
|
||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 1
|
bl_order: int = get_panel_order('optimization')
|
||||||
bl_options = {'DEFAULT_CLOSED'}
|
bl_options = set() if not should_open_by_default('OPTIMIZATION') else {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draws the optimization panel interface with material, mesh cleanup and join mesh tools"""
|
"""Draws the optimization panel interface with material, mesh cleanup and join mesh tools"""
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
# Materials Box
|
# Materials section
|
||||||
materials_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Optimization.materials_title"), icon='MATERIAL')
|
||||||
col: UILayout = materials_box.column(align=True)
|
|
||||||
col.label(text=t("Optimization.materials_title"), icon='MATERIAL')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
# Material Operations
|
|
||||||
col.operator(AvatarToolkit_OT_CombineMaterials.bl_idname, icon='MATERIAL')
|
col.operator(AvatarToolkit_OT_CombineMaterials.bl_idname, icon='MATERIAL')
|
||||||
|
|
||||||
# Mesh Cleanup Box
|
# Mesh Cleanup section
|
||||||
cleanup_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Optimization.cleanup_title"), icon='MESH_DATA')
|
||||||
col: UILayout = cleanup_box.column(align=True)
|
col.operator(AvatarToolkit_OT_RemoveDoubles.bl_idname, icon='MESH_DATA')
|
||||||
col.label(text=t("Optimization.cleanup_title"), icon='MESH_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
# Remove Doubles Row
|
# Join Meshes section
|
||||||
row: UILayout = col.row(align=True)
|
col = draw_section_header(layout, t("Optimization.join_meshes_title"), icon='OBJECT_DATA')
|
||||||
row.operator(AvatarToolkit_OT_RemoveDoubles.bl_idname, icon='MESH_DATA')
|
draw_operator_row(col, [
|
||||||
|
(AvatarToolkit_OT_JoinAllMeshes.bl_idname, t("Optimization.join_all_meshes"), 'OBJECT_DATA'),
|
||||||
# Join Meshes Box
|
(AvatarToolkit_OT_JoinSelectedMeshes.bl_idname, t("Optimization.join_selected_meshes"), 'RESTRICT_SELECT_OFF')
|
||||||
join_box: UILayout = layout.box()
|
])
|
||||||
col: UILayout = join_box.column(align=True)
|
|
||||||
col.label(text=t("Optimization.join_meshes_title"), icon='OBJECT_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
# Join Meshes Row
|
|
||||||
row: UILayout = col.row(align=True)
|
|
||||||
row.operator(AvatarToolkit_OT_JoinAllMeshes.bl_idname, icon='OBJECT_DATA')
|
|
||||||
row.operator(AvatarToolkit_OT_JoinSelectedMeshes.bl_idname, icon='RESTRICT_SELECT_OFF')
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""Panel ordering and organization guide for Avatar Toolkit UI
|
||||||
|
This module defines the standard panel order and grouping for the Avatar Toolkit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Main Panel
|
||||||
|
MAIN_PANEL_ORDER = -1 # Always first (parent panel)
|
||||||
|
QUICK_ACCESS_ORDER = 0
|
||||||
|
OPTIMIZATION_ORDER = 1
|
||||||
|
TOOLS_ORDER = 2
|
||||||
|
CUSTOM_TOOLS_ORDER = 3
|
||||||
|
CUSTOM_AVATAR_ORDER = 4
|
||||||
|
TRANSLATION_ORDER = 5
|
||||||
|
VISEMES_ORDER = 6
|
||||||
|
EYE_TRACKING_ORDER = 7
|
||||||
|
TEXTURE_ATLAS_ORDER = 8
|
||||||
|
VRM_UNITY_ORDER = 9
|
||||||
|
MMD_ORDER = 10
|
||||||
|
SETTINGS_ORDER = 11
|
||||||
|
|
||||||
|
# Panel open/closed by default
|
||||||
|
PANELS_OPEN_BY_DEFAULT = {
|
||||||
|
'QUICK_ACCESS': False,
|
||||||
|
'OPTIMIZATION': True,
|
||||||
|
'TOOLS': True,
|
||||||
|
'CUSTOM_TOOLS': True,
|
||||||
|
'CUSTOM_AVATAR': True,
|
||||||
|
'VISEMES': True,
|
||||||
|
'EYE_TRACKING': True,
|
||||||
|
'TEXTURE_ATLAS': True,
|
||||||
|
'VRM_UNITY': True,
|
||||||
|
'MMD': True,
|
||||||
|
'SETTINGS': True,
|
||||||
|
'TRANSLATION': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_panel_order(panel_name: str) -> int:
|
||||||
|
"""Get the recommended bl_order value for a panel"""
|
||||||
|
order_map = {
|
||||||
|
'quick_access': QUICK_ACCESS_ORDER,
|
||||||
|
'optimization': OPTIMIZATION_ORDER,
|
||||||
|
'tools': TOOLS_ORDER,
|
||||||
|
'custom_tools': CUSTOM_TOOLS_ORDER,
|
||||||
|
'custom_avatar': CUSTOM_AVATAR_ORDER,
|
||||||
|
'translation': TRANSLATION_ORDER,
|
||||||
|
'visemes': VISEMES_ORDER,
|
||||||
|
'eye_tracking': EYE_TRACKING_ORDER,
|
||||||
|
'texture_atlas': TEXTURE_ATLAS_ORDER,
|
||||||
|
'vrm_unity': VRM_UNITY_ORDER,
|
||||||
|
'mmd': MMD_ORDER,
|
||||||
|
'settings': SETTINGS_ORDER,
|
||||||
|
}
|
||||||
|
return order_map.get(panel_name.lower(), 99)
|
||||||
|
|
||||||
|
def should_open_by_default(panel_name: str) -> bool:
|
||||||
|
"""Check if a panel should be open by default"""
|
||||||
|
return PANELS_OPEN_BY_DEFAULT.get(panel_name.upper(), True)
|
||||||
+130
-148
@@ -10,6 +10,8 @@ from bpy.types import (
|
|||||||
Object
|
Object
|
||||||
)
|
)
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from .ui_utils import UIStyle, draw_section_header, draw_operator_row
|
||||||
|
from .panel_layout import get_panel_order, should_open_by_default
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.common import (
|
from ..core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
@@ -17,13 +19,24 @@ from ..core.common import (
|
|||||||
get_armature_list,
|
get_armature_list,
|
||||||
get_armature_stats
|
get_armature_stats
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Module-level cache for UI performance (avoids Blender scene property write restrictions)
|
||||||
|
_validation_cache = {}
|
||||||
|
_stats_cache = {}
|
||||||
|
|
||||||
|
def clear_armature_caches():
|
||||||
|
"""Clear all armature-related caches - called when armature changes"""
|
||||||
|
global _validation_cache, _stats_cache
|
||||||
|
_validation_cache.clear()
|
||||||
|
_stats_cache.clear()
|
||||||
|
|
||||||
from ..functions.pose_mode import (
|
from ..functions.pose_mode import (
|
||||||
AvatarToolkit_OT_StartPoseMode,
|
AvatarToolkit_OT_StartPoseMode,
|
||||||
AvatarToolkit_OT_StopPoseMode,
|
AvatarToolkit_OT_StopPoseMode,
|
||||||
AvatarToolkit_OT_ApplyPoseAsShapekey,
|
AvatarToolkit_OT_ApplyPoseAsShapekey,
|
||||||
AvatarToolkit_OT_ApplyPoseAsRest
|
AvatarToolkit_OT_ApplyPoseAsRest
|
||||||
)
|
)
|
||||||
from ..core.armature_validation import validate_armature, AvatarToolkit_OT_ValidateTPose
|
from ..core.armature_validation import validate_armature, AvatarToolkit_OT_ValidateTPose, is_pmx_model
|
||||||
from ..core.importers.importer import AvatarToolKit_OT_Import
|
from ..core.importers.importer import AvatarToolKit_OT_Import
|
||||||
from ..core.resonite_utils import AvatarToolKit_OT_ExportResonite
|
from ..core.resonite_utils import AvatarToolKit_OT_ExportResonite
|
||||||
from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeArmature
|
from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeArmature
|
||||||
@@ -68,37 +81,55 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|||||||
bl_region_type: str = 'UI'
|
bl_region_type: str = 'UI'
|
||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 0
|
bl_order: int = get_panel_order('quick_access')
|
||||||
|
bl_options = {'DEFAULT_CLOSED'} if should_open_by_default('QUICK_ACCESS') else set()
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the panel layout"""
|
"""Draw the panel layout"""
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Armature Selection Box
|
|
||||||
armature_box: UILayout = layout.box()
|
|
||||||
col: UILayout = armature_box.column(align=True)
|
|
||||||
col.label(text=t("QuickAccess.select_armature"), icon='ARMATURE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
# Armature Selection
|
# Armature Selection
|
||||||
|
col = draw_section_header(layout, t("QuickAccess.select_armature"), icon='ARMATURE_DATA')
|
||||||
col.prop(context.scene.avatar_toolkit, "active_armature", text="")
|
col.prop(context.scene.avatar_toolkit, "active_armature", text="")
|
||||||
|
|
||||||
# Armature Validation
|
# Get active armature
|
||||||
active_armature: Optional[Object] = get_active_armature(context)
|
active_armature: Optional[Object] = get_active_armature(context)
|
||||||
if active_armature:
|
if active_armature:
|
||||||
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True)
|
# Validation Section
|
||||||
|
col = draw_section_header(layout, t("Validation.label", "Armature Validation"), icon='CHECKMARK')
|
||||||
|
|
||||||
|
# Main validate button with prominent styling
|
||||||
|
validate_row = col.row(align=True)
|
||||||
|
validate_row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
|
||||||
|
validate_row.operator("avatar_toolkit.validate_armature_manual",
|
||||||
|
text=t("Validation.validate_now", "Validate Armature Now"),
|
||||||
|
icon='CHECKMARK')
|
||||||
|
|
||||||
|
# Validation mode selector
|
||||||
|
col.prop(props, "validation_mode", text=t("Settings.validation_mode", "Mode"))
|
||||||
|
|
||||||
|
# Show validation results if flag is set
|
||||||
|
if props.show_validation_results:
|
||||||
|
# Cache validation results
|
||||||
|
cache_key = f"validation_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}"
|
||||||
|
|
||||||
|
if cache_key not in _validation_cache:
|
||||||
|
_validation_cache[cache_key] = validate_armature(active_armature, detailed_messages=True)
|
||||||
|
|
||||||
|
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = _validation_cache[cache_key]
|
||||||
|
|
||||||
# Check if this is a PMX model
|
# Check if this is a PMX model
|
||||||
is_pmx_model = False
|
pmx_detected = is_pmx_model(active_armature)
|
||||||
if hasattr(active_armature, 'mmd_type') or (hasattr(active_armature, 'parent') and active_armature.parent and hasattr(active_armature.parent, 'mmd_type')):
|
|
||||||
is_pmx_model = True
|
|
||||||
|
|
||||||
info_box = col.box()
|
results_box = col.box()
|
||||||
|
row = results_box.row()
|
||||||
|
row.prop(props, "show_validation_results", text=t("Validation.results", "Validation Results"),
|
||||||
|
icon='TRIA_DOWN' if props.show_validation_results else 'TRIA_RIGHT', emboss=False)
|
||||||
|
|
||||||
# If it's a PMX model, display a prominent notice
|
# PMX Model Notice
|
||||||
if is_pmx_model:
|
if pmx_detected:
|
||||||
pmx_box = info_box.box()
|
pmx_box = results_box.box()
|
||||||
pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO')
|
pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO')
|
||||||
|
|
||||||
validation_mode = context.scene.avatar_toolkit.validation_mode
|
validation_mode = context.scene.avatar_toolkit.validation_mode
|
||||||
@@ -108,38 +139,35 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|||||||
else:
|
else:
|
||||||
pmx_box.label(text=t("Armature.validation.pmx_model_basic"))
|
pmx_box.label(text=t("Armature.validation.pmx_model_basic"))
|
||||||
|
|
||||||
|
# Validation Results
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
# Display non-standard bones and hierarchy issues
|
# Display found bones
|
||||||
if messages and len(messages) > 0:
|
if messages and len(messages) > 0:
|
||||||
# Found Bones section
|
bones_section = results_box.box()
|
||||||
validation_box = info_box.box()
|
row = bones_section.row()
|
||||||
row = validation_box.row()
|
row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"),
|
||||||
row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False)
|
icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False)
|
||||||
if props.show_found_bones and len(messages) > 0:
|
if props.show_found_bones:
|
||||||
for line in messages[0].split('\n'):
|
for line in messages[0].split('\n'):
|
||||||
validation_box.label(text=line)
|
bones_section.label(text=line)
|
||||||
|
|
||||||
# Main validation status
|
# Status message
|
||||||
validation_box = info_box.box()
|
status_box = results_box.box()
|
||||||
row = validation_box.row()
|
row = status_box.row()
|
||||||
row.alert = True
|
row.alert = True
|
||||||
row.label(text=t("Validation.status.failed"))
|
row.label(text=t("Validation.status.failed"), icon='ERROR')
|
||||||
|
|
||||||
# Detailed validation message
|
# Error explanation
|
||||||
validation_box = info_box.box()
|
error_box = results_box.box()
|
||||||
row = validation_box.row()
|
error_box.alert = True
|
||||||
row.alert = True
|
error_box.label(text=t("Validation.message.failed.line1"))
|
||||||
row.label(text=t("Validation.message.failed.line1"))
|
error_box.label(text=t("Validation.message.failed.line2"))
|
||||||
row = validation_box.row()
|
error_box.label(text=t("Validation.message.failed.line3"))
|
||||||
row.alert = True
|
|
||||||
row.label(text=t("Validation.message.failed.line2"))
|
|
||||||
row = validation_box.row()
|
|
||||||
row.alert = True
|
|
||||||
row.label(text=t("Validation.message.failed.line3"))
|
|
||||||
|
|
||||||
# Non-Standard Bones section
|
# Non-Standard Bones section
|
||||||
validation_box = info_box.box()
|
if non_standard_messages or pmx_detected:
|
||||||
row = validation_box.row()
|
ns_section = results_box.box()
|
||||||
|
row = ns_section.row()
|
||||||
row.alert = True
|
row.alert = True
|
||||||
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"),
|
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"),
|
||||||
icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
|
icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
|
||||||
@@ -147,154 +175,108 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|||||||
if non_standard_messages and len(non_standard_messages) > 0:
|
if non_standard_messages and len(non_standard_messages) > 0:
|
||||||
for message in non_standard_messages:
|
for message in non_standard_messages:
|
||||||
for line in message.split('\n'):
|
for line in message.split('\n'):
|
||||||
sub_row = validation_box.row()
|
sub_row = ns_section.row()
|
||||||
sub_row.alert = True
|
sub_row.alert = True
|
||||||
sub_row.label(text=line)
|
sub_row.label(text=line)
|
||||||
|
elif pmx_detected:
|
||||||
|
ns_section.alert = True
|
||||||
|
ns_section.label(text=t("Armature.validation.pmx_model_basic"))
|
||||||
|
ns_section.label(text=t("Armature.validation.pmx_model_strict"))
|
||||||
|
ns_section.label(text=t("Armature.validation.pmx_model_standardize"))
|
||||||
else:
|
else:
|
||||||
# For PMX models, if no non-standard messages but it's a PMX model,
|
ns_section.label(text=t("Validation.no_non_standard_issues"))
|
||||||
# we should still indicate there might be non-standard bones
|
|
||||||
if is_pmx_model:
|
|
||||||
sub_row = validation_box.row()
|
|
||||||
sub_row.alert = True
|
|
||||||
sub_row.label(text=t("Armature.validation.pmx_model_basic"))
|
|
||||||
|
|
||||||
sub_row = validation_box.row()
|
|
||||||
sub_row.alert = True
|
|
||||||
sub_row.label(text=t("Armature.validation.pmx_model_strict"))
|
|
||||||
|
|
||||||
sub_row = validation_box.row()
|
|
||||||
sub_row.alert = True
|
|
||||||
sub_row.label(text=t("Armature.validation.pmx_model_standardize"))
|
|
||||||
|
|
||||||
else:
|
|
||||||
sub_row = validation_box.row()
|
|
||||||
sub_row.label(text=t("Validation.no_non_standard_issues"))
|
|
||||||
|
|
||||||
# Hierarchy Issues section
|
# Hierarchy Issues section
|
||||||
validation_box = info_box.box()
|
if hierarchy_messages:
|
||||||
row = validation_box.row()
|
hier_section = results_box.box()
|
||||||
|
row = hier_section.row()
|
||||||
row.alert = True
|
row.alert = True
|
||||||
row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"),
|
row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"),
|
||||||
icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False)
|
icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False)
|
||||||
if props.show_hierarchy:
|
if props.show_hierarchy:
|
||||||
if hierarchy_messages:
|
|
||||||
for message in hierarchy_messages:
|
for message in hierarchy_messages:
|
||||||
sub_row = validation_box.row()
|
sub_row = hier_section.row()
|
||||||
sub_row.alert = True
|
sub_row.alert = True
|
||||||
sub_row.label(text=message)
|
sub_row.label(text=message)
|
||||||
else:
|
|
||||||
sub_row = validation_box.row()
|
|
||||||
sub_row.label(text=t("Validation.no_hierarchy_issues"))
|
|
||||||
|
|
||||||
# Scale Issues section
|
# Scale Issues section
|
||||||
validation_box = info_box.box()
|
if scale_messages:
|
||||||
row = validation_box.row()
|
scale_section = results_box.box()
|
||||||
|
row = scale_section.row()
|
||||||
row.alert = True
|
row.alert = True
|
||||||
row.prop(props, "show_scale_issues", text=t("Validation.section.scale_issues"),
|
row.prop(props, "show_scale_issues", text=t("Validation.section.scale_issues"),
|
||||||
icon='TRIA_DOWN' if props.show_scale_issues else 'TRIA_RIGHT', emboss=False)
|
icon='TRIA_DOWN' if props.show_scale_issues else 'TRIA_RIGHT', emboss=False)
|
||||||
if props.show_scale_issues:
|
if props.show_scale_issues:
|
||||||
if scale_messages:
|
|
||||||
for scale_msg in scale_messages:
|
for scale_msg in scale_messages:
|
||||||
sub_row = validation_box.row()
|
sub_row = scale_section.row()
|
||||||
sub_row.alert = True
|
sub_row.alert = True
|
||||||
sub_row.label(text=scale_msg)
|
sub_row.label(text=scale_msg)
|
||||||
else:
|
|
||||||
sub_row = validation_box.row()
|
|
||||||
sub_row.label(text=t("Validation.no_scale_issues"))
|
|
||||||
|
|
||||||
pose_box = layout.box()
|
|
||||||
col = pose_box.column(align=True)
|
|
||||||
col.label(text=t("Validation.tpose.label"), icon='ARMATURE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolkit_OT_ValidateTPose.bl_idname, icon='CHECKMARK')
|
|
||||||
|
|
||||||
if props.show_tpose_validation:
|
|
||||||
validation_box = col.box()
|
|
||||||
if props.tpose_validation_result:
|
|
||||||
validation_box.label(text=t("Validation.tpose.valid"), icon='CHECKMARK')
|
|
||||||
else:
|
|
||||||
row = validation_box.row()
|
|
||||||
row.alert = True
|
|
||||||
row.label(text=t("Validation.tpose.warning"), icon='ERROR')
|
|
||||||
|
|
||||||
for msg in props.tpose_validation_messages:
|
|
||||||
row = validation_box.row()
|
|
||||||
row.alert = True
|
|
||||||
row.label(text=msg.name)
|
|
||||||
else:
|
|
||||||
# If no specific issues, show acceptable message
|
|
||||||
if messages and len(messages) > 0:
|
|
||||||
info_box.label(text=messages[0], icon='INFO')
|
|
||||||
if len(messages) > 1:
|
|
||||||
info_box.label(text=messages[1])
|
|
||||||
if len(messages) > 2:
|
|
||||||
info_box.label(text=messages[2])
|
|
||||||
else:
|
|
||||||
info_box.label(text=t("Validation.no_messages"), icon='INFO')
|
|
||||||
elif is_valid and not is_acceptable:
|
elif is_valid and not is_acceptable:
|
||||||
row = info_box.row()
|
# Valid armature - show stats
|
||||||
split = row.split(factor=0.6)
|
stats_cache_key = f"stats_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}"
|
||||||
split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
|
|
||||||
stats = get_armature_stats(active_armature)
|
if stats_cache_key not in _stats_cache:
|
||||||
|
_stats_cache[stats_cache_key] = get_armature_stats(active_armature)
|
||||||
|
|
||||||
|
stats = _stats_cache[stats_cache_key]
|
||||||
|
|
||||||
|
status_box = results_box.box()
|
||||||
|
row = status_box.row()
|
||||||
|
row.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
|
||||||
|
split = row.split(factor=0.4)
|
||||||
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
|
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
|
||||||
|
|
||||||
if stats['has_pose']:
|
if stats['has_pose']:
|
||||||
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
|
results_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
|
||||||
elif is_valid and is_acceptable:
|
|
||||||
# Show acceptable standard message
|
|
||||||
if messages and len(messages) > 0:
|
|
||||||
info_box.label(text=messages[0], icon='INFO')
|
|
||||||
|
|
||||||
# Only try to access additional messages if they exist
|
elif is_valid and is_acceptable:
|
||||||
if len(messages) > 1:
|
# Acceptable standard
|
||||||
info_box.label(text=messages[1])
|
status_box = results_box.box()
|
||||||
if len(messages) > 2:
|
status_box.label(text=t("Armature.validation.acceptable_standard.success"), icon='INFO')
|
||||||
info_box.label(text=messages[2])
|
status_box.label(text=t("Armature.validation.acceptable_standard.note"))
|
||||||
else:
|
status_box.label(text=t("Armature.validation.acceptable_standard.option"))
|
||||||
info_box.label(text=t("Validation.no_messages"), icon='INFO')
|
|
||||||
|
|
||||||
# Add standardize button
|
# Add standardize button
|
||||||
standardize_box = info_box.box()
|
standardize_box = results_box.box()
|
||||||
standardize_box.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname,
|
standardize_box.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname,
|
||||||
text=t("QuickAccess.standardize_armature"),
|
text=t("QuickAccess.standardize_armature"),
|
||||||
icon='MODIFIER')
|
icon='MODIFIER')
|
||||||
|
|
||||||
# Validation Mode Warnings
|
# T-Pose Validation
|
||||||
validation_mode = context.scene.avatar_toolkit.validation_mode
|
col = draw_section_header(layout, t("Validation.tpose.label"), icon='ARMATURE_DATA')
|
||||||
if validation_mode == 'BASIC':
|
col.operator(AvatarToolkit_OT_ValidateTPose.bl_idname, text=t("Validation.tpose.validate_now"), icon='CHECKMARK')
|
||||||
warning_row = info_box.box()
|
|
||||||
warning_row.alert = True
|
if props.show_tpose_validation:
|
||||||
warning_row.label(text=t("QuickAccess.validation_basic_warning"), icon='INFO')
|
validation_result_col = col.column(align=True)
|
||||||
warning_row.label(text=t("QuickAccess.validation_basic_details"))
|
if props.tpose_validation_result:
|
||||||
elif validation_mode == 'NONE':
|
validation_result_col.label(text=t("Validation.tpose.valid"), icon='CHECKMARK')
|
||||||
warning_row = info_box.box()
|
else:
|
||||||
warning_row.alert = True
|
validation_result_col.alert = True
|
||||||
warning_row.label(text=t("QuickAccess.validation_none_warning"), icon='ERROR')
|
validation_result_col.label(text=t("Validation.tpose.warning"), icon='ERROR')
|
||||||
warning_row.label(text=t("QuickAccess.validation_none_details"))
|
|
||||||
|
for msg in props.tpose_validation_messages:
|
||||||
|
validation_result_col.label(text=msg.name)
|
||||||
|
|
||||||
# Pose Mode Controls
|
# Pose Mode Controls
|
||||||
pose_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("QuickAccess.pose_controls"), icon='ARMATURE_DATA')
|
||||||
col = pose_box.column(align=True)
|
|
||||||
col.label(text=t("QuickAccess.pose_controls"), icon='ARMATURE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
if context.mode == "POSE":
|
if context.mode == "POSE":
|
||||||
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, icon='POSE_HLT')
|
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, icon='POSE_HLT')
|
||||||
col.separator(factor=0.5)
|
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
|
||||||
col.operator(AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, icon='MOD_ARMATURE')
|
draw_operator_row(col, [
|
||||||
col.operator(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, icon='MOD_ARMATURE')
|
(AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, t("QuickAccess.apply_pose_as_rest.label"), 'MOD_ARMATURE'),
|
||||||
|
(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, t("QuickAccess.apply_pose_as_shapekey.label"), 'MOD_ARMATURE')
|
||||||
|
])
|
||||||
else:
|
else:
|
||||||
col.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, icon='POSE_HLT')
|
col.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, icon='POSE_HLT')
|
||||||
|
|
||||||
# Import/Export Box
|
# Import/Export Section
|
||||||
import_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("QuickAccess.import_export"), icon='IMPORT')
|
||||||
col = import_box.column(align=True)
|
|
||||||
col.label(text=t("QuickAccess.import_export"), icon='IMPORT')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
# Import/Export Buttons
|
# Import/Export Buttons
|
||||||
button_row: UILayout = col.row(align=True)
|
draw_operator_row(col, [
|
||||||
button_row.scale_y = 1.5
|
(AvatarToolKit_OT_Import.bl_idname, t("QuickAccess.import"), 'IMPORT'),
|
||||||
button_row.operator(AvatarToolKit_OT_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT')
|
(AvatarToolKit_OT_ExportMenu.bl_idname, t("QuickAccess.export"), 'EXPORT')
|
||||||
button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT')
|
], scale_y=UIStyle.PRIMARY_BUTTON_SCALE)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""Base classes for reusable search operators"""
|
||||||
|
|
||||||
|
from typing import Set, Callable, Optional
|
||||||
|
from bpy.types import Operator, Context, Event, WindowManager
|
||||||
|
|
||||||
|
|
||||||
|
class SearchOperatorBase(Operator):
|
||||||
|
"""
|
||||||
|
Reusable base class for search/selection operators.
|
||||||
|
|
||||||
|
This is an abstract base class - do not use directly.
|
||||||
|
Subclass and implement your specific search operator instead.
|
||||||
|
|
||||||
|
Subclasses should:
|
||||||
|
1. Define bl_idname, bl_label, bl_description
|
||||||
|
2. Define search_property_name (name of EnumProperty)
|
||||||
|
3. Define target_property_name (name of property to set on scene)
|
||||||
|
4. Define get_items_func (function to get enum items)
|
||||||
|
5. Optionally override get_enum_property() to customize the enum
|
||||||
|
|
||||||
|
This was created because search in ATK was all over the place and inconsistent, this way we have a standard way to do it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Mark this as abstract by setting a non-Blender-compatible idname
|
||||||
|
bl_idname = "wm.search_operator_base" # Will be overridden in subclasses
|
||||||
|
bl_label = "Search and Select"
|
||||||
|
bl_options = {'REGISTER', 'INTERNAL'}
|
||||||
|
|
||||||
|
# These should be overridden in subclasses
|
||||||
|
search_property_name: str = "search_enum"
|
||||||
|
target_property_name: str = "target_property"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_items_func(scene, context) -> list:
|
||||||
|
"""Override this to provide enum items. Return list of (id, name, description) tuples"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_enum_property(self) -> None:
|
||||||
|
"""
|
||||||
|
Create the enum property dynamically. Override if you need custom behavior.
|
||||||
|
This is called during class creation.
|
||||||
|
"""
|
||||||
|
import bpy
|
||||||
|
setattr(
|
||||||
|
type(self),
|
||||||
|
self.search_property_name,
|
||||||
|
bpy.props.EnumProperty(
|
||||||
|
name="Search",
|
||||||
|
description="Select item",
|
||||||
|
items=self.get_items_func
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
"""Set the target property from the search selection"""
|
||||||
|
search_value = getattr(self, self.search_property_name, None)
|
||||||
|
if search_value:
|
||||||
|
setattr(context.scene.avatar_toolkit, self.target_property_name, search_value)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||||
|
"""Open search popup"""
|
||||||
|
wm: WindowManager = context.window_manager
|
||||||
|
wm.invoke_search_popup(self)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class ArmatureSearchOperator(SearchOperatorBase):
|
||||||
|
"""Specialized search operator for selecting armatures"""
|
||||||
|
|
||||||
|
bl_label = "Search Armatures"
|
||||||
|
search_property_name: str = "search_armature_enum"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_items_func(scene, context) -> list:
|
||||||
|
"""Get list of all armature objects in scene"""
|
||||||
|
import bpy
|
||||||
|
return [
|
||||||
|
(obj.name, obj.name, "")
|
||||||
|
for obj in bpy.data.objects
|
||||||
|
if obj.type == 'ARMATURE'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MeshSearchOperator(SearchOperatorBase):
|
||||||
|
"""Specialized search operator for selecting meshes"""
|
||||||
|
|
||||||
|
bl_label = "Search Meshes"
|
||||||
|
search_property_name: str = "search_mesh_enum"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_items_func(scene, context) -> list:
|
||||||
|
"""Get list of all mesh objects without armature modifiers"""
|
||||||
|
import bpy
|
||||||
|
return [
|
||||||
|
(obj.name, obj.name, "")
|
||||||
|
for obj in bpy.data.objects
|
||||||
|
if obj.type == 'MESH'
|
||||||
|
and not any(mod.type == 'ARMATURE' for mod in obj.modifiers)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BoneSearchOperator(SearchOperatorBase):
|
||||||
|
"""Specialized search operator for selecting bones from active armature"""
|
||||||
|
|
||||||
|
bl_label = "Search Bones"
|
||||||
|
search_property_name: str = "search_bone_enum"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_items_func(scene, context) -> list:
|
||||||
|
"""Get list of all bones from active armature"""
|
||||||
|
from ..core.common import get_active_armature
|
||||||
|
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
(bone.name, bone.name, "")
|
||||||
|
for bone in armature.data.bones
|
||||||
|
]
|
||||||
+15
-23
@@ -9,6 +9,8 @@ from bpy.types import (
|
|||||||
Event
|
Event
|
||||||
)
|
)
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from .ui_utils import UIStyle, draw_section_header, wrap_text_label
|
||||||
|
from .panel_layout import get_panel_order, should_open_by_default
|
||||||
from ..core.translations import t, get_languages_list
|
from ..core.translations import t, get_languages_list
|
||||||
from ..core.armature_validation import AvatarToolkit_OT_HighlightProblemBones, AvatarToolkit_OT_ClearBoneHighlighting
|
from ..core.armature_validation import AvatarToolkit_OT_HighlightProblemBones, AvatarToolkit_OT_ClearBoneHighlighting
|
||||||
|
|
||||||
@@ -26,8 +28,10 @@ class AvatarToolkit_OT_TranslationRestartPopup(Operator):
|
|||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
layout.label(text=t("Language.changed.success"))
|
col = layout.column(align=True)
|
||||||
layout.label(text=t("Language.changed.restart"))
|
col.label(text=t("Language.changed.success"))
|
||||||
|
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
|
||||||
|
wrap_text_label(col, t("Language.changed.restart"), max_length=50)
|
||||||
|
|
||||||
class AvatarToolKit_PT_SettingsPanel(Panel):
|
class AvatarToolKit_PT_SettingsPanel(Panel):
|
||||||
"""Settings panel for Avatar Toolkit containing language preferences"""
|
"""Settings panel for Avatar Toolkit containing language preferences"""
|
||||||
@@ -37,8 +41,8 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
bl_region_type: str = 'UI'
|
bl_region_type: str = 'UI'
|
||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 8
|
bl_order: int = get_panel_order('settings')
|
||||||
bl_options = {'DEFAULT_CLOSED'}
|
bl_options = set() if not should_open_by_default('SETTINGS') else {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the settings panel layout with language selection"""
|
"""Draw the settings panel layout with language selection"""
|
||||||
@@ -46,30 +50,18 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Language Settings
|
# Language Settings
|
||||||
lang_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Settings.language"), icon='WORLD')
|
||||||
col: UILayout = lang_box.column(align=True)
|
|
||||||
row: UILayout = col.row()
|
|
||||||
row.scale_y = 1.2
|
|
||||||
row.label(text=t("Settings.language"), icon='WORLD')
|
|
||||||
col.separator()
|
|
||||||
col.prop(props, "language", text="")
|
col.prop(props, "language", text="")
|
||||||
|
|
||||||
# Validation Settings
|
# Validation Settings with help text
|
||||||
val_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Settings.validation_mode"), icon='CHECKMARK')
|
||||||
col = val_box.column(align=True)
|
|
||||||
row = col.row()
|
|
||||||
row.scale_y = 1.2
|
|
||||||
row.label(text=t("Settings.validation_mode"), icon='CHECKMARK')
|
|
||||||
col.separator()
|
|
||||||
col.prop(props, "validation_mode", text="")
|
col.prop(props, "validation_mode", text="")
|
||||||
|
# Help text for validation mode
|
||||||
|
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
|
||||||
|
wrap_text_label(col, "Select how strictly to validate armature bone structure and naming conventions.", max_length=40)
|
||||||
|
|
||||||
# Bone Highlighting Settings
|
# Bone Highlighting Settings
|
||||||
bone_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Settings.bone_highlighting"), icon='BONE_DATA')
|
||||||
col = bone_box.column(align=True)
|
|
||||||
row = col.row()
|
|
||||||
row.scale_y = 1.2
|
|
||||||
row.label(text=t("Settings.bone_highlighting"), icon='BONE_DATA')
|
|
||||||
col.separator()
|
|
||||||
col.prop(props, "highlight_problem_bones")
|
col.prop(props, "highlight_problem_bones")
|
||||||
if props.highlight_problem_bones:
|
if props.highlight_problem_bones:
|
||||||
col.operator(AvatarToolkit_OT_HighlightProblemBones.bl_idname, icon='COLOR')
|
col.operator(AvatarToolkit_OT_HighlightProblemBones.bl_idname, icon='COLOR')
|
||||||
|
|||||||
+30
-52
@@ -2,6 +2,8 @@ import bpy
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
from bpy.types import Panel, Context, UILayout, Operator, UIList
|
from bpy.types import Panel, Context, UILayout, Operator, UIList
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from .ui_utils import UIStyle, draw_section_header, draw_operator_row
|
||||||
|
from .panel_layout import get_panel_order, should_open_by_default
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
|
|
||||||
from ..core.resonite_utils import AvatarToolkit_OT_ConvertResonite
|
from ..core.resonite_utils import AvatarToolkit_OT_ConvertResonite
|
||||||
@@ -29,8 +31,8 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
|
|||||||
bl_region_type: str = 'UI'
|
bl_region_type: str = 'UI'
|
||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 2
|
bl_order: int = get_panel_order('tools')
|
||||||
bl_options = {'DEFAULT_CLOSED'}
|
bl_options = set() if not should_open_by_default('TOOLS') else {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the tools panel interface"""
|
"""Draw the tools panel interface"""
|
||||||
@@ -38,94 +40,70 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
|
|||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# General Tools
|
# General Tools
|
||||||
tools_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.general_title"), icon='TOOL_SETTINGS')
|
||||||
col: UILayout = tools_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.general_title"), icon='TOOL_SETTINGS')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolkit_OT_ConvertResonite.bl_idname, text=t("Tools.convert_resonite"), icon='EXPORT')
|
col.operator(AvatarToolkit_OT_ConvertResonite.bl_idname, text=t("Tools.convert_resonite"), icon='EXPORT')
|
||||||
|
|
||||||
# Separation Tools
|
# Separation Tools
|
||||||
sep_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.separate_title"), icon='MOD_EXPLODE')
|
||||||
col = sep_box.column(align=True)
|
draw_operator_row(col, [
|
||||||
col.label(text=t("Tools.separate_title"), icon='MOD_EXPLODE')
|
(AvatarToolKit_OT_SeparateByMaterials.bl_idname, t("Tools.separate_materials"), 'MATERIAL'),
|
||||||
col.separator(factor=0.5)
|
(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, t("Tools.separate_loose"), 'MESH_DATA')
|
||||||
row: UILayout = col.row(align=True)
|
])
|
||||||
row.operator(AvatarToolKit_OT_SeparateByMaterials.bl_idname, text=t("Tools.separate_materials"), icon='MATERIAL')
|
|
||||||
row.operator(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, text=t("Tools.separate_loose"), icon='MESH_DATA')
|
|
||||||
|
|
||||||
# Bone Tools
|
# Bone Tools
|
||||||
bone_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.bone_title"), icon='BONE_DATA')
|
||||||
col = bone_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.bone_title"), icon='BONE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolKit_OT_CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade"), icon='BONE_DATA')
|
col.operator(AvatarToolKit_OT_CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade"), icon='BONE_DATA')
|
||||||
col.operator(AvatarToolKit_OT_FlipCurrentKeyFrames.bl_idname, text=t("Tools.flip_pose_frames"), icon="ACTION")
|
col.operator(AvatarToolKit_OT_FlipCurrentKeyFrames.bl_idname, text=t("Tools.flip_pose_frames"), icon="ACTION")
|
||||||
|
|
||||||
# Mesh Tools
|
# Mesh Tools
|
||||||
mesh_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.mesh_title"), icon='MESH_DATA')
|
||||||
col = mesh_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.mesh_title"), icon='MESH_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolkit_OT_SelectShortestSeamPath.bl_idname, text=t("Tools.find_shortest_seam_path"), icon="MESH_DATA")
|
col.operator(AvatarToolkit_OT_SelectShortestSeamPath.bl_idname, text=t("Tools.find_shortest_seam_path"), icon="MESH_DATA")
|
||||||
col.operator(AvatarToolkit_OT_ApplyModifierForShapkeyObj.bl_idname, text=t("Tools.apply_modifier_on_shapekey_obj"), icon="SHAPEKEY_DATA")
|
col.operator(AvatarToolkit_OT_ApplyModifierForShapkeyObj.bl_idname, text=t("Tools.apply_modifier_on_shapekey_obj"), icon="SHAPEKEY_DATA")
|
||||||
col.operator(AvatarToolkit_OT_ExplodeMesh.bl_idname, text=t("Tools.explode_mesh"), icon="MOD_EXPLODE")
|
col.operator(AvatarToolkit_OT_ExplodeMesh.bl_idname, text=t("Tools.explode_mesh"), icon="MOD_EXPLODE")
|
||||||
|
|
||||||
|
|
||||||
# Standardization Tools
|
# Standardization Tools
|
||||||
standardize_box: UILayout = bone_box.box()
|
col = draw_section_header(layout, t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE')
|
||||||
col = standardize_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, icon='CHECKMARK')
|
col.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, icon='CHECKMARK')
|
||||||
|
|
||||||
# Weight Tools
|
# Weight Tools
|
||||||
weight_box: UILayout = bone_box.box()
|
col = draw_section_header(layout, t("Tools.weight_title"), icon='GROUP_BONE')
|
||||||
col = weight_box.column(align=True)
|
|
||||||
col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
|
col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
|
||||||
col.prop(toolkit, "preserve_parent_bones")
|
col.prop(toolkit, "preserve_parent_bones")
|
||||||
col.prop(toolkit, "target_bone_type")
|
col.prop(toolkit, "target_bone_type")
|
||||||
col.prop(toolkit, "list_only_mode")
|
col.prop(toolkit, "list_only_mode")
|
||||||
|
|
||||||
if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0:
|
if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0:
|
||||||
box = weight_box.box()
|
sub_col = col.box()
|
||||||
row = box.row()
|
row = sub_col.row()
|
||||||
row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "",
|
row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "",
|
||||||
toolkit, "zero_weight_bones",
|
toolkit, "zero_weight_bones",
|
||||||
toolkit, "zero_weight_bones_index")
|
toolkit, "zero_weight_bones_index")
|
||||||
|
|
||||||
col = box.column(align=True)
|
sub_col.operator(AvatarToolKit_OT_RemoveSelectedBones.bl_idname,
|
||||||
col.operator(AvatarToolKit_OT_RemoveSelectedBones.bl_idname,
|
|
||||||
text=t("Tools.remove_selected_bones"))
|
text=t("Tools.remove_selected_bones"))
|
||||||
|
|
||||||
row = col.row(align=True)
|
# Combine weight
|
||||||
row.operator(AvatarToolKit_OT_RemoveZeroWeightBones.bl_idname, text=t("Tools.clean_weights"), icon='GROUP_BONE')
|
draw_operator_row(col, [
|
||||||
row.operator(AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE')
|
(AvatarToolKit_OT_RemoveZeroWeightBones.bl_idname, t("Tools.clean_weights"), 'GROUP_BONE'),
|
||||||
row = col.row(align=True)
|
(AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, t("Tools.clean_constraints"), 'CONSTRAINT_BONE')
|
||||||
row.operator(AvatarToolKit_OT_RemoveZeroWeightVertexGroups.bl_idname, text=t("Tools.clean_vertex_groups"), icon='CONSTRAINT_BONE')
|
])
|
||||||
|
col.operator(AvatarToolKit_OT_RemoveZeroWeightVertexGroups.bl_idname, text=t("Tools.clean_vertex_groups"), icon='CONSTRAINT_BONE')
|
||||||
|
|
||||||
# Merge Tools
|
# Merge Tools
|
||||||
merge_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.merge_title"), icon='AUTOMERGE_ON')
|
||||||
col = merge_box.column(align=True)
|
draw_operator_row(col, [
|
||||||
col.label(text=t("Tools.merge_title"), icon='AUTOMERGE_ON')
|
(AvatarToolkit_OT_MergeToActive.bl_idname, t("Tools.merge_to_active"), 'BONE_DATA'),
|
||||||
col.separator(factor=0.5)
|
(AvatarToolkit_OT_MergeToParent.bl_idname, t("Tools.merge_to_parent"), 'BONE_DATA')
|
||||||
row = col.row(align=True)
|
])
|
||||||
row.operator(AvatarToolkit_OT_MergeToActive.bl_idname, text=t("Tools.merge_to_active"), icon='BONE_DATA')
|
|
||||||
row.operator(AvatarToolkit_OT_MergeToParent.bl_idname, text=t("Tools.merge_to_parent"), icon='BONE_DATA')
|
|
||||||
col.operator(AvatarToolkit_OT_ConnectBones.bl_idname, text=t("Tools.connect_bones"), icon='BONE_DATA')
|
col.operator(AvatarToolkit_OT_ConnectBones.bl_idname, text=t("Tools.connect_bones"), icon='BONE_DATA')
|
||||||
|
|
||||||
# Additional Tools
|
# Additional Tools
|
||||||
extra_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.additional_title"), icon='TOOL_SETTINGS')
|
||||||
col = extra_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.additional_title"), icon='TOOL_SETTINGS')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolkit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
|
col.operator(AvatarToolkit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
|
||||||
col.operator(AvatarToolkit_OT_CleanShapekeys.bl_idname, text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
|
col.operator(AvatarToolkit_OT_CleanShapekeys.bl_idname, text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
|
||||||
|
|
||||||
# Rigify Tools
|
# Rigify Tools
|
||||||
rigify_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.rigify_title"), icon='ARMATURE_DATA')
|
||||||
col = rigify_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.rigify_title"), icon='ARMATURE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolkit_OT_ConvertRigifyToUnity.bl_idname, icon='ARMATURE_DATA')
|
col.operator(AvatarToolkit_OT_ConvertRigifyToUnity.bl_idname, icon='ARMATURE_DATA')
|
||||||
col.prop(context.scene.avatar_toolkit, "merge_twist_bones")
|
col.prop(context.scene.avatar_toolkit, "merge_twist_bones")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,732 @@
|
|||||||
|
# GPL License
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from typing import Set, Dict, List, Optional, Any
|
||||||
|
from bpy.types import (
|
||||||
|
Operator,
|
||||||
|
Panel,
|
||||||
|
Context,
|
||||||
|
UILayout,
|
||||||
|
WindowManager,
|
||||||
|
Event,
|
||||||
|
Object
|
||||||
|
)
|
||||||
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from .panel_layout import get_panel_order, should_open_by_default
|
||||||
|
from ..core.translations import t
|
||||||
|
from ..core.logging_setup import logger
|
||||||
|
from ..core.common import get_active_armature, ProgressTracker
|
||||||
|
|
||||||
|
# Module-level cache for UI performance (avoids Blender scene property write restrictions)
|
||||||
|
_ui_cache = {
|
||||||
|
'translation_status': {},
|
||||||
|
'deepl_config': {},
|
||||||
|
'libretranslate_config': {},
|
||||||
|
'last_refresh_frame': 0,
|
||||||
|
'cache_refresh_interval': 30
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_TranslateNames(Operator):
|
||||||
|
"""Translate names using the translation system"""
|
||||||
|
bl_idname: str = "avatar_toolkit.translate_names"
|
||||||
|
bl_label: str = t("Translation.translate_names")
|
||||||
|
bl_description: str = t("Translation.translate_names_desc")
|
||||||
|
|
||||||
|
translation_type: bpy.props.EnumProperty(
|
||||||
|
items=[
|
||||||
|
('bones', t("Translation.type.bones"), t("Translation.type.bones_desc")),
|
||||||
|
('shapekeys', t("Translation.type.shapekeys"), t("Translation.type.shapekeys_desc")),
|
||||||
|
('materials', t("Translation.type.materials"), t("Translation.type.materials_desc")),
|
||||||
|
('objects', t("Translation.type.objects"), t("Translation.type.objects_desc")),
|
||||||
|
('all', t("Translation.type.all"), t("Translation.type.all_desc"))
|
||||||
|
],
|
||||||
|
default='bones'
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
logger.info(f"Starting translation operation: {self.translation_type}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ..core.translation_manager import get_avatar_translation_manager
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
|
||||||
|
# Set up progress callback for detailed feedback
|
||||||
|
def progress_callback(current: int, total: int, message: str):
|
||||||
|
progress_percent = (current / max(total, 1)) * 100
|
||||||
|
logger.info(f"Translation progress: {current}/{total} ({progress_percent:.1f}%) - {message}")
|
||||||
|
context.area.header_text_set(f"Translating: {current}/{total} - {message}")
|
||||||
|
|
||||||
|
manager.set_progress_callback(progress_callback)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
|
||||||
|
total_steps = 0
|
||||||
|
if self.translation_type == 'bones' or self.translation_type == 'all':
|
||||||
|
if armature:
|
||||||
|
total_steps += len(armature.data.bones)
|
||||||
|
if self.translation_type == 'shapekeys' or self.translation_type == 'all':
|
||||||
|
meshes = [obj for obj in context.scene.objects if obj.type == 'MESH']
|
||||||
|
for mesh in meshes:
|
||||||
|
if mesh.data.shape_keys:
|
||||||
|
total_steps += len(mesh.data.shape_keys.key_blocks)
|
||||||
|
if self.translation_type == 'materials' or self.translation_type == 'all':
|
||||||
|
materials = set()
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
if obj.type == 'MESH' and obj.data.materials:
|
||||||
|
for mat in obj.data.materials:
|
||||||
|
if mat:
|
||||||
|
materials.add(mat)
|
||||||
|
total_steps += len(materials)
|
||||||
|
if self.translation_type == 'objects' or self.translation_type == 'all':
|
||||||
|
objects = [obj for obj in context.scene.objects if obj.type in {'MESH', 'ARMATURE', 'EMPTY'}]
|
||||||
|
total_steps += len(objects)
|
||||||
|
|
||||||
|
logger.info(f"Translation operation will process approximately {total_steps} items")
|
||||||
|
|
||||||
|
with ProgressTracker(context, total_steps, "Translation") as progress:
|
||||||
|
if self.translation_type == 'bones' or self.translation_type == 'all':
|
||||||
|
if armature:
|
||||||
|
logger.info(f"Starting bone translation for armature: {armature.name}")
|
||||||
|
self.report({'INFO'}, f"Translating {len(armature.data.bones)} bones...")
|
||||||
|
|
||||||
|
bone_results = manager.translate_armature_bones(armature, apply_results=True)
|
||||||
|
results.extend(bone_results)
|
||||||
|
|
||||||
|
successful_bones = sum(1 for r in bone_results if r.method not in ['failed', 'skipped'])
|
||||||
|
progress.step(f"Bones: {successful_bones}/{len(bone_results)} translated")
|
||||||
|
logger.info(f"Bone translation complete: {successful_bones}/{len(bone_results)} successful")
|
||||||
|
else:
|
||||||
|
self.report({'WARNING'}, t("Translation.no_armature"))
|
||||||
|
logger.warning("No armature selected for bone translation")
|
||||||
|
|
||||||
|
if self.translation_type == 'shapekeys' or self.translation_type == 'all':
|
||||||
|
meshes = [obj for obj in context.scene.objects if obj.type == 'MESH']
|
||||||
|
logger.info(f"Starting shape key translation for {len(meshes)} mesh objects")
|
||||||
|
|
||||||
|
total_shapekeys = 0
|
||||||
|
for mesh in meshes:
|
||||||
|
if mesh.data.shape_keys:
|
||||||
|
shapekey_count = len(mesh.data.shape_keys.key_blocks)
|
||||||
|
self.report({'INFO'}, f"Translating {shapekey_count} shape keys in {mesh.name}...")
|
||||||
|
|
||||||
|
shapekey_results = manager.translate_object_shapekeys(mesh, apply_results=True)
|
||||||
|
results.extend(shapekey_results)
|
||||||
|
total_shapekeys += len(shapekey_results)
|
||||||
|
|
||||||
|
successful_shapekeys = sum(1 for r in results[-total_shapekeys:] if r.method not in ['failed', 'skipped'])
|
||||||
|
progress.step(f"Shape keys: {successful_shapekeys}/{total_shapekeys} translated")
|
||||||
|
logger.info(f"Shape key translation complete: {successful_shapekeys}/{total_shapekeys} successful")
|
||||||
|
|
||||||
|
if self.translation_type == 'materials' or self.translation_type == 'all':
|
||||||
|
logger.info("Starting material translation")
|
||||||
|
self.report({'INFO'}, "Translating materials...")
|
||||||
|
|
||||||
|
material_results = manager.translate_scene_materials(apply_results=True)
|
||||||
|
results.extend(material_results)
|
||||||
|
|
||||||
|
successful_materials = sum(1 for r in material_results if r.method not in ['failed', 'skipped'])
|
||||||
|
progress.step(f"Materials: {successful_materials}/{len(material_results)} translated")
|
||||||
|
logger.info(f"Material translation complete: {successful_materials}/{len(material_results)} successful")
|
||||||
|
|
||||||
|
if self.translation_type == 'objects' or self.translation_type == 'all':
|
||||||
|
logger.info("Starting object translation")
|
||||||
|
self.report({'INFO'}, "Translating objects...")
|
||||||
|
|
||||||
|
object_results = manager.translate_scene_objects(apply_results=True)
|
||||||
|
results.extend(object_results)
|
||||||
|
|
||||||
|
successful_objects = sum(1 for r in object_results if r.method not in ['failed', 'skipped'])
|
||||||
|
progress.step(f"Objects: {successful_objects}/{len(object_results)} translated")
|
||||||
|
logger.info(f"Object translation complete: {successful_objects}/{len(object_results)} successful")
|
||||||
|
|
||||||
|
manager.set_progress_callback(None)
|
||||||
|
context.area.header_text_set(None)
|
||||||
|
|
||||||
|
# Final results summary
|
||||||
|
successful = sum(1 for r in results if r.method not in ['failed', 'skipped'])
|
||||||
|
total = len(results)
|
||||||
|
|
||||||
|
dictionary_count = sum(1 for r in results if r.method == 'dictionary')
|
||||||
|
api_count = sum(1 for r in results if r.method == 'api')
|
||||||
|
cache_count = sum(1 for r in results if r.method == 'cache')
|
||||||
|
failed_count = sum(1 for r in results if r.method == 'failed')
|
||||||
|
|
||||||
|
logger.info(f"Translation summary: {successful}/{total} successful (Dictionary: {dictionary_count}, API: {api_count}, Cache: {cache_count}, Failed: {failed_count})")
|
||||||
|
|
||||||
|
if successful > 0:
|
||||||
|
success_msg = f"Successfully translated {successful}/{total} items"
|
||||||
|
if dictionary_count > 0:
|
||||||
|
success_msg += f" (Dictionary: {dictionary_count}"
|
||||||
|
if api_count > 0:
|
||||||
|
success_msg += f", API: {api_count}"
|
||||||
|
if cache_count > 0:
|
||||||
|
success_msg += f", Cache: {cache_count}"
|
||||||
|
if dictionary_count > 0 or api_count > 0 or cache_count > 0:
|
||||||
|
success_msg += ")"
|
||||||
|
|
||||||
|
self.report({'INFO'}, success_msg)
|
||||||
|
else:
|
||||||
|
if total > 0:
|
||||||
|
self.report({'WARNING'}, f"No translations were applied ({total} items checked)")
|
||||||
|
else:
|
||||||
|
self.report({'WARNING'}, "No items found to translate")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
try:
|
||||||
|
manager.set_progress_callback(None)
|
||||||
|
context.area.header_text_set(None)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.error(f"Translation operation failed: {e}", exc_info=True)
|
||||||
|
self.report({'ERROR'}, f"Translation failed: {str(e)}")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_TestTranslationService(Operator):
|
||||||
|
"""Test the currently selected translation service"""
|
||||||
|
bl_idname: str = "avatar_toolkit.test_translation_service"
|
||||||
|
bl_label: str = t("Translation.test_service")
|
||||||
|
bl_description: str = t("Translation.test_service_desc")
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
logger.info("Starting translation service test")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ..core.translation_manager import get_avatar_translation_manager
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
|
||||||
|
self.report({'INFO'}, "Testing translation service...")
|
||||||
|
context.area.header_text_set("Testing translation service...")
|
||||||
|
|
||||||
|
# Test translation with a simple word
|
||||||
|
test_word = "テスト" # "Test" in Japanese
|
||||||
|
logger.info(f"Testing translation of '{test_word}'")
|
||||||
|
|
||||||
|
result = manager.translate_single(test_word, "auto")
|
||||||
|
|
||||||
|
# Clear status
|
||||||
|
context.area.header_text_set(None)
|
||||||
|
|
||||||
|
if result.method == "failed":
|
||||||
|
logger.error(f"Translation test failed: {result}")
|
||||||
|
self.report({'ERROR'}, t("Translation.test_failed"))
|
||||||
|
else:
|
||||||
|
service_info = f" ({result.service})" if result.service else ""
|
||||||
|
success_msg = f"Translation test successful: '{test_word}' → '{result.translated}' via {result.method}{service_info}"
|
||||||
|
logger.info(f"Translation test successful: {result}")
|
||||||
|
self.report({'INFO'}, success_msg)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
try:
|
||||||
|
context.area.header_text_set(None)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.error(f"Translation service test failed: {e}", exc_info=True)
|
||||||
|
self.report({'ERROR'}, f"Service test failed: {str(e)}")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ClearTranslationCache(Operator):
|
||||||
|
"""Clear all translation caches"""
|
||||||
|
bl_idname: str = "avatar_toolkit.clear_translation_cache"
|
||||||
|
bl_label: str = t("Translation.clear_cache")
|
||||||
|
bl_description: str = t("Translation.clear_cache_desc")
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
try:
|
||||||
|
from ..core.translation_manager import get_avatar_translation_manager
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
manager.clear_all_caches()
|
||||||
|
|
||||||
|
self.report({'INFO'}, t("Translation.cache_cleared"))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to clear translation cache: {e}")
|
||||||
|
self.report({'ERROR'}, f"Failed to clear cache: {str(e)}")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ConfigureDeepL(Operator):
|
||||||
|
"""Configure DeepL API settings"""
|
||||||
|
bl_idname: str = "avatar_toolkit.configure_deepl"
|
||||||
|
bl_label: str = t("Translation.configure_deepl")
|
||||||
|
bl_description: str = t("Translation.configure_deepl_desc")
|
||||||
|
|
||||||
|
api_key: bpy.props.StringProperty(
|
||||||
|
name=t("Translation.deepl_api_key"),
|
||||||
|
description=t("Translation.deepl_api_key_desc"),
|
||||||
|
default="",
|
||||||
|
subtype='PASSWORD'
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
try:
|
||||||
|
if not self.api_key.strip():
|
||||||
|
self.report({'ERROR'}, "API key cannot be empty")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
from ..core.translation_manager import configure_translation_service
|
||||||
|
success = configure_translation_service("deepl", api_key=self.api_key.strip())
|
||||||
|
|
||||||
|
if success:
|
||||||
|
_ui_cache['deepl_config'].clear()
|
||||||
|
_ui_cache['translation_status'].clear()
|
||||||
|
if 'batch_info' in _ui_cache:
|
||||||
|
del _ui_cache['batch_info']
|
||||||
|
self.report({'INFO'}, "DeepL API configured successfully")
|
||||||
|
return {'FINISHED'}
|
||||||
|
else:
|
||||||
|
self.report({'ERROR'}, "Failed to configure DeepL API - check your API key")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"DeepL configuration failed: {e}")
|
||||||
|
self.report({'ERROR'}, f"Configuration failed: {str(e)}")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||||
|
# Load existing API key if available
|
||||||
|
try:
|
||||||
|
from ..core.addon_preferences import get_preference
|
||||||
|
existing_key = get_preference("deepl_api_key", "")
|
||||||
|
if existing_key:
|
||||||
|
# Show only first/last few characters for security
|
||||||
|
if len(existing_key) > 8:
|
||||||
|
display_key = existing_key[:4] + "..." + existing_key[-4:]
|
||||||
|
self.api_key = existing_key # Keep full key for editing
|
||||||
|
else:
|
||||||
|
self.api_key = existing_key
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
wm: WindowManager = context.window_manager
|
||||||
|
return wm.invoke_props_dialog(self, width=400)
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
|
info_box = layout.box()
|
||||||
|
info_col = info_box.column()
|
||||||
|
info_col.label(text="DeepL API Configuration", icon='SETTINGS')
|
||||||
|
info_col.separator()
|
||||||
|
info_col.label(text="1. Visit deepl.com/pro to get your free API key")
|
||||||
|
info_col.label(text="2. Free tier: 500,000 characters/month")
|
||||||
|
info_col.label(text="3. Higher quality than other services")
|
||||||
|
info_col.label(text="4. The Fastest Option due to native batching support")
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
layout.prop(self, "api_key")
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ConfigureLibreTranslate(Operator):
|
||||||
|
"""Configure LibreTranslate server settings"""
|
||||||
|
bl_idname: str = "avatar_toolkit.configure_libretranslate"
|
||||||
|
bl_label: str = t("Translation.configure_libretranslate")
|
||||||
|
bl_description: str = t("Translation.configure_libretranslate_desc")
|
||||||
|
|
||||||
|
server_url: bpy.props.StringProperty(
|
||||||
|
name=t("Translation.server_url"),
|
||||||
|
description=t("Translation.server_url_desc"),
|
||||||
|
default="https://libretranslate.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
api_key: bpy.props.StringProperty(
|
||||||
|
name=t("Translation.api_key"),
|
||||||
|
description=t("Translation.api_key_desc"),
|
||||||
|
default="",
|
||||||
|
subtype='PASSWORD'
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
try:
|
||||||
|
if not self.server_url.strip():
|
||||||
|
self.report({'ERROR'}, "Server URL cannot be empty")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
from ..core.translation_manager import configure_translation_service
|
||||||
|
success = configure_translation_service("libretranslate",
|
||||||
|
server_url=self.server_url.strip(),
|
||||||
|
api_key=self.api_key.strip() if self.api_key.strip() else None)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
_ui_cache['libretranslate_config'].clear()
|
||||||
|
_ui_cache['translation_status'].clear()
|
||||||
|
if 'batch_info' in _ui_cache:
|
||||||
|
del _ui_cache['batch_info']
|
||||||
|
self.report({'INFO'}, f"LibreTranslate server configured: {self.server_url}")
|
||||||
|
return {'FINISHED'}
|
||||||
|
else:
|
||||||
|
self.report({'ERROR'}, "Failed to connect to LibreTranslate server")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"LibreTranslate configuration failed: {e}")
|
||||||
|
self.report({'ERROR'}, f"Configuration failed: {str(e)}")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||||
|
# Load existing server URL and API key if available
|
||||||
|
try:
|
||||||
|
from ..core.addon_preferences import get_preference
|
||||||
|
existing_url = get_preference("libretranslate_url", "https://libretranslate.com")
|
||||||
|
existing_api_key = get_preference("libretranslate_api_key", "")
|
||||||
|
self.server_url = existing_url
|
||||||
|
self.api_key = existing_api_key
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
wm: WindowManager = context.window_manager
|
||||||
|
return wm.invoke_props_dialog(self, width=500)
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
|
info_box = layout.box()
|
||||||
|
info_col = info_box.column()
|
||||||
|
info_col.label(text="LibreTranslate Server Configuration", icon='SETTINGS')
|
||||||
|
info_col.separator()
|
||||||
|
info_col.label(text="⚠ libretranslate.com requires payment for API access")
|
||||||
|
info_col.label(text="✓ You can run your own LibreTranslate server")
|
||||||
|
info_col.label(text="✓ Or find community-hosted instances")
|
||||||
|
info_col.separator()
|
||||||
|
info_col.label(text="Examples:")
|
||||||
|
info_col.label(text=" • Your server: https://translate.yoursite.com")
|
||||||
|
info_col.label(text=" • Docker local: http://localhost:5000")
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
layout.prop(self, "server_url")
|
||||||
|
layout.prop(self, "api_key")
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_TranslationStats(Operator):
|
||||||
|
"""Show translation statistics"""
|
||||||
|
bl_idname: str = "avatar_toolkit.translation_stats"
|
||||||
|
bl_label: str = t("Translation.show_stats")
|
||||||
|
bl_description: str = t("Translation.show_stats_desc")
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||||
|
wm: WindowManager = context.window_manager
|
||||||
|
return wm.invoke_props_dialog(self, width=400)
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ..core.translation_manager import get_avatar_translation_manager
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
stats = manager.get_translation_stats()
|
||||||
|
|
||||||
|
dict_box = layout.box()
|
||||||
|
dict_box.label(text="Dictionary Translations", icon='BOOKMARKS')
|
||||||
|
dict_stats = stats['dictionary_translations']
|
||||||
|
for category, count in dict_stats.items():
|
||||||
|
if count > 0:
|
||||||
|
dict_box.label(text=f"{category.title()}: {count}")
|
||||||
|
|
||||||
|
cache_box = layout.box()
|
||||||
|
cache_box.label(text="Translation Cache", icon='FILE_CACHE')
|
||||||
|
cache_stats = stats['cache_stats']
|
||||||
|
cache_box.label(text=f"Language pairs: {cache_stats['language_pairs']}")
|
||||||
|
cache_box.label(text=f"Total cached: {cache_stats['total_entries']}")
|
||||||
|
|
||||||
|
service_box = layout.box()
|
||||||
|
service_box.label(text="Translation Services", icon='WORLD')
|
||||||
|
service_box.label(text=f"Current mode: {stats['current_mode']}")
|
||||||
|
service_box.label(text=f"Primary service: {stats['primary_service']}")
|
||||||
|
|
||||||
|
available_services = stats['available_services']
|
||||||
|
if available_services:
|
||||||
|
service_box.label(text="Available services:")
|
||||||
|
for service_id, service_name in available_services:
|
||||||
|
service_box.label(text=f" • {service_name}")
|
||||||
|
else:
|
||||||
|
service_box.label(text="No services available", icon='ERROR')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
layout.label(text=f"Error loading stats: {str(e)}", icon='ERROR')
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_TranslationPanel(Panel):
|
||||||
|
"""Translation panel for Avatar Toolkit"""
|
||||||
|
bl_label: str = t("Translation.label")
|
||||||
|
bl_idname: str = "OBJECT_PT_avatar_toolkit_translation"
|
||||||
|
bl_space_type: str = 'VIEW_3D'
|
||||||
|
bl_region_type: str = 'UI'
|
||||||
|
bl_category: str = CATEGORY_NAME
|
||||||
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
|
bl_order: int = get_panel_order('translation')
|
||||||
|
bl_options = set() if not should_open_by_default('TRANSLATION') else {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
"""Draw the translation panel layout"""
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
|
||||||
|
# Translation Service Settings
|
||||||
|
service_box: UILayout = layout.box()
|
||||||
|
col: UILayout = service_box.column(align=True)
|
||||||
|
row: UILayout = col.row()
|
||||||
|
row.scale_y = 1.2
|
||||||
|
row.label(text=t("Translation.service_settings"), icon='WORLD')
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
col.prop(props, "translation_service", text="")
|
||||||
|
|
||||||
|
col.prop(props, "translation_mode", text="")
|
||||||
|
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.prop(props, "translation_expand",
|
||||||
|
icon="TRIA_DOWN" if props.translation_expand else "TRIA_RIGHT",
|
||||||
|
icon_only=True, emboss=False)
|
||||||
|
row.label(text=t("Translation.advanced_settings"))
|
||||||
|
|
||||||
|
if props.translation_expand:
|
||||||
|
config_col = service_box.column(align=True)
|
||||||
|
|
||||||
|
# MyMemory settings (no configuration needed)
|
||||||
|
if props.translation_service == 'mymemory':
|
||||||
|
config_col.separator()
|
||||||
|
config_col.label(text="MyMemory Configuration:", icon='CHECKMARK')
|
||||||
|
success_col = config_col.column()
|
||||||
|
success_col.alert = False
|
||||||
|
success_col.label(text="✓ No API key required!", icon='CHECKMARK')
|
||||||
|
success_col.label(text="✓ Completely free service")
|
||||||
|
success_col.label(text="✓ 1000 translations per day")
|
||||||
|
success_col.label(text="✓ Slowest Option due to no native batching")
|
||||||
|
success_col.label(text="✓ Ready to use!")
|
||||||
|
|
||||||
|
elif props.translation_service == 'libretranslate':
|
||||||
|
config_col.separator()
|
||||||
|
config_col.label(text="LibreTranslate Configuration:", icon='SETTINGS')
|
||||||
|
|
||||||
|
# Check current server configuration (cached to avoid performance issues)
|
||||||
|
try:
|
||||||
|
if 'libretranslate_url' not in _ui_cache['libretranslate_config']:
|
||||||
|
from ..core.addon_preferences import get_preference
|
||||||
|
_ui_cache['libretranslate_config']['libretranslate_url'] = get_preference("libretranslate_url", "https://libretranslate.com")
|
||||||
|
|
||||||
|
server_url = _ui_cache['libretranslate_config']['libretranslate_url']
|
||||||
|
|
||||||
|
info_col = config_col.column()
|
||||||
|
info_col.alert = False
|
||||||
|
info_col.label(text=f"Server: {server_url}", icon='URL')
|
||||||
|
|
||||||
|
if "libretranslate.com" in server_url.lower():
|
||||||
|
warning_col = config_col.column()
|
||||||
|
warning_col.alert = True
|
||||||
|
warning_col.label(text="⚠ Default server requires payment", icon='ERROR')
|
||||||
|
warning_col.label(text="Configure your own LibreTranslate server")
|
||||||
|
else:
|
||||||
|
success_col = config_col.column()
|
||||||
|
success_col.alert = False
|
||||||
|
success_col.label(text="✓ Custom server configured", icon='CHECKMARK')
|
||||||
|
|
||||||
|
config_row = config_col.row()
|
||||||
|
config_row.operator("avatar_toolkit.configure_libretranslate", text="Configure Server", icon='SETTINGS')
|
||||||
|
except Exception as e:
|
||||||
|
config_col.label(text="LibreTranslate configuration error", icon='ERROR')
|
||||||
|
|
||||||
|
elif props.translation_service == 'deepl':
|
||||||
|
config_col.separator()
|
||||||
|
config_col.label(text="DeepL Configuration:", icon='SETTINGS')
|
||||||
|
|
||||||
|
# Check if API key is configured (cached to avoid performance issues)
|
||||||
|
try:
|
||||||
|
if 'deepl_api_key' not in _ui_cache['deepl_config']:
|
||||||
|
from ..core.addon_preferences import get_preference
|
||||||
|
_ui_cache['deepl_config']['deepl_api_key'] = get_preference("deepl_api_key", "")
|
||||||
|
|
||||||
|
deepl_api_key = _ui_cache['deepl_config']['deepl_api_key']
|
||||||
|
|
||||||
|
if deepl_api_key and deepl_api_key.strip():
|
||||||
|
success_col = config_col.column()
|
||||||
|
success_col.alert = False
|
||||||
|
success_col.label(text="✓ API key configured", icon='CHECKMARK')
|
||||||
|
success_col.label(text="✓ High quality translations")
|
||||||
|
success_col.label(text="✓ 500,000 chars/month free")
|
||||||
|
success_col.label(text="✓ Ready to use!")
|
||||||
|
|
||||||
|
reconfig_row = config_col.row()
|
||||||
|
reconfig_row.operator("avatar_toolkit.configure_deepl", text="Reconfigure API Key", icon='SETTINGS')
|
||||||
|
else:
|
||||||
|
warning_col = config_col.column()
|
||||||
|
warning_col.alert = True
|
||||||
|
warning_col.label(text="⚠ API key required!", icon='ERROR')
|
||||||
|
warning_col.label(text="Get free key at deepl.com/pro")
|
||||||
|
warning_col.label(text="500,000 characters/month free")
|
||||||
|
|
||||||
|
config_row = config_col.row()
|
||||||
|
config_row.operator("avatar_toolkit.configure_deepl", text="Configure API Key", icon='PLUS')
|
||||||
|
except Exception as e:
|
||||||
|
config_col.label(text="DeepL configuration error", icon='ERROR')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Language Settings
|
||||||
|
lang_box: UILayout = layout.box()
|
||||||
|
col = lang_box.column(align=True)
|
||||||
|
row = col.row()
|
||||||
|
row.scale_y = 1.2
|
||||||
|
row.label(text=t("Translation.language_settings"), icon='SYNTAX_ON')
|
||||||
|
col.separator()
|
||||||
|
col.prop(props, "translation_source_language", text="From")
|
||||||
|
col.prop(props, "translation_target_language", text="To")
|
||||||
|
|
||||||
|
# Quick Actions
|
||||||
|
action_box: UILayout = layout.box()
|
||||||
|
col = action_box.column(align=True)
|
||||||
|
row = col.row()
|
||||||
|
row.scale_y = 1.2
|
||||||
|
row.label(text=t("Translation.quick_actions"), icon='PLAY')
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
# Translate buttons
|
||||||
|
row = col.row(align=True)
|
||||||
|
op_bones = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Bones", icon='BONE_DATA')
|
||||||
|
op_bones.translation_type = 'bones'
|
||||||
|
|
||||||
|
op_shapes = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Shape Keys", icon='SHAPEKEY_DATA')
|
||||||
|
op_shapes.translation_type = 'shapekeys'
|
||||||
|
|
||||||
|
row = col.row(align=True)
|
||||||
|
op_mats = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Materials", icon='MATERIAL_DATA')
|
||||||
|
op_mats.translation_type = 'materials'
|
||||||
|
|
||||||
|
op_objs = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Objects", icon='OBJECT_DATA')
|
||||||
|
op_objs.translation_type = 'objects'
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
op_all = col.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Translate All", icon='WORLD')
|
||||||
|
op_all.translation_type = 'all'
|
||||||
|
|
||||||
|
# Utility buttons
|
||||||
|
util_box: UILayout = layout.box()
|
||||||
|
col = util_box.column(align=True)
|
||||||
|
row = col.row()
|
||||||
|
row.scale_y = 1.2
|
||||||
|
row.label(text=t("Translation.utilities"), icon='TOOL_SETTINGS')
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.operator(AvatarToolkit_OT_TestTranslationService.bl_idname, icon='PLAY')
|
||||||
|
row.operator(AvatarToolkit_OT_TranslationStats.bl_idname, icon='INFO')
|
||||||
|
|
||||||
|
col.operator(AvatarToolkit_OT_ClearTranslationCache.bl_idname, icon='TRASH')
|
||||||
|
|
||||||
|
status_box = layout.box()
|
||||||
|
status_col = status_box.column()
|
||||||
|
|
||||||
|
try:
|
||||||
|
status_cache_key = f"translation_status_{props.translation_service}_{props.translation_mode}"
|
||||||
|
|
||||||
|
# Refresh cache periodically
|
||||||
|
frame = context.scene.frame_current
|
||||||
|
cache_expired = (frame - _ui_cache['last_refresh_frame'] >= _ui_cache['cache_refresh_interval']) or status_cache_key not in _ui_cache['translation_status']
|
||||||
|
|
||||||
|
if cache_expired:
|
||||||
|
from ..core.translation_manager import get_available_translation_services, get_avatar_translation_manager
|
||||||
|
|
||||||
|
manager = get_avatar_translation_manager()
|
||||||
|
available_services = get_available_translation_services()
|
||||||
|
|
||||||
|
_ui_cache['translation_status'][status_cache_key] = {
|
||||||
|
'available_services': available_services,
|
||||||
|
'manager': manager,
|
||||||
|
'cache_stats': None
|
||||||
|
}
|
||||||
|
_ui_cache['last_refresh_frame'] = frame
|
||||||
|
|
||||||
|
try:
|
||||||
|
stats = manager.get_translation_stats()
|
||||||
|
_ui_cache['translation_status'][status_cache_key]['cache_stats'] = stats['cache_stats']
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Use cached data
|
||||||
|
cached_data = _ui_cache['translation_status'].get(status_cache_key, {})
|
||||||
|
available_services = cached_data.get('available_services', [])
|
||||||
|
cache_stats = cached_data.get('cache_stats')
|
||||||
|
|
||||||
|
if available_services:
|
||||||
|
status_col.label(text="Translation services ready", icon='CHECKMARK')
|
||||||
|
|
||||||
|
# Show current service status
|
||||||
|
current_service = props.translation_service
|
||||||
|
service_available = any(service_id == current_service for service_id, _ in available_services)
|
||||||
|
|
||||||
|
if service_available:
|
||||||
|
service_name = next((name for sid, name in available_services if sid == current_service), current_service)
|
||||||
|
status_col.label(text=f"Active: {service_name}", icon='WORLD')
|
||||||
|
|
||||||
|
# Show translation mode
|
||||||
|
mode_display = {
|
||||||
|
'hybrid': 'Dictionary + API',
|
||||||
|
'dictionary_only': 'Dictionary Only',
|
||||||
|
'api_only': 'API Only'
|
||||||
|
}.get(props.translation_mode, props.translation_mode)
|
||||||
|
status_col.label(text=f"Mode: {mode_display}", icon='SETTINGS')
|
||||||
|
|
||||||
|
# Show cache status
|
||||||
|
if cache_stats and cache_stats['total_entries'] > 0:
|
||||||
|
status_col.label(text=f"Cache: {cache_stats['total_entries']} translations", icon='FILE_CACHE')
|
||||||
|
|
||||||
|
# Show batch translation capability
|
||||||
|
try:
|
||||||
|
if 'batch_info' not in _ui_cache:
|
||||||
|
from ..core.translation_manager import get_batch_translation_info
|
||||||
|
_ui_cache['batch_info'] = get_batch_translation_info()
|
||||||
|
|
||||||
|
batch_info = _ui_cache['batch_info'].get(current_service, {})
|
||||||
|
if batch_info.get('supports_batch', False):
|
||||||
|
batch_type = batch_info.get('batch_type', 'individual')
|
||||||
|
if batch_type == 'native':
|
||||||
|
status_col.label(text="⚡ DeepL Native batch translation (up to 50x faster)", icon='LIGHT')
|
||||||
|
elif batch_type == 'concurrent':
|
||||||
|
if current_service == 'mymemory':
|
||||||
|
status_col.label(text="⚡ Slowest Option, no native Batching", icon='LIGHT')
|
||||||
|
else:
|
||||||
|
status_col.label(text="⚡ Slightly Faster then MyMemory processing (3x faster)", icon='LIGHT')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
warning_col = status_col.column()
|
||||||
|
warning_col.alert = True
|
||||||
|
warning_col.label(text=f"Service unavailable: {props.translation_service}", icon='ERROR')
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
warning_col = status_col.column()
|
||||||
|
warning_col.alert = True
|
||||||
|
warning_col.label(text="No translation services available", icon='ERROR')
|
||||||
|
|
||||||
|
if props.translation_service == 'mymemory':
|
||||||
|
warning_col.label(text="Internet connection required")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_col = status_col.column()
|
||||||
|
error_col.alert = True
|
||||||
|
error_col.label(text="Translation system error", icon='ERROR')
|
||||||
|
logger.error(f"Status display error: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(context.area, 'header_text') and context.area.header_text:
|
||||||
|
progress_col = status_col.column()
|
||||||
|
progress_col.alert = False
|
||||||
|
progress_col.label(text=context.area.header_text, icon='TIME')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
"""UI utilities and styling helpers for consistent Avatar Toolkit panel design"""
|
||||||
|
|
||||||
|
from typing import Callable, Optional
|
||||||
|
from bpy.types import UILayout, Context, Operator
|
||||||
|
|
||||||
|
|
||||||
|
class UIStyle:
|
||||||
|
"""Centralized UI styling constants for consistent appearance"""
|
||||||
|
|
||||||
|
SECTION_SEPARATOR_FACTOR: float = 0.5
|
||||||
|
SUBSECTION_SEPARATOR_FACTOR: float = 0.3
|
||||||
|
PRIMARY_BUTTON_SCALE: float = 1.5
|
||||||
|
STANDARD_BUTTON_SCALE: float = 1.0
|
||||||
|
COMPACT_BUTTON_SCALE: float = 0.9
|
||||||
|
DEFAULT_PADDING: float = 1.0
|
||||||
|
COMPACT_PADDING: float = 0.5
|
||||||
|
|
||||||
|
CATEGORY_ICONS = {
|
||||||
|
'optimization': 'MOD_SMOOTH',
|
||||||
|
'tools': 'TOOL_SETTINGS',
|
||||||
|
'custom': 'TOOL_OPTIONS',
|
||||||
|
'eye_tracking': 'OBJECT_CAMERA',
|
||||||
|
'settings': 'PREFERENCES',
|
||||||
|
'import_export': 'EXPORT',
|
||||||
|
'pose': 'POSE_HLT',
|
||||||
|
'materials': 'MATERIAL',
|
||||||
|
'mesh': 'MESH_DATA',
|
||||||
|
'bones': 'BONE_DATA',
|
||||||
|
'vfx': 'MOD_DISPLACE'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def draw_section_header(layout: UILayout, title: str, icon: str = 'NONE', separator: bool = True) -> UILayout:
|
||||||
|
"""Draw a consistent section header with optional icon and separator"""
|
||||||
|
header_box = layout.box()
|
||||||
|
col = header_box.column(align=True)
|
||||||
|
row = col.row()
|
||||||
|
row.scale_y = 1.2
|
||||||
|
row.label(text=title, icon=icon)
|
||||||
|
|
||||||
|
if separator:
|
||||||
|
col.separator(factor=UIStyle.SECTION_SEPARATOR_FACTOR)
|
||||||
|
|
||||||
|
return col
|
||||||
|
|
||||||
|
|
||||||
|
def draw_subsection(layout: UILayout, title: str, icon: str = 'NONE') -> UILayout:
|
||||||
|
"""Draw a subsection with reduced visual weight (no box)"""
|
||||||
|
col = layout.column(align=True)
|
||||||
|
row = col.row()
|
||||||
|
row.label(text=title, icon=icon)
|
||||||
|
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
|
||||||
|
return col
|
||||||
|
|
||||||
|
|
||||||
|
def draw_info_text(layout: UILayout, text: str, icon: str = 'INFO') -> None:
|
||||||
|
"""Draw informational text that can wrap (replaces multiple labels)"""
|
||||||
|
col = layout.column()
|
||||||
|
col.alert = False
|
||||||
|
# Split long text for wrapping
|
||||||
|
row = col.row()
|
||||||
|
row.label(text=text, icon=icon)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_warning_text(layout: UILayout, text: str) -> None:
|
||||||
|
"""Draw warning-styled text"""
|
||||||
|
col = layout.column()
|
||||||
|
col.alert = True
|
||||||
|
row = col.row()
|
||||||
|
row.label(text=text, icon='ERROR')
|
||||||
|
|
||||||
|
|
||||||
|
def draw_primary_button(layout: UILayout, operator_idname: str, text: str = "",
|
||||||
|
icon: str = 'NONE', **kwargs) -> None:
|
||||||
|
"""Draw a primary action button with standard scaling"""
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
|
||||||
|
row.operator(operator_idname, text=text, icon=icon, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_operator_row(layout: UILayout, operators: list[tuple[str, str, str]],
|
||||||
|
scale_y: float = 1.0, equal_width: bool = True) -> None:
|
||||||
|
"""Draw multiple operators in a single row with consistent sizing"""
|
||||||
|
if not operators:
|
||||||
|
return
|
||||||
|
|
||||||
|
row = layout.row(align=equal_width)
|
||||||
|
row.scale_y = scale_y
|
||||||
|
|
||||||
|
for op_id, text, icon in operators:
|
||||||
|
row.operator(op_id, text=text, icon=icon)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_collapsible_section(layout: UILayout, title: str, icon: str,
|
||||||
|
draw_func: Callable[[UILayout], None],
|
||||||
|
context: Context, storage_attr: str) -> None:
|
||||||
|
"""Draw a collapsible section (using context scene properties for state)"""
|
||||||
|
col = layout.column(align=True)
|
||||||
|
row = col.row()
|
||||||
|
|
||||||
|
scene = context.scene
|
||||||
|
attr_name = f"_ui_expand_{storage_attr}"
|
||||||
|
is_expanded = getattr(scene, attr_name, False)
|
||||||
|
icon_name = 'DISCLOSURE_TRI_DOWN' if is_expanded else 'DISCLOSURE_TRI_RIGHT'
|
||||||
|
row.prop(scene, attr_name, text="", icon=icon_name, emboss=False)
|
||||||
|
row.label(text=title, icon=icon)
|
||||||
|
|
||||||
|
if is_expanded:
|
||||||
|
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
|
||||||
|
draw_func(col)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_operator_disable_feedback(operator: Operator, layout: UILayout,
|
||||||
|
is_disabled: bool, reason: str = "") -> UILayout:
|
||||||
|
"""Prepare layout for disabled operator with visual feedback"""
|
||||||
|
if is_disabled:
|
||||||
|
layout.enabled = False
|
||||||
|
return layout
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_text_label(layout: UILayout, text: str, max_length: int = 50) -> None:
|
||||||
|
"""Draw a label that wraps long text across multiple lines"""
|
||||||
|
words = text.split()
|
||||||
|
current_line = ""
|
||||||
|
|
||||||
|
col = layout.column()
|
||||||
|
|
||||||
|
for word in words:
|
||||||
|
test_line = (current_line + " " + word).strip()
|
||||||
|
if len(test_line) > max_length and current_line:
|
||||||
|
col.label(text=current_line)
|
||||||
|
current_line = word
|
||||||
|
else:
|
||||||
|
current_line = test_line
|
||||||
|
|
||||||
|
if current_line:
|
||||||
|
col.label(text=current_line)
|
||||||
+6
-4
@@ -2,6 +2,7 @@ import bpy
|
|||||||
from bpy.types import Panel, Context, UILayout, Object, ShapeKey
|
from bpy.types import Panel, Context, UILayout, Object, ShapeKey
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from .panel_layout import get_panel_order, should_open_by_default
|
||||||
from ..core.common import get_active_armature
|
from ..core.common import get_active_armature
|
||||||
from ..functions.visemes import AvatarToolkit_OT_PreviewVisemes, AvatarToolkit_OT_CreateVisemes
|
from ..functions.visemes import AvatarToolkit_OT_PreviewVisemes, AvatarToolkit_OT_CreateVisemes
|
||||||
|
|
||||||
@@ -13,8 +14,8 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
|
|||||||
bl_region_type: str = 'UI'
|
bl_region_type: str = 'UI'
|
||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 5
|
bl_order: int = get_panel_order('visemes')
|
||||||
bl_options: set[str] = {'DEFAULT_CLOSED'}
|
bl_options: set[str] = set() if not should_open_by_default('VISEMES') else {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the visemes panel interface with shape key selection and preview controls"""
|
"""Draw the visemes panel interface with shape key selection and preview controls"""
|
||||||
@@ -33,8 +34,9 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
|
|||||||
else:
|
else:
|
||||||
col.label(text=t("Visemes.no_armature"), icon='ERROR')
|
col.label(text=t("Visemes.no_armature"), icon='ERROR')
|
||||||
|
|
||||||
# Get selected mesh
|
# Get selected mesh using safe identifier
|
||||||
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
from ..core.common import get_mesh_from_identifier
|
||||||
|
mesh_obj = get_mesh_from_identifier(props.viseme_mesh)
|
||||||
if not mesh_obj or not mesh_obj.data or not mesh_obj.data.shape_keys:
|
if not mesh_obj or not mesh_obj.data or not mesh_obj.data.shape_keys:
|
||||||
layout.label(text=t("Visemes.no_shapekeys"))
|
layout.label(text=t("Visemes.no_shapekeys"))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import Panel, Context, UILayout
|
||||||
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from .panel_layout import get_panel_order, should_open_by_default
|
||||||
|
from ..core.translations import t
|
||||||
|
from ..core.common import get_active_armature
|
||||||
|
from ..core.vrm_unity_converter import detect_vrm_armature
|
||||||
|
from ..functions.tools.vrm_unity_conversion import AvatarToolkit_OT_ConvertVRMToUnity
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_VRMUnityPanel(Panel):
|
||||||
|
"""Panel for VRM to Unity conversion tools"""
|
||||||
|
bl_label = t("VRM.panel.label")
|
||||||
|
bl_idname = "OBJECT_PT_avatar_toolkit_vrm_unity"
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = CATEGORY_NAME
|
||||||
|
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
|
bl_order = get_panel_order('vrm_unity')
|
||||||
|
bl_options = set() if not should_open_by_default('VRM_UNITY') else {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
"""Draw the VRM to Unity conversion panel interface"""
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
|
# VRM Conversion Tools
|
||||||
|
vrm_box: UILayout = layout.box()
|
||||||
|
col: UILayout = vrm_box.column(align=True)
|
||||||
|
col.label(text=t("VRM.converter.title"), icon='ARMATURE_DATA')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
# Check if we have an active armature
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
|
||||||
|
if not armature:
|
||||||
|
col.label(text=t("VRM.no_armature_selected"), icon='ERROR')
|
||||||
|
col.label(text=t("VRM.select_armature_to_convert"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the armature appears to be VRM
|
||||||
|
is_vrm = detect_vrm_armature(armature)
|
||||||
|
|
||||||
|
if is_vrm:
|
||||||
|
col.label(text=t("VRM.armature_name", name=armature.name), icon='CHECKMARK')
|
||||||
|
col.label(text=t("VRM.armature_detected"), icon='INFO')
|
||||||
|
col.separator(factor=0.3)
|
||||||
|
|
||||||
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
col.prop(toolkit, 'vrm_remove_colliders', text=t("VRM.remove_colliders"))
|
||||||
|
col.prop(toolkit, 'vrm_remove_root', text=t("VRM.remove_root_bone"))
|
||||||
|
col.separator(factor=0.2)
|
||||||
|
|
||||||
|
col.operator(
|
||||||
|
AvatarToolkit_OT_ConvertVRMToUnity.bl_idname,
|
||||||
|
text=t("VRM.convert_to_unity_format"),
|
||||||
|
icon='EXPORT'
|
||||||
|
)
|
||||||
|
|
||||||
|
info_box = vrm_box.box()
|
||||||
|
info_col = info_box.column(align=True)
|
||||||
|
info_col.label(text=t("VRM.conversion_info.title"), icon='INFO')
|
||||||
|
info_col.label(text=t("VRM.conversion_info.renames_bones"))
|
||||||
|
info_col.label(text=t("VRM.conversion_info.removes_colliders"))
|
||||||
|
info_col.label(text=t("VRM.conversion_info.removes_root"))
|
||||||
|
info_col.label(text=t("VRM.conversion_info.maintains_hierarchy"))
|
||||||
|
info_col.label(text=t("VRM.conversion_info.validates_results"))
|
||||||
|
info_col.label(text=t("VRM.conversion_info.preserves_animations"))
|
||||||
|
|
||||||
|
else:
|
||||||
|
col.label(text=t("VRM.armature_name", name=armature.name), icon='ERROR')
|
||||||
|
col.label(text=t("VRM.no_vrm_bones_detected"), icon='CANCEL')
|
||||||
|
col.separator(factor=0.3)
|
||||||
|
|
||||||
|
row = col.row()
|
||||||
|
row.enabled = False
|
||||||
|
row.operator(
|
||||||
|
AvatarToolkit_OT_ConvertVRMToUnity.bl_idname,
|
||||||
|
text=t("VRM.convert_to_unity_format"),
|
||||||
|
icon='CANCEL'
|
||||||
|
)
|
||||||
|
|
||||||
|
help_box = vrm_box.box()
|
||||||
|
help_col = help_box.column(align=True)
|
||||||
|
help_col.label(text=t("VRM.detection_failed.title"), icon='QUESTION')
|
||||||
|
help_col.label(text=t("VRM.detection_failed.not_vrm_format"))
|
||||||
|
help_col.label(text=t("VRM.detection_failed.bones_start_with"))
|
||||||
|
help_col.label(text=t("VRM.detection_failed.need_five_bones"))
|
||||||
|
help_col.label(text=t("VRM.detection_failed.check_bone_names"))
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user