Attach Meshes
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from mathutils import Vector
|
||||||
from bpy.types import Context, Object, Modifier, EditBone, Operator
|
from bpy.types import Context, Object, Modifier, EditBone, Operator
|
||||||
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable
|
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable
|
||||||
from ..core.logging_setup import logger
|
from ..core.logging_setup import logger
|
||||||
@@ -592,3 +593,26 @@ def fix_zero_length_bones(armature: Object) -> None:
|
|||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_bone_orientation(mesh, vertices):
|
||||||
|
"""Calculate optimal bone orientation based on mesh geometry."""
|
||||||
|
|
||||||
|
if not vertices:
|
||||||
|
return Vector((0, 0, 0.1)), 0.0
|
||||||
|
|
||||||
|
coords = [mesh.data.vertices[v.index].co for v in vertices]
|
||||||
|
min_co = Vector(map(min, zip(*coords)))
|
||||||
|
max_co = Vector(map(max, zip(*coords)))
|
||||||
|
dimensions = max_co - min_co
|
||||||
|
|
||||||
|
roll_angle = 0.0
|
||||||
|
|
||||||
|
return dimensions, roll_angle
|
||||||
|
|
||||||
|
def add_armature_modifier(mesh: Object, armature: Object):
|
||||||
|
"""Add armature modifier to mesh."""
|
||||||
|
for mod in mesh.modifiers:
|
||||||
|
if mod.type == 'ARMATURE':
|
||||||
|
mesh.modifiers.remove(mod)
|
||||||
|
|
||||||
|
modifier = mesh.modifiers.new('Armature', 'ARMATURE')
|
||||||
|
modifier.object = armature
|
||||||
+28
-18
@@ -304,59 +304,69 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
merge_armature_into: StringProperty(
|
merge_armature_into: StringProperty(
|
||||||
name=t('CustomPanel.merge_into'),
|
name=t('MergeArmature.into'),
|
||||||
description=t('CustomPanel.merge_into_desc'),
|
description=t('MergeArmature.into_desc'),
|
||||||
default=""
|
default=""
|
||||||
)
|
)
|
||||||
|
|
||||||
merge_armature: StringProperty(
|
merge_armature: StringProperty(
|
||||||
name=t('CustomPanel.merge_from'),
|
name=t('MergeArmature.from'),
|
||||||
description=t('CustomPanel.merge_from_desc'),
|
description=t('MergeArmature.from_desc'),
|
||||||
default=""
|
default=""
|
||||||
)
|
)
|
||||||
|
|
||||||
attach_mesh: StringProperty(
|
attach_mesh: StringProperty(
|
||||||
name=t('CustomPanel.attach_mesh'),
|
name=t('AttachMesh.select'),
|
||||||
description=t('CustomPanel.attach_mesh_desc'),
|
description=t('AttachMesh.select_desc'),
|
||||||
default=""
|
default=""
|
||||||
)
|
)
|
||||||
|
|
||||||
attach_bone: StringProperty(
|
attach_bone: StringProperty(
|
||||||
name=t('CustomPanel.attach_bone'),
|
name=t('AttachBone.select'),
|
||||||
description=t('CustomPanel.attach_bone_desc'),
|
description=t('AttachBone.select_desc'),
|
||||||
default=""
|
default=""
|
||||||
)
|
)
|
||||||
|
|
||||||
merge_all_bones: BoolProperty(
|
merge_all_bones: BoolProperty(
|
||||||
name=t('CustomPanel.merge_all_bones'),
|
name=t('MergeArmature.merge_all'),
|
||||||
description=t('CustomPanel.merge_all_bones_desc'),
|
description=t('MergeArmature.merge_all_desc'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
apply_transforms: BoolProperty(
|
apply_transforms: BoolProperty(
|
||||||
name=t('CustomPanel.apply_transforms'),
|
name=t('MergeArmature.apply_transforms'),
|
||||||
description=t('CustomPanel.apply_transforms_desc'),
|
description=t('MergeArmature.apply_transforms_desc'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
join_meshes: BoolProperty(
|
join_meshes: BoolProperty(
|
||||||
name=t('CustomPanel.join_meshes'),
|
name=t('MergeArmature.join_meshes'),
|
||||||
description=t('CustomPanel.join_meshes_desc'),
|
description=t('MergeArmature.join_meshes_desc'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
remove_zero_weights: BoolProperty(
|
remove_zero_weights: BoolProperty(
|
||||||
name=t('CustomPanel.remove_zero_weights'),
|
name=t('MergeArmature.remove_zero_weights'),
|
||||||
description=t('CustomPanel.remove_zero_weights_desc'),
|
description=t('MergeArmature.remove_zero_weights_desc'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
cleanup_shape_keys: BoolProperty(
|
cleanup_shape_keys: BoolProperty(
|
||||||
name=t('CustomPanel.cleanup_shape_keys'),
|
name=t('MergeArmature.cleanup_shape_keys'),
|
||||||
description=t('CustomPanel.cleanup_shape_keys_desc'),
|
description=t('MergeArmature.cleanup_shape_keys_desc'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
attach_mesh: StringProperty(
|
||||||
|
name=t("Tools.attach_mesh_select"),
|
||||||
|
description=t("Tools.attach_mesh_select_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
attach_bone: StringProperty(
|
||||||
|
name=t("Tools.attach_bone_select"),
|
||||||
|
description=t("Tools.attach_bone_select_desc")
|
||||||
|
)
|
||||||
|
|
||||||
def register() -> None:
|
def register() -> None:
|
||||||
"""Register the Avatar Toolkit property group"""
|
"""Register the Avatar Toolkit property group"""
|
||||||
logger.info("Registering Avatar Toolkit properties")
|
logger.info("Registering Avatar Toolkit properties")
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from typing import List, Optional, Dict, Set
|
from typing import List, Optional, Dict, Set
|
||||||
from mathutils import Vector
|
|
||||||
from bpy.types import Context, Object, Operator
|
from bpy.types import Context, Object, Operator
|
||||||
|
|
||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
fix_zero_length_bones,
|
fix_zero_length_bones,
|
||||||
clear_unused_data_blocks,
|
clear_unused_data_blocks,
|
||||||
validate_armature,
|
|
||||||
join_mesh_objects,
|
join_mesh_objects,
|
||||||
fix_uv_coordinates,
|
|
||||||
remove_unused_shapekeys
|
remove_unused_shapekeys
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +36,7 @@ class AvatarToolkit_OT_MergeArmature(Operator):
|
|||||||
|
|
||||||
if not base_armature or not merge_armature:
|
if not base_armature or not merge_armature:
|
||||||
logger.error(f"Armature not found: {merge_armature_name}")
|
logger.error(f"Armature not found: {merge_armature_name}")
|
||||||
self.report({'ERROR'}, t('MergeArmature.error.notFound', name=merge_armature_name))
|
self.report({'ERROR'}, t('MergeArmature.error.not_found', name=merge_armature_name))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
# Remove Rigid Bodies and Joints
|
# Remove Rigid Bodies and Joints
|
||||||
@@ -80,21 +76,6 @@ class AvatarToolkit_OT_MergeArmature(Operator):
|
|||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, str(e))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
def calculate_bone_orientation(mesh, vertices):
|
|
||||||
"""Calculate optimal bone orientation based on mesh geometry."""
|
|
||||||
|
|
||||||
if not vertices:
|
|
||||||
return Vector((0, 0, 0.1)), 0.0
|
|
||||||
|
|
||||||
coords = [mesh.data.vertices[v.index].co for v in vertices]
|
|
||||||
min_co = Vector(map(min, zip(*coords)))
|
|
||||||
max_co = Vector(map(max, zip(*coords)))
|
|
||||||
dimensions = max_co - min_co
|
|
||||||
|
|
||||||
roll_angle = 0.0
|
|
||||||
|
|
||||||
return dimensions, roll_angle
|
|
||||||
|
|
||||||
def delete_rigidbodies_and_joints(armature: Object):
|
def delete_rigidbodies_and_joints(armature: Object):
|
||||||
"""Delete rigid bodies and joints associated with the armature."""
|
"""Delete rigid bodies and joints associated with the armature."""
|
||||||
to_delete = []
|
to_delete = []
|
||||||
@@ -398,15 +379,6 @@ def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str):
|
|||||||
vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE')
|
vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE')
|
||||||
mesh.vertex_groups.remove(vg_from)
|
mesh.vertex_groups.remove(vg_from)
|
||||||
|
|
||||||
def add_armature_modifier(mesh: Object, armature: Object):
|
|
||||||
"""Add armature modifier to mesh."""
|
|
||||||
for mod in mesh.modifiers:
|
|
||||||
if mod.type == 'ARMATURE':
|
|
||||||
mesh.modifiers.remove(mod)
|
|
||||||
|
|
||||||
modifier = mesh.modifiers.new('Armature', 'ARMATURE')
|
|
||||||
modifier.object = armature
|
|
||||||
|
|
||||||
def remove_unused_vertex_groups(mesh: Object):
|
def remove_unused_vertex_groups(mesh: Object):
|
||||||
"""Remove vertex groups with no weights."""
|
"""Remove vertex groups with no weights."""
|
||||||
for vg in mesh.vertex_groups:
|
for vg in mesh.vertex_groups:
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import Operator, Context, Object
|
||||||
|
from mathutils import Vector
|
||||||
|
from typing import Set, Optional
|
||||||
|
|
||||||
|
from ...core.logging_setup import logger
|
||||||
|
from ...core.translations import t
|
||||||
|
from ...core.common import (
|
||||||
|
get_active_armature,
|
||||||
|
validate_armature,
|
||||||
|
get_all_meshes,
|
||||||
|
ProgressTracker,
|
||||||
|
calculate_bone_orientation,
|
||||||
|
add_armature_modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_AttachMesh(Operator):
|
||||||
|
"""Attach a mesh to an armature bone with automatic weight setup"""
|
||||||
|
bl_idname = "avatar_toolkit.attach_mesh"
|
||||||
|
bl_label = t("AttachMesh.label")
|
||||||
|
bl_description = t("AttachMesh.desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
return armature is not None and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
try:
|
||||||
|
logger.info("Starting mesh attachment process")
|
||||||
|
|
||||||
|
mesh_name = context.scene.avatar_toolkit.attach_mesh
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
attach_bone_name = context.scene.avatar_toolkit.attach_bone
|
||||||
|
mesh = bpy.data.objects.get(mesh_name)
|
||||||
|
|
||||||
|
with ProgressTracker(context, 10, "Attaching Mesh") as progress:
|
||||||
|
# Validation steps
|
||||||
|
is_valid, error_msg = validate_mesh_transforms(mesh)
|
||||||
|
if not is_valid:
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
progress.step(t("AttachMesh.validate_transforms"))
|
||||||
|
|
||||||
|
is_valid, error_msg = validate_mesh_name(armature, mesh_name)
|
||||||
|
if not is_valid:
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
progress.step(t("AttachMesh.validate_name"))
|
||||||
|
|
||||||
|
# Parent mesh to armature
|
||||||
|
mesh.parent = armature
|
||||||
|
mesh.parent_type = 'OBJECT'
|
||||||
|
progress.step(t("AttachMesh.parent_mesh"))
|
||||||
|
|
||||||
|
# Setup vertex groups
|
||||||
|
if mesh.vertex_groups:
|
||||||
|
for vg in mesh.vertex_groups:
|
||||||
|
mesh.vertex_groups.remove(vg)
|
||||||
|
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
mesh.select_set(True)
|
||||||
|
context.view_layer.objects.active = mesh
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bpy.ops.mesh.select_all(action='SELECT')
|
||||||
|
vg = mesh.vertex_groups.new(name=mesh_name)
|
||||||
|
bpy.ops.object.vertex_group_assign()
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
progress.step(t("AttachMesh.setup_weights"))
|
||||||
|
|
||||||
|
# Create and setup bone
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
armature.select_set(True)
|
||||||
|
context.view_layer.objects.active = armature
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
attach_to_bone = armature.data.edit_bones.get(attach_bone_name)
|
||||||
|
if not attach_to_bone:
|
||||||
|
raise ValueError(t("AttachMesh.error.bone_not_found", bone=attach_bone_name))
|
||||||
|
|
||||||
|
mesh_bone = armature.data.edit_bones.new(mesh_name)
|
||||||
|
mesh_bone.parent = attach_to_bone
|
||||||
|
progress.step(t("AttachMesh.create_bone"))
|
||||||
|
|
||||||
|
# Calculate bone placement
|
||||||
|
verts_in_group = [v for v in mesh.data.vertices
|
||||||
|
for g in v.groups if g.group == vg.index]
|
||||||
|
dimensions, roll_angle = calculate_bone_orientation(mesh, verts_in_group)
|
||||||
|
|
||||||
|
# Set bone position and orientation
|
||||||
|
center = Vector((0, 0, 0))
|
||||||
|
for v in verts_in_group:
|
||||||
|
center += mesh.data.vertices[v.index].co
|
||||||
|
center /= len(verts_in_group)
|
||||||
|
|
||||||
|
mesh_bone.head = center
|
||||||
|
mesh_bone.tail = center + Vector((0, 0, max(0.1, dimensions.z)))
|
||||||
|
mesh_bone.roll = roll_angle
|
||||||
|
progress.step(t("AttachMesh.position_bone"))
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
add_armature_modifier(mesh, armature)
|
||||||
|
progress.step(t("AttachMesh.add_modifier"))
|
||||||
|
|
||||||
|
logger.info(f"Successfully attached mesh {mesh_name} to bone {attach_bone_name}")
|
||||||
|
self.report({'INFO'}, t("AttachMesh.success"))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to attach mesh: {str(e)}")
|
||||||
|
self.report({'ERROR'}, str(e))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
def validate_mesh_transforms(mesh):
|
||||||
|
"""Validate mesh transforms are suitable for attaching."""
|
||||||
|
if not mesh:
|
||||||
|
return False, "Mesh not found"
|
||||||
|
|
||||||
|
# Check for non-uniform scale
|
||||||
|
scale = mesh.scale
|
||||||
|
if abs(scale[0] - scale[1]) > 0.001 or abs(scale[1] - scale[2]) > 0.001:
|
||||||
|
return False, "Mesh has non-uniform scale. Please apply scale (Ctrl+A)"
|
||||||
|
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def validate_mesh_name(armature, mesh_name):
|
||||||
|
"""Validate mesh name doesn't conflict with existing bones."""
|
||||||
|
if mesh_name in armature.data.bones:
|
||||||
|
return False, f"Bone named '{mesh_name}' already exists in armature"
|
||||||
|
return True, ""
|
||||||
@@ -317,48 +317,64 @@
|
|||||||
|
|
||||||
"CustomPanel.label": "Custom Avatar Tools",
|
"CustomPanel.label": "Custom Avatar Tools",
|
||||||
"CustomPanel.merge_mode": "Merge Mode",
|
"CustomPanel.merge_mode": "Merge Mode",
|
||||||
"CustomPanel.merge_mode_desc": "Select mode for merging operations",
|
"CustomPanel.mesh_selection": "Mesh Selection",
|
||||||
|
"CustomPanel.select_mesh": "Select Mesh",
|
||||||
|
"CustomPanel.select_bone": "Select Bone",
|
||||||
|
"CustomPanel.select_armature": "Select Armature",
|
||||||
"CustomPanel.mode.armature": "Armature",
|
"CustomPanel.mode.armature": "Armature",
|
||||||
"CustomPanel.mode.armature_desc": "Merge armatures together",
|
"CustomPanel.mode.armature_desc": "Merge armatures together",
|
||||||
"CustomPanel.mode.mesh": "Mesh",
|
"CustomPanel.mode.mesh": "Mesh",
|
||||||
"CustomPanel.mode.mesh_desc": "Attach meshes to armature",
|
"CustomPanel.mode.mesh_desc": "Attach meshes to armature",
|
||||||
"CustomPanel.mergeArmatures": "Merge Armatures",
|
|
||||||
"CustomPanel.warn.twoArmatures": "Need at least two armatures to merge",
|
"AttachMesh.label": "Attach Mesh",
|
||||||
"CustomPanel.warn.noArmOrMesh1": "No armature or meshes found",
|
"AttachMesh.desc": "Attach a mesh to an armature bone with automatic weight setup",
|
||||||
"CustomPanel.warn.noArmOrMesh2": "Please add required objects first",
|
"AttachMesh.search_desc": "Search for meshes to attach",
|
||||||
"CustomPanel.merge_into": "Merge Into",
|
"AttachMesh.select": "Select Mesh to Attach",
|
||||||
"CustomPanel.merge_into_desc": "Target armature to merge into",
|
"AttachMesh.select_desc": "Choose a mesh to attach to the armature",
|
||||||
"CustomPanel.merge_from": "Merge From",
|
"AttachMesh.success": "Mesh attached successfully",
|
||||||
"CustomPanel.merge_from_desc": "Source armature to merge",
|
"AttachMesh.warn_no_armature": "Select an armature and mesh to attach",
|
||||||
"CustomPanel.toMerge": "To Merge",
|
"AttachMesh.validate_transforms": "Validating mesh transforms",
|
||||||
"CustomPanel.attachMesh1": "Attach Mesh",
|
"AttachMesh.validate_name": "Validating mesh name",
|
||||||
"CustomPanel.attachMesh2": "Select Mesh",
|
"AttachMesh.parent_mesh": "Parenting mesh to armature",
|
||||||
"CustomPanel.attach_mesh": "Mesh to Attach",
|
"AttachMesh.setup_weights": "Setting up vertex weights",
|
||||||
"CustomPanel.attach_mesh_desc": "Select mesh to attach",
|
"AttachMesh.create_bone": "Creating attachment bone",
|
||||||
"CustomPanel.attachToBone": "Attach to Bone",
|
"AttachMesh.position_bone": "Positioning bone",
|
||||||
"CustomPanel.attach_bone": "Target Bone",
|
"AttachMesh.add_modifier": "Adding armature modifier",
|
||||||
"CustomPanel.attach_bone_desc": "Select bone to attach to",
|
"AttachMesh.error.bone_not_found": "Attach bone '{bone}' not found",
|
||||||
"CustomPanel.merge_same_bones": "Merge Same Bones",
|
"AttachMesh.error.mesh_not_found": "Mesh not found",
|
||||||
"CustomPanel.merge_same_bones_desc": "Merge bones with matching names",
|
"AttachMesh.error.non_uniform_scale": "Mesh has non-uniform scale. Please apply scale",
|
||||||
"CustomPanel.apply_transforms": "Apply Transforms",
|
"AttachBone.search_desc": "Search for target bone",
|
||||||
"CustomPanel.apply_transforms_desc": "Apply all transformations before merging",
|
"AttachBone.select": "Select Target Bone",
|
||||||
"CustomPanel.join_meshes": "Join Meshes",
|
"AttachBone.select_desc": "Choose the bone to attach the mesh to",
|
||||||
"CustomPanel.join_meshes_desc": "Join meshes after merging",
|
|
||||||
"CustomPanel.remove_zero_weights": "Remove Zero Weights",
|
|
||||||
"CustomPanel.remove_zero_weights_desc": "Remove vertex groups with no weights",
|
|
||||||
"CustomPanel.cleanup_shape_keys": "Clean Shape Keys",
|
|
||||||
"CustomPanel.cleanup_shape_keys_desc": "Remove unused shape keys",
|
|
||||||
"CustomPanel.merge_all_bones": "Merge Same Bones",
|
|
||||||
"CustomPanel.merge_all_bones_desc": "Merge bones with matching names",
|
|
||||||
"CustomPanel.mergeInto": "Merge Into",
|
|
||||||
"MergeArmature.label": "Merge Armatures",
|
"MergeArmature.label": "Merge Armatures",
|
||||||
"MergeArmature.desc": "Merge two armatures together",
|
"MergeArmature.desc": "Merge two armatures together",
|
||||||
"MergeArmature.error.notFound": "Armature '{name}' not found",
|
"MergeArmature.options": "Merge Options",
|
||||||
"MergeArmature.success": "Armatures merged successfully",
|
"MergeArmature.warn_two": "Need at least two armatures to merge",
|
||||||
"MergeArmature.error.checkTransforms": "Please check parent transformations",
|
"MergeArmature.into": "Merge Into",
|
||||||
"MergeArmature.error.pleaseFix": "Please fix parent relationships",
|
"MergeArmature.into_desc": "Target armature to merge into",
|
||||||
|
"MergeArmature.into_search_desc": "Search for target armature",
|
||||||
|
"MergeArmature.from": "Merge From",
|
||||||
|
"MergeArmature.from_desc": "Source armature to merge from",
|
||||||
|
"MergeArmature.from_search_desc": "Search for source armature",
|
||||||
|
"MergeArmature.error.not_found": "Armature '{name}' not found",
|
||||||
"MergeArmature.error.transforms_not_aligned": "Transforms must be applied to merge this armature, either do this via the manual method or via apply transform checkmark",
|
"MergeArmature.error.transforms_not_aligned": "Transforms must be applied to merge this armature, either do this via the manual method or via apply transform checkmark",
|
||||||
|
"MergeArmature.error.check_transforms": "Please check parent transformations",
|
||||||
|
"MergeArmature.error.fix_parents": "Please fix parent relationships",
|
||||||
|
"MergeArmature.progress.removing_rigidbodies": "Removing rigid bodies and joints",
|
||||||
|
"MergeArmature.progress.validating": "Validating armatures",
|
||||||
|
"MergeArmature.progress.merging": "Merging armatures",
|
||||||
|
"MergeArmature.success": "Armatures merged successfully",
|
||||||
|
"MergeArmature.merge_all": "Merge Same Bones",
|
||||||
|
"MergeArmature.merge_all_desc": "Merge bones with matching names",
|
||||||
|
"MergeArmature.apply_transforms": "Apply Transforms",
|
||||||
|
"MergeArmature.apply_transforms_desc": "Apply all transformations before merging",
|
||||||
|
"MergeArmature.join_meshes": "Join Meshes",
|
||||||
|
"MergeArmature.join_meshes_desc": "Join meshes after merging",
|
||||||
|
"MergeArmature.remove_zero_weights": "Remove Zero Weights",
|
||||||
|
"MergeArmature.remove_zero_weights_desc": "Remove vertex groups with no weights",
|
||||||
|
"MergeArmature.cleanup_shape_keys": "Clean Shape Keys",
|
||||||
|
"MergeArmature.cleanup_shape_keys_desc": "Remove unused shape keys",
|
||||||
|
|
||||||
"Settings.label": "Settings",
|
"Settings.label": "Settings",
|
||||||
"Settings.language": "Language",
|
"Settings.language": "Language",
|
||||||
|
|||||||
+62
-66
@@ -13,13 +13,12 @@ from ..core.common import (
|
|||||||
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
|
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
|
||||||
bl_idname = "avatar_toolkit.search_merge_armature_into"
|
bl_idname = "avatar_toolkit.search_merge_armature_into"
|
||||||
bl_label = ""
|
bl_label = ""
|
||||||
bl_description = t('CustomPanel.search_merge_into_desc')
|
bl_description = t('MergeArmature.into_search_desc')
|
||||||
bl_property = "search_merge_armature_into_enum"
|
bl_property = "search_merge_armature_into_enum"
|
||||||
|
|
||||||
# Define the enum property within the operator class
|
|
||||||
search_merge_armature_into_enum: bpy.props.EnumProperty(
|
search_merge_armature_into_enum: bpy.props.EnumProperty(
|
||||||
name=t('CustomPanel.merge_into'),
|
name=t('MergeArmature.into'),
|
||||||
description=t('CustomPanel.merge_into_desc'),
|
description=t('MergeArmature.into_desc'),
|
||||||
items=get_armature_list
|
items=get_armature_list
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,12 +33,12 @@ class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
|
|||||||
class AvatarToolkit_OT_SearchMergeArmature(Operator):
|
class AvatarToolkit_OT_SearchMergeArmature(Operator):
|
||||||
bl_idname = "avatar_toolkit.search_merge_armature"
|
bl_idname = "avatar_toolkit.search_merge_armature"
|
||||||
bl_label = ""
|
bl_label = ""
|
||||||
bl_description = t('CustomPanel.search_merge_desc')
|
bl_description = t('MergeArmature.from_search_desc')
|
||||||
bl_property = "search_merge_armature_enum"
|
bl_property = "search_merge_armature_enum"
|
||||||
|
|
||||||
search_merge_armature_enum: bpy.props.EnumProperty(
|
search_merge_armature_enum: bpy.props.EnumProperty(
|
||||||
name=t('CustomPanel.merge_from'),
|
name=t('MergeArmature.from'),
|
||||||
description=t('CustomPanel.merge_from_desc'),
|
description=t('MergeArmature.from_desc'),
|
||||||
items=get_armature_list
|
items=get_armature_list
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,15 +53,17 @@ class AvatarToolkit_OT_SearchMergeArmature(Operator):
|
|||||||
class AvatarToolkit_OT_SearchAttachMesh(Operator):
|
class AvatarToolkit_OT_SearchAttachMesh(Operator):
|
||||||
bl_idname = "avatar_toolkit.search_attach_mesh"
|
bl_idname = "avatar_toolkit.search_attach_mesh"
|
||||||
bl_label = ""
|
bl_label = ""
|
||||||
bl_description = t('CustomPanel.search_mesh_desc')
|
bl_description = t('AttachMesh.search_desc')
|
||||||
bl_property = "search_attach_mesh_enum"
|
bl_property = "search_attach_mesh_enum"
|
||||||
|
|
||||||
search_attach_mesh_enum: bpy.props.EnumProperty(
|
search_attach_mesh_enum: bpy.props.EnumProperty(
|
||||||
name=t('CustomPanel.attach_mesh'),
|
name=t('AttachMesh.select'),
|
||||||
description=t('CustomPanel.attach_mesh_desc'),
|
description=t('AttachMesh.select_desc'),
|
||||||
items=lambda self, context: [
|
items=lambda self, context: [
|
||||||
(obj.name, obj.name, "")
|
(obj.name, obj.name, "")
|
||||||
for obj in get_all_meshes(context)
|
for obj in bpy.data.objects
|
||||||
|
if obj.type == 'MESH'
|
||||||
|
and not any(mod.type == 'ARMATURE' for mod in obj.modifiers)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,12 +78,12 @@ class AvatarToolkit_OT_SearchAttachMesh(Operator):
|
|||||||
class AvatarToolkit_OT_SearchAttachBone(Operator):
|
class AvatarToolkit_OT_SearchAttachBone(Operator):
|
||||||
bl_idname = "avatar_toolkit.search_attach_bone"
|
bl_idname = "avatar_toolkit.search_attach_bone"
|
||||||
bl_label = ""
|
bl_label = ""
|
||||||
bl_description = t('CustomPanel.search_bone_desc')
|
bl_description = t('AttachBone.search_desc')
|
||||||
bl_property = "search_attach_bone_enum"
|
bl_property = "search_attach_bone_enum"
|
||||||
|
|
||||||
search_attach_bone_enum: bpy.props.EnumProperty(
|
search_attach_bone_enum: bpy.props.EnumProperty(
|
||||||
name=t('CustomPanel.attach_bone'),
|
name=t('AttachBone.select'),
|
||||||
description=t('CustomPanel.attach_bone_desc'),
|
description=t('AttachBone.select_desc'),
|
||||||
items=lambda self, context: [
|
items=lambda self, context: [
|
||||||
(bone.name, bone.name, "")
|
(bone.name, bone.name, "")
|
||||||
for bone in get_active_armature(context).data.bones
|
for bone in get_active_armature(context).data.bones
|
||||||
@@ -109,7 +110,6 @@ class AvatarToolKit_PT_CustomPanel(Panel):
|
|||||||
bl_options = {'DEFAULT_CLOSED'}
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the custom avatar tools panel interface"""
|
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
@@ -119,113 +119,109 @@ class AvatarToolKit_PT_CustomPanel(Panel):
|
|||||||
col.label(text=t('CustomPanel.merge_mode'), icon='TOOL_SETTINGS')
|
col.label(text=t('CustomPanel.merge_mode'), icon='TOOL_SETTINGS')
|
||||||
col.separator(factor=0.5)
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
# Create a row for the mode buttons with increased scale
|
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.scale_y = 1.5
|
row.scale_y = 1.5
|
||||||
row.prop(toolkit, "merge_mode", expand=True)
|
row.prop(toolkit, "merge_mode", expand=True)
|
||||||
|
|
||||||
# Armature Merging Tools
|
|
||||||
if toolkit.merge_mode == 'ARMATURE':
|
if toolkit.merge_mode == 'ARMATURE':
|
||||||
self.draw_armature_tools(layout, context)
|
self.draw_armature_tools(layout, context)
|
||||||
# Mesh Attachment Tools
|
|
||||||
else:
|
else:
|
||||||
self.draw_mesh_tools(layout, context)
|
self.draw_mesh_tools(layout, context)
|
||||||
|
|
||||||
def draw_armature_tools(self, layout: UILayout, context: Context) -> None:
|
def draw_armature_tools(self, layout: UILayout, context: Context) -> None:
|
||||||
"""Draw the armature merging tools section"""
|
|
||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Merge Settings Box
|
# Merge Settings Box
|
||||||
settings_box: UILayout = layout.box()
|
settings_box: UILayout = layout.box()
|
||||||
col: UILayout = settings_box.column(align=True)
|
col: UILayout = settings_box.column(align=True)
|
||||||
col.label(text=t('CustomPanel.mergeArmatures'), icon='ARMATURE_DATA')
|
col.label(text=t('MergeArmature.label'), icon='ARMATURE_DATA')
|
||||||
col.separator(factor=0.5)
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
if len(get_armature_list(context)) <= 1:
|
if len(get_armature_list(context)) <= 1:
|
||||||
col.label(text=t('CustomPanel.warn.twoArmatures'), icon='INFO')
|
col.label(text=t('MergeArmature.warn_two'), icon='INFO')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Merge Options
|
# Options Box with better spacing
|
||||||
options_box: UILayout = layout.box()
|
options_box: UILayout = layout.box()
|
||||||
col: UILayout = options_box.column(align=True)
|
col: UILayout = options_box.column(align=True)
|
||||||
col.label(text=t('Tools.merge_title'), icon='SETTINGS')
|
col.label(text=t('MergeArmature.options'), icon='SETTINGS')
|
||||||
col.separator(factor=0.5)
|
col.separator(factor=0.5)
|
||||||
col.prop(toolkit, "merge_all_bones")
|
|
||||||
col.prop(toolkit, "apply_transforms")
|
|
||||||
col.prop(toolkit, "join_meshes")
|
|
||||||
col.prop(toolkit, "remove_zero_weights")
|
|
||||||
col.prop(toolkit, "cleanup_shape_keys")
|
|
||||||
|
|
||||||
# Armature Selection Box
|
# Group related options together
|
||||||
|
transform_col = col.column(align=True)
|
||||||
|
transform_col.prop(toolkit, "merge_all_bones")
|
||||||
|
transform_col.prop(toolkit, "apply_transforms")
|
||||||
|
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
cleanup_col = col.column(align=True)
|
||||||
|
cleanup_col.prop(toolkit, "join_meshes")
|
||||||
|
cleanup_col.prop(toolkit, "remove_zero_weights")
|
||||||
|
cleanup_col.prop(toolkit, "cleanup_shape_keys")
|
||||||
|
|
||||||
|
# Selection Box with consistent styling
|
||||||
selection_box: UILayout = layout.box()
|
selection_box: UILayout = layout.box()
|
||||||
col: UILayout = selection_box.column(align=True)
|
col: UILayout = selection_box.column(align=True)
|
||||||
col.label(text=t('QuickAccess.select_armature'), icon='BONE_DATA')
|
col.label(text=t('CustomPanel.select_armature'), icon='BONE_DATA')
|
||||||
col.separator(factor=0.5)
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
# Armature selection with better alignment
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.label(text=t('CustomPanel.mergeInto'))
|
row.label(text=t('MergeArmature.into'), icon='ARMATURE_DATA')
|
||||||
row.operator("avatar_toolkit.search_merge_armature_into",
|
row.operator("avatar_toolkit.search_merge_armature_into",
|
||||||
text=toolkit.merge_armature_into,
|
text=toolkit.merge_armature_into)
|
||||||
icon='ARMATURE_DATA')
|
|
||||||
|
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.label(text=t('CustomPanel.toMerge'))
|
row.label(text=t('MergeArmature.from'), icon='ARMATURE_DATA')
|
||||||
row.operator("avatar_toolkit.search_merge_armature",
|
row.operator("avatar_toolkit.search_merge_armature",
|
||||||
text=toolkit.merge_armature,
|
text=toolkit.merge_armature)
|
||||||
icon='ARMATURE_DATA')
|
|
||||||
|
|
||||||
# Merge Button
|
# Merge button with emphasis
|
||||||
merge_col: UILayout = layout.column(align=True)
|
merge_box: UILayout = layout.box()
|
||||||
merge_col.scale_y = 1.2
|
col = merge_box.column(align=True)
|
||||||
merge_col.operator("avatar_toolkit.merge_armatures", icon='ARMATURE_DATA')
|
row = col.row(align=True)
|
||||||
|
row.scale_y = 1.5
|
||||||
|
row.operator("avatar_toolkit.merge_armatures", icon='ARMATURE_DATA')
|
||||||
|
|
||||||
def draw_mesh_tools(self, layout: UILayout, context: Context) -> None:
|
def draw_mesh_tools(self, layout: UILayout, context: Context) -> None:
|
||||||
"""Draw the mesh attachment tools section"""
|
|
||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Mesh Tools Box
|
# Mesh Tools Box
|
||||||
tools_box: UILayout = layout.box()
|
tools_box: UILayout = layout.box()
|
||||||
col: UILayout = tools_box.column(align=True)
|
col: UILayout = tools_box.column(align=True)
|
||||||
col.label(text=t('CustomPanel.attachMesh1'), icon='MESH_DATA')
|
col.label(text=t('AttachMesh.label'), icon='MESH_DATA')
|
||||||
col.separator(factor=0.5)
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
if not get_active_armature(context) or not get_all_meshes(context):
|
if not get_active_armature(context) or not get_all_meshes(context):
|
||||||
col.label(text=t('CustomPanel.warn.noArmOrMesh1'), icon='INFO')
|
col.label(text=t('AttachMesh.warn_no_armature'), icon='INFO')
|
||||||
col.label(text=t('CustomPanel.warn.noArmOrMesh2'))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Mesh Options Box
|
# Selection Box with consistent styling
|
||||||
options_box: UILayout = layout.box()
|
|
||||||
col: UILayout = options_box.column(align=True)
|
|
||||||
col.label(text=t('Tools.merge_title'), icon='SETTINGS')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.prop(toolkit, "join_meshes")
|
|
||||||
|
|
||||||
# Selection Box
|
|
||||||
selection_box: UILayout = layout.box()
|
selection_box: UILayout = layout.box()
|
||||||
col: UILayout = selection_box.column(align=True)
|
col: UILayout = selection_box.column(align=True)
|
||||||
col.label(text=t('Tools.merge_title'), icon='OBJECT_DATA')
|
col.label(text=t('CustomPanel.mesh_selection'), icon='OBJECT_DATA')
|
||||||
col.separator(factor=0.5)
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
# Selection rows with icons and better alignment
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.label(text=t('CustomPanel.mergeInto'))
|
row.label(text=t('CustomPanel.select_armature'), icon='ARMATURE_DATA')
|
||||||
row.operator("avatar_toolkit.search_merge_armature_into",
|
row.operator("avatar_toolkit.search_merge_armature_into",
|
||||||
text=toolkit.merge_armature_into,
|
text=toolkit.merge_armature_into)
|
||||||
icon='ARMATURE_DATA')
|
|
||||||
|
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.label(text=t('CustomPanel.attachMesh2'))
|
row.label(text=t('CustomPanel.select_mesh'), icon='MESH_DATA')
|
||||||
row.operator("avatar_toolkit.search_attach_mesh",
|
row.operator("avatar_toolkit.search_attach_mesh",
|
||||||
text=toolkit.attach_mesh,
|
text=toolkit.attach_mesh)
|
||||||
icon='MESH_DATA')
|
|
||||||
|
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.label(text=t('CustomPanel.attachToBone'))
|
row.label(text=t('CustomPanel.select_bone'), icon='BONE_DATA')
|
||||||
row.operator("avatar_toolkit.search_attach_bone",
|
row.operator("avatar_toolkit.search_attach_bone",
|
||||||
text=toolkit.attach_bone,
|
text=toolkit.attach_bone)
|
||||||
icon='BONE_DATA')
|
|
||||||
|
# Attach button with emphasis
|
||||||
|
attach_box: UILayout = layout.box()
|
||||||
|
col = attach_box.column(align=True)
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.scale_y = 1.5
|
||||||
|
row.operator("avatar_toolkit.attach_mesh", icon='ARMATURE_DATA')
|
||||||
|
|
||||||
# Attach Button
|
|
||||||
attach_col: UILayout = layout.column(align=True)
|
|
||||||
attach_col.scale_y = 1.2
|
|
||||||
attach_col.operator("avatar_toolkit.attach_mesh", icon='ARMATURE_DATA')
|
|
||||||
|
|||||||
Reference in New Issue
Block a user