From 82dbabac92911852bf78a62b64de1d7ca62cecce Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Tue, 6 Jan 2015 11:12:03 -0500 Subject: [PATCH] Apply convex hull bugfix from CGAL, work around next issue by avoiding using std::set iterators, updated test cases. Fixes #1089 --- openscad.pro | 1 + src/cgalutils.cc | 35 +- src/convex_hull_3_bugfix.h | 960 ++++++++++++++++++ testdata/scad/bugs/issue1089b.scad | 3 + tests/CMakeLists.txt | 4 +- .../cgalpngtest/issue1089b-expected.png | Bin 0 -> 6488 bytes .../monotonepngtest/issue1089b-expected.png | Bin 0 -> 6520 bytes .../opencsgtest/issue1089b-expected.png | Bin 0 -> 6717 bytes 8 files changed, 993 insertions(+), 10 deletions(-) create mode 100644 src/convex_hull_3_bugfix.h create mode 100644 testdata/scad/bugs/issue1089b.scad create mode 100644 tests/regression/cgalpngtest/issue1089b-expected.png create mode 100644 tests/regression/monotonepngtest/issue1089b-expected.png create mode 100644 tests/regression/opencsgtest/issue1089b-expected.png diff --git a/openscad.pro b/openscad.pro index f9478c79..cbcef540 100644 --- a/openscad.pro +++ b/openscad.pro @@ -467,6 +467,7 @@ HEADERS += src/cgal.h \ src/CGALRenderer.h \ src/CGAL_Nef_polyhedron.h \ src/CGAL_Nef3_workaround.h \ + src/convex_hull_3_bugfix.h \ src/cgalworker.h \ src/Polygon2d-CGAL.h diff --git a/src/cgalutils.cc b/src/cgalutils.cc index 971f12f7..97044e76 100644 --- a/src/cgalutils.cc +++ b/src/cgalutils.cc @@ -9,10 +9,19 @@ #include "node.h" #include "cgal.h" -#include #include #include #include + +#include +#include + +#if CGAL_VERSION_NR > CGAL_VERSION_NUMBER(4,5,1) +#include +#else +#include "convex_hull_3_bugfix.h" +#endif + #include "svg.h" #include "Reindexer.h" @@ -48,12 +57,14 @@ static CGAL_Nef_polyhedron *createNefPolyhedronFromPolySet(const PolySet &ps) PolySet ps_tri(3, psq.convexValue()); PolysetUtils::tessellate_faces(psq, ps_tri); if (ps_tri.is_convex()) { - typedef CGAL::Exact_predicates_inexact_constructions_kernel K; + typedef CGAL::Epick K; // Collect point cloud - std::set points; + // NB! CGAL's convex_hull_3() doesn't like std::set iterators, so we use a list + // instead. + std::list points; for (int i = 0; i < ps.polygons.size(); i++) { for (int j = 0; j < ps.polygons[i].size(); j++) { - points.insert(vector_convert(ps.polygons[i][j])); + points.push_back(vector_convert(ps.polygons[i][j])); } } @@ -115,9 +126,11 @@ namespace CGALUtils { bool applyHull(const Geometry::ChildList &children, PolySet &result) { - typedef CGAL::Exact_predicates_inexact_constructions_kernel K; + typedef CGAL::Epick K; // Collect point cloud - std::set points; + // NB! CGAL's convex_hull_3() doesn't like std::set iterators, so we use a list + // instead. + std::list points; BOOST_FOREACH(const Geometry::ChildItem &item, children) { const shared_ptr &chgeom = item.second; @@ -125,7 +138,7 @@ namespace CGALUtils { if (N) { if (!N->isEmpty()) { for (CGAL_Nef_polyhedron3::Vertex_const_iterator i = N->p3->vertices_begin(); i != N->p3->vertices_end(); ++i) { - points.insert(K::Point_3(to_double(i->point()[0]),to_double(i->point()[1]),to_double(i->point()[2]))); + points.push_back(vector_convert(i->point())); } } } else { @@ -133,7 +146,7 @@ namespace CGALUtils { if (ps) { BOOST_FOREACH(const Polygon &p, ps->polygons) { BOOST_FOREACH(const Vector3d &v, p) { - points.insert(K::Point_3(v[0], v[1], v[2])); + points.push_back(K::Point_3(v[0], v[1], v[2])); } } } @@ -149,6 +162,10 @@ namespace CGALUtils { try { CGAL::Polyhedron_3 r; CGAL::convex_hull_3(points.begin(), points.end(), r); + PRINTDB("After hull vertices: %d", r.size_of_vertices()); + PRINTDB("After hull facets: %d", r.size_of_facets()); + PRINTDB("After hull closed: %d", r.is_closed()); + PRINTDB("After hull valid: %d", r.is_valid()); success = !createPolySetFromPolyhedron(r, result); } catch (const CGAL::Assertion_exception &e) { @@ -206,7 +223,7 @@ namespace CGALUtils { while (++it != children.end()) { operands[1] = it->second.get(); - typedef CGAL::Exact_predicates_inexact_constructions_kernel Hull_kernel; + typedef CGAL::Epick Hull_kernel; std::list P[2]; std::list > result_parts; diff --git a/src/convex_hull_3_bugfix.h b/src/convex_hull_3_bugfix.h new file mode 100644 index 00000000..5fb7cd30 --- /dev/null +++ b/src/convex_hull_3_bugfix.h @@ -0,0 +1,960 @@ +/* + This file contains a bugfix against CGAL-4.5.1, see: + http://cgal-discuss.949826.n4.nabble.com/Epick-convex-hull-3-assertion-td4660264.html +*/ + +// Copyright (c) 2001,2011 Max-Planck-Institute Saarbruecken (Germany). +// All rights reserved. +// +// This file is part of CGAL (www.cgal.org). +// You can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Licensees holding a valid commercial license may use this file in +// accordance with the commercial license agreement provided with the software. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +// WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +// +// $URL$ +// $Id$ +// +// +// Author(s) : Susan Hert +// : Amol Prakash +// : Andreas Fabri + +#ifndef CGAL_CONVEX_HULL_3_H +#define CGAL_CONVEX_HULL_3_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#ifndef CGAL_CH_NO_POSTCONDITIONS +#include +#endif // CGAL_CH_NO_POSTCONDITIONS + + +namespace CGAL { + +namespace internal{ namespace Convex_hull_3{ + +//struct to select the default traits class for computing convex hull +template< class Point_3, + class Is_floating_point=typename boost::is_floating_point::Kernel::FT>::type, + class Has_filtered_predicates_tag=typename Kernel_traits::Kernel::Has_filtered_predicates_tag > +struct Default_traits_for_Chull_3{ + typedef typename Kernel_traits::Kernel type; +}; + +//FT is a floating point type and Kernel is a filtered kernel +template +struct Default_traits_for_Chull_3{ + typedef Convex_hull_traits_3< typename Kernel_traits::Kernel > type; +}; + +template +struct Default_polyhedron_for_Chull_3{ + typedef CGAL::Polyhedron_3 type; +}; + +template +struct Default_polyhedron_for_Chull_3 >{ + typedef typename Convex_hull_traits_3::Polyhedron_3 type; +}; + +//utility class to select the right version of internal predicate Is_on_positive_side_of_plane_3 +template ::Kernel::FT>::type, + class Has_filtered_predicates_tag=typename Kernel_traits::Kernel::Has_filtered_predicates_tag, + class Has_cartesian_tag=typename Kernel_traits::Kernel::Kernel_tag, + class Has_classical_point_type = + typename boost::is_same< + typename Kernel_traits::Kernel::Point_3, + typename Traits::Point_3 >::type + > +struct Use_advanced_filtering{ + typedef CGAL::Tag_false type; +}; + +template +struct Use_advanced_filtering{ + typedef typename Kernel_traits::Kernel K; + typedef CGAL::Boolean_tag type; +}; + +//Predicates internally used +template ::type > +class Is_on_positive_side_of_plane_3{ + typedef typename Traits::Point_3 Point_3; + typename Traits::Plane_3 plane; + typename Traits::Has_on_positive_side_3 has_on_positive_side; +public: + typedef Protect_FPU_rounding Protector; + + Is_on_positive_side_of_plane_3(const Traits& traits,const Point_3& p,const Point_3& q,const Point_3& r) + :plane(traits.construct_plane_3_object()(p,q,r)),has_on_positive_side(traits.has_on_positive_side_3_object()) {} + + bool operator() (const Point_3& s) const + { + return has_on_positive_side(plane,s); + } +}; + + +//This predicate uses copy of the code from the statically filtered version of +//Orientation_3. The rational is that the plane is a member of the functor +//so optimization are done to avoid doing several time operations on the plane. +//The main operator() first tries the static version of the predicate, then uses +//interval arithmetic (the protector must be created before using this predicate) +//and in case of failure, exact arithmetic is used. +template +class Is_on_positive_side_of_plane_3,Tag_true>{ + typedef Simple_cartesian::Type> PK; + typedef Simple_cartesian CK; + typedef Convex_hull_traits_3 Traits; + typedef typename Traits::Point_3 Point_3; + + Cartesian_converter to_CK; + Cartesian_converter to_PK; + + const Point_3& p,q,r; + mutable typename CK::Plane_3* ck_plane; + mutable typename PK::Plane_3* pk_plane; + + double m10,m20,m21,Maxx,Maxy,Maxz; + + static const int STATIC_FILTER_FAILURE = 555; + + //this function is a made from the statically filtered version of Orientation_3 + int static_filtered(double psx,double psy, double psz) const{ + + // Then semi-static filter. + double apsx = CGAL::abs(psx); + double apsy = CGAL::abs(psy); + double apsz = CGAL::abs(psz); + + double maxx = (Maxx < apsx)? apsx : Maxx; + double maxy = (Maxy < apsy)? apsy : Maxy; + double maxz = (Maxz < apsz)? apsz : Maxz; + + double det = psx*m10 - m20*psy + m21*psz; + + // Sort maxx < maxy < maxz. + if (maxx > maxz) + std::swap(maxx, maxz); + if (maxy > maxz) + std::swap(maxy, maxz); + else if (maxy < maxx) + std::swap(maxx, maxy); + + // Protect against underflow in the computation of eps. + if (maxx < 1e-97) /* cbrt(min_double/eps) */ { + if (maxx == 0) + return 0; + } + // Protect against overflow in the computation of det. + else if (maxz < 1e102) /* cbrt(max_double [hadamard]/4) */ { + double eps = 5.1107127829973299e-15 * maxx * maxy * maxz; + if (det > eps) return 1; + if (det < -eps) return -1; + } + return STATIC_FILTER_FAILURE; + } + +public: + typedef typename Interval_nt_advanced::Protector Protector; + + Is_on_positive_side_of_plane_3(const Traits&,const Point_3& p_,const Point_3& q_,const Point_3& r_) + :p(p_),q(q_),r(r_),ck_plane(NULL),pk_plane(NULL) + { + double pqx = q.x() - p.x(); + double pqy = q.y() - p.y(); + double pqz = q.z() - p.z(); + double prx = r.x() - p.x(); + double pry = r.y() - p.y(); + double prz = r.z() - p.z(); + + + m10 = pqy*prz - pry*pqz; + m20 = pqx*prz - prx*pqz; + m21 = pqx*pry - prx*pqy; + + double aprx = CGAL::abs(prx); + double apry = CGAL::abs(pry); + double aprz = CGAL::abs(prz); + + Maxx = CGAL::abs(pqx); + if (Maxx < aprx) Maxx = aprx; + Maxy = CGAL::abs(pqy); + if (Maxy < apry) Maxy = apry; + Maxz = CGAL::abs(pqz); + if (Maxz < aprz) Maxz = aprz; + } + + ~Is_on_positive_side_of_plane_3(){ + if (ck_plane!=NULL) delete ck_plane; + if (pk_plane!=NULL) delete pk_plane; + } + + bool operator() (const Point_3& s) const + { + double psx = s.x() - p.x(); + double psy = s.y() - p.y(); + double psz = s.z() - p.z(); + + int static_res = static_filtered(psx,psy,psz); + if (static_res != STATIC_FILTER_FAILURE) + return static_res == 1; + + try{ + if (ck_plane==NULL) + ck_plane=new typename CK::Plane_3(to_CK(p),to_CK(q),to_CK(r)); + return ck_plane->has_on_positive_side(to_CK(s)); + } + catch (Uncertain_conversion_exception){ + if (pk_plane==NULL) + pk_plane=new typename PK::Plane_3(to_PK(p),to_PK(q),to_PK(r)); + return pk_plane->has_on_positive_side(to_PK(s)); + } + } +}; + + +template +class Build_coplanar_poly : public Modifier_base { + public: + Build_coplanar_poly(ForwardIterator i, ForwardIterator j) + { + start = i; + end = j; + } + void operator()( HDS& hds) { + Polyhedron_incremental_builder_3 B(hds,true); + ForwardIterator iter = start; + int count = 0; + while (iter != end) + { + count++; + iter++; + } + B.begin_surface(count, 1, 2*count); + iter = start; + while (iter != end) + { + B.add_vertex(*iter); + iter++; + } + iter = start; + B.begin_facet(); + int p = 0; + while (p < count) + { + B.add_vertex_to_facet(p); + p++; + } + B.end_facet(); + B.end_surface(); + } + private: + ForwardIterator start; + ForwardIterator end; +}; + + +namespace internal { namespace Convex_hull_3{ + +BOOST_MPL_HAS_XXX_TRAIT_NAMED_DEF(Traits_has_typedef_Traits_xy_3,Traits_xy_3,false) +BOOST_MPL_HAS_XXX_TRAIT_NAMED_DEF(Traits_has_typedef_Traits_yz_3,Traits_xy_3,false) +BOOST_MPL_HAS_XXX_TRAIT_NAMED_DEF(Traits_has_typedef_Traits_xz_3,Traits_xy_3,false) + +template ::value && + Traits_has_typedef_Traits_yz_3::value && + Traits_has_typedef_Traits_xz_3::value +> +struct Projection_traits{ + typedef typename Kernel_traits::Kernel K; + typedef CGAL::Projection_traits_xy_3 Traits_xy_3; + typedef CGAL::Projection_traits_yz_3 Traits_yz_3; + typedef CGAL::Projection_traits_xz_3 Traits_xz_3; +}; + +template +struct Projection_traits{ + typedef typename T::Traits_xy_3 Traits_xy_3; + typedef typename T::Traits_yz_3 Traits_yz_3; + typedef typename T::Traits_xz_3 Traits_xz_3; +}; + +} } //end of namespace internal::Convex_hull_3 + +template +void coplanar_3_hull(InputIterator first, InputIterator beyond, + const Point_3& p1, const Point_3& p2, const Point_3& p3, + Polyhedron_3& P, const Traits& /* traits */) +{ + typedef typename internal::Convex_hull_3::Projection_traits PTraits; + typedef typename PTraits::Traits_xy_3 Traits_xy_3; + typedef typename PTraits::Traits_yz_3 Traits_yz_3; + typedef typename PTraits::Traits_xz_3 Traits_xz_3; + + std::list CH_2; + typedef typename std::list::iterator CH_2_iterator; + + Traits_xy_3 traits_xy; + typename Traits_xy_3::Left_turn_2 left_turn_in_xy = traits_xy.left_turn_2_object(); + if ( left_turn_in_xy(p1,p2,p3) || left_turn_in_xy(p2,p1,p3) ) + convex_hull_points_2( first, beyond, + std::back_inserter(CH_2), + traits_xy ); + else{ + Traits_yz_3 traits_yz; + typename Traits_yz_3::Left_turn_2 left_turn_in_yz = traits_yz.left_turn_2_object(); + if ( left_turn_in_yz(p1,p2,p3) || left_turn_in_yz(p2,p1,p3) ) + convex_hull_points_2( first, beyond, + std::back_inserter(CH_2), + traits_yz ); + else{ + Traits_xz_3 traits_xz; + typename Traits_xz_3::Left_turn_2 left_turn_in_xz = traits_xz.left_turn_2_object(); + CGAL_assertion( left_turn_in_xz(p1,p2,p3) || left_turn_in_xz(p2,p1,p3) ); + convex_hull_points_2( first, beyond, + std::back_inserter(CH_2), + traits_xz ); + } + } + + typedef typename Polyhedron_3::Halfedge_data_structure HDS; + + Build_coplanar_poly poly(CH_2.begin(),CH_2.end()); + P.delegate(poly); +} + + +// +// visible is the set of facets visible from point and reachable from +// start_facet. +// +template +void +find_visible_set(TDS_2& tds, + const typename Traits::Point_3& point, + typename TDS_2::Face_handle start, + std::list& visible, + std::map& outside, + const Traits& traits) +{ + typedef typename Traits::Plane_3 Plane_3; + typedef typename TDS_2::Face_handle Face_handle; + typedef typename TDS_2::Vertex_handle Vertex_handle; + typename Traits::Has_on_positive_side_3 has_on_positive_side = + traits.has_on_positive_side_3_object(); + + std::vector vertices; + vertices.reserve(10); + int VISITED=1, BORDER=2; + visible.clear(); + typename std::list::iterator vis_it; + visible.push_back(start); + start->info() = VISITED; + vertices.push_back(start->vertex(0)); + vertices.push_back(start->vertex(1)); + vertices.push_back(start->vertex(2)); + start->vertex(0)->info() = start->vertex(1)->info() = start->vertex(2)->info() = VISITED; + + for (vis_it = visible.begin(); vis_it != visible.end(); vis_it++) + { + // check all the neighbors of the current face to see if they have + // already been visited or not and if not whether they are visible + // or not. + + for(int i=0; i < 3; i++) { + // the facet on the other side of the current halfedge + Face_handle f = (*vis_it)->neighbor(i); + // if haven't already seen this facet + if (f->info() == 0) { + f->info() = VISITED; + Plane_3 plane(f->vertex(0)->point(),f->vertex(1)->point(),f->vertex(2)->point()); + int ind = f->index(*vis_it); + if ( has_on_positive_side(plane, point) ){ // is visible + visible.push_back(f); + Vertex_handle vh = f->vertex(ind); + if(vh->info() == 0){ vertices.push_back(vh); vh->info() = VISITED;} + } else { + f->info() = BORDER; + f->vertex(TDS_2::cw(ind))->info() = BORDER; + f->vertex(TDS_2::ccw(ind))->info() = BORDER; + outside.insert(std::make_pair(f->vertex(TDS_2::cw(ind)), + typename TDS_2::Edge(f,ind))); + } + } else if(f->info() == BORDER) { + int ind = f->index(*vis_it); + f->vertex(TDS_2::cw(ind))->info() = BORDER; + f->vertex(TDS_2::ccw(ind))->info() = BORDER; + outside.insert(std::make_pair(f->vertex(TDS_2::cw(ind)), + typename TDS_2::Edge(f,ind))); + } + } + } + + for(typename std::vector::iterator vit = vertices.begin(); + vit != vertices.end(); + ++vit){ + if((*vit)->info() != BORDER){ + tds.delete_vertex(*vit); + } else { + (*vit)->info() = 0; + } + } + +} + +// using a third template parameter for the point instead of getting it from +// the traits class as it should be is required by M$VC6 +template +typename std::list::iterator +farthest_outside_point(Face_handle f, std::list& outside_set, + const Traits& traits) +{ + + typedef typename std::list::iterator Outside_set_iterator; + CGAL_ch_assertion(!outside_set.empty()); + + typename Traits::Plane_3 plane(f->vertex(0)->point(),f->vertex(1)->point(),f->vertex(2)->point()); + + typename Traits::Less_signed_distance_to_plane_3 less_dist_to_plane = + traits.less_signed_distance_to_plane_3_object(); + Outside_set_iterator farthest_it = + std::max_element(outside_set.begin(), + outside_set.end(), + boost::bind(less_dist_to_plane, plane, _1, _2)); + return farthest_it; +} + +template +void +partition_outside_sets(const std::list& new_facets, + std::list& vis_outside_set, + std::list& pending_facets, + const Traits& traits) +{ + typename std::list::const_iterator f_list_it; + typename std::list::iterator point_it, to_splice; + + // walk through all the new facets and check each unassigned outside point + // to see if it belongs to the outside set of this new facet. + for (f_list_it = new_facets.begin(); (f_list_it != new_facets.end()) && (! vis_outside_set.empty()); + ++f_list_it) + { + Face_handle f = *f_list_it; + Is_on_positive_side_of_plane_3 is_on_positive_side( + traits,f->vertex(0)->point(),f->vertex(1)->point(),f->vertex(2)->point()); + std::list& point_list = f->points; + + for (point_it = vis_outside_set.begin();point_it != vis_outside_set.end();){ + if( is_on_positive_side(*point_it) ) { + to_splice = point_it; + ++point_it; + point_list.splice(point_list.end(), vis_outside_set, to_splice); + } else { + ++point_it; + } + } + if(! point_list.empty()){ + pending_facets.push_back(f); + f->it = boost::prior(pending_facets.end()); + } else { + f->it = pending_facets.end(); + } + } + + + for (; f_list_it != new_facets.end();++f_list_it) + (*f_list_it)->it = pending_facets.end(); +} + + + +template +void +ch_quickhull_3_scan(TDS_2& tds, + std::list& pending_facets, + const Traits& traits) +{ + typedef typename TDS_2::Edge Edge; + typedef typename TDS_2::Face_handle Face_handle; + typedef typename TDS_2::Vertex_handle Vertex_handle; + typedef typename Traits::Point_3 Point_3; + typedef std::list Outside_set; + typedef typename std::list::iterator Outside_set_iterator; + typedef std::map Border_edges; + + std::list visible_set; + typename std::list::iterator vis_set_it; + Outside_set vis_outside_set; + Border_edges border; + + while (!pending_facets.empty()) + { + vis_outside_set.clear(); + + Face_handle f_handle = pending_facets.front(); + + Outside_set_iterator farthest_pt_it = farthest_outside_point(f_handle, f_handle->points, traits); + Point_3 farthest_pt = *farthest_pt_it; + f_handle->points.erase(farthest_pt_it); + find_visible_set(tds, farthest_pt, f_handle, visible_set, border, traits); + + // for each visible facet + for (vis_set_it = visible_set.begin(); vis_set_it != visible_set.end(); + vis_set_it++) + { + + // add its outside set to the global outside set list + std::list& point_list = (*vis_set_it)->points; + if(! point_list.empty()){ + vis_outside_set.splice(vis_outside_set.end(), point_list, point_list.begin(), point_list.end()); + } + + if((*vis_set_it)->it != pending_facets.end()){ + pending_facets.erase((*vis_set_it)->it); + } + (*vis_set_it)->info() = 0; + } + + std::vector edges; + edges.reserve(border.size()); + typename Border_edges::iterator it = border.begin(); + Edge e = it->second; + e.first->info() = 0; + edges.push_back(e); + border.erase(it); + while(! border.empty()){ + it = border.find(e.first->vertex(TDS_2::ccw(e.second))); + assert(it != border.end()); + e = it->second; + e.first->info() = 0; + edges.push_back(e); + border.erase(it); + } + + // If we want to reuse the faces we must only pass |edges| many, and call delete_face for the others. + // Also create facets if necessary + std::ptrdiff_t diff = visible_set.size() - edges.size(); + if(diff < 0){ + for(int i = 0; i<-diff;i++){ + visible_set.push_back(tds.create_face()); + } + } else { + for(int i = 0; ipoint() = farthest_pt; + vh->info() = 0; + + // now partition the set of outside set points among the new facets. + + partition_outside_sets(visible_set, vis_outside_set, + pending_facets, traits); + + } +} + +template +void non_coplanar_quickhull_3(std::list& points, + TDS_2& tds, const Traits& traits) +{ + typedef typename Traits::Point_3 Point_3; + + typedef typename TDS_2::Face_handle Face_handle; + typedef typename TDS_2::Face_iterator Face_iterator; + typedef typename std::list::iterator P3_iterator; + + std::list pending_facets; + + typename Is_on_positive_side_of_plane_3::Protector p; + + // for each facet, look at each unassigned point and decide if it belongs + // to the outside set of this facet. + for(Face_iterator fit = tds.faces_begin(); fit != tds.faces_end(); ++fit){ + Is_on_positive_side_of_plane_3 is_on_positive_side( + traits,fit->vertex(0)->point(),fit->vertex(1)->point(),fit->vertex(2)->point() ); + for (P3_iterator point_it = points.begin() ; point_it != points.end(); ) + { + if( is_on_positive_side(*point_it) ) { + P3_iterator to_splice = point_it; + ++point_it; + fit->points.splice(fit->points.end(), points, to_splice); + } else { + ++point_it; + } + } + } + // add all the facets with non-empty outside sets to the set of facets for + // further consideration + for(Face_iterator fit = tds.faces_begin(); fit != tds.faces_end(); ++fit){ + if (! fit->points.empty()){ + pending_facets.push_back(fit); + fit->it = boost::prior(pending_facets.end()); + } else { + fit->it = pending_facets.end(); + } + } + + + ch_quickhull_3_scan(tds, pending_facets, traits); + + //std::cout << "|V(tds)| = " << tds.number_of_vertices() << std::endl; +// CGAL_ch_expensive_postcondition(all_points_inside(points.begin(), +// points.end(),P,traits)); +// CGAL_ch_postcondition(is_strongly_convex_3(P, traits)); +} + + +namespace internal{ + +template +class Build_convex_hull_from_TDS_2 : public CGAL::Modifier_base { + typedef std::map Vertex_map; + + const TDS& t; + template + static unsigned get_vertex_index( Vertex_map& vertex_map, + typename TDS::Vertex_handle vh, + Builder& builder, + unsigned& vindex) + { + std::pair + res=vertex_map.insert(std::make_pair(vh,vindex)); + if (res.second){ + builder.add_vertex(vh->point()); + ++vindex; + } + return res.first->second; + } + +public: + Build_convex_hull_from_TDS_2(const TDS& t_):t(t_) + { + CGAL_assertion(t.dimension()==2); + } + void operator()( HDS& hds) { + // Postcondition: `hds' is a valid polyhedral surface. + + CGAL::Polyhedron_incremental_builder_3 B( hds, true); + Vertex_map vertex_map; + //start the surface + B.begin_surface( t.number_of_vertices(), t.number_of_faces()); + unsigned vindex=0; + for (typename TDS::Face_iterator it=t.faces_begin();it!=t.faces_end();++it) + { + unsigned i0=get_vertex_index(vertex_map,it->vertex(0),B,vindex); + unsigned i1=get_vertex_index(vertex_map,it->vertex(1),B,vindex); + unsigned i2=get_vertex_index(vertex_map,it->vertex(2),B,vindex); + B.begin_facet(); + B.add_vertex_to_facet( i0 ); + B.add_vertex_to_facet( i1 ); + B.add_vertex_to_facet( i2 ); + B.end_facet(); + } + B.end_surface(); + } +}; + +} //namespace internal + +template +void +ch_quickhull_polyhedron_3(std::list& points, + InputIterator point1_it, InputIterator point2_it, + InputIterator point3_it, Polyhedron_3& P, + const Traits& traits) +{ + typedef typename Traits::Point_3 Point_3; + typedef typename Traits::Plane_3 Plane_3; + typedef typename std::list::iterator P3_iterator; + + typedef Triangulation_data_structure_2< + Triangulation_vertex_base_with_info_2 >, + Convex_hull_face_base_2 > Tds; + typedef typename Tds::Vertex_handle Vertex_handle; + typedef typename Tds::Face_handle Face_handle; + + // found three points that are not collinear, so construct the plane defined + // by these points and then find a point that has maximum distance from this + // plane. + typename Traits::Construct_plane_3 construct_plane = + traits.construct_plane_3_object(); + Plane_3 plane = construct_plane(*point3_it, *point2_it, *point1_it); + typedef typename Traits::Less_signed_distance_to_plane_3 Dist_compare; + Dist_compare compare_dist = traits.less_signed_distance_to_plane_3_object(); + + typename Traits::Coplanar_3 coplanar = traits.coplanar_3_object(); + // find both min and max here since using signed distance. If all points + // are on the negative side of the plane, the max element will be on the + // plane. + std::pair min_max; + min_max = CGAL::min_max_element(points.begin(), points.end(), + boost::bind(compare_dist, plane, _1, _2), + boost::bind(compare_dist, plane, _1, _2)); + P3_iterator max_it; + if (coplanar(*point1_it, *point2_it, *point3_it, *min_max.second)) + { + max_it = min_max.first; + // want the orientation of the points defining the plane to be positive + // so have to reorder these points if all points were on negative side + // of plane + std::swap(*point1_it, *point3_it); + } + else + max_it = min_max.second; + + // if the maximum distance point is on the plane then all are coplanar + if (coplanar(*point1_it, *point2_it, *point3_it, *max_it)) { + coplanar_3_hull(points.begin(), points.end(), *point1_it, *point2_it, *point3_it, P, traits); + } else { + Tds tds; + Vertex_handle v0 = tds.create_vertex(); v0->set_point(*point1_it); + Vertex_handle v1 = tds.create_vertex(); v1->set_point(*point2_it); + Vertex_handle v2 = tds.create_vertex(); v2->set_point(*point3_it); + Vertex_handle v3 = tds.create_vertex(); v3->set_point(*max_it); + + v0->info() = v1->info() = v2->info() = v3->info() = 0; + Face_handle f0 = tds.create_face(v0,v1,v2); + Face_handle f1 = tds.create_face(v3,v1,v0); + Face_handle f2 = tds.create_face(v3,v2,v1); + Face_handle f3 = tds.create_face(v3,v0,v2); + tds.set_dimension(2); + f0->set_neighbors(f2, f3, f1); + f1->set_neighbors(f0, f3, f2); + f2->set_neighbors(f0, f1, f3); + f3->set_neighbors(f0, f2, f1); + + points.erase(point1_it); + points.erase(point2_it); + points.erase(point3_it); + points.erase(max_it); + if (!points.empty()){ + non_coplanar_quickhull_3(points, tds, traits); + internal::Build_convex_hull_from_TDS_2 builder(tds); + P.delegate(builder); + } + else + P.make_tetrahedron(v0->point(),v1->point(),v2->point(),v3->point()); + } + +} + +} } //namespace internal::Convex_hull_3 + +template +void +convex_hull_3(InputIterator first, InputIterator beyond, + Object& ch_object, const Traits& traits) +{ + typedef typename Traits::Point_3 Point_3; + typedef std::list Point_3_list; + typedef typename Point_3_list::iterator P3_iterator; + typedef std::pair P3_iterator_pair; + + if (first == beyond) // No point + return; + + // If the first and last point are equal the collinearity test some lines below will always be true. + Point_3_list points(first, beyond); + std::size_t size = points.size(); + while((size > 1) && (points.front() == points.back())){ + points.pop_back(); + --size; + } + + if ( size == 1 ) // 1 point + { + ch_object = make_object(*points.begin()); + return; + } + else if ( size == 2 ) // 2 points + { + typedef typename Traits::Segment_3 Segment_3; + typename Traits::Construct_segment_3 construct_segment = + traits.construct_segment_3_object(); + Segment_3 seg = construct_segment(*points.begin(), *(++points.begin())); + ch_object = make_object(seg); + return; + } + else if ( size == 3 ) // 3 points + { + typedef typename Traits::Triangle_3 Triangle_3; + typename Traits::Construct_triangle_3 construct_triangle = + traits.construct_triangle_3_object(); + Triangle_3 tri = construct_triangle(*(points.begin()), + *(++points.begin()), + *(--points.end())); + ch_object = make_object(tri); + return; + } + + // at least 4 points + typename Traits::Collinear_3 collinear = traits.collinear_3_object(); + + P3_iterator point1_it = points.begin(); + P3_iterator point2_it = points.begin(); + point2_it++; + P3_iterator point3_it = points.end(); + point3_it--; + + // find three that are not collinear + while (point2_it != points.end() && + collinear(*point1_it,*point2_it,*point3_it)) + point2_it++; + + + // all are collinear, so the answer is a segment + if (point2_it == points.end()) + { + typedef typename Traits::Less_distance_to_point_3 Less_dist; + + Less_dist less_dist = traits.less_distance_to_point_3_object(); + P3_iterator_pair endpoints = + min_max_element(points.begin(), points.end(), + boost::bind(less_dist, *points.begin(), _1, _2), + boost::bind(less_dist, *points.begin(), _1, _2)); + + typename Traits::Construct_segment_3 construct_segment = + traits.construct_segment_3_object(); + typedef typename Traits::Segment_3 Segment_3; + + Segment_3 seg = construct_segment(*endpoints.first, *endpoints.second); + ch_object = make_object(seg); + return; + } + + // result will be a polyhedron + typename internal::Convex_hull_3::Default_polyhedron_for_Chull_3::type P; + + P3_iterator minx, maxx, miny, it; + minx = maxx = miny = it = points.begin(); + ++it; + for(; it != points.end(); ++it){ + if(it->x() < minx->x()) minx = it; + if(it->x() > maxx->x()) maxx = it; + if(it->y() < miny->y()) miny = it; + } + if(! collinear(*minx, *maxx, *miny) ){ + internal::Convex_hull_3::ch_quickhull_polyhedron_3(points, minx, maxx, miny, P, traits); + } else { + internal::Convex_hull_3::ch_quickhull_polyhedron_3(points, point1_it, point2_it, point3_it, P, traits); + } + CGAL_assertion(P.size_of_vertices()>=3); + if (boost::next(P.vertices_begin(),3) == P.vertices_end()){ + typedef typename Traits::Triangle_3 Triangle_3; + typename Traits::Construct_triangle_3 construct_triangle = + traits.construct_triangle_3_object(); + Triangle_3 tri = construct_triangle(P.halfedges_begin()->vertex()->point(), + P.halfedges_begin()->next()->vertex()->point(), + P.halfedges_begin()->opposite()->vertex()->point()); + ch_object = make_object(tri); + } + else + ch_object = make_object(P); +} + + +template +void convex_hull_3(InputIterator first, InputIterator beyond, + Object& ch_object) +{ + typedef typename std::iterator_traits::value_type Point_3; + typedef typename internal::Convex_hull_3::Default_traits_for_Chull_3::type Traits; + convex_hull_3(first, beyond, ch_object, Traits()); +} + + + +template +void convex_hull_3(InputIterator first, InputIterator beyond, + Polyhedron_3& polyhedron, const Traits& traits) +{ + typedef typename Traits::Point_3 Point_3; + typedef std::list Point_3_list; + typedef typename Point_3_list::iterator P3_iterator; + + Point_3_list points(first, beyond); + CGAL_ch_precondition(points.size() > 3); + + // at least 4 points + typename Traits::Collinear_3 collinear = traits.collinear_3_object(); + typename Traits::Equal_3 equal = traits.equal_3_object(); + + P3_iterator point1_it = points.begin(); + P3_iterator point2_it = points.begin(); + point2_it++; + + // find three that are not collinear + while (point2_it != points.end() && equal(*point1_it,*point2_it)) + ++point2_it; + + CGAL_ch_precondition_msg(point2_it != points.end(), + "All points are equal; cannot construct polyhedron."); + + P3_iterator point3_it = point2_it; + ++point3_it; + + CGAL_ch_precondition_msg(point3_it != points.end(), + "Only two points with different coordinates; cannot construct polyhedron."); + + while (point3_it != points.end() && collinear(*point1_it,*point2_it,*point3_it)) + ++point3_it; + + CGAL_ch_precondition_msg(point3_it != points.end(), + "All points are collinear; cannot construct polyhedron."); + + polyhedron.clear(); + // result will be a polyhedron + internal::Convex_hull_3::ch_quickhull_polyhedron_3(points, point1_it, point2_it, point3_it, + polyhedron, traits); + +} + + +template +void convex_hull_3(InputIterator first, InputIterator beyond, + Polyhedron_3& polyhedron) +{ + typedef typename std::iterator_traits::value_type Point_3; + typedef typename internal::Convex_hull_3::Default_traits_for_Chull_3::type Traits; + convex_hull_3(first, beyond, polyhedron, Traits()); +} + +} // namespace CGAL + +#endif // CGAL_CONVEX_HULL_3_H diff --git a/testdata/scad/bugs/issue1089b.scad b/testdata/scad/bugs/issue1089b.scad new file mode 100644 index 00000000..4cf13cb9 --- /dev/null +++ b/testdata/scad/bugs/issue1089b.scad @@ -0,0 +1,3 @@ +hull() { + polyhedron(points = [[21.015921990298082278, -9.6968661252791008565, 17.385898562432661407], [21.015921990298082278, 9.4292289939816065214, 11.538464467977924244], [21.245998787904895266, 9.1447008983105675384, 10.607815001220002316], [21.245998787904895266, -0.41834666131978626158, 13.531532048447369121], [21.245998787904895266, -9.9813942209501398395, 16.455249095674737703], [16.150059612458484537, -10.033205852946363024, 17.385898562432661407], [16.150059612458484537, 9.0928892663143443542, 11.538464467977924244], [16.380136410065297525, 8.8083611706433053712, 10.607815001220002316], [16.380136410065297525, -0.75468638898704798468, 13.531532048447369121], [16.380136410065297525, -10.317733948617402007, 16.455249095674737703], [15.17688713689056712, -10.100473798479814747, 17.165875026644524581], [15.17688713689056712, 9.0256213207808926313, 11.318440932189787418], [15.406963934497380109, 8.7410932251098536483, 10.38779146543186549], [15.406963934497380109, -0.82195433452049937451, 13.311508512659232295], [15.406963934497380109, -10.38500189415085373, 16.235225559886600877], [14.152135476691794835, -10.17119106380654614, 16.934569274178546294], [14.152135476691794835, 8.954904055454161238, 11.087135179723812684], [14.367990727181227939, 8.6694249043464353122, 10.153374950800477805], [14.367990727181227939, -0.89362265528391870983, 13.077091998027846387], [14.367990727181227939, -10.456670214914272066, 16.000809045255213192], [13.110820024653460081, -10.233961476664669377, 16.729256504918232906], [13.110820024653460081, 8.8921336425960380012, 10.881822410463495743], [13.309600712109528331, 8.6055965081559246954, 9.9446016739871083701], [13.309600712109528331, -0.95745105147442877147, 12.868318721214476952], [13.309600712109528331, -10.520498611104782682, 15.792035768441845534], [12.078282570414469177, -10.294222837947037519, 16.532150473574372285], [12.078282570414469177, 8.8318722813136698591, 10.684716379119635121], [12.262745467026753943, 8.5445178276236166681, 9.7448223118344454718], [12.262745467026753943, -1.0185297320067365767, 12.668539359061812277], [12.262745467026753943, -10.58157729163709071, 15.592256406289180859], [11.031163105767490151, -10.348029755406946251, 16.356155976708063093], [11.031163105767490151, 8.7780653638537611272, 10.508721882253327706], [11.198443837973723447, 8.4898133798050405829, 9.5658921254443249893], [11.198443837973723447, -1.0732341798253119958, 12.489609172671691795], [11.198443837973723447, -10.636281739455666795, 15.413326219899060376], [9.9920867282612331195, -10.397762464907449598, 16.193487613614035325], [9.9920867282612331195, 8.7283326543532577801, 10.346053519159298162], [10.144967804693861169, 8.4393978978853763095, 9.4009905143952519069], [10.144967804693861169, -1.1236496617449773794, 12.324707561622618712], [10.144967804693861169, -10.686697221375331068, 15.248424608849987294], [8.9400347163076823165, -10.442561786292975157, 16.046955635953878527], [8.9400347163076823165, 8.6835333329677322212, 10.199521541499144917], [9.0756439452541037838, 8.3938624405481636614, 9.2520507445299653426], [9.0756439452541037838, -1.1691851190821893614, 12.175767791757333924], [9.0756439452541037838, -10.732232678712543716, 15.09948483898470073], [7.8956741722639147696, -10.481705796269976005, 15.918921348422623296], [7.8956741722639147696, 8.6443893229907313724, 10.071487253967887909], [8.0168163367886453585, 8.3541708868691326728, 9.1222255222471790859], [8.0168163367886453585, -1.2088766727612223484, 12.045942569474547668], [8.0168163367886453585, -10.771924232391574705, 14.969659616701914473], [6.839564786514664263, -10.51746028460065574, 15.801973686643666639], [6.839564786514664263, 8.6086348346600516379, 9.9545395921889312518], [6.9433634354009168277, 8.3178424250433771192, 9.0034004777589125013], [6.9433634354009168277, -1.2452051345869770138, 11.927117524986281083], [6.9433634354009168277, -10.808252694217330259, 14.850834572213647888], [5.7911812064893624097, -10.545968319665455226, 15.708728105504132344], [5.7911812064893624097, 8.5801267995952521517, 9.861294011049396957], [5.8804599030361934808, 8.2889226275126546284, 8.9088080822795312486], [5.8804599030361934808, -1.2741249321176979503, 11.83252512950689983], [5.8804599030361934808, -10.837172491748052749, 14.756242176734266636], [4.7318927519149163174, -10.572647626142034127, 15.621464026055862817], [4.7318927519149163174, 8.5534474931186732505, 9.77402993160112743], [4.8037743563616075448, 8.2618321038161273151, 8.8201989719106297372], [4.8037743563616075448, -1.3012154558142254857, 11.743916019137996543], [4.8037743563616075448, -10.864263015444580063, 14.667633066365365124], [3.6807521204854736219, -10.590485240237288878, 15.563119819284901624], [3.6807521204854736219, 8.5356098790234185003, 9.7156857248301662366], [3.7380754567641063524, 8.2437189311402967462, 8.7609534536348423472], [3.7380754567641063524, -1.3193286284900567207, 11.684670500862209153], [3.7380754567641063524, -10.882376188120410632, 14.608387548089577734], [2.6191654551378640292, -10.608065925320802947, 15.505615989444741842], [2.6191654551378640292, 8.518029193939904431, 9.6581818949900064553], [2.659056271348898548, 8.2258902040489321195, 8.7026383149438082398], [2.659056271348898548, -1.3371573555814209033, 11.626355362171176822], [2.659056271348898548, -10.900204915211775258, 14.550072409398543627], [1.5571301313920737908, -10.615277603175528043, 15.482027654049947074], [1.5571301313920737908, 8.5108175160851793351, 9.634593559595211687], [1.5798452641030620303, 8.2185212496694965978, 8.6785355512163384617], [1.5798452641030620303, -1.3445263099608570911, 11.602252598443705267], [1.5798452641030620303, -10.90757386959121078, 14.525969645671073849], [0.5035077216434232783, -10.622159719563320834, 15.459517265642222839], [0.5035077216434232783, 8.5039353996973865435, 9.6120831711874892278], [0.5113668014119475691, 8.2115727243029539295, 8.6558079488265811108], [0.5113668014119475691, -1.3514748353273995374, 11.579524996053947916], [0.5113668014119475691, -10.914522394957753448, 14.503242043281314722], [-0.55969246949480999653, -10.620097256783905593, 15.466263277424799227], [-0.55969246949480999653, 8.5059978624768017852, 9.6188291829700638402], [-0.56928712712562057341, 8.2136396155613287107, 8.6625684455111535698], [-0.56928712712562057341, -1.3494079440690252003, 11.586285492738520375], [-0.56928712712562057341, -10.912455503699378667, 14.510002539965888957], [-1.6125195715995086498, -10.616272236324949318, 15.478774355608731383], [-1.6125195715995086498, 8.5098228829357580594, 9.6313402611539977727], [-1.6367003391161654147, 8.2175366669705987022, 8.6753151263175354302], [-1.6367003391161654147, -1.3455108926597549868, 11.599032173544902236], [-1.6367003391161654147, -10.908558452290108676, 14.522749220772269041], [-2.6750035922459902693, -10.605089410934800398, 15.515351729318147989], [-2.6750035922459902693, 8.5210057083259069799, 9.6679176348634126015], [-2.7166291222015424012, 8.2288874074141755699, 8.7124417254191364179], [-2.7166291222015424012, -1.334160152216178119, 11.636158772646505], [-2.7166291222015424012, -10.897207711846531808, 14.559875819873871805], [-3.7267390106727740218, -10.590568334550255258, 15.562848030033741509], [-3.7267390106727740218, 8.5355267847104521195, 9.7154139355790061217], [-3.7829348904509538798, 8.2436170965810333655, 8.7606203678000085233], [-3.7829348904509538798, -1.3194304630493196573, 11.684337415027377105], [-3.7829348904509538798, -10.882478022679674012, 14.60805446225474391], [-4.7876230981018181865, -10.570275178726245002, 15.629223951898024936], [-4.7876230981018181865, 8.5558199405344623756, 9.7817898574432895487], [-4.8612365692795815875, 8.2642414847358942609, 8.828079701800975343], [-4.8612365692795815875, -1.2988060748944600942, 11.751796749028343925], [-4.8612365692795815875, -10.861853634524813117, 14.67551379625571073], [-5.8369967953404051642, -10.545075590745145888, 15.711648090230719177], [-5.8369967953404051642, 8.5810195285155614897, 9.8642139957759837898], [-5.9251502312697619246, 8.2897860538008281139, 8.9116322224150632536], [-5.9251502312697619246, -1.2732615058295262411, 11.835349269642431835], [-5.9251502312697619246, -10.836309065459879264, 14.759066316869798641], [-6.895398463720493254, -10.515689075464781155, 15.807767050683622756], [-6.895398463720493254, 8.6104060437959262231, 9.9603329562288873689], [-7.0009241527878049993, 8.3196667739247160966, 9.0093676540744773718], [-7.0009241527878049993, -1.2433807857056371482, 11.933084701301844177], [-7.0009241527878049993, -10.806428345335991281, 14.856801748529212759], [-7.9411432512251920457, -10.47984140089607763, 15.925019510913235976], [-7.9411432512251920457, 8.6462537183646297478, 10.077585416458500589], [-8.0611639241462889061, 8.355995453894500713, 9.1281934120797192378], [-8.0611639241462889061, -1.2070521057358516437, 12.05191045930708782], [-8.0611639241462889061, -10.770099665366206665, 14.975627506534454625], [-8.9961818731247724656, -10.441385424593725872, 16.050803341698156146], [-8.9961818731247724656, 8.6847096946669815054, 10.203369247243418982], [-9.1335113425972984658, 8.3951080877002439706, 9.2561250727790511661], [-9.1335113425972984658, -1.1679394719301097183, 12.179842120006419748], [-9.1335113425972984658, -10.730987031560463407, 15.103559167233786553], [-10.037034938423907704, -10.394932932465536624, 16.202742597210743725], [-10.037034938423907704, 8.7311621867951707543, 10.355308502756006561], [-10.188799857367065727, 8.4421771354154273581, 9.4100809907477991345], [-10.188799857367065727, -1.120870424214927441, 12.33379803797516594], [-10.188799857367065727, -10.68391798384528002, 15.257515085202534522], [-11.087832325460665217, -10.347438306258311513, 16.358090519704578725], [-11.087832325460665217, 8.7786568130023958645, 10.510656425249845114], [-11.25682453492664159, 8.4904901774226715361, 9.5681058307041340072], [-11.25682453492664159, -1.0725573822076825969, 12.491822877931500813], [-11.25682453492664159, -10.635604941838035842, 15.415539925158867618], [-12.130826484007744881, -10.28998208592428476, 16.546021348432333298], [-12.130826484007744881, 8.8361130333364226175, 10.698587253977599687], [-12.316465776491348549, 8.5488233456115647613, 9.7589050266192529648], [-12.316465776491348549, -1.0142242140187893717, 12.68262207384661977], [-12.316465776491348549, -10.577271773649142617, 15.606339121073986576], [-13.167274479509028851, -10.232609601207819594, 16.733678290296118973], [-13.167274479509028851, 8.8934855180528877838, 10.886244195841383586], [-13.36775592988668393, 8.6070496903803430655, 9.9493548188709652891], [-13.36775592988668393, -0.9559978692500109565, 12.873071866098332094], [-13.36775592988668393, -10.519045428880364312, 15.796788913325700676], [-14.205661711313787521, -10.165804085335658158, 16.952189286815759317], [-14.205661711313787521, 8.96029103392504922, 11.104755192361025706], [-14.423211305781006075, 8.6749218454890240082, 10.171354635130359867], [-14.423211305781006075, -0.88812571414133034686, 13.095071682357726672], [-14.423211305781006075, -10.45117327377168337, 16.018788729585093478], [-15.232758954180525279, -10.098059861404790283, 17.173770659047207232], [-15.232758954180525279, 9.0280352578559170951, 11.326336564592470069], [-15.464523864863501501, 8.7436243006743357142, 10.396070240569519783], [-15.464523864863501501, -0.81942325895601786367, 13.319787287796886588], [-15.464523864863501501, -10.382470818586371664, 16.24350433502425517], [-16.263941232543412951, -10.022378056180794914, 17.421314689835732281], [-16.263941232543412951, 9.1037170630799124638, 11.573880595380996894], [-16.512649129623643063, 8.8205321051208152028, 10.647624334125161738], [-16.512649129623643063, -0.74251545450953948535, 13.571341381352528543], [-16.512649129623643063, -10.305563014139892175, 16.495058428579895349], [-17.249434174697078959, -9.946811825017595865, 17.668480694904857842], [-17.249434174697078959, 9.1792832942431115129, 11.821046600450122455], [-17.503165223042536525, 8.8964795095546467252, 10.896037100784631946], [-17.503165223042536525, -0.66656805007570774091, 13.819754148011998751], [-17.503165223042536525, -10.229615609706060653, 16.743471195239365557]], faces = [[0, 1, 6, 5], [1, 2, 7, 6], [2, 3, 8, 7], [3, 4, 9, 8], [4, 0, 5, 9], [5, 6, 11, 10], [6, 7, 12, 11], [7, 8, 13, 12], [8, 9, 14, 13], [9, 5, 10, 14], [10, 11, 16, 15], [11, 12, 17, 16], [12, 13, 18, 17], [13, 14, 19, 18], [14, 10, 15, 19], [15, 16, 21, 20], [16, 17, 22, 21], [17, 18, 23, 22], [18, 19, 24, 23], [19, 15, 20, 24], [20, 21, 26, 25], [21, 22, 27, 26], [22, 23, 28, 27], [23, 24, 29, 28], [24, 20, 25, 29], [25, 26, 31, 30], [26, 27, 32, 31], [27, 28, 33, 32], [28, 29, 34, 33], [29, 25, 30, 34], [30, 31, 36, 35], [31, 32, 37, 36], [32, 33, 38, 37], [33, 34, 39, 38], [34, 30, 35, 39], [35, 36, 41, 40], [36, 37, 42, 41], [37, 38, 43, 42], [38, 39, 44, 43], [39, 35, 40, 44], [40, 41, 46, 45], [41, 42, 47, 46], [42, 43, 48, 47], [43, 44, 49, 48], [44, 40, 45, 49], [45, 46, 51, 50], [46, 47, 52, 51], [47, 48, 53, 52], [48, 49, 54, 53], [49, 45, 50, 54], [50, 51, 56, 55], [51, 52, 57, 56], [52, 53, 58, 57], [53, 54, 59, 58], [54, 50, 55, 59], [55, 56, 61, 60], [56, 57, 62, 61], [57, 58, 63, 62], [58, 59, 64, 63], [59, 55, 60, 64], [60, 61, 66, 65], [61, 62, 67, 66], [62, 63, 68, 67], [63, 64, 69, 68], [64, 60, 65, 69], [65, 66, 71, 70], [66, 67, 72, 71], [67, 68, 73, 72], [68, 69, 74, 73], [69, 65, 70, 74], [70, 71, 76, 75], [71, 72, 77, 76], [72, 73, 78, 77], [73, 74, 79, 78], [74, 70, 75, 79], [75, 76, 81, 80], [76, 77, 82, 81], [77, 78, 83, 82], [78, 79, 84, 83], [79, 75, 80, 84], [80, 81, 86, 85], [81, 82, 87, 86], [82, 83, 88, 87], [83, 84, 89, 88], [84, 80, 85, 89], [85, 86, 91, 90], [86, 87, 92, 91], [87, 88, 93, 92], [88, 89, 94, 93], [89, 85, 90, 94], [90, 91, 96, 95], [91, 92, 97, 96], [92, 93, 98, 97], [93, 94, 99, 98], [94, 90, 95, 99], [95, 96, 101, 100], [96, 97, 102, 101], [97, 98, 103, 102], [98, 99, 104, 103], [99, 95, 100, 104], [100, 101, 106, 105], [101, 102, 107, 106], [102, 103, 108, 107], [103, 104, 109, 108], [104, 100, 105, 109], [105, 106, 111, 110], [106, 107, 112, 111], [107, 108, 113, 112], [108, 109, 114, 113], [109, 105, 110, 114], [110, 111, 116, 115], [111, 112, 117, 116], [112, 113, 118, 117], [113, 114, 119, 118], [114, 110, 115, 119], [115, 116, 121, 120], [116, 117, 122, 121], [117, 118, 123, 122], [118, 119, 124, 123], [119, 115, 120, 124], [120, 121, 126, 125], [121, 122, 127, 126], [122, 123, 128, 127], [123, 124, 129, 128], [124, 120, 125, 129], [125, 126, 131, 130], [126, 127, 132, 131], [127, 128, 133, 132], [128, 129, 134, 133], [129, 125, 130, 134], [130, 131, 136, 135], [131, 132, 137, 136], [132, 133, 138, 137], [133, 134, 139, 138], [134, 130, 135, 139], [135, 136, 141, 140], [136, 137, 142, 141], [137, 138, 143, 142], [138, 139, 144, 143], [139, 135, 140, 144], [140, 141, 146, 145], [141, 142, 147, 146], [142, 143, 148, 147], [143, 144, 149, 148], [144, 140, 145, 149], [145, 146, 151, 150], [146, 147, 152, 151], [147, 148, 153, 152], [148, 149, 154, 153], [149, 145, 150, 154], [150, 151, 156, 155], [151, 152, 157, 156], [152, 153, 158, 157], [153, 154, 159, 158], [154, 150, 155, 159], [155, 156, 161, 160], [156, 157, 162, 161], [157, 158, 163, 162], [158, 159, 164, 163], [159, 155, 160, 164], [160, 161, 166, 165], [161, 162, 167, 166], [162, 163, 168, 167], [163, 164, 169, 168], [164, 160, 165, 169], [4, 3, 2, 1, 0], [165, 166, 167, 168, 169]], convexity = 5); +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 608b60c1..efbbc163 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1217,10 +1217,12 @@ list(APPEND BUGS_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue584.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue945c.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue945d.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1089.scad + ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1089b.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1105.scad) list(APPEND EXPORT3D_TEST_FILES ${BUGS_FILES}) list(REMOVE_ITEM EXPORT3D_TEST_FILES - ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue899.scad) + ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue899.scad + ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1089.scad) #list(APPEND EXPORTCSG_TEST_FILES ) list(APPEND ALL_2D_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue899.scad) diff --git a/tests/regression/cgalpngtest/issue1089b-expected.png b/tests/regression/cgalpngtest/issue1089b-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..f90390f007711b59155a5345dcffe00aa4d6e4fa GIT binary patch literal 6488 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 g^8YhNbxJPyBG!J!|cK_WAaA&OUpu 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 literal 0 HcmV?d00001