diff --git a/source/blender/bmesh/intern/bmesh_opdefines.cc b/source/blender/bmesh/intern/bmesh_opdefines.cc index c3e2117dd27..aa09761bea5 100644 --- a/source/blender/bmesh/intern/bmesh_opdefines.cc +++ b/source/blender/bmesh/intern/bmesh_opdefines.cc @@ -1344,6 +1344,8 @@ static BMOpDefine bmo_dissolve_edges_def = { {"use_verts", BMO_OP_SLOT_BOOL}, /* Split off face corners to maintain surrounding geometry. */ {"use_face_split", BMO_OP_SLOT_BOOL}, + /* Do not dissolve verts between 2 edges when their angle exceeds this threshold. */ + {"angle_threshold", BMO_OP_SLOT_FLT}, {{'\0'}}, }, /*slot_types_out*/ diff --git a/source/blender/bmesh/intern/bmesh_query.cc b/source/blender/bmesh/intern/bmesh_query.cc index 156d40eb42a..1dc0e66319b 100644 --- a/source/blender/bmesh/intern/bmesh_query.cc +++ b/source/blender/bmesh/intern/bmesh_query.cc @@ -595,7 +595,7 @@ bool BM_vert_is_edge_pair_manifold(const BMVert *v) return false; } -bool BM_vert_edge_pair(BMVert *v, BMEdge **r_e_a, BMEdge **r_e_b) +bool BM_vert_edge_pair(const BMVert *v, BMEdge **r_e_a, BMEdge **r_e_b) { BMEdge *e_a = v->e; if (e_a) { diff --git a/source/blender/bmesh/intern/bmesh_query.hh b/source/blender/bmesh/intern/bmesh_query.hh index 364196ee65e..fc12963ff4e 100644 --- a/source/blender/bmesh/intern/bmesh_query.hh +++ b/source/blender/bmesh/intern/bmesh_query.hh @@ -289,7 +289,7 @@ bool BM_vert_is_edge_pair_manifold(const BMVert *v) ATTR_WARN_UNUSED_RESULT ATTR * * \return true when only 2 verts are found. */ -bool BM_vert_edge_pair(BMVert *v, BMEdge **r_e_a, BMEdge **r_e_b); +bool BM_vert_edge_pair(const BMVert *v, BMEdge **r_e_a, BMEdge **r_e_b); /** * Return true if the vertex is connected to _any_ faces. * diff --git a/source/blender/bmesh/operators/bmo_dissolve.cc b/source/blender/bmesh/operators/bmo_dissolve.cc index c2e6bd811f0..7567fc0a7d0 100644 --- a/source/blender/bmesh/operators/bmo_dissolve.cc +++ b/source/blender/bmesh/operators/bmo_dissolve.cc @@ -8,6 +8,8 @@ * Removes isolated geometry regions without creating holes in the mesh. */ +#include + #include "MEM_guardedalloc.h" #include "BLI_math_vector.h" @@ -31,6 +33,11 @@ using blender::Vector; #define EDGE_MARK 1 #define EDGE_TAG 2 #define EDGE_ISGC 8 +/** + * Set when the edge is part of a chain, + * where at least of it's vertices has exactly one other connected edge. + */ +#define EDGE_CHAIN 16 #define VERT_MARK 1 #define VERT_MARK_PAIR 4 @@ -38,6 +45,10 @@ using blender::Vector; #define VERT_ISGC 8 #define VERT_MARK_TEAR 16 +/* -------------------------------------------------------------------- */ +/** \name Internal Utility API + * \{ */ + static bool UNUSED_FUNCTION(check_hole_in_region)(BMesh *bm, BMFace *f) { BMWalker regwalker; @@ -73,6 +84,58 @@ static bool UNUSED_FUNCTION(check_hole_in_region)(BMesh *bm, BMFace *f) return true; } +/** + * Calculates the angle of an edge pair, from a combination of raw angle and normal angle. + */ +static float bmo_vert_calc_edge_angle_blended(const BMVert *v) +{ + BMEdge *e_pair[2]; + const bool is_edge_pair = BM_vert_edge_pair(v, &e_pair[0], &e_pair[1]); + + BLI_assert(is_edge_pair); + UNUSED_VARS_NDEBUG(is_edge_pair); + + /* Compute the angle between the edges. Start with the raw angle. */ + BMVert *v_a = BM_edge_other_vert(e_pair[0], v); + BMVert *v_b = BM_edge_other_vert(e_pair[1], v); + float angle = M_PI - angle_v3v3v3(v_a->co, v->co, v_b->co); + + /* There are two ways to measure the angle around a vert with two edges. The first is to + * measure the raw angle between the two neighboring edges, the second is to measure the + * angle of the edges around the vertex normal vector. When the vert is an edge pair + * between two faces, The normal measurement is better in general. In the specific case of + * a vert between two faces, but the faces have a *very* sharp angle between them, then the + * raw angle is better, because the normal is perpendicular to average of the two faces, + * and if the faces are folded almost 180 degrees, the vertex normal becomes more an more + * edge-on to the faces, meaning the angle *around the normal* becomes more and more flat, + * even if it makes a sharp angle when viewed from the side. + * + * When the faces become very folded, the `raw_factor` adds some of the "as seen from the side" + * angle back into the computation, making the algorithm behave more intuitively. + * + * The `raw_factor` is computed as follows: + * - When not a face pair, part this is skipped, and the raw angle is used. + * - When a face pair is co-planar, or has an angle up to 90 degrees, `raw_factor` is 0.0. + * - As angle increases from 90 to 180 degrees, `raw_factor` increases from 0.0 to 1.0. + */ + BMFace *f_pair[2]; + if (BM_edge_face_pair(v->e, &f_pair[0], &f_pair[1])) { + /* Due to merges, the normals are not currently trustworthy. Compute them. */ + float no_a[3], no_b[3]; + BM_face_calc_normal(f_pair[0], no_a); + BM_face_calc_normal(f_pair[1], no_b); + + /* Now determine the raw factor based on how folded the faces are.*/ + const float raw_factor = std::clamp(-dot_v3v3(no_a, no_b), 0.0f, 1.0f); + + /* Blend the two ways of computing the angle. */ + float normal_angle = M_PI - angle_on_axis_v3v3v3_v3(v_a->co, v->co, v_b->co, v->no); + angle = interpf(angle, normal_angle, raw_factor); + } + + return angle; +} + static void bm_face_split(BMesh *bm, const short oflag, bool use_edge_delete) { BLI_Stack *edge_delete_verts; @@ -117,6 +180,12 @@ static void bm_face_split(BMesh *bm, const short oflag, bool use_edge_delete) } } +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Public Execute Functions + * \{ */ + void bmo_dissolve_faces_exec(BMesh *bm, BMOperator *op) { BMOIter oiter; @@ -258,7 +327,20 @@ void bmo_dissolve_edges_exec(BMesh *bm, BMOperator *op) BMEdge *e, *e_next; BMVert *v, *v_next; - const bool use_verts = BMO_slot_bool_get(op->slots_in, "use_verts"); + /* Even when geometry has exact angles like 0 or 90 or 180 deg, `angle_on_axis_v3v3v3_v3` + * can return slightly incorrect values due to cos/sin functions, floating point error, etc. + * This lets the test ignore that tiny bit of math error so users won't notice. */ + const float angle_epsilon = RAD2DEGF(0.0001f); + + const float angle_threshold = BMO_slot_float_get(op->slots_in, "angle_threshold"); + + /* Use verts when told to... except, do *not* use verts when angle_threshold is 0.0. */ + const bool use_verts = BMO_slot_bool_get(op->slots_in, "use_verts") && + (angle_threshold > angle_epsilon); + + /* If angle threshold is 180, don't bother with angle math, just dissolve everything. */ + const bool dissolve_all = (angle_threshold > M_PI - angle_epsilon); + const bool use_face_split = BMO_slot_bool_get(op->slots_in, "use_face_split"); if (use_face_split) { @@ -291,12 +373,22 @@ void bmo_dissolve_edges_exec(BMesh *bm, BMOperator *op) } } - /* tag all verts/edges connected to faces */ - /* Any element tagged with xxx_ISGC is an edge or vert of a face that borders an edge to be - * dissolved, and it could end up being cleaned up after a face merge has made it irrelevant. */ + /* Tag certain geometry around the selected edges, for later processing. */ BMO_ITER (e, &eiter, op->slots_in, "edges", BM_EDGE) { + + /* Connected edge chains have endpoints with edge pairs. The existing behavior was to dissolve + * the verts, both in the middle, and at the ends, of any selected edges in chains. Mark these + * kind of edges, so we know to skip the angle threshold test later. */ + if (BM_vert_is_edge_pair(e->v1) || BM_vert_is_edge_pair(e->v2)) { + BMO_edge_flag_enable(bm, e, EDGE_CHAIN); + } + BMFace *f_pair[2]; if (BM_edge_face_pair(e, &f_pair[0], &f_pair[1])) { + /* Tag all the edges and verts of the two faces on either side of this edge. + * This edge is going to be dissolved, and after that happens, some of those elements of the + * surrounding faces might end up as loose geometry, depending on how the dissolve affected + * geometry near them. Tag them `*_ISGC`, to be checked later, and cleaned up if loose. */ uint j; for (j = 0; j < 2; j++) { BMLoop *l_first, *l_iter; @@ -335,12 +427,63 @@ void bmo_dissolve_edges_exec(BMesh *bm, BMOperator *op) /* If dissolving verts, then evaluate each VERT_MARK vert. */ if (use_verts) { - BM_ITER_MESH_MUTABLE (v, v_next, &iter, bm, BM_VERTS_OF_MESH) { - if (BMO_vert_flag_test(bm, v, VERT_MARK)) { - if (BM_vert_is_edge_pair(v)) { - BM_vert_collapse_edge(bm, v->e, v, true, true, true); - } + BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) { + if (!BMO_vert_flag_test(bm, v, VERT_MARK)) { + continue; } + + /* If it is not an edge pair, it cannot be merged. */ + BMEdge *e_pair[2]; + if (BM_vert_edge_pair(v, &e_pair[0], &e_pair[1]) == false) { + BMO_vert_flag_disable(bm, v, VERT_MARK); + continue; + } + + /* At an angle threshold of 180, dissolve everything, skip the math of the angle test. */ + if (dissolve_all) { + /* VERT_MARK remains enabled. */ + continue; + } + + /* Verts in edge chains ignore the angle test. This maintains the previous behavior, + * where such verts were not subject to the angle threshold. + * + * When edge chains are selected for dissolve, all edge-pair verts at *both* ends of each + * selected edge will be dissolved, combining the selected edges into their neighbors. + * + * Note that when only *part* of a chain is selected, this *will* alter unselected edges, + * because selected edges will merge *into their unselected neighbors*. This too, has been + * maintained, for consistency with the previous (but possibly unintentional) behavior. */ + if (BMO_edge_flag_test(bm, e_pair[0], EDGE_CHAIN) || + BMO_edge_flag_test(bm, e_pair[1], EDGE_CHAIN)) + { + /* VERT_MARK remains enabled. */ + continue; + } + + /* If the angle at the vert is larger than the threshold, it cannot be merged. */ + if (bmo_vert_calc_edge_angle_blended(v) > angle_threshold - angle_epsilon) { + BMO_vert_flag_disable(bm, v, VERT_MARK); + continue; + } + } + + /* Dissolve all verts that remain tagged. This is done in a separate iteration pass. Otherwise + * the early dissolves would alter the angles measured at neighboring verts tested later. */ + BM_ITER_MESH_MUTABLE (v, v_next, &iter, bm, BM_VERTS_OF_MESH) { + if (!BMO_vert_flag_test(bm, v, VERT_MARK)) { + continue; + } + + /* Ensured in the previous loop. */ + BLI_assert(BM_vert_is_edge_pair(v)); + + /* Merge the header flags on the two edges that will be merged. */ + BMEdge *e_pair[2]; + BM_vert_edge_pair(v, &e_pair[0], &e_pair[1]); + BM_elem_flag_merge_ex(e_pair[0], e_pair[1], BM_ELEM_HIDDEN); + + BM_vert_collapse_edge(bm, v->e, v, true, true, true); } } } @@ -616,3 +759,5 @@ void bmo_dissolve_degenerate_exec(BMesh *bm, BMOperator *op) bm_mesh_edge_collapse_flagged(bm, op->flag, EDGE_COLLAPSE); } } + +/** \} */ diff --git a/source/blender/editors/mesh/editmesh_tools.cc b/source/blender/editors/mesh/editmesh_tools.cc index b0c1edabc6e..d13527981ce 100644 --- a/source/blender/editors/mesh/editmesh_tools.cc +++ b/source/blender/editors/mesh/editmesh_tools.cc @@ -5798,8 +5798,11 @@ static void edbm_dissolve_prop__use_verts(wmOperatorType *ot, bool value, int fl { PropertyRNA *prop; - prop = RNA_def_boolean( - ot->srna, "use_verts", value, "Dissolve Vertices", "Dissolve remaining vertices"); + prop = RNA_def_boolean(ot->srna, + "use_verts", + value, + "Dissolve Vertices", + "Dissolve remaining vertices which connect to only two edges"); if (flag) { RNA_def_property_flag(prop, PropertyFlag(flag)); @@ -5821,6 +5824,22 @@ static void edbm_dissolve_prop__use_boundary_tear(wmOperatorType *ot) "Tear Boundary", "Split off face corners instead of merging faces"); } +static void edbm_dissolve_prop__use_angle_threshold(wmOperatorType *ot) +{ + PropertyRNA *prop = RNA_def_float_rotation( + ot->srna, + "angle_threshold", + 0, + nullptr, + 0.0f, + DEG2RADF(180.0f), + "Angle Threshold", + "Remaining vertices which separate edge pairs are preserved if their edge angle exceeds " + "this threshold.", + 0.0f, + DEG2RADF(180.0f)); + RNA_def_property_float_default(prop, DEG2RADF(20.0f)); +} static wmOperatorStatus edbm_dissolve_verts_exec(bContext *C, wmOperator *op) { @@ -5891,6 +5910,7 @@ static wmOperatorStatus edbm_dissolve_edges_exec(bContext *C, wmOperator *op) { const bool use_verts = RNA_boolean_get(op->ptr, "use_verts"); const bool use_face_split = RNA_boolean_get(op->ptr, "use_face_split"); + const float angle_threshold = RNA_float_get(op->ptr, "angle_threshold"); const Scene *scene = CTX_data_scene(C); ViewLayer *view_layer = CTX_data_view_layer(C); @@ -5905,12 +5925,14 @@ static wmOperatorStatus edbm_dissolve_edges_exec(bContext *C, wmOperator *op) BM_custom_loop_normals_to_vector_layer(em->bm); - if (!EDBM_op_callf(em, - op, - "dissolve_edges edges=%he use_verts=%b use_face_split=%b", - BM_ELEM_SELECT, - use_verts, - use_face_split)) + if (!EDBM_op_callf( + em, + op, + "dissolve_edges edges=%he use_verts=%b use_face_split=%b angle_threshold=%f", + BM_ELEM_SELECT, + use_verts, + use_face_split, + angle_threshold)) { continue; } @@ -5942,6 +5964,7 @@ void MESH_OT_dissolve_edges(wmOperatorType *ot) ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; edbm_dissolve_prop__use_verts(ot, true, 0); + edbm_dissolve_prop__use_angle_threshold(ot); edbm_dissolve_prop__use_face_split(ot); } @@ -6036,6 +6059,32 @@ static wmOperatorStatus edbm_dissolve_mode_exec(bContext *C, wmOperator *op) return edbm_dissolve_faces_exec(C, op); } +static bool dissolve_mode_poll_property(const bContext *C, wmOperator *op, const PropertyRNA *prop) +{ + UNUSED_VARS(op); + + const char *prop_id = RNA_property_identifier(prop); + + Object *obedit = CTX_data_edit_object(C); + const BMEditMesh *em = BKE_editmesh_from_object(obedit); + bool is_edge_select_mode = false; + + if (em->selectmode & SCE_SELECT_VERTEX) { + /* Pass. */ + } + if (em->selectmode & SCE_SELECT_EDGE) { + is_edge_select_mode = true; + } + + if (!is_edge_select_mode) { + /* Angle Threshold is only used in edge select mode. */ + if (STREQ(prop_id, "angle_threshold")) { + return false; + } + } + return true; +} + void MESH_OT_dissolve_mode(wmOperatorType *ot) { /* identifiers */ @@ -6046,6 +6095,7 @@ void MESH_OT_dissolve_mode(wmOperatorType *ot) /* API callbacks. */ ot->exec = edbm_dissolve_mode_exec; ot->poll = ED_operator_editmesh; + ot->poll_property = dissolve_mode_poll_property; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; @@ -6053,6 +6103,7 @@ void MESH_OT_dissolve_mode(wmOperatorType *ot) edbm_dissolve_prop__use_verts(ot, false, PROP_SKIP_SAVE); edbm_dissolve_prop__use_face_split(ot); edbm_dissolve_prop__use_boundary_tear(ot); + edbm_dissolve_prop__use_angle_threshold(ot); } /** \} */ @@ -6295,12 +6346,14 @@ static wmOperatorStatus edbm_delete_edgeloop_exec(bContext *C, wmOperator *op) } } - if (!EDBM_op_callf(em, - op, - "dissolve_edges edges=%he use_verts=%b use_face_split=%b", - BM_ELEM_SELECT, - true, - use_face_split)) + if (!EDBM_op_callf( + em, + op, + "dissolve_edges edges=%he use_verts=%b use_face_split=%b angle_threshold=%f", + BM_ELEM_SELECT, + true, + use_face_split, + M_PI)) { continue; } diff --git a/tests/python/operators.py b/tests/python/operators.py index bc00df4e091..59eff04e659 100644 --- a/tests/python/operators.py +++ b/tests/python/operators.py @@ -112,8 +112,21 @@ def main(): ), SpecMeshTest( - "CylinderDissolveEdges.UseVertsTrue", "testCylinderDissolveEdges", "expectedCylinderDissolveEdges.DissolveAllVerts", - [OperatorSpecEditMode("dissolve_edges", {"use_verts": True}, "EDGE", {0, 5, 6, 9})], + "CylinderDissolveEdges.AngleThrehsoldNoDissolve", "testCylinderDissolveEdges", "expectedCylinderDissolveEdges.DissolveNoVerts", + [OperatorSpecEditMode("dissolve_edges", {"use_verts": True, + "angle_threshold": 0}, "EDGE", {0, 5, 6, 9})], + ), + + SpecMeshTest( + "CylinderDissolveEdges.AngleThresholdSomeDissolve", "testCylinderDissolveEdges", "expectedCylinderDissolveEdges.DissolveThresh.218166", + [OperatorSpecEditMode("dissolve_edges", {"use_verts": True, + "angle_threshold": 0.218166}, "EDGE", {0, 5, 6, 9})], + ), + + SpecMeshTest( + "CylinderDissolveEdges.AngleThresholdAllDissolve", "testCylinderDissolveEdges", "expectedCylinderDissolveEdges.DissolveAllVerts", + [OperatorSpecEditMode("dissolve_edges", {"use_verts": True, + "angle_threshold": 3.14159}, "EDGE", {0, 5, 6, 9})], ), # dissolve faces