diff --git a/src/libslic3r/AABBMesh.hpp b/src/libslic3r/AABBMesh.hpp index 6a08c4303..312d8926d 100644 --- a/src/libslic3r/AABBMesh.hpp +++ b/src/libslic3r/AABBMesh.hpp @@ -72,9 +72,9 @@ public: double m_t = infty(); int m_face_id = -1; const AABBMesh *m_mesh = nullptr; - Vec3d m_dir; - Vec3d m_source; - Vec3d m_normal; + Vec3d m_dir = Vec3d::Zero(); + Vec3d m_source = Vec3d::Zero(); + Vec3d m_normal = Vec3d::Zero(); friend class AABBMesh; // A valid object of this class can only be obtained from diff --git a/src/libslic3r/BranchingTree/BranchingTree.cpp b/src/libslic3r/BranchingTree/BranchingTree.cpp index 6463a30de..98261311b 100644 --- a/src/libslic3r/BranchingTree/BranchingTree.cpp +++ b/src/libslic3r/BranchingTree/BranchingTree.cpp @@ -5,7 +5,7 @@ #include #include -#include "libslic3r/SLA/SupportTreeUtils.hpp" +#include "libslic3r/TriangleMesh.hpp" namespace Slic3r { namespace branchingtree { @@ -76,18 +76,20 @@ void build_tree(PointCloud &nodes, Builder &builder) switch (type) { case BED: { closest_node.weight = w; - if (closest_it->dst_branching > nodes.properties().max_branch_length()) { - auto hl_br_len = float(nodes.properties().max_branch_length()) / 2.f; - Node new_node {{node.pos.x(), node.pos.y(), node.pos.z() - hl_br_len}, node.Rmin}; - new_node.id = int(nodes.next_junction_id()); - new_node.weight = nodes.get(node_id).weight + hl_br_len; - new_node.left = node.id; + double max_br_len = nodes.properties().max_branch_length(); + if (closest_it->dst_branching > max_br_len) { + std::optional avo = builder.suggest_avoidance(node, max_br_len); + if (!avo) + break; + + Node new_node {*avo, node.Rmin}; + new_node.weight = nodes.get(node_id).weight + (node.pos - *avo).norm(); + new_node.left = node.id; if ((routed = builder.add_bridge(node, new_node))) { size_t new_idx = nodes.insert_junction(new_node); ptsqueue.push(new_idx); } - } - else if ((routed = builder.add_ground_bridge(node, closest_node))) { + } else if ((routed = builder.add_ground_bridge(node, closest_node))) { closest_node.left = closest_node.right = node_id; nodes.get(closest_node_id) = closest_node; nodes.mark_unreachable(closest_node_id); diff --git a/src/libslic3r/BranchingTree/BranchingTree.hpp b/src/libslic3r/BranchingTree/BranchingTree.hpp index c88410b3a..7e59e6f1a 100644 --- a/src/libslic3r/BranchingTree/BranchingTree.hpp +++ b/src/libslic3r/BranchingTree/BranchingTree.hpp @@ -5,7 +5,6 @@ #include #include "libslic3r/ExPolygon.hpp" -#include "libslic3r/BoundingBox.hpp" namespace Slic3r { namespace branchingtree { @@ -21,6 +20,7 @@ class Properties ExPolygons m_bed_shape; public: + // Maximum slope for bridges of the tree Properties &max_slope(double val) noexcept { @@ -77,6 +77,11 @@ struct Node {} }; +inline bool is_occupied(const Node &n) +{ + return n.left != Node::ID_NONE && n.right != Node::ID_NONE; +} + // An output interface for the branching tree generator function. Consider each // method as a callback and implement the actions that need to be done. class Builder @@ -100,6 +105,12 @@ public: // Add an anchor bridge to the model body virtual bool add_mesh_bridge(const Node &from, const Node &to) = 0; + virtual std::optional suggest_avoidance(const Node &from, + float max_bridge_len) const + { + return {}; + } + // Report nodes that can not be routed to an endpoint (model or ground) virtual void report_unroutable(const Node &j) = 0; diff --git a/src/libslic3r/BranchingTree/PointCloud.cpp b/src/libslic3r/BranchingTree/PointCloud.cpp index f1d7ae521..1497f0894 100644 --- a/src/libslic3r/BranchingTree/PointCloud.cpp +++ b/src/libslic3r/BranchingTree/PointCloud.cpp @@ -1,82 +1,15 @@ #include "PointCloud.hpp" -#include "libslic3r/Geometry.hpp" #include "libslic3r/Tesselate.hpp" +#include "libslic3r/SLA/SupportTreeUtils.hpp" #include namespace Slic3r { namespace branchingtree { -std::optional find_merge_pt(const Vec3f &A, - const Vec3f &B, - float critical_angle) +std::optional find_merge_pt(const Vec3f &A, const Vec3f &B, float max_slope) { - // The idea is that A and B both have their support cones. But searching - // for the intersection of these support cones is difficult and its enough - // to reduce this problem to 2D and search for the intersection of two - // rays that merge somewhere between A and B. The 2D plane is a vertical - // slice of the 3D scene where the 2D Y axis is equal to the 3D Z axis and - // the 2D X axis is determined by the XY direction of the AB vector. - // - // Z^ - // | A * - // | . . B * - // | . . . . - // | . . . . - // | . x . - // -------------------> XY - - // Determine the transformation matrix for the 2D projection: - Vec3f diff = {B.x() - A.x(), B.y() - A.y(), 0.f}; - Vec3f dir = diff.normalized(); // TODO: avoid normalization - - Eigen::Matrix tr2D; - tr2D.row(0) = Vec3f{dir.x(), dir.y(), dir.z()}; - tr2D.row(1) = Vec3f{0.f, 0.f, 1.f}; - - // Transform the 2 vectors A and B into 2D vector 'a' and 'b'. Here we can - // omit 'a', pretend that its the origin and use BA as the vector b. - Vec2f b = tr2D * (B - A); - - // Get the square sine of the ray emanating from 'a' towards 'b'. This ray might - // exceed the allowed angle but that is corrected subsequently. - // The sign of the original sine is also needed, hence b.y is multiplied by - // abs(b.y) - float b_sqn = b.squaredNorm(); - float sin2sig_a = b_sqn > EPSILON ? (b.y() * std::abs(b.y())) / b_sqn : 0.f; - - // sine2 from 'b' to 'a' is the opposite of sine2 from a to b - float sin2sig_b = -sin2sig_a; - - // Derive the allowed angles from the given critical angle. - // critical_angle is measured from the horizontal X axis. - // The rays need to go downwards which corresponds to negative angles - - float sincrit = std::sin(critical_angle); // sine of the critical angle - float sin2crit = -sincrit * sincrit; // signed sine squared - sin2sig_a = std::min(sin2sig_a, sin2crit); // Do the angle saturation of both rays - sin2sig_b = std::min(sin2sig_b, sin2crit); // - float sin2_a = std::abs(sin2sig_a); // Get cosine squared values - float sin2_b = std::abs(sin2sig_b); - float cos2_a = 1.f - sin2_a; - float cos2_b = 1.f - sin2_b; - - // Derive the new direction vectors. This is by square rooting the sin2 - // and cos2 values and restoring the original signs - Vec2f Da = {std::copysign(std::sqrt(cos2_a), b.x()), std::copysign(std::sqrt(sin2_a), sin2sig_a)}; - Vec2f Db = {-std::copysign(std::sqrt(cos2_b), b.x()), std::copysign(std::sqrt(sin2_b), sin2sig_b)}; - - // Determine where two rays ([0, 0], Da), (b, Db) intersect. - // Based on - // https://stackoverflow.com/questions/27459080/given-two-points-and-two-direction-vectors-find-the-point-where-they-intersect - // One ray is emanating from (0, 0) so the formula is simplified - double t1 = (Db.y() * b.x() - b.y() * Db.x()) / - (Da.x() * Db.y() - Da.y() * Db.x()); - - Vec2f mp = t1 * Da; - Vec3f Mp = A + tr2D.transpose() * mp; - - return t1 >= 0.f ? Mp : Vec3f{}; + return sla::find_merge_pt(A, B, max_slope); } void to_eigen_mesh(const indexed_triangle_set &its, @@ -141,8 +74,6 @@ std::vector sample_mesh(const indexed_triangle_set &its, double radius) std::vector sample_bed(const ExPolygons &bed, float z, double radius) { - std::vector ret; - auto triangles = triangulate_expolygons_3d(bed, z); indexed_triangle_set its; its.vertices.reserve(triangles.size()); @@ -198,7 +129,10 @@ PointCloud::PointCloud(std::vector meshpts, for (size_t i = 0; i < m_leafs.size(); ++i) { Node &n = m_leafs[i]; - n.id = int(LEAFS_BEGIN + i); + n.id = int(LEAFS_BEGIN + i); + n.left = Node::ID_NONE; + n.right = Node::ID_NONE; + m_ktree.insert({n.pos, n.id}); } } diff --git a/src/libslic3r/BranchingTree/PointCloud.hpp b/src/libslic3r/BranchingTree/PointCloud.hpp index f99b17990..03b935f76 100644 --- a/src/libslic3r/BranchingTree/PointCloud.hpp +++ b/src/libslic3r/BranchingTree/PointCloud.hpp @@ -5,7 +5,7 @@ #include "BranchingTree.hpp" -#include "libslic3r/Execution/Execution.hpp" +//#include "libslic3r/Execution/Execution.hpp" #include "libslic3r/MutablePriorityQueue.hpp" #include "libslic3r/BoostAdapter.hpp" @@ -78,14 +78,6 @@ private: rtree /* ? */> m_ktree; - bool is_outside_support_cone(const Vec3f &supp, const Vec3f &pt) const - { - Vec3d D = (pt - supp).cast(); - double dot_sq = -D.z() * std::abs(-D.z()); - - return dot_sq < D.squaredNorm() * cos2bridge_slope; - } - template static auto *get_node(PC &&pc, size_t id) { @@ -104,6 +96,14 @@ private: public: + bool is_outside_support_cone(const Vec3f &supp, const Vec3f &pt) const + { + Vec3d D = (pt - supp).cast(); + double dot_sq = -D.z() * std::abs(-D.z()); + + return dot_sq < D.squaredNorm() * cos2bridge_slope; + } + static constexpr auto Unqueued = size_t(-1); struct ZCompareFn @@ -255,18 +255,42 @@ public: } }; +template constexpr bool IsTraverseFn = std::is_invocable_v; + +struct TraverseReturnT +{ + bool to_left : 1; // if true, continue traversing to the left + bool to_right : 1; // if true, continue traversing to the right +}; + template void traverse(PC &&pc, size_t root, Fn &&fn) { if (auto nodeptr = pc.find(root); nodeptr != nullptr) { auto &nroot = *nodeptr; - fn(nroot); - if (nroot.left >= 0) traverse(pc, nroot.left, fn); - if (nroot.right >= 0) traverse(pc, nroot.right, fn); + TraverseReturnT ret{true, true}; + + if constexpr (std::is_same_v, void>) { + // Our fn has no return value + fn(nroot); + } else { + // Fn returns instructions about how to continue traversing + ret = fn(nroot); + } + + if (ret.to_left && nroot.left >= 0) + traverse(pc, nroot.left, fn); + if (ret.to_right && nroot.right >= 0) + traverse(pc, nroot.right, fn); } } void build_tree(PointCloud &pcloud, Builder &builder); +inline void build_tree(PointCloud &&pc, Builder &builder) +{ + build_tree(pc, builder); +} + }} // namespace Slic3r::branchingtree #endif // POINTCLOUD_HPP diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 6217a11df..7e02598d8 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -323,6 +323,7 @@ set(SLIC3R_SOURCES SLA/SupportTreeMesher.hpp SLA/SupportTreeMesher.cpp SLA/SupportTreeUtils.hpp + SLA/SupportTreeUtilsLegacy.hpp SLA/SupportTreeBuilder.cpp SLA/SupportTree.hpp SLA/SupportTree.cpp diff --git a/src/libslic3r/Optimize/BruteforceOptimizer.hpp b/src/libslic3r/Optimize/BruteforceOptimizer.hpp index 2daef538e..2f6b42224 100644 --- a/src/libslic3r/Optimize/BruteforceOptimizer.hpp +++ b/src/libslic3r/Optimize/BruteforceOptimizer.hpp @@ -13,7 +13,9 @@ template long num_iter(const std::array &idx, size_t gridsz) { long ret = 0; - for (size_t i = 0; i < N; ++i) ret += idx[i] * std::pow(gridsz, i); + for (size_t i = 0; i < N; ++i) + ret += idx[i] * std::pow(gridsz, i); + return ret; } diff --git a/src/libslic3r/Optimize/NLoptOptimizer.hpp b/src/libslic3r/Optimize/NLoptOptimizer.hpp index f5d314046..9e423ff91 100644 --- a/src/libslic3r/Optimize/NLoptOptimizer.hpp +++ b/src/libslic3r/Optimize/NLoptOptimizer.hpp @@ -13,7 +13,7 @@ #include -#include +#include "Optimizer.hpp" namespace Slic3r { namespace opt { @@ -40,69 +40,163 @@ struct IsNLoptAlg> { static const constexpr bool value = true; }; +// NLopt can wrap any of its algorithms to use the augmented lagrangian method +// for deriving an object function from all equality and inequality constraints +// This way one can use algorithms that do not support these constraints natively +template struct NLoptAUGLAG {}; + +template +struct IsNLoptAlg>> { + static const constexpr bool value = true; +}; + +template struct IsNLoptAlg>> { + static const constexpr bool value = true; +}; + template using NLoptOnly = std::enable_if_t::value, T>; +template struct GetNLoptAlg_ { + static constexpr nlopt_algorithm Local = NLOPT_NUM_ALGORITHMS; + static constexpr nlopt_algorithm Global = NLOPT_NUM_ALGORITHMS; + static constexpr bool IsAUGLAG = false; +}; + +template struct GetNLoptAlg_> { + static constexpr nlopt_algorithm Local = NLOPT_NUM_ALGORITHMS; + static constexpr nlopt_algorithm Global = a; + static constexpr bool IsAUGLAG = false; +}; + +template +struct GetNLoptAlg_> { + static constexpr nlopt_algorithm Local = l; + static constexpr nlopt_algorithm Global = g; + static constexpr bool IsAUGLAG = false; +}; + +template constexpr nlopt_algorithm GetNLoptAlg_Global = GetNLoptAlg_>::Global; +template constexpr nlopt_algorithm GetNLoptAlg_Local = GetNLoptAlg_>::Local; +template constexpr bool IsAUGLAG = GetNLoptAlg_>::IsAUGLAG; + +template struct GetNLoptAlg_> { + static constexpr nlopt_algorithm Local = GetNLoptAlg_Local; + static constexpr nlopt_algorithm Global = GetNLoptAlg_Global; + static constexpr bool IsAUGLAG = true; +}; enum class OptDir { MIN, MAX }; // Where to optimize -struct NLopt { // Helper RAII class for nlopt_opt +struct NLoptRAII { // Helper RAII class for nlopt_opt nlopt_opt ptr = nullptr; - template explicit NLopt(A&&...a) + template explicit NLoptRAII(A&&...a) { ptr = nlopt_create(std::forward(a)...); } - NLopt(const NLopt&) = delete; - NLopt(NLopt&&) = delete; - NLopt& operator=(const NLopt&) = delete; - NLopt& operator=(NLopt&&) = delete; + NLoptRAII(const NLoptRAII&) = delete; + NLoptRAII(NLoptRAII&&) = delete; + NLoptRAII& operator=(const NLoptRAII&) = delete; + NLoptRAII& operator=(NLoptRAII&&) = delete; - ~NLopt() { nlopt_destroy(ptr); } + ~NLoptRAII() { nlopt_destroy(ptr); } }; -template class NLoptOpt {}; +// Map a generic function to each argument following the mapping function +template +Fn for_each_argument(Fn &&fn, Args&&...args) +{ + // see https://www.fluentcpp.com/2019/03/05/for_each_arg-applying-a-function-to-each-argument-of-a-function-in-cpp/ + (fn(std::forward(args)),...); -// Optimizers based on NLopt. -template class NLoptOpt> { -protected: + return fn; +} + +// Call fn on each element of the input tuple tup. +template +Fn for_each_in_tuple(Fn fn, Tup &&tup) +{ + auto mpfn = [&fn](auto&...pack) { + for_each_argument(fn, pack...); + }; + + std::apply(mpfn, tup); + + return fn; +} + +// Wrap each element of the tuple tup into a wrapper class W and return +// a new tuple with each element being of type W where T_i is the type of +// i-th element of tup. +template class W, class...Args> +auto wrap_tup(const std::tuple &tup) +{ + return std::tuple...>(tup); +} + +template> +class NLoptOpt { StopCriteria m_stopcr; - OptDir m_dir; + StopCriteria m_loc_stopcr; + OptDir m_dir = OptDir::MIN; - template using TOptData = - std::tuple*, NLoptOpt*, nlopt_opt>; + static constexpr double ConstraintEps = 1e-6; + + template struct OptData { + Fn fn; + NLoptOpt *self = nullptr; + nlopt_opt opt_raw = nullptr; + + OptData(const Fn &f): fn{f} {} + + OptData(const Fn &f, NLoptOpt *s, nlopt_opt nlopt_raw) + : fn{f}, self{s}, opt_raw{nlopt_raw} {} + }; template static double optfunc(unsigned n, const double *params, - double *gradient, - void *data) + double *gradient, void *data) { - assert(n >= N); + assert(n == N); - auto tdata = static_cast*>(data); + auto tdata = static_cast*>(data); - if (std::get<1>(*tdata)->m_stopcr.stop_condition()) - nlopt_force_stop(std::get<2>(*tdata)); + if (tdata->self->m_stopcr.stop_condition()) + nlopt_force_stop(tdata->opt_raw); - auto fnptr = std::get<0>(*tdata); auto funval = to_arr(params); double scoreval = 0.; - using RetT = decltype((*fnptr)(funval)); + using RetT = decltype(tdata->fn(funval)); if constexpr (std::is_convertible_v>) { - ScoreGradient score = (*fnptr)(funval); + ScoreGradient score = tdata->fn(funval); for (size_t i = 0; i < n; ++i) gradient[i] = (*score.gradient)[i]; scoreval = score.score; } else { - scoreval = (*fnptr)(funval); + scoreval = tdata->fn(funval); } return scoreval; } + template + static double constrain_func(unsigned n, const double *params, + double *gradient, void *data) + { + assert(n == N); + + auto tdata = static_cast*>(data); + auto funval = to_arr(params); + + return tdata->fn(funval); + } + template - void set_up(NLopt &nl, const Bounds& bounds) + static void set_up(NLoptRAII &nl, + const Bounds &bounds, + const StopCriteria &stopcr) { std::array lb, ub; @@ -114,23 +208,45 @@ protected: nlopt_set_lower_bounds(nl.ptr, lb.data()); nlopt_set_upper_bounds(nl.ptr, ub.data()); - double abs_diff = m_stopcr.abs_score_diff(); - double rel_diff = m_stopcr.rel_score_diff(); - double stopval = m_stopcr.stop_score(); + double abs_diff = stopcr.abs_score_diff(); + double rel_diff = stopcr.rel_score_diff(); + double stopval = stopcr.stop_score(); if(!std::isnan(abs_diff)) nlopt_set_ftol_abs(nl.ptr, abs_diff); if(!std::isnan(rel_diff)) nlopt_set_ftol_rel(nl.ptr, rel_diff); if(!std::isnan(stopval)) nlopt_set_stopval(nl.ptr, stopval); - if(m_stopcr.max_iterations() > 0) - nlopt_set_maxeval(nl.ptr, m_stopcr.max_iterations()); + if(stopcr.max_iterations() > 0) + nlopt_set_maxeval(nl.ptr, stopcr.max_iterations()); } - template - Result optimize(NLopt &nl, Fn &&fn, const Input &initvals) + template + Result optimize(NLoptRAII &nl, Fn &&fn, const Input &initvals, + const std::tuple &equalities, + const std::tuple &inequalities) { Result r; - TOptData data = std::make_tuple(&fn, this, nl.ptr); + OptData data {fn, this, nl.ptr}; + + auto do_for_each_eq = [this, &nl](auto &arg) { + arg.self = this; + arg.opt_raw = nl.ptr; + using F = decltype(arg.fn); + nlopt_add_equality_constraint (nl.ptr, constrain_func, &arg, ConstraintEps); + }; + + auto do_for_each_ineq = [this, &nl](auto &arg) { + arg.self = this; + arg.opt_raw = nl.ptr; + using F = decltype(arg.fn); + nlopt_add_inequality_constraint (nl.ptr, constrain_func, &arg, ConstraintEps); + }; + + auto eq_data = wrap_tup(equalities); + for_each_in_tuple(do_for_each_eq, eq_data); + + auto ineq_data = wrap_tup(inequalities); + for_each_in_tuple(do_for_each_ineq, ineq_data); switch(m_dir) { case OptDir::MIN: @@ -147,51 +263,81 @@ protected: public: - template - Result optimize(Func&& func, + template + Result optimize(Fn&& f, const Input &initvals, - const Bounds& bounds) + const Bounds& bounds, + const std::tuple &equalities, + const std::tuple &inequalities) { - NLopt nl{alg, N}; - set_up(nl, bounds); + if constexpr (IsAUGLAG) { + NLoptRAII nl_wrap{NLOPT_AUGLAG, N}; + set_up(nl_wrap, bounds, get_criteria()); - return optimize(nl, std::forward(func), initvals); + NLoptRAII nl_glob{GetNLoptAlg_Global, N}; + set_up(nl_glob, bounds, get_criteria()); + nlopt_set_local_optimizer(nl_wrap.ptr, nl_glob.ptr); + + if constexpr (GetNLoptAlg_Local < NLOPT_NUM_ALGORITHMS) { + NLoptRAII nl_loc{GetNLoptAlg_Local, N}; + set_up(nl_loc, bounds, m_loc_stopcr); + nlopt_set_local_optimizer(nl_glob.ptr, nl_loc.ptr); + + return optimize(nl_wrap, std::forward(f), initvals, + equalities, inequalities); + } else { + return optimize(nl_wrap, std::forward(f), initvals, + equalities, inequalities); + } + } else { + NLoptRAII nl_glob{GetNLoptAlg_Global, N}; + set_up(nl_glob, bounds, get_criteria()); + + if constexpr (GetNLoptAlg_Local < NLOPT_NUM_ALGORITHMS) { + NLoptRAII nl_loc{GetNLoptAlg_Local, N}; + set_up(nl_loc, bounds, m_loc_stopcr); + nlopt_set_local_optimizer(nl_glob.ptr, nl_loc.ptr); + + return optimize(nl_glob, std::forward(f), initvals, + equalities, inequalities); + } else { + return optimize(nl_glob, std::forward(f), initvals, + equalities, inequalities); + } + } + + assert(false); + + return {}; } - explicit NLoptOpt(StopCriteria stopcr = {}) : m_stopcr(stopcr) {} + explicit NLoptOpt(const StopCriteria &stopcr_glob = {}) + : m_stopcr(stopcr_glob) + {} void set_criteria(const StopCriteria &cr) { m_stopcr = cr; } const StopCriteria &get_criteria() const noexcept { return m_stopcr; } - void set_dir(OptDir dir) noexcept { m_dir = dir; } + void set_loc_criteria(const StopCriteria &cr) { m_loc_stopcr = cr; } + const StopCriteria &get_loc_criteria() const noexcept { return m_loc_stopcr; } + + void set_dir(OptDir dir) noexcept { m_dir = dir; } void seed(long s) { nlopt_srand(s); } }; -template -class NLoptOpt>: public NLoptOpt> -{ - using Base = NLoptOpt>; -public: - - template - Result optimize(Fn&& f, - const Input &initvals, - const Bounds& bounds) - { - NLopt nl_glob{glob, N}, nl_loc{loc, N}; - - Base::set_up(nl_glob, bounds); - Base::set_up(nl_loc, bounds); - nlopt_set_local_optimizer(nl_glob.ptr, nl_loc.ptr); - - return Base::optimize(nl_glob, std::forward(f), initvals); - } - - explicit NLoptOpt(StopCriteria stopcr = {}) : Base{stopcr} {} +template struct AlgFeatures_ { + static constexpr bool SupportsInequalities = false; + static constexpr bool SupportsEqualities = false; }; } // namespace detail; +template constexpr bool SupportsEqualities = + detail::AlgFeatures_>::SupportsEqualities; + +template constexpr bool SupportsInequalities = + detail::AlgFeatures_>::SupportsInequalities; + // Optimizers based on NLopt. template class Optimizer> { detail::NLoptOpt m_opt; @@ -201,12 +347,24 @@ public: Optimizer& to_max() { m_opt.set_dir(detail::OptDir::MAX); return *this; } Optimizer& to_min() { m_opt.set_dir(detail::OptDir::MIN); return *this; } - template + template Result optimize(Func&& func, const Input &initvals, - const Bounds& bounds) + const Bounds& bounds, + const std::tuple &eq_constraints = {}, + const std::tuple &ineq_constraint = {}) { - return m_opt.optimize(std::forward(func), initvals, bounds); + static_assert(std::tuple_size_v> == 0 + || SupportsEqualities, + "Equality constraints are not supported."); + + static_assert(std::tuple_size_v> == 0 + || SupportsInequalities, + "Inequality constraints are not supported."); + + return m_opt.optimize(std::forward(func), initvals, bounds, + eq_constraints, + ineq_constraint); } explicit Optimizer(StopCriteria stopcr = {}) : m_opt(stopcr) {} @@ -219,14 +377,56 @@ public: const StopCriteria &get_criteria() const { return m_opt.get_criteria(); } void seed(long s) { m_opt.seed(s); } + + void set_loc_criteria(const StopCriteria &cr) { m_opt.set_loc_criteria(cr); } + const StopCriteria &get_loc_criteria() const noexcept { return m_opt.get_loc_criteria(); } }; // Predefinded NLopt algorithms -using AlgNLoptGenetic = detail::NLoptAlgComb; -using AlgNLoptSubplex = detail::NLoptAlg; -using AlgNLoptSimplex = detail::NLoptAlg; -using AlgNLoptDIRECT = detail::NLoptAlg; -using AlgNLoptMLSL = detail::NLoptAlg; +using AlgNLoptGenetic = detail::NLoptAlgComb; +using AlgNLoptSubplex = detail::NLoptAlg; +using AlgNLoptSimplex = detail::NLoptAlg; +using AlgNLoptCobyla = detail::NLoptAlg; +using AlgNLoptDIRECT = detail::NLoptAlg; +using AlgNLoptORIG_DIRECT = detail::NLoptAlg; +using AlgNLoptISRES = detail::NLoptAlg; +using AlgNLoptAGS = detail::NLoptAlg; + +using AlgNLoptMLSL_Subplx = detail::NLoptAlgComb; +using AlgNLoptMLSL_Cobyla = detail::NLoptAlgComb; +using AlgNLoptGenetic_Subplx = detail::NLoptAlgComb; + +// To craft auglag algorithms (constraint support through object function transformation) +using detail::NLoptAUGLAG; + +namespace detail { + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = true; +}; + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = false; +}; + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = false; +}; + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = true; +}; + +template struct AlgFeatures_> { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = true; +}; + +} // namespace detail }} // namespace Slic3r::opt diff --git a/src/libslic3r/Optimize/Optimizer.hpp b/src/libslic3r/Optimize/Optimizer.hpp index bf95d9ee0..6212a5f59 100644 --- a/src/libslic3r/Optimize/Optimizer.hpp +++ b/src/libslic3r/Optimize/Optimizer.hpp @@ -12,6 +12,15 @@ namespace Slic3r { namespace opt { +template +using FloatingOnly = std::enable_if_t::value, O>; + +template> +constexpr T NaN = std::numeric_limits::quiet_NaN(); + +constexpr float NaNf = NaN; +constexpr double NaNd = NaN; + // A type to hold the complete result of the optimization. template struct Result { int resultcode; // Method dependent @@ -79,7 +88,7 @@ public: double stop_score() const { return m_stop_score; } - StopCriteria & max_iterations(double val) + StopCriteria & max_iterations(unsigned val) { m_max_iterations = val; return *this; } @@ -137,16 +146,25 @@ public: // For each dimension an interval (Bound) has to be given marking the bounds // for that dimension. // + // Optionally, some constraints can be given in the form of double(Input) + // functors. The parameters eq_constraints and ineq_constraints can be used + // to add equality and inequality (<= 0) constraints to the optimization. + // Note that it is up the the particular method if it accepts these + // constraints. + // // initvals have to be within the specified bounds, otherwise its undefined // behavior. // // Func can return a score of type double or optionally a ScoreGradient // class to indicate the function gradient for a optimization methods that // make use of the gradient. - template + template Result optimize(Func&& /*func*/, const Input &/*initvals*/, - const Bounds& /*bounds*/) { return {}; } + const Bounds& /*bounds*/, + const std::tuple &eq_constraints = {}, + const std::tuple &ineq_constraint = {} + ) { return {}; } // optional for randomized methods: void seed(long /*s*/) {} diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 206844b7b..9dad49d4c 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -495,12 +495,14 @@ static std::vector s_Preset_sla_print_options { "faded_layers", "supports_enable", "support_tree_type", + "support_head_front_diameter", "support_head_penetration", "support_head_width", "support_pillar_diameter", "support_small_pillar_diameter_percent", "support_max_bridges_on_pillar", + "support_max_weight_on_model", "support_pillar_connection_mode", "support_buildplate_only", "support_pillar_widening_factor", @@ -511,6 +513,25 @@ static std::vector s_Preset_sla_print_options { "support_max_bridge_length", "support_max_pillar_link_distance", "support_object_elevation", + + "branchingsupport_head_front_diameter", + "branchingsupport_head_penetration", + "branchingsupport_head_width", + "branchingsupport_pillar_diameter", + "branchingsupport_small_pillar_diameter_percent", + "branchingsupport_max_bridges_on_pillar", + "branchingsupport_max_weight_on_model", + "branchingsupport_pillar_connection_mode", + "branchingsupport_buildplate_only", + "branchingsupport_pillar_widening_factor", + "branchingsupport_base_diameter", + "branchingsupport_base_height", + "branchingsupport_base_safety_distance", + "branchingsupport_critical_angle", + "branchingsupport_max_bridge_length", + "branchingsupport_max_pillar_link_distance", + "branchingsupport_object_elevation", + "support_points_density_relative", "support_points_minimal_distance", "slice_closing_radius", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index bc6d81b2d..cf2753b18 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -181,7 +181,8 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SLAMaterialSpeed); static inline const t_config_enum_values s_keys_map_SLASupportTreeType = { {"default", int(sla::SupportTreeType::Default)}, - {"branching", int(sla::SupportTreeType::Branching)} + {"branching", int(sla::SupportTreeType::Branching)}, + //TODO: {"organic", int(sla::SupportTreeType::Organic)} }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SLASupportTreeType); @@ -3272,6 +3273,219 @@ void PrintConfigDef::init_extruder_option_keys() assert(std::is_sorted(m_extruder_retract_keys.begin(), m_extruder_retract_keys.end())); } +void PrintConfigDef::init_sla_support_params(const std::string &prefix) +{ + ConfigOptionDef* def; + + constexpr const char * pretext_unavailable = L("Unavailable for this method.\n"); + std::string pretext; + + def = this->add(prefix + "support_head_front_diameter", coFloat); + def->label = L("Pinhead front diameter"); + def->category = L("Supports"); + def->tooltip = L("Diameter of the pointing side of the head"); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.4)); + + def = this->add(prefix + "support_head_penetration", coFloat); + def->label = L("Head penetration"); + def->category = L("Supports"); + def->tooltip = L("How much the pinhead has to penetrate the model surface"); + def->sidetext = L("mm"); + def->mode = comAdvanced; + def->min = 0; + def->set_default_value(new ConfigOptionFloat(0.2)); + + def = this->add(prefix + "support_head_width", coFloat); + def->label = L("Pinhead width"); + def->category = L("Supports"); + def->tooltip = L("Width from the back sphere center to the front sphere center"); + def->sidetext = L("mm"); + def->min = 0; + def->max = 20; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(1.0)); + + def = this->add(prefix + "support_pillar_diameter", coFloat); + def->label = L("Pillar diameter"); + def->category = L("Supports"); + def->tooltip = L("Diameter in mm of the support pillars"); + def->sidetext = L("mm"); + def->min = 0; + def->max = 15; + def->mode = comSimple; + def->set_default_value(new ConfigOptionFloat(1.0)); + + def = this->add(prefix + "support_small_pillar_diameter_percent", coPercent); + def->label = L("Small pillar diameter percent"); + def->category = L("Supports"); + def->tooltip = L("The percentage of smaller pillars compared to the normal pillar diameter " + "which are used in problematic areas where a normal pilla cannot fit."); + def->sidetext = L("%"); + def->min = 1; + def->max = 100; + def->mode = comExpert; + def->set_default_value(new ConfigOptionPercent(50)); + + pretext = ""; + if (prefix == "branching") + pretext = pretext_unavailable; + + def = this->add(prefix + "support_max_bridges_on_pillar", coInt); + def->label = L("Max bridges on a pillar"); + def->tooltip = pretext + L( + "Maximum number of bridges that can be placed on a pillar. Bridges " + "hold support point pinheads and connect to pillars as small branches."); + def->min = 0; + def->max = 50; + def->mode = comExpert; + def->set_default_value(new ConfigOptionInt(prefix == "branching" ? 2 : 3)); + + pretext = ""; + if (prefix.empty()) + pretext = pretext_unavailable; + + def = this->add(prefix + "support_max_weight_on_model", coFloat); + def->label = L("Max weight on model"); + def->category = L("Supports"); + def->tooltip = pretext + L( + "Maximum weight of sub-trees that terminate on the model instead of the print bed. The weight is the sum of the lenghts of all " + "branches emanating from the endpoint."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(10.)); + + pretext = ""; + if (prefix == "branching") + pretext = pretext_unavailable; + + def = this->add(prefix + "support_pillar_connection_mode", coEnum); + def->label = L("Pillar connection mode"); + def->tooltip = pretext + L("Controls the bridge type between two neighboring pillars." + " Can be zig-zag, cross (double zig-zag) or dynamic which" + " will automatically switch between the first two depending" + " on the distance of the two pillars."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = ConfigOptionEnum::get_enum_names(); + def->enum_labels = ConfigOptionEnum::get_enum_names(); + def->enum_labels[0] = L("Zig-Zag"); + def->enum_labels[1] = L("Cross"); + def->enum_labels[2] = L("Dynamic"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(SLAPillarConnectionMode::dynamic)); + + def = this->add(prefix + "support_buildplate_only", coBool); + def->label = L("Support on build plate only"); + def->category = L("Supports"); + def->tooltip = L("Only create support if it lies on a build plate. Don't create support on a print."); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add(prefix + "support_pillar_widening_factor", coFloat); + def->label = L("Pillar widening factor"); + def->category = L("Supports"); + + pretext = ""; + if (prefix.empty()) + pretext = pretext_unavailable; + + def->tooltip = pretext + + L("Merging bridges or pillars into another pillars can " + "increase the radius. Zero means no increase, one means " + "full increase. The exact amount of increase is unspecified and can " + "change in the future."); + + def->min = 0; + def->max = 1; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(0.5)); + + def = this->add(prefix + "support_base_diameter", coFloat); + def->label = L("Support base diameter"); + def->category = L("Supports"); + def->tooltip = L("Diameter in mm of the pillar base"); + def->sidetext = L("mm"); + def->min = 0; + def->max = 30; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(4.0)); + + def = this->add(prefix + "support_base_height", coFloat); + def->label = L("Support base height"); + def->category = L("Supports"); + def->tooltip = L("The height of the pillar base cone"); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(1.0)); + + def = this->add(prefix + "support_base_safety_distance", coFloat); + def->label = L("Support base safety distance"); + def->category = L("Supports"); + def->tooltip = L( + "The minimum distance of the pillar base from the model in mm. " + "Makes sense in zero elevation mode where a gap according " + "to this parameter is inserted between the model and the pad."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 10; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(1)); + + def = this->add(prefix + "support_critical_angle", coFloat); + def->label = L("Critical angle"); + def->category = L("Supports"); + def->tooltip = L("The default angle for connecting support sticks and junctions."); + def->sidetext = L("°"); + def->min = 0; + def->max = 90; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(45)); + + def = this->add(prefix + "support_max_bridge_length", coFloat); + def->label = L("Max bridge length"); + def->category = L("Supports"); + def->tooltip = L("The max length of a bridge"); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + + double default_val = 15.0; + if (prefix == "branching") + default_val = 5.0; + + def->set_default_value(new ConfigOptionFloat(default_val)); + + pretext = ""; + if (prefix == "branching") + pretext = pretext_unavailable; + + def = this->add(prefix + "support_max_pillar_link_distance", coFloat); + def->label = L("Max pillar linking distance"); + def->category = L("Supports"); + def->tooltip = pretext + L("The max distance of two pillars to get linked with each other." + " A zero value will prohibit pillar cascading."); + def->sidetext = L("mm"); + def->min = 0; // 0 means no linking + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(10.0)); + + def = this->add(prefix + "support_object_elevation", coFloat); + def->label = L("Object elevation"); + def->category = L("Supports"); + def->tooltip = L("How much the supports should lift up the supported object. " + "If \"Pad around object\" is enabled, this value is ignored."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 150; // This is the max height of print on SL1 + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(5.0)); +} + void PrintConfigDef::init_sla_params() { ConfigOptionDef* def; @@ -3614,177 +3828,12 @@ void PrintConfigDef::init_sla_params() def->enum_labels = ConfigOptionEnum::get_enum_names(); def->enum_labels[0] = L("Default"); def->enum_labels[1] = L("Branching"); - def->mode = comAdvanced; + // TODO: def->enum_labels[2] = L("Organic"); + def->mode = comSimple; def->set_default_value(new ConfigOptionEnum(sla::SupportTreeType::Default)); - def = this->add("support_head_front_diameter", coFloat); - def->label = L("Pinhead front diameter"); - def->category = L("Supports"); - def->tooltip = L("Diameter of the pointing side of the head"); - def->sidetext = L("mm"); - def->min = 0; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(0.4)); - - def = this->add("support_head_penetration", coFloat); - def->label = L("Head penetration"); - def->category = L("Supports"); - def->tooltip = L("How much the pinhead has to penetrate the model surface"); - def->sidetext = L("mm"); - def->mode = comAdvanced; - def->min = 0; - def->set_default_value(new ConfigOptionFloat(0.2)); - - def = this->add("support_head_width", coFloat); - def->label = L("Pinhead width"); - def->category = L("Supports"); - def->tooltip = L("Width from the back sphere center to the front sphere center"); - def->sidetext = L("mm"); - def->min = 0; - def->max = 20; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(1.0)); - - def = this->add("support_pillar_diameter", coFloat); - def->label = L("Pillar diameter"); - def->category = L("Supports"); - def->tooltip = L("Diameter in mm of the support pillars"); - def->sidetext = L("mm"); - def->min = 0; - def->max = 15; - def->mode = comSimple; - def->set_default_value(new ConfigOptionFloat(1.0)); - - def = this->add("support_small_pillar_diameter_percent", coPercent); - def->label = L("Small pillar diameter percent"); - def->category = L("Supports"); - def->tooltip = L("The percentage of smaller pillars compared to the normal pillar diameter " - "which are used in problematic areas where a normal pilla cannot fit."); - def->sidetext = L("%"); - def->min = 1; - def->max = 100; - def->mode = comExpert; - def->set_default_value(new ConfigOptionPercent(50)); - - def = this->add("support_max_bridges_on_pillar", coInt); - def->label = L("Max bridges on a pillar"); - def->tooltip = L( - "Maximum number of bridges that can be placed on a pillar. Bridges " - "hold support point pinheads and connect to pillars as small branches."); - def->min = 0; - def->max = 50; - def->mode = comExpert; - def->set_default_value(new ConfigOptionInt(3)); - - def = this->add("support_pillar_connection_mode", coEnum); - def->label = L("Pillar connection mode"); - def->tooltip = L("Controls the bridge type between two neighboring pillars." - " Can be zig-zag, cross (double zig-zag) or dynamic which" - " will automatically switch between the first two depending" - " on the distance of the two pillars."); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = ConfigOptionEnum::get_enum_names(); - def->enum_labels = ConfigOptionEnum::get_enum_names(); - def->enum_labels[0] = L("Zig-Zag"); - def->enum_labels[1] = L("Cross"); - def->enum_labels[2] = L("Dynamic"); - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionEnum(SLAPillarConnectionMode::dynamic)); - - def = this->add("support_buildplate_only", coBool); - def->label = L("Support on build plate only"); - def->category = L("Supports"); - def->tooltip = L("Only create support if it lies on a build plate. Don't create support on a print."); - def->mode = comSimple; - def->set_default_value(new ConfigOptionBool(false)); - - def = this->add("support_pillar_widening_factor", coFloat); - def->label = L("Pillar widening factor"); - def->category = L("Supports"); - def->tooltip = L( - "Merging bridges or pillars into another pillars can " - "increase the radius. Zero means no increase, one means " - "full increase. The exact amount of increase is unspecified and can " - "change in the future. What is garanteed is that thickness will not " - "exceed \"support_base_diameter\""); - - def->min = 0; - def->max = 1; - def->mode = comExpert; - def->set_default_value(new ConfigOptionFloat(0.15)); - - def = this->add("support_base_diameter", coFloat); - def->label = L("Support base diameter"); - def->category = L("Supports"); - def->tooltip = L("Diameter in mm of the pillar base"); - def->sidetext = L("mm"); - def->min = 0; - def->max = 30; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(4.0)); - - def = this->add("support_base_height", coFloat); - def->label = L("Support base height"); - def->category = L("Supports"); - def->tooltip = L("The height of the pillar base cone"); - def->sidetext = L("mm"); - def->min = 0; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(1.0)); - - def = this->add("support_base_safety_distance", coFloat); - def->label = L("Support base safety distance"); - def->category = L("Supports"); - def->tooltip = L( - "The minimum distance of the pillar base from the model in mm. " - "Makes sense in zero elevation mode where a gap according " - "to this parameter is inserted between the model and the pad."); - def->sidetext = L("mm"); - def->min = 0; - def->max = 10; - def->mode = comExpert; - def->set_default_value(new ConfigOptionFloat(1)); - - def = this->add("support_critical_angle", coFloat); - def->label = L("Critical angle"); - def->category = L("Supports"); - def->tooltip = L("The default angle for connecting support sticks and junctions."); - def->sidetext = L("°"); - def->min = 0; - def->max = 90; - def->mode = comExpert; - def->set_default_value(new ConfigOptionFloat(45)); - - def = this->add("support_max_bridge_length", coFloat); - def->label = L("Max bridge length"); - def->category = L("Supports"); - def->tooltip = L("The max length of a bridge"); - def->sidetext = L("mm"); - def->min = 0; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(15.0)); - - def = this->add("support_max_pillar_link_distance", coFloat); - def->label = L("Max pillar linking distance"); - def->category = L("Supports"); - def->tooltip = L("The max distance of two pillars to get linked with each other." - " A zero value will prohibit pillar cascading."); - def->sidetext = L("mm"); - def->min = 0; // 0 means no linking - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(10.0)); - - def = this->add("support_object_elevation", coFloat); - def->label = L("Object elevation"); - def->category = L("Supports"); - def->tooltip = L("How much the supports should lift up the supported object. " - "If \"Pad around object\" is enabled, this value is ignored."); - def->sidetext = L("mm"); - def->min = 0; - def->max = 150; // This is the max height of print on SL1 - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(5.0)); + init_sla_support_params(""); + init_sla_support_params("branching"); def = this->add("support_points_density_relative", coInt); def->label = L("Support points density"); @@ -4753,6 +4802,23 @@ Points get_bed_shape(const PrintConfig &cfg) Points get_bed_shape(const SLAPrinterConfig &cfg) { return to_points(cfg.bed_shape.values); } +std::string get_sla_suptree_prefix(const DynamicPrintConfig &config) +{ + const auto *suptreetype = config.option>("support_tree_type"); + std::string slatree = ""; + if (suptreetype) { + auto ttype = static_cast(suptreetype->getInt()); + switch (ttype) { + case sla::SupportTreeType::Branching: slatree = "branching"; break; + case sla::SupportTreeType::Organic: slatree = "organic"; break; + default: + ; + } + } + + return slatree; +} + } // namespace Slic3r #include diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index a36125a07..ca6679051 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -185,6 +185,7 @@ private: void init_fff_params(); void init_extruder_option_keys(); void init_sla_params(); + void init_sla_support_params(const std::string &method_prefix); std::vector m_extruder_option_keys; std::vector m_extruder_retract_keys; @@ -861,6 +862,8 @@ PRINT_CONFIG_CLASS_DEFINE( // Generate only ground facing supports ((ConfigOptionBool, support_buildplate_only)) + ((ConfigOptionFloat, support_max_weight_on_model)) + // TODO: unimplemented at the moment. This coefficient will have an impact // when bridges and pillars are merged. The resulting pillar should be a bit // thicker than the ones merging into it. How much thicker? I don't know @@ -889,6 +892,62 @@ PRINT_CONFIG_CLASS_DEFINE( // and the model object's bounding box bottom. Units in mm. ((ConfigOptionFloat, support_object_elevation))/*= 5.0*/ + + // Branching tree + + // Diameter in mm of the pointing side of the head. + ((ConfigOptionFloat, branchingsupport_head_front_diameter))/*= 0.2*/ + + // How much the pinhead has to penetrate the model surface + ((ConfigOptionFloat, branchingsupport_head_penetration))/*= 0.2*/ + + // Width in mm from the back sphere center to the front sphere center. + ((ConfigOptionFloat, branchingsupport_head_width))/*= 1.0*/ + + // Radius in mm of the support pillars. + ((ConfigOptionFloat, branchingsupport_pillar_diameter))/*= 0.8*/ + + // The percentage of smaller pillars compared to the normal pillar diameter + // which are used in problematic areas where a normal pilla cannot fit. + ((ConfigOptionPercent, branchingsupport_small_pillar_diameter_percent)) + + // How much bridge (supporting another pinhead) can be placed on a pillar. + ((ConfigOptionInt, branchingsupport_max_bridges_on_pillar)) + + // How the pillars are bridged together + ((ConfigOptionEnum, branchingsupport_pillar_connection_mode)) + + // Generate only ground facing supports + ((ConfigOptionBool, branchingsupport_buildplate_only)) + + ((ConfigOptionFloat, branchingsupport_max_weight_on_model)) + + ((ConfigOptionFloat, branchingsupport_pillar_widening_factor)) + + // Radius in mm of the pillar base. + ((ConfigOptionFloat, branchingsupport_base_diameter))/*= 2.0*/ + + // The height of the pillar base cone in mm. + ((ConfigOptionFloat, branchingsupport_base_height))/*= 1.0*/ + + // The minimum distance of the pillar base from the model in mm. + ((ConfigOptionFloat, branchingsupport_base_safety_distance)) /*= 1.0*/ + + // The default angle for connecting support sticks and junctions. + ((ConfigOptionFloat, branchingsupport_critical_angle))/*= 45*/ + + // The max length of a bridge in mm + ((ConfigOptionFloat, branchingsupport_max_bridge_length))/*= 15.0*/ + + // The max distance of two pillars to get cross linked. + ((ConfigOptionFloat, branchingsupport_max_pillar_link_distance)) + + // The elevation in Z direction upwards. This is the space between the pad + // and the model object's bounding box bottom. Units in mm. + ((ConfigOptionFloat, branchingsupport_object_elevation))/*= 5.0*/ + + + /////// Following options influence automatic support points placement: ((ConfigOptionInt, support_points_density_relative)) ((ConfigOptionFloat, support_points_minimal_distance)) @@ -1108,6 +1167,8 @@ Points get_bed_shape(const DynamicPrintConfig &cfg); Points get_bed_shape(const PrintConfig &cfg); Points get_bed_shape(const SLAPrinterConfig &cfg); +std::string get_sla_suptree_prefix(const DynamicPrintConfig &config); + // ModelConfig is a wrapper around DynamicPrintConfig with an addition of a timestamp. // Each change of ModelConfig is tracked by assigning a new timestamp from a global counter. // The counter is used for faster synchronization of the background slicing thread diff --git a/src/libslic3r/SLA/BranchingTreeSLA.cpp b/src/libslic3r/SLA/BranchingTreeSLA.cpp index 72314c9d8..bb9712616 100644 --- a/src/libslic3r/SLA/BranchingTreeSLA.cpp +++ b/src/libslic3r/SLA/BranchingTreeSLA.cpp @@ -13,20 +13,28 @@ namespace Slic3r { namespace sla { +inline constexpr const auto &beam_ex_policy = ex_tbb; + class BranchingTreeBuilder: public branchingtree::Builder { SupportTreeBuilder &m_builder; const SupportableMesh &m_sm; const branchingtree::PointCloud &m_cloud; + std::vector m_pillars; // to put an index over them + + // cache succesfull ground connections + mutable std::map m_gnd_connections; + mutable execution::SpinningMutex m_gnd_connections_mtx; + // Scaling of the input value 'widening_factor:<0, 1>' to produce resonable // widening behaviour - static constexpr double WIDENING_SCALE = 0.02; + static constexpr double WIDENING_SCALE = 0.05; - double get_radius(const branchingtree::Node &j) + double get_radius(const branchingtree::Node &j) const { double w = WIDENING_SCALE * m_sm.cfg.pillar_widening_factor * j.weight; - return std::min(m_sm.cfg.base_radius_mm, double(j.Rmin) + w); + return double(j.Rmin) + w; } std::vector m_unroutable_pinheads; @@ -78,6 +86,44 @@ class BranchingTreeBuilder: public branchingtree::Builder { }); } + void discard_subtree_rescure(size_t root) + { + // Discard all the support points connecting to this branch. + // As a last resort, try to route child nodes to ground and stop + // traversing if any child branch succeeds. + traverse(m_cloud, root, [this](const branchingtree::Node &node) { + branchingtree::TraverseReturnT ret{true, true}; + + int suppid_parent = m_cloud.get_leaf_id(node.id); + int suppid_left = branchingtree::Node::ID_NONE; + int suppid_right = branchingtree::Node::ID_NONE; + + double glvl = ground_level(m_sm); + branchingtree::Node dst = node; + dst.pos.z() = glvl; + dst.weight += node.pos.z() - glvl; + + if (node.left >= 0 && add_ground_bridge(m_cloud.get(node.left), dst)) + ret.to_left = false; + else + suppid_left = m_cloud.get_leaf_id(node.left); + + if (node.right >= 0 && add_ground_bridge(m_cloud.get(node.right), dst)) + ret.to_right = false; + else + suppid_right = m_cloud.get_leaf_id(node.right); + + if (suppid_parent >= 0) + m_unroutable_pinheads.emplace_back(suppid_parent); + if (suppid_left >= 0) + m_unroutable_pinheads.emplace_back(suppid_left); + if (suppid_right >= 0) + m_unroutable_pinheads.emplace_back(suppid_right); + + return ret; + }); + } + public: BranchingTreeBuilder(SupportTreeBuilder &builder, const SupportableMesh &sm, @@ -98,13 +144,24 @@ public: bool add_mesh_bridge(const branchingtree::Node &from, const branchingtree::Node &to) override; + std::optional suggest_avoidance(const branchingtree::Node &from, + float max_bridge_len) const override; + void report_unroutable(const branchingtree::Node &j) override { - BOOST_LOG_TRIVIAL(error) << "Cannot route junction at " << j.pos.x() - << " " << j.pos.y() << " " << j.pos.z(); + double glvl = ground_level(m_sm); + branchingtree::Node dst = j; + dst.pos.z() = glvl; + dst.weight += j.pos.z() - glvl; + if (add_ground_bridge(j, dst)) + return; + + BOOST_LOG_TRIVIAL(warning) << "Cannot route junction at " << j.pos.x() + << " " << j.pos.y() << " " << j.pos.z(); // Discard all the support points connecting to this branch. - discard_subtree(j.id); + discard_subtree_rescure(j.id); +// discard_subtree(j.id); } const std::vector& unroutable_pinheads() const @@ -113,15 +170,28 @@ public: } bool is_valid() const override { return !m_builder.ctl().stopcondition(); } + + const std::vector & pillars() const { return m_pillars; } + + const GroundConnection *ground_conn(size_t pillar) const + { + const GroundConnection *ret = nullptr; + + auto it = m_gnd_connections.find(m_pillars[pillar].id); + if (it != m_gnd_connections.end()) + ret = &it->second; + + return ret; + } }; bool BranchingTreeBuilder::add_bridge(const branchingtree::Node &from, - const branchingtree::Node &to) + const branchingtree::Node &to) { Vec3d fromd = from.pos.cast(), tod = to.pos.cast(); double fromR = get_radius(from), toR = get_radius(to); Beam beam{Ball{fromd, fromR}, Ball{tod, toR}}; - auto hit = beam_mesh_hit(ex_tbb, m_sm.emesh, beam, + auto hit = beam_mesh_hit(beam_ex_policy , m_sm.emesh, beam, m_sm.cfg.safety_distance_mm); bool ret = hit.distance() > (tod - fromd).norm(); @@ -144,8 +214,8 @@ bool BranchingTreeBuilder::add_merger(const branchingtree::Node &node, Beam beam2{Ball{from2d, closestR}, Ball{tod, mergeR}}; auto sd = m_sm.cfg.safety_distance_mm ; - auto hit1 = beam_mesh_hit(ex_tbb, m_sm.emesh, beam1, sd); - auto hit2 = beam_mesh_hit(ex_tbb, m_sm.emesh, beam2, sd); + auto hit1 = beam_mesh_hit(beam_ex_policy , m_sm.emesh, beam1, sd); + auto hit2 = beam_mesh_hit(beam_ex_policy , m_sm.emesh, beam2, sd); bool ret = hit1.distance() > (tod - from1d).norm() && hit2.distance() > (tod - from2d).norm(); @@ -156,12 +226,31 @@ bool BranchingTreeBuilder::add_merger(const branchingtree::Node &node, bool BranchingTreeBuilder::add_ground_bridge(const branchingtree::Node &from, const branchingtree::Node &to) { - bool ret = search_ground_route(ex_tbb, m_builder, m_sm, - sla::Junction{from.pos.cast(), - get_radius(from)}, - get_radius(to)).first; + bool ret = false; - if (ret) { + namespace bgi = boost::geometry::index; + + auto it = m_gnd_connections.find(from.id); + const GroundConnection *connptr = nullptr; + + if (it == m_gnd_connections.end()) { + sla::Junction j{from.pos.cast(), get_radius(from)}; + Vec3d init_dir = (to.pos - from.pos).cast().normalized(); + + auto conn = deepsearch_ground_connection(beam_ex_policy , m_sm, j, + get_radius(to), init_dir); + + // Remember that this node was tested if can go to ground, don't + // test it with any other destination ground point because + // it is unlikely that search_ground_route would find a better solution + connptr = &(m_gnd_connections[from.id] = conn); + } else { + connptr = &(it->second); + } + + if (connptr && *connptr) { + m_pillars.emplace_back(from); + ret = true; build_subtree(from.id); } @@ -171,17 +260,20 @@ bool BranchingTreeBuilder::add_ground_bridge(const branchingtree::Node &from, bool BranchingTreeBuilder::add_mesh_bridge(const branchingtree::Node &from, const branchingtree::Node &to) { + if (from.weight > m_sm.cfg.max_weight_on_model_support) + return false; + sla::Junction fromj = {from.pos.cast(), get_radius(from)}; auto anchor = m_sm.cfg.ground_facing_only ? std::optional{} : // If no mesh connections are allowed - calculate_anchor_placement(ex_tbb, m_sm, fromj, + calculate_anchor_placement(beam_ex_policy , m_sm, fromj, to.pos.cast()); if (anchor) { sla::Junction toj = {anchor->junction_point(), anchor->r_back_mm}; - auto hit = beam_mesh_hit(ex_tbb, m_sm.emesh, + auto hit = beam_mesh_hit(beam_ex_policy , m_sm.emesh, Beam{{fromj.pos, fromj.r}, {toj.pos, toj.r}}, 0.); if (hit.distance() > distance(fromj.pos, toj.pos)) { @@ -197,6 +289,70 @@ bool BranchingTreeBuilder::add_mesh_bridge(const branchingtree::Node &from, return bool(anchor); } +static std::optional get_avoidance(const GroundConnection &conn, + float maxdist) +{ + std::optional ret; + + if (conn) { + if (conn.path.size() > 1) { + ret = conn.path[1].pos.cast(); + } else { + Vec3f pbeg = conn.path[0].pos.cast(); + Vec3f pend = conn.pillar_base->pos.cast(); + pbeg.z() = std::max(pbeg.z() - maxdist, pend.z()); + ret = pbeg; + } + } + + return ret; +} + +std::optional BranchingTreeBuilder::suggest_avoidance( + const branchingtree::Node &from, float max_bridge_len) const +{ + std::optional ret; + + double glvl = ground_level(m_sm); + branchingtree::Node dst = from; + dst.pos.z() = glvl; + dst.weight += from.pos.z() - glvl; + sla::Junction j{from.pos.cast(), get_radius(from)}; + + auto found_it = m_gnd_connections.end(); + { + std::lock_guard lk{m_gnd_connections_mtx}; + found_it = m_gnd_connections.find(from.id); + } + + if (found_it != m_gnd_connections.end()) { + ret = get_avoidance(found_it->second, max_bridge_len); + } else { + auto conn = deepsearch_ground_connection( + beam_ex_policy , m_sm, j, get_radius(dst), sla::DOWN); + + { + std::lock_guard lk{m_gnd_connections_mtx}; + m_gnd_connections[from.id] = conn; + } + + ret = get_avoidance(conn, max_bridge_len); + } + + return ret; +} + +inline void build_pillars(SupportTreeBuilder &builder, + BranchingTreeBuilder &vbuilder, + const SupportableMesh &sm) +{ + for (size_t pill_id = 0; pill_id < vbuilder.pillars().size(); ++pill_id) { + auto * conn = vbuilder.ground_conn(pill_id); + if (conn) + build_ground_connection(builder, sm, *conn); + } +} + void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &sm) { auto coordfn = [&sm](size_t id, size_t dim) { return sm.pts[id].pos(dim); }; @@ -221,7 +377,7 @@ void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &s for (auto &h : heads) if (h && h->is_valid()) { leafs.emplace_back(h->junction_point().cast(), h->r_back_mm); - h->id = leafs.size() - 1; + h->id = long(leafs.size() - 1); builder.add_head(h->id, *h); } @@ -240,15 +396,29 @@ void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &s props.sampling_radius()); auto bedpts = branchingtree::sample_bed(props.bed_shape(), - props.ground_level(), + float(props.ground_level()), props.sampling_radius()); + for (auto &bp : bedpts) + bp.Rmin = sm.cfg.head_back_radius_mm; + branchingtree::PointCloud nodes{std::move(meshpts), std::move(bedpts), std::move(leafs), props}; BranchingTreeBuilder vbuilder{builder, sm, nodes}; + + execution::for_each(ex_tbb, + size_t(0), + nodes.get_leafs().size(), + [&nodes, &vbuilder](size_t leaf_idx) { + vbuilder.suggest_avoidance(nodes.get_leafs()[leaf_idx], + nodes.properties().max_branch_length()); + }); + branchingtree::build_tree(nodes, vbuilder); + build_pillars(builder, vbuilder, sm); + for (size_t id : vbuilder.unroutable_pinheads()) builder.head(id).invalidate(); diff --git a/src/libslic3r/SLA/DefaultSupportTree.cpp b/src/libslic3r/SLA/DefaultSupportTree.cpp index 9e21fca45..429cd45c6 100644 --- a/src/libslic3r/SLA/DefaultSupportTree.cpp +++ b/src/libslic3r/SLA/DefaultSupportTree.cpp @@ -340,14 +340,17 @@ bool DefaultSupportTree::connect_to_nearpillar(const Head &head, return true; } -bool DefaultSupportTree::create_ground_pillar(const Vec3d &hjp, +bool DefaultSupportTree::create_ground_pillar(const Junction &hjp, const Vec3d &sourcedir, - double radius, long head_id) { auto [ret, pillar_id] = sla::create_ground_pillar(suptree_ex_policy, - m_builder, m_sm, hjp, - sourcedir, radius, radius, + m_builder, + m_sm, + hjp.pos, + sourcedir, + hjp.r, + hjp.r, head_id); if (pillar_id >= 0) // Save the pillar endpoint in the spatial index @@ -362,20 +365,19 @@ void DefaultSupportTree::add_pinheads() // The minimum distance for two support points to remain valid. const double /*constexpr*/ D_SP = 0.1; - // Get the points that are too close to each other and keep only the - // first one + // Get the points that are too close to each other and keep only the + // first one auto aliases = cluster(m_points, D_SP, 2); PtIndices filtered_indices; filtered_indices.reserve(aliases.size()); m_iheads.reserve(aliases.size()); - m_iheadless.reserve(aliases.size()); for(auto& a : aliases) { // Here we keep only the front point of the cluster. filtered_indices.emplace_back(a.front()); } - // calculate the normals to the triangles for filtered points + // calculate the normals to the triangles for filtered points auto nmls = normals(suptree_ex_policy, m_points, m_sm.emesh, m_sm.cfg.head_front_radius_mm, m_thr, filtered_indices); @@ -404,22 +406,22 @@ void DefaultSupportTree::add_pinheads() Vec3d n = nmls.row(Eigen::Index(i)); - // for all normals we generate the spherical coordinates and - // saturate the polar angle to 45 degrees from the bottom then - // convert back to standard coordinates to get the new normal. - // Then we just create a quaternion from the two normals - // (Quaternion::FromTwoVectors) and apply the rotation to the - // arrow head. + // for all normals we generate the spherical coordinates and + // saturate the polar angle to 45 degrees from the bottom then + // convert back to standard coordinates to get the new normal. + // Then we just create a quaternion from the two normals + // (Quaternion::FromTwoVectors) and apply the rotation to the + // arrow head. auto [polar, azimuth] = dir_to_spheric(n); - // skip if the tilt is not sane + // skip if the tilt is not sane if (polar < PI - m_sm.cfg.normal_cutoff_angle) return; - // We saturate the polar angle to 3pi/4 + // We saturate the polar angle to 3pi/4 polar = std::max(polar, PI - m_sm.cfg.bridge_slope); - // save the head (pinpoint) position + // save the head (pinpoint) position Vec3d hp = m_points.row(fidx); double lmin = m_sm.cfg.head_width_mm, lmax = lmin; @@ -428,18 +430,17 @@ void DefaultSupportTree::add_pinheads() lmin = 0., lmax = m_sm.cfg.head_penetration_mm; } - // The distance needed for a pinhead to not collide with model. + // The distance needed for a pinhead to not collide with model. double w = lmin + 2 * back_r + 2 * m_sm.cfg.head_front_radius_mm - m_sm.cfg.head_penetration_mm; double pin_r = double(m_sm.pts[fidx].head_front_radius); - // Reassemble the now corrected normal + // Reassemble the now corrected normal auto nn = spheric_to_dir(polar, azimuth).normalized(); - // check available distance - AABBMesh::hit_result t = pinhead_mesh_intersect(hp, nn, pin_r, - back_r, w); + // check available distance + AABBMesh::hit_result t = pinhead_mesh_intersect(hp, nn, pin_r, back_r, w); if (t.distance() < w) { // Let's try to optimize this angle, there might be a @@ -509,10 +510,10 @@ void DefaultSupportTree::classify() ground_head_indices.reserve(m_iheads.size()); m_iheads_onmodel.reserve(m_iheads.size()); - // First we decide which heads reach the ground and can be full - // pillars and which shall be connected to the model surface (or - // search a suitable path around the surface that leads to the - // ground -- TODO) + // First we decide which heads reach the ground and can be full + // pillars and which shall be connected to the model surface (or + // search a suitable path around the surface that leads to the + // ground -- TODO) for(unsigned i : m_iheads) { m_thr(); @@ -530,10 +531,10 @@ void DefaultSupportTree::classify() m_head_to_ground_scans[i] = hit; } - // We want to search for clusters of points that are far enough - // from each other in the XY plane to not cross their pillar bases - // These clusters of support points will join in one pillar, - // possibly in their centroid support point. + // We want to search for clusters of points that are far enough + // from each other in the XY plane to not cross their pillar bases + // These clusters of support points will join in one pillar, + // possibly in their centroid support point. auto pointfn = [this](unsigned i) { return m_builder.head(i).junction_point(); @@ -559,13 +560,13 @@ void DefaultSupportTree::routing_to_ground() for (auto &cl : m_pillar_clusters) { m_thr(); - // place all the centroid head positions into the index. We - // will query for alternative pillar positions. If a sidehead - // cannot connect to the cluster centroid, we have to search - // for another head with a full pillar. Also when there are two - // elements in the cluster, the centroid is arbitrary and the - // sidehead is allowed to connect to a nearby pillar to - // increase structural stability. + // place all the centroid head positions into the index. We + // will query for alternative pillar positions. If a sidehead + // cannot connect to the cluster centroid, we have to search + // for another head with a full pillar. Also when there are two + // elements in the cluster, the centroid is arbitrary and the + // sidehead is allowed to connect to a nearby pillar to + // increase structural stability. if (cl.empty()) continue; @@ -587,7 +588,7 @@ void DefaultSupportTree::routing_to_ground() Head &h = m_builder.head(hid); - if (!create_ground_pillar(h.junction_point(), h.dir, h.r_back_mm, h.id)) { + if (!create_ground_pillar(h.junction(), h.dir, h.id)) { BOOST_LOG_TRIVIAL(warning) << "Pillar cannot be created for support point id: " << hid; m_iheads_onmodel.emplace_back(h.id); @@ -595,11 +596,11 @@ void DefaultSupportTree::routing_to_ground() } } - // now we will go through the clusters ones again and connect the - // sidepoints with the cluster centroid (which is a ground pillar) - // or a nearby pillar if the centroid is unreachable. + // now we will go through the clusters ones again and connect the + // sidepoints with the cluster centroid (which is a ground pillar) + // or a nearby pillar if the centroid is unreachable. size_t ci = 0; - for (auto cl : m_pillar_clusters) { + for (const std::vector &cl : m_pillar_clusters) { m_thr(); auto cidx = cl_centroids[ci++]; @@ -615,10 +616,9 @@ void DefaultSupportTree::routing_to_ground() if (!connect_to_nearpillar(sidehead, centerpillarID) && !search_pillar_and_connect(sidehead)) { - Vec3d pstart = sidehead.junction_point(); // Vec3d pend = Vec3d{pstart.x(), pstart.y(), gndlvl}; // Could not find a pillar, create one - create_ground_pillar(pstart, sidehead.dir, sidehead.r_back_mm, sidehead.id); + create_ground_pillar(sidehead.junction(), sidehead.dir, sidehead.id); } } } @@ -664,10 +664,10 @@ bool DefaultSupportTree::connect_to_model_body(Head &head) zangle = std::max(zangle, PI/4); double h = std::sin(zangle) * head.fullwidth(); - // The width of the tail head that we would like to have... + // The width of the tail head that we would like to have... h = std::min(hit.distance() - head.r_back_mm, h); - // If this is a mini pillar dont bother with the tail width, can be 0. + // If this is a mini pillar dont bother with the tail width, can be 0. if (head.r_back_mm < m_sm.cfg.head_back_radius_mm) h = std::max(h, 0.); else if (h <= 0.) return false; @@ -773,23 +773,23 @@ void DefaultSupportTree::interconnect_pillars() // Ideally every pillar should be connected with at least one of its // neighbors if that neighbor is within max_pillar_link_distance - // Pillars with height exceeding H1 will require at least one neighbor - // to connect with. Height exceeding H2 require two neighbors. + // Pillars with height exceeding H1 will require at least one neighbor + // to connect with. Height exceeding H2 require two neighbors. double H1 = m_sm.cfg.max_solo_pillar_height_mm; double H2 = m_sm.cfg.max_dual_pillar_height_mm; double d = m_sm.cfg.max_pillar_link_distance_mm; - //A connection between two pillars only counts if the height ratio is - // bigger than 50% + // A connection between two pillars only counts if the height ratio is + // bigger than 50% double min_height_ratio = 0.5; std::set pairs; - // A function to connect one pillar with its neighbors. THe number of - // neighbors is given in the configuration. This function if called - // for every pillar in the pillar index. A pair of pillar will not - // be connected multiple times this is ensured by the 'pairs' set which - // remembers the processed pillar pairs + // A function to connect one pillar with its neighbors. THe number of + // neighbors is given in the configuration. This function if called + // for every pillar in the pillar index. A pair of pillar will not + // be connected multiple times this is ensured by the 'pairs' set which + // remembers the processed pillar pairs auto cascadefn = [this, d, &pairs, min_height_ratio, H1] (const PointIndexEl& el) { @@ -797,10 +797,10 @@ void DefaultSupportTree::interconnect_pillars() const Pillar& pillar = m_builder.pillar(el.second); // actual pillar - // Get the max number of neighbors a pillar should connect to + // Get the max number of neighbors a pillar should connect to unsigned neighbors = m_sm.cfg.pillar_cascade_neighbors; - // connections are already enough for the pillar + // connections are already enough for the pillar if(pillar.links >= neighbors) return; double max_d = d * pillar.r_start / m_sm.cfg.head_back_radius_mm; @@ -809,7 +809,7 @@ void DefaultSupportTree::interconnect_pillars() return distance(e.first, qp) < max_d; }); - // sort the result by distance (have to check if this is needed) + // sort the result by distance (have to check if this is needed) std::sort(qres.begin(), qres.end(), [qp](const PointIndexEl& e1, const PointIndexEl& e2){ return distance(e1.first, qp) < distance(e2.first, qp); @@ -821,23 +821,23 @@ void DefaultSupportTree::interconnect_pillars() auto a = el.second, b = re.second; - // Get unique hash for the given pair (order doesn't matter) + // Get unique hash for the given pair (order doesn't matter) auto hashval = pairhash(a, b); - // Search for the pair amongst the remembered pairs + // Search for the pair amongst the remembered pairs if(pairs.find(hashval) != pairs.end()) continue; const Pillar& neighborpillar = m_builder.pillar(re.second); - // this neighbor is occupied, skip + // this neighbor is occupied, skip if (neighborpillar.links >= neighbors) continue; if (neighborpillar.r_start < pillar.r_start) continue; if(interconnect(pillar, neighborpillar)) { pairs.insert(hashval); - // If the interconnection length between the two pillars is - // less than 50% of the longer pillar's height, don't count + // If the interconnection length between the two pillars is + // less than 50% of the longer pillar's height, don't count if(pillar.height < H1 || neighborpillar.height / pillar.height > min_height_ratio) m_builder.increment_links(pillar); @@ -848,30 +848,30 @@ void DefaultSupportTree::interconnect_pillars() } - // connections are enough for one pillar + // connections are enough for one pillar if(pillar.links >= neighbors) break; } }; - // Run the cascade for the pillars in the index + // Run the cascade for the pillars in the index m_pillar_index.foreach(cascadefn); - // We would be done here if we could allow some pillars to not be - // connected with any neighbors. But this might leave the support tree - // unprintable. - // - // The current solution is to insert additional pillars next to these - // lonely pillars. One or even two additional pillar might get inserted - // depending on the length of the lonely pillar. + // We would be done here if we could allow some pillars to not be + // connected with any neighbors. But this might leave the support tree + // unprintable. + // + // The current solution is to insert additional pillars next to these + // lonely pillars. One or even two additional pillar might get inserted + // depending on the length of the lonely pillar. size_t pillarcount = m_builder.pillarcount(); - // Again, go through all pillars, this time in the whole support tree - // not just the index. + // Again, go through all pillars, this time in the whole support tree + // not just the index. for(size_t pid = 0; pid < pillarcount; pid++) { auto pillar = [this, pid]() { return m_builder.pillar(pid); }; - // Decide how many additional pillars will be needed: + // Decide how many additional pillars will be needed: unsigned needpillars = 0; if (pillar().bridges > m_sm.cfg.max_bridges_on_pillar) @@ -887,17 +887,17 @@ void DefaultSupportTree::interconnect_pillars() needpillars = std::max(pillar().links, needpillars) - pillar().links; if (needpillars == 0) continue; - // Search for new pillar locations: + // Search for new pillar locations: bool found = false; double alpha = 0; // goes to 2Pi double r = 2 * m_sm.cfg.base_radius_mm; Vec3d pillarsp = pillar().startpoint(); - // temp value for starting point detection + // temp value for starting point detection Vec3d sp(pillarsp.x(), pillarsp.y(), pillarsp.z() - r); - // A vector of bool for placement feasbility + // A vector of bool for placement feasbility std::vector canplace(needpillars, false); std::vector spts(needpillars); // vector of starting points @@ -916,12 +916,12 @@ void DefaultSupportTree::interconnect_pillars() s.y() += std::sin(a) * r; spts[n] = s; - // Check the path vertically down + // Check the path vertically down Vec3d check_from = s + Vec3d{0., 0., pillar().r_start}; auto hr = bridge_mesh_intersect(check_from, DOWN, pillar().r_start); Vec3d gndsp{s.x(), s.y(), gnd}; - // If the path is clear, check for pillar base collisions + // If the path is clear, check for pillar base collisions canplace[n] = std::isinf(hr.distance()) && std::sqrt(m_sm.emesh.squared_distance(gndsp)) > min_dist; @@ -930,7 +930,7 @@ void DefaultSupportTree::interconnect_pillars() found = std::all_of(canplace.begin(), canplace.end(), [](bool v) { return v; }); - // 20 angles will be tried... + // 20 angles will be tried... alpha += 0.1 * PI; } diff --git a/src/libslic3r/SLA/DefaultSupportTree.hpp b/src/libslic3r/SLA/DefaultSupportTree.hpp index ff0269978..08990a8df 100644 --- a/src/libslic3r/SLA/DefaultSupportTree.hpp +++ b/src/libslic3r/SLA/DefaultSupportTree.hpp @@ -1,7 +1,7 @@ #ifndef LEGACYSUPPORTTREE_HPP #define LEGACYSUPPORTTREE_HPP -#include "SupportTreeUtils.hpp" +#include "SupportTreeUtilsLegacy.hpp" #include #include @@ -62,7 +62,6 @@ class DefaultSupportTree { PtIndices m_iheads; // support points with pinhead PtIndices m_iheads_onmodel; - PtIndices m_iheadless; // headless support points std::map m_head_to_ground_scans; @@ -176,9 +175,8 @@ class DefaultSupportTree { // jp is the starting junction point which needs to be routed down. // sourcedir is the allowed direction of an optional bridge between the // jp junction and the final pillar. - bool create_ground_pillar(const Vec3d &jp, + bool create_ground_pillar(const Junction &jp, const Vec3d &sourcedir, - double radius, long head_id = SupportTreeNode::ID_UNSET); void add_pillar_base(long pid) diff --git a/src/libslic3r/SLA/SupportTree.cpp b/src/libslic3r/SLA/SupportTree.cpp index b221c4330..cfafdf7e9 100644 --- a/src/libslic3r/SLA/SupportTree.cpp +++ b/src/libslic3r/SLA/SupportTree.cpp @@ -18,6 +18,8 @@ #include #include +#include + //! macro used to mark string used at localization, //! return same string #define L(s) Slic3r::I18N::translate(s) @@ -30,6 +32,9 @@ indexed_triangle_set create_support_tree(const SupportableMesh &sm, auto builder = make_unique(ctl); if (sm.cfg.enabled) { + Benchmark bench; + bench.start(); + switch (sm.cfg.tree_type) { case SupportTreeType::Default: { create_default_tree(*builder, sm); @@ -39,9 +44,18 @@ indexed_triangle_set create_support_tree(const SupportableMesh &sm, create_branching_tree(*builder, sm); break; } + case SupportTreeType::Organic: { + // TODO + } default:; } + bench.stop(); + + BOOST_LOG_TRIVIAL(info) << "Support tree creation took: " + << bench.getElapsedSec() + << " seconds"; + builder->merge_and_cleanup(); // clean metadata, leave only the meshes. } diff --git a/src/libslic3r/SLA/SupportTree.hpp b/src/libslic3r/SLA/SupportTree.hpp index 0ac29ff7a..e0d7db97d 100644 --- a/src/libslic3r/SLA/SupportTree.hpp +++ b/src/libslic3r/SLA/SupportTree.hpp @@ -50,7 +50,7 @@ struct SupportTreeConfig // when bridges and pillars are merged. The resulting pillar should be a bit // thicker than the ones merging into it. How much thicker? I don't know // but it will be derived from this value. - double pillar_widening_factor = .05; + double pillar_widening_factor = .5; // Radius in mm of the pillar base. double base_radius_mm = 2.0; @@ -76,12 +76,20 @@ struct SupportTreeConfig double pillar_base_safety_distance_mm = 0.5; unsigned max_bridges_on_pillar = 3; - + + double max_weight_on_model_support = 10.f; + double head_fullwidth() const { return 2 * head_front_radius_mm + head_width_mm + 2 * head_back_radius_mm - head_penetration_mm; } + double safety_distance() const { return safety_distance_mm; } + double safety_distance(double r) const + { + return std::min(safety_distance_mm, r * safety_distance_mm / head_back_radius_mm); + } + // ///////////////////////////////////////////////////////////////////////// // Compile time configuration values (candidates for runtime) // ///////////////////////////////////////////////////////////////////////// @@ -89,13 +97,15 @@ struct SupportTreeConfig // The max Z angle for a normal at which it will get completely ignored. static const double constexpr normal_cutoff_angle = 150.0 * M_PI / 180.0; - // The shortest distance of any support structure from the model surface + // The safety gap between a support structure and model body. For support + // struts smaller than head_back_radius, the safety distance is scaled + // down accordingly. see method safety_distance() static const double constexpr safety_distance_mm = 0.5; static const double constexpr max_solo_pillar_height_mm = 15.0; static const double constexpr max_dual_pillar_height_mm = 35.0; - static const double constexpr optimizer_rel_score_diff = 1e-6; - static const unsigned constexpr optimizer_max_iterations = 1000; + static const double constexpr optimizer_rel_score_diff = 1e-10; + static const unsigned constexpr optimizer_max_iterations = 2000; static const unsigned constexpr pillar_cascade_neighbors = 3; }; @@ -115,11 +125,11 @@ struct SupportableMesh : emesh{trmsh}, pts{sp}, cfg{c} {} - explicit SupportableMesh(const AABBMesh &em, - const SupportPoints &sp, - const SupportTreeConfig &c) - : emesh{em}, pts{sp}, cfg{c} - {} +// explicit SupportableMesh(const AABBMesh &em, +// const SupportPoints &sp, +// const SupportTreeConfig &c) +// : emesh{em}, pts{sp}, cfg{c} +// {} }; inline double ground_level(const SupportableMesh &sm) diff --git a/src/libslic3r/SLA/SupportTreeBuilder.hpp b/src/libslic3r/SLA/SupportTreeBuilder.hpp index 29d34ab8e..93fbead99 100644 --- a/src/libslic3r/SLA/SupportTreeBuilder.hpp +++ b/src/libslic3r/SLA/SupportTreeBuilder.hpp @@ -67,25 +67,32 @@ struct SupportTreeNode long id = ID_UNSET; // For identification withing a tree. }; +// A junction connecting bridges and pillars +struct Junction: public SupportTreeNode { + double r = 1; + Vec3d pos; + + Junction(const Vec3d &tr, double r_mm) : r(r_mm), pos(tr) {} +}; + // A pinhead originating from a support point struct Head: public SupportTreeNode { Vec3d dir = DOWN; Vec3d pos = {0, 0, 0}; - + double r_back_mm = 1; double r_pin_mm = 0.5; double width_mm = 2; double penetration_mm = 0.5; - // If there is a pillar connecting to this head, then the id will be set. long pillar_id = ID_UNSET; - + long bridge_id = ID_UNSET; - + inline void invalidate() { id = ID_UNSET; } inline bool is_valid() const { return id >= 0; } - + Head(double r_big_mm, double r_small_mm, double length_mm, @@ -103,21 +110,21 @@ struct Head: public SupportTreeNode { { return real_width() - penetration_mm; } - + + inline Junction junction() const + { + Junction j{pos + (fullwidth() - r_back_mm) * dir, r_back_mm}; + j.id = -this->id; // Remember that this junction is from a head + + return j; + } + inline Vec3d junction_point() const { - return pos + (fullwidth() - r_back_mm) * dir; + return junction().pos; } }; -// A junction connecting bridges and pillars -struct Junction: public SupportTreeNode { - double r = 1; - Vec3d pos; - - Junction(const Vec3d &tr, double r_mm) : r(r_mm), pos(tr) {} -}; - // A straight pillar. Only has an endpoint and a height. No explicit starting // point is given, as it would allow the pillar to be angled. // Some connection info with other primitives can also be tracked. @@ -189,6 +196,10 @@ struct DiffBridge: public Bridge { DiffBridge(const Vec3d &p_s, const Vec3d &p_e, double r_s, double r_e) : Bridge{p_s, p_e, r_s}, end_r{r_e} {} + + DiffBridge(const Junction &j_s, const Junction &j_e) + : Bridge{j_s.pos, j_e.pos, j_s.r}, end_r{j_e.r} + {} }; // This class will hold the support tree parts (not meshes, but logical parts) diff --git a/src/libslic3r/SLA/SupportTreeMesher.cpp b/src/libslic3r/SLA/SupportTreeMesher.cpp index 3f0b6c841..6d91de7e6 100644 --- a/src/libslic3r/SLA/SupportTreeMesher.cpp +++ b/src/libslic3r/SLA/SupportTreeMesher.cpp @@ -165,7 +165,8 @@ indexed_triangle_set halfcone(double baseheight, { assert(steps > 0); - if (baseheight <= 0 || steps <= 0) return {}; + if (baseheight <= 0 || steps <= 0 || (r_bottom <= 0. && r_top <= 0.)) + return {}; indexed_triangle_set base; diff --git a/src/libslic3r/SLA/SupportTreeStrategies.hpp b/src/libslic3r/SLA/SupportTreeStrategies.hpp index 487f43575..2cef1a8a9 100644 --- a/src/libslic3r/SLA/SupportTreeStrategies.hpp +++ b/src/libslic3r/SLA/SupportTreeStrategies.hpp @@ -5,7 +5,7 @@ namespace Slic3r { namespace sla { -enum class SupportTreeType { Default, Branching }; +enum class SupportTreeType { Default, Branching, Organic }; enum class PillarConnectionMode { zigzag, cross, dynamic }; }} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SupportTreeUtils.hpp b/src/libslic3r/SLA/SupportTreeUtils.hpp index 38515f879..93f370c32 100644 --- a/src/libslic3r/SLA/SupportTreeUtils.hpp +++ b/src/libslic3r/SLA/SupportTreeUtils.hpp @@ -6,10 +6,13 @@ #include #include +#include #include #include #include +#include +#include #include namespace Slic3r { namespace sla { @@ -23,30 +26,6 @@ using Slic3r::opt::AlgNLoptGenetic; using Slic3r::Geometry::dir_to_spheric; using Slic3r::Geometry::spheric_to_dir; -// 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> -IntegerOnly 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; -} - // Give points on a 3D ring with given center, radius and orientation // method based on: // https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space @@ -131,31 +110,40 @@ inline StopCriteria get_criteria(const SupportTreeConfig &cfg) // A simple sphere with a center and a radius struct Ball { Vec3d p; double R; }; -struct Beam { // Defines a set of rays displaced along a cone's surface - static constexpr size_t SAMPLES = 8; +template +struct Beam_ { // Defines a set of rays displaced along a cone's surface + static constexpr size_t SAMPLES = Samples; - Vec3d src; - Vec3d dir; + Vec3d src; + Vec3d dir; double r1; double r2; // radius of the beam 1 unit further from src in dir direction - Beam(const Vec3d &s, const Vec3d &d, double R1, double R2): - src{s}, dir{d}, r1{R1}, r2{R2} {}; + Beam_(const Vec3d &s, const Vec3d &d, double R1, double R2) + : src{s}, dir{d}, r1{R1}, r2{R2} {}; - Beam(const Ball &src_ball, const Ball &dst_ball): - src{src_ball.p}, dir{dirv(src_ball.p, dst_ball.p)}, r1{src_ball.R} + Beam_(const Ball &src_ball, const Ball &dst_ball) + : src{src_ball.p}, dir{dirv(src_ball.p, dst_ball.p)}, r1{src_ball.R} { - r2 = src_ball.R + - (dst_ball.R - src_ball.R) / distance(src_ball.p, dst_ball.p); + r2 = src_ball.R; + auto d = distance(src_ball.p, dst_ball.p); + + if (d > EPSILON) + r2 += (dst_ball.R - src_ball.R) / d; } - Beam(const Vec3d &s, const Vec3d &d, double R) + Beam_(const Vec3d &s, const Vec3d &d, double R) : src{s}, dir{d}, r1{R}, r2{R} {} }; -template -Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd) +using Beam = Beam_<8>; + +template +Hit beam_mesh_hit(Ex policy, + const AABBMesh &mesh, + const Beam_ &beam, + double sd) { Vec3d src = beam.src; Vec3d dst = src + beam.dir; @@ -164,19 +152,19 @@ Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd) Vec3d D = (dst - src); Vec3d dir = D.normalized(); - PointRing ring{dir}; + PointRing ring{dir}; using Hit = AABBMesh::hit_result; // Hit results - std::array hits; + std::array hits; execution::for_each( - ex, size_t(0), hits.size(), + policy, size_t(0), hits.size(), [&mesh, r_src, r_dst, src, dst, &ring, dir, sd, &hits](size_t i) { Hit &hit = hits[i]; - // Point on the circle on the pin sphere + // Point on the circle on the pin sphere Vec3d p_src = ring.get(i, src, r_src + sd); Vec3d p_dst = ring.get(i, dst, r_dst + sd); Vec3d raydir = (p_dst - p_src).normalized(); @@ -193,7 +181,7 @@ Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd) } } else hit = hr; - }, std::min(execution::max_concurrency(ex), Beam::SAMPLES)); + }, std::min(execution::max_concurrency(policy), RayCount)); return min_hit(hits.begin(), hits.end()); } @@ -283,199 +271,12 @@ Hit pinhead_mesh_hit(Ex ex, template Hit pinhead_mesh_hit(Ex ex, - const AABBMesh &mesh, - const Head &head, - double safety_d) + const AABBMesh &mesh, + const Head &head, + double safety_d) { return pinhead_mesh_hit(ex, mesh, head.pos, head.dir, head.r_pin_mm, - head.r_back_mm, head.width_mm, safety_d); -} - -template -std::optional search_widening_path(Ex policy, - const SupportableMesh &sm, - const Vec3d &jp, - const Vec3d &dir, - double radius, - double new_radius) -{ - double w = radius + 2 * sm.cfg.head_back_radius_mm; - double stopval = w + jp.z() - ground_level(sm); - Optimizer solver(get_criteria(sm.cfg).stop_score(stopval)); - - auto [polar, azimuth] = dir_to_spheric(dir); - - double fallback_ratio = radius / sm.cfg.head_back_radius_mm; - - auto oresult = solver.to_max().optimize( - [&policy, &sm, jp, radius, new_radius](const opt::Input<3> &input) { - auto &[plr, azm, t] = input; - - auto d = spheric_to_dir(plr, azm).normalized(); - - auto sd = new_radius * sm.cfg.safety_distance_mm / - sm.cfg.head_back_radius_mm; - - double ret = pinhead_mesh_hit(policy, sm.emesh, jp, d, radius, - new_radius, t, sd) - .distance(); - - Beam beam{jp + t * d, d, new_radius}; - double down = beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); - - if (ret > t && std::isinf(down)) - ret += jp.z() - ground_level(sm); - - return ret; - }, - initvals({polar, azimuth, w}), // start with what we have - bounds({ - {PI - sm.cfg.bridge_slope, PI}, // Must not exceed the slope limit - {-PI, PI}, // azimuth can be a full search - {radius + sm.cfg.head_back_radius_mm, - fallback_ratio * sm.cfg.max_bridge_length_mm} - })); - - if (oresult.score >= stopval) { - polar = std::get<0>(oresult.optimum); - azimuth = std::get<1>(oresult.optimum); - double t = std::get<2>(oresult.optimum); - Vec3d endp = jp + t * spheric_to_dir(polar, azimuth); - - return DiffBridge(jp, endp, radius, sm.cfg.head_back_radius_mm); - } - - return {}; -} - -// This is a proxy function for pillar creation which will mind the gap -// between the pad and the model bottom in zero elevation mode. -// 'pinhead_junctionpt' is the starting junction point which needs to be -// routed down. sourcedir is the allowed direction of an optional bridge -// between the jp junction and the final pillar. -template -std::pair create_ground_pillar( - Ex policy, - SupportTreeBuilder &builder, - const SupportableMesh &sm, - const Vec3d &pinhead_junctionpt, - const Vec3d &sourcedir, - double radius, - double end_radius, - long head_id = SupportTreeNode::ID_UNSET) -{ - Vec3d jp = pinhead_junctionpt, endp = jp, dir = sourcedir; - long pillar_id = SupportTreeNode::ID_UNSET; - bool can_add_base = false, non_head = false; - - double gndlvl = 0.; // The Z level where pedestals should be - double jp_gnd = 0.; // The lowest Z where a junction center can be - double gap_dist = 0.; // The gap distance between the model and the pad - - double r2 = radius + (end_radius - radius) / (jp.z() - ground_level(sm)); - - auto to_floor = [&gndlvl](const Vec3d &p) { return Vec3d{p.x(), p.y(), gndlvl}; }; - - auto eval_limits = [&sm, &radius, &can_add_base, &gndlvl, &gap_dist, &jp_gnd] - (bool base_en = true) - { - can_add_base = base_en && radius >= sm.cfg.head_back_radius_mm; - double base_r = can_add_base ? sm.cfg.base_radius_mm : 0.; - gndlvl = ground_level(sm); - if (!can_add_base) gndlvl -= sm.pad_cfg.wall_thickness_mm; - jp_gnd = gndlvl + (can_add_base ? 0. : sm.cfg.head_back_radius_mm); - gap_dist = sm.cfg.pillar_base_safety_distance_mm + base_r + EPSILON; - }; - - eval_limits(); - - // We are dealing with a mini pillar that's potentially too long - if (radius < sm.cfg.head_back_radius_mm && jp.z() - gndlvl > 20 * radius) - { - std::optional diffbr = - search_widening_path(policy, sm, jp, dir, radius, - sm.cfg.head_back_radius_mm); - - if (diffbr && diffbr->endp.z() > jp_gnd) { - auto &br = builder.add_diffbridge(*diffbr); - if (head_id >= 0) builder.head(head_id).bridge_id = br.id; - endp = diffbr->endp; - radius = diffbr->end_r; - builder.add_junction(endp, radius); - non_head = true; - dir = diffbr->get_dir(); - eval_limits(); - } else return {false, pillar_id}; - } - - if (sm.cfg.object_elevation_mm < EPSILON) - { - // get a suitable direction for the corrector bridge. It is the - // original sourcedir's azimuth but the polar angle is saturated to the - // configured bridge slope. - auto [polar, azimuth] = dir_to_spheric(dir); - polar = PI - sm.cfg.bridge_slope; - Vec3d d = spheric_to_dir(polar, azimuth).normalized(); - auto sd = radius * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; - double t = beam_mesh_hit(policy, sm.emesh, Beam{endp, d, radius, r2}, sd).distance(); - double tmax = std::min(sm.cfg.max_bridge_length_mm, t); - t = 0.; - - double zd = endp.z() - jp_gnd; - double tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); - tmax = std::min(tmax, tmax2); - - Vec3d nexp = endp; - double dlast = 0.; - while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || - !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius, r2}, sd).distance())) && - t < tmax) - { - t += radius; - nexp = endp + t * d; - } - - if (dlast < gap_dist && can_add_base) { - nexp = endp; - t = 0.; - can_add_base = false; - eval_limits(can_add_base); - - zd = endp.z() - jp_gnd; - tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); - tmax = std::min(tmax, tmax2); - - while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || - !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius}, sd).distance())) && t < tmax) { - t += radius; - nexp = endp + t * d; - } - } - - // Could not find a path to avoid the pad gap - if (dlast < gap_dist) return {false, pillar_id}; - - if (t > 0.) { // Need to make additional bridge - const Bridge& br = builder.add_bridge(endp, nexp, radius); - if (head_id >= 0) builder.head(head_id).bridge_id = br.id; - - builder.add_junction(nexp, radius); - endp = nexp; - non_head = true; - } - } - - Vec3d gp = to_floor(endp); - double h = endp.z() - gp.z(); - - pillar_id = head_id >= 0 && !non_head ? builder.add_pillar(head_id, h) : - builder.add_pillar(gp, h, radius, end_radius); - - if (can_add_base) - builder.add_pillar_base(pillar_id, sm.cfg.base_height_mm, - sm.cfg.base_radius_mm); - - return {true, pillar_id}; + head.r_back_mm, head.width_mm, safety_d); } inline double distance(const SupportPoint &a, const SupportPoint &b) @@ -530,13 +331,13 @@ bool optimize_pinhead_placement(Ex policy, double back_r = head.r_back_mm; - // skip if the tilt is not sane + // skip if the tilt is not sane if (polar < PI - m.cfg.normal_cutoff_angle) return false; - // We saturate the polar angle to 3pi/4 + // We saturate the polar angle to 3pi/4 polar = std::max(polar, PI - m.cfg.bridge_slope); - // save the head (pinpoint) position + // save the head (pinpoint) position Vec3d hp = head.pos; double lmin = m.cfg.head_width_mm, lmax = lmin; @@ -545,28 +346,26 @@ bool optimize_pinhead_placement(Ex policy, lmin = 0., lmax = m.cfg.head_penetration_mm; } - // The distance needed for a pinhead to not collide with model. + // The distance needed for a pinhead to not collide with model. double w = lmin + 2 * back_r + 2 * m.cfg.head_front_radius_mm - m.cfg.head_penetration_mm; double pin_r = head.r_pin_mm; - // Reassemble the now corrected normal + // Reassemble the now corrected normal auto nn = spheric_to_dir(polar, azimuth).normalized(); - double sd = back_r * m.cfg.safety_distance_mm / - m.cfg.head_back_radius_mm; + double sd = m.cfg.safety_distance(back_r); - // check available distance - Hit t = pinhead_mesh_hit(policy, m.emesh, hp, nn, pin_r, back_r, w, - sd); + // check available distance + Hit t = pinhead_mesh_hit(policy, m.emesh, hp, nn, pin_r, back_r, w, sd); if (t.distance() < w) { // Let's try to optimize this angle, there might be a // viable normal that doesn't collide with the model // geometry and its very close to the default. - Optimizer solver(get_criteria(m.cfg).stop_score(w).max_iterations(100)); + Optimizer solver(get_criteria(m.cfg).stop_score(w).max_iterations(100)); solver.seed(0); // we want deterministic behavior auto oresult = solver.to_max().optimize( @@ -627,7 +426,7 @@ std::optional calculate_pinhead_placement(Ex policy, }; if (optimize_pinhead_placement(policy, sm, head)) { - head.id = suppt_idx; + head.id = long(suppt_idx); return head; } @@ -635,81 +434,335 @@ std::optional calculate_pinhead_placement(Ex policy, return {}; } -template -std::pair connect_to_ground(Ex policy, - SupportTreeBuilder &builder, - const SupportableMesh &sm, - const Junction &j, - const Vec3d &dir, - double end_r) +struct GroundConnection { + // Currently, a ground connection will contain at most 2 additional junctions + // which will not require any allocations. If I come up with an algo that + // can produce a route to ground with more junctions, this design will be + // able to handle that. + static constexpr size_t MaxExpectedJunctions = 3; + + boost::container::small_vector path; + std::optional pillar_base; + + operator bool() const { return pillar_base.has_value() && !path.empty(); } +}; + +inline long build_ground_connection(SupportTreeBuilder &builder, + const SupportableMesh &sm, + const GroundConnection &conn) { - auto hjp = j.pos; - double r = j.r; - auto sd = r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; - double r2 = j.r + (end_r - j.r) / (j.pos.z() - ground_level(sm)); + long ret = SupportTreeNode::ID_UNSET; - double t = beam_mesh_hit(policy, sm.emesh, Beam{hjp, dir, r, r2}, sd).distance(); - double d = 0, tdown = 0; - t = std::min(t, sm.cfg.max_bridge_length_mm * r / sm.cfg.head_back_radius_mm); + if (!conn) + return ret; - while (d < t && - !std::isinf(tdown = beam_mesh_hit(policy, sm.emesh, - Beam{hjp + d * dir, DOWN, r, r2}, sd) - .distance())) { - d += r; + auto it = conn.path.begin(); + auto itnx = std::next(it); + + while (itnx != conn.path.end()) { + builder.add_diffbridge(*it, *itnx); + builder.add_junction(*itnx); + ++it; ++itnx; } - if(!std::isinf(tdown)) - return {false, SupportTreeNode::ID_UNSET}; + auto gp = conn.path.back().pos; + gp.z() = ground_level(sm); + double h = conn.path.back().pos.z() - gp.z(); - Vec3d endp = hjp + d * dir; - auto ret = create_ground_pillar(policy, builder, sm, endp, dir, r, end_r); + if (conn.pillar_base->r_top < sm.cfg.head_back_radius_mm) { + h += sm.pad_cfg.wall_thickness_mm; + gp.z() -= sm.pad_cfg.wall_thickness_mm; + } - if (ret.second >= 0) { - builder.add_bridge(hjp, endp, r); - builder.add_junction(endp, r); +// TODO: does not work yet +// if (conn.path.back().id < 0) { +// // this is a head +// long head_id = std::abs(conn.path.back().id); +// ret = builder.add_pillar(head_id, h); +// } else + + ret = builder.add_pillar(gp, h, conn.path.back().r, conn.pillar_base->r_top); + + if (conn.pillar_base->r_top >= sm.cfg.head_back_radius_mm) + builder.add_pillar_base(ret, conn.pillar_base->height, conn.pillar_base->r_bottom); + + return ret; +} + +template +constexpr bool IsWideningFn = std::is_invocable_r_v; + +// A widening function can determine how many ray samples should a beam contain +// (see in beam_mesh_hit) +template struct BeamSamples { static constexpr size_t Value = 8; }; +template constexpr size_t BeamSamplesV = BeamSamples>::Value; + +// To use with check_ground_route, full will check the bridge and the pillar, +// PillarOnly checks only the pillar for collisions. +enum class GroundRouteCheck { Full, PillarOnly }; + +// Returns the collision point with mesh if there is a collision or a ground point, +// given a source point with a direction of a potential avoidance bridge and +// a bridge length. +template> > +Vec3d check_ground_route( + Ex policy, + const SupportableMesh &sm, + const Junction &source, // source location + const Vec3d &dir, // direction of the bridge from the source + double bridge_len, // lenght of the avoidance bridge + WideningFn &&wideningfn, // Widening strategy + GroundRouteCheck type = GroundRouteCheck::Full + ) +{ + static const constexpr auto Samples = BeamSamplesV; + + Vec3d ret; + + const auto sd = sm.cfg.safety_distance(source.r); + const auto gndlvl = ground_level(sm); + + // Intersection of the suggested bridge with ground plane. If the bridge + // spans below ground, stop it at ground level. + double t = (gndlvl - source.pos.z()) / dir.z(); + bridge_len = std::min(t, bridge_len); + + Vec3d bridge_end = source.pos + bridge_len * dir; + + double down_l = bridge_end.z() - gndlvl; + double bridge_r = wideningfn(Ball{source.pos, source.r}, dir, bridge_len); + double brhit_dist = 0.; + + if (bridge_len > EPSILON && type == GroundRouteCheck::Full) { + // beam_mesh_hit with a zero lenght bridge is invalid + + Beam_ bridgebeam{Ball{source.pos, source.r}, + Ball{bridge_end, bridge_r}}; + + auto brhit = beam_mesh_hit(policy, sm.emesh, bridgebeam, sd); + brhit_dist = brhit.distance(); + } else { + brhit_dist = bridge_len; + } + + if (brhit_dist < bridge_len) { + ret = (source.pos + brhit_dist * dir); + } else if (down_l > 0.) { + // check if pillar can be placed below + auto gp = Vec3d{bridge_end.x(), bridge_end.y(), gndlvl}; + double end_radius = wideningfn( + Ball{bridge_end, bridge_r}, DOWN, bridge_end.z() - gndlvl); + + Beam_ gndbeam {{bridge_end, bridge_r}, {gp, end_radius}}; + auto gndhit = beam_mesh_hit(policy, sm.emesh, gndbeam, sd); + double gnd_hit_d = std::min(gndhit.distance(), down_l + EPSILON); + + if (source.r >= sm.cfg.head_back_radius_mm && gndhit.distance() > down_l && sm.cfg.object_elevation_mm < EPSILON) { + // Dealing with zero elevation mode, to not route pillars + // into the gap between the optional pad and the model + double gap = std::sqrt(sm.emesh.squared_distance(gp)); + double base_r = std::max(sm.cfg.base_radius_mm, end_radius); + double min_gap = sm.cfg.pillar_base_safety_distance_mm + base_r; + + if (gap < min_gap) { + gnd_hit_d = down_l - min_gap + gap; + } + } + + ret = Vec3d{bridge_end.x(), bridge_end.y(), bridge_end.z() - gnd_hit_d}; + } else { + ret = bridge_end; } return ret; } -template -std::pair search_ground_route(Ex policy, - SupportTreeBuilder &builder, - const SupportableMesh &sm, - const Junction &j, - double end_radius, - const Vec3d &init_dir = DOWN) +// Searching a ground connection from an arbitrary source point. +// Currently, the result will contain one avoidance bridge (at most) and a +// pillar to the ground, if it's feasible +template> > +GroundConnection deepsearch_ground_connection( + Ex policy, + const SupportableMesh &sm, + const Junction &source, + WideningFn &&wideningfn, + const Vec3d &init_dir = DOWN) { - double downdst = j.pos.z() - ground_level(sm); + constexpr unsigned MaxIterationsGlobal = 5000; + constexpr unsigned MaxIterationsLocal = 100; + constexpr double RelScoreDiff = 0.05; - auto res = connect_to_ground(policy, builder, sm, j, init_dir, end_radius); - if (res.first) - return res; + const auto gndlvl = ground_level(sm); - // Optimize bridge direction: - // Straight path failed so we will try to search for a suitable - // direction out of the cavity. - auto [polar, azimuth] = dir_to_spheric(init_dir); + // The used solver (AlgNLoptMLSL_Subplx search method) is composed of a global (MLSL) + // and a local (Subplex) search method. Criteria can be set in a way that + // local searches are quick and less accurate. The global method will only + // consider the max iteration number and the stop score (Z level <= ground) - Optimizer solver(get_criteria(sm.cfg).stop_score(1e6)); - solver.seed(0); // we want deterministic behavior + auto criteria = get_criteria(sm.cfg); // get defaults from cfg + criteria.max_iterations(MaxIterationsGlobal); + criteria.abs_score_diff(NaNd); + criteria.rel_score_diff(NaNd); + criteria.stop_score(gndlvl); - auto sd = j.r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; - auto oresult = solver.to_max().optimize( - [&j, sd, &policy, &sm, &downdst, &end_radius](const opt::Input<2> &input) { - auto &[plr, azm] = input; - Vec3d n = spheric_to_dir(plr, azm).normalized(); - Beam beam{Ball{j.pos, j.r}, Ball{j.pos + downdst * n, end_radius}}; - return beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); - }, - initvals({polar, azimuth}), // let's start with what we have - bounds({ {PI - sm.cfg.bridge_slope, PI}, {-PI, PI} }) - ); + auto criteria_loc = criteria; + criteria_loc.max_iterations(MaxIterationsLocal); + criteria_loc.abs_score_diff(EPSILON); + criteria_loc.rel_score_diff(RelScoreDiff); - Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized(); + Optimizer solver(criteria); + solver.set_loc_criteria(criteria_loc); + solver.seed(0); // require repeatability - return connect_to_ground(policy, builder, sm, j, bridgedir, end_radius); + // functor returns the z height of collision point, given a polar and + // azimuth angles as bridge direction and bridge length. The route is + // traced from source, through this bridge and an attached pillar. If there + // is a collision with the mesh, the Z height is returned. Otherwise the + // z level of ground is returned. + auto z_fn = [&](const opt::Input<3> &input) { + // solver suggests polar, azimuth and bridge length values: + auto &[plr, azm, bridge_len] = input; + + Vec3d n = spheric_to_dir(plr, azm); + + Vec3d hitpt = check_ground_route(policy, sm, source, n, bridge_len, wideningfn); + + return hitpt.z(); + }; + + // Calculate the initial direction of the search by + // saturating the polar angle to max tilt defined in config + auto [plr_init, azm_init] = dir_to_spheric(init_dir); + plr_init = std::max(plr_init, PI - sm.cfg.bridge_slope); + + auto bound_constraints = + bounds({ + {PI - sm.cfg.bridge_slope, PI}, // bounds for polar angle + {-PI, PI}, // bounds for azimuth + {0., sm.cfg.max_bridge_length_mm} // bounds bridge length + }); + + // The optimizer can navigate fairly well on the mesh surface, finding + // lower and lower Z coordinates as collision points. MLSL is not a local + // search method, so it should not be trapped in a local minima. Eventually, + // this search should arrive at a ground location. + auto oresult = solver.to_min().optimize( + z_fn, + initvals({plr_init, azm_init, 0.}), + bound_constraints + ); + + GroundConnection conn; + + // Extract and apply the result + auto [plr, azm, bridge_l] = oresult.optimum; + Vec3d n = spheric_to_dir(plr, azm); + assert(std::abs(n.norm() - 1.) < EPSILON); + + double t = (gndlvl - source.pos.z()) / n.z(); + bridge_l = std::min(t, bridge_l); + + // Now the optimizer gave a possible route to ground with a bridge direction + // and length. This length can be shortened further by brute-force queries + // of free route straigt down for a possible pillar. + // NOTE: This requirement could be incorporated into the optimization as a + // constraint, but it would not find quickly enough an accurate solution, + // and it would be very hard to define a stop score which is very useful in + // terminating the search as soon as the ground is found. + double l = 0., l_max = bridge_l; + double zlvl = std::numeric_limits::infinity(); + while(zlvl > gndlvl && l <= l_max) { + + zlvl = check_ground_route(policy, sm, source, n, l, wideningfn, + GroundRouteCheck::PillarOnly).z(); + + if (zlvl <= gndlvl) + bridge_l = l; + + l += source.r; + } + + Vec3d bridge_end = source.pos + bridge_l * n; + Vec3d gp{bridge_end.x(), bridge_end.y(), gndlvl}; + + double bridge_r = wideningfn(Ball{source.pos, source.r}, n, bridge_l); + double down_l = bridge_end.z() - gndlvl; + double end_radius = wideningfn(Ball{bridge_end, bridge_r}, DOWN, down_l); + double base_r = std::max(sm.cfg.base_radius_mm, end_radius); + + // Even if the search was not succesful, the result is populated by the + // source and the last best result of the optimization. + conn.path.emplace_back(source); + if (bridge_l > EPSILON) + conn.path.emplace_back(Junction{bridge_end, bridge_r}); + + // The resulting ground connection is only valid if the pillar base is set. + // At this point it will only be set if the search was succesful. + if (z_fn(opt::Input<3>({plr, azm, bridge_l})) <= gndlvl) + conn.pillar_base = + Pedestal{gp, sm.cfg.base_height_mm, base_r, end_radius}; + + return conn; +} + +// Ground route search with a predefined end radius +template +GroundConnection deepsearch_ground_connection(Ex policy, + const SupportableMesh &sm, + const Junction &source, + double end_radius, + const Vec3d &init_dir = DOWN) +{ + double gndlvl = ground_level(sm); + auto wfn = [end_radius, gndlvl](const Ball &src, const Vec3d &dir, double len) { + if (len < EPSILON) + return src.R; + + Vec3d dst = src.p + len * dir; + double widening = end_radius - src.R; + double zlen = dst.z() - gndlvl; + double full_len = len + zlen; + double r = src.R + widening * len / full_len; + + return r; + }; + + static_assert(IsWideningFn, "Not a widening function"); + + return deepsearch_ground_connection(policy, sm, source, wfn, init_dir); +} + +struct DefaultWideningModel { + static constexpr double WIDENING_SCALE = 0.02; + const SupportableMesh &sm; + + double operator()(const Ball &src, const Vec3d & /*dir*/, double len) { + static_assert(IsWideningFn, + "DefaultWideningModel is not a widening function"); + + double w = WIDENING_SCALE * sm.cfg.pillar_widening_factor * len; + return std::max(src.R, sm.cfg.head_back_radius_mm) + w; + }; +}; + +template<> struct BeamSamples { + static constexpr size_t Value = 16; +}; + +template +GroundConnection deepsearch_ground_connection(Ex policy, + const SupportableMesh &sm, + const Junction &source, + const Vec3d &init_dir = DOWN) +{ + return deepsearch_ground_connection(policy, sm, source, + DefaultWideningModel{sm}, init_dir); } template @@ -729,8 +782,7 @@ bool optimize_anchor_placement(Ex policy, double lmax = std::min(sm.cfg.head_width_mm, distance(from.pos, anchor.pos) - 2 * from.r); - double sd = anchor.r_back_mm * sm.cfg.safety_distance_mm / - sm.cfg.head_back_radius_mm; + double sd = sm.cfg.safety_distance(anchor.r_back_mm); Optimizer solver(get_criteria(sm.cfg) .stop_score(anchor.fullwidth()) @@ -796,6 +848,92 @@ std::optional calculate_anchor_placement(Ex policy, return anchor; } +inline bool is_outside_support_cone(const Vec3f &supp, + const Vec3f &pt, + float angle) +{ + using namespace Slic3r; + + Vec3d D = (pt - supp).cast(); + double dot_sq = -D.z() * std::abs(-D.z()); + + return dot_sq < + D.squaredNorm() * std::cos(angle) * std::abs(std::cos(angle)); +} + +inline // TODO: should be in a cpp +std::optional find_merge_pt(const Vec3f &A, + const Vec3f &B, + float critical_angle) +{ + // The idea is that A and B both have their support cones. But searching + // for the intersection of these support cones is difficult and its enough + // to reduce this problem to 2D and search for the intersection of two + // rays that merge somewhere between A and B. The 2D plane is a vertical + // slice of the 3D scene where the 2D Y axis is equal to the 3D Z axis and + // the 2D X axis is determined by the XY direction of the AB vector. + // + // Z^ + // | A * + // | . . B * + // | . . . . + // | . . . . + // | . x . + // -------------------> XY + + // Determine the transformation matrix for the 2D projection: + Vec3f diff = {B.x() - A.x(), B.y() - A.y(), 0.f}; + Vec3f dir = diff.normalized(); // TODO: avoid normalization + + Eigen::Matrix tr2D; + tr2D.row(0) = Vec3f{dir.x(), dir.y(), dir.z()}; + tr2D.row(1) = Vec3f{0.f, 0.f, 1.f}; + + // Transform the 2 vectors A and B into 2D vector 'a' and 'b'. Here we can + // omit 'a', pretend that its the origin and use BA as the vector b. + Vec2f b = tr2D * (B - A); + + // Get the square sine of the ray emanating from 'a' towards 'b'. This ray might + // exceed the allowed angle but that is corrected subsequently. + // The sign of the original sine is also needed, hence b.y is multiplied by + // abs(b.y) + float b_sqn = b.squaredNorm(); + float sin2sig_a = b_sqn > EPSILON ? (b.y() * std::abs(b.y())) / b_sqn : 0.f; + + // sine2 from 'b' to 'a' is the opposite of sine2 from a to b + float sin2sig_b = -sin2sig_a; + + // Derive the allowed angles from the given critical angle. + // critical_angle is measured from the horizontal X axis. + // The rays need to go downwards which corresponds to negative angles + + float sincrit = std::sin(critical_angle); // sine of the critical angle + float sin2crit = -sincrit * sincrit; // signed sine squared + sin2sig_a = std::min(sin2sig_a, sin2crit); // Do the angle saturation of both rays + sin2sig_b = std::min(sin2sig_b, sin2crit); // + float sin2_a = std::abs(sin2sig_a); // Get cosine squared values + float sin2_b = std::abs(sin2sig_b); + float cos2_a = 1.f - sin2_a; + float cos2_b = 1.f - sin2_b; + + // Derive the new direction vectors. This is by square rooting the sin2 + // and cos2 values and restoring the original signs + Vec2f Da = {std::copysign(std::sqrt(cos2_a), b.x()), std::copysign(std::sqrt(sin2_a), sin2sig_a)}; + Vec2f Db = {-std::copysign(std::sqrt(cos2_b), b.x()), std::copysign(std::sqrt(sin2_b), sin2sig_b)}; + + // Determine where two rays ([0, 0], Da), (b, Db) intersect. + // Based on + // https://stackoverflow.com/questions/27459080/given-two-points-and-two-direction-vectors-find-the-point-where-they-intersect + // One ray is emanating from (0, 0) so the formula is simplified + double t1 = (Db.y() * b.x() - b.y() * Db.x()) / + (Da.x() * Db.y() - Da.y() * Db.x()); + + Vec2f mp = t1 * Da; + Vec3f Mp = A + tr2D.transpose() * mp; + + return t1 >= 0.f ? Mp : Vec3f{}; +} + }} // namespace Slic3r::sla #endif // SLASUPPORTTREEUTILS_H diff --git a/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp b/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp new file mode 100644 index 000000000..b504d82fb --- /dev/null +++ b/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp @@ -0,0 +1,300 @@ +#ifndef SUPPORTTREEUTILSLEGACY_HPP +#define SUPPORTTREEUTILSLEGACY_HPP + +#include "SupportTreeUtils.hpp" + +// Old functions are gathered here that are used in DefaultSupportTree +// to maintain functionality that was well tested. + +namespace Slic3r { namespace sla { + +// 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> +IntegerOnly 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; +} + +template +std::optional search_widening_path(Ex policy, + const SupportableMesh &sm, + const Vec3d &jp, + const Vec3d &dir, + double radius, + double new_radius) +{ + double w = radius + 2 * sm.cfg.head_back_radius_mm; + double stopval = w + jp.z() - ground_level(sm); + Optimizer solver(get_criteria(sm.cfg).stop_score(stopval)); + + auto [polar, azimuth] = dir_to_spheric(dir); + + double fallback_ratio = radius / sm.cfg.head_back_radius_mm; + + auto oresult = solver.to_max().optimize( + [&policy, &sm, jp, radius, new_radius](const opt::Input<3> &input) { + auto &[plr, azm, t] = input; + + auto d = spheric_to_dir(plr, azm).normalized(); + + auto sd = sm.cfg.safety_distance(new_radius); + + double ret = pinhead_mesh_hit(policy, sm.emesh, jp, d, radius, + new_radius, t, sd) + .distance(); + + Beam beam{jp + t * d, d, new_radius}; + double down = beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); + + if (ret > t && std::isinf(down)) + ret += jp.z() - ground_level(sm); + + return ret; + }, + initvals({polar, azimuth, w}), // start with what we have + bounds({ + {PI - sm.cfg.bridge_slope, PI}, // Must not exceed the slope limit + {-PI, PI}, // azimuth can be a full search + {radius + sm.cfg.head_back_radius_mm, + fallback_ratio * sm.cfg.max_bridge_length_mm} + })); + + if (oresult.score >= stopval) { + polar = std::get<0>(oresult.optimum); + azimuth = std::get<1>(oresult.optimum); + double t = std::get<2>(oresult.optimum); + Vec3d endp = jp + t * spheric_to_dir(polar, azimuth); + + return DiffBridge(jp, endp, radius, sm.cfg.head_back_radius_mm); + } + + return {}; +} + +// This is a proxy function for pillar creation which will mind the gap +// between the pad and the model bottom in zero elevation mode. +// 'pinhead_junctionpt' is the starting junction point which needs to be +// routed down. sourcedir is the allowed direction of an optional bridge +// between the jp junction and the final pillar. +template +std::pair create_ground_pillar( + Ex policy, + SupportTreeBuilder &builder, + const SupportableMesh &sm, + const Vec3d &pinhead_junctionpt, + const Vec3d &sourcedir, + double radius, + double end_radius, + long head_id = SupportTreeNode::ID_UNSET) +{ + Vec3d jp = pinhead_junctionpt, endp = jp, dir = sourcedir; + long pillar_id = SupportTreeNode::ID_UNSET; + bool can_add_base = false, non_head = false; + + double gndlvl = 0.; // The Z level where pedestals should be + double jp_gnd = 0.; // The lowest Z where a junction center can be + double gap_dist = 0.; // The gap distance between the model and the pad + + double r2 = radius + (end_radius - radius) / (jp.z() - ground_level(sm)); + + auto to_floor = [&gndlvl](const Vec3d &p) { return Vec3d{p.x(), p.y(), gndlvl}; }; + + auto eval_limits = [&sm, &radius, &can_add_base, &gndlvl, &gap_dist, &jp_gnd] + (bool base_en = true) + { + can_add_base = base_en && radius >= sm.cfg.head_back_radius_mm; + double base_r = can_add_base ? sm.cfg.base_radius_mm : 0.; + gndlvl = ground_level(sm); + if (!can_add_base) gndlvl -= sm.pad_cfg.wall_thickness_mm; + jp_gnd = gndlvl + (can_add_base ? 0. : sm.cfg.head_back_radius_mm); + gap_dist = sm.cfg.pillar_base_safety_distance_mm + base_r + EPSILON; + }; + + eval_limits(); + + // We are dealing with a mini pillar that's potentially too long + if (radius < sm.cfg.head_back_radius_mm && jp.z() - gndlvl > 20 * radius) + { + std::optional diffbr = + search_widening_path(policy, sm, jp, dir, radius, + sm.cfg.head_back_radius_mm); + + if (diffbr && diffbr->endp.z() > jp_gnd) { + auto &br = builder.add_diffbridge(*diffbr); + if (head_id >= 0) builder.head(head_id).bridge_id = br.id; + endp = diffbr->endp; + radius = diffbr->end_r; + builder.add_junction(endp, radius); + non_head = true; + dir = diffbr->get_dir(); + eval_limits(); + } else return {false, pillar_id}; + } + + if (sm.cfg.object_elevation_mm < EPSILON) + { + // get a suitable direction for the corrector bridge. It is the + // original sourcedir's azimuth but the polar angle is saturated to the + // configured bridge slope. + auto [polar, azimuth] = dir_to_spheric(dir); + polar = PI - sm.cfg.bridge_slope; + Vec3d d = spheric_to_dir(polar, azimuth).normalized(); + auto sd = radius * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; + double t = beam_mesh_hit(policy, sm.emesh, Beam{endp, d, radius, r2}, sd).distance(); + double tmax = std::min(sm.cfg.max_bridge_length_mm, t); + t = 0.; + + double zd = endp.z() - jp_gnd; + double tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); + tmax = std::min(tmax, tmax2); + + Vec3d nexp = endp; + double dlast = 0.; + while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || + !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius, r2}, sd).distance())) && + t < tmax) + { + t += radius; + nexp = endp + t * d; + } + + if (dlast < gap_dist && can_add_base) { + nexp = endp; + t = 0.; + can_add_base = false; + eval_limits(can_add_base); + + zd = endp.z() - jp_gnd; + tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); + tmax = std::min(tmax, tmax2); + + while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || + !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius}, sd).distance())) && t < tmax) { + t += radius; + nexp = endp + t * d; + } + } + + // Could not find a path to avoid the pad gap + if (dlast < gap_dist) return {false, pillar_id}; + + if (t > 0.) { // Need to make additional bridge + const Bridge& br = builder.add_bridge(endp, nexp, radius); + if (head_id >= 0) builder.head(head_id).bridge_id = br.id; + + builder.add_junction(nexp, radius); + endp = nexp; + non_head = true; + } + } + + Vec3d gp = to_floor(endp); + double h = endp.z() - gp.z(); + + pillar_id = head_id >= 0 && !non_head ? builder.add_pillar(head_id, h) : + builder.add_pillar(gp, h, radius, end_radius); + + if (can_add_base) + builder.add_pillar_base(pillar_id, sm.cfg.base_height_mm, + sm.cfg.base_radius_mm); + + return {true, pillar_id}; +} + +template +std::pair connect_to_ground(Ex policy, + SupportTreeBuilder &builder, + const SupportableMesh &sm, + const Junction &j, + const Vec3d &dir, + double end_r) +{ + auto hjp = j.pos; + double r = j.r; + auto sd = r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; + double r2 = j.r + (end_r - j.r) / (j.pos.z() - ground_level(sm)); + + double t = beam_mesh_hit(policy, sm.emesh, Beam{hjp, dir, r, r2}, sd).distance(); + double d = 0, tdown = 0; + t = std::min(t, sm.cfg.max_bridge_length_mm * r / sm.cfg.head_back_radius_mm); + + while (d < t && + !std::isinf(tdown = beam_mesh_hit(policy, sm.emesh, + Beam{hjp + d * dir, DOWN, r, r2}, sd) + .distance())) { + d += r; + } + + if(!std::isinf(tdown)) + return {false, SupportTreeNode::ID_UNSET}; + + Vec3d endp = hjp + d * dir; + auto ret = create_ground_pillar(policy, builder, sm, endp, dir, r, end_r); + + if (ret.second >= 0) { + builder.add_bridge(hjp, endp, r); + builder.add_junction(endp, r); + } + + return ret; +} + +template +std::pair search_ground_route(Ex policy, + SupportTreeBuilder &builder, + const SupportableMesh &sm, + const Junction &j, + double end_radius, + const Vec3d &init_dir = DOWN) +{ + double downdst = j.pos.z() - ground_level(sm); + + auto res = connect_to_ground(policy, builder, sm, j, init_dir, end_radius); + if (res.first) + return res; + + // Optimize bridge direction: + // Straight path failed so we will try to search for a suitable + // direction out of the cavity. + auto [polar, azimuth] = dir_to_spheric(init_dir); + + Optimizer solver(get_criteria(sm.cfg).stop_score(1e6)); + solver.seed(0); // we want deterministic behavior + + auto sd = j.r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; + auto oresult = solver.to_max().optimize( + [&j, sd, &policy, &sm, &downdst, &end_radius](const opt::Input<2> &input) { + auto &[plr, azm] = input; + Vec3d n = spheric_to_dir(plr, azm).normalized(); + Beam beam{Ball{j.pos, j.r}, Ball{j.pos + downdst * n, end_radius}}; + return beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); + }, + initvals({polar, azimuth}), // let's start with what we have + bounds({ {PI - sm.cfg.bridge_slope, PI}, {-PI, PI} }) + ); + + Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized(); + + return connect_to_ground(policy, builder, sm, j, bridgedir, end_radius); +} + +}} // namespace Slic3r::sla + +#endif // SUPPORTTREEUTILSLEGACY_HPP diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index d9b8e33df..d991dfe05 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -44,28 +44,63 @@ sla::SupportTreeConfig make_support_cfg(const SLAPrintObjectConfig& c) scfg.enabled = c.supports_enable.getBool(); scfg.tree_type = c.support_tree_type.value; - scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); - double pillar_r = 0.5 * c.support_pillar_diameter.getFloat(); - scfg.head_back_radius_mm = pillar_r; - scfg.head_fallback_radius_mm = - 0.01 * c.support_small_pillar_diameter_percent.getFloat() * pillar_r; - scfg.head_penetration_mm = c.support_head_penetration.getFloat(); - scfg.head_width_mm = c.support_head_width.getFloat(); - scfg.object_elevation_mm = is_zero_elevation(c) ? - 0. : c.support_object_elevation.getFloat(); - scfg.bridge_slope = c.support_critical_angle.getFloat() * PI / 180.0 ; - scfg.max_bridge_length_mm = c.support_max_bridge_length.getFloat(); - scfg.max_pillar_link_distance_mm = c.support_max_pillar_link_distance.getFloat(); - scfg.pillar_connection_mode = c.support_pillar_connection_mode.value; - scfg.ground_facing_only = c.support_buildplate_only.getBool(); - scfg.pillar_widening_factor = c.support_pillar_widening_factor.getFloat(); - scfg.base_radius_mm = 0.5*c.support_base_diameter.getFloat(); - scfg.base_height_mm = c.support_base_height.getFloat(); - scfg.pillar_base_safety_distance_mm = - c.support_base_safety_distance.getFloat() < EPSILON ? - scfg.safety_distance_mm : c.support_base_safety_distance.getFloat(); - - scfg.max_bridges_on_pillar = unsigned(c.support_max_bridges_on_pillar.getInt()); + + switch(scfg.tree_type) { + case sla::SupportTreeType::Default: { + scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); + double pillar_r = 0.5 * c.support_pillar_diameter.getFloat(); + scfg.head_back_radius_mm = pillar_r; + scfg.head_fallback_radius_mm = + 0.01 * c.support_small_pillar_diameter_percent.getFloat() * pillar_r; + scfg.head_penetration_mm = c.support_head_penetration.getFloat(); + scfg.head_width_mm = c.support_head_width.getFloat(); + scfg.object_elevation_mm = is_zero_elevation(c) ? + 0. : c.support_object_elevation.getFloat(); + scfg.bridge_slope = c.support_critical_angle.getFloat() * PI / 180.0 ; + scfg.max_bridge_length_mm = c.support_max_bridge_length.getFloat(); + scfg.max_pillar_link_distance_mm = c.support_max_pillar_link_distance.getFloat(); + scfg.pillar_connection_mode = c.support_pillar_connection_mode.value; + scfg.ground_facing_only = c.support_buildplate_only.getBool(); + scfg.pillar_widening_factor = c.support_pillar_widening_factor.getFloat(); + scfg.base_radius_mm = 0.5*c.support_base_diameter.getFloat(); + scfg.base_height_mm = c.support_base_height.getFloat(); + scfg.pillar_base_safety_distance_mm = + c.support_base_safety_distance.getFloat() < EPSILON ? + scfg.safety_distance_mm : c.support_base_safety_distance.getFloat(); + + scfg.max_bridges_on_pillar = unsigned(c.support_max_bridges_on_pillar.getInt()); + scfg.max_weight_on_model_support = c.support_max_weight_on_model.getFloat(); + break; + } + case sla::SupportTreeType::Branching: + [[fallthrough]]; + case sla::SupportTreeType::Organic:{ + scfg.head_front_radius_mm = 0.5*c.branchingsupport_head_front_diameter.getFloat(); + double pillar_r = 0.5 * c.branchingsupport_pillar_diameter.getFloat(); + scfg.head_back_radius_mm = pillar_r; + scfg.head_fallback_radius_mm = + 0.01 * c.branchingsupport_small_pillar_diameter_percent.getFloat() * pillar_r; + scfg.head_penetration_mm = c.branchingsupport_head_penetration.getFloat(); + scfg.head_width_mm = c.branchingsupport_head_width.getFloat(); + scfg.object_elevation_mm = is_zero_elevation(c) ? + 0. : c.branchingsupport_object_elevation.getFloat(); + scfg.bridge_slope = c.branchingsupport_critical_angle.getFloat() * PI / 180.0 ; + scfg.max_bridge_length_mm = c.branchingsupport_max_bridge_length.getFloat(); + scfg.max_pillar_link_distance_mm = c.branchingsupport_max_pillar_link_distance.getFloat(); + scfg.pillar_connection_mode = c.branchingsupport_pillar_connection_mode.value; + scfg.ground_facing_only = c.branchingsupport_buildplate_only.getBool(); + scfg.pillar_widening_factor = c.branchingsupport_pillar_widening_factor.getFloat(); + scfg.base_radius_mm = 0.5*c.branchingsupport_base_diameter.getFloat(); + scfg.base_height_mm = c.branchingsupport_base_height.getFloat(); + scfg.pillar_base_safety_distance_mm = + c.branchingsupport_base_safety_distance.getFloat() < EPSILON ? + scfg.safety_distance_mm : c.branchingsupport_base_safety_distance.getFloat(); + + scfg.max_bridges_on_pillar = unsigned(c.branchingsupport_max_bridges_on_pillar.getInt()); + scfg.max_weight_on_model_support = c.branchingsupport_max_weight_on_model.getFloat(); + break; + } + } return scfg; } @@ -827,6 +862,7 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector fast_round_up(double a) return a == 0.49999999999999994 ? I(0) : I(floor(a + 0.5)); } +template using SamePair = std::pair; + } // namespace Slic3r #endif // _libslic3r_h_ diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 7f8e70e6c..85fe3e53e 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -374,22 +374,42 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) bool is_default_tree = treetype == sla::SupportTreeType::Default; bool is_branching_tree = treetype == sla::SupportTreeType::Branching; - toggle_field("support_head_front_diameter", supports_en); - toggle_field("support_head_penetration", supports_en); - toggle_field("support_head_width", supports_en); - toggle_field("support_pillar_diameter", supports_en); - toggle_field("support_small_pillar_diameter_percent", supports_en); + toggle_field("support_tree_type", supports_en); + + toggle_field("support_head_front_diameter", supports_en && is_default_tree); + toggle_field("support_head_penetration", supports_en && is_default_tree); + toggle_field("support_head_width", supports_en && is_default_tree); + toggle_field("support_pillar_diameter", supports_en && is_default_tree); + toggle_field("support_small_pillar_diameter_percent", supports_en && is_default_tree); toggle_field("support_max_bridges_on_pillar", supports_en && is_default_tree); toggle_field("support_pillar_connection_mode", supports_en && is_default_tree); - toggle_field("support_tree_type", supports_en); - toggle_field("support_buildplate_only", supports_en); - toggle_field("support_base_diameter", supports_en); - toggle_field("support_base_height", supports_en); - toggle_field("support_base_safety_distance", supports_en); - toggle_field("support_critical_angle", supports_en); - toggle_field("support_max_bridge_length", supports_en); + toggle_field("support_buildplate_only", supports_en && is_default_tree); + toggle_field("support_base_diameter", supports_en && is_default_tree); + toggle_field("support_base_height", supports_en && is_default_tree); + toggle_field("support_base_safety_distance", supports_en && is_default_tree); + toggle_field("support_critical_angle", supports_en && is_default_tree); + toggle_field("support_max_bridge_length", supports_en && is_default_tree); toggle_field("support_max_pillar_link_distance", supports_en && is_default_tree); - toggle_field("support_pillar_widening_factor", supports_en && is_branching_tree); + toggle_field("support_pillar_widening_factor", false); + toggle_field("support_max_weight_on_model", false); + + toggle_field("branchingsupport_head_front_diameter", supports_en && is_branching_tree); + toggle_field("branchingsupport_head_penetration", supports_en && is_branching_tree); + toggle_field("branchingsupport_head_width", supports_en && is_branching_tree); + toggle_field("branchingsupport_pillar_diameter", supports_en && is_branching_tree); + toggle_field("branchingsupport_small_pillar_diameter_percent", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_bridges_on_pillar", false); + toggle_field("branchingsupport_pillar_connection_mode", false); + toggle_field("branchingsupport_buildplate_only", supports_en && is_branching_tree); + toggle_field("branchingsupport_base_diameter", supports_en && is_branching_tree); + toggle_field("branchingsupport_base_height", supports_en && is_branching_tree); + toggle_field("branchingsupport_base_safety_distance", supports_en && is_branching_tree); + toggle_field("branchingsupport_critical_angle", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_bridge_length", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_pillar_link_distance", false); + toggle_field("branchingsupport_pillar_widening_factor", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_weight_on_model", supports_en && is_branching_tree); + toggle_field("support_points_density_relative", supports_en); toggle_field("support_points_minimal_distance", supports_en); @@ -406,7 +426,8 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) bool zero_elev = config->opt_bool("pad_around_object") && pad_en; - toggle_field("support_object_elevation", supports_en && !zero_elev); + toggle_field("support_object_elevation", supports_en && is_default_tree && !zero_elev); + toggle_field("branchingsupport_object_elevation", supports_en && is_branching_tree && !zero_elev); toggle_field("pad_object_gap", zero_elev); toggle_field("pad_around_object_everywhere", zero_elev); toggle_field("pad_object_connector_stride", zero_elev); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 1ab23b344..0da9e5b83 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -553,10 +553,13 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) : const bool supports_enable = selection == _("None") ? false : true; new_conf.set_key_value("supports_enable", new ConfigOptionBool(supports_enable)); + std::string treetype = get_sla_suptree_prefix(new_conf); + if (selection == _("Everywhere")) - new_conf.set_key_value("support_buildplate_only", new ConfigOptionBool(false)); + new_conf.set_key_value(treetype + "support_buildplate_only", new ConfigOptionBool(false)); else if (selection == _("Support on build plate only")) - new_conf.set_key_value("support_buildplate_only", new ConfigOptionBool(true)); + new_conf.set_key_value(treetype + "support_buildplate_only", new ConfigOptionBool(true)); + } tab->load_config(new_conf); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 0b0cfc8ae..ad7b92e2e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1038,8 +1038,11 @@ void Tab::load_key_value(const std::string& opt_key, const boost::any& value, bo static wxString support_combo_value_for_config(const DynamicPrintConfig &config, bool is_fff) { + std::string slatree = is_fff ? "" : get_sla_suptree_prefix(config); + const std::string support = is_fff ? "support_material" : "supports_enable"; - const std::string buildplate_only = is_fff ? "support_material_buildplate_only" : "support_buildplate_only"; + const std::string buildplate_only = is_fff ? "support_material_buildplate_only" : slatree + "support_buildplate_only"; + return ! config.opt_bool(support) ? _("None") : @@ -1082,7 +1085,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) if (is_fff ? (opt_key == "support_material" || opt_key == "support_material_auto" || opt_key == "support_material_buildplate_only") : - (opt_key == "supports_enable" || opt_key == "support_buildplate_only")) + (opt_key == "supports_enable" || opt_key == "support_tree_type" || opt_key == get_sla_suptree_prefix(*m_config) + "support_buildplate_only")) og_freq_chng_params->set_value("support", support_combo_value_for_config(*m_config, is_fff)); if (! is_fff && (opt_key == "pad_enable" || opt_key == "pad_around_object")) @@ -4821,6 +4824,60 @@ void TabSLAMaterial::update() wxGetApp().mainframe->on_config_changed(m_config); } +static void add_options_into_line(ConfigOptionsGroupShp &optgroup, + const std::vector> &prefixes, + const std::string &optkey) +{ + auto opt = optgroup->get_option(prefixes.front().first + optkey); + Line line{ opt.opt.label, "" }; + line.full_width = 1; + for (auto &prefix : prefixes) { + opt = optgroup->get_option(prefix.first + optkey); + opt.opt.label = prefix.second; + opt.opt.width = 12; // TODO + line.append_option(opt); + } + optgroup->append_line(line); +} + +void TabSLAPrint::build_sla_support_params(const std::vector> &prefixes, + const Slic3r::GUI::PageShp &page) +{ + + auto optgroup = page->new_optgroup(L("Support head")); + add_options_into_line(optgroup, prefixes, "support_head_front_diameter"); + add_options_into_line(optgroup, prefixes, "support_head_penetration"); + add_options_into_line(optgroup, prefixes, "support_head_width"); + + optgroup = page->new_optgroup(L("Support pillar")); + add_options_into_line(optgroup, prefixes, "support_pillar_diameter"); + add_options_into_line(optgroup, prefixes, "support_small_pillar_diameter_percent"); + add_options_into_line(optgroup, prefixes, "support_max_bridges_on_pillar"); + + add_options_into_line(optgroup, prefixes, "support_pillar_connection_mode"); + add_options_into_line(optgroup, prefixes, "support_buildplate_only"); + add_options_into_line(optgroup, prefixes, "support_pillar_widening_factor"); + add_options_into_line(optgroup, prefixes, "support_max_weight_on_model"); + add_options_into_line(optgroup, prefixes, "support_base_diameter"); + add_options_into_line(optgroup, prefixes, "support_base_height"); + add_options_into_line(optgroup, prefixes, "support_base_safety_distance"); + + // Mirrored parameter from Pad page for toggling elevation on the same page + add_options_into_line(optgroup, prefixes, "support_object_elevation"); + + Line line{ "", "" }; + line.full_width = 1; + line.widget = [this](wxWindow* parent) { + return description_line_widget(parent, &m_support_object_elevation_description_line); + }; + optgroup->append_line(line); + + optgroup = page->new_optgroup(L("Connection of the support sticks and junctions")); + add_options_into_line(optgroup, prefixes, "support_critical_angle"); + add_options_into_line(optgroup, prefixes, "support_max_bridge_length"); + add_options_into_line(optgroup, prefixes, "support_max_pillar_link_distance"); +} + void TabSLAPrint::build() { m_presets = &m_preset_bundle->sla_prints; @@ -4833,41 +4890,12 @@ void TabSLAPrint::build() optgroup->append_single_option_line("faded_layers"); page = add_options_page(L("Supports"), "support"/*"sla_supports"*/); + optgroup = page->new_optgroup(L("Supports")); optgroup->append_single_option_line("supports_enable"); optgroup->append_single_option_line("support_tree_type"); - optgroup = page->new_optgroup(L("Support head")); - optgroup->append_single_option_line("support_head_front_diameter"); - optgroup->append_single_option_line("support_head_penetration"); - optgroup->append_single_option_line("support_head_width"); - - optgroup = page->new_optgroup(L("Support pillar")); - optgroup->append_single_option_line("support_pillar_diameter"); - optgroup->append_single_option_line("support_small_pillar_diameter_percent"); - optgroup->append_single_option_line("support_max_bridges_on_pillar"); - - optgroup->append_single_option_line("support_pillar_connection_mode"); - optgroup->append_single_option_line("support_buildplate_only"); - optgroup->append_single_option_line("support_pillar_widening_factor"); - optgroup->append_single_option_line("support_base_diameter"); - optgroup->append_single_option_line("support_base_height"); - optgroup->append_single_option_line("support_base_safety_distance"); - - // Mirrored parameter from Pad page for toggling elevation on the same page - optgroup->append_single_option_line("support_object_elevation"); - - Line line{ "", "" }; - line.full_width = 1; - line.widget = [this](wxWindow* parent) { - return description_line_widget(parent, &m_support_object_elevation_description_line); - }; - optgroup->append_line(line); - - optgroup = page->new_optgroup(L("Connection of the support sticks and junctions")); - optgroup->append_single_option_line("support_critical_angle"); - optgroup->append_single_option_line("support_max_bridge_length"); - optgroup->append_single_option_line("support_max_pillar_link_distance"); + build_sla_support_params({{"", "Default"}, {"branching", "Branching"}}, page); optgroup = page->new_optgroup(L("Automatic generation")); optgroup->append_single_option_line("support_points_density_relative"); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index f5dd4c522..c060eb7fd 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -339,7 +339,7 @@ public: void on_roll_back_value(const bool to_sys = false); - PageShp add_options_page(const wxString& title, const std::string& icon, bool is_extruder_pages = false); + PageShp add_options_page(const wxString& title, const std::string& icon, bool is_extruder_pages = false); static wxString translate_category(const wxString& title, Preset::Type preset_type); virtual void OnActivate(); @@ -526,6 +526,12 @@ public: class TabSLAPrint : public Tab { + // Methods are a vector of method prefix -> method label pairs + // method prefix is the prefix whith which all the config values are prefixed + // for a particular method. The label is the friendly name for the method + void build_sla_support_params(const std::vector> &methods, + const Slic3r::GUI::PageShp &page); + public: TabSLAPrint(wxBookCtrlBase* parent) : Tab(parent, _(L("Print Settings")), Slic3r::Preset::TYPE_SLA_PRINT) {} diff --git a/tests/data/U_overhang.obj b/tests/data/U_overhang.obj new file mode 100644 index 000000000..70453e58f --- /dev/null +++ b/tests/data/U_overhang.obj @@ -0,0 +1,76 @@ +#### +# +# OBJ File Generated by Meshlab +# +#### +# Object U_overhang.obj +# +# Vertices: 16 +# Faces: 28 +# +#### +vn 1.570797 1.570796 1.570796 +v 10.000000 10.000000 11.000000 +vn 4.712389 1.570796 -1.570796 +v 10.000000 1.000000 10.000000 +vn 1.570796 1.570796 -1.570796 +v 10.000000 10.000000 10.000000 +vn 1.570796 -1.570796 1.570796 +v 10.000000 0.000000 11.000000 +vn 4.712389 1.570796 1.570796 +v 10.000000 1.000000 1.000000 +vn 1.570797 1.570796 -1.570796 +v 10.000000 10.000000 0.000000 +vn 1.570796 1.570796 1.570796 +v 10.000000 10.000000 1.000000 +vn 1.570796 -1.570796 -1.570796 +v 10.000000 0.000000 0.000000 +vn -1.570796 1.570796 1.570796 +v 0.000000 10.000000 1.000000 +vn -4.712389 1.570796 1.570796 +v 0.000000 1.000000 1.000000 +vn -1.570796 -1.570796 -1.570796 +v 0.000000 0.000000 0.000000 +vn -1.570797 1.570796 -1.570796 +v 0.000000 10.000000 0.000000 +vn -4.712389 1.570796 -1.570796 +v 0.000000 1.000000 10.000000 +vn -1.570797 1.570796 1.570796 +v 0.000000 10.000000 11.000000 +vn -1.570796 1.570796 -1.570796 +v 0.000000 10.000000 10.000000 +vn -1.570796 -1.570796 1.570796 +v 0.000000 0.000000 11.000000 +# 16 vertices, 0 vertices normals + +f 1//1 2//2 3//3 +f 2//2 4//4 5//5 +f 4//4 2//2 1//1 +f 5//5 6//6 7//7 +f 5//5 8//8 6//6 +f 8//8 5//5 4//4 +f 9//9 5//5 7//7 +f 5//5 9//9 10//10 +f 11//11 6//6 8//8 +f 6//6 11//11 12//12 +f 12//12 10//10 9//9 +f 10//10 11//11 13//13 +f 11//11 10//10 12//12 +f 13//13 14//14 15//15 +f 13//13 16//16 14//14 +f 16//16 13//13 11//11 +f 6//6 9//9 7//7 +f 9//9 6//6 12//12 +f 11//11 4//4 16//16 +f 4//4 11//11 8//8 +f 13//13 3//3 2//2 +f 3//3 13//13 15//15 +f 5//5 13//13 2//2 +f 13//13 5//5 10//10 +f 14//14 4//4 1//1 +f 4//4 14//14 16//16 +f 3//3 14//14 1//1 +f 14//14 3//3 15//15 +# 28 faces, 0 coords texture + +# End of File diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index 24e9552c5..2a800cc50 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp sla_test_utils.hpp sla_test_utils.cpp sla_supptgen_tests.cpp sla_raycast_tests.cpp + sla_supptreeutils_tests.cpp sla_archive_readwrite_tests.cpp) # mold linker for successful linking needs also to link TBB library and link it before libslic3r. diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index 8ea91d57a..a733e77cc 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -32,13 +31,6 @@ const char *const SUPPORT_TEST_MODELS[] = { } // namespace -TEST_CASE("Pillar pairhash should be unique", "[SLASupportGeneration]") { - test_pairhash(); - test_pairhash(); - test_pairhash(); - test_pairhash(); -} - TEST_CASE("Support point generator should be deterministic if seeded", "[SLASupportGeneration], [SLAPointGen]") { TriangleMesh mesh = load_model("A_upsidedown.obj"); @@ -173,106 +165,6 @@ TEST_CASE("DefaultSupports::FloorSupportsDoNotPierceModel", "[SLASupportGenerati // for (auto &fname: SUPPORT_TEST_MODELS) test_supports(fname, supportcfg); //} -bool is_outside_support_cone(const Vec3f &supp, const Vec3f &pt, float angle) -{ - Vec3d D = (pt - supp).cast(); - double dot_sq = -D.z() * std::abs(-D.z()); - - return dot_sq < - D.squaredNorm() * std::cos(angle) * std::abs(std::cos(angle)); -} - -TEST_CASE("BranchingSupports::MergePointFinder", "[SLASupportGeneration][Branching]") { - SECTION("Identical points have the same merge point") { - Vec3f a{0.f, 0.f, 0.f}, b = a; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - REQUIRE((*mergept - b).norm() < EPSILON); - REQUIRE((*mergept - a).norm() < EPSILON); - } - - // ^ Z - // | a * - // | - // | b * <= mergept - SECTION("Points at different heights have the lower point as mergepoint") { - Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 0.f, -1.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - REQUIRE((*mergept - b).squaredNorm() < 2 * EPSILON); - } - - // -|---------> X - // a b - // * * - // * <= mergept - SECTION("Points at different X have mergept in the middle at lower Z") { - Vec3f a{0.f, 0.f, 0.f}, b = {1.f, 0.f, 0.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - - // Distance of mergept should be equal from both input points - float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); - - REQUIRE(D < EPSILON); - REQUIRE(!is_outside_support_cone(a, *mergept, slope)); - REQUIRE(!is_outside_support_cone(b, *mergept, slope)); - } - - // -|---------> Y - // a b - // * * - // * <= mergept - SECTION("Points at different Y have mergept in the middle at lower Z") { - Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 1.f, 0.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - - // Distance of mergept should be equal from both input points - float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); - - REQUIRE(D < EPSILON); - REQUIRE(!is_outside_support_cone(a, *mergept, slope)); - REQUIRE(!is_outside_support_cone(b, *mergept, slope)); - } - - SECTION("Points separated by less than critical angle have the lower point as mergept") { - Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -2.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - REQUIRE((*mergept - b).norm() < 2 * EPSILON); - } - - // -|----------------------------> Y - // a b - // * * <= mergept * - // - SECTION("Points at same height have mergepoint in the middle if critical angle is zero ") { - Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -1.f}; - auto slope = EPSILON; - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - Vec3f middle = (b + a) / 2.; - REQUIRE((*mergept - middle).norm() < 4 * EPSILON); - } -} TEST_CASE("BranchingSupports::ElevatedSupportsDoNotPierceModel", "[SLASupportGeneration][Branching]") { diff --git a/tests/sla_print/sla_supptreeutils_tests.cpp b/tests/sla_print/sla_supptreeutils_tests.cpp new file mode 100644 index 000000000..c8abbb831 --- /dev/null +++ b/tests/sla_print/sla_supptreeutils_tests.cpp @@ -0,0 +1,377 @@ +#include +#include + +#include + +#include "libslic3r/Execution/ExecutionSeq.hpp" +#include "libslic3r/SLA/SupportTreeUtils.hpp" +#include "libslic3r/SLA/SupportTreeUtilsLegacy.hpp" + +// Test pair hash for 'nums' random number pairs. +template void test_pairhash() +{ + const constexpr size_t nums = 1000; + I A[nums] = {0}, B[nums] = {0}; + std::unordered_set CH; + std::unordered_map> ints; + + std::random_device rd; + std::mt19937 gen(rd()); + + const I Ibits = int(sizeof(I) * CHAR_BIT); + const II IIbits = int(sizeof(II) * CHAR_BIT); + + int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; + if (std::is_signed::value) bits -= 1; + const I Imin = 0; + const I Imax = I(std::pow(2., bits) - 1); + + std::uniform_int_distribution dis(Imin, Imax); + + for (size_t i = 0; i < nums;) { + I a = dis(gen); + if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; } + } + + for (size_t i = 0; i < nums;) { + I b = dis(gen); + if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; } + } + + for (size_t i = 0; i < nums; ++i) { + I a = A[i], b = B[i]; + + REQUIRE(a != b); + + II hash_ab = Slic3r::sla::pairhash(a, b); + II hash_ba = Slic3r::sla::pairhash(b, a); + REQUIRE(hash_ab == hash_ba); + + auto it = ints.find(hash_ab); + + if (it != ints.end()) { + REQUIRE(( + (it->second.first == a && it->second.second == b) || + (it->second.first == b && it->second.second == a) + )); + } else + ints[hash_ab] = std::make_pair(a, b); + } +} + +TEST_CASE("Pillar pairhash should be unique", "[suptreeutils]") { + test_pairhash(); + test_pairhash(); + test_pairhash(); + test_pairhash(); +} + +static void eval_ground_conn(const Slic3r::sla::GroundConnection &conn, + const Slic3r::sla::SupportableMesh &sm, + const Slic3r::sla::Junction &j, + double end_r, + const std::string &stl_fname = "output.stl") +{ + using namespace Slic3r; + +//#ifndef NDEBUG + + sla::SupportTreeBuilder builder; + + if (!conn) + builder.add_junction(j); + + sla::build_ground_connection(builder, sm, conn); + + indexed_triangle_set mesh = *sm.emesh.get_triangle_mesh(); + its_merge(mesh, builder.merged_mesh()); + + its_write_stl_ascii(stl_fname.c_str(), "stl_fname", mesh); +//#endif + + REQUIRE(bool(conn)); + + // The route should include the source and one avoidance junction. + REQUIRE(conn.path.size() == 2); + + // Check if the radius increases with each node + REQUIRE(conn.path.front().r < conn.path.back().r); + REQUIRE(conn.path.back().r < conn.pillar_base->r_top); + + // The end radius and the pillar base's upper radius should match + REQUIRE(conn.pillar_base->r_top == Approx(end_r)); +} + +TEST_CASE("Pillar search dumb case", "[suptreeutils]") { + using namespace Slic3r; + + constexpr double FromR = 0.5; + auto j = sla::Junction{Vec3d::Zero(), FromR}; + + SECTION("with empty mesh") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + constexpr double EndR = 1.; + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, sla::DOWN); + + REQUIRE(conn); +// REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + } + + SECTION("with zero R source and destination") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + j.r = 0.; + constexpr double EndR = 0.; + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, sla::DOWN); + + REQUIRE(conn); +// REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + REQUIRE(conn.pillar_base->r_top == Approx(0.)); + } + + SECTION("with zero init direction") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + constexpr double EndR = 1.; + Vec3d init_dir = Vec3d::Zero(); + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, init_dir); + + REQUIRE(conn); +// REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + } +} + +TEST_CASE("Avoid disk below junction", "[suptreeutils]") +{ + // In this test there will be a disk mesh with some radius, centered at + // (0, 0, 0) and above the disk, a junction from which the support pillar + // should be routed. The algorithm needs to find an avoidance route. + + using namespace Slic3r; + + constexpr double FromRadius = .5; + constexpr double EndRadius = 1.; + constexpr double CylRadius = 4.; + constexpr double CylHeight = 1.; + + sla::SupportTreeConfig cfg; + + indexed_triangle_set disk = its_make_cylinder(CylRadius, CylHeight); + + // 2.5 * CyRadius height should be enough to be able to insert a bridge + // with 45 degree tilt above the disk. + sla::Junction j{Vec3d{0., 0., 2.5 * CylRadius}, FromRadius}; + + sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg}; + + SECTION("with elevation") { + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_tbb, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } + + SECTION("without elevation") { + sm.cfg.object_elevation_mm = 0.; + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_tbb, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk_ze.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } +} + +TEST_CASE("Avoid disk below junction with barrier on the side", "[suptreeutils]") +{ + // In this test there will be a disk mesh with some radius, centered at + // (0, 0, 0) and above the disk, a junction from which the support pillar + // should be routed. The algorithm needs to find an avoidance route. + + using namespace Slic3r; + + constexpr double FromRadius = .5; + constexpr double EndRadius = 1.; + constexpr double CylRadius = 4.; + constexpr double CylHeight = 1.; + constexpr double JElevX = 2.5; + + sla::SupportTreeConfig cfg; + + indexed_triangle_set disk = its_make_cylinder(CylRadius, CylHeight); + indexed_triangle_set wall = its_make_cube(1., 2 * CylRadius, JElevX * CylRadius); + its_translate(wall, Vec3f{float(FromRadius), -float(CylRadius), 0.f}); + its_merge(disk, wall); + + // 2.5 * CyRadius height should be enough to be able to insert a bridge + // with 45 degree tilt above the disk. + sla::Junction j{Vec3d{0., 0., JElevX * CylRadius}, FromRadius}; + + sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg}; + + SECTION("with elevation") { + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk_with_barrier.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } + + SECTION("without elevation") { + sm.cfg.object_elevation_mm = 0.; + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk_with_barrier_ze.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } +} + +TEST_CASE("Find ground route just above ground", "[suptreeutils]") { + using namespace Slic3r; + + sla::SupportTreeConfig cfg; + cfg.object_elevation_mm = 0.; + + sla::Junction j{Vec3d{0., 0., 2. * cfg.head_back_radius_mm}, cfg.head_back_radius_mm}; + + sla::SupportableMesh sm{{}, sla::SupportPoints{}, cfg}; + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, Geometry::spheric_to_dir(3 * PI/ 4, PI)); + + REQUIRE(conn); + + REQUIRE(conn.pillar_base->pos.z() >= Approx(ground_level(sm))); +} + +TEST_CASE("BranchingSupports::MergePointFinder", "[suptreeutils]") { + using namespace Slic3r; + + SECTION("Identical points have the same merge point") { + Vec3f a{0.f, 0.f, 0.f}, b = a; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + REQUIRE((*mergept - b).norm() < EPSILON); + REQUIRE((*mergept - a).norm() < EPSILON); + } + + // ^ Z + // | a * + // | + // | b * <= mergept + SECTION("Points at different heights have the lower point as mergepoint") { + Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 0.f, -1.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + REQUIRE((*mergept - b).squaredNorm() < 2 * EPSILON); + } + + // -|---------> X + // a b + // * * + // * <= mergept + SECTION("Points at different X have mergept in the middle at lower Z") { + Vec3f a{0.f, 0.f, 0.f}, b = {1.f, 0.f, 0.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + + // Distance of mergept should be equal from both input points + float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); + + REQUIRE(D < EPSILON); + REQUIRE(!sla::is_outside_support_cone(a, *mergept, slope)); + REQUIRE(!sla::is_outside_support_cone(b, *mergept, slope)); + } + + // -|---------> Y + // a b + // * * + // * <= mergept + SECTION("Points at different Y have mergept in the middle at lower Z") { + Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 1.f, 0.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + + // Distance of mergept should be equal from both input points + float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); + + REQUIRE(D < EPSILON); + REQUIRE(!sla::is_outside_support_cone(a, *mergept, slope)); + REQUIRE(!sla::is_outside_support_cone(b, *mergept, slope)); + } + + SECTION("Points separated by less than critical angle have the lower point as mergept") { + Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -2.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + REQUIRE((*mergept - b).norm() < 2 * EPSILON); + } + + // -|----------------------------> Y + // a b + // * * <= mergept * + // + SECTION("Points at same height have mergepoint in the middle if critical angle is zero ") { + Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -1.f}; + auto slope = EPSILON; + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + Vec3f middle = (b + a) / 2.; + REQUIRE((*mergept - middle).norm() < 4 * EPSILON); + } +} + diff --git a/tests/sla_print/sla_test_utils.cpp b/tests/sla_print/sla_test_utils.cpp index 69feea31f..b601cef11 100644 --- a/tests/sla_print/sla_test_utils.cpp +++ b/tests/sla_print/sla_test_utils.cpp @@ -118,7 +118,7 @@ void test_supports(const std::string &obj_filename, // Create the special index-triangle mesh with spatial indexing which // is the input of the support point and support mesh generators - AABBMesh emesh{mesh}; + sla::SupportableMesh sm{mesh.its, {}, supportcfg}; #ifdef SLIC3R_HOLE_RAYCASTER if (hollowingcfg.enabled) @@ -130,23 +130,23 @@ void test_supports(const std::string &obj_filename, // Create the support point generator sla::SupportPointGenerator::Config autogencfg; autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); - sla::SupportPointGenerator point_gen{emesh, autogencfg, [] {}, [](int) {}}; + sla::SupportPointGenerator point_gen{sm.emesh, autogencfg, [] {}, [](int) {}}; point_gen.seed(0); // Make the test repeatable point_gen.execute(out.model_slices, out.slicegrid); // Get the calculated support points. - std::vector support_points = point_gen.output(); + sm.pts = point_gen.output(); int validityflags = ASSUME_NO_REPAIR; // If there is no elevation, support points shall be removed from the // bottom of the object. if (std::abs(supportcfg.object_elevation_mm) < EPSILON) { - sla::remove_bottom_points(support_points, zmin + supportcfg.base_height_mm); + sla::remove_bottom_points(sm.pts, zmin + supportcfg.base_height_mm); } else { // Should be support points at least on the bottom of the model - REQUIRE_FALSE(support_points.empty()); + REQUIRE_FALSE(sm.pts.empty()); // Also the support mesh should not be empty. validityflags |= ASSUME_NO_EMPTY; @@ -154,7 +154,6 @@ void test_supports(const std::string &obj_filename, // Generate the actual support tree sla::SupportTreeBuilder treebuilder; - sla::SupportableMesh sm{emesh, support_points, supportcfg}; switch (sm.cfg.tree_type) { case sla::SupportTreeType::Default: { @@ -182,6 +181,18 @@ void test_supports(const std::string &obj_filename, if (std::abs(supportcfg.object_elevation_mm) < EPSILON) allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm; +#ifndef NDEBUG + if (!(obb.min.z() >= Approx(allowed_zmin)) || !(obb.max.z() <= Approx(zmax))) + { + indexed_triangle_set its; + treebuilder.retrieve_full_mesh(its); + TriangleMesh m{its}; + m.merge(mesh); + m.WriteOBJFile((Catch::getResultCapture().getCurrentTestName() + "_" + + obj_filename).c_str()); + } +#endif + REQUIRE(obb.min.z() >= Approx(allowed_zmin)); REQUIRE(obb.max.z() <= Approx(zmax)); diff --git a/tests/sla_print/sla_test_utils.hpp b/tests/sla_print/sla_test_utils.hpp index 187a72d54..103b2f66a 100644 --- a/tests/sla_print/sla_test_utils.hpp +++ b/tests/sla_print/sla_test_utils.hpp @@ -14,14 +14,12 @@ #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/SLA/Pad.hpp" #include "libslic3r/SLA/SupportTreeBuilder.hpp" -#include "libslic3r/SLA/SupportTreeUtils.hpp" #include "libslic3r/SLA/SupportPointGenerator.hpp" #include "libslic3r/SLA/AGGRaster.hpp" #include "libslic3r/SLA/ConcaveHull.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/SVG.hpp" -#include "libslic3r/Format/OBJ.hpp" using namespace Slic3r; @@ -111,58 +109,6 @@ inline void test_support_model_collision( test_support_model_collision(obj_filename, input_supportcfg, hcfg, {}); } -// Test pair hash for 'nums' random number pairs. -template void test_pairhash() -{ - const constexpr size_t nums = 1000; - I A[nums] = {0}, B[nums] = {0}; - std::unordered_set CH; - std::unordered_map> ints; - - std::random_device rd; - std::mt19937 gen(rd()); - - const I Ibits = int(sizeof(I) * CHAR_BIT); - const II IIbits = int(sizeof(II) * CHAR_BIT); - - int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; - if (std::is_signed::value) bits -= 1; - const I Imin = 0; - const I Imax = I(std::pow(2., bits) - 1); - - std::uniform_int_distribution dis(Imin, Imax); - - for (size_t i = 0; i < nums;) { - I a = dis(gen); - if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; } - } - - for (size_t i = 0; i < nums;) { - I b = dis(gen); - if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; } - } - - for (size_t i = 0; i < nums; ++i) { - I a = A[i], b = B[i]; - - REQUIRE(a != b); - - II hash_ab = sla::pairhash(a, b); - II hash_ba = sla::pairhash(b, a); - REQUIRE(hash_ab == hash_ba); - - auto it = ints.find(hash_ab); - - if (it != ints.end()) { - REQUIRE(( - (it->second.first == a && it->second.second == b) || - (it->second.first == b && it->second.second == a) - )); - } else - ints[hash_ab] = std::make_pair(a, b); - } -} - // SLA Raster test utils: using TPixel = uint8_t;