Skip to content

Commit 3f0c13f

Browse files
authored
Merge pull request #18 from Loop3D/fix/expand-clipper
Fix/expand clipper
2 parents 2173262 + 3a8648a commit 3f0c13f

4 files changed

Lines changed: 292 additions & 17 deletions

File tree

loop_cgal/bindings.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ PYBIND11_MODULE(_loop_cgal, m)
2525
.def("cut_with_surface", &TriMesh::cutWithSurface, py::arg("surface"),
2626
py::arg("preserve_intersection") = false,
2727
py::arg("preserve_intersection_clipper") = false,
28-
py::arg("use_exact_kernel") = true)
28+
py::arg("use_exact_kernel") = true,
29+
"Cut mesh with a clipper surface. Returns the number of faces removed "
30+
"(0 indicates the clipper did not intersect or did not extend beyond the mesh).")
2931
.def("remesh", &TriMesh::remesh, py::arg("split_long_edges") = true,
3032
py::arg("target_edge_length") = 10.0,
3133
py::arg("number_of_iterations") = 3,

src/mesh.cpp

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
#include <CGAL/Polygon_mesh_processing/bbox.h>
66
#include <CGAL/Polygon_mesh_processing/clip.h>
77
#include <CGAL/Polygon_mesh_processing/corefinement.h>
8-
#include <CGAL/Polygon_mesh_processing/merge_border_vertices.h>
8+
#include <CGAL/Polygon_mesh_processing/stitch_borders.h>
99
#include <CGAL/Polygon_mesh_processing/remesh.h>
1010
#include <CGAL/Polygon_mesh_processing/self_intersections.h>
11+
#include <CGAL/Polygon_mesh_processing/compute_normal.h>
1112
#include <CGAL/Polygon_mesh_processing/triangulate_faces.h>
1213
#include <CGAL/Simple_cartesian.h>
1314
#include <CGAL/Surface_mesh.h>
@@ -248,6 +249,7 @@ void TriMesh::remesh(bool split_long_edges,
248249
if (split_long_edges)
249250
if (LoopCGAL::verbose)
250251
std::cout << "Splitting long edges in iteration " << iter + 1 << ".\n";
252+
251253
PMP::split_long_edges(
252254
edges(_mesh), target_edge_length, _mesh,
253255
CGAL::parameters::edge_is_constrained_map(_edge_is_constrained_map));
@@ -289,12 +291,11 @@ void TriMesh::reverseFaceOrientation()
289291
}
290292
}
291293

292-
void TriMesh::cutWithSurface(TriMesh &clipper,
294+
int TriMesh::cutWithSurface(TriMesh &clipper,
293295
bool preserve_intersection,
294296
bool preserve_intersection_clipper,
295297
bool use_exact_kernel)
296298
{
297-
298299
if (LoopCGAL::verbose)
299300
{
300301
std::cout << "Cutting mesh with surface." << std::endl;
@@ -304,29 +305,124 @@ void TriMesh::cutWithSurface(TriMesh &clipper,
304305
if (!CGAL::is_valid_polygon_mesh(_mesh, LoopCGAL::verbose))
305306
{
306307
std::cerr << "Error: Source mesh is invalid!" << std::endl;
307-
return;
308+
return 0;
308309
}
309310

310311
if (!CGAL::is_valid_polygon_mesh(clipper._mesh, LoopCGAL::verbose))
311312
{
312313
std::cerr << "Error: Clipper mesh is invalid!" << std::endl;
313-
return;
314+
return 0;
314315
}
315316

316317
if (_mesh.number_of_vertices() == 0 || _mesh.number_of_faces() == 0)
317318
{
318319
std::cerr << "Error: Source mesh is empty!" << std::endl;
319-
return;
320+
return 0;
320321
}
321322

322323
if (clipper._mesh.number_of_vertices() == 0 ||
323324
clipper._mesh.number_of_faces() == 0)
324325
{
325326
std::cerr << "Error: Clipper mesh is empty!" << std::endl;
326-
return;
327+
return 0;
328+
}
329+
330+
// Merge any collocated border vertices on the target before clipping.
331+
// Collocated vertices produce zero-area faces whose degenerate bounding boxes
332+
// can make PMP::do_intersect return false even when the meshes overlap.
333+
PMP::stitch_borders(_mesh);
334+
// Remove any isolated (orphan) vertices — they survive PMP::clip unchanged
335+
// and would pollute the output point set with stale positions.
336+
PMP::remove_isolated_vertices(_mesh);
337+
338+
// -----------------------------------------------------------------------
339+
// Grow the clipper by extruding a skirt of new triangles outward from each
340+
// boundary edge. Each skirt quad's normal matches its adjacent border face,
341+
// so the extension is along the surface tangent — not a global scale.
342+
// Interior vertices and faces are completely untouched, so the cut location
343+
// is preserved exactly for both planar and curved (listric) clippers.
344+
// -----------------------------------------------------------------------
345+
CGAL::Bbox_3 target_bb = PMP::bbox(_mesh);
346+
const double target_diag = std::sqrt(
347+
CGAL::square(target_bb.xmax() - target_bb.xmin()) +
348+
CGAL::square(target_bb.ymax() - target_bb.ymin()) +
349+
CGAL::square(target_bb.zmax() - target_bb.zmin()));
350+
351+
// Pass 1: accumulate per-ver tex outward directions from each adjacent border face.
352+
// For a border halfedge h (source→target), the outward direction is fn × d,
353+
// where fn is the adjacent face normal and d is the normalised edge direction.
354+
// This lies in the face's tangent plane and points away from its interior.
355+
std::map<TriangleMesh::Vertex_index, Vector> outward_sum;
356+
for (auto he : clipper._mesh.halfedges())
357+
{
358+
if (!clipper._mesh.is_border(he)) continue;
359+
360+
const Point &ps = clipper._mesh.point(clipper._mesh.source(he));
361+
const Point &pt = clipper._mesh.point(clipper._mesh.target(he));
362+
Vector d = pt - ps;
363+
const double d_len = std::sqrt(d.squared_length());
364+
if (d_len < 1e-10) continue;
365+
d = d / d_len;
366+
367+
const auto adj_face = clipper._mesh.face(clipper._mesh.opposite(he));
368+
const Vector fn = PMP::compute_face_normal(adj_face, clipper._mesh);
369+
const Vector out = CGAL::cross_product(fn, d);
370+
const double out_len = std::sqrt(out.squared_length());
371+
if (out_len < 1e-10) continue;
372+
373+
auto vs = clipper._mesh.source(he);
374+
auto vt = clipper._mesh.target(he);
375+
outward_sum.try_emplace(vs, 0.0, 0.0, 0.0);
376+
outward_sum.try_emplace(vt, 0.0, 0.0, 0.0);
377+
outward_sum[vs] = outward_sum[vs] + out / out_len;
378+
outward_sum[vt] = outward_sum[vt] + out / out_len;
379+
}
380+
381+
// Pass 2: copy the clipper, merge any collocated border vertices (they
382+
// produce degenerate faces whose normals are near-zero, which would cause
383+
// skirt edges to be silently skipped and leave bridge gaps), then add one
384+
// new vertex per boundary vertex pushed outward by target_diag, and stitch
385+
// a skirt quad (two triangles) per boundary edge. The winding order
386+
// (source, target, target_new) produces normals consistent with the
387+
// adjacent interior face.
388+
TriangleMesh extended_mesh = clipper._mesh;
389+
PMP::stitch_borders(extended_mesh);
390+
391+
std::map<TriangleMesh::Vertex_index, TriangleMesh::Vertex_index> skirt_vertex;
392+
for (auto &[v, dir_sum] : outward_sum)
393+
{
394+
const double len = std::sqrt(dir_sum.squared_length());
395+
if (len < 1e-10) continue;
396+
const Vector dir = dir_sum / len;
397+
const Point &p = extended_mesh.point(v);
398+
skirt_vertex[v] = extended_mesh.add_vertex(
399+
Point(p.x() + target_diag * dir.x(),
400+
p.y() + target_diag * dir.y(),
401+
p.z() + target_diag * dir.z()));
402+
}
403+
404+
for (auto he : clipper._mesh.halfedges())
405+
{
406+
if (!clipper._mesh.is_border(he)) continue;
407+
auto vs = clipper._mesh.source(he);
408+
auto vt = clipper._mesh.target(he);
409+
if (!skirt_vertex.count(vs) || !skirt_vertex.count(vt)) continue;
410+
auto vs_new = skirt_vertex[vs];
411+
auto vt_new = skirt_vertex[vt];
412+
extended_mesh.add_face(vs, vt, vt_new);
413+
extended_mesh.add_face(vs, vt_new, vs_new);
327414
}
328415

329-
bool intersection = PMP::do_intersect(_mesh, clipper._mesh);
416+
if (LoopCGAL::verbose)
417+
std::cout << " cutWithSurface: added skirt of "
418+
<< skirt_vertex.size() << " new vertices over target_diag="
419+
<< target_diag << "\n";
420+
421+
TriMesh scaled_clipper(std::move(extended_mesh));
422+
423+
const int faces_before = static_cast<int>(_mesh.number_of_faces());
424+
425+
bool intersection = PMP::do_intersect(_mesh, scaled_clipper._mesh);
330426
if (intersection)
331427
{
332428
// Clip tm with clipper
@@ -337,21 +433,18 @@ void TriMesh::cutWithSurface(TriMesh &clipper,
337433

338434
try
339435
{
340-
// bool flag =
341-
// PMP::clip(_mesh, clipper._mesh, CGAL::parameters::clip_volume(false));
342436
bool flag = false;
343437
try
344438
{
345439
if (use_exact_kernel){
346-
Exact_Mesh exact_clipper = convert_to_exact(clipper);
440+
Exact_Mesh exact_clipper = convert_to_exact(scaled_clipper);
347441
Exact_Mesh exact_mesh = convert_to_exact(*this);
348442
flag = PMP::clip(exact_mesh, exact_clipper, CGAL::parameters::clip_volume(false));
349-
set_mesh(convert_to_double_mesh(exact_mesh));
443+
set_mesh(convert_to_double_mesh(exact_mesh));
350444
}
351445
else{
352-
flag = PMP::clip(_mesh, clipper._mesh, CGAL::parameters::clip_volume(false));
446+
flag = PMP::clip(_mesh, scaled_clipper._mesh, CGAL::parameters::clip_volume(false));
353447
}
354-
355448
}
356449
catch (const std::exception &e)
357450
{
@@ -384,6 +477,15 @@ void TriMesh::cutWithSurface(TriMesh &clipper,
384477
<< std::endl;
385478
}
386479
}
480+
481+
const int faces_after = static_cast<int>(_mesh.number_of_faces());
482+
if (faces_after >= faces_before && LoopCGAL::verbose)
483+
{
484+
std::cerr << "Warning: cutWithSurface removed no faces (before="
485+
<< faces_before << ", after=" << faces_after
486+
<< "). Clipper may not extend beyond the mesh." << std::endl;
487+
}
488+
return faces_before - faces_after;
387489
}
388490

389491
NumpyMesh TriMesh::save(double area_threshold,

src/mesh.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ class TriMesh
2828
TriMesh(const pybind11::array_t<double> &vertices,
2929
const pybind11::array_t<int> &triangles);
3030

31-
// Method to cut the mesh with another surface object
32-
void cutWithSurface(TriMesh &surface,
31+
// Method to cut the mesh with another surface object.
32+
// Returns the number of faces removed (0 = no-op / bad cut).
33+
int cutWithSurface(TriMesh &surface,
3334
bool preserve_intersection = false,
3435
bool preserve_intersection_clipper = false,
3536
bool use_exact_kernel = true);
@@ -47,6 +48,9 @@ class TriMesh
4748
const TriangleMesh& get_mesh() const { return _mesh; }
4849
void set_mesh(const TriangleMesh& mesh) { _mesh = mesh; }
4950
private:
51+
// Internal constructor used by cutWithSurface to wrap a scaled copy.
52+
explicit TriMesh(TriangleMesh m) : _mesh(std::move(m)) { init(); }
53+
5054
std::set<TriangleMesh::Edge_index> _fixedEdges;
5155
TriangleMesh _mesh; // The underlying CGAL surface mesh
5256
CGAL::Boolean_property_map<std::set<TriangleMesh::Edge_index>>

0 commit comments

Comments
 (0)