From a0632ec50827cf95a58c787d8c37fff3757a56d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 6 Mar 2026 11:08:03 +0100 Subject: [PATCH 01/35] add new implementation of Booleans operations on triangle soups in particular, it handles non-manifold situations --- .../triangle_soup_boolean_operations_3.h | 742 ++++++++++++++++++ .../PMP_Boolean_operations/CMakeLists.txt | 2 + .../data-nm_boolop/cube3_minus_prism.off | 48 ++ .../data-nm_boolop/cube4.off | 22 + .../data-nm_boolop/cube_0.off | 24 + .../data-nm_boolop/cube_1.off | 24 + .../data-nm_boolop/cube_2.off | 24 + .../data-nm_boolop/nested_cubes_1.off | 24 + .../data-nm_boolop/nested_cubes_2.off | 24 + .../data-nm_boolop/pair_of_nm_cubes.off | 42 + .../data-nm_boolop/tangent_cube1.off | 24 + .../data-nm_boolop/tangent_cube2.off | 24 + .../test_boolean_on_triangle_soup.cmd | 6 + .../test_boolean_on_triangle_soup.cpp | 75 ++ 14 files changed, 1105 insertions(+) create mode 100644 PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube3_minus_prism.off create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube4.off create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_0.off create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_1.off create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_2.off create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/nested_cubes_1.off create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/nested_cubes_2.off create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/pair_of_nm_cubes.off create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/tangent_cube1.off create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/tangent_cube2.off create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cmd create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cpp diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h new file mode 100644 index 000000000000..8a8f8109d1e0 --- /dev/null +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -0,0 +1,742 @@ +// Copyright (c) 2026 GeometryFactory (France). +// All rights reserved. +// +// This file is part of CGAL (www.cgal.org). +// +// $URL$ +// $Id$ +// SPDX-License-Identifier: LicenseRef-Commercial +// +// +// Author(s) : Sébastien Loriot + +#ifndef CGAL_POLYGON_MESH_PROCESSING_TRIANGLE_SOUP_BOOLEAN_OPERATIONS_3_H +#define CGAL_POLYGON_MESH_PROCESSING_TRIANGLE_SOUP_BOOLEAN_OPERATIONS_3_H + +#include +#include +#include + +#include + +#ifdef CGAL_BO3_TIMERS +#include +#endif + +namespace CGAL { +namespace boolops_3 { + +//TODO: update nm_edges for identical coplanar status (less test below then) +template +std::vector +connected_components(const Polygons& triangles, + const EdgeMap& edge_map, + std::vector& ccids, + NM_edges& nm_edges) +{ + std::vector one_triangle_per_cc; + + std::size_t nb_poly = triangles.size(); + ccids.resize(nb_poly); + std::size_t i=0; + + std::vector handled(nb_poly, false); + for (std::size_t tid=0; tid queue; + queue.push_back(tid); + while(!queue.empty()) + { + std::size_t q_tid = queue.back(); + queue.pop_back(); + if ( handled[q_tid]) continue; + handled[q_tid]=true; + ccids[q_tid]=i; + + + for (int k=0; k<3; ++k) // TODO: change that if you want more than triangles + { + auto p = CGAL::make_sorted_pair(triangles[q_tid][k], triangles[q_tid][(k+1)%3]); + auto it = edge_map[p.first].find(p.second); + CGAL_assertion(it!=edge_map[p.first].end()); + const auto& tids = it->second; + switch( tids.size() ) + { + case 1: + break; + case 2: + { + std::size_t n_tid = tids[0]!=q_tid ? tids[0] : tids[1]; + if (!handled[n_tid]) + queue.push_back(n_tid); + } + break; + default: + nm_edges.emplace_back(p.first, it); + //nm edge + break; + } + } + } + one_triangle_per_cc.push_back(tid); + ++i; + } + return one_triangle_per_cc; +} + +struct Selection_data +{ + std::vector ccids; + std::vector coplanar_patches; + std::vector coplanar_patches_for_union_and_intersection; + std::vector< std::vector > cc_is_inside_per_mesh; // TODO: use an optional + std::vector input_ids; +}; + +template +Selection_data +select(const std::vector& points, + const std::vector& triangles, + std::vector& input_ids, + const unsigned int nb_meshes) +{ + //TODO: use Concurrency_tag + namespace PMP = CGAL::Polygon_mesh_processing; + namespace pred = PMP::Corefinement; + Selection_data data_out; + + // fill edge map + typedef std::size_t PID; + typedef std::size_t TID; + std::vector< std::unordered_map > > edge_map(points.size()); + using Boundary_edge_location = std::pair >::const_iterator>; + + for (TID tid=0; tid& ccids = data_out.ccids; + std::vector< Boundary_edge_location > nm_edges; + std::vector one_triangle_per_cc = + boolops_3::connected_components(triangles, edge_map, ccids, nm_edges); + std::size_t nb_cc = one_triangle_per_cc.size(); + + // TODO: direct filling rather that sort + unique? + //~ std::sort(nm_edges.begin(), nm_edges.end(), + //~ [](const std::pair& p1, const std::pair& p2) + //~ { + //~ if (p1.first!=p2.first) return p1.first > > triangles_per_cc(nb_cc); + for (std::size_t tid=0; tid cc_points = points; + PMP::remove_isolated_points_in_polygon_soup(cc_points, triangles_per_cc[i]); + CGAL::IO::write_OFF("cc_"+std::to_string(i)+".off", cc_points, triangles_per_cc[i], CGAL::parameters::stream_precision(17)); + } +#endif + + + std::size_t nb_handled = 0; + std::vector cc_handled(nb_cc, false); + std::vector& coplanar_patches=data_out.coplanar_patches; + std::vector& coplanar_patches_for_union_and_intersection=data_out.coplanar_patches_for_union_and_intersection; + std::vector< std::vector >& cc_is_inside_per_mesh=data_out.cc_is_inside_per_mesh; + + coplanar_patches.assign(nb_cc, false); + coplanar_patches_for_union_and_intersection.assign(nb_cc, false); +#ifdef CGAL_BO_AUTOREF_DEBUG + std::vector< std::vector > cc_is_inside_per_mesh_init(nb_meshes, std::vector(nb_cc, false)); + int case_id=-1; +#endif + cc_is_inside_per_mesh.assign(nb_meshes, std::vector(nb_cc, false)); // TODO: use an optional + + for (auto [pid, it] : nm_edges) + { + if (nb_handled==nb_cc) break; + + bool all_handled=true; + for (TID tid : it->second) + { + if (!cc_handled[ccids[tid]]) + { + all_handled=false; + break; + } + } + + if (all_handled) continue; + + bool all_from_same_mesh = true; + + for (TID tid : it->second) + { + if (input_ids[tid]!=input_ids[it->second.front()]) + { + all_from_same_mesh=false; + break; + } + } + if (all_from_same_mesh) continue; +#ifdef CGAL_BO_AUTOREF_DEBUG + std::cout << "case #" << ++case_id << " looking at edge " << pid << " " << it->first << "\n"; + std::cout << " incident CCs:"; + for (TID tid : it->second) std::cout << " " << ccids[tid] << "("<< input_ids[tid] <<")"; + std::cout << "\n"; +#endif + for (TID tid : it->second) + { + if (!cc_handled[ccids[tid]]) ++nb_handled; + cc_handled[ccids[tid]] = true; + } + + std::vector> to_sort; + to_sort.reserve(it->second.size()); + + std::size_t i0=pid, i1=it->first; + for (TID tid : it->second) + { + for (int i=0; i<3; ++i) + { + if (triangles[tid][i]!=i0 && triangles[tid][i]!=i1) + to_sort.emplace_back(i, tid); + } + } + + // we arbitrarily take the last triangle + std::pair tref=to_sort.front(); + if (triangles[tref.second][(tref.first+1)%3]!=i0) + std::swap(i0,i1); + + const Exact_point& pref=points[triangles[tref.second][tref.first]]; + const Exact_point& p0=points[i0]; + const Exact_point& p1=points[i1]; + + //Considering the plane with normal vector [o_prime,o] and containing o. + //We define the counterclockwise order around o when looking from + //the side of the plane containing o_prime. + //We consider the portion of the plane defined by rotating a ray starting at o + //from the planar projection of p1 to the planar projection of p2 in + //counterclockwise order. + //The predicates indicates whether the planar projection of point q lies in this + //portion of the plane. + //Preconditions: + // o_prime,o,p1 are not collinear + // o_prime,o,p2 are not collinear + // o_prime,o,q are not collinear + // o_prime,o,p1,q are not coplanar or coplanar_orientation(o,o_prime,p1,q)==NEGATIVE + // o_prime,o,p2,q are not coplanar or coplanar_orientation(o,o_prime,p2,q)==NEGATIVE + // template + // bool sorted_around_edge( + // const typename Kernel::Point_3& o_prime, const typename Kernel::Point_3& o, + // const typename Kernel::Point_3& p1, const typename Kernel::Point_3& p2, + // const typename Kernel::Point_3& q) + auto less = [&tref, &pref, &points, &triangles, &p0, &p1](const std::pair& e1, + const std::pair& e2) + { + using EPECK = CGAL::Exact_predicates_exact_constructions_kernel; + // handling coplanar + //TODO: we will make several time the comparision with the same point, not sure if it is coslty. Maybe group them? + if (triangles[e1.second][e1.first]==triangles[e2.second][e2.first]) + return e1.second < e2.second; + if (triangles[e1.second][e1.first]==triangles[tref.second][tref.first]) + return true; + if (triangles[e2.second][e2.first]==triangles[tref.second][tref.first]) + return false; + + return pred::sorted_around_edge(p0, p1, pref, + points[triangles[e2.second][e2.first]], + points[triangles[e1.second][e1.first]]); + }; + +#ifdef CGAL_BO_AUTOREF_DEBUG + std::set third_point; + for (const std::pair& p : to_sort) + third_point.insert(triangles[p.second][p.first]); + + if (third_point.size()!=to_sort.size()+1) + std::cerr << " presence of coplanar triangles\n"; +#endif + + std::sort(std::next(to_sort.begin()), to_sort.end(), less); + boost::dynamic_bitset<> is_inside_mesh(nb_meshes, 0); //indicate the status while turning around the edge + + std::vector init(nb_meshes, false); + init[input_ids[tref.second]]=true; + is_inside_mesh[input_ids[tref.second]]=true; + for (std::size_t pos=1; pos& p = to_sort[pos]; + if (init[input_ids[p.second]]) continue; + init[input_ids[p.second]]=true; + CGAL_assertion(pos!=to_sort.size()-1); // not possible since there must be at least 2 triangles per mesh + + //check if collinear with ref + if (triangles[p.second][p.first]==triangles[tref.second][tref.first]) + { + // incompatible orientation -> flip the bit + if (triangles[p.second][(p.first+1)%3]==triangles[tref.second][(tref.first+1)%3]) + is_inside_mesh.set(input_ids[p.second]); + } + else + // incompatible orientation -> flip the bit + if (triangles[p.second][(p.first+1)%3]==triangles[tref.second][(tref.first+1)%3]) + is_inside_mesh.set(input_ids[p.second]); + } +#ifdef CGAL_BO_AUTOREF_DEBUG + auto print_t_info = [&](std::size_t tid) + { + std::cout << " " << tid << " from mesh " << input_ids[tid] << " part of CC " << ccids[tid] << " " + << points[triangles[tid][0]] << " " << points[triangles[tid][1]] << " " << points[triangles[tid][2]] << "\n"; + }; + + int nb=-1; + for (const std::pair& p : to_sort) + { + std::ofstream debug("case_"+ std::to_string(case_id)+"_"+std::to_string(++nb)+".polylines.txt"); + debug << 4 << " " << points[triangles[p.second][0]] << " "<< " " << points[triangles[p.second][1]] << " " << points[triangles[p.second][2]] << " " << points[triangles[p.second][0]] << "\n"; + debug.close(); + print_t_info(p.second); + } + std::cout << " is_inside_mesh = "; + for (unsigned int i=0; i& p = to_sort[pos]; + ++pos; + std::vector mesh_ids; + mesh_ids.push_back(input_ids[p.second]); + + while (pos1) + { + coplanar_patches[ccids[p.second]]=true; + std::sort(mesh_ids.begin(), mesh_ids.end()); + + //check orientation -> mark only if all the orientations are identical + bool same_orientation=true; + for (std::size_t k=start_pos+1;k > triangles_of_intersect_per_cc(nb_cc); + std::vector< std::pair > points_and_cc; + points_and_cc.reserve(nb_cc-nb_handled); + for (std::size_t cc=0; cc bb_per_cc(nb_cc); + for (std::size_t tid=0; tid pt.x() || t_bbox.xmax() < pt.x() || + t_bbox.ymin() > pt.y() || t_bbox.ymax() < pt.y() || + t_bbox.zmax() < pt.z() ) continue; + triangles_of_intersect_per_cc[cc].push_back(tid); + } + } + + //bbox of 2 CCs that are not nested --> cc are disjoint (because points are shared) + //TODO: this part works as is only for 2 meshes + for (std::size_t cc=0; cc& subtri=triangles_of_intersect_per_cc[cc]; + const Exact_point& pt = points[triangles[one_triangle_per_cc[cc]][0]]; + Exact_predicates_exact_constructions_kernel::Ray_3 ray(pt, Exact_predicates_exact_constructions_kernel::Vector_3(0.,0.,1.)); + using T3 = Exact_predicates_exact_constructions_kernel::Triangle_3; + std::vector> to_sort; + for (std::size_t tid : subtri) + { + T3 tr(points[triangles[tid][0]],points[triangles[tid][1]],points[triangles[tid][2]]); + if (do_intersect(ray, tr)) + to_sort.emplace_back(tid, tr); + } + + std::sort(to_sort.begin(), to_sort.end(), + [&pt](const std::pair& p1, const std::pair& p2) + { + return compare_distance(pt, p1.second, p2.second) == CGAL::SMALLER; + } + ); + + bool is_outside = true; + for (const std::pair& p : to_sort) + { + CGAL::Orientation ori = orientation(p.second[0], p.second[1], p.second[2], pt); + if (ori == CGAL::COPLANAR) continue; + is_outside = (ori == POSITIVE); + + break; + } + + cc_handled[cc]=true; + if (!is_outside) + // TODO: clearly working only for 2 meshes + cc_is_inside_per_mesh[ (input_ids[one_triangle_per_cc[cc]]+1)%2 ][cc]=true; + } + } + } + + + data_out.input_ids.swap(input_ids); + return data_out; +} + +struct Autoref_visitor +{ + Autoref_visitor(std::vector& input_ids, + std::size_t nbt1, std::size_t nbt2) + : input_ids(input_ids) + , nbt1(nbt1) + , nbt2(nbt2) + {} + + inline void number_of_output_triangles(std::size_t nbt) + { + input_ids.resize(nbt, -1); + } + inline void verbatim_triangle_copy(std::size_t tgt_id, std::size_t src_id) + { + input_ids[tgt_id] = src_id < nbt1? 0 : 1; + } + inline void new_subtriangle(std::size_t tgt_id, std::size_t src_id) + { + input_ids[tgt_id] = src_id < nbt1? 0 : 1; + } + inline void delete_triangle(std::size_t /*src_id*/) {} + + std::vector& input_ids; + std::size_t nbt1; + std::size_t nbt2; +}; + +template +Selection_data +prepare_data(const PointRange& points_1, const TriangleRange& triangles_1, + const PointRange& points_2, const TriangleRange& triangles_2, + PointRange& out_points, TriangleRange& out_triangles) +{ + namespace PMP = CGAL::Polygon_mesh_processing; + + out_points.reserve(points_1.size()+points_2.size()); + out_points.insert(out_points.end(), points_1.begin(), points_1.end()); + out_points.insert(out_points.end(), points_2.begin(), points_2.end()); + out_triangles.reserve(triangles_1.size()+triangles_2.size()); + out_triangles.insert(out_triangles.end(), triangles_1.begin(), triangles_1.end()); + std::size_t nbt_1=triangles_1.size(), nbt_2=triangles_2.size(); + + for (auto t : triangles_2) + { + for (int i=0;i<3;++i) + t[i]+=points_1.size(); + out_triangles.push_back(t); + } + +#ifdef CGAL_BO3_TIMERS + Real_timer timer; + time.start(); +#endif + std::vector input_ids(nbt_1+nbt_2, 0); + for (std::size_t i=nbt_1; i(out_points, out_triangles, input_ids, 2); +} + +template +void +update_output(PointRange& out_points, TriangleRange& out_triangles, std::size_t nbt, const std::vector& to_keep, const std::vector& to_flip=std::vector()) +{ + if (nbt==out_triangles.size() && to_flip.empty()) return; + + TriangleRange triangles; + triangles.reserve(nbt); + + PointRange points; + points.reserve(nbt<3?3:((nbt-2)/2+1)); + std::vector point_map(out_points.size(), out_points.size()); + + auto point_id = [&](std::size_t i) + { + if (point_map[i]==out_points.size()) + { + point_map[i]=points.size(); + points.push_back(out_points[i]); + } + return point_map[i]; + }; + + for (std::size_t ti=0; ti +void compute_union(const PointRange& points_1, const TriangleRange& triangles_1, + const PointRange& points_2, const TriangleRange& triangles_2, + PointRange& out_points, TriangleRange& out_triangles) +{ + using namespace boolops_3; + Selection_data data_out=prepare_data(points_1, triangles_1, points_2, triangles_2, out_points, out_triangles); + const unsigned int nb_meshes=2; + + std::vector to_keep(out_triangles.size(), false); + std::size_t nb_out_triangles=0; + + for (std::size_t ti=0; ti(out_points, out_triangles, nb_out_triangles, to_keep); +} + +template +void compute_intersection(const PointRange& points_1, const TriangleRange& triangles_1, + const PointRange& points_2, const TriangleRange& triangles_2, + PointRange& out_points, TriangleRange& out_triangles) +{ + using namespace boolops_3; + Selection_data data_out=prepare_data(points_1, triangles_1, points_2, triangles_2, out_points, out_triangles); + const unsigned int nb_meshes=2; + + std::vector to_keep(out_triangles.size(), false); + std::size_t nb_out_triangles=0; + + for (std::size_t ti=0; ti(out_points, out_triangles, nb_out_triangles, to_keep); +} + +template +void compute_difference(const PointRange& points_1, const TriangleRange& triangles_1, + const PointRange& points_2, const TriangleRange& triangles_2, + PointRange& out_points, TriangleRange& out_triangles) +{ + using namespace boolops_3; + Selection_data data_out=prepare_data(points_1, triangles_1, points_2, triangles_2, out_points, out_triangles); + // const unsigned int nb_meshes=2; + + std::vector to_keep(out_triangles.size(), false); + std::vector to_flip(out_triangles.size(), false); + std::size_t nb_out_triangles=0; + + for (std::size_t ti=0; ti(out_points, out_triangles, nb_out_triangles, to_keep, to_flip); +} + +} } // CGAL::Polygon_mesh_processing namespace + +#endif // CGAL_POLYGON_MESH_PROCESSING_TRIANGLE_SOUP_BOOLEAN_OPERATIONS_3_H diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt b/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt index c36ca7682eb5..aadae9045bed 100644 --- a/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt @@ -24,12 +24,14 @@ create_single_source_cgal_program("test_isolevel_refinement.cpp") create_single_source_cgal_program("test_pmp_clip.cpp") create_single_source_cgal_program("test_snap_rounding.cpp") create_single_source_cgal_program("test_mesh_kernel.cpp") +create_single_source_cgal_program("test_boolean_on_triangle_soup.cpp") find_package(TBB QUIET) include(CGAL_TBB_support) if(TARGET CGAL::TBB_support) target_link_libraries(test_autorefinement PRIVATE CGAL::TBB_support) target_link_libraries(test_snap_rounding PRIVATE CGAL::TBB_support) + target_link_libraries(test_boolean_on_triangle_soup PRIVATE CGAL::TBB_support) else() message(STATUS "NOTICE: Intel TBB was not found. Tests will use sequential code.") endif() diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube3_minus_prism.off b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube3_minus_prism.off new file mode 100644 index 000000000000..30228694bd49 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube3_minus_prism.off @@ -0,0 +1,48 @@ +OFF +16 28 0 + +-1 0 -1 +1 0 -1 +0 2 -1 +1 0 1 +0 2 1 +-1 0 1 +-2 2 -1 +2 2 -1 +-2 -2 -1 +-2 -2 1 +-2 2 1 +2 -2 1 +2 -2 -1 +2 2 1 +0 2 -1 +0 2 1 +3 9 3 5 +3 8 6 0 +3 8 9 10 +3 12 7 13 +3 8 12 11 +3 0 6 14 +3 2 15 13 +3 4 14 6 +3 11 3 9 +3 4 5 0 +3 2 1 3 +3 3 1 0 +3 12 0 1 +3 8 0 12 +3 12 1 7 +3 1 2 7 +3 0 5 3 +3 10 6 8 +3 5 4 10 +3 3 15 2 +3 13 11 12 +3 0 14 4 +3 11 9 8 +3 3 13 15 +3 11 13 3 +3 13 7 2 +3 6 10 4 +3 9 5 10 + diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube4.off b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube4.off new file mode 100644 index 000000000000..023b160982b3 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube4.off @@ -0,0 +1,22 @@ +OFF +8 12 0 +0 1 -1 +0 2 -1 +1 2 -1 +1 1 -1 +0 1 1 +0 2 1 +1 2 1 +1 1 1 +3 0 1 3 +3 3 1 2 +3 0 4 1 +3 1 4 5 +3 3 2 7 +3 7 2 6 +3 4 0 3 +3 7 4 3 +3 6 4 7 +3 6 5 4 +3 1 5 6 +3 2 1 6 diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_0.off b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_0.off new file mode 100644 index 000000000000..3d7bf7101998 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_0.off @@ -0,0 +1,24 @@ +OFF +8 12 0 + +-0.5 -0.5 0.5 +0.5 -0.5 0.5 +0.5 -0.5 -0.5 +-0.5 -0.5 -0.5 +-0.5 0.5 -0.5 +-0.5 0.5 0.5 +0.5 0.5 0.5 +0.5 0.5 -0.5 +3 4 5 6 +3 1 0 3 +3 6 1 2 +3 0 1 6 +3 3 0 5 +3 7 2 3 +3 6 7 4 +3 3 2 1 +3 2 7 6 +3 6 5 0 +3 5 4 3 +3 3 4 7 + diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_1.off b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_1.off new file mode 100644 index 000000000000..534270d2bc86 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_1.off @@ -0,0 +1,24 @@ +OFF +8 12 0 + +0.054291486740112305 0.26874995231628418 -0.34933948516845703 +1.0542914867401123 0.26874995231628418 -0.34933948516845703 +1.0542914867401123 0.26874995231628418 -1.349339485168457 +0.054291486740112305 0.26874995231628418 -1.349339485168457 +0.054291486740112305 1.2687499523162842 -1.349339485168457 +0.054291486740112305 1.2687499523162842 -0.34933948516845703 +1.0542914867401123 1.2687499523162842 -0.34933948516845703 +1.0542914867401123 1.2687499523162842 -1.349339485168457 +3 4 5 6 +3 1 0 3 +3 6 1 2 +3 0 1 6 +3 3 0 5 +3 7 2 3 +3 6 7 4 +3 3 2 1 +3 2 7 6 +3 6 5 0 +3 5 4 3 +3 3 4 7 + diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_2.off b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_2.off new file mode 100644 index 000000000000..bc4ce81514fc --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/cube_2.off @@ -0,0 +1,24 @@ +OFF +8 12 0 + +-0.109773188829422 -0.065066307783126831 1.1743011474609375 +0.89022684097290039 -0.065066307783126831 1.1743011474609375 +0.89022684097290039 -0.065066307783126831 0.1743011474609375 +-0.109773188829422 -0.065066307783126831 0.1743011474609375 +-0.109773188829422 0.93493366241455078 0.1743011474609375 +-0.109773188829422 0.93493366241455078 1.1743011474609375 +0.89022684097290039 0.93493366241455078 1.1743011474609375 +0.89022684097290039 0.93493366241455078 0.1743011474609375 +3 4 5 6 +3 1 0 3 +3 6 1 2 +3 0 1 6 +3 3 0 5 +3 7 2 3 +3 6 7 4 +3 3 2 1 +3 2 7 6 +3 6 5 0 +3 5 4 3 +3 3 4 7 + diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/nested_cubes_1.off b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/nested_cubes_1.off new file mode 100644 index 000000000000..824fad12f3b2 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/nested_cubes_1.off @@ -0,0 +1,24 @@ +OFF +8 12 0 + +-2 -2 2 +2 -2 2 +2 -2 -2 +-2 -2 -2 +-2 2 -2 +-2 2 2 +2 2 2 +2 2 -2 +3 4 5 6 +3 1 0 3 +3 6 1 2 +3 0 1 6 +3 3 0 5 +3 7 2 3 +3 6 7 4 +3 3 2 1 +3 2 7 6 +3 6 5 0 +3 5 4 3 +3 3 4 7 + diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/nested_cubes_2.off b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/nested_cubes_2.off new file mode 100644 index 000000000000..cb0bd77ae734 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/nested_cubes_2.off @@ -0,0 +1,24 @@ +OFF +8 12 0 + +-1 -1 1 +1 -1 1 +1 -1 -1 +-1 -1 -1 +-1 1 -1 +-1 1 1 +1 1 1 +1 1 -1 +3 4 5 6 +3 1 0 3 +3 6 1 2 +3 0 1 6 +3 3 0 5 +3 7 2 3 +3 6 7 4 +3 3 2 1 +3 2 7 6 +3 6 5 0 +3 5 4 3 +3 3 4 7 + diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/pair_of_nm_cubes.off b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/pair_of_nm_cubes.off new file mode 100644 index 000000000000..6bf14d1d0ed4 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/pair_of_nm_cubes.off @@ -0,0 +1,42 @@ +OFF +14 24 0 + +-0.5 -0.5 0.5 +0.5 -0.5 0.5 +0.5 -0.5 -0.5 +-0.5 -0.5 -0.5 +-0.5 0.5 -0.5 +-0.5 0.5 0.5 +0.5 0.5 0.5 +0.5 0.5 -0.5 +0.5 1.5 0.5 +1.5 1.5 0.5 +1.5 1.5 -0.5 +0.5 1.5 -0.5 +1.5 0.5 -0.5 +1.5 0.5 0.5 +3 4 5 6 +3 1 0 3 +3 6 1 2 +3 0 1 6 +3 3 0 5 +3 7 2 3 +3 9 10 11 +3 13 6 7 +3 9 13 12 +3 9 8 6 +3 8 11 7 +3 10 12 7 +3 6 7 4 +3 3 2 1 +3 2 7 6 +3 6 5 0 +3 5 4 3 +3 3 4 7 +3 11 8 9 +3 7 12 13 +3 12 10 9 +3 6 13 9 +3 7 6 8 +3 7 11 10 + diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/tangent_cube1.off b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/tangent_cube1.off new file mode 100644 index 000000000000..3d7bf7101998 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/tangent_cube1.off @@ -0,0 +1,24 @@ +OFF +8 12 0 + +-0.5 -0.5 0.5 +0.5 -0.5 0.5 +0.5 -0.5 -0.5 +-0.5 -0.5 -0.5 +-0.5 0.5 -0.5 +-0.5 0.5 0.5 +0.5 0.5 0.5 +0.5 0.5 -0.5 +3 4 5 6 +3 1 0 3 +3 6 1 2 +3 0 1 6 +3 3 0 5 +3 7 2 3 +3 6 7 4 +3 3 2 1 +3 2 7 6 +3 6 5 0 +3 5 4 3 +3 3 4 7 + diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/tangent_cube2.off b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/tangent_cube2.off new file mode 100644 index 000000000000..5870a6f385f8 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/data-nm_boolop/tangent_cube2.off @@ -0,0 +1,24 @@ +OFF +8 12 0 + +-0.25 -0.25 1.5 +0.25 -0.25 1.5 +0.25 -0.25 0.5 +-0.25 -0.25 0.5 +-0.25 0.25 0.5 +-0.25 0.25 1.5 +0.25 0.25 1.5 +0.25 0.25 0.5 +3 4 5 6 +3 1 0 3 +3 6 1 2 +3 0 1 6 +3 3 0 5 +3 7 2 3 +3 6 7 4 +3 3 2 1 +3 2 7 6 +3 6 5 0 +3 5 4 3 +3 3 4 7 + diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cmd b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cmd new file mode 100644 index 000000000000..ee48564603cd --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cmd @@ -0,0 +1,6 @@ +data-nm_boolop/cube_0.off data-nm_boolop/cube_1.off +data-nm_boolop/cube3_minus_prism.off data-nm_boolop/cube4.off +data-nm_boolop/pair_of_nm_cubes.off data-nm_boolop/cube_2.off +data-nm_boolop/nested_cube1.off data-nm_boolop/nested_cube2.off +data-nm_boolop/nested_cube2.off data-nm_boolop/nested_cube1.off +data-nm_boolop/tangent_cube1.off data-nm_boolop/tangent_cube2.off diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cpp b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cpp new file mode 100644 index 000000000000..28a9824a608b --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include + +#include + +#include + +#include + +#ifdef CGAL_LINKED_WITH_TBB +using Concurrency_tag = CGAL::Parallel_tag; +#else +using Concurrency_tag = CGAL::Sequential_tag; +#endif + +typedef CGAL::Exact_predicates_exact_constructions_kernel EPECK; +typedef EPECK::Point_3 Exact_point; +typedef CGAL::Surface_mesh Exact_mesh; +namespace PMP = CGAL::Polygon_mesh_processing; +namespace params = CGAL::parameters; + + +void run(std::string f1, std::string f2) +{ + std::cout << "Running with " << f1 << " and " << f2 << "\n"; + using Triangle = boost::container::small_vector; + + std::vector points_1, points_2; + std::vector triangles_1, triangles_2; + + CGAL::IO::read_polygon_soup(f1, points_1, triangles_1); + CGAL::IO::read_polygon_soup(f2, points_2, triangles_2); + +{ + std::vector points_res; + std::vector triangles_res; + CGAL::Real_timer timer; + timer.start(); + PMP::compute_union(points_1, triangles_1, points_2, triangles_2, points_res, triangles_res); + timer.stop(); + std::cout << "Done in " << timer.time() << "\n"; + + CGAL::IO::write_OFF("union.off", points_res, triangles_res, params::stream_precision(17)); +} +{ + std::vector points_res; + std::vector triangles_res; + CGAL::Real_timer timer; + timer.start(); + PMP::compute_intersection(points_1, triangles_1, points_2, triangles_2, points_res, triangles_res); + timer.stop(); + std::cout << "Done in " << timer.time() << "\n"; + + CGAL::IO::write_OFF("intersection.off", points_res, triangles_res, params::stream_precision(17)); +} +{ + std::vector points_res; + std::vector triangles_res; + CGAL::Real_timer timer; + timer.start(); + PMP::compute_difference(points_1, triangles_1, points_2, triangles_2, points_res, triangles_res); + timer.stop(); + std::cout << "Done in " << timer.time() << "\n"; + + CGAL::IO::write_OFF("difference.off", points_res, triangles_res, params::stream_precision(17)); +} +} + +int main(int argc, char** argv) +{ + for (int i=0; i< (argc-1)/2; ++i) + run(argv[2*i+1], argv[2*i+2]); +} From b0d8650a5391c717c47b53fd3a73165386190380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Wed, 8 Apr 2026 11:41:39 +0200 Subject: [PATCH 02/35] add an option to test for intersection only triangles from different ranges note this will prevent self-intersections to be resolved! --- .../Polygon_mesh_processing/autorefinement.h | 88 ++++++++++++++++++- .../triangle_soup_boolean_operations_3.h | 10 +-- .../internal/parameters_interface.h | 1 + 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h index 5de664695d45..56f06a0cccae 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h @@ -976,6 +976,74 @@ void generate_subtriangles(std::size_t ti, #endif namespace autorefine_impl{ + +// +template +void compute_intersecting_pairs(const PointRange& points, const PolygonRange& triangles, + std::size_t split_id, std::vector& si_pairs) +{ + typedef CGAL::Box_intersection_d::ID_FROM_BOX_ADDRESS Box_policy; + typedef CGAL::Box_intersection_d::Box_with_info_d Box; + + std::vector boxes_1, boxes_2; + std::vector boxes_1_ptr, boxes_2_ptr; + + boxes_1.reserve(split_id); + boxes_1_ptr.reserve(boxes_1.size()); + for (std::size_t i=0;i; + using TBB_vector = tbb::concurrent_vector; + std::conditional_t > collecter; +#else + std::vector& collecter=si_pairs; +#endif + auto callback = [&points, &triangles, &collecter] + (const Box* b1, const Box* b2) + { + std::size_t i1=b1->info(), i2=b2->info(); + + if (do_intersect(typename Kernel::Triangle_3(points[triangles[i1][0]],points[triangles[i1][1]],points[triangles[i1][2]]), + typename Kernel::Triangle_3(points[triangles[i2][0]],points[triangles[i2][1]],points[triangles[i2][2]]))) + { + collecter.emplace_back(i1,i2); + } + }; + + //using pointers in box_intersection_d is about 10% faster + CGAL::box_intersection_d(boxes_1_ptr.begin(), boxes_1_ptr.end(), + boxes_2_ptr.begin(), boxes_2_ptr.end(), + callback, cutoff ); + +#ifdef CGAL_LINKED_WITH_TBB + if constexpr (need_copy) + { + si_pairs.reserve(collecter.size()); + std::copy(collecter.begin(), collecter.end(), std::back_inserter(si_pairs)); + } + else + si_pairs.swap(collecter); +#endif +} + + // Forward declaration struct Wrap_snap_visitor; @@ -991,6 +1059,7 @@ bool autorefine_triangle_soup(PointRange& soup_points, { using parameters::choose_parameter; using parameters::get_parameter; + using parameters::is_default_parameter; typedef typename GetPolygonSoupGeomTraits::type GT; typedef typename GetPointMap::const_type Point_map; @@ -1026,7 +1095,24 @@ bool autorefine_triangle_soup(PointRange& soup_points, // collect intersecting pairs of triangles CGAL_PMP_AUTOREFINE_VERBOSE("collect intersecting pairs"); - triangle_soup_self_intersections(soup_points, soup_triangles, std::back_inserter(si_pairs), np); + +#if defined(CGAL_AUTOREFINE_DEBUG_COUNTERS) || defined(CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS) + CGAL::Real_timer timer; + timer.start(); +#endif + + if constexpr(!parameters::is_default_parameter::value) + { + std::size_t split_id=get_parameter(np, internal_np::split_triangle_range_at); + compute_intersecting_pairs(soup_points, soup_triangles, split_id, si_pairs); + } + else + triangle_soup_self_intersections(soup_points, soup_triangles, std::back_inserter(si_pairs), np); + +#if defined(CGAL_AUTOREFINE_DEBUG_COUNTERS) || defined(CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS) + timer.stop(); + std::cout << "collecting intersecting pairs took " << timer.time() << "\n"; +#endif if (si_pairs.empty()) { diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 8a8f8109d1e0..56f69f4a5d74 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -537,7 +537,7 @@ prepare_data(const PointRange& points_1, const TriangleRange& triangles_1, #ifdef CGAL_BO3_TIMERS Real_timer timer; - time.start(); + timer.start(); #endif std::vector input_ids(nbt_1+nbt_2, 0); for (std::size_t i=nbt_1; i Date: Thu, 16 Apr 2026 18:30:50 +0200 Subject: [PATCH 03/35] add draft code for self-union --- .../PMP_Boolean_operations/CMakeLists.txt | 2 + .../triangle_soup_boolean_operations_3.h | 239 ++++++++++++++++++ 2 files changed, 241 insertions(+) diff --git a/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt b/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt index be20fd221279..720b306f3a66 100644 --- a/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt +++ b/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt @@ -23,6 +23,7 @@ create_single_source_cgal_program("triangle_mesh_autorefinement.cpp") create_single_source_cgal_program("snap_polygon_soup.cpp") create_single_source_cgal_program("mesh_slicer_example.cpp") create_single_source_cgal_program("mesh_kernel_example.cpp") +create_single_source_cgal_program("self_union.cpp") find_package(Eigen3 QUIET) include(CGAL_Eigen3_support) @@ -49,6 +50,7 @@ include(CGAL_TBB_support) if(TARGET CGAL::TBB_support) target_link_libraries(soup_autorefinement PRIVATE CGAL::TBB_support) target_link_libraries(snap_polygon_soup PRIVATE CGAL::TBB_support) + target_link_libraries(self_union PRIVATE CGAL::TBB_support) create_single_source_cgal_program("corefinement_parallel_union_meshes.cpp") target_link_libraries(corefinement_parallel_union_meshes PRIVATE CGAL::TBB_support) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 56f69f4a5d74..63ceb9cd19f3 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -16,6 +16,7 @@ #include #include #include +#include #include @@ -609,6 +610,244 @@ update_output(PointRange& out_points, TriangleRange& out_triangles, std::size_t namespace Polygon_mesh_processing { +template +void compute_self_union(PointRange& points, TriangleRange& triangles) +{ + namespace PMP = CGAL::Polygon_mesh_processing; + using namespace boolops_3; + +#ifdef CGAL_BO3_TIMERS + Real_timer timer; + timer.start(); +#endif + // TODO: convert points to EPECK if not already the case + using Exact_point = typename std::iterator_traits::value_type; + PMP::autorefine_triangle_soup(points, triangles, CGAL::parameters::concurrency_tag(Concurrency_tag())); +#ifdef CGAL_BO3_TIMERS + timer.stop(); + std::cout << "autorefine done in " << timer.time() << "\n"; + timer.reset(); +#endif +#ifdef CGAL_BO_AUTOREF_DEBUG + CGAL::IO::write_OFF("autorefined.off", out_points, out_triangles, CGAL::parameters::stream_precision(17)); +#endif + + //TODO: use Concurrency_tagf + namespace PMP = CGAL::Polygon_mesh_processing; + namespace pred = PMP::Corefinement; + //~ Selection_data data_out; + + // fill edge map (TODO: move to a function) + typedef std::size_t PID; + typedef std::size_t TID; + std::vector< std::unordered_map > > edge_map(points.size()); + using Boundary_edge_location = std::pair >::const_iterator>; + + for (TID tid=0; tid ccids; + std::vector< Boundary_edge_location > nm_edges; + std::vector one_triangle_per_cc = + boolops_3::connected_components(triangles, edge_map, ccids, nm_edges); + std::size_t nb_cc = one_triangle_per_cc.size(); + + + using UF_CC = CGAL::Union_find; + using UF_handle = UF_CC::handle; + + UF_CC uf_cc; + std::vector uf_handles(nb_cc); + for (std::size_t i=0; i edges + // TODO we need split graph into polylines I guess + std::vector > cc_to_edges(nb_cc); + std::vector pts_on_edge(points.size(), false); // TODO: fill it in autoref visitor? + + for (auto [pid, it] : nm_edges) + { + std::set ccs; + pts_on_edge[pid]=true; + pts_on_edge[it->first]=true; + for (TID tid : it->second) + ccs.insert(ccids[tid]); + for (std::size_t ccid : ccs) + cc_to_edges[ccid].emplace_back(pid, it); + } + + std::size_t min_pt_id=0; + for (std::size_t i=1; ipoints[i].z()) min_pt_id=i; + } + else + if (points[min_pt_id].y()>points[i].y()) min_pt_id=i; + } + else + if (points[min_pt_id].x()>points[i].x()) min_pt_id=i; + } + + //TODO: temporary condition assuming extreme point is manifold, need another selection method otherwise + CGAL_precondition(!pts_on_edge[min_pt_id]); + + std::size_t selected_tid=triangles.size(); + for (TID tid=0; tid ccs_handled(nb_cc, false); + std::vector cc_stack; + cc_stack.push_back(ccids[selected_tid]); + + // main loop to classify cc + while(!cc_stack.empty()) + { + std::size_t ccid = cc_stack.back(); + cc_stack.pop_back(); + + if (ccs_handled[ccid]) continue; + ccs_handled[ccid]=true; + + // iterate on all non-manifold edges of the current cc + for (auto [pid, it] : cc_to_edges[ccid]) + { +#ifdef CGAL_BO_AUTOREF_DEBUG + std::cout << "case #" << ++case_id << " looking at edge " << pid << " " << it->first << "\n"; + std::cout << " incident CCs:"; + for (TID tid : it->second) std::cout << " " << ccids[tid] << "("<< input_ids[tid] <<")"; + std::cout << "\n"; +#endif + + std::vector> to_sort; + to_sort.reserve(it->second.size()); + + std::size_t i0=pid, i1=it->first; + for (TID tid : it->second) + { + for (int i=0; i<3; ++i) + { + if (triangles[tid][i]!=i0 && triangles[tid][i]!=i1) + to_sort.emplace_back(i, tid); + } + if (ccids[tid]==ccid && to_sort.size()>1) + std::swap(to_sort.front(), to_sort.back()); + } + + std::pair tref=to_sort.front(); // tid is from ccid + if (triangles[tref.second][(tref.first+1)%3]!=i0) + std::swap(i0,i1); + + const Exact_point& pref=points[triangles[tref.second][tref.first]]; + const Exact_point& p0=points[i0]; + const Exact_point& p1=points[i1]; + + //Considering the plane with normal vector [o_prime,o] and containing o. + //We define the counterclockwise order around o when looking from + //the side of the plane containing o_prime. + //We consider the portion of the plane defined by rotating a ray starting at o + //from the planar projection of p1 to the planar projection of p2 in + //counterclockwise order. + //The predicates indicates whether the planar projection of point q lies in this + //portion of the plane. + //Preconditions: + // o_prime,o,p1 are not collinear + // o_prime,o,p2 are not collinear + // o_prime,o,q are not collinear + // o_prime,o,p1,q are not coplanar or coplanar_orientation(o,o_prime,p1,q)==NEGATIVE + // o_prime,o,p2,q are not coplanar or coplanar_orientation(o,o_prime,p2,q)==NEGATIVE + // template + // bool sorted_around_edge( + // const typename Kernel::Point_3& o_prime, const typename Kernel::Point_3& o, + // const typename Kernel::Point_3& p1, const typename Kernel::Point_3& p2, + // const typename Kernel::Point_3& q) + auto less = [&tref, &pref, &points, &triangles, &p0, &p1](const std::pair& e1, + const std::pair& e2) + { + using EPECK = CGAL::Exact_predicates_exact_constructions_kernel; + // handling coplanar + //TODO: we will make several time the comparision with the same point, not sure if it is coslty. Maybe group them? + if (triangles[e1.second][e1.first]==triangles[e2.second][e2.first]) + return e1.second < e2.second; + if (triangles[e1.second][e1.first]==triangles[tref.second][tref.first]) + return true; + if (triangles[e2.second][e2.first]==triangles[tref.second][tref.first]) + return false; + + return pred::sorted_around_edge(p0, p1, pref, + points[triangles[e2.second][e2.first]], + points[triangles[e1.second][e1.first]]); + }; + + #ifdef CGAL_BO_AUTOREF_DEBUG + std::set third_point; + for (const std::pair& p : to_sort) + third_point.insert(triangles[p.second][p.first]); + + if (third_point.size()!=to_sort.size()+1) + std::cerr << " presence of coplanar triangles\n"; + #endif + + std::sort(std::next(to_sort.begin()), to_sort.end(), less); + + // TODO: we need to make sure orentations are compatible + + std::size_t n_ccid = ccids[to_sort[1].second]; + uf_handles[n_ccid] = uf_cc.find(uf_handles[n_ccid]); + uf_handles[ccid] = uf_cc.find(uf_handles[ccid]); + uf_cc.unify_sets(uf_handles[n_ccid], uf_handles[ccid]); + if (!ccs_handled[n_ccid]) + cc_stack.push_back(n_ccid); + } + } + + // mark ccids to keep + std::size_t ref_cc = ccids[selected_tid]; + UF_handle ref_handle = uf_cc.find(uf_handles[ref_cc]); + std::vector selected_ccs(nb_cc); + for (std::size_t ccid=0; ccid to_keep(triangles.size(), false); + std::size_t nb_out_triangles=0; + for (std::size_t tid=0; tid(points, triangles, nb_out_triangles, to_keep); +} + template void compute_union(const PointRange& points_1, const TriangleRange& triangles_1, const PointRange& points_2, const TriangleRange& triangles_2, From 626c7469e328520b80e3204c29f9838437464e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 11 Aug 2026 11:19:57 +0200 Subject: [PATCH 04/35] add reference manual --- .../PackageDescription.txt | 13 ++- .../triangle_soup_boolean_operations_3.h | 102 ++++++++++++++++-- .../test_boolean_on_triangle_soup.cpp | 8 +- 3 files changed, 110 insertions(+), 13 deletions(-) diff --git a/PMP_Boolean_operations/doc/PMP_Boolean_operations/PackageDescription.txt b/PMP_Boolean_operations/doc/PMP_Boolean_operations/PackageDescription.txt index 0e9d4cd3af18..2f2252fce1a5 100644 --- a/PMP_Boolean_operations/doc/PMP_Boolean_operations/PackageDescription.txt +++ b/PMP_Boolean_operations/doc/PMP_Boolean_operations/PackageDescription.txt @@ -6,8 +6,12 @@ /// Functions to corefine triangulated surface meshes and triangle soups. /// \ingroup PkgPMPBooleanOperationsRef -/// \defgroup PMP_boolop_grp Boolean Operations -/// Functions to compute triangulated the union, difference, and intersection of meshes. +/// \defgroup PMP_boolop_grp Boolean Operations on Triangle Meshes +/// Functions to compute the union, difference, and intersection of triangle meshes. +/// \ingroup PkgPMPBooleanOperationsRef + +/// \defgroup PMP_boolop_soup_grp Boolean Operations on Triangle Soups +/// Functions to compute triangulated the union, difference, and intersection of triangle soups (possibly non-manifold). /// \ingroup PkgPMPBooleanOperationsRef /// \defgroup PMP_clip_grp Clipping and Splitting @@ -66,6 +70,11 @@ The page \ref bgl_namedparameters "Named Parameters" describes their usage. - `CGAL::Polygon_mesh_processing::surface_intersection()` (deprecated) - `CGAL::Polygon_mesh_processing::intersection_polylines()` +\cgalCRPSection{General Boolean Operations} +- `CGAL::Polygon_mesh_processing::compute_union()` +- `CGAL::Polygon_mesh_processing::compute_difference()` +- `CGAL::Polygon_mesh_processing::compute_intersection()` + \cgalCRPSection{Clipping and Splitting} - `CGAL::Polygon_mesh_processing::clip()` - `CGAL::Polygon_mesh_processing::split()` diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 63ceb9cd19f3..3c8e8550bc8f 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -13,6 +13,8 @@ #ifndef CGAL_POLYGON_MESH_PROCESSING_TRIANGLE_SOUP_BOOLEAN_OPERATIONS_3_H #define CGAL_POLYGON_MESH_PROCESSING_TRIANGLE_SOUP_BOOLEAN_OPERATIONS_3_H +#include + #include #include #include @@ -610,9 +612,19 @@ update_output(PointRange& out_points, TriangleRange& out_triangles, std::size_t namespace Polygon_mesh_processing { -template -void compute_self_union(PointRange& points, TriangleRange& triangles) +#ifndef DOXYGEN_RUNNING +template +void compute_self_union(PointRange& points, TriangleRange& triangles, + const NamedParameters& np = parameters::default_values()) { + CGAL_USE(np); + + typedef typename internal_np::Lookup_named_param_def < + internal_np::concurrency_tag_t, + NamedParameters, + Sequential_tag + > ::type Concurrency_tag; + namespace PMP = CGAL::Polygon_mesh_processing; using namespace boolops_3; @@ -847,12 +859,58 @@ void compute_self_union(PointRange& points, TriangleRange& triangles) } boolops_3::update_output(points, triangles, nb_out_triangles, to_keep); } +#endif -template +/** + * \ingroup PMP_boolop_soup_grp + * + * computes the union of two volumes, each bounded by a triangle soup. + * + * \pre \link CGAL::Polygon_mesh_processing::does_triangle_soup_self_intersect() `!CGAL::Polygon_mesh_processing::does_triangle_soup_self_intersect(points_1, triangles_1)` \endlink + * \pre \link CGAL::Polygon_mesh_processing::does_triangle_soup_self_intersect() `!CGAL::Polygon_mesh_processing::does_triangle_soup_self_intersect(points_2, triangles_2)` \endlink + * + * @tparam PointRange a model of the concepts `RandomAccessContainer` whose value type is `GeomTraits::Point_3` as value type, `GeomTraits` being the type of the named parameter `geom_traits`. + * @tparam TriangleRange a model of the concept `RandomAccessContainer` whose `value_type` is a model of the concept `RandomAccessContainer` whose `value_type` is `std::size_t`. + * + * @param points_1 points of the first triangle soup. + * @param triangles_1 triangles of the first triangle soup, each element in the vector describes a triangle using the indices of the points in `points_1`. + * + * @param points_2 points of the second triangle soup. + * @param triangles_2 triangles of the second triangle soup, each element in the vector describes a triangle using the indices of the points in `points_2`. + * + * @param out_points points of the output triangle soup bounding the volume result of the Boolean operation. + * @param out_triangles triangles of the ouput triangle soup, each element in the vector describes a triangle using the indices of the points in `out_points`. + * + * @param np optional sequence of named parameters among the ones listed below + * + * \cgalNamedParamsBegin + * \cgalParamNBegin{concurrency_tag} + * \cgalParamDescription{a tag indicating if the task should be done using one or several threads.} + * \cgalParamType{Either `CGAL::Sequential_tag`, or `CGAL::Parallel_tag`, or `CGAL::Parallel_if_available_tag`} + * \cgalParamDefault{`CGAL::Sequential_tag`} + * \cgalParamNEnd + * \cgalParamNBegin{geom_traits} + * \cgalParamDescription{an instance of a geometric traits class} + * \cgalParamType{a class model of `Kernel`} + * \cgalParamDefault{a \cgal Kernel deduced from the point type, using `CGAL::Kernel_traits`} + * \cgalParamExtra{The geometric traits class must be compatible with the vertex point type.} + * \cgalParamNEnd + * \cgalNamedParamsEnd + */ +template void compute_union(const PointRange& points_1, const TriangleRange& triangles_1, const PointRange& points_2, const TriangleRange& triangles_2, - PointRange& out_points, TriangleRange& out_triangles) + PointRange& out_points, TriangleRange& out_triangles, + const NamedParameters& np = parameters::default_values()) { + CGAL_USE(np); + + typedef typename internal_np::Lookup_named_param_def < + internal_np::concurrency_tag_t, + NamedParameters, + Sequential_tag + > ::type Concurrency_tag; + using namespace boolops_3; Selection_data data_out=prepare_data(points_1, triangles_1, points_2, triangles_2, out_points, out_triangles); const unsigned int nb_meshes=2; @@ -891,11 +949,25 @@ void compute_union(const PointRange& points_1, const TriangleRange& triangles_1, boolops_3::update_output(out_points, out_triangles, nb_out_triangles, to_keep); } -template +/** + * \ingroup PMP_boolop_soup_grp + * computes the intersection of two volumes, each bounded by a triangle soup. + * \copydetails CGAL::Polygon_mesh_processing::compute_union() + */ +template void compute_intersection(const PointRange& points_1, const TriangleRange& triangles_1, const PointRange& points_2, const TriangleRange& triangles_2, - PointRange& out_points, TriangleRange& out_triangles) + PointRange& out_points, TriangleRange& out_triangles, + const NamedParameters& np = parameters::default_values()) { + CGAL_USE(np); + + typedef typename internal_np::Lookup_named_param_def < + internal_np::concurrency_tag_t, + NamedParameters, + Sequential_tag + > ::type Concurrency_tag; + using namespace boolops_3; Selection_data data_out=prepare_data(points_1, triangles_1, points_2, triangles_2, out_points, out_triangles); const unsigned int nb_meshes=2; @@ -935,11 +1007,25 @@ void compute_intersection(const PointRange& points_1, const TriangleRange& trian boolops_3::update_output(out_points, out_triangles, nb_out_triangles, to_keep); } -template +/** + * \ingroup PMP_boolop_soup_grp + * computes the difference of two volumes, each bounded by a triangle soup. + * \copydetails CGAL::Polygon_mesh_processing::compute_union() + */ +template void compute_difference(const PointRange& points_1, const TriangleRange& triangles_1, const PointRange& points_2, const TriangleRange& triangles_2, - PointRange& out_points, TriangleRange& out_triangles) + PointRange& out_points, TriangleRange& out_triangles, + const NamedParameters& np = parameters::default_values()) { + CGAL_USE(np); + + typedef typename internal_np::Lookup_named_param_def < + internal_np::concurrency_tag_t, + NamedParameters, + Sequential_tag + > ::type Concurrency_tag; + using namespace boolops_3; Selection_data data_out=prepare_data(points_1, triangles_1, points_2, triangles_2, out_points, out_triangles); // const unsigned int nb_meshes=2; diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cpp b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cpp index 28a9824a608b..32dc568a3ef8 100644 --- a/PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cpp +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_boolean_on_triangle_soup.cpp @@ -33,12 +33,14 @@ void run(std::string f1, std::string f2) CGAL::IO::read_polygon_soup(f1, points_1, triangles_1); CGAL::IO::read_polygon_soup(f2, points_2, triangles_2); + auto bo_np = CGAL::parameters::concurrency_tag(Concurrency_tag()); + { std::vector points_res; std::vector triangles_res; CGAL::Real_timer timer; timer.start(); - PMP::compute_union(points_1, triangles_1, points_2, triangles_2, points_res, triangles_res); + PMP::compute_union(points_1, triangles_1, points_2, triangles_2, points_res, triangles_res, bo_np); timer.stop(); std::cout << "Done in " << timer.time() << "\n"; @@ -49,7 +51,7 @@ void run(std::string f1, std::string f2) std::vector triangles_res; CGAL::Real_timer timer; timer.start(); - PMP::compute_intersection(points_1, triangles_1, points_2, triangles_2, points_res, triangles_res); + PMP::compute_intersection(points_1, triangles_1, points_2, triangles_2, points_res, triangles_res, bo_np); timer.stop(); std::cout << "Done in " << timer.time() << "\n"; @@ -60,7 +62,7 @@ void run(std::string f1, std::string f2) std::vector triangles_res; CGAL::Real_timer timer; timer.start(); - PMP::compute_difference(points_1, triangles_1, points_2, triangles_2, points_res, triangles_res); + PMP::compute_difference(points_1, triangles_1, points_2, triangles_2, points_res, triangles_res, bo_np); timer.stop(); std::cout << "Done in " << timer.time() << "\n"; From 5669fe3da334086b9375f04255aa4cfb4fd758ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 11 Aug 2026 11:26:40 +0200 Subject: [PATCH 05/35] add the self-union example as a test for now --- .../PMP_Boolean_operations/CMakeLists.txt | 2 + .../PMP_Boolean_operations/self_union.cpp | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt b/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt index 1728e914ed38..3035f32ee0fa 100644 --- a/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt @@ -8,6 +8,7 @@ project(PMP_Boolean_operations_Tests) find_package(CGAL REQUIRED) create_single_source_cgal_program("autorefinement_sm.cpp") +create_single_source_cgal_program("self_union.cpp") create_single_source_cgal_program("corefine_non_manifold.cpp" ) create_single_source_cgal_program("polygon_mesh_slicer_test.cpp") create_single_source_cgal_program("surface_intersection_sm_poly.cpp") @@ -30,6 +31,7 @@ find_package(TBB QUIET) include(CGAL_TBB_support) if(TARGET CGAL::TBB_support) target_link_libraries(test_autorefinement PRIVATE CGAL::TBB_support) + target_link_libraries(self_union PRIVATE CGAL::TBB_support) target_link_libraries(test_snap_rounding PRIVATE CGAL::TBB_support) target_link_libraries(test_boolean_on_triangle_soup PRIVATE CGAL::TBB_support) else() diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp b/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp new file mode 100644 index 000000000000..c81c77ae21a5 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include + +#include + +#include + +#include + +#ifdef CGAL_LINKED_WITH_TBB +using Concurrency_tag = CGAL::Parallel_tag; +#else +using Concurrency_tag = CGAL::Sequential_tag; +#endif + +typedef CGAL::Exact_predicates_exact_constructions_kernel EPECK; +typedef EPECK::Point_3 Exact_point; +typedef CGAL::Surface_mesh Exact_mesh; +using Triangle = boost::container::small_vector; +namespace PMP = CGAL::Polygon_mesh_processing; +namespace params = CGAL::parameters; + +int main(int argc, char** argv) +{ + std::vector points; + std::vector triangles; + + const std::string filename = argc == 1 ? CGAL::data_file_path("meshes/elephant.off") + : std::string(argv[1]); + + if (!CGAL::IO::read_polygon_soup(filename, points, triangles)) + { + std::cerr << "Error reading " << filename << "\n"; + return 1; + } + + CGAL::Real_timer timer; + timer.start(); + PMP::compute_self_union(points, triangles, CGAL::parameters::concurrency_tag(Concurrency_tag())); + timer.stop(); + std::cout << "Done in " << timer.time() << "\n"; + + CGAL::IO::write_OFF("self_union.off", points, triangles, params::stream_precision(17)); + + return 0; +} From 9b3a6cb19246904aeca58b7820dc5967a74a01f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 11 Aug 2026 11:30:18 +0200 Subject: [PATCH 06/35] update changes --- Installation/CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Installation/CHANGES.md b/Installation/CHANGES.md index b0ac14e4654c..19c2303a3a2e 100644 --- a/Installation/CHANGES.md +++ b/Installation/CHANGES.md @@ -26,6 +26,9 @@ Release date: December 2026 - The corefinement based operations (including Boolean operations) has been optimized to better handle cases when some identical faces are shared between the input meshes. This leads to a significant speed up in those cases. +- Added new multithreading compatible functions operating directly on triangle soups, enabling the handling of non-manifold input and output volumes: + `CGAL::Polygon_mesh_processing::compute_union()`, `CGAL::Polygon_mesh_processing::compute_intersection()`, and + `CGAL::Polygon_mesh_processing::compute_difference()`. ## [Release 6.2](https://github.com/CGAL/cgal/releases/tag/v6.2) From 32a58b6371390d97f48a7f99ed719fc6c3ab8507 Mon Sep 17 00:00:00 2001 From: Andreas Fabri Date: Wed, 20 May 2026 16:46:00 +0100 Subject: [PATCH 07/35] PMP: Corefinement polish documentation --- .../include/CGAL/Polygon_mesh_processing/autorefinement.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h index 2b18d225b86c..4c7c46124461 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h @@ -1836,10 +1836,10 @@ bool autorefine_triangle_soup(PointRange& soup_points, /** * \ingroup PMP_corefinement_grp - * refines a triangle mesh so that no triangles intersects in their interior. + * refines a triangle mesh so that no pair of triangles intersects in their interior. * * Note that this function is only provided as a shortcut for calling `autorefine_triangle_soup()` - * with a mesh. For any advance usage the aforementioned function should be called directly. + * with a mesh. For any advanced usage the aforementioned function should be called directly. * * @tparam TriangleMesh a model of `HalfedgeListGraph`, `FaceListGraph`, and `MutableFaceGraph` * @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" From 84d3b9943d61913668156616df0cc6b5a3c0fa36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 11 Aug 2026 12:06:44 +0200 Subject: [PATCH 08/35] improved doc --- .../include/CGAL/Polygon_mesh_processing/autorefinement.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h index 4c7c46124461..6912c6541741 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h @@ -1836,7 +1836,9 @@ bool autorefine_triangle_soup(PointRange& soup_points, /** * \ingroup PMP_corefinement_grp - * refines a triangle mesh so that no pair of triangles intersects in their interior. + * refines a triangle mesh to ensure no pair of triangles intersects, execpt at a shared edge or vertex. + * If the input does self-intersect, then the intersection will be resolved in the output but the + * mesh will still be self-intersecting. * * Note that this function is only provided as a shortcut for calling `autorefine_triangle_soup()` * with a mesh. For any advanced usage the aforementioned function should be called directly. From 537b09457ab992138d6cab4a8ac00683f07ecc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 11 Aug 2026 12:06:55 +0200 Subject: [PATCH 09/35] mention Sosno --- .../doc/PMP_Boolean_operations/PMP_Boolean_operations.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt b/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt index 8da42ef9df0e..34a898912d71 100644 --- a/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt +++ b/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt @@ -18,7 +18,7 @@ of the geometry processing toolkit, with numerous applications in CAD and computer graphics. As an example, it constitutes the essential operation of Constructive Solid Geometry (CSG). This \cgal package offers functions to compute the union, intersection, and difference -of volumes bounded by triangle meshes. The core of the method is the corefinement technique, +of volumes bounded by triangle meshes and triangle soups. The core of the method is the corefinement technique, which refines two meshes such that they become conformal. This technique is re-used to also propose functions that enable trimming a mesh ("clipping") and bisecting a mesh ("splitting") with a plane, a box, or another mesh. @@ -323,5 +323,7 @@ The kernel functions and the specialization of clipping and plane refinement to inputs were implemented in 2025 by Léo Valque, inspired by the work of Sorgente et al. \cgalCite{sorgente2022polyhedron}. +The illustrations of the package pays homage to French sculptor and painter Sacha Sosno. + */ } /* namespace CGAL */ From f84b34e20443ac7a809fe159cd8d04397d028a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 11 Aug 2026 17:02:41 +0200 Subject: [PATCH 10/35] add secton about non-manifold BO in the user manual --- .../doc/PMP_Boolean_operations/Doxyfile.in | 2 -- .../PMP_Boolean_operations.txt | 18 +++++++++++++++++- .../PMP_Boolean_operations/fig/i_nm_bo.png | Bin 0 -> 172608 bytes 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 PMP_Boolean_operations/doc/PMP_Boolean_operations/fig/i_nm_bo.png diff --git a/PMP_Boolean_operations/doc/PMP_Boolean_operations/Doxyfile.in b/PMP_Boolean_operations/doc/PMP_Boolean_operations/Doxyfile.in index ef1b78846943..eaf44efac471 100644 --- a/PMP_Boolean_operations/doc/PMP_Boolean_operations/Doxyfile.in +++ b/PMP_Boolean_operations/doc/PMP_Boolean_operations/Doxyfile.in @@ -8,5 +8,3 @@ HIDE_UNDOC_CLASSES = true WARN_IF_UNDOCUMENTED = false EXCLUDE_SYMBOLS += experimental - -HTML_EXTRA_FILES = ${CGAL_PACKAGE_DOC_DIR}/fig/kernel.png diff --git a/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt b/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt index 34a898912d71..1665a9f1e17e 100644 --- a/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt +++ b/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt @@ -145,7 +145,7 @@ possible, the input mesh will still be corefined. If the input meshes contain triangles that are coplanar, it is guaranteed that the triangulations of those triangles in the two corefined meshes will be identical. Also all edges of those refined triangles will be reported as intersection -edges (both in the function `CGAL::Polygon_mesh_processing::surface_intersection()` and in the named parameter +edges (both in the function `CGAL::Polygon_mesh_processing::polyline_intersection()` and in the named parameter `edge_is_constrained_map()` of corefine prefixed functions). \subsubsection coref_bolop_input_autorefsub Autorefinement @@ -154,6 +154,22 @@ There is no restriction on the input for the function `CGAL::Polygon_mesh_proces Degenerate triangles will be eliminated, duplicated points will be merged. If several triangles are coplanar, the output will contain as many copies of the triangulation of those refined triangles. +\subsection tsoup_bolop_subsec Computing Boolean Operations on Triangle Soups +The Boolean operation functions based on corefine operate on triangle meshes. This data structure is not meant to +handle non-manifold edges. As it is sometime desirable to handle such cases, we also provide operations that are +taking as input triangle soups, representing possibly non-manifold volumes, and providing the result of a Boolean +operation as a triangle soup. In the following example, we use the function `CGAL::Polygon_mesh_processing::compute_difference()` +to iteratively subtract pieces of a puzzle. + +\cgalFigureAnchor{nm_no_fig} +
+ +
+\cgalFigureCaptionBegin{nm_no_fig} +Iterative subtraction of the pieces of a puzzle from David's torso. The result of the second operation is non-manifold and +so are all subsequent operations, until the last one. +\cgalFigureCaptionEnd + \subsection coref_valid_subsec Kernel Choice and Output Validity diff --git a/PMP_Boolean_operations/doc/PMP_Boolean_operations/fig/i_nm_bo.png b/PMP_Boolean_operations/doc/PMP_Boolean_operations/fig/i_nm_bo.png new file mode 100644 index 0000000000000000000000000000000000000000..6d650f92c82f2134b0f5d5d45c71bdd411dc7847 GIT binary patch literal 172608 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 tyxi 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ⅅ3yt?TWU3ScX(v@<+nRaBk|VQ48TL;;;uXj?Va|hnGIIon*$w&$-h;DJ+I1 zulG#LH13l@6ZmKZx!R&{y7f6Z`5_y6_kGUqb)Pu>ant@4mh^k?0+I>-a2gH*2IKU2 zBo?G#8j3oqk#bRwd4fttvJUW==B?2Ii+^Or7pYW1Sha@MqXA^+2O+3qJgIaeSJ{xW z#5S3b`H;ZuTY6yjlP{s;7T4BRT1v@nr9KP80JZ!fK<4O7q&+e)|1sCO9>}E^??9hn81o^!n}{ITGL14o~Rs%XMorfE%mASA+t!?#%q3e5>`kNL~Pwau1qfy z6ej@p@40jKK`tgUqGAy$LI6-Hw!~m}9foM{$_xbEFx9h(tQYO3_-I9uQE#h^jHvov z{G?gSOO>f5oD5J`d6S6kKDz*Jjh0G85=hHpJ)O_~XtCgo(T?zaXjPrk#@3^3@@UTww*eLgB{;vNDMwqSa{K~4 z-2R1O5E+K$E_5p@rKCff>AC8`my16#ZN0j6wU*@FXAR`g<04ONTt0@D^lvqnl+<1B zKNuNJk~YCUD)bv)3cc6me_E6v_^3UR1MldAk|V=8tipfSv}G#o4AiHxqy3Y%e?TDa zo|8ef-YtvAR~OfB*MwLBsuH2bc=@B?oqXEYKeZYABlW= z$CSXI7V}ceEOl8&UFjU9Aopc~?D8{CE*G2LLT)k1V!|Y;S*!A1J!PT8H~Cjy07no- zxEo9jqB7q#WFK$dPY3`T-G_@2r2Bt3LFh#RgLX-6=zz5zg@?L~2_QPs~9~wDa`44zu~_`>z|eH(=h~)z=l7H`0-9!`!s0!*O|p?9 z5(V`lEj(YH?=2b7Uk$n|z!BXQsp(}6wD|mP& zmqrkl@6&nl=2nIcevt=STW)^vZd-i2KhKFMVdBH0YV)D+qjt$Rf120}5i2tz7jf|5 zSoHrJF9fzzrL4+Kqu}e+@`+OYd{!r*2`oy^)ngdNI_L!jG?HUbDn=2jJ7l2yo!AP3 z2n>lNk@W-ZNRTsgncnb89HhX+O0)kOg<@?7Dqc=ZL+=)S9>mm=N3NNNE;;M9n%yab z8a};P)R1+T0i51xQD^-)EjjKZ<6B~=d82$gr4}}E(tVTTDqiQyEAKxTnTEzx5=lo` z3~kaMmkI|i^*X-B3fs+eQ-5=1O$hciOVgfgCO%Fe(FLz~Z2MB}PFj!d)6u=t?HyFF z`iABC`ug(z6yJL;i5aYLdqv{hgR-xvc;B&4UM8=;-9Ru5(Vi9q-Ia_5kVPo6mnpLA zS(vVmHRf%od0i8cmexbxMI&3KGa+F4M*Yw|9bgP^WBLt?;CUl8EY9W1Zuj4M;=mEh zc-!lS=cOB0AjP;3E@IWJfA}&Wn&n};ur04Q0dSww&}SOZx8ccc^mvqr*u3vJY|_xc z!lW@eOQNn;Xb(bLC*A+nAdhtZ{3Eo$C8&1TU z^xeqwVBNI~q*)jWx7DRw3ruz$x;enSPdb83665?LW{ zcs^H#(BL#!ru6YWCST>oIQGYg_(i;gG2X8v=YpX{eEWAb{mX$J(MrHzUvB^fn5HOz zQzaDAw3uVW;6Cs9_r}1V>95=O1bpOZh&!Gja6YI`@ajK(Yx226<4HG%l-1c7qjz_S zc!BTiFug_wEIy0gq5x!uP!R(!hrEdsKWpAe6N<|7{UQxr#g90eP6_JTjJY-YN6>XV`8`KA~M2SP@3!|n)ptCoxB}+S6cMnzhUGErAS;dLM!Mba z(NC957m@x)uI5BG_Lnlo=W5^QoDA626g+A2W5IkIO`69M_>3k5hN?kI{SJFe>;T$B zZzo9ywM~#>WaHN$<6dHr4V()#m%1^kEj5aoSa;c-HY{%`i-}^wI#gT zd-gQmLC}`$k8iNYguW)e<4lJ8M3|8MR02A$Mfw3{rk2y2k@iIo$*Mue^)57PTHH4@ zu5aPylZJA)%{cFM46}2ao7Md?MDJDkEmd}FOT3f~DrkThsITdU2^c}=lKkKC1o1#F zf;zOwA;45k;~F=gjJe^))>e{rtQfGfVAz#OJMjV>|_@$^vvc; z;&1A0l8DttLWpN|(dqNw5pSoVKB^Qhn+Zz&hPe-)Pii;tB+AiDA`syF#LKE3;GXr@^ZLV|8h40OBqYxL)k``rG(0z{wHs#U`=3NYJ4> zvpeoeEDt?9$)|FDC0bQORC}9^pV-o=i3KJpL ziBfZaUOh=ib`Zv`YD^J&koiv7W2G?hom|-WinpCBVa{xDHmr$)mTEy{hgVS-%Hti%Y?hyn zxQ*)@rGhPy*xt7hH6Due5W=kzvR>j51`EfOL$%G`Yw%YUj0%xP#aHKOp*`_0WZ{EG z^UtF0D=D0wy#Ax8=t@97m(lyfv_=5L0g%B0QAJ$)K?X4088FFL0mW6$8lA4i3!3l< zA$;ejJ0kJ7oiOr6oX_XA?*YwBqWu1|vt`^txWT+D*Zw?s?xkA_cZ!+Y7o0E^Z#`}$ z^iy+bmEX#z`l*K;JU3LR^l0ViTeuXy(y9i1CJVO~_V!UDG%~uCS}Q*`*V|3 zlNy(knuw^6$B$7T1LxoW$WhlyI<^SttR#MGb_N&VayYba;Hoxmq}R^$L3CiMJvf`^SjSF> z?vm)+@P+-uv*m&-A>F_n?ufjvz+#U}w=QTjPmr{H*MHN90jD0U{(wESvrht?O#YmG zGr-&8XxniA8aJwp9Y2%0W)*Fe%31O5kyi~&1)EhdxZb1Upw|XDzfL~f5Iv&T;#C=}-nZ}#231Iu(PBuP zKQOa|TKN_b3A$CQC&T#F@;O3%b9LdbOfIqx>rgJ(p!Gc@b9{@cs=a0@`sDe(N!PXa zcQiUef&?;jB*C%^bM3hfR|k~X@Y{5aqsx48o6vYu{ux3;*9+z+{kHj!3TEKU6_X2p z|MD)wbKqA-zn?**69ZI692z$d&uTbGL66ohL6@9t(?T1%al5#Bk%8CM#$9!V+zo@n z?*?A8$+>)VJzh1peSAhz=v$gy<>gT%^TRFo)m22+(8h;q>Oy;MlD5esI#%eBHP(Wy zDZ?#RLN-P-m&ubZ;?!Nis_v>%nf+=m@`MBLXP6SfGDUBM2W#;Ifu%=MW2ZOT^0(kp<3L#r!F~l2EOxmy)+nC1#Z7Gfo zO{xe*CPPNFzI5$iPDlS?S@mW9PkQV8$rJ+~i8xGiSta-`yLs;8cD;Lp<*Y<=`X~6~ zsN%&ir*4(LnB(z|gk1}6%x0oZF5%dF!@ib@H3N`Tpp)R^+B97+S*tldz1!gq|5%Vq z_T2ry?dy(sf;gHKI+$;?Csmua+r6EqSw@3^}BAtj9km+bX817*fJ~VFH`#G zIRa)y8DG&`>l8H&le6lkRa+s!bRo06WVsg|SM)UCOVRVm?WYrSI)*EZnGPt^CytX> ztsaT%9_!s5>>RZ_yJmc?{nc!cY2)BfUa-d}xi9W&2kt4FzO5GCGM=ZrWkL&+M?j#F zvl3l;X-_G=Kg`IVQz6f-t<`-#`4+}>HDRwZY~tvT)BMlWDbpkpN-_E0M`s2VK=#J< z{r$n&v0fS(TlHXk>$vW5Bm9majJ!|HaCI9-;Lfgb395U0Y*(!riMB58#6R*?e0uVP zB{s6G&&?x|xWjALYVa`=Y{%or-io@rhUB=~o&$&NikYD!#TWaHn|TMTtv$T zR_)^a;qvAzVucA?oOGu_O4i^pQLQKUcP-B zHLK`7BT57H@3QFN?QufBl1Up99~SNZ_!p|ZJpl2cQaktV5;B}*U{#rZ&On% zyL6u0B^C$qaxYJt`Sh;ggDR+lE9Cc%QV)Om_1{Kpx!Na!z61@&$AGs3$xJdPX*9W) z#~Qek_mxfL#2N8TuxOYJ<~6zIjY$zb=VM1`QizOo(NR)Q7iODlRAU5LRt8b{(ix`s zjK2J;miFVprPW)VYjyn#VRiVRy9$5-gFs?mdd#6xNE5RB4&D(p% zfp?0NS#iId;op`l5yz?wc|*}RNo3&Hk9w?AC6C25<=pNB_Z?g+a5g9sTOQFf;m6B^ z=H}<)`BhJ$x&5jK=-K+Beu-vX@p!+HcLNekxp^u|$8K+!7x_wRqTIu#@*$3;nK{>` za>}pE*exPbi;~uKzL{4d2JYd%nBtv-c2ZCgbtaOMYH_v4mo-}l*b==$V|#@Mi1b82 z#{tD!Dz1mzvrogIDun+sk+h#CauS_V`KG&plUZ<3p&M69S-%!o>9d+}(my}^He~6A zNgh53EUVoYD4`KU=12#RBa;b0SL!YxCA%^C0@YSUxg~;bT@BaRQBRzGC_8#448A_= znk&k6!Y}rGK?c+A;B;i!Ak_f&mY7;JQyn)_EQp@n2oT(Rzydzf5f<-N5T9E)Cq}(Y z$D6aybIQKo^RTx2@5uPefJUFhO3tzl0vO8G>8!L8H-o-!ZpIqg0ty=1-G=b6%2IOi zbGX76E3XBhH|kxmWaj3*Te6kcm=l4rcBen>h?tYwwsAF$tD2fZagfjWsS7`t9M!mF zB3?6+pj17Vw!mBA)q>5+C+)Xu&XADcv4}Bz796Usd<}Z}2fwWL3~ z(Nqwxrvd*R1%LnWVvxHQu>I@WaB8HD$Ggwha}L8GDOCBk z+>1D~Pd&;^hR21v-77O0HiT&&;bkTbshn;o+QegXe0_p~3O(*SG5+ru^2?g-Njnk` zLvq|6$nMoB$k8T>>MrdRBpGQ~;RV`2r~GH6G=pr3QZiyBs1XHU^)I6Pw>QRF4Wd_# zsZI#v^kRyvWT3=SQ?|t%F~@dF+P34O)IXHJ1WH4$H6FHnP=`qIv>CB4wGYuVDtFaHFLL}ydbJ_o}%2~is1d&Qi|iD z|9W7KvF>cQ(c_W#-)Hk`qYUVIo+LX1*~*I~>J2vO{rlaMo3knE0tN?fAFbsk6{vrSlOJt6p$0_=$vTansENH3@eOY zWwQxki+DBHDkltWlP-|#egrxUB~ddy)a@gF!(Eyh@4u@xO=X`Y`N^e-?XV~%LhN1g ziCyvd{J(zH{r1Ve{*wv?6=_9e#E5H$;^QCAoVC(Es{~rz3K{x{xw&VF2IHNKT;2kO zZ{5o(cWw?4r#c*KQ+I^f>$*7(6-nd4RlK<+GTdN6=P(HVrriYTs=(#SPdNzU)QJA? zU#<;x@JODCc3Tw7%P4nr)V(Gdub3XByL_WNnZQukWz}kUQM&$DV(=?%6^G8LpEYTP zCQsUFSfxhwv)%%0YGZZFXFA0D6Ade50ap^nmpdEr?#EIvn;8gSd>&;X#B`lqz_gr@MnHMna*U{L?}DxAE%HTP zEh8T&dc3rwLSLjaEDEq0>2}TYWJ=5|;yVk_`s^<2>zyg>kwc`@(^AQ4j2lZ-R991xXc#goNnS`SE{#Y+KLhQus+ZUtL&S_+3-N>24^h zp~PpEmL6wK@^62FZ@aNe(MGs%Sjlt=zn~ zfwM+yk3+?i`mpdwxcu@9<*_W4Fhb9aj|TcBwjT-I6FV9WounHsi*^{D26sHJkSUk7 z`e>f~%uX@XNZj8bdnD^h*39#~NYH6qV{U1-ivAA2tX0%vka`CTCT63`m$~ z|NBoaZ6dtKJX)@pbVb_^v15*mShB>+8&+`5rbwL>8z79P7PUs++Pa>;N6)Amij=Xt zp02EPE>2K^k>1Bl;2}?>nE>s!m@~NN_`Z0hQFn=1(;}&@gl}w7uZ#4qk+)qj{*K<1 z%_#$n$t?fmQ1!>MgW76Fg*;PTxWm+c9_RL+&S?46$iB_NK~Rn_iq1B>^AQhN;)NH> zJA1YgX-|Q6{`z^Vym)Y+hX zzHH4Jmbm6Fg3UV^OY4{C9?QLt+vZTzb&v8zL@a3l(r%NrV>I@sTOIL1R7?UBABwfS zX=+7V*ZwInetQ+WId(YfV66~PvN64W4w1IP`z6L7P51^sfeAssC1f_Jb{0mTVSp4P zY+!MOO$sRh(v5mCeIpBPiLd^m_l-k6ZX_b~+dV4kBW;q5iqU2Ian;cNu{^wLBB5-% zfhsX?t+tKm_!|Cswfq%J^==bz3MY&p;)?0&R#(R;ORU++H-KgSucV|P)?LcpAQ+s` zphOB4p5?W=Jz^}R>tFL^hzmIiWhox%5-l==94d_AZ)nrO} z2ALoMR4ygp6ZE%7FAHv9zg}=OVC40z&l%jLW9Yd88 z2aPZCT)TFcn~J^3g-^iZlDcN)v2@Va3P9mPQ1WSD>En}@Y?O!#P+(}ndH@hDSJAT_ z%YmJ=ecbpTwb`eN=BcI~!5|F8N1&epI?Fh|*ju7#D1?(oHpD-HUKS93^ANhVD{i~_ z?10Ag6hT;;9PgOdjo4KRxVYU%H)cg2G z)xUGj|5=7sA|JJ)EH|rPHOr6uFDaE|QhN!@CQqTWFIg}{&N`fL6;v3fQrz!w^c+j3 zcPh8gB@(N?o%YRfvU72yKn)yjV*+&gY=({CX51En;`v^3%Ni~&1$vc~j=%$9@maH6sk>Jfp}J3yyo-b=hRk^IkSO1k(t-(BFL7-ppI9)p-3^Sd zy_?MCoD%=T0+Gw@4?xzhNd~ii;FH9Z$EYvK#$3KGvA9ab=vU2tyK3qafJME1)IAh@ zd#Ue40TY1nCELs{?s@{x=G=X5pm}fT&{?%y47_*iXQKtWlSedY!-bhYq4OhamV#6l8)f($C=&T-I1HB zhVy=-mCsT+Wd+r(4g2xmJ{MNMud_RFT-^E5&?m3bgUE`gCypq)oUbLSEzrE2`)qys zMQbH11Fe}`g^VWjHBU{TjK?*-YyZli-n8FS6i3VO&ar{+bO{DAGSQ^Q!`k?hyxGU$ z0V?z;eiK^Ap6Aq#PY&yQbY?Ik{saTw*U>%H@ScWPF6u*%N;@7gk;aU3V|>|uu8R55 zpI~uRG4*nJ6j5ha_4q9*HP_!B+ z=c~@!*O7qg{rU+d0@Dc}Z^?0PxFBzdma>D9h-hO2CFEkdX(ey#__&OVLf<_7k26}g z-xCl=N6**#gjsMgFBi76fGsk5^DY(+u1no8a6YZ_UNl$8n2d4``GlX@RI)Afk&`US zk!7-2AZ4^>O{PphNwywBzXf9c4f%kEmP(;%NpUfD);0zNiS~My0KaxE_s{~L;iJ8sNcAC9thk#|% zL3N6im%N~5=7}G#?D1lpqnJ`sB(bO_(cNp;KABa<6>jU@Uv2ZO-e6%!iSVL4^mQ~)y@l+$&aUl)_i=~j}*m&7YkPLwnWZtwa~J(OJ=N{xmC$^&y! z(rIfB&!tOyoI`>IujqBZQw9$k!qwNLy|5QNgRBub+24G%GnJ zg}RYSuBnY>;din2RI4CCmHc-h`#4_`Yb@y7;zafL=D}j`B5*n7x0xX)KRw}(8xf91 zE7@x8mF<)`AT zWa+M61||HedaCzQml@RzQ;t3%CCSgbsjTiZ;8giopF0NYpk2L`%6x?~7PCWds&wVm zm`uvvdUhjFok$DnE(FCUtpuUrVzj`Db@V5u&#P1zf@51MB`d*J9f5?CnSnR_SH75w zUfP4e*X1Ry&aEO5mFE*11GZKwlurL6+zTeav%a-Cz1W{4mY$u``!*0&32f1z zy9L&m$8Xo`m7AXfYcb)c7VcpI!~(wZ+~}x3VKNigy!JdPG)Uc2{W`Hv|IH7XzFl;Z zcHJOU7BqhcSQ4tow;m4!idQw|Kjc-43uW220XkRMhRXE`<2kNA16l5FS`O_{a$3H+ zLGx=r&crwWqb8G|Th~Dx)dpj(9G{apt4M8Fc>>pcmEUx&YUr>-e4|}t_?(Br%M0|0 z3m+T6MDXmOd1kO3@1D z=w~#{xVk*i`Ocz5%5%$Pc0omWT(bL{8KVpDJ9V6HZ$*=GdL<20U{vIE?_-$jdaQ_a z>*1p}|6t#LwGw{j?iDEEpTDk`VfQ4-Wr268ykA#`Zt@yk+WH1(538oD7jypnPZEvBuLEct#yW4bxvJhs<#IOe7CaIlipd(*yZM7z&|!O?tY)I zi0KYW=c^W;AD&sm;6H$>@-anVowhNQObO81h(uC-Sru?!oJ3ao*z{syuSuHeZDljI zdKg6!#RlDZh^}0dz3SGN>fNYPEncue15wr4O!dE$lqU#xDP?~Svzi;H8=(g8UQ=rN zsR)r6xgT+Pkyi0tIq8lP(=%mQ1QNmC3}cp>57wz(v0R|FE=U*K8~N+4JsQ58!p#`L z4u{3XRpVus*Cg&$Gns~<+{R1&3(A6E81&MpO|wj-xEmADsGBN&pHpij#kY`na` zTH15k4&?=$)DR`~8OVS~-L8%QJD9z12k@t*m>JocTzuzLq6^=A!X#~0MiPVLY#gq; z){TT8v*fmsLNwCN0NZ8b)3%e847zTmE(!KbB8f_qxgVLK?FkEt$vEgvp@ZSWdm;MG zP#l{_p8%wFJ=j|?9eu^HFEigQ1jD(;3O&q|w`gcpR#Y(!hc(7?QGz_qN7zx-XkjOZ zMJG=7-Z2?9YCit@AI>?ZqIt4uhxN^YgW65ZHFnF{GR_4?#pO|uFBI}YqI?e=5 z=^Pa8=&bMV$S6^hB*cZLjN~o6fnq_RotQM+`r1@%^BY-iA6<-kYZ`6ejS`i%yl%ViJFrnV+W^L5BD#WY}B<|dr*6D8cc z|LJ0;pWHg842woec_@jq+al6)z>_d-ozo}NyiA`Nesm8l3u%yuGi@cExQ;wP0!Q1I zzMO>~NE5%gnonRo2(yYucSrojAD zxB|bg5d8FOT9KOs_M7c%#)x{Vm-YtjA+o!IOgRnu#ecnGEQm_6)J?Hc-TR&HC@0&y3 zp`P!M!4LYAt`UomI^u5 zNG=yu+mJ-tRvpDUhIdANYE@#sWc}VuRlt54f=(Zn=Qbid&hNpecNm`Dlr8qbli_i^ zaaU`MR+7*K;ZwR}nnsa^f7?~czsiTaH%PjnXP72rKg0N1a|Q5VkS#e+^!2?_4tfyYf{@%g)rFEnRF*@*<_ z;FpRRJSN-d4{=QB^A)C%=v2E##phuV0&ykv{eF<^Q$T^c?)m;I$P;ePuYH$zc@YJX zhO*m7OBCM32`+xJ^GW3z&>=Cc4d@FILTCILxP0-g& zS?^vnck0sF_3FiH2H_MvFDAUJ#{Qk_z*)H4C$Mi^o*a^uron{~H1_plj-!Z%Ch{8z zA)tk6P{bYY8<(0xDOS?IU0R4p)4y{^@PIpp4KPJCiZfpEtWXJ_ZF_YX|V9EbtE z-Un1>@?;uDLDg$-{vkuXiRaPn!4H~@cyD~iee6E5J0~KRrnK^Frla`N$6HFaK5g_;W^j1e`@^E_)w^Z)RwS@^o=Vptx zMLlhzB)Dv8+{ws!sCa*h>iqDoXuQgX;$34%u4j_E`p2l`O0n9 z;PR70*6@zwa_JYnSOPP9rdEv2)5snrH z>L}B$82-@ILLgy3@S`tBj7vo{-C!0g8d8^}yi2l{#9Fe1Pq+fOf}uap36AdwoJjUj z8`AA!IDaq(QBaFC0z5wE>PlVFhRSwrH|dR?Z2z_%kz`)G1@4%3$I=cl71-bsflp?{ zUPP5{i>;+#Quq3FZS#mR`I2%D)iN#nGLYxC66h5;t=2X#6yI_gMOGCaNjg1Il3LP2 zels_cX#XrCCI!x_(o?b~uH4UA9lZqk^zNLBj>l8lz>^Fw_5v8a2)S*pSL18pFKILb z8@h;4+wVIQ(0cQ%X!8S>u7J4XR3Kxp#55DPSW<^%#X8Ajyp4}CUIHLsVDowG z(^oBi3WU0(DX{M1vhW~RI$OI;J&h16aF+9x3NUmN`c)_g@yzMvzZGz8Y|8`U)36Yg zdJ)x-*ggfIRnY!4p>9Ch-+;nBENUfsbt93B3?fuTj4beATFeP@!dRk?%T={{=QRF+ zWFB)Wj5C_n+huwf&PDZB2!j^lZ=`#WRGhN=$(7D+wF{^EFSRA<(SsCGy`KERXCErl zzx{CZmi4u*9Bxo_(0C``tn1&=x{zU=gmNe}L~I&&J-CQRH`-mT@c2rN8kw&H^LFjw zMr$7)q^9AQX*7E);<&p>sY+^oLuBHdZw@|)2OYco50ERr)U>u|3=xsAF>M*>*E+-3 z34SSV=uBA(@RH9>EL{Orf_cc|yw%L8i!UHEi#mYioeD8l<6KVtMsQ0l?0rjbx_Qfj zNKPugL^c_W=V3%~LoG9en$eIoo+nLEI2&EgXX)|fZnto3^|5}YSykw(z0zBbA+QnC z_WL>CcvsH}mRJrL&RnK39>*~Ce(F^4YpNH3hLH|!tKY|##@AX$l6b-Ma2?ES@eiTB zejRURllrg&ZZcKXabI_kNT2*VqEi^6+`nt^_|#c|(#xZz#(u?#)xS=*CW@4&OtHvH zv_clr6M)3ZXvu^EE0Z9BWK7Ld(~;88D;^gTf(%LYhMsEXYEscH#760|bM`Nt0Nd`^ z-QWCJ@{jObJoV+em#vS!@KQsbFMuXC-^}g}E$8Hb=}?tqUgi;pxA^c3IxU2*CsOS( zSvtpM>@W3SM+IFQ`Mw-!Sw~hu%yCh#*|?VI>vVRTxFUMqVqgFFu??{B z`zvmjp;3uSxPO`U9%k~7t}Ae$IPLomnHUh$a96XJt86(32LT-nsIOYUmbuxFUn1`y zmA*k7efzu^^Oo1DMnjI9t^DKs(DuFNDhghL3ZP-Kal|Uo*<_oBtiGC6NgdE!9I*i} zGwuoiTTkV}Tx%KSzea`nu}&12pbyzd?Tbk&ro&CqXP#~66bxfMb2~lbIAz}Q{Lc#T zaRFlmu(|fSdEofZavwE*u{-G{<0abKQs#%7r7a!gQ|ah!W(NuTl|n{L%iHJIlP7=Q zqj)@}3_hRa1~i%Ia?FmG_lqDqA-b5z{V9$@v6|75{><_fGS^%oqentNkLB(|{cD^< z+}edLbeH^#aw;70i%f|7An>n92i#%L`m4coOTs%}s@U@6z6Aqt?*&LXJFXh`>cDlW zRmv&;df1JgTy5^#gmR$TbvnA~aSRzw>$UDX*^CI8eoPXP7KdjJc%4ns<5B&PEksvb zhopBrZB%gmEF*rW$Qkb+Mavni>1k#br*gkw}a-H?b5xvdRbGJQ28Ufa%O#}I`_to%IV!o1Qq4$+ zSgldb7y3Wkw*P^c-VSlV`ldEyp~t^i2+v)5z>Kl~5Kw#GwJI)E&=SvY^aK}y@fg00 zhwNmPe^FyXrA$Wqgl(*?OCW0}h*u^Q?^Is-2(~X(t7qO?qz<=epqL&zz!+PjK9kFj3)kqoQAZP9P`#yg|>-`NTzp8hfk zqD%z(9j!44Lw=bVj(Wqid?plozH{2~eR8Pa6RtsYFki}hszZqY>(}R8l>ayYJs;=m znBWJ4u0=ZgCr#ziwP1v``n7-Xmh=coVlhN36weoKI5mIaLaA#3jm(9WQ1*h@zA}={ zHti~pqB~a#II`nXLb3J368$x|P{H0%a#Y9Tn8PHB&VuINngn9ihct}Y!_2f;=?K?q?@l z4Ov$89yJ^MjnmKrMDxFJ|1Wdw|N9|TL3m+G0O43ROzeC9WVcDR{)F~U7ZNt4R(kiX{wjBWqf^TI8!++tc5O9tWZ@K(V%C{Trj=-b zviMO;{9y=ST*g6~ZLFQrq`pq{!a229N$p_W%VBXv3tXq)9YYb{3RPyZgZ32gy7=CJ z(!>!vZi@06Tx4)rR$SZOr8Ln7>iz{e)g!r5jzK9yh#}X~y^l?oX#2KDhg-%!2Zn=P z?;8~4#-(@rUL+e_Qm}KWMk5n*@8AU6)fZHgO8D0^_oOK$1x(9wR>ULAZah3GlMKsi zEiz*k^rgHpcTSG>$WvO)b4FiU!ZdW!?}AH&-t-7NwEIPpBD2Rn2jo+yN&N*CUa4a? zq2K?!3fc?J7DJ4Ep!P%auS`T(ZCW^R6zI4=e=>UcL|5pr65E16l(s997tsb?yOosf zma52B37VjGdQSLSIrNYJDcM>P^W1);kL*R?Wsig@cK1jS*LEHJ^KJg|xn50Vz(?W*KW1?MK*|=g~9i;>LS^tM00kR=h@s- z-1;5cl(4Q&0e%%i-Zj!z(y-}tzgfKNZMI$ou?VbZrvR+ z8H#ah!gB~L>9tC$Mv)v|O?e?oi<}>M`uih#jTr@hlRegIWEkhk!BSw<+?xzvzID4p z5wZM@gG)Z?&APrM7NfTRQq-Gvh68)fQ8e|Grtxgv_;JaZ;Vxjlt-z5d)zb3j2T;=j83Fhnr9+K6ze~DMKWGfs68Qm zu5Z4`-Ea?bFBh-AXAs2|jK|7ASra8=MXGP;w_b*I=X#b(*Ilj7d)A3Z1v+~N{r0GhT z^!VB)0m7%SpkkP3arYBf&T34 zE3=_)^bx(O*<^9lm$xT&BHgkzw~auMRdFT^Y4YU$FudWg!1Ak%S{Tm%?`{E@g{4Xr zw@>*}CN7X>RgHQy3-6L!9b8{hQ?Z}52!?}ba&vA*&{g`;;34s*zOoJ>n-3Bz*GrVK z{SKEn=**g53jo!TAt2>!f^DD8qsw}1{VETKU1?j?bak~h%Gm>`0{EEuSfOdw>-w#) zrxIh=r*|A8)~)n_yureChwbB6XxXrh0E+CRzq!dO4z#(p#hLz`sRjFuPLq&;(ShUS zr@$M00NPpTBe~Xfh(()o&C`Ap+udZ?bIv}82(+0pdQ!>D&|esUb54y?pJdX!@QFLf zbkHB^Xd6wBkgUn9BAD@qv(|Pm80Jj$pHeVC*dH71hgm z!KnmITgx$X`|A&A1!A(wH=ki*}BF zZQiaM^mmZSlJI`SI@y&B-s+_ z-Q=^eihKB$x3E)u|1iEK($xUh^@iCqf;LGAUUt#jtYs)OEiB6{1PmOq&V|$K?DNBG zv>o44J;YnXi~n4N*U8*yzzp125gNHyG>mO=BL)4o9p2~X z-`v{Sil5l3)PCNJYmcfjmt_wURHR1bbvtw=Cmu8%uKZ{i{43IwrpbO}Hxl#C$@KJ; zs;@UMKUPbpyIk(jWy6I*!`MSI{Aq<7|JbmA6c^^QklwOc{w;1P4%CjJzwK^`>)W6w z8YbQISdX-?M;mu_Y8|uPv`vPW-Rb4pO z4yB3<#2xTp2pJ!^Q)i$4eeznJX>)Ca`WxUqe3d>vuv(BP1C_$kIB zs5sDc;kq{)@V#;ma^$lVZO&;P^>gw$@o5tHfSle1$o(MgL(58|KV;U{Xi1-Lj?x(s z=LGGe)>Tt2HS|kLCP{W_6aMN$U;)Hs7&JcNvuuCD7Cc?J>{XUTpEIJnSD|cqUh%Mq&}R5FqTdMYu79z+&Ah zRQn;5kaTRns}vx6sR!F8T@u<`&3*9VThDN{{Dgx>Xyv6MoCya9vJkQvQ_u?K!0Ul; z-=&s{Jx*4^Wi*u+*|cA#+1r$~>Q`O$u~$Y@)iFw7>|{L=G>Z-%Q=7SXkCPG$CPen`IiJ{_L=TEc*!?-Fpn^J0cUu^+ zIA>oBh1NeAcJlh2MkQW*7r?c1;L~c1-L~C*Qkw`TC#+kVeG2q0t<@CS;401ZWA0co zui+T}Twa+SRh6MeU5NQ+aoX2eO;KH5>dfsgKuh{Eje_X&{+cBwmcD+}+b?+2FTe7- z{K{ZAuiKseqNA-@k5DBF%eyYS%VIU`ASGZw--YfpAX` zK@b1xAT~WQcj;`9M};Q3EW7 z*2&p{#ROGzLHX`h@Z1{lk zBBXw$MJVgk+VrB{gV$Ku0&Akk!*dSgMFQtZbOD)GZ@Nr&dA(D`r}0!M^mByIHU9bpEgw52}o#}s&M%N0O@#syz& z;=3RSZJ;*(Zptp`dj)gj?K*Zyfu=r+RQ%DzyvglOi=6p`hv)D{dkCBdASTNM{E4$W zP`nOohFCemTDkR7gy2n7--0xUKJqtRlt?0e;v-HA5QCn1AK4TKcx0AKw?@i~PM8SF zcRTc}>39+rLu@^BZ5)^(;1aFEPb?VvZJP@3Rm98$5H;>$-kHEKO|M$L~+a= zv~y>cDd3Ma$iVZAq}{iguLpjXO^s9&?`~xgpJx%bUI3EUdit5b#!Hoj(It9|jAH8; z5^0w0?$F~#N5#`~-q6?S4H>MdAo1ZIrG zC4McgOG(7FE#|F@M%a}kX=vIr2yvll+(h_EQ`DpbF3|h)-$Wd@AJ{ouDu1UXIKKWi zQK7I73^}QTnnN^D_RhiDF}rT=;%N!9Wx9M4gok%p8d-fdC*2PI`zalOPau)q+VQFfRj?OvaTIp&UwlfbGAO7MV1LR6h ze6nC4W!K>aFPh7|??_Fu!ZE^k6-pA>98VMVA?5a(3#*GtZO$1t2 z2fNAQynfSS)~m`VOb3!KvI`i3h&#wyQblFzBP`k8oKs-Yc*g$eFhGKx^nwASKoiCt z=(WdOTW}TLGtI)bS)U49K{V%xX#iyDx9#kbB8AXS_9YvnJ`2i)Z@~VUY{sv~O?CZ; zG0?ihkkzhVTLOG^z}p(-C0tUYh?a(@i-(8jw@{zXp_Z!qu0AiJ=IN7Z1}{T}@=4_i z@Txl$AfswBA&}<}+Fa-48E*KaCoGE&4-Rn?y}I!~O4a|LI?hJe{1<#CuK<5G%Xo;D z3UJOX`ILvQvdPpaI8SGln-??lCVT%}ajb&{0Oa?6Qd={o@{nr}A(f)bU9}vp4&j<~ zrTa}D1y&31c?1N$(3i~2c=^Gy6l5;6w?x1!xD{x7TG^{ioV+CWA3Vb~&|Dt$JHRJD z6I=;c_K=_Yw0La4Xs%xjvpAy{WQ{myE9pIBw>oweyL2o&%O4~AJ;*0? z#uj!(2cr{cXO+COG8tdm6z3%*N5wkqIj^de2k6A9F{HNDFRTV4UUWPlkKTwB9nBJg zW_qXQIEW?9#fK+}E5}RgqxBJ$QnE1CnPT7V6RquJb)*~Rv_A^5%v?WifcTcI**5$l zqF~zzA-Et+%mMBi`_e{2>*1#?Oe?Ktf4}Q@tY~9)&r+(q1L3qPrdYP`enJ@7-ZKTxfDF&I-n&$=~qW$C{`$JE|Qo z>Yaxr?t!HP+m)?scV&Lkk3_(Rb_Hpt9kbcl)|0DGH2qAjN2Z#QVq~;D(L&=Bl+5yv zZYMNc!lQLyX$%O-1Z}@5GW`A`|5@+*bHojK$^HOHQ(X`yd>WpytlHWssNoA7e$cJe z(k zHU&}v^{Cz+N(@itCZHD9r*PC$Jn?zQV6vRSbsWU?$e3q$m&aB=FOq4LgQ3d%1!2iK z>kx+4Zw=|a!`V=%xNq=ycQB6lQ}K4&`nayvv{VBoKyBD0?{5G2w>6?95MLQoHJT$^ zTjX&W<8wiX75de%x@ln9#>VX`W2tx(&mp(zgWKoiD941&A3a^C+{$4~QJ_e>EO}K` z%jMnf!iSiGj^@^z%S&?6EKI00c2M9dxH;{>Uki3%a|n~_#OJ?^?7d6#WuYsoT3+w( zy3|Y$PWG}VqGKfz6$F@fQ zbayBl(LCx$Ut>FG| z)0qi7^`BH;8)xiE${EHH{Uorll27zZqUj9Nux`l}q~mt3hWcr%s!>_Ge7 ziT6w^cZ}QQkckIo3}lVb`utWqcwfCKGi0uo8@m4Wd%lJiI)1Xlb)6e(wpUP?+TnC0 zLGWG{5Qznn`=$H5GRvQ+!9J5ra=6@z9rG0iE{sZs7lNT|ywG*N8{~}0G2E28Ju~=Z z;QPgT1&G$yVy=A4{XB_2$%StCqMjvqR#7?zbwLy)(xk5rNe1Z z%9g{JJct2Bqs|E2F9=Xpp=i73ET52~j~Fp%_?%tHy&+tO0>Ly|vF2|MnwhQrjenzu z#AJU}u>V-PL~y494{`LvVmcJL)@(QO1oKnPz;iO6bORdk@di|amp>Vx*H;T0Hqd2p z9bN}fve9mVpkTOP3`${zbS&Rfq>)y;d=J;IXS0!_@Ust5Azesx@hnwy+XIze*ykJW zZnQ$e8WKjF6DP^aeATQf!9X}Ny* zY|@`2%F`7M)3AZXG=UamMOiA00xtnW6zAGMARnpF*O$M;RP#*yv^X~u9i(gT@y9pqY0olKpxb#@@Jt z8>?>YpdeO`B*wRbA=bMnVrsoF!P&9&-rZLKEgB4WMqQ5b4TqYAk)rRg$kXn@*TVDF zZ+Fi**gvQ4)W00IijYf{ngM`69JZzz#kOGRwR^7xaLy6Yj2?XoQPp$A9e3V}M}ZzU zp&w>!>eRJVQHgO$Tvco}MLK{}ppZ-%8Q6Aln&QCFi*9C5DIMc3tdC z?Uu{6{0F&eHcobvVl9$c`K>os%Nt{yU>2(6I=JQLb%b+a;+%Drj4^vbkwT!!3I!*d zARn>ucri=xZ>}ir!T!H)=1n&VpSHGudl>$HkeFP=BcpS~#Ak>X0(5UB z4U=a2`ZNMD;x7Eqsr&BWq*>Mx@4rRFf}TE?2|IuChd(4yZ0A%O1W(7pyvIy?+PrDU ze|_;$-<>$v5mC-0t5W#_vf z=pZ1tiij%^AQlEGu@|_@K zGT-{n{*P@1R3ZFw(}ul|;LZjfa6JKQ0Z9E{QYX zF~_YzQM8wEwY6WyRx1{W0leRs8dbKB57+8<^bVGzRL()qv+Zbk-FgcKGvb#ALrH>Q zS+h`hXX4I3F)&*m4^heWh<8Ob7Ekf$1kb?NIs%;~1fJWNmzJY+!neSR_{vLGgDdoG z_q#B_p!e9q!andKNy4yzD!QRi<0Jq^n25F>*qXEMO%&v=6j}TI0r{iPUwEfV*lOa( zt|qMzdCu2bN6=GZy_%TGbSaL&g224s8}qIBjPAz6JP2lBUtM%R-m=S@6rTBiDbP5P z{d;ca-P-%3|7dM>4}3QLL_*f@T+oS-&E+LX8o&$ND@6?-4&la6zr@~$%94JDnCxVm zA)hZT7W%UayrfrXhn63&k2#7~SKEg~-*_ACGCDG_PmU@f$%`NmXao&L)gZv|Vw*x< z??q5BSIWrhM2hK!=>wbpMx0)6t=+%2^e}TUI4NnhZ1^)(EmMhsl;=JdgW$oQ$T^wu zm6ZQ7V>P@MSo-=Jj~?|ixuXU9$HN`OM%9db0?RX7S*a3lK*v-IB0P#CVI|H;A!eR- zea3Ip9J}66mr|{gYa6cp;wFXk1_{nQLl4sYd)h)l6tYex4N<5m3zsZB}Er+mgZd! zXSJ182sEuf$eP)Ym(bOcQ^9qmK2upTIYmK+x~Ba7wYy$%vi}mo2_C82MBQe3!H^WM z#m(6AGvTKEYO^8*bXJji=A6Ply_T`*CAPJ{;Y3n_Z+{-#Z%wJnN0S(Y6{>sYeX+wUUo7^nA_8U4^2safLd;M zR3bfSCIcO8hDs0{Out$rk(+TVmD4@?;^Owaq74k^3cw;#1?7bG+{sEfJU|?{u><^I zg@t9u94R>UbQDoyq_zgA9sjn5iH?y;uc=iOP&_5qVTX>V-jz+bjH2BdxG=Q;AvHN| zFS)B@_C(7sV88AP;4u{nl7RglNJyp4H+#I_&H3s~uHbL7pGBs@>7cT!t7gx%7}6{5 z=HlXns!KIvM(CzbSBGoT-^Exkx7O7onIWmSEMT++SHO(ZFM}7NHfZ9ihn)&}$3q1$ z_-caxyGJH0g^Op#2d>iRf%)nN^&&W*?f)RS?DC$m3mv=emmvaOGs^bJ`a_&Gcdn`i zM>GHTUGzN+D5O1F6b?SP29zsceEGV6NNGbJnz!+JY#J4tLi6tw!c~tGGAEyUoDi#4 zqHGjLJdV{8@HXk&0zTT`}C2p{Is2_~R`+x=X)V zO&Aps!N%%5j^P8ZdjEx)gI*E!zXdTnJ#(+Ifftwsd`*`^;LVd5@vnK?=?&P7k!xKa z)GxH9XRElh;~n}nrr0Ng z$KA;h;!R%nouLrfK;G$Ch8;{KWo5&}>L|Ns4sBXvn2*7*?5h=7X=t_~Wcz}@jCI>lONEar4 zYTrCZ#&9oH_BZ>e@LYi!et%Ohvo41S{+R*EdDG<9jU{ZlLU2s))jTr1jzy4(Y{g8l zWi@`f?AMCF0(L?}o*gQ1G$u4kuN+;9TQgl@&biZns~NKVuSn(dEUIV_j2#=T({Ea( z#dx{D0L4`AMbp-PPCNA%nLf#$eF1n&&?yS7eE#Maq+mol>JYK4G5NWuWNetpE>*E_ zQrvJ-Uu0TU>l5U==-+T#BmuV9u}k%5AnC5@R)bO!p`)V6RA}yXR;!CbQtDFdH*_+qL%fM&xs!uz(R#0ouaU!dssRzdHWa8qJL^n~*B z9d;+|{T$WYEG8zSpowYq9fR&zuv=t^9UofnXa{JzIZHW8mT)IZn zeOkEBrAQ4p7IUFoMo}f8y$-xy?TKxt)ZH$il-mAgn6a}rg5>zfb8-JLkPRAPUQ??8 zP;?gVy3-b~BAxp?jHH=>pS`vmk~Q6InPz|kZaMZ!&)QQUj$mn~l9^y_#UE3g*II8_ ztCdjm_;On6mZzmK_~=WYLr=Vnox6+$NlagJugcIQUiXZF^+X>8@JD|TCq9OFZTrsb zJaFdEJk82J;*~iH;}qc2>5y-*Vl>QCSl5ZrX@SP{CSB}IyqnVVY;#t7vu&bECOf%QJW#hU_` zkulJtI@g`-D66ik0mra{VBC*p+u44k8MSjjqNQBv`}kP3mZXb1jsl_66>?mD$5}* z*-|m0BPK`5|8o-rub^qLaUy57{J?QzD5X$?gBEaA z)R1{nC1T58B9~a&$g@(q;@M(oG-ndzQ}dZ!utmDK0BT%eH+D`72|OKbJts$@FcuNV z!q3Td;rqHj;IRjT65S;l%1hN7Oz<^^$la<)(zE9HyBe`Ee)+mv>!xlhl61(G%@u2<4M zkC&9GQqJq6Xdkjp0gYPd@w2RC##uIz*6Ou=bH~@7;&#b21CEVplE578RYYuARgkN5 z;hwnSa|r(OeW*Y)kJYH)V~bZ!cx8$TVmb=Xq>4^KJpdaFH834I@8C^#Zdga z!^b)rVzu}ft5hn9Z8BS>*uzW)`d{Gj%fDm#i*RfpD#l2(yHY_n_A`Dn{V_giP(%OE zv>x?Io)Veh+7(Jc(hxk*KI5N0`SEGm_vIx3lX?9PO* zYu1(tljc8*0E1W+vTOgytyymw9LbsMuzL|DIRnn~Qi6k}twXo(VDY1i;2v{SdjOI(D`dcc(n}=X5?RC(U zG^{561SgL@(f(Txpex}WDpl+kAlN^Lqytkidm@lH`WYOUY-p48l0Uzpn{0iib;nI+ zbbM?3sOBb7{MSGjEQ3poml)7clDd8Sh3#S;($iAUZf}umbfoYBo|ewqg~(%J(hT4m zakS7Gj&#`w-0Ob<*_X-AJg>)9OY?L(iz}X1uG7&RVPG1(aivTlx!M``&E~5U2egD5=HTB`C2z<@Y-%IUN`&KOsqM#7C z_>A-q;4#-Q#$LxfYP{#S?L8AX3fm)rrU{ZXN1rftR2h^xeL57Vd@4Z1gJ+?6fV95% znH+De0*jmq%<~d|btE|*5WSWi{BqO!?*2)qb@dDRx?1m|ik480x3G&4t+=JmlX179 zoFK;EkMx=-aP#nC38G>7V|<56H0jg!OVm$Kwdw!07NDQ9Qa-m|%b z5tZ(P8&a#tRA#Y`Lnhlm;%N8`lir8X1RL_itUm_@Sw+i276jQ-IV z^4H*?h6wU_KXAyK0(Gk2qf?NZPXuln%S83HSK(VCK$VN$npLVMQ0h3cQJ*b_f4g)J z{y8)i@yxu*I4EG8S>yzX-o`7i35s&P9L&&MRjOvHoPOe&UmQgn0vm(Kex|HD+uWPy z$(bX_%?J71h#nDyE|vYGwVjkfX5pe(_HUbt6h$`UJ=|=R+A}|026vb^c1BgJtZ?e* zVA2k~X1(12PpG5GnS2SHuH-0XY5yW(jAUg1+QNXhPW40KH@^XSglMpeW+aiU6+sLiCg@$ zcJD#8Kog0!nAkHFIBqF-1@ZvPwTg7*OBlY}mhJ9M;cP2mSelGu2z)^UwdN%Txvach zJ7;Hmrn+`(PUFx={`GhNvsXS!YMZ(VD6}6Mf8F;$7AMOZ^)yh_ekCAicZk84sFx{$L%JhR z07Dx0=wZVQTE{>y5Mse-0dHb@83yC55dJTc*Q6!*cj)c5q`gBzIVo+5sT7{m<{n$hbBylOk^ zR`GFGn6)Kg<4ISc=8Xb$s?LM5#0nl$?DBPIKB|5BCh&lYy*0uMQPj(wz-1fI z<&*xRMmQ5w3+%zxEX2tEPDhVt2r1Tu?)zt(vJI5*Q!?7B+E$ zj^@FqzWyRA=fzxm(rXa4f2HW!4+q7K!Ch`Ycp35H9w+hH1#*%!@Y)d@lE;q0{PDr| z=@jfKpnq!4p|V8(`ok3K}+IMT?oxzc$wwaGa0rIl>w!#kSmrR5Qt zP04!(Ts|)3)5bIz^Gw7Fg$6Mf=5egyxTk5KkdxRcg#GBH}Ztu2y zB533s*st7My%q2`e^^%{j8W7mCX{yOAm;9Yps`25nO<$^pdhsF%f3WY2yD0u92Re0 zNOb$RMs-dZtKI$}hpYr)x%U-Z6KveDq49Bjd!lucsV-S&`I+Aj1iP!QubW}WfC*MA~*hNCG=8hEF zKnou_J1W5sCaaN@3eJZL{orJ!(Mavjdn^v=&UNTjt!=GfM5fuW?ufA-=ve4`x7;Gr zqPTE4HwqI1^uCSanz zwJ6Ml*n}+z(!n@tpgaETr4f9#{NSttWU-{7ztVG9l8E*F!Q&huu2~BT)VDz;{pxQM z!7l;5rYF6{1b}6$FuV2jJJUK^s-zFWH;T_$y1XZ^@BHDqJ815e$>>P~Z3@!2Fd)&@ zIopj^UvVYx@9xBPsk(^T?_OMFDbd;g?(DBO;#~20J&^P+t@osb(<%H#gh5TWktlSg`F12a5S^UG0depx*w>#CuU;oqb6+&FxEg2Ti0K-`*`9?d3o28QGFI;J#{I zlvQaFRti5c-Z{2ZY213ZP6s zxiPlv;8GgGYg`mA!|B&?49k?p@r_O=w-O5NUE1|Uy|aNC~-sQ)HAxb3+Lnw!yK^hQ||L;_lciE(f%6^`>)dj zj^^{*=(Qo>KuDb9jfW+swL)7VQg=(vQbsUl8J_8V-J`kdNlFy%MO9HGf=l?W@<{xC z>Pd@#qoOfZg1FF+8((tW7!>~04qF`JTm5P_`cay`Md`#Nw^isp~CX8c6-?pW=_36AYqnQ^TlJKTLL?5>=HuI=>v7aGL7E&IE8 zOhvuT`mLvM_(99(&t-Kov!iwnay~v`@q`049%M}5dUJAO{vWp9JCN%5`ya2YWEG+4 zQZg#8k$vruWGCB2D0{@c_6XUb5TR>aLiQe4$WFq&_PQkNx|gf$YyMvE_xJPr{PE$x z`=8f+dEWCp=W!lG+-k4X;+Z~`$5ViQV(esmM+6jauqd-A5`d(S&EwI4zeZq z2hyazO}FmQLppg0tA^FaSB<@o)FR*NQlB!04j(;%c?Z6$3-FE^ke_7bJ{*mmb+=od zXOwJkObZepS6Z140~ki9fP65=@9drYLCpIRU~G9gpgESl9+kR!cHCO^(1}6kH ziy+76PB|o9XH`V@(y%@3sEM}gJFBk-LU|gS?_FQPf06YFuAmZm_gO1B-ivl) z#p5z0i>3L#XkNeh1$mft1o3k8;OLhlf#-W{ol*kQsYb;<97t9D&sQ9=mLy!@scS}m_98r?GCm)b*?l9xd797%j*HT2m!Z960|hH z+>`Avsx}VgfnSO6u-~r;1g4Rb0`U;3hWS{Mk-!~7yGtQxCOimww+hzK_1y%8e zydiNAMol}D@%6S{k4WOpy5MDOZV`L-V2Zk6TlfD#+5n@qiO>e?S%Y;CoVP7p*$!> z`R3|^$gqdy=&r-Gcq>5la)`5`6cs_Anq!u|f4t>&_3oX#4bt~#8YN^qZ}cSkO}KU^jU zb(*W%t>LTfmeAF8m+ad%7U%#26$<66o?&Qch{S0fkei20ieFU1?P8F_o&G{SCWr3e z;ex=i80{=s_7|^k))8>+O4sz-kiX$~#g?mDA!w7b)@m84r+t8|SR#s0Q&ysr^)|OD z>wT4ATQHM2i5VpE+olA}y@Y!+aZ8p;S&~kM@4QP?BH+}0V6D7f4->g9ch zColqo8lO#@Xupt{7AmL1@dXsq#m{;z8U@pPY!XbREF5w6BuveyV;fjJk8$oz z)nZ@e+hxZ#t1=ox2RiQ9glY%V!?NlfPC~tZW00`{0jIlPI?}g|B<_6odbr*#W^YgC zp~R~6=|{f*Vs5IPebOQBh&Lj9$4@V7HlOrCBk8WVC8ob?)2h+KG)piJZCvhf0&)iA z|7!|am+M~O98rK}c3-eYk+w2V1bzSR?@;2J?)exAfIqcfgDHfp?Z?q+)-yu|w9>b! zR#U|`2Yhd!j8n`ot2#!u!ETL$OodG_J4zJ|6nWFf#s2FA3p;t^SXYLd$x-tr{9shG zJdE+GQo#f42|=4YDPb$x3Q(c26~lM*M+#Dnem_jNr*1cI^xE60YV#tq>A5oO;`9;e z>3x7S-UGLX2#(%F@zmg~@b3D8L>~K^#p!2R^4HI)xyWVhzun9VUxDMU%qo+_TT930 z34#?*R=o3a*noz-nxV|;NdAZdB#O5v3rLj;_cH$w0@klmO^`XJa{>l(JE|v|DG3!{ zNAw`NNrw&_lIvFOLCGw}3XNLiN$HVGFMVv_FC^%=A+7cUj(nSk2f{CtcYqj;;JHE^ zEL*%p5o(#_m2-3-4T<)sq`+$3KW%;M`QEN#&0SkhKLn|Eu&Kv_KNos)Nb1~M}p zF};6<0v`a*Zk8-!f#28^M^-P*v3mrPBZ>&fYIjAFLn)w9Wd*Cda%@YzF-eNHI=G*> ztmC=#jh`igbUNL%H}|CaQ1GWK0Fes_fuDZliPV*doIYZdm&EiZwgqmC!(xkx?XQgqQs1| z{5uCDB7i-K5)tsP5INj_pty3kN}SP#)vmQD42a|9Gb_J@ok~87e7;I=N?|QK9l~5x za5WNEcdE@_;xR_jBzcv5*+Zk-#{XqL%{LV~P>L|pw+F@%+J42}ay7rq*)+micg4TasErG~@Egmi&KYG!KsCkpC}BRgcUEI@nQEo*NFbg==Zi_( zb3T(n@#(qTuz{K~+*@o=7|}h7XkBrCA0DOe$ssS|As_mSFLZU!R=JuPkvz;Q5kg-f zaOy>?>j^^&nygBe;MR)2$hJp@idTQvet_Z<<_jJDU?a3{-Ivsk-JH|8ib=FI&l)-d z%uTC!j&HT^csgoe=5tvPY8q>6VmSKh0tjX``DH6@*XQh{W91N{A)}$opcxGFy!jT^ z%D1AoIBr1j_Y44mlLNjQy>N#h0A0@~i74)&>R$JttnWuij#mI4bd- zn(5l>jJ7r9i}Z8+6+PXwS=X?9cx8X1#mLE2}uJR{=#nA_qIDwzEcg}?Zn(! z)>iv)AR9YUZ64q#4IH0R9L%cbx(7QXP7{|fi8cxIn$`i&uARmZcfNm)Z_YG;4J$G^ z#`iadC)WikRE^E^qvRGpru+R~E+K6$wA~YqelONPDgu$MlcjcY&>k04sF8=Vh%F4- zQizQ$o%7hkGjO0AF;t55o}-X|fD543|3${GBH=v8s4Xgxlgi{sG$^48x`&IA){|6B zZHlt_GHiACH8Nww#GDkCjtYQgroyW z&l;StqGXq@R=vywF-uF{ER??QmqhOOHCF9!+H&3&J2(qdh7ds6h-H&I3(|L|H)R54 zVC53Tq-PEoKO?gVi)roar+8*d|vq!b&sj)f4EWVnXbTiMc%G znA{}ES`WGWgqzYzM&E1WMjxX2@xuuvvVgl~E@2Wg`9)eu9@0l=Z|Nmr$jJ{W*5Hi4 zL<8S!HJ=aUOmUGc#SwGzN#iT${C`JgYPFcVjo!GeBs9##sd>oONRs1V4WESSFoh6?L%fxi1VpCTWx&sJQ(iyS0?^O>|mP z@M{$d)Ya)BBTy zK@rCL{Jue=+cU&hKt(diH0_s0fq8a}mBE9K>8E#O&5_JxrU+Nys|hib<(72CbV)hd zz?oAp@t5s>Y&`sFitQRKn#ntPyY%Whg8K2AL1MYLvmz7tCen$m*v=x8Bb=i0XuRk; zj3UYT$xx5p^@P3!$GKddd~J@~2UmZLcAPx%bSiWRI{w1&$ddE(N8IIIjf1x2WY`2tHHwsR;mqrg@Hb!m@u_<}d zYDUCX*6~B46~B1}`Dw~$RX~4;iSwj2jlVSPqcir^|LDi*wZ6*T@hLg*1P%k1%sG3tq*(%NDkf(IGTXnP| z-sgr%l+5O5cqU#igugoXG0-Ku*ajW1r_2qZ`NgS#sgqM=1yDEZ@yP{Yn59YTInQbA zIlj|wFnr|0hn{cgmdPBuKjvk0)E*@<^`^eKv5gB<#tK|qc5-KkoYEKxz;4wQ&2sDq8tR?Y~-%otI#H99ZK{}cNhTFR;!~wDkoI)nuv4|z4UV^q= zdx&f`4YpV+6%&q>hCVVU46i)GC!zd$`-aS568t5lI%){pK%(3X&^kZ%qlO?h=_ZxG z#Ets((gN65LWE*Lb>H?u+#&(qI^=%cIG>aYH?uR@AQnMLth9*n0wb7l#yM~HiRbv} z!odawR)!8R_6P+HX51h1|FE2J6m;F#3C9f>iz%*Tq-@{aqiIpJ3+&+lPfkw*QAzJS z_Gd~cq1g7kuBDEf;}}b5kmiBt=(KJV+k8c%wZt`n^dd~DMaE*UaGjeP6^1vvP?ycZc(`wzX3kw6@ZySV%^Q4VFl( zm+KUw!`9soq)>|N_*V9S0Y10!J@u2nTwbMaK@giv99;|_Nr-ERal-nxF@km%GJF(R zV@r*yuJ-bE7caP&1WFJlJt$lrkqu5LOS6=F2J_RivHE0*xCr@vWd!g9-%mlBC7jd1 z0LON}-Qef2xN%jKc7gFmBLvEuj2G611%1r2$4|bpV(_|51u@v;h4pp~zI?2KKlmcN z#|97pUkMwlN;+YnBhsTC+d)4?J<6-)vFSxT=&JY2YIgL@Sj99HV8+5de560PnIJHj zaQAbkfbf>YW}lxqX0`M_YC@xA+%&RO9WWRqrwa5X1_Clvs-#pER{`tYe zo@rLjdcx!rP0V6OKNs^GH6yoxHZB9OIzdHA0lCqn`>Q%j=#j2?Xl%+8e$Tu~&&(Zy zuHBI?l{ssuNLgS0W4`1GrCaz4v<{*;OmD@OM&^dJQa#!@?w(FhWx#2CSpzPY2eMJ3 z#@bP`K4cQ&&yt@2fr*=Poko;cJ-}NFRRX};-#hZ#VyERL>Ss|_m%>JZ|9p%>=%=Q@ z!#WBHtG0L-xQ7OEi8NfSq%>$`fw5vFCBn9_-VDa#i00p@mygKJ$We|Q z7jc5@OjZaVdPZHXTZ+@l${g)#t9rc9 z`H-QgQ*yR?GK!0FdAq0N7*t>(F2KK{fj@N;tv+}VN!UB57C8L(0(hm$H_xa%&V#1| zrBkRuJ^#MQW`YkTB5;2np?lRhaa8QUmR+1nC_70}GqOz3`X~7*flWeb453Cp`vOSU z;SH_LVX;+!OR|;Bq6v`THKbB~g{+k1JriHQ>+d(xA-+C%_)}+#q|{Ckl9GW@xgZ5K=}3S}9`)7p2h?!#O(m?eQwyf)1WY{Tm zUGfoDSycq~{2A#Pju?dMal>4QZ~^Z6Oa?|P#Qq5b3}aAww@tu`Q6#w|h8`%~eOBs9 zdu3~HERN49O!l7Rd+oIWz(@f;4dk^L>2`JAS4D>9mAG7VHGi*Y2cC@ACgY6}EE*G6 z1%+?a(lx5urI11Oj95if-SeVTe%O{vn0i>OxPD3Y4byhZ5z!a_Sa??mVo7tI0D6c+n0znS8GhXr^g> z@{4{L2a4jgM(q{}@x+_AYjX1p#p zD&9Z0c7@|pF`Ufg#_x_!;Vq;G%}1P~FxX3~lCE`WmeWLxtSNJCZm+8iK;1u@0T$QR zT%W2uE#Y5A3&#owKST^l=Wl@j?b)lV^5MDrPld)7&S`G{JG=z1Y8+r&3VcsI$Pwiy z*a*wETlE1$IZ5Wh)w}gRK%x}Q%IiE5umrw-wFWWz!fy>UT^-)lhP{WMzru3|_eA!A zh%!A{b>6X)etJ`=LNq<0&NX}TscbnRBcBdZ$}L3)QkD|OB^K_@c>CT{Ah-v{U!5iOhL9VNx9QrE!IXjNW3 zl}z&Hpb|GD{-kFHOuO9_x^-L#m-N^&y>S)8X!V9zgXEv9POr9)wyY#$NNxCPGjgnP z8D>{0XB@^8C95!HGG-5{XE?5FmWFolD8pJ&yyu*ToaHh?I3f66YcO5#)~tW{=j>s7 zbq-ad0>h6QJPJBaB^*?cC{(E;Yjsg-4ins32t(Ta-SIhy8IxF|lhD;B5{FuiBzm7p zv@vQF)#H|9n?h|K8vkPM%ft)NHdGAYvlBb223e3iuEgIL00(}krdFdf{PY=6V@$Iw zP89wzyB}|!kjFlNsBbx0)&c|OJqYNfOWIX82GjI8^jGPu%Z3<`l1%aXAMv|sr>B@zPFgGr8%zd=c^LxGux#-j zp6;&*kuO&(l~%cmd)N8cgMXoRAqSAkI!xjnly9leV6FN((Q5IQ&<0q+9Tp)pCPV=N zu!cyn6%3>AFV#Q@{)zCdDXqsVbsO~YfY;$6|Lu{|j&la`{6;6=`GY}btWjS%=ZP*~ zsN7GG=G0D!>iFLrOS@=o!R&BNciVA8J#n1C?B>j}(bvY(-}#Gx_7|%)lU?M0n5x0w0058F0m4CL)x2aa?LRQsP0B(EAXg1&UO+mhcs_$JQpg}7-|)_fuh^vH(|#?c=YGV)Oa#}@aN@=v5$}xwmjn-oLIh_NpZnW*y0Dd@E7W)Vk!PS40p4H<$A@vICHKm&K z$W35i?i3~IEoIY9SISHEYBxk21?(vmN`E8Svx}XBnBQKYq>`j`6;`0z5XzIhyfdU$Ar9{>i8AnaW$kjZ7d% z-qXC-is-=&)p_|vP4152Y<={51D#kBrsj{k0MQLPynF$$~ z1alJ#?QZQYXPiK0+1AXuf!LLD#;jt ztUW8z!d6H*{iAi0yWu%WiW*_AX65f)Ev6D_;gMy9Ozu-*Yo3*hfc*k0HwR}eXiTZ` za2Z3p%{H)saz;11R*~Rb6MP(xFQ!}d$~I2*e`N2p%P!^*I>xLr4ByicN_CM!$fQ?0 zsgTRFu)iXX*qJzOwo+Q}G9r=>4>zqew)jyB?X2f^2ct7Pz9$W`6@PJR0al@gJ)u-_6ofIJ&Lzut23mA2*V)th;J!|~+NYMcE4M=vJ@5oMK+Xmr<9gAWm7X9r6AZpBclbW8!qo8j1!fcx#~IH2!D#&V6L3}DgDE< z2J5*cI%EizYsm{ZF7rRQ%;nABX8Skh!gI}%&hnq8H9g0V+nG-dMoenBhmOj*Is*RS zVZUmq=y;>)4}EfWRv{X{y6!{t#&fERwn4$gIC^K^6c%6qkh*gI%~z|6d|_>u2Cc5v z2AGR(T6fNpBgRQL!6+TaZEpO9fT&tCW|*P@;LgGI3HDUY_h8k5ekbP9iI3B=^MXfq z(jB`MGMIe#6q5I(Py-!J&j^k_fH~)wa2!?Unq{2xw9h?(t!1lKc?BI7d|$WcuE_yE z?l0aTFsCea&|+7ujy+zx9rW;|_^2F*R0%Z0bX8f+-CmRlTlba=9pIvWmbi|4j=hTI zVX@&b@ye_&IR>1<*o&*>n>ooPj5*#N6}XGs^V#z+W1SahHRs~)TOKK+ zm)3W`S|GlV)KZR&2JHDl)f@5z8BUDRfmBZa&W>OiE<`_zhDVsd=A(o12yNa_IU_hq z&bS{m)x5{@f^$_siA=QsfstrDNTY9a#kefc;cjo3rgNJ7!2+fceD(T)F9^-AUwjOzw1b~=<8Y;oR$0*GPi>p%jS9Ay?d(9V-jPi6XsV{eu)+^p$e z0$7M@vx6N5ir*zPlw)}ZH!zH#JZ9WB$~)R!3v1_8TUq^{3Qzfk1#xT|Gw@*4(wlBn zo{sjCt1AzM;<%BE48qYDB{!w5hcR$c!!hk&cCVC@Vk^Fz7R*S2|Mx!W#cb|pmi+N% zWRkFcb{|L+4Y0gb&kCjQ_CsyH{xwTksn7Y5veHBI?-_csQ%L7g->4w&p|h|${8{$N ztyivn7K5!-?y*~t3LTl6pWAY=sn20mZ^k>Q3Z(stT{q%J{7cjwy{OE7+>OxbIdUl1 zDKDM4@yQiSs#c8 z**@C!RgDzA$c+>0tSXPe3=(3SsM#i@x4-U;C##yEzs_=h#Z!RU$=8=u3UH#N3BoXkf z#<72totVdoQOmiGD)l4UrUSFsA<;G8t|3xrqomn?3g7}{UT1Oc1!t2z$E=3VdBKG( zY$j*}U+8*b-9@jJI8?b(hV+YaF}bB8WvGgzHG1uAV6IR{G_yPnkR6pkpI_i{M4&WWrY&?Z#NN}-Q+_>a$40d`lSsYSz2Qa_T2RkXufkVB?91g1mOLg<-P za&}erXnb4U60~i=5oLwd#S$t7h74@5TsFb4c*mc!?8RWpP)aP}OX+kf9cug-hd=tF z5r!dJ;B$GtmuPIQkZ;5g^ORHF)BsYrN!*(Z4OjutZu%jm6#7PTR}Z);V>T#I>*gE+ zxSzorIoXHGN1HKfXOC+~Ka@aYl#pWFKz>v3A6PSryCyy3dtXoU=G4AywOH$9^&i+L zX@>$=G-#^JvzRKHz6Y>Ub?xARCT+_qv&&x>c*N21Z?V8!Z9|t-TkvPr++unUbH?| z6;_2O86rE*!wi?xn&SVnmq*X*sDgq*r=MjLr^76bk@6N_8PJGGX+XR)=+AqRsRs9R zTn5raC+dxx;WEFZ9JxRV1?#%?JhS8?4BLE$^AaO>R;vQ30bh!eIpLeXVun|u!GEC> z*M(?4UWV*n-SAZLJxD9N+I<1>4P*d2aPaD7gN0$+{%De^Len6ChbMp9CysIKo)LZC6tL;~N)a z57YFe)CF9LBlIctl-^inrt#xY)ek<-m%O~LZSHfk-rr$r`v!sXlX2Z1Dl^CPhN%{N zyUiVl2>wUfp4WKfI>t zSDf`K5ONFD5_(E=>hm${>`@r*hxX7I$ys&+36WWsQPcegzb7~Xtn@C7OgU%8!HZaD zUkef~i>6ARUy!iUM65ok2>Zme+!vM*h&Nt;(Nc)3l9$8y)&;$$8di<-8&MyH>WkM1 zH2(*L1!*KbKyAeca=6F4j?67g@gi#4%>=C3dY0!^jIwujRx6IR`J(j3sHSr0Eq|Kx zg=cw3HexK_9k-6Jz6V$pv@p!u)x&UzH?9L#Z_=6d8<*5 zTKe2)RSfF%tI~L(GN5La^=Wxu%qhS?{QGPAf`&F_pUC*u5+;$R#w18n#0AN-S&A@# znW$N$3OK0;XY_9jA4LzS1xcemGo>6N(wEw(TKXNQF5<5EiB zR3mgBnhqg{qja-3Mdq0i?=Hz`B}X(Mi!qGFI>FhsH#g>+LS4K3&vNvK`{(ifs_Ctt zLy3VKB`J6G0iTiS`)k&GWnQQFEBVylo=%q+P6RCJmt;9+zx1E(G45#g-Kr@Ph3Q?i zIuhs?Qs`5mGBdLsZsyv4y}Hcxc+-^WG(s?y7y2A#%7!+i+&&)h{1FUK7B{!hBxBl4 zSxImhBO&5?3YS0Yw19vG5mLcR7_6K+BzEZ|S%+AE@S8%N$dLjDBoC6<{ykiz19-qDy~2kn8x^=c z0)11Ii%{^!XPbRY&kH1%vBK2|epiro9MR(^duududE~o>QI>0Nr{E>5fDzT=q_3?>xKg2ltF_Mo1$>vw zPQnSS+bzd{!{K#cffAu1M$0%I#t!@RgAlx4Qs-r_9LWJn=i1IU{H>B70+K>0+pgEn zqSfN@z^c_dU7djS`L<$}DzMWJE$eq4=Gax}FIUVMbDPAOf{8&d+ zJotbThIz-KHCp{Gsg|N?@>`y0$z8{1-oiU^qu9;g)AhCT$9uuSbeU%>RBr!#O#gAx zCL#66f)#cT_qTG1^zTVge{D6&^#xlkA#CSiH@TydkwvS*_yO#GMoR3ZY$fmK7=&$g zZ9kQa)MYrEX0N;Et8TqiVFyZugFH;BZn{QLtI5RmWv*`z-O&BbWpeWGcUCE7Tc-L zmGejpP9K_hYWY#9g4KzWvEcNB;Y!0r%EU@Dl>(0`3eEdsS4NV?gq!EI$om|u>@oeY z_wX7+Gq>8Hqy<=_!BRX?%r2u*y3u&703^P@aO;hn;)-X9ORaFqXn~uJ#wcmiN@>qx zcKd0=gu;#(E38{jmkP!a2 zS?ej_Cj;bL(3D;RJYs5902li%8)p`Ibr@qp<@a`9!&o(<{P&TsR%rzr62hbcHWa>8 z=V0%O&0%7>$Im$Rm@dL_AYIaPw7*mB;SDpW?@nU)1s0>9*f!}MfvF0$2yLo&(vZQe zgg>DqO>qGB`{gqKb*#Y57nO5(k?wt=cI-oA~4y0{dg!|`_HXO zW0$N?xaGpmWnd{wl;=mqI+El1C!g7bG2w(c#$Q{|6a+-8YHU*uil`LeFG0g4*;IW1 ze%B#=zO`!~ZzxHzqSoi^&W7EJSY+97!WlyJzKVMeA}a4uDRjiLZdRH5mA2nvlq%#YJ9)*N)Ju~4fvKdv4(Le`^A2wKh@Q` zk!0U$?pqB48zA6>yw`DK>oTg@jrPV2viZ3q>E)*K;e$`htl4NMo%CZKH zi}J-Hcp>&sm63whc9w^xp|yTzPe7a`a7I}H^Cs`vuU)GV+FpmY4YsUOF~@Db@Joo~ z^1l3e3K-<)f$nH;fr;&R!Z1T$90-U#)RCfxe^{xEX$XcgwLE zp(`{mSO4YDzT0sayo7yz_-zM7I^pWGj2)eOGr&TnqXjK-|Ct^0rg()mC|2Z+7XLwW zeCyGzyum;x8pEuUPIe&0;cxIluo?C&M$kmPT(%!hXk~=8QUox!I_>>iNDwvW5^CpM z_{v)7wt?&0zPtGQL8CBV#U*&)&C_Qit5++L1F7Vbn^tu1Zv83Ty=n$5tD2=^fv;G$ z{8&QCiE*oeD<$M|+K7ke9Ot(R`36@uqSI6)SxK{Cv$D6z@L@%vdV0p#iQ|_%mr>my z368aqr<~s`pd(!eUpy1)$x)u!yH?(q#CP92=j!3tCd2_1C}~yJH*^B@ZsU*6iBD}$ z!I^+WEn)8^A@g9SXdnpu+2@n&ZTi#PIEa7ZlyLX#eBR_rFVjVgP5#)`7kcYB7j&$5Ex?8C04sZF?_$MQQdH; z2Uq^D!KJ2dN8{wa_^fqqY3`NgfI5pr{e&8yet*yh?DcKVsfTuIdyCO(8kazZ2WNMo zu5TL&KJy2h6rfoEHwJefQ!#A6++R>hoo8HI<^BDk_5EP)*k#xt*!WUH>Kctqafy74 z$mlgTsjoO$ZEFgA4CE3m>eT)sv|ho71NU6lOOJ1WXiRDl?q_BH)+LD zUrBTv+|;P_OBk5$2WUbX<09fas~Tt%jDK>YB*pR4DVN~z5eK*pdfgLv7K@~_XL#Dm z)99dmCkuv~r3kW0`a+xows3+UWP?9&rv=^Dotv)q{rT^aCH9unD(W4%zbme!+!Vv? zacB`jv?(f81Qpx_-+wFG#!2z_ZWc0pf@8+uy-kI}1p3ZJ2Y>jmF39Pv$i(%}8&G<- zdZFojN6SvJVjN8HQ=&{Hm8NXJTj|&kd)zilTcUc_vVxkZ;;&yfzZ*aKymYBH`&1Xx z3@rGD#Q&-ghF_1Pk6lEljtC!v&X$dSN$;+k^J<5ut_blHoJ;N8hc#*Yg9<``CH|AS7^mGkE8g(2ZZf-iRLHeESR? zjKf)U(dTdKKZJQ-q)90`IHv(_kv0c1){K4yGFcv`FOvR+A?f%2>O#D}pv`HedGrdF zFJV8RI-=wL7U8b~V-0TC=AV1~SBdgVy01(U{9y8+V&#H)#_UPr^csUJTYl=rqP7oS zq44WPY7XDOjMAC|TReatML#;g=E>koUif<0suXvGxb;$Q71Ooow2{L>w@0UzwBwMX z>XR0npgn>$fbF+rAMP-7D4$s}Np{SPwzwYyY(e8qEr4px^8gje;8Hfe^}GOQ@jwQw zIv%b*buI7~t|G1geQC$IZP6jca5l4odVJHFRAW z@Id>nO)`-}@x4jx*gJO>GCO0?;{I`S8!!kEgB8&?tl-Q+7SZAcMzfeGYWfj`s-hok z>@A1_e-*adSXiX(SffJiC#+~zwB&X?r6y)wSE10*Hlms5+WoBf)-O!B0rFXMKT$~N z`QX^vtB0ScT%2#;n6f>72>bEtw1MOqDR@{$$oxGBX@MGJUf9{GrB5&+DrsP?4L{FL z5&zRA3=;N?(DQ0B+D|`|VKt;sM(EL}La?qx?n4(3c=qs{XqcfJKdQaV%dkExqv#4h zH|lu-Sx3z<1WBoPVRoINr6!=&C{vPRMSODOVnSUGs%4mo-IXbG4JJ`fd87Wru!!`aOe`Z@RTK~V0|Ki2bV zm&EL?vH>71>6Ey^3(1NI&>5MJev&kRHv;m+o5f)5#M?X|fFCw-Sm@Nyi2*GZaNJ21 zMr3jU>vi$FZ0^r6%%2)d?Z>i}n-m(lhE?QQ!nkpp$M9f%@bh5hNVs-&8P58{Gg|`R z?r*f^18@R>=Gay)G?~%XX7w7>HSF+P?O^l9f zn@vgGky0G1BG6Vw7?e;O!g6g_xOF_YQ*39TAi+b5b__oqCoz2D!i>%(v#IE2-9nD8 zb<>of=pf-w@3%s%a2UXjaG+XKu5YYg`=r-k`7_})meqN=o7DGf2Uj7a00csG0Cu7= z^o#IXd6|xMBwrT4 zWH_EwtEc7u;ch8`GJlE#OG`-;#*`hMV*Alo;A$Cf^*S3f^w>CzDDdbhIu;s4E5q10 z^xHs{&t=r|qkXT?f@k8gW$)S7-E?iaY!{$Wy;0b)r@n=Sa=t5}z)x3tQAYshxDa=B zo^iFfu5w%l$laS(hfDWOMLv{4m3&K~VRQRH)?sa_hmA-k9h=`a7d=^@^X;^;s3!eo zu$s_q76zz)mr>DU)vw?cJ8COd6@cM_2b_$GX_^0xQ_W(D;YHiWb9)q2sMlkKSxZ5@ z{BJ5=y$LNdH{#bF|NUIat}U_rkHRC&@g>8(vz^0#etkbbbExjI?gKID?l7OXY6$|b z6XXnx(A*H829;A*?}>4xv6k{)%)A3#Dz0c=Wkosdb-Gh%|I9u7Fgt8F_uGvsvnTtK z#c2-M^sKc^J64QTc5YuKCTIzV#S=i-UBY(jBK`aO`KD>=igWQ*z z(}$<$kIS|)7xO`A($|P6$nPATuMdoDs6yyLC`gUTZ2b!(ac2+fTLy@g0d@~c8c!r8GpzGa%lE4)ztK9^j0FeHEj z94*Y)iHOMgssAR&A730shGjK8I_G~s_um^g@smC*+)QN8*wRlZyV|%%58(t}dw@#) zsf|g4G~EDz@efwdI)Z!_@!M5BHTWloMld$Ho6Zco?^TgbESDJ=PuSEOOPazf-;dV2 z;^pTQHmVNo-DpQYR}6<(8Q-Agr5m$H&&wVb-_^0~Up01Om9)s5r513ETLW!X5i zU!CvhaG#^)A02f{z>AmX4GbdQU*yUXmssJPK~XFqvB7fxm(zQa!C18aRc(K5mD{T)u~ugFqCfW8>35jH zmee9*=uZ=J4(E*O&#I6m=4RmE+>bAqE(Vx|f|k}jWkYiOQ^?wWAt*FeOh~7VZr8>p zqRXxaIpf?pb#yy;qP6D_47MFcU%8TVUSEE})X9Qz;Vx0Kq zR!Qt5x0diKvwFk?UZo1tf+xa%U9MiWVtmk>9i20bZM&O|?Blv1YKI$fjuqS!MeO3d za+aOBr)|tYCAc?pk5nVi>t*RA{xggHyBqMgEmFeZ0Y>hAFiDescTE)Bzu|7dRU!lT6+m-WKz*zMeh&}#0G=5Sluq`G11BrJi}b1 z_M>x;f`P}*v+lHG>^a?6u}}Re;>Nl3SDgArY0A!>XH9?E+`B71S6M2-D8gVM7h5E<6QpMbcA{|NH82avw2sqwvBMm_0G*wy3& z!iFZoA<&!7_`G6Mi{^!sXe$09T64~qFrURZ2o&92{|US7`MxmFi7 z&|-ZJ(fxGI0SOpjvVgAvY|5f@x-rpT&==jc$PTD+MINvtp|0E3_KR2eC0(4TV#g*DF9ro`nHA0bqwx6U+|&D%$+;)}nXQWcwlxnfKz>>ts}9FWRKgk<L@YC%%T`eTnrDOq}|D>+H}izOv7u9R~N)0&9xB|Fw4yU*lWhQYq=oQQrl zc<;InYqYD>4d?WoRg4uj6^Myo@s7A6&P9rOC*oH{WzBam+qCt9|`rVG_iCP2vS3Au+`&nme5g zpC2_^7-M^Y~qxVoiJ7X30!SfCJ1b^f5^3Lk_eVgFg>TMaIBCMP8$7Lt(a(?A|l zHZv8-{;Ii@tdoA^7BDANO^0aDcRjDrrCNVdhFi4zoVLqD2)pYQ>Uxt&aD~h??CH*n zb_E6S)(LAGF<}!HO#Un4coYY0n?0uB9O86)S35C;LDI2TphPs`ReiA8eHC^gS56J+>gNZk``~B z)XAxX-|Oox_IBc8oqzBpHpb<)HFVyj6aCZ1D4D~Y{XerYWiQ5GR7=Hd=%B>lxT=F| zDmUZniM$p<$@HC8!hM?ymyWF}R)4!73=jr%Jt5jL8_*eY61@7!*0_oFE5XMFAi!`f z*PoO|4iUpJlB1X{OwLkJ-G~nW;8q?2XeVYe9@)DIjgS`aGbH)=qFUViQ)@_Urtv|R zBRmi(l*{kig^rmA0%iH>GR345l}N#B^0D7lG;3U&vRP?HshHOSH9HsXNNm%Dy$!Cm z@p0GCE3i1J?zL)g_v$Anw6yvSsc6)&wIbQ?#9nwf_0SVms9e$WZ$c%E-k%vN?#*ACyyG9=zD}eMq}cQA=6NN8@e}q&#iiHKB>&kucz5#W;5VDr zS8+nB9$9d-Yo2(zTsP~aF`d5(?08iEU*8Y)cur>Qz1R!S)_;wv>U)vC;I{u2EXO}b z$bt%`eFg6=X5RSELfkNuh%^IhsrUFxI%8=hk4s z+b3~lCAmU=zSUUiJyR*cO4xuktZlogI>d@aB1Grtna5qQHdXwG*iY@Bs}a(W&%qLq zK#3WIYQ3SXb`+&%7x?R%SfgUK z^n}43JO2{z=xDNAJQkm&h zCz^P`{*A8a<(7S5ndJDKlb0LgI{3%vMC?x7TD=?)hCXY$dhW>JUW=!NMkriDoG7j6 zW<{Z%U^q!qFZK8U`*l;ZLby|fin`dK8_rg6cOX7n174A41urf`#zv{L3x}k|V%y-9 zMhYE}6msu7@xC%QY5VGgUbJv8a2}?!{C|agcT^K!(6)%6C`CX4sTPV#Q|X~g6Od38 z2ps~_i$Lg2x`=>)QW82!mm*RUI+0FUp7ZS=$)1xvyEn5l zb7$_IXP$hkgU?W_G)^qW5LU^m!G>4117qJ5`&`GjpL4&xy%s?%kYO36<5Su_6+eFh zT0a|fHE4HUkkrf=`-hPNtD#kd^QikoYR7OLcDqUGYTKxSo(GY*AC@;!CR^kRyIQ%N z5ubr>g5+MTIGKwxNr3_7S<&jtj1XWM+NaF5GPLlPic7~cWcvP$&&z7hL~nNq-xZpj zXD@bj1s&bH?`<>Ly1Ru-E`96ec&-u<(58N+ zBk$2D`*~pTqTms%c`ZOGhwG2Acr$A$gzfP;f09Hcbw^BJMZsXtTgzr)J-16g08|!X z{lRJO%Q#Zi?^7XpKP+bGs7hD7j8b5CwMQkTH9WMd0vAS_BGj#`9@Muh9Xd@H0s^jk z=zZ;G2-iWVw#CkPngGj!1XIbk;&r+3K4>Mi; zPZ9t~8zpXad@my#J^q5a`0)3<0pUWeUq%G~Sc?}lr-R7|7?{p9ZI0{`xh}km1|4up z`C7ff(})ms0Ft6wkK(IIamhv(UT3HF%!J?tSL14rmDP(5d?x+EnkAEk4R^5n&{4DW`7IBw#KNeqAsNb5$WS&{&6#vq z;x0aRrJ}L!8CWnVD>NHzb_rI@`UiA{SfqniK7JYxHU8jKS5;Lrzqkg_-h6ZUqEkQ* z;{doVfYpf6n!cgc78Tl?!3Y)`sO;vEvo`5jP%o$*;^z;|cCIIv`g-v!>Ug>5okGb$ zEwJenP){Z?@Rg3pkBa`fy&5n?OMD)qPRmCD^R2OeZ?6lix0}-1P&e)_Y?<5obReZP9=xmrw3N{RW*7e8>S+ku-*W2p#&Kq z3@%IE0|?!R&yVdjw-i7{r0K>2nGp*#v$d9$sD@ij!n=320o42sDHF=c=-E7;NHXQ8 z*&A>H09hX$4E^%Fm<= z+uA%q%`)3Sr)+|&@XVh0{-_M~%rg<5d>)c`W~YQeakOt^%DR4{fHUxl-L#SdwdbD% zRiU--MSwZ#d~0YN4wvfRn-)lq?YCtRcB8+AUk-L=v>=ABIF;i2v(ZMyC%+pJizD?& z7r63}xbQ#_>rJVm|7GU!4U)u^K1wBhV*STQL1DdHMmbIO*zVwO-i%S;(i4Lcvu^5d ze2?`#wI`iM%p~7b1xQ?I&E+;R%3H*}cA$LwSh9*aOLMQLYRSEfq;{oyKxl0$>7AtL z$D^Umm<*L9Qyv2jNfZPzK?CYJAHjFAQ~Ke?14`Iks)wJebI_GG-d2g|4V;`lK%Jne zfBXkr7+R!qkgAFwl~WqQ-`*(G$#}tH%CtJVGEOE$NqvsS>uJx{kzsaYU9{o|jSbkt zcI#-?wFnI}9DgH7XR;&bb&#B-X18Bfhc)wf?h)y1(ebDM_pu zmPPzLvW_98m=Tdzn|W7;nT3Gj#sCA<)W@m0^2~i`!Fs)XBsb(Vt9Me+90@I-0qbGv z;u}`YEr6X8Xqh)hJRKCemQn*S5q@vj9)?T(ZaLO)^mZM+=eNQfUX_s<=yT$z+PiRo zC7o909L!62J5CuV0KGoyn#H1P73$w)-4%(yJ~Jh(NaN6tXc@YIfKQ5}Q3x05j{CJ- z&N&!@pR7mn?i%)u-{Nx73uGv%0?$H;7c&E|k0l&HmC@NtG?(U}dD+f!H?gL}$PUF6 zBAZ0L%|>jy@|WFz%Txw?XV%cT3=FActcN_k^i5e0KquQe?_joM1I}ZB|Cm#nRX2P0 zwaqzLT}9W@=up%|Wh@)C*29p-x$*VLF_V16!)~wHCc^1={xvg|RuS9v<2UP{J~|?p zD@yp9WdMb_&wWe>0!riVtZD)1sZIGCT%;`@%0So|Ddu8J+pqG`)OD8uL~su>AHSZy z=6^QG1p9ty>z6k3tQe;>i9=~5FXQrPP<4JDn4%f0VQ#SErb~%?}2Ma|g|gdA2M;ZWh+V zp6)2Pb4J!M45>_E4>sysCEk`$%HI`tII<*tB-{N}YvDx^sV)!MF*Dz-rs@V|lQHRM zxd1s+D{Fr66FDF}&EH#BI7}_B%Y8B!LA>&@lG%?8 ze=)r%5$xnrK*U_Y=_fMllPbVZcleZCu3|~{sjVv}vq)?Ji zvA;A_lC=jmozHZ5MxLET+NgU1DBdH04V6&Ng3p?xWIHyJ2U;|O>dhukc0e+}Ya@9o zdP=tbQPep|(o~rpc4n*e`v(ZTDa!%P<>kr|K{V*SA}RhXm);}z^ZmQ=L=Jt}rps3v zTM}H!!US3YGb=uURZKclS2wK=4d4DA9!RkEScmx%7o;#jS;+ ze#2|j!*@p)V@rv8M68cXm9POX2-!$xQn3@DuxqJQQ~mqqMa{K^bsA2Z6h+I*!kY|i zCddrUjX4TFNsP3Py7(el8?ugaz}n5n&ic$E5NK4&(9ew_^ z`e`o6IRs^-5lv^6eI;K*rn$77jR&m-nE2tz4_?q-`ugeZG;8TJXaIv5a(u{%HYxze$CvM?WlPz zGyz`nJzZ_s?o~VxZmMVjr42}#zKWrO%81p?@hEni8gWWT9l+SmQ|4_)KHgeceYNDQ zq4OrR#chT=eCqQ@9p?F$|`wG1YZCrM;cS18nJ!#UbUKerU!*4gYnxV?H6) zp;BxZ%8!RZYE$Y~@iG(Ti@)$gO2CaCxkK1|Q78JHYz07T?TkQUU~yJx*Ek|X7e*`K z3Y*uXWP=-+&!%OviHuh{NCcTORt+Hy6`H0->Pq4j${2aVJ=DVl1(U;d1sH*tpv7Ed zJyBGk~Dfq6X_@xBs%!= zy78A?2GT16#8&l@+(chm9G8F2cEVg>VD>E*?;WpA`8#&KKPXEq#KxWw5)P`v zYH`dHy(G)nF4FuQg`G5&e#PDxWgfk2Gd1rLm|H;caCRCnPC^_f-}yGrP>gnVt0SK=Xp z-5enSH(Po&4x=xpM~S$%QXO+!hb|qzQ}oKs_?3#t)_ERdp^Gb&0GipY@7-lr0ODVr zENcBlzZAYM4!h$=S3>9c$7el**K2i0_@G5yn^*Rgmn|kaF0-M0xALNyArFau+MxY# z^&!K_#)RlR`-hJ0UNK4D)f_QMyIrjp5i1nj08wIju)yS=oP!xF-vaFQw#grL+Rhzf z=Otpi2?6q&F;c#n1UAb4^qXSpPPtR9m%elMP1$^M=zR;cL8&~bbXM1Kw70h(7!NzS z|5B0k*3K^aisgmD57$m0oP-z?2j?J;RdQ|xo}T3KZ)BAHLtjr_Ncgy@TPZG%#Wm;u zZci3JpS{a(93r>NG)Y=tH3T@oY4?&Fa!-9xR>A8`cj+m`UGi+6biS1rTUKiyua|{; zJoKpc;Md}aJpnL5G}bTqDai+EG+1D2&Df=fWfLu&LrZ!Y0=qq5+xi@ane5 zngvhF*BfeR*>!VO6BQ4uk%b3M_Dw&c$ZpMuz@*a*L0}(T*;H{Tk#KA}~pLqVM8}4bI9bA|__SEeVcq+rPniY-446 z2D7J-PAM-gt3kodFQq_pB9zDC_gqakpMHz$xS>>IR@&SNRh#btI(h&ETkBUGGdF*m z5niOCK|n4#fi6roj!}MZCXj zxu@=OPI12fuWl_DsVf{=rDenTE{#WXo$f_xfjf+ye*L_MNs%8sxBN4miEhoaQgB6v zuA5LC;)`upiP^vz4lA7AyG4OK=eHizv_>+PeB_~HP9rKU21@$u1_udw$!3wXeDcUX zQ%{$MKPT)&v*@T6$BWK8f~w7LGbueS_Te;`uiFnzKE9o27&|f+pI9+sj{R7Q zh!zTTPr?f`TNwlv^XbF(#XxCB2St|m;QYC6{wmrHvP8fMG>Sv|9?A4ze~At!2yis2 z9IZf=bPnha1sE!jj;sl_JWkJMxpvmjCKy$vw|sw+>CiZN6^!U@JnkItGQVenGU^jX zVBl%{#QY;V%}Fvl*FM_yiY6a(rLLREly3TG)z8q)$ps*E4&e5Fb>0SZ9R@$`Z(1z zXIW+KL}=w--+~q@&G|nWijE^h*8;lD)QnU_dd6vr+lIV z?vol78QGd2#qx8C$AFGW14>4&lh(Vm`%krbUJk{N#!ttooo13)D+0Tz*yaTn*1Z51 z4aWi7X1uR?wMyGRy&&oKQ(h71xD3X+frpr6Hsf^@AM`9NC%RY@Zlc&C9tdXNNXqs`(Ht zH+o!_lV7sJ1QU03d!|MMYIM$;(?nX78lR#{`T{)_^%~FbSP}Hk82)ot&eFXWRAVih zhd>T6_T1*jU0X9Cry7bHZoYD_uk2{Z@}u1`6I9ws?bF?KQBpK~PgN;0kjcZQyi9G) z>!O)(XiGnQ@I{DfD0r3yR8)6Zv=AGj-&^CH!|69a-T!6jvTgs^b&EocshPxo^pECW zpy9;V6f_9Mi?8O#2oiGUM+FW$0TK`Jaby{7bw%Gg^)f3(gf-2eFqW*IiAi)~_)>73 zMHZ(eXZ!?5%Am~Z%fzhJp;&%RyU)a^xbGnGr!bBCvSR1+k$?}w>PFok8@Gm8+~eS{Ny_|k8WtV4{J2~` zvnnZ9AHt6syZRtueo1KWvpEyUZ`#`j#Alp90Wh^4h--N=bG4Q_+8Qc)5oi*H_Au+J z{}fLDKM^nLt27a;l94wGTtJHgDQ0C?U=JYbBXf*r|6O|~sbxE?k^0ssy zbmW!##53@R3~rEzm>LY!+VkNMYt27H>r#~zM_`{eL3lRU_#B3#ZEpYVQU8k0I z+g|y}X88s;`eW{mnr|=VBeAwG8E{#rq@OK0TV%x`Y;(60E~+d#4D&k>c>A2#N1@KL z6Ya9IbM>E14~l3=({)pRZWuqVLk5H)17sTh_EH;vPaYfn6YZ&J?%L2Yiv*5{=wkfq z^d~4X+$c+l$1Ut92o?1D;v&XvrabbNbtFeZ&LbQKt-#gOI8sU>21~+mopc^^`{sgLH4xR>=zc~0xYFTP z^1l4yNH; znn|VU`)!Ms#mnzM^5bS*m3X}5Z2a0de^&YGz+}e$73*KO2Bf=yK<5o?#Sf1en2bJw zbI=Mz*{^fY&`{DJ9=Xh8&J23)jokOzcI>KtC$O=mw1F*@UKM5H0kJqRxEJ0qgpLKu zbp86mFn1$klJdtxqh+p85Sk%t~tnGNrA{q>F*R%c28 z6P|*cXnVZeGBg2%g7VzQ%N^YG2cMSAr=AKe`9|!^_sxirmgGB?e=<;3uK^FUtB?ic z45oMfWp#kb{bL90@VbhHr?V?9%U>$A3aJld{j<{94*l=mmui3#_q3CUmQC=z z2kA=IQzX#Q>Q&jKDDw{gSipGyxeYsTf@E=`78}WM3r$V%YGsoWpd)0h?ddYop#Q=nw%|VavwkpG^(agK@?J_7L zVI{%2XDB|KP8grpX*axK(_oRR*`6;B_<^4z{%`m|b?+J{p7kQxibltFv)PvE>WB7^ z8@kx-v14EFBfHoIZ`;_P?lAJ_$--Xi_H8DxqtT!bZtQ!W(!&SF@snpG#u468KL1|w z&-H@FWkb_JyE962Kczi8^VjFk$6go-vW>65^p}=M{rAaOD)qk)|Cw&||2cm7UFz2h g{y)FFCw@LTJN3jHaj57ZbP70BmB5On_sv882if^-uK)l5 literal 0 HcmV?d00001 From d1e8f1f2d3b2834d55e0231181e09d5e3045c542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 11 Aug 2026 17:09:41 +0200 Subject: [PATCH 11/35] update SPDX --- .../triangle_soup_boolean_operations_3.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 3c8e8550bc8f..6b5c6ee015d6 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -5,7 +5,7 @@ // // $URL$ // $Id$ -// SPDX-License-Identifier: LicenseRef-Commercial +// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial // // // Author(s) : Sébastien Loriot From fe2f6c15c50b992d2192d5731e7f4dd382e8471a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Mon, 17 Aug 2026 12:00:08 +0200 Subject: [PATCH 12/35] fix function name --- .../doc/PMP_Boolean_operations/PMP_Boolean_operations.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt b/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt index 1665a9f1e17e..421bb87ed929 100644 --- a/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt +++ b/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt @@ -145,7 +145,7 @@ possible, the input mesh will still be corefined. If the input meshes contain triangles that are coplanar, it is guaranteed that the triangulations of those triangles in the two corefined meshes will be identical. Also all edges of those refined triangles will be reported as intersection -edges (both in the function `CGAL::Polygon_mesh_processing::polyline_intersection()` and in the named parameter +edges (both in the function `CGAL::Polygon_mesh_processing::intersection_polylines()` and in the named parameter `edge_is_constrained_map()` of corefine prefixed functions). \subsubsection coref_bolop_input_autorefsub Autorefinement From ec3a1aa4412e7839b070802b6b8c1240d726afd5 Mon Sep 17 00:00:00 2001 From: Sebastien Loriot Date: Mon, 5 Oct 2026 09:22:52 +0200 Subject: [PATCH 13/35] typos Co-authored-by: Mael --- .../doc/PMP_Boolean_operations/PMP_Boolean_operations.txt | 2 +- .../include/CGAL/Polygon_mesh_processing/autorefinement.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt b/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt index 421bb87ed929..164f3575ef37 100644 --- a/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt +++ b/PMP_Boolean_operations/doc/PMP_Boolean_operations/PMP_Boolean_operations.txt @@ -339,7 +339,7 @@ The kernel functions and the specialization of clipping and plane refinement to inputs were implemented in 2025 by Léo Valque, inspired by the work of Sorgente et al. \cgalCite{sorgente2022polyhedron}. -The illustrations of the package pays homage to French sculptor and painter Sacha Sosno. +The illustrations of the package pay homage to French sculptor and painter Sacha Sosno. */ } /* namespace CGAL */ diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h index 6912c6541741..d4f179efdf9c 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h @@ -1836,7 +1836,7 @@ bool autorefine_triangle_soup(PointRange& soup_points, /** * \ingroup PMP_corefinement_grp - * refines a triangle mesh to ensure no pair of triangles intersects, execpt at a shared edge or vertex. + * refines a triangle mesh to ensure no pair of triangles intersects, except at a shared edge or vertex. * If the input does self-intersect, then the intersection will be resolved in the output but the * mesh will still be self-intersecting. * From 3c43d786c61a40404754e1632de2316f44078d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Thu, 8 Oct 2026 12:52:35 +0200 Subject: [PATCH 14/35] add soup_union example --- .../PMP_Boolean_operations/CMakeLists.txt | 1 + .../PMP_Boolean_operations/soup_union.cpp | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp diff --git a/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt b/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt index ae035b0858de..06775b8846e3 100644 --- a/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt +++ b/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt @@ -18,6 +18,7 @@ create_single_source_cgal_program("corefinement_mesh_union_with_attributes.cpp") create_single_source_cgal_program("corefinement_polyhedron_union.cpp") create_single_source_cgal_program("corefinement_LCC.cpp") create_single_source_cgal_program("soup_autorefinement.cpp") +create_single_source_cgal_program("soup_union.cpp") create_single_source_cgal_program("surface_mesh_intersection.cpp") create_single_source_cgal_program("triangle_mesh_autorefinement.cpp") create_single_source_cgal_program("snap_polygon_soup.cpp") diff --git a/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp b/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp new file mode 100644 index 000000000000..3199df5a2ad5 --- /dev/null +++ b/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp @@ -0,0 +1,46 @@ +#include + +#include +#include + +#include +#include +#include + +typedef CGAL::Exact_predicates_exact_constructions_kernel K; +typedef K::Point_3 Point; + +namespace PMP = CGAL::Polygon_mesh_processing; + +int main(int argc, char* argv[]) +{ + const std::string filename1 = + (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/blobby.off"); + const std::string filename2 = + (argc > 2) ? argv[2] : CGAL::data_file_path("meshes/eight.off"); + + std::vector points1, points2; + std::vector> triangles1, triangles2; + + if(!CGAL::IO::read_polygon_soup(filename1, points1, triangles1) || + !CGAL::IO::read_polygon_soup(filename2, points2, triangles2)) + { + std::cerr << "Invalid input." << std::endl; + return 1; + } + + std::vector points_output; + std::vector> triangles_output; + + PMP::compute_union(points1, triangles1, + points2, triangles2, + points_output, triangles_output); + + std::cout << "Union was successfully computed\n"; + + CGAL::IO::write_polygon_soup( + "union.off", points_output, triangles_output, + CGAL::parameters::stream_precision(17)); + + return 0; +} \ No newline at end of file From fdc09b2e4bcdf82b2775017090f8d45b747de4e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Fri, 9 Oct 2026 16:27:05 +0200 Subject: [PATCH 15/35] fill the point map is now parallellize --- .../PMP_Boolean_operations/CMakeLists.txt | 1 + .../PMP_Boolean_operations/soup_union.cpp | 14 ++++++--- .../Polygon_mesh_processing/autorefinement.h | 31 ++++++++++++++++--- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt b/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt index 06775b8846e3..1f6e746c1f03 100644 --- a/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt +++ b/PMP_Boolean_operations/examples/PMP_Boolean_operations/CMakeLists.txt @@ -50,6 +50,7 @@ find_package(TBB QUIET) include(CGAL_TBB_support) if(TARGET CGAL::TBB_support) target_link_libraries(soup_autorefinement PRIVATE CGAL::TBB_support) + target_link_libraries(soup_union PRIVATE CGAL::TBB_support) target_link_libraries(snap_polygon_soup PRIVATE CGAL::TBB_support) create_single_source_cgal_program("corefinement_parallel_union_meshes.cpp") diff --git a/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp b/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp index 3199df5a2ad5..b05f0899bbf8 100644 --- a/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp +++ b/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp @@ -22,10 +22,15 @@ int main(int argc, char* argv[]) std::vector points1, points2; std::vector> triangles1, triangles2; - if(!CGAL::IO::read_polygon_soup(filename1, points1, triangles1) || - !CGAL::IO::read_polygon_soup(filename2, points2, triangles2)) + if(!CGAL::IO::read_polygon_soup(filename1, points1, triangles1)) { - std::cerr << "Invalid input." << std::endl; + std::cerr << "Failed to read file: " << filename1 << std::endl; + return 1; + } + + if(!CGAL::IO::read_polygon_soup(filename2, points2, triangles2)) + { + std::cerr << "Failed to read file: " << filename2 << std::endl; return 1; } @@ -34,7 +39,8 @@ int main(int argc, char* argv[]) PMP::compute_union(points1, triangles1, points2, triangles2, - points_output, triangles_output); + points_output, triangles_output, + CGAL::parameters::concurrency_tag(CGAL::Parallel_if_available_tag())); std::cout << "Union was successfully computed\n"; diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h index d4f179efdf9c..dae4dbf63dea 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h @@ -1473,21 +1473,42 @@ bool autorefine_triangle_soup(PointRange& soup_points, Point_id_map point_id_map; #if ! defined(CGAL_NDEBUG) || defined(CGAL_DEBUG_PMP_AUTOREFINE) - std::vector exact_soup_points; +#ifdef CGAL_LINKED_WITH_TBB + typedef std::conditional_t, + std::vector> Exact_point_range; +#else + typedef std::vector Exact_point_range; +#endif + Exact_point_range exact_soup_points; #endif // TODO: parallel_for? // for input points, we on purpose keep duplicated points and isolated points - for (std::size_t pid = 0; pidfirst); + exact_soup_points.push_back(insert_res.first->first); #endif + }; +#ifdef CGAL_LINKED_WITH_TBB + if constexpr (parallel_execution) + { + tbb::parallel_for(tbb::blocked_range(0, soup_points.size()), + [&](const tbb::blocked_range& r) { + for (std::size_t pid = r.begin(); pid != r.end(); ++pid) + insert_point_id(pid); + }); + } + else +#endif + for (std::size_t pid = 0; pid Date: Fri, 9 Oct 2026 17:28:40 +0200 Subject: [PATCH 16/35] [COREFINE] use a canonical vector for face triangulation --- .../internal/Corefinement/Visitor.h | 59 ++++++++++++++----- .../internal/Corefinement/face_graph_utils.h | 4 ++ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/Visitor.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/Visitor.h index 3794336dad27..19bdcc5a7d6f 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/Visitor.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/Visitor.h @@ -1344,7 +1344,18 @@ class Surface_intersection_visitor_for_corefinement{ q = nodes.to_exact(get(vpm,f_vertices[1])), r = nodes.to_exact(get(vpm,f_vertices[2])); ///TODO use a positive normal and remove all workaround to guarantee that triangulation of coplanar patches are compatible - CDT_traits traits(typename EK::Construct_normal_3()(p,q,r)); + typename EK::Vector_3 n = typename EK::Construct_normal_3()(p,q,r); + typename EK::Point_3 o(CGAL::ORIGIN); + + bool orientation_flipped = false; + if ( typename EK::Less_xyz_3()(o+n,o) ) + { + n=-n; + orientation_flipped = true; + } + + + CDT_traits traits(n); CDT cdt(traits); // insert triangle points @@ -1352,7 +1363,7 @@ class Surface_intersection_visitor_for_corefinement{ //we can do this to_exact because these are supposed to be input points. triangle_vertices[0]=cdt.insert_outside_affine_hull(p); triangle_vertices[1]=cdt.insert_outside_affine_hull(q); - triangle_vertices[2]=cdt.tds().insert_dim_up(cdt.infinite_vertex(), false); + triangle_vertices[2]=cdt.tds().insert_dim_up(cdt.infinite_vertex(), orientation_flipped); triangle_vertices[2]->set_point(r); triangle_vertices[0]->info()=f_indices[0]; @@ -1378,6 +1389,7 @@ class Surface_intersection_visitor_for_corefinement{ // is already tight and the call in Intersection_nodes::finalize() will not fix anything } } + //insert points on edges if (it_fb!=face_boundaries.end()) //if f not a triangle? { @@ -1386,9 +1398,18 @@ class Surface_intersection_visitor_for_corefinement{ for (int i=0;i<3;++i) { int oi=-1; - CGAL_assertion_code(bool is_edge = ) - cdt.is_edge(triangle_vertices[i], triangle_vertices[(i+1)%3], infinite_faces[i], oi); - CGAL_assertion(is_edge); + if (orientation_flipped) + { + CGAL_assertion_code(bool is_edge = ) + cdt.is_edge(triangle_vertices[(i+1)%3], triangle_vertices[i], infinite_faces[i], oi); + CGAL_assertion(is_edge); + } + else + { + CGAL_assertion_code(bool is_edge = ) + cdt.is_edge(triangle_vertices[i], triangle_vertices[(i+1)%3], infinite_faces[i], oi); + CGAL_assertion(is_edge); + } CGAL_assertion( cdt.is_infinite( infinite_faces[i]->vertex(oi) ) ); } @@ -1400,22 +1421,30 @@ class Surface_intersection_visitor_for_corefinement{ //handle case of halfedge starting at triangle_vertices[i] // and ending at triangle_vertices[(i+1)%3] - const Node_ids& ids_on_edge=f_boundary.node_ids_array[i]; + Node_ids& ids_on_edge=f_boundary.node_ids_array[i]; CDT_Vertex_handle previous=triangle_vertices[i]; - Node_id prev_index=f_indices[i];// node-id of the mesh vertex + Node_id prev_index=orientation_flipped?f_indices[(i+1)%3]:f_indices[i];// node-id of the mesh vertex halfedge_descriptor hedge = next(f_boundary.halfedges[(i+2)%3],tm); - CGAL_assertion( source(hedge,tm)==f_boundary.vertices[i] ); + if (orientation_flipped) + for (std::size_t l=0;lvertex(cw(infinite_vertex)) to fh->vertex(ccw(infinite_vertex)) + if (orientation_flipped) + std::reverse(ids_on_edge.begin(), ids_on_edge.end()); for(Node_id id : ids_on_edge) { CDT_Vertex_handle vh=insert_point_on_ch_edge(cdt,infinite_faces[i],nodes.exact_node(id)); vh->info()=id; id_to_CDT_vh.insert(std::make_pair(id,vh)); - edge_to_hedge[std::make_pair(prev_index,id)]=hedge; + auto key = orientation_flipped ? std::make_pair(id, prev_index) + : std::make_pair(prev_index,id); + edge_to_hedge[key]=hedge; previous=vh; - hedge=next(hedge,tm); + hedge=orientation_flipped?prev(hedge,tm):next(hedge,tm); prev_index=id; } } @@ -1424,9 +1453,11 @@ class Surface_intersection_visitor_for_corefinement{ CGAL_assertion( target(hd,tm) == f_boundary.vertices[(i+1)%3] ); CGAL_assertion( source(hd,tm) == f_boundary.vertices[ i ] ); } - CGAL_assertion(hedge==f_boundary.halfedges[i]); - edge_to_hedge[std::make_pair(prev_index,f_indices[(i+1)%3])] = - it_fb->second.halfedges[i]; + CGAL_assertion(orientation_flipped || hedge==f_boundary.halfedges[i]); + CGAL_assertion(!orientation_flipped || hedge==next(f_boundary.halfedges[(i+2)%3],tm)); + auto key = orientation_flipped ? std::make_pair(f_indices[i], prev_index) + : std::make_pair(prev_index,f_indices[(i+1)%3]); + edge_to_hedge[key] = hedge/* it_fb->second.halfedges[i] */; } } @@ -1507,7 +1538,7 @@ class Surface_intersection_visitor_for_corefinement{ // import the triangle in `cdt` in the face `f` of `tm` triangulate_a_face(f, tm, nodes, node_ids, node_id_to_vertex, - edge_to_hedge, cdt, vpm, output_builder, user_visitor); + edge_to_hedge, cdt, vpm, orientation_flipped, output_builder, user_visitor); // TODO Here we do the update only for internal edges. // Update for border halfedges could be done during the split diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/face_graph_utils.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/face_graph_utils.h index 4276641ce7c7..e65b13ad5b11 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/face_graph_utils.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/face_graph_utils.h @@ -526,6 +526,7 @@ triangulate_a_face( ::halfedge_descriptor>& edge_to_hedge, const CDT& cdt, const VertexPointMap& vpm, + bool orientation_flipped, OutputBuilder& output_builder, UserVisitor& user_visitor) { @@ -592,6 +593,9 @@ triangulate_a_face( typename CDT::Vertex_handle cdt_v1=it->vertex(1); typename CDT::Vertex_handle cdt_v2=it->vertex(2); + if (orientation_flipped) + std::swap(cdt_v0, cdt_v2); + Node_id i0=cdt_v0->info(), i1=cdt_v1->info(), i2=cdt_v2->info(); CGAL_assertion(edge_to_hedge.count(std::make_pair(i0,i1))!= 0); From 90b387dfbd1215f9e745234c6e1eace58f32dfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Mon, 12 Oct 2026 18:29:04 +0200 Subject: [PATCH 17/35] Rewrite connected_components to use vector of adjacence instead of an edge_map --- .../triangle_soup_boolean_operations_3.h | 253 ++++++++++++------ 1 file changed, 171 insertions(+), 82 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 6b5c6ee015d6..24966214c7cc 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -26,58 +26,62 @@ #include #endif +#ifdef CGAL_LINKED_WITH_TBB +#include +#endif + namespace CGAL { namespace boolops_3 { //TODO: update nm_edges for identical coplanar status (less test below then) -template +template std::vector connected_components(const Polygons& triangles, - const EdgeMap& edge_map, + const EdgeConnectivity& adjacent_edges, + const FaceConnectivity& adjacent_faces, std::vector& ccids, NM_edges& nm_edges) { + auto is_handled = [&](std::size_t tid){ return ccids[tid]!=std::size_t(-1); }; std::vector one_triangle_per_cc; std::size_t nb_poly = triangles.size(); - ccids.resize(nb_poly); + ccids.resize(nb_poly, std::size_t(-1)); std::size_t i=0; - std::vector handled(nb_poly, false); + std::vector queue; for (std::size_t tid=0; tid queue; + if (is_handled(tid)) continue; queue.push_back(tid); + ccids[tid]=i; while(!queue.empty()) { std::size_t q_tid = queue.back(); queue.pop_back(); - if ( handled[q_tid]) continue; - handled[q_tid]=true; - ccids[q_tid]=i; - for (int k=0; k<3; ++k) // TODO: change that if you want more than triangles { - auto p = CGAL::make_sorted_pair(triangles[q_tid][k], triangles[q_tid][(k+1)%3]); - auto it = edge_map[p.first].find(p.second); - CGAL_assertion(it!=edge_map[p.first].end()); - const auto& tids = it->second; + auto ei = adjacent_edges[q_tid][k]; + const auto &tids = adjacent_faces[ei]; + CGAL_assertion(tids.size() > 0); switch( tids.size() ) { case 1: + // boundary edge break; case 2: { std::size_t n_tid = tids[0]!=q_tid ? tids[0] : tids[1]; - if (!handled[n_tid]) + if (!is_handled(n_tid)){ + ccids[n_tid]=i; queue.push_back(n_tid); + } } break; default: - nm_edges.emplace_back(p.first, it); - //nm edge + // non-manifold edge + nm_edges.emplace_back(ei); break; } } @@ -104,42 +108,94 @@ select(const std::vector& points, std::vector& input_ids, const unsigned int nb_meshes) { - //TODO: use Concurrency_tag namespace PMP = CGAL::Polygon_mesh_processing; namespace pred = PMP::Corefinement; Selection_data data_out; - // fill edge map - typedef std::size_t PID; - typedef std::size_t TID; - std::vector< std::unordered_map > > edge_map(points.size()); - using Boundary_edge_location = std::pair >::const_iterator>; + // Create connectivity information + using PID = std::size_t; + using EID = std::size_t; + using TID = std::size_t; - for (TID tid=0; tid; + std::vector partial_edges; + partial_edges.resize(3 * triangles.size()); +#ifdef CGAL_LINKED_WITH_TBB + if (std::is_same_v) + { + tbb::parallel_for(std::size_t(0), triangles.size(), [&](std::size_t tid) + { + auto add_edge = [&](std::size_t idx, PID a, PID b){ + if (b < a) std::swap(a, b); + partial_edges[idx] = Edge_record{a, b, tid}; + }; + add_edge(3 * tid + 0, triangles[tid][0], triangles[tid][1]); + add_edge(3 * tid + 1, triangles[tid][1], triangles[tid][2]); + add_edge(3 * tid + 2, triangles[tid][2], triangles[tid][0]); + }); + } + else +#endif + { + for (TID tid = 0; tid < triangles.size(); ++tid) + { + auto add_edge = [&](std::size_t idx, PID a, PID b){ + if (b < a) std::swap(a, b); + partial_edges[idx] = Edge_record{a, b, tid}; + }; + add_edge(3 * tid + 0, triangles[tid][0], triangles[tid][1]); + add_edge(3 * tid + 1, triangles[tid][1], triangles[tid][2]); + add_edge(3 * tid + 2, triangles[tid][2], triangles[tid][0]); + } + } + +#ifdef CGAL_LINKED_WITH_TBB + if (std::is_same_v) + { + tbb::parallel_sort(partial_edges.begin(), partial_edges.end()); + } + else +#endif + { + std::sort(partial_edges.begin(), partial_edges.end()); + } + + // TODO parallellize (need a mutex to push on small_vector) + std::vector> adjacent_edges(triangles.size()); + std::vector> adjacent_faces; + adjacent_faces.reserve(partial_edges.size()); + for (std::size_t i = 0; i < partial_edges.size(); ) { - auto p = CGAL::make_sorted_pair(triangles[tid][0], triangles[tid][1]); - edge_map[p.first][p.second].push_back(tid); - p = CGAL::make_sorted_pair(triangles[tid][1], triangles[tid][2]); - edge_map[p.first][p.second].push_back(tid); - p = CGAL::make_sorted_pair(triangles[tid][2], triangles[tid][0]); - edge_map[p.first][p.second].push_back(tid); + const PID a = std::get<0>(partial_edges[i]); + const PID b = std::get<1>(partial_edges[i]); + const std::size_t idx = adjacent_faces.size(); + adjacent_faces.emplace_back(); + CGAL_assertion(adjacent_faces.back().empty()); + do + { + const TID tid = std::get<2>(partial_edges[i]); + adjacent_faces.back().push_back(tid); + ++i; + + for(std::size_t j = 0; j < 3; ++j) + if ((triangles[tid][j] == a && triangles[tid][(j+1)%3] == b) || + (triangles[tid][j] == b && triangles[tid][(j+1)%3] == a)){ + adjacent_edges[tid][(j+2)%3] = idx; + break; + } + } while (i < partial_edges.size() && std::get<0>(partial_edges[i]) == a && std::get<1>(partial_edges[i]) == b); + CGAL_assertion(!adjacent_faces.back().empty()); } // extract connected_components bounded by non-manifold edges std::vector& ccids = data_out.ccids; - std::vector< Boundary_edge_location > nm_edges; - std::vector one_triangle_per_cc = - boolops_3::connected_components(triangles, edge_map, ccids, nm_edges); + std::vector< EID > nm_edges; + std::vector one_triangle_per_cc = boolops_3::connected_components(triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); std::size_t nb_cc = one_triangle_per_cc.size(); - // TODO: direct filling rather that sort + unique? - //~ std::sort(nm_edges.begin(), nm_edges.end(), - //~ [](const std::pair& p1, const std::pair& p2) - //~ { - //~ if (p1.first!=p2.first) return p1.first& points, } #endif - std::size_t nb_handled = 0; std::vector cc_handled(nb_cc, false); std::vector& coplanar_patches=data_out.coplanar_patches; @@ -169,12 +224,13 @@ select(const std::vector& points, #endif cc_is_inside_per_mesh.assign(nb_meshes, std::vector(nb_cc, false)); // TODO: use an optional - for (auto [pid, it] : nm_edges) + for (auto e : nm_edges) { if (nb_handled==nb_cc) break; bool all_handled=true; - for (TID tid : it->second) + auto &tids = adjacent_faces[e]; + for (TID tid : tids) { if (!cc_handled[ccids[tid]]) { @@ -187,9 +243,9 @@ select(const std::vector& points, bool all_from_same_mesh = true; - for (TID tid : it->second) + for (TID tid : tids) { - if (input_ids[tid]!=input_ids[it->second.front()]) + if (input_ids[tid]!=input_ids[tids.front()]) { all_from_same_mesh=false; break; @@ -197,34 +253,30 @@ select(const std::vector& points, } if (all_from_same_mesh) continue; #ifdef CGAL_BO_AUTOREF_DEBUG - std::cout << "case #" << ++case_id << " looking at edge " << pid << " " << it->first << "\n"; + std::cout << "case #" << ++case_id << " looking at edge " << pid << " " << e << "\n"; std::cout << " incident CCs:"; - for (TID tid : it->second) std::cout << " " << ccids[tid] << "("<< input_ids[tid] <<")"; + for (TID tid : tids) std::cout << " " << ccids[tid] << "("<< input_ids[tid] <<")"; std::cout << "\n"; #endif - for (TID tid : it->second) + for (TID tid : tids) { if (!cc_handled[ccids[tid]]) ++nb_handled; cc_handled[ccids[tid]] = true; } std::vector> to_sort; - to_sort.reserve(it->second.size()); + to_sort.reserve(tids.size()); - std::size_t i0=pid, i1=it->first; - for (TID tid : it->second) - { + for (TID tid : tids){ for (int i=0; i<3; ++i) - { - if (triangles[tid][i]!=i0 && triangles[tid][i]!=i1) + if( adjacent_edges[tid][i] == e ) to_sort.emplace_back(i, tid); - } } // we arbitrarily take the last triangle std::pair tref=to_sort.front(); - if (triangles[tref.second][(tref.first+1)%3]!=i0) - std::swap(i0,i1); + std::size_t i0=triangles[tref.second][(tref.first+1)%3], + i1=triangles[tref.second][(tref.first+2)%3]; const Exact_point& pref=points[triangles[tref.second][tref.first]]; const Exact_point& p0=points[i0]; @@ -650,28 +702,63 @@ void compute_self_union(PointRange& points, TriangleRange& triangles, //~ Selection_data data_out; // fill edge map (TODO: move to a function) - typedef std::size_t PID; - typedef std::size_t TID; - std::vector< std::unordered_map > > edge_map(points.size()); - using Boundary_edge_location = std::pair >::const_iterator>; + using PID = std::size_t; + using EID = std::size_t; + using TID = std::size_t; + using Edge_record = std::tuple; - for (TID tid=0; tid partial_edges(3 * triangles.size()); + for (TID tid = 0; tid < triangles.size(); ++tid) { - auto p = CGAL::make_sorted_pair(triangles[tid][0], triangles[tid][1]); - edge_map[p.first][p.second].push_back(tid); - p = CGAL::make_sorted_pair(triangles[tid][1], triangles[tid][2]); - edge_map[p.first][p.second].push_back(tid); - p = CGAL::make_sorted_pair(triangles[tid][2], triangles[tid][0]); - edge_map[p.first][p.second].push_back(tid); + auto add_edge = [&](std::size_t idx, PID a, PID b) + { + if (b < a) std::swap(a, b); + partial_edges[idx] = Edge_record{a, b, tid}; + }; + add_edge(3 * tid, triangles[tid][0], triangles[tid][1]); + add_edge(3 * tid + 1, triangles[tid][1], triangles[tid][2]); + add_edge(3 * tid + 2, triangles[tid][2], triangles[tid][0]); + } + std::sort(partial_edges.begin(), partial_edges.end()); + + std::vector> adjacent_edges(triangles.size()); + std::vector> adjacent_faces; + adjacent_faces.reserve(partial_edges.size()); + for (std::size_t i = 0; i < partial_edges.size(); ) + { + const PID a = std::get<0>(partial_edges[i]); + const PID b = std::get<1>(partial_edges[i]); + const EID eid = adjacent_faces.size(); + adjacent_faces.emplace_back(); + do + { + const TID tid = std::get<2>(partial_edges[i++]); + adjacent_faces.back().push_back(tid); + for (std::size_t j = 0; j < 3; ++j) + if ((triangles[tid][j] == a && triangles[tid][(j+1)%3] == b) || + (triangles[tid][j] == b && triangles[tid][(j+1)%3] == a)) + { + adjacent_edges[tid][(j+2)%3] = eid; + break; + } + } while (i < partial_edges.size() && std::get<0>(partial_edges[i]) == a && std::get<1>(partial_edges[i]) == b); } // extract connected_components bounded by non-manifold edges (TODO: move to a function) std::vector ccids; - std::vector< Boundary_edge_location > nm_edges; + std::vector nm_edges; std::vector one_triangle_per_cc = - boolops_3::connected_components(triangles, edge_map, ccids, nm_edges); + boolops_3::connected_components(triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); std::size_t nb_cc = one_triangle_per_cc.size(); + // Get the vertices associate to an edge + auto edge_vertices = [&](EID eid) { + std::size_t tid = adjacent_faces[eid].front(); + for(std::size_t k=0; k<3; ++k) + if(adjacent_edges[tid][k] == eid) + return std::make_pair(triangles[tid][(k+1)%3], triangles[tid][(k+2)%3]); + }; + using UF_CC = CGAL::Union_find; using UF_handle = UF_CC::handle; @@ -684,18 +771,19 @@ void compute_self_union(PointRange& points, TriangleRange& triangles, // cc -> edges // TODO we need split graph into polylines I guess - std::vector > cc_to_edges(nb_cc); + std::vector > cc_to_edges(nb_cc); std::vector pts_on_edge(points.size(), false); // TODO: fill it in autoref visitor? - for (auto [pid, it] : nm_edges) + for (EID eid : nm_edges) { std::set ccs; + const auto [pid, other_pid] = edge_vertices(eid); pts_on_edge[pid]=true; - pts_on_edge[it->first]=true; - for (TID tid : it->second) + pts_on_edge[other_pid]=true; + for (TID tid : adjacent_faces[eid]) ccs.insert(ccids[tid]); for (std::size_t ccid : ccs) - cc_to_edges[ccid].emplace_back(pid, it); + cc_to_edges[ccid].push_back(eid); } std::size_t min_pt_id=0; @@ -746,20 +834,21 @@ void compute_self_union(PointRange& points, TriangleRange& triangles, ccs_handled[ccid]=true; // iterate on all non-manifold edges of the current cc - for (auto [pid, it] : cc_to_edges[ccid]) + for (EID eid : cc_to_edges[ccid]) { + const auto [pid, other_pid] = edge_vertices(eid); #ifdef CGAL_BO_AUTOREF_DEBUG - std::cout << "case #" << ++case_id << " looking at edge " << pid << " " << it->first << "\n"; + std::cout << "case #" << ++case_id << " looking at edge " << pid << " " << other_pid << "\n"; std::cout << " incident CCs:"; - for (TID tid : it->second) std::cout << " " << ccids[tid] << "("<< input_ids[tid] <<")"; + for (TID tid : adjacent_faces[eid]) std::cout << " " << ccids[tid] << "("<< input_ids[tid] <<")"; std::cout << "\n"; #endif std::vector> to_sort; - to_sort.reserve(it->second.size()); + to_sort.reserve(adjacent_faces[eid].size()); - std::size_t i0=pid, i1=it->first; - for (TID tid : it->second) + std::size_t i0=pid, i1=other_pid; + for (TID tid : adjacent_faces[eid]) { for (int i=0; i<3; ++i) { From c9b584ee7c71aa79809ebae2b77da7e4bf9f28ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Tue, 13 Oct 2026 12:21:34 +0200 Subject: [PATCH 18/35] Add test corefinement vs autorefine --- .../PMP_Boolean_operations/CMakeLists.txt | 1 + .../test_corefinement_vs_autorefine.cmd | 21 +++ .../test_corefinement_vs_autorefine.cpp | 131 ++++++++++++++++++ .../test_corefinement_vs_autorefine_full.cmd | 31 +++++ 4 files changed, 184 insertions(+) create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cmd create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp create mode 100644 PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine_full.cmd diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt b/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt index 3035f32ee0fa..a52f5b7347ae 100644 --- a/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/CMakeLists.txt @@ -17,6 +17,7 @@ create_single_source_cgal_program("test_corefine.cpp") create_single_source_cgal_program("test_coref_epic_points_identity.cpp") create_single_source_cgal_program("test_corefinement_and_constraints.cpp") create_single_source_cgal_program("test_corefinement_and_constraints_nearest.cpp") +create_single_source_cgal_program("test_corefinement_vs_autorefine.cpp") create_single_source_cgal_program("test_corefinement_bool_op.cpp") create_single_source_cgal_program("test_corefinement_cavities.cpp") create_single_source_cgal_program("test_corefinement_nm_bo.cpp") diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cmd b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cmd new file mode 100644 index 000000000000..ff852e0e0235 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cmd @@ -0,0 +1,21 @@ +${CGAL_DATA_DIR}/meshes/elephant.off ${CGAL_DATA_DIR}/meshes/sphere.off +${CGAL_DATA_DIR}/meshes/open_cube.off data-coref/incompatible_with_open_cube.off +data-coref/floating_squares.off data-coref/hexa.off +data-coref/star_tgt1-0.off data-coref/star_tgt2-0.off +data-coref/star_tgt1-0.off data-coref/star_tgt2-1.off +data-coref/star_tgt1-0.off data-coref/star_tgt2-2.off +data-coref/star_tgt1-0.off data-coref/star_tgt2-3.off +data-coref/star_tgt1-0.off data-coref/star_tgt2-4.off +data-coref/star_tgt1-0.off data-coref/star_tgt2-5.off +data-coref/star_tgt1-1.off data-coref/star_tgt2-0.off +data-coref/star_tgt1-1.off data-coref/star_tgt2-1.off +data-coref/star_tgt1-1.off data-coref/star_tgt2-2.off +data-coref/star_tgt1-1.off data-coref/star_tgt2-3.off +data-coref/star_tgt1-1.off data-coref/star_tgt2-4.off +data-coref/star_tgt1-1.off data-coref/star_tgt2-5.off +data-coref/star_tgt1-2.off data-coref/star_tgt2-0.off +data-coref/star_tgt1-2.off data-coref/star_tgt2-1.off +data-coref/star_tgt1-2.off data-coref/star_tgt2-2.off +data-coref/star_tgt1-2.off data-coref/star_tgt2-3.off +data-coref/star_tgt1-2.off data-coref/star_tgt2-4.off +data-coref/star_tgt1-2.off data-coref/star_tgt2-5.off diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp new file mode 100644 index 000000000000..9d21d6b07118 --- /dev/null +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace PMP = CGAL::Polygon_mesh_processing; +namespace CFR = PMP::Corefinement; + +using Kernel = CGAL::Exact_predicates_exact_constructions_kernel; +using Point_3 = Kernel::Point_3; +using Mesh = CGAL::Surface_mesh; +using Polygon = std::vector; +using Polygons = std::vector; +using Points = std::vector; +using face_descriptor = boost::graph_traits::face_descriptor; + +using Triangle = std::array; + +void test(const std::string& p_filename, const std::string& q_filename) +{ + Mesh P, Q; + + const std::filesystem::path p_path(p_filename); + const std::filesystem::path q_path(q_filename); + std::cout << "Testing " << p_path.filename().string() << " and " << q_path.filename().string() << '\n'; + + if (!CGAL::IO::read_polygon_mesh(p_filename, P)) + { + std::cerr << "Failed to read polygon mesh: " << p_filename << '\n'; + std::exit(EXIT_FAILURE); + } + if (!CGAL::IO::read_polygon_mesh(q_filename, Q)) + { + std::cerr << "Failed to read polygon mesh: " << q_filename << '\n'; + std::exit(EXIT_FAILURE); + } + + // Convert the inputs to triangle soups. + Points p_points, q_points; + Polygons p_polygons, q_polygons; + PMP::polygon_mesh_to_polygon_soup(P, p_points, p_polygons); + PMP::polygon_mesh_to_polygon_soup(Q, q_points, q_polygons); + + // Results computed directly on triangle soups. + const std::array names = {{ + "union", "intersection", "P - Q", "Q - P" + }}; + + std::array soup_points; + std::array soup_polygons; + + PMP::compute_union(p_points, p_polygons, q_points, q_polygons, soup_points[CFR::UNION], soup_polygons[CFR::UNION]); + PMP::compute_intersection(p_points, p_polygons, q_points, q_polygons, soup_points[CFR::INTERSECTION], soup_polygons[CFR::INTERSECTION]); + PMP::compute_difference(p_points, p_polygons, q_points, q_polygons, soup_points[CFR::TM1_MINUS_TM2], soup_polygons[CFR::TM1_MINUS_TM2]); + PMP::compute_difference(q_points, q_polygons, p_points, p_polygons, soup_points[CFR::TM2_MINUS_TM1], soup_polygons[CFR::TM2_MINUS_TM1]); + + // Results computed by the existing mesh API. + Mesh union_mesh, intersection_mesh, p_minus_q_mesh, q_minus_p_mesh; + std::array, 4> corefine_outputs{&union_mesh, &intersection_mesh, &p_minus_q_mesh, &q_minus_p_mesh}; + const std::array corefine_success = PMP::corefine_and_compute_boolean_operations(P, Q, corefine_outputs); + + for (std::size_t i = 0; i < 4; ++i) + { + if (!corefine_success[i] ){ + std::cout << names[i] << " is not manifold\n"; + continue; + } + std::cout << "Checking " << names[i] << "... "; + + // Convert the mesh result to a soup for comparison. + Mesh M; + PMP::polygon_soup_to_polygon_mesh(soup_points[i], soup_polygons[i], M); + + std::vector m1_only; + std::vector m2_only; + PMP::match_faces(*(corefine_outputs[i].value()), M, boost::make_function_output_iterator([](const auto&) {}), + std::back_inserter(m1_only), + std::back_inserter(m2_only)); + const bool equal = m1_only.empty() && m2_only.empty(); + + if (!equal) + { + std::cerr << "FAILED: results are different\n"; + assert(false); + } + else + std::cout << " OK\n"; + } +} + +int main(int argc, char** argv) +{ + const std::filesystem::path program_path(argv[0]); + + if (argc < 3) + { + std::cerr << "Usage: " << program_path.filename().string() << " P.off Q.off\n"; + return EXIT_FAILURE; + } + + if(argc == 3) + { + test(argv[1], argv[2]); + return EXIT_SUCCESS; + } + + if ((argc-1)%2==0) + { + // cmd mode + for (int i=1; i Date: Wed, 14 Oct 2026 10:08:03 +0200 Subject: [PATCH 19/35] Add timers in autoref-boolop --- .../Polygon_mesh_processing/autorefinement.h | 8 +++++ .../triangle_soup_boolean_operations_3.h | 32 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h index dae4dbf63dea..cbf7aa69c7c6 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h @@ -1511,6 +1511,10 @@ bool autorefine_triangle_soup(PointRange& soup_points, insert_point_id(pid); } +#ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS + std::cout << timer.time() << "s to fill point id map" << std::endl; +#endif + TriangleRange soup_triangles_out; soup_triangles_out.reserve(soup_triangles.size()); @@ -1547,6 +1551,10 @@ bool autorefine_triangle_soup(PointRange& soup_points, } } +#ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS + std::cout << timer.time() << " to build the output" << std::endl; +#endif + // import refined triangles // Lambda to retrieve the id of intersection points diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 24966214c7cc..0656607939e2 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -108,6 +108,11 @@ select(const std::vector& points, std::vector& input_ids, const unsigned int nb_meshes) { +#ifdef CGAL_BO3_TIMERS + Real_timer t; + t.start(); +#endif + namespace PMP = CGAL::Polygon_mesh_processing; namespace pred = PMP::Corefinement; Selection_data data_out; @@ -537,6 +542,9 @@ select(const std::vector& points, data_out.input_ids.swap(input_ids); +#ifdef CGAL_BO3_TIMERS + std::cout << "select intern in " << t.time() << "\n"; +#endif return data_out; } @@ -611,7 +619,12 @@ prepare_data(const PointRange& points_1, const TriangleRange& triangles_1, #ifdef CGAL_BO_AUTOREF_DEBUG CGAL::IO::write_OFF("autorefined.off", out_points, out_triangles, CGAL::parameters::stream_precision(17)); #endif - return select(out_points, out_triangles, input_ids, 2); + auto res =select(out_points, out_triangles, input_ids, 2); +#ifdef CGAL_BO3_TIMERS + timer.stop(); + std::cout << "select done in " << timer.time() << "\n"; +#endif + return res; } template @@ -751,6 +764,9 @@ void compute_self_union(PointRange& points, TriangleRange& triangles, boolops_3::connected_components(triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); std::size_t nb_cc = one_triangle_per_cc.size(); + std::sort(nm_edges.begin(), nm_edges.end()); + nm_edges.erase(std::unique(nm_edges.begin(), nm_edges.end()), nm_edges.end()); + // Get the vertices associate to an edge auto edge_vertices = [&](EID eid) { std::size_t tid = adjacent_faces[eid].front(); @@ -1000,10 +1016,18 @@ void compute_union(const PointRange& points_1, const TriangleRange& triangles_1, Sequential_tag > ::type Concurrency_tag; +#ifdef CGAL_BO3_TIMERS + Real_timer t; + t.start(); +#endif + using namespace boolops_3; Selection_data data_out=prepare_data(points_1, triangles_1, points_2, triangles_2, out_points, out_triangles); const unsigned int nb_meshes=2; +#ifdef CGAL_BO3_TIMERS + std::cout << "Prepare_data + autoref: " << t.time() << "\n"; +#endif std::vector to_keep(out_triangles.size(), false); std::size_t nb_out_triangles=0; @@ -1035,7 +1059,13 @@ void compute_union(const PointRange& points_1, const TriangleRange& triangles_1, } } +#ifdef CGAL_BO3_TIMERS + std::cout << "Collect patches: " << t.time() << "\n"; +#endif boolops_3::update_output(out_points, out_triangles, nb_out_triangles, to_keep); +#ifdef CGAL_BO3_TIMERS + std::cout << "BO3 Building output: " << t.time() << "\n"; +#endif } /** From a368e207203dcd17581135be2fc1d04a81b0f60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Wed, 14 Oct 2026 12:00:49 +0200 Subject: [PATCH 20/35] Duplace non manifold edges before to compare faces --- .../include/CGAL/Polygon_mesh_processing/autorefinement.h | 5 ++++- .../test_corefinement_vs_autorefine.cpp | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h index cbf7aa69c7c6..792faf35c6e7 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h @@ -1749,7 +1749,10 @@ bool autorefine_triangle_soup(PointRange& soup_points, #ifndef CGAL_NDEBUG CGAL_PMP_AUTOREFINE_VERBOSE("check soup"); - CGAL_assertion( !does_triangle_soup_self_intersect(exact_soup_points, soup_triangles_out) ); + // If split_triangle_range is defined, intersection can still be present inside one of the range + if constexpr(parameters::is_default_parameter::value){ + CGAL_assertion( !does_triangle_soup_self_intersect(exact_soup_points, soup_triangles_out) ); + } #else #ifdef CGAL_DEBUG_PMP_AUTOREFINE CGAL_PMP_AUTOREFINE_VERBOSE("check soup"); diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp index 9d21d6b07118..059459f6224b 100644 --- a/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp @@ -83,6 +83,7 @@ void test(const std::string& p_filename, const std::string& q_filename) // Convert the mesh result to a soup for comparison. Mesh M; + PMP::duplicate_non_manifold_edges_in_polygon_soup(soup_points[i], soup_polygons[i]); PMP::polygon_soup_to_polygon_mesh(soup_points[i], soup_polygons[i], M); std::vector m1_only; From 77947bfe21bac585d162977dcf9b751e16c39ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Wed, 14 Oct 2026 14:01:14 +0200 Subject: [PATCH 21/35] compatibility with filesystem --- .../test/PMP_Boolean_operations/self_union.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp b/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp index c81c77ae21a5..da746faecc9e 100644 --- a/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp @@ -27,8 +27,8 @@ int main(int argc, char** argv) std::vector points; std::vector triangles; - const std::string filename = argc == 1 ? CGAL::data_file_path("meshes/elephant.off") - : std::string(argv[1]); + auto filename = argc == 1 ? CGAL::data_file_path("meshes/elephant.off") + : std::string(argv[1]); if (!CGAL::IO::read_polygon_soup(filename, points, triangles)) { From dbdb777cdf786c15cce2782d5f3c149cceb1398a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Wed, 14 Oct 2026 14:04:42 +0200 Subject: [PATCH 22/35] typo --- .../test/PMP_Boolean_operations/self_union.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp b/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp index da746faecc9e..2c941b2d95b4 100644 --- a/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/self_union.cpp @@ -27,8 +27,7 @@ int main(int argc, char** argv) std::vector points; std::vector triangles; - auto filename = argc == 1 ? CGAL::data_file_path("meshes/elephant.off") - : std::string(argv[1]); + auto filename = argc == 1 ? CGAL::data_file_path("meshes/elephant.off") : argv[1]; if (!CGAL::IO::read_polygon_soup(filename, points, triangles)) { From a73179ec6ea9057b4e716b262e0b0f5237e5b66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Wed, 14 Oct 2026 14:48:03 +0200 Subject: [PATCH 23/35] fix exact point positions --- .../include/CGAL/Polygon_mesh_processing/autorefinement.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h index 792faf35c6e7..a94a4f6c2742 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h @@ -1483,7 +1483,11 @@ bool autorefine_triangle_soup(PointRange& soup_points, Exact_point_range exact_soup_points; #endif - // TODO: parallel_for? + +#if ! defined(CGAL_NDEBUG) || defined(CGAL_DEBUG_PMP_AUTOREFINE) + exact_soup_points.resize(soup_points.size()); +#endif + // for input points, we on purpose keep duplicated points and isolated points const auto insert_point_id = [&](std::size_t pid) { @@ -1492,7 +1496,7 @@ bool autorefine_triangle_soup(PointRange& soup_points, #endif point_id_map.insert(std::make_pair(to_exact(get(pm,soup_points[pid])), pid)); #if ! defined(CGAL_NDEBUG) || defined(CGAL_DEBUG_PMP_AUTOREFINE) - exact_soup_points.push_back(insert_res.first->first); + exact_soup_points[pid]=insert_res.first->first; #endif }; #ifdef CGAL_LINKED_WITH_TBB From cb92dbcce861e7fcec1401467363d99e7eedb4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Wed, 14 Oct 2026 15:32:34 +0200 Subject: [PATCH 24/35] constexpr --- Property_map/include/CGAL/property_map.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Property_map/include/CGAL/property_map.h b/Property_map/include/CGAL/property_map.h index 113b46dda7fc..6d9ab6ad9111 100644 --- a/Property_map/include/CGAL/property_map.h +++ b/Property_map/include/CGAL/property_map.h @@ -47,6 +47,7 @@ struct Static_boolean_property_map typedef bool reference; typedef boost::read_write_property_map_tag category; + constexpr inline friend value_type get(Static_boolean_property_map, const key_type&) @@ -54,6 +55,7 @@ struct Static_boolean_property_map return default_value; } + constexpr inline friend void put(Static_boolean_property_map, const key_type&, value_type) From 1a65720fb340d38f657c51324343239a62c646b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Wed, 14 Oct 2026 16:42:37 +0200 Subject: [PATCH 25/35] update timers --- .../Polygon_mesh_processing/autorefinement.h | 48 ++++++++++--------- .../triangle_soup_boolean_operations_3.h | 33 ++++++++++--- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h index 792faf35c6e7..dfd0d87067b5 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h @@ -65,10 +65,6 @@ //#define CGAL_AUTOREFINE_DEBUG_COUNTERS //#define CGAL_AUTOREF_DEBUG_DEPTH -#ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS -#include -#endif - #if defined(CGAL_AUTOREFINE_DEBUG_COUNTERS) || defined(CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS) #include #endif @@ -1114,7 +1110,7 @@ bool autorefine_triangle_soup(PointRange& soup_points, #if defined(CGAL_AUTOREFINE_DEBUG_COUNTERS) || defined(CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS) timer.stop(); std::cout << "collecting intersecting pairs took (" << mode << ") " << timer.time() << "\n"; - t.reset(); + timer.reset(); #endif if (si_pairs.empty()) @@ -1182,7 +1178,7 @@ bool autorefine_triangle_soup(PointRange& soup_points, CGAL_PMP_AUTOREFINE_VERBOSE("compute intersections ("+std::to_string(si_pairs.size())+")"); #ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS - t.start(); + timer.start(); #endif std::set > intersecting_triangles; std::set > coplanar_triangles; @@ -1271,13 +1267,13 @@ bool autorefine_triangle_soup(PointRange& soup_points, } } #ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS - t.stop(); - std::cout << t.time() << " sec. for #2" << std::endl; - t.reset(); + timer.stop(); + std::cout << timer.time() << " sec. for #2" << std::endl; + timer.reset(); #endif #ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS - t.start(); + timer.start(); #endif // deduplicate inserted segments @@ -1383,9 +1379,9 @@ bool autorefine_triangle_soup(PointRange& soup_points, } #ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS - t.stop(); - std::cout << t.time() << " sec. for #3" << std::endl; - t.reset(); + timer.stop(); + std::cout << timer.time() << " sec. for #3" << std::endl; + timer.reset(); #endif CGAL_PMP_AUTOREFINE_VERBOSE("triangulate faces ("+std::to_string(triangles.size())+")"); @@ -1434,7 +1430,7 @@ bool autorefine_triangle_soup(PointRange& soup_points, }; #ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS - t.start(); + timer.start(); #endif #ifdef CGAL_LINKED_WITH_TBB if (parallel_execution) @@ -1453,9 +1449,10 @@ bool autorefine_triangle_soup(PointRange& soup_points, } #ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS - t.stop(); - std::cout << t.time() << " sec. for #1" << std::endl; - t.reset(); + timer.stop(); + std::cout << timer.time() << " sec. for #1" << std::endl; + timer.reset(); + timer.start(); #endif // brute force output: create a soup, orient and to-mesh @@ -1512,7 +1509,10 @@ bool autorefine_triangle_soup(PointRange& soup_points, } #ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS - std::cout << timer.time() << "s to fill point id map" << std::endl; + timer.stop(); + std::cout << timer.time() << " sec. to fill point id map" << std::endl; + timer.reset(); + timer.start(); #endif TriangleRange soup_triangles_out; @@ -1552,7 +1552,9 @@ bool autorefine_triangle_soup(PointRange& soup_points, } #ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS - std::cout << timer.time() << " to build the output" << std::endl; + timer.stop(); + std::cout << timer.time() << " sec. to build the output" << std::endl; + timer.reset(); #endif // import refined triangles @@ -1572,7 +1574,7 @@ bool autorefine_triangle_soup(PointRange& soup_points, }; #ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS - t.start(); + timer.start(); #endif std::size_t offset = soup_triangles_out.size(); @@ -1742,9 +1744,9 @@ bool autorefine_triangle_soup(PointRange& soup_points, #ifdef CGAL_AUTOREF_USE_DEBUG_PARALLEL_TIMERS - t.stop(); - std::cout << t.time() << " sec. for #4 (" << mode << ")" << std::endl; - t.reset(); + timer.stop(); + std::cout << timer.time() << " sec. for #4 (" << mode << ")" << std::endl; + timer.reset(); #endif #ifndef CGAL_NDEBUG diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 0656607939e2..551a6efcd09a 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -22,14 +22,16 @@ #include -#ifdef CGAL_BO3_TIMERS -#include -#endif +#include #ifdef CGAL_LINKED_WITH_TBB #include #endif +#if defined(CGAL_BO3_TIMERS) || defined(CGAL_BO3_SELECT_TIMERS) +#include +#endif + namespace CGAL { namespace boolops_3 { @@ -108,7 +110,7 @@ select(const std::vector& points, std::vector& input_ids, const unsigned int nb_meshes) { -#ifdef CGAL_BO3_TIMERS +#ifdef CGAL_BO3_SELECT_TIMERS Real_timer t; t.start(); #endif @@ -154,6 +156,10 @@ select(const std::vector& points, } } +#ifdef CGAL_BO3_SELECT_TIMERS + std::cout << t.time() << " sec. to collect edge records\n"; +#endif + #ifdef CGAL_LINKED_WITH_TBB if (std::is_same_v) { @@ -165,7 +171,11 @@ select(const std::vector& points, std::sort(partial_edges.begin(), partial_edges.end()); } - // TODO parallellize (need a mutex to push on small_vector) +#ifdef CGAL_BO3_SELECT_TIMERS + std::cout << t.time() << " sec. to sort edge records\n"; +#endif + + // Build adjacencies between edges and faces std::vector> adjacent_edges(triangles.size()); std::vector> adjacent_faces; adjacent_faces.reserve(partial_edges.size()); @@ -192,12 +202,20 @@ select(const std::vector& points, CGAL_assertion(!adjacent_faces.back().empty()); } +#ifdef CGAL_BO3_SELECT_TIMERS + std::cout << t.time() << " sec. to build adjacencies\n"; +#endif + // extract connected_components bounded by non-manifold edges std::vector& ccids = data_out.ccids; std::vector< EID > nm_edges; std::vector one_triangle_per_cc = boolops_3::connected_components(triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); std::size_t nb_cc = one_triangle_per_cc.size(); +#ifdef CGAL_BO3_SELECT_TIMERS + std::cout << t.time() << " sec. to compute connected components\n"; +#endif + // Erase duplicates std::sort(nm_edges.begin(), nm_edges.end()); nm_edges.erase(std::unique(nm_edges.begin(), nm_edges.end()), nm_edges.end()); @@ -542,8 +560,8 @@ select(const std::vector& points, data_out.input_ids.swap(input_ids); -#ifdef CGAL_BO3_TIMERS - std::cout << "select intern in " << t.time() << "\n"; +#ifdef CGAL_BO3_SELECT_TIMERS + std::cout << "select done in " << t.time() << "\n"; #endif return data_out; } @@ -615,6 +633,7 @@ prepare_data(const PointRange& points_1, const TriangleRange& triangles_1, timer.stop(); std::cout << "autorefine done in " << timer.time() << "\n"; timer.reset(); + timer.start(); #endif #ifdef CGAL_BO_AUTOREF_DEBUG CGAL::IO::write_OFF("autorefined.off", out_points, out_triangles, CGAL::parameters::stream_precision(17)); From 2814758ea18c38b5f58183d020a7cd476a65273a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Wed, 14 Oct 2026 16:43:56 +0200 Subject: [PATCH 26/35] Parallelize build connectivity --- .../triangle_soup_boolean_operations_3.h | 116 ++++++++++++++---- 1 file changed, 95 insertions(+), 21 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 551a6efcd09a..51c788d474cf 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -178,28 +178,102 @@ select(const std::vector& points, // Build adjacencies between edges and faces std::vector> adjacent_edges(triangles.size()); std::vector> adjacent_faces; - adjacent_faces.reserve(partial_edges.size()); - for (std::size_t i = 0; i < partial_edges.size(); ) + +#ifdef CGAL_LINKED_WITH_TBB + if constexpr(std::is_same_v) { - const PID a = std::get<0>(partial_edges[i]); - const PID b = std::get<1>(partial_edges[i]); - const std::size_t idx = adjacent_faces.size(); - adjacent_faces.emplace_back(); - CGAL_assertion(adjacent_faces.back().empty()); - do + // Cut partial_edges in blocks and compute the offset on edges for each block + const std::size_t block_size = 4096; + const std::size_t nb_blocks = (partial_edges.size() + block_size - 1) / block_size; + + std::vector counts(nb_blocks, 0); + + // count edge groups whose first record belongs to each block. + tbb::parallel_for(tbb::blocked_range(0, nb_blocks, 1), [&](const tbb::blocked_range& r) { - const TID tid = std::get<2>(partial_edges[i]); - adjacent_faces.back().push_back(tid); - ++i; + for (std::size_t bi = r.begin(); bi != r.end(); ++bi){ + const std::size_t begin = bi * block_size; + const std::size_t end = (std::min)(begin + block_size, partial_edges.size()); + + for (std::size_t i = begin; i < end; ++i) + if (i == 0 || std::get<0>(partial_edges[i]) != std::get<0>(partial_edges[i-1]) || + std::get<1>(partial_edges[i]) != std::get<1>(partial_edges[i-1])) + ++counts[bi]; + } + }); - for(std::size_t j = 0; j < 3; ++j) - if ((triangles[tid][j] == a && triangles[tid][(j+1)%3] == b) || - (triangles[tid][j] == b && triangles[tid][(j+1)%3] == a)){ - adjacent_edges[tid][(j+2)%3] = idx; - break; + // Compute offset for each block + std::vector offsets(nb_blocks+1, 0); + for (std::size_t i=0; i < nb_blocks; ++i) + offsets[i+1] = offsets[i] + counts[i]; + + adjacent_faces.resize(offsets.back()); + + tbb::parallel_for(tbb::blocked_range(0, nb_blocks, 1), [&](const tbb::blocked_range& r) + { + for (std::size_t bi = r.begin(); bi != r.end(); ++bi) + { + std::size_t begin = bi * block_size; + const std::size_t end = (std::min)(begin + block_size, partial_edges.size()); + std::size_t idx = offsets[bi]; + + // Skip a group that started in the previous block. + if(begin != 0) + while(std::get<0>(partial_edges[begin-1]) == std::get<0>(partial_edges[begin]) && + std::get<1>(partial_edges[begin-1]) == std::get<1>(partial_edges[begin])) + ++begin; + + for (std::size_t i = begin; i < end; ) + { + const PID a = std::get<0>(partial_edges[i]); + const PID b = std::get<1>(partial_edges[i]); + + auto& faces = adjacent_faces[idx]; + CGAL_assertion(faces.empty()); + + // may extend beyond the block's end, but is processed only once. + do + { + const TID tid = std::get<2>(partial_edges[i]); + const auto& t = triangles[tid]; + faces.push_back(tid); + + const std::size_t k = (t[0] == a || t[0] == b) ? ((t[1] == a || t[1] == b) ? 2 : 1) : 0; + CGAL_assertion((t[(k+1)%3] == a && t[(k+2)%3] == b) || (t[(k+1)%3] == b && t[(k+2)%3] == a)); + adjacent_edges[tid][k] = idx; + ++i; + } while (i < partial_edges.size() && std::get<0>(partial_edges[i]) == a && + std::get<1>(partial_edges[i]) == b); + ++idx; } - } while (i < partial_edges.size() && std::get<0>(partial_edges[i]) == a && std::get<1>(partial_edges[i]) == b); - CGAL_assertion(!adjacent_faces.back().empty()); + CGAL_assertion(idx == offsets[bi+1]); + } + }); + } + else +#endif + { + adjacent_faces.reserve(partial_edges.size()); + for (std::size_t i = 0; i < partial_edges.size(); ) + { + const PID a = std::get<0>(partial_edges[i]); + const PID b = std::get<1>(partial_edges[i]); + const std::size_t idx = adjacent_faces.size(); + adjacent_faces.emplace_back(); + CGAL_assertion(adjacent_faces.back().empty()); + do + { + const TID tid = std::get<2>(partial_edges[i]); + const auto& t = triangles[tid]; + adjacent_faces.back().push_back(tid); + ++i; + + std::size_t k = (t[0] == a || t[0] == b) ? ((t[1] == a || t[1] == b) ? 2 : 1) : 0; + CGAL_assertion((t[(k+1)%3] == a && t[(k+2)%3] == b) || (t[(k+1)%3] == b && t[(k+2)%3] == a)); + adjacent_edges[tid][k] = idx; + } while (i < partial_edges.size() && std::get<0>(partial_edges[i]) == a && std::get<1>(partial_edges[i]) == b); + CGAL_assertion(!adjacent_faces.back().empty()); + } } #ifdef CGAL_BO3_SELECT_TIMERS @@ -789,9 +863,9 @@ void compute_self_union(PointRange& points, TriangleRange& triangles, // Get the vertices associate to an edge auto edge_vertices = [&](EID eid) { std::size_t tid = adjacent_faces[eid].front(); - for(std::size_t k=0; k<3; ++k) - if(adjacent_edges[tid][k] == eid) - return std::make_pair(triangles[tid][(k+1)%3], triangles[tid][(k+2)%3]); + std::size_t k = adjacent_edges[tid][0] == eid ? 0 :adjacent_edges[tid][1] == eid ? 1 : 2; + CGAL_assertion(adjacent_edges[tid][k] == eid); + return std::make_pair(triangles[tid][(k+1)%3], triangles[tid][(k+2)%3]); }; From 659dc932c10424e417c0ea1393543ce41e808751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Thu, 15 Oct 2026 11:00:52 +0200 Subject: [PATCH 27/35] Parallelize the call of connected components using split_at_range --- .../triangle_soup_boolean_operations_3.h | 114 ++++++++++++++++-- 1 file changed, 101 insertions(+), 13 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 51c788d474cf..e3ee4869f73b 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -38,7 +38,11 @@ namespace boolops_3 { //TODO: update nm_edges for identical coplanar status (less test below then) template std::vector -connected_components(const Polygons& triangles, +connected_components(std::size_t begin, + std::size_t end, + int offset, + int increment, + const Polygons& triangles, const EdgeConnectivity& adjacent_edges, const FaceConnectivity& adjacent_faces, std::vector& ccids, @@ -47,12 +51,10 @@ connected_components(const Polygons& triangles, auto is_handled = [&](std::size_t tid){ return ccids[tid]!=std::size_t(-1); }; std::vector one_triangle_per_cc; - std::size_t nb_poly = triangles.size(); - ccids.resize(nb_poly, std::size_t(-1)); - std::size_t i=0; + std::size_t i=offset; std::vector queue; - for (std::size_t tid=0; tid +std::vector +connected_components(const Polygons& triangles, + const EdgeConnectivity& adjacent_edges, + const FaceConnectivity& adjacent_faces, + std::vector& ccids, + NM_edges& nm_edges) +{ + std::size_t nb_poly = triangles.size(); + ccids.resize(nb_poly, std::size_t(-1)); + return connected_components(0, nb_poly, 0, 1, triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); +} + +#ifdef CGAL_LINKED_WITH_TBB +template +std::vector +parallel_connected_components(const size_t split_range_at, + const size_t new_triangles_at, + const Polygons& triangles, + const EdgeConnectivity& adjacent_edges, + const FaceConnectivity& adjacent_faces, + std::vector& ccids, + NM_edges& nm_edges) +{ + std::size_t nb_poly = triangles.size(); + ccids.resize(nb_poly, std::size_t(-1)); + std::vector trs_per_cc_1, trs_per_cc_2, trs_per_cc_end, merged_trs_per_cc; + + tbb::task_group task_group; + task_group.run([&](){ + trs_per_cc_1 = connected_components(0, split_range_at, 0, 2, triangles,adjacent_edges, adjacent_faces, ccids, nm_edges); + }); + NM_edges nm_edges_cc_2; + trs_per_cc_2 = connected_components(split_range_at, new_triangles_at, 1, 2, triangles, adjacent_edges, adjacent_faces, ccids, nm_edges_cc_2); + task_group.wait(); + nm_edges.insert(nm_edges.end(), nm_edges_cc_2.begin(), nm_edges_cc_2.end()); + std::size_t max_size = (std::max)(trs_per_cc_1.size(), trs_per_cc_2.size()); + trs_per_cc_end = connected_components(new_triangles_at, nb_poly, max_size, 1, triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); + + // New triangles are + CGAL_assertion_code( + for(std::size_t i=new_triangles_at; i ccids; @@ -108,6 +165,13 @@ Selection_data select(const std::vector& points, const std::vector& triangles, std::vector& input_ids, +#ifdef CGAL_LINKED_WITH_TBB + std::size_t split_at, + std::size_t new_triangles_at, +#else + std::size_t /* split_at */, + std::size_t /* new_triangles_at */, +#endif const unsigned int nb_meshes) { #ifdef CGAL_BO3_SELECT_TIMERS @@ -283,7 +347,17 @@ select(const std::vector& points, // extract connected_components bounded by non-manifold edges std::vector& ccids = data_out.ccids; std::vector< EID > nm_edges; - std::vector one_triangle_per_cc = boolops_3::connected_components(triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); +#ifdef CGAL_LINKED_WITH_TBB + std::vector one_triangle_per_cc; + if constexpr(std::is_same_v) + { + one_triangle_per_cc = boolops_3::parallel_connected_components(split_at, new_triangles_at, triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); + } + else +#endif + { + one_triangle_per_cc = boolops_3::connected_components(triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); + } std::size_t nb_cc = one_triangle_per_cc.size(); #ifdef CGAL_BO3_SELECT_TIMERS @@ -313,6 +387,12 @@ select(const std::vector& points, std::vector& coplanar_patches_for_union_and_intersection=data_out.coplanar_patches_for_union_and_intersection; std::vector< std::vector >& cc_is_inside_per_mesh=data_out.cc_is_inside_per_mesh; + for(std::size_t cc=0; cc input_ids(nbt_1+nbt_2, 0); for (std::size_t i=nbt_1; i(out_points, out_triangles, input_ids, 2); + auto res =select(out_points, out_triangles, input_ids, nbt_1, nbt_1+nbt_2, 2); #ifdef CGAL_BO3_TIMERS timer.stop(); std::cout << "select done in " << timer.time() << "\n"; @@ -850,7 +938,7 @@ void compute_self_union(PointRange& points, TriangleRange& triangles, } while (i < partial_edges.size() && std::get<0>(partial_edges[i]) == a && std::get<1>(partial_edges[i]) == b); } - // extract connected_components bounded by non-manifold edges (TODO: move to a function) + // extract connected_components bounded by non-manifold edges std::vector ccids; std::vector nm_edges; std::vector one_triangle_per_cc = From 9f1f37c5d303c16900b4b3d0335627ca5990a5e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Thu, 15 Oct 2026 11:26:25 +0200 Subject: [PATCH 28/35] Move misplace #ifdef CGAL_LINKED_WITH_TBB --- .../triangle_soup_boolean_operations_3.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index e3ee4869f73b..24e373afc7ad 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -347,8 +347,8 @@ select(const std::vector& points, // extract connected_components bounded by non-manifold edges std::vector& ccids = data_out.ccids; std::vector< EID > nm_edges; -#ifdef CGAL_LINKED_WITH_TBB std::vector one_triangle_per_cc; +#ifdef CGAL_LINKED_WITH_TBB if constexpr(std::is_same_v) { one_triangle_per_cc = boolops_3::parallel_connected_components(split_at, new_triangles_at, triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); From 8d50e1eb3c4c1705d5acb8dc42a3ba813bc1ff1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Thu, 15 Oct 2026 11:56:07 +0200 Subject: [PATCH 29/35] Remove leftover assertion --- .../triangle_soup_boolean_operations_3.h | 5 ----- 1 file changed, 5 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 24e373afc7ad..9483372e64d9 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -135,11 +135,6 @@ parallel_connected_components(const size_t split_range_at, std::size_t max_size = (std::max)(trs_per_cc_1.size(), trs_per_cc_2.size()); trs_per_cc_end = connected_components(new_triangles_at, nb_poly, max_size, 1, triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); - // New triangles are - CGAL_assertion_code( - for(std::size_t i=new_triangles_at; i Date: Thu, 15 Oct 2026 12:45:20 +0200 Subject: [PATCH 30/35] Remove now unused argument --- .../triangle_soup_boolean_operations_3.h | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index 9483372e64d9..c838337d163c 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -36,13 +36,12 @@ namespace CGAL { namespace boolops_3 { //TODO: update nm_edges for identical coplanar status (less test below then) -template +template std::vector connected_components(std::size_t begin, std::size_t end, int offset, int increment, - const Polygons& triangles, const EdgeConnectivity& adjacent_edges, const FaceConnectivity& adjacent_faces, std::vector& ccids, @@ -96,44 +95,42 @@ connected_components(std::size_t begin, return one_triangle_per_cc; } -template +template std::vector -connected_components(const Polygons& triangles, - const EdgeConnectivity& adjacent_edges, +connected_components(const EdgeConnectivity& adjacent_edges, const FaceConnectivity& adjacent_faces, std::vector& ccids, NM_edges& nm_edges) { - std::size_t nb_poly = triangles.size(); + std::size_t nb_poly = adjacent_edges.size(); ccids.resize(nb_poly, std::size_t(-1)); - return connected_components(0, nb_poly, 0, 1, triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); + return connected_components(0, nb_poly, 0, 1, adjacent_edges, adjacent_faces, ccids, nm_edges); } #ifdef CGAL_LINKED_WITH_TBB -template +template std::vector parallel_connected_components(const size_t split_range_at, const size_t new_triangles_at, - const Polygons& triangles, const EdgeConnectivity& adjacent_edges, const FaceConnectivity& adjacent_faces, std::vector& ccids, NM_edges& nm_edges) { - std::size_t nb_poly = triangles.size(); + std::size_t nb_poly = adjacent_edges.size(); ccids.resize(nb_poly, std::size_t(-1)); std::vector trs_per_cc_1, trs_per_cc_2, trs_per_cc_end, merged_trs_per_cc; tbb::task_group task_group; task_group.run([&](){ - trs_per_cc_1 = connected_components(0, split_range_at, 0, 2, triangles,adjacent_edges, adjacent_faces, ccids, nm_edges); + trs_per_cc_1 = connected_components(0, split_range_at, 0, 2, adjacent_edges, adjacent_faces, ccids, nm_edges); }); NM_edges nm_edges_cc_2; - trs_per_cc_2 = connected_components(split_range_at, new_triangles_at, 1, 2, triangles, adjacent_edges, adjacent_faces, ccids, nm_edges_cc_2); + trs_per_cc_2 = connected_components(split_range_at, new_triangles_at, 1, 2, adjacent_edges, adjacent_faces, ccids, nm_edges_cc_2); task_group.wait(); nm_edges.insert(nm_edges.end(), nm_edges_cc_2.begin(), nm_edges_cc_2.end()); std::size_t max_size = (std::max)(trs_per_cc_1.size(), trs_per_cc_2.size()); - trs_per_cc_end = connected_components(new_triangles_at, nb_poly, max_size, 1, triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); + trs_per_cc_end = connected_components(new_triangles_at, nb_poly, max_size, 1, adjacent_edges, adjacent_faces, ccids, nm_edges); merged_trs_per_cc.resize(2 * max_size + trs_per_cc_end.size(), std::size_t(-1)); for(std::size_t i=0; i& points, #ifdef CGAL_LINKED_WITH_TBB if constexpr(std::is_same_v) { - one_triangle_per_cc = boolops_3::parallel_connected_components(split_at, new_triangles_at, triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); + one_triangle_per_cc = boolops_3::parallel_connected_components(split_at, new_triangles_at, adjacent_edges, adjacent_faces, ccids, nm_edges); } else #endif { - one_triangle_per_cc = boolops_3::connected_components(triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); + one_triangle_per_cc = boolops_3::connected_components(adjacent_edges, adjacent_faces, ccids, nm_edges); } std::size_t nb_cc = one_triangle_per_cc.size(); From c4677c31eecdc0edfd7817fda92dcfbf870fea0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Thu, 15 Oct 2026 14:10:45 +0200 Subject: [PATCH 31/35] Transfer point map to compute_intersecting_pairs --- .../Polygon_mesh_processing/autorefinement.h | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h index 276bef3800ed..6801f7265468 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/autorefinement.h @@ -975,9 +975,10 @@ void generate_subtriangles(std::size_t ti, namespace autorefine_impl{ // -template +template void compute_intersecting_pairs(const PointRange& points, const PolygonRange& triangles, - std::size_t split_id, std::vector& si_pairs) + std::size_t split_id, std::vector& si_pairs, + const PointMap& pm) { typedef CGAL::Box_intersection_d::ID_FROM_BOX_ADDRESS Box_policy; typedef CGAL::Box_intersection_d::Box_with_info_d Box; @@ -989,7 +990,7 @@ void compute_intersecting_pairs(const PointRange& points, const PolygonRange& tr boxes_1_ptr.reserve(boxes_1.size()); for (std::size_t i=0;i& collecter=si_pairs; #endif - auto callback = [&points, &triangles, &collecter] + auto callback = [&points, &triangles, &collecter, &pm] (const Box* b1, const Box* b2) { std::size_t i1=b1->info(), i2=b2->info(); - if (do_intersect(typename Kernel::Triangle_3(points[triangles[i1][0]],points[triangles[i1][1]],points[triangles[i1][2]]), - typename Kernel::Triangle_3(points[triangles[i2][0]],points[triangles[i2][1]],points[triangles[i2][2]]))) + if (do_intersect(typename Kernel::Triangle_3(get(pm, points[triangles[i1][0]]),get(pm, points[triangles[i1][1]]),get(pm, points[triangles[i1][2]])), + typename Kernel::Triangle_3(get(pm, points[triangles[i2][0]]),get(pm, points[triangles[i2][1]]),get(pm, points[triangles[i2][2]])))) { collecter.emplace_back(i1,i2); } @@ -1102,7 +1103,7 @@ bool autorefine_triangle_soup(PointRange& soup_points, if constexpr(!parameters::is_default_parameter::value) { std::size_t split_id=get_parameter(np, internal_np::split_triangle_range_at); - compute_intersecting_pairs(soup_points, soup_triangles, split_id, si_pairs); + compute_intersecting_pairs(soup_points, soup_triangles, split_id, si_pairs, pm); } else triangle_soup_self_intersections(soup_points, soup_triangles, std::back_inserter(si_pairs), np); From b4e8c1887ce6a0abeb618634125e5cfcd301ce16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valque=20L=C3=A9o?= Date: Thu, 15 Oct 2026 16:00:58 +0200 Subject: [PATCH 32/35] Correct argument error in calling connected_components --- .../triangle_soup_boolean_operations_3.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h index c838337d163c..7311f36e4354 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/triangle_soup_boolean_operations_3.h @@ -934,7 +934,7 @@ void compute_self_union(PointRange& points, TriangleRange& triangles, std::vector ccids; std::vector nm_edges; std::vector one_triangle_per_cc = - boolops_3::connected_components(triangles, adjacent_edges, adjacent_faces, ccids, nm_edges); + boolops_3::connected_components(adjacent_edges, adjacent_faces, ccids, nm_edges); std::size_t nb_cc = one_triangle_per_cc.size(); std::sort(nm_edges.begin(), nm_edges.end()); From 15898c23da9854b791fa8e1bf68e88b592cb523c Mon Sep 17 00:00:00 2001 From: Andreas Fabri Date: Sat, 17 Oct 2026 08:08:36 +0100 Subject: [PATCH 33/35] When windows.h is included Polygon is #defined --- .../test_corefinement_vs_autorefine.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp index 059459f6224b..4cb019011630 100644 --- a/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp +++ b/PMP_Boolean_operations/test/PMP_Boolean_operations/test_corefinement_vs_autorefine.cpp @@ -23,8 +23,8 @@ namespace CFR = PMP::Corefinement; using Kernel = CGAL::Exact_predicates_exact_constructions_kernel; using Point_3 = Kernel::Point_3; using Mesh = CGAL::Surface_mesh; -using Polygon = std::vector; -using Polygons = std::vector; +using Polygon_3 = std::vector; +using Polygons = std::vector; using Points = std::vector; using face_descriptor = boost::graph_traits::face_descriptor; From db0cde1486607ee59153aad0e7ddb082a21046e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Mon, 19 Oct 2026 09:38:44 +0200 Subject: [PATCH 34/35] filepath --- .../examples/PMP_Boolean_operations/soup_union.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp b/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp index b05f0899bbf8..7cf9871c0ad4 100644 --- a/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp +++ b/PMP_Boolean_operations/examples/PMP_Boolean_operations/soup_union.cpp @@ -14,9 +14,9 @@ namespace PMP = CGAL::Polygon_mesh_processing; int main(int argc, char* argv[]) { - const std::string filename1 = + const auto filename1 = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/blobby.off"); - const std::string filename2 = + const auto filename2 = (argc > 2) ? argv[2] : CGAL::data_file_path("meshes/eight.off"); std::vector points1, points2; From e0881bc1dd109659bd1feb1e416c30d26c9a206d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Mon, 19 Oct 2026 09:39:48 +0200 Subject: [PATCH 35/35] add constexpr for default property map --- .../internal/Corefinement/face_graph_utils.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/face_graph_utils.h b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/face_graph_utils.h index e65b13ad5b11..08b3872a0587 100644 --- a/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/face_graph_utils.h +++ b/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/internal/Corefinement/face_graph_utils.h @@ -34,13 +34,16 @@ template struct No_mark { friend + constexpr bool get(No_mark, typename boost::graph_traits::edge_descriptor) { return false; } - friend void put(No_mark, - typename boost::graph_traits::edge_descriptor, bool) + friend + constexpr + void put(No_mark, + typename boost::graph_traits::edge_descriptor, bool) {} };