From 93cc17dd720d5b4d24a8022851b5c8baa09ffcde Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Sat, 20 Sep 2025 12:45:30 +1000 Subject: [PATCH] Fix #78916: unpredictable results with merge by distance The merge by distance operator now has an optional merge centroid option, when it is enabled, groups of merged vertices are averaged and moved to their centroid position. This allows for more predictable results in cases where vertices that form loops would have otherwise collapsed unevenly and ended up with jagged lines. Ref !146478 --- .../blender/bmesh/intern/bmesh_opdefines.cc | 3 ++ .../bmesh/operators/bmo_removedoubles.cc | 46 +++++++++++++++++++ source/blender/editors/mesh/editmesh_tools.cc | 15 +++++- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/source/blender/bmesh/intern/bmesh_opdefines.cc b/source/blender/bmesh/intern/bmesh_opdefines.cc index 0d1670653bc..a21008aa5b3 100644 --- a/source/blender/bmesh/intern/bmesh_opdefines.cc +++ b/source/blender/bmesh/intern/bmesh_opdefines.cc @@ -623,6 +623,9 @@ static BMOpDefine bmo_weld_verts_def = { { /* Maps welded vertices to verts they should weld to. */ {"targetmap", BMO_OP_SLOT_MAPPING, {eBMOpSlotSubType_Elem(BMO_OP_SLOT_SUBTYPE_MAP_ELEM)}}, + /* Merged vertices to their centroid position, + * otherwise the position of the target vertex is used. */ + {"use_centroid", BMO_OP_SLOT_BOOL}, {{'\0'}}, }, /*slot_types_out*/ diff --git a/source/blender/bmesh/operators/bmo_removedoubles.cc b/source/blender/bmesh/operators/bmo_removedoubles.cc index 10aa558fc52..51e59d84e6a 100644 --- a/source/blender/bmesh/operators/bmo_removedoubles.cc +++ b/source/blender/bmesh/operators/bmo_removedoubles.cc @@ -187,6 +187,7 @@ void bmo_weld_verts_exec(BMesh *bm, BMOperator *op) BMLoop *l; BMFace *f; BMOpSlot *slot_targetmap = BMO_slot_get(op->slots_in, "targetmap"); + const bool use_centroid = BMO_slot_bool_get(op->slots_in, "use_centroid"); /* Maintain selection history. */ const bool has_selected = !BLI_listbase_is_empty(&bm->selected); @@ -197,6 +198,51 @@ void bmo_weld_verts_exec(BMesh *bm, BMOperator *op) targetmap_all = BLI_ghash_ptr_new(__func__); } + if (use_centroid) { + GHash *clusters = BLI_ghash_ptr_new(__func__); + + /* Group vertices by their survivor. */ + BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) { + BMVert *v_dst = static_cast(BMO_slot_map_elem_get(slot_targetmap, v)); + if (v_dst && v_dst != v) { + void **cluster_p; + if (!BLI_ghash_ensure_p(clusters, v_dst, &cluster_p)) { + *cluster_p = MEM_new>(__func__); + } + blender::Vector *cluster = static_cast *>(*cluster_p); + cluster->append(v); + } + } + + /* Compute centroid for each survivor. */ + GHashIterator gh_iter; + GHASH_ITER (gh_iter, clusters) { + BMVert *v_dst = static_cast(BLI_ghashIterator_getKey(&gh_iter)); + blender::Vector *cluster = static_cast *>( + BLI_ghashIterator_getValue(&gh_iter)); + + float centroid[3]; + copy_v3_v3(centroid, v_dst->co); + int count = 1; /* Include `v_dst`. */ + + for (BMVert *v_duplicate : *cluster) { + add_v3_v3(centroid, v_duplicate->co); + count++; + } + + mul_v3_fl(centroid, 1.0f / float(count)); + copy_v3_v3(v_dst->co, centroid); + } + + /* Free temporary cluster storage. */ + GHASH_ITER (gh_iter, clusters) { + blender::Vector *cluster = static_cast *>( + BLI_ghashIterator_getValue(&gh_iter)); + MEM_delete(cluster); + } + BLI_ghash_free(clusters, nullptr, nullptr); + } + /* mark merge verts for deletion */ BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) { BMVert *v_dst = static_cast(BMO_slot_map_elem_get(slot_targetmap, v)); diff --git a/source/blender/editors/mesh/editmesh_tools.cc b/source/blender/editors/mesh/editmesh_tools.cc index 089efd813c9..e1a9b7a56ab 100644 --- a/source/blender/editors/mesh/editmesh_tools.cc +++ b/source/blender/editors/mesh/editmesh_tools.cc @@ -3581,7 +3581,13 @@ static wmOperatorStatus edbm_remove_doubles_exec(bContext *C, wmOperator *op) BMO_op_exec(em->bm, &bmop); - if (!EDBM_op_callf(em, op, "weld_verts targetmap=%S", &bmop, "targetmap.out")) { + if (!EDBM_op_callf(em, + op, + "weld_verts targetmap=%S use_centroid=%b", + &bmop, + "targetmap.out", + RNA_boolean_get(op->ptr, "use_centroid"))) + { BMO_op_finish(em->bm, &bmop); continue; } @@ -3640,6 +3646,13 @@ void MESH_OT_remove_doubles(wmOperatorType *ot) "Maximum distance between elements to merge", 1e-5f, 10.0f); + RNA_def_boolean(ot->srna, + "use_centroid", + true, + "Centroid Merge", + "Move vertices to the centroid of the duplicate cluster, " + "otherwise the vertex closest to the centroid is used."); + RNA_def_boolean(ot->srna, "use_unselected", false,