Merge branch 'Current-Dev' into patch-1

This commit is contained in:
Onan Chew
2025-03-31 18:36:53 -04:00
committed by GitHub
49 changed files with 4763 additions and 2986 deletions
+10 -4
View File
@@ -6,9 +6,14 @@ from ..core.logging_setup import logger
from bpy.types import AddonPreferences
from typing import Any, Dict
# Get the directory of the current file
PREFERENCES_DIR = os.path.dirname(os.path.abspath(__file__))
PREFERENCES_FILE = os.path.join(PREFERENCES_DIR, "preferences.json")
# Get the user preferences directory instead of addon directory
def get_preferences_path():
user_path = bpy.utils.resource_path('USER')
addon_prefs_dir = os.path.join(user_path, "config", "avatar_toolkit_prefs")
os.makedirs(addon_prefs_dir, exist_ok=True)
return os.path.join(addon_prefs_dir, "preferences.json")
PREFERENCES_FILE = get_preferences_path()
def get_current_version():
main_dir = os.path.dirname(os.path.dirname(__file__))
@@ -59,4 +64,5 @@ def get_addon_preferences(context):
if not os.path.exists(PREFERENCES_FILE):
save_preference("language", 0) # Set default language to 0 (auto)
save_preference("validation_mode", "STRICT") # Set default validation mode
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
+566
View File
@@ -0,0 +1,566 @@
import bpy
import math
from mathutils import Vector, Color
from typing import Tuple, List, Dict, Set, Optional, Union
from bpy.types import Object, Bone, Operator
from ..core.common import get_armature_list, get_active_armature
from ..core.translations import t
from ..core.dictionaries import (
standard_bones,
bone_hierarchy,
finger_hierarchy,
acceptable_bone_hierarchy,
acceptable_bone_names
)
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]]]:
"""
Validates armature and returns validation results
"""
logger.debug(f"Validating armature: {armature.name if armature else 'None'}")
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
messages: List[str] = []
hierarchy_messages: List[str] = []
non_standard_messages: List[str] = []
scale_messages: List[str] = []
if validation_mode == 'NONE':
logger.debug("Validation mode is NONE, skipping validation")
if detailed_messages:
return True, [], False, [], [], []
else:
return True, [], False
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
logger.warning("Basic armature check failed")
if detailed_messages:
return False, [t("Armature.validation.basic_check_failed")], False, [], [], []
else:
return False, [t("Armature.validation.basic_check_failed")], False
found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones}
logger.debug(f"Found {len(found_bones)} bones in armature")
is_acceptable = check_acceptable_standards(found_bones)
# List all bones in armature
bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()])
messages.append(t("Armature.validation.found_bones", bones=bone_list))
# Basic validation for both STRICT and LIMITED modes
# Check for missing required bones
essential_bones = {standard_bones[key] for key in ['hips', 'spine', 'chest', 'neck', 'head']}
missing_bones = [bone for bone in essential_bones if bone not in found_bones]
if missing_bones:
missing_list = "\n".join([f"- {bone}" for bone in missing_bones])
logger.warning(f"Missing essential bones: {', '.join(missing_bones)}")
hierarchy_messages.append(t("Armature.validation.missing_bones", bones=missing_list))
if validation_mode == 'STRICT':
logger.debug("Performing strict validation")
# Add scale issue detection in STRICT mode
scale_issues = detect_scale_issues(found_bones)
if scale_issues:
logger.warning(f"Found {len(scale_issues)} scale issues")
# CHANGE: Don't combine into a single string, keep as separate items
scale_messages.extend(scale_issues)
# Validate bone hierarchy
for parent, child in bone_hierarchy:
if parent in found_bones and child in found_bones:
if not validate_bone_hierarchy(found_bones, parent, child):
logger.warning(f"Invalid hierarchy: {parent} -> {child}")
hierarchy_messages.append(t("Armature.validation.invalid_hierarchy",
parent=parent, child=child))
# Validate symmetry
logger.debug("Validating bone symmetry")
symmetry_pairs = [('arm', 'L', 'R'), ('leg', 'L', 'R')]
for base, left, right in symmetry_pairs:
if not validate_symmetry(found_bones, base, left, right):
logger.warning(f"Asymmetric bones found: {base}")
hierarchy_messages.append(t("Armature.validation.asymmetric_bones", bone=base))
if (not validate_symmetry(found_bones, 'hand', 'L', 'R') and
not validate_symmetry(found_bones, 'wrist', 'L', 'R')):
logger.warning("Asymmetric hand/wrist bones found")
hierarchy_messages.append(t("Armature.validation.asymmetric_hand_wrist"))
# Validate finger hierarchies
logger.debug("Validating finger hierarchies")
for side in ['left', 'right']:
for finger_chain in finger_hierarchy[side]:
if all(bone in found_bones for bone in finger_chain):
if not validate_finger_chain(found_bones, finger_chain):
logger.warning(f"Invalid finger hierarchy: {finger_chain[0]}")
hierarchy_messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0]))
# Non-standard bones check
non_standard_bones = []
required_patterns = [
'Hips', 'Spine', 'Chest', 'Neck', 'Head',
'Upper', 'Lower', 'Hand', 'Foot', 'Toe',
'Thumb', 'Index', 'Middle', 'Ring', 'Pinky',
'Eye'
]
for bone_name in found_bones:
if any(pattern in bone_name for pattern in required_patterns):
is_standard = bone_name in standard_bones.values()
is_acceptable_bone = any(bone_name in names for names in acceptable_bone_names.values())
if not (is_standard or is_acceptable_bone):
non_standard_bones.append(bone_name)
if non_standard_bones:
non_standard_list = "\n".join([f"- {bone}" for bone in non_standard_bones])
non_standard_messages.append(t("Armature.validation.non_standard_bones", bones=non_standard_list))
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line1"))
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line2"))
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line3"))
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line4"))
non_standard_messages.append("") # Add a blank line for spacing
non_standard_messages.append(t("Armature.validation.standardize_note.line1"))
non_standard_messages.append(t("Armature.validation.standardize_note.line2"))
non_standard_messages.append(t("Armature.validation.standardize_note.line3"))
# Combine messages in correct order
messages.extend(non_standard_messages)
is_valid = len(non_standard_messages) == 0 and len(hierarchy_messages) == 0 and len(scale_messages) == 0
if not is_valid and is_acceptable:
if non_standard_bones:
logger.info("Armature has non-standard bones but is acceptable")
if detailed_messages:
return False, messages, False, hierarchy_messages, scale_messages, non_standard_messages
else:
return False, messages, False
logger.info("Armature meets acceptable standards")
messages = [
t("Armature.validation.acceptable_standard.success"),
t("Armature.validation.acceptable_standard.note"),
t("Armature.validation.acceptable_standard.option")
]
if detailed_messages:
return True, messages, True, [], [], []
else:
return True, messages, True
logger.info(f"Armature validation complete. Valid: {is_valid}")
if detailed_messages:
return is_valid, messages, False, hierarchy_messages, scale_messages, non_standard_messages
else:
return is_valid, messages, False
def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool:
"""Validate if there is a valid parent-child relationship between bones"""
if parent_name not in bones or child_name not in bones:
return False
return bones[child_name].parent == bones[parent_name]
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"""
# Extract left and right bone names from both hierarchies
left_bone_names = set()
right_bone_names = set()
# Add standard bones
for key, value in standard_bones.items():
if base in key.lower():
if '_l' in key.lower():
left_bone_names.add(value)
elif '_r' in key.lower():
right_bone_names.add(value)
# Add acceptable bones
for key, names in acceptable_bone_names.items():
if base in key.lower():
if '_l' in key.lower():
left_bone_names.update(names)
elif '_r' in key.lower():
right_bone_names.update(names)
# Check if at least one pair exists and matches
left_exists = any(name in bones for name in left_bone_names)
right_exists = any(name in bones for name in right_bone_names)
return left_exists == right_exists
def validate_finger_chain(bones: Dict[str, Bone], chain: Tuple[str, ...]) -> bool:
"""Validate if a finger bone chain has correct hierarchy"""
for i in range(len(chain) - 1):
if not validate_bone_hierarchy(bones, chain[i], chain[i + 1]):
return False
return True
def check_acceptable_standards(bones: Dict[str, Bone]) -> bool:
"""Check if armature matches acceptable non-standard hierarchy"""
logger.debug("Checking for acceptable standards")
# Check if bones exist in acceptable list
for bone_category, acceptable_names in acceptable_bone_names.items():
found = False
for name in acceptable_names:
if name in bones:
found = True
break
if not found:
logger.debug(f"Missing acceptable bone for category: {bone_category}")
return False
# Validate acceptable hierarchy
for parent, child in acceptable_bone_hierarchy:
if parent in bones and child in bones:
if not validate_bone_hierarchy(bones, parent, child):
logger.debug(f"Invalid acceptable hierarchy: {parent} -> {child}")
return False
logger.debug("Armature meets acceptable standards")
return True
def validate_tpose(armature):
"""Validate if armature is in a proper T-pose"""
logger.debug(f"Validating T-pose for armature: {armature.name if armature else 'None'}")
if not armature or armature.type != 'ARMATURE':
logger.warning("No valid armature for T-pose validation")
return False, [t("Validation.tpose.no_armature")]
issues = []
if armature.mode == 'POSE':
bones_collection = armature.pose.bones
get_direction = lambda bone: bone.matrix.to_3x3().col[1].normalized()
else:
bones_collection = armature.data.bones
get_direction = lambda bone: bone.y_axis
# Get left and right upper arm bones using standard bone names
left_arm = None
right_arm = None
left_arm_candidates = [standard_bones['left_arm']] # UpperArm.L
if 'arm_l' in acceptable_bone_names:
left_arm_candidates.extend(acceptable_bone_names['arm_l'])
right_arm_candidates = [standard_bones['right_arm']] # UpperArm.R
if 'arm_r' in acceptable_bone_names:
right_arm_candidates.extend(acceptable_bone_names['arm_r'])
for name in left_arm_candidates:
if name in armature.data.bones:
left_arm = armature.data.bones[name]
logger.debug(f"Found left arm bone: {name}")
break
for name in right_arm_candidates:
if name in armature.data.bones:
right_arm = armature.data.bones[name]
logger.debug(f"Found right arm bone: {name}")
break
# Check arm bones are horizontal
if left_arm:
direction = left_arm.y_axis
if abs(direction.x) < 0.7: # Not pointing mostly along X axis
logger.warning("Left arm is not horizontal")
issues.append(t("Validation.tpose.left_arm_not_horizontal"))
if right_arm:
direction = right_arm.y_axis
if abs(direction.x) < 0.7: # Not pointing mostly along X axis
logger.warning("Right arm is not horizontal")
issues.append(t("Validation.tpose.right_arm_not_horizontal"))
spine = None
spine_candidates = [standard_bones['spine']] # Spine
if 'spine' in acceptable_bone_names:
spine_candidates.extend(acceptable_bone_names['spine'])
for name in spine_candidates:
if name in armature.data.bones:
spine = armature.data.bones[name]
logger.debug(f"Found spine bone: {name}")
break
if spine:
direction = spine.y_axis
if abs(direction.z) < 0.7: # Not pointing mostly along Z axis
logger.warning("Spine is not vertical")
issues.append(t("Validation.tpose.spine_not_vertical"))
if issues:
logger.warning(f"T-pose validation failed with {len(issues)} issues")
return False, issues
logger.info("T-pose validation successful")
return True, []
def detect_scale_issues(bones):
"""Detect bones with abnormal scale (too small or too large)"""
logger.debug("Detecting scale issues")
scale_issues = []
# Calculate median bone length for reference (more robust than average)
lengths = [bone.length for bone in bones.values()]
lengths.sort()
if not lengths:
logger.debug("No bones with length found")
return []
median_length = lengths[len(lengths) // 2]
# Filter out zero-length bones for standard deviation calculation
non_zero_lengths = [l for l in lengths if l > 0.0001]
if not non_zero_lengths:
logger.debug("No non-zero length bones found")
return []
mean = sum(non_zero_lengths) / len(non_zero_lengths)
variance = sum((l - mean) ** 2 for l in non_zero_lengths) / len(non_zero_lengths)
std_dev = math.sqrt(variance)
small_threshold = max(median_length * 0.05, mean - 3 * std_dev)
large_threshold = min(median_length * 15, mean + 5 * std_dev)
logger.debug(f"Scale thresholds - small: {small_threshold}, large: {large_threshold}")
# Get finger bones from standard and acceptable bone dictionaries
finger_bone_names = set()
for key in standard_bones:
if any(finger in key.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']):
finger_bone_names.add(standard_bones[key])
for key, names in acceptable_bone_names.items():
if any(finger in key.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']):
finger_bone_names.update(names)
for name, bone in bones.items():
is_finger = (name in finger_bone_names or
any(finger in name.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']))
if bone.length < small_threshold and not is_finger:
logger.debug(f"Bone {name} is too small: {bone.length}")
scale_issues.append(f"- {name}: {t('Validation.scale_issue.too_small')} ({bone.length:.4f})")
elif bone.length > large_threshold:
logger.debug(f"Bone {name} is too large: {bone.length}")
scale_issues.append(f"- {name}: {t('Validation.scale_issue.too_large')} ({bone.length:.4f})")
logger.debug(f"Found {len(scale_issues)} scale issues")
return scale_issues
def clear_bone_highlighting(armature: Object) -> None:
"""Clear bone highlighting by removing bone collections and resetting colors"""
logger.debug(f"Clearing bone highlighting for armature: {armature.name if armature else 'None'}")
if not armature or armature.type != 'ARMATURE':
logger.warning("No valid armature for clearing bone highlighting")
return
current_mode = bpy.context.mode
collection_name = "Problem Bones"
if collection_name in armature.data.collections:
problem_collection = armature.data.collections[collection_name]
armature.data.collections.remove(problem_collection)
logger.debug("Removed problem bones collection")
for bone in armature.data.bones:
bone.color.palette = 'DEFAULT'
if len(armature.data.collections) == 0:
armature.data.show_bone_colors = False
logger.debug("Disabled bone colors display")
logger.info("Bone highlighting cleared")
return
class AvatarToolkit_OT_HighlightProblemBones(Operator):
"""Highlight bones that fail validation in the 3D viewport"""
bl_idname = "avatar_toolkit.highlight_problem_bones"
bl_label = t("Validation.highlight_problem_bones")
bl_description = t("Validation.highlight_problem_bones_desc")
@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 highlighting problem bones")
self.report({'ERROR'}, t("Validation.no_armature"))
return {'CANCELLED'}
logger.info(f"Highlighting problem bones for armature: {armature.name}")
current_mode = context.mode
if current_mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
context.view_layer.objects.active = armature
# First remove all bone collections
collection_name = "Problem Bones"
if collection_name in armature.data.collections:
problem_collection = armature.data.collections[collection_name]
armature.data.collections.remove(problem_collection)
logger.debug("Removed existing problem bones collection")
is_valid, messages, _ = validate_armature(armature)
if is_valid:
logger.info("No validation issues found")
self.report({'INFO'}, t("Validation.no_issues"))
bpy.ops.object.mode_set(mode='EDIT')
return {'FINISHED'}
problem_collection = armature.data.collections.new(name="Problem Bones")
logger.debug("Created new problem bones collection")
armature.data.show_bone_colors = True
bpy.ops.object.mode_set(mode='EDIT')
# Extract bone names from validation messages
problem_bones = self._extract_problem_bones(messages)
# Assign bones to collection and set colors
highlighted_count = 0
for category, bone_names in problem_bones.items():
for bone_name in bone_names:
if bone_name in armature.data.edit_bones:
bone = armature.data.edit_bones[bone_name]
problem_collection.assign(bone)
if 'hierarchy' in category.lower():
bone.color.palette = 'THEME09' # Orange
elif 'scale' in category.lower():
bone.color.palette = 'THEME03' # Yellow
else:
bone.color.palette = 'THEME01' # Red
highlighted_count += 1
logger.info(f"Highlighted {highlighted_count} problem bones")
self.report({'INFO'}, t("Validation.highlighting_complete"))
return {'FINISHED'}
def _extract_problem_bones(self, messages):
problem_bones = {
"Hierarchy Issues": [],
"Scale Issues": [],
"Missing Bones": []
}
# Extract bone names from validation messages
for message in messages:
if isinstance(message, str):
# Parse message to extract bone names
for line in message.split('\n'):
if '- ' in line:
bone_name = line.split('- ')[1].strip()
if ':' in bone_name: # Handle "bone_name: message" format
bone_name = bone_name.split(':')[0].strip()
if 'hierarchy' in message.lower():
problem_bones["Hierarchy Issues"].append(bone_name)
elif 'scale' in message.lower():
problem_bones["Scale Issues"].append(bone_name)
else:
problem_bones["Missing Bones"].append(bone_name)
logger.debug(f"Extracted problem bones: {problem_bones}")
return problem_bones
class AvatarToolkit_OT_ValidateTPose(Operator):
"""Validate if armature is in a proper T-pose"""
bl_idname = "avatar_toolkit.validate_tpose"
bl_label = t("Validation.tpose.label")
bl_description = t("Validation.tpose.desc")
@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 T-pose validation")
self.report({'ERROR'}, t("Validation.no_armature"))
return {'CANCELLED'}
logger.info(f"Validating T-pose for armature: {armature.name}")
is_valid, messages = validate_tpose(armature)
props = context.scene.avatar_toolkit
props.tpose_validation_result = is_valid
props.tpose_validation_messages.clear()
for msg in messages:
item = props.tpose_validation_messages.add()
item.name = msg
props.show_tpose_validation = True
if is_valid:
logger.info("T-pose validation successful")
self.report({'INFO'}, t("Validation.tpose.valid"))
else:
for msg in messages:
self.report({'WARNING'}, msg)
logger.warning("T-pose validation failed")
self.report({'WARNING'}, t("Validation.tpose.warning"))
return {'FINISHED'}
class AvatarToolkit_OT_ClearBoneHighlighting(Operator):
"""Clear bone highlighting and reset bone colors"""
bl_idname = "avatar_toolkit.clear_bone_highlighting"
bl_label = t("Validation.clear_bone_highlighting")
bl_description = t("Validation.clear_bone_highlighting_desc")
@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 clearing bone highlighting")
self.report({'ERROR'}, t("Validation.no_armature"))
return {'CANCELLED'}
logger.info(f"Clearing bone highlighting for armature: {armature.name}")
current_mode = context.mode
# Switch to object mode as collection editing is not possible in edit mode
if current_mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
context.view_layer.objects.active = armature
collection_name = "Problem Bones"
if collection_name in armature.data.collections:
# Remove the collection
problem_collection = armature.data.collections[collection_name]
armature.data.collections.remove(problem_collection)
logger.debug("Removed problem bones collection")
bpy.ops.object.mode_set(mode='EDIT')
# Reset all bone colors
for bone in armature.data.edit_bones:
bone.color.palette = 'DEFAULT'
# Turn off bone colors display if no other collections are using it
if len(armature.data.collections) == 0:
armature.data.show_bone_colors = False
logger.debug("Disabled bone colors display")
bpy.ops.object.mode_set(mode='OBJECT')
logger.info("Bone highlighting cleared")
self.report({'INFO'}, t("Validation.highlighting_cleared"))
return {'FINISHED'}
+64 -67
View File
@@ -1,6 +1,5 @@
import os
import bpy
import sys
import typing
import inspect
import pkgutil
@@ -24,7 +23,6 @@ def init() -> None:
global modules
global ordered_classes
# Configure logging first
from .logging_setup import configure_logging
configure_logging(False)
@@ -32,14 +30,24 @@ def init() -> None:
configure_logging(get_preference("enable_logging", False))
print("Auto-load init starting")
modules = get_all_submodules(Path(__file__).parent.parent)
package_name = __package__.rsplit('.', 1)[0]
directory = Path(__file__).parent.parent
modules = get_all_submodules(directory, package_name)
ordered_classes = get_ordered_classes_to_register(modules)
print(f"Found modules: {modules}")
print(f"Found classes: {ordered_classes}")
def register() -> None:
"""Register all discovered classes and modules"""
global modules, ordered_classes
print("Registering classes")
if not ordered_classes:
print("Warning: No classes to register")
ordered_classes = []
for cls in ordered_classes:
print(f"Registering: {cls}")
try:
@@ -47,6 +55,10 @@ def register() -> None:
except ValueError:
continue
if not modules:
print("Warning: No modules to register")
modules = []
for module in modules:
if module.__name__ == __name__:
continue
@@ -67,44 +79,29 @@ def unregister() -> None:
if hasattr(module, "unregister"):
module.unregister()
def get_manifest_id() -> str:
"""Get the addon ID from the manifest file"""
manifest_path = Path(__file__).parent.parent / "blender_manifest.toml"
with open(manifest_path, "rb") as f:
manifest = tomllib.load(f)
return manifest["id"]
def get_all_submodules(directory: Path) -> List[Any]:
def get_all_submodules(directory: Path, package_name: str) -> List[Any]:
"""Discover and import all submodules in the given directory"""
modules = []
addon_id = get_manifest_id()
for root, dirs, files in os.walk(directory):
if "__pycache__" in root:
continue
path = Path(root)
if path == directory:
package_name = f"bl_ext.user_default.{addon_id}"
else:
relative_path = path.relative_to(directory).as_posix().replace('/', '.')
package_name = f"bl_ext.user_default.{addon_id}.{relative_path}"
for name in sorted(iter_module_names(path)):
modules.append(importlib.import_module(f".{name}", package_name))
return modules
return list(iter_submodules(directory, package_name))
def iter_submodules(path: Path, package_name: str) -> Generator[Any, None, None]:
def iter_submodules(directory: Path, package_name: str) -> Generator[Any, None, None]:
"""Iterate through submodules in a package"""
for name in sorted(iter_module_names(path)):
yield importlib.import_module("." + name, package_name)
for name in sorted(iter_submodule_names(directory)):
try:
yield importlib.import_module("." + name, package_name)
print(f"Successfully imported {name} from {package_name}")
except ImportError as e:
print(f"Error importing {name} from {package_name}: {e}")
def iter_module_names(path: Path) -> Generator[str, None, None]:
def iter_submodule_names(path: Path, root: str = "") -> Generator[str, None, None]:
"""Iterate through module names in a directory"""
print(f"Scanning path: {path}")
modules_list = list(pkgutil.iter_modules([str(path)]))
print(f"Found these modules: {modules_list}")
for _, module_name, is_pkg in modules_list:
if not is_pkg:
print(f"Found module: {module_name}")
yield module_name
for _, module_name, is_package in pkgutil.iter_modules([str(path)]):
if is_package:
sub_path = path / module_name
sub_root = root + module_name + "."
yield from iter_submodule_names(sub_path, sub_root)
else:
yield root + module_name
def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
"""Get a topologically sorted list of classes to register"""
@@ -112,28 +109,37 @@ def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
def get_register_deps_dict(modules: List[Any]) -> Dict[Type, Set[Type]]:
"""Get dependencies dictionary for class registration"""
my_classes = set(iter_classes_to_register(modules))
my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")}
deps_dict = {}
classes_to_register = set(iter_classes_to_register(modules))
for cls in classes_to_register:
deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register))
for cls in my_classes:
deps_dict[cls] = set()
deps_dict[cls].update(iter_deps_from_annotations(cls, my_classes))
deps_dict[cls].update(iter_deps_from_parent_id(cls, my_classes_by_idname))
return deps_dict
def iter_own_register_deps(cls: Type, classes_to_register: Set[Type]) -> Generator[Type, None, None]:
"""Iterate through a class's own registration dependencies"""
yield from (dep for dep in iter_register_deps(cls) if dep in classes_to_register)
def iter_register_deps(cls: Type) -> Generator[Type, None, None]:
"""Iterate through all registration dependencies of a class"""
def iter_deps_from_annotations(cls: Type, my_classes: Set[Type]) -> Generator[Type, None, None]:
"""Iterate through dependencies from class annotations"""
for value in typing.get_type_hints(cls, {}, {}).values():
dependency = get_dependency_from_annotation(value)
if dependency is not None:
if dependency is not None and dependency in my_classes:
yield dependency
def iter_deps_from_parent_id(cls: Type, my_classes_by_idname: Dict[str, Type]) -> Generator[Type, None, None]:
"""Iterate through dependencies from panel parent IDs"""
if bpy.types.Panel in cls.__bases__:
parent_idname = getattr(cls, "bl_parent_id", None)
if parent_idname is not None:
parent_cls = my_classes_by_idname.get(parent_idname)
if parent_cls is not None:
yield parent_cls
def get_dependency_from_annotation(value: Any) -> Optional[Type]:
"""Get dependency type from a type annotation"""
if isinstance(value, tuple) and len(value) == 2:
if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
return value[1]["type"]
if isinstance(value, bpy.props._PropertyDeferred):
return value.keywords.get("type")
return None
def iter_classes_to_register(modules: List[Any]) -> Generator[Type, None, None]:
@@ -164,7 +170,8 @@ def get_register_base_types() -> Set[Type]:
"Panel", "Operator", "PropertyGroup",
"AddonPreferences", "Header", "Menu",
"Node", "NodeSocket", "NodeTree",
"UIList", "RenderEngine"
"UIList", "RenderEngine",
"Gizmo", "GizmoGroup",
])
def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
@@ -172,25 +179,15 @@ def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
sorted_list = []
sorted_values = set()
panels_to_sort = [(value, deps) for value, deps in deps_dict.items()
if hasattr(value, 'bl_parent_id')]
base_panels = [(value, deps) for value, deps in deps_dict.items()
if not hasattr(value, 'bl_parent_id')]
for value, deps in base_panels:
if len(deps) == 0:
sorted_list.append(value)
sorted_values.add(value)
while len(deps_dict) > len(sorted_values):
while len(deps_dict) > 0:
unsorted = []
for value, deps in deps_dict.items():
if value not in sorted_values:
if len(deps - sorted_values) == 0:
sorted_list.append(value)
sorted_values.add(value)
else:
unsorted.append(value)
if len(deps) == 0:
sorted_list.append(value)
sorted_values.add(value)
else:
unsorted.append(value)
deps_dict = {value: deps_dict[value] - sorted_values for value in unsorted}
return sorted_list
+89 -115
View File
@@ -10,7 +10,7 @@ import numpy.typing as npt
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type
from mathutils import Vector, Matrix
from bpy.types import (Context, Object, Modifier, EditBone, Operator,
from bpy.types import (Context, Object, Modifier, EditBone, Operator, Material,
VertexGroup, ShapeKey, Bone, Mesh, Armature, PropertyGroup)
from functools import lru_cache
from bpy.props import PointerProperty, IntProperty, StringProperty
@@ -20,6 +20,47 @@ from ..core.translations import t
from ..core.dictionaries import bone_names
from .dictionaries import reverse_bone_lookup, bone_names
class SceneMatClass(PropertyGroup):
mat: PointerProperty(type=Material)
register_class(SceneMatClass)
class MaterialListBool:
#For the love that is holy do not ever touch these. If this was java I would make these private
#They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown
#This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up.
#The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading.
old_list: dict[str,list[Material]] = {}
bool_material_list_expand: dict[str,bool] = {}
def set_bool(self, value: bool) -> None:
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = value
if value == False:
MaterialListBool.old_list[bpy.context.scene.name] = []
def get_bool(self) -> bool:
newlist: list[Material] = []
for obj in bpy.context.scene.objects:
if len(obj.material_slots)>0:
for mat_slot in obj.material_slots:
if mat_slot.material:
if mat_slot.material not in newlist:
newlist.append(mat_slot.material)
still_the_same: bool = True
if bpy.context.scene.name in MaterialListBool.old_list:
for item in newlist:
if item not in MaterialListBool.old_list[bpy.context.scene.name]:
still_the_same = False
break
for item in MaterialListBool.old_list[bpy.context.scene.name]:
if item not in newlist:
still_the_same = False
break
else:
still_the_same = False
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
class ProgressTracker:
"""Universal progress tracking for Avatar Toolkit operations"""
@@ -67,89 +108,6 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N
if not armatures:
return [('NONE', t("Armature.validation.no_armature"), '')]
return armatures
def validate_armature(armature: Object) -> Tuple[bool, List[str]]:
"""Enhanced armature validation with multiple validation modes"""
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
messages: List[str] = []
if validation_mode == 'NONE':
return True, []
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
return False, [t("Armature.validation.basic_check_failed")]
found_bones: Dict[str, Bone] = {bone.name.lower(): bone for bone in armature.data.bones}
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
missing_bones: List[str] = []
for bone in essential_bones:
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
missing_bones.append(bone)
if missing_bones:
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
if validation_mode == 'STRICT':
hierarchy: List[Tuple[str, str]] = [
('hips', 'spine'), ('spine', 'chest'),
('chest', 'neck'), ('neck', 'head')
]
for parent, child in hierarchy:
if not validate_bone_hierarchy(found_bones, parent, child):
messages.append(t("Armature.validation.invalid_hierarchy",
parent=parent, child=child))
symmetry_pairs: List[Tuple[str, str, str]] = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
for base, left, right in symmetry_pairs:
if not validate_symmetry(found_bones, base, left, right):
messages.append(t("Armature.validation.asymmetric_bones", bone=base))
if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
messages.append(t("Armature.validation.asymmetric_hand_wrist"))
is_valid: bool = len(messages) == 0
return is_valid, messages
def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool:
"""Validate if there is a valid parent-child relationship between bones"""
parent_bone: Optional[Bone] = None
child_bone: Optional[Bone] = None
for alt_name in bone_names[parent_name]:
if alt_name in bones:
parent_bone = bones[alt_name]
break
for alt_name in bone_names[child_name]:
if alt_name in bones:
child_bone = bones[alt_name]
break
if not parent_bone or not child_bone:
return False
return child_bone.parent == parent_bone
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"""
left_patterns: List[str] = [
f"{base}.{left}",
f"{base}_{left}",
f"{left}_{base}"
]
right_patterns: List[str] = [
f"{base}.{right}",
f"{base}_{right}",
f"{right}_{base}"
]
left_exists: bool = any(pattern in bones for pattern in left_patterns)
right_exists: bool = any(pattern in bones for pattern in right_patterns)
return left_exists and right_exists
def auto_select_single_armature(context: Context) -> None:
"""Automatically select armature if only one exists in scene"""
@@ -321,51 +279,67 @@ def validate_meshes(meshes: List[Object]) -> Tuple[bool, str]:
return False, t("Optimization.non_mesh_objects")
return True, ""
def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]:
"""Combines multiple mesh objects into a single mesh with proper cleanup and UV fixing"""
try:
# Store UV maps before joining
uv_maps_data = {}
for mesh in meshes:
uv_maps_data[mesh.name] = {uv.name: uv.data.copy() for uv in mesh.data.uv_layers}
def fast_uv_fix(obj: Object) -> None:
"""Fast UV coordinate fixing for joined meshes"""
if not obj or not obj.data or not obj.data.uv_layers:
return
current_mode = bpy.context.mode
if current_mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
# Process all UV layers at once
bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.pack_islands(margin=0.001)
if current_mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode=current_mode)
def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]:
"""Combines multiple mesh objects into a single mesh with optimized performance"""
try:
if not meshes:
return None
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
for mesh in meshes:
# Create a list of valid meshes
valid_meshes = [mesh for mesh in meshes if mesh.name in bpy.data.objects]
if not valid_meshes:
return None
for mesh in valid_meshes:
mesh.select_set(True)
if context.selected_objects:
context.view_layer.objects.active = context.selected_objects[0]
context.view_layer.objects.active = valid_meshes[0]
if progress:
progress.step(t("Optimization.joining_meshes"))
if progress:
progress.step(t("Optimization.joining_meshes"))
bpy.ops.object.join()
bpy.ops.object.join()
joined_mesh = context.active_object
if progress:
progress.step(t("Optimization.applying_transforms"))
if progress:
progress.step(t("Optimization.applying_transforms"))
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
if progress:
progress.step(t("Optimization.fixing_uvs"))
if progress:
progress.step(t("Optimization.fixing_uvs"))
fix_uv_coordinates(context)
# Restore UV maps after joining
joined_mesh = context.active_object
for uv_name, uv_data in uv_maps_data.items():
for map_name, map_data in uv_data.items():
if map_name not in joined_mesh.data.uv_layers:
joined_mesh.data.uv_layers.new(name=map_name)
joined_mesh.data.uv_layers[map_name].data.foreach_set("uv", map_data)
return context.active_object
return None
fast_uv_fix(joined_mesh)
return joined_mesh
except Exception as e:
logger.error(f"Failed to join meshes: {str(e)}")
return None
def fix_uv_coordinates(context: Context) -> None:
"""Normalizes and fixes UV coordinates for the active mesh object"""
obj: Object = context.object
+586 -2
View File
@@ -354,8 +354,6 @@ resonite_translations = {
'thumb_2_r': "thumb2.R",
'thumb_3_r': "thumb3.R"
}
# Create reverse lookup dictionary (conversion/translation)
reverse_bone_lookup = {}
for preferred_name, name_list in bone_names.items():
@@ -364,3 +362,589 @@ for preferred_name, name_list in bone_names.items():
standard_bones = {
# Core Structure
'hips': 'Hips',
'spine': 'Spine',
'chest': 'Chest',
'upper_chest': 'Chest.Up',
'neck': 'Neck',
'head': 'Head',
# Arms
'left_arm': 'UpperArm.L',
'left_elbow': 'LowerArm.L',
'left_wrist': 'Hand.L',
'right_arm': 'UpperArm.R',
'right_elbow': 'LowerArm.R',
'right_wrist': 'Hand.R',
# Legs
'left_leg': 'UpperLeg.L',
'left_knee': 'LowerLeg.L',
'left_ankle': 'Foot.L',
'left_toe': 'Toes.L',
'right_leg': 'UpperLeg.R',
'right_knee': 'LowerLeg.R',
'right_ankle': 'Foot.R',
'right_toe': 'Toes.R',
# Fingers Left
'thumb_1_l': 'Thumb1.L',
'thumb_2_l': 'Thumb2.L',
'thumb_3_l': 'Thumb3.L',
'index_1_l': 'Index1.L',
'index_2_l': 'Index2.L',
'index_3_l': 'Index3.L',
'middle_1_l': 'Middle1.L',
'middle_2_l': 'Middle2.L',
'middle_3_l': 'Middle3.L',
'ring_1_l': 'Ring1.L',
'ring_2_l': 'Ring2.L',
'ring_3_l': 'Ring3.L',
'pinkie_1_l': 'Pinky1.L',
'pinkie_2_l': 'Pinky2.L',
'pinkie_3_l': 'Pinky3.L',
# Fingers Right
'thumb_1_r': 'Thumb1.R',
'thumb_2_r': 'Thumb2.R',
'thumb_3_r': 'Thumb3.R',
'index_1_r': 'Index1.R',
'index_2_r': 'Index2.R',
'index_3_r': 'Index3.R',
'middle_1_r': 'Middle1.R',
'middle_2_r': 'Middle2.R',
'middle_3_r': 'Middle3.R',
'ring_1_r': 'Ring1.R',
'ring_2_r': 'Ring2.R',
'ring_3_r': 'Ring3.R',
'pinkie_1_r': 'Pinky1.R',
'pinkie_2_r': 'Pinky2.R',
'pinkie_3_r': 'Pinky3.R',
# Eyes
'left_eye': 'Eye.L',
'right_eye': 'Eye.R'
}
bone_hierarchy = [
('Hips', 'Spine'),
('Spine', 'Chest'),
('Chest', 'Chest.Up'),
('Chest.Up', 'Neck'),
('Neck', 'Head'),
('Head', 'Eye.L'),
('Head', 'Eye.R'),
# Left Arm Chain
('Chest.Up', 'UpperArm.L'),
('UpperArm.L', 'LowerArm.L'),
('LowerArm.L', 'Hand.L'),
# Right Arm Chain
('Chest.Up', 'UpperArm.R'),
('UpperArm.R', 'LowerArm.R'),
('LowerArm.R', 'Hand.R'),
# Left Leg Chain
('Hips', 'UpperLeg.L'),
('UpperLeg.L', 'LowerLeg.L'),
('LowerLeg.L', 'Foot.L'),
('Foot.L', 'Toes.L'),
# Right Leg Chain
('Hips', 'UpperLeg.R'),
('UpperLeg.R', 'LowerLeg.R'),
('LowerLeg.R', 'Foot.R'),
('Foot.R', 'Toes.R')
]
finger_hierarchy = {
'left': [
('Hand.L', 'Thumb1.L', 'Thumb2.L', 'Thumb3.L'),
('Hand.L', 'Index1.L', 'Index2.L', 'Index3.L'),
('Hand.L', 'Middle1.L', 'Middle2.L', 'Middle3.L'),
('Hand.L', 'Ring1.L', 'Ring2.L', 'Ring3.L'),
('Hand.L', 'Pinky1.L', 'Pinky2.L', 'Pinky3.L')
],
'right': [
('Hand.R', 'Thumb1.R', 'Thumb2.R', 'Thumb3.R'),
('Hand.R', 'Index1.R', 'Index2.R', 'Index3.R'),
('Hand.R', 'Middle1.R', 'Middle2.R', 'Middle3.R'),
('Hand.R', 'Ring1.R', 'Ring2.R', 'Ring3.R'),
('Hand.R', 'Pinky1.R', 'Pinky2.R', 'Pinky3.R')
]
}
acceptable_bone_hierarchy = [
# Right side chain
('Hips', 'Chest'),
('Chest', 'Shoulder.R'),
('Shoulder.R', 'Arm.R'),
('Arm.R', 'Elbow.R'),
('Elbow.R', 'Wrist.R'),
('Hips', 'Leg.R'),
('Leg.R', 'Knee.R'),
('Knee.R', 'Foot.R'),
('Foot.R', 'Toes.R'),
# Left side chain
('Chest', 'Shoulder.L'),
('Shoulder.L', 'Arm.L'),
('Arm.L', 'Elbow.L'),
('Elbow.L', 'Wrist.L'),
('Hips', 'Leg.L'),
('Leg.L', 'Knee.L'),
('Knee.L', 'Foot.L'),
('Foot.L', 'Toes.L'),
# Head and Eyes
('Chest', 'Neck'),
('Neck', 'Head'),
('Head', 'Eye_L'),
('Head', 'Eye_R'),
('Head', 'LeftEye'),
('Head', 'RightEye'),
# Unity humanoid naming
('Hips', 'Spine'),
('Spine', 'Chest'),
('Chest', 'UpperChest'),
('UpperChest', 'Neck'),
('Neck', 'Head'),
('Head', 'LeftEye'),
('Head', 'RightEye'),
]
acceptable_bone_names = {
'hips': ['Hips', 'pelvis', 'root', 'Root', 'ROOT'],
'chest': ['Chest', 'spine1', 'Spine1', 'spine_01', 'SPINE1', 'Spine01'],
'neck': ['Neck', 'neck_01', 'Neck01'],
'head': ['Head', 'head_01', 'Head01'],
'eye_l': ['Eye_L', 'LeftEye', 'lefteye', 'eye_left', 'EyeLeft'],
'eye_r': ['Eye_R', 'RightEye', 'righteye', 'eye_right', 'EyeRight'],
'shoulder_r': ['Shoulder.R', 'clavicle_r', 'ClavicleRight', 'RightShoulder'],
'arm_r': ['Arm.R', 'upperarm_r', 'UpperArmRight', 'RightArm'],
'elbow_r': ['Elbow.R', 'lowerarm_r', 'ForearmRight', 'RightForeArm'],
'wrist_r': ['Wrist.R', 'hand_r', 'HandRight', 'RightHand'],
'leg_r': ['Leg.R', 'thigh_r', 'ThighRight', 'RightLeg', 'RightUpLeg'],
'knee_r': ['Knee.R', 'calf_r', 'CalfRight', 'RightShin', 'RightLowerLeg'],
'foot_r': ['Foot.R', 'foot_r', 'FootRight', 'RightFoot'],
'toes_r': ['Toes.R', 'ball_r', 'ToeRight', 'RightToeBase'],
'shoulder_l': ['Shoulder.L', 'clavicle_l', 'ClavicleLeft', 'LeftShoulder'],
'arm_l': ['Arm.L', 'upperarm_l', 'UpperArmLeft', 'LeftArm'],
'elbow_l': ['Elbow.L', 'lowerarm_l', 'ForearmLeft', 'LeftForeArm'],
'wrist_l': ['Wrist.L', 'hand_l', 'HandLeft', 'LeftHand'],
'leg_l': ['Leg.L', 'thigh_l', 'ThighLeft', 'LeftLeg', 'LeftUpLeg'],
'knee_l': ['Knee.L', 'calf_l', 'CalfLeft', 'LeftShin', 'LeftLowerLeg'],
'foot_l': ['Foot.L', 'foot_l', 'FootLeft', 'LeftFoot'],
'toes_l': ['Toes.L', 'ball_l', 'ToeLeft', 'LeftToeBase'],
# Add finger bones for left hand
'thumb_0_l': ['Thumb0_L'],
'thumb_1_l': ['Thumb1_L'],
'thumb_2_l': ['Thumb2_L'],
'index_1_l': ['IndexFinger1_L'],
'index_2_l': ['IndexFinger2_L'],
'index_3_l': ['IndexFinger3_L'],
'middle_1_l': ['MiddleFinger1_L'],
'middle_2_l': ['MiddleFinger2_L'],
'middle_3_l': ['MiddleFinger3_L'],
'ring_1_l': ['RingFinger1_L'],
'ring_2_l': ['RingFinger2_L'],
'ring_3_l': ['RingFinger3_L'],
# Add finger bones for right hand
'thumb_0_r': ['Thumb0_R', 'ThumbO_R'],
'thumb_1_r': ['Thumb1_R'],
'thumb_2_r': ['Thumb2_R'],
'index_1_r': ['IndexFinger1_R'],
'index_2_r': ['IndexFinger2_R'],
'index_3_r': ['IndexFinger3_R'],
'middle_1_r': ['MiddleFinger1_R'],
'middle_2_r': ['MiddleFinger2_R'],
'middle_3_r': ['MiddleFinger3_R'],
'ring_1_r': ['RingFinger1_R'],
'ring_2_r': ['RingFinger2_R'],
'ring_3_r': ['RingFinger3_R'],
'breast_upper_1_l': ['BreastUpper1_L'],
'breast_upper_2_l': ['BreastUpper2_L'],
'breast_upper_1_r': ['BreastUpper1_R'],
'breast_upper_2_r': ['BreastUpper2_R'],
'ear_upper_l': ['UpperEar.L', 'Upper Ear.L', 'Upper Ear_L'],
'ear_upper_r': ['UpperEar.R', 'Upper Ear.R', 'Upper Ear_R'],
'ear_lower_l': ['LowerEar.L', 'Lower Ear.L', 'Lower Ear_L'],
'ear_lower_r': ['LowerEar.R', 'Lower Ear.R', 'Lower Ear_R'],
'ears_upper': ['Ears Upper', 'EarsUpper', 'ears_upper'],
'ears_lower': ['Ears Lower', 'EarsLower', 'ears_lower']
}
rigify_unity_names = {
"DEF-spine": "Hips",
"DEF-spine.001": "Spine",
"DEF-spine.002": "Chest",
"DEF-spine.003": "UpperChest",
"DEF-neck": "Neck",
"DEF-head": "Head",
"DEF-shoulder.L": "LeftShoulder",
"DEF-upper_arm.L": "LeftUpperArm",
"DEF-forearm.L": "LeftLowerArm",
"DEF-hand.L": "LeftHand",
"DEF-shoulder.R": "RightShoulder",
"DEF-upper_arm.R": "RightUpperArm",
"DEF-forearm.R": "RightLowerArm",
"DEF-hand.R": "RightHand",
"DEF-thigh.L": "LeftUpperLeg",
"DEF-shin.L": "LeftLowerLeg",
"DEF-foot.L": "LeftFoot",
"DEF-toe.L": "LeftToes",
"DEF-thigh.R": "RightUpperLeg",
"DEF-shin.R": "RightLowerLeg",
"DEF-foot.R": "RightFoot",
"DEF-toe.R": "RightToes"
}
rigify_basic_unity_names = {
"spine": "Hips",
"spine.001": "Spine",
"spine.002": "Chest",
"spine.003": "UpperChest",
"neck": "Neck",
"head": "Head",
"shoulder.L": "LeftShoulder",
"upper_arm.L": "LeftUpperArm",
"forearm.L": "LeftLowerArm",
"hand.L": "LeftHand",
"shoulder.R": "RightShoulder",
"upper_arm.R": "RightUpperArm",
"forearm.R": "RightLowerArm",
"hand.R": "RightHand",
"thigh.L": "LeftUpperLeg",
"shin.L": "LeftLowerLeg",
"foot.L": "LeftFoot",
"toe.L": "LeftToes",
"thigh.R": "RightUpperLeg",
"shin.R": "RightLowerLeg",
"foot.R": "RightFoot",
"toe.R": "RightToes"
}
rigify_unnecessary_bones = [
'face',
'ear.l', 'ear.r',
'forehead',
'cheek.t.l', 'cheek.t.r',
'cheek.b.l', 'cheek.b.r',
'brow.t.l', 'brow.t.r',
'brow.b.l', 'brow.b.r',
'jaw',
'chin',
'nose',
'temple.l', 'temple.r',
'teeth',
'lip',
'lid',
'heel',
'pelvis.'
]
# Non-standard bone mappings to standard bones
non_standard_mappings = {
'hips': [
'mixamorig:Hips', 'mixamorig_Hips',
'ORG-spine', 'spine', 'root',
'hip', 'pelvis'
],
'spine': [
'mixamorig:Spine', 'mixamorig_Spine',
'ORG-spine.001', 'spine.001',
'abdomenLower', 'lowerback'
],
'chest': [
'mixamorig:Spine1', 'mixamorig_Spine1',
'ORG-spine.002', 'spine.002',
'abdomenUpper', 'upperback', 'spine1'
],
'upper_chest': [
'mixamorig:Spine2', 'mixamorig_Spine2',
'ORG-spine.003', 'spine.003',
'chestLower', 'chest', 'spine2'
],
'neck': [
'mixamorig:Neck', 'mixamorig_Neck',
'ORG-spine.004', 'spine.004', 'neck',
'neckLower'
],
'head': [
'mixamorig:Head', 'mixamorig_Head',
'ORG-spine.005', 'spine.005', 'face', 'head'
],
'left_shoulder': [
'mixamorig:LeftShoulder', 'mixamorig_LeftShoulder',
'ORG-shoulder.L', 'shoulder.L',
'lCollar', 'lShldr', 'lClavicle'
],
'left_arm': [
'mixamorig:LeftArm', 'mixamorig_LeftArm',
'ORG-upper_arm.L', 'upper_arm.L',
'lShldrBend', 'lShldrTwist', 'lArm'
],
'left_elbow': [
'mixamorig:LeftForeArm', 'mixamorig_LeftForeArm',
'ORG-forearm.L', 'forearm.L',
'lForearmBend', 'lElbow', 'lForeArm'
],
'left_wrist': [
'mixamorig:LeftHand', 'mixamorig_LeftHand',
'ORG-hand.L', 'hand.L',
'lHand', 'lWrist'
],
'right_shoulder': [
'mixamorig:RightShoulder', 'mixamorig_RightShoulder',
'ORG-shoulder.R', 'shoulder.R',
'rCollar', 'rShldr', 'rClavicle'
],
'right_arm': [
'mixamorig:RightArm', 'mixamorig_RightArm',
'ORG-upper_arm.R', 'upper_arm.R',
'rShldrBend', 'rShldrTwist', 'rArm'
],
'right_elbow': [
'mixamorig:RightForeArm', 'mixamorig_RightForeArm',
'ORG-forearm.R', 'forearm.R',
'rForearmBend', 'rElbow', 'rForeArm'
],
'right_wrist': [
'mixamorig:RightHand', 'mixamorig_RightHand',
'ORG-hand.R', 'hand.R',
'rHand', 'rWrist'
],
'left_leg': [
'mixamorig:LeftUpLeg', 'mixamorig_LeftUpLeg',
'ORG-thigh.L', 'thigh.L',
'lThighBend', 'lThigh'
],
'left_knee': [
'mixamorig:LeftLeg', 'mixamorig_LeftLeg',
'ORG-shin.L', 'shin.L',
'lShin', 'lKnee', 'lLeg'
],
'left_ankle': [
'mixamorig:LeftFoot', 'mixamorig_LeftFoot',
'ORG-foot.L', 'foot.L',
'lFoot', 'lAnkle'
],
'left_toe': [
'mixamorig:LeftToeBase', 'mixamorig_LeftToeBase',
'ORG-toe.L', 'toe.L',
'lToe'
],
'right_leg': [
'mixamorig:RightUpLeg', 'mixamorig_RightUpLeg',
'ORG-thigh.R', 'thigh.R',
'rThighBend', 'rThigh'
],
'right_knee': [
'mixamorig:RightLeg', 'mixamorig_RightLeg',
'ORG-shin.R', 'shin.R',
'rShin', 'rKnee', 'rLeg'
],
'right_ankle': [
'mixamorig:RightFoot', 'mixamorig_RightFoot',
'ORG-foot.R', 'foot.R',
'rFoot', 'rAnkle'
],
'right_toe': [
'mixamorig:RightToeBase', 'mixamorig_RightToeBase',
'ORG-toe.R', 'toe.R',
'rToe'
],
'thumb_1_l': [
'mixamorig:LeftHandThumb1', 'mixamorig_LeftHandThumb1',
'ORG-thumb.01.L', 'thumb.01.L',
'lThumb1'
],
'thumb_2_l': [
'mixamorig:LeftHandThumb2', 'mixamorig_LeftHandThumb2',
'ORG-thumb.02.L', 'thumb.02.L',
'lThumb2'
],
'thumb_3_l': [
'mixamorig:LeftHandThumb3', 'mixamorig_LeftHandThumb3',
'ORG-thumb.03.L', 'thumb.03.L',
'lThumb3'
],
'index_1_l': [
'mixamorig:LeftHandIndex1', 'mixamorig_LeftHandIndex1',
'ORG-f_index.01.L', 'f_index.01.L',
'lIndex1'
],
'index_2_l': [
'mixamorig:LeftHandIndex2', 'mixamorig_LeftHandIndex2',
'ORG-f_index.02.L', 'f_index.02.L',
'lIndex2'
],
'index_3_l': [
'mixamorig:LeftHandIndex3', 'mixamorig_LeftHandIndex3',
'ORG-f_index.03.L', 'f_index.03.L',
'lIndex3'
],
'middle_1_l': [
'mixamorig:LeftHandMiddle1', 'mixamorig_LeftHandMiddle1',
'ORG-f_middle.01.L', 'f_middle.01.L',
'lMid1'
],
'middle_2_l': [
'mixamorig:LeftHandMiddle2', 'mixamorig_LeftHandMiddle2',
'ORG-f_middle.02.L', 'f_middle.02.L',
'lMid2'
],
'middle_3_l': [
'mixamorig:LeftHandMiddle3', 'mixamorig_LeftHandMiddle3',
'ORG-f_middle.03.L', 'f_middle.03.L',
'lMid3'
],
'ring_1_l': [
'mixamorig:LeftHandRing1', 'mixamorig_LeftHandRing1',
'ORG-f_ring.01.L', 'f_ring.01.L',
'lRing1'
],
'ring_2_l': [
'mixamorig:LeftHandRing2', 'mixamorig_LeftHandRing2',
'ORG-f_ring.02.L', 'f_ring.02.L',
'lRing2'
],
'ring_3_l': [
'mixamorig:LeftHandRing3', 'mixamorig_LeftHandRing3',
'ORG-f_ring.03.L', 'f_ring.03.L',
'lRing3'
],
'pinkie_1_l': [
'mixamorig:LeftHandPinky1', 'mixamorig_LeftHandPinky1',
'ORG-f_pinky.01.L', 'f_pinky.01.L',
'lPinky1'
],
'pinkie_2_l': [
'mixamorig:LeftHandPinky2', 'mixamorig_LeftHandPinky2',
'ORG-f_pinky.02.L', 'f_pinky.02.L',
'lPinky2'
],
'pinkie_3_l': [
'mixamorig:LeftHandPinky3', 'mixamorig_LeftHandPinky3',
'ORG-f_pinky.03.L', 'f_pinky.03.L',
'lPinky3'
],
'thumb_1_r': [
'mixamorig:RightHandThumb1', 'mixamorig_RightHandThumb1',
'ORG-thumb.01.R', 'thumb.01.R',
'rThumb1'
],
'thumb_2_r': [
'mixamorig:RightHandThumb2', 'mixamorig_RightHandThumb2',
'ORG-thumb.02.R', 'thumb.02.R',
'rThumb2'
],
'thumb_3_r': [
'mixamorig:RightHandThumb3', 'mixamorig_RightHandThumb3',
'ORG-thumb.03.R', 'thumb.03.R',
'rThumb3'
],
'index_1_r': [
'mixamorig:RightHandIndex1', 'mixamorig_RightHandIndex1',
'ORG-f_index.01.R', 'f_index.01.R',
'rIndex1'
],
'index_2_r': [
'mixamorig:RightHandIndex2', 'mixamorig_RightHandIndex2',
'ORG-f_index.02.R', 'f_index.02.R',
'rIndex2'
],
'index_3_r': [
'mixamorig:RightHandIndex3', 'mixamorig_RightHandIndex3',
'ORG-f_index.03.R', 'f_index.03.R',
'rIndex3'
],
'middle_1_r': [
'mixamorig:RightHandMiddle1', 'mixamorig_RightHandMiddle1',
'ORG-f_middle.01.R', 'f_middle.01.R',
'rMid1'
],
'middle_2_r': [
'mixamorig:RightHandMiddle2', 'mixamorig_RightHandMiddle2',
'ORG-f_middle.02.R', 'f_middle.02.R',
'rMid2'
],
'middle_3_r': [
'mixamorig:RightHandMiddle3', 'mixamorig_RightHandMiddle3',
'ORG-f_middle.03.R', 'f_middle.03.R',
'rMid3'
],
'ring_1_r': [
'mixamorig:RightHandRing1', 'mixamorig_RightHandRing1',
'ORG-f_ring.01.R', 'f_ring.01.R',
'rRing1'
],
'ring_2_r': [
'mixamorig:RightHandRing2', 'mixamorig_RightHandRing2',
'ORG-f_ring.02.R', 'f_ring.02.R',
'rRing2'
],
'ring_3_r': [
'mixamorig:RightHandRing3', 'mixamorig_RightHandRing3',
'ORG-f_ring.03.R', 'f_ring.03.R',
'rRing3'
],
'pinkie_1_r': [
'mixamorig:RightHandPinky1', 'mixamorig_RightHandPinky1',
'ORG-f_pinky.01.R', 'f_pinky.01.R',
'rPinky1'
],
'pinkie_2_r': [
'mixamorig:RightHandPinky2', 'mixamorig_RightHandPinky2',
'ORG-f_pinky.02.R', 'f_pinky.02.R',
'rPinky2'
],
'pinkie_3_r': [
'mixamorig:RightHandPinky3', 'mixamorig_RightHandPinky3',
'ORG-f_pinky.03.R', 'f_pinky.03.R',
'rPinky3'
],
'left_eye': [
'mixamorig:LeftEye', 'mixamorig_LeftEye',
'ORG-eye.L', 'eye.L',
'lEye'
],
'right_eye': [
'mixamorig:RightEye', 'mixamorig_RightEye',
'ORG-eye.R', 'eye.R',
'rEye'
]
}
for category, mappings in non_standard_mappings.items():
if category in bone_names:
bone_names[category].extend(mappings)
else:
bone_names[category] = mappings
-271
View File
@@ -1,271 +0,0 @@
import bpy
import struct
import mathutils
import traceback
import os
from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeOutputMaterial
def read_pmd_header(file):
# Read PMD header information
magic = file.read(3)
if magic != b'Pmd':
raise ValueError("Invalid PMD file")
version = struct.unpack('<f', file.read(4))[0]
# Read additional header fields
model_name = file.read(20).decode('shift-jis').rstrip('\0')
comment = file.read(256).decode('shift-jis').rstrip('\0')
return version, model_name, comment
def read_pmd_vertex(file):
# Read PMD vertex information
position = struct.unpack('<3f', file.read(12))
normal = struct.unpack('<3f', file.read(12))
uv = struct.unpack('<2f', file.read(8))
bone_indices = list(struct.unpack('<2H', file.read(4)))
bone_weights = struct.unpack('<b', file.read(1))[0] / 100
edge_flag = struct.unpack('<b', file.read(1))[0]
return position, normal, uv, bone_indices, bone_weights, edge_flag
def read_pmd_material(file):
# Read PMD material information
diffuse_color = struct.unpack('<4f', file.read(16))
specular_color = struct.unpack('<3f', file.read(12))
specular_intensity = struct.unpack('<f', file.read(4))[0]
ambient_color = struct.unpack('<3f', file.read(12))
toon_index = struct.unpack('<b', file.read(1))[0]
edge_flag = struct.unpack('<b', file.read(1))[0]
vertex_count = struct.unpack('<i', file.read(4))[0]
texture_file_name = file.read(20).decode('shift-jis').rstrip('\0')
return diffuse_color, specular_color, specular_intensity, ambient_color, toon_index, edge_flag, vertex_count, texture_file_name
def read_pmd_bone(file):
# Read PMD bone information
bone_name = file.read(20).decode('shift-jis').rstrip('\0')
parent_bone_index = struct.unpack('<h', file.read(2))[0]
tail_pos_bone_index = struct.unpack('<h', file.read(2))[0]
bone_type = struct.unpack('<b', file.read(1))[0]
ik_parent_bone_index = struct.unpack('<h', file.read(2))[0]
bone_head_pos = struct.unpack('<3f', file.read(12))
return bone_name, parent_bone_index, tail_pos_bone_index, bone_type, ik_parent_bone_index, bone_head_pos
def read_pmd_ik(file):
# Read PMD IK information
ik_bone_index = struct.unpack('<h', file.read(2))[0]
ik_target_bone_index = struct.unpack('<h', file.read(2))[0]
ik_chain_length = struct.unpack('<b', file.read(1))[0]
iterations = struct.unpack('<h', file.read(2))[0]
limit_angle = struct.unpack('<f', file.read(4))[0]
ik_child_bone_indices = []
for _ in range(ik_chain_length):
ik_child_bone_index = struct.unpack('<h', file.read(2))[0]
ik_child_bone_indices.append(ik_child_bone_index)
return ik_bone_index, ik_target_bone_index, ik_chain_length, iterations, limit_angle, ik_child_bone_indices
def read_pmd_morph(file):
# Read PMD morph information
morph_name = file.read(20).decode('shift-jis').rstrip('\0')
morph_vertex_count = struct.unpack('<i', file.read(4))[0]
morph_type = struct.unpack('<b', file.read(1))[0]
morph_vertices = []
for _ in range(morph_vertex_count):
morph_vertex_index = struct.unpack('<i', file.read(4))[0]
morph_vertex_pos = struct.unpack('<3f', file.read(12))
morph_vertices.append((morph_vertex_index, morph_vertex_pos))
return morph_name, morph_vertex_count, morph_type, morph_vertices
def import_pmd(filepath):
try:
with open(filepath, 'rb') as file:
version, model_name, comment = read_pmd_header(file)
# Read vertices
vertex_count = struct.unpack('<i', file.read(4))[0]
vertices = []
for _ in range(vertex_count):
position, normal, uv, bone_indices, bone_weights, edge_flag = read_pmd_vertex(file)
vertices.append((position, normal, uv, bone_indices, bone_weights, edge_flag))
# Read faces
face_count = struct.unpack('<i', file.read(4))[0]
faces = []
for _ in range(face_count // 3):
face_indices = struct.unpack('<3i', file.read(12))
faces.append(face_indices)
# Read materials
material_count = struct.unpack('<i', file.read(4))[0]
materials = []
for _ in range(material_count):
diffuse_color, specular_color, specular_intensity, ambient_color, toon_index, edge_flag, vertex_count, texture_file_name = read_pmd_material(file)
materials.append((diffuse_color, specular_color, specular_intensity, ambient_color, toon_index, edge_flag, vertex_count, texture_file_name))
# Read bones
bone_count = struct.unpack('<h', file.read(2))[0]
bones = []
for _ in range(bone_count):
bone_name, parent_bone_index, tail_pos_bone_index, bone_type, ik_parent_bone_index, bone_head_pos = read_pmd_bone(file)
bones.append((bone_name, parent_bone_index, tail_pos_bone_index, bone_type, ik_parent_bone_index, bone_head_pos))
# Read IKs
ik_count = struct.unpack('<h', file.read(2))[0]
iks = []
for _ in range(ik_count):
ik_bone_index, ik_target_bone_index, ik_chain_length, iterations, limit_angle, ik_child_bone_indices = read_pmd_ik(file)
iks.append((ik_bone_index, ik_target_bone_index, ik_chain_length, iterations, limit_angle, ik_child_bone_indices))
# Read morphs
morph_count = struct.unpack('<h', file.read(2))[0]
morphs = []
for _ in range(morph_count):
morph_name, morph_vertex_count, morph_type, morph_vertices = read_pmd_morph(file)
morphs.append((morph_name, morph_vertex_count, morph_type, morph_vertices))
# Create Blender objects and assign PMD data
mesh = bpy.data.meshes.new(model_name)
mesh.from_pydata([v[0] for v in vertices], [], faces)
mesh.update()
obj = bpy.data.objects.new(model_name, mesh)
bpy.context.collection.objects.link(obj)
# Assign vertex normals
for i, vertex in enumerate(vertices):
mesh.vertices[i].normal = vertex[1]
# Assign UV coordinates
uv_layer = mesh.uv_layers.new()
for i, vertex in enumerate(vertices):
uv_layer.data[i].uv = vertex[2]
# Assign materials
for material_data in materials:
material: bpy.types.Material
if f"Material_{len(mesh.materials)}" in bpy.data.materials:
material = bpy.data.materials[f"Material_{len(mesh.materials)}"]
else:
material = bpy.data.materials.new(f"Material_{len(mesh.materials)}")
material.use_nodes = True
for node in [node for node in material.node_tree.nodes]:
material.node_tree.nodes.remove(node)
principled_node: ShaderNodeBsdfPrincipled = material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
principled_node.location.x = 7.29706335067749
principled_node.location.y = 298.918212890625
principled_node.inputs["Base Color"].default_value = material_data[0]
principled_node.inputs["Specular Tint"].default_value = [material_data[1][0],material_data[1][1],material_data[1][2],1.0]
principled_node.inputs["Specular IOR Level"].default_value = material_data[2]
output_node: ShaderNodeOutputMaterial = material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
output_node.location.x = 297.29705810546875
output_node.location.y = 298.918212890625
albedo_node: ShaderNodeTexImage = material.node_tree.nodes.new(type="ShaderNodeTexImage")
albedo_node.location.x = -588.6177978515625
albedo_node.location.y = 414.1948547363281
if texture_file_name in bpy.data.images:
albedo_node.image = bpy.data.images[texture_file_name]
else:
albedo_node.image = bpy.data.images.new(name=texture_file_name,width=32,height=32)
albedo_node.image.filepath = os.path.join(os.path.dirname(filepath),texture_file_name)
albedo_node.image.source = 'FILE'
albedo_node.image.reload()
material.node_tree.links.new(principled_node.inputs["Base Color"], albedo_node.outputs["Color"])
material.node_tree.links.new(principled_node.inputs["Alpha"], albedo_node.outputs["Alpha"])
material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs["BSDF"])
#material.ambient = material_data[5] #TODO: this doesn't exist
# Set other material properties based on the PMX data
if not (material.name in mesh.materials):
mesh.materials.append(material)
#surprised this works - @989onan
end: int = cur_polygon_index+material_data[15]-1
for face in mesh.polygons.items()[cur_polygon_index:end]:
face[1].material_index = mesh.materials.find(material.name)
cur_polygon_index = cur_polygon_index+material_data[15]
# Set other material properties based on the PMD data
# Create armature and assign bones
armature = bpy.data.armatures.new(model_name + "_Armature")
armature_obj = bpy.data.objects.new(model_name + "_Armature", armature)
bpy.context.collection.objects.link(armature_obj)
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
for bone_data in bones:
bone = armature.edit_bones.new(bone_data[0])
bone.head = bone_data[5]
if bone_data[1] != -1:
parent_bone = armature.edit_bones[bone_data[1]]
bone.parent = parent_bone
bone.tail = parent_bone.head
else:
bone.tail = bone.head + mathutils.Vector((0, 0.1, 0))
# Set other bone properties based on the PMD data
bpy.ops.object.mode_set(mode='OBJECT')
# Assign bone weights to the mesh
for i, vertex in enumerate(vertices):
for j in range(2):
if vertex[3][j] != 65535:
bone_name = bones[vertex[3][j]][0]
weight = vertex[4] if j == 0 else 1 - vertex[4]
vertex_group = obj.vertex_groups.get(bone_name)
if not vertex_group:
vertex_group = obj.vertex_groups.new(name=bone_name)
vertex_group.add([i], weight, 'REPLACE')
# Assign IK constraints to bones
for ik_data in iks:
ik_bone = armature.bones[bones[ik_data[0]][0]]
ik_target_bone = armature.bones[bones[ik_data[1]][0]]
ik_constraint = ik_bone.constraints.new('IK')
ik_constraint.target = armature_obj
ik_constraint.subtarget = ik_target_bone.name
ik_constraint.chain_count = ik_data[2]
ik_constraint.iterations = ik_data[3]
ik_constraint.limit_mode = 'LIMITDIST_INSIDE'
ik_constraint.limit_mode_max_x = ik_data[4]
# Assign morphs to the mesh
for morph_data in morphs:
morph_name = morph_data[0]
morph_type = morph_data[2]
if morph_type == 0: # Vertex morph
shape_key = obj.shape_key_add(name=morph_name)
for vertex_data in morph_data[3]:
vertex_index = vertex_data[0]
vertex_offset = vertex_data[1]
shape_key.data[vertex_index].co += mathutils.Vector(vertex_offset)
print(f"Successfully imported PMD file: {filepath}")
print(f"Model Name: {model_name}")
print(f"Comment: {comment}")
except Exception:
print(f"Error importing PMD file: {filepath}")
print(f"Error details: {traceback.format_exc()}")
-861
View File
@@ -1,861 +0,0 @@
from io import BufferedReader
import os
import bpy
import struct
import traceback
import mathutils
from mathutils import Matrix, Vector
class PMXVertex:
def __init__(self, position, normal, uv, bone_indices, bone_weights, edge_scale, additional_uvs):
self.position = position
self.normal = normal
self.uv = uv
self.bone_indices = bone_indices
self.bone_weights = bone_weights
self.edge_scale = edge_scale
self.additional_uvs = additional_uvs
class PMXBone:
def __init__(self, name, english_name, position, parent_index, layer, flag,
tail_position, inherit_parent_index, inherit_influence,
fixed_axis, local_x, local_z, external_key,
ik_target_index, ik_loop_count, ik_limit_rad, ik_links):
self.name = name
self.english_name = english_name
self.position = position
self.parent_index = parent_index
self.layer = layer
self.flag = flag
self.tail_position = tail_position
self.inherit_parent_index = inherit_parent_index
self.inherit_influence = inherit_influence
self.fixed_axis = fixed_axis
self.local_x = local_x
self.local_z = local_z
self.external_key = external_key
self.ik_target_index = ik_target_index
self.ik_loop_count = ik_loop_count
self.ik_limit_rad = ik_limit_rad
self.ik_links = ik_links
class PMXMaterial:
def __init__(self, name, english_name, diffuse, specular, specular_strength,
ambient, flag, edge_color, edge_size, texture_index,
sphere_texture_index, sphere_mode, toon_sharing_flag,
toon_texture_index, comment, surface_count):
self.name = name
self.english_name = english_name
self.diffuse = diffuse
self.specular = specular
self.specular_strength = specular_strength
self.ambient = ambient
self.flag = flag
self.edge_color = edge_color
self.edge_size = edge_size
self.texture_index = texture_index
self.sphere_texture_index = sphere_texture_index
self.sphere_mode = sphere_mode
self.toon_sharing_flag = toon_sharing_flag
self.toon_texture_index = toon_texture_index
self.comment = comment
self.surface_count = surface_count
class PMXMorph:
def __init__(self, name, english_name, panel, morph_type, offsets):
self.name = name
self.english_name = english_name
self.panel = panel
self.morph_type = morph_type
self.offsets = offsets
class PMXRigidBody:
def __init__(self, name, bone_index, group, shape_type, size, position, rotation, mass, linear_damping, angular_damping, restitution, friction, mode):
self.name = name
self.bone_index = bone_index
self.group = group
self.shape_type = shape_type
self.size = size
self.position = position
self.rotation = rotation
self.mass = mass
self.linear_damping = linear_damping
self.angular_damping = angular_damping
self.restitution = restitution
self.friction = friction
self.mode = mode
class PMXJoint:
def __init__(self, name, joint_type, rigid_body_a, rigid_body_b, position, rotation, linear_limit_min, linear_limit_max, angular_limit_min, angular_limit_max, spring_constant_translation, spring_constant_rotation):
self.name = name
self.joint_type = joint_type
self.rigid_body_a = rigid_body_a
self.rigid_body_b = rigid_body_b
self.position = position
self.rotation = rotation
self.linear_limit_min = linear_limit_min
self.linear_limit_max = linear_limit_max
self.angular_limit_min = angular_limit_min
self.angular_limit_max = angular_limit_max
self.spring_constant_translation = spring_constant_translation
self.spring_constant_rotation = spring_constant_rotation
def read_pmx_header(file: BufferedReader):
magic = file.read(4)
if magic != b'PMX ':
raise ValueError("Invalid PMX file")
version = struct.unpack('<f', file.read(4))[0]
data_size = struct.unpack('<b', file.read(1))[0]
encoding = struct.unpack('<b', file.read(1))[0]
additional_uvs = struct.unpack('<b', file.read(1))[0]
vertex_index_size = struct.unpack('<b', file.read(1))[0]
texture_index_size = struct.unpack('<b', file.read(1))[0]
material_index_size = struct.unpack('<b', file.read(1))[0]
bone_index_size = struct.unpack('<b', file.read(1))[0]
morph_index_size = struct.unpack('<b', file.read(1))[0]
rigid_body_index_size = struct.unpack('<b', file.read(1))[0]
model_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
model_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
model_comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
model_english_comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
return (version, encoding, additional_uvs, vertex_index_size, texture_index_size,
material_index_size, bone_index_size, morph_index_size, rigid_body_index_size,
model_name, model_english_name, model_comment, model_english_comment)
def read_index_size(index, types):
struct_format = "<??"
byte_size = 0
if index == 1:
struct_format = replace_char(struct_format, 2, types[0])
byte_size = 1
elif index == 2:
struct_format = replace_char(struct_format, 2, types[1])
byte_size = 2
else:
struct_format = replace_char(struct_format, 2, types[2])
byte_size = 4
return struct_format, byte_size
def replace_char(string, index, character):
temp = list(string)
temp[index] = character
return "".join(temp)
def read_morph(file: BufferedReader, vertex_struct, vertex_size):
try:
name_length = struct.unpack('<i', file.read(4))[0]
name = str(file.read(name_length), 'utf-16-le', errors='replace')
english_name_length = struct.unpack('<i', file.read(4))[0]
english_name = str(file.read(english_name_length), 'utf-16-le', errors='replace')
panel = int.from_bytes(file.read(1), byteorder='little', signed=True)
morph_type = int.from_bytes(file.read(1), byteorder='little', signed=True)
# Read offset count with error checking
offset_count_bytes = file.read(4)
if len(offset_count_bytes) != 4:
return PMXMorph(name, english_name, panel, morph_type, [])
offset_count = struct.unpack('<i', offset_count_bytes)[0]
offsets = []
if morph_type == 1: # Vertex morph
for _ in range(offset_count):
vertex_index = struct.unpack(replace_char(vertex_struct, 1, '1'), file.read(vertex_size))[0]
offset = struct.unpack('<3f', file.read(12))
offsets.append((vertex_index, offset))
return PMXMorph(name, english_name, panel, morph_type, offsets)
except:
return PMXMorph("", "", 0, 0, [])
def validate_pmx_data(header_data, vertices, faces, materials, bones):
"""Validate PMX data integrity"""
if not vertices:
raise ValueError("No vertices found in PMX file")
if not faces:
raise ValueError("No faces found in PMX file")
if not materials:
raise ValueError("No materials found in PMX file")
if not bones:
raise ValueError("No bones found in PMX file")
return True
def handle_import_error(context, error_msg):
"""Handle import errors with user feedback"""
context.window_manager.progress_end()
bpy.ops.ui.popup_menu(message=error_msg)
return {'CANCELLED'}
def read_vertex(file: BufferedReader, string_build, byte_size, additional_uvs):
position = struct.unpack('<3f', file.read(12))
normal = struct.unpack('<3f', file.read(12))
uv = struct.unpack('<2f', file.read(8))
uv = [uv[0], (1.0-uv[1])-1.0]
additional_uv_read = []
for _ in range(additional_uvs):
additional_uv_read.append(struct.unpack('<4f', file.read(16)))
weight_deform_type = struct.unpack('<B', file.read(1))[0]
bone_indices = []
bone_weights = []
if weight_deform_type == 0: # BDEF1
string_build = replace_char(string_build, 1, '1')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*1)))
bone_weights = [1.0]
elif weight_deform_type == 1: # BDEF2
string_build = replace_char(string_build, 1, '2')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*2)))
weight = struct.unpack('<f', file.read(4))[0]
bone_weights = [weight, 1.0-weight]
elif weight_deform_type == 2: # BDEF4
string_build = replace_char(string_build, 1, '4')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*4)))
bone_weights = list(struct.unpack('<4f', file.read(16)))
elif weight_deform_type == 3: # SDEF
string_build = replace_char(string_build, 1, '2')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*2)))
weight = struct.unpack('<f', file.read(4))[0]
bone_weights = [weight, 1.0-weight]
# Skip SDEF data as we don't use it
file.read(36) # 3 vectors of 3 floats each (C, R0, R1)
elif weight_deform_type == 4: # QDEF
string_build = replace_char(string_build, 1, '4')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*4)))
bone_weights = list(struct.unpack('<4f', file.read(16)))
edge_scale = struct.unpack('<f', file.read(4))[0]
return PMXVertex(position, normal, uv, bone_indices, bone_weights, edge_scale, additional_uv_read)
def read_material(file: BufferedReader, string_build, byte_size):
material_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
material_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
diffuse_color = struct.unpack('<4f', file.read(16))
specular_color = struct.unpack('<3f', file.read(12))
specular_strength = struct.unpack('<f', file.read(4))[0]
ambient_color = struct.unpack('<3f', file.read(12))
flag = struct.unpack('<b', file.read(1))[0]
edge_color = struct.unpack('<4f', file.read(16))
edge_size = struct.unpack('<f', file.read(4))[0]
texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
sphere_texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
sphere_mode = struct.unpack('<b', file.read(1))[0]
toon_sharing_flag = struct.unpack('<b', file.read(1))[0]
if toon_sharing_flag == 0:
toon_texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
else:
toon_texture_index = struct.unpack('<b', file.read(1))[0]
comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
surface_count = int(struct.unpack('<i', file.read(4))[0]/3)
return PMXMaterial(material_name, material_english_name, diffuse_color, specular_color,
specular_strength, ambient_color, flag, edge_color, edge_size,
texture_index, sphere_texture_index, sphere_mode,
toon_sharing_flag, toon_texture_index, comment, surface_count)
def create_material_nodes(material: bpy.types.Material, texture_path: str, diffuse_color, specular_color, specular_strength, toon_texture_path=None):
material.use_nodes = True
nodes = material.node_tree.nodes
links = material.node_tree.links
nodes.clear()
principled = nodes.new("ShaderNodeBsdfPrincipled")
principled.location = (0, 0)
principled.inputs["Base Color"].default_value = diffuse_color
principled.inputs["Specular IOR Level"].default_value = specular_strength
principled.inputs["Specular Tint"].default_value = (*specular_color, 1.0)
# Handle transparency
if diffuse_color[3] < 1.0:
material.blend_method = 'HASHED'
principled.inputs["Alpha"].default_value = diffuse_color[3]
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (300, 0)
# Main texture
if texture_path and os.path.exists(texture_path):
texture = nodes.new("ShaderNodeTexImage")
texture.location = (-300, 0)
texture.image = bpy.data.images.load(texture_path)
links.new(texture.outputs["Color"], principled.inputs["Base Color"])
links.new(texture.outputs["Alpha"], principled.inputs["Alpha"])
# Toon texture
if toon_texture_path and os.path.exists(toon_texture_path):
toon = nodes.new("ShaderNodeTexImage")
toon.location = (-300, -300)
toon.image = bpy.data.images.load(toon_texture_path)
mix = nodes.new("ShaderNodeMixRGB")
mix.location = (-50, -150)
mix.blend_type = 'MULTIPLY'
links.new(toon.outputs["Color"], mix.inputs[2])
links.new(mix.outputs["Color"], principled.inputs["Base Color"])
links.new(principled.outputs["BSDF"], output.inputs["Surface"])
def read_bone(file: BufferedReader, string_build, byte_size):
bone_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
bone_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
position = struct.unpack('<3f', file.read(12))
parent_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
layer = struct.unpack('<i', file.read(4))[0]
flag = struct.unpack('<H', file.read(2))[0]
tail_position = [None, None, None]
inherit_bone_parent_index = 0
inherit_bone_parent_influence = 0.0
fixed_axis = [0.0, 0.0, 0.0]
local_x_vector = [0.0, 0.0, 0.0]
local_z_vector = [0.0, 0.0, 0.0]
external_key = 0
ik_target_bone_index = 0
ik_loop_count = -1
ik_limit_radian = 0.0
ik_links = []
if not (flag & 0x0001):
tail_position = struct.unpack('<3f', file.read(12))
else:
tail_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
if flag & 0x0100 or flag & 0x0200:
inherit_bone_parent_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
inherit_bone_parent_influence = struct.unpack('<f', file.read(4))[0]
if flag & 0x0400:
fixed_axis = struct.unpack('<3f', file.read(12))
if flag & 0x0800:
local_x_vector = struct.unpack('<3f', file.read(12))
local_z_vector = struct.unpack('<3f', file.read(12))
if flag & 0x2000:
external_key = struct.unpack('<i', file.read(4))[0]
if flag & 0x0020:
ik_target_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
ik_loop_count = struct.unpack('<i', file.read(4))[0]
ik_limit_radian = struct.unpack('<f', file.read(4))[0]
ik_link_count = struct.unpack('<i', file.read(4))[0]
for _ in range(ik_link_count):
ik_link_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
ik_link_limit = struct.unpack('<b', file.read(1))[0]
if ik_link_limit == 1:
angle_limit = (struct.unpack('<3f', file.read(12)), struct.unpack('<3f', file.read(12)))
ik_links.append((ik_link_bone_index, True, angle_limit))
else:
ik_links.append((ik_link_bone_index, False, None))
return PMXBone(bone_name, bone_english_name, position, parent_bone_index, layer,
flag, tail_position, inherit_bone_parent_index, inherit_bone_parent_influence,
fixed_axis, local_x_vector, local_z_vector, external_key,
ik_target_bone_index, ik_loop_count, ik_limit_radian, ik_links)
def create_bone_constraints(armature_obj: bpy.types.Object, bones: list[PMXBone]):
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='POSE')
# Clear existing constraints
for pose_bone in armature_obj.pose.bones:
while pose_bone.constraints:
pose_bone.constraints.remove(pose_bone.constraints[0])
# Handle rotation inheritance first
for bone_data in bones:
pose_bone = armature_obj.pose.bones.get(bone_data.name)
if not pose_bone or bone_data.parent_index < 0:
continue
# Check if bone has vertex groups
if not pose_bone.bone.use_deform:
continue
if bone_data.flag & 0x0100: # Rotation inheritance
if bone_data.inherit_parent_index >= 0:
constraint = pose_bone.constraints.new('COPY_ROTATION')
constraint.name = "MMD Rotation"
constraint.target = armature_obj
constraint.subtarget = bones[bone_data.inherit_parent_index].name
constraint.influence = bone_data.inherit_influence
constraint.target_space = 'LOCAL'
constraint.owner_space = 'LOCAL'
# Then handle IK constraints
for bone_data in bones:
pose_bone = armature_obj.pose.bones.get(bone_data.name)
if not pose_bone:
continue
# Skip non-deforming bones
if not pose_bone.bone.use_deform:
continue
if bone_data.flag & 0x0020: # IK
if bone_data.ik_target_index >= 0:
constraint = pose_bone.constraints.new('IK')
constraint.name = "MMD IK"
constraint.target = armature_obj
constraint.subtarget = bones[bone_data.ik_target_index].name
constraint.chain_count = min(len(bone_data.ik_links), 3)
constraint.iterations = min(bone_data.ik_loop_count, 8)
constraint.use_tail = False
constraint.use_stretch = False
# Configure IK chain
for link_bone_index, has_limits, angle_limits in bone_data.ik_links:
link_pose_bone = armature_obj.pose.bones.get(bones[link_bone_index].name)
if link_pose_bone and link_pose_bone.bone.use_deform:
link_pose_bone.rotation_mode = 'XYZ'
link_pose_bone.use_ik_limit_x = True
link_pose_bone.use_ik_limit_y = True
link_pose_bone.use_ik_limit_z = True
if has_limits and angle_limits:
min_angles, max_angles = angle_limits
link_pose_bone.ik_min_x = max(-1.4, min_angles[0])
link_pose_bone.ik_max_x = min(1.4, max_angles[0])
link_pose_bone.ik_min_y = max(-1.4, min_angles[1])
link_pose_bone.ik_max_y = min(1.4, max_angles[1])
link_pose_bone.ik_min_z = max(-1.4, min_angles[2])
link_pose_bone.ik_max_z = min(1.4, max_angles[2])
# Reset pose to default state
bpy.ops.pose.select_all(action='SELECT')
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
def setup_physics(obj: bpy.types.Object, armature_obj: bpy.types.Object, rigid_bodies: list[PMXRigidBody], joints: list[PMXJoint]):
"""Set up physics for PMX model"""
# Create rigid body collection if it doesn't exist
if 'RigidBodies' not in bpy.data.collections:
rigid_body_collection = bpy.data.collections.new('RigidBodies')
bpy.context.scene.collection.children.link(rigid_body_collection)
else:
rigid_body_collection = bpy.data.collections['RigidBodies']
# Create rigid bodies
for rb in rigid_bodies:
# Create mesh based on shape type
if rb.shape_type == 0: # Sphere
bpy.ops.mesh.primitive_uv_sphere_add(radius=rb.size[0])
elif rb.shape_type == 1: # Box
bpy.ops.mesh.primitive_cube_add()
bpy.context.active_object.scale = rb.size
elif rb.shape_type == 2: # Capsule
bpy.ops.mesh.primitive_cylinder_add(radius=rb.size[0], depth=rb.size[1])
rb_obj = bpy.context.active_object
rb_obj.name = f"RB_{rb.name}"
rb_obj.location = rb.position
rb_obj.rotation_euler = rb.rotation
# Set up rigid body physics
rb_obj.rigid_body.type = 'ACTIVE' if rb.mode == 0 else 'PASSIVE'
rb_obj.rigid_body.mass = rb.mass
rb_obj.rigid_body.linear_damping = rb.linear_damping
rb_obj.rigid_body.angular_damping = rb.angular_damping
rb_obj.rigid_body.restitution = rb.restitution
rb_obj.rigid_body.friction = rb.friction
# Parent to bone if specified
if rb.bone_index >= 0:
rb_obj.parent = armature_obj
rb_obj.parent_type = 'BONE'
rb_obj.parent_bone = bones[rb.bone_index].name
# Move to rigid body collection
rigid_body_collection.objects.link(rb_obj)
bpy.context.scene.collection.objects.unlink(rb_obj)
# Create joints
for joint in joints:
empty = bpy.data.objects.new(f"Joint_{joint.name}", None)
empty.empty_display_type = 'ARROWS'
empty.location = joint.position
empty.rotation_euler = joint.rotation
bpy.context.scene.collection.objects.link(empty)
# Set up constraint
constraint = empty.constraints.new('RIGID_BODY_JOINT')
constraint.target = rigid_bodies[joint.rigid_body_a]
constraint.child = rigid_bodies[joint.rigid_body_b]
constraint.use_limit_lin_x = True
constraint.use_limit_lin_y = True
constraint.use_limit_lin_z = True
constraint.use_limit_ang_x = True
constraint.use_limit_ang_y = True
constraint.use_limit_ang_z = True
# Set limits
constraint.limit_lin_x_lower = joint.linear_limit_min[0]
constraint.limit_lin_x_upper = joint.linear_limit_max[0]
constraint.limit_lin_y_lower = joint.linear_limit_min[1]
constraint.limit_lin_y_upper = joint.linear_limit_max[1]
constraint.limit_lin_z_lower = joint.linear_limit_min[2]
constraint.limit_lin_z_upper = joint.linear_limit_max[2]
constraint.limit_ang_x_lower = joint.angular_limit_min[0]
constraint.limit_ang_x_upper = joint.angular_limit_max[0]
constraint.limit_ang_y_lower = joint.angular_limit_min[1]
constraint.limit_ang_y_upper = joint.angular_limit_max[1]
constraint.limit_ang_z_lower = joint.angular_limit_min[2]
constraint.limit_ang_z_upper = joint.angular_limit_max[2]
def create_armature(model_name: str, bones: list[PMXBone]) -> bpy.types.Object:
# Handle CJK characters in model name
if isinstance(model_name, bytes):
try:
model_name = model_name.decode('gbk') # Try Chinese encoding first
except UnicodeDecodeError:
try:
model_name = model_name.decode('utf-8')
except UnicodeDecodeError:
try:
model_name = model_name.decode('shift-jis')
except UnicodeDecodeError:
model_name = model_name.decode('latin1')
armature = bpy.data.armatures.new(f"{model_name}_Armature")
armature_obj = bpy.data.objects.new(f"{model_name}_Armature", armature)
bpy.context.collection.objects.link(armature_obj)
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
# First pass: Create bones with proper names and types
edit_bones = []
for i, bone_data in enumerate(bones):
bone_name = bone_data.name if bone_data.name else bone_data.english_name
if not bone_name:
bone_name = f"bone_{i}"
edit_bone = armature.edit_bones.new(bone_name)
edit_bone.head = Vector(bone_data.position)
# Handle different bone types based on flags and names
is_expression = bool(bone_data.flag & 0x0004)
is_rotation_influenced = bool(bone_data.flag & 0x0100)
is_ik = bool(bone_data.flag & 0x0020)
is_twist = "twist" in bone_name.lower()
if is_twist:
# Twist bones need specific handling
parent_pos = bones[bone_data.parent_index].position if bone_data.parent_index >= 0 else None
if parent_pos:
direction = Vector(bone_data.position) - Vector(parent_pos)
if direction.length > 0.001:
edit_bone.tail = edit_bone.head + direction.normalized() * 0.1
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.05, 0))
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.05, 0))
elif is_expression:
edit_bone.tail = edit_bone.head + Vector((0, 0.02, 0))
edit_bone.use_deform = False
elif is_ik:
if bone_data.ik_links:
target_pos = bones[bone_data.ik_links[0][0]].position
direction = Vector(target_pos) - Vector(edit_bone.head)
if direction.length > 0.001:
edit_bone.tail = edit_bone.head + direction.normalized() * 0.1
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.1, 0))
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.1, 0))
elif is_rotation_influenced:
# Handle rotation influenced bones
if bone_data.inherit_parent_index >= 0:
target_pos = bones[bone_data.inherit_parent_index].position
direction = Vector(target_pos) - Vector(edit_bone.head)
if direction.length > 0.001:
edit_bone.tail = edit_bone.head + direction.normalized() * 0.08
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.08, 0))
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.08, 0))
else:
# Standard bones
if bone_data.tail_position[0] is not None:
edit_bone.tail = Vector(bone_data.tail_position)
else:
child_positions = [bones[j].position for j in range(len(bones))
if bones[j].parent_index == i]
if child_positions:
avg_child_pos = Vector((0, 0, 0))
for pos in child_positions:
avg_child_pos += Vector(pos)
avg_child_pos /= len(child_positions)
edit_bone.tail = avg_child_pos
else:
bone_length = 0.1 if bone_data.layer == 0 else 0.05
edit_bone.tail = edit_bone.head + Vector((0, bone_length, 0))
edit_bones.append(edit_bone)
# Second pass: Set up hierarchy and orientations
for i, bone_data in enumerate(bones):
edit_bone = edit_bones[i]
# Parent bones
if bone_data.parent_index >= 0:
parent_bone = edit_bones[bone_data.parent_index]
edit_bone.parent = parent_bone
# Connect bones only if they should be connected
if (Vector(bone_data.position) - Vector(parent_bone.tail)).length < 0.01:
edit_bone.use_connect = True
# Handle bone orientation
if bone_data.fixed_axis != [0.0, 0.0, 0.0]:
edit_bone.align_roll(Vector(bone_data.fixed_axis))
elif bone_data.local_x != [0.0, 0.0, 0.0]:
x_axis = Vector(bone_data.local_x).normalized()
z_axis = Vector(bone_data.local_z).normalized()
y_axis = z_axis.cross(x_axis)
# Create and apply orientation matrix
matrix_3x3 = Matrix((x_axis, y_axis, z_axis)).to_3x3()
matrix_4x4 = matrix_3x3.to_4x4()
edit_bone.matrix = matrix_4x4
bpy.ops.object.mode_set(mode='OBJECT')
return armature_obj
def assign_vertex_weights(obj: bpy.types.Object, vertices: list[PMXVertex], bones: list[PMXBone]):
# Pre-create vertex groups
vertex_groups = {}
for bone in bones:
vertex_groups[bone.name] = obj.vertex_groups.new(name=bone.name)
# Batch assign weights
for vertex_index, vertex in enumerate(vertices):
for bone_idx, weight in zip(vertex.bone_indices, vertex.bone_weights):
if bone_idx != -1 and weight > 0:
vertex_groups[bones[bone_idx].name].add([vertex_index], weight, 'REPLACE')
def assign_materials(obj: bpy.types.Object, materials: list[PMXMaterial], textures: list[str], base_path: str):
current_face_index = 0
for material in materials:
# Create or get material
mat_name = material.name or f"Material_{len(obj.data.materials)}"
if mat_name in bpy.data.materials:
mat = bpy.data.materials[mat_name]
else:
mat = bpy.data.materials.new(name=mat_name)
# Set up material nodes
texture_path = None
if material.texture_index >= 0 and material.texture_index < len(textures):
texture_path = os.path.join(base_path, textures[material.texture_index])
create_material_nodes(mat, texture_path, material.diffuse, material.specular,
material.specular_strength)
# Assign material to mesh
if mat.name not in obj.data.materials:
obj.data.materials.append(mat)
# Assign faces to material
mat_index = obj.data.materials.find(mat.name)
for face in obj.data.polygons[current_face_index:current_face_index + material.surface_count]:
face.material_index = mat_index
current_face_index += material.surface_count
def import_pmx(filepath: str):
wm = bpy.context.window_manager
wm.progress_begin(0, 100)
try:
with open(filepath, 'rb') as file:
# Read header (5%)
wm.progress_update(5)
header_data = read_pmx_header(file)
version, encoding, additional_uvs, vertex_index_size, texture_index_size, \
material_index_size, bone_index_size, morph_index_size, rigid_body_index_size, \
model_name, model_english_name, model_comment, model_english_comment = header_data
# Set up index size formats (10%)
wm.progress_update(10)
vertex_struct, vertex_size = read_index_size(vertex_index_size, 'BHi')
bone_struct, bone_size = read_index_size(bone_index_size, 'bhi')
texture_struct, texture_size = read_index_size(texture_index_size, 'bhi')
# Read vertices (25%)
vertex_count = struct.unpack('<i', file.read(4))[0]
vertices = []
for i in range(vertex_count):
vertices.append(read_vertex(file, bone_struct, bone_size, additional_uvs))
if i % 1000 == 0:
wm.progress_update(10 + (i/vertex_count * 15))
# Read faces (35%)
wm.progress_update(35)
face_count = struct.unpack('<i', file.read(4))[0] // 3
faces = []
for _ in range(face_count):
if vertex_index_size == 1:
faces.append(struct.unpack('<3B', file.read(3)))
elif vertex_index_size == 2:
faces.append(struct.unpack('<3H', file.read(6)))
else:
faces.append(struct.unpack('<3i', file.read(12)))
# Read textures (45%)
wm.progress_update(45)
texture_count = struct.unpack('<i', file.read(4))[0]
textures = []
for _ in range(texture_count):
texture_path = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
textures.append(texture_path)
# Read materials (55%)
wm.progress_update(55)
material_count = struct.unpack('<i', file.read(4))[0]
materials = []
for _ in range(material_count):
materials.append(read_material(file, texture_struct, texture_size))
# Read bones (65%)
wm.progress_update(65)
bone_count = struct.unpack('<i', file.read(4))[0]
bones = []
for _ in range(bone_count):
bones.append(read_bone(file, bone_struct, bone_size))
# Read morphs (75%)
wm.progress_update(75)
morph_count = struct.unpack('<i', file.read(4))[0]
morphs = []
for _ in range(morph_count):
morphs.append(read_morph(file, vertex_struct, vertex_size))
# Read rigid bodies (85%)
wm.progress_update(85)
try:
rigid_body_count_bytes = file.read(4)
if len(rigid_body_count_bytes) == 4:
rigid_body_count = struct.unpack('<i', rigid_body_count_bytes)[0]
rigid_bodies = []
for _ in range(rigid_body_count):
rigid_bodies.append(read_rigid_body(file, bone_struct, bone_size))
else:
rigid_bodies = []
except:
rigid_bodies = []
# Read joints (90%)
wm.progress_update(90)
try:
joint_count_bytes = file.read(4)
if len(joint_count_bytes) == 4:
joint_count = struct.unpack('<i', joint_count_bytes)[0]
joints = []
for _ in range(joint_count):
joints.append(read_joint(file, rigid_body_struct, rigid_body_size))
else:
joints = []
except:
joints = []
# Validate data (92%)
wm.progress_update(92)
validate_pmx_data(header_data, vertices, faces, materials, bones)
# Create mesh and object (94%)
wm.progress_update(94)
mesh = bpy.data.meshes.new(model_name)
mesh.from_pydata([v.position for v in vertices], [], faces)
mesh.update()
obj = bpy.data.objects.new(model_name, mesh)
bpy.context.collection.objects.link(obj)
# Create and set up armature (96%)
wm.progress_update(96)
armature_obj = create_armature(model_name, bones)
obj.parent = armature_obj
# Create shape keys (97%)
wm.progress_update(97)
for morph in morphs:
if morph.morph_type == 1:
if not obj.data.shape_keys:
obj.shape_key_add(name='Basis')
shape_key = obj.shape_key_add(name=morph.name)
for vertex_index, offset in morph.offsets:
shape_key.data[vertex_index].co = (
vertices[vertex_index].position[0] + offset[0],
vertices[vertex_index].position[1] + offset[1],
vertices[vertex_index].position[2] + offset[2]
)
# Set up physics (98%)
wm.progress_update(98)
setup_physics(obj, armature_obj, rigid_bodies, joints)
# Final setup (99%)
wm.progress_update(99)
base_path = os.path.dirname(filepath)
assign_materials(obj, materials, textures, base_path)
assign_vertex_weights(obj, vertices, bones)
# Add armature modifier
mod = obj.modifiers.new(name="Armature", type='ARMATURE')
mod.object = armature_obj
# Set proper scale and orientation
armature_obj.scale = (0.08, 0.08, 0.08)
armature_obj.rotation_euler = (1.5708, 0, 0)
# Select objects and set active
armature_obj.select_set(True)
obj.select_set(True)
bpy.context.view_layer.objects.active = armature_obj
# Disable automatic mirroring
armature_obj.data.use_mirror_x = False
# Add constraints
create_bone_constraints(armature_obj, bones)
# Apply transforms
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
# Ensure object mode
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='OBJECT')
wm.progress_end()
return {'FINISHED'}
except Exception as e:
wm.progress_end()
error_msg = f"PMX Import Error: {str(e)}\n{traceback.format_exc()}"
print(error_msg) # Console output for debugging
return {'CANCELLED'}
-9
View File
@@ -7,8 +7,6 @@ from bpy.types import Operator, Context
from bpy_extras.io_utils import ImportHelper
from typing import Optional, Callable, Dict, List, Union, Set
from ..common import clear_default_objects
from .import_pmx import import_pmx
from .import_pmd import import_pmd
from ..translations import t
# Configure logging
@@ -122,13 +120,6 @@ import_types: Dict[str, ImportMethod] = {
method=lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)
),
"vrm": lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath),
"pmx": lambda directory, files, filepath: import_pmx(bpy.context, filepath,
scale=1.0,
use_mipmap=True,
sph_blend_factor=1.0,
spa_blend_factor=1.0
),
"pmd": lambda directory, files, filepath: import_pmd(filepath),
"animx": (lambda directory, files, filepath : bpy.ops.avatar_toolkit.animx_importer(directory=directory,files=files,filepath=filepath)),
}
+18
View File
@@ -1,8 +1,10 @@
import logging
import traceback
from typing import Optional, Any
from bpy.types import Context
logger = logging.getLogger('avatar_toolkit')
_original_error = logger.error
def configure_logging(enabled: bool = False) -> None:
"""Configure logging for Avatar Toolkit"""
@@ -18,6 +20,15 @@ def configure_logging(enabled: bool = False) -> None:
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
def error_with_traceback(msg, *args, **kwargs):
if kwargs.get('exc_info', False) or isinstance(msg, Exception):
full_msg = f"{msg}\n{traceback.format_exc()}"
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False})
else:
_original_error(msg, *args, **kwargs)
logger.error = error_with_traceback
def update_logging_state(self: Any, context: Context) -> None:
"""Update logging state based on user preference"""
@@ -25,3 +36,10 @@ def update_logging_state(self: Any, context: Context) -> None:
enabled = self.enable_logging
save_preference("enable_logging", enabled)
configure_logging(enabled)
def highlight_problem_bones(self: Any, context: Context) -> None:
"""Log when problem bones are highlighted"""
from .addon_preferences import save_preference
enabled = self.highlight_problem_bones
save_preference("highlight_problem_bones", enabled)
logger.debug(f"Problem bone highlighting {'enabled' if enabled else 'disabled'}")
+152
View File
@@ -0,0 +1,152 @@
# thank you https://stackoverflow.com/a/71432759
from __future__ import annotations
from typing import Optional
from bpy.types import Image, Material
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
class Rectangle_Obj:
x: int = 0
y: int = 0
w: int = 0
h: int = 0
down: Rectangle_Obj = None
used: bool = False
right: Rectangle_Obj = None
def __init__(self, x:int, y:int, w:int, h:int, down=None, used =False, right=None):
self.x = x
self.y = y
self.w = w
self.h = h
self.down = down
self.used = used
self.right = right
def split(self, w, h) -> Rectangle_Obj:
self.used = True
self.down = Rectangle_Obj(x=self.x, y=self.y + h, w=self.w, h=self.h - h)
self.right = Rectangle_Obj(x=self.x + w, y=self.y, w=self.w - w, h=h)
return self
def find(self, w, h) -> Optional[Rectangle_Obj]:
if self.used:
return self.right.find(w, h) or self.down.find(w, h)
elif (w <= self.w) and (h <= self.h):
return self
return None
class MaterialImageList:
albedo: Image
normal: Image
emission: Image
ambient_occlusion: Image
height: Image
roughness: Image
fit: Rectangle_Obj
material: Material
def __init__(self):
pass
x: int = 0
y: int = 0
w: int = 0
h: int = 0
class BinPacker(object):
root: Rectangle_Obj
bin: list[MaterialImageList] = []
def __init__(self, structure: list[MaterialImageList]):
self.root = None
self.bin = structure
def fit(self):
structure = self.bin
structure_len = len(self.bin)
w: int = 0
h: int = 0
if structure_len > 0:
w = structure[0].w
h = structure[0].h
self.root = Rectangle_Obj(x=0, y=0, w=w, h=h)
for img in structure:
w = img.w
h = img.h
node = self.root.find(w, h)
if node:
img.fit = node.split(w, h)
else:
img.fit = self.grow_node(w, h)
return structure
def grow_node(self, w, h) -> Optional[Rectangle_Obj]:
can_grow_right = (h <= self.root.h)
can_grow_down = (w <= self.root.w)
should_grow_right = can_grow_right and (self.root.h >= (self.root.w + w))
should_grow_down = can_grow_down and (self.root.w >= (self.root.h + h))
if should_grow_right:
return self.grow_right(w, h)
elif should_grow_down:
return self.grow_down(w, h)
elif can_grow_right:
return self.grow_right(w, h)
elif can_grow_down:
return self.grow_down(w, h)
return None
def grow_right(self, w, h) -> Optional[Rectangle_Obj]:
self.root = Rectangle_Obj(
used=True,
x=0,
y=0,
w=self.root.w + w,
h=self.root.h,
down=self.root,
right=Rectangle_Obj(x=self.root.w, y=0, w=w, h=self.root.h))
node = self.root.find(w, h)
if node:
return node.split(w, h)
return None
def grow_down(self, w, h) -> Optional[Rectangle_Obj]:
self.root = Rectangle_Obj(
used=True,
x=0,
y=0,
w=self.root.w,
h=self.root.h + h,
down=Rectangle_Obj(x=0, y=self.root.h, w=self.root.w, h=h),
right=self.root
)
node = self.root.find(w, h)
if node:
return node.split(w, h)
return None
+271 -53
View File
@@ -14,15 +14,28 @@ from .logging_setup import logger
from .translations import t, get_languages_list, update_language
from .addon_preferences import get_preference, save_preference
from .updater import get_version_list
from .common import get_armature_list, get_active_armature, get_all_meshes
from .common import get_armature_list, get_active_armature, get_all_meshes, SceneMatClass
from ..functions.visemes import VisemePreview
from ..functions.eye_tracking import set_rotation
class ValidationMessageItem(PropertyGroup):
"""Property group for validation message items"""
name: StringProperty(name="Message")
class ZeroWeightBoneItem(PropertyGroup):
"""Property group for zero weight bone list items"""
name: StringProperty(name="Bone Name")
selected: BoolProperty(name="Selected", default=True)
has_children: BoolProperty(name="Has Children", default=False)
is_deform: BoolProperty(name="Is Deform Bone", default=False)
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
"""Updates validation mode and saves preference"""
logger.info(f"Updating validation mode to: {self.validation_mode}")
save_preference("validation_mode", self.validation_mode)
def update_logging_state(self: PropertyGroup, context: Context) -> None:
"""Updates logging state and configures logging"""
logger.info(f"Updating logging state to: {self.enable_logging}")
@@ -30,13 +43,142 @@ def update_logging_state(self: PropertyGroup, context: Context) -> None:
from .logging_setup import configure_logging
configure_logging(self.enable_logging)
def update_shape_intensity(self: PropertyGroup, context: Context) -> None:
"""Updates shape key intensity and refreshes preview"""
if self.viseme_preview_mode:
VisemePreview.update_preview(context)
def highlight_problem_bones(self: PropertyGroup, context: Context) -> None:
"""Updates problem bone highlighting state and saves preference"""
logger.info(f"Updating problem bone highlighting to: {self.highlight_problem_bones}")
save_preference("highlight_problem_bones", self.highlight_problem_bones)
def get_mesh_objects(self, context):
meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH']
if not meshes:
return [('NONE', t("Visemes.no_meshes"), '')]
return meshes
class AvatarToolkitSceneProperties(PropertyGroup):
"""Property group containing Avatar Toolkit scene-level settings and properties"""
show_found_bones: BoolProperty(
name="Show Found Bones",
default=False
)
show_non_standard: BoolProperty(
name="Show Non-Standard Bones",
default=False
)
show_hierarchy: BoolProperty(
name="Show Hierarchy Issues",
default=False
)
material_search_filter: StringProperty(
name=t("TextureAtlas.search_materials"),
description=t("TextureAtlas.search_materials_desc"),
default=""
)
def get_texture_node_list(self: Material, context: Context) -> list[tuple]:
if self.use_nodes:
Object.Enum = [((i.image.name if i.image else i.name+"_image"),
(i.image.name if i.image else "node with no image..."),
(i.image.name if i.image else i.name), index+1)
for index, i in enumerate(self.node_tree.nodes)
if i.bl_idname == "ShaderNodeTexImage"]
if not len(Object.Enum):
Object.Enum = [(t("TextureAtlas.error.label"),
t("TextureAtlas.no_images_error.desc"),
t("TextureAtlas.error.label"), 0)]
else:
Object.Enum = [(t("TextureAtlas.error.label"),
t("TextureAtlas.no_nodes_error.desc"),
t("TextureAtlas.error.label"), 0)]
Object.Enum.append((t("TextureAtlas.none.label"),
t("TextureAtlas.none.label"),
t("TextureAtlas.none.label"), 0))
return Object.Enum
Material.texture_atlas_albedo = EnumProperty(
name=t("TextureAtlas.albedo"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.albedo").lower()),
default=0,
items=get_texture_node_list
)
Material.texture_atlas_normal = EnumProperty(
name=t("TextureAtlas.normal"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.normal").lower()),
default=0,
items=get_texture_node_list
)
Material.texture_atlas_emission = EnumProperty(
name=t("TextureAtlas.emission"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.emission").lower()),
default=0,
items=get_texture_node_list
)
Material.texture_atlas_ambient_occlusion = EnumProperty(
name=t("TextureAtlas.ambient_occlusion"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.ambient_occlusion").lower()),
default=0,
items=get_texture_node_list
)
Material.texture_atlas_height = EnumProperty(
name=t("TextureAtlas.height"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()),
default=0,
items=get_texture_node_list
)
Material.texture_atlas_roughness = EnumProperty(
name=t("TextureAtlas.roughness"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.roughness").lower()),
default=0,
items=get_texture_node_list
)
list_only_mode: BoolProperty(
name=t("Tools.list_only_mode"),
description=t("Tools.list_only_mode_desc"),
default=False
)
Material.include_in_atlas = BoolProperty(
name=t("TextureAtlas.include_in_atlas"),
description=t("TextureAtlas.include_in_atlas_desc"),
default=False
)
Material.material_expanded = BoolProperty(
name=t("TextureAtlas.material_expanded"),
description=t("TextureAtlas.material_expanded_desc"),
default=False
)
texture_atlas_Has_Mat_List_Shown: BoolProperty(
name=t("TextureAtlas.list_shown"),
description=t("TextureAtlas.list_shown_desc"),
default=False
)
texture_atlas_material_index: IntProperty(
default=-1,
get=lambda self: -1,
set=lambda self, context: None
)
materials: CollectionProperty(
type=SceneMatClass
)
avatar_toolkit_updater_version_list: EnumProperty(
items=get_version_list,
@@ -151,9 +293,10 @@ class AvatarToolkitSceneProperties(PropertyGroup):
description=t("Visemes.mouth_ch_desc")
)
viseme_mesh: StringProperty(
viseme_mesh: EnumProperty(
name=t("Visemes.mesh_select"),
description=t("Visemes.mesh_select_desc"),
items=get_mesh_objects
)
shape_intensity: FloatProperty(
@@ -167,38 +310,37 @@ class AvatarToolkitSceneProperties(PropertyGroup):
)
viseme_preview_selection: EnumProperty(
name=t("Visemes.preview_selection"),
description=t("Visemes.preview_selection_desc"),
items=[
('vrc.v_aa', 'AA', 'A as in "bat"'),
('vrc.v_ch', 'CH', 'Ch as in "choose"'),
('vrc.v_dd', 'DD', 'D as in "dog"'),
('vrc.v_ih', 'IH', 'I as in "bit"'),
('vrc.v_ff', 'FF', 'F as in "fox"'),
('vrc.v_e', 'E', 'E as in "bet"'),
('vrc.v_kk', 'KK', 'K as in "cat"'),
('vrc.v_nn', 'NN', 'N as in "net"'),
('vrc.v_oh', 'OH', 'O as in "hot"'),
('vrc.v_ou', 'OU', 'O as in "go"'),
('vrc.v_pp', 'PP', 'P as in "pat"'),
('vrc.v_rr', 'RR', 'R as in "red"'),
('vrc.v_sil', 'SIL', 'Silence'),
('vrc.v_ss', 'SS', 'S as in "sit"'),
('vrc.v_th', 'TH', 'Th as in "think"')
],
update=lambda s, c: VisemePreview.update_preview(c)
)
name=t("Visemes.preview_selection"),
description=t("Visemes.preview_selection_desc"),
items=[
('vrc.v_aa', 'AA', 'A as in "bat"'),
('vrc.v_ch', 'CH', 'Ch as in "choose"'),
('vrc.v_dd', 'DD', 'D as in "dog"'),
('vrc.v_ih', 'IH', 'I as in "bit"'),
('vrc.v_ff', 'FF', 'F as in "fox"'),
('vrc.v_e', 'E', 'E as in "bet"'),
('vrc.v_kk', 'KK', 'K as in "cat"'),
('vrc.v_nn', 'NN', 'N as in "net"'),
('vrc.v_oh', 'OH', 'O as in "hot"'),
('vrc.v_ou', 'OU', 'O as in "go"'),
('vrc.v_pp', 'PP', 'P as in "pat"'),
('vrc.v_rr', 'RR', 'R as in "red"'),
('vrc.v_sil', 'SIL', 'Silence'),
('vrc.v_ss', 'SS', 'S as in "sit"'),
('vrc.v_th', 'TH', 'Th as in "think"')
],
update=lambda s, c: VisemePreview.update_preview(c)
)
eye_tracking_type: EnumProperty(
name=t("EyeTracking.type"),
description=t("EyeTracking.type_desc"),
items=[
('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")),
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
],
default='AV3'
)
name=t("EyeTracking.type"),
description=t("EyeTracking.type_desc"),
items=[
('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")),
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
],
default='AV3'
)
eye_mode: EnumProperty(
name=t("EyeTracking.mode"),
@@ -337,12 +479,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=""
)
merge_all_bones: BoolProperty(
name=t('MergeArmature.merge_all'),
description=t('MergeArmature.merge_all_desc'),
default=True
)
apply_transforms: BoolProperty(
name=t('MergeArmature.apply_transforms'),
description=t('MergeArmature.apply_transforms_desc'),
@@ -361,33 +497,115 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=True
)
preserve_parent_bones: BoolProperty(
name=t("Tools.preserve_parent_bones"),
description=t("Tools.preserve_parent_bones_desc"),
default=True
)
target_bone_type: EnumProperty(
name=t("Tools.target_bone_type"),
description=t("Tools.target_bone_type_desc"),
items=[
('ALL', t("Tools.target_all_bones"), ""),
('DEFORM', t("Tools.target_deform_bones"), ""),
('NON_DEFORM', t("Tools.target_non_deform_bones"), "")
],
default='ALL'
)
zero_weight_bones: CollectionProperty(
type=ZeroWeightBoneItem,
name="Zero Weight Bones",
description="List of bones with zero weights"
)
zero_weight_bones_index: IntProperty(
name="Zero Weight Bone Index",
default=0
)
list_only_mode: BoolProperty(
name=t("Tools.list_only_mode"),
description=t("Tools.list_only_mode_desc"),
default=False
)
cleanup_shape_keys: BoolProperty(
name=t('MergeArmature.cleanup_shape_keys'),
description=t('MergeArmature.cleanup_shape_keys_desc'),
default=True
)
merge_twist_bones: BoolProperty(
name=t("Tools.merge_twist_bones"),
description=t("Tools.merge_twist_bones_desc"),
default=True
)
highlight_problem_bones: BoolProperty(
name=t("Settings.highlight_problem_bones"),
description=t("Settings.highlight_problem_bones_desc"),
default=get_preference("highlight_problem_bones", True),
update=highlight_problem_bones
)
show_scale_issues: BoolProperty(
name="Show Scale Issues",
default=False
)
tpose_validation_result: BoolProperty(
name="T-Pose Validation Result",
default=True
)
tpose_validation_messages: CollectionProperty(
type=bpy.types.PropertyGroup,
name="T-Pose Validation Messages"
)
show_tpose_validation: BoolProperty(
name="Show T-Pose Validation Results",
default=False
)
standardize_fix_names: BoolProperty(
name=t("Tools.standardize_fix_names"),
description=t("Tools.standardize_fix_names_desc"),
default=True
)
standardize_fix_hierarchy: BoolProperty(
name=t("Tools.standardize_fix_hierarchy"),
description=t("Tools.standardize_fix_hierarchy_desc"),
default=True
)
standardize_fix_scale: BoolProperty(
name=t("Tools.standardize_fix_scale"),
description=t("Tools.standardize_fix_scale_desc"),
default=True
)
def register() -> None:
"""Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties")
try:
bpy.utils.register_class(AvatarToolkitSceneProperties)
except ValueError:
# Class already registered, we can continue
pass
# Only register the property, not the classes (auto_load will handle that)
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
logger.debug("Properties registered successfully")
def unregister() -> None:
"""Unregister the Avatar Toolkit property group"""
logger.info("Unregistering Avatar Toolkit properties")
try:
del bpy.types.Scene.avatar_toolkit
except:
pass
try:
bpy.utils.unregister_class(AvatarToolkitSceneProperties)
except RuntimeError:
pass
logger.debug("Properties unregistered successfully")
# Remove the property
if hasattr(bpy.types.Scene, "avatar_toolkit"):
try:
del bpy.types.Scene.avatar_toolkit
logger.debug("Removed avatar_toolkit property")
except Exception as e:
logger.warning(f"Failed to remove avatar_toolkit property: {e}")
# Not fatal - continue
+2 -1
View File
@@ -10,6 +10,7 @@ from bpy.types import Context, Operator
from ..core.translations import t
from ..core.dictionaries import bone_names, resonite_translations
from ..core.logging_setup import logger
from ..core.armature_validation import validate_armature
from .resonite_loader import resonite_animx, resonite_types
@@ -51,7 +52,7 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
is_valid, _, _ = validate_armature(armature)
return is_valid
def execute(self, context: Context) -> Set[str]:
+77 -6
View File
@@ -17,11 +17,17 @@ from typing import Dict, List, Tuple, Optional, Set, Any
GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
# Define which version series this installation can update to
# 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
ALLOWED_VERSION_SERIES = ["0.2"]
is_checking_for_update: bool = False
update_needed: bool = False
latest_version: Optional[str] = None
latest_version_str: str = ''
version_list: Optional[Dict[str, List[str]]] = None
last_manual_check_time: float = 0
main_dir: str = os.path.dirname(os.path.dirname(__file__))
downloads_dir: str = os.path.join(main_dir, "downloads")
@@ -34,7 +40,9 @@ class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator):
bl_options = {'INTERNAL'}
def execute(self, context: bpy.types.Context) -> Set[str]:
global last_manual_check_time
check_for_update_background()
last_manual_check_time = time.time() # Reset the timer on manual check
return {'FINISHED'}
@@ -80,7 +88,16 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: bpy.types.Context) -> None:
global last_manual_check_time
layout = self.layout
# Auto-check for updates when panel is drawn, but not too frequently
current_time = time.time()
if current_time - last_manual_check_time > 300: # 5 minutes between auto-checks
if not is_checking_for_update and not update_needed:
check_for_update_background()
last_manual_check_time = current_time
draw_updater_panel(context, layout)
@@ -158,11 +175,23 @@ def get_github_releases() -> bool:
return True
def check_for_update_available() -> bool:
global latest_version, latest_version_str
global latest_version, latest_version_str, version_list
if not version_list:
return False
latest_version = max(version_list.keys(), key=lambda v: [int(x) for x in v.split('.')])
# Filter versions by allowed version series
compatible_versions = {}
for v, info in version_list.items():
for prefix in ALLOWED_VERSION_SERIES:
if v.startswith(prefix):
compatible_versions[v] = info
break
if not compatible_versions:
print(f"No compatible versions found in series: {', '.join(ALLOWED_VERSION_SERIES)}")
return False
latest_version = max(compatible_versions.keys(), key=lambda v: [int(x) for x in v.split('.')])
latest_version_str = latest_version
current_version = get_current_version()
@@ -195,11 +224,37 @@ def update_now(latest: bool = False) -> None:
if not version_list:
print("No version list available. Please check for updates first.")
return
if latest:
update_link = version_list[latest_version_str][0]
# Filter compatible versions
compatible_versions = {}
for v, info in version_list.items():
for prefix in ALLOWED_VERSION_SERIES:
if v.startswith(prefix):
compatible_versions[v] = info
break
if not compatible_versions:
print(f"No compatible versions found in series: {', '.join(ALLOWED_VERSION_SERIES)}")
return
latest_compatible = max(compatible_versions.keys(), key=lambda v: [int(x) for x in v.split('.')])
update_link = version_list[latest_compatible][0]
else:
update_link = version_list[bpy.context.scene.avatar_toolkit_updater_version_list][0]
selected_version = bpy.context.scene.avatar_toolkit_updater_version_list
# Check if selected version is compatible
is_compatible = False
for prefix in ALLOWED_VERSION_SERIES:
if selected_version.startswith(prefix):
is_compatible = True
break
if not is_compatible:
print(f"Selected version {selected_version} is not in allowed series: {', '.join(ALLOWED_VERSION_SERIES)}")
return
update_link = version_list[selected_version][0]
download_file(update_link)
ui_refresh()
@@ -274,7 +329,17 @@ def finish_update(error: str = '') -> None:
ui_refresh()
def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
return [(v, v, '') for v in version_list.keys()] if version_list else []
if not version_list:
return []
compatible_versions = []
for v in version_list.keys():
for prefix in ALLOWED_VERSION_SERIES:
if v.startswith(prefix):
compatible_versions.append(v)
break
return [(v, v, '') for v in compatible_versions]
def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
box = layout.box()
@@ -287,6 +352,12 @@ def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -
col.separator()
# Show compatibility info
col.label(text=f"Update series: {', '.join(s + '.x' for s in ALLOWED_VERSION_SERIES)}", icon='INFO')
col.label(text=f"Blender version: {bpy.app.version_string}", icon='BLENDER')
col.separator()
# Update check/status section
if is_checking_for_update:
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,