-
-
Save ochafik/7e32ee750d2db4c37119b6be8ae42451 to your computer and use it in GitHub Desktop.
The ultimate union of CGAL::{Polyhedron_3,Nef_polyhedron_3} with fast-union optimizations and edge case detection for OpenSCAD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| shared_ptr<const Geometry> applyUnion3DFastPolyhedron( | |
| Geometry::Geometries::iterator chbegin, Geometry::Geometries::iterator chend, | |
| const Tree* tree) | |
| { | |
| typedef std::pair<shared_ptr<Polyhedron>, int> QueueItem; | |
| struct QueueItemGreater { | |
| // stable sort for priority_queue by facets, then progress mark | |
| bool operator()(const QueueItem &lhs, const QueueItem& rhs) const | |
| { | |
| size_t l = lhs.first->numFacets(); | |
| size_t r = rhs.first->numFacets(); | |
| return (l > r) || (l == r && lhs.second > rhs.second); | |
| } | |
| }; | |
| try { | |
| Geometry::Geometries children; | |
| children.insert(children.end(), chbegin, chend); | |
| // TODO: bucket things in the queue by rough edge count. Then pick non-overlaps from there! | |
| // // We could move the feature check around this call and make this the only | |
| // // applyUnion3D method (only other meaningful change should be the faster | |
| // // queue creation). | |
| // reduceFastUnionableGeometries(children, tree); | |
| // if (children.empty()) return nullptr; | |
| // if (children.size() == 1) { | |
| // // Fast-skip to avoid useless conversion to nef. | |
| // return children.begin()->second; | |
| // } | |
| // We'll fill the queue in one go to get linear time construction. | |
| std::vector<QueueItem> queueItems; | |
| queueItems.reserve(children.size()); | |
| size_t total_facets = 0; | |
| for (auto &item : children) { | |
| auto chgeom = item.second; | |
| if (!chgeom || chgeom->isEmpty()) { | |
| continue; | |
| } | |
| // TODO(ochafik): Have that helper return futures, and wait for them in batch. | |
| shared_ptr<Polyhedron> poly; | |
| if (auto ps = dynamic_pointer_cast<const PolySet>(chgeom)) { | |
| poly = make_shared<Polyhedron>(*ps); | |
| } else if (auto nef = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(chgeom)) { | |
| // assert(!"Unsupported: CGAL_Nef_polyhedron!"); | |
| poly = make_shared<Polyhedron>(*nef); | |
| } else { | |
| assert(!"Unsupported format"); | |
| continue; | |
| } | |
| total_facets += poly->numFacets(); | |
| auto node_mark = item.first ? item.first->progress_mark : -1; | |
| queueItems.emplace_back(poly, node_mark); | |
| } | |
| // Build the queue in linear time (don't add items one by one!). | |
| std::priority_queue<QueueItem, std::vector<QueueItem>, QueueItemGreater> | |
| q(queueItems.begin(), queueItems.end()); | |
| LOG(message_group::Warning, getLocation(chbegin->first), | |
| "", "Union of %1$lu geometries (%2$lu total facets)", q.size(), total_facets); | |
| { | |
| ScopedTimer timer("Union of all Polyhedrons"); | |
| progress_tick(); | |
| while (q.size() > 1) { | |
| auto p1 = q.top(); | |
| q.pop(); | |
| auto p2 = q.top(); | |
| q.pop(); | |
| ScopedTimer timer(" Union of 2 Polyhedron"); | |
| LOG(message_group::Echo, getLocation(chbegin->first), | |
| "", " Polyhedron (%1$lu) += Polyhedron (%2$lu)", p1.first->numFacets(), p2.first->numFacets()); | |
| *p1.first += *p2.first; | |
| q.emplace(p1.first, -1); | |
| progress_tick(); | |
| } | |
| } | |
| if (q.size() == 1) { | |
| return shared_ptr<const Geometry>(q.top().first->toGeometry()); | |
| } else { | |
| return nullptr; | |
| } | |
| } | |
| catch (const CGAL::Failure_exception &e) { | |
| LOG(message_group::Error, Location::NONE, "", "CGAL error in CGALUtils::applyUnion3DFast: %1$s", e.what()); | |
| } | |
| return nullptr; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. | |
| #pragma once | |
| #include <CGAL/Polygon_mesh_processing/corefinement.h> | |
| #include <CGAL/Polygon_mesh_processing/manifoldness.h> | |
| #include <CGAL/Polygon_mesh_processing/orient_polygon_soup_extension.h> | |
| #include <CGAL/Polygon_mesh_processing/orientation.h> | |
| #include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h> | |
| #include <CGAL/Polygon_mesh_processing/triangulate_faces.h> | |
| #include <CGAL/Surface_mesh.h> | |
| #include <unordered_set> | |
| #include "bounding_boxes.h" | |
| #include "cgal.h" | |
| #include "cgalutils.h" | |
| #include "linalg.h" | |
| #include "polyset.h" | |
| #include "Reindexer.h" | |
| #include "scoped_timer.h" | |
| namespace PMP = CGAL::Polygon_mesh_processing; | |
| namespace params = PMP::parameters; | |
| /*! A mutable polyhedron backed by a CGAL::Polyhedron_3 and fast Polygon Mesh | |
| * Processing (PMP) CSG functions when possible (manifold cases), or by a | |
| * CGAL::Nef_polyhedron_3 when it's not (non manifold cases). | |
| * | |
| * Note that even `cube(1); translate([1, 0, 0]) cube(1)` is considered | |
| * non-manifold because of shared vertices. PMP seems to be fine with edges | |
| * that share segments with others, so long as there's no shared vertex. | |
| * | |
| * Also, keeps track of the bounding boxes of past operations to fast-track | |
| * unions of disjoint bodies. | |
| */ | |
| class Polyhedron | |
| { | |
| // https://doc.cgal.org/latest/Kernel_d/structCGAL_1_1Epeck__d.html | |
| typedef CGAL::Epeck kernel_t; | |
| // typedef CGAL::Simple_cartesian<CGAL::Gmpq> kernel_t; | |
| typedef CGAL::Point_3<kernel_t> point_t; | |
| typedef CGAL::Polyhedron_3<kernel_t> polyhedron_t; | |
| typedef CGAL::Nef_polyhedron_3<kernel_t> nef_polyhedron_t; | |
| // This contains data either as a polyhedron, or as a nef polyhedron. | |
| // | |
| // We stick to nef polyhedra in presence of non-manifold geometry (detected in | |
| // operations where the operands share any vertex), which breaks the | |
| // algorithms of Polygon Mesh Processing library that operate on normal | |
| // (non-nef) polyhedra. | |
| boost::variant<shared_ptr<polyhedron_t>, shared_ptr<nef_polyhedron_t>> data; | |
| BoundingBoxes bboxes; | |
| // Used to determine if two polyhedra share vertices. This is a no-no for | |
| // Polygon Mesh Processing boolean functions (in that case we use Nefs). | |
| std::unordered_set<polyhedron_t::Point_3> vertex_set; | |
| public: | |
| /*! Builds a polyhedron using the provided, untrusted PolySet. | |
| * Face orientation is checked (and reversed if needed), faces are | |
| * triangulated (requirement of Polygon Mesh Processing functions), | |
| * and we check manifoldness (we use a nef polyhedra for non-manifold cases). | |
| */ | |
| Polyhedron(const PolySet &ps) | |
| { | |
| ScopedTimer timer("Polyhedron(PolySet)"); | |
| auto polyhedron = make_shared<polyhedron_t>(); | |
| auto err = CGALUtils::createPolyhedronFromPolySet(ps, *polyhedron); | |
| assert(!err); | |
| data = polyhedron; | |
| bboxes.add(CGALUtils::boundingBox(ps)); | |
| PMP::triangulate_faces(*polyhedron); | |
| if (!PMP::is_outward_oriented(*polyhedron, params::all_default())) { | |
| LOG(message_group::Warning, Location::NONE, "", | |
| "PolySet's %1$lu faces were oriented inwards and need inversion.", | |
| polyhedron->size_of_facets()); | |
| PMP::reverse_face_orientations(polyhedron->facet_handles(), | |
| *polyhedron); | |
| } | |
| size_t nonManifoldVertexCount = 0; | |
| for(auto &v : CGAL::vertices(*polyhedron)) | |
| // for (auto it = polyhedron->vertices_begin(), end = polyhedron->vertices_end(); it != end; it++) | |
| { | |
| // auto &v = *it; | |
| if (PMP::is_non_manifold_vertex(v, *polyhedron)) { | |
| nonManifoldVertexCount++; | |
| } | |
| } | |
| if (!nonManifoldVertexCount) { | |
| LOG(message_group::Warning, Location::NONE, "", | |
| "PolySet had %1$lu non-manifold vertices. Converting polyhedron to nef polyhedron.", | |
| nonManifoldVertexCount); | |
| convertToNefPolyhedron(); | |
| } | |
| } | |
| /*! Builds a polyhedron using a legacy nef polyhedron object. | |
| * This transitional method will disappear when this Polyhedron object is | |
| * fully integrated and replaces all of CGAL_Nef_polyhedron's uses. | |
| */ | |
| Polyhedron(const CGAL_Nef_polyhedron3 &nef) | |
| { | |
| ScopedTimer timer("Polyhedron(CGAL_Nef_polyhedron3)"); | |
| auto polyhedron = make_shared<polyhedron_t>(); | |
| CGAL_Polyhedron poly; | |
| nef.convert_to_polyhedron(poly); | |
| CGALUtils::copyPolyhedron(poly, *polyhedron); | |
| data = polyhedron; | |
| bboxes.add(CGALUtils::boundingBox(nef)); | |
| } | |
| bool isEmpty() const { return numFacets() == 0; } | |
| size_t numFacets() const | |
| { | |
| if (auto poly = boost::get<shared_ptr<polyhedron_t>>(data)) { | |
| return poly->size_of_facets(); | |
| } | |
| if (auto nef = boost::get<shared_ptr<nef_polyhedron_t>>(data)) { | |
| return nef->number_of_facets(); | |
| } | |
| assert(!"Invalid Polyhedron.data state"); | |
| return false; | |
| } | |
| size_t numVertices() const | |
| { | |
| if (auto poly = boost::get<shared_ptr<polyhedron_t>>(data)) { | |
| return poly->size_of_vertices(); | |
| } | |
| if (auto nef = boost::get<shared_ptr<nef_polyhedron_t>>(data)) { | |
| return nef->number_of_vertices(); | |
| } | |
| assert(!"Invalid Polyhedron.data state"); | |
| return false; | |
| } | |
| bool isManifold() const | |
| { | |
| if (auto poly = boost::get<shared_ptr<polyhedron_t>>(data)) { | |
| // We don't keep simple polyhedra if they're not manifold (see contructor) | |
| return true; | |
| } | |
| if (auto nef = boost::get<shared_ptr<nef_polyhedron_t>>(data)) { | |
| return nef->is_simple(); | |
| } | |
| assert(!"Invalid Polyhedron.data state"); | |
| return false; | |
| } | |
| shared_ptr<const Geometry> toGeometry() const | |
| { | |
| if (auto nef = boost::get<shared_ptr<nef_polyhedron_t>>(data)) { | |
| auto ps = make_shared<PolySet>(3); | |
| auto err = CGALUtils::createPolySetFromNefPolyhedron3(*nef, *ps); | |
| assert(!err); | |
| return ps; | |
| } | |
| else if (auto poly = boost::get<shared_ptr<polyhedron_t>>(data)) { | |
| auto ps = make_shared<PolySet>(3); | |
| auto err = CGALUtils::createPolySetFromPolyhedron(*poly, *ps); | |
| assert(!err); | |
| return ps; | |
| } | |
| else { | |
| assert(!"Invalid Polyhedron.data state"); | |
| return nullptr; | |
| } | |
| } | |
| void clear() | |
| { | |
| data = make_shared<polyhedron_t>(); | |
| bboxes.clear(); | |
| } | |
| /*! In-place union (this may also mutate/corefine the other polyhedron). */ | |
| void operator+=(Polyhedron &other) | |
| { | |
| if (!bboxes.intersects(other.bboxes)) { | |
| polyBinOp("fast union", other, | |
| [&](polyhedron_t &destinationPoly, polyhedron_t &otherPoly) { | |
| CGALUtils::appendToPolyhedron(otherPoly, destinationPoly); | |
| }); | |
| } | |
| else { | |
| if (!isManifold() || !other.isManifold() || sharesAnyVertexWith(other)) { | |
| nefPolyBinOp( | |
| "union", other, [&](nef_polyhedron_t &destinationNef, nef_polyhedron_t &otherNef) { | |
| destinationNef += otherNef; | |
| }); | |
| } | |
| else { | |
| polyBinOp("union", other, | |
| [&](polyhedron_t &destinationPoly, polyhedron_t &otherPoly) { | |
| PMP::corefine_and_compute_union( | |
| destinationPoly, otherPoly, destinationPoly, | |
| params::all_default(), params::all_default()); | |
| }); | |
| } | |
| } | |
| bboxes += other.bboxes; | |
| } | |
| /*! In-place intersection (this may also mutate/corefine the other polyhedron). */ | |
| void operator*=(Polyhedron &other) | |
| { | |
| if (!bboxes.intersects(other.bboxes)) { | |
| printf("Empty intersection difference!\n"); | |
| clear(); | |
| return; | |
| } | |
| if (!isManifold() || !other.isManifold() || sharesAnyVertexWith(other)) { | |
| nefPolyBinOp("intersection", other, | |
| [&](nef_polyhedron_t &destinationNef, | |
| nef_polyhedron_t &otherNef) { destinationNef *= otherNef; }); | |
| } | |
| else { | |
| polyBinOp("intersection", other, | |
| [&](polyhedron_t &destinationPoly, polyhedron_t &otherPoly) { | |
| PMP::corefine_and_compute_intersection( | |
| destinationPoly, otherPoly, destinationPoly, | |
| params::all_default(), params::all_default()); | |
| }); | |
| } | |
| bboxes *= other.bboxes; | |
| } | |
| /*! In-place difference (this may also mutate/corefine the other polyhedron). */ | |
| void operator-=(Polyhedron &other) | |
| { | |
| if (!bboxes.intersects(other.bboxes)) { | |
| printf("Non intersecting difference!\n"); | |
| return; | |
| } | |
| // Note: we don't need to change the bbox. | |
| if (!isManifold() || !other.isManifold() || sharesAnyVertexWith(other)) { | |
| nefPolyBinOp("intersection", other, | |
| [&](nef_polyhedron_t &destinationNef, | |
| nef_polyhedron_t &otherNef) { destinationNef -= otherNef; }); | |
| } | |
| else { | |
| polyBinOp("difference", other, | |
| [&](polyhedron_t &destinationPoly, polyhedron_t &otherPoly) { | |
| PMP::corefine_and_compute_difference( | |
| destinationPoly, otherPoly, destinationPoly, | |
| params::all_default(), params::all_default()); | |
| }); | |
| } | |
| } | |
| /*! In-place minkowksi operation. If the other polyhedron is non-convex, | |
| * it is modified during the computation, i.e., it is decomposed into convex pieces. | |
| */ | |
| void minkowski(Polyhedron &other) | |
| { | |
| nefPolyBinOp("minkowski", other, | |
| [&](nef_polyhedron_t &destinationNef, nef_polyhedron_t &otherNef) { | |
| destinationNef = CGAL::minkowski_sum_3(destinationNef, otherNef); | |
| }); | |
| } | |
| private: | |
| /*! Templated so it can apply to a Nef Polyhedron or a normal Polyhedron. */ | |
| template <typename T> | |
| static void foreachVertexUntilTrue(const T& poly, const std::function<bool(const point_t& pt)>& f) { | |
| for (auto fi = poly.facets_begin(); fi != poly.facets_end(); ++fi) { | |
| auto hc = fi->facet_begin(); | |
| auto hc_end = hc; | |
| do { | |
| auto &v = *((hc++)->vertex()); | |
| if (f(v.point())) return; | |
| } while (hc != hc_end); | |
| } | |
| } | |
| /*! Iterate over all vertices' points until the function returns true (for done). */ | |
| void foreachVertexUntilTrue(const std::function<bool(const point_t& pt)>& f) const | |
| { | |
| if (auto poly = boost::get<shared_ptr<polyhedron_t>>(data)) | |
| { | |
| polyhedron_t::Vertex_const_iterator vi; | |
| CGAL_forall_vertices(vi, *poly) { | |
| if (f(vi->point())) return; | |
| } | |
| } | |
| else if (auto nef = boost::get<shared_ptr<nef_polyhedron_t>>(data)) | |
| { | |
| nef_polyhedron_t::Vertex_const_iterator vi; | |
| CGAL_forall_vertices(vi, *nef) { | |
| if (f(vi->point())) return; | |
| } | |
| } | |
| else { | |
| assert(!"Invalid Polyhedron.data state"); | |
| } | |
| } | |
| bool sharesAnyVertexWith(const Polyhedron &other) const | |
| { | |
| if (other.numVertices() < numVertices()) { | |
| // The other has less vertices to index! | |
| return other.sharesAnyVertexWith(*this); | |
| } | |
| ScopedTimer timer("sharesAnyVertexWith"); | |
| std::unordered_set<polyhedron_t::Point_3> vertices; | |
| foreachVertexUntilTrue([&](const auto &p) { | |
| vertices.insert(p); | |
| return false; | |
| }); | |
| auto foundCollision = false; | |
| other.foreachVertexUntilTrue([&](const auto &p) { | |
| return foundCollision = vertices.find(p) != vertices.end(); | |
| }); | |
| return foundCollision; | |
| } | |
| /*! Runs a binary operation that operates on nef polyhedra, stores the result in | |
| * the first one and potentially mutates (e.g. corefines) the second. */ | |
| void nefPolyBinOp(const std::string &opName, Polyhedron &other, | |
| const std::function<void(nef_polyhedron_t &destinationNef, | |
| nef_polyhedron_t &otherNef)> &operation) | |
| { | |
| ScopedTimer timer(std::string("nef ") + opName); | |
| auto &destinationNef = convertToNefPolyhedron(); | |
| auto &otherNef = other.convertToNefPolyhedron(); | |
| auto manifoldBefore = isManifold() && other.isManifold(); | |
| operation(destinationNef, otherNef); | |
| auto manifoldNow = isManifold(); | |
| if (manifoldNow != manifoldBefore) { | |
| LOG(message_group::Echo, Location::NONE, "", | |
| "Nef operation got %1$s operands and produced a %2$s result", | |
| manifoldBefore ? "manifold" : "non-manifold", manifoldNow ? "manifold" : "non-manifold"); | |
| } | |
| } | |
| /*! Runs a binary operation that operates on polyhedra, stores the result in | |
| * the first one and potentially mutates (e.g. corefines) the second. */ | |
| void polyBinOp( | |
| const std::string &opName, Polyhedron &other, | |
| const std::function<void(polyhedron_t &destinationPoly, polyhedron_t &otherPoly)> &operation) | |
| { | |
| ScopedTimer timer(std::string("mesh ") + opName); | |
| auto &destinationPoly = convertToPolyhedron(); | |
| auto &otherPoly = other.convertToPolyhedron(); | |
| operation(destinationPoly, otherPoly); | |
| } | |
| nef_polyhedron_t &convertToNefPolyhedron() | |
| { | |
| if (auto poly = boost::get<shared_ptr<polyhedron_t>>(data)) { | |
| ScopedTimer timer("Polyhedron -> Nef"); | |
| auto nef = make_shared<nef_polyhedron_t>(*poly); | |
| data = nef; | |
| return *nef; | |
| } | |
| else if (auto nef = boost::get<shared_ptr<nef_polyhedron_t>>(data)) { | |
| return *nef; | |
| } | |
| else { | |
| throw "Bad data state"; | |
| } | |
| } | |
| polyhedron_t &convertToPolyhedron() | |
| { | |
| if (auto nef = boost::get<shared_ptr<nef_polyhedron_t>>(data)) { | |
| ScopedTimer timer("Nef -> Polyhedron"); | |
| auto poly = make_shared<polyhedron_t>(); | |
| nef->convert_to_polyhedron(*poly); | |
| data = poly; | |
| return *poly; | |
| } | |
| else if (auto poly = boost::get<shared_ptr<polyhedron_t>>(data)) { | |
| return *poly; | |
| } | |
| else { | |
| throw "Bad data state"; | |
| } | |
| } | |
| /* | |
| void check() | |
| { | |
| std::cout << "Using parallel mode? " | |
| << std::is_same<CGAL::Parallel_if_available_tag, CGAL::Parallel_tag>::value | |
| << std::endl; | |
| bool intersecting = PMP::does_self_intersect<CGAL::Parallel_if_available_tag>( | |
| polyhedron, CGAL::parameters::vertex_point_map(get(CGAL::vertex_point, polyhedron))); | |
| std::cout << (intersecting ? "There are self-intersections." : "There is no | |
| self-intersection.") | |
| << std::endl; | |
| } | |
| void fix_manifoldness() | |
| { | |
| std::cout << "Before stitching : " << std::endl; | |
| std::cout << "\t Number of vertices :\t" << polyhedron.size_of_vertices() << std::endl; | |
| std::cout << "\t Number of halfedges :\t" << polyhedron.size_of_halfedges() << std::endl; | |
| std::cout << "\t Number of facets :\t" << polyhedron.size_of_facets() << std::endl; | |
| PMP::stitch_borders(polyhedron); | |
| std::cout << "Stitching done : " << std::endl; | |
| std::cout << "\t Number of vertices :\t" << polyhedron.size_of_vertices() << std::endl; | |
| std::cout << "\t Number of halfedges :\t" << polyhedron.size_of_halfedges() << std::endl; | |
| std::cout << "\t Number of facets :\t" << polyhedron.size_of_facets() << std::endl; | |
| // Fix manifoldness by splitting non-manifold vertices | |
| std::size_t new_vertices_nb = | |
| PMP::duplicate_non_manifold_vertices(polyhedron, params::all_default()); | |
| std::cout << new_vertices_nb << " vertices have been added to fix mesh manifoldness" | |
| << std::endl; | |
| Reindexer<polyhedron_t::Point_3> vertices; | |
| std::vector<std::vector<std::size_t>> polygons; | |
| for (auto fi = polyhedron.facets_begin(); fi != polyhedron.facets_end(); ++fi) { | |
| auto hc = fi->facet_begin(); | |
| auto hc_end = hc; | |
| std::vector<std::size_t> polygon; | |
| do { | |
| auto &v = *((hc++)->vertex()); | |
| size_t idx = vertices.lookup(v.point()); | |
| polygon.push_back(idx); | |
| } while (hc != hc_end); | |
| polygons.emplace_back(polygon); | |
| } | |
| auto points = vertices.getArray(); | |
| auto numpoints_before = points.size(); | |
| auto numpolygons_before = polygons.size(); | |
| PMP::duplicate_non_manifold_edges_in_polygon_soup(points, polygons); | |
| auto numpoints_after = points.size(); | |
| auto numpolygons_after = polygons.size(); | |
| printf("POINTS:: BEFORE: %lu; AFTER: %lu\n", numpoints_before, numpoints_after); | |
| printf("POLYS: BEFORE: %lu; AFTER: %lu\n", numpolygons_before, numpolygons_after); | |
| polyhedron_t repaired; | |
| PMP::polygon_soup_to_polygon_mesh(points, polygons, repaired); | |
| polyhedron = repaired; | |
| } | |
| */ | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment