1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENCE BLOCK #####
20 "name": "Bone Selection Sets",
21 "author": "Inês Almeida, Sybren A. Stüvel, Antony Riakiotakis, Dan Eicher",
23 "blender": (2, 75, 0),
24 "location": "Properties > Object Data (Armature) > Selection Sets",
25 "description": "List of Bone sets for easy selection while animating",
27 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Animation/SelectionSets",
29 "category": "Animation",
33 from bpy
.types
import (
40 from bpy
.props
import (
48 # Data Structure ##############################################################
50 # Note: bones are stored by name, this means that if the bone is renamed,
51 # there can be problems. However, bone renaming is unlikely during animation
52 class SelectionEntry(PropertyGroup
):
53 name
= StringProperty(name
="Bone Name")
56 class SelectionSet(PropertyGroup
):
57 name
= StringProperty(name
="Set Name")
58 bone_ids
= CollectionProperty(type=SelectionEntry
)
61 # UI Panel w/ UIList ##########################################################
63 class POSE_MT_selection_sets_specials(Menu
):
64 bl_label
= "Selection Sets Specials"
66 def draw(self
, context
):
69 layout
.operator("pose.selection_set_delete_all", icon
='X')
70 layout
.operator("pose.selection_set_remove_bones", icon
='X')
71 layout
.operator("pose.selection_set_copy", icon
='COPYDOWN')
72 layout
.operator("pose.selection_set_paste", icon
='PASTEDOWN')
75 class POSE_PT_selection_sets(Panel
):
76 bl_label
= "Selection Sets"
77 bl_space_type
= 'PROPERTIES'
78 bl_region_type
= 'WINDOW'
82 def poll(cls
, context
):
83 return (context
.object and
84 context
.object.type == 'ARMATURE' and
87 def draw(self
, context
):
93 row
.enabled
= (context
.mode
== 'POSE')
96 rows
= 4 if len(arm
.selection_sets
) > 0 else 1
98 "POSE_UL_selection_set", "", # type and unique id
99 arm
, "selection_sets", # pointer to the CollectionProperty
100 arm
, "active_selection_set", # pointer to the active identifier
104 # add/remove/specials UI list Menu
105 col
= row
.column(align
=True)
106 col
.operator("pose.selection_set_add", icon
='ZOOMIN', text
="")
107 col
.operator("pose.selection_set_remove", icon
='ZOOMOUT', text
="")
108 col
.menu("POSE_MT_selection_sets_specials", icon
='DOWNARROW_HLT', text
="")
110 # move up/down arrows
111 if len(arm
.selection_sets
) > 0:
113 col
.operator("pose.selection_set_move", icon
='TRIA_UP', text
="").direction
= 'UP'
114 col
.operator("pose.selection_set_move", icon
='TRIA_DOWN', text
="").direction
= 'DOWN'
119 sub
= row
.row(align
=True)
120 sub
.operator("pose.selection_set_assign", text
="Assign")
121 sub
.operator("pose.selection_set_unassign", text
="Remove")
123 sub
= row
.row(align
=True)
124 sub
.operator("pose.selection_set_select", text
="Select")
125 sub
.operator("pose.selection_set_deselect", text
="Deselect")
128 class POSE_UL_selection_set(UIList
):
129 def draw_item(self
, context
, layout
, data
, set, icon
, active_data
, active_propname
, index
):
130 layout
.prop(set, "name", text
="", icon
='GROUP_BONE', emboss
=False)
133 class POSE_MT_selection_set_create(Menu
):
134 bl_label
= "Choose Selection Set"
136 def draw(self
, context
):
138 layout
.operator("pose.selection_set_add_and_assign",
139 text
="New Selection Set")
142 class POSE_MT_selection_sets(Menu
):
143 bl_label
= 'Select Selection Set'
146 def poll(cls
, context
):
147 return POSE_OT_selection_set_select
.poll(context
)
149 def draw(self
, context
):
151 layout
.operator_context
= 'EXEC_DEFAULT'
152 for idx
, sel_set
in enumerate(context
.object.selection_sets
):
153 props
= layout
.operator(POSE_OT_selection_set_select
.bl_idname
, text
=sel_set
.name
)
154 props
.selection_set_index
= idx
157 # Operators ###################################################################
159 class PluginOperator(Operator
):
161 def poll(cls
, context
):
162 return (context
.object and
163 context
.object.type == 'ARMATURE' and
164 context
.mode
== 'POSE')
167 class NeedSelSetPluginOperator(PluginOperator
):
169 def poll(cls
, context
):
170 if not super().poll(context
):
173 return 0 <= arm
.active_selection_set
< len(arm
.selection_sets
)
176 class POSE_OT_selection_set_delete_all(PluginOperator
):
177 bl_idname
= "pose.selection_set_delete_all"
178 bl_label
= "Delete All Sets"
179 bl_description
= "Deletes All Selection Sets"
180 bl_options
= {'UNDO', 'REGISTER'}
182 def execute(self
, context
):
184 arm
.selection_sets
.clear()
188 class POSE_OT_selection_set_remove_bones(PluginOperator
):
189 bl_idname
= "pose.selection_set_remove_bones"
190 bl_label
= "Remove Bones from Sets"
191 bl_description
= "Removes the Active Bones from All Sets"
192 bl_options
= {'UNDO', 'REGISTER'}
194 def execute(self
, context
):
197 # iterate only the selected bones in current pose that are not hidden
198 for bone
in context
.selected_pose_bones
:
199 for selset
in arm
.selection_sets
:
200 if bone
.name
in selset
.bone_ids
:
201 idx
= selset
.bone_ids
.find(bone
.name
)
202 selset
.bone_ids
.remove(idx
)
207 class POSE_OT_selection_set_move(NeedSelSetPluginOperator
):
208 bl_idname
= "pose.selection_set_move"
209 bl_label
= "Move Selection Set in List"
210 bl_description
= "Move the active Selection Set up/down the list of sets"
211 bl_options
= {'UNDO', 'REGISTER'}
213 direction
= EnumProperty(
214 name
="Move Direction",
215 description
="Direction to move the active Selection Set: UP (default) or DOWN",
217 ('UP', "Up", "", -1),
218 ('DOWN', "Down", "", 1),
224 def poll(cls
, context
):
225 if not super().poll(context
):
228 return len(arm
.selection_sets
) > 1
230 def execute(self
, context
):
233 active_idx
= arm
.active_selection_set
234 new_idx
= active_idx
+ (-1 if self
.direction
== 'UP' else 1)
236 if new_idx
< 0 or new_idx
>= len(arm
.selection_sets
):
239 arm
.selection_sets
.move(active_idx
, new_idx
)
240 arm
.active_selection_set
= new_idx
245 class POSE_OT_selection_set_add(PluginOperator
):
246 bl_idname
= "pose.selection_set_add"
247 bl_label
= "Create Selection Set"
248 bl_description
= "Creates a new empty Selection Set"
249 bl_options
= {'UNDO', 'REGISTER'}
251 def execute(self
, context
):
253 sel_sets
= arm
.selection_sets
254 new_sel_set
= sel_sets
.add()
255 new_sel_set
.name
= uniqify("SelectionSet", sel_sets
.keys())
257 # select newly created set
258 arm
.active_selection_set
= len(sel_sets
) - 1
263 class POSE_OT_selection_set_remove(NeedSelSetPluginOperator
):
264 bl_idname
= "pose.selection_set_remove"
265 bl_label
= "Delete Selection Set"
266 bl_description
= "Delete a Selection Set"
267 bl_options
= {'UNDO', 'REGISTER'}
269 def execute(self
, context
):
272 arm
.selection_sets
.remove(arm
.active_selection_set
)
274 # change currently active selection set
275 numsets
= len(arm
.selection_sets
)
276 if (arm
.active_selection_set
> (numsets
- 1) and numsets
> 0):
277 arm
.active_selection_set
= len(arm
.selection_sets
) - 1
282 class POSE_OT_selection_set_assign(PluginOperator
):
283 bl_idname
= "pose.selection_set_assign"
284 bl_label
= "Add Bones to Selection Set"
285 bl_description
= "Add selected bones to Selection Set"
286 bl_options
= {'UNDO', 'REGISTER'}
288 def invoke(self
, context
, event
):
291 if not (arm
.active_selection_set
< len(arm
.selection_sets
)):
292 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT",
293 name
="POSE_MT_selection_set_create")
295 bpy
.ops
.pose
.selection_set_assign('EXEC_DEFAULT')
299 def execute(self
, context
):
301 act_sel_set
= arm
.selection_sets
[arm
.active_selection_set
]
303 # iterate only the selected bones in current pose that are not hidden
304 for bone
in context
.selected_pose_bones
:
305 if bone
.name
not in act_sel_set
.bone_ids
:
306 bone_id
= act_sel_set
.bone_ids
.add()
307 bone_id
.name
= bone
.name
312 class POSE_OT_selection_set_unassign(NeedSelSetPluginOperator
):
313 bl_idname
= "pose.selection_set_unassign"
314 bl_label
= "Remove Bones from Selection Set"
315 bl_description
= "Remove selected bones from Selection Set"
316 bl_options
= {'UNDO', 'REGISTER'}
318 def execute(self
, context
):
320 act_sel_set
= arm
.selection_sets
[arm
.active_selection_set
]
322 # iterate only the selected bones in current pose that are not hidden
323 for bone
in context
.selected_pose_bones
:
324 if bone
.name
in act_sel_set
.bone_ids
:
325 idx
= act_sel_set
.bone_ids
.find(bone
.name
)
326 act_sel_set
.bone_ids
.remove(idx
)
331 class POSE_OT_selection_set_select(NeedSelSetPluginOperator
):
332 bl_idname
= "pose.selection_set_select"
333 bl_label
= "Select Selection Set"
334 bl_description
= "Add Selection Set bones to current selection"
335 bl_options
= {'UNDO', 'REGISTER'}
337 selection_set_index
= IntProperty(
338 name
='Selection Set Index',
340 description
='Which Selection Set to select; -1 uses the active Selection Set')
342 def execute(self
, context
):
345 if self
.selection_set_index
== -1:
346 idx
= arm
.active_selection_set
348 idx
= self
.selection_set_index
349 sel_set
= arm
.selection_sets
[idx
]
351 for bone
in context
.visible_pose_bones
:
352 if bone
.name
in sel_set
.bone_ids
:
353 bone
.bone
.select
= True
358 class POSE_OT_selection_set_deselect(NeedSelSetPluginOperator
):
359 bl_idname
= "pose.selection_set_deselect"
360 bl_label
= "Deselect Selection Set"
361 bl_description
= "Remove Selection Set bones from current selection"
362 bl_options
= {'UNDO', 'REGISTER'}
364 def execute(self
, context
):
366 act_sel_set
= arm
.selection_sets
[arm
.active_selection_set
]
368 for bone
in context
.selected_pose_bones
:
369 if bone
.name
in act_sel_set
.bone_ids
:
370 bone
.bone
.select
= False
375 class POSE_OT_selection_set_add_and_assign(PluginOperator
):
376 bl_idname
= "pose.selection_set_add_and_assign"
377 bl_label
= "Create and Add Bones to Selection Set"
378 bl_description
= "Creates a new Selection Set with the currently selected bones"
379 bl_options
= {'UNDO', 'REGISTER'}
381 def execute(self
, context
):
382 bpy
.ops
.pose
.selection_set_add('EXEC_DEFAULT')
383 bpy
.ops
.pose
.selection_set_assign('EXEC_DEFAULT')
387 class POSE_OT_selection_set_copy(NeedSelSetPluginOperator
):
388 bl_idname
= "pose.selection_set_copy"
389 bl_label
= "Copy Selection Set to Clipboard"
390 bl_description
= "Converts the Selection Set to JSON and places it on the clipboard"
391 bl_options
= {'UNDO', 'REGISTER'}
393 def execute(self
, context
):
394 context
.window_manager
.clipboard
= to_json(context
)
395 self
.report({'INFO'}, 'Copied Selection Set to Clipboard')
399 class POSE_OT_selection_set_paste(PluginOperator
):
400 bl_idname
= "pose.selection_set_paste"
401 bl_label
= "Paste Selection Set from Clipboard"
402 bl_description
= "Adds a new Selection Set from copied JSON on the clipboard"
403 bl_options
= {'UNDO', 'REGISTER'}
405 def execute(self
, context
):
409 from_json(context
, context
.window_manager
.clipboard
)
410 except (json
.JSONDecodeError
, KeyError):
411 self
.report({'ERROR'}, 'The clipboard does not contain a Selection Set')
413 # Select the pasted Selection Set.
414 context
.object.active_selection_set
= len(context
.object.selection_sets
) - 1
419 # Registry ####################################################################
422 POSE_MT_selection_set_create
,
423 POSE_MT_selection_sets_specials
,
424 POSE_MT_selection_sets
,
425 POSE_PT_selection_sets
,
426 POSE_UL_selection_set
,
429 POSE_OT_selection_set_delete_all
,
430 POSE_OT_selection_set_remove_bones
,
431 POSE_OT_selection_set_move
,
432 POSE_OT_selection_set_add
,
433 POSE_OT_selection_set_remove
,
434 POSE_OT_selection_set_assign
,
435 POSE_OT_selection_set_unassign
,
436 POSE_OT_selection_set_select
,
437 POSE_OT_selection_set_deselect
,
438 POSE_OT_selection_set_add_and_assign
,
439 POSE_OT_selection_set_copy
,
440 POSE_OT_selection_set_paste
,
444 def add_sss_button(self
, context
):
445 self
.layout
.menu('POSE_MT_selection_sets')
448 def to_json(context
) -> str:
449 """Convert the active bone selection set of the current rig to JSON."""
453 active_idx
= arm
.active_selection_set
454 sel_set
= arm
.selection_sets
[active_idx
]
457 'name': sel_set
.name
,
458 'bones': [bone_id
.name
for bone_id
in sel_set
.bone_ids
]
462 def from_json(context
, as_json
: str):
463 """Add the single bone selection set from JSON to the current rig."""
466 sel_set
= json
.loads(as_json
)
468 sel_sets
= context
.object.selection_sets
469 new_sel_set
= sel_sets
.add()
470 new_sel_set
.name
= uniqify(sel_set
['name'], sel_sets
.keys())
472 for bone_name
in sel_set
['bones']:
473 bone_id
= new_sel_set
.bone_ids
.add()
474 bone_id
.name
= bone_name
477 def uniqify(name
: str, other_names
: list) -> str:
478 """Return a unique name with .xxx suffix if necessary.
480 >>> uniqify('hey', ['there'])
482 >>> uniqify('hey', ['hey.001', 'hey.005'])
484 >>> uniqify('hey', ['hey', 'hey.001', 'hey.005'])
486 >>> uniqify('hey', ['hey', 'hey.005', 'hey.001'])
488 >>> uniqify('hey', ['hey', 'hey.005', 'hey.001', 'hey.left'])
490 >>> uniqify('hey', ['hey', 'hey.001', 'hey.002'])
493 It also works with a dict_keys object:
494 >>> uniqify('hey', {'hey': 1, 'hey.005': 1, 'hey.001': 1}.keys())
498 if name
not in other_names
:
501 # Construct the list of numbers already in use.
502 offset
= len(name
) + 1
503 others
= (n
[offset
:] for n
in other_names
504 if n
.startswith(name
+ '.'))
505 numbers
= sorted(int(suffix
) for suffix
in others
508 # Find the first unused number.
514 return "{}.{:03d}".format(name
, min_index
)
517 # store keymaps here to access after registration
523 bpy
.utils
.register_class(cls
)
525 bpy
.types
.Object
.selection_sets
= CollectionProperty(
527 name
="Selection Sets",
528 description
="List of groups of bones for easy selection"
530 bpy
.types
.Object
.active_selection_set
= IntProperty(
531 name
="Active Selection Set",
532 description
="Index of the currently active selection set",
536 wm
= bpy
.context
.window_manager
537 km
= wm
.keyconfigs
.addon
.keymaps
.new(name
='Pose')
539 kmi
= km
.keymap_items
.new('wm.call_menu', 'W', 'PRESS', alt
=True, shift
=True)
540 kmi
.properties
.name
= 'POSE_MT_selection_sets'
541 addon_keymaps
.append((km
, kmi
))
543 bpy
.types
.VIEW3D_MT_select_pose
.append(add_sss_button
)
548 bpy
.utils
.unregister_class(cls
)
550 del bpy
.types
.Object
.selection_sets
551 del bpy
.types
.Object
.active_selection_set
554 for km
, kmi
in addon_keymaps
:
555 km
.keymap_items
.remove(kmi
)
556 addon_keymaps
.clear()
559 if __name__
== "__main__":