diff --git a/__init__.py b/__init__.py index b3474ab..ea24b53 100644 --- a/__init__.py +++ b/__init__.py @@ -1,18 +1,3 @@ -""" -Armature Mesher - Blender Addon -Converts a selected armature into an envelope-style mesh using each bone's -head/tail radius values (exactly like Envelope display mode). - -Supports: - - Object mode → rest pose (arm.bones) - - Pose mode → current pose (obj.pose.bones) - - Edit mode → current edit-bone positions - -Geometry: - - UV-sphere at head and tail (head_radius / tail_radius) - - Capped frustum cylinder connecting the two spheres -""" - import bpy import bmesh import math @@ -37,22 +22,36 @@ def _rotation_matrix_to_axis(target_axis: Vector) -> Matrix: return Matrix.Rotation(angle, 4, cross.normalized()) -def add_sphere(bm: bmesh.types.BMesh, center: Vector, radius: float, segments: int): - """Add a closed UV-sphere to bm.""" +def add_sphere(bm: bmesh.types.BMesh, center: Vector, radius: float, segments: int, axis: Vector = None): + """Add a closed UV-sphere to bm. If axis is provided, orient the sphere so + its polar axis (sphere Z) aligns with `axis` — this makes the equator sit + correctly where frustum rings expect it. + """ rings = max(segments // 2, 2) verts = [] + rot = None + if axis is not None: + # If axis is near-zero length, ignore orientation. + if axis.length >= 1e-6: + rot = _rotation_matrix_to_axis(axis.normalized()) + for r in range(rings + 1): phi = math.pi * r / rings sin_phi = math.sin(phi) cos_phi = math.cos(phi) for s in range(segments): theta = 2.0 * math.pi * s / segments - v = bm.verts.new(center + Vector(( + local = Vector(( radius * sin_phi * math.cos(theta), radius * sin_phi * math.sin(theta), radius * cos_phi, - ))) + )) + if rot is not None: + offset = (rot @ local.to_4d()).to_3d() + else: + offset = local + v = bm.verts.new(center + offset) verts.append(v) for r in range(rings): @@ -89,8 +88,8 @@ def add_capped_frustum(bm: bmesh.types.BMesh, rot = _rotation_matrix_to_axis(ax) # Place rings at the sphere equators along the bone axis - head_ring_center = head_center + ax * head_radius - tail_ring_center = tail_center - ax * tail_radius + head_ring_center = head_center # + ax * head_radius + tail_ring_center = tail_center # - ax * tail_radius # Skip if rings would overlap (bone too short relative to radii) if (tail_ring_center - head_ring_center).dot(ax) < 1e-6: @@ -176,35 +175,37 @@ class ARMATURE_OT_build_envelope_mesh(bpy.types.Operator): def parts(mat, rad): return (world_mat @ mat, max(rad, 0.001)) - def meshify(head_matrix, head_radius, tail_matrix, tail_radius): + def meshify(head_matrix, head_radius, tail_matrix, tail_radius, draw_head=True): head_matrix_final, head_radius_final = parts(head_matrix, head_radius) tail_matrix_final, tail_radius_final = parts(tail_matrix, tail_radius) - add_sphere(bm, head_matrix_final, head_radius_final, segs) - add_sphere(bm, tail_matrix_final, tail_radius_final, segs) + # Orient spheres so their equators align with the bone axis + axis_vec = tail_matrix_final - head_matrix_final + if draw_head: + add_sphere(bm, head_matrix_final, head_radius_final, segs, axis=axis_vec) + add_sphere(bm, tail_matrix_final, tail_radius_final, segs, axis=axis_vec) add_capped_frustum(bm, head_matrix_final, head_radius_final, tail_matrix_final, tail_radius_final, segs) - if mode == 'EDIT': - # edit_bones is accessible while in edit mode - for bone in arm_obj.data.edit_bones: - meshify(bone.head, bone.head_radius, bone.tail, bone.tail_radius) - else: # OBJECT / everything else → rest pose - for bone in arm_obj.data.bones: - meshify(bone.head_local, bone.head_radius, bone.tail_local, bone.tail_radius) + # pose mode or object mode: + if mode == "EDIT": + bone_list = arm_obj.data.edit_bones + for bone in bone_list: + meshify(bone.head, bone.head_radius, bone.tail, bone.tail_radius, bone.parent is None) + else: + bone_list = arm_obj.pose.bones + for bone in bone_list: + meshify(bone.head, bone.bone.head_radius, bone.tail, bone.bone.tail_radius, bone.parent is None) # Must be in OBJECT mode to create and link new mesh objects. if original_mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') - # Merge verts from shared joints (parent/child bone connections) - bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) - mesh = bpy.data.meshes.new(arm_obj.name + "_envelope_mesh") bm.to_mesh(mesh) bm.free() @@ -213,18 +214,6 @@ class ARMATURE_OT_build_envelope_mesh(bpy.types.Operator): result_obj = bpy.data.objects.new(arm_obj.name + "_envelope", mesh) context.collection.objects.link(result_obj) - # Re-enter edit mode on the armature if that's where we came from, - # then make the new mesh the active/selected object. - if original_mode == 'EDIT': - arm_obj.select_set(True) - context.view_layer.objects.active = arm_obj - bpy.ops.object.mode_set(mode='EDIT') - arm_obj.select_set(False) - - bpy.ops.object.select_all(action='DESELECT') - result_obj.select_set(True) - context.view_layer.objects.active = result_obj - source_label = { 'OBJECT': "rest pose", 'POSE': "current pose", @@ -246,22 +235,13 @@ class VIEW3D_PT_armature_mesher(bpy.types.Panel): bl_region_type = 'UI' bl_category = "Armature" - @classmethod - def poll(cls, context): - obj = context.active_object - return obj is not None and obj.type == 'ARMATURE' + # @classmethod + # def poll(cls, context): + # obj = context.active_object + # return obj is not None and obj.type == 'ARMATURE' def draw(self, context): layout = self.layout - obj = context.active_object - mode_labels = { - 'OBJECT': "Object → rest pose", - 'POSE': "Pose → current pose", - 'EDIT': "Edit → edit bones", - } - layout.label(text=obj.name) - layout.label(text=mode_labels.get(obj.mode, obj.mode), icon='INFO') - layout.separator() op = layout.operator( ARMATURE_OT_build_envelope_mesh.bl_idname, text="Build Envelope Mesh",