diff --git a/core/common.py b/core/common.py index 6e08f93..2c7ce75 100644 --- a/core/common.py +++ b/core/common.py @@ -57,3 +57,14 @@ def get_armature(context, armature_name=None) -> Optional[Object]: if obj.type == "ARMATURE": return obj return next((obj for obj in context.view_layer.objects if obj.type == 'ARMATURE'), None) + + +def duplicatebone(b: bpy.types.EditBone) -> bpy.types.EditBone: + arm = bpy.context.object.data + cb = arm.edit_bones.new(b.name) + + cb.head = b.head + cb.tail = b.tail + cb.matrix = b.matrix + cb.parent = b.parent + return cb \ No newline at end of file diff --git a/functions/digitigrade_legs.py b/functions/digitigrade_legs.py new file mode 100644 index 0000000..27ea3e5 --- /dev/null +++ b/functions/digitigrade_legs.py @@ -0,0 +1,117 @@ +import bpy +from ..core import common +from ..core import register_wrap +from .translations import t +import re + + +@register_wrap +class CreateDigitigradeLegs(bpy.types.Operator): + bl_idname = "avatar_toolkit.createdigitigradelegs" + bl_label = t('Tools.create_digitigrade_legs.label') + bl_description = t('Tools.create_digitigrade_legs.desc') + + @classmethod + def poll(cls, context): + if(context.active_object is None): + return False + if(context.selected_editable_bones is not None): + if(len(context.selected_editable_bones) == 2): + return True + return False + + def execute(self, context): + + for digi0 in context.selected_editable_bones: + digi1: bpy.types.EditBone = None + digi2: bpy.types.EditBone = None + digi3: bpy.types.EditBone = None + + try: + digi1 = digi0.children[0] + digi2 = digi1.children[0] + digi3 = digi2.children[0] + except: + print("bone format incorrect! Please select a chain of 4 continious bones!") #TODO: Show this to user. this is an error. + return {'CANCELLED'} + digi4 = None + try: + digi4 = digi3.children[0] + + except: + print("no toe bone. Continuing.") + digi0.select = True + digi1.select = True + digi2.select = True + digi3.select = True + if(digi4): + digi4.select = True + bpy.ops.armature.roll_clear() + bpy.ops.armature.select_all(action='DESELECT') + + #creating transform for upper leg + digi0.select = True + bpy.ops.transform.create_orientation(name="Toolkit_digi0", overwrite=True) + bpy.ops.armature.select_all(action='DESELECT') + + + #duplicate digi0 and assign it to thigh + thigh = common.duplicatebone(digi0) + bpy.ops.armature.select_all(action='DESELECT') + + #make digi2 parrallel to digi1 + digi2.align_orientation(digi0) + + #extrude thigh + thigh.select_tail = True + bpy.ops.armature.extrude_move(ARMATURE_OT_extrude={"forked":False},TRANSFORM_OT_translate=None) + #set new bone to calf varible + bpy.ops.armature.select_more() + calf = context.selected_bones[0] + bpy.ops.armature.select_all(action='DESELECT') + + #set calf end to digi2 end + calf.tail = digi2.tail + + #make copy of calf, flip it, and then align bone so that it's head is moved to match in align phase + flipedcalf = common.duplicatebone(calf) + bpy.ops.armature.select_all(action='DESELECT') + flipedcalf.select = True + bpy.ops.armature.switch_direction() + bpy.ops.armature.select_all(action='DESELECT') + flippeddigi1 = common.duplicatebone(digi1) + bpy.ops.armature.select_all(action='DESELECT') + flippeddigi1.select = True + bpy.ops.armature.switch_direction() + bpy.ops.armature.select_all(action='DESELECT') + + + + #align flipped calf to flipped middle leg to move the head + flipedcalf.align_orientation(flippeddigi1) + + flipedcalf.length = flippeddigi1.length + + #assign calf tail to flipped calf head so it moves calf's tail to be out at the perfect parallelagram + calf.head = flipedcalf.tail + + #delete helper bones + bpy.ops.armature.select_all(action='DESELECT') + flippeddigi1.select = True + bpy.ops.armature.delete() + bpy.ops.armature.select_all(action='DESELECT') + flipedcalf.select = True + bpy.ops.armature.delete() + bpy.ops.armature.select_all(action='DESELECT') + + + + #reparent the foot to the new calf so it will be part of the new foot IK chain + digi3.parent = calf + #Tada! It's done! now to rename the old 3 segments that make up the old part to noik so resonite doesn't try to select them + + digi0.name = re.compile(re.escape(""), re.IGNORECASE).sub("",digi0.name)+"" + digi1.name = re.compile(re.escape(""), re.IGNORECASE).sub("",digi1.name)+"" + digi2.name = re.compile(re.escape(""), re.IGNORECASE).sub("",digi2.name)+"" + #finally fully done! + return {'FINISHED'} \ No newline at end of file diff --git a/functions/translations.py b/functions/translations.py index 4f84a1d..a265c81 100644 --- a/functions/translations.py +++ b/functions/translations.py @@ -44,7 +44,7 @@ def load_translations() -> None: print("Default translation file 'en_US.json' not found.") def t(phrase: str, *args, **kwargs) -> str: - output: str = dictionary.get(phrase) + output: str = dictionary.get(phrase, None) if output is None: if verbose: print('Warning: Unknown phrase: ' + phrase) diff --git a/ui/tools.py b/ui/tools.py index 408a95d..0593bcf 100644 --- a/ui/tools.py +++ b/ui/tools.py @@ -2,6 +2,7 @@ import bpy from ..core.register import register_wrap from .panel import AvatarToolkitPanel from bpy.types import Context +from ..functions.digitigrade_legs import CreateDigitigradeLegs @register_wrap class AvatarToolkitToolsPanel(bpy.types.Panel): @@ -19,4 +20,5 @@ class AvatarToolkitToolsPanel(bpy.types.Panel): row = layout.row(align=True) row.scale_y = 1.5 - row.operator("avatar_toolkit.convert_to_resonite", text="Translate to Resonite") \ No newline at end of file + row.operator("avatar_toolkit.convert_to_resonite", text="Translate to Resonite") + row.operator(CreateDigitigradeLegs.bl_idname, text="Create Digitigrade Legs") \ No newline at end of file