Deeper test coverage for support tree generation.

Restructuring for testability.
This commit is contained in:
tamasmeszaros 2019-09-26 09:42:08 +02:00
parent 277f6786d8
commit 705e82ec8e
14 changed files with 2884 additions and 2674 deletions

View File

@ -178,6 +178,11 @@ add_library(libslic3r STATIC
SLA/SLABoilerPlate.hpp
SLA/SLAPad.hpp
SLA/SLAPad.cpp
SLA/SLASupportTreeBuilder.hpp
SLA/SLASupportTreeAlgorithm.hpp
SLA/SLASupportTreeAlgorithm.cpp
SLA/SLASupportTreeBuilder.cpp
SLA/SLAConcurrency.hpp
SLA/SLASupportTree.hpp
SLA/SLASupportTree.cpp
SLA/SLASupportTreeIGL.cpp

View File

@ -1,8 +1,9 @@
#ifndef SLACOMMON_HPP
#define SLACOMMON_HPP
#include <Eigen/Geometry>
#include <memory>
#include <vector>
#include <Eigen/Geometry>
// #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
/// alternative (raw) input format for the SLASupportTree
class EigenMesh3D {

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 = false;
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

@ -10,7 +10,7 @@
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 = ");
out += projectname + "\n";
@ -21,7 +21,7 @@ std::string SLARasterWriter::createIniContent(const std::string& projectname) co
return out;
}
void SLARasterWriter::flpXY(ClipperLib::Polygon &poly)
void RasterWriter::flpXY(ClipperLib::Polygon &poly)
{
for(auto& p : poly.Contour) std::swap(p.X, p.Y);
std::reverse(poly.Contour.begin(), poly.Contour.end());
@ -32,7 +32,7 @@ void SLARasterWriter::flpXY(ClipperLib::Polygon &poly)
}
}
void SLARasterWriter::flpXY(ExPolygon &poly)
void RasterWriter::flpXY(ExPolygon &poly)
{
for(auto& p : poly.contour.points) p = Point(p.y(), p.x());
std::reverse(poly.contour.points.begin(), poly.contour.points.end());
@ -43,7 +43,7 @@ void SLARasterWriter::flpXY(ExPolygon &poly)
}
}
SLARasterWriter::SLARasterWriter(const Raster::Resolution &res,
RasterWriter::RasterWriter(const Raster::Resolution &res,
const Raster::PixelDim &pixdim,
const std::array<bool, 2> &mirror,
double gamma)
@ -53,7 +53,7 @@ SLARasterWriter::SLARasterWriter(const Raster::Resolution &res,
m_mirror[1] = !m_mirror[1];
}
void SLARasterWriter::save(const std::string &fpath, const std::string &prjname)
void RasterWriter::save(const std::string &fpath, const std::string &prjname)
{
try {
Zipper zipper(fpath); // zipper with no compression
@ -103,7 +103,7 @@ std::string get_cfg_value(const DynamicPrintConfig &cfg, const std::string &key)
} // 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["expTime"] = get_cfg_value(cfg, "exposure_time");
@ -118,7 +118,7 @@ void SLARasterWriter::set_config(const DynamicPrintConfig &cfg)
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["numFade"] = std::to_string(stats.num_fade);

View File

@ -22,7 +22,7 @@ namespace Slic3r { namespace sla {
// 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
// write out the result into a zipped archive.
class SLARasterWriter
class RasterWriter
{
public:
enum Orientation {
@ -73,15 +73,15 @@ private:
static void flpXY(ExPolygon& poly);
public:
SLARasterWriter(const Raster::Resolution &res,
RasterWriter(const Raster::Resolution &res,
const Raster::PixelDim &pixdim,
const std::array<bool, 2> &mirror,
double gamma = 1.);
SLARasterWriter(const SLARasterWriter& ) = delete;
SLARasterWriter& operator=(const SLARasterWriter&) = delete;
SLARasterWriter(SLARasterWriter&& m) = default;
SLARasterWriter& operator=(SLARasterWriter&&) = default;
RasterWriter(const RasterWriter& ) = delete;
RasterWriter& operator=(const RasterWriter&) = delete;
RasterWriter(RasterWriter&& m) = default;
RasterWriter& operator=(RasterWriter&&) = default;
inline void layers(unsigned cnt) { if(cnt > 0) m_layers_rst.resize(cnt); }
inline unsigned layers() const { return unsigned(m_layers_rst.size()); }

File diff suppressed because it is too large Load Diff

View File

@ -2,23 +2,14 @@
#define SLASUPPORTTREE_HPP
#include <vector>
#include <array>
#include <cstdint>
#include <memory>
#include <Eigen/Geometry>
#include "SLACommon.hpp"
#include "SLAPad.hpp"
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 Model;
class ModelInstance;
@ -31,13 +22,17 @@ using ExPolygons = std::vector<ExPolygon>;
namespace sla {
enum class PillarConnectionMode {
enum class PillarConnectionMode
{
zigzag,
cross,
dynamic
};
struct SupportConfig {
struct SupportConfig
{
bool enabled = true;
// Radius in mm of the pointing side of the head.
double head_front_radius_mm = 0.2;
@ -108,93 +103,78 @@ struct SupportConfig {
static const unsigned max_bridges_on_pillar;
};
struct PadConfig;
enum class MeshType { Support, Pad };
/// A Control structure for the support calculation. Consists of the status
/// 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
std::function<void(unsigned, const std::string&)> statuscb =
[](unsigned, const std::string&){};
StatusFn statuscb = [](unsigned, const std::string&){};
// 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
// if true, throw an appropriate exception. (TriangleMeshSlicer needs this)
// consider it a hard abort. stopcondition is permits the algorithm to
// terminate itself
std::function<void(void)> cancelfn = [](){};
CancelFn cancelfn = [](){};
};
/* ************************************************************************** */
struct SupportableMesh
{
EigenMesh3D emesh;
SupportPoints pts;
SupportConfig cfg;
explicit SupportableMesh(const TriangleMesh & trmsh,
const SupportPoints &sp,
const SupportConfig &c)
: emesh{trmsh}, pts{sp}, cfg{c}
{}
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.
class SLASupportTree {
class Impl; // persistent support data
std::unique_ptr<Impl> m_impl;
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 = {});
class SupportTree
{
JobController m_ctl;
public:
SLASupportTree(double ground_level = 0.0);
SLASupportTree(const std::vector<SupportPoint>& pts,
const EigenMesh3D& em,
const SupportConfig& cfg = {},
const Controller& ctl = {});
using UPtr = std::unique_ptr<SupportTree>;
SLASupportTree(const SLASupportTree&) = delete;
SLASupportTree& operator=(const SLASupportTree&) = delete;
SLASupportTree(SLASupportTree &&o);
SLASupportTree &operator=(SLASupportTree &&o);
static UPtr create(const SupportableMesh &input,
const JobController &ctl = {});
~SLASupportTree();
virtual ~SupportTree() = default;
/// Get the whole mesh united into the output TriangleMesh
/// WITHOUT THE PAD
const TriangleMesh& merged_mesh() const;
virtual const TriangleMesh &retrieve_mesh(MeshType meshtype) const = 0;
void merged_mesh_with_pad(TriangleMesh&) const;
std::vector<ExPolygons> slice(const std::vector<float> &,
float closing_radius) const;
/// Adding the "pad" (base pool) under the supports
/// Adding the "pad" under the supports.
/// 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
/// from the supports.
const TriangleMesh& add_pad(const ExPolygons& modelbase,
const PadConfig& pcfg) const;
/// Get the pad geometry
const TriangleMesh& get_pad() const;
void remove_pad();
virtual const TriangleMesh &add_pad(const ExPolygons &modelbase,
const PadConfig & pcfg) = 0;
virtual void remove_pad() = 0;
std::vector<ExPolygons> slice(const std::vector<float> &,
float closing_radius) const;
void retrieve_full_mesh(TriangleMesh &outmesh) const;
const JobController &ctl() const { return m_ctl; }
};
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,292 @@
#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 SupportTreeAlgorithm {
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;
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);
SupportTreeAlgorithm(SupportTreeBuilder & builder, const SupportableMesh &sm);
public:
SupportTreeAlgorithm(const SupportTreeAlgorithm &) = delete;
SupportTreeAlgorithm(SupportTreeAlgorithm &&) = delete;
SupportTreeAlgorithm& operator=(const SupportTreeAlgorithm &) = delete;
SupportTreeAlgorithm& operator=(SupportTreeAlgorithm &&) = delete;
// 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

@ -0,0 +1,462 @@
#include "SLASupportTreeBuilder.hpp"
#include "SLASupportTreeAlgorithm.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)
{
// 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;
}
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 SupportTreeAlgorithm::execute(*this, sm);
}
}
}

View File

@ -0,0 +1,449 @@
#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;
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;
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> Pillar& add_pillar(unsigned headid, Args&&... args)
{
std::lock_guard<Mutex> lk(m_mutex);
assert(headid < m_head_indices.size());
Head &head = m_heads[m_head_indices[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 m_pillars.back();
}
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++;
}
template<class...Args> Pillar& add_pillar(Args&&...args)
{
std::lock_guard<Mutex> lk(m_mutex);
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 m_pillars.back();
}
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();
}
template<class...Args> const Bridge& add_bridge(Args&&... args)
{
return _add_bridge(m_bridges, std::forward<Args>(args)...);
}
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)];
}
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

View File

@ -32,17 +32,19 @@
namespace Slic3r {
using SupportTreePtr = std::unique_ptr<sla::SLASupportTree>;
class SLAPrintObject::SupportData
class SLAPrintObject::SupportData : public sla::SupportableMesh
{
public:
sla::EigenMesh3D emesh; // index-triangle representation
std::vector<sla::SupportPoint> support_points; // all the support points (manual/auto)
SupportTreePtr support_tree_ptr; // the supports
sla::SupportTree::UPtr support_tree_ptr; // the 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 {
@ -583,7 +585,8 @@ bool is_zero_elevation(const SLAPrintObjectConfig &c) {
// Compile the argument for support creation from the static print config.
sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c) {
sla::SupportConfig scfg;
scfg.enabled = c.supports_enable.getBool();
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_penetration_mm = c.support_head_penetration.getFloat();
@ -890,10 +893,10 @@ void SLAPrint::process()
// Now let's extract the result.
const std::vector<sla::SupportPoint>& points = auto_supports.output();
this->throw_if_canceled();
po.m_supportdata->support_points = points;
po.m_supportdata->pts = 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
// the update status to GLGizmoSlaSupports
@ -905,7 +908,7 @@ void SLAPrint::process()
else {
// There are either some points on the front-end, or the user
// 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
@ -915,7 +918,7 @@ void SLAPrint::process()
? po.m_config.pad_wall_thickness.getFloat()
: po.m_config.support_base_height.getFloat();
remove_bottom_points(po.m_supportdata->support_points,
remove_bottom_points(po.m_supportdata->pts,
po.m_supportdata->emesh.ground_level(),
tolerance);
}
@ -929,42 +932,28 @@ void SLAPrint::process()
sla::PadConfig pcfg = make_pad_cfg(po.m_config);
if (pcfg.embed_object)
po.m_supportdata->emesh.ground_level_offset(
pcfg.wall_thickness_mm);
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;
po.m_supportdata->emesh.ground_level_offset(pcfg.wall_thickness_mm);
po.m_supportdata->cfg = make_support_cfg(po.m_config);
// scaling for the sub operations
double d = ostepd * OBJ_STEP_LEVELS[slaposSupportTree] / 100.0;
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;
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,
OBJ_STEP_LABELS(slaposSupportTree),
SlicingStatus::DEFAULT,
logmsg);
SlicingStatus::DEFAULT, logmsg);
};
ctl.stopcondition = [this](){ return canceled(); };
ctl.stopcondition = [this]() { return canceled(); };
ctl.cancelfn = [this]() { throw_if_canceled(); };
po.m_supportdata->support_tree_ptr.reset(
new SLASupportTree(po.m_supportdata->support_points,
po.m_supportdata->emesh, scfg, ctl));
po.m_supportdata->create_support_tree(ctl);
if (!po.m_config.supports_enable.getBool()) return;
throw_if_canceled();
@ -973,10 +962,9 @@ void SLAPrint::process()
// This is to prevent "Done." being displayed during merged_mesh()
m_report_status(*this, -1, L("Visualizing supports"));
po.m_supportdata->support_tree_ptr->merged_mesh();
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.
if(po.support_mesh().empty())
@ -1014,7 +1002,7 @@ void SLAPrint::process()
}
po.m_supportdata->support_tree_ptr->add_pad(bp, pcfg);
auto &pad_mesh = po.m_supportdata->support_tree_ptr->get_pad();
auto &pad_mesh = po.m_supportdata->support_tree_ptr->retrieve_mesh(MeshType::Pad);
if (!validate_pad(pad_mesh, pcfg))
throw std::runtime_error(
@ -1393,7 +1381,7 @@ void SLAPrint::process()
if(canceled()) return;
// 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());
printer.layers(lvlcnt);
@ -1456,7 +1444,7 @@ void SLAPrint::process()
tbb::parallel_for<unsigned, decltype(lvlfn)>(0, lvlcnt, lvlfn);
// Set statistics values to the printer
sla::SLARasterWriter::PrintStatistics stats;
sla::RasterWriter::PrintStatistics stats;
stats.used_material = (m_print_statistics.objects_used_material +
m_print_statistics.support_used_material) /
1000;
@ -1651,7 +1639,7 @@ bool SLAPrint::invalidate_state_by_config_options(const std::vector<t_config_opt
return invalidated;
}
sla::SLARasterWriter & SLAPrint::init_printer()
sla::RasterWriter & SLAPrint::init_printer()
{
sla::Raster::Resolution res;
sla::Raster::PixelDim pxdim;
@ -1666,7 +1654,7 @@ sla::SLARasterWriter & SLAPrint::init_printer()
mirror[X] = m_printer_config.display_mirror_x.getBool();
mirror[Y] = m_printer_config.display_mirror_y.getBool();
if (get_printer_orientation() == sla::SLARasterWriter::roPortrait) {
if (get_printer_orientation() == sla::RasterWriter::roPortrait) {
std::swap(w, h);
std::swap(pw, ph);
@ -1679,7 +1667,7 @@ sla::SLARasterWriter & SLAPrint::init_printer()
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, mirror, gamma));
m_printer->set_config(m_full_print_config);
return *m_printer;
}
@ -1865,7 +1853,7 @@ const SliceRecord SliceRecord::EMPTY(0, std::nanf(""), 0.f);
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
@ -1916,18 +1904,20 @@ TriangleMesh SLAPrintObject::get_mesh(SLAPrintObjectStep step) const
const TriangleMesh& SLAPrintObject::support_mesh() const
{
if(m_config.supports_enable.getBool() && m_supportdata &&
m_supportdata->support_tree_ptr) {
return m_supportdata->support_tree_ptr->merged_mesh();
}
sla::SupportTree::UPtr &stree = m_supportdata->support_tree_ptr;
if(m_config.supports_enable.getBool() && m_supportdata && stree)
return stree->retrieve_mesh(sla::MeshType::Support);
return EMPTY_MESH;
}
const TriangleMesh& SLAPrintObject::pad_mesh() const
{
if(m_config.pad_enable.getBool() && m_supportdata && m_supportdata->support_tree_ptr)
return m_supportdata->support_tree_ptr->get_pad();
sla::SupportTree::UPtr &stree = m_supportdata->support_tree_ptr;
if(m_config.pad_enable.getBool() && m_supportdata && stree)
return stree->retrieve_mesh(sla::MeshType::Pad);
return EMPTY_MESH;
}

View File

@ -440,7 +440,7 @@ private:
std::vector<PrintLayer> m_printer_input;
// The printer itself
std::unique_ptr<sla::SLARasterWriter> m_printer;
std::unique_ptr<sla::RasterWriter> m_printer;
// Estimated print time, material consumed.
SLAPrintStatistics m_print_statistics;
@ -459,14 +459,14 @@ private:
double status() const { return m_st; }
} m_report_status;
sla::SLARasterWriter &init_printer();
sla::RasterWriter &init_printer();
inline sla::SLARasterWriter::Orientation get_printer_orientation() const
inline sla::RasterWriter::Orientation get_printer_orientation() const
{
auto ro = m_printer_config.display_orientation.getInt();
return ro == sla::SLARasterWriter::roPortrait ?
sla::SLARasterWriter::roPortrait :
sla::SLARasterWriter::roLandscape;
return ro == sla::RasterWriter::roPortrait ?
sla::RasterWriter::roPortrait :
sla::RasterWriter::roLandscape;
}
friend SLAPrintObject;

View File

@ -1,3 +1,5 @@
#include <map>
#include <gtest/gtest.h>
#include "libslic3r/libslic3r.h"
@ -5,7 +7,8 @@
#include "libslic3r/SLAPrint.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/SLA/SLAPad.hpp"
#include "libslic3r/SLA/SLASupportTree.hpp"
#include "libslic3r/SLA/SLASupportTreeBuilder.hpp"
#include "libslic3r/SLA/SLASupportTreeAlgorithm.hpp"
#include "libslic3r/SLA/SLAAutoSupports.hpp"
#include "libslic3r/MTUtils.hpp"
@ -104,11 +107,52 @@ struct SupportByproducts
{
std::vector<float> slicegrid;
std::vector<ExPolygons> model_slices;
sla::SLASupportTree supporttree;
sla::SupportTreeBuilder supporttree;
};
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::Pillar &pillar : stree.pillars()) {
if (std::abs(pillar.endpoint().z() - gnd) < EPSILON) {
double h = pillar.height;
if (h > H1) ASSERT_GE(pillar.links, 1);
else if(h > H2) { ASSERT_GE(pillar.links, 2); }
}
ASSERT_LE(pillar.links, cfg.pillar_cascade_neighbors);
ASSERT_LE(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.;
ASSERT_TRUE(slope >= cfg.bridge_slope || slope <= -cfg.bridge_slope);
};
for (auto &bridge : stree.bridges()) chck_bridge(bridge, max_bridgelen);
ASSERT_LE(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);
ASSERT_LE(max_bridgelen, md);
}
void test_supports(const std::string & obj_filename,
const sla::SupportConfig &supportcfg,
SupportByproducts & out)
@ -119,13 +163,13 @@ void test_supports(const std::string & obj_filename,
ASSERT_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;
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, []{});
@ -158,9 +202,12 @@ void test_supports(const std::string & obj_filename,
}
// Generate the actual support tree
sla::SLASupportTree supporttree(support_points, emesh, supportcfg);
sla::SupportTreeBuilder treebuilder;
treebuilder.build(sla::SupportableMesh{emesh, support_points, supportcfg});
const TriangleMesh &output_mesh = supporttree.merged_mesh();
check_support_tree_integrity(treebuilder, supportcfg);
const TriangleMesh &output_mesh = treebuilder.retrieve_mesh();
check_validity(output_mesh, validityflags);
@ -171,7 +218,7 @@ void test_supports(const std::string & obj_filename,
// Move out the support tree into the byproducts, we can examine it further
// in various tests.
out.supporttree = std::move(supporttree);
out.supporttree = std::move(treebuilder);
}
void test_supports(const std::string & obj_filename,
@ -232,6 +279,33 @@ const char *const SUPPORT_TEST_MODELS[] = {
} // namespace
template <class I, class II> void test_pairhash()
{
std::map<II, std::pair<I, I> > ints;
for (I i = 0; i < 1000; ++i)
for (I j = 0; j < 1000; ++j) {
if (j != i) {
II hash_ij = sla::pairhash<I, II>(i, j);
II hash_ji = sla::pairhash<I, II>(j, i);
ASSERT_EQ(hash_ij, hash_ji);
auto it = ints.find(hash_ij);
if (it != ints.end()) {
ASSERT_TRUE(
(it->second.first == i && it->second.second == j) ||
(it->second.first == j && it->second.second == i));
} else ints[hash_ij] = std::make_pair(i, j);
}
}
}
TEST(SLASupportGeneration, PillarPairHashShouldBeUnique) {
test_pairhash<int, long>();
test_pairhash<unsigned, unsigned>();
test_pairhash<unsigned, unsigned long>();
}
TEST(SLASupportGeneration, FlatPadGeometryIsValid) {
sla::PadConfig padcfg;