Skip to content

Commit 1dbd712

Browse files
committed
feat(meshing): trimesh_remesh — protect_boundary + protect_sharp_edges_angle_deg
Expose CGAL's edge_is_constrained_map + protect_constraints for isotropic_remeshing. Two new optional kwargs (both default-disabled, backward-compatible): - protect_boundary=True → all boundary edges constrained; preserves boundary curves verbatim including sharp corners that the default smoothing pass otherwise rounds. - protect_sharp_edges_angle_deg=>0 → also constrain interior edges with dihedral angle >= threshold (via PMP::detect_sharp_edges). Pattern matches existing edge_is_constrained_map usage in src/{isolines,geodesics,booleans}.cpp.
1 parent b435eb0 commit 1dbd712

3 files changed

Lines changed: 65 additions & 8 deletions

File tree

src/compas_cgal/meshing.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ def trimesh_remesh(
1414
target_edge_length: float,
1515
number_of_iterations: int = 10,
1616
do_project: bool = True,
17+
protect_boundary: bool = False,
18+
protect_sharp_edges_angle_deg: float = 0.0,
1719
) -> VerticesFacesNumpy:
1820
"""Remeshing of a triangle mesh.
1921
@@ -27,15 +29,26 @@ def trimesh_remesh(
2729
Number of remeshing iterations.
2830
do_project : bool, optional
2931
If True, reproject vertices onto the input surface when they are created or displaced.
32+
protect_boundary : bool, optional
33+
If True, constrain all boundary edges so they are NOT split, collapsed,
34+
or flipped during remeshing. Use this to preserve the input mesh's
35+
boundary curve verbatim — including sharp corners that the default
36+
smoothing pass would otherwise round.
37+
protect_sharp_edges_angle_deg : float, optional
38+
Dihedral threshold in degrees for interior feature detection. Edges
39+
whose adjacent faces form a dihedral angle ≥ this value are marked
40+
constrained and preserved. ``0.0`` disables interior-feature
41+
detection (default).
3042
3143
Returns
3244
-------
3345
VerticesFacesNumpy
3446
3547
Notes
3648
-----
37-
This remeshing function only constrains the edges on the boundary of the mesh.
38-
Protecting specific features or edges is not implemented yet.
49+
Without ``protect_boundary`` or ``protect_sharp_edges_angle_deg`` set,
50+
boundary edges follow CGAL's default remeshing behaviour: re-sampled
51+
to ``target_edge_length`` (visible corner rounding is the cost).
3952
4053
Examples
4154
--------
@@ -52,7 +65,15 @@ def trimesh_remesh(
5265
V, F = mesh
5366
V = np.asarray(V, dtype=np.float64, order="C")
5467
F = np.asarray(F, dtype=np.int32, order="C")
55-
return _meshing.pmp_trimesh_remesh(V, F, target_edge_length, number_of_iterations, do_project)
68+
return _meshing.pmp_trimesh_remesh(
69+
V,
70+
F,
71+
target_edge_length,
72+
number_of_iterations,
73+
do_project,
74+
protect_boundary,
75+
protect_sharp_edges_angle_deg,
76+
)
5677

5778

5879
def trimesh_dual(

src/meshing.cpp

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <CGAL/boost/graph/Dual.h>
66
#include <CGAL/boost/graph/helpers.h>
77
#include <CGAL/Polygon_mesh_processing/compute_normal.h>
8+
#include <CGAL/Polygon_mesh_processing/detect_features.h>
89

910
#include <iostream>
1011
#include <fstream>
@@ -43,21 +44,52 @@ pmp_trimesh_remesh(
4344
Eigen::Ref<const compas::RowMatrixXi> faces_a,
4445
double target_edge_length,
4546
unsigned int number_of_iterations,
46-
bool do_project)
47+
bool do_project,
48+
bool protect_boundary,
49+
double protect_sharp_edges_angle_deg)
4750
{
4851
// Convert input matrices to CGAL mesh and keep a copy for projection
4952
compas::Mesh original_mesh = compas::mesh_from_vertices_and_faces(vertices_a, faces_a);
5053
compas::Mesh mesh_a = compas::mesh_from_vertices_and_faces(vertices_a, faces_a);
5154

55+
// Build an edge-is-constrained property map. Edges marked True are
56+
// not split / collapsed / flipped during isotropic_remeshing — this
57+
// is how features are preserved.
58+
auto ecm = mesh_a.add_property_map<
59+
boost::graph_traits<compas::Mesh>::edge_descriptor, bool>(
60+
"e:is_constrained", false).first;
61+
62+
// Constrain all boundary edges when requested. CGAL's
63+
// isotropic_remeshing otherwise re-samples boundary edges per
64+
// target_edge_length, which rounds visible corners.
65+
if (protect_boundary) {
66+
for (auto e : edges(mesh_a)) {
67+
auto h = halfedge(e, mesh_a);
68+
if (is_border(h, mesh_a) || is_border(opposite(h, mesh_a), mesh_a)) {
69+
put(ecm, e, true);
70+
}
71+
}
72+
}
73+
74+
// Detect sharp interior edges (dihedral > threshold) and constrain
75+
// them too. 0.0 disables (default).
76+
if (protect_sharp_edges_angle_deg > 0.0) {
77+
CGAL::Polygon_mesh_processing::detect_sharp_edges(
78+
mesh_a, protect_sharp_edges_angle_deg, ecm);
79+
}
80+
5281
// Perform isotropic remeshing
5382
CGAL::Polygon_mesh_processing::isotropic_remeshing(
5483
faces(mesh_a),
5584
target_edge_length,
5685
mesh_a,
5786
CGAL::Polygon_mesh_processing::parameters::number_of_iterations(number_of_iterations)
58-
.do_project(do_project));
87+
.do_project(do_project)
88+
.edge_is_constrained_map(ecm)
89+
.protect_constraints(protect_boundary
90+
|| protect_sharp_edges_angle_deg > 0.0));
91+
5992

60-
6193
// Clean up the mesh
6294
mesh_a.collect_garbage();
6395

@@ -888,7 +920,9 @@ NB_MODULE(_meshing, m) {
888920
"faces_a"_a,
889921
"target_edge_length"_a,
890922
"number_of_iterations"_a = 10,
891-
"do_project"_a = true
923+
"do_project"_a = true,
924+
"protect_boundary"_a = false,
925+
"protect_sharp_edges_angle_deg"_a = 0.0
892926
);
893927

894928
m.def(

src/meshing.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ pmp_trimesh_remesh(
6363
Eigen::Ref<const compas::RowMatrixXi> faces_a,
6464
double target_edge_length,
6565
unsigned int number_of_iterations = 10,
66-
bool do_project = true);
66+
bool do_project = true,
67+
bool protect_boundary = false,
68+
double protect_sharp_edges_angle_deg = 0.0);
6769

6870

6971
/**

0 commit comments

Comments
 (0)