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
389491NumpyMesh TriMesh::save (double area_threshold,
0 commit comments