Node Wrangler: simplify preview_node operator
[blender-addons.git] / rigify / operators / copy_mirror_parameters.py
blob8bc78b8904633b340eb33c8f7d1b2b20679eee33
1 # SPDX-FileCopyrightText: 2021-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
6 import importlib
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')
32 @classmethod
33 def poll(cls, context):
34 return (
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):
46 try:
47 module = importlib.import_module(self.module_name)
48 filter_rig_class = getattr(module, self.class_name)
49 except (KeyError, AttributeError, ImportError):
50 self.report(
51 {'ERROR'}, f"Cannot find class {self.class_name} in {self.module_name}")
52 return {'CANCELLED'}
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)
59 num_copied = 0
61 # If copying collection references, include the toggle
62 is_coll_refs = self.property_name.endswith(REFS_LIST_SUFFIX)
63 if is_coll_refs:
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
77 do_mirror = False
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:
83 do_mirror = True
85 # Assign the final value
86 sel_params = get_rigify_params(sel_pbone)
88 if is_coll_refs:
89 copy_ref_list(getattr(sel_params, self.property_name), value, mirror=do_mirror)
90 else:
91 new_value = mirror_name(value) if do_mirror else value
92 setattr(sel_params, self.property_name, new_value)
94 if is_coll_refs:
95 setattr(sel_params, coll_refs_toggle_prop, coll_refs_toggle_val) # noqa
97 num_copied += 1
99 if num_copied:
100 self.report({'INFO'}, f"Copied the value to {num_copied} bones.")
101 return {'FINISHED'}
102 else:
103 self.report({'WARNING'}, "No suitable selected bones to copy to.")
104 return {'CANCELLED'}
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)
129 else:
130 return 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:
139 return False
140 else:
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)
147 if x_mirror:
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)
159 else:
160 to_bone['rigify_parameters'] = param_dict
161 else:
162 try:
163 del to_bone['rigify_parameters']
164 except KeyError:
165 pass
166 return True
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'}
177 @classmethod
178 def poll(cls, context):
179 obj = context.object
180 if not obj or obj.type != 'ARMATURE' or obj.mode != 'POSE':
181 return False
182 sel_bones = context.selected_pose_bones
183 if not sel_bones:
184 return False
185 for pb in sel_bones:
186 mirrored_name = mirror_name(pb.name)
187 if mirrored_name != pb.name and mirrored_name in obj.pose.bones:
188 return True
189 return False
191 def execute(self, context):
192 rig = context.object
194 num_mirrored = 0
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))
199 if not flip_bone:
200 # Bones without an opposite will just be ignored.
201 continue
202 if flip_bone != pb and flip_bone.bone.select:
203 self.report(
204 {'ERROR'},
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!")
207 return {'CANCELLED'}
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.
214 continue
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.")
220 return {'FINISHED'}
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(
232 name="Match Type",
233 description="Only mirror rigify parameters to selected bones which have the same rigify "
234 "type as the active bone",
235 default=False
238 @classmethod
239 def poll(cls, context):
240 obj = context.object
241 if not obj or obj.type != 'ARMATURE' or obj.mode != 'POSE':
242 return False
244 active = context.active_pose_bone
245 if not active or not get_rigify_type(active):
246 return False
248 select = context.selected_pose_bones
249 if len(select) < 2 or active not in select:
250 return False
252 return True
254 def execute(self, context):
255 active_bone = context.active_pose_bone
257 num_copied = 0
258 for pb in context.selected_pose_bones:
259 if pb == active_bone:
260 continue
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.")
266 return {'FINISHED'}
269 def draw_copy_mirror_ops(self, context):
270 layout = self.layout
271 if context.mode == 'POSE':
272 layout.separator()
273 op = layout.operator(POSE_OT_rigify_copy_parameters.bl_idname,
274 icon='DUPLICATE', text="Copy Only Parameters")
275 op.match_type = True
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 # =============================================
284 # Registration
286 classes = (
287 POSE_OT_rigify_copy_single_parameter,
288 POSE_OT_rigify_mirror_parameters,
289 POSE_OT_rigify_copy_parameters
293 def register():
294 from bpy.utils import register_class
295 for cls in classes:
296 register_class(cls)
298 from ..ui import VIEW3D_MT_rigify
299 VIEW3D_MT_rigify.append(draw_copy_mirror_ops)
302 def unregister():
303 from bpy.utils import unregister_class
304 for cls in classes:
305 unregister_class(cls)
307 from ..ui import VIEW3D_MT_rigify
308 VIEW3D_MT_rigify.remove(draw_copy_mirror_ops)