Deeper test coverage for support tree generation.
Restructuring for testability.
This commit is contained in:
parent
277f6786d8
commit
705e82ec8e
@ -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
|
||||
|
@ -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 {
|
||||
|
56
src/libslic3r/SLA/SLAConcurrency.hpp
Normal file
56
src/libslic3r/SLA/SLAConcurrency.hpp
Normal 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
|
@ -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);
|
||||
|
@ -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
@ -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; }
|
||||
};
|
||||
|
||||
}
|
||||
|
1383
src/libslic3r/SLA/SLASupportTreeAlgorithm.cpp
Normal file
1383
src/libslic3r/SLA/SLASupportTreeAlgorithm.cpp
Normal file
File diff suppressed because it is too large
Load Diff
292
src/libslic3r/SLA/SLASupportTreeAlgorithm.hpp
Normal file
292
src/libslic3r/SLA/SLASupportTreeAlgorithm.hpp
Normal 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
|
462
src/libslic3r/SLA/SLASupportTreeBuilder.cpp
Normal file
462
src/libslic3r/SLA/SLASupportTreeBuilder.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
449
src/libslic3r/SLA/SLASupportTreeBuilder.hpp
Normal file
449
src/libslic3r/SLA/SLASupportTreeBuilder.hpp
Normal 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
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user