Merge branch 'tm_sla_tests'

This commit is contained in:
tamasmeszaros 2019-10-04 18:34:08 +02:00
commit 5bf3d5aabf
69 changed files with 54707 additions and 4293 deletions

View file

@ -16,9 +16,9 @@ const std::string USAGE_STR = {
namespace Slic3r { namespace sla { namespace Slic3r { namespace sla {
Contour3D create_base_pool(const Polygons &ground_layer, Contour3D create_pad(const Polygons &ground_layer,
const ExPolygons &holes = {}, const ExPolygons &holes = {},
const PoolConfig& cfg = PoolConfig()); const PadConfig& cfg = PadConfig());
Contour3D walls(const Polygon& floor_plate, const Polygon& ceiling, Contour3D walls(const Polygon& floor_plate, const Polygon& ceiling,
double floor_z_mm, double ceiling_z_mm, double floor_z_mm, double ceiling_z_mm,
@ -45,7 +45,7 @@ int main(const int argc, const char *argv[]) {
model.align_to_origin(); model.align_to_origin();
ExPolygons ground_slice; ExPolygons ground_slice;
sla::base_plate(model, ground_slice, 0.1f); sla::pad_plate(model, ground_slice, 0.1f);
if(ground_slice.empty()) return EXIT_FAILURE; if(ground_slice.empty()) return EXIT_FAILURE;
ground_slice = offset_ex(ground_slice, 0.5); ground_slice = offset_ex(ground_slice, 0.5);
@ -56,10 +56,10 @@ int main(const int argc, const char *argv[]) {
bench.start(); bench.start();
sla::PoolConfig cfg; sla::PadConfig cfg;
cfg.min_wall_height_mm = 0; cfg.min_wall_height_mm = 0;
cfg.edge_radius_mm = 0; cfg.edge_radius_mm = 0;
mesh = sla::create_base_pool(to_polygons(ground_slice), {}, cfg); mesh = sla::create_pad(to_polygons(ground_slice), {}, cfg);
bench.stop(); bench.stop();

View file

@ -156,7 +156,7 @@ namespace agg
//------------------------------------------------------------------- //-------------------------------------------------------------------
template<class VertexSource> template<class VertexSource>
void add_path(VertexSource& vs, unsigned path_id=0) void add_path(VertexSource &&vs, unsigned path_id=0)
{ {
double x; double x;
double y; double y;

View file

@ -15,7 +15,7 @@ public:
PointClass max; PointClass max;
bool defined; bool defined;
BoundingBoxBase() : defined(false), min(PointClass::Zero()), max(PointClass::Zero()) {} BoundingBoxBase() : min(PointClass::Zero()), max(PointClass::Zero()), defined(false) {}
BoundingBoxBase(const PointClass &pmin, const PointClass &pmax) : BoundingBoxBase(const PointClass &pmin, const PointClass &pmax) :
min(pmin), max(pmax), defined(pmin(0) < pmax(0) && pmin(1) < pmax(1)) {} min(pmin), max(pmax), defined(pmin(0) < pmax(0) && pmin(1) < pmax(1)) {}
BoundingBoxBase(const std::vector<PointClass>& points) : min(PointClass::Zero()), max(PointClass::Zero()) BoundingBoxBase(const std::vector<PointClass>& points) : min(PointClass::Zero()), max(PointClass::Zero())
@ -59,7 +59,7 @@ template <class PointClass>
class BoundingBox3Base : public BoundingBoxBase<PointClass> class BoundingBox3Base : public BoundingBoxBase<PointClass>
{ {
public: public:
BoundingBox3Base() : BoundingBoxBase<PointClass>() {}; BoundingBox3Base() : BoundingBoxBase<PointClass>() {}
BoundingBox3Base(const PointClass &pmin, const PointClass &pmax) : BoundingBox3Base(const PointClass &pmin, const PointClass &pmax) :
BoundingBoxBase<PointClass>(pmin, pmax) BoundingBoxBase<PointClass>(pmin, pmax)
{ if (pmin(2) >= pmax(2)) BoundingBoxBase<PointClass>::defined = false; } { if (pmin(2) >= pmax(2)) BoundingBoxBase<PointClass>::defined = false; }
@ -100,6 +100,33 @@ public:
} }
}; };
// Will prevent warnings caused by non existing definition of template in hpp
extern template void BoundingBoxBase<Point>::scale(double factor);
extern template void BoundingBoxBase<Vec2d>::scale(double factor);
extern template void BoundingBoxBase<Vec3d>::scale(double factor);
extern template void BoundingBoxBase<Point>::offset(coordf_t delta);
extern template void BoundingBoxBase<Vec2d>::offset(coordf_t delta);
extern template void BoundingBoxBase<Point>::merge(const Point &point);
extern template void BoundingBoxBase<Vec2d>::merge(const Vec2d &point);
extern template void BoundingBoxBase<Point>::merge(const Points &points);
extern template void BoundingBoxBase<Vec2d>::merge(const Pointfs &points);
extern template void BoundingBoxBase<Point>::merge(const BoundingBoxBase<Point> &bb);
extern template void BoundingBoxBase<Vec2d>::merge(const BoundingBoxBase<Vec2d> &bb);
extern template Point BoundingBoxBase<Point>::size() const;
extern template Vec2d BoundingBoxBase<Vec2d>::size() const;
extern template double BoundingBoxBase<Point>::radius() const;
extern template double BoundingBoxBase<Vec2d>::radius() const;
extern template Point BoundingBoxBase<Point>::center() const;
extern template Vec2d BoundingBoxBase<Vec2d>::center() const;
extern template void BoundingBox3Base<Vec3d>::merge(const Vec3d &point);
extern template void BoundingBox3Base<Vec3d>::merge(const Pointf3s &points);
extern template void BoundingBox3Base<Vec3d>::merge(const BoundingBox3Base<Vec3d> &bb);
extern template Vec3d BoundingBox3Base<Vec3d>::size() const;
extern template double BoundingBox3Base<Vec3d>::radius() const;
extern template void BoundingBox3Base<Vec3d>::offset(coordf_t delta);
extern template Vec3d BoundingBox3Base<Vec3d>::center() const;
extern template coordf_t BoundingBox3Base<Vec3d>::max_size() const;
class BoundingBox : public BoundingBoxBase<Point> class BoundingBox : public BoundingBoxBase<Point>
{ {
public: public:
@ -113,9 +140,9 @@ public:
// to encompass the original bounding box. // to encompass the original bounding box.
void align_to_grid(const coord_t cell_size); void align_to_grid(const coord_t cell_size);
BoundingBox() : BoundingBoxBase<Point>() {}; BoundingBox() : BoundingBoxBase<Point>() {}
BoundingBox(const Point &pmin, const Point &pmax) : BoundingBoxBase<Point>(pmin, pmax) {}; BoundingBox(const Point &pmin, const Point &pmax) : BoundingBoxBase<Point>(pmin, pmax) {}
BoundingBox(const Points &points) : BoundingBoxBase<Point>(points) {}; BoundingBox(const Points &points) : BoundingBoxBase<Point>(points) {}
BoundingBox(const Lines &lines); BoundingBox(const Lines &lines);
friend BoundingBox get_extents_rotated(const Points &points, double angle); friend BoundingBox get_extents_rotated(const Points &points, double angle);
@ -124,25 +151,25 @@ public:
class BoundingBox3 : public BoundingBox3Base<Vec3crd> class BoundingBox3 : public BoundingBox3Base<Vec3crd>
{ {
public: public:
BoundingBox3() : BoundingBox3Base<Vec3crd>() {}; BoundingBox3() : BoundingBox3Base<Vec3crd>() {}
BoundingBox3(const Vec3crd &pmin, const Vec3crd &pmax) : BoundingBox3Base<Vec3crd>(pmin, pmax) {}; BoundingBox3(const Vec3crd &pmin, const Vec3crd &pmax) : BoundingBox3Base<Vec3crd>(pmin, pmax) {}
BoundingBox3(const Points3& points) : BoundingBox3Base<Vec3crd>(points) {}; BoundingBox3(const Points3& points) : BoundingBox3Base<Vec3crd>(points) {}
}; };
class BoundingBoxf : public BoundingBoxBase<Vec2d> class BoundingBoxf : public BoundingBoxBase<Vec2d>
{ {
public: public:
BoundingBoxf() : BoundingBoxBase<Vec2d>() {}; BoundingBoxf() : BoundingBoxBase<Vec2d>() {}
BoundingBoxf(const Vec2d &pmin, const Vec2d &pmax) : BoundingBoxBase<Vec2d>(pmin, pmax) {}; BoundingBoxf(const Vec2d &pmin, const Vec2d &pmax) : BoundingBoxBase<Vec2d>(pmin, pmax) {}
BoundingBoxf(const std::vector<Vec2d> &points) : BoundingBoxBase<Vec2d>(points) {}; BoundingBoxf(const std::vector<Vec2d> &points) : BoundingBoxBase<Vec2d>(points) {}
}; };
class BoundingBoxf3 : public BoundingBox3Base<Vec3d> class BoundingBoxf3 : public BoundingBox3Base<Vec3d>
{ {
public: public:
BoundingBoxf3() : BoundingBox3Base<Vec3d>() {}; BoundingBoxf3() : BoundingBox3Base<Vec3d>() {}
BoundingBoxf3(const Vec3d &pmin, const Vec3d &pmax) : BoundingBox3Base<Vec3d>(pmin, pmax) {}; BoundingBoxf3(const Vec3d &pmin, const Vec3d &pmax) : BoundingBox3Base<Vec3d>(pmin, pmax) {}
BoundingBoxf3(const std::vector<Vec3d> &points) : BoundingBox3Base<Vec3d>(points) {}; BoundingBoxf3(const std::vector<Vec3d> &points) : BoundingBox3Base<Vec3d>(points) {}
BoundingBoxf3 transformed(const Transform3d& matrix) const; BoundingBoxf3 transformed(const Transform3d& matrix) const;
}; };

View file

@ -176,8 +176,13 @@ add_library(libslic3r STATIC
miniz_extension.cpp miniz_extension.cpp
SLA/SLACommon.hpp SLA/SLACommon.hpp
SLA/SLABoilerPlate.hpp SLA/SLABoilerPlate.hpp
SLA/SLABasePool.hpp SLA/SLAPad.hpp
SLA/SLABasePool.cpp SLA/SLAPad.cpp
SLA/SLASupportTreeBuilder.hpp
SLA/SLASupportTreeBuildsteps.hpp
SLA/SLASupportTreeBuildsteps.cpp
SLA/SLASupportTreeBuilder.cpp
SLA/SLAConcurrency.hpp
SLA/SLASupportTree.hpp SLA/SLASupportTree.hpp
SLA/SLASupportTree.cpp SLA/SLASupportTree.cpp
SLA/SLASupportTreeIGL.cpp SLA/SLASupportTreeIGL.cpp
@ -215,6 +220,7 @@ target_link_libraries(libslic3r
qhull qhull
semver semver
tbb tbb
${CMAKE_DL_LIBS}
) )
if(WIN32) if(WIN32)

View file

@ -194,6 +194,19 @@ ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polygons &input)
return retval; return retval;
} }
ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const ExPolygons &input)
{
ClipperLib::Paths retval;
for (auto &ep : input) {
retval.emplace_back(Slic3rMultiPoint_to_ClipperPath(ep.contour));
for (auto &h : ep.holes)
retval.emplace_back(Slic3rMultiPoint_to_ClipperPath(h));
}
return retval;
}
ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polylines &input) ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polylines &input)
{ {
ClipperLib::Paths retval; ClipperLib::Paths retval;
@ -472,14 +485,16 @@ ExPolygons offset2_ex(const ExPolygons &expolygons, const float delta1,
return union_ex(polys); return union_ex(polys);
} }
template <class T> template<class T, class TSubj, class TClip>
T T _clipper_do(const ClipperLib::ClipType clipType,
_clipper_do(const ClipperLib::ClipType clipType, const Polygons &subject, TSubj && subject,
const Polygons &clip, const ClipperLib::PolyFillType fillType, const bool safety_offset_) TClip && clip,
const ClipperLib::PolyFillType fillType,
const bool safety_offset_)
{ {
// read input // read input
ClipperLib::Paths input_subject = Slic3rMultiPoints_to_ClipperPaths(subject); ClipperLib::Paths input_subject = Slic3rMultiPoints_to_ClipperPaths(std::forward<TSubj>(subject));
ClipperLib::Paths input_clip = Slic3rMultiPoints_to_ClipperPaths(clip); ClipperLib::Paths input_clip = Slic3rMultiPoints_to_ClipperPaths(std::forward<TClip>(clip));
// perform safety offset // perform safety offset
if (safety_offset_) { if (safety_offset_) {
@ -648,12 +663,26 @@ _clipper_ln(ClipperLib::ClipType clipType, const Lines &subject, const Polygons
return retval; return retval;
} }
ClipperLib::PolyTree ClipperLib::PolyTree union_pt(const Polygons &subject, bool safety_offset_)
union_pt(const Polygons &subject, bool safety_offset_)
{ {
return _clipper_do<ClipperLib::PolyTree>(ClipperLib::ctUnion, subject, Polygons(), ClipperLib::pftEvenOdd, safety_offset_); return _clipper_do<ClipperLib::PolyTree>(ClipperLib::ctUnion, subject, Polygons(), ClipperLib::pftEvenOdd, safety_offset_);
} }
ClipperLib::PolyTree union_pt(const ExPolygons &subject, bool safety_offset_)
{
return _clipper_do<ClipperLib::PolyTree>(ClipperLib::ctUnion, subject, Polygons(), ClipperLib::pftEvenOdd, safety_offset_);
}
ClipperLib::PolyTree union_pt(Polygons &&subject, bool safety_offset_)
{
return _clipper_do<ClipperLib::PolyTree>(ClipperLib::ctUnion, std::move(subject), Polygons(), ClipperLib::pftEvenOdd, safety_offset_);
}
ClipperLib::PolyTree union_pt(ExPolygons &&subject, bool safety_offset_)
{
return _clipper_do<ClipperLib::PolyTree>(ClipperLib::ctUnion, std::move(subject), Polygons(), ClipperLib::pftEvenOdd, safety_offset_);
}
Polygons Polygons
union_pt_chained(const Polygons &subject, bool safety_offset_) union_pt_chained(const Polygons &subject, bool safety_offset_)
{ {
@ -664,28 +693,123 @@ union_pt_chained(const Polygons &subject, bool safety_offset_)
return retval; return retval;
} }
void traverse_pt(ClipperLib::PolyNodes &nodes, Polygons* retval) static ClipperLib::PolyNodes order_nodes(const ClipperLib::PolyNodes &nodes)
{
// collect ordering points
Points ordering_points;
ordering_points.reserve(nodes.size());
for (const ClipperLib::PolyNode *node : nodes)
ordering_points.emplace_back(Point(node->Contour.front().X, node->Contour.front().Y));
// perform the ordering
ClipperLib::PolyNodes ordered_nodes = chain_clipper_polynodes(ordering_points, nodes);
return ordered_nodes;
}
enum class e_ordering {
ORDER_POLYNODES,
DONT_ORDER_POLYNODES
};
template<e_ordering o>
void foreach_node(const ClipperLib::PolyNodes &nodes,
std::function<void(const ClipperLib::PolyNode *)> fn);
template<> void foreach_node<e_ordering::DONT_ORDER_POLYNODES>(
const ClipperLib::PolyNodes & nodes,
std::function<void(const ClipperLib::PolyNode *)> fn)
{
for (auto &n : nodes) fn(n);
}
template<> void foreach_node<e_ordering::ORDER_POLYNODES>(
const ClipperLib::PolyNodes & nodes,
std::function<void(const ClipperLib::PolyNode *)> fn)
{
auto ordered_nodes = order_nodes(nodes);
for (auto &n : ordered_nodes) fn(n);
}
template<e_ordering o>
void _traverse_pt(const ClipperLib::PolyNodes &nodes, Polygons *retval)
{ {
/* use a nearest neighbor search to order these children /* use a nearest neighbor search to order these children
TODO: supply start_near to chained_path() too? */ TODO: supply start_near to chained_path() too? */
// collect ordering points
Points ordering_points;
ordering_points.reserve(nodes.size());
for (ClipperLib::PolyNode *pn : nodes)
ordering_points.emplace_back(Point(pn->Contour.front().X, pn->Contour.front().Y));
// perform the ordering
ClipperLib::PolyNodes ordered_nodes = chain_clipper_polynodes(ordering_points, nodes);
// push results recursively // push results recursively
for (ClipperLib::PolyNode *pn : ordered_nodes) { foreach_node<o>(nodes, [&retval](const ClipperLib::PolyNode *node) {
// traverse the next depth // traverse the next depth
traverse_pt(pn->Childs, retval); _traverse_pt<o>(node->Childs, retval);
retval->emplace_back(ClipperPath_to_Slic3rPolygon(pn->Contour)); retval->emplace_back(ClipperPath_to_Slic3rPolygon(node->Contour));
if (pn->IsHole()) if (node->IsHole()) retval->back().reverse(); // ccw
retval->back().reverse(); // ccw });
} }
template<e_ordering o>
void _traverse_pt(const ClipperLib::PolyNode *tree, ExPolygons *retval)
{
if (!retval || !tree) return;
ExPolygons &retv = *retval;
std::function<void(const ClipperLib::PolyNode*, ExPolygon&)> hole_fn;
auto contour_fn = [&retv, &hole_fn](const ClipperLib::PolyNode *pptr) {
ExPolygon poly;
poly.contour.points = ClipperPath_to_Slic3rPolygon(pptr->Contour);
auto fn = std::bind(hole_fn, std::placeholders::_1, poly);
foreach_node<o>(pptr->Childs, fn);
retv.push_back(poly);
};
hole_fn = [&contour_fn](const ClipperLib::PolyNode *pptr, ExPolygon& poly)
{
poly.holes.emplace_back();
poly.holes.back().points = ClipperPath_to_Slic3rPolygon(pptr->Contour);
foreach_node<o>(pptr->Childs, contour_fn);
};
contour_fn(tree);
}
template<e_ordering o>
void _traverse_pt(const ClipperLib::PolyNodes &nodes, ExPolygons *retval)
{
// Here is the actual traverse
foreach_node<o>(nodes, [&retval](const ClipperLib::PolyNode *node) {
_traverse_pt<o>(node, retval);
});
}
void traverse_pt(const ClipperLib::PolyNode *tree, ExPolygons *retval)
{
_traverse_pt<e_ordering::ORDER_POLYNODES>(tree, retval);
}
void traverse_pt_unordered(const ClipperLib::PolyNode *tree, ExPolygons *retval)
{
_traverse_pt<e_ordering::DONT_ORDER_POLYNODES>(tree, retval);
}
void traverse_pt(const ClipperLib::PolyNodes &nodes, Polygons *retval)
{
_traverse_pt<e_ordering::ORDER_POLYNODES>(nodes, retval);
}
void traverse_pt(const ClipperLib::PolyNodes &nodes, ExPolygons *retval)
{
_traverse_pt<e_ordering::ORDER_POLYNODES>(nodes, retval);
}
void traverse_pt_unordered(const ClipperLib::PolyNodes &nodes, Polygons *retval)
{
_traverse_pt<e_ordering::DONT_ORDER_POLYNODES>(nodes, retval);
}
void traverse_pt_unordered(const ClipperLib::PolyNodes &nodes, ExPolygons *retval)
{
_traverse_pt<e_ordering::DONT_ORDER_POLYNODES>(nodes, retval);
} }
Polygons simplify_polygons(const Polygons &subject, bool preserve_collinear) Polygons simplify_polygons(const Polygons &subject, bool preserve_collinear)

View file

@ -34,6 +34,7 @@ Slic3r::ExPolygons PolyTreeToExPolygons(ClipperLib::PolyTree& polytree);
ClipperLib::Path Slic3rMultiPoint_to_ClipperPath(const Slic3r::MultiPoint &input); ClipperLib::Path Slic3rMultiPoint_to_ClipperPath(const Slic3r::MultiPoint &input);
ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polygons &input); ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polygons &input);
ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const ExPolygons &input);
ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polylines &input); ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polylines &input);
Slic3r::Polygon ClipperPath_to_Slic3rPolygon(const ClipperLib::Path &input); Slic3r::Polygon ClipperPath_to_Slic3rPolygon(const ClipperLib::Path &input);
Slic3r::Polyline ClipperPath_to_Slic3rPolyline(const ClipperLib::Path &input); Slic3r::Polyline ClipperPath_to_Slic3rPolyline(const ClipperLib::Path &input);
@ -215,8 +216,19 @@ inline Slic3r::ExPolygons union_ex(const Slic3r::Surfaces &subject, bool safety_
ClipperLib::PolyTree union_pt(const Slic3r::Polygons &subject, bool safety_offset_ = false); ClipperLib::PolyTree union_pt(const Slic3r::Polygons &subject, bool safety_offset_ = false);
ClipperLib::PolyTree union_pt(const Slic3r::ExPolygons &subject, bool safety_offset_ = false);
ClipperLib::PolyTree union_pt(Slic3r::Polygons &&subject, bool safety_offset_ = false);
ClipperLib::PolyTree union_pt(Slic3r::ExPolygons &&subject, bool safety_offset_ = false);
Slic3r::Polygons union_pt_chained(const Slic3r::Polygons &subject, bool safety_offset_ = false); Slic3r::Polygons union_pt_chained(const Slic3r::Polygons &subject, bool safety_offset_ = false);
void traverse_pt(ClipperLib::PolyNodes &nodes, Slic3r::Polygons* retval);
void traverse_pt(const ClipperLib::PolyNodes &nodes, Slic3r::Polygons *retval);
void traverse_pt(const ClipperLib::PolyNodes &nodes, Slic3r::ExPolygons *retval);
void traverse_pt(const ClipperLib::PolyNode *tree, Slic3r::ExPolygons *retval);
void traverse_pt_unordered(const ClipperLib::PolyNodes &nodes, Slic3r::Polygons *retval);
void traverse_pt_unordered(const ClipperLib::PolyNodes &nodes, Slic3r::ExPolygons *retval);
void traverse_pt_unordered(const ClipperLib::PolyNode *tree, Slic3r::ExPolygons *retval);
/* OTHER */ /* OTHER */
Slic3r::Polygons simplify_polygons(const Slic3r::Polygons &subject, bool preserve_collinear = false); Slic3r::Polygons simplify_polygons(const Slic3r::Polygons &subject, bool preserve_collinear = false);

View file

@ -24,6 +24,9 @@ public:
ExPolygon& operator=(const ExPolygon &other) { contour = other.contour; holes = other.holes; return *this; } ExPolygon& operator=(const ExPolygon &other) { contour = other.contour; holes = other.holes; return *this; }
ExPolygon& operator=(ExPolygon &&other) { contour = std::move(other.contour); holes = std::move(other.holes); return *this; } ExPolygon& operator=(ExPolygon &&other) { contour = std::move(other.contour); holes = std::move(other.holes); return *this; }
inline explicit ExPolygon(const Polygon &p): contour(p) {}
inline explicit ExPolygon(Polygon &&p): contour(std::move(p)) {}
Polygon contour; Polygon contour;
Polygons holes; Polygons holes;

View file

@ -15,39 +15,41 @@
namespace Slic3r { namespace Slic3r {
bool load_obj(const char *path, Model *model, const char *object_name_in) bool load_obj(const char *path, TriangleMesh *meshptr)
{ {
if(meshptr == nullptr) return false;
// Parse the OBJ file. // Parse the OBJ file.
ObjParser::ObjData data; ObjParser::ObjData data;
if (! ObjParser::objparse(path, data)) { if (! ObjParser::objparse(path, data)) {
// die "Failed to parse $file\n" if !-e $path; // die "Failed to parse $file\n" if !-e $path;
return false; return false;
} }
// Count the faces and verify, that all faces are triangular. // Count the faces and verify, that all faces are triangular.
size_t num_faces = 0; size_t num_faces = 0;
size_t num_quads = 0; size_t num_quads = 0;
for (size_t i = 0; i < data.vertices.size(); ) { for (size_t i = 0; i < data.vertices.size(); ) {
size_t j = i; size_t j = i;
for (; j < data.vertices.size() && data.vertices[j].coordIdx != -1; ++ j) ; for (; j < data.vertices.size() && data.vertices[j].coordIdx != -1; ++ j) ;
if (i == j) if (i == j)
continue; continue;
size_t face_vertices = j - i; size_t face_vertices = j - i;
if (face_vertices != 3 && face_vertices != 4) { if (face_vertices != 3 && face_vertices != 4) {
// Non-triangular and non-quad faces are not supported as of now. // Non-triangular and non-quad faces are not supported as of now.
return false; return false;
} }
if (face_vertices == 4) if (face_vertices == 4)
++ num_quads; ++ num_quads;
++ num_faces; ++ num_faces;
i = j + 1; i = j + 1;
} }
// Convert ObjData into STL. // Convert ObjData into STL.
TriangleMesh mesh; TriangleMesh &mesh = *meshptr;
stl_file &stl = mesh.stl; stl_file &stl = mesh.stl;
stl.stats.type = inmemory; stl.stats.type = inmemory;
stl.stats.number_of_facets = int(num_faces + num_quads); stl.stats.number_of_facets = uint32_t(num_faces + num_quads);
stl.stats.original_num_facets = int(num_faces + num_quads); stl.stats.original_num_facets = int(num_faces + num_quads);
// stl_allocate clears all the allocated data to zero, all normals are set to zeros as well. // stl_allocate clears all the allocated data to zero, all normals are set to zeros as well.
stl_allocate(&stl); stl_allocate(&stl);
@ -68,14 +70,14 @@ bool load_obj(const char *path, Model *model, const char *object_name_in)
++ num_normals; ++ num_normals;
} }
} }
if (data.vertices[i].coordIdx != -1) { if (data.vertices[i].coordIdx != -1) {
// This is a quad. Produce the other triangle. // This is a quad. Produce the other triangle.
stl_facet &facet2 = stl.facet_start[i_face++]; stl_facet &facet2 = stl.facet_start[i_face++];
facet2.vertex[0] = facet.vertex[0]; facet2.vertex[0] = facet.vertex[0];
facet2.vertex[1] = facet.vertex[2]; facet2.vertex[1] = facet.vertex[2];
const ObjParser::ObjVertex &vertex = data.vertices[i++]; const ObjParser::ObjVertex &vertex = data.vertices[i++];
memcpy(facet2.vertex[2].data(), &data.coordinates[vertex.coordIdx * 4], 3 * sizeof(float)); memcpy(facet2.vertex[2].data(), &data.coordinates[vertex.coordIdx * 4], 3 * sizeof(float));
if (vertex.normalIdx != -1) { if (vertex.normalIdx != -1) {
normal(0) += data.normals[vertex.normalIdx*3]; normal(0) += data.normals[vertex.normalIdx*3];
normal(1) += data.normals[vertex.normalIdx*3+1]; normal(1) += data.normals[vertex.normalIdx*3+1];
normal(2) += data.normals[vertex.normalIdx*3+2]; normal(2) += data.normals[vertex.normalIdx*3+2];
@ -96,25 +98,37 @@ bool load_obj(const char *path, Model *model, const char *object_name_in)
if (len > EPSILON) if (len > EPSILON)
facet.normal = normal / len; facet.normal = normal / len;
} }
} }
stl_get_size(&stl); stl_get_size(&stl);
mesh.repair(); mesh.repair();
if (mesh.facets_count() == 0) { if (mesh.facets_count() == 0) {
// die "This STL file couldn't be read because it's empty.\n" // die "This OBJ file couldn't be read because it's empty.\n"
return false; return false;
} }
std::string object_name;
if (object_name_in == nullptr) {
const char *last_slash = strrchr(path, DIR_SEPARATOR);
object_name.assign((last_slash == nullptr) ? path : last_slash + 1);
} else
object_name.assign(object_name_in);
model->add_object(object_name.c_str(), path, std::move(mesh));
return true; return true;
} }
bool load_obj(const char *path, Model *model, const char *object_name_in)
{
TriangleMesh mesh;
bool ret = load_obj(path, &mesh);
if (ret) {
std::string object_name;
if (object_name_in == nullptr) {
const char *last_slash = strrchr(path, DIR_SEPARATOR);
object_name.assign((last_slash == nullptr) ? path : last_slash + 1);
} else
object_name.assign(object_name_in);
model->add_object(object_name.c_str(), path, std::move(mesh));
}
return ret;
}
bool store_obj(const char *path, TriangleMesh *mesh) bool store_obj(const char *path, TriangleMesh *mesh)
{ {
//FIXME returning false even if write failed. //FIXME returning false even if write failed.

View file

@ -5,8 +5,10 @@ namespace Slic3r {
class TriangleMesh; class TriangleMesh;
class Model; class Model;
class ModelObject;
// Load an OBJ file into a provided model. // Load an OBJ file into a provided model.
extern bool load_obj(const char *path, TriangleMesh *mesh);
extern bool load_obj(const char *path, Model *model, const char *object_name = nullptr); extern bool load_obj(const char *path, Model *model, const char *object_name = nullptr);
extern bool store_obj(const char *path, TriangleMesh *mesh); extern bool store_obj(const char *path, TriangleMesh *mesh);

View file

@ -14,6 +14,11 @@
using boost::polygon::voronoi_builder; using boost::polygon::voronoi_builder;
using boost::polygon::voronoi_diagram; using boost::polygon::voronoi_diagram;
namespace ClipperLib {
class PolyNode;
using PolyNodes = std::vector<PolyNode*>;
}
namespace Slic3r { namespace Geometry { namespace Slic3r { namespace Geometry {
// Generic result of an orientation predicate. // Generic result of an orientation predicate.

View file

@ -252,22 +252,15 @@ template<class T> struct remove_cvref
template<class T> using remove_cvref_t = typename remove_cvref<T>::type; template<class T> using remove_cvref_t = typename remove_cvref<T>::type;
template<template<class> class C, class T>
class Container : public C<remove_cvref_t<T>>
{
public:
explicit Container(size_t count, T &&initval)
: C<remove_cvref_t<T>>(count, initval)
{}
};
template<class T> using DefaultContainer = std::vector<T>; template<class T> using DefaultContainer = std::vector<T>;
/// Exactly like Matlab https://www.mathworks.com/help/matlab/ref/linspace.html /// Exactly like Matlab https://www.mathworks.com/help/matlab/ref/linspace.html
template<class T, class I, template<class> class C = DefaultContainer> template<class T, class I, template<class> class Container = DefaultContainer>
inline C<remove_cvref_t<T>> linspace(const T &start, const T &stop, const I &n) inline Container<remove_cvref_t<T>> linspace(const T &start,
const T &stop,
const I &n)
{ {
Container<C, T> vals(n, T()); Container<remove_cvref_t<T>> vals(n, T());
T stride = (stop - start) / n; T stride = (stop - start) / n;
size_t i = 0; size_t i = 0;
@ -282,10 +275,13 @@ inline C<remove_cvref_t<T>> linspace(const T &start, const T &stop, const I &n)
/// in the closest multiple of 'stride' less than or equal to 'end' and /// in the closest multiple of 'stride' less than or equal to 'end' and
/// leaving 'stride' space between each value. /// leaving 'stride' space between each value.
/// Very similar to Matlab [start:stride:end] notation. /// Very similar to Matlab [start:stride:end] notation.
template<class T, template<class> class C = DefaultContainer> template<class T, template<class> class Container = DefaultContainer>
inline C<remove_cvref_t<T>> grid(const T &start, const T &stop, const T &stride) inline Container<remove_cvref_t<T>> grid(const T &start,
const T &stop,
const T &stride)
{ {
Container<C, T> vals(size_t(std::ceil((stop - start) / stride)), T()); Container<remove_cvref_t<T>>
vals(size_t(std::ceil((stop - start) / stride)), T());
int i = 0; int i = 0;
std::generate(vals.begin(), vals.end(), [&i, start, stride] { std::generate(vals.begin(), vals.end(), [&i, start, stride] {
@ -387,10 +383,12 @@ unscaled(const Eigen::Matrix<Tin, N, EigenArgs...> &v) noexcept
return v.template cast<Tout>() * SCALING_FACTOR; return v.template cast<Tout>() * SCALING_FACTOR;
} }
template<class T> inline std::vector<T> reserve_vector(size_t capacity) template<class T, class I, class... Args> // Arbitrary allocator can be used
inline IntegerOnly<I, std::vector<T, Args...>> reserve_vector(I capacity)
{ {
std::vector<T> ret; std::vector<T, Args...> ret;
ret.reserve(capacity); if (capacity > I(0)) ret.reserve(size_t(capacity));
return ret; return ret;
} }

View file

@ -2694,6 +2694,17 @@ void PrintConfigDef::init_sla_params()
def->max = 30; def->max = 30;
def->mode = comExpert; def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(0.)); def->set_default_value(new ConfigOptionFloat(0.));
def = this->add("pad_brim_size", coFloat);
def->label = L("Pad brim size");
def->tooltip = L("How far should the pad extend around the contained geometry");
def->category = L("Pad");
// def->tooltip = L("");
def->sidetext = L("mm");
def->min = 0;
def->max = 30;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(1.6));
def = this->add("pad_max_merge_distance", coFloat); def = this->add("pad_max_merge_distance", coFloat);
def->label = L("Max merge distance"); def->label = L("Max merge distance");
@ -2734,6 +2745,13 @@ void PrintConfigDef::init_sla_params()
def->tooltip = L("Create pad around object and ignore the support elevation"); def->tooltip = L("Create pad around object and ignore the support elevation");
def->mode = comSimple; def->mode = comSimple;
def->set_default_value(new ConfigOptionBool(false)); def->set_default_value(new ConfigOptionBool(false));
def = this->add("pad_around_object_everywhere", coBool);
def->label = L("Pad around object everywhere");
def->category = L("Pad");
def->tooltip = L("Force pad around object everywhere");
def->mode = comSimple;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("pad_object_gap", coFloat); def = this->add("pad_object_gap", coFloat);
def->label = L("Pad object gap"); def->label = L("Pad object gap");

View file

@ -1022,6 +1022,9 @@ public:
// The height of the pad from the bottom to the top not considering the pit // The height of the pad from the bottom to the top not considering the pit
ConfigOptionFloat pad_wall_height /*= 5*/; ConfigOptionFloat pad_wall_height /*= 5*/;
// How far should the pad extend around the contained geometry
ConfigOptionFloat pad_brim_size;
// The greatest distance where two individual pads are merged into one. The // The greatest distance where two individual pads are merged into one. The
// distance is measured roughly from the centroids of the pads. // distance is measured roughly from the centroids of the pads.
@ -1042,7 +1045,9 @@ public:
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
// Disable the elevation (ignore its value) and use the zero elevation mode // Disable the elevation (ignore its value) and use the zero elevation mode
ConfigOptionBool pad_around_object; ConfigOptionBool pad_around_object;
ConfigOptionBool pad_around_object_everywhere;
// This is the gap between the object bottom and the generated pad // This is the gap between the object bottom and the generated pad
ConfigOptionFloat pad_object_gap; ConfigOptionFloat pad_object_gap;
@ -1082,10 +1087,12 @@ protected:
OPT_PTR(pad_enable); OPT_PTR(pad_enable);
OPT_PTR(pad_wall_thickness); OPT_PTR(pad_wall_thickness);
OPT_PTR(pad_wall_height); OPT_PTR(pad_wall_height);
OPT_PTR(pad_brim_size);
OPT_PTR(pad_max_merge_distance); OPT_PTR(pad_max_merge_distance);
// OPT_PTR(pad_edge_radius); // OPT_PTR(pad_edge_radius);
OPT_PTR(pad_wall_slope); OPT_PTR(pad_wall_slope);
OPT_PTR(pad_around_object); OPT_PTR(pad_around_object);
OPT_PTR(pad_around_object_everywhere);
OPT_PTR(pad_object_gap); OPT_PTR(pad_object_gap);
OPT_PTR(pad_object_connector_stride); OPT_PTR(pad_object_connector_stride);
OPT_PTR(pad_object_connector_width); OPT_PTR(pad_object_connector_width);

View file

@ -16,6 +16,7 @@
#include <random> #include <random>
namespace Slic3r { namespace Slic3r {
namespace sla {
/*float SLAAutoSupports::approximate_geodesic_distance(const Vec3d& p1, const Vec3d& p2, Vec3d& n1, Vec3d& n2) /*float SLAAutoSupports::approximate_geodesic_distance(const Vec3d& p1, const Vec3d& p2, Vec3d& n1, Vec3d& n2)
{ {
@ -48,9 +49,16 @@ float SLAAutoSupports::distance_limit(float angle) const
return 1./(2.4*get_required_density(angle)); return 1./(2.4*get_required_density(angle));
}*/ }*/
SLAAutoSupports::SLAAutoSupports(const TriangleMesh& mesh, const sla::EigenMesh3D& emesh, const std::vector<ExPolygons>& slices, const std::vector<float>& heights, SLAAutoSupports::SLAAutoSupports(const sla::EigenMesh3D & emesh,
const Config& config, std::function<void(void)> throw_on_cancel, std::function<void(int)> statusfn) const std::vector<ExPolygons> &slices,
: m_config(config), m_emesh(emesh), m_throw_on_cancel(throw_on_cancel), m_statusfn(statusfn) const std::vector<float> & heights,
const Config & config,
std::function<void(void)> throw_on_cancel,
std::function<void(int)> statusfn)
: m_config(config)
, m_emesh(emesh)
, m_throw_on_cancel(throw_on_cancel)
, m_statusfn(statusfn)
{ {
process(slices, heights); process(slices, heights);
project_onto_mesh(m_output); project_onto_mesh(m_output);
@ -505,6 +513,21 @@ void SLAAutoSupports::uniformly_cover(const ExPolygons& islands, Structure& stru
} }
} }
void remove_bottom_points(std::vector<SupportPoint> &pts, double gnd_lvl, double tolerance)
{
// get iterator to the reorganized vector end
auto endit =
std::remove_if(pts.begin(), pts.end(),
[tolerance, gnd_lvl](const sla::SupportPoint &sp) {
double diff = std::abs(gnd_lvl -
double(sp.pos(Z)));
return diff <= tolerance;
});
// erase all elements after the new end
pts.erase(endit, pts.end());
}
#ifdef SLA_AUTOSUPPORTS_DEBUG #ifdef SLA_AUTOSUPPORTS_DEBUG
void SLAAutoSupports::output_structures(const std::vector<Structure>& structures) void SLAAutoSupports::output_structures(const std::vector<Structure>& structures)
{ {
@ -533,4 +556,5 @@ void SLAAutoSupports::output_expolygons(const ExPolygons& expolys, const std::st
} }
#endif #endif
} // namespace sla
} // namespace Slic3r } // namespace Slic3r

View file

@ -11,20 +11,22 @@
// #define SLA_AUTOSUPPORTS_DEBUG // #define SLA_AUTOSUPPORTS_DEBUG
namespace Slic3r { namespace Slic3r {
namespace sla {
class SLAAutoSupports { class SLAAutoSupports {
public: public:
struct Config { struct Config {
float density_relative; float density_relative {1.f};
float minimal_distance; float minimal_distance {1.f};
float head_diameter; float head_diameter {0.4f};
/////////////// ///////////////
inline float support_force() const { return 7.7f / density_relative; } // a force one point can support (arbitrary force unit) inline float support_force() const { return 7.7f / density_relative; } // a force one point can support (arbitrary force unit)
inline float tear_pressure() const { return 1.f; } // pressure that the display exerts (the force unit per mm2) inline float tear_pressure() const { return 1.f; } // pressure that the display exerts (the force unit per mm2)
}; };
SLAAutoSupports(const TriangleMesh& mesh, const sla::EigenMesh3D& emesh, const std::vector<ExPolygons>& slices, SLAAutoSupports(const sla::EigenMesh3D& emesh, const std::vector<ExPolygons>& slices,
const std::vector<float>& heights, const Config& config, std::function<void(void)> throw_on_cancel, std::function<void(int)> statusfn); const std::vector<float>& heights, const Config& config, std::function<void(void)> throw_on_cancel, std::function<void(int)> statusfn);
const std::vector<sla::SupportPoint>& output() { return m_output; } const std::vector<sla::SupportPoint>& output() { return m_output; }
struct MyLayer; struct MyLayer;
@ -199,7 +201,9 @@ private:
std::function<void(int)> m_statusfn; std::function<void(int)> m_statusfn;
}; };
void remove_bottom_points(std::vector<SupportPoint> &pts, double gnd_lvl, double tolerance);
} // namespace sla
} // namespace Slic3r } // namespace Slic3r

View file

@ -1,922 +0,0 @@
#include "SLABasePool.hpp"
#include "SLABoilerPlate.hpp"
#include "boost/log/trivial.hpp"
#include "SLABoostAdapter.hpp"
#include "ClipperUtils.hpp"
#include "Tesselate.hpp"
#include "MTUtils.hpp"
// For debugging:
// #include <fstream>
// #include <libnest2d/tools/benchmark.h>
// #include "SVG.hpp"
namespace Slic3r { namespace sla {
/// This function will return a triangulation of a sheet connecting an upper
/// and a lower plate given as input polygons. It will not triangulate the
/// plates themselves only the sheet. The caller has to specify the lower and
/// upper z levels in world coordinates as well as the offset difference
/// between the sheets. If the lower_z_mm is higher than upper_z_mm or the
/// offset difference is negative, the resulting triangle orientation will be
/// reversed.
///
/// IMPORTANT: This is not a universal triangulation algorithm. It assumes
/// that the lower and upper polygons are offsetted versions of the same
/// original polygon. In general, it assumes that one of the polygons is
/// completely inside the other. The offset difference is the reference
/// distance from the inner polygon's perimeter to the outer polygon's
/// perimeter. The real distance will be variable as the clipper offset has
/// different strategies (rounding, etc...). This algorithm should have
/// O(2n + 3m) complexity where n is the number of upper vertices and m is the
/// number of lower vertices.
Contour3D walls(const Polygon& lower, const Polygon& upper,
double lower_z_mm, double upper_z_mm,
double offset_difference_mm, ThrowOnCancel thr)
{
Contour3D ret;
if(upper.points.size() < 3 || lower.size() < 3) return ret;
// The concept of the algorithm is relatively simple. It will try to find
// the closest vertices from the upper and the lower polygon and use those
// as starting points. Then it will create the triangles sequentially using
// an edge from the upper polygon and a vertex from the lower or vice versa,
// depending on the resulting triangle's quality.
// The quality is measured by a scalar value. So far it looks like it is
// enough to derive it from the slope of the triangle's two edges connecting
// the upper and the lower part. A reference slope is calculated from the
// height and the offset difference.
// Offset in the index array for the ceiling
const auto offs = upper.points.size();
// Shorthand for the vertex arrays
auto& upoints = upper.points, &lpoints = lower.points;
auto& rpts = ret.points; auto& ind = ret.indices;
// If the Z levels are flipped, or the offset difference is negative, we
// will interpret that as the triangles normals should be inverted.
bool inverted = upper_z_mm < lower_z_mm || offset_difference_mm < 0;
// Copy the points into the mesh, convert them from 2D to 3D
rpts.reserve(upoints.size() + lpoints.size());
ind.reserve(2 * upoints.size() + 2 * lpoints.size());
for (auto &p : upoints)
rpts.emplace_back(unscaled(p.x()), unscaled(p.y()), upper_z_mm);
for (auto &p : lpoints)
rpts.emplace_back(unscaled(p.x()), unscaled(p.y()), lower_z_mm);
// Create pointing indices into vertex arrays. u-upper, l-lower
size_t uidx = 0, lidx = offs, unextidx = 1, lnextidx = offs + 1;
// Simple squared distance calculation.
auto distfn = [](const Vec3d& p1, const Vec3d& p2) {
auto p = p1 - p2; return p.transpose() * p;
};
// We need to find the closest point on lower polygon to the first point on
// the upper polygon. These will be our starting points.
double distmin = std::numeric_limits<double>::max();
for(size_t l = lidx; l < rpts.size(); ++l) {
thr();
double d = distfn(rpts[l], rpts[uidx]);
if(d < distmin) { lidx = l; distmin = d; }
}
// Set up lnextidx to be ahead of lidx in cyclic mode
lnextidx = lidx + 1;
if(lnextidx == rpts.size()) lnextidx = offs;
// This will be the flip switch to toggle between upper and lower triangle
// creation mode
enum class Proceed {
UPPER, // A segment from the upper polygon and one vertex from the lower
LOWER // A segment from the lower polygon and one vertex from the upper
} proceed = Proceed::UPPER;
// Flags to help evaluating loop termination.
bool ustarted = false, lstarted = false;
// The variables for the fitness values, one for the actual and one for the
// previous.
double current_fit = 0, prev_fit = 0;
// Every triangle of the wall has two edges connecting the upper plate with
// the lower plate. From the length of these two edges and the zdiff we
// can calculate the momentary squared offset distance at a particular
// position on the wall. The average of the differences from the reference
// (squared) offset distance will give us the driving fitness value.
const double offsdiff2 = std::pow(offset_difference_mm, 2);
const double zdiff2 = std::pow(upper_z_mm - lower_z_mm, 2);
// Mark the current vertex iterator positions. If the iterators return to
// the same position, the loop can be terminated.
size_t uendidx = uidx, lendidx = lidx;
do { thr(); // check throw if canceled
prev_fit = current_fit;
switch(proceed) { // proceed depending on the current state
case Proceed::UPPER:
if(!ustarted || uidx != uendidx) { // there are vertices remaining
// Get the 3D vertices in order
const Vec3d& p_up1 = rpts[uidx];
const Vec3d& p_low = rpts[lidx];
const Vec3d& p_up2 = rpts[unextidx];
// Calculate fitness: the average of the two connecting edges
double a = offsdiff2 - (distfn(p_up1, p_low) - zdiff2);
double b = offsdiff2 - (distfn(p_up2, p_low) - zdiff2);
current_fit = (std::abs(a) + std::abs(b)) / 2;
if(current_fit > prev_fit) { // fit is worse than previously
proceed = Proceed::LOWER;
} else { // good to go, create the triangle
inverted
? ind.emplace_back(int(unextidx), int(lidx), int(uidx))
: ind.emplace_back(int(uidx), int(lidx), int(unextidx));
// Increment the iterators, rotate if necessary
++uidx; ++unextidx;
if(unextidx == offs) unextidx = 0;
if(uidx == offs) uidx = 0;
ustarted = true; // mark the movement of the iterators
// so that the comparison to uendidx can be made correctly
}
} else proceed = Proceed::LOWER;
break;
case Proceed::LOWER:
// Mode with lower segment, upper vertex. Same structure:
if(!lstarted || lidx != lendidx) {
const Vec3d& p_low1 = rpts[lidx];
const Vec3d& p_low2 = rpts[lnextidx];
const Vec3d& p_up = rpts[uidx];
double a = offsdiff2 - (distfn(p_up, p_low1) - zdiff2);
double b = offsdiff2 - (distfn(p_up, p_low2) - zdiff2);
current_fit = (std::abs(a) + std::abs(b)) / 2;
if(current_fit > prev_fit) {
proceed = Proceed::UPPER;
} else {
inverted
? ind.emplace_back(int(uidx), int(lnextidx), int(lidx))
: ind.emplace_back(int(lidx), int(lnextidx), int(uidx));
++lidx; ++lnextidx;
if(lnextidx == rpts.size()) lnextidx = offs;
if(lidx == rpts.size()) lidx = offs;
lstarted = true;
}
} else proceed = Proceed::UPPER;
break;
} // end of switch
} while(!ustarted || !lstarted || uidx != uendidx || lidx != lendidx);
return ret;
}
/// Offsetting with clipper and smoothing the edges into a curvature.
void offset(ExPolygon& sh, coord_t distance, bool edgerounding = true) {
using ClipperLib::ClipperOffset;
using ClipperLib::jtRound;
using ClipperLib::jtMiter;
using ClipperLib::etClosedPolygon;
using ClipperLib::Paths;
using ClipperLib::Path;
auto&& ctour = Slic3rMultiPoint_to_ClipperPath(sh.contour);
auto&& holes = Slic3rMultiPoints_to_ClipperPaths(sh.holes);
// If the input is not at least a triangle, we can not do this algorithm
if(ctour.size() < 3 ||
std::any_of(holes.begin(), holes.end(),
[](const Path& p) { return p.size() < 3; })
) {
BOOST_LOG_TRIVIAL(error) << "Invalid geometry for offsetting!";
return;
}
auto jointype = edgerounding? jtRound : jtMiter;
ClipperOffset offs;
offs.ArcTolerance = scaled<double>(0.01);
Paths result;
offs.AddPath(ctour, jointype, etClosedPolygon);
offs.AddPaths(holes, jointype, etClosedPolygon);
offs.Execute(result, static_cast<double>(distance));
// Offsetting reverts the orientation and also removes the last vertex
// so boost will not have a closed polygon.
bool found_the_contour = false;
sh.holes.clear();
for(auto& r : result) {
if(ClipperLib::Orientation(r)) {
// We don't like if the offsetting generates more than one contour
// but throwing would be an overkill. Instead, we should warn the
// caller about the inability to create correct geometries
if(!found_the_contour) {
auto rr = ClipperPath_to_Slic3rPolygon(r);
sh.contour.points.swap(rr.points);
found_the_contour = true;
} else {
BOOST_LOG_TRIVIAL(warning)
<< "Warning: offsetting result is invalid!";
}
} else {
// TODO If there are multiple contours we can't be sure which hole
// belongs to the first contour. (But in this case the situation is
// bad enough to let it go...)
sh.holes.emplace_back(ClipperPath_to_Slic3rPolygon(r));
}
}
}
void offset(Polygon &sh, coord_t distance, bool edgerounding = true)
{
using ClipperLib::ClipperOffset;
using ClipperLib::jtRound;
using ClipperLib::jtMiter;
using ClipperLib::etClosedPolygon;
using ClipperLib::Paths;
using ClipperLib::Path;
auto &&ctour = Slic3rMultiPoint_to_ClipperPath(sh);
// If the input is not at least a triangle, we can not do this algorithm
if (ctour.size() < 3) {
BOOST_LOG_TRIVIAL(error) << "Invalid geometry for offsetting!";
return;
}
ClipperOffset offs;
offs.ArcTolerance = 0.01 * scaled(1.);
Paths result;
offs.AddPath(ctour, edgerounding ? jtRound : jtMiter, etClosedPolygon);
offs.Execute(result, static_cast<double>(distance));
// Offsetting reverts the orientation and also removes the last vertex
// so boost will not have a closed polygon.
bool found_the_contour = false;
for (auto &r : result) {
if (ClipperLib::Orientation(r)) {
// We don't like if the offsetting generates more than one contour
// but throwing would be an overkill. Instead, we should warn the
// caller about the inability to create correct geometries
if (!found_the_contour) {
auto rr = ClipperPath_to_Slic3rPolygon(r);
sh.points.swap(rr.points);
found_the_contour = true;
} else {
BOOST_LOG_TRIVIAL(warning)
<< "Warning: offsetting result is invalid!";
}
}
}
}
/// Unification of polygons (with clipper) preserving holes as well.
ExPolygons unify(const ExPolygons& shapes) {
using ClipperLib::ptSubject;
ExPolygons retv;
bool closed = true;
bool valid = true;
ClipperLib::Clipper clipper;
for(auto& path : shapes) {
auto clipperpath = Slic3rMultiPoint_to_ClipperPath(path.contour);
if(!clipperpath.empty())
valid &= clipper.AddPath(clipperpath, ptSubject, closed);
auto clipperholes = Slic3rMultiPoints_to_ClipperPaths(path.holes);
for(auto& hole : clipperholes) {
if(!hole.empty())
valid &= clipper.AddPath(hole, ptSubject, closed);
}
}
if(!valid) BOOST_LOG_TRIVIAL(warning) << "Unification of invalid shapes!";
ClipperLib::PolyTree result;
clipper.Execute(ClipperLib::ctUnion, result, ClipperLib::pftNonZero);
retv.reserve(static_cast<size_t>(result.Total()));
// Now we will recursively traverse the polygon tree and serialize it
// into an ExPolygon with holes. The polygon tree has the clipper-ish
// PolyTree structure which alternates its nodes as contours and holes
// A "declaration" of function for traversing leafs which are holes
std::function<void(ClipperLib::PolyNode*, ExPolygon&)> processHole;
// Process polygon which calls processHoles which than calls processPoly
// again until no leafs are left.
auto processPoly = [&retv, &processHole](ClipperLib::PolyNode *pptr) {
ExPolygon poly;
poly.contour.points = ClipperPath_to_Slic3rPolygon(pptr->Contour);
for(auto h : pptr->Childs) { processHole(h, poly); }
retv.push_back(poly);
};
// Body of the processHole function
processHole = [&processPoly](ClipperLib::PolyNode *pptr, ExPolygon& poly)
{
poly.holes.emplace_back();
poly.holes.back().points = ClipperPath_to_Slic3rPolygon(pptr->Contour);
for(auto c : pptr->Childs) processPoly(c);
};
// Wrapper for traversing.
auto traverse = [&processPoly] (ClipperLib::PolyNode *node)
{
for(auto ch : node->Childs) {
processPoly(ch);
}
};
// Here is the actual traverse
traverse(&result);
return retv;
}
Polygons unify(const Polygons& shapes) {
using ClipperLib::ptSubject;
bool closed = true;
bool valid = true;
ClipperLib::Clipper clipper;
for(auto& path : shapes) {
auto clipperpath = Slic3rMultiPoint_to_ClipperPath(path);
if(!clipperpath.empty())
valid &= clipper.AddPath(clipperpath, ptSubject, closed);
}
if(!valid) BOOST_LOG_TRIVIAL(warning) << "Unification of invalid shapes!";
ClipperLib::Paths result;
clipper.Execute(ClipperLib::ctUnion, result, ClipperLib::pftNonZero);
Polygons ret;
for (ClipperLib::Path &p : result) {
Polygon pp = ClipperPath_to_Slic3rPolygon(p);
if (!pp.is_clockwise()) ret.emplace_back(std::move(pp));
}
return ret;
}
// Function to cut tiny connector cavities for a given polygon. The input poly
// will be offsetted by "padding" and small rectangle shaped cavities will be
// inserted along the perimeter in every "stride" distance. The stick rectangles
// will have a with about "stick_width". The input dimensions are in world
// measure, not the scaled clipper units.
void breakstick_holes(ExPolygon& poly,
double padding,
double stride,
double stick_width,
double penetration)
{
// SVG svg("bridgestick_plate.svg");
// svg.draw(poly);
auto transf = [stick_width, penetration, padding, stride](Points &pts) {
// The connector stick will be a small rectangle with dimensions
// stick_width x (penetration + padding) to have some penetration
// into the input polygon.
Points out;
out.reserve(2 * pts.size()); // output polygon points
// stick bottom and right edge dimensions
double sbottom = scaled(stick_width);
double sright = scaled(penetration + padding);
// scaled stride distance
double sstride = scaled(stride);
double t = 0;
// process pairs of vertices as an edge, start with the last and
// first point
for (size_t i = pts.size() - 1, j = 0; j < pts.size(); i = j, ++j) {
// Get vertices and the direction vectors
const Point &a = pts[i], &b = pts[j];
Vec2d dir = b.cast<double>() - a.cast<double>();
double nrm = dir.norm();
dir /= nrm;
Vec2d dirp(-dir(Y), dir(X));
// Insert start point
out.emplace_back(a);
// dodge the start point, do not make sticks on the joins
while (t < sbottom) t += sbottom;
double tend = nrm - sbottom;
while (t < tend) { // insert the stick on the polygon perimeter
// calculate the stick rectangle vertices and insert them
// into the output.
Point p1 = a + (t * dir).cast<coord_t>();
Point p2 = p1 + (sright * dirp).cast<coord_t>();
Point p3 = p2 + (sbottom * dir).cast<coord_t>();
Point p4 = p3 + (sright * -dirp).cast<coord_t>();
out.insert(out.end(), {p1, p2, p3, p4});
// continue along the perimeter
t += sstride;
}
t = t - nrm;
// Insert edge endpoint
out.emplace_back(b);
}
// move the new points
out.shrink_to_fit();
pts.swap(out);
};
if(stride > 0.0 && stick_width > 0.0 && padding > 0.0) {
transf(poly.contour.points);
for (auto &h : poly.holes) transf(h.points);
}
// svg.draw(poly);
// svg.Close();
}
/// This method will create a rounded edge around a flat polygon in 3d space.
/// 'base_plate' parameter is the target plate.
/// 'radius' is the radius of the edges.
/// 'degrees' is tells how much of a circle should be created as the rounding.
/// It should be in degrees, not radians.
/// 'ceilheight_mm' is the Z coordinate of the flat polygon in 3D space.
/// 'dir' Is the direction of the round edges: inward or outward
/// 'thr' Throws if a cancel signal was received
/// 'last_offset' An auxiliary output variable to save the last offsetted
/// version of 'base_plate'
/// 'last_height' An auxiliary output to save the last z coordinate of the
/// offsetted base_plate. In other words, where the rounded edges end.
Contour3D round_edges(const ExPolygon& base_plate,
double radius_mm,
double degrees,
double ceilheight_mm,
bool dir,
ThrowOnCancel thr,
ExPolygon& last_offset, double& last_height)
{
auto ob = base_plate;
auto ob_prev = ob;
double wh = ceilheight_mm, wh_prev = wh;
Contour3D curvedwalls;
int steps = 30;
double stepx = radius_mm / steps;
coord_t s = dir? 1 : -1;
degrees = std::fmod(degrees, 180);
// we use sin for x distance because we interpret the angle starting from
// PI/2
int tos = degrees < 90?
int(radius_mm*std::cos(degrees * PI / 180 - PI/2) / stepx) : steps;
for(int i = 1; i <= tos; ++i) {
thr();
ob = base_plate;
double r2 = radius_mm * radius_mm;
double xx = i*stepx;
double x2 = xx*xx;
double stepy = std::sqrt(r2 - x2);
offset(ob, s * scaled(xx));
wh = ceilheight_mm - radius_mm + stepy;
Contour3D pwalls;
double prev_x = xx - (i - 1) * stepx;
pwalls = walls(ob.contour, ob_prev.contour, wh, wh_prev, s*prev_x, thr);
curvedwalls.merge(pwalls);
ob_prev = ob;
wh_prev = wh;
}
if(degrees > 90) {
double tox = radius_mm - radius_mm*std::cos(degrees * PI / 180 - PI/2);
int tos = int(tox / stepx);
for(int i = 1; i <= tos; ++i) {
thr();
ob = base_plate;
double r2 = radius_mm * radius_mm;
double xx = radius_mm - i*stepx;
double x2 = xx*xx;
double stepy = std::sqrt(r2 - x2);
offset(ob, s * scaled(xx));
wh = ceilheight_mm - radius_mm - stepy;
Contour3D pwalls;
double prev_x = xx - radius_mm + (i - 1)*stepx;
pwalls =
walls(ob_prev.contour, ob.contour, wh_prev, wh, s*prev_x, thr);
curvedwalls.merge(pwalls);
ob_prev = ob;
wh_prev = wh;
}
}
last_offset = std::move(ob);
last_height = wh;
return curvedwalls;
}
inline Point centroid(Points& pp) {
Point c;
switch(pp.size()) {
case 0: break;
case 1: c = pp.front(); break;
case 2: c = (pp[0] + pp[1]) / 2; break;
default: {
auto MAX = std::numeric_limits<Point::coord_type>::max();
auto MIN = std::numeric_limits<Point::coord_type>::min();
Point min = {MAX, MAX}, max = {MIN, MIN};
for(auto& p : pp) {
if(p(0) < min(0)) min(0) = p(0);
if(p(1) < min(1)) min(1) = p(1);
if(p(0) > max(0)) max(0) = p(0);
if(p(1) > max(1)) max(1) = p(1);
}
c(0) = min(0) + (max(0) - min(0)) / 2;
c(1) = min(1) + (max(1) - min(1)) / 2;
// TODO: fails for non convex cluster
// c = std::accumulate(pp.begin(), pp.end(), Point{0, 0});
// x(c) /= coord_t(pp.size()); y(c) /= coord_t(pp.size());
break;
}
}
return c;
}
inline Point centroid(const Polygon& poly) {
return poly.centroid();
}
/// A fake concave hull that is constructed by connecting separate shapes
/// with explicit bridges. Bridges are generated from each shape's centroid
/// to the center of the "scene" which is the centroid calculated from the shape
/// centroids (a star is created...)
Polygons concave_hull(const Polygons& polys, double maxd_mm, ThrowOnCancel thr)
{
namespace bgi = boost::geometry::index;
using SpatElement = std::pair<Point, unsigned>;
using SpatIndex = bgi::rtree< SpatElement, bgi::rstar<16, 4> >;
if(polys.empty()) return Polygons();
const double max_dist = scaled(maxd_mm);
Polygons punion = unify(polys); // could be redundant
if(punion.size() == 1) return punion;
// We get the centroids of all the islands in the 2D slice
Points centroids; centroids.reserve(punion.size());
std::transform(punion.begin(), punion.end(), std::back_inserter(centroids),
[](const Polygon& poly) { return centroid(poly); });
SpatIndex ctrindex;
unsigned idx = 0;
for(const Point &ct : centroids) ctrindex.insert(std::make_pair(ct, idx++));
// Centroid of the centroids of islands. This is where the additional
// connector sticks are routed.
Point cc = centroid(centroids);
punion.reserve(punion.size() + centroids.size());
idx = 0;
std::transform(centroids.begin(), centroids.end(),
std::back_inserter(punion),
[&centroids, &ctrindex, cc, max_dist, &idx, thr]
(const Point& c)
{
thr();
double dx = x(c) - x(cc), dy = y(c) - y(cc);
double l = std::sqrt(dx * dx + dy * dy);
double nx = dx / l, ny = dy / l;
Point& ct = centroids[idx];
std::vector<SpatElement> result;
ctrindex.query(bgi::nearest(ct, 2), std::back_inserter(result));
double dist = max_dist;
for (const SpatElement &el : result)
if (el.second != idx) {
dist = Line(el.first, ct).length();
break;
}
idx++;
if (dist >= max_dist) return Polygon();
Polygon r;
auto& ctour = r.points;
ctour.reserve(3);
ctour.emplace_back(cc);
Point d(scaled(nx), scaled(ny));
ctour.emplace_back(c + Point( -y(d), x(d) ));
ctour.emplace_back(c + Point( y(d), -x(d) ));
offset(r, scaled(1.));
return r;
});
// This is unavoidable...
punion = unify(punion);
return punion;
}
void base_plate(const TriangleMesh & mesh,
ExPolygons & output,
const std::vector<float> &heights,
ThrowOnCancel thrfn)
{
if (mesh.empty()) return;
// m.require_shared_vertices(); // TriangleMeshSlicer needs this
TriangleMeshSlicer slicer(&mesh);
std::vector<ExPolygons> out; out.reserve(heights.size());
slicer.slice(heights, 0.f, &out, thrfn);
size_t count = 0; for(auto& o : out) count += o.size();
// Now we have to unify all slice layers which can be an expensive operation
// so we will try to simplify the polygons
ExPolygons tmp; tmp.reserve(count);
for(ExPolygons& o : out)
for(ExPolygon& e : o) {
auto&& exss = e.simplify(scaled<double>(0.1));
for(ExPolygon& ep : exss) tmp.emplace_back(std::move(ep));
}
ExPolygons utmp = unify(tmp);
for(auto& o : utmp) {
auto&& smp = o.simplify(scaled<double>(0.1));
output.insert(output.end(), smp.begin(), smp.end());
}
}
void base_plate(const TriangleMesh &mesh,
ExPolygons & output,
float h,
float layerh,
ThrowOnCancel thrfn)
{
auto bb = mesh.bounding_box();
float gnd = float(bb.min(Z));
std::vector<float> heights = {float(bb.min(Z))};
for(float hi = gnd + layerh; hi <= gnd + h; hi += layerh)
heights.emplace_back(hi);
base_plate(mesh, output, heights, thrfn);
}
Contour3D create_base_pool(const Polygons &ground_layer,
const ExPolygons &obj_self_pad = {},
const PoolConfig& cfg = PoolConfig())
{
// for debugging:
// Benchmark bench;
// bench.start();
double mergedist = 2*(1.8*cfg.min_wall_thickness_mm + 4*cfg.edge_radius_mm)+
cfg.max_merge_distance_mm;
// Here we get the base polygon from which the pad has to be generated.
// We create an artificial concave hull from this polygon and that will
// serve as the bottom plate of the pad. We will offset this concave hull
// and then offset back the result with clipper with rounding edges ON. This
// trick will create a nice rounded pad shape.
Polygons concavehs = concave_hull(ground_layer, mergedist, cfg.throw_on_cancel);
const double thickness = cfg.min_wall_thickness_mm;
const double wingheight = cfg.min_wall_height_mm;
const double fullheight = wingheight + thickness;
const double slope = cfg.wall_slope;
const double wingdist = wingheight / std::tan(slope);
const double bottom_offs = (thickness + wingheight) / std::tan(slope);
// scaled values
const coord_t s_thickness = scaled(thickness);
const coord_t s_eradius = scaled(cfg.edge_radius_mm);
const coord_t s_safety_dist = 2*s_eradius + coord_t(0.8*s_thickness);
const coord_t s_wingdist = scaled(wingdist);
const coord_t s_bottom_offs = scaled(bottom_offs);
auto& thrcl = cfg.throw_on_cancel;
Contour3D pool;
for(Polygon& concaveh : concavehs) {
if(concaveh.points.empty()) return pool;
// Here lies the trick that does the smoothing only with clipper offset
// calls. The offset is configured to round edges. Inner edges will
// be rounded because we offset twice: ones to get the outer (top) plate
// and again to get the inner (bottom) plate
auto outer_base = concaveh;
offset(outer_base, s_safety_dist + s_wingdist + s_thickness);
ExPolygon bottom_poly; bottom_poly.contour = outer_base;
offset(bottom_poly, -s_bottom_offs);
// Punching a hole in the top plate for the cavity
ExPolygon top_poly;
ExPolygon middle_base;
ExPolygon inner_base;
top_poly.contour = outer_base;
if(wingheight > 0) {
inner_base.contour = outer_base;
offset(inner_base, -(s_thickness + s_wingdist + s_eradius));
middle_base.contour = outer_base;
offset(middle_base, -s_thickness);
top_poly.holes.emplace_back(middle_base.contour);
auto& tph = top_poly.holes.back().points;
std::reverse(tph.begin(), tph.end());
}
ExPolygon ob; ob.contour = outer_base; double wh = 0;
// now we will calculate the angle or portion of the circle from
// pi/2 that will connect perfectly with the bottom plate.
// this is a tangent point calculation problem and the equation can
// be found for example here:
// http://www.ambrsoft.com/TrigoCalc/Circles2/CirclePoint/CirclePointDistance.htm
// the y coordinate would be:
// y = cy + (r^2*py - r*px*sqrt(px^2 + py^2 - r^2) / (px^2 + py^2)
// where px and py are the coordinates of the point outside the circle
// cx and cy are the circle center, r is the radius
// We place the circle center to (0, 0) in the calculation the make
// things easier.
// to get the angle we use arcsin function and subtract 90 degrees then
// flip the sign to get the right input to the round_edge function.
double r = cfg.edge_radius_mm;
double cy = 0;
double cx = 0;
double px = thickness + wingdist;
double py = r - fullheight;
double pxcx = px - cx;
double pycy = py - cy;
double b_2 = pxcx*pxcx + pycy*pycy;
double r_2 = r*r;
double D = std::sqrt(b_2 - r_2);
double vy = (r_2*pycy - r*pxcx*D) / b_2;
double phi = -(std::asin(vy/r) * 180 / PI - 90);
// Generate the smoothed edge geometry
if(s_eradius > 0) pool.merge(round_edges(ob,
r,
phi,
0, // z position of the input plane
true,
thrcl,
ob, wh));
// Now that we have the rounded edge connecting the top plate with
// the outer side walls, we can generate and merge the sidewall geometry
pool.merge(walls(ob.contour, bottom_poly.contour, wh, -fullheight,
bottom_offs, thrcl));
if(wingheight > 0) {
// Generate the smoothed edge geometry
wh = 0;
ob = middle_base;
if(s_eradius) pool.merge(round_edges(middle_base,
r,
phi - 90, // from tangent lines
0, // z position of the input plane
false,
thrcl,
ob, wh));
// Next is the cavity walls connecting to the top plate's
// artificially created hole.
pool.merge(walls(inner_base.contour, ob.contour, -wingheight,
wh, -wingdist, thrcl));
}
if (cfg.embed_object) {
ExPolygons bttms = diff_ex(to_polygons(bottom_poly),
to_polygons(obj_self_pad));
assert(!bttms.empty());
std::sort(bttms.begin(), bttms.end(),
[](const ExPolygon& e1, const ExPolygon& e2) {
return e1.contour.area() > e2.contour.area();
});
if(wingheight > 0) inner_base.holes = bttms.front().holes;
else top_poly.holes = bttms.front().holes;
auto straight_walls =
[&pool](const Polygon &cntr, coord_t z_low, coord_t z_high) {
auto lines = cntr.lines();
for (auto &l : lines) {
auto s = coord_t(pool.points.size());
auto& pts = pool.points;
pts.emplace_back(unscale(l.a.x(), l.a.y(), z_low));
pts.emplace_back(unscale(l.b.x(), l.b.y(), z_low));
pts.emplace_back(unscale(l.a.x(), l.a.y(), z_high));
pts.emplace_back(unscale(l.b.x(), l.b.y(), z_high));
pool.indices.emplace_back(s, s + 1, s + 3);
pool.indices.emplace_back(s, s + 3, s + 2);
}
};
coord_t z_lo = -scaled(fullheight), z_hi = -scaled(wingheight);
for (ExPolygon &ep : bttms) {
pool.merge(triangulate_expolygon_3d(ep, -fullheight, true));
for (auto &h : ep.holes) straight_walls(h, z_lo, z_hi);
}
// Skip the outer contour, triangulate the holes
for (auto it = std::next(bttms.begin()); it != bttms.end(); ++it) {
pool.merge(triangulate_expolygon_3d(*it, -wingheight));
straight_walls(it->contour, z_lo, z_hi);
}
} else {
// Now we need to triangulate the top and bottom plates as well as
// the cavity bottom plate which is the same as the bottom plate
// but it is elevated by the thickness.
pool.merge(triangulate_expolygon_3d(bottom_poly, -fullheight, true));
}
pool.merge(triangulate_expolygon_3d(top_poly));
if(wingheight > 0)
pool.merge(triangulate_expolygon_3d(inner_base, -wingheight));
}
return pool;
}
void create_base_pool(const Polygons &ground_layer, TriangleMesh& out,
const ExPolygons &holes, const PoolConfig& cfg)
{
// For debugging:
// bench.stop();
// std::cout << "Pad creation time: " << bench.getElapsedSec() << std::endl;
// std::fstream fout("pad_debug.obj", std::fstream::out);
// if(fout.good()) pool.to_obj(fout);
out.merge(mesh(create_base_pool(ground_layer, holes, cfg)));
}
}
}

View file

@ -1,92 +0,0 @@
#ifndef SLABASEPOOL_HPP
#define SLABASEPOOL_HPP
#include <vector>
#include <functional>
#include <cmath>
namespace Slic3r {
class ExPolygon;
class Polygon;
using ExPolygons = std::vector<ExPolygon>;
using Polygons = std::vector<Polygon>;
class TriangleMesh;
namespace sla {
using ThrowOnCancel = std::function<void(void)>;
/// Calculate the polygon representing the silhouette from the specified height
void base_plate(const TriangleMesh& mesh, // input mesh
ExPolygons& output, // Output will be merged with
float samplingheight = 0.1f, // The height range to sample
float layerheight = 0.05f, // The sampling height
ThrowOnCancel thrfn = [](){}); // Will be called frequently
void base_plate(const TriangleMesh& mesh, // input mesh
ExPolygons& output, // Output will be merged with
const std::vector<float>&, // Exact Z levels to sample
ThrowOnCancel thrfn = [](){}); // Will be called frequently
// Function to cut tiny connector cavities for a given polygon. The input poly
// will be offsetted by "padding" and small rectangle shaped cavities will be
// inserted along the perimeter in every "stride" distance. The stick rectangles
// will have a with about "stick_width". The input dimensions are in world
// measure, not the scaled clipper units.
void breakstick_holes(ExPolygon &poly,
double padding,
double stride,
double stick_width,
double penetration = 0.0);
Polygons concave_hull(const Polygons& polys, double max_dist_mm = 50,
ThrowOnCancel throw_on_cancel = [](){});
struct PoolConfig {
double min_wall_thickness_mm = 2;
double min_wall_height_mm = 5;
double max_merge_distance_mm = 50;
double edge_radius_mm = 1;
double wall_slope = std::atan(1.0); // Universal constant for Pi/4
struct EmbedObject {
double object_gap_mm = 0.5;
double stick_stride_mm = 10;
double stick_width_mm = 0.3;
double stick_penetration_mm = 0.1;
bool enabled = false;
operator bool() const { return enabled; }
} embed_object;
ThrowOnCancel throw_on_cancel = [](){};
inline PoolConfig() {}
inline PoolConfig(double wt, double wh, double md, double er, double slope):
min_wall_thickness_mm(wt),
min_wall_height_mm(wh),
max_merge_distance_mm(md),
edge_radius_mm(er),
wall_slope(slope) {}
};
/// Calculate the pool for the mesh for SLA printing
void create_base_pool(const Polygons& base_plate,
TriangleMesh& output_mesh,
const ExPolygons& holes,
const PoolConfig& = PoolConfig());
/// Returns the elevation needed for compensating the pad.
inline double get_pad_elevation(const PoolConfig& cfg) {
return cfg.min_wall_thickness_mm;
}
inline double get_pad_fullheight(const PoolConfig& cfg) {
return cfg.min_wall_height_mm + cfg.min_wall_thickness_mm;
}
}
}
#endif // SLABASEPOOL_HPP

View file

@ -8,35 +8,19 @@
#include <libslic3r/ExPolygon.hpp> #include <libslic3r/ExPolygon.hpp>
#include <libslic3r/TriangleMesh.hpp> #include <libslic3r/TriangleMesh.hpp>
#include "SLACommon.hpp"
#include "SLASpatIndex.hpp"
namespace Slic3r { namespace Slic3r {
namespace sla { namespace sla {
/// Get x and y coordinates (because we are eigenizing...)
inline coord_t x(const Point& p) { return p(0); }
inline coord_t y(const Point& p) { return p(1); }
inline coord_t& x(Point& p) { return p(0); }
inline coord_t& y(Point& p) { return p(1); }
inline coordf_t x(const Vec3d& p) { return p(0); }
inline coordf_t y(const Vec3d& p) { return p(1); }
inline coordf_t z(const Vec3d& p) { return p(2); }
inline coordf_t& x(Vec3d& p) { return p(0); }
inline coordf_t& y(Vec3d& p) { return p(1); }
inline coordf_t& z(Vec3d& p) { return p(2); }
inline coord_t& x(Vec3crd& p) { return p(0); }
inline coord_t& y(Vec3crd& p) { return p(1); }
inline coord_t& z(Vec3crd& p) { return p(2); }
inline coord_t x(const Vec3crd& p) { return p(0); }
inline coord_t y(const Vec3crd& p) { return p(1); }
inline coord_t z(const Vec3crd& p) { return p(2); }
/// Intermediate struct for a 3D mesh /// Intermediate struct for a 3D mesh
struct Contour3D { struct Contour3D {
Pointf3s points; Pointf3s points;
std::vector<Vec3i> indices; std::vector<Vec3i> indices;
void merge(const Contour3D& ctr) { Contour3D& merge(const Contour3D& ctr)
{
auto s3 = coord_t(points.size()); auto s3 = coord_t(points.size());
auto s = indices.size(); auto s = indices.size();
@ -44,21 +28,27 @@ struct Contour3D {
indices.insert(indices.end(), ctr.indices.begin(), ctr.indices.end()); indices.insert(indices.end(), ctr.indices.begin(), ctr.indices.end());
for(size_t n = s; n < indices.size(); n++) { for(size_t n = s; n < indices.size(); n++) {
auto& idx = indices[n]; x(idx) += s3; y(idx) += s3; z(idx) += s3; auto& idx = indices[n]; idx.x() += s3; idx.y() += s3; idx.z() += s3;
} }
return *this;
} }
void merge(const Pointf3s& triangles) { Contour3D& merge(const Pointf3s& triangles)
{
const size_t offs = points.size(); const size_t offs = points.size();
points.insert(points.end(), triangles.begin(), triangles.end()); points.insert(points.end(), triangles.begin(), triangles.end());
indices.reserve(indices.size() + points.size() / 3); indices.reserve(indices.size() + points.size() / 3);
for(int i = (int)offs; i < (int)points.size(); i += 3) for(int i = int(offs); i < int(points.size()); i += 3)
indices.emplace_back(i, i + 1, i + 2); indices.emplace_back(i, i + 1, i + 2);
return *this;
} }
// Write the index triangle structure to OBJ file for debugging purposes. // Write the index triangle structure to OBJ file for debugging purposes.
void to_obj(std::ostream& stream) { void to_obj(std::ostream& stream)
{
for(auto& p : points) { for(auto& p : points) {
stream << "v " << p.transpose() << "\n"; stream << "v " << p.transpose() << "\n";
} }
@ -72,6 +62,31 @@ struct Contour3D {
using ClusterEl = std::vector<unsigned>; using ClusterEl = std::vector<unsigned>;
using ClusteredPoints = std::vector<ClusterEl>; using ClusteredPoints = std::vector<ClusterEl>;
// Clustering a set of points by the given distance.
ClusteredPoints cluster(const std::vector<unsigned>& indices,
std::function<Vec3d(unsigned)> pointfn,
double dist,
unsigned max_points);
ClusteredPoints cluster(const PointSet& points,
double dist,
unsigned max_points);
ClusteredPoints cluster(
const std::vector<unsigned>& indices,
std::function<Vec3d(unsigned)> pointfn,
std::function<bool(const PointIndexEl&, const PointIndexEl&)> predicate,
unsigned max_points);
// Calculate the normals for the selected points (from 'points' set) on the
// mesh. This will call squared distance for each point.
PointSet normals(const PointSet& points,
const EigenMesh3D& mesh,
double eps = 0.05, // min distance from edges
std::function<void()> throw_on_cancel = [](){},
const std::vector<unsigned>& selected_points = {});
/// Mesh from an existing contour. /// Mesh from an existing contour.
inline TriangleMesh mesh(const Contour3D& ctour) { inline TriangleMesh mesh(const Contour3D& ctour) {
return {ctour.points, ctour.indices}; return {ctour.points, ctour.indices};

View file

@ -1,8 +1,9 @@
#ifndef SLACOMMON_HPP #ifndef SLACOMMON_HPP
#define SLACOMMON_HPP #define SLACOMMON_HPP
#include <Eigen/Geometry>
#include <memory> #include <memory>
#include <vector>
#include <Eigen/Geometry>
// #define SLIC3R_SLA_NEEDS_WINDTREE // #define SLIC3R_SLA_NEEDS_WINDTREE
@ -69,6 +70,8 @@ struct SupportPoint
} }
}; };
using SupportPoints = std::vector<SupportPoint>;
/// An index-triangle structure for libIGL functions. Also serves as an /// An index-triangle structure for libIGL functions. Also serves as an
/// alternative (raw) input format for the SLASupportTree /// alternative (raw) input format for the SLASupportTree
class EigenMesh3D { class EigenMesh3D {
@ -175,6 +178,8 @@ public:
} }
}; };
using PointSet = Eigen::MatrixXd;
} // namespace sla } // namespace sla
} // namespace Slic3r } // namespace Slic3r

View file

@ -0,0 +1,56 @@
#ifndef SLACONCURRENCY_H
#define SLACONCURRENCY_H
#include <tbb/spin_mutex.h>
#include <tbb/mutex.h>
#include <tbb/parallel_for.h>
namespace Slic3r {
namespace sla {
// Set this to true to enable full parallelism in this module.
// Only the well tested parts will be concurrent if this is set to false.
const constexpr bool USE_FULL_CONCURRENCY = true;
template<bool> struct _ccr {};
template<> struct _ccr<true>
{
using SpinningMutex = tbb::spin_mutex;
using BlockingMutex = tbb::mutex;
template<class It, class Fn>
static inline void enumerate(It from, It to, Fn fn)
{
auto iN = to - from;
size_t N = iN < 0 ? 0 : size_t(iN);
tbb::parallel_for(size_t(0), N, [from, fn](size_t n) {
fn(*(from + decltype(iN)(n)), n);
});
}
};
template<> struct _ccr<false>
{
private:
struct _Mtx { inline void lock() {} inline void unlock() {} };
public:
using SpinningMutex = _Mtx;
using BlockingMutex = _Mtx;
template<class It, class Fn>
static inline void enumerate(It from, It to, Fn fn)
{
for (auto it = from; it != to; ++it) fn(*it, size_t(it - from));
}
};
using ccr = _ccr<USE_FULL_CONCURRENCY>;
using ccr_seq = _ccr<false>;
using ccr_par = _ccr<true>;
}} // namespace Slic3r::sla
#endif // SLACONCURRENCY_H

View file

@ -0,0 +1,870 @@
#include "SLAPad.hpp"
#include "SLABoilerPlate.hpp"
#include "SLASpatIndex.hpp"
#include "boost/log/trivial.hpp"
#include "SLABoostAdapter.hpp"
#include "ClipperUtils.hpp"
#include "Tesselate.hpp"
#include "MTUtils.hpp"
// For debugging:
// #include <fstream>
// #include <libnest2d/tools/benchmark.h>
#include "SVG.hpp"
#include "I18N.hpp"
#include <boost/log/trivial.hpp>
//! macro used to mark string used at localization,
//! return same string
#define L(s) Slic3r::I18N::translate(s)
namespace Slic3r { namespace sla {
namespace {
/// This function will return a triangulation of a sheet connecting an upper
/// and a lower plate given as input polygons. It will not triangulate the
/// plates themselves only the sheet. The caller has to specify the lower and
/// upper z levels in world coordinates as well as the offset difference
/// between the sheets. If the lower_z_mm is higher than upper_z_mm or the
/// offset difference is negative, the resulting triangle orientation will be
/// reversed.
///
/// IMPORTANT: This is not a universal triangulation algorithm. It assumes
/// that the lower and upper polygons are offsetted versions of the same
/// original polygon. In general, it assumes that one of the polygons is
/// completely inside the other. The offset difference is the reference
/// distance from the inner polygon's perimeter to the outer polygon's
/// perimeter. The real distance will be variable as the clipper offset has
/// different strategies (rounding, etc...). This algorithm should have
/// O(2n + 3m) complexity where n is the number of upper vertices and m is the
/// number of lower vertices.
Contour3D walls(
const Polygon &lower,
const Polygon &upper,
double lower_z_mm,
double upper_z_mm,
double offset_difference_mm,
ThrowOnCancel thr = [] {})
{
Contour3D ret;
if(upper.points.size() < 3 || lower.size() < 3) return ret;
// The concept of the algorithm is relatively simple. It will try to find
// the closest vertices from the upper and the lower polygon and use those
// as starting points. Then it will create the triangles sequentially using
// an edge from the upper polygon and a vertex from the lower or vice versa,
// depending on the resulting triangle's quality.
// The quality is measured by a scalar value. So far it looks like it is
// enough to derive it from the slope of the triangle's two edges connecting
// the upper and the lower part. A reference slope is calculated from the
// height and the offset difference.
// Offset in the index array for the ceiling
const auto offs = upper.points.size();
// Shorthand for the vertex arrays
auto& upts = upper.points, &lpts = lower.points;
auto& rpts = ret.points; auto& ind = ret.indices;
// If the Z levels are flipped, or the offset difference is negative, we
// will interpret that as the triangles normals should be inverted.
bool inverted = upper_z_mm < lower_z_mm || offset_difference_mm < 0;
// Copy the points into the mesh, convert them from 2D to 3D
rpts.reserve(upts.size() + lpts.size());
ind.reserve(2 * upts.size() + 2 * lpts.size());
for (auto &p : upts)
rpts.emplace_back(unscaled(p.x()), unscaled(p.y()), upper_z_mm);
for (auto &p : lpts)
rpts.emplace_back(unscaled(p.x()), unscaled(p.y()), lower_z_mm);
// Create pointing indices into vertex arrays. u-upper, l-lower
size_t uidx = 0, lidx = offs, unextidx = 1, lnextidx = offs + 1;
// Simple squared distance calculation.
auto distfn = [](const Vec3d& p1, const Vec3d& p2) {
auto p = p1 - p2; return p.transpose() * p;
};
// We need to find the closest point on lower polygon to the first point on
// the upper polygon. These will be our starting points.
double distmin = std::numeric_limits<double>::max();
for(size_t l = lidx; l < rpts.size(); ++l) {
thr();
double d = distfn(rpts[l], rpts[uidx]);
if(d < distmin) { lidx = l; distmin = d; }
}
// Set up lnextidx to be ahead of lidx in cyclic mode
lnextidx = lidx + 1;
if(lnextidx == rpts.size()) lnextidx = offs;
// This will be the flip switch to toggle between upper and lower triangle
// creation mode
enum class Proceed {
UPPER, // A segment from the upper polygon and one vertex from the lower
LOWER // A segment from the lower polygon and one vertex from the upper
} proceed = Proceed::UPPER;
// Flags to help evaluating loop termination.
bool ustarted = false, lstarted = false;
// The variables for the fitness values, one for the actual and one for the
// previous.
double current_fit = 0, prev_fit = 0;
// Every triangle of the wall has two edges connecting the upper plate with
// the lower plate. From the length of these two edges and the zdiff we
// can calculate the momentary squared offset distance at a particular
// position on the wall. The average of the differences from the reference
// (squared) offset distance will give us the driving fitness value.
const double offsdiff2 = std::pow(offset_difference_mm, 2);
const double zdiff2 = std::pow(upper_z_mm - lower_z_mm, 2);
// Mark the current vertex iterator positions. If the iterators return to
// the same position, the loop can be terminated.
size_t uendidx = uidx, lendidx = lidx;
do { thr(); // check throw if canceled
prev_fit = current_fit;
switch(proceed) { // proceed depending on the current state
case Proceed::UPPER:
if(!ustarted || uidx != uendidx) { // there are vertices remaining
// Get the 3D vertices in order
const Vec3d& p_up1 = rpts[uidx];
const Vec3d& p_low = rpts[lidx];
const Vec3d& p_up2 = rpts[unextidx];
// Calculate fitness: the average of the two connecting edges
double a = offsdiff2 - (distfn(p_up1, p_low) - zdiff2);
double b = offsdiff2 - (distfn(p_up2, p_low) - zdiff2);
current_fit = (std::abs(a) + std::abs(b)) / 2;
if(current_fit > prev_fit) { // fit is worse than previously
proceed = Proceed::LOWER;
} else { // good to go, create the triangle
inverted
? ind.emplace_back(int(unextidx), int(lidx), int(uidx))
: ind.emplace_back(int(uidx), int(lidx), int(unextidx));
// Increment the iterators, rotate if necessary
++uidx; ++unextidx;
if(unextidx == offs) unextidx = 0;
if(uidx == offs) uidx = 0;
ustarted = true; // mark the movement of the iterators
// so that the comparison to uendidx can be made correctly
}
} else proceed = Proceed::LOWER;
break;
case Proceed::LOWER:
// Mode with lower segment, upper vertex. Same structure:
if(!lstarted || lidx != lendidx) {
const Vec3d& p_low1 = rpts[lidx];
const Vec3d& p_low2 = rpts[lnextidx];
const Vec3d& p_up = rpts[uidx];
double a = offsdiff2 - (distfn(p_up, p_low1) - zdiff2);
double b = offsdiff2 - (distfn(p_up, p_low2) - zdiff2);
current_fit = (std::abs(a) + std::abs(b)) / 2;
if(current_fit > prev_fit) {
proceed = Proceed::UPPER;
} else {
inverted
? ind.emplace_back(int(uidx), int(lnextidx), int(lidx))
: ind.emplace_back(int(lidx), int(lnextidx), int(uidx));
++lidx; ++lnextidx;
if(lnextidx == rpts.size()) lnextidx = offs;
if(lidx == rpts.size()) lidx = offs;
lstarted = true;
}
} else proceed = Proceed::UPPER;
break;
} // end of switch
} while(!ustarted || !lstarted || uidx != uendidx || lidx != lendidx);
return ret;
}
// Same as walls() but with identical higher and lower polygons.
Contour3D inline straight_walls(const Polygon &plate,
double lo_z,
double hi_z,
ThrowOnCancel thr)
{
return walls(plate, plate, lo_z, hi_z, .0 /*offset_diff*/, thr);
}
// As it shows, the current offset_ex in ClipperUtils hangs if used in jtRound
// mode
ClipperLib::Paths fast_offset(const ClipperLib::Paths &paths,
coord_t delta,
ClipperLib::JoinType jointype)
{
using ClipperLib::ClipperOffset;
using ClipperLib::etClosedPolygon;
using ClipperLib::Paths;
using ClipperLib::Path;
ClipperOffset offs;
offs.ArcTolerance = scaled<double>(0.01);
for (auto &p : paths)
// If the input is not at least a triangle, we can not do this algorithm
if(p.size() < 3) {
BOOST_LOG_TRIVIAL(error) << "Invalid geometry for offsetting!";
return {};
}
offs.AddPaths(paths, jointype, etClosedPolygon);
Paths result;
offs.Execute(result, static_cast<double>(delta));
return result;
}
// Function to cut tiny connector cavities for a given polygon. The input poly
// will be offsetted by "padding" and small rectangle shaped cavities will be
// inserted along the perimeter in every "stride" distance. The stick rectangles
// will have a with about "stick_width". The input dimensions are in world
// measure, not the scaled clipper units.
void breakstick_holes(Points& pts,
double padding,
double stride,
double stick_width,
double penetration)
{
if(stride <= EPSILON || stick_width <= EPSILON || padding <= EPSILON)
return;
// SVG svg("bridgestick_plate.svg");
// svg.draw(poly);
// The connector stick will be a small rectangle with dimensions
// stick_width x (penetration + padding) to have some penetration
// into the input polygon.
Points out;
out.reserve(2 * pts.size()); // output polygon points
// stick bottom and right edge dimensions
double sbottom = scaled(stick_width);
double sright = scaled(penetration + padding);
// scaled stride distance
double sstride = scaled(stride);
double t = 0;
// process pairs of vertices as an edge, start with the last and
// first point
for (size_t i = pts.size() - 1, j = 0; j < pts.size(); i = j, ++j) {
// Get vertices and the direction vectors
const Point &a = pts[i], &b = pts[j];
Vec2d dir = b.cast<double>() - a.cast<double>();
double nrm = dir.norm();
dir /= nrm;
Vec2d dirp(-dir(Y), dir(X));
// Insert start point
out.emplace_back(a);
// dodge the start point, do not make sticks on the joins
while (t < sbottom) t += sbottom;
double tend = nrm - sbottom;
while (t < tend) { // insert the stick on the polygon perimeter
// calculate the stick rectangle vertices and insert them
// into the output.
Point p1 = a + (t * dir).cast<coord_t>();
Point p2 = p1 + (sright * dirp).cast<coord_t>();
Point p3 = p2 + (sbottom * dir).cast<coord_t>();
Point p4 = p3 + (sright * -dirp).cast<coord_t>();
out.insert(out.end(), {p1, p2, p3, p4});
// continue along the perimeter
t += sstride;
}
t = t - nrm;
// Insert edge endpoint
out.emplace_back(b);
}
// move the new points
out.shrink_to_fit();
pts.swap(out);
}
template<class...Args>
ExPolygons breakstick_holes(const ExPolygons &input, Args...args)
{
ExPolygons ret = input;
for (ExPolygon &p : ret) {
breakstick_holes(p.contour.points, args...);
for (auto &h : p.holes) breakstick_holes(h.points, args...);
}
return ret;
}
/// A fake concave hull that is constructed by connecting separate shapes
/// with explicit bridges. Bridges are generated from each shape's centroid
/// to the center of the "scene" which is the centroid calculated from the shape
/// centroids (a star is created...)
class ConcaveHull {
Polygons m_polys;
Point centroid(const Points& pp) const
{
Point c;
switch(pp.size()) {
case 0: break;
case 1: c = pp.front(); break;
case 2: c = (pp[0] + pp[1]) / 2; break;
default: {
auto MAX = std::numeric_limits<Point::coord_type>::max();
auto MIN = std::numeric_limits<Point::coord_type>::min();
Point min = {MAX, MAX}, max = {MIN, MIN};
for(auto& p : pp) {
if(p(0) < min(0)) min(0) = p(0);
if(p(1) < min(1)) min(1) = p(1);
if(p(0) > max(0)) max(0) = p(0);
if(p(1) > max(1)) max(1) = p(1);
}
c(0) = min(0) + (max(0) - min(0)) / 2;
c(1) = min(1) + (max(1) - min(1)) / 2;
break;
}
}
return c;
}
inline Point centroid(const Polygon &poly) const { return poly.centroid(); }
Points calculate_centroids() const
{
// We get the centroids of all the islands in the 2D slice
Points centroids = reserve_vector<Point>(m_polys.size());
std::transform(m_polys.begin(), m_polys.end(),
std::back_inserter(centroids),
[this](const Polygon &poly) { return centroid(poly); });
return centroids;
}
void merge_polygons() { m_polys = union_(m_polys); }
void add_connector_rectangles(const Points &centroids,
coord_t max_dist,
ThrowOnCancel thr)
{
namespace bgi = boost::geometry::index;
using PointIndexElement = std::pair<Point, unsigned>;
using PointIndex = bgi::rtree<PointIndexElement, bgi::rstar<16, 4>>;
// Centroid of the centroids of islands. This is where the additional
// connector sticks are routed.
Point cc = centroid(centroids);
PointIndex ctrindex;
unsigned idx = 0;
for(const Point &ct : centroids)
ctrindex.insert(std::make_pair(ct, idx++));
m_polys.reserve(m_polys.size() + centroids.size());
idx = 0;
for (const Point &c : centroids) {
thr();
double dx = c.x() - cc.x(), dy = c.y() - cc.y();
double l = std::sqrt(dx * dx + dy * dy);
double nx = dx / l, ny = dy / l;
const Point &ct = centroids[idx];
std::vector<PointIndexElement> result;
ctrindex.query(bgi::nearest(ct, 2), std::back_inserter(result));
double dist = max_dist;
for (const PointIndexElement &el : result)
if (el.second != idx) {
dist = Line(el.first, ct).length();
break;
}
idx++;
if (dist >= max_dist) return;
Polygon r;
r.points.reserve(3);
r.points.emplace_back(cc);
Point d(scaled(nx), scaled(ny));
r.points.emplace_back(c + Point(-d.y(), d.x()));
r.points.emplace_back(c + Point(d.y(), -d.x()));
offset(r, scaled<float>(1.));
m_polys.emplace_back(r);
}
}
public:
ConcaveHull(const ExPolygons& polys, double merge_dist, ThrowOnCancel thr)
: ConcaveHull{to_polygons(polys), merge_dist, thr} {}
ConcaveHull(const Polygons& polys, double mergedist, ThrowOnCancel thr)
{
if(polys.empty()) return;
m_polys = polys;
merge_polygons();
if(m_polys.size() == 1) return;
Points centroids = calculate_centroids();
add_connector_rectangles(centroids, scaled(mergedist), thr);
merge_polygons();
}
// const Polygons & polygons() const { return m_polys; }
ExPolygons to_expolygons() const
{
auto ret = reserve_vector<ExPolygon>(m_polys.size());
for (const Polygon &p : m_polys) ret.emplace_back(ExPolygon(p));
return ret;
}
void offset_waffle_style(coord_t delta) {
ClipperLib::Paths paths = Slic3rMultiPoints_to_ClipperPaths(m_polys);
paths = fast_offset(paths, 2 * delta, ClipperLib::jtRound);
paths = fast_offset(paths, -delta, ClipperLib::jtRound);
m_polys = ClipperPaths_to_Slic3rPolygons(paths);
}
static inline coord_t get_waffle_offset(const PadConfig &c)
{
return scaled(c.brim_size_mm + c.wing_distance());
}
static inline double get_merge_distance(const PadConfig &c)
{
return 2. * (1.8 * c.wall_thickness_mm) + c.max_merge_dist_mm;
}
};
// Part of the pad configuration that is used for 3D geometry generation
struct PadConfig3D {
double thickness, height, wing_height, slope;
explicit PadConfig3D(const PadConfig &cfg2d)
: thickness{cfg2d.wall_thickness_mm}
, height{cfg2d.full_height()}
, wing_height{cfg2d.wall_height_mm}
, slope{cfg2d.wall_slope}
{}
inline double bottom_offset() const
{
return (thickness + wing_height) / std::tan(slope);
}
};
// Outer part of the skeleton is used to generate the waffled edges of the pad.
// Inner parts will not be waffled or offsetted. Inner parts are only used if
// pad is generated around the object and correspond to holes and inner polygons
// in the model blueprint.
struct PadSkeleton { ExPolygons inner, outer; };
PadSkeleton divide_blueprint(const ExPolygons &bp)
{
ClipperLib::PolyTree ptree = union_pt(bp);
PadSkeleton ret;
ret.inner.reserve(size_t(ptree.Total()));
ret.outer.reserve(size_t(ptree.Total()));
for (ClipperLib::PolyTree::PolyNode *node : ptree.Childs) {
ExPolygon poly(ClipperPath_to_Slic3rPolygon(node->Contour));
for (ClipperLib::PolyTree::PolyNode *child : node->Childs) {
if (child->IsHole()) {
poly.holes.emplace_back(
ClipperPath_to_Slic3rPolygon(child->Contour));
traverse_pt_unordered(child->Childs, &ret.inner);
}
else traverse_pt_unordered(child, &ret.inner);
}
ret.outer.emplace_back(poly);
}
return ret;
}
// A helper class for storing polygons and maintaining a spatial index of their
// bounding boxes.
class Intersector {
BoxIndex m_index;
ExPolygons m_polys;
public:
// Add a new polygon to the index
void add(const ExPolygon &ep)
{
m_polys.emplace_back(ep);
m_index.insert(BoundingBox{ep}, unsigned(m_index.size()));
}
// Check an arbitrary polygon for intersection with the indexed polygons
bool intersects(const ExPolygon &poly)
{
// Create a suitable query bounding box.
auto bb = poly.contour.bounding_box();
std::vector<BoxIndexEl> qres = m_index.query(bb, BoxIndex::qtIntersects);
// Now check intersections on the actual polygons (not just the boxes)
bool is_overlap = false;
auto qit = qres.begin();
while (!is_overlap && qit != qres.end())
is_overlap = is_overlap || poly.overlaps(m_polys[(qit++)->second]);
return is_overlap;
}
};
// This dummy intersector to implement the "force pad everywhere" feature
struct DummyIntersector
{
inline void add(const ExPolygon &) {}
inline bool intersects(const ExPolygon &) { return true; }
};
template<class _Intersector>
class _AroundPadSkeleton : public PadSkeleton
{
// A spatial index used to be able to efficiently find intersections of
// support polygons with the model polygons.
_Intersector m_intersector;
public:
_AroundPadSkeleton(const ExPolygons &support_blueprint,
const ExPolygons &model_blueprint,
const PadConfig & cfg,
ThrowOnCancel thr)
{
// We need to merge the support and the model contours in a special
// way in which the model contours have to be substracted from the
// support contours. The pad has to have a hole in which the model can
// fit perfectly (thus the substraction -- diff_ex). Also, the pad has
// to be eliminated from areas where there is no need for a pad, due
// to missing supports.
add_supports_to_index(support_blueprint);
auto model_bp_offs =
offset_ex(model_blueprint,
scaled<float>(cfg.embed_object.object_gap_mm),
ClipperLib::jtMiter, 1);
ConcaveHull fullcvh =
wafflized_concave_hull(support_blueprint, model_bp_offs, cfg, thr);
auto model_bp_sticks =
breakstick_holes(model_bp_offs, cfg.embed_object.object_gap_mm,
cfg.embed_object.stick_stride_mm,
cfg.embed_object.stick_width_mm,
cfg.embed_object.stick_penetration_mm);
ExPolygons fullpad = diff_ex(fullcvh.to_expolygons(), model_bp_sticks);
remove_redundant_parts(fullpad);
PadSkeleton divided = divide_blueprint(fullpad);
outer = std::move(divided.outer);
inner = std::move(divided.inner);
}
private:
// Add the support blueprint to the search index to be queried later
void add_supports_to_index(const ExPolygons &supp_bp)
{
for (auto &ep : supp_bp) m_intersector.add(ep);
}
// Create the wafflized pad around all object in the scene. This pad doesnt
// have any holes yet.
ConcaveHull wafflized_concave_hull(const ExPolygons &supp_bp,
const ExPolygons &model_bp,
const PadConfig &cfg,
ThrowOnCancel thr)
{
auto allin = reserve_vector<ExPolygon>(supp_bp.size() + model_bp.size());
for (auto &ep : supp_bp) allin.emplace_back(ep.contour);
for (auto &ep : model_bp) allin.emplace_back(ep.contour);
ConcaveHull ret{allin, ConcaveHull::get_merge_distance(cfg), thr};
ret.offset_waffle_style(ConcaveHull::get_waffle_offset(cfg));
return ret;
}
// To remove parts of the pad skeleton which do not host any supports
void remove_redundant_parts(ExPolygons &parts)
{
auto endit = std::remove_if(parts.begin(), parts.end(),
[this](const ExPolygon &p) {
return !m_intersector.intersects(p);
});
parts.erase(endit, parts.end());
}
};
using AroundPadSkeleton = _AroundPadSkeleton<Intersector>;
using BrimPadSkeleton = _AroundPadSkeleton<DummyIntersector>;
class BelowPadSkeleton : public PadSkeleton
{
public:
BelowPadSkeleton(const ExPolygons &support_blueprint,
const ExPolygons &model_blueprint,
const PadConfig & cfg,
ThrowOnCancel thr)
{
outer.reserve(support_blueprint.size() + model_blueprint.size());
for (auto &ep : support_blueprint) outer.emplace_back(ep.contour);
for (auto &ep : model_blueprint) outer.emplace_back(ep.contour);
ConcaveHull ochull{outer, ConcaveHull::get_merge_distance(cfg), thr};
ochull.offset_waffle_style(ConcaveHull::get_waffle_offset(cfg));
outer = ochull.to_expolygons();
}
};
// Offset the contour only, leave the holes untouched
template<class...Args>
ExPolygon offset_contour_only(const ExPolygon &poly, coord_t delta, Args...args)
{
ExPolygons tmp = offset_ex(poly.contour, float(delta), args...);
if (tmp.empty()) return {};
Polygons holes = poly.holes;
for (auto &h : holes) h.reverse();
tmp = diff_ex(to_polygons(tmp), holes);
if (tmp.empty()) return {};
return tmp.front();
}
bool add_cavity(Contour3D &pad, ExPolygon &top_poly, const PadConfig3D &cfg,
ThrowOnCancel thr)
{
auto logerr = []{BOOST_LOG_TRIVIAL(error)<<"Could not create pad cavity";};
double wing_distance = cfg.wing_height / std::tan(cfg.slope);
coord_t delta_inner = -scaled(cfg.thickness + wing_distance);
coord_t delta_middle = -scaled(cfg.thickness);
ExPolygon inner_base = offset_contour_only(top_poly, delta_inner);
ExPolygon middle_base = offset_contour_only(top_poly, delta_middle);
if (inner_base.empty() || middle_base.empty()) { logerr(); return false; }
ExPolygons pdiff = diff_ex(top_poly, middle_base.contour);
if (pdiff.size() != 1) { logerr(); return false; }
top_poly = pdiff.front();
double z_min = -cfg.wing_height, z_max = 0;
double offset_difference = -wing_distance;
pad.merge(walls(inner_base.contour, middle_base.contour, z_min, z_max,
offset_difference, thr));
pad.merge(triangulate_expolygon_3d(inner_base, z_min, NORMALS_UP));
return true;
}
Contour3D create_outer_pad_geometry(const ExPolygons & skeleton,
const PadConfig3D &cfg,
ThrowOnCancel thr)
{
Contour3D ret;
for (const ExPolygon &pad_part : skeleton) {
ExPolygon top_poly{pad_part};
ExPolygon bottom_poly =
offset_contour_only(pad_part, -scaled(cfg.bottom_offset()));
if (bottom_poly.empty()) continue;
double z_min = -cfg.height, z_max = 0;
ret.merge(walls(top_poly.contour, bottom_poly.contour, z_max, z_min,
cfg.bottom_offset(), thr));
if (cfg.wing_height > 0. && add_cavity(ret, top_poly, cfg, thr))
z_max = -cfg.wing_height;
for (auto &h : bottom_poly.holes)
ret.merge(straight_walls(h, z_max, z_min, thr));
ret.merge(triangulate_expolygon_3d(bottom_poly, z_min, NORMALS_DOWN));
ret.merge(triangulate_expolygon_3d(top_poly, NORMALS_UP));
}
return ret;
}
Contour3D create_inner_pad_geometry(const ExPolygons & skeleton,
const PadConfig3D &cfg,
ThrowOnCancel thr)
{
Contour3D ret;
double z_max = 0., z_min = -cfg.height;
for (const ExPolygon &pad_part : skeleton) {
ret.merge(straight_walls(pad_part.contour, z_max, z_min,thr));
for (auto &h : pad_part.holes)
ret.merge(straight_walls(h, z_max, z_min, thr));
ret.merge(triangulate_expolygon_3d(pad_part, z_min, NORMALS_DOWN));
ret.merge(triangulate_expolygon_3d(pad_part, z_max, NORMALS_UP));
}
return ret;
}
Contour3D create_pad_geometry(const PadSkeleton &skelet,
const PadConfig & cfg,
ThrowOnCancel thr)
{
#ifndef NDEBUG
SVG svg("pad_skeleton.svg");
svg.draw(skelet.outer, "green");
svg.draw(skelet.inner, "blue");
svg.Close();
#endif
PadConfig3D cfg3d(cfg);
return create_outer_pad_geometry(skelet.outer, cfg3d, thr)
.merge(create_inner_pad_geometry(skelet.inner, cfg3d, thr));
}
Contour3D create_pad_geometry(const ExPolygons &supp_bp,
const ExPolygons &model_bp,
const PadConfig & cfg,
ThrowOnCancel thr)
{
PadSkeleton skelet;
if (cfg.embed_object.enabled) {
if (cfg.embed_object.everywhere)
skelet = BrimPadSkeleton(supp_bp, model_bp, cfg, thr);
else
skelet = AroundPadSkeleton(supp_bp, model_bp, cfg, thr);
} else
skelet = BelowPadSkeleton(supp_bp, model_bp, cfg, thr);
return create_pad_geometry(skelet, cfg, thr);
}
} // namespace
void pad_blueprint(const TriangleMesh & mesh,
ExPolygons & output,
const std::vector<float> &heights,
ThrowOnCancel thrfn)
{
if (mesh.empty()) return;
TriangleMeshSlicer slicer(&mesh);
auto out = reserve_vector<ExPolygons>(heights.size());
slicer.slice(heights, 0.f, &out, thrfn);
size_t count = 0;
for(auto& o : out) count += o.size();
// Unification is expensive, a simplify also speeds up the pad generation
auto tmp = reserve_vector<ExPolygon>(count);
for(ExPolygons& o : out)
for(ExPolygon& e : o) {
auto&& exss = e.simplify(scaled<double>(0.1));
for(ExPolygon& ep : exss) tmp.emplace_back(std::move(ep));
}
ExPolygons utmp = union_ex(tmp);
for(auto& o : utmp) {
auto&& smp = o.simplify(scaled<double>(0.1));
output.insert(output.end(), smp.begin(), smp.end());
}
}
void pad_blueprint(const TriangleMesh &mesh,
ExPolygons & output,
float h,
float layerh,
ThrowOnCancel thrfn)
{
float gnd = float(mesh.bounding_box().min(Z));
std::vector<float> slicegrid = grid(gnd, gnd + h, layerh);
pad_blueprint(mesh, output, slicegrid, thrfn);
}
void create_pad(const ExPolygons &sup_blueprint,
const ExPolygons &model_blueprint,
TriangleMesh & out,
const PadConfig & cfg,
ThrowOnCancel thr)
{
Contour3D t = create_pad_geometry(sup_blueprint, model_blueprint, cfg, thr);
out.merge(mesh(std::move(t)));
}
std::string PadConfig::validate() const
{
static const double constexpr MIN_BRIM_SIZE_MM = .1;
if (brim_size_mm < MIN_BRIM_SIZE_MM ||
bottom_offset() > brim_size_mm + wing_distance() ||
ConcaveHull::get_waffle_offset(*this) <= MIN_BRIM_SIZE_MM)
return L("Pad brim size is too small for the current configuration.");
return "";
}
}} // namespace Slic3r::sla

View file

@ -0,0 +1,94 @@
#ifndef SLABASEPOOL_HPP
#define SLABASEPOOL_HPP
#include <vector>
#include <functional>
#include <cmath>
#include <string>
namespace Slic3r {
class ExPolygon;
class Polygon;
using ExPolygons = std::vector<ExPolygon>;
using Polygons = std::vector<Polygon>;
class TriangleMesh;
namespace sla {
using ThrowOnCancel = std::function<void(void)>;
/// Calculate the polygon representing the silhouette.
void pad_blueprint(
const TriangleMesh &mesh, // input mesh
ExPolygons & output, // Output will be merged with
const std::vector<float> &, // Exact Z levels to sample
ThrowOnCancel thrfn = [] {}); // Function that throws if cancel was requested
void pad_blueprint(
const TriangleMesh &mesh,
ExPolygons & output,
float samplingheight = 0.1f, // The height range to sample
float layerheight = 0.05f, // The sampling height
ThrowOnCancel thrfn = [] {});
struct PadConfig {
double wall_thickness_mm = 1.;
double wall_height_mm = 1.;
double max_merge_dist_mm = 50;
double wall_slope = std::atan(1.0); // Universal constant for Pi/4
double brim_size_mm = 1.6;
struct EmbedObject {
double object_gap_mm = 1.;
double stick_stride_mm = 10.;
double stick_width_mm = 0.5;
double stick_penetration_mm = 0.1;
bool enabled = false;
bool everywhere = false;
operator bool() const { return enabled; }
} embed_object;
inline PadConfig() = default;
inline PadConfig(double thickness,
double height,
double mergedist,
double slope)
: wall_thickness_mm(thickness)
, wall_height_mm(height)
, max_merge_dist_mm(mergedist)
, wall_slope(slope)
{}
inline double bottom_offset() const
{
return (wall_thickness_mm + wall_height_mm) / std::tan(wall_slope);
}
inline double wing_distance() const
{
return wall_height_mm / std::tan(wall_slope);
}
inline double full_height() const
{
return wall_height_mm + wall_thickness_mm;
}
/// Returns the elevation needed for compensating the pad.
inline double required_elevation() const { return wall_thickness_mm; }
std::string validate() const;
};
void create_pad(const ExPolygons &support_contours,
const ExPolygons &model_contours,
TriangleMesh & output_mesh,
const PadConfig & = PadConfig(),
ThrowOnCancel throw_on_cancel = []{});
} // namespace sla
} // namespace Slic3r
#endif // SLABASEPOOL_HPP

View file

@ -5,6 +5,7 @@
#include "SLARaster.hpp" #include "SLARaster.hpp"
#include "libslic3r/ExPolygon.hpp" #include "libslic3r/ExPolygon.hpp"
#include "libslic3r/MTUtils.hpp"
#include <libnest2d/backends/clipper/clipper_polygon.hpp> #include <libnest2d/backends/clipper/clipper_polygon.hpp>
// For rasterizing // For rasterizing
@ -32,25 +33,30 @@ inline const ClipperLib::Paths& holes(const ClipperLib::Polygon& p) { return p.H
namespace sla { namespace sla {
const Raster::TMirroring Raster::NoMirror = {false, false};
const Raster::TMirroring Raster::MirrorX = {true, false};
const Raster::TMirroring Raster::MirrorY = {false, true};
const Raster::TMirroring Raster::MirrorXY = {true, true};
using TPixelRenderer = agg::pixfmt_gray8; // agg::pixfmt_rgb24;
using TRawRenderer = agg::renderer_base<TPixelRenderer>;
using TPixel = TPixelRenderer::color_type;
using TRawBuffer = agg::rendering_buffer;
using TBuffer = std::vector<TPixelRenderer::pixel_type>;
using TRendererAA = agg::renderer_scanline_aa_solid<TRawRenderer>;
class Raster::Impl { class Raster::Impl {
public: public:
using TPixelRenderer = agg::pixfmt_gray8; // agg::pixfmt_rgb24;
using TRawRenderer = agg::renderer_base<TPixelRenderer>;
using TPixel = TPixelRenderer::color_type;
using TRawBuffer = agg::rendering_buffer;
using TBuffer = std::vector<TPixelRenderer::pixel_type>;
using TRendererAA = agg::renderer_scanline_aa_solid<TRawRenderer>;
static const TPixel ColorWhite; static const TPixel ColorWhite;
static const TPixel ColorBlack; static const TPixel ColorBlack;
using Format = Raster::Format; using Format = Raster::RawData;
private: private:
Raster::Resolution m_resolution; Raster::Resolution m_resolution;
// Raster::PixelDim m_pxdim;
Raster::PixelDim m_pxdim_scaled; // used for scaled coordinate polygons Raster::PixelDim m_pxdim_scaled; // used for scaled coordinate polygons
TBuffer m_buf; TBuffer m_buf;
TRawBuffer m_rbuf; TRawBuffer m_rbuf;
@ -59,74 +65,49 @@ private:
TRendererAA m_renderer; TRendererAA m_renderer;
std::function<double(double)> m_gammafn; std::function<double(double)> m_gammafn;
std::array<bool, 2> m_mirror; Trafo m_trafo;
Format m_fmt = Format::PNG;
inline void flipy(agg::path_storage& path) const { inline void flipy(agg::path_storage& path) const {
path.flip_y(0, m_resolution.height_px); path.flip_y(0, double(m_resolution.height_px));
} }
inline void flipx(agg::path_storage& path) const { inline void flipx(agg::path_storage& path) const {
path.flip_x(0, m_resolution.width_px); path.flip_x(0, double(m_resolution.width_px));
} }
public: public:
inline Impl(const Raster::Resolution & res,
inline Impl(const Raster::Resolution& res, const Raster::PixelDim &pd, const Raster::PixelDim & pd,
const std::array<bool, 2>& mirror, double gamma = 1.0): const Trafo &trafo)
m_resolution(res), : m_resolution(res)
// m_pxdim(pd), , m_pxdim_scaled(SCALING_FACTOR / pd.w_mm, SCALING_FACTOR / pd.h_mm)
m_pxdim_scaled(SCALING_FACTOR / pd.w_mm, SCALING_FACTOR / pd.h_mm), , m_buf(res.pixels())
m_buf(res.pixels()), , m_rbuf(reinterpret_cast<TPixelRenderer::value_type *>(m_buf.data()),
m_rbuf(reinterpret_cast<TPixelRenderer::value_type*>(m_buf.data()), unsigned(res.width_px),
res.width_px, res.height_px, unsigned(res.height_px),
int(res.width_px*TPixelRenderer::num_components)), int(res.width_px * TPixelRenderer::num_components))
m_pixfmt(m_rbuf), , m_pixfmt(m_rbuf)
m_raw_renderer(m_pixfmt), , m_raw_renderer(m_pixfmt)
m_renderer(m_raw_renderer), , m_renderer(m_raw_renderer)
m_mirror(mirror) , m_trafo(trafo)
{ {
m_renderer.color(ColorWhite); m_renderer.color(ColorWhite);
if(gamma > 0) m_gammafn = agg::gamma_power(gamma); if (trafo.gamma > 0) m_gammafn = agg::gamma_power(trafo.gamma);
else m_gammafn = agg::gamma_threshold(0.5); else m_gammafn = agg::gamma_threshold(0.5);
clear(); clear();
} }
inline Impl(const Raster::Resolution& res,
const Raster::PixelDim &pd,
Format fmt,
double gamma = 1.0):
Impl(res, pd, {false, false}, gamma)
{
switch (fmt) {
case Format::PNG: m_mirror = {false, true}; break;
case Format::RAW: m_mirror = {false, false}; break;
}
m_fmt = fmt;
}
template<class P> void draw(const P &poly) { template<class P> void draw(const P &poly) {
agg::rasterizer_scanline_aa<> ras; agg::rasterizer_scanline_aa<> ras;
agg::scanline_p8 scanlines; agg::scanline_p8 scanlines;
ras.gamma(m_gammafn); ras.gamma(m_gammafn);
auto&& path = to_path(contour(poly));
if(m_mirror[X]) flipx(path); ras.add_path(to_path(contour(poly)));
if(m_mirror[Y]) flipy(path); for(auto& h : holes(poly)) ras.add_path(to_path(h));
ras.add_path(path);
for(auto& h : holes(poly)) {
auto&& holepath = to_path(h);
if(m_mirror[X]) flipx(holepath);
if(m_mirror[Y]) flipy(holepath);
ras.add_path(holepath);
}
agg::render_scanlines(ras, scanlines, m_renderer); agg::render_scanlines(ras, scanlines, m_renderer);
} }
@ -135,11 +116,16 @@ public:
} }
inline TBuffer& buffer() { return m_buf; } inline TBuffer& buffer() { return m_buf; }
inline const TBuffer& buffer() const { return m_buf; }
inline Format format() const { return m_fmt; }
inline const Raster::Resolution resolution() { return m_resolution; } inline const Raster::Resolution resolution() { return m_resolution; }
inline const Raster::PixelDim pixdim()
{
return {SCALING_FACTOR / m_pxdim_scaled.w_mm,
SCALING_FACTOR / m_pxdim_scaled.h_mm};
}
private: private:
inline double getPx(const Point& p) { inline double getPx(const Point& p) {
return p(0) * m_pxdim_scaled.w_mm; return p(0) * m_pxdim_scaled.w_mm;
@ -162,49 +148,67 @@ private:
return p.Y * m_pxdim_scaled.h_mm; return p.Y * m_pxdim_scaled.h_mm;
} }
template<class PointVec> agg::path_storage to_path(const PointVec& poly) template<class PointVec> agg::path_storage _to_path(const PointVec& v)
{ {
agg::path_storage path; agg::path_storage path;
auto it = poly.begin(); auto it = v.begin();
path.move_to(getPx(*it), getPy(*it)); path.move_to(getPx(*it), getPy(*it));
while(++it != v.end()) path.line_to(getPx(*it), getPy(*it));
path.line_to(getPx(v.front()), getPy(v.front()));
return path;
}
template<class PointVec> agg::path_storage _to_path_flpxy(const PointVec& v)
{
agg::path_storage path;
auto it = v.begin();
path.move_to(getPy(*it), getPx(*it));
while(++it != v.end()) path.line_to(getPy(*it), getPx(*it));
path.line_to(getPy(v.front()), getPx(v.front()));
return path;
}
template<class PointVec> agg::path_storage to_path(const PointVec &v)
{
auto path = m_trafo.flipXY ? _to_path_flpxy(v) : _to_path(v);
path.translate_all_paths(m_trafo.origin_x * m_pxdim_scaled.w_mm,
m_trafo.origin_y * m_pxdim_scaled.h_mm);
if(m_trafo.mirror_x) flipx(path);
if(m_trafo.mirror_y) flipy(path);
while(++it != poly.end())
path.line_to(getPx(*it), getPy(*it));
path.line_to(getPx(poly.front()), getPy(poly.front()));
return path; return path;
} }
}; };
const Raster::Impl::TPixel Raster::Impl::ColorWhite = Raster::Impl::TPixel(255); const TPixel Raster::Impl::ColorWhite = TPixel(255);
const Raster::Impl::TPixel Raster::Impl::ColorBlack = Raster::Impl::TPixel(0); const TPixel Raster::Impl::ColorBlack = TPixel(0);
Raster::Raster() { reset(); }
Raster::Raster(const Raster::Resolution &r,
const Raster::PixelDim & pd,
const Raster::Trafo & tr)
{
reset(r, pd, tr);
}
template<> Raster::Raster() { reset(); };
Raster::~Raster() = default; Raster::~Raster() = default;
// Raster::Raster(Raster &&m) = default; Raster::Raster(Raster &&m) = default;
// Raster& Raster::operator=(Raster&&) = default; Raster &Raster::operator=(Raster &&) = default;
// FIXME: remove after migrating to higher version of windows compiler
Raster::Raster(Raster &&m): m_impl(std::move(m.m_impl)) {}
Raster& Raster::operator=(Raster &&m) {
m_impl = std::move(m.m_impl); return *this;
}
void Raster::reset(const Raster::Resolution &r, const Raster::PixelDim &pd, void Raster::reset(const Raster::Resolution &r, const Raster::PixelDim &pd,
Format fmt, double gamma) const Trafo &trafo)
{ {
m_impl.reset(); m_impl.reset();
m_impl.reset(new Impl(r, pd, fmt, gamma)); m_impl.reset(new Impl(r, pd, trafo));
}
void Raster::reset(const Raster::Resolution &r, const Raster::PixelDim &pd,
const std::array<bool, 2>& mirror, double gamma)
{
m_impl.reset();
m_impl.reset(new Impl(r, pd, mirror, gamma));
} }
void Raster::reset() void Raster::reset()
@ -214,9 +218,16 @@ void Raster::reset()
Raster::Resolution Raster::resolution() const Raster::Resolution Raster::resolution() const
{ {
if(m_impl) return m_impl->resolution(); if (m_impl) return m_impl->resolution();
return Resolution{0, 0};
}
return Resolution(0, 0); Raster::PixelDim Raster::pixel_dimensions() const
{
if (m_impl) return m_impl->pixdim();
return PixelDim{0., 0.};
} }
void Raster::clear() void Raster::clear()
@ -227,103 +238,83 @@ void Raster::clear()
void Raster::draw(const ExPolygon &expoly) void Raster::draw(const ExPolygon &expoly)
{ {
assert(m_impl);
m_impl->draw(expoly); m_impl->draw(expoly);
} }
void Raster::draw(const ClipperLib::Polygon &poly) void Raster::draw(const ClipperLib::Polygon &poly)
{ {
assert(m_impl);
m_impl->draw(poly); m_impl->draw(poly);
} }
void Raster::save(std::ostream& stream, Format fmt) uint8_t Raster::read_pixel(size_t x, size_t y) const
{ {
assert(m_impl); assert (m_impl);
if(!stream.good()) return; TPixel::value_type px;
m_impl->buffer()[y * resolution().width_px + x].get(px);
switch(fmt) { return px;
case Format::PNG: {
auto& b = m_impl->buffer();
size_t out_len = 0;
void * rawdata = tdefl_write_image_to_png_file_in_memory(
b.data(),
int(resolution().width_px),
int(resolution().height_px), 1, &out_len);
if(rawdata == nullptr) break;
stream.write(static_cast<const char*>(rawdata),
std::streamsize(out_len));
MZ_FREE(rawdata);
break;
}
case Format::RAW: {
stream << "P5 "
<< m_impl->resolution().width_px << " "
<< m_impl->resolution().height_px << " "
<< "255 ";
auto sz = m_impl->buffer().size()*sizeof(Impl::TBuffer::value_type);
stream.write(reinterpret_cast<const char*>(m_impl->buffer().data()),
std::streamsize(sz));
}
}
} }
void Raster::save(std::ostream &stream) PNGImage & PNGImage::serialize(const Raster &raster)
{ {
save(stream, m_impl->format()); size_t s = 0;
m_buffer.clear();
void *rawdata = tdefl_write_image_to_png_file_in_memory(
get_internals(raster).buffer().data(),
int(raster.resolution().width_px),
int(raster.resolution().height_px), 1, &s);
// On error, data() will return an empty vector. No other info can be
// retrieved from miniz anyway...
if (rawdata == nullptr) return *this;
auto ptr = static_cast<std::uint8_t*>(rawdata);
m_buffer.reserve(s);
std::copy(ptr, ptr + s, std::back_inserter(m_buffer));
MZ_FREE(rawdata);
return *this;
} }
RawBytes Raster::save(Format fmt) std::ostream &operator<<(std::ostream &stream, const Raster::RawData &bytes)
{ {
assert(m_impl); stream.write(reinterpret_cast<const char *>(bytes.data()),
std::streamsize(bytes.size()));
std::vector<std::uint8_t> data; size_t s = 0;
return stream;
switch(fmt) {
case Format::PNG: {
void *rawdata = tdefl_write_image_to_png_file_in_memory(
m_impl->buffer().data(),
int(resolution().width_px),
int(resolution().height_px), 1, &s);
if(rawdata == nullptr) break;
auto ptr = static_cast<std::uint8_t*>(rawdata);
data.reserve(s); std::copy(ptr, ptr + s, std::back_inserter(data));
MZ_FREE(rawdata);
break;
}
case Format::RAW: {
auto header = std::string("P5 ") +
std::to_string(m_impl->resolution().width_px) + " " +
std::to_string(m_impl->resolution().height_px) + " " + "255 ";
auto sz = m_impl->buffer().size()*sizeof(Impl::TBuffer::value_type);
s = sz + header.size();
data.reserve(s);
auto buff = reinterpret_cast<std::uint8_t*>(m_impl->buffer().data());
std::copy(header.begin(), header.end(), std::back_inserter(data));
std::copy(buff, buff+sz, std::back_inserter(data));
break;
}
}
return {std::move(data)};
} }
RawBytes Raster::save() Raster::RawData::~RawData() = default;
PPMImage & PPMImage::serialize(const Raster &raster)
{ {
return save(m_impl->format()); auto header = std::string("P5 ") +
std::to_string(raster.resolution().width_px) + " " +
std::to_string(raster.resolution().height_px) + " " + "255 ";
const auto &impl = get_internals(raster);
auto sz = impl.buffer().size() * sizeof(TBuffer::value_type);
size_t s = sz + header.size();
m_buffer.clear();
m_buffer.reserve(s);
auto buff = reinterpret_cast<const std::uint8_t*>(impl.buffer().data());
std::copy(header.begin(), header.end(), std::back_inserter(m_buffer));
std::copy(buff, buff+sz, std::back_inserter(m_buffer));
return *this;
} }
const Raster::Impl &Raster::RawData::get_internals(const Raster &raster)
{
return *raster.m_impl;
} }
}
} // namespace sla
} // namespace Slic3r
#endif // SLARASTER_CPP #endif // SLARASTER_CPP

View file

@ -8,45 +8,13 @@
#include <utility> #include <utility>
#include <cstdint> #include <cstdint>
#include <libslic3r/ExPolygon.hpp>
namespace ClipperLib { struct Polygon; } namespace ClipperLib { struct Polygon; }
namespace Slic3r { namespace Slic3r {
class ExPolygon;
namespace sla { namespace sla {
// Raw byte buffer paired with its size. Suitable for compressed PNG data.
class RawBytes {
std::vector<std::uint8_t> m_buffer;
public:
RawBytes() = default;
RawBytes(std::vector<std::uint8_t>&& data): m_buffer(std::move(data)) {}
size_t size() const { return m_buffer.size(); }
const uint8_t * data() { return m_buffer.data(); }
RawBytes(const RawBytes&) = delete;
RawBytes& operator=(const RawBytes&) = delete;
// /////////////////////////////////////////////////////////////////////////
// FIXME: the following is needed for MSVC2013 compatibility
// /////////////////////////////////////////////////////////////////////////
// RawBytes(RawBytes&&) = default;
// RawBytes& operator=(RawBytes&&) = default;
RawBytes(RawBytes&& mv) : m_buffer(std::move(mv.m_buffer)) {}
RawBytes& operator=(RawBytes&& mv) {
m_buffer = std::move(mv.m_buffer);
return *this;
}
// /////////////////////////////////////////////////////////////////////////
};
/** /**
* @brief Raster captures an anti-aliased monochrome canvas where vectorial * @brief Raster captures an anti-aliased monochrome canvas where vectorial
* polygons can be rasterized. Fill color is always white and the background is * polygons can be rasterized. Fill color is always white and the background is
@ -60,10 +28,28 @@ class Raster {
std::unique_ptr<Impl> m_impl; std::unique_ptr<Impl> m_impl;
public: public:
/// Supported compression types // Raw byte buffer paired with its size. Suitable for compressed image data.
enum class Format { class RawData
RAW, //!> Uncompressed pixel data {
PNG //!> PNG compression protected:
std::vector<std::uint8_t> m_buffer;
const Impl& get_internals(const Raster& raster);
public:
RawData() = default;
RawData(std::vector<std::uint8_t>&& data): m_buffer(std::move(data)) {}
virtual ~RawData();
RawData(const RawData &) = delete;
RawData &operator=(const RawData &) = delete;
RawData(RawData &&) = default;
RawData &operator=(RawData &&) = default;
size_t size() const { return m_buffer.size(); }
const uint8_t * data() const { return m_buffer.data(); }
virtual RawData& serialize(const Raster &/*raster*/) { return *this; }
virtual std::string get_file_extension() const = 0;
}; };
/// Type that represents a resolution in pixels. /// Type that represents a resolution in pixels.
@ -86,11 +72,36 @@ public:
w_mm(px_width_mm), h_mm(px_height_mm) {} w_mm(px_width_mm), h_mm(px_height_mm) {}
}; };
/// Constructor taking the resolution and the pixel dimension. enum Orientation { roLandscape, roPortrait };
template <class...Args> Raster(Args...args) {
reset(std::forward<Args>(args)...); using TMirroring = std::array<bool, 2>;
} static const TMirroring NoMirror;
static const TMirroring MirrorX;
static const TMirroring MirrorY;
static const TMirroring MirrorXY;
struct Trafo {
bool mirror_x = false, mirror_y = false, flipXY = false;
coord_t origin_x = 0, origin_y = 0;
// If gamma is zero, thresholding will be performed which disables AA.
double gamma = 1.;
// Portrait orientation will make sure the drawed polygons are rotated
// by 90 degrees.
Trafo(Orientation o = roLandscape, const TMirroring &mirror = NoMirror)
// XY flipping implicitly does an X mirror
: mirror_x(o == roPortrait ? !mirror[0] : mirror[0])
, mirror_y(!mirror[1]) // Makes raster origin to be top left corner
, flipXY(o == roPortrait)
{}
};
Raster();
Raster(const Resolution &r,
const PixelDim & pd,
const Trafo & tr = {});
Raster(const Raster& cpy) = delete; Raster(const Raster& cpy) = delete;
Raster& operator=(const Raster& cpy) = delete; Raster& operator=(const Raster& cpy) = delete;
Raster(Raster&& m); Raster(Raster&& m);
@ -98,18 +109,10 @@ public:
~Raster(); ~Raster();
/// Reallocated everything for the given resolution and pixel dimension. /// Reallocated everything for the given resolution and pixel dimension.
/// The third parameter is either the X, Y mirroring or a supported format void reset(const Resolution& r,
/// for which the correct mirroring will be configured. const PixelDim& pd,
void reset(const Resolution&, const Trafo &tr = {});
const PixelDim&,
const std::array<bool, 2>& mirror,
double gamma = 1.0);
void reset(const Resolution& r,
const PixelDim& pd,
Format o,
double gamma = 1.0);
/** /**
* Release the allocated resources. Drawing in this state ends in * Release the allocated resources. Drawing in this state ends in
* unspecified behavior. * unspecified behavior.
@ -118,6 +121,7 @@ public:
/// Get the resolution of the raster. /// Get the resolution of the raster.
Resolution resolution() const; Resolution resolution() const;
PixelDim pixel_dimensions() const;
/// Clear the raster with black color. /// Clear the raster with black color.
void clear(); void clear();
@ -126,24 +130,28 @@ public:
void draw(const ExPolygon& poly); void draw(const ExPolygon& poly);
void draw(const ClipperLib::Polygon& poly); void draw(const ClipperLib::Polygon& poly);
// Saving the raster: uint8_t read_pixel(size_t w, size_t h) const;
// It is possible to override the format given in the constructor but
// be aware that the mirroring will not be modified. inline bool empty() const { return ! bool(m_impl); }
/// Save the raster on the specified stream.
void save(std::ostream& stream, Format);
void save(std::ostream& stream);
/// Save into a continuous byte stream which is returned.
RawBytes save(Format fmt);
RawBytes save();
}; };
// This prevents the duplicate default constructor warning on MSVC2013 class PNGImage: public Raster::RawData {
template<> Raster::Raster(); public:
PNGImage& serialize(const Raster &raster) override;
std::string get_file_extension() const override { return "png"; }
};
class PPMImage: public Raster::RawData {
public:
PPMImage& serialize(const Raster &raster) override;
std::string get_file_extension() const override { return "ppm"; }
};
std::ostream& operator<<(std::ostream &stream, const Raster::RawData &bytes);
} // sla } // sla
} // Slic3r } // Slic3r
#endif // SLARASTER_HPP #endif // SLARASTER_HPP

View file

@ -10,7 +10,7 @@
namespace Slic3r { namespace sla { namespace Slic3r { namespace sla {
std::string SLARasterWriter::createIniContent(const std::string& projectname) const std::string RasterWriter::createIniContent(const std::string& projectname) const
{ {
std::string out("action = print\njobDir = "); std::string out("action = print\njobDir = ");
out += projectname + "\n"; out += projectname + "\n";
@ -21,39 +21,14 @@ std::string SLARasterWriter::createIniContent(const std::string& projectname) co
return out; return out;
} }
void SLARasterWriter::flpXY(ClipperLib::Polygon &poly) RasterWriter::RasterWriter(const Raster::Resolution &res,
{ const Raster::PixelDim & pixdim,
for(auto& p : poly.Contour) std::swap(p.X, p.Y); const Raster::Trafo & trafo,
std::reverse(poly.Contour.begin(), poly.Contour.end()); double gamma)
: m_res(res), m_pxdim(pixdim), m_trafo(trafo), m_gamma(gamma)
for(auto& h : poly.Holes) { {}
for(auto& p : h) std::swap(p.X, p.Y);
std::reverse(h.begin(), h.end());
}
}
void SLARasterWriter::flpXY(ExPolygon &poly) void RasterWriter::save(const std::string &fpath, const std::string &prjname)
{
for(auto& p : poly.contour.points) p = Point(p.y(), p.x());
std::reverse(poly.contour.points.begin(), poly.contour.points.end());
for(auto& h : poly.holes) {
for(auto& p : h.points) p = Point(p.y(), p.x());
std::reverse(h.points.begin(), h.points.end());
}
}
SLARasterWriter::SLARasterWriter(const Raster::Resolution &res,
const Raster::PixelDim &pixdim,
const std::array<bool, 2> &mirror,
double gamma)
: m_res(res), m_pxdim(pixdim), m_mirror(mirror), m_gamma(gamma)
{
// PNG raster will implicitly do an Y mirror
m_mirror[1] = !m_mirror[1];
}
void SLARasterWriter::save(const std::string &fpath, const std::string &prjname)
{ {
try { try {
Zipper zipper(fpath); // zipper with no compression Zipper zipper(fpath); // zipper with no compression
@ -103,7 +78,7 @@ std::string get_cfg_value(const DynamicPrintConfig &cfg, const std::string &key)
} // namespace } // namespace
void SLARasterWriter::set_config(const DynamicPrintConfig &cfg) void RasterWriter::set_config(const DynamicPrintConfig &cfg)
{ {
m_config["layerHeight"] = get_cfg_value(cfg, "layer_height"); m_config["layerHeight"] = get_cfg_value(cfg, "layer_height");
m_config["expTime"] = get_cfg_value(cfg, "exposure_time"); m_config["expTime"] = get_cfg_value(cfg, "exposure_time");
@ -118,7 +93,7 @@ void SLARasterWriter::set_config(const DynamicPrintConfig &cfg)
m_config["prusaSlicerVersion"] = SLIC3R_BUILD_ID; m_config["prusaSlicerVersion"] = SLIC3R_BUILD_ID;
} }
void SLARasterWriter::set_statistics(const PrintStatistics &stats) void RasterWriter::set_statistics(const PrintStatistics &stats)
{ {
m_config["usedMaterial"] = std::to_string(stats.used_material); m_config["usedMaterial"] = std::to_string(stats.used_material);
m_config["numFade"] = std::to_string(stats.num_fade); m_config["numFade"] = std::to_string(stats.num_fade);

View file

@ -15,20 +15,17 @@
namespace Slic3r { namespace sla { namespace Slic3r { namespace sla {
// Implementation for PNG raster output // API to write the zipped sla output layers and metadata.
// Implementation uses PNG raster output.
// Be aware that if a large number of layers are allocated, it can very well // Be aware that if a large number of layers are allocated, it can very well
// exhaust the available memory especially on 32 bit platform. // exhaust the available memory especially on 32 bit platform.
// This class is designed to be used in parallel mode. Layers have an ID and // This class is designed to be used in parallel mode. Layers have an ID and
// each layer can be written and compressed independently (in parallel). // each layer can be written and compressed independently (in parallel).
// At the end when all layers where written, the save method can be used to // At the end when all layers where written, the save method can be used to
// write out the result into a zipped archive. // write out the result into a zipped archive.
class SLARasterWriter class RasterWriter
{ {
public: public:
enum Orientation {
roLandscape,
roPortrait
};
// Used for addressing parameters of set_statistics() // Used for addressing parameters of set_statistics()
struct PrintStatistics struct PrintStatistics
@ -45,7 +42,7 @@ private:
// A struct to bind the raster image data and its compressed bytes together. // A struct to bind the raster image data and its compressed bytes together.
struct Layer { struct Layer {
Raster raster; Raster raster;
RawBytes rawbytes; PNGImage rawbytes;
Layer() = default; Layer() = default;
@ -61,70 +58,55 @@ private:
// parallel. Later we can write every layer to the disk sequentially. // parallel. Later we can write every layer to the disk sequentially.
std::vector<Layer> m_layers_rst; std::vector<Layer> m_layers_rst;
Raster::Resolution m_res; Raster::Resolution m_res;
Raster::PixelDim m_pxdim; Raster::PixelDim m_pxdim;
std::array<bool, 2> m_mirror; Raster::Trafo m_trafo;
double m_gamma; double m_gamma;
std::map<std::string, std::string> m_config; std::map<std::string, std::string> m_config;
std::string createIniContent(const std::string& projectname) const; std::string createIniContent(const std::string& projectname) const;
static void flpXY(ClipperLib::Polygon& poly);
static void flpXY(ExPolygon& poly);
public: public:
SLARasterWriter(const Raster::Resolution &res,
const Raster::PixelDim &pixdim, // SLARasterWriter is using Raster in custom mirroring mode
const std::array<bool, 2> &mirror, RasterWriter(const Raster::Resolution &res,
double gamma = 1.); const Raster::PixelDim & pixdim,
const Raster::Trafo & trafo,
double gamma = 1.);
SLARasterWriter(const SLARasterWriter& ) = delete; RasterWriter(const RasterWriter& ) = delete;
SLARasterWriter& operator=(const SLARasterWriter&) = delete; RasterWriter& operator=(const RasterWriter&) = delete;
SLARasterWriter(SLARasterWriter&& m) = default; RasterWriter(RasterWriter&& m) = default;
SLARasterWriter& operator=(SLARasterWriter&&) = default; RasterWriter& operator=(RasterWriter&&) = default;
inline void layers(unsigned cnt) { if(cnt > 0) m_layers_rst.resize(cnt); } inline void layers(unsigned cnt) { if(cnt > 0) m_layers_rst.resize(cnt); }
inline unsigned layers() const { return unsigned(m_layers_rst.size()); } inline unsigned layers() const { return unsigned(m_layers_rst.size()); }
template<class Poly> void draw_polygon(const Poly& p, unsigned lyr, template<class Poly> void draw_polygon(const Poly& p, unsigned lyr)
Orientation o = roPortrait)
{ {
assert(lyr < m_layers_rst.size()); assert(lyr < m_layers_rst.size());
m_layers_rst[lyr].raster.draw(p);
switch (o) {
case roPortrait: {
Poly poly(p);
flpXY(poly);
m_layers_rst[lyr].raster.draw(poly);
break;
}
case roLandscape:
m_layers_rst[lyr].raster.draw(p);
break;
}
} }
inline void begin_layer(unsigned lyr) { inline void begin_layer(unsigned lyr) {
if(m_layers_rst.size() <= lyr) m_layers_rst.resize(lyr+1); if(m_layers_rst.size() <= lyr) m_layers_rst.resize(lyr+1);
m_layers_rst[lyr].raster.reset(m_res, m_pxdim, m_mirror, m_gamma); m_layers_rst[lyr].raster.reset(m_res, m_pxdim, m_trafo);
} }
inline void begin_layer() { inline void begin_layer() {
m_layers_rst.emplace_back(); m_layers_rst.emplace_back();
m_layers_rst.front().raster.reset(m_res, m_pxdim, m_mirror, m_gamma); m_layers_rst.front().raster.reset(m_res, m_pxdim, m_trafo);
} }
inline void finish_layer(unsigned lyr_id) { inline void finish_layer(unsigned lyr_id) {
assert(lyr_id < m_layers_rst.size()); assert(lyr_id < m_layers_rst.size());
m_layers_rst[lyr_id].rawbytes = m_layers_rst[lyr_id].rawbytes.serialize(m_layers_rst[lyr_id].raster);
m_layers_rst[lyr_id].raster.save(Raster::Format::PNG);
m_layers_rst[lyr_id].raster.reset(); m_layers_rst[lyr_id].raster.reset();
} }
inline void finish_layer() { inline void finish_layer() {
if(!m_layers_rst.empty()) { if(!m_layers_rst.empty()) {
m_layers_rst.back().rawbytes = m_layers_rst.back().rawbytes.serialize(m_layers_rst.back().raster);
m_layers_rst.back().raster.save(Raster::Format::PNG);
m_layers_rst.back().raster.reset(); m_layers_rst.back().raster.reset();
} }
} }

View file

@ -39,14 +39,19 @@ public:
insert(std::make_pair(v, unsigned(idx))); insert(std::make_pair(v, unsigned(idx)));
} }
std::vector<PointIndexEl> query(std::function<bool(const PointIndexEl&)>); std::vector<PointIndexEl> query(std::function<bool(const PointIndexEl&)>) const;
std::vector<PointIndexEl> nearest(const Vec3d&, unsigned k); std::vector<PointIndexEl> nearest(const Vec3d&, unsigned k) const;
std::vector<PointIndexEl> query(const Vec3d &v, unsigned k) const // wrapper
{
return nearest(v, k);
}
// For testing // For testing
size_t size() const; size_t size() const;
bool empty() const { return size() == 0; } bool empty() const { return size() == 0; }
void foreach(std::function<void(const PointIndexEl& el)> fn); void foreach(std::function<void(const PointIndexEl& el)> fn);
void foreach(std::function<void(const PointIndexEl& el)> fn) const;
}; };
using BoxIndexEl = std::pair<Slic3r::BoundingBox, unsigned>; using BoxIndexEl = std::pair<Slic3r::BoundingBox, unsigned>;

File diff suppressed because it is too large Load diff

View file

@ -2,24 +2,14 @@
#define SLASUPPORTTREE_HPP #define SLASUPPORTTREE_HPP
#include <vector> #include <vector>
#include <array>
#include <cstdint>
#include <memory> #include <memory>
#include <Eigen/Geometry> #include <Eigen/Geometry>
#include "SLACommon.hpp" #include "SLACommon.hpp"
#include "SLAPad.hpp"
namespace Slic3r { namespace Slic3r {
// Needed types from Point.hpp
typedef int32_t coord_t;
typedef Eigen::Matrix<double, 3, 1, Eigen::DontAlign> Vec3d;
typedef Eigen::Matrix<float, 3, 1, Eigen::DontAlign> Vec3f;
typedef Eigen::Matrix<coord_t, 3, 1, Eigen::DontAlign> Vec3crd;
typedef std::vector<Vec3d> Pointf3s;
typedef std::vector<Vec3crd> Points3;
class TriangleMesh; class TriangleMesh;
class Model; class Model;
class ModelInstance; class ModelInstance;
@ -32,13 +22,17 @@ using ExPolygons = std::vector<ExPolygon>;
namespace sla { namespace sla {
enum class PillarConnectionMode { enum class PillarConnectionMode
{
zigzag, zigzag,
cross, cross,
dynamic dynamic
}; };
struct SupportConfig { struct SupportConfig
{
bool enabled = true;
// Radius in mm of the pointing side of the head. // Radius in mm of the pointing side of the head.
double head_front_radius_mm = 0.2; double head_front_radius_mm = 0.2;
@ -85,6 +79,11 @@ struct SupportConfig {
// The shortest distance between a pillar base perimeter from the model // The shortest distance between a pillar base perimeter from the model
// body. This is only useful when elevation is set to zero. // body. This is only useful when elevation is set to zero.
double pillar_base_safety_distance_mm = 0.5; double pillar_base_safety_distance_mm = 0.5;
double head_fullwidth() const {
return 2 * head_front_radius_mm + head_width_mm +
2 * head_back_radius_mm - head_penetration_mm;
}
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
// Compile time configuration values (candidates for runtime) // Compile time configuration values (candidates for runtime)
@ -104,101 +103,78 @@ struct SupportConfig {
static const unsigned max_bridges_on_pillar; static const unsigned max_bridges_on_pillar;
}; };
struct PoolConfig; enum class MeshType { Support, Pad };
/// A Control structure for the support calculation. Consists of the status /// A Control structure for the support calculation. Consists of the status
/// indicator callback and the stop condition predicate. /// indicator callback and the stop condition predicate.
struct Controller { struct JobController
{
using StatusFn = std::function<void(unsigned, const std::string&)>;
using StopCond = std::function<bool(void)>;
using CancelFn = std::function<void(void)>;
// This will signal the status of the calculation to the front-end // This will signal the status of the calculation to the front-end
std::function<void(unsigned, const std::string&)> statuscb = StatusFn statuscb = [](unsigned, const std::string&){};
[](unsigned, const std::string&){};
// Returns true if the calculation should be aborted. // Returns true if the calculation should be aborted.
std::function<bool(void)> stopcondition = [](){ return false; }; StopCond stopcondition = [](){ return false; };
// Similar to cancel callback. This should check the stop condition and // Similar to cancel callback. This should check the stop condition and
// if true, throw an appropriate exception. (TriangleMeshSlicer needs this) // if true, throw an appropriate exception. (TriangleMeshSlicer needs this)
// consider it a hard abort. stopcondition is permits the algorithm to // consider it a hard abort. stopcondition is permits the algorithm to
// terminate itself // terminate itself
std::function<void(void)> cancelfn = [](){}; CancelFn cancelfn = [](){};
}; };
using PointSet = Eigen::MatrixXd; struct SupportableMesh
{
EigenMesh3D emesh;
SupportPoints pts;
SupportConfig cfg;
//EigenMesh3D to_eigenmesh(const TriangleMesh& m); explicit SupportableMesh(const TriangleMesh & trmsh,
const SupportPoints &sp,
// needed for find best rotation const SupportConfig &c)
//EigenMesh3D to_eigenmesh(const ModelObject& model); : emesh{trmsh}, pts{sp}, cfg{c}
{}
// Simple conversion of 'vector of points' to an Eigen matrix
//PointSet to_point_set(const std::vector<sla::SupportPoint>&); explicit SupportableMesh(const EigenMesh3D &em,
const SupportPoints &sp,
const SupportConfig &c)
/* ************************************************************************** */ : emesh{em}, pts{sp}, cfg{c}
{}
};
/// The class containing mesh data for the generated supports. /// The class containing mesh data for the generated supports.
class SLASupportTree { class SupportTree
class Impl; // persistent support data {
std::unique_ptr<Impl> m_impl; JobController m_ctl;
Impl& get() { return *m_impl; }
const Impl& get() const { return *m_impl; }
friend void add_sla_supports(Model&,
const SupportConfig&,
const Controller&);
// The generation algorithm is quite long and will be captured in a separate
// class with private data, helper methods, etc... This data is only needed
// during the calculation whereas the Impl class contains the persistent
// data, mostly the meshes.
class Algorithm;
// Generate the 3D supports for a model intended for SLA print. This
// will instantiate the Algorithm class and call its appropriate methods
// with status indication.
bool generate(const std::vector<SupportPoint>& pts,
const EigenMesh3D& mesh,
const SupportConfig& cfg = {},
const Controller& ctl = {});
public: public:
using UPtr = std::unique_ptr<SupportTree>;
SLASupportTree(double ground_level = 0.0);
SLASupportTree(const std::vector<SupportPoint>& pts,
const EigenMesh3D& em,
const SupportConfig& cfg = {},
const Controller& ctl = {});
SLASupportTree(const SLASupportTree&) = delete; static UPtr create(const SupportableMesh &input,
SLASupportTree& operator=(const SLASupportTree&) = delete; const JobController &ctl = {});
~SLASupportTree(); virtual ~SupportTree() = default;
/// Get the whole mesh united into the output TriangleMesh virtual const TriangleMesh &retrieve_mesh(MeshType meshtype) const = 0;
/// WITHOUT THE PAD
const TriangleMesh& merged_mesh() const;
void merged_mesh_with_pad(TriangleMesh&) const; /// Adding the "pad" under the supports.
std::vector<ExPolygons> slice(const std::vector<float> &,
float closing_radius) const;
/// Adding the "pad" (base pool) under the supports
/// modelbase will be used according to the embed_object flag in PoolConfig. /// modelbase will be used according to the embed_object flag in PoolConfig.
/// If set, the plate will interpreted as the model's intrinsic pad. /// If set, the plate will be interpreted as the model's intrinsic pad.
/// Otherwise, the modelbase will be unified with the base plate calculated /// Otherwise, the modelbase will be unified with the base plate calculated
/// from the supports. /// from the supports.
const TriangleMesh& add_pad(const ExPolygons& modelbase, virtual const TriangleMesh &add_pad(const ExPolygons &modelbase,
const PoolConfig& pcfg) const; const PadConfig & pcfg) = 0;
/// Get the pad geometry virtual void remove_pad() = 0;
const TriangleMesh& get_pad() const;
std::vector<ExPolygons> slice(const std::vector<float> &,
void remove_pad(); float closing_radius) const;
void retrieve_full_mesh(TriangleMesh &outmesh) const;
const JobController &ctl() const { return m_ctl; }
}; };
} }

View file

@ -0,0 +1,525 @@
#include "SLASupportTreeBuilder.hpp"
#include "SLASupportTreeBuildsteps.hpp"
namespace Slic3r {
namespace sla {
Contour3D sphere(double rho, Portion portion, double fa) {
Contour3D ret;
// prohibit close to zero radius
if(rho <= 1e-6 && rho >= -1e-6) return ret;
auto& vertices = ret.points;
auto& facets = ret.indices;
// Algorithm:
// Add points one-by-one to the sphere grid and form facets using relative
// coordinates. Sphere is composed effectively of a mesh of stacked circles.
// adjust via rounding to get an even multiple for any provided angle.
double angle = (2*PI / floor(2*PI / fa));
// Ring to be scaled to generate the steps of the sphere
std::vector<double> ring;
for (double i = 0; i < 2*PI; i+=angle) ring.emplace_back(i);
const auto sbegin = size_t(2*std::get<0>(portion)/angle);
const auto send = size_t(2*std::get<1>(portion)/angle);
const size_t steps = ring.size();
const double increment = 1.0 / double(steps);
// special case: first ring connects to 0,0,0
// insert and form facets.
if(sbegin == 0)
vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*sbegin*2.0*rho));
auto id = coord_t(vertices.size());
for (size_t i = 0; i < ring.size(); i++) {
// Fixed scaling
const double z = -rho + increment*rho*2.0 * (sbegin + 1.0);
// radius of the circle for this step.
const double r = std::sqrt(std::abs(rho*rho - z*z));
Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r);
vertices.emplace_back(Vec3d(b(0), b(1), z));
if (sbegin == 0)
facets.emplace_back((i == 0) ?
Vec3crd(coord_t(ring.size()), 0, 1) :
Vec3crd(id - 1, 0, id));
++id;
}
// General case: insert and form facets for each step,
// joining it to the ring below it.
for (size_t s = sbegin + 2; s < send - 1; s++) {
const double z = -rho + increment*double(s*2.0*rho);
const double r = std::sqrt(std::abs(rho*rho - z*z));
for (size_t i = 0; i < ring.size(); i++) {
Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r);
vertices.emplace_back(Vec3d(b(0), b(1), z));
auto id_ringsize = coord_t(id - int(ring.size()));
if (i == 0) {
// wrap around
facets.emplace_back(Vec3crd(id - 1, id,
id + coord_t(ring.size() - 1)));
facets.emplace_back(Vec3crd(id - 1, id_ringsize, id));
} else {
facets.emplace_back(Vec3crd(id_ringsize - 1, id_ringsize, id));
facets.emplace_back(Vec3crd(id - 1, id_ringsize - 1, id));
}
id++;
}
}
// special case: last ring connects to 0,0,rho*2.0
// only form facets.
if(send >= size_t(2*PI / angle)) {
vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*send*2.0*rho));
for (size_t i = 0; i < ring.size(); i++) {
auto id_ringsize = coord_t(id - int(ring.size()));
if (i == 0) {
// third vertex is on the other side of the ring.
facets.emplace_back(Vec3crd(id - 1, id_ringsize, id));
} else {
auto ci = coord_t(id_ringsize + coord_t(i));
facets.emplace_back(Vec3crd(ci - 1, ci, id));
}
}
}
id++;
return ret;
}
Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d &sp)
{
Contour3D ret;
auto steps = int(ssteps);
auto& points = ret.points;
auto& indices = ret.indices;
points.reserve(2*ssteps);
double a = 2*PI/steps;
Vec3d jp = sp;
Vec3d endp = {sp(X), sp(Y), sp(Z) + h};
// Upper circle points
for(int i = 0; i < steps; ++i) {
double phi = i*a;
double ex = endp(X) + r*std::cos(phi);
double ey = endp(Y) + r*std::sin(phi);
points.emplace_back(ex, ey, endp(Z));
}
// Lower circle points
for(int i = 0; i < steps; ++i) {
double phi = i*a;
double x = jp(X) + r*std::cos(phi);
double y = jp(Y) + r*std::sin(phi);
points.emplace_back(x, y, jp(Z));
}
// Now create long triangles connecting upper and lower circles
indices.reserve(2*ssteps);
auto offs = steps;
for(int i = 0; i < steps - 1; ++i) {
indices.emplace_back(i, i + offs, offs + i + 1);
indices.emplace_back(i, offs + i + 1, i + 1);
}
// Last triangle connecting the first and last vertices
auto last = steps - 1;
indices.emplace_back(0, last, offs);
indices.emplace_back(last, offs + last, offs);
// According to the slicing algorithms, we need to aid them with generating
// a watertight body. So we create a triangle fan for the upper and lower
// ending of the cylinder to close the geometry.
points.emplace_back(jp); int ci = int(points.size() - 1);
for(int i = 0; i < steps - 1; ++i)
indices.emplace_back(i + offs + 1, i + offs, ci);
indices.emplace_back(offs, steps + offs - 1, ci);
points.emplace_back(endp); ci = int(points.size() - 1);
for(int i = 0; i < steps - 1; ++i)
indices.emplace_back(ci, i, i + 1);
indices.emplace_back(steps - 1, 0, ci);
return ret;
}
Head::Head(double r_big_mm,
double r_small_mm,
double length_mm,
double penetration,
const Vec3d &direction,
const Vec3d &offset,
const size_t circlesteps)
: steps(circlesteps)
, dir(direction)
, tr(offset)
, r_back_mm(r_big_mm)
, r_pin_mm(r_small_mm)
, width_mm(length_mm)
, penetration_mm(penetration)
{
assert(width_mm > 0.);
assert(r_back_mm > 0.);
assert(r_pin_mm > 0.);
// We create two spheres which will be connected with a robe that fits
// both circles perfectly.
// Set up the model detail level
const double detail = 2*PI/steps;
// We don't generate whole circles. Instead, we generate only the
// portions which are visible (not covered by the robe) To know the
// exact portion of the bottom and top circles we need to use some
// rules of tangent circles from which we can derive (using simple
// triangles the following relations:
// The height of the whole mesh
const double h = r_big_mm + r_small_mm + width_mm;
double phi = PI/2 - std::acos( (r_big_mm - r_small_mm) / h );
// To generate a whole circle we would pass a portion of (0, Pi)
// To generate only a half horizontal circle we can pass (0, Pi/2)
// The calculated phi is an offset to the half circles needed to smooth
// the transition from the circle to the robe geometry
auto&& s1 = sphere(r_big_mm, make_portion(0, PI/2 + phi), detail);
auto&& s2 = sphere(r_small_mm, make_portion(PI/2 + phi, PI), detail);
for(auto& p : s2.points) p.z() += h;
mesh.merge(s1);
mesh.merge(s2);
for(size_t idx1 = s1.points.size() - steps, idx2 = s1.points.size();
idx1 < s1.points.size() - 1;
idx1++, idx2++)
{
coord_t i1s1 = coord_t(idx1), i1s2 = coord_t(idx2);
coord_t i2s1 = i1s1 + 1, i2s2 = i1s2 + 1;
mesh.indices.emplace_back(i1s1, i2s1, i2s2);
mesh.indices.emplace_back(i1s1, i2s2, i1s2);
}
auto i1s1 = coord_t(s1.points.size()) - coord_t(steps);
auto i2s1 = coord_t(s1.points.size()) - 1;
auto i1s2 = coord_t(s1.points.size());
auto i2s2 = coord_t(s1.points.size()) + coord_t(steps) - 1;
mesh.indices.emplace_back(i2s2, i2s1, i1s1);
mesh.indices.emplace_back(i1s2, i2s2, i1s1);
// To simplify further processing, we translate the mesh so that the
// last vertex of the pointing sphere (the pinpoint) will be at (0,0,0)
for(auto& p : mesh.points) p.z() -= (h + r_small_mm - penetration_mm);
}
Pillar::Pillar(const Vec3d &jp, const Vec3d &endp, double radius, size_t st):
r(radius), steps(st), endpt(endp), starts_from_head(false)
{
assert(steps > 0);
height = jp(Z) - endp(Z);
if(height > EPSILON) { // Endpoint is below the starting point
// We just create a bridge geometry with the pillar parameters and
// move the data.
Contour3D body = cylinder(radius, height, st, endp);
mesh.points.swap(body.points);
mesh.indices.swap(body.indices);
}
}
Pillar &Pillar::add_base(double baseheight, double radius)
{
if(baseheight <= 0) return *this;
if(baseheight > height) baseheight = height;
assert(steps >= 0);
auto last = int(steps - 1);
if(radius < r ) radius = r;
double a = 2*PI/steps;
double z = endpt(Z) + baseheight;
for(size_t i = 0; i < steps; ++i) {
double phi = i*a;
double x = endpt(X) + r*std::cos(phi);
double y = endpt(Y) + r*std::sin(phi);
base.points.emplace_back(x, y, z);
}
for(size_t i = 0; i < steps; ++i) {
double phi = i*a;
double x = endpt(X) + radius*std::cos(phi);
double y = endpt(Y) + radius*std::sin(phi);
base.points.emplace_back(x, y, z - baseheight);
}
auto ep = endpt; ep(Z) += baseheight;
base.points.emplace_back(endpt);
base.points.emplace_back(ep);
auto& indices = base.indices;
auto hcenter = int(base.points.size() - 1);
auto lcenter = int(base.points.size() - 2);
auto offs = int(steps);
for(int i = 0; i < last; ++i) {
indices.emplace_back(i, i + offs, offs + i + 1);
indices.emplace_back(i, offs + i + 1, i + 1);
indices.emplace_back(i, i + 1, hcenter);
indices.emplace_back(lcenter, offs + i + 1, offs + i);
}
indices.emplace_back(0, last, offs);
indices.emplace_back(last, offs + last, offs);
indices.emplace_back(hcenter, last, 0);
indices.emplace_back(offs, offs + last, lcenter);
return *this;
}
Bridge::Bridge(const Vec3d &j1, const Vec3d &j2, double r_mm, size_t steps):
r(r_mm), startp(j1), endp(j2)
{
using Quaternion = Eigen::Quaternion<double>;
Vec3d dir = (j2 - j1).normalized();
double d = distance(j2, j1);
mesh = cylinder(r, d, steps);
auto quater = Quaternion::FromTwoVectors(Vec3d{0,0,1}, dir);
for(auto& p : mesh.points) p = quater * p + j1;
}
CompactBridge::CompactBridge(const Vec3d &sp,
const Vec3d &ep,
const Vec3d &n,
double r,
bool endball,
size_t steps)
{
Vec3d startp = sp + r * n;
Vec3d dir = (ep - startp).normalized();
Vec3d endp = ep - r * dir;
Bridge br(startp, endp, r, steps);
mesh.merge(br.mesh);
// now add the pins
double fa = 2*PI/steps;
auto upperball = sphere(r, Portion{PI / 2 - fa, PI}, fa);
for(auto& p : upperball.points) p += startp;
if(endball) {
auto lowerball = sphere(r, Portion{0, PI/2 + 2*fa}, fa);
for(auto& p : lowerball.points) p += endp;
mesh.merge(lowerball);
}
mesh.merge(upperball);
}
Pad::Pad(const TriangleMesh &support_mesh,
const ExPolygons & model_contours,
double ground_level,
const PadConfig & pcfg,
ThrowOnCancel thr)
: cfg(pcfg)
, zlevel(ground_level + pcfg.full_height() - pcfg.required_elevation())
{
thr();
ExPolygons sup_contours;
float zstart = float(zlevel);
float zend = zstart + float(pcfg.full_height() + EPSILON);
pad_blueprint(support_mesh, sup_contours, grid(zstart, zend, 0.1f), thr);
create_pad(sup_contours, model_contours, tmesh, pcfg);
tmesh.translate(0, 0, float(zlevel));
if (!tmesh.empty()) tmesh.require_shared_vertices();
}
const TriangleMesh &SupportTreeBuilder::add_pad(const ExPolygons &modelbase,
const PadConfig & cfg)
{
m_pad = Pad{merged_mesh(), modelbase, ground_level, cfg, ctl().cancelfn};
return m_pad.tmesh;
}
SupportTreeBuilder::SupportTreeBuilder(SupportTreeBuilder &&o)
: m_heads(std::move(o.m_heads))
, m_head_indices{std::move(o.m_head_indices)}
, m_pillars{std::move(o.m_pillars)}
, m_bridges{std::move(o.m_bridges)}
, m_crossbridges{std::move(o.m_crossbridges)}
, m_compact_bridges{std::move(o.m_compact_bridges)}
, m_pad{std::move(o.m_pad)}
, m_meshcache{std::move(o.m_meshcache)}
, m_meshcache_valid{o.m_meshcache_valid}
, m_model_height{o.m_model_height}
, ground_level{o.ground_level}
{}
SupportTreeBuilder::SupportTreeBuilder(const SupportTreeBuilder &o)
: m_heads(o.m_heads)
, m_head_indices{o.m_head_indices}
, m_pillars{o.m_pillars}
, m_bridges{o.m_bridges}
, m_crossbridges{o.m_crossbridges}
, m_compact_bridges{o.m_compact_bridges}
, m_pad{o.m_pad}
, m_meshcache{o.m_meshcache}
, m_meshcache_valid{o.m_meshcache_valid}
, m_model_height{o.m_model_height}
, ground_level{o.ground_level}
{}
SupportTreeBuilder &SupportTreeBuilder::operator=(SupportTreeBuilder &&o)
{
m_heads = std::move(o.m_heads);
m_head_indices = std::move(o.m_head_indices);
m_pillars = std::move(o.m_pillars);
m_bridges = std::move(o.m_bridges);
m_crossbridges = std::move(o.m_crossbridges);
m_compact_bridges = std::move(o.m_compact_bridges);
m_pad = std::move(o.m_pad);
m_meshcache = std::move(o.m_meshcache);
m_meshcache_valid = o.m_meshcache_valid;
m_model_height = o.m_model_height;
ground_level = o.ground_level;
return *this;
}
SupportTreeBuilder &SupportTreeBuilder::operator=(const SupportTreeBuilder &o)
{
m_heads = o.m_heads;
m_head_indices = o.m_head_indices;
m_pillars = o.m_pillars;
m_bridges = o.m_bridges;
m_crossbridges = o.m_crossbridges;
m_compact_bridges = o.m_compact_bridges;
m_pad = o.m_pad;
m_meshcache = o.m_meshcache;
m_meshcache_valid = o.m_meshcache_valid;
m_model_height = o.m_model_height;
ground_level = o.ground_level;
return *this;
}
const TriangleMesh &SupportTreeBuilder::merged_mesh() const
{
if (m_meshcache_valid) return m_meshcache;
Contour3D merged;
for (auto &head : m_heads) {
if (ctl().stopcondition()) break;
if (head.is_valid()) merged.merge(head.mesh);
}
for (auto &stick : m_pillars) {
if (ctl().stopcondition()) break;
merged.merge(stick.mesh);
merged.merge(stick.base);
}
for (auto &j : m_junctions) {
if (ctl().stopcondition()) break;
merged.merge(j.mesh);
}
for (auto &cb : m_compact_bridges) {
if (ctl().stopcondition()) break;
merged.merge(cb.mesh);
}
for (auto &bs : m_bridges) {
if (ctl().stopcondition()) break;
merged.merge(bs.mesh);
}
for (auto &bs : m_crossbridges) {
if (ctl().stopcondition()) break;
merged.merge(bs.mesh);
}
if (ctl().stopcondition()) {
// In case of failure we have to return an empty mesh
m_meshcache = TriangleMesh();
return m_meshcache;
}
m_meshcache = mesh(merged);
// The mesh will be passed by const-pointer to TriangleMeshSlicer,
// which will need this.
if (!m_meshcache.empty()) m_meshcache.require_shared_vertices();
BoundingBoxf3 &&bb = m_meshcache.bounding_box();
m_model_height = bb.max(Z) - bb.min(Z);
m_meshcache_valid = true;
return m_meshcache;
}
double SupportTreeBuilder::full_height() const
{
if (merged_mesh().empty() && !pad().empty())
return pad().cfg.full_height();
double h = mesh_height();
if (!pad().empty()) h += pad().cfg.required_elevation();
return h;
}
const TriangleMesh &SupportTreeBuilder::merge_and_cleanup()
{
// in case the mesh is not generated, it should be...
auto &ret = merged_mesh();
// Doing clear() does not garantee to release the memory.
m_heads = {};
m_head_indices = {};
m_pillars = {};
m_junctions = {};
m_bridges = {};
m_compact_bridges = {};
return ret;
}
const TriangleMesh &SupportTreeBuilder::retrieve_mesh(MeshType meshtype) const
{
switch(meshtype) {
case MeshType::Support: return merged_mesh();
case MeshType::Pad: return pad().tmesh;
}
return m_meshcache;
}
bool SupportTreeBuilder::build(const SupportableMesh &sm)
{
ground_level = sm.emesh.ground_level() - sm.cfg.object_elevation_mm;
return SupportTreeBuildsteps::execute(*this, sm);
}
}
}

View file

@ -0,0 +1,496 @@
#ifndef SUPPORTTREEBUILDER_HPP
#define SUPPORTTREEBUILDER_HPP
#include "SLAConcurrency.hpp"
#include "SLABoilerPlate.hpp"
#include "SLASupportTree.hpp"
#include "SLAPad.hpp"
#include <libslic3r/MTUtils.hpp>
namespace Slic3r {
namespace sla {
/**
* Terminology:
*
* Support point:
* The point on the model surface that needs support.
*
* Pillar:
* A thick column that spans from a support point to the ground and has
* a thick cone shaped base where it touches the ground.
*
* Ground facing support point:
* A support point that can be directly connected with the ground with a pillar
* that does not collide or cut through the model.
*
* Non ground facing support point:
* A support point that cannot be directly connected with the ground (only with
* the model surface).
*
* Head:
* The pinhead that connects to the model surface with the sharp end end
* to a pillar or bridge stick with the dull end.
*
* Headless support point:
* A support point on the model surface for which there is not enough place for
* the head. It is either in a hole or there is some barrier that would collide
* with the head geometry. The headless support point can be ground facing and
* non ground facing as well.
*
* Bridge:
* A stick that connects two pillars or a head with a pillar.
*
* Junction:
* A small ball in the intersection of two or more sticks (pillar, bridge, ...)
*
* CompactBridge:
* A bridge that connects a headless support point with the model surface or a
* nearby pillar.
*/
using Coordf = double;
using Portion = std::tuple<double, double>;
inline Portion make_portion(double a, double b) {
return std::make_tuple(a, b);
}
template<class Vec> double distance(const Vec& p) {
return std::sqrt(p.transpose() * p);
}
template<class Vec> double distance(const Vec& pp1, const Vec& pp2) {
auto p = pp2 - pp1;
return distance(p);
}
Contour3D sphere(double rho, Portion portion = make_portion(0.0, 2.0*PI),
double fa=(2*PI/360));
// Down facing cylinder in Z direction with arguments:
// r: radius
// h: Height
// ssteps: how many edges will create the base circle
// sp: starting point
Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d &sp = {0,0,0});
const constexpr long ID_UNSET = -1;
struct Head {
Contour3D mesh;
size_t steps = 45;
Vec3d dir = {0, 0, -1};
Vec3d tr = {0, 0, 0};
double r_back_mm = 1;
double r_pin_mm = 0.5;
double width_mm = 2;
double penetration_mm = 0.5;
// For identification purposes. This will be used as the index into the
// container holding the head structures. See SLASupportTree::Impl
long id = ID_UNSET;
// If there is a pillar connecting to this head, then the id will be set.
long pillar_id = ID_UNSET;
long bridge_id = ID_UNSET;
inline void invalidate() { id = ID_UNSET; }
inline bool is_valid() const { return id >= 0; }
Head(double r_big_mm,
double r_small_mm,
double length_mm,
double penetration,
const Vec3d &direction = {0, 0, -1}, // direction (normal to the dull end)
const Vec3d &offset = {0, 0, 0}, // displacement
const size_t circlesteps = 45);
void transform()
{
using Quaternion = Eigen::Quaternion<double>;
// We rotate the head to the specified direction The head's pointing
// side is facing upwards so this means that it would hold a support
// point with a normal pointing straight down. This is the reason of
// the -1 z coordinate
auto quatern = Quaternion::FromTwoVectors(Vec3d{0, 0, -1}, dir);
for(auto& p : mesh.points) p = quatern * p + tr;
}
inline double fullwidth() const
{
return 2 * r_pin_mm + width_mm + 2*r_back_mm - penetration_mm;
}
inline Vec3d junction_point() const
{
return tr + ( 2 * r_pin_mm + width_mm + r_back_mm - penetration_mm)*dir;
}
inline double request_pillar_radius(double radius) const
{
const double rmax = r_back_mm;
return radius > 0 && radius < rmax ? radius : rmax;
}
};
struct Junction {
Contour3D mesh;
double r = 1;
size_t steps = 45;
Vec3d pos;
long id = ID_UNSET;
Junction(const Vec3d& tr, double r_mm, size_t stepnum = 45):
r(r_mm), steps(stepnum), pos(tr)
{
mesh = sphere(r_mm, make_portion(0, PI), 2*PI/steps);
for(auto& p : mesh.points) p += tr;
}
};
struct Pillar {
Contour3D mesh;
Contour3D base;
double r = 1;
size_t steps = 0;
Vec3d endpt;
double height = 0;
long id = ID_UNSET;
// If the pillar connects to a head, this is the id of that head
bool starts_from_head = true; // Could start from a junction as well
long start_junction_id = ID_UNSET;
// How many bridges are connected to this pillar
unsigned bridges = 0;
// How many pillars are cascaded with this one
unsigned links = 0;
Pillar(const Vec3d& jp, const Vec3d& endp,
double radius = 1, size_t st = 45);
Pillar(const Junction &junc, const Vec3d &endp)
: Pillar(junc.pos, endp, junc.r, junc.steps)
{}
Pillar(const Head &head, const Vec3d &endp, double radius = 1)
: Pillar(head.junction_point(), endp,
head.request_pillar_radius(radius), head.steps)
{}
inline Vec3d startpoint() const
{
return {endpt(X), endpt(Y), endpt(Z) + height};
}
inline const Vec3d& endpoint() const { return endpt; }
Pillar& add_base(double baseheight = 3, double radius = 2);
};
// A Bridge between two pillars (with junction endpoints)
struct Bridge {
Contour3D mesh;
double r = 0.8;
long id = ID_UNSET;
Vec3d startp = Vec3d::Zero(), endp = Vec3d::Zero();
Bridge(const Vec3d &j1,
const Vec3d &j2,
double r_mm = 0.8,
size_t steps = 45);
};
// A bridge that spans from model surface to model surface with small connecting
// edges on the endpoints. Used for headless support points.
struct CompactBridge {
Contour3D mesh;
long id = ID_UNSET;
CompactBridge(const Vec3d& sp,
const Vec3d& ep,
const Vec3d& n,
double r,
bool endball = true,
size_t steps = 45);
};
// A wrapper struct around the pad
struct Pad {
TriangleMesh tmesh;
PadConfig cfg;
double zlevel = 0;
Pad() = default;
Pad(const TriangleMesh &support_mesh,
const ExPolygons & model_contours,
double ground_level,
const PadConfig & pcfg,
ThrowOnCancel thr);
bool empty() const { return tmesh.facets_count() == 0; }
};
// This class will hold the support tree meshes with some additional
// bookkeeping as well. Various parts of the support geometry are stored
// separately and are merged when the caller queries the merged mesh. The
// merged result is cached for fast subsequent delivery of the merged mesh
// which can be quite complex. The support tree creation algorithm can use an
// instance of this class as a somewhat higher level tool for crafting the 3D
// support mesh. Parts can be added with the appropriate methods such as
// add_head or add_pillar which forwards the constructor arguments and fills
// the IDs of these substructures. The IDs are basically indices into the
// arrays of the appropriate type (heads, pillars, etc...). One can later query
// e.g. a pillar for a specific head...
//
// The support pad is considered an auxiliary geometry and is not part of the
// merged mesh. It can be retrieved using a dedicated method (pad())
class SupportTreeBuilder: public SupportTree {
// For heads it is beneficial to use the same IDs as for the support points.
std::vector<Head> m_heads;
std::vector<size_t> m_head_indices;
std::vector<Pillar> m_pillars;
std::vector<Junction> m_junctions;
std::vector<Bridge> m_bridges;
std::vector<Bridge> m_crossbridges;
std::vector<CompactBridge> m_compact_bridges;
Pad m_pad;
using Mutex = ccr::SpinningMutex;
mutable TriangleMesh m_meshcache;
mutable Mutex m_mutex;
mutable bool m_meshcache_valid = false;
mutable double m_model_height = 0; // the full height of the model
template<class...Args>
const Bridge& _add_bridge(std::vector<Bridge> &br, Args&&... args)
{
std::lock_guard<Mutex> lk(m_mutex);
br.emplace_back(std::forward<Args>(args)...);
br.back().id = long(br.size() - 1);
m_meshcache_valid = false;
return br.back();
}
public:
double ground_level = 0;
SupportTreeBuilder() = default;
SupportTreeBuilder(SupportTreeBuilder &&o);
SupportTreeBuilder(const SupportTreeBuilder &o);
SupportTreeBuilder& operator=(SupportTreeBuilder &&o);
SupportTreeBuilder& operator=(const SupportTreeBuilder &o);
template<class...Args> Head& add_head(unsigned id, Args&&... args)
{
std::lock_guard<Mutex> lk(m_mutex);
m_heads.emplace_back(std::forward<Args>(args)...);
m_heads.back().id = id;
if (id >= m_head_indices.size()) m_head_indices.resize(id + 1);
m_head_indices[id] = m_heads.size() - 1;
m_meshcache_valid = false;
return m_heads.back();
}
template<class...Args> long add_pillar(long headid, Args&&... args)
{
std::lock_guard<Mutex> lk(m_mutex);
if (m_pillars.capacity() < m_heads.size())
m_pillars.reserve(m_heads.size() * 10);
assert(headid >= 0 && size_t(headid) < m_head_indices.size());
Head &head = m_heads[m_head_indices[size_t(headid)]];
m_pillars.emplace_back(head, std::forward<Args>(args)...);
Pillar& pillar = m_pillars.back();
pillar.id = long(m_pillars.size() - 1);
head.pillar_id = pillar.id;
pillar.start_junction_id = head.id;
pillar.starts_from_head = true;
m_meshcache_valid = false;
return pillar.id;
}
void add_pillar_base(long pid, double baseheight = 3, double radius = 2)
{
std::lock_guard<Mutex> lk(m_mutex);
assert(pid >= 0 && size_t(pid) < m_pillars.size());
m_pillars[size_t(pid)].add_base(baseheight, radius);
}
void increment_bridges(const Pillar& pillar)
{
std::lock_guard<Mutex> lk(m_mutex);
assert(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size());
if(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size())
m_pillars[size_t(pillar.id)].bridges++;
}
void increment_links(const Pillar& pillar)
{
std::lock_guard<Mutex> lk(m_mutex);
assert(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size());
if(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size())
m_pillars[size_t(pillar.id)].links++;
}
unsigned bridgecount(const Pillar &pillar) const {
std::lock_guard<Mutex> lk(m_mutex);
assert(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size());
return pillar.bridges;
}
template<class...Args> long add_pillar(Args&&...args)
{
std::lock_guard<Mutex> lk(m_mutex);
if (m_pillars.capacity() < m_heads.size())
m_pillars.reserve(m_heads.size() * 10);
m_pillars.emplace_back(std::forward<Args>(args)...);
Pillar& pillar = m_pillars.back();
pillar.id = long(m_pillars.size() - 1);
pillar.starts_from_head = false;
m_meshcache_valid = false;
return pillar.id;
}
const Pillar& head_pillar(unsigned headid) const
{
std::lock_guard<Mutex> lk(m_mutex);
assert(headid < m_head_indices.size());
const Head& h = m_heads[m_head_indices[headid]];
assert(h.pillar_id >= 0 && h.pillar_id < long(m_pillars.size()));
return m_pillars[size_t(h.pillar_id)];
}
template<class...Args> const Junction& add_junction(Args&&... args)
{
std::lock_guard<Mutex> lk(m_mutex);
m_junctions.emplace_back(std::forward<Args>(args)...);
m_junctions.back().id = long(m_junctions.size() - 1);
m_meshcache_valid = false;
return m_junctions.back();
}
const Bridge& add_bridge(const Vec3d &s, const Vec3d &e, double r, size_t n = 45)
{
return _add_bridge(m_bridges, s, e, r, n);
}
const Bridge& add_bridge(long headid, const Vec3d &endp, size_t s = 45)
{
std::lock_guard<Mutex> lk(m_mutex);
assert(headid >= 0 && size_t(headid) < m_head_indices.size());
Head &h = m_heads[m_head_indices[size_t(headid)]];
m_bridges.emplace_back(h.junction_point(), endp, h.r_back_mm, s);
m_bridges.back().id = long(m_bridges.size() - 1);
h.bridge_id = m_bridges.back().id;
m_meshcache_valid = false;
return m_bridges.back();
}
template<class...Args> const Bridge& add_crossbridge(Args&&... args)
{
return _add_bridge(m_crossbridges, std::forward<Args>(args)...);
}
template<class...Args> const CompactBridge& add_compact_bridge(Args&&...args)
{
std::lock_guard<Mutex> lk(m_mutex);
m_compact_bridges.emplace_back(std::forward<Args>(args)...);
m_compact_bridges.back().id = long(m_compact_bridges.size() - 1);
m_meshcache_valid = false;
return m_compact_bridges.back();
}
Head &head(unsigned id)
{
std::lock_guard<Mutex> lk(m_mutex);
assert(id < m_head_indices.size());
m_meshcache_valid = false;
return m_heads[m_head_indices[id]];
}
inline size_t pillarcount() const {
std::lock_guard<Mutex> lk(m_mutex);
return m_pillars.size();
}
inline const std::vector<Pillar> &pillars() const { return m_pillars; }
inline const std::vector<Head> &heads() const { return m_heads; }
inline const std::vector<Bridge> &bridges() const { return m_bridges; }
inline const std::vector<Bridge> &crossbridges() const { return m_crossbridges; }
template<class T> inline IntegerOnly<T, const Pillar&> pillar(T id) const
{
std::lock_guard<Mutex> lk(m_mutex);
assert(id >= 0 && size_t(id) < m_pillars.size() &&
size_t(id) < std::numeric_limits<size_t>::max());
return m_pillars[size_t(id)];
}
template<class T> inline IntegerOnly<T, Pillar&> pillar(T id)
{
std::lock_guard<Mutex> lk(m_mutex);
assert(id >= 0 && size_t(id) < m_pillars.size() &&
size_t(id) < std::numeric_limits<size_t>::max());
return m_pillars[size_t(id)];
}
const Pad& pad() const { return m_pad; }
// WITHOUT THE PAD!!!
const TriangleMesh &merged_mesh() const;
// WITH THE PAD
double full_height() const;
// WITHOUT THE PAD!!!
inline double mesh_height() const
{
if (!m_meshcache_valid) merged_mesh();
return m_model_height;
}
// Intended to be called after the generation is fully complete
const TriangleMesh & merge_and_cleanup();
// Implement SupportTree interface:
const TriangleMesh &add_pad(const ExPolygons &modelbase,
const PadConfig & pcfg) override;
void remove_pad() override { m_pad = Pad(); }
virtual const TriangleMesh &retrieve_mesh(
MeshType meshtype = MeshType::Support) const override;
bool build(const SupportableMesh &supportable_mesh);
};
}} // namespace Slic3r::sla
#endif // SUPPORTTREEBUILDER_HPP

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,289 @@
#ifndef SLASUPPORTTREEALGORITHM_H
#define SLASUPPORTTREEALGORITHM_H
#include <cstdint>
#include "SLASupportTreeBuilder.hpp"
namespace Slic3r {
namespace sla {
// The minimum distance for two support points to remain valid.
const double /*constexpr*/ D_SP = 0.1;
enum { // For indexing Eigen vectors as v(X), v(Y), v(Z) instead of numbers
X, Y, Z
};
inline Vec2d to_vec2(const Vec3d& v3) {
return {v3(X), v3(Y)};
}
// This function returns the position of the centroid in the input 'clust'
// vector of point indices.
template<class DistFn>
long cluster_centroid(const ClusterEl& clust,
const std::function<Vec3d(size_t)> &pointfn,
DistFn df)
{
switch(clust.size()) {
case 0: /* empty cluster */ return ID_UNSET;
case 1: /* only one element */ return 0;
case 2: /* if two elements, there is no center */ return 0;
default: ;
}
// The function works by calculating for each point the average distance
// from all the other points in the cluster. We create a selector bitmask of
// the same size as the cluster. The bitmask will have two true bits and
// false bits for the rest of items and we will loop through all the
// permutations of the bitmask (combinations of two points). Get the
// distance for the two points and add the distance to the averages.
// The point with the smallest average than wins.
// The complexity should be O(n^2) but we will mostly apply this function
// for small clusters only (cca 3 elements)
std::vector<bool> sel(clust.size(), false); // create full zero bitmask
std::fill(sel.end() - 2, sel.end(), true); // insert the two ones
std::vector<double> avgs(clust.size(), 0.0); // store the average distances
do {
std::array<size_t, 2> idx;
for(size_t i = 0, j = 0; i < clust.size(); i++) if(sel[i]) idx[j++] = i;
double d = df(pointfn(clust[idx[0]]),
pointfn(clust[idx[1]]));
// add the distance to the sums for both associated points
for(auto i : idx) avgs[i] += d;
// now continue with the next permutation of the bitmask with two 1s
} while(std::next_permutation(sel.begin(), sel.end()));
// Divide by point size in the cluster to get the average (may be redundant)
for(auto& a : avgs) a /= clust.size();
// get the lowest average distance and return the index
auto minit = std::min_element(avgs.begin(), avgs.end());
return long(minit - avgs.begin());
}
inline Vec3d dirv(const Vec3d& startp, const Vec3d& endp) {
return (endp - startp).normalized();
}
class PillarIndex {
PointIndex m_index;
using Mutex = ccr::BlockingMutex;
mutable Mutex m_mutex;
public:
template<class...Args> inline void guarded_insert(Args&&...args)
{
std::lock_guard<Mutex> lck(m_mutex);
m_index.insert(std::forward<Args>(args)...);
}
template<class...Args>
inline std::vector<PointIndexEl> guarded_query(Args&&...args) const
{
std::lock_guard<Mutex> lck(m_mutex);
return m_index.query(std::forward<Args>(args)...);
}
template<class...Args> inline void insert(Args&&...args)
{
m_index.insert(std::forward<Args>(args)...);
}
template<class...Args>
inline std::vector<PointIndexEl> query(Args&&...args) const
{
return m_index.query(std::forward<Args>(args)...);
}
template<class Fn> inline void foreach(Fn fn) { m_index.foreach(fn); }
template<class Fn> inline void guarded_foreach(Fn fn)
{
std::lock_guard<Mutex> lck(m_mutex);
m_index.foreach(fn);
}
PointIndex guarded_clone()
{
std::lock_guard<Mutex> lck(m_mutex);
return m_index;
}
};
// Helper function for pillar interconnection where pairs of already connected
// pillars should be checked for not to be processed again. This can be done
// in constant time with a set of hash values uniquely representing a pair of
// integers. The order of numbers within the pair should not matter, it has
// the same unique hash. The hash value has to have twice as many bits as the
// arguments need. If the same integral type is used for args and return val,
// make sure the arguments use only the half of the type's bit depth.
template<class I, class DoubleI = IntegerOnly<I>>
IntegerOnly<DoubleI> pairhash(I a, I b)
{
using std::ceil; using std::log2; using std::max; using std::min;
static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT);
static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT);
static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits;
I g = min(a, b), l = max(a, b);
// Assume the hash will fit into the output variable
assert((g ? (ceil(log2(g))) : 0) <= shift);
assert((l ? (ceil(log2(l))) : 0) <= shift);
return (DoubleI(g) << shift) + l;
}
class SupportTreeBuildsteps {
const SupportConfig& m_cfg;
const EigenMesh3D& m_mesh;
const std::vector<SupportPoint>& m_support_pts;
using PtIndices = std::vector<unsigned>;
PtIndices m_iheads; // support points with pinhead
PtIndices m_iheadless; // headless support points
// supp. pts. connecting to model: point index and the ray hit data
std::vector<std::pair<unsigned, EigenMesh3D::hit_result>> m_iheads_onmodel;
// normals for support points from model faces.
PointSet m_support_nmls;
// Clusters of points which can reach the ground directly and can be
// bridged to one central pillar
std::vector<PtIndices> m_pillar_clusters;
// This algorithm uses the SupportTreeBuilder class to fill gradually
// the support elements (heads, pillars, bridges, ...)
SupportTreeBuilder& m_builder;
// support points in Eigen/IGL format
PointSet m_points;
// throw if canceled: It will be called many times so a shorthand will
// come in handy.
ThrowOnCancel m_thr;
// A spatial index to easily find strong pillars to connect to.
PillarIndex m_pillar_index;
// When bridging heads to pillars... TODO: find a cleaner solution
ccr::BlockingMutex m_bridge_mutex;
inline double ray_mesh_intersect(const Vec3d& s,
const Vec3d& dir)
{
return m_mesh.query_ray_hit(s, dir).distance();
}
// This function will test if a future pinhead would not collide with the
// model geometry. It does not take a 'Head' object because those are
// created after this test. Parameters: s: The touching point on the model
// surface. dir: This is the direction of the head from the pin to the back
// r_pin, r_back: the radiuses of the pin and the back sphere width: This
// is the full width from the pin center to the back center m: The object
// mesh.
// The return value is the hit result from the ray casting. If the starting
// point was inside the model, an "invalid" hit_result will be returned
// with a zero distance value instead of a NAN. This way the result can
// be used safely for comparison with other distances.
EigenMesh3D::hit_result pinhead_mesh_intersect(
const Vec3d& s,
const Vec3d& dir,
double r_pin,
double r_back,
double width);
// Checking bridge (pillar and stick as well) intersection with the model.
// If the function is used for headless sticks, the ins_check parameter
// have to be true as the beginning of the stick might be inside the model
// geometry.
// The return value is the hit result from the ray casting. If the starting
// point was inside the model, an "invalid" hit_result will be returned
// with a zero distance value instead of a NAN. This way the result can
// be used safely for comparison with other distances.
EigenMesh3D::hit_result bridge_mesh_intersect(
const Vec3d& s,
const Vec3d& dir,
double r,
bool ins_check = false);
// Helper function for interconnecting two pillars with zig-zag bridges.
bool interconnect(const Pillar& pillar, const Pillar& nextpillar);
// For connecting a head to a nearby pillar.
bool connect_to_nearpillar(const Head& head, long nearpillar_id);
bool search_pillar_and_connect(const Head& head);
// This is a proxy function for pillar creation which will mind the gap
// between the pad and the model bottom in zero elevation mode.
void create_ground_pillar(const Vec3d &jp,
const Vec3d &sourcedir,
double radius,
long head_id = ID_UNSET);
public:
SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm);
// Now let's define the individual steps of the support generation algorithm
// Filtering step: here we will discard inappropriate support points
// and decide the future of the appropriate ones. We will check if a
// pinhead is applicable and adjust its angle at each support point. We
// will also merge the support points that are just too close and can
// be considered as one.
void filter();
// Pinhead creation: based on the filtering results, the Head objects
// will be constructed (together with their triangle meshes).
void add_pinheads();
// Further classification of the support points with pinheads. If the
// ground is directly reachable through a vertical line parallel to the
// Z axis we consider a support point as pillar candidate. If touches
// the model geometry, it will be marked as non-ground facing and
// further steps will process it. Also, the pillars will be grouped
// into clusters that can be interconnected with bridges. Elements of
// these groups may or may not be interconnected. Here we only run the
// clustering algorithm.
void classify();
// Step: Routing the ground connected pinheads, and interconnecting
// them with additional (angled) bridges. Not all of these pinheads
// will be a full pillar (ground connected). Some will connect to a
// nearby pillar using a bridge. The max number of such side-heads for
// a central pillar is limited to avoid bad weight distribution.
void routing_to_ground();
// Step: routing the pinheads that would connect to the model surface
// along the Z axis downwards. For now these will actually be connected with
// the model surface with a flipped pinhead. In the future here we could use
// some smart algorithms to search for a safe path to the ground or to a
// nearby pillar that can hold the supported weight.
void routing_to_model();
void interconnect_pillars();
// Step: process the support points where there is not enough space for a
// full pinhead. In this case we will use a rounded sphere as a touching
// point and use a thinner bridge (let's call it a stick).
void routing_headless ();
inline void merge_result() { m_builder.merged_mesh(); }
static bool execute(SupportTreeBuilder & builder, const SupportableMesh &sm);
};
}
}
#endif // SLASUPPORTTREEALGORITHM_H

View file

@ -77,7 +77,7 @@ bool PointIndex::remove(const PointIndexEl& el)
} }
std::vector<PointIndexEl> std::vector<PointIndexEl>
PointIndex::query(std::function<bool(const PointIndexEl &)> fn) PointIndex::query(std::function<bool(const PointIndexEl &)> fn) const
{ {
namespace bgi = boost::geometry::index; namespace bgi = boost::geometry::index;
@ -86,7 +86,7 @@ PointIndex::query(std::function<bool(const PointIndexEl &)> fn)
return ret; return ret;
} }
std::vector<PointIndexEl> PointIndex::nearest(const Vec3d &el, unsigned k = 1) std::vector<PointIndexEl> PointIndex::nearest(const Vec3d &el, unsigned k = 1) const
{ {
namespace bgi = boost::geometry::index; namespace bgi = boost::geometry::index;
std::vector<PointIndexEl> ret; ret.reserve(k); std::vector<PointIndexEl> ret; ret.reserve(k);
@ -104,6 +104,11 @@ void PointIndex::foreach(std::function<void (const PointIndexEl &)> fn)
for(auto& el : m_impl->m_store) fn(el); for(auto& el : m_impl->m_store) fn(el);
} }
void PointIndex::foreach(std::function<void (const PointIndexEl &)> fn) const
{
for(const auto &el : m_impl->m_store) fn(el);
}
/* ************************************************************************** /* **************************************************************************
* BoxIndex implementation * BoxIndex implementation
* ************************************************************************** */ * ************************************************************************** */
@ -274,6 +279,8 @@ double EigenMesh3D::squared_distance(const Vec3d &p, int& i, Vec3d& c) const {
* Misc functions * Misc functions
* ****************************************************************************/ * ****************************************************************************/
namespace {
bool point_on_edge(const Vec3d& p, const Vec3d& e1, const Vec3d& e2, bool point_on_edge(const Vec3d& p, const Vec3d& e1, const Vec3d& e2,
double eps = 0.05) double eps = 0.05)
{ {
@ -289,11 +296,13 @@ template<class Vec> double distance(const Vec& pp1, const Vec& pp2) {
return std::sqrt(p.transpose() * p); return std::sqrt(p.transpose() * p);
} }
}
PointSet normals(const PointSet& points, PointSet normals(const PointSet& points,
const EigenMesh3D& mesh, const EigenMesh3D& mesh,
double eps, double eps,
std::function<void()> thr, // throw on cancel std::function<void()> thr, // throw on cancel
const std::vector<unsigned>& pt_indices = {}) const std::vector<unsigned>& pt_indices)
{ {
if(points.rows() == 0 || mesh.V().rows() == 0 || mesh.F().rows() == 0) if(points.rows() == 0 || mesh.V().rows() == 0 || mesh.F().rows() == 0)
return {}; return {};
@ -419,9 +428,17 @@ PointSet normals(const PointSet& points,
return ret; return ret;
} }
namespace bgi = boost::geometry::index; namespace bgi = boost::geometry::index;
using Index3D = bgi::rtree< PointIndexEl, bgi::rstar<16, 4> /* ? */ >; using Index3D = bgi::rtree< PointIndexEl, bgi::rstar<16, 4> /* ? */ >;
namespace {
bool cmp_ptidx_elements(const PointIndexEl& e1, const PointIndexEl& e2)
{
return e1.second < e2.second;
};
ClusteredPoints cluster(Index3D &sindex, ClusteredPoints cluster(Index3D &sindex,
unsigned max_points, unsigned max_points,
std::function<std::vector<PointIndexEl>( std::function<std::vector<PointIndexEl>(
@ -433,25 +450,22 @@ ClusteredPoints cluster(Index3D &sindex,
// each other // each other
std::function<void(Elems&, Elems&)> group = std::function<void(Elems&, Elems&)> group =
[&sindex, &group, max_points, qfn](Elems& pts, Elems& cluster) [&sindex, &group, max_points, qfn](Elems& pts, Elems& cluster)
{ {
for(auto& p : pts) { for(auto& p : pts) {
std::vector<PointIndexEl> tmp = qfn(sindex, p); std::vector<PointIndexEl> tmp = qfn(sindex, p);
auto cmp = [](const PointIndexEl& e1, const PointIndexEl& e2){
return e1.second < e2.second; std::sort(tmp.begin(), tmp.end(), cmp_ptidx_elements);
};
std::sort(tmp.begin(), tmp.end(), cmp);
Elems newpts; Elems newpts;
std::set_difference(tmp.begin(), tmp.end(), std::set_difference(tmp.begin(), tmp.end(),
cluster.begin(), cluster.end(), cluster.begin(), cluster.end(),
std::back_inserter(newpts), cmp); std::back_inserter(newpts), cmp_ptidx_elements);
int c = max_points && newpts.size() + cluster.size() > max_points? int c = max_points && newpts.size() + cluster.size() > max_points?
int(max_points - cluster.size()) : int(newpts.size()); int(max_points - cluster.size()) : int(newpts.size());
cluster.insert(cluster.end(), newpts.begin(), newpts.begin() + c); cluster.insert(cluster.end(), newpts.begin(), newpts.begin() + c);
std::sort(cluster.begin(), cluster.end(), cmp); std::sort(cluster.begin(), cluster.end(), cmp_ptidx_elements);
if(!newpts.empty() && (!max_points || cluster.size() < max_points)) if(!newpts.empty() && (!max_points || cluster.size() < max_points))
group(newpts, cluster); group(newpts, cluster);
@ -479,7 +493,6 @@ ClusteredPoints cluster(Index3D &sindex,
return result; return result;
} }
namespace {
std::vector<PointIndexEl> distance_queryfn(const Index3D& sindex, std::vector<PointIndexEl> distance_queryfn(const Index3D& sindex,
const PointIndexEl& p, const PointIndexEl& p,
double dist, double dist,
@ -496,7 +509,8 @@ std::vector<PointIndexEl> distance_queryfn(const Index3D& sindex,
return tmp; return tmp;
} }
}
} // namespace
// Clustering a set of points by the given criteria // Clustering a set of points by the given criteria
ClusteredPoints cluster( ClusteredPoints cluster(
@ -558,5 +572,5 @@ ClusteredPoints cluster(const PointSet& pts, double dist, unsigned max_points)
}); });
} }
} } // namespace sla
} } // namespace Slic3r

View file

@ -1,6 +1,6 @@
#include "SLAPrint.hpp" #include "SLAPrint.hpp"
#include "SLA/SLASupportTree.hpp" #include "SLA/SLASupportTree.hpp"
#include "SLA/SLABasePool.hpp" #include "SLA/SLAPad.hpp"
#include "SLA/SLAAutoSupports.hpp" #include "SLA/SLAAutoSupports.hpp"
#include "ClipperUtils.hpp" #include "ClipperUtils.hpp"
#include "Geometry.hpp" #include "Geometry.hpp"
@ -32,17 +32,19 @@
namespace Slic3r { namespace Slic3r {
using SupportTreePtr = std::unique_ptr<sla::SLASupportTree>; class SLAPrintObject::SupportData : public sla::SupportableMesh
class SLAPrintObject::SupportData
{ {
public: public:
sla::EigenMesh3D emesh; // index-triangle representation sla::SupportTree::UPtr support_tree_ptr; // the supports
std::vector<sla::SupportPoint> support_points; // all the support points (manual/auto)
SupportTreePtr support_tree_ptr; // the supports
std::vector<ExPolygons> support_slices; // sliced supports std::vector<ExPolygons> support_slices; // sliced supports
inline SupportData(const TriangleMesh &trmesh) : emesh(trmesh) {} inline SupportData(const TriangleMesh &t): sla::SupportableMesh{t, {}, {}} {}
sla::SupportTree::UPtr &create_support_tree(const sla::JobController &ctl)
{
support_tree_ptr = sla::SupportTree::create(*this, ctl);
return support_tree_ptr;
}
}; };
namespace { namespace {
@ -53,7 +55,7 @@ const std::array<unsigned, slaposCount> OBJ_STEP_LEVELS =
30, // slaposObjectSlice, 30, // slaposObjectSlice,
20, // slaposSupportPoints, 20, // slaposSupportPoints,
10, // slaposSupportTree, 10, // slaposSupportTree,
10, // slaposBasePool, 10, // slaposPad,
30, // slaposSliceSupports, 30, // slaposSliceSupports,
}; };
@ -64,7 +66,7 @@ std::string OBJ_STEP_LABELS(size_t idx)
case slaposObjectSlice: return L("Slicing model"); case slaposObjectSlice: return L("Slicing model");
case slaposSupportPoints: return L("Generating support points"); case slaposSupportPoints: return L("Generating support points");
case slaposSupportTree: return L("Generating support tree"); case slaposSupportTree: return L("Generating support tree");
case slaposBasePool: return L("Generating pad"); case slaposPad: return L("Generating pad");
case slaposSliceSupports: return L("Slicing supports"); case slaposSliceSupports: return L("Slicing supports");
default:; default:;
} }
@ -583,7 +585,8 @@ bool is_zero_elevation(const SLAPrintObjectConfig &c) {
// Compile the argument for support creation from the static print config. // Compile the argument for support creation from the static print config.
sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c) { sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c) {
sla::SupportConfig scfg; sla::SupportConfig scfg;
scfg.enabled = c.supports_enable.getBool();
scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat();
scfg.head_back_radius_mm = 0.5*c.support_pillar_diameter.getFloat(); scfg.head_back_radius_mm = 0.5*c.support_pillar_diameter.getFloat();
scfg.head_penetration_mm = c.support_head_penetration.getFloat(); scfg.head_penetration_mm = c.support_head_penetration.getFloat();
@ -612,12 +615,13 @@ sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c) {
return scfg; return scfg;
} }
sla::PoolConfig::EmbedObject builtin_pad_cfg(const SLAPrintObjectConfig& c) { sla::PadConfig::EmbedObject builtin_pad_cfg(const SLAPrintObjectConfig& c) {
sla::PoolConfig::EmbedObject ret; sla::PadConfig::EmbedObject ret;
ret.enabled = is_zero_elevation(c); ret.enabled = is_zero_elevation(c);
if(ret.enabled) { if(ret.enabled) {
ret.everywhere = c.pad_around_object_everywhere.getBool();
ret.object_gap_mm = c.pad_object_gap.getFloat(); ret.object_gap_mm = c.pad_object_gap.getFloat();
ret.stick_width_mm = c.pad_object_connector_width.getFloat(); ret.stick_width_mm = c.pad_object_connector_width.getFloat();
ret.stick_stride_mm = c.pad_object_connector_stride.getFloat(); ret.stick_stride_mm = c.pad_object_connector_stride.getFloat();
@ -628,17 +632,15 @@ sla::PoolConfig::EmbedObject builtin_pad_cfg(const SLAPrintObjectConfig& c) {
return ret; return ret;
} }
sla::PoolConfig make_pool_config(const SLAPrintObjectConfig& c) { sla::PadConfig make_pad_cfg(const SLAPrintObjectConfig& c) {
sla::PoolConfig pcfg; sla::PadConfig pcfg;
pcfg.min_wall_thickness_mm = c.pad_wall_thickness.getFloat(); pcfg.wall_thickness_mm = c.pad_wall_thickness.getFloat();
pcfg.wall_slope = c.pad_wall_slope.getFloat() * PI / 180.0; pcfg.wall_slope = c.pad_wall_slope.getFloat() * PI / 180.0;
// We do not support radius for now pcfg.max_merge_dist_mm = c.pad_max_merge_distance.getFloat();
pcfg.edge_radius_mm = 0.0; //c.pad_edge_radius.getFloat(); pcfg.wall_height_mm = c.pad_wall_height.getFloat();
pcfg.brim_size_mm = c.pad_brim_size.getFloat();
pcfg.max_merge_distance_mm = c.pad_max_merge_distance.getFloat();
pcfg.min_wall_height_mm = c.pad_wall_height.getFloat();
// set builtin pad implicitly ON // set builtin pad implicitly ON
pcfg.embed_object = builtin_pad_cfg(c); pcfg.embed_object = builtin_pad_cfg(c);
@ -646,6 +648,13 @@ sla::PoolConfig make_pool_config(const SLAPrintObjectConfig& c) {
return pcfg; return pcfg;
} }
bool validate_pad(const TriangleMesh &pad, const sla::PadConfig &pcfg)
{
// An empty pad can only be created if embed_object mode is enabled
// and the pad is not forced everywhere
return !pad.empty() || (pcfg.embed_object.enabled && !pcfg.embed_object.everywhere);
}
} }
std::string SLAPrint::validate() const std::string SLAPrint::validate() const
@ -663,17 +672,12 @@ std::string SLAPrint::validate() const
sla::SupportConfig cfg = make_support_cfg(po->config()); sla::SupportConfig cfg = make_support_cfg(po->config());
double pinhead_width =
2 * cfg.head_front_radius_mm +
cfg.head_width_mm +
2 * cfg.head_back_radius_mm -
cfg.head_penetration_mm;
double elv = cfg.object_elevation_mm; double elv = cfg.object_elevation_mm;
sla::PoolConfig::EmbedObject builtinpad = builtin_pad_cfg(po->config());
if(supports_en && !builtinpad.enabled && elv < pinhead_width ) sla::PadConfig padcfg = make_pad_cfg(po->config());
sla::PadConfig::EmbedObject &builtinpad = padcfg.embed_object;
if(supports_en && !builtinpad.enabled && elv < cfg.head_fullwidth())
return L( return L(
"Elevation is too low for object. Use the \"Pad around " "Elevation is too low for object. Use the \"Pad around "
"object\" feature to print the object without elevation."); "object\" feature to print the object without elevation.");
@ -686,6 +690,9 @@ std::string SLAPrint::validate() const
"distance' has to be greater than the 'Pad object gap' " "distance' has to be greater than the 'Pad object gap' "
"parameter to avoid this."); "parameter to avoid this.");
} }
std::string pval = padcfg.validate();
if (!pval.empty()) return pval;
} }
double expt_max = m_printer_config.max_exposure_time.getFloat(); double expt_max = m_printer_config.max_exposure_time.getFloat();
@ -876,8 +883,7 @@ void SLAPrint::process()
// Construction of this object does the calculation. // Construction of this object does the calculation.
this->throw_if_canceled(); this->throw_if_canceled();
SLAAutoSupports auto_supports(po.transformed_mesh(), SLAAutoSupports auto_supports(po.m_supportdata->emesh,
po.m_supportdata->emesh,
po.get_model_slices(), po.get_model_slices(),
heights, heights,
config, config,
@ -887,10 +893,10 @@ void SLAPrint::process()
// Now let's extract the result. // Now let's extract the result.
const std::vector<sla::SupportPoint>& points = auto_supports.output(); const std::vector<sla::SupportPoint>& points = auto_supports.output();
this->throw_if_canceled(); this->throw_if_canceled();
po.m_supportdata->support_points = points; po.m_supportdata->pts = points;
BOOST_LOG_TRIVIAL(debug) << "Automatic support points: " BOOST_LOG_TRIVIAL(debug) << "Automatic support points: "
<< po.m_supportdata->support_points.size(); << po.m_supportdata->pts.size();
// Using RELOAD_SLA_SUPPORT_POINTS to tell the Plater to pass // Using RELOAD_SLA_SUPPORT_POINTS to tell the Plater to pass
// the update status to GLGizmoSlaSupports // the update status to GLGizmoSlaSupports
@ -902,29 +908,19 @@ void SLAPrint::process()
else { else {
// There are either some points on the front-end, or the user // There are either some points on the front-end, or the user
// removed them on purpose. No calculation will be done. // removed them on purpose. No calculation will be done.
po.m_supportdata->support_points = po.transformed_support_points(); po.m_supportdata->pts = po.transformed_support_points();
} }
// If the zero elevation mode is engaged, we have to filter out all the // If the zero elevation mode is engaged, we have to filter out all the
// points that are on the bottom of the object // points that are on the bottom of the object
if (is_zero_elevation(po.config())) { if (is_zero_elevation(po.config())) {
double gnd = po.m_supportdata->emesh.ground_level();
auto & pts = po.m_supportdata->support_points;
double tolerance = po.config().pad_enable.getBool() double tolerance = po.config().pad_enable.getBool()
? po.m_config.pad_wall_thickness.getFloat() ? po.m_config.pad_wall_thickness.getFloat()
: po.m_config.support_base_height.getFloat(); : po.m_config.support_base_height.getFloat();
// get iterator to the reorganized vector end remove_bottom_points(po.m_supportdata->pts,
auto endit = std::remove_if( po.m_supportdata->emesh.ground_level(),
pts.begin(), tolerance);
pts.end(),
[tolerance, gnd](const sla::SupportPoint &sp) {
double diff = std::abs(gnd - double(sp.pos(Z)));
return diff <= tolerance;
});
// erase all elements after the new end
pts.erase(endit, pts.end());
} }
}; };
@ -933,45 +929,31 @@ void SLAPrint::process()
{ {
if(!po.m_supportdata) return; if(!po.m_supportdata) return;
sla::PoolConfig pcfg = make_pool_config(po.m_config); sla::PadConfig pcfg = make_pad_cfg(po.m_config);
if (pcfg.embed_object) if (pcfg.embed_object)
po.m_supportdata->emesh.ground_level_offset( po.m_supportdata->emesh.ground_level_offset(pcfg.wall_thickness_mm);
pcfg.min_wall_thickness_mm);
po.m_supportdata->cfg = make_support_cfg(po.m_config);
if(!po.m_config.supports_enable.getBool()) {
// Generate empty support tree. It can still host a pad
po.m_supportdata->support_tree_ptr.reset(
new SLASupportTree(po.m_supportdata->emesh.ground_level()));
return;
}
sla::SupportConfig scfg = make_support_cfg(po.m_config);
sla::Controller ctl;
// scaling for the sub operations // scaling for the sub operations
double d = ostepd * OBJ_STEP_LEVELS[slaposSupportTree] / 100.0; double d = ostepd * OBJ_STEP_LEVELS[slaposSupportTree] / 100.0;
double init = m_report_status.status(); double init = m_report_status.status();
JobController ctl;
ctl.statuscb = [this, d, init](unsigned st, const std::string &logmsg) ctl.statuscb = [this, d, init](unsigned st, const std::string &logmsg) {
{
double current = init + st * d; double current = init + st * d;
if(std::round(m_report_status.status()) < std::round(current)) if (std::round(m_report_status.status()) < std::round(current))
m_report_status(*this, current, m_report_status(*this, current,
OBJ_STEP_LABELS(slaposSupportTree), OBJ_STEP_LABELS(slaposSupportTree),
SlicingStatus::DEFAULT, SlicingStatus::DEFAULT, logmsg);
logmsg);
}; };
ctl.stopcondition = [this]() { return canceled(); };
ctl.stopcondition = [this](){ return canceled(); };
ctl.cancelfn = [this]() { throw_if_canceled(); }; ctl.cancelfn = [this]() { throw_if_canceled(); };
po.m_supportdata->support_tree_ptr.reset( po.m_supportdata->create_support_tree(ctl);
new SLASupportTree(po.m_supportdata->support_points,
po.m_supportdata->emesh, scfg, ctl)); if (!po.m_config.supports_enable.getBool()) return;
throw_if_canceled(); throw_if_canceled();
@ -980,10 +962,9 @@ void SLAPrint::process()
// This is to prevent "Done." being displayed during merged_mesh() // This is to prevent "Done." being displayed during merged_mesh()
m_report_status(*this, -1, L("Visualizing supports")); m_report_status(*this, -1, L("Visualizing supports"));
po.m_supportdata->support_tree_ptr->merged_mesh();
BOOST_LOG_TRIVIAL(debug) << "Processed support point count " BOOST_LOG_TRIVIAL(debug) << "Processed support point count "
<< po.m_supportdata->support_points.size(); << po.m_supportdata->pts.size();
// Check the mesh for later troubleshooting. // Check the mesh for later troubleshooting.
if(po.support_mesh().empty()) if(po.support_mesh().empty())
@ -993,7 +974,7 @@ void SLAPrint::process()
}; };
// This step generates the sla base pad // This step generates the sla base pad
auto base_pool = [this](SLAPrintObject& po) { auto generate_pad = [this](SLAPrintObject& po) {
// this step can only go after the support tree has been created // this step can only go after the support tree has been created
// and before the supports had been sliced. (or the slicing has to be // and before the supports had been sliced. (or the slicing has to be
// repeated) // repeated)
@ -1001,10 +982,10 @@ void SLAPrint::process()
if(po.m_config.pad_enable.getBool()) if(po.m_config.pad_enable.getBool())
{ {
// Get the distilled pad configuration from the config // Get the distilled pad configuration from the config
sla::PoolConfig pcfg = make_pool_config(po.m_config); sla::PadConfig pcfg = make_pad_cfg(po.m_config);
ExPolygons bp; // This will store the base plate of the pad. ExPolygons bp; // This will store the base plate of the pad.
double pad_h = sla::get_pad_fullheight(pcfg); double pad_h = pcfg.full_height();
const TriangleMesh &trmesh = po.transformed_mesh(); const TriangleMesh &trmesh = po.transformed_mesh();
// This call can get pretty time consuming // This call can get pretty time consuming
@ -1015,15 +996,19 @@ void SLAPrint::process()
// we sometimes call it "builtin pad" is enabled so we will // we sometimes call it "builtin pad" is enabled so we will
// get a sample from the bottom of the mesh and use it for pad // get a sample from the bottom of the mesh and use it for pad
// creation. // creation.
sla::base_plate(trmesh, sla::pad_blueprint(trmesh, bp, float(pad_h),
bp, float(po.m_config.layer_height.getFloat()),
float(pad_h), thrfn);
float(po.m_config.layer_height.getFloat()),
thrfn);
} }
pcfg.throw_on_cancel = thrfn;
po.m_supportdata->support_tree_ptr->add_pad(bp, pcfg); po.m_supportdata->support_tree_ptr->add_pad(bp, pcfg);
auto &pad_mesh = po.m_supportdata->support_tree_ptr->retrieve_mesh(MeshType::Pad);
if (!validate_pad(pad_mesh, pcfg))
throw std::runtime_error(
L("No pad can be generated for this model with the "
"current configuration"));
} else if(po.m_supportdata && po.m_supportdata->support_tree_ptr) { } else if(po.m_supportdata && po.m_supportdata->support_tree_ptr) {
po.m_supportdata->support_tree_ptr->remove_pad(); po.m_supportdata->support_tree_ptr->remove_pad();
} }
@ -1191,9 +1176,8 @@ void SLAPrint::process()
{ {
ClipperPolygon poly; ClipperPolygon poly;
// We need to reverse if flpXY OR is_lefthanded is true but // We need to reverse if is_lefthanded is true but
// not if both are true which is a logical inequality (XOR) bool needreverse = is_lefthanded;
bool needreverse = /*flpXY !=*/ is_lefthanded;
// should be a move // should be a move
poly.Contour.reserve(polygon.contour.size() + 1); poly.Contour.reserve(polygon.contour.size() + 1);
@ -1396,7 +1380,7 @@ void SLAPrint::process()
if(canceled()) return; if(canceled()) return;
// Set up the printer, allocate space for all the layers // Set up the printer, allocate space for all the layers
sla::SLARasterWriter &printer = init_printer(); sla::RasterWriter &printer = init_printer();
auto lvlcnt = unsigned(m_printer_input.size()); auto lvlcnt = unsigned(m_printer_input.size());
printer.layers(lvlcnt); printer.layers(lvlcnt);
@ -1416,11 +1400,9 @@ void SLAPrint::process()
SpinMutex slck; SpinMutex slck;
auto orientation = get_printer_orientation();
// procedure to process one height level. This will run in parallel // procedure to process one height level. This will run in parallel
auto lvlfn = auto lvlfn =
[this, &slck, &printer, increment, &dstatus, &pst, orientation] [this, &slck, &printer, increment, &dstatus, &pst]
(unsigned level_id) (unsigned level_id)
{ {
if(canceled()) return; if(canceled()) return;
@ -1431,7 +1413,7 @@ void SLAPrint::process()
printer.begin_layer(level_id); printer.begin_layer(level_id);
for(const ClipperLib::Polygon& poly : printlayer.transformed_slices()) for(const ClipperLib::Polygon& poly : printlayer.transformed_slices())
printer.draw_polygon(poly, level_id, orientation); printer.draw_polygon(poly, level_id);
// Finish the layer for later saving it. // Finish the layer for later saving it.
printer.finish_layer(level_id); printer.finish_layer(level_id);
@ -1459,7 +1441,7 @@ void SLAPrint::process()
tbb::parallel_for<unsigned, decltype(lvlfn)>(0, lvlcnt, lvlfn); tbb::parallel_for<unsigned, decltype(lvlfn)>(0, lvlcnt, lvlfn);
// Set statistics values to the printer // Set statistics values to the printer
sla::SLARasterWriter::PrintStatistics stats; sla::RasterWriter::PrintStatistics stats;
stats.used_material = (m_print_statistics.objects_used_material + stats.used_material = (m_print_statistics.objects_used_material +
m_print_statistics.support_used_material) / m_print_statistics.support_used_material) /
1000; 1000;
@ -1478,12 +1460,12 @@ void SLAPrint::process()
slaposFn pobj_program[] = slaposFn pobj_program[] =
{ {
slice_model, support_points, support_tree, base_pool, slice_supports slice_model, support_points, support_tree, generate_pad, slice_supports
}; };
// We want to first process all objects... // We want to first process all objects...
std::vector<SLAPrintObjectStep> level1_obj_steps = { std::vector<SLAPrintObjectStep> level1_obj_steps = {
slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposBasePool slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposPad
}; };
// and then slice all supports to allow preview to be displayed ASAP // and then slice all supports to allow preview to be displayed ASAP
@ -1654,12 +1636,11 @@ bool SLAPrint::invalidate_state_by_config_options(const std::vector<t_config_opt
return invalidated; return invalidated;
} }
sla::SLARasterWriter & SLAPrint::init_printer() sla::RasterWriter & SLAPrint::init_printer()
{ {
sla::Raster::Resolution res; sla::Raster::Resolution res;
sla::Raster::PixelDim pxdim; sla::Raster::PixelDim pxdim;
std::array<bool, 2> mirror; std::array<bool, 2> mirror;
double gamma;
double w = m_printer_config.display_width.getFloat(); double w = m_printer_config.display_width.getFloat();
double h = m_printer_config.display_height.getFloat(); double h = m_printer_config.display_height.getFloat();
@ -1669,20 +1650,18 @@ sla::SLARasterWriter & SLAPrint::init_printer()
mirror[X] = m_printer_config.display_mirror_x.getBool(); mirror[X] = m_printer_config.display_mirror_x.getBool();
mirror[Y] = m_printer_config.display_mirror_y.getBool(); mirror[Y] = m_printer_config.display_mirror_y.getBool();
if (get_printer_orientation() == sla::SLARasterWriter::roPortrait) { auto orientation = get_printer_orientation();
if (orientation == sla::Raster::roPortrait) {
std::swap(w, h); std::swap(w, h);
std::swap(pw, ph); std::swap(pw, ph);
// XY flipping implicitly does an X mirror
mirror[X] = !mirror[X];
} }
res = sla::Raster::Resolution{pw, ph}; res = sla::Raster::Resolution{pw, ph};
pxdim = sla::Raster::PixelDim{w / pw, h / ph}; pxdim = sla::Raster::PixelDim{w / pw, h / ph};
sla::Raster::Trafo tr{orientation, mirror};
gamma = m_printer_config.gamma_correction.getFloat(); tr.gamma = m_printer_config.gamma_correction.getFloat();
m_printer.reset(new sla::SLARasterWriter(res, pxdim, mirror, gamma)); m_printer.reset(new sla::RasterWriter(res, pxdim, tr));
m_printer->set_config(m_full_print_config); m_printer->set_config(m_full_print_config);
return *m_printer; return *m_printer;
} }
@ -1730,6 +1709,7 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector<t_conf
|| opt_key == "supports_enable" || opt_key == "supports_enable"
|| opt_key == "support_object_elevation" || opt_key == "support_object_elevation"
|| opt_key == "pad_around_object" || opt_key == "pad_around_object"
|| opt_key == "pad_around_object_everywhere"
|| opt_key == "slice_closing_radius") { || opt_key == "slice_closing_radius") {
steps.emplace_back(slaposObjectSlice); steps.emplace_back(slaposObjectSlice);
} else if ( } else if (
@ -1754,6 +1734,7 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector<t_conf
steps.emplace_back(slaposSupportTree); steps.emplace_back(slaposSupportTree);
} else if ( } else if (
opt_key == "pad_wall_height" opt_key == "pad_wall_height"
|| opt_key == "pad_brim_size"
|| opt_key == "pad_max_merge_distance" || opt_key == "pad_max_merge_distance"
|| opt_key == "pad_wall_slope" || opt_key == "pad_wall_slope"
|| opt_key == "pad_edge_radius" || opt_key == "pad_edge_radius"
@ -1762,7 +1743,7 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector<t_conf
|| opt_key == "pad_object_connector_width" || opt_key == "pad_object_connector_width"
|| opt_key == "pad_object_connector_penetration" || opt_key == "pad_object_connector_penetration"
) { ) {
steps.emplace_back(slaposBasePool); steps.emplace_back(slaposPad);
} else { } else {
// All keys should be covered. // All keys should be covered.
assert(false); assert(false);
@ -1782,12 +1763,12 @@ bool SLAPrintObject::invalidate_step(SLAPrintObjectStep step)
if (step == slaposObjectSlice) { if (step == slaposObjectSlice) {
invalidated |= this->invalidate_all_steps(); invalidated |= this->invalidate_all_steps();
} else if (step == slaposSupportPoints) { } else if (step == slaposSupportPoints) {
invalidated |= this->invalidate_steps({ slaposSupportTree, slaposBasePool, slaposSliceSupports }); invalidated |= this->invalidate_steps({ slaposSupportTree, slaposPad, slaposSliceSupports });
invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval); invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval);
} else if (step == slaposSupportTree) { } else if (step == slaposSupportTree) {
invalidated |= this->invalidate_steps({ slaposBasePool, slaposSliceSupports }); invalidated |= this->invalidate_steps({ slaposPad, slaposSliceSupports });
invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval); invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval);
} else if (step == slaposBasePool) { } else if (step == slaposPad) {
invalidated |= this->invalidate_steps({slaposSliceSupports}); invalidated |= this->invalidate_steps({slaposSliceSupports});
invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval); invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval);
} else if (step == slaposSliceSupports) { } else if (step == slaposSliceSupports) {
@ -1813,8 +1794,8 @@ double SLAPrintObject::get_elevation() const {
// its walls but currently it is half of its thickness. Whatever it // its walls but currently it is half of its thickness. Whatever it
// will be in the future, we provide the config to the get_pad_elevation // will be in the future, we provide the config to the get_pad_elevation
// method and we will have the correct value // method and we will have the correct value
sla::PoolConfig pcfg = make_pool_config(m_config); sla::PadConfig pcfg = make_pad_cfg(m_config);
if(!pcfg.embed_object) ret += sla::get_pad_elevation(pcfg); if(!pcfg.embed_object) ret += pcfg.required_elevation();
} }
return ret; return ret;
@ -1825,7 +1806,7 @@ double SLAPrintObject::get_current_elevation() const
if (is_zero_elevation(m_config)) return 0.; if (is_zero_elevation(m_config)) return 0.;
bool has_supports = is_step_done(slaposSupportTree); bool has_supports = is_step_done(slaposSupportTree);
bool has_pad = is_step_done(slaposBasePool); bool has_pad = is_step_done(slaposPad);
if(!has_supports && !has_pad) if(!has_supports && !has_pad)
return 0; return 0;
@ -1866,7 +1847,7 @@ const SliceRecord SliceRecord::EMPTY(0, std::nanf(""), 0.f);
const std::vector<sla::SupportPoint>& SLAPrintObject::get_support_points() const const std::vector<sla::SupportPoint>& SLAPrintObject::get_support_points() const
{ {
return m_supportdata? m_supportdata->support_points : EMPTY_SUPPORT_POINTS; return m_supportdata? m_supportdata->pts : EMPTY_SUPPORT_POINTS;
} }
const std::vector<ExPolygons> &SLAPrintObject::get_support_slices() const const std::vector<ExPolygons> &SLAPrintObject::get_support_slices() const
@ -1896,7 +1877,7 @@ bool SLAPrintObject::has_mesh(SLAPrintObjectStep step) const
switch (step) { switch (step) {
case slaposSupportTree: case slaposSupportTree:
return ! this->support_mesh().empty(); return ! this->support_mesh().empty();
case slaposBasePool: case slaposPad:
return ! this->pad_mesh().empty(); return ! this->pad_mesh().empty();
default: default:
return false; return false;
@ -1908,7 +1889,7 @@ TriangleMesh SLAPrintObject::get_mesh(SLAPrintObjectStep step) const
switch (step) { switch (step) {
case slaposSupportTree: case slaposSupportTree:
return this->support_mesh(); return this->support_mesh();
case slaposBasePool: case slaposPad:
return this->pad_mesh(); return this->pad_mesh();
default: default:
return TriangleMesh(); return TriangleMesh();
@ -1917,18 +1898,20 @@ TriangleMesh SLAPrintObject::get_mesh(SLAPrintObjectStep step) const
const TriangleMesh& SLAPrintObject::support_mesh() const const TriangleMesh& SLAPrintObject::support_mesh() const
{ {
if(m_config.supports_enable.getBool() && m_supportdata && sla::SupportTree::UPtr &stree = m_supportdata->support_tree_ptr;
m_supportdata->support_tree_ptr) {
return m_supportdata->support_tree_ptr->merged_mesh(); if(m_config.supports_enable.getBool() && m_supportdata && stree)
} return stree->retrieve_mesh(sla::MeshType::Support);
return EMPTY_MESH; return EMPTY_MESH;
} }
const TriangleMesh& SLAPrintObject::pad_mesh() const const TriangleMesh& SLAPrintObject::pad_mesh() const
{ {
if(m_config.pad_enable.getBool() && m_supportdata && m_supportdata->support_tree_ptr) sla::SupportTree::UPtr &stree = m_supportdata->support_tree_ptr;
return m_supportdata->support_tree_ptr->get_pad();
if(m_config.pad_enable.getBool() && m_supportdata && stree)
return stree->retrieve_mesh(sla::MeshType::Pad);
return EMPTY_MESH; return EMPTY_MESH;
} }

View file

@ -21,7 +21,7 @@ enum SLAPrintObjectStep : unsigned int {
slaposObjectSlice, slaposObjectSlice,
slaposSupportPoints, slaposSupportPoints,
slaposSupportTree, slaposSupportTree,
slaposBasePool, slaposPad,
slaposSliceSupports, slaposSliceSupports,
slaposCount slaposCount
}; };
@ -54,7 +54,7 @@ public:
bool is_left_handed() const { return m_left_handed; } bool is_left_handed() const { return m_left_handed; }
struct Instance { struct Instance {
Instance(ObjectID instance_id, const Point &shift, float rotation) : instance_id(instance_id), shift(shift), rotation(rotation) {} Instance(ObjectID inst_id, const Point &shft, float rot) : instance_id(inst_id), shift(shft), rotation(rot) {}
bool operator==(const Instance &rhs) const { return this->instance_id == rhs.instance_id && this->shift == rhs.shift && this->rotation == rhs.rotation; } bool operator==(const Instance &rhs) const { return this->instance_id == rhs.instance_id && this->shift == rhs.shift && this->rotation == rhs.rotation; }
// ID of the corresponding ModelInstance. // ID of the corresponding ModelInstance.
ObjectID instance_id; ObjectID instance_id;
@ -440,7 +440,7 @@ private:
std::vector<PrintLayer> m_printer_input; std::vector<PrintLayer> m_printer_input;
// The printer itself // The printer itself
std::unique_ptr<sla::SLARasterWriter> m_printer; std::unique_ptr<sla::RasterWriter> m_printer;
// Estimated print time, material consumed. // Estimated print time, material consumed.
SLAPrintStatistics m_print_statistics; SLAPrintStatistics m_print_statistics;
@ -459,14 +459,13 @@ private:
double status() const { return m_st; } double status() const { return m_st; }
} m_report_status; } m_report_status;
sla::SLARasterWriter &init_printer(); sla::RasterWriter &init_printer();
inline sla::SLARasterWriter::Orientation get_printer_orientation() const inline sla::Raster::Orientation get_printer_orientation() const
{ {
auto ro = m_printer_config.display_orientation.getInt(); auto ro = m_printer_config.display_orientation.getInt();
return ro == sla::SLARasterWriter::roPortrait ? return ro == sla::Raster::roPortrait ? sla::Raster::roPortrait :
sla::SLARasterWriter::roPortrait : sla::Raster::roLandscape;
sla::SLARasterWriter::roLandscape;
} }
friend SLAPrintObject; friend SLAPrintObject;

View file

@ -10,12 +10,15 @@ namespace Slic3r {
class ExPolygon; class ExPolygon;
typedef std::vector<ExPolygon> ExPolygons; typedef std::vector<ExPolygon> ExPolygons;
extern std::vector<Vec3d> triangulate_expolygon_3d (const ExPolygon &poly, coordf_t z = 0, bool flip = false); const bool constexpr NORMALS_UP = false;
extern std::vector<Vec3d> triangulate_expolygons_3d(const ExPolygons &polys, coordf_t z = 0, bool flip = false); const bool constexpr NORMALS_DOWN = true;
extern std::vector<Vec2d> triangulate_expolygon_2d (const ExPolygon &poly, bool flip = false);
extern std::vector<Vec2d> triangulate_expolygons_2d(const ExPolygons &polys, bool flip = false); extern std::vector<Vec3d> triangulate_expolygon_3d (const ExPolygon &poly, coordf_t z = 0, bool flip = NORMALS_UP);
extern std::vector<Vec2f> triangulate_expolygon_2f (const ExPolygon &poly, bool flip = false); extern std::vector<Vec3d> triangulate_expolygons_3d(const ExPolygons &polys, coordf_t z = 0, bool flip = NORMALS_UP);
extern std::vector<Vec2f> triangulate_expolygons_2f(const ExPolygons &polys, bool flip = false); extern std::vector<Vec2d> triangulate_expolygon_2d (const ExPolygon &poly, bool flip = NORMALS_UP);
extern std::vector<Vec2d> triangulate_expolygons_2d(const ExPolygons &polys, bool flip = NORMALS_UP);
extern std::vector<Vec2f> triangulate_expolygon_2f (const ExPolygon &poly, bool flip = NORMALS_UP);
extern std::vector<Vec2f> triangulate_expolygons_2f(const ExPolygons &polys, bool flip = NORMALS_UP);
} // namespace Slic3r } // namespace Slic3r

View file

@ -236,7 +236,7 @@ bool TriangleMesh::needed_repair() const
|| this->stl.stats.backwards_edges > 0; || this->stl.stats.backwards_edges > 0;
} }
void TriangleMesh::WriteOBJFile(const char* output_file) void TriangleMesh::WriteOBJFile(const char* output_file) const
{ {
its_write_obj(this->its, output_file); its_write_obj(this->its, output_file);
} }

View file

@ -31,7 +31,7 @@ public:
float volume(); float volume();
void check_topology(); void check_topology();
bool is_manifold() const { return this->stl.stats.connected_facets_3_edge == (int)this->stl.stats.number_of_facets; } bool is_manifold() const { return this->stl.stats.connected_facets_3_edge == (int)this->stl.stats.number_of_facets; }
void WriteOBJFile(const char* output_file); void WriteOBJFile(const char* output_file) const;
void scale(float factor); void scale(float factor);
void scale(const Vec3d &versor); void scale(const Vec3d &versor);
void translate(float x, float y, float z); void translate(float x, float y, float z);

View file

@ -417,7 +417,7 @@ void GLVolume::render(int color_id, int detection_id, int worldmatrix_id) const
} }
bool GLVolume::is_sla_support() const { return this->composite_id.volume_id == -int(slaposSupportTree); } bool GLVolume::is_sla_support() const { return this->composite_id.volume_id == -int(slaposSupportTree); }
bool GLVolume::is_sla_pad() const { return this->composite_id.volume_id == -int(slaposBasePool); } bool GLVolume::is_sla_pad() const { return this->composite_id.volume_id == -int(slaposPad); }
std::vector<int> GLVolumeCollection::load_object( std::vector<int> GLVolumeCollection::load_object(
const ModelObject *model_object, const ModelObject *model_object,
@ -501,7 +501,7 @@ void GLVolumeCollection::load_object_auxiliary(
TriangleMesh convex_hull = mesh.convex_hull_3d(); TriangleMesh convex_hull = mesh.convex_hull_3d();
for (const std::pair<size_t, size_t>& instance_idx : instances) { for (const std::pair<size_t, size_t>& instance_idx : instances) {
const ModelInstance& model_instance = *print_object->model_object()->instances[instance_idx.first]; const ModelInstance& model_instance = *print_object->model_object()->instances[instance_idx.first];
this->volumes.emplace_back(new GLVolume((milestone == slaposBasePool) ? GLVolume::SLA_PAD_COLOR : GLVolume::SLA_SUPPORT_COLOR)); this->volumes.emplace_back(new GLVolume((milestone == slaposPad) ? GLVolume::SLA_PAD_COLOR : GLVolume::SLA_SUPPORT_COLOR));
GLVolume& v = *this->volumes.back(); GLVolume& v = *this->volumes.back();
v.indexed_vertex_array.load_mesh(mesh); v.indexed_vertex_array.load_mesh(mesh);
v.indexed_vertex_array.finalize_geometry(opengl_initialized); v.indexed_vertex_array.finalize_geometry(opengl_initialized);

View file

@ -349,15 +349,18 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config)
toggle_field("pad_wall_thickness", pad_en); toggle_field("pad_wall_thickness", pad_en);
toggle_field("pad_wall_height", pad_en); toggle_field("pad_wall_height", pad_en);
toggle_field("pad_brim_size", pad_en);
toggle_field("pad_max_merge_distance", pad_en); toggle_field("pad_max_merge_distance", pad_en);
// toggle_field("pad_edge_radius", supports_en); // toggle_field("pad_edge_radius", supports_en);
toggle_field("pad_wall_slope", pad_en); toggle_field("pad_wall_slope", pad_en);
toggle_field("pad_around_object", pad_en); toggle_field("pad_around_object", pad_en);
toggle_field("pad_around_object_everywhere", pad_en);
bool zero_elev = config->opt_bool("pad_around_object") && pad_en; bool zero_elev = config->opt_bool("pad_around_object") && pad_en;
toggle_field("support_object_elevation", supports_en && !zero_elev); toggle_field("support_object_elevation", supports_en && !zero_elev);
toggle_field("pad_object_gap", zero_elev); toggle_field("pad_object_gap", zero_elev);
toggle_field("pad_around_object_everywhere", zero_elev);
toggle_field("pad_object_connector_stride", zero_elev); toggle_field("pad_object_connector_stride", zero_elev);
toggle_field("pad_object_connector_width", zero_elev); toggle_field("pad_object_connector_width", zero_elev);
toggle_field("pad_object_connector_penetration", zero_elev); toggle_field("pad_object_connector_penetration", zero_elev);

View file

@ -1767,7 +1767,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
// SLA steps to pull the preview meshes for. // SLA steps to pull the preview meshes for.
typedef std::array<SLAPrintObjectStep, 2> SLASteps; typedef std::array<SLAPrintObjectStep, 2> SLASteps;
SLASteps sla_steps = { slaposSupportTree, slaposBasePool }; SLASteps sla_steps = { slaposSupportTree, slaposPad };
struct SLASupportState { struct SLASupportState {
std::array<PrintStateBase::StateWithTimeStamp, std::tuple_size<SLASteps>::value> step; std::array<PrintStateBase::StateWithTimeStamp, std::tuple_size<SLASteps>::value> step;
}; };
@ -5340,8 +5340,8 @@ void GLCanvas3D::_load_sla_shells()
m_volumes.volumes.back()->extruder_id = obj->model_object()->volumes.front()->extruder_id(); m_volumes.volumes.back()->extruder_id = obj->model_object()->volumes.front()->extruder_id();
if (obj->is_step_done(slaposSupportTree) && obj->has_mesh(slaposSupportTree)) if (obj->is_step_done(slaposSupportTree) && obj->has_mesh(slaposSupportTree))
add_volume(*obj, -int(slaposSupportTree), instance, obj->support_mesh(), GLVolume::SLA_SUPPORT_COLOR, true); add_volume(*obj, -int(slaposSupportTree), instance, obj->support_mesh(), GLVolume::SLA_SUPPORT_COLOR, true);
if (obj->is_step_done(slaposBasePool) && obj->has_mesh(slaposBasePool)) if (obj->is_step_done(slaposPad) && obj->has_mesh(slaposPad))
add_volume(*obj, -int(slaposBasePool), instance, obj->pad_mesh(), GLVolume::SLA_PAD_COLOR, false); add_volume(*obj, -int(slaposPad), instance, obj->pad_mesh(), GLVolume::SLA_PAD_COLOR, false);
} }
double shift_z = obj->get_current_elevation(); double shift_z = obj->get_current_elevation();
for (unsigned int i = initial_volumes_count; i < m_volumes.volumes.size(); ++ i) { for (unsigned int i = initial_volumes_count; i < m_volumes.volumes.size(); ++ i) {

View file

@ -261,7 +261,7 @@ bool MainFrame::can_export_supports() const
const PrintObjects& objects = m_plater->sla_print().objects(); const PrintObjects& objects = m_plater->sla_print().objects();
for (const SLAPrintObject* object : objects) for (const SLAPrintObject* object : objects)
{ {
if (object->has_mesh(slaposBasePool) || object->has_mesh(slaposSupportTree)) if (object->has_mesh(slaposPad) || object->has_mesh(slaposSupportTree))
{ {
can_export = true; can_export = true;
break; break;

View file

@ -4462,10 +4462,10 @@ void Plater::export_stl(bool extended, bool selection_only)
bool is_left_handed = object->is_left_handed(); bool is_left_handed = object->is_left_handed();
TriangleMesh pad_mesh; TriangleMesh pad_mesh;
bool has_pad_mesh = object->has_mesh(slaposBasePool); bool has_pad_mesh = object->has_mesh(slaposPad);
if (has_pad_mesh) if (has_pad_mesh)
{ {
pad_mesh = object->get_mesh(slaposBasePool); pad_mesh = object->get_mesh(slaposPad);
pad_mesh.transform(mesh_trafo_inv); pad_mesh.transform(mesh_trafo_inv);
} }
@ -4641,7 +4641,7 @@ void Plater::reslice_SLA_supports(const ModelObject &object, bool postpone_error
// Otherwise calculate everything, but start with the provided object. // Otherwise calculate everything, but start with the provided object.
if (!this->p->background_processing_enabled()) { if (!this->p->background_processing_enabled()) {
task.single_model_instance_only = true; task.single_model_instance_only = true;
task.to_object_step = slaposBasePool; task.to_object_step = slaposPad;
} }
this->p->background_process.set_task(task); this->p->background_process.set_task(task);
// and let the background processing start. // and let the background processing start.

View file

@ -476,11 +476,13 @@ const std::vector<std::string>& Preset::sla_print_options()
"pad_enable", "pad_enable",
"pad_wall_thickness", "pad_wall_thickness",
"pad_wall_height", "pad_wall_height",
"pad_brim_size",
"pad_max_merge_distance", "pad_max_merge_distance",
// "pad_edge_radius", // "pad_edge_radius",
"pad_wall_slope", "pad_wall_slope",
"pad_object_gap", "pad_object_gap",
"pad_around_object", "pad_around_object",
"pad_around_object_everywhere",
"pad_object_connector_stride", "pad_object_connector_stride",
"pad_object_connector_width", "pad_object_connector_width",
"pad_object_connector_penetration", "pad_object_connector_penetration",

View file

@ -3551,12 +3551,14 @@ void TabSLAPrint::build()
optgroup->append_single_option_line("pad_enable"); optgroup->append_single_option_line("pad_enable");
optgroup->append_single_option_line("pad_wall_thickness"); optgroup->append_single_option_line("pad_wall_thickness");
optgroup->append_single_option_line("pad_wall_height"); optgroup->append_single_option_line("pad_wall_height");
optgroup->append_single_option_line("pad_brim_size");
optgroup->append_single_option_line("pad_max_merge_distance"); optgroup->append_single_option_line("pad_max_merge_distance");
// TODO: Disabling this parameter for the beta release // TODO: Disabling this parameter for the beta release
// optgroup->append_single_option_line("pad_edge_radius"); // optgroup->append_single_option_line("pad_edge_radius");
optgroup->append_single_option_line("pad_wall_slope"); optgroup->append_single_option_line("pad_wall_slope");
optgroup->append_single_option_line("pad_around_object"); optgroup->append_single_option_line("pad_around_object");
optgroup->append_single_option_line("pad_around_object_everywhere");
optgroup->append_single_option_line("pad_object_gap"); optgroup->append_single_option_line("pad_object_gap");
optgroup->append_single_option_line("pad_object_connector_stride"); optgroup->append_single_option_line("pad_object_connector_stride");
optgroup->append_single_option_line("pad_object_connector_width"); optgroup->append_single_option_line("pad_object_connector_width");

View file

@ -29,4 +29,5 @@ target_link_libraries(test_gtest_common INTERFACE GTest::GTest GTest::Main)
add_subdirectory(libnest2d) add_subdirectory(libnest2d)
add_subdirectory(timeutils) add_subdirectory(timeutils)
add_subdirectory(sla_print)

20
tests/data/20mm_cube.obj Normal file
View file

@ -0,0 +1,20 @@
v 20.000000 20.000000 0.000000
v 20.000000 0.000000 0.000000
v 0.000000 0.000000 0.000000
v 0.000000 20.000000 0.000000
v 20.000000 20.000000 20.000000
v 0.000000 20.000000 20.000000
v 0.000000 0.000000 20.000000
v 20.000000 0.000000 20.000000
f 1 2 3
f 1 3 4
f 5 6 7
f 5 7 8
f 1 5 8
f 1 8 2
f 2 8 7
f 2 7 3
f 3 7 6
f 3 6 4
f 5 1 4
f 5 4 6

20
tests/data/2x20x10.obj Normal file
View file

@ -0,0 +1,20 @@
v 2.000000 20.000000 0.000000
v 2.000000 0.000000 0.000000
v 0.000000 0.000000 0.000000
v 0.000000 20.000000 0.000000
v 2.000000 20.000000 10.000000
v 0.000000 20.000000 10.000000
v 0.000000 0.000000 10.000000
v 2.000000 0.000000 10.000000
f 1 2 3
f 1 3 4
f 5 6 7
f 5 7 8
f 1 5 8
f 1 8 2
f 2 8 7
f 2 7 3
f 3 7 6
f 3 6 4
f 5 1 4
f 5 4 6

1867
tests/data/A.obj Normal file

File diff suppressed because it is too large Load diff

2504
tests/data/A_upsidedown.obj Normal file

File diff suppressed because it is too large Load diff

68
tests/data/V.obj Normal file
View file

@ -0,0 +1,68 @@
####
#
# OBJ File Generated by Meshlab
#
####
# Object V.obj
#
# Vertices: 14
# Faces: 24
#
####
vn -0.835484 -0.584839 0.000000
v 41.480000 44.020000 0.000000
vn 0.000000 0.000000 1.768192
v 51.480000 44.020000 15.000000
vn 0.000000 0.000000 0.960070
v 41.480000 44.020000 15.000000
vn 0.000000 0.000000 0.413330
v 55.480000 24.020000 15.000000
vn 0.000000 -0.982794 0.000000
v 65.480003 24.020000 15.000000
vn -0.000000 -0.000000 -2.776433
v 60.480000 31.162861 0.000000
vn -0.000000 -0.000000 -0.099828
v 79.480003 44.020000 0.000000
vn -0.000000 -0.000000 -0.265332
v 55.480000 24.020000 0.000000
vn 0.661947 0.463363 0.000000
v 51.480000 44.020000 0.000000
vn 0.624900 0.437430 0.000000
v 60.480000 31.162861 15.000000
vn -0.661947 0.463363 0.000000
v 69.480003 44.020000 15.000000
vn -1.286846 0.900793 0.000000
v 69.480003 44.020000 0.000000
vn 0.000000 0.982794 0.000000
v 79.480003 44.020000 15.000000
vn 0.451362 -0.315954 0.000000
v 65.480003 24.020000 0.000000
# 14 vertices, 0 vertices normals
f 3//3 1//1 4//4
f 4//4 1//1 8//8
f 1//1 3//3 2//2
f 9//9 1//1 2//2
f 2//2 3//3 4//4
f 10//10 2//2 4//4
f 5//5 10//10 4//4
f 13//13 11//11 5//5
f 11//11 10//10 5//5
f 4//4 8//8 5//5
f 5//5 8//8 14//14
f 1//1 9//9 8//8
f 8//8 9//9 6//6
f 6//6 12//12 7//7
f 6//6 7//7 8//8
f 8//8 7//7 14//14
f 9//9 2//2 10//10
f 6//6 9//9 10//10
f 10//10 11//11 6//6
f 6//6 11//11 12//12
f 12//12 11//11 13//13
f 7//7 12//12 13//13
f 13//13 5//5 14//14
f 7//7 13//13 14//14
# 24 faces, 0 coords texture
# End of File

38
tests/data/V_standing.obj Normal file
View file

@ -0,0 +1,38 @@
v -14.000000 0.000000 20.000000
v -14.000000 15.000000 20.000000
v 0.000000 0.000000 0.000000
v 0.000000 15.000000 0.000000
v -4.000000 0.000000 20.000000
v -4.000000 15.000000 20.000000
v 5.000000 0.000000 7.142860
v 10.000000 0.000000 0.000000
v 24.000000 0.000000 20.000000
v 14.000000 0.000000 20.000000
v 10.000000 15.000000 0.000000
v 5.000000 15.000000 7.142860
v 14.000000 15.000000 20.000000
v 24.000000 15.000000 20.000000
f 1 2 3
f 3 2 4
f 2 1 5
f 6 2 5
f 5 1 3
f 7 5 3
f 8 7 3
f 9 10 8
f 10 7 8
f 3 4 8
f 8 4 11
f 2 6 4
f 4 6 12
f 12 13 14
f 12 14 4
f 4 14 11
f 6 5 7
f 12 6 7
f 7 10 12
f 12 10 13
f 13 10 9
f 14 13 9
f 9 8 11
f 14 9 11

44
tests/data/bridge.obj Normal file
View file

@ -0,0 +1,44 @@
v 75.000000 84.500000 8.000000
v 125.000000 84.500000 8.000000
v 75.000000 94.500000 8.000000
v 120.000000 84.500000 5.000000
v 125.000000 94.500000 8.000000
v 75.000000 84.500000 0.000000
v 80.000000 84.500000 5.000000
v 125.000000 84.500000 0.000000
v 125.000000 94.500000 0.000000
v 80.000000 94.500000 5.000000
v 75.000000 94.500000 0.000000
v 120.000000 94.500000 5.000000
v 120.000000 84.500000 0.000000
v 80.000000 94.500000 0.000000
v 80.000000 84.500000 0.000000
v 120.000000 94.500000 0.000000
f 1 2 3
f 2 1 4
f 3 2 5
f 3 6 1
f 1 7 4
f 2 4 8
f 2 9 5
f 5 10 3
f 11 6 3
f 6 7 1
f 7 12 4
f 4 13 8
f 8 9 2
f 5 9 12
f 5 12 10
f 10 11 3
f 11 14 6
f 15 7 6
f 10 12 7
f 12 13 4
f 13 9 8
f 12 9 16
f 14 11 10
f 6 14 15
f 15 14 7
f 7 14 10
f 16 13 12
f 16 9 13

View file

@ -0,0 +1,72 @@
v -10.000000 -10.000000 -5.000000
v -10.000000 -10.000000 5.000000
v -10.000000 10.000000 -5.000000
v -10.000000 10.000000 5.000000
v 10.000000 -10.000000 -5.000000
v 10.000000 -10.000000 5.000000
v -5.000000 -5.000000 -5.000000
v 5.000000 -5.000000 -5.000000
v 5.000000 5.000000 -5.000000
v 5.000000 10.000000 -5.000000
v -5.000000 5.000000 -5.000000
v 0.000000 5.000000 -5.000000
v 5.000000 0.000000 -5.000000
v 0.000000 0.000000 -5.000000
v 10.000000 5.000000 -5.000000
v 5.000000 10.000000 5.000000
v -5.000000 -5.000000 5.000000
v 5.000000 0.000000 5.000000
v 5.000000 -5.000000 5.000000
v -5.000000 5.000000 5.000000
v 10.000000 5.000000 5.000000
v 5.000000 5.000000 5.000000
v 0.000000 5.000000 5.000000
v 0.000000 0.000000 5.000000
f 1 2 3
f 3 2 4
f 2 1 5
f 6 2 5
f 7 8 5
f 9 3 10
f 11 3 12
f 12 13 14
f 1 3 11
f 1 11 7
f 1 7 5
f 12 3 9
f 5 8 13
f 5 13 9
f 13 12 9
f 15 5 9
f 3 4 10
f 10 4 16
f 17 2 6
f 18 19 6
f 20 4 17
f 21 22 6
f 19 17 6
f 4 2 17
f 23 4 20
f 22 4 23
f 22 18 6
f 22 23 18
f 22 16 4
f 24 18 23
f 6 5 15
f 21 6 15
f 21 15 22
f 22 15 9
f 10 16 22
f 9 10 22
f 11 20 17
f 7 11 17
f 12 23 20
f 11 12 20
f 14 24 12
f 12 24 23
f 24 14 13
f 18 24 13
f 18 13 19
f 19 13 8
f 19 8 17
f 17 8 7

View file

@ -0,0 +1,112 @@
####
#
# OBJ File Generated by Meshlab
#
####
# Object cube_with_concave_hole_enlarged.obj
#
# Vertices: 24
# Faces: 48
#
####
vn 1.107149 0.000000 0.000000
v 76.634163 17.865837 0.000000
vn 0.000000 0.000000 0.321750
v 68.557083 25.942917 16.154167
vn 0.000000 0.000000 2.356194
v 52.402920 25.942917 16.154167
vn 0.000000 0.000000 0.321751
v 76.634163 50.174164 16.154167
vn 0.000000 0.000000 2.034444
v 68.557083 42.097084 16.154167
vn 0.000000 0.000000 0.463648
v 76.634163 17.865837 16.154167
vn 0.000000 0.000000 0.785398
v 52.402920 34.020000 16.154167
vn 0.000000 0.000000 0.321750
v 44.325836 17.865837 16.154167
vn 0.000000 0.982794 0.000000
v 52.402920 50.174164 16.154167
vn 0.000000 1.570796 0.000000
v 52.402920 50.174164 0.000000
vn 0.000000 0.000000 -0.463648
v 52.402920 34.020000 0.000000
vn 0.000000 0.000000 -0.321751
v 44.325836 17.865837 0.000000
vn 0.000000 0.000000 -0.463648
v 76.634163 50.174164 0.000000
vn 0.000000 0.000000 -1.570796
v 44.325836 42.097084 0.000000
vn -0.588003 0.000000 0.000000
v 44.325836 42.097084 16.154167
vn 0.000000 1.570796 0.000000
v 52.402920 42.097084 16.154167
vn -1.107149 0.000000 0.000000
v 52.402920 42.097084 0.000000
vn 0.000000 -0.463648 0.000000
v 60.480000 42.097084 16.154167
vn 0.000000 -1.107149 0.000000
v 68.557083 42.097084 0.000000
vn 1.570796 0.000000 0.000000
v 60.480000 42.097084 0.000000
vn 0.000000 -0.463647 0.000000
v 60.480000 34.020000 0.000000
vn 0.000000 -1.570796 0.000000
v 60.480000 34.020000 16.154167
vn 1.107149 0.000000 0.000000
v 52.402920 25.942917 0.000000
vn 0.000000 0.785398 0.000000
v 68.557083 25.942917 0.000000
# 24 vertices, 0 vertices normals
f 6//6 1//1 4//4
f 4//4 1//1 13//13
f 1//1 6//6 8//8
f 12//12 1//1 8//8
f 2//2 3//3 8//8
f 16//16 4//4 9//9
f 5//5 4//4 18//18
f 18//18 7//7 22//22
f 6//6 4//4 5//5
f 6//6 5//5 2//2
f 6//6 2//2 8//8
f 18//18 4//4 16//16
f 8//8 3//3 7//7
f 8//8 7//7 16//16
f 7//7 18//18 16//16
f 15//15 8//8 16//16
f 4//4 13//13 9//9
f 9//9 13//13 10//10
f 24//24 1//1 12//12
f 11//11 23//23 12//12
f 19//19 13//13 24//24
f 14//14 17//17 12//12
f 23//23 24//24 12//12
f 13//13 1//1 24//24
f 20//20 13//13 19//19
f 17//17 13//13 20//20
f 17//17 11//11 12//12
f 17//17 20//20 11//11
f 17//17 10//10 13//13
f 21//21 11//11 20//20
f 12//12 8//8 15//15
f 14//14 12//12 15//15
f 14//14 15//15 17//17
f 17//17 15//15 16//16
f 9//9 10//10 17//17
f 16//16 9//9 17//17
f 5//5 19//19 24//24
f 2//2 5//5 24//24
f 18//18 20//20 19//19
f 5//5 18//18 19//19
f 22//22 21//21 18//18
f 18//18 21//21 20//20
f 21//21 22//22 7//7
f 11//11 21//21 7//7
f 11//11 7//7 23//23
f 23//23 7//7 3//3
f 23//23 3//3 24//24
f 24//24 3//3 2//2
# 48 faces, 0 coords texture
# End of File

View file

@ -0,0 +1,112 @@
####
#
# OBJ File Generated by Meshlab
#
####
# Object cube_with_concave_hole_enlarged.obj
#
# Vertices: 24
# Faces: 48
#
####
vn 0.000000 0.000000 -1.570796
v 68.557083 17.865835 0.000000
vn 0.000000 0.000000 -1.107149
v 52.402916 17.865835 0.000000
vn 0.000000 -0.463648 0.000000
v 68.557083 17.865835 32.308331
vn 0.000000 -1.570796 0.000000
v 52.402916 17.865835 32.308331
vn 2.356194 0.000000 0.000000
v 68.557083 25.942917 24.231247
vn 1.570796 0.000000 0.000000
v 68.557083 50.174164 24.231247
vn 2.034444 0.000000 0.000000
v 68.557083 42.097084 8.077083
vn 2.677945 0.000000 0.000000
v 68.557083 34.020000 24.231247
vn 0.000000 1.570796 0.000000
v 68.557083 50.174164 0.000000
vn 0.000000 1.570796 0.000000
v 52.402916 50.174164 24.231247
vn -0.321750 0.000000 0.000000
v 52.402916 25.942917 8.077083
vn -1.570796 0.000000 0.000000
v 52.402916 42.097084 32.308331
vn -0.321751 0.000000 0.000000
v 52.402916 50.174164 0.000000
vn -2.356194 0.000000 0.000000
v 52.402916 42.097084 8.077083
vn -0.785398 0.000000 0.000000
v 52.402916 42.097084 16.154165
vn 0.000000 0.000000 0.588003
v 68.557083 42.097084 32.308331
vn 0.000000 1.107149 0.000000
v 52.402916 42.097084 24.231247
vn 0.000000 1.570796 0.000000
v 68.557083 42.097084 24.231247
vn 0.000000 0.000000 1.570796
v 68.557083 25.942917 8.077083
vn 0.000000 -0.463647 0.000000
v 68.557083 42.097084 16.154165
vn 0.000000 0.000000 -1.570796
v 68.557083 34.020000 16.154165
vn 0.000000 0.000000 -0.463648
v 52.402916 34.020000 16.154165
vn 0.000000 0.000000 -1.570796
v 52.402916 34.020000 24.231247
vn 0.000000 0.000000 -0.463648
v 52.402916 25.942917 24.231247
# 24 vertices, 0 vertices normals
f 1//1 2//2 9//9
f 9//9 2//2 13//13
f 2//2 1//1 3//3
f 4//4 2//2 3//3
f 19//19 5//5 3//3
f 18//18 9//9 6//6
f 7//7 9//9 20//20
f 20//20 8//8 21//21
f 1//1 9//9 7//7
f 1//1 7//7 19//19
f 1//1 19//19 3//3
f 20//20 9//9 18//18
f 3//3 5//5 8//8
f 3//3 8//8 18//18
f 8//8 20//20 18//18
f 16//16 3//3 18//18
f 9//9 13//13 6//6
f 6//6 13//13 10//10
f 11//11 2//2 4//4
f 23//23 24//24 4//4
f 14//14 13//13 11//11
f 12//12 17//17 4//4
f 24//24 11//11 4//4
f 13//13 2//2 11//11
f 15//15 13//13 14//14
f 17//17 13//13 15//15
f 17//17 23//23 4//4
f 17//17 15//15 23//23
f 17//17 10//10 13//13
f 22//22 23//23 15//15
f 4//4 3//3 16//16
f 12//12 4//4 16//16
f 12//12 16//16 17//17
f 17//17 16//16 18//18
f 6//6 10//10 17//17
f 18//18 6//6 17//17
f 7//7 14//14 11//11
f 19//19 7//7 11//11
f 20//20 15//15 14//14
f 7//7 20//20 14//14
f 21//21 22//22 20//20
f 20//20 22//22 15//15
f 22//22 21//21 8//8
f 23//23 22//22 8//8
f 23//23 8//8 24//24
f 24//24 8//8 5//5
f 24//24 5//5 11//11
f 11//11 5//5 19//19
# 48 faces, 0 coords texture
# End of File

View file

@ -0,0 +1,48 @@
v 0.000000 0.000000 0.000000
v 0.000000 0.000000 10.000000
v 0.000000 20.000000 0.000000
v 0.000000 20.000000 10.000000
v 20.000000 0.000000 0.000000
v 20.000000 0.000000 10.000000
v 5.000000 5.000000 0.000000
v 15.000000 5.000000 0.000000
v 5.000000 15.000000 0.000000
v 20.000000 20.000000 0.000000
v 15.000000 15.000000 0.000000
v 20.000000 20.000000 10.000000
v 5.000000 5.000000 10.000000
v 5.000000 15.000000 10.000000
v 15.000000 5.000000 10.000000
v 15.000000 15.000000 10.000000
f 1 2 3
f 3 2 4
f 2 1 5
f 6 2 5
f 7 8 5
f 9 3 10
f 1 3 9
f 11 9 10
f 1 9 7
f 1 7 5
f 5 8 10
f 8 11 10
f 3 4 10
f 10 4 12
f 13 2 6
f 14 4 13
f 15 13 6
f 4 2 13
f 12 4 14
f 12 16 6
f 12 14 16
f 16 15 6
f 6 5 10
f 12 6 10
f 9 14 13
f 7 9 13
f 11 16 14
f 9 11 14
f 16 11 15
f 15 11 8
f 15 8 13
f 13 8 7

File diff suppressed because it is too large Load diff

36702
tests/data/frog_legs.obj Normal file

File diff suppressed because it is too large Load diff

86
tests/data/ipadstand.obj Normal file
View file

@ -0,0 +1,86 @@
v 17.434467 -0.000000 9.500000
v 14.281480 10.000000 9.500000
v 0.000000 0.000000 9.500000
v 31.715948 10.000000 9.500000
v 62.234474 0.000000 20.000000
v 31.715948 10.000000 20.000000
v 17.434467 -0.000000 20.000000
v 62.234474 10.000000 20.000000
v 98.207970 10.000000 0.000000
v 98.207970 0.000000 10.000000
v 98.207970 0.000000 0.000000
v 98.207970 10.000000 20.000000
v 98.207970 0.000000 20.000000
v 81.660965 -0.000000 10.000000
v 90.054985 10.000000 10.000000
v 78.507980 10.000000 10.000000
v 93.207970 0.000000 10.000000
v 14.281480 10.000000 20.000000
v 0.000000 0.000000 20.000000
v 87.434471 0.000000 20.000000
v 84.281479 10.000000 20.000000
v 0.000000 10.000000 20.000000
v 0.000000 0.000000 0.000000
v 0.000000 10.000000 0.000000
v 62.234474 0.000000 30.000000
v 66.960976 10.000000 30.000000
v 62.234474 10.000000 30.000000
v 70.113960 0.000000 30.000000
v 67.705338 10.000000 28.710720
v 71.678711 0.000000 27.289770
f 1 2 3
f 2 1 4
f 5 6 7
f 6 5 8
f 9 10 11
f 10 12 13
f 12 10 9
f 14 15 16
f 15 14 17
f 18 3 2
f 3 18 19
f 20 12 21
f 12 20 13
f 18 22 19
f 22 3 19
f 3 22 23
f 23 22 24
f 9 23 24
f 23 9 11
f 25 26 27
f 26 25 28
f 24 2 9
f 2 24 22
f 2 22 18
f 6 16 4
f 16 6 8
f 16 8 29
f 29 8 27
f 29 27 26
f 9 15 12
f 15 9 4
f 4 9 2
f 15 4 16
f 12 15 21
f 27 5 25
f 5 27 8
f 13 17 10
f 17 13 20
f 30 5 14
f 5 30 25
f 25 30 28
f 10 23 11
f 23 10 1
f 1 10 17
f 1 17 14
f 1 14 7
f 7 14 5
f 3 23 1
f 20 15 17
f 15 20 21
f 16 30 14
f 30 26 28
f 26 30 16
f 26 16 29
f 7 4 1
f 4 7 6

179
tests/data/overhang.obj Normal file
View file

@ -0,0 +1,179 @@
v 1364.685059 614.398010 20.002499
v 1389.685059 614.398010 20.002499
v 1377.185059 589.398987 20.002499
v 1389.685059 589.398987 20.002499
v 1389.685059 564.398987 20.001499
v 1364.685059 589.398987 20.002499
v 1364.685059 564.398987 20.001499
v 1360.935059 589.398987 17.001499
v 1360.935059 585.646973 17.001499
v 1357.185059 564.398987 17.001499
v 1364.685059 589.398987 17.001499
v 1364.685059 571.899963 17.001499
v 1364.685059 564.398987 17.001499
v 1348.436035 564.398987 17.001499
v 1352.809082 589.398987 17.001499
v 1357.184082 589.398987 17.001499
v 1357.183105 614.398010 17.001499
v 1364.685059 606.895996 17.001499
v 1364.685059 614.398010 17.001499
v 1352.186035 564.398987 20.001499
v 1363.654053 589.398987 23.300499
v 1359.467041 589.398987 23.300499
v 1358.371094 564.398987 23.300499
v 1385.561035 564.398987 23.300499
v 1373.063110 589.398987 23.300499
v 1368.808105 564.398987 23.300499
v 1387.623047 589.398987 23.300499
v 1387.623047 585.276001 23.300499
v 1389.685059 589.398987 23.300499
v 1389.685059 572.645996 23.300499
v 1389.685059 564.398987 23.300499
v 1367.777100 589.398987 23.300499
v 1366.747070 564.398987 23.300499
v 1354.312012 589.398987 23.300499
v 1352.186035 564.398987 23.300499
v 1389.685059 614.398010 23.300499
v 1377.317017 614.398010 23.300499
v 1381.439087 589.398987 23.300499
v 1368.807007 614.398010 23.300499
v 1368.808105 589.398987 23.300499
v 1356.439087 614.398010 23.300499
v 1357.405029 589.398987 23.300499
v 1360.562012 614.398010 23.300499
v 1348.705078 614.398010 23.300499
v 1350.445068 589.398987 23.300499
v 1389.685059 606.153015 23.300499
v 1347.352051 589.398987 23.300499
v 1346.560059 589.398987 23.300499
v 1346.560059 594.159912 17.001499
v 1346.560059 589.398987 17.001499
v 1346.560059 605.250427 23.300499
v 1346.560059 614.398010 23.300499
v 1346.560059 614.398010 20.825829
v 1346.560059 614.398010 17.001499
v 1346.560059 564.398987 19.101339
v 1346.560059 567.548584 23.300499
v 1346.560059 564.398987 17.002033
v 1346.560059 564.398987 23.001850
v 1346.560059 564.398987 23.300499
v 1346.560059 575.118958 17.001499
v 1346.560059 574.754028 23.300499
f 1 2 3
f 3 4 5
f 3 6 1
f 5 7 3
f 3 7 6
f 3 2 4
f 8 9 10
f 11 10 9
f 12 10 11
f 13 10 12
f 10 14 15
f 8 16 17
f 11 18 1
f 11 1 6
f 13 12 7
f 19 17 1
f 7 20 14
f 7 14 10
f 10 13 7
f 18 19 1
f 12 11 6
f 12 6 7
f 15 17 16
f 18 8 19
f 17 19 8
f 15 16 10
f 8 10 16
f 8 18 9
f 11 9 18
f 21 22 23
f 24 25 26
f 27 24 28
f 29 28 24
f 30 29 24
f 31 30 24
f 26 32 33
f 23 34 35
f 36 37 38
f 25 39 40
f 22 41 42
f 39 43 21
f 34 44 45
f 7 5 24
f 7 24 26
f 37 36 2
f 2 1 39
f 2 39 37
f 30 31 5
f 26 33 7
f 41 43 1
f 36 46 2
f 5 4 29
f 5 29 30
f 4 2 46
f 4 46 29
f 23 35 20
f 20 7 33
f 20 33 23
f 43 39 1
f 31 24 5
f 1 17 44
f 1 44 41
f 25 38 37
f 39 25 37
f 25 24 38
f 38 24 27
f 23 33 21
f 21 33 32
f 34 42 41
f 44 34 41
f 46 36 27
f 38 27 36
f 34 45 35
f 45 44 47
f 21 43 22
f 41 22 43
f 32 40 39
f 21 32 39
f 34 23 42
f 22 42 23
f 32 26 40
f 25 40 26
f 27 28 46
f 29 46 28
f 48 49 50
f 48 51 49
f 52 49 51
f 53 49 52
f 54 49 53
f 55 56 57
f 58 56 55
f 59 56 58
f 50 60 48
f 61 57 56
f 60 57 61
f 61 48 60
f 49 54 17
f 57 14 20
f 55 57 20
f 57 60 14
f 60 50 15
f 60 15 14
f 50 49 17
f 50 17 15
f 45 47 61
f 45 61 56
f 52 51 44
f 20 35 59
f 20 59 58
f 54 53 17
f 44 17 53
f 44 53 52
f 58 55 20
f 48 61 47
f 56 59 35
f 56 35 45
f 51 48 47
f 51 47 44

11
tests/data/pyramid.obj Normal file
View file

@ -0,0 +1,11 @@
v 10.000000 10.000000 40.000000
v 0.000000 0.000000 0.000000
v 20.000000 0.000000 0.000000
v 20.000000 20.000000 0.000000
v 0.000000 20.000000 0.000000
f 1 2 3
f 1 4 5
f 4 2 5
f 2 4 3
f 4 1 3
f 5 2 1

204
tests/data/sloping_hole.obj Normal file
View file

@ -0,0 +1,204 @@
v -20.000000 -20.000000 -5.000000
v -20.000000 -20.000000 5.000000
v -20.000000 20.000000 -5.000000
v -20.000000 20.000000 5.000000
v 20.000000 -20.000000 -5.000000
v 20.000000 -20.000000 5.000000
v 4.462940 7.431450 -5.000000
v 20.000000 20.000000 -5.000000
v -19.142099 0.000000 -5.000000
v -18.833099 -2.079120 -5.000000
v -17.919500 -4.067370 -5.000000
v -16.441200 -5.877850 -5.000000
v -14.462900 -7.431450 -5.000000
v -12.071100 -8.660250 -5.000000
v -9.370160 -9.510560 -5.000000
v -3.521740 -9.945220 -5.000000
v -6.478260 -9.945220 -5.000000
v -0.629840 -9.510560 -5.000000
v 2.071070 -8.660250 -5.000000
v 6.441230 -5.877850 -5.000000
v 4.462940 -7.431450 -5.000000
v -12.071100 8.660250 -5.000000
v -9.370160 9.510560 -5.000000
v 7.919480 -4.067370 -5.000000
v 8.833100 -2.079120 -5.000000
v -6.478260 9.945220 -5.000000
v -0.629840 9.510560 -5.000000
v 2.071070 8.660250 -5.000000
v 9.142140 0.000000 -5.000000
v 8.833100 2.079120 -5.000000
v -3.521740 9.945220 -5.000000
v 7.919480 4.067370 -5.000000
v 6.441230 5.877850 -5.000000
v -14.462900 7.431450 -5.000000
v -16.441200 5.877850 -5.000000
v -17.919500 4.067370 -5.000000
v -18.833099 2.079120 -5.000000
v 20.000000 20.000000 5.000000
v 3.521740 -9.945220 5.000000
v -8.833100 -2.079120 5.000000
v -9.142140 0.000000 5.000000
v -8.833100 2.079120 5.000000
v 6.478260 -9.945220 5.000000
v -7.919480 4.067370 5.000000
v -6.441230 5.877850 5.000000
v -4.462940 7.431450 5.000000
v -2.071070 8.660250 5.000000
v 0.629840 9.510560 5.000000
v 12.071100 -8.660250 5.000000
v 9.370160 -9.510560 5.000000
v 3.521740 9.945220 5.000000
v 6.478260 9.945220 5.000000
v 9.370160 9.510560 5.000000
v 12.071100 8.660250 5.000000
v 14.462900 7.431450 5.000000
v 16.441200 -5.877850 5.000000
v 14.462900 -7.431450 5.000000
v 16.441200 5.877850 5.000000
v 17.919500 4.067370 5.000000
v 18.833099 -2.079120 5.000000
v 17.919500 -4.067370 5.000000
v 18.833099 2.079120 5.000000
v 19.142099 0.000000 5.000000
v 0.629840 -9.510560 5.000000
v -2.071070 -8.660250 5.000000
v -4.462940 -7.431450 5.000000
v -6.441230 -5.877850 5.000000
v -7.919480 -4.067370 5.000000
f 1 2 3
f 3 2 4
f 2 1 5
f 6 2 5
f 7 3 8
f 1 3 9
f 1 9 10
f 1 10 11
f 1 11 12
f 1 12 13
f 1 13 14
f 1 14 5
f 14 15 5
f 16 5 17
f 18 5 16
f 19 5 18
f 20 5 21
f 19 21 5
f 22 3 23
f 5 20 24
f 5 24 8
f 24 25 8
f 23 3 26
f 27 3 28
f 29 30 8
f 26 3 31
f 30 32 8
f 31 3 27
f 32 33 8
f 28 3 7
f 33 7 8
f 29 8 25
f 34 3 22
f 35 3 34
f 36 3 35
f 37 3 36
f 9 3 37
f 17 5 15
f 3 4 8
f 8 4 38
f 39 2 6
f 4 2 40
f 4 40 41
f 4 41 42
f 43 39 6
f 4 42 44
f 4 44 45
f 38 4 46
f 38 46 47
f 38 47 48
f 49 50 6
f 38 48 51
f 50 43 6
f 38 51 52
f 38 52 53
f 38 53 54
f 38 54 55
f 56 57 6
f 38 55 58
f 38 58 59
f 60 61 6
f 38 59 62
f 38 63 6
f 38 62 63
f 63 60 6
f 61 56 6
f 64 2 39
f 65 2 64
f 66 2 65
f 67 2 66
f 68 2 67
f 40 2 68
f 45 46 4
f 57 49 6
f 6 5 8
f 38 6 8
f 42 41 37
f 37 41 9
f 40 10 41
f 41 10 9
f 44 42 36
f 36 42 37
f 45 44 35
f 35 44 36
f 34 46 45
f 35 34 45
f 22 47 46
f 34 22 46
f 23 48 47
f 22 23 47
f 26 51 48
f 23 26 48
f 31 52 51
f 26 31 51
f 27 53 52
f 31 27 52
f 28 54 53
f 27 28 53
f 7 55 54
f 28 7 54
f 33 58 55
f 7 33 55
f 32 59 58
f 33 32 58
f 30 62 59
f 32 30 59
f 29 63 62
f 30 29 62
f 60 63 29
f 25 60 29
f 61 60 25
f 24 61 25
f 56 61 24
f 20 56 24
f 56 20 57
f 57 20 21
f 57 21 49
f 49 21 19
f 49 19 50
f 50 19 18
f 50 18 43
f 43 18 16
f 43 16 39
f 39 16 17
f 39 17 64
f 64 17 15
f 64 15 65
f 65 15 14
f 65 14 66
f 66 14 13
f 66 13 67
f 67 13 12
f 67 12 68
f 68 12 11
f 68 11 40
f 40 11 10

View file

@ -0,0 +1,14 @@
v 6.000589 -22.998209 0.000000
v 22.001024 -49.999874 0.000000
v -9.999578 -49.999870 0.000000
v 6.000714 -32.237164 28.001925
v 11.167055 -37.972702 18.960167
v 6.000602 -26.539246 10.732185
f 1 2 3
f 4 5 6
f 3 2 5
f 3 5 4
f 3 4 6
f 3 6 1
f 6 5 2
f 6 2 1

View file

@ -0,0 +1,144 @@
v 66.713348 104.286667 0.000000
v 66.713348 95.713333 0.000000
v 65.666687 94.666672 0.000000
v 75.286682 95.713333 0.000000
v 76.333344 105.333336 0.000000
v 76.333344 94.666672 0.000000
v 65.666687 105.333328 0.000000
v 75.286682 104.286667 0.000000
v 71.106682 104.586662 2.800000
v 66.413353 104.586662 2.800000
v 75.586685 104.586662 2.800000
v 66.413353 99.893333 2.800000
v 66.413353 95.413338 2.800000
v 71.106682 95.413338 2.800000
v 75.586685 95.413338 2.800000
v 75.586685 100.106667 2.800000
v 74.540016 103.540001 2.800000
v 70.032013 103.540001 2.800000
v 67.460007 103.540001 2.800000
v 67.460007 100.968002 2.800000
v 67.460007 96.459999 2.800000
v 74.540016 99.031998 2.800000
v 74.540016 96.459999 2.800000
v 70.032013 96.459999 2.800000
v 123.666718 94.666672 0.000000
v 134.333313 94.666672 0.000000
v 124.413361 95.413338 2.800000
v 129.106674 95.413338 2.800000
v 133.586670 95.413338 2.800000
v 123.666718 105.333328 0.000000
v 124.413361 104.586662 2.800000
v 124.413361 99.893333 2.800000
v 134.333313 105.333328 0.000000
v 129.106674 104.586662 2.800000
v 133.586670 104.586662 2.800000
v 133.586670 100.106667 2.800000
v 124.713318 104.286667 0.000000
v 124.713318 95.713333 0.000000
v 133.286713 95.713333 0.000000
v 133.286713 104.286667 0.000000
v 132.540024 103.540001 2.800000
v 128.032028 103.540009 2.800000
v 125.460007 103.540001 2.800000
v 125.460007 100.968002 2.800000
v 125.460007 96.459999 2.800000
v 132.540024 99.031998 2.800000
v 132.540024 96.459999 2.800000
v 128.032028 96.459999 2.800000
f 1 2 3
f 4 5 6
f 7 5 1
f 7 1 3
f 3 2 6
f 8 5 4
f 2 4 6
f 1 5 8
f 5 7 9
f 7 10 9
f 5 9 11
f 7 3 10
f 3 12 10
f 3 13 12
f 3 6 13
f 6 14 13
f 6 15 14
f 5 11 16
f 6 5 15
f 5 16 15
f 8 17 18
f 1 8 19
f 8 18 19
f 2 20 21
f 2 1 20
f 1 19 20
f 8 4 22
f 4 23 22
f 8 22 17
f 4 24 23
f 4 2 24
f 2 21 24
f 25 26 27
f 26 28 27
f 26 29 28
f 30 25 31
f 25 32 31
f 25 27 32
f 33 30 34
f 30 31 34
f 33 34 35
f 33 35 36
f 26 33 29
f 33 36 29
f 37 38 25
f 39 33 26
f 30 33 37
f 30 37 25
f 25 38 26
f 40 33 39
f 38 39 26
f 37 33 40
f 40 41 42
f 37 40 43
f 40 42 43
f 38 44 45
f 38 37 44
f 37 43 44
f 40 39 46
f 39 47 46
f 40 46 41
f 39 48 47
f 39 38 48
f 38 45 48
f 17 9 10
f 17 11 9
f 11 17 16
f 16 17 22
f 23 16 22
f 16 23 15
f 23 24 15
f 24 21 15
f 18 17 10
f 19 18 10
f 20 19 10
f 20 10 12
f 20 12 21
f 14 15 21
f 21 12 13
f 14 21 13
f 42 41 31
f 43 42 31
f 44 43 31
f 44 31 32
f 44 32 45
f 28 29 45
f 45 32 27
f 28 45 27
f 41 34 31
f 41 35 34
f 35 41 36
f 36 41 46
f 47 36 46
f 36 47 29
f 47 48 29
f 48 45 29

View file

@ -0,0 +1,5 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests.cpp tests_main.cpp)
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r ${Boost_LIBRARIES} ${TBB_LIBRARIES} ${Boost_LIBRARIES})
catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")

View file

@ -0,0 +1,647 @@
#include <catch2/catch.hpp>
#include <unordered_set>
#include <unordered_map>
#include <random>
// Debug
#include <fstream>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Format/OBJ.hpp"
#include "libslic3r/SLAPrint.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/SLA/SLAPad.hpp"
#include "libslic3r/SLA/SLASupportTreeBuilder.hpp"
#include "libslic3r/SLA/SLASupportTreeBuildsteps.hpp"
#include "libslic3r/SLA/SLAAutoSupports.hpp"
#include "libslic3r/SLA/SLARaster.hpp"
#include "libslic3r/MTUtils.hpp"
#include "libslic3r/SVG.hpp"
#include "libslic3r/Format/OBJ.hpp"
#if defined(WIN32) || defined(_WIN32)
#define PATH_SEPARATOR R"(\)"
#else
#define PATH_SEPARATOR R"(/)"
#endif
namespace {
using namespace Slic3r;
TriangleMesh load_model(const std::string &obj_filename)
{
TriangleMesh mesh;
auto fpath = TEST_DATA_DIR PATH_SEPARATOR + obj_filename;
load_obj(fpath.c_str(), &mesh);
return mesh;
}
enum e_validity {
ASSUME_NO_EMPTY = 1,
ASSUME_MANIFOLD = 2,
ASSUME_NO_REPAIR = 4
};
void check_validity(const TriangleMesh &input_mesh,
int flags = ASSUME_NO_EMPTY | ASSUME_MANIFOLD |
ASSUME_NO_REPAIR)
{
TriangleMesh mesh{input_mesh};
if (flags & ASSUME_NO_EMPTY) {
REQUIRE_FALSE(mesh.empty());
} else if (mesh.empty())
return; // If it can be empty and it is, there is nothing left to do.
REQUIRE(stl_validate(&mesh.stl));
bool do_update_shared_vertices = false;
mesh.repair(do_update_shared_vertices);
if (flags & ASSUME_NO_REPAIR) {
REQUIRE_FALSE(mesh.needed_repair());
}
if (flags & ASSUME_MANIFOLD) {
mesh.require_shared_vertices();
if (!mesh.is_manifold()) mesh.WriteOBJFile("non_manifold.obj");
REQUIRE(mesh.is_manifold());
}
}
struct PadByproducts
{
ExPolygons model_contours;
ExPolygons support_contours;
TriangleMesh mesh;
};
void test_pad(const std::string & obj_filename,
const sla::PadConfig &padcfg,
PadByproducts & out)
{
REQUIRE(padcfg.validate().empty());
TriangleMesh mesh = load_model(obj_filename);
REQUIRE_FALSE(mesh.empty());
// Create pad skeleton only from the model
Slic3r::sla::pad_blueprint(mesh, out.model_contours);
REQUIRE_FALSE(out.model_contours.empty());
// Create the pad geometry for the model contours only
Slic3r::sla::create_pad({}, out.model_contours, out.mesh, padcfg);
check_validity(out.mesh);
auto bb = out.mesh.bounding_box();
REQUIRE(bb.max.z() - bb.min.z() == Approx(padcfg.full_height()));
}
void test_pad(const std::string & obj_filename,
const sla::PadConfig &padcfg = {})
{
PadByproducts byproducts;
test_pad(obj_filename, padcfg, byproducts);
}
struct SupportByproducts
{
std::string obj_fname;
std::vector<float> slicegrid;
std::vector<ExPolygons> model_slices;
sla::SupportTreeBuilder supporttree;
TriangleMesh input_mesh;
};
const constexpr float CLOSING_RADIUS = 0.005f;
void check_support_tree_integrity(const sla::SupportTreeBuilder &stree,
const sla::SupportConfig &cfg)
{
double gnd = stree.ground_level;
double H1 = cfg.max_solo_pillar_height_mm;
double H2 = cfg.max_dual_pillar_height_mm;
for (const sla::Head &head : stree.heads()) {
REQUIRE((!head.is_valid() || head.pillar_id != sla::ID_UNSET ||
head.bridge_id != sla::ID_UNSET));
}
for (const sla::Pillar &pillar : stree.pillars()) {
if (std::abs(pillar.endpoint().z() - gnd) < EPSILON) {
double h = pillar.height;
if (h > H1) REQUIRE(pillar.links >= 1);
else if(h > H2) { REQUIRE(pillar.links >= 2); }
}
REQUIRE(pillar.links <= cfg.pillar_cascade_neighbors);
REQUIRE(pillar.bridges <= cfg.max_bridges_on_pillar);
}
double max_bridgelen = 0.;
auto chck_bridge = [&cfg](const sla::Bridge &bridge, double &max_brlen) {
Vec3d n = bridge.endp - bridge.startp;
double d = sla::distance(n);
max_brlen = std::max(d, max_brlen);
double z = n.z();
double polar = std::acos(z / d);
double slope = -polar + PI / 2.;
REQUIRE(std::abs(slope) >= cfg.bridge_slope - EPSILON);
};
for (auto &bridge : stree.bridges()) chck_bridge(bridge, max_bridgelen);
REQUIRE(max_bridgelen <= cfg.max_bridge_length_mm);
max_bridgelen = 0;
for (auto &bridge : stree.crossbridges()) chck_bridge(bridge, max_bridgelen);
double md = cfg.max_pillar_link_distance_mm / std::cos(-cfg.bridge_slope);
REQUIRE(max_bridgelen <= md);
}
void test_supports(const std::string & obj_filename,
const sla::SupportConfig &supportcfg,
SupportByproducts & out)
{
using namespace Slic3r;
TriangleMesh mesh = load_model(obj_filename);
REQUIRE_FALSE(mesh.empty());
TriangleMeshSlicer slicer{&mesh};
auto bb = mesh.bounding_box();
double zmin = bb.min.z();
double zmax = bb.max.z();
double gnd = zmin - supportcfg.object_elevation_mm;
auto layer_h = 0.05f;
out.slicegrid = grid(float(gnd), float(zmax), layer_h);
slicer.slice(out.slicegrid , CLOSING_RADIUS, &out.model_slices, []{});
// Create the special index-triangle mesh with spatial indexing which
// is the input of the support point and support mesh generators
sla::EigenMesh3D emesh{mesh};
// Create the support point generator
sla::SLAAutoSupports::Config autogencfg;
autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm);
sla::SLAAutoSupports point_gen{emesh, out.model_slices, out.slicegrid,
autogencfg, [] {}, [](int) {}};
// Get the calculated support points.
std::vector<sla::SupportPoint> support_points = point_gen.output();
int validityflags = ASSUME_NO_REPAIR;
// If there is no elevation, support points shall be removed from the
// bottom of the object.
if (std::abs(supportcfg.object_elevation_mm) < EPSILON) {
sla::remove_bottom_points(support_points, zmin,
supportcfg.base_height_mm);
} else {
// Should be support points at least on the bottom of the model
REQUIRE_FALSE(support_points.empty());
// Also the support mesh should not be empty.
validityflags |= ASSUME_NO_EMPTY;
}
// Generate the actual support tree
sla::SupportTreeBuilder treebuilder;
treebuilder.build(sla::SupportableMesh{emesh, support_points, supportcfg});
check_support_tree_integrity(treebuilder, supportcfg);
const TriangleMesh &output_mesh = treebuilder.retrieve_mesh();
check_validity(output_mesh, validityflags);
// Quick check if the dimensions and placement of supports are correct
auto obb = output_mesh.bounding_box();
double allowed_zmin = zmin - supportcfg.object_elevation_mm;
if (std::abs(supportcfg.object_elevation_mm) < EPSILON)
allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm;
REQUIRE(obb.min.z() >= allowed_zmin);
REQUIRE(obb.max.z() <= zmax);
// Move out the support tree into the byproducts, we can examine it further
// in various tests.
out.obj_fname = std::move(obj_filename);
out.supporttree = std::move(treebuilder);
out.input_mesh = std::move(mesh);
}
void test_supports(const std::string & obj_filename,
const sla::SupportConfig &supportcfg = {})
{
SupportByproducts byproducts;
test_supports(obj_filename, supportcfg, byproducts);
}
void export_failed_case(const std::vector<ExPolygons> &support_slices,
const SupportByproducts &byproducts)
{
for (size_t n = 0; n < support_slices.size(); ++n) {
const ExPolygons &sup_slice = support_slices[n];
const ExPolygons &mod_slice = byproducts.model_slices[n];
Polygons intersections = intersection(sup_slice, mod_slice);
std::stringstream ss;
if (!intersections.empty()) {
ss << byproducts.obj_fname << std::setprecision(4) << n << ".svg";
SVG svg(ss.str());
svg.draw(sup_slice, "green");
svg.draw(mod_slice, "blue");
svg.draw(intersections, "red");
svg.Close();
}
}
TriangleMesh m;
byproducts.supporttree.retrieve_full_mesh(m);
m.merge(byproducts.input_mesh);
m.repair();
m.require_shared_vertices();
m.WriteOBJFile(byproducts.obj_fname.c_str());
}
void test_support_model_collision(
const std::string & obj_filename,
const sla::SupportConfig &input_supportcfg = {})
{
SupportByproducts byproducts;
sla::SupportConfig supportcfg = input_supportcfg;
// Set head penetration to a small negative value which should ensure that
// the supports will not touch the model body.
supportcfg.head_penetration_mm = -0.15;
// TODO: currently, the tailheads penetrating into the model body do not
// respect the penetration parameter properly. No issues were reported so
// far but we should definitely fix this.
supportcfg.ground_facing_only = true;
test_supports(obj_filename, supportcfg, byproducts);
// Slice the support mesh given the slice grid of the model.
std::vector<ExPolygons> support_slices =
byproducts.supporttree.slice(byproducts.slicegrid, CLOSING_RADIUS);
// The slices originate from the same slice grid so the numbers must match
bool support_mesh_is_empty =
byproducts.supporttree.retrieve_mesh(sla::MeshType::Pad).empty() &&
byproducts.supporttree.retrieve_mesh(sla::MeshType::Support).empty();
if (support_mesh_is_empty)
REQUIRE(support_slices.empty());
else
REQUIRE(support_slices.size() == byproducts.model_slices.size());
bool notouch = true;
for (size_t n = 0; notouch && n < support_slices.size(); ++n) {
const ExPolygons &sup_slice = support_slices[n];
const ExPolygons &mod_slice = byproducts.model_slices[n];
Polygons intersections = intersection(sup_slice, mod_slice);
notouch = notouch && intersections.empty();
}
if (!notouch) export_failed_case(support_slices, byproducts);
REQUIRE(notouch);
}
const char * const BELOW_PAD_TEST_OBJECTS[] = {
"20mm_cube.obj",
"V.obj",
};
const char * const AROUND_PAD_TEST_OBJECTS[] = {
"20mm_cube.obj",
"V.obj",
"frog_legs.obj",
"cube_with_concave_hole_enlarged.obj",
};
const char *const SUPPORT_TEST_MODELS[] = {
"cube_with_concave_hole_enlarged_standing.obj",
"A_upsidedown.obj",
"extruder_idler.obj"
};
} // namespace
// Test pair hash for 'nums' random number pairs.
template <class I, class II> void test_pairhash()
{
const constexpr size_t nums = 1000;
I A[nums] = {0}, B[nums] = {0};
std::unordered_set<I> CH;
std::unordered_map<II, std::pair<I, I>> ints;
std::random_device rd;
std::mt19937 gen(rd());
const I Ibits = int(sizeof(I) * CHAR_BIT);
const II IIbits = int(sizeof(II) * CHAR_BIT);
const int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits;
const I Imax = I(std::pow(2., bits) - 1);
std::uniform_int_distribution<I> dis(0, Imax);
for (size_t i = 0; i < nums;) {
I a = dis(gen);
if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; }
}
for (size_t i = 0; i < nums;) {
I b = dis(gen);
if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; }
}
for (size_t i = 0; i < nums; ++i) {
I a = A[i], b = B[i];
REQUIRE(a != b);
II hash_ab = sla::pairhash<I, II>(a, b);
II hash_ba = sla::pairhash<I, II>(b, a);
REQUIRE(hash_ab == hash_ba);
auto it = ints.find(hash_ab);
if (it != ints.end()) {
REQUIRE((
(it->second.first == a && it->second.second == b) ||
(it->second.first == b && it->second.second == a)
));
} else
ints[hash_ab] = std::make_pair(a, b);
}
}
TEST_CASE("Pillar pairhash should be unique", "[SLASupportGeneration]") {
test_pairhash<int, long>();
test_pairhash<unsigned, unsigned>();
test_pairhash<unsigned, unsigned long>();
}
TEST_CASE("Flat pad geometry is valid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Disable wings
padcfg.wall_height_mm = .0;
for (auto &fname : BELOW_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("WingedPadGeometryIsValid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Add some wings to the pad to test the cavity
padcfg.wall_height_mm = 1.;
for (auto &fname : BELOW_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("FlatPadAroundObjectIsValid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Add some wings to the pad to test the cavity
padcfg.wall_height_mm = 0.;
// padcfg.embed_object.stick_stride_mm = 0.;
padcfg.embed_object.enabled = true;
padcfg.embed_object.everywhere = true;
for (auto &fname : AROUND_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("WingedPadAroundObjectIsValid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Add some wings to the pad to test the cavity
padcfg.wall_height_mm = 1.;
padcfg.embed_object.enabled = true;
padcfg.embed_object.everywhere = true;
for (auto &fname : AROUND_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("ElevatedSupportGeometryIsValid", "[SLASupportGeneration]") {
sla::SupportConfig supportcfg;
supportcfg.object_elevation_mm = 5.;
for (auto fname : SUPPORT_TEST_MODELS) test_supports(fname);
}
TEST_CASE("FloorSupportGeometryIsValid", "[SLASupportGeneration]") {
sla::SupportConfig supportcfg;
supportcfg.object_elevation_mm = 0;
for (auto &fname: SUPPORT_TEST_MODELS) test_supports(fname, supportcfg);
}
TEST_CASE("ElevatedSupportsDoNotPierceModel", "[SLASupportGeneration]") {
sla::SupportConfig supportcfg;
for (auto fname : SUPPORT_TEST_MODELS)
test_support_model_collision(fname, supportcfg);
}
TEST_CASE("FloorSupportsDoNotPierceModel", "[SLASupportGeneration]") {
sla::SupportConfig supportcfg;
supportcfg.object_elevation_mm = 0;
for (auto fname : SUPPORT_TEST_MODELS)
test_support_model_collision(fname, supportcfg);
}
TEST_CASE("DefaultRasterShouldBeEmpty", "[SLARasterOutput]") {
sla::Raster raster;
REQUIRE(raster.empty());
}
TEST_CASE("InitializedRasterShouldBeNONEmpty", "[SLARasterOutput]") {
// Default Prusa SL1 display parameters
sla::Raster::Resolution res{2560, 1440};
sla::Raster::PixelDim pixdim{120. / res.width_px, 68. / res.height_px};
sla::Raster raster;
raster.reset(res, pixdim);
REQUIRE_FALSE(raster.empty());
REQUIRE(raster.resolution().width_px == res.width_px);
REQUIRE(raster.resolution().height_px == res.height_px);
REQUIRE(raster.pixel_dimensions().w_mm == Approx(pixdim.w_mm));
REQUIRE(raster.pixel_dimensions().h_mm == Approx(pixdim.h_mm));
}
using TPixel = uint8_t;
static constexpr const TPixel FullWhite = 255;
static constexpr const TPixel FullBlack = 0;
template <class A, int N> constexpr int arraysize(const A (&)[N]) { return N; }
static void check_raster_transformations(sla::Raster::Orientation o,
sla::Raster::TMirroring mirroring)
{
double disp_w = 120., disp_h = 68.;
sla::Raster::Resolution res{2560, 1440};
sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
sla::Raster::Trafo trafo{o, mirroring};
trafo.origin_x = bb.center().x();
trafo.origin_y = bb.center().y();
sla::Raster raster{res, pixdim, trafo};
// create box of size 32x32 pixels (not 1x1 to avoid antialiasing errors)
coord_t pw = 32 * coord_t(std::ceil(scaled<double>(pixdim.w_mm)));
coord_t ph = 32 * coord_t(std::ceil(scaled<double>(pixdim.h_mm)));
ExPolygon box;
box.contour.points = {{-pw, -ph}, {pw, -ph}, {pw, ph}, {-pw, ph}};
double tr_x = scaled<double>(20.), tr_y = tr_x;
box.translate(tr_x, tr_y);
ExPolygon expected_box = box;
// Now calculate the position of the translated box according to output
// trafo.
if (o == sla::Raster::Orientation::roPortrait) expected_box.rotate(PI / 2.);
if (mirroring[X])
for (auto &p : expected_box.contour.points) p.x() = -p.x();
if (mirroring[Y])
for (auto &p : expected_box.contour.points) p.y() = -p.y();
raster.draw(box);
Point expected_coords = expected_box.contour.bounding_box().center();
double rx = unscaled(expected_coords.x() + bb.center().x()) / pixdim.w_mm;
double ry = unscaled(expected_coords.y() + bb.center().y()) / pixdim.h_mm;
auto w = size_t(std::floor(rx));
auto h = res.height_px - size_t(std::floor(ry));
REQUIRE((w < res.width_px && h < res.height_px));
auto px = raster.read_pixel(w, h);
if (px != FullWhite) {
sla::PNGImage img;
std::fstream outf("out.png", std::ios::out);
outf << img.serialize(raster);
}
REQUIRE(px == FullWhite);
}
TEST_CASE("MirroringShouldBeCorrect", "[SLARasterOutput]") {
sla::Raster::TMirroring mirrorings[] = {sla::Raster::NoMirror,
sla::Raster::MirrorX,
sla::Raster::MirrorY,
sla::Raster::MirrorXY};
sla::Raster::Orientation orientations[] = {sla::Raster::roLandscape,
sla::Raster::roPortrait};
for (auto orientation : orientations)
for (auto &mirror : mirrorings)
check_raster_transformations(orientation, mirror);
}
static ExPolygon square_with_hole(double v)
{
ExPolygon poly;
coord_t V = scaled(v / 2.);
poly.contour.points = {{-V, -V}, {V, -V}, {V, V}, {-V, V}};
poly.holes.emplace_back();
V = V / 2;
poly.holes.front().points = {{-V, V}, {V, V}, {V, -V}, {-V, -V}};
return poly;
}
static double pixel_area(TPixel px, const sla::Raster::PixelDim &pxdim)
{
return (pxdim.h_mm * pxdim.w_mm) * px * 1. / (FullWhite - FullBlack);
}
static double raster_white_area(const sla::Raster &raster)
{
if (raster.empty()) return std::nan("");
auto res = raster.resolution();
double a = 0;
for (size_t x = 0; x < res.width_px; ++x)
for (size_t y = 0; y < res.height_px; ++y) {
auto px = raster.read_pixel(x, y);
a += pixel_area(px, raster.pixel_dimensions());
}
return a;
}
static double predict_error(const ExPolygon &p, const sla::Raster::PixelDim &pd)
{
auto lines = p.lines();
double pix_err = pixel_area(FullWhite, pd) / 2.;
// Worst case is when a line is parallel to the shorter axis of one pixel,
// when the line will be composed of the max number of pixels
double pix_l = std::min(pd.h_mm, pd.w_mm);
double error = 0.;
for (auto &l : lines)
error += (unscaled(l.length()) / pix_l) * pix_err;
return error;
}
TEST_CASE("RasterizedPolygonAreaShouldMatch", "[SLARasterOutput]") {
double disp_w = 120., disp_h = 68.;
sla::Raster::Resolution res{2560, 1440};
sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
sla::Raster raster{res, pixdim};
auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
ExPolygon poly = square_with_hole(10.);
poly.translate(bb.center().x(), bb.center().y());
raster.draw(poly);
double a = poly.area() / (scaled<double>(1.) * scaled(1.));
double ra = raster_white_area(raster);
double diff = std::abs(a - ra);
REQUIRE(diff <= predict_error(poly, pixdim));
raster.clear();
poly = square_with_hole(60.);
poly.translate(bb.center().x(), bb.center().y());
raster.draw(poly);
a = poly.area() / (scaled<double>(1.) * scaled(1.));
ra = raster_white_area(raster);
diff = std::abs(a - ra);
REQUIRE(diff <= predict_error(poly, pixdim));
}

View file

@ -0,0 +1,2 @@
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>