Files
cgal/PMP_Boolean_operations/include/CGAL/Polygon_mesh_processing/kernel.h
T

510 lines
22 KiB
C++

// Copyright (c) 2025 GeometryFactory (France).
// All rights reserved.
//
// This file is part of CGAL (www.cgal.org).
//
// $URL$
// $Id$
// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial
//
//
// Author(s) : Sebastien Loriot, Léo Valque
#ifndef CGAL_POLYGON_MESH_PROCESSING_KERNEL_H
#define CGAL_POLYGON_MESH_PROCESSING_KERNEL_H
#include <CGAL/license/Polygon_mesh_processing/corefinement.h>
#include <CGAL/Polygon_mesh_processing/clip.h>
#include <CGAL/Polygon_mesh_processing/internal/clip_convex.h>
#include <CGAL/Polygon_mesh_processing/internal/Three_point_cut_plane_traits.h>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/Cartesian_converter.h>
#include <boost/property_map/property_map.hpp>
#include <algorithm>
#include <array>
#include <functional>
#include <iterator>
#include <optional>
#include <random>
namespace CGAL {
namespace Polygon_mesh_processing {
namespace internal {
template <typename PolygonMesh,
typename FaceRange,
typename NamedParameters = parameters::Default_named_parameters,
typename NamedParametersOut = parameters::Default_named_parameters>
void
kernel(const FaceRange& face_range,
const PolygonMesh& pm,
PolygonMesh& kernel,
const NamedParameters& np = parameters::default_values(),
const NamedParametersOut& np_out = parameters::default_values(),
bool used_to_find_a_point = false,
std::optional<typename GetGeomTraits<PolygonMesh, NamedParameters>::type::Point_3> *p = nullptr)
{
using parameters::choose_parameter;
using parameters::get_parameter;
using parameters::is_default_parameter;
// graph typedefs
using BGT = boost::graph_traits<PolygonMesh>;
using face_descriptor = typename BGT::face_descriptor;
// using edge_descriptor = typename BGT::edge_descriptor;
using halfedge_descriptor = typename BGT::halfedge_descriptor;
using vertex_descriptor = typename BGT::vertex_descriptor;
using GT = typename GetGeomTraits<PolygonMesh, NamedParameters>::type;
using EK = Exact_predicates_exact_constructions_kernel;
using K2EK = Cartesian_converter<GT, EK>;
using EK2K = Cartesian_converter<EK, GT>;
K2EK to_exact;
EK2K from_exact;
auto vpm = choose_parameter(get_parameter(np, internal_np::vertex_point),
get_const_property_map(vertex_point, pm));
auto vpm_out = choose_parameter(get_parameter(np_out, internal_np::vertex_point),
get_property_map(vertex_point, kernel));
using DefaultF2FMap = Constant_property_map<face_descriptor, face_descriptor>;
constexpr bool is_face_to_face_map = !parameters::is_default_parameter<NamedParametersOut, internal_np::face_to_face_map_t>::value;
auto f2f_map = choose_parameter<DefaultF2FMap>(get_parameter(np_out, internal_np::face_to_face_map));
using Point_3 = typename GT::Point_3;
using EPoint_3 = typename EK::Point_3;
using EVector_3 = typename EK::Vector_3;
using Plane_3 = typename Three_point_cut_plane_traits<EK>::Plane_3;
using KernelPointMap = typename boost::property_map<PolygonMesh, dynamic_vertex_property_t<EPoint_3> >::type;
bool bbox_filtering = choose_parameter(get_parameter(np, internal_np::use_bounding_box_filtering), true);
bool shuffle_planes = choose_parameter(get_parameter(np, internal_np::shuffle_planes), true);
bool check_euler_characteristic = !choose_parameter(get_parameter(np, internal_np::allow_open_input), false) && std::size_t(std::distance(face_range.begin(), face_range.end()))==faces(pm).size();
unsigned seed = choose_parameter(get_parameter(np, internal_np::random_seed), unsigned(-1));
auto rng = is_default_parameter<NamedParameters, internal_np::random_seed_t>::value ? std::default_random_engine(): std::default_random_engine(seed);
// Immediate exit if the input is well-formed and not of genus zero
if(check_euler_characteristic && (vertices(pm).size() - edges(pm).size() + faces(pm).size() != 2)){
clear(kernel);
return;
}
// Build the starting cube
KernelPointMap kvpm = get(CGAL::dynamic_vertex_property_t<EPoint_3>(), kernel);
if(is_empty(kernel))
make_hexahedron(bbox(pm), kernel, parameters::vertex_point_map(vpm_out));
for(vertex_descriptor v: vertices(kernel))
put(kvpm, v, to_exact(get(vpm_out, v)));
Bbox_3 bb3 = bbox(kernel);
vertex_descriptor start_vertex = *vertices(kernel).begin();
if constexpr(is_face_to_face_map)
for(face_descriptor f: faces(kernel))
put(f2f_map, f, BGT::null_face());
std::array<vertex_descriptor, 6> bbox_vertices;
if(bbox_filtering){
// We compute and store the vertices that realized the bbox
struct Bbox_entry {
std::size_t index;
std::function<double(const EPoint_3&)> bound;
std::function<double(const Bbox_3&)> value;
};
std::array<Bbox_entry,6> entries {{
{0, [](const EPoint_3& p){ return to_interval(p.x()).first; }, [](const Bbox_3& b){ return b.xmin(); }},
{1, [](const EPoint_3& p){ return to_interval(p.x()).second; }, [](const Bbox_3& b){ return b.xmax(); }},
{2, [](const EPoint_3& p){ return to_interval(p.y()).first; }, [](const Bbox_3& b){ return b.ymin(); }},
{3, [](const EPoint_3& p){ return to_interval(p.y()).second; }, [](const Bbox_3& b){ return b.ymax(); }},
{4, [](const EPoint_3& p){ return to_interval(p.z()).first; }, [](const Bbox_3& b){ return b.zmin(); }},
{5, [](const EPoint_3& p){ return to_interval(p.z()).second; }, [](const Bbox_3& b){ return b.zmax(); }}
}};
for (const auto& e : entries){
for (vertex_descriptor v : vertices(kernel)){
std::size_t i = e.index;
double bound = e.bound(get(kvpm, v));
if (bound == e.value(bb3)){
bbox_vertices[i] = v;
break;
}
}
}
}
// Get the planes and possibly shuffle them
Three_point_cut_plane_traits<EK> kgt;
auto oriented_side = kgt.oriented_side_3_object();
auto orthogonal_vector = kgt.construct_orthogonal_vector_3_object();
std::vector<face_descriptor> planes(face_range.begin(), face_range.end());
if(shuffle_planes)
std::shuffle(planes.begin(), planes.end(), rng);
// Cut iteratively the temporary kernel by halfspaces
for(auto f: planes){
auto h = halfedge(f, pm);
Plane_3 plane(to_exact(get(vpm,source(h, pm))),
to_exact(get(vpm,target(h, pm))),
to_exact(get(vpm,target(next(h, pm), pm))));
if(plane.is_degenerate())
continue;
if(bbox_filtering && vertices(kernel).size() >= 3 && faces(kernel).size()>1){
// Early exit if the plane does not cut the bbox of the temporary kernel
// By looking the sign of the plane value, we can check only two corners
EVector_3 normal = orthogonal_vector(plane);
// Look extreme corner according to the plane normal
EPoint_3 corner( is_positive(normal.x())?bb3.xmax():bb3.xmin(),
is_positive(normal.y())?bb3.ymax():bb3.ymin(),
is_positive(normal.z())?bb3.zmax():bb3.zmin());
if(oriented_side(plane, corner) != ON_POSITIVE_SIDE)
continue;
// Look the opposite corner
EPoint_3 opposite_corner( is_positive(normal.x())?bb3.xmin():bb3.xmax(),
is_positive(normal.y())?bb3.ymin():bb3.ymax(),
is_positive(normal.z())?bb3.zmin():bb3.zmax());
if(oriented_side(plane, opposite_corner) == ON_POSITIVE_SIDE){
clear(kernel); // empty
return;
}
if constexpr(is_face_to_face_map)
start_vertex = clip_convex(kernel, plane, CGAL::parameters::clip_volume(true).
geom_traits(kgt).
do_not_triangulate_faces(true).
vertex_point_map(kvpm).
bounding_box(&bbox_vertices).
starting_vertex_descriptor(start_vertex).
face_to_face_map(f2f_map),
f);
else
start_vertex = clip_convex(kernel, plane, CGAL::parameters::clip_volume(true).
geom_traits(kgt).
do_not_triangulate_faces(true).
vertex_point_map(kvpm).
bounding_box(&bbox_vertices).
starting_vertex_descriptor(start_vertex));
if (is_empty(kernel)) return;
// update bbox, ( By looking which bbox_vertices have changed, it is possible to avoid recomputing all of them at each step )
bb3 = get(kvpm, bbox_vertices[0]).bbox()+get(kvpm, bbox_vertices[1]).bbox()+get(kvpm, bbox_vertices[2]).bbox()+
get(kvpm, bbox_vertices[3]).bbox()+get(kvpm, bbox_vertices[4]).bbox()+get(kvpm, bbox_vertices[5]).bbox();
}
else
{
if constexpr(is_face_to_face_map)
start_vertex = clip_convex(kernel, plane, CGAL::parameters::clip_volume(true).
geom_traits(kgt).
do_not_triangulate_faces(true).
vertex_point_map(kvpm).
starting_vertex_descriptor(start_vertex).
face_to_face_map(f2f_map),
f);
else
start_vertex = clip_convex(kernel, plane, CGAL::parameters::clip_volume(true).
geom_traits(kgt).
do_not_triangulate_faces(true).
vertex_point_map(kvpm).
starting_vertex_descriptor(start_vertex));
if (is_empty(kernel)) return;
}
}
if(used_to_find_a_point){
// Get the centroid
EPoint_3 centroid(ORIGIN);
for(auto v: vertices(kernel))
centroid += EVector_3(ORIGIN, get(kvpm, v)) / vertices(kernel).size();
// Approximate the centroid
Point_3 double_centroid(to_double(centroid.x()), to_double(centroid.y()), to_double(centroid.z()));
// Check if the approximate_centroid is inside the kernel
bool is_valid = true;
for(face_descriptor f: faces(kernel)){
halfedge_descriptor h = halfedge(f, kernel);
Plane_3 plane(get(kvpm,source(h, kernel)),
get(kvpm,target(h, kernel)),
get(kvpm,target(next(h, kernel), kernel)));
if(oriented_side(plane, centroid) != ON_NEGATIVE_SIDE){
is_valid = false;
break;
}
}
// If not, refine the centroid position
if(!is_valid)
centroid.exact();
// Return the centroid
*p = from_exact(centroid);
return;
}
// Convert points of the kernel to the type of the input mesh
for(vertex_descriptor v : vertices(kernel))
put(vpm_out, v, from_exact(get(kvpm, v)));
};
} // namespace internal
/**
* \ingroup PMP_kernel_grp
*
* \brief computes the kernel of the given faces of a polygon mesh.
*
* The kernel is defined as the convex polyhedron that is the intersection
* of all the halfspaces on the negative side of the oriented planes defined by a range of faces
* of the input mesh. The kernel may be empty or degenerate to a lower-dimensional convex shape.
*
* In the implementation, a starting shape is iteratively clipped by the faces.
* By default, the bounding box of the input mesh is used as starting shape.
* However, the parameter `out` may be non-empty: In this case, it must be a convex polyhedron and will be used as starting shape.
*
* The algorithm assumes that the faces of the input range form a closed surface as to perform a quick exit if the genus is non-zero.
* This precondition can be relaxed using the named parameter `allow_open_input`.
* In that case, the resulting kernel may contain faces of the starting shape.
*
* In case of a degenerate kernel:
* <ul>
* <li>If the dimension of the kernel is `2` (i.e., the kernel is a convex polygon in 3D), the output mesh consists of a single face.</li>
* <li>If the dimension of the kernel is `1` (i.e., the kernel is a line segment), the output mesh consists two isolated vertices.</li>
* <li>If the dimension of the kernel is `0` (i.e., the kernel is a single point), the output mesh contains one isolated vertex.</li>
* </ul>
*
* @tparam FaceRange a model of `ConstRange` with `boost::graph_traits<PolygonMesh>::%face_descriptor` as value type
* @tparam PolygonMesh a model of `VertexListGraph`, `HalfedgeListGraph` and `FaceListGraph`
* @tparam PolygonMeshOut a model of `MutableFaceGraph`, `VertexListGraph` and `FaceListGraph`
*
* @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters"
* @tparam NamedParametersOut a sequence of \ref bgl_namedparameters "Named Parameters"
*
* @param face_range the range of faces used
* @param pm input surface mesh
* @param out output surface mesh
* @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below
*
* \cgalNamedParamsBegin
* \cgalParamNBegin{allow_open_input}
* \cgalParamDescription{If set to `true`, the input mesh is allowed to have boundaries.}
* \cgalParamType{Boolean}
* \cgalParamDefault{`false`}
* \cgalParamNEnd
*
* \cgalParamNBegin{vertex_point_map}
* \cgalParamDescription{a property map associating points to the vertices of `pm`}
* \cgalParamType{a class model of `ReadWritePropertyMap` with `boost::graph_traits<PolygonMesh>::%vertex_descriptor`
* as key type and `%Point_3` as value type}
* \cgalParamDefault{`boost::get(CGAL::vertex_point, pm)`}
* \cgalParamExtra{If this parameter is omitted, an internal property map for `CGAL::vertex_point_t` must be available in PolygonMesh. }
* \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`}
* \cgalParamNEnd
*
* \cgalParamNBegin{random_seed}
* \cgalParamDescription{is used to initialize the random number generator of the algorithm.}
* \cgalParamType{unsigned int}
* \cgalParamDefault{use `std::default_random_engine()`}
* \cgalParamNEnd
*
* \cond SKIP_IN_MANUAL
*
* \cgalParamNBegin{use_bounding_box_filtering}
* \cgalParamDescription{Enables the use of the bounding box of the temporary kernel to compute the intersection of a plane with it, improving runtime in most scenarios.}
* \cgalParamType{Boolean}
* \cgalParamDefault{`true`}
* \cgalParamNEnd
*
* \cgalParamNBegin{shuffle_planes}
* \cgalParamDescription{If set to `true`, the planes are considered in a random order to compute the kernel, improving runtime in most scenarios.}
* \cgalParamType{Boolean}
* \cgalParamDefault{`true`}
* \cgalParamNEnd
*
* \cgalParamNBegin{visitor}
* \cgalParamDescription{a visitor used to track the creation of new faces, edges, and faces.
* Note that as there is no mesh associated with `plane`,
* `boost::graph_traits<PolygonMesh>::null_halfedge()` and `boost::graph_traits<PolygonMesh>::null_face()` will be used when calling
* functions of the visitor expecting a halfedge or a face from `plane`. Similarly, `pm` will be used as the mesh of `plane`.}
* \cgalParamType{a class model of `PMPCorefinementVisitor`}
* \cgalParamDefault{`Corefinement::Default_visitor<PolygonMesh>`}
* \cgalParamNEnd
*
* \endcond
* \cgalNamedParamsEnd
*
* @param np_out an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below
*
* \cgalNamedParamsBegin
* \cgalParamNBegin{vertex_point_map}
* \cgalParamDescription{a property map associating points to the vertices of `out`}
* \cgalParamType{a class model of `ReadWritePropertyMap` with `boost::graph_traits<PolygonMesh>::%vertex_descriptor`
* as key type and `%Point_3` as value type}
* \cgalParamDefault{`boost::get(CGAL::vertex_point, out)`}
* \cgalParamExtra{If this parameter is omitted, an internal property map for `CGAL::vertex_point_t` must be available in PolygonMesh. }
* \cgalParamNEnd
* \cgalParamNBegin{face_to_face_map}
* \cgalParamDescription{a property map storing, for each face of the output mesh, a face of the input mesh that defined the clipping plane that created it
(or `boost::graph_traits<PolygonMeshOut>::%null_face` if the face belongs to the starting shape)}
* \cgalParamType{a class model of `ReadWritePropertyMap` with
* `boost::graph_traits<PolygonMeshOut>::%face_descriptor` as key type and
* `boost::graph_traits<PolygonMesh>::%face_descriptor` as value type}
* \cgalParamDefault{unused}
* \cgalParamNEnd
* \cgalNamedParamsEnd
*/
template <typename FaceRange,
typename PolygonMesh,
typename NamedParameters = parameters::Default_named_parameters,
typename NamedParametersOut = parameters::Default_named_parameters>
void
kernel(const FaceRange& face_range,
const PolygonMesh& pm,
PolygonMesh& out,
const NamedParameters& np = parameters::default_values(),
const NamedParametersOut& np_out = parameters::default_values())
{
internal::kernel(face_range, pm, out, np, np_out);
}
/**
* \ingroup PMP_kernel_grp
*
* \brief computes the kernel of the given polygon mesh.
*
* This is a convenience overload that calls the overload above
* on all faces of the mesh.
*/
template <typename PolygonMesh,
typename NamedParameters = parameters::Default_named_parameters,
typename NamedParametersOut = parameters::Default_named_parameters>
void
kernel(const PolygonMesh& pm,
PolygonMesh& out,
const NamedParameters& np = parameters::default_values(),
const NamedParametersOut& np_out = parameters::default_values())
{
kernel(faces(pm), pm, out, np, np_out);
}
/**
* \ingroup PMP_kernel_grp
*
* \brief indicates whether the kernel of the given faces of a polygon mesh is empty.
*
* The kernel is defined as the convex polyhedron that is the intersection
* of all the halfspaces on the negative side of the oriented planes defined by a range of faces
* of the input mesh.
*
* See `CGAL::Polygon_mesh_processing::kernel()` for a comprehensive description of the parameters.
*/
template <typename FaceRange,
typename PolygonMesh,
typename CGAL_NP_TEMPLATE_PARAMETERS>
bool has_empty_kernel(const FaceRange& face_range,
const PolygonMesh& pm,
const CGAL_NP_CLASS& np = parameters::default_values())
{
PolygonMesh k;
kernel(face_range, pm, k, np);
return is_empty(k);
}
/**
* \ingroup PMP_kernel_grp
*
* \brief indicates whether the kernel of the given polygon mesh is empty.
*
* The kernel is defined as the convex polyhedron that is the intersection
* of all the halfspaces on the negative side of the oriented planes defined by a range of faces
* of the input mesh.
*
* See `CGAL::Polygon_mesh_processing::kernel()` for a comprehensive description of the parameters.
*/
template <typename PolygonMesh,
typename CGAL_NP_TEMPLATE_PARAMETERS>
bool has_empty_kernel(const PolygonMesh& pm,
const CGAL_NP_CLASS& np = parameters::default_values())
{
return has_empty_kernel(faces(pm), pm, np);
}
/**
* \ingroup PMP_kernel_grp
*
* \brief returns a point inside the kernel of the given faces of a polygon mesh.
*
* The kernel is defined as the convex polyhedron that is the intersection
* of all the halfspaces on the negative side of the oriented planes defined by a range of faces
* of the input mesh.
*
* See `CGAL::Polygon_mesh_processing::kernel()` for a comprehensive description of the parameters.
*
* \return `std::nullopt` if and only if the kernel is empty.
*/
template <typename FaceRange,
typename PolygonMesh,
typename NamedParameters = parameters::Default_named_parameters>
#ifdef DOXYGEN_RUNNING
std::optional<Point_3>
#else
std::optional<typename GetGeomTraits<PolygonMesh, NamedParameters>::type::Point_3>
#endif
kernel_point(const FaceRange& face_range,
const PolygonMesh& pm,
const NamedParameters& np = parameters::default_values())
{
std::optional<typename GetGeomTraits<PolygonMesh, NamedParameters>::type::Point_3> res;
PolygonMesh k;
internal::kernel(face_range, pm, k, np, parameters::default_values(), true, &res);
// If the kernel is empty or degenerated with strictly inside option, return empty
if(is_empty(k))
return std::nullopt;
return res;
}
/**
* \ingroup PMP_kernel_grp
*
* \brief returns a point inside the kernel of the given polygon mesh.
*
* The kernel is defined as the convex polyhedron that is the intersection
* of all the halfspaces on the negative side of the oriented planes defined by a range of faces
* of the input mesh.
*
* See `CGAL::Polygon_mesh_processing::kernel()` for a comprehensive description of the parameters.
*
* \return `std::nullopt` if and only if the kernel is empty.
*/
template <typename PolygonMesh,
typename CGAL_NP_TEMPLATE_PARAMETERS>
#ifdef DOXYGEN_RUNNING
std::optional<Point_3>
#else
std::optional<typename GetGeomTraits<PolygonMesh, CGAL_NP_CLASS>::type::Point_3>
#endif
kernel_point(const PolygonMesh& pm,
const CGAL_NP_CLASS& np = parameters::default_values())
{
return kernel_point(faces(pm), pm, np);
}
} // namespace Polygon_mesh_processing
} // namespace CGAL
#endif // CGAL_POLYGON_MESH_PROCESSING_KERNEL_H