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
This commit is contained in:
Campbell Barton
2025-09-20 12:45:30 +10:00
parent bdae3e28a2
commit 93cc17dd72
3 changed files with 63 additions and 1 deletions

View File

@@ -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*/

View File

@@ -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<BMVert *>(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<blender::Vector<BMVert *>>(__func__);
}
blender::Vector<BMVert *> *cluster = static_cast<blender::Vector<BMVert *> *>(*cluster_p);
cluster->append(v);
}
}
/* Compute centroid for each survivor. */
GHashIterator gh_iter;
GHASH_ITER (gh_iter, clusters) {
BMVert *v_dst = static_cast<BMVert *>(BLI_ghashIterator_getKey(&gh_iter));
blender::Vector<BMVert *> *cluster = static_cast<blender::Vector<BMVert *> *>(
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<BMVert *> *cluster = static_cast<blender::Vector<BMVert *> *>(
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<BMVert *>(BMO_slot_map_elem_get(slot_targetmap, v));

View File

@@ -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,