1 # SPDX-FileCopyrightText: 2021-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
8 from ..utils
.layers
import REFS_TOGGLE_SUFFIX
, REFS_LIST_SUFFIX
, is_collection_ref_list_prop
, copy_ref_list
9 from ..utils
.naming
import Side
, get_name_base_and_sides
, mirror_name
10 from ..utils
.misc
import property_to_python
12 from ..utils
.rig
import get_rigify_type
, get_rigify_params
13 from ..rig_lists
import get_rig_class
16 # =============================================
17 # Single parameter copy button
19 # noinspection PyPep8Naming
20 class POSE_OT_rigify_copy_single_parameter(bpy
.types
.Operator
):
21 bl_idname
= "pose.rigify_copy_single_parameter"
22 bl_label
= "Copy Option To Selected Rigs"
23 bl_description
= "Copy this property value to all selected rigs of the appropriate type"
24 bl_options
= {'UNDO', 'INTERNAL'}
26 property_name
: bpy
.props
.StringProperty(name
='Property Name')
27 mirror_bone
: bpy
.props
.BoolProperty(name
='Mirror As Bone Name')
29 module_name
: bpy
.props
.StringProperty(name
='Module Name')
30 class_name
: bpy
.props
.StringProperty(name
='Class Name')
33 def poll(cls
, context
):
35 context
.active_object
and context
.active_object
.type == 'ARMATURE'
36 and context
.active_pose_bone
37 and context
.active_object
.data
.get('rig_id') is None
38 and get_rigify_type(context
.active_pose_bone
)
39 and len(context
.selected_pose_bones
) > 1
42 def invoke(self
, context
, event
):
43 return context
.window_manager
.invoke_confirm(self
, event
)
45 def execute(self
, context
):
47 module
= importlib
.import_module(self
.module_name
)
48 filter_rig_class
= getattr(module
, self
.class_name
)
49 except (KeyError, AttributeError, ImportError):
51 {'ERROR'}, f
"Cannot find class {self.class_name} in {self.module_name}")
54 active_pbone
= context
.active_pose_bone
55 active_split
= get_name_base_and_sides(active_pbone
.name
)
57 params
= get_rigify_params(active_pbone
)
58 value
= getattr(params
, self
.property_name
)
61 # If copying collection references, include the toggle
62 is_coll_refs
= self
.property_name
.endswith(REFS_LIST_SUFFIX
)
64 assert is_collection_ref_list_prop(value
)
65 coll_refs_toggle_prop
= self
.property_name
[:-len(REFS_LIST_SUFFIX
)] + REFS_TOGGLE_SUFFIX
66 coll_refs_toggle_val
= getattr(params
, coll_refs_toggle_prop
)
68 # Copy to different bones of appropriate rig types
69 for sel_pbone
in context
.selected_pose_bones
:
70 rig_type
= get_rigify_type(sel_pbone
)
72 if rig_type
and sel_pbone
!= active_pbone
:
73 rig_class
= get_rig_class(rig_type
)
75 if rig_class
and issubclass(rig_class
, filter_rig_class
):
76 # If mirror requested and copying to a different side bone, mirror the value
79 if self
.mirror_bone
and active_split
.side
!= Side
.MIDDLE
and value
:
80 sel_split
= get_name_base_and_sides(sel_pbone
.name
)
82 if sel_split
.side
== -active_split
.side
:
85 # Assign the final value
86 sel_params
= get_rigify_params(sel_pbone
)
89 copy_ref_list(getattr(sel_params
, self
.property_name
), value
, mirror
=do_mirror
)
91 new_value
= mirror_name(value
) if do_mirror
else value
92 setattr(sel_params
, self
.property_name
, new_value
)
95 setattr(sel_params
, coll_refs_toggle_prop
, coll_refs_toggle_val
) # noqa
100 self
.report({'INFO'}, f
"Copied the value to {num_copied} bones.")
103 self
.report({'WARNING'}, "No suitable selected bones to copy to.")
107 def make_copy_parameter_button(layout
, property_name
, *, base_class
, mirror_bone
=False):
108 """Displays a button that copies the property to selected rig of the specified base type."""
109 props
= layout
.operator(
110 POSE_OT_rigify_copy_single_parameter
.bl_idname
, icon
='DUPLICATE', text
='')
111 props
.property_name
= property_name
112 props
.mirror_bone
= mirror_bone
113 props
.module_name
= base_class
.__module
__
114 props
.class_name
= base_class
.__name
__
117 def recursive_mirror(value
):
118 """Mirror strings(.L/.R) in any mixed structure of dictionaries/lists."""
120 if isinstance(value
, dict):
121 return {key
: recursive_mirror(val
) for key
, val
in value
.items()}
123 elif isinstance(value
, list):
124 return [recursive_mirror(elem
) for elem
in value
]
126 elif isinstance(value
, str):
127 return mirror_name(value
)
133 def copy_rigify_params(from_bone
: bpy
.types
.PoseBone
, to_bone
: bpy
.types
.PoseBone
, *,
134 match_type
=False, x_mirror
=False) -> bool:
135 rig_type
= get_rigify_type(to_bone
)
136 from_type
= get_rigify_type(from_bone
)
138 if match_type
and rig_type
!= from_type
:
141 rig_type
= to_bone
.rigify_type
= from_type
143 from_params
= from_bone
.get('rigify_parameters')
144 if from_params
and rig_type
:
145 param_dict
= property_to_python(from_params
)
148 to_bone
['rigify_parameters'] = recursive_mirror(param_dict
)
150 # Bone collection references must be mirrored specially
151 from_params_typed
= get_rigify_params(from_bone
)
152 to_params_typed
= get_rigify_params(to_bone
)
154 for prop_name
in param_dict
.keys():
155 if prop_name
.endswith(REFS_LIST_SUFFIX
):
156 ref_list
= getattr(from_params_typed
, prop_name
)
157 if is_collection_ref_list_prop(ref_list
):
158 copy_ref_list(getattr(to_params_typed
, prop_name
), ref_list
, mirror
=True)
160 to_bone
['rigify_parameters'] = param_dict
163 del to_bone
['rigify_parameters']
169 # noinspection PyPep8Naming
170 class POSE_OT_rigify_mirror_parameters(bpy
.types
.Operator
):
171 """Mirror Rigify type and parameters of selected bones to the opposite side. Names should end in L/R"""
173 bl_idname
= "pose.rigify_mirror_parameters"
174 bl_label
= "Mirror Rigify Parameters"
175 bl_options
= {'REGISTER', 'UNDO'}
178 def poll(cls
, context
):
180 if not obj
or obj
.type != 'ARMATURE' or obj
.mode
!= 'POSE':
182 sel_bones
= context
.selected_pose_bones
186 mirrored_name
= mirror_name(pb
.name
)
187 if mirrored_name
!= pb
.name
and mirrored_name
in obj
.pose
.bones
:
191 def execute(self
, context
):
196 # First make sure that all selected bones can be mirrored unambiguously.
197 for pb
in context
.selected_pose_bones
:
198 flip_bone
= rig
.pose
.bones
.get(mirror_name(pb
.name
))
200 # Bones without an opposite will just be ignored.
202 if flip_bone
!= pb
and flip_bone
.bone
.select
:
205 f
"Bone {pb.name} selected on both sides, mirroring would be ambiguous, "
206 f
"aborting. Only select the left or right side, not both!")
209 # Then mirror the parameters.
210 for pb
in context
.selected_pose_bones
:
211 flip_bone
= rig
.pose
.bones
.get(mirror_name(pb
.name
))
212 if flip_bone
== pb
or not flip_bone
:
213 # Bones without an opposite will just be ignored.
216 num_mirrored
+= copy_rigify_params(pb
, flip_bone
, match_type
=False, x_mirror
=True)
218 self
.report({'INFO'}, f
"Mirrored parameters of {num_mirrored} bones.")
223 # noinspection PyPep8Naming
224 class POSE_OT_rigify_copy_parameters(bpy
.types
.Operator
):
225 """Copy Rigify type and parameters from active to selected bones"""
227 bl_idname
= "pose.rigify_copy_parameters"
228 bl_label
= "Copy Rigify Parameters to Selected"
229 bl_options
= {'REGISTER', 'UNDO'}
231 match_type
: bpy
.props
.BoolProperty(
233 description
="Only mirror rigify parameters to selected bones which have the same rigify "
234 "type as the active bone",
239 def poll(cls
, context
):
241 if not obj
or obj
.type != 'ARMATURE' or obj
.mode
!= 'POSE':
244 active
= context
.active_pose_bone
245 if not active
or not get_rigify_type(active
):
248 select
= context
.selected_pose_bones
249 if len(select
) < 2 or active
not in select
:
254 def execute(self
, context
):
255 active_bone
= context
.active_pose_bone
258 for pb
in context
.selected_pose_bones
:
259 if pb
== active_bone
:
261 num_copied
+= copy_rigify_params(active_bone
, pb
, match_type
=self
.match_type
)
263 self
.report({'INFO'},
264 f
"Copied {get_rigify_type(active_bone)} parameters to {num_copied} bones.")
269 def draw_copy_mirror_ops(self
, context
):
271 if context
.mode
== 'POSE':
273 op
= layout
.operator(POSE_OT_rigify_copy_parameters
.bl_idname
,
274 icon
='DUPLICATE', text
="Copy Only Parameters")
276 op
= layout
.operator(POSE_OT_rigify_copy_parameters
.bl_idname
,
277 icon
='DUPLICATE', text
="Copy Type & Parameters")
278 op
.match_type
= False
279 layout
.operator(POSE_OT_rigify_mirror_parameters
.bl_idname
,
280 icon
='MOD_MIRROR', text
="Mirror Type & Parameters")
283 # =============================================
287 POSE_OT_rigify_copy_single_parameter
,
288 POSE_OT_rigify_mirror_parameters
,
289 POSE_OT_rigify_copy_parameters
294 from bpy
.utils
import register_class
298 from ..ui
import VIEW3D_MT_rigify
299 VIEW3D_MT_rigify
.append(draw_copy_mirror_ops
)
303 from bpy
.utils
import unregister_class
305 unregister_class(cls
)
307 from ..ui
import VIEW3D_MT_rigify
308 VIEW3D_MT_rigify
.remove(draw_copy_mirror_ops
)