diff --git a/CHANGELOG.md b/CHANGELOG.md index 5968392..85b3a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +1.6.0 + +- added: World space copy/paste from `Pin tool` panel can now copy/paste multiple selected posebones positions + - paste according to names + - aware of names and armatures if multiple armatures + - should take parenting into account 1.5.2 diff --git a/OP_world_copy_paste.py b/OP_world_copy_paste.py index d14fc42..1001189 100644 --- a/OP_world_copy_paste.py +++ b/OP_world_copy_paste.py @@ -1,3 +1,4 @@ +from numpy import dstack import bpy from . import fn @@ -13,10 +14,71 @@ class AW_OT_world_space_copy(bpy.types.Operator): return context.object and context.mode == 'POSE' and context.active_pose_bone def execute(self, context): - bpy.types.ViewLayer.world_space_store = context.object.matrix_world @ context.active_pose_bone.matrix + ### only active pose bone + # bpy.types.ViewLayer.world_space_store = context.object.matrix_world @ context.active_pose_bone.matrix + + if len(context.selected_pose_bones) == 1: + bpy.types.ViewLayer.world_space_store = context.object.matrix_world @ context.active_pose_bone.matrix + self.report({'INFO'}, 'Copied active pose bone matrix') + return {"FINISHED"} + + pose_bones = [b for b in context.selected_pose_bones] + armatures = set([b.id_data for b in pose_bones]) + + pose_l = [] # pose_d = {} + if len(armatures) == 1: + # only relate to bone name + for b in context.selected_pose_bones: + # pose_d[b.name] = context.object.matrix_world @ b.matrix + pose_l.append([b.name, b.id_data.matrix_world @ b.matrix]) + self.report({'INFO'}, f'Copied {len(pose_l)} pose bones matrices') + + else: + # for obj in context.objects_in_mode_unique_data: + for b in context.selected_pose_bones: + # pose_d[f'{b.id_data.name}/{b.name}'] = b.id_data.matrix_world @ b.matrix + pose_l.append([b.name, b.id_data.matrix_world @ b.matrix, b.id_data.name]) + self.report({'INFO'}, f'Copied {len(pose_l)} pose bones matrices (multi-armatures)') + + bpy.types.ViewLayer.world_space_store = pose_l return {"FINISHED"} +def set_matrix_and_key(pose, context=None, key=True): + context = context or bpy.context + ct = 0 + if isinstance(context.view_layer.world_space_store, list): + ## list structure : [0 bone_name, 1 matrix, 2[:armature_obj_name]] + # Paste in parental hierarchical relation (seems right order by default but need update between bones !) + for pb in context.selected_pose_bones: + for blist in pose: + + if pb.name != blist[0]: + continue + + # skip if there is an object name and obj name + if len(blist) > 2 and blist[2] != pb.id_data.name: + continue + + print(f'Paste "{pb.name}" position') + pb.matrix = pb.id_data.matrix_world.inverted() @ blist[1] + ct += 1 + + context.evaluated_depsgraph_get() # Refresh depsgraph so children position is placed according to parent position + break # go to next pose bone + else: + context.active_pose_bone.matrix = context.object.matrix_world.inverted() @ pose + ct = 1 + + if not ct: + return + + if key: + bpy.ops.anim.keyframe_insert(type='LocRotScale') + + return ct + + class AW_OT_world_space_paste(bpy.types.Operator): bl_idname = "pose.world_space_paste" bl_label = "World Paste" @@ -32,23 +94,26 @@ class AW_OT_world_space_paste(bpy.types.Operator): def execute(self, context): - context.active_pose_bone.matrix = context.object.matrix_world.inverted() @ bpy.context.view_layer.world_space_store - + ct = set_matrix_and_key(context.view_layer.world_space_store, context=context) + if not ct: + self.report({'ERROR'}, 'No pose pasted') + return {"CANCELLED"} + if ct != len(context.selected_pose_bones): + self.report({'WARNING'}, f'{ct}/{len(context.selected_pose_bones)} bone position pasted') + else: + self.report({'INFO'}, f'{ct} bone position pasted') + ## only if autokey is On # if context.scene.tool_settings.use_keyframe_insert_auto: # bpy.ops.anim.keyframe_insert_menu(type='LocRotScale') # Available - ## always paste location rotation scale - bpy.ops.anim.keyframe_insert(type='LocRotScale') - - return {"FINISHED"} class AW_OT_world_space_paste_next(bpy.types.Operator): bl_idname = "pose.world_space_paste_next" bl_label = "World Paste Jump" bl_description = "Paste world space transforms and keyframe available chanels\ - \nThen jump to prev/next key\ + \nThen jump to prev/next key (on active pose bone)\ \nKey Loc rot scale only" bl_options = {"REGISTER", "UNDO"} @@ -62,12 +127,11 @@ class AW_OT_world_space_paste_next(bpy.types.Operator): prev: bpy.props.BoolProperty() def execute(self, context): - # apply matrix - context.active_pose_bone.matrix = context.object.matrix_world.inverted() @ bpy.context.view_layer.world_space_store - - # insert keyframe at value - bpy.ops.anim.keyframe_insert(type='LocRotScale') # delete args to use default - + ct = set_matrix_and_key(context.view_layer.world_space_store, context=context) + if not ct: + self.report({'ERROR'}, 'No pose pasted') + return {"CANCELLED"} + # jump to next key act = fn.get_obj_action(context.object) if not act: @@ -115,8 +179,14 @@ class AW_OT_world_space_paste_next_frame(bpy.types.Operator): prev: bpy.props.BoolProperty() def execute(self, context): - # apply matrix - context.active_pose_bone.matrix = context.object.matrix_world.inverted() @ bpy.context.view_layer.world_space_store + ct = set_matrix_and_key(context.view_layer.world_space_store, context=context) + if not ct: + self.report({'ERROR'}, 'No pose pasted') + return {"CANCELLED"} + if ct != len(context.selected_pose_bones): + self.report({'WARNING'}, f'{ct}/{len(context.selected_pose_bones)} bone position pasted') + else: + self.report({'INFO'}, f'{ct} bone position pasted') # context.object.keyframe_insert(data_path, index=-1, frame=bpy.context.scene.frame_current, group="", options={'INSERTKEY_AVAILABLE'}) @@ -129,20 +199,13 @@ class AW_OT_world_space_paste_next_frame(bpy.types.Operator): # 'BUILTIN_KSI_VisualLoc', 'BUILTIN_KSI_VisualRot', 'BUILTIN_KSI_VisualScaling', # 'BUILTIN_KSI_VisualLocRot', 'BUILTIN_KSI_VisualLocRotScale', 'BUILTIN_KSI_VisualLocScale', 'BUILTIN_KSI_VisualRotScale' - ## insert keyframe at value ## Insert Keyframes for specified Keying Set, with menu of available Keying Sets if undefined # bpy.ops.anim.keyframe_insert_menu(type='Available', always_prompt=False) ## Insert keyframes on the current frame for all properties in the specified Keying Set - bpy.ops.anim.keyframe_insert(type='LocRotScale') # jump to next frame - act = fn.get_obj_action(context.object) - if not act: - self.report({'ERROR'}, 'No action on armature') - return {'CANCELLED'} - offset = -1 if self.prev else 1 new_frame = context.scene.frame_current + offset diff --git a/__init__.py b/__init__.py index 1d69e90..5bbb1b9 100644 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "Auto Walk", "description": "Develop a walk/run cycles along a curve and pin feets", "author": "Samuel Bernou", - "version": (1, 5, 2), + "version": (1, 6, 0), "blender": (3, 0, 0), "location": "View3D", "warning": "",