Merge branch 'blender-v4.5-release'

This commit is contained in:
Campbell Barton
2025-06-05 15:41:51 +10:00
6 changed files with 240 additions and 27 deletions

View File

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

View File

@@ -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) {

View File

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

View File

@@ -8,6 +8,8 @@
* Removes isolated geometry regions without creating holes in the mesh.
*/
#include <cmath>
#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);
}
}
/** \} */

View File

@@ -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;
}

View File

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