/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved. * * SPDX-License-Identifier: GPL-2.0-or-later */ /** \file * \ingroup edarmature */ #include #include "DNA_action_types.h" #include "DNA_anim_types.h" #include "DNA_armature_types.h" #include "DNA_constraint_types.h" #include "DNA_object_types.h" #include "DNA_scene_types.h" #include "BLI_listbase.h" #include "BLI_map.hh" #include "BLI_string.h" #include "BKE_action.hh" #include "BKE_armature.hh" #include "BKE_constraint.h" #include "BKE_context.hh" #include "BKE_layer.hh" #include "BKE_modifier.hh" #include "BKE_object.hh" #include "BKE_report.hh" #include "DEG_depsgraph.hh" #include "RNA_access.hh" #include "RNA_define.hh" #include "WM_api.hh" #include "WM_types.hh" #include "ED_armature.hh" #include "ED_keyframing.hh" #include "ED_mesh.hh" #include "ED_object.hh" #include "ED_object_vgroup.hh" #include "ED_outliner.hh" #include "ED_screen.hh" #include "ED_select_utils.hh" #include "ED_view3d.hh" #include "ANIM_armature.hh" #include "ANIM_bonecolor.hh" #include "ANIM_keyingsets.hh" #include "armature_intern.hh" using blender::Span; using blender::Vector; /* ***************** Pose Select Utilities ********************* */ /* NOTE: SEL_TOGGLE is assumed to have already been handled! */ static void pose_do_bone_select(bPoseChannel *pchan, const int select_mode) { /* select pchan only if selectable, but deselect works always */ switch (select_mode) { case SEL_SELECT: if (!(pchan->bone->flag & BONE_UNSELECTABLE)) { pchan->bone->flag |= BONE_SELECTED; } break; case SEL_DESELECT: pchan->bone->flag &= ~(BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL); break; case SEL_INVERT: if (pchan->bone->flag & BONE_SELECTED) { pchan->bone->flag &= ~(BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL); } else if (!(pchan->bone->flag & BONE_UNSELECTABLE)) { pchan->bone->flag |= BONE_SELECTED; } break; } } void ED_pose_bone_select_tag_update(Object *ob) { BLI_assert(ob->type == OB_ARMATURE); bArmature *arm = static_cast(ob->data); WM_main_add_notifier(NC_OBJECT | ND_BONE_SELECT, ob); WM_main_add_notifier(NC_GEOM | ND_DATA, ob); if (arm->flag & ARM_HAS_VIZ_DEPS) { /* mask modifier ('armature' mode), etc. */ DEG_id_tag_update(&ob->id, ID_RECALC_GEOMETRY); } DEG_id_tag_update(&arm->id, ID_RECALC_SELECT); } void ED_pose_bone_select(Object *ob, bPoseChannel *pchan, bool select, bool change_active) { bArmature *arm; /* sanity checks */ /* XXX: actually, we can probably still get away with no object - at most we have no updates */ if (ELEM(nullptr, ob, ob->pose, pchan, pchan->bone)) { return; } arm = static_cast(ob->data); /* can only change selection state if bone can be modified */ if (PBONE_SELECTABLE(arm, pchan->bone)) { /* change selection state - activate too if selected */ if (select) { pchan->bone->flag |= BONE_SELECTED; if (change_active) { arm->act_bone = pchan->bone; } } else { pchan->bone->flag &= ~BONE_SELECTED; if (change_active) { arm->act_bone = nullptr; } } /* TODO: select and activate corresponding vgroup? */ ED_pose_bone_select_tag_update(ob); } } bool ED_armature_pose_select_pick_bone(const Scene *scene, ViewLayer *view_layer, View3D *v3d, Object *ob, Bone *bone, const SelectPick_Params ¶ms) { bool found = false; bool changed = false; if (ob->pose) { if (bone && ((bone->flag & BONE_UNSELECTABLE) == 0)) { found = true; } } if (params.sel_op == SEL_OP_SET) { if ((found && params.select_passthrough) && (bone->flag & BONE_SELECTED)) { found = false; } else if (found || params.deselect_all) { /* Deselect everything. */ /* Don't use #BKE_object_pose_base_array_get_unique * because we may be selecting from object mode. */ FOREACH_VISIBLE_BASE_BEGIN (scene, view_layer, v3d, base_iter) { Object *ob_iter = base_iter->object; if ((ob_iter->type == OB_ARMATURE) && (ob_iter->mode & OB_MODE_POSE)) { if (ED_pose_deselect_all(ob_iter, SEL_DESELECT, true)) { ED_pose_bone_select_tag_update(ob_iter); } } } FOREACH_VISIBLE_BASE_END; changed = true; } } if (found) { BKE_view_layer_synced_ensure(scene, view_layer); Object *ob_act = BKE_view_layer_active_object_get(view_layer); BLI_assert(BKE_view_layer_edit_object_get(view_layer) == nullptr); /* If the bone cannot be affected, don't do anything. */ bArmature *arm = static_cast(ob->data); /* Since we do unified select, we don't shift+select a bone if the * armature object was not active yet. * NOTE(@ideasman42): special exception for armature mode so we can do multi-select * we could check for multi-select explicitly but think its fine to * always give predictable behavior in weight paint mode. */ if ((ob_act == nullptr) || ((ob_act != ob) && (ob_act->mode & OB_MODE_ALL_WEIGHT_PAINT) == 0)) { /* When we are entering into posemode via toggle-select, * from another active object - always select the bone. */ if (params.sel_op == SEL_OP_SET) { /* Re-select the bone again later in this function. */ bone->flag &= ~BONE_SELECTED; } } switch (params.sel_op) { case SEL_OP_ADD: { bone->flag |= (BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL); arm->act_bone = bone; break; } case SEL_OP_SUB: { bone->flag &= ~(BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL); break; } case SEL_OP_XOR: { if (bone->flag & BONE_SELECTED) { /* If not active, we make it active. */ if (bone != arm->act_bone) { arm->act_bone = bone; } else { bone->flag &= ~(BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL); } } else { bone->flag |= (BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL); arm->act_bone = bone; } break; } case SEL_OP_SET: { bone->flag |= (BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL); arm->act_bone = bone; break; } case SEL_OP_AND: { BLI_assert_unreachable(); /* Doesn't make sense for picking. */ break; } } if (ob_act) { /* In weight-paint we select the associated vertex group too. */ if (ob_act->mode & OB_MODE_ALL_WEIGHT_PAINT) { if (bone == arm->act_bone) { blender::ed::object::vgroup_select_by_name(ob_act, bone->name); DEG_id_tag_update(&ob_act->id, ID_RECALC_GEOMETRY); } } /* If there are some dependencies for visualizing armature state * (e.g. Mask Modifier in 'Armature' mode), force update. */ else if (arm->flag & ARM_HAS_VIZ_DEPS) { /* NOTE: ob not ob_act here is intentional - it's the source of the * bones being selected [#37247]. */ DEG_id_tag_update(&ob->id, ID_RECALC_GEOMETRY); } /* Tag armature for copy-on-evaluation update (since act_bone is in armature not object). */ DEG_id_tag_update(&arm->id, ID_RECALC_SYNC_TO_EVAL); } changed = true; } return changed || found; } bool ED_armature_pose_select_pick_with_buffer(const Scene *scene, ViewLayer *view_layer, View3D *v3d, Base *base, const GPUSelectResult *hit_results, const int hits, const SelectPick_Params ¶ms, bool do_nearest) { Object *ob = base->object; Bone *nearBone; if (!ob || !ob->pose) { return false; } /* Callers happen to already get the active base */ Base *base_dummy = nullptr; nearBone = ED_armature_pick_bone_from_selectbuffer( {base}, hit_results, hits, true, do_nearest, &base_dummy); return ED_armature_pose_select_pick_bone(scene, view_layer, v3d, ob, nearBone, params); } void ED_armature_pose_select_in_wpaint_mode(const Scene *scene, ViewLayer *view_layer, Base *base_select) { BLI_assert(base_select && (base_select->object->type == OB_ARMATURE)); BKE_view_layer_synced_ensure(scene, view_layer); Object *ob_active = BKE_view_layer_active_object_get(view_layer); BLI_assert(ob_active && (ob_active->mode & OB_MODE_ALL_WEIGHT_PAINT)); VirtualModifierData virtual_modifier_data; ModifierData *md = BKE_modifiers_get_virtual_modifierlist(ob_active, &virtual_modifier_data); for (; md; md = md->next) { if (md->type == eModifierType_Armature) { ArmatureModifierData *amd = reinterpret_cast(md); Object *ob_arm = amd->object; if (ob_arm != nullptr) { Base *base_arm = BKE_view_layer_base_find(view_layer, ob_arm); if ((base_arm != nullptr) && (base_arm != base_select) && (base_arm->flag & BASE_SELECTED)) { blender::ed::object::base_select(base_arm, blender::ed::object::BA_DESELECT); } } } } if ((base_select->flag & BASE_SELECTED) == 0) { blender::ed::object::base_select(base_select, blender::ed::object::BA_SELECT); } } bool ED_pose_deselect_all(Object *ob, int select_mode, const bool ignore_visibility) { bArmature *arm = static_cast(ob->data); /* we call this from outliner too */ if (ob->pose == nullptr) { return false; } /* Determine if we're selecting or deselecting */ if (select_mode == SEL_TOGGLE) { select_mode = SEL_SELECT; LISTBASE_FOREACH (bPoseChannel *, pchan, &ob->pose->chanbase) { if (ignore_visibility || blender::animrig::bone_is_visible_pchan(arm, pchan)) { if (pchan->bone->flag & BONE_SELECTED) { select_mode = SEL_DESELECT; break; } } } } /* Set the flags accordingly */ bool changed = false; LISTBASE_FOREACH (bPoseChannel *, pchan, &ob->pose->chanbase) { /* ignore the pchan if it isn't visible or if its selection cannot be changed */ if (ignore_visibility || blender::animrig::bone_is_visible_pchan(arm, pchan)) { int flag_prev = pchan->bone->flag; pose_do_bone_select(pchan, select_mode); changed = (changed || flag_prev != pchan->bone->flag); } } return changed; } static bool ed_pose_is_any_selected(Object *ob, bool ignore_visibility) { bArmature *arm = static_cast(ob->data); LISTBASE_FOREACH (bPoseChannel *, pchan, &ob->pose->chanbase) { if (ignore_visibility || blender::animrig::bone_is_visible_pchan(arm, pchan)) { if (pchan->bone->flag & BONE_SELECTED) { return true; } } } return false; } static bool ed_pose_is_any_selected_multi(const Span bases, bool ignore_visibility) { for (Base *base : bases) { Object *ob_iter = base->object; if (ed_pose_is_any_selected(ob_iter, ignore_visibility)) { return true; } } return false; } bool ED_pose_deselect_all_multi_ex(const Span bases, int select_mode, const bool ignore_visibility) { if (select_mode == SEL_TOGGLE) { select_mode = ed_pose_is_any_selected_multi(bases, ignore_visibility) ? SEL_DESELECT : SEL_SELECT; } bool changed_multi = false; for (Base *base : bases) { Object *ob_iter = base->object; if (ED_pose_deselect_all(ob_iter, select_mode, ignore_visibility)) { ED_pose_bone_select_tag_update(ob_iter); changed_multi = true; } } return changed_multi; } bool ED_pose_deselect_all_multi(bContext *C, int select_mode, const bool ignore_visibility) { Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); ViewContext vc = ED_view3d_viewcontext_init(C, depsgraph); Vector bases = BKE_object_pose_base_array_get_unique(vc.scene, vc.view_layer, vc.v3d); return ED_pose_deselect_all_multi_ex(bases, select_mode, ignore_visibility); } /* ***************** Selections ********************** */ static void selectconnected_posebonechildren(Object *ob, Bone *bone, int extend) { /* stop when unconnected child is encountered, or when unselectable bone is encountered */ if (!(bone->flag & BONE_CONNECTED) || (bone->flag & BONE_UNSELECTABLE)) { return; } if (extend) { bone->flag &= ~BONE_SELECTED; } else { bone->flag |= BONE_SELECTED; } LISTBASE_FOREACH (Bone *, curBone, &bone->childbase) { selectconnected_posebonechildren(ob, curBone, extend); } } /* within active object context */ /* previously known as "selectconnected_posearmature" */ static wmOperatorStatus pose_select_connected_invoke(bContext *C, wmOperator *op, const wmEvent *event) { Bone *bone, *curBone, *next = nullptr; const bool extend = RNA_boolean_get(op->ptr, "extend"); view3d_operator_needs_gpu(C); Base *base = nullptr; bone = ED_armature_pick_bone(C, event->mval, !extend, &base); if (!bone) { return OPERATOR_CANCELLED; } /* Select parents */ for (curBone = bone; curBone; curBone = next) { /* ignore bone if cannot be selected */ if ((curBone->flag & BONE_UNSELECTABLE) == 0) { if (extend) { curBone->flag &= ~BONE_SELECTED; } else { curBone->flag |= BONE_SELECTED; } if (curBone->flag & BONE_CONNECTED) { next = curBone->parent; } else { next = nullptr; } } else { next = nullptr; } } /* Select children */ LISTBASE_FOREACH (Bone *, curBone, &bone->childbase) { selectconnected_posebonechildren(base->object, curBone, extend); } ED_outliner_select_sync_from_pose_bone_tag(C); ED_pose_bone_select_tag_update(base->object); return OPERATOR_FINISHED; } static bool pose_select_linked_pick_poll(bContext *C) { return (ED_operator_view3d_active(C) && ED_operator_posemode(C)); } void POSE_OT_select_linked_pick(wmOperatorType *ot) { PropertyRNA *prop; /* identifiers */ ot->name = "Select Connected"; ot->idname = "POSE_OT_select_linked_pick"; ot->description = "Select bones linked by parent/child connections under the mouse cursor"; /* callbacks */ /* leave 'exec' unset */ ot->invoke = pose_select_connected_invoke; ot->poll = pose_select_linked_pick_poll; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; /* props */ prop = RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend selection instead of deselecting everything first"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); } static wmOperatorStatus pose_select_linked_exec(bContext *C, wmOperator * /*op*/) { Bone *curBone, *next = nullptr; CTX_DATA_BEGIN_WITH_ID (C, bPoseChannel *, pchan, visible_pose_bones, Object *, ob) { if ((pchan->bone->flag & BONE_SELECTED) == 0) { continue; } bArmature *arm = static_cast(ob->data); /* Select parents */ for (curBone = pchan->bone; curBone; curBone = next) { if (PBONE_SELECTABLE(arm, curBone)) { curBone->flag |= BONE_SELECTED; if (curBone->flag & BONE_CONNECTED) { next = curBone->parent; } else { next = nullptr; } } else { next = nullptr; } } /* Select children */ LISTBASE_FOREACH (Bone *, curBone, &pchan->bone->childbase) { selectconnected_posebonechildren(ob, curBone, false); } ED_pose_bone_select_tag_update(ob); } CTX_DATA_END; ED_outliner_select_sync_from_pose_bone_tag(C); return OPERATOR_FINISHED; } void POSE_OT_select_linked(wmOperatorType *ot) { /* identifiers */ ot->name = "Select Connected"; ot->idname = "POSE_OT_select_linked"; ot->description = "Select all bones linked by parent/child connections to the current selection"; /* callbacks */ ot->exec = pose_select_linked_exec; ot->poll = ED_operator_posemode; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } /* -------------------------------------- */ static wmOperatorStatus pose_de_select_all_exec(bContext *C, wmOperator *op) { int action = RNA_enum_get(op->ptr, "action"); Scene *scene = CTX_data_scene(C); int multipaint = scene->toolsettings->multipaint; if (action == SEL_TOGGLE) { action = CTX_DATA_COUNT(C, selected_pose_bones) ? SEL_DESELECT : SEL_SELECT; } Object *ob_prev = nullptr; /* Set the flags. */ CTX_DATA_BEGIN_WITH_ID (C, bPoseChannel *, pchan, visible_pose_bones, Object *, ob) { bArmature *arm = static_cast(ob->data); pose_do_bone_select(pchan, action); if (ob_prev != ob) { /* Weight-paint or mask modifiers need depsgraph updates. */ if (multipaint || (arm->flag & ARM_HAS_VIZ_DEPS)) { DEG_id_tag_update(&ob->id, ID_RECALC_GEOMETRY); } /* need to tag armature for cow updates, or else selection doesn't update */ DEG_id_tag_update(&arm->id, ID_RECALC_SYNC_TO_EVAL); ob_prev = ob; } } CTX_DATA_END; ED_outliner_select_sync_from_pose_bone_tag(C); WM_event_add_notifier(C, NC_OBJECT | ND_BONE_SELECT, nullptr); return OPERATOR_FINISHED; } void POSE_OT_select_all(wmOperatorType *ot) { /* identifiers */ ot->name = "(De)select All"; ot->idname = "POSE_OT_select_all"; ot->description = "Toggle selection status of all bones"; /* API callbacks. */ ot->exec = pose_de_select_all_exec; ot->poll = ED_operator_posemode; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; WM_operator_properties_select_all(ot); } /* -------------------------------------- */ static wmOperatorStatus pose_select_parent_exec(bContext *C, wmOperator * /*op*/) { Object *ob = BKE_object_pose_armature_get(CTX_data_active_object(C)); bArmature *arm = static_cast(ob->data); bPoseChannel *pchan, *parent; /* Determine if there is an active bone */ pchan = CTX_data_active_pose_bone(C); if (pchan) { parent = pchan->parent; if ((parent) && !(parent->bone->flag & (BONE_HIDDEN_P | BONE_UNSELECTABLE))) { parent->bone->flag |= BONE_SELECTED; arm->act_bone = parent->bone; } else { return OPERATOR_CANCELLED; } } else { return OPERATOR_CANCELLED; } ED_outliner_select_sync_from_pose_bone_tag(C); ED_pose_bone_select_tag_update(ob); return OPERATOR_FINISHED; } void POSE_OT_select_parent(wmOperatorType *ot) { /* identifiers */ ot->name = "Select Parent Bone"; ot->idname = "POSE_OT_select_parent"; ot->description = "Select bones that are parents of the currently selected bones"; /* API callbacks. */ ot->exec = pose_select_parent_exec; ot->poll = ED_operator_posemode; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } /* -------------------------------------- */ static wmOperatorStatus pose_select_constraint_target_exec(bContext *C, wmOperator * /*op*/) { bool found = false; CTX_DATA_BEGIN (C, bPoseChannel *, pchan, visible_pose_bones) { if (pchan->bone->flag & BONE_SELECTED) { LISTBASE_FOREACH (bConstraint *, con, &pchan->constraints) { ListBase targets = {nullptr, nullptr}; if (BKE_constraint_targets_get(con, &targets)) { LISTBASE_FOREACH (bConstraintTarget *, ct, &targets) { Object *ob = ct->tar; /* Any armature that is also in pose mode should be selected. */ if ((ct->subtarget[0] != '\0') && (ob != nullptr) && (ob->type == OB_ARMATURE) && (ob->mode == OB_MODE_POSE)) { bPoseChannel *pchanc = BKE_pose_channel_find_name(ob->pose, ct->subtarget); if ((pchanc) && !(pchanc->bone->flag & BONE_UNSELECTABLE)) { pchanc->bone->flag |= BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL; ED_pose_bone_select_tag_update(ob); found = true; } } } BKE_constraint_targets_flush(con, &targets, true); } } } } CTX_DATA_END; if (!found) { return OPERATOR_CANCELLED; } ED_outliner_select_sync_from_pose_bone_tag(C); return OPERATOR_FINISHED; } void POSE_OT_select_constraint_target(wmOperatorType *ot) { /* identifiers */ ot->name = "Select Constraint Target"; ot->idname = "POSE_OT_select_constraint_target"; ot->description = "Select bones used as targets for the currently selected bones"; /* API callbacks. */ ot->exec = pose_select_constraint_target_exec; ot->poll = ED_operator_posemode; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } /* -------------------------------------- */ /* No need to convert to multi-objects. Just like we keep the non-active bones * selected we then keep the non-active objects untouched (selected/unselected). */ static wmOperatorStatus pose_select_hierarchy_exec(bContext *C, wmOperator *op) { Object *ob = BKE_object_pose_armature_get(CTX_data_active_object(C)); bArmature *arm = static_cast(ob->data); bPoseChannel *pchan_act; int direction = RNA_enum_get(op->ptr, "direction"); const bool add_to_sel = RNA_boolean_get(op->ptr, "extend"); bool changed = false; pchan_act = BKE_pose_channel_active_if_bonecoll_visible(ob); if (pchan_act == nullptr) { return OPERATOR_CANCELLED; } if (direction == BONE_SELECT_PARENT) { if (pchan_act->parent) { Bone *bone_parent; bone_parent = pchan_act->parent->bone; if (PBONE_SELECTABLE(arm, bone_parent)) { if (!add_to_sel) { pchan_act->bone->flag &= ~BONE_SELECTED; } bone_parent->flag |= BONE_SELECTED; arm->act_bone = bone_parent; changed = true; } } } else { /* direction == BONE_SELECT_CHILD */ Bone *bone_child = nullptr; int pass; /* first pass, only connected bones (the logical direct child) */ for (pass = 0; pass < 2 && (bone_child == nullptr); pass++) { LISTBASE_FOREACH (bPoseChannel *, pchan_iter, &ob->pose->chanbase) { /* possible we have multiple children, some invisible */ if (PBONE_SELECTABLE(arm, pchan_iter->bone)) { if (pchan_iter->parent == pchan_act) { if ((pass == 1) || (pchan_iter->bone->flag & BONE_CONNECTED)) { bone_child = pchan_iter->bone; break; } } } } } if (bone_child) { arm->act_bone = bone_child; if (!add_to_sel) { pchan_act->bone->flag &= ~BONE_SELECTED; } bone_child->flag |= BONE_SELECTED; changed = true; } } if (changed == false) { return OPERATOR_CANCELLED; } ED_outliner_select_sync_from_pose_bone_tag(C); ED_pose_bone_select_tag_update(ob); return OPERATOR_FINISHED; } void POSE_OT_select_hierarchy(wmOperatorType *ot) { static const EnumPropertyItem direction_items[] = { {BONE_SELECT_PARENT, "PARENT", 0, "Select Parent", ""}, {BONE_SELECT_CHILD, "CHILD", 0, "Select Child", ""}, {0, nullptr, 0, nullptr, nullptr}, }; /* identifiers */ ot->name = "Select Hierarchy"; ot->idname = "POSE_OT_select_hierarchy"; ot->description = "Select immediate parent/children of selected bones"; /* API callbacks. */ ot->exec = pose_select_hierarchy_exec; ot->poll = ED_operator_posemode; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; /* props */ ot->prop = RNA_def_enum( ot->srna, "direction", direction_items, BONE_SELECT_PARENT, "Direction", ""); RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend the selection"); } /* -------------------------------------- */ /* Modes for the `select_grouped` operator. */ enum class SelectRelatedMode { SAME_COLLECTION = 0, SAME_COLOR, SAME_KEYINGSET, CHILDREN, IMMEDIATE_CHILDREN, PARENT, SIBLINGS, }; static bool pose_select_same_color(bContext *C, const bool extend) { /* Get a set of all the colors of the selected bones. */ blender::Set used_colors; blender::Set updated_objects; bool changed_any_selection = false; /* Old approach that we may want to reinstate behind some option at some point. This will match * against the colors of all selected bones, instead of just the active one. It also explains why * there is a set of colors to begin with. * * CTX_DATA_BEGIN (C, bPoseChannel *, pchan, selected_pose_bones) { * auto color = blender::animrig::ANIM_bonecolor_posebone_get(pchan); * used_colors.add(color); * } * CTX_DATA_END; */ if (!extend) { CTX_DATA_BEGIN_WITH_ID (C, bPoseChannel *, pchan, selected_pose_bones, Object *, ob) { pchan->bone->flag &= ~BONE_SELECTED; updated_objects.add(ob); changed_any_selection = true; } CTX_DATA_END; } /* Use the color of the active pose bone. */ bPoseChannel *active_pose_bone = CTX_data_active_pose_bone(C); auto color = blender::animrig::ANIM_bonecolor_posebone_get(active_pose_bone); used_colors.add(color); /* Select all visible bones that have the same color. */ CTX_DATA_BEGIN_WITH_ID (C, bPoseChannel *, pchan, visible_pose_bones, Object *, ob) { Bone *bone = pchan->bone; if (bone->flag & (BONE_UNSELECTABLE | BONE_SELECTED)) { /* Skip bones that are unselectable or already selected. */ continue; } auto color = blender::animrig::ANIM_bonecolor_posebone_get(pchan); if (!used_colors.contains(color)) { continue; } bone->flag |= BONE_SELECTED; changed_any_selection = true; updated_objects.add(ob); } CTX_DATA_END; if (!changed_any_selection) { return false; } for (Object *ob : updated_objects) { ED_pose_bone_select_tag_update(ob); } return true; } static bool pose_select_same_collection(bContext *C, const bool extend) { bool changed_any_selection = false; blender::Set updated_objects; /* Refuse to do anything if there is no active pose bone. */ bPoseChannel *active_pchan = CTX_data_active_pose_bone(C); if (!active_pchan) { return false; } if (!extend) { /* Deselect all the bones. */ CTX_DATA_BEGIN_WITH_ID (C, bPoseChannel *, pchan, selected_pose_bones, Object *, ob) { pchan->bone->flag &= ~BONE_SELECTED; updated_objects.add(ob); changed_any_selection = true; } CTX_DATA_END; } /* Build a set of bone collection names, to allow cross-Armature selection. */ blender::Set collection_names; LISTBASE_FOREACH (BoneCollectionReference *, bcoll_ref, &active_pchan->bone->runtime.collections) { collection_names.add(bcoll_ref->bcoll->name); } /* Select all bones that match any of the collection names. */ CTX_DATA_BEGIN_WITH_ID (C, bPoseChannel *, pchan, visible_pose_bones, Object *, ob) { Bone *bone = pchan->bone; if (bone->flag & (BONE_UNSELECTABLE | BONE_SELECTED)) { continue; } LISTBASE_FOREACH (BoneCollectionReference *, bcoll_ref, &bone->runtime.collections) { if (!collection_names.contains(bcoll_ref->bcoll->name)) { continue; } bone->flag |= BONE_SELECTED; changed_any_selection = true; updated_objects.add(ob); } } CTX_DATA_END; for (Object *ob : updated_objects) { ED_pose_bone_select_tag_update(ob); } return changed_any_selection; } /* Useful to get the selection before modifying it. */ static blender::Set get_selected_pose_bones(Object *pose_object) { blender::Set selected_pose_bones; bArmature *arm = static_cast((pose_object) ? pose_object->data : nullptr); LISTBASE_FOREACH (bPoseChannel *, pchan, &pose_object->pose->chanbase) { if (PBONE_SELECTED(arm, pchan->bone)) { selected_pose_bones.add(pchan); } } return selected_pose_bones; } static bool pose_bone_is_below_one_of(bPoseChannel &bone, const blender::Set &potential_parents) { bPoseChannel *bone_iter = &bone; while (bone_iter) { if (potential_parents.contains(bone_iter)) { return true; } bone_iter = bone_iter->parent; } return false; } static void deselect_pose_bones(const blender::Set &pose_bones) { for (bPoseChannel *pose_bone : pose_bones) { if (!pose_bone) { /* There may be a nullptr in the set if selecting siblings of root bones. */ continue; } pose_bone->bone->flag &= ~BONE_SELECTED; } } /* Selects children of currently selected bones in all objects in pose mode. If `all` is true, a * bone will be selected if any bone in it's parent hierarchy is selected. If false, only bones * whose direct parent is selected are changed. */ static bool pose_select_children(bContext *C, const bool all, const bool extend) { Vector objects = BKE_object_pose_array_get_unique( CTX_data_scene(C), CTX_data_view_layer(C), CTX_wm_view3d(C)); bool changed_any_selection = false; for (Object *pose_object : objects) { bArmature *arm = static_cast(pose_object->data); BLI_assert(arm); blender::Set selected_pose_bones = get_selected_pose_bones(pose_object); if (!extend) { deselect_pose_bones(selected_pose_bones); } LISTBASE_FOREACH (bPoseChannel *, pchan, &pose_object->pose->chanbase) { if (!PBONE_SELECTABLE(arm, pchan->bone)) { continue; } if (all) { if (pose_bone_is_below_one_of(*pchan, selected_pose_bones)) { pose_do_bone_select(pchan, SEL_SELECT); changed_any_selection = true; } } else { if (selected_pose_bones.contains(pchan->parent)) { pose_do_bone_select(pchan, SEL_SELECT); changed_any_selection = true; } } } ED_pose_bone_select_tag_update(pose_object); } return changed_any_selection; } static bool pose_select_parents(bContext *C, const bool extend) { Vector objects = BKE_object_pose_array_get_unique( CTX_data_scene(C), CTX_data_view_layer(C), CTX_wm_view3d(C)); bool changed_any_selection = false; for (Object *pose_object : objects) { bArmature *arm = static_cast(pose_object->data); BLI_assert(arm); blender::Set selected_pose_bones = get_selected_pose_bones(pose_object); if (!extend) { deselect_pose_bones(selected_pose_bones); } for (bPoseChannel *pchan : selected_pose_bones) { if (!pchan->parent) { continue; } if (!PBONE_SELECTABLE(arm, pchan->parent->bone)) { continue; } pose_do_bone_select(pchan->parent, SEL_SELECT); changed_any_selection = true; } ED_pose_bone_select_tag_update(pose_object); } return changed_any_selection; } static bool pose_select_siblings(bContext *C, const bool extend) { Vector objects = BKE_object_pose_array_get_unique( CTX_data_scene(C), CTX_data_view_layer(C), CTX_wm_view3d(C)); bool changed_any_selection = false; for (Object *pose_object : objects) { bArmature *arm = static_cast(pose_object->data); BLI_assert(arm); blender::Set parents_of_selected; LISTBASE_FOREACH (bPoseChannel *, pchan, &pose_object->pose->chanbase) { if (PBONE_SELECTED(arm, pchan->bone)) { parents_of_selected.add(pchan->parent); } } if (!extend) { deselect_pose_bones(parents_of_selected); } LISTBASE_FOREACH (bPoseChannel *, pchan, &pose_object->pose->chanbase) { if (!PBONE_SELECTABLE(arm, pchan->bone)) { continue; } /* Checking if the bone is already selected so `changed_any_selection` stays true to its * word. */ if (parents_of_selected.contains(pchan->parent) && !PBONE_SELECTED(arm, pchan->bone)) { pose_do_bone_select(pchan, SEL_SELECT); changed_any_selection = true; } } ED_pose_bone_select_tag_update(pose_object); } return changed_any_selection; } static bool pose_select_same_keyingset(bContext *C, ReportList *reports, bool extend) { using namespace blender::animrig; Scene *scene = CTX_data_scene(C); ViewLayer *view_layer = CTX_data_view_layer(C); bool changed_multi = false; KeyingSet *ks = scene_get_active_keyingset(CTX_data_scene(C)); /* sanity checks: validate Keying Set and object */ if (ks == nullptr) { BKE_report(reports, RPT_ERROR, "No active Keying Set to use"); return false; } if (validate_keyingset(C, nullptr, ks) != ModifyKeyReturn::SUCCESS) { if (ks->paths.first == nullptr) { if ((ks->flag & KEYINGSET_ABSOLUTE) == 0) { BKE_report(reports, RPT_ERROR, "Use another Keying Set, as the active one depends on the currently " "selected items or cannot find any targets due to unsuitable context"); } else { BKE_report(reports, RPT_ERROR, "Keying Set does not contain any paths"); } } return false; } /* if not extending selection, deselect all selected first */ if (extend == false) { CTX_DATA_BEGIN (C, bPoseChannel *, pchan, visible_pose_bones) { if ((pchan->bone->flag & BONE_UNSELECTABLE) == 0) { pchan->bone->flag &= ~BONE_SELECTED; } } CTX_DATA_END; } Vector objects = BKE_object_pose_array_get_unique(scene, view_layer, CTX_wm_view3d(C)); for (const int ob_index : objects.index_range()) { Object *ob = BKE_object_pose_armature_get(objects[ob_index]); bArmature *arm = static_cast((ob) ? ob->data : nullptr); bPose *pose = (ob) ? ob->pose : nullptr; bool changed = false; /* Sanity checks. */ if (ELEM(nullptr, ob, pose, arm)) { continue; } /* iterate over elements in the Keying Set, setting selection depending on whether * that bone is visible or not... */ LISTBASE_FOREACH (KS_Path *, ksp, &ks->paths) { /* only items related to this object will be relevant */ if ((ksp->id == &ob->id) && (ksp->rna_path != nullptr)) { bPoseChannel *pchan = nullptr; char boneName[sizeof(pchan->name)]; if (!BLI_str_quoted_substr(ksp->rna_path, "bones[", boneName, sizeof(boneName))) { continue; } pchan = BKE_pose_channel_find_name(pose, boneName); if (pchan) { /* select if bone is visible and can be affected */ if (PBONE_SELECTABLE(arm, pchan->bone)) { pchan->bone->flag |= BONE_SELECTED; changed = true; } } } } if (changed || !extend) { ED_pose_bone_select_tag_update(ob); changed_multi = true; } } return changed_multi; } static wmOperatorStatus pose_select_grouped_exec(bContext *C, wmOperator *op) { Object *ob = BKE_object_pose_armature_get(CTX_data_active_object(C)); const SelectRelatedMode mode = SelectRelatedMode(RNA_enum_get(op->ptr, "type")); const bool extend = RNA_boolean_get(op->ptr, "extend"); bool changed = false; /* sanity check */ if (ob->pose == nullptr) { return OPERATOR_CANCELLED; } /* selection types */ switch (mode) { case SelectRelatedMode::SAME_COLLECTION: changed = pose_select_same_collection(C, extend); break; case SelectRelatedMode::SAME_COLOR: changed = pose_select_same_color(C, extend); break; case SelectRelatedMode::SAME_KEYINGSET: changed = pose_select_same_keyingset(C, op->reports, extend); break; case SelectRelatedMode::CHILDREN: changed = pose_select_children(C, true, extend); break; case SelectRelatedMode::IMMEDIATE_CHILDREN: changed = pose_select_children(C, false, extend); break; case SelectRelatedMode::PARENT: changed = pose_select_parents(C, extend); break; case SelectRelatedMode::SIBLINGS: changed = pose_select_siblings(C, extend); break; default: printf("pose_select_grouped() - Unknown selection type %d\n", int(mode)); break; } /* report done status */ if (changed) { ED_outliner_select_sync_from_pose_bone_tag(C); return OPERATOR_FINISHED; } return OPERATOR_CANCELLED; } void POSE_OT_select_grouped(wmOperatorType *ot) { static const EnumPropertyItem prop_select_grouped_types[] = { {int(SelectRelatedMode::SAME_COLLECTION), "COLLECTION", 0, "Collection", "Same collections as the active bone"}, {int(SelectRelatedMode::SAME_COLOR), "COLOR", 0, "Color", "Same color as the active bone"}, {int(SelectRelatedMode::SAME_KEYINGSET), "KEYINGSET", 0, "Keying Set", "All bones affected by active Keying Set"}, {int(SelectRelatedMode::CHILDREN), "CHILDREN", 0, "Children", "Select all children of currently selected bones"}, {int(SelectRelatedMode::IMMEDIATE_CHILDREN), "CHILDREN_IMMEDIATE", 0, "Immediate Children", "Select direct children of currently selected bones"}, {int(SelectRelatedMode::PARENT), "PARENT", 0, "Parents", "Select the parents of currently selected bones"}, {int(SelectRelatedMode::SIBLINGS), "SIBILINGS", 0, "Siblings", "Select all bones that have the same parent as currently selected bones"}, {0, nullptr, 0, nullptr, nullptr}, }; /* identifiers */ ot->name = "Select Grouped"; ot->description = "Select all visible bones grouped by similar properties"; ot->idname = "POSE_OT_select_grouped"; /* API callbacks. */ ot->invoke = WM_menu_invoke; ot->exec = pose_select_grouped_exec; ot->poll = ED_operator_posemode; /* TODO: expand to support edit mode as well. */ /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; /* properties */ RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend selection instead of deselecting everything first"); ot->prop = RNA_def_enum(ot->srna, "type", prop_select_grouped_types, 0, "Type", ""); } /* -------------------------------------- */ /* Add the given selection flags to the bone flags. */ static void bone_selection_flags_add(bPoseChannel *pchan, const eBone_Flag new_selection_flags) { pchan->bone->flag |= (new_selection_flags & BONE_SELECTED); } /* Set the bone flags to the given selection flags. */ static void bone_selection_flags_set(bPoseChannel *pchan, const eBone_Flag new_selection_flags) { pchan->bone->flag &= ~(BONE_SELECTED | BONE_TIPSEL | BONE_ROOTSEL); pchan->bone->flag |= (new_selection_flags & BONE_SELECTED); } /** * \note clone of #armature_select_mirror_exec keep in sync */ static wmOperatorStatus pose_select_mirror_exec(bContext *C, wmOperator *op) { const Scene *scene = CTX_data_scene(C); ViewLayer *view_layer = CTX_data_view_layer(C); Object *ob_active = CTX_data_active_object(C); const bool is_weight_paint = (ob_active->mode & OB_MODE_WEIGHT_PAINT) != 0; const bool active_only = RNA_boolean_get(op->ptr, "only_active"); const bool extend = RNA_boolean_get(op->ptr, "extend"); const auto set_bone_selection_flags = extend ? bone_selection_flags_add : bone_selection_flags_set; Vector objects = BKE_object_pose_array_get_unique(scene, view_layer, CTX_wm_view3d(C)); for (Object *ob : objects) { bArmature *arm = static_cast(ob->data); bPoseChannel *pchan_mirror_act = nullptr; /* Remember the pre-mirroring selection flags of the bones. */ blender::Map old_selection_flags; LISTBASE_FOREACH (bPoseChannel *, pchan, &ob->pose->chanbase) { /* Treat invisible bones as deselected. */ const int flags = blender::animrig::bone_is_visible_pchan(arm, pchan) ? pchan->bone->flag : 0; old_selection_flags.add_new(pchan, eBone_Flag(flags)); } LISTBASE_FOREACH (bPoseChannel *, pchan, &ob->pose->chanbase) { if (!PBONE_SELECTABLE(arm, pchan->bone)) { continue; } bPoseChannel *pchan_mirror = BKE_pose_channel_get_mirrored(ob->pose, pchan->name); if (!pchan_mirror) { /* If a bone cannot be mirrored, keep its flags as-is. This makes it possible to select * the spine and an arm, and still flip the selection to the other arm (without losing * the selection on the spine). */ continue; } if (pchan->bone == arm->act_bone) { pchan_mirror_act = pchan_mirror; } /* If active-only, don't touch unrelated bones. */ if (active_only && !ELEM(arm->act_bone, pchan->bone, pchan_mirror->bone)) { continue; } const eBone_Flag flags_mirror = old_selection_flags.lookup(pchan_mirror); set_bone_selection_flags(pchan, flags_mirror); } if (pchan_mirror_act) { arm->act_bone = pchan_mirror_act->bone; /* In weight-paint we select the associated vertex group too. */ if (is_weight_paint) { blender::ed::object::vgroup_select_by_name(ob_active, pchan_mirror_act->name); DEG_id_tag_update(&ob_active->id, ID_RECALC_GEOMETRY); } } WM_event_add_notifier(C, NC_OBJECT | ND_BONE_SELECT, ob); /* Need to tag armature for cow updates, or else selection doesn't update. */ DEG_id_tag_update(&arm->id, ID_RECALC_SYNC_TO_EVAL); } ED_outliner_select_sync_from_pose_bone_tag(C); return OPERATOR_FINISHED; } void POSE_OT_select_mirror(wmOperatorType *ot) { /* identifiers */ ot->name = "Select Mirror"; ot->idname = "POSE_OT_select_mirror"; ot->description = "Mirror the bone selection"; /* API callbacks. */ ot->exec = pose_select_mirror_exec; ot->poll = ED_operator_posemode; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; /* properties */ RNA_def_boolean( ot->srna, "only_active", false, "Active Only", "Only operate on the active bone"); RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend the selection"); }