Anim: Duplicate rename operator for bones

This implements an operator that duplicates edit bones
and does a search replace at the same time.
Crucially, the search replace is done before name collisions
are checked so this avoids a potential rename to append `.001` by
blender.

When the "replace" is left empty, the "search" is just removed.
The search and replace has no fancy regex logic built in.

When called, the operator pops up a GUI

On a technical level this is very much identical to the existing
`ARMATURE_OT_duplicate` but I had to create a separate
operator that has an invoke function for the menu. Otherwise the menu
pops up every time you press `shift+D`.
I refactored out a separate function to avoid code duplication

Pull Request: https://projects.blender.org/blender/blender/pulls/154553
This commit is contained in:
Christoph Lendenfeld 2026-05-12 11:07:43 +02:00 • committed by Christoph Lendenfeld
parent a80b9d811a
commit f67497581b
4 changed files with 76 additions and 2 deletions

View file

@ -5632,6 +5632,7 @@ class VIEW3D_MT_edit_armature(Menu):
layout.operator("armature.extrude_forked")
layout.operator("armature.duplicate_move")
layout.operator("armature.duplicate_rename")
layout.operator("armature.fill")
layout.separator()

View file

@ -1107,12 +1107,14 @@ EditBone *duplicateEditBone(EditBone *cur_bone,
return duplicateEditBoneObjects(cur_bone, name, editbones, ob, ob);
}
static wmOperatorStatus armature_duplicate_selected_exec(bContext *C, wmOperator *op)
static wmOperatorStatus armature_duplicate_selected(bContext *C,
const bool do_flip_names,
StringRefNull search,
StringRefNull replace)
{
const Main *bmain = CTX_data_main(C);
const Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
const bool do_flip_names = RNA_boolean_get(op->ptr, "do_flip_names");
/* cancel if nothing selected */
if (CTX_DATA_COUNT(C, selected_bones) == 0) {
@ -1166,6 +1168,11 @@ static wmOperatorStatus armature_duplicate_selected_exec(bContext *C, wmOperator
new_bone_name = new_bone_name_buff;
}
}
std::string buffer(new_bone_name);
if (!search.is_empty()) {
BLI_string_replace(buffer, search, replace);
new_bone_name = buffer.data();
}
ebone = duplicateEditBone(ebone_iter, new_bone_name, arm->edbo, ob);
@ -1242,6 +1249,12 @@ static wmOperatorStatus armature_duplicate_selected_exec(bContext *C, wmOperator
return OPERATOR_FINISHED;
}
static wmOperatorStatus armature_duplicate_selected_exec(bContext *C, wmOperator *op)
{
const bool do_flip_names = RNA_boolean_get(op->ptr, "do_flip_names");
return armature_duplicate_selected(C, do_flip_names, "", "");
}
void ARMATURE_OT_duplicate(wmOperatorType *ot)
{
/* identifiers */
@ -1264,6 +1277,64 @@ void ARMATURE_OT_duplicate(wmOperatorType *ot)
"Try to flip names of the bones, if possible, instead of adding a number extension");
}
static wmOperatorStatus armature_duplicate_rename_exec(bContext *C, wmOperator *op)
{
const bool do_flip_names = RNA_boolean_get(op->ptr, "do_flip_names");
std::string search = RNA_string_get(op->ptr, "search");
std::string replace = RNA_string_get(op->ptr, "replace");
if (search.empty()) {
BKE_report(op->reports, RPT_ERROR, "No search term defined");
return OPERATOR_CANCELLED;
}
return armature_duplicate_selected(C, do_flip_names, search, replace);
}
static wmOperatorStatus armature_duplicate_rename_invoke(bContext *C,
wmOperator *op,
const wmEvent *event)
{
return WM_operator_props_popup_confirm(C, op, event);
}
void ARMATURE_OT_duplicate_rename(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Duplicate and Rename";
ot->idname = "ARMATURE_OT_duplicate_rename";
ot->description =
"Make copies of the selected bones within the same armature and replace a part of their "
"name";
/* API callbacks. */
ot->invoke = armature_duplicate_rename_invoke;
ot->exec = armature_duplicate_rename_exec;
ot->poll = ED_operator_editarmature;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
RNA_def_boolean(
ot->srna,
"do_flip_names",
false,
"Flip Names",
"Try to flip names of the bones, if possible, instead of adding a number extension");
RNA_def_string(ot->srna,
"search",
nullptr,
MAXBONENAME,
"Search",
"A part of the current bone name that will be replaced");
RNA_def_string(ot->srna,
"replace",
nullptr,
MAXBONENAME,
"Replace",
"The substitute to be inserted into the place of the given search term. If left "
"empty the search term will be removed");
}
/* Get the duplicated or existing mirrored copy of the bone. */
static EditBone *get_symmetrized_bone(bArmature *arm, EditBone *bone)
{

View file

@ -57,6 +57,7 @@ void ARMATURE_OT_shortest_path_pick(wmOperatorType *ot);
void ARMATURE_OT_delete(wmOperatorType *ot);
void ARMATURE_OT_dissolve(wmOperatorType *ot);
void ARMATURE_OT_duplicate(wmOperatorType *ot);
void ARMATURE_OT_duplicate_rename(wmOperatorType *ot);
void ARMATURE_OT_symmetrize(wmOperatorType *ot);
void ARMATURE_OT_extrude(wmOperatorType *ot);
void ARMATURE_OT_hide(wmOperatorType *ot);

View file

@ -49,6 +49,7 @@ void ED_operatortypes_armature()
WM_operatortype_append(ARMATURE_OT_delete);
WM_operatortype_append(ARMATURE_OT_dissolve);
WM_operatortype_append(ARMATURE_OT_duplicate);
WM_operatortype_append(ARMATURE_OT_duplicate_rename);
WM_operatortype_append(ARMATURE_OT_symmetrize);
WM_operatortype_append(ARMATURE_OT_extrude);
WM_operatortype_append(ARMATURE_OT_hide);