Working small-to-normal support merging

Fixed fatal bug with anchors for mini supports

Make the optimization cleaner in support generatior

Much better widening behaviour

Add an optimizer interface and the NLopt implementation into libslic3r

New optimizer based only on nlopt C interfase
Fix build and tests
This commit is contained in:
tamasmeszaros 2020-07-16 11:49:30 +02:00
parent 8cb115a035
commit 927b81ea97
9 changed files with 831 additions and 395 deletions

View file

@ -203,6 +203,7 @@ add_library(libslic3r STATIC
SimplifyMeshImpl.hpp
SimplifyMesh.cpp
MarchingSquares.hpp
Optimizer.hpp
${OpenVDBUtils_SOURCES}
SLA/Pad.hpp
SLA/Pad.cpp

369
src/libslic3r/Optimizer.hpp Normal file
View file

@ -0,0 +1,369 @@
#ifndef NLOPTOPTIMIZER_HPP
#define NLOPTOPTIMIZER_HPP
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4244)
#pragma warning(disable: 4267)
#endif
#include <nlopt.h>
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#include <utility>
#include <tuple>
#include <array>
#include <cmath>
#include <functional>
#include <limits>
#include <cassert>
namespace Slic3r { namespace opt {
// A type to hold the complete result of the optimization.
template<size_t N> struct Result {
int resultcode;
std::array<double, N> optimum;
double score;
};
// An interval of possible input values for optimization
class Bound {
double m_min, m_max;
public:
Bound(double min = std::numeric_limits<double>::min(),
double max = std::numeric_limits<double>::max())
: m_min(min), m_max(max)
{}
double min() const noexcept { return m_min; }
double max() const noexcept { return m_max; }
};
// Helper types for optimization function input and bounds
template<size_t N> using Input = std::array<double, N>;
template<size_t N> using Bounds = std::array<Bound, N>;
// A type for specifying the stop criteria. Setter methods can be concatenated
class StopCriteria {
// If the absolute value difference between two scores.
double m_abs_score_diff = std::nan("");
// If the relative value difference between two scores.
double m_rel_score_diff = std::nan("");
// Stop if this value or better is found.
double m_stop_score = std::nan("");
// A predicate that if evaluates to true, the optimization should terminate
// and the best result found prior to termination should be returned.
std::function<bool()> m_stop_condition = [] { return false; };
// The max allowed number of iterations.
unsigned m_max_iterations = 0;
public:
StopCriteria & abs_score_diff(double val)
{
m_abs_score_diff = val; return *this;
}
double abs_score_diff() const { return m_abs_score_diff; }
StopCriteria & rel_score_diff(double val)
{
m_rel_score_diff = val; return *this;
}
double rel_score_diff() const { return m_rel_score_diff; }
StopCriteria & stop_score(double val)
{
m_stop_score = val; return *this;
}
double stop_score() const { return m_stop_score; }
StopCriteria & max_iterations(double val)
{
m_max_iterations = val; return *this;
}
double max_iterations() const { return m_max_iterations; }
template<class Fn> StopCriteria & stop_condition(Fn &&cond)
{
m_stop_condition = cond; return *this;
}
bool stop_condition() { return m_stop_condition(); }
};
// Helper to be used in static_assert.
template<class T> struct always_false { enum { value = false }; };
// Basic interface to optimizer object
template<class Method, class Enable = void> class Optimizer {
public:
Optimizer(const StopCriteria &)
{
static_assert(always_false<Method>::value,
"Optimizer unimplemented for given method!");
}
Optimizer<Method, Enable> &to_min() { return *this; }
Optimizer<Method, Enable> &to_max() { return *this; }
Optimizer<Method, Enable> &set_criteria(const StopCriteria &) { return *this; }
StopCriteria get_criteria() const { return {}; };
template<class Func, size_t N>
Result<N> optimize(Func&& func,
const Input<N> &initvals,
const Bounds<N>& bounds) { return {}; }
// optional for randomized methods:
void seed(long /*s*/) {}
};
namespace detail {
// Helper types for NLopt algorithm selection in template contexts
template<nlopt_algorithm alg> struct NLoptAlg {};
// NLopt can combine multiple algorithms if one is global an other is a local
// method. This is how template specializations can be informed about this fact.
template<nlopt_algorithm gl_alg, nlopt_algorithm lc_alg = NLOPT_LN_NELDERMEAD>
struct NLoptAlgComb {};
template<class M> struct IsNLoptAlg {
static const constexpr bool value = false;
};
template<nlopt_algorithm a> struct IsNLoptAlg<NLoptAlg<a>> {
static const constexpr bool value = true;
};
template<nlopt_algorithm a1, nlopt_algorithm a2>
struct IsNLoptAlg<NLoptAlgComb<a1, a2>> {
static const constexpr bool value = true;
};
template<class M, class T = void>
using NLoptOnly = std::enable_if_t<IsNLoptAlg<M>::value, T>;
// Convert any collection to tuple. This is useful for object functions taking
// an argument list of doubles. Make things cleaner on the call site of
// optimize().
template<size_t I, std::size_t N, class T, class C> struct to_tuple_ {
static auto call(const C &c)
{
return std::tuple_cat(std::tuple<T>(c[N-I]),
to_tuple_<I-1, N, T, C>::call(c));
}
};
template<size_t N, class T, class C> struct to_tuple_<0, N, T, C> {
static auto call(const C &c) { return std::tuple<>(); }
};
// C array to tuple
template<std::size_t N, class T> auto carray_tuple(const T *v)
{
return to_tuple_<N, N, T, const T*>::call(v);
}
// Helper to convert C style array to std::array
template<size_t N, class T> auto to_arr(const T (&a) [N])
{
std::array<T, N> r;
std::copy(std::begin(a), std::end(a), std::begin(r));
return r;
}
enum class OptDir { MIN, MAX }; // Where to optimize
struct NLopt { // Helper RAII class for nlopt_opt
nlopt_opt ptr = nullptr;
template<class...A> explicit NLopt(A&&...a)
{
ptr = nlopt_create(std::forward<A>(a)...);
}
NLopt(const NLopt&) = delete;
NLopt(NLopt&&) = delete;
NLopt& operator=(const NLopt&) = delete;
NLopt& operator=(NLopt&&) = delete;
~NLopt() { nlopt_destroy(ptr); }
};
template<class Method> class NLoptOpt {};
// Optimizers based on NLopt.
template<nlopt_algorithm alg> class NLoptOpt<NLoptAlg<alg>> {
protected:
StopCriteria m_stopcr;
OptDir m_dir;
template<class Fn> using TOptData =
std::tuple<std::remove_reference_t<Fn>*, NLoptOpt*, nlopt_opt>;
template<class Fn, size_t N>
static double optfunc(unsigned n, const double *params,
double *gradient,
void *data)
{
assert(n >= N);
auto tdata = static_cast<TOptData<Fn>*>(data);
if (std::get<1>(*tdata)->m_stopcr.stop_condition())
nlopt_force_stop(std::get<2>(*tdata));
auto fnptr = std::get<0>(*tdata);
auto funval = carray_tuple<N>(params);
return std::apply(*fnptr, funval);
}
template<size_t N>
void set_up(NLopt &nl, const Bounds<N>& bounds)
{
std::array<double, N> lb, ub;
for (size_t i = 0; i < N; ++i) {
lb[i] = bounds[i].min();
ub[i] = bounds[i].max();
}
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();
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(this->m_stopcr.max_iterations() > 0)
nlopt_set_maxeval(nl.ptr, this->m_stopcr.max_iterations());
}
template<class Fn, size_t N>
Result<N> optimize(NLopt &nl, Fn &&fn, const Input<N> &initvals)
{
Result<N> r;
TOptData<Fn> data = std::make_tuple(&fn, this, nl.ptr);
switch(m_dir) {
case OptDir::MIN:
nlopt_set_min_objective(nl.ptr, optfunc<Fn, N>, &data); break;
case OptDir::MAX:
nlopt_set_max_objective(nl.ptr, optfunc<Fn, N>, &data); break;
}
r.optimum = initvals;
r.resultcode = nlopt_optimize(nl.ptr, r.optimum.data(), &r.score);
return r;
}
public:
template<class Func, size_t N>
Result<N> optimize(Func&& func,
const Input<N> &initvals,
const Bounds<N>& bounds)
{
NLopt nl{alg, N};
set_up(nl, bounds);
return optimize(nl, std::forward<Func>(func), initvals);
}
explicit NLoptOpt(StopCriteria stopcr = {}) : m_stopcr(stopcr) {}
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 seed(long s) { nlopt_srand(s); }
};
template<nlopt_algorithm glob, nlopt_algorithm loc>
class NLoptOpt<NLoptAlgComb<glob, loc>>: public NLoptOpt<NLoptAlg<glob>>
{
using Base = NLoptOpt<NLoptAlg<glob>>;
public:
template<class Fn, size_t N>
Result<N> optimize(Fn&& f,
const Input<N> &initvals,
const Bounds<N>& 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<Fn>(f), initvals);
}
explicit NLoptOpt(StopCriteria stopcr = {}) : Base{stopcr} {}
};
} // namespace detail;
// Optimizers based on NLopt.
template<class M> class Optimizer<M, detail::NLoptOnly<M>> {
detail::NLoptOpt<M> m_opt;
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<class Func, size_t N>
Result<N> optimize(Func&& func,
const Input<N> &initvals,
const Bounds<N>& bounds)
{
return m_opt.optimize(std::forward<Func>(func), initvals, bounds);
}
explicit Optimizer(StopCriteria stopcr = {}) : m_opt(stopcr) {}
Optimizer &set_criteria(const StopCriteria &cr)
{
m_opt.set_criteria(cr); return *this;
}
const StopCriteria &get_criteria() const { return m_opt.get_criteria(); }
void seed(long s) { m_opt.seed(s); }
};
template<size_t N> Bounds<N> bounds(const Bound (&b) [N]) { return detail::to_arr(b); }
template<size_t N> Input<N> initvals(const double (&a) [N]) { return detail::to_arr(a); }
// Predefinded NLopt algorithms that are used in the codebase
using AlgNLoptGenetic = detail::NLoptAlgComb<NLOPT_GN_ESCH>;
using AlgNLoptSubplex = detail::NLoptAlg<NLOPT_LN_SBPLX>;
using AlgNLoptSimplex = detail::NLoptAlg<NLOPT_LN_NELDERMEAD>;
// Helper defs for pre-crafted global and local optimizers that work well.
using DefaultGlobalOptimizer = Optimizer<AlgNLoptGenetic>;
using DefaultLocalOptimizer = Optimizer<AlgNLoptSubplex>;
}} // namespace Slic3r::opt
#endif // NLOPTOPTIMIZER_HPP

View file

@ -87,7 +87,7 @@ public:
inline Vec3d position() const { return m_source + m_dir * m_t; }
inline int face() const { return m_face_id; }
inline bool is_valid() const { return m_mesh != nullptr; }
inline bool is_hit() const { return !std::isinf(m_t); }
inline bool is_hit() const { return m_face_id >= 0 && !std::isinf(m_t); }
inline const Vec3d& normal() const {
assert(is_valid());

View file

@ -156,6 +156,11 @@ const TriangleMesh &SupportTreeBuilder::merged_mesh(size_t steps) const
merged.merge(get_mesh(bs, steps));
}
for (auto &bs : m_diffbridges) {
if (ctl().stopcondition()) break;
merged.merge(get_mesh(bs, steps));
}
for (auto &anch : m_anchors) {
if (ctl().stopcondition()) break;
merged.merge(get_mesh(anch, steps));

View file

@ -177,6 +177,14 @@ struct Bridge: public SupportTreeNode {
Vec3d get_dir() const { return (endp - startp).normalized(); }
};
struct DiffBridge: public Bridge {
double end_r;
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}
{}
};
// A wrapper struct around the pad
struct Pad {
TriangleMesh tmesh;
@ -216,6 +224,7 @@ class SupportTreeBuilder: public SupportTree {
std::vector<Junction> m_junctions;
std::vector<Bridge> m_bridges;
std::vector<Bridge> m_crossbridges;
std::vector<DiffBridge> m_diffbridges;
std::vector<Pedestal> m_pedestals;
std::vector<Anchor> m_anchors;
@ -228,8 +237,8 @@ class SupportTreeBuilder: public SupportTree {
mutable bool m_meshcache_valid = false;
mutable double m_model_height = 0; // the full height of the model
template<class...Args>
const Bridge& _add_bridge(std::vector<Bridge> &br, Args&&... args)
template<class BridgeT, class...Args>
const BridgeT& _add_bridge(std::vector<BridgeT> &br, Args&&... args)
{
std::lock_guard<Mutex> lk(m_mutex);
br.emplace_back(std::forward<Args>(args)...);
@ -331,17 +340,6 @@ public:
return pillar.id;
}
const Pillar& head_pillar(unsigned headid) const
{
std::lock_guard<Mutex> lk(m_mutex);
assert(headid < m_head_indices.size());
const Head& h = m_heads[m_head_indices[headid]];
assert(h.pillar_id >= 0 && h.pillar_id < long(m_pillars.size()));
return m_pillars[size_t(h.pillar_id)];
}
template<class...Args> const Junction& add_junction(Args&&... args)
{
std::lock_guard<Mutex> lk(m_mutex);
@ -375,6 +373,11 @@ public:
return _add_bridge(m_crossbridges, std::forward<Args>(args)...);
}
template<class...Args> const DiffBridge& add_diffbridge(Args&&... args)
{
return _add_bridge(m_diffbridges, std::forward<Args>(args)...);
}
Head &head(unsigned id)
{
std::lock_guard<Mutex> lk(m_mutex);

View file

@ -1,18 +1,25 @@
#include <libslic3r/SLA/SupportTreeBuildsteps.hpp>
#include <libslic3r/SLA/SpatIndex.hpp>
#include <libnest2d/optimizers/nlopt/genetic.hpp>
#include <libnest2d/optimizers/nlopt/subplex.hpp>
#include <libslic3r/Optimizer.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace sla {
using libnest2d::opt::initvals;
using libnest2d::opt::bound;
using libnest2d::opt::StopCriteria;
using libnest2d::opt::GeneticOptimizer;
using libnest2d::opt::SubplexOptimizer;
using Slic3r::opt::initvals;
using Slic3r::opt::bounds;
using Slic3r::opt::StopCriteria;
using Slic3r::opt::Optimizer;
using Slic3r::opt::AlgNLoptSubplex;
using Slic3r::opt::AlgNLoptGenetic;
StopCriteria get_criteria(const SupportTreeConfig &cfg)
{
return StopCriteria{}
.rel_score_diff(cfg.optimizer_rel_score_diff)
.max_iterations(cfg.optimizer_max_iterations);
}
template<class C, class Hit = IndexedMesh::hit_result>
static Hit min_hit(const C &hits)
@ -454,24 +461,52 @@ bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head,
return true;
}
bool SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp,
bool SupportTreeBuildsteps::create_ground_pillar(const Vec3d &hjp,
const Vec3d &sourcedir,
double radius,
long head_id)
{
double sd = m_cfg.pillar_base_safety_distance_mm;
Vec3d jp = hjp, endp = jp, dir = sourcedir;
long pillar_id = SupportTreeNode::ID_UNSET;
bool can_add_base = radius >= m_cfg.head_back_radius_mm;
double base_r = can_add_base ? m_cfg.base_radius_mm : 0.;
double gndlvl = m_builder.ground_level;
if (!can_add_base) gndlvl -= m_mesh.ground_level_offset();
Vec3d endp = {jp(X), jp(Y), gndlvl};
double min_dist = sd + base_r + EPSILON;
bool normal_mode = true;
Vec3d dir = sourcedir;
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
auto to_floor = [&gndlvl](const Vec3d &p) { return Vec3d{p.x(), p.y(), gndlvl}; };
auto eval_limits = [this, &radius, &can_add_base, &gndlvl, &gap_dist, &jp_gnd]
(bool base_en = true)
{
can_add_base = base_en && radius >= m_cfg.head_back_radius_mm;
double base_r = can_add_base ? m_cfg.base_radius_mm : 0.;
gndlvl = m_builder.ground_level;
if (!can_add_base) gndlvl -= m_mesh.ground_level_offset();
jp_gnd = gndlvl + (can_add_base ? 0. : m_cfg.head_back_radius_mm);
gap_dist = m_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 < m_cfg.head_back_radius_mm && jp.z() - gndlvl > 20 * radius)
{
std::optional<DiffBridge> diffbr =
search_widening_path(jp, dir, radius, m_cfg.head_back_radius_mm);
if (diffbr && diffbr->endp.z() > jp_gnd) {
auto &br = m_builder.add_diffbridge(diffbr.value());
if (head_id >= 0) m_builder.head(head_id).bridge_id = br.id;
endp = diffbr->endp;
radius = diffbr->end_r;
m_builder.add_junction(endp, radius);
non_head = true;
dir = diffbr->get_dir();
eval_limits();
} else return false;
}
if (m_cfg.object_elevation_mm < EPSILON)
{
// get a suitable direction for the corrector bridge. It is the
@ -479,95 +514,112 @@ bool SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp,
// configured bridge slope.
auto [polar, azimuth] = dir_to_spheric(dir);
polar = PI - m_cfg.bridge_slope;
Vec3d dir = spheric_to_dir(polar, azimuth).normalized();
Vec3d d = spheric_to_dir(polar, azimuth).normalized();
double t = bridge_mesh_distance(endp, dir, radius);
double tmax = std::min(m_cfg.max_bridge_length_mm, t);
t = 0.;
// Check the distance of the endpoint and the closest point on model
// body. It should be greater than the min_dist which is
// the safety distance from the model. It includes the pad gap if in
// zero elevation mode.
//
// Try to move along the established bridge direction to dodge the
// forbidden region for the endpoint.
double t = -radius;
bool succ = true;
while (std::sqrt(m_mesh.squared_distance(to_floor(endp))) < min_dist ||
!std::isinf(bridge_mesh_distance(endp, DOWN, radius))) {
double zd = endp.z() - jp_gnd;
double tmax2 = zd / std::sqrt(1 - m_cfg.bridge_slope * m_cfg.bridge_slope);
tmax = std::min(tmax, tmax2);
Vec3d nexp = endp;
double dlast = 0.;
while (((dlast = std::sqrt(m_mesh.squared_distance(to_floor(nexp)))) < gap_dist ||
!std::isinf(bridge_mesh_distance(nexp, DOWN, radius))) && t < tmax) {
t += radius;
endp = jp + t * dir;
normal_mode = false;
if (t > m_cfg.max_bridge_length_mm || endp(Z) < gndlvl) {
if (head_id >= 0) m_builder.add_pillar(head_id, 0.);
succ = false;
break;
}
nexp = endp + t * d;
}
if (!succ) {
if (can_add_base) {
if (dlast < gap_dist && can_add_base) {
nexp = endp;
t = 0.;
can_add_base = false;
base_r = 0.;
gndlvl -= m_mesh.ground_level_offset();
min_dist = sd + base_r + EPSILON;
endp = {jp(X), jp(Y), gndlvl + radius};
eval_limits(can_add_base);
t = -radius;
while (std::sqrt(m_mesh.squared_distance(to_floor(endp))) < min_dist ||
!std::isinf(bridge_mesh_distance(endp, DOWN, radius))) {
zd = endp.z() - jp_gnd;
tmax2 = zd / std::sqrt(1 - m_cfg.bridge_slope * m_cfg.bridge_slope);
tmax = std::min(tmax, tmax2);
while (((dlast = std::sqrt(m_mesh.squared_distance(to_floor(nexp)))) < gap_dist ||
!std::isinf(bridge_mesh_distance(nexp, DOWN, radius))) && t < tmax) {
t += radius;
endp = jp + t * dir;
normal_mode = false;
if (t > m_cfg.max_bridge_length_mm || endp(Z) < (gndlvl + radius)) {
if (head_id >= 0) m_builder.add_pillar(head_id, 0.);
return false;
}
}
} else return false;
nexp = endp + t * d;
}
}
double h = (jp - endp).norm();
// Could not find a path to avoid the pad gap
if (dlast < gap_dist) return false;
// Check if the deduced route is sane and exit with error if not.
if (bridge_mesh_distance(jp, dir, radius) < h) {
if (head_id >= 0) m_builder.add_pillar(head_id, 0.);
return false;
if (t > 0.) { // Need to make additional bridge
const Bridge& br = m_builder.add_bridge(endp, nexp, radius);
if (head_id >= 0) m_builder.head(head_id).bridge_id = br.id;
m_builder.add_junction(nexp, radius);
endp = nexp;
non_head = true;
}
}
// Straigh path down, no area to dodge
if (normal_mode) {
pillar_id = head_id >= 0 ? m_builder.add_pillar(head_id, h) :
m_builder.add_pillar(endp, h, radius);
Vec3d gp = to_floor(endp);
double h = endp.z() - gp.z();
pillar_id = head_id >= 0 && !non_head ? m_builder.add_pillar(head_id, h) :
m_builder.add_pillar(gp, h, radius);
if (can_add_base)
add_pillar_base(pillar_id);
} else {
// Insert the bridge to get around the forbidden area
Vec3d pgnd{endp.x(), endp.y(), gndlvl};
pillar_id = m_builder.add_pillar(pgnd, endp.z() - gndlvl, radius);
if (can_add_base)
add_pillar_base(pillar_id);
m_builder.add_bridge(jp, endp, radius);
m_builder.add_junction(endp, radius);
// Add a degenerated pillar and the bridge.
// The degenerate pillar will have zero length and it will
// prevent from queries of head_pillar() to have non-existing
// pillar when the head should have one.
if (head_id >= 0)
m_builder.add_pillar(head_id, 0.);
}
if(pillar_id >= 0) // Save the pillar endpoint in the spatial index
m_pillar_index.guarded_insert(endp, unsigned(pillar_id));
m_pillar_index.guarded_insert(m_builder.pillar(pillar_id).endpt,
unsigned(pillar_id));
return true;
}
std::optional<DiffBridge> SupportTreeBuildsteps::search_widening_path(
const Vec3d &jp, const Vec3d &dir, double radius, double new_radius)
{
double w = radius + 2 * m_cfg.head_back_radius_mm;
double stopval = w + jp.z() - m_builder.ground_level;
Optimizer<AlgNLoptSubplex> solver(get_criteria(m_cfg).stop_score(stopval));
auto [polar, azimuth] = dir_to_spheric(dir);
double fallback_ratio = radius / m_cfg.head_back_radius_mm;
auto oresult = solver.to_max().optimize(
[this, jp, radius, new_radius](double plr, double azm, double t) {
auto d = spheric_to_dir(plr, azm).normalized();
double ret = pinhead_mesh_intersect(jp, d, radius, new_radius, t)
.distance();
double down = bridge_mesh_distance(jp + t * d, d, new_radius);
if (ret > t && std::isinf(down))
ret += jp.z() - m_builder.ground_level;
return ret;
},
initvals({polar, azimuth, w}), // start with what we have
bounds({
{PI - m_cfg.bridge_slope, PI}, // Must not exceed the slope limit
{-PI, PI}, // azimuth can be a full search
{radius + m_cfg.head_back_radius_mm,
fallback_ratio * m_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, m_cfg.head_back_radius_mm);
}
return {};
}
void SupportTreeBuildsteps::filter()
{
// Get the points that are too close to each other and keep only the
@ -592,13 +644,21 @@ void SupportTreeBuildsteps::filter()
// not be enough space for the pinhead. Filtering is applied for
// these reasons.
ccr::SpinningMutex mutex;
auto addfn = [&mutex](PtIndices &container, unsigned val) {
std::lock_guard<ccr::SpinningMutex> lk(mutex);
container.emplace_back(val);
};
std::vector<Head> heads; heads.reserve(m_support_pts.size());
for (const SupportPoint &sp : m_support_pts) {
m_thr();
heads.emplace_back(
std::nan(""),
sp.head_front_radius,
0.,
m_cfg.head_penetration_mm,
Vec3d::Zero(), // dir
sp.pos.cast<double>() // displacement
);
}
auto filterfn = [this, &nmls, addfn](unsigned fidx, size_t i) {
std::function<void(unsigned, size_t, double)> filterfn;
filterfn = [this, &nmls, &heads, &filterfn](unsigned fidx, size_t i, double back_r) {
m_thr();
auto n = nmls.row(Eigen::Index(i));
@ -616,15 +676,20 @@ void SupportTreeBuildsteps::filter()
if (polar < PI - m_cfg.normal_cutoff_angle) return;
// We saturate the polar angle to 3pi/4
polar = std::max(polar, 3*PI / 4);
polar = std::max(polar, PI - m_cfg.bridge_slope);
// save the head (pinpoint) position
Vec3d hp = m_points.row(fidx);
double lmin = m_cfg.head_width_mm, lmax = lmin;
if (back_r < m_cfg.head_back_radius_mm) {
lmin = 0., lmax = m_cfg.head_penetration_mm;
}
// The distance needed for a pinhead to not collide with model.
double w = m_cfg.head_width_mm +
m_cfg.head_back_radius_mm +
2*m_cfg.head_front_radius_mm;
double w = lmin + 2 * back_r + 2 * m_cfg.head_front_radius_mm -
m_cfg.head_penetration_mm;
double pin_r = double(m_support_pts[fidx].head_front_radius);
@ -632,113 +697,69 @@ void SupportTreeBuildsteps::filter()
auto nn = spheric_to_dir(polar, azimuth).normalized();
// check available distance
IndexedMesh::hit_result t
= pinhead_mesh_intersect(hp, // touching point
nn, // normal
pin_r,
m_cfg.head_back_radius_mm,
w);
if(t.distance() <= w) {
IndexedMesh::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
// viable normal that doesn't collide with the model
// geometry and its very close to the default.
StopCriteria stc;
stc.max_iterations = m_cfg.optimizer_max_iterations;
stc.relative_score_difference = m_cfg.optimizer_rel_score_diff;
stc.stop_score = w; // space greater than w is enough
GeneticOptimizer solver(stc);
solver.seed(0); // we want deterministic behavior
// stc.stop_score = w; // space greater than w is enough
Optimizer<AlgNLoptGenetic> solver(get_criteria(m_cfg));
solver.seed(0);
//solver.seed(0); // we want deterministic behavior
auto oresult = solver.optimize_max(
[this, pin_r, w, hp](double plr, double azm)
auto oresult = solver.to_max().optimize(
[this, pin_r, back_r, hp](double plr, double azm, double l)
{
auto dir = spheric_to_dir(plr, azm).normalized();
double score = pinhead_mesh_intersect(
hp, dir, pin_r, m_cfg.head_back_radius_mm, w).distance();
hp, dir, pin_r, back_r, l).distance();
return score;
},
initvals(polar, azimuth), // start with what we have
bound(3 * PI / 4, PI), // Must not exceed the tilt limit
bound(-PI, PI) // azimuth can be a full search
);
initvals({polar, azimuth, (lmin + lmax) / 2.}), // start with what we have
bounds({
{PI - m_cfg.bridge_slope, PI}, // Must not exceed the tilt limit
{-PI, PI}, // azimuth can be a full search
{lmin, lmax}
}));
if(oresult.score > w) {
polar = std::get<0>(oresult.optimum);
azimuth = std::get<1>(oresult.optimum);
nn = spheric_to_dir(polar, azimuth).normalized();
lmin = std::get<2>(oresult.optimum);
t = IndexedMesh::hit_result(oresult.score);
}
}
// save the verified and corrected normal
m_support_nmls.row(fidx) = nn;
if (t.distance() > w) {
// Check distance from ground, we might have zero elevation.
if (hp(Z) + w * nn(Z) < m_builder.ground_level) {
addfn(m_iheadless, fidx);
} else {
// mark the point for needing a head.
addfn(m_iheads, fidx);
if (t.distance() > w && hp(Z) + w * nn(Z) >= m_builder.ground_level) {
Head &h = heads[fidx];
h.id = fidx; h.dir = nn; h.width_mm = lmin; h.r_back_mm = back_r;
} else if (back_r > m_cfg.head_fallback_radius_mm) {
filterfn(fidx, i, m_cfg.head_fallback_radius_mm);
}
} else if (polar >= 3 * PI / 4) {
// Headless supports do not tilt like the headed ones
// so the normal should point almost to the ground.
addfn(m_iheadless, fidx);
}
};
ccr::enumerate(filtered_indices.begin(), filtered_indices.end(), filterfn);
ccr::enumerate(filtered_indices.begin(), filtered_indices.end(),
[this, &filterfn](unsigned fidx, size_t i) {
filterfn(fidx, i, m_cfg.head_back_radius_mm);
});
for (size_t i = 0; i < heads.size(); ++i)
if (heads[i].is_valid()) {
m_builder.add_head(i, heads[i]);
m_iheads.emplace_back(i);
}
m_thr();
}
void SupportTreeBuildsteps::add_pinheads()
{
for (unsigned i : m_iheads) {
m_thr();
m_builder.add_head(
i,
m_cfg.head_back_radius_mm,
m_support_pts[i].head_front_radius,
m_cfg.head_width_mm,
m_cfg.head_penetration_mm,
m_support_nmls.row(i), // dir
m_support_pts[i].pos.cast<double>() // displacement
);
}
for (unsigned i : m_iheadless) {
const auto R = double(m_support_pts[i].head_front_radius);
// The support point position on the mesh
Vec3d sph = m_support_pts[i].pos.cast<double>();
// Get an initial normal from the filtering step
Vec3d n = m_support_nmls.row(i);
// First we need to determine the available space for a mini pinhead.
// The goal is the move away from the model a little bit to make the
// contact point small as possible and avoid pearcing the model body.
double back_r = m_cfg.head_fallback_radius_mm;
double max_w = 2 * R;
double pin_space = std::min(max_w,
pinhead_mesh_intersect(sph, n, R, back_r,
max_w, 0.)
.distance());
if (pin_space <= 0) continue;
m_iheads.emplace_back(i);
m_builder.add_head(i, back_r, R, pin_space,
m_cfg.head_penetration_mm, n, sph);
}
}
void SupportTreeBuildsteps::classify()
@ -755,7 +776,7 @@ void SupportTreeBuildsteps::classify()
for(unsigned i : m_iheads) {
m_thr();
auto& head = m_builder.head(i);
Head &head = m_builder.head(i);
double r = head.r_back_mm;
Vec3d headjp = head.junction_point();
@ -843,12 +864,9 @@ void SupportTreeBuildsteps::routing_to_ground()
auto cidx = cl_centroids[ci++];
// TODO: don't consider the cluster centroid but calculate a
// central position where the pillar can be placed. this way
// the weight is distributed more effectively on the pillar.
auto centerpillarID = m_builder.head_pillar(cidx).id;
auto q = m_pillar_index.query(m_builder.head(cidx).junction_point(), 1);
if (!q.empty()) {
long centerpillarID = q.front().second;
for (auto c : cl) {
m_thr();
if (c == cidx) continue;
@ -865,6 +883,7 @@ void SupportTreeBuildsteps::routing_to_ground()
}
}
}
}
bool SupportTreeBuildsteps::connect_to_ground(Head &head, const Vec3d &dir)
{
@ -899,23 +918,18 @@ bool SupportTreeBuildsteps::connect_to_ground(Head &head)
// direction out of the cavity.
auto [polar, azimuth] = dir_to_spheric(head.dir);
StopCriteria stc;
stc.max_iterations = m_cfg.optimizer_max_iterations;
stc.relative_score_difference = m_cfg.optimizer_rel_score_diff;
stc.stop_score = 1e6;
GeneticOptimizer solver(stc);
Optimizer<AlgNLoptGenetic> solver(get_criteria(m_cfg).stop_score(1e6));
solver.seed(0); // we want deterministic behavior
double r_back = head.r_back_mm;
Vec3d hjp = head.junction_point();
auto oresult = solver.optimize_max(
auto oresult = solver.to_max().optimize(
[this, hjp, r_back](double plr, double azm) {
Vec3d n = spheric_to_dir(plr, azm).normalized();
return bridge_mesh_distance(hjp, n, r_back);
},
initvals(polar, azimuth), // let's start with what we have
bound(3*PI/4, PI), // Must not exceed the slope limit
bound(-PI, PI) // azimuth can be a full range search
initvals({polar, azimuth}), // let's start with what we have
bounds({ {PI - m_cfg.bridge_slope, PI}, {-PI, PI} })
);
Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized();
@ -944,7 +958,9 @@ bool SupportTreeBuildsteps::connect_to_model_body(Head &head)
// The width of the tail head that we would like to have...
h = std::min(hit.distance() - head.r_back_mm, h);
if(h <= 0.) return false;
// If this is a mini pillar dont bother with the tail width, can be 0.
if (head.r_back_mm < m_cfg.head_back_radius_mm) h = std::max(h, 0.);
else if (h <= 0.) return false;
Vec3d endp{hjp(X), hjp(Y), hjp(Z) - hit.distance() + h};
auto center_hit = m_mesh.query_ray_hit(hjp, DOWN);

View file

@ -45,6 +45,11 @@ inline Vec3d spheric_to_dir(const std::pair<double, double> &v)
return spheric_to_dir(v.first, v.second);
}
inline Vec3d spheric_to_dir(const std::array<double, 2> &v)
{
return spheric_to_dir(v[0], v[1]);
}
// 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
@ -249,7 +254,8 @@ class SupportTreeBuildsteps {
double width)
{
return pinhead_mesh_intersect(s, dir, r_pin, r_back, width,
m_cfg.safety_distance_mm);
r_back * m_cfg.safety_distance_mm /
m_cfg.head_back_radius_mm);
}
// Checking bridge (pillar and stick as well) intersection with the model.
@ -271,7 +277,9 @@ class SupportTreeBuildsteps {
const Vec3d& dir,
double r)
{
return bridge_mesh_intersect(s, dir, r, m_cfg.safety_distance_mm);
return bridge_mesh_intersect(s, dir, r,
r * m_cfg.safety_distance_mm /
m_cfg.head_back_radius_mm);
}
template<class...Args>
@ -311,6 +319,11 @@ class SupportTreeBuildsteps {
m_builder.add_pillar_base(pid, m_cfg.base_height_mm, m_cfg.base_radius_mm);
}
std::optional<DiffBridge> search_widening_path(const Vec3d &jp,
const Vec3d &dir,
double radius,
double new_radius);
public:
SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm);

View file

@ -94,6 +94,24 @@ inline Contour3D get_mesh(const Bridge &br, size_t steps)
return mesh;
}
inline Contour3D get_mesh(const DiffBridge &br, size_t steps)
{
double h = br.get_length();
Contour3D mesh = halfcone(h, br.r, br.end_r, Vec3d::Zero(), steps);
using Quaternion = Eigen::Quaternion<double>;
// We rotate the head to the specified direction. The head's pointing
// side is facing upwards so this means that it would hold a support
// point with a normal pointing straight down. This is the reason of
// the -1 z coordinate
auto quatern = Quaternion::FromTwoVectors(Vec3d{0, 0, 1}, br.get_dir());
for(auto& p : mesh.points) p = quatern * p + br.startp;
return mesh;
}
}} // namespace Slic3r::sla
#endif // SUPPORTTREEMESHER_HPP

View file

@ -4,6 +4,8 @@
#include "sla_test_utils.hpp"
#include <libslic3r/SLA/SupportTreeMesher.hpp>
namespace {
const char *const BELOW_PAD_TEST_OBJECTS[] = {
@ -228,3 +230,12 @@ TEST_CASE("Triangle mesh conversions should be correct", "[SLAConversions]")
cntr.from_obj(infile);
}
}
TEST_CASE("halfcone test", "[halfcone]") {
sla::DiffBridge br{Vec3d{1., 1., 1.}, Vec3d{10., 10., 10.}, 0.25, 0.5};
TriangleMesh m = sla::to_triangle_mesh(sla::get_mesh(br, 45));
m.require_shared_vertices();
m.WriteOBJFile("Halfcone.obj");
}