Merge branch 'master' into et_tm_sla_volumes_6-SPE-1285

This commit is contained in:
tamasmeszaros 2023-01-18 16:40:12 +01:00
commit 76d0e11699
72 changed files with 9347 additions and 4714 deletions

View File

@ -0,0 +1,2 @@
min_slic3r_version = 2.6.0-alpha0
1.0.0 Initial

File diff suppressed because it is too large Load Diff

View File

@ -67,9 +67,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

View File

@ -36,6 +36,10 @@ static const std::string MODEL_PREFIX = "model:";
// are phased out, then we will revert to the original name.
//static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version";
static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version2";
// Url to index archive zip that contains latest indicies
static const std::string INDEX_ARCHIVE_URL= "https://files.prusa3d.com/wp-content/uploads/repository/vendor_indices.zip";
// Url to folder with vendor profile files. Used when downloading new profiles that are not in resources folder.
static const std::string PROFILE_FOLDER_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/";
const std::string AppConfig::SECTION_FILAMENTS = "filaments";
const std::string AppConfig::SECTION_MATERIALS = "sla_materials";
@ -68,6 +72,8 @@ void AppConfig::set_defaults()
// If set, the "- default -" selections of print/filament/printer are suppressed, if there is a valid preset available.
if (get("no_defaults").empty())
set("no_defaults", "1");
if (get("no_templates").empty())
set("no_templates", "0");
if (get("show_incompatible_presets").empty())
set("show_incompatible_presets", "0");
@ -665,6 +671,26 @@ std::string AppConfig::version_check_url() const
return from_settings.empty() ? VERSION_CHECK_URL : from_settings;
}
std::string AppConfig::index_archive_url() const
{
#if 0
// this code is for debug & testing purposes only - changed url wont get trough inner checks anyway.
auto from_settings = get("index_archive_url");
return from_settings.empty() ? INDEX_ARCHIVE_URL : from_settings;
#endif
return INDEX_ARCHIVE_URL;
}
std::string AppConfig::profile_folder_url() const
{
#if 0
// this code is for debug & testing purposes only - changed url wont get trough inner checks anyway.
auto from_settings = get("profile_folder_url");
return from_settings.empty() ? PROFILE_FOLDER_URL : from_settings;
#endif
return PROFILE_FOLDER_URL;
}
bool AppConfig::exists()
{
return boost::filesystem::exists(config_path());

View File

@ -139,6 +139,11 @@ public:
// Get the Slic3r version check url.
// This returns a hardcoded string unless it is overriden by "version_check_url" in the ini file.
std::string version_check_url() const;
// Get the Slic3r url to vendor index archive zip.
std::string index_archive_url() const;
// Get the Slic3r url to folder with vendor profile files.
std::string profile_folder_url() const;
// Returns the original Slic3r version found in the ini file before it was overwritten
// by the current version

View File

@ -5,7 +5,7 @@
#include <optional>
#include <algorithm>
#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<Vec3f> 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);

View File

@ -5,7 +5,6 @@
#include <admesh/stl.h>
#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<Vec3f> 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;

View File

@ -1,82 +1,15 @@
#include "PointCloud.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Tesselate.hpp"
#include "libslic3r/SLA/SupportTreeUtils.hpp"
#include <igl/random_points_on_mesh.h>
namespace Slic3r { namespace branchingtree {
std::optional<Vec3f> find_merge_pt(const Vec3f &A,
const Vec3f &B,
float critical_angle)
std::optional<Vec3f> 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<float, 2, 3> 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<Node> sample_mesh(const indexed_triangle_set &its, double radius)
std::vector<Node> sample_bed(const ExPolygons &bed, float z, double radius)
{
std::vector<Vec3f> 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<Node> 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});
}
}

View File

@ -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<PointIndexEl, boost::geometry::index::rstar<16, 4> /* ? */>
m_ktree;
bool is_outside_support_cone(const Vec3f &supp, const Vec3f &pt) const
{
Vec3d D = (pt - supp).cast<double>();
double dot_sq = -D.z() * std::abs(-D.z());
return dot_sq < D.squaredNorm() * cos2bridge_slope;
}
template<class PC>
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>();
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<class Fn> constexpr bool IsTraverseFn = std::is_invocable_v<Fn, Node&>;
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<class PC, class Fn> 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<std::invoke_result_t<Fn, decltype(nroot)>, 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

View File

@ -82,12 +82,11 @@ inline std::tuple<Vec2d, double> detect_bridging_direction(const Polygons &to_co
if (floating_polylines.empty()) {
// consider this area anchored from all sides, pick bridging direction that will likely yield shortest bridges
//use 3mm resolution (should be quite fast, and rough estimation should not cause any problems here)
auto [pc1, pc2] = compute_principal_components(overhang_area, 3.0);
if (pc2 == Vec2d::Zero()) { // overhang may be smaller than resolution. In this case, any direction is ok
auto [pc1, pc2] = compute_principal_components(overhang_area);
if (pc2 == Vec2f::Zero()) { // overhang may be smaller than resolution. In this case, any direction is ok
return {Vec2d{1.0,0.0}, 0.0};
} else {
return {pc2.normalized(), 0.0};
return {pc2.normalized().cast<double>(), 0.0};
}
}

View File

@ -331,6 +331,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

View File

@ -445,8 +445,11 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
f->angle = surface_fill.params.angle;
f->adapt_fill_octree = (surface_fill.params.pattern == ipSupportCubic) ? support_fill_octree : adaptive_fill_octree;
if (surface_fill.params.pattern == ipLightning)
dynamic_cast<FillLightning::Filler*>(f.get())->generator = lightning_generator;
if (surface_fill.params.pattern == ipLightning) {
auto *lf = dynamic_cast<FillLightning::Filler*>(f.get());
lf->generator = lightning_generator;
lf->num_raft_layers = this->object()->slicing_parameters().raft_layers();
}
if (perimeter_generator.value == PerimeterGeneratorType::Arachne && surface_fill.params.pattern == ipConcentric) {
FillConcentric *fill_concentric = dynamic_cast<FillConcentric *>(f.get());

View File

@ -13,7 +13,7 @@ void Filler::_fill_surface_single(
ExPolygon expolygon,
Polylines &polylines_out)
{
const Layer &layer = generator->getTreesForLayer(this->layer_id);
const Layer &layer = generator->getTreesForLayer(this->layer_id - this->num_raft_layers);
Polylines fill_lines = layer.convertToLines(to_polygons(expolygon), scaled<coord_t>(0.5 * this->spacing - this->overlap));
if (params.dont_connect() || fill_lines.size() <= 1) {

View File

@ -22,6 +22,7 @@ public:
~Filler() override = default;
Generator *generator { nullptr };
size_t num_raft_layers { 0 };
protected:
Fill* clone() const override { return new Filler(*this); }

View File

@ -577,12 +577,22 @@ void Layer::sort_perimeters_into_islands(
}
if (! sample_set) {
// If there is no infill, take a sample of some inner perimeter.
for (uint32_t iperimeter : extrusions.first)
if (const ExtrusionEntity &ee = *this_layer_region.perimeters().entities[iperimeter]; ! ee.role().is_external()) {
sample = ee.first_point();
for (uint32_t iperimeter : extrusions.first) {
const ExtrusionEntity &ee = *this_layer_region.perimeters().entities[iperimeter];
if (ee.is_collection()) {
for (const ExtrusionEntity *ee2 : dynamic_cast<const ExtrusionEntityCollection&>(ee).entities)
if (! ee2->role().is_external()) {
sample = ee2->first_point();
sample_set = true;
goto loop_end;
}
} else if (! ee.role().is_external()) {
sample = ee.first_point();
sample_set = true;
break;
}
}
loop_end:
if (! sample_set) {
if (! extrusions.second.empty()) {
// If there is no inner perimeter, take a sample of some gap fill extrusion.
@ -752,7 +762,9 @@ void Layer::sort_perimeters_into_islands(
const PrintRegionConfig &region_config = this_layer_region.region().config();
const auto bbox_eps = scaled<coord_t>(
EPSILON + print_config.gcode_resolution.value +
(region_config.fuzzy_skin.value == FuzzySkinType::None ? 0. : region_config.fuzzy_skin_thickness.value));
(region_config.fuzzy_skin.value == FuzzySkinType::None ? 0. : region_config.fuzzy_skin_thickness.value
//FIXME it looks as if Arachne could extend open lines by fuzzy_skin_point_dist, which does not seem right.
+ region_config.fuzzy_skin_point_dist.value));
auto point_inside_surface_dist2 =
[&lslices = this->lslices, &lslices_ex = this->lslices_ex, bbox_eps]
(const size_t lslice_idx, const Point &point) {

View File

@ -13,7 +13,9 @@ template<size_t N>
long num_iter(const std::array<size_t, N> &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;
}

View File

@ -13,7 +13,7 @@
#include <utility>
#include <libslic3r/Optimize/Optimizer.hpp>
#include "Optimizer.hpp"
namespace Slic3r { namespace opt {
@ -40,69 +40,163 @@ struct IsNLoptAlg<NLoptAlgComb<a1, a2>> {
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<class Alg> struct NLoptAUGLAG {};
template<nlopt_algorithm a1, nlopt_algorithm a2>
struct IsNLoptAlg<NLoptAUGLAG<NLoptAlgComb<a1, a2>>> {
static const constexpr bool value = true;
};
template<nlopt_algorithm a> struct IsNLoptAlg<NLoptAUGLAG<NLoptAlg<a>>> {
static const constexpr bool value = true;
};
template<class M, class T = void>
using NLoptOnly = std::enable_if_t<IsNLoptAlg<M>::value, T>;
template<class M> struct GetNLoptAlg_ {
static constexpr nlopt_algorithm Local = NLOPT_NUM_ALGORITHMS;
static constexpr nlopt_algorithm Global = NLOPT_NUM_ALGORITHMS;
static constexpr bool IsAUGLAG = false;
};
template<nlopt_algorithm a> struct GetNLoptAlg_<NLoptAlg<a>> {
static constexpr nlopt_algorithm Local = NLOPT_NUM_ALGORITHMS;
static constexpr nlopt_algorithm Global = a;
static constexpr bool IsAUGLAG = false;
};
template<nlopt_algorithm g, nlopt_algorithm l>
struct GetNLoptAlg_<NLoptAlgComb<g, l>> {
static constexpr nlopt_algorithm Local = l;
static constexpr nlopt_algorithm Global = g;
static constexpr bool IsAUGLAG = false;
};
template<class M> constexpr nlopt_algorithm GetNLoptAlg_Global = GetNLoptAlg_<remove_cvref_t<M>>::Global;
template<class M> constexpr nlopt_algorithm GetNLoptAlg_Local = GetNLoptAlg_<remove_cvref_t<M>>::Local;
template<class M> constexpr bool IsAUGLAG = GetNLoptAlg_<remove_cvref_t<M>>::IsAUGLAG;
template<class M> struct GetNLoptAlg_<NLoptAUGLAG<M>> {
static constexpr nlopt_algorithm Local = GetNLoptAlg_Local<M>;
static constexpr nlopt_algorithm Global = GetNLoptAlg_Global<M>;
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<class...A> explicit NLopt(A&&...a)
template<class...A> explicit NLoptRAII(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;
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 Method> class NLoptOpt {};
// Map a generic function to each argument following the mapping function
template<class Fn, class...Args>
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>(args)),...);
// Optimizers based on NLopt.
template<nlopt_algorithm alg> class NLoptOpt<NLoptAlg<alg>> {
protected:
return fn;
}
// Call fn on each element of the input tuple tup.
template<class Fn, class Tup>
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<T_i> where T_i is the type of
// i-th element of tup.
template<template<class> class W, class...Args>
auto wrap_tup(const std::tuple<Args...> &tup)
{
return std::tuple<W<Args>...>(tup);
}
template<class M, class = NLoptOnly<M>>
class NLoptOpt {
StopCriteria m_stopcr;
OptDir m_dir;
StopCriteria m_loc_stopcr;
OptDir m_dir = OptDir::MIN;
template<class Fn> using TOptData =
std::tuple<std::remove_reference_t<Fn>*, NLoptOpt*, nlopt_opt>;
static constexpr double ConstraintEps = 1e-6;
template<class Fn> 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<class Fn, size_t N>
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<TOptData<Fn>*>(data);
auto tdata = static_cast<OptData<Fn>*>(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<N>(params);
double scoreval = 0.;
using RetT = decltype((*fnptr)(funval));
using RetT = decltype(tdata->fn(funval));
if constexpr (std::is_convertible_v<RetT, ScoreGradient<N>>) {
ScoreGradient<N> score = (*fnptr)(funval);
ScoreGradient<N> 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<class Fn, size_t N>
static double constrain_func(unsigned n, const double *params,
double *gradient, void *data)
{
assert(n == N);
auto tdata = static_cast<OptData<Fn>*>(data);
auto funval = to_arr<N>(params);
return tdata->fn(funval);
}
template<size_t N>
void set_up(NLopt &nl, const Bounds<N>& bounds)
static void set_up(NLoptRAII &nl,
const Bounds<N> &bounds,
const StopCriteria &stopcr)
{
std::array<double, N> 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<class Fn, size_t N>
Result<N> optimize(NLopt &nl, Fn &&fn, const Input<N> &initvals)
template<class Fn, size_t N, class...EqFns, class...IneqFns>
Result<N> optimize(NLoptRAII &nl, Fn &&fn, const Input<N> &initvals,
const std::tuple<EqFns...> &equalities,
const std::tuple<IneqFns...> &inequalities)
{
Result<N> r;
TOptData<Fn> data = std::make_tuple(&fn, this, nl.ptr);
OptData<Fn> 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<F, N>, &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<F, N>, &arg, ConstraintEps);
};
auto eq_data = wrap_tup<OptData>(equalities);
for_each_in_tuple(do_for_each_eq, eq_data);
auto ineq_data = wrap_tup<OptData>(inequalities);
for_each_in_tuple(do_for_each_ineq, ineq_data);
switch(m_dir) {
case OptDir::MIN:
@ -147,51 +263,81 @@ protected:
public:
template<class Func, size_t N>
Result<N> optimize(Func&& func,
template<class Fn, size_t N, class...EqFns, class...IneqFns>
Result<N> optimize(Fn&& f,
const Input<N> &initvals,
const Bounds<N>& bounds)
const Bounds<N>& bounds,
const std::tuple<EqFns...> &equalities,
const std::tuple<IneqFns...> &inequalities)
{
NLopt nl{alg, N};
set_up(nl, bounds);
if constexpr (IsAUGLAG<M>) {
NLoptRAII nl_wrap{NLOPT_AUGLAG, N};
set_up(nl_wrap, bounds, get_criteria());
return optimize(nl, std::forward<Func>(func), initvals);
NLoptRAII nl_glob{GetNLoptAlg_Global<M>, N};
set_up(nl_glob, bounds, get_criteria());
nlopt_set_local_optimizer(nl_wrap.ptr, nl_glob.ptr);
if constexpr (GetNLoptAlg_Local<M> < NLOPT_NUM_ALGORITHMS) {
NLoptRAII nl_loc{GetNLoptAlg_Local<M>, 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<Fn>(f), initvals,
equalities, inequalities);
} else {
return optimize(nl_wrap, std::forward<Fn>(f), initvals,
equalities, inequalities);
}
} else {
NLoptRAII nl_glob{GetNLoptAlg_Global<M>, N};
set_up(nl_glob, bounds, get_criteria());
if constexpr (GetNLoptAlg_Local<M> < NLOPT_NUM_ALGORITHMS) {
NLoptRAII nl_loc{GetNLoptAlg_Local<M>, 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<Fn>(f), initvals,
equalities, inequalities);
} else {
return optimize(nl_glob, std::forward<Fn>(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<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} {}
template<class Alg> struct AlgFeatures_ {
static constexpr bool SupportsInequalities = false;
static constexpr bool SupportsEqualities = false;
};
} // namespace detail;
template<class Alg> constexpr bool SupportsEqualities =
detail::AlgFeatures_<remove_cvref_t<Alg>>::SupportsEqualities;
template<class Alg> constexpr bool SupportsInequalities =
detail::AlgFeatures_<remove_cvref_t<Alg>>::SupportsInequalities;
// Optimizers based on NLopt.
template<class M> class Optimizer<M, detail::NLoptOnly<M>> {
detail::NLoptOpt<M> 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<class Func, size_t N>
template<class Func, size_t N, class...EqFns, class...IneqFns>
Result<N> optimize(Func&& func,
const Input<N> &initvals,
const Bounds<N>& bounds)
const Bounds<N>& bounds,
const std::tuple<EqFns...> &eq_constraints = {},
const std::tuple<IneqFns...> &ineq_constraint = {})
{
return m_opt.optimize(std::forward<Func>(func), initvals, bounds);
static_assert(std::tuple_size_v<std::tuple<EqFns...>> == 0
|| SupportsEqualities<M>,
"Equality constraints are not supported.");
static_assert(std::tuple_size_v<std::tuple<IneqFns...>> == 0
|| SupportsInequalities<M>,
"Inequality constraints are not supported.");
return m_opt.optimize(std::forward<Func>(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<NLOPT_GN_ESCH>;
using AlgNLoptSubplex = detail::NLoptAlg<NLOPT_LN_SBPLX>;
using AlgNLoptSimplex = detail::NLoptAlg<NLOPT_LN_NELDERMEAD>;
using AlgNLoptDIRECT = detail::NLoptAlg<NLOPT_GN_DIRECT>;
using AlgNLoptMLSL = detail::NLoptAlg<NLOPT_GN_MLSL>;
using AlgNLoptGenetic = detail::NLoptAlgComb<NLOPT_GN_ESCH>;
using AlgNLoptSubplex = detail::NLoptAlg<NLOPT_LN_SBPLX>;
using AlgNLoptSimplex = detail::NLoptAlg<NLOPT_LN_NELDERMEAD>;
using AlgNLoptCobyla = detail::NLoptAlg<NLOPT_LN_COBYLA>;
using AlgNLoptDIRECT = detail::NLoptAlg<NLOPT_GN_DIRECT>;
using AlgNLoptORIG_DIRECT = detail::NLoptAlg<NLOPT_GN_ORIG_DIRECT>;
using AlgNLoptISRES = detail::NLoptAlg<NLOPT_GN_ISRES>;
using AlgNLoptAGS = detail::NLoptAlg<NLOPT_GN_AGS>;
using AlgNLoptMLSL_Subplx = detail::NLoptAlgComb<NLOPT_GN_MLSL_LDS, NLOPT_LN_SBPLX>;
using AlgNLoptMLSL_Cobyla = detail::NLoptAlgComb<NLOPT_GN_MLSL, NLOPT_LN_COBYLA>;
using AlgNLoptGenetic_Subplx = detail::NLoptAlgComb<NLOPT_GN_ESCH, NLOPT_LN_SBPLX>;
// To craft auglag algorithms (constraint support through object function transformation)
using detail::NLoptAUGLAG;
namespace detail {
template<> struct AlgFeatures_<AlgNLoptCobyla> {
static constexpr bool SupportsInequalities = true;
static constexpr bool SupportsEqualities = true;
};
template<> struct AlgFeatures_<AlgNLoptISRES> {
static constexpr bool SupportsInequalities = true;
static constexpr bool SupportsEqualities = false;
};
template<> struct AlgFeatures_<AlgNLoptORIG_DIRECT> {
static constexpr bool SupportsInequalities = true;
static constexpr bool SupportsEqualities = false;
};
template<> struct AlgFeatures_<AlgNLoptAGS> {
static constexpr bool SupportsInequalities = true;
static constexpr bool SupportsEqualities = true;
};
template<class M> struct AlgFeatures_<NLoptAUGLAG<M>> {
static constexpr bool SupportsInequalities = true;
static constexpr bool SupportsEqualities = true;
};
} // namespace detail
}} // namespace Slic3r::opt

View File

@ -12,6 +12,15 @@
namespace Slic3r { namespace opt {
template<class T, class O = T>
using FloatingOnly = std::enable_if_t<std::is_floating_point<T>::value, O>;
template<class T, class = FloatingOnly<T>>
constexpr T NaN = std::numeric_limits<T>::quiet_NaN();
constexpr float NaNf = NaN<float>;
constexpr double NaNd = NaN<double>;
// A type to hold the complete result of the optimization.
template<size_t N> 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<N>)
// 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<class Func, size_t N>
template<class Func, size_t N, class...EqConstraints, class...IneqConstraints>
Result<N> optimize(Func&& /*func*/,
const Input<N> &/*initvals*/,
const Bounds<N>& /*bounds*/) { return {}; }
const Bounds<N>& /*bounds*/,
const std::tuple<EqConstraints...> &eq_constraints = {},
const std::tuple<IneqConstraints...> &ineq_constraint = {}
) { return {}; }
// optional for randomized methods:
void seed(long /*s*/) {}

View File

@ -146,6 +146,11 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem
res.changelog_url = changelog_url->second.data();
}
const auto templates_profile = vendor_section.find("templates_profile");
if (templates_profile != vendor_section.not_found()) {
res.templates_profile = templates_profile->second.data() == "1";
}
if (! load_all) {
return res;
}
@ -200,6 +205,10 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem
}
model.bed_model = section.second.get<std::string>("bed_model", "");
model.bed_texture = section.second.get<std::string>("bed_texture", "");
model.thumbnail = section.second.get<std::string>("thumbnail", "");
if (model.thumbnail.empty())
model.thumbnail = model.id + "_thumbnail.png";
if (! model.id.empty() && ! model.variants.empty())
res.models.push_back(std::move(model));
}
@ -336,7 +345,8 @@ std::string Preset::label() const
bool is_compatible_with_print(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer)
{
if (preset.vendor != nullptr && preset.vendor != active_printer.vendor)
// templates_profile vendor profiles should be decided as same vendor profiles
if (preset.vendor != nullptr && preset.vendor != active_printer.vendor && !preset.vendor->templates_profile)
// The current profile has a vendor assigned and it is different from the active print's vendor.
return false;
auto &condition = preset.preset.compatible_prints_condition();
@ -358,7 +368,8 @@ bool is_compatible_with_print(const PresetWithVendorProfile &preset, const Prese
bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer, const DynamicPrintConfig *extra_config)
{
if (preset.vendor != nullptr && preset.vendor != active_printer.vendor)
// templates_profile vendor profiles should be decided as same vendor profiles
if (preset.vendor != nullptr && preset.vendor != active_printer.vendor && !preset.vendor->templates_profile)
// The current profile has a vendor assigned and it is different from the active print's vendor.
return false;
auto &condition = preset.preset.compatible_printers_condition();
@ -495,12 +506,14 @@ static std::vector<std::string> 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_enforcers_only",
@ -512,6 +525,25 @@ static std::vector<std::string> 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",
@ -1165,6 +1197,7 @@ size_t PresetCollection::update_compatible_internal(const PresetWithVendorProfil
if (opt)
config.set_key_value("num_extruders", new ConfigOptionInt((int)static_cast<const ConfigOptionFloats*>(opt)->values.size()));
bool some_compatible = false;
std::vector<size_t> indices_of_template_presets;
for (size_t idx_preset = m_num_default_presets; idx_preset < m_presets.size(); ++ idx_preset) {
bool selected = idx_preset == m_idx_selected;
Preset &preset_selected = m_presets[idx_preset];
@ -1181,7 +1214,29 @@ size_t PresetCollection::update_compatible_internal(const PresetWithVendorProfil
m_idx_selected = size_t(-1);
if (selected)
preset_selected.is_compatible = preset_edited.is_compatible;
if (preset_edited.vendor && preset_edited.vendor->templates_profile) {
indices_of_template_presets.push_back(idx_preset);
}
}
// filter out template profiles where profile with same alias and compability exists
if (!indices_of_template_presets.empty()) {
for (size_t idx_preset = m_num_default_presets; idx_preset < m_presets.size(); ++idx_preset) {
if (m_presets[idx_preset].vendor && !m_presets[idx_preset].vendor->templates_profile && m_presets[idx_preset].is_compatible) {
std::string preset_alias = m_presets[idx_preset].alias;
for (size_t idx_of_template_in_presets : indices_of_template_presets) {
if (m_presets[idx_of_template_in_presets].alias == preset_alias) {
// unselect selected template filament if there is non-template alias compatible
if (idx_of_template_in_presets == m_idx_selected && (unselect_if_incompatible == PresetSelectCompatibleType::Always || unselect_if_incompatible == PresetSelectCompatibleType::OnlyIfWasCompatible)) {
m_idx_selected = size_t(-1);
}
m_presets[idx_of_template_in_presets].is_compatible = false;
break;
}
}
}
}
}
// Update visibility of the default profiles here if the defaults are suppressed, the current profile is not compatible and we don't want to select another compatible profile.
if (m_idx_selected >= m_num_default_presets && m_default_suppressed)
for (size_t i = 0; i < m_num_default_presets; ++ i)
@ -2079,6 +2134,25 @@ namespace PresetUtils {
}
return out;
}
bool vendor_profile_has_all_resources(const VendorProfile& vp)
{
namespace fs = boost::filesystem;
std::string vendor_folder = Slic3r::data_dir() + "/vendor/" + vp.id + "/";
std::string rsrc_folder = Slic3r::resources_dir() + "/profiles/" + vp.id + "/";
std::string cache_folder = Slic3r::data_dir() + "/cache/" + vp.id + "/";
for (const VendorProfile::PrinterModel& model : vp.models) {
for (const std::string& res : { model.bed_texture, model.bed_model, model.thumbnail } ) {
if (! res.empty()
&& !fs::exists(fs::path(vendor_folder + res))
&& !fs::exists(fs::path(rsrc_folder + res))
&& !fs::exists(fs::path(cache_folder + res)))
return false;
}
}
return true;
}
} // namespace PresetUtils
} // namespace Slic3r

View File

@ -34,6 +34,7 @@ public:
Semver config_version;
std::string config_update_url;
std::string changelog_url;
bool templates_profile { false };
struct PrinterVariant {
PrinterVariant() {}
@ -52,6 +53,7 @@ public:
// Vendor & Printer Model specific print bed model & texture.
std::string bed_model;
std::string bed_texture;
std::string thumbnail;
PrinterVariant* variant(const std::string &name) {
for (auto &v : this->variants)
@ -619,6 +621,7 @@ namespace PresetUtils {
const VendorProfile::PrinterModel* system_printer_model(const Preset &preset);
std::string system_printer_bed_model(const Preset& preset);
std::string system_printer_bed_texture(const Preset& preset);
bool vendor_profile_has_all_resources(const VendorProfile& vp);
} // namespace PresetUtils

View File

@ -159,6 +159,7 @@ void PresetBundle::setup_directories()
data_dir,
data_dir / "vendor",
data_dir / "cache",
data_dir / "cache" / "vendor",
data_dir / "shapes",
#ifdef SLIC3R_PROFILE_USE_PRESETS_SUBDIR
// Store the print/filament/printer presets into a "presets" directory.
@ -1307,17 +1308,26 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_configbundle(
try {
pt::read_ini(ifs, tree);
} catch (const boost::property_tree::ini_parser::ini_parser_error &err) {
throw Slic3r::RuntimeError(format("Failed loading config bundle \"%1%\"\nError: \"%2%\" at line %3%", path, err.message(), err.line()).c_str());
// This throw was uncatched. While other similar problems later are just returning empty pair.
//throw Slic3r::RuntimeError(format("Failed loading config bundle \"%1%\"\nError: \"%2%\" at line %3%", path, err.message(), err.line()).c_str());
BOOST_LOG_TRIVIAL(error) << format("Failed loading config bundle \"%1%\"\nError: \"%2%\" at line %3%", path, err.message(), err.line()).c_str();
return std::make_pair(PresetsConfigSubstitutions{}, 0);
}
}
const VendorProfile *vendor_profile = nullptr;
if (flags.has(LoadConfigBundleAttribute::LoadSystem) || flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) {
auto vp = VendorProfile::from_ini(tree, path);
if (vp.models.size() == 0) {
VendorProfile vp;
try {
vp = VendorProfile::from_ini(tree, path);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: Failed to open profile file.") % path;
return std::make_pair(PresetsConfigSubstitutions{}, 0);
}
if (vp.models.size() == 0 && !vp.templates_profile) {
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer model defined.") % path;
return std::make_pair(PresetsConfigSubstitutions{}, 0);
} else if (vp.num_variants() == 0) {
} else if (vp.num_variants() == 0 && !vp.templates_profile) {
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer variant defined") % path;
return std::make_pair(PresetsConfigSubstitutions{}, 0);
}
@ -1359,6 +1369,9 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_configbundle(
} else if (boost::starts_with(section.first, "filament:")) {
presets = &this->filaments;
preset_name = section.first.substr(9);
if (vendor_profile && vendor_profile->templates_profile) {
preset_name += " @Template";
}
} else if (boost::starts_with(section.first, "sla_print:")) {
presets = &this->sla_prints;
preset_name = section.first.substr(10);

View File

@ -3,53 +3,97 @@
namespace Slic3r {
// returns two eigenvectors of the area covered by given polygons. The vectors are sorted by their corresponding eigenvalue, largest first
std::tuple<Vec2d, Vec2d> compute_principal_components(const Polygons &polys, const double unscaled_resolution)
// returns triangle area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance
// none of the values is divided/normalized by area.
// The function computes intgeral over the area of the triangle, with function f(x,y) = x for first moments of area (y is analogous)
// f(x,y) = x^2 for second moment of area
// and f(x,y) = x*y for second moment of area covariance
std::tuple<float, Vec2f, Vec2f, float> compute_moments_of_area_of_triangle(const Vec2f &a, const Vec2f &b, const Vec2f &c)
{
// USING UNSCALED VALUES
const Vec2d pixel_size = Vec2d(unscaled_resolution, unscaled_resolution);
const auto bb = get_extents(polys);
const Vec2i pixel_count = unscaled(bb.size()).cwiseQuotient(pixel_size).cast<int>() + Vec2i::Ones();
// based on the following guide:
// Denote the vertices of S by a, b, c. Then the map
// g:(u,v)↦a+u(ba)+v(ca) ,
// which in coordinates appears as
// g:(u,v)↦{x(u,v)y(u,v)=a1+u(b1a1)+v(c1a1)=a2+u(b2a2)+v(c2a2) ,(1)
// obviously maps S bijectively onto S. Therefore the transformation formula for multiple integrals steps into action, and we obtain
// ∫Sf(x,y)d(x,y)=∫Sf(x(u,v),y(u,v))Jg(u,v) d(u,v) .
// In the case at hand the Jacobian determinant is a constant: From (1) we obtain
// Jg(u,v)=det[xuyuxvyv]=(b1a1)(c2a2)(c1a1)(b2a2) .
// Therefore we can write
// ∫Sf(x,y)d(x,y)=Jg∫10∫1u0f~(u,v) dv du ,
// where f~ denotes the pullback of f to S:
// f~(u,v):=f(x(u,v),y(u,v)) .
// Don't forget taking the absolute value of Jg!
std::vector<Linef> lines{};
for (Line l : to_lines(polys)) { lines.emplace_back(unscaled(l.a), unscaled(l.b)); }
AABBTreeIndirect::Tree<2, double> tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines);
auto is_inside = [&](const Vec2d &point) {
size_t nearest_line_index_out = 0;
Vec2d nearest_point_out = Vec2d::Zero();
auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, nearest_line_index_out, nearest_point_out);
if (distance < 0) return false;
const Linef &line = lines[nearest_line_index_out];
Vec2d v1 = line.b - line.a;
Vec2d v2 = point - line.a;
if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { return true; }
return false;
};
float jacobian_determinant_abs = std::abs((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y()));
double pixel_area = pixel_size.x() * pixel_size.y();
Vec2d centroid_accumulator = Vec2d::Zero();
Vec2d second_moment_of_area_accumulator = Vec2d::Zero();
double second_moment_of_area_covariance_accumulator = 0.0;
double area = 0.0;
// coordinate transform: gx(u,v) = a.x + u * (b.x - a.x) + v * (c.x - a.x)
// coordinate transform: gy(u,v) = a.y + u * (b.y - a.y) + v * (c.y - a.y)
// second moment of area for x: f(x, y) = x^2;
// f(gx(u,v), gy(u,v)) = gx(u,v)^2 = ... (long expanded form)
for (int x = 0; x < pixel_count.x(); x++) {
for (int y = 0; y < pixel_count.y(); y++) {
Vec2d position = unscaled(bb.min) + pixel_size.cwiseProduct(Vec2d{x, y});
if (is_inside(position)) {
area += pixel_area;
centroid_accumulator += pixel_area * position;
second_moment_of_area_accumulator += pixel_area * position.cwiseProduct(position);
second_moment_of_area_covariance_accumulator += pixel_area * position.x() * position.y();
}
// result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du
// integral_0^1 integral_0^(1 - u) (a + u (b - a) + v (c - a))^2 dv du = 1/12 (a^2 + a (b + c) + b^2 + b c + c^2)
Vec2f second_moment_of_area_xy = jacobian_determinant_abs *
(a.cwiseProduct(a) + b.cwiseProduct(b) + b.cwiseProduct(c) + c.cwiseProduct(c) +
a.cwiseProduct(b + c)) /
12.0f;
// second moment of area covariance : f(x, y) = x*y;
// f(gx(u,v), gy(u,v)) = gx(u,v)*gy(u,v) = ... (long expanded form)
//(a_1 + u * (b_1 - a_1) + v * (c_1 - a_1)) * (a_2 + u * (b_2 - a_2) + v * (c_2 - a_2))
// == (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2))
// intermediate result: integral_0^(1 - u) (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2)) dv =
// 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u - 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2
// b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) result = integral_0^1 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u -
// 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2 b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) du =
// 1/24 (a_2 (b_1 + c_1) + a_1 (2 a_2 + b_2 + c_2) + b_2 c_1 + b_1 c_2 + 2 b_1 b_2 + 2 c_1 c_2)
// result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du
float second_moment_of_area_covariance = jacobian_determinant_abs * (1.0f / 24.0f) *
(a.y() * (b.x() + c.x()) + a.x() * (2.0f * a.y() + b.y() + c.y()) + b.y() * c.x() +
b.x() * c.y() + 2.0f * b.x() * b.y() + 2.0f * c.x() * c.y());
float area = jacobian_determinant_abs * 0.5f;
Vec2f first_moment_of_area_xy = jacobian_determinant_abs * (a + b + c) / 6.0f;
return {area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance};
};
// returns two eigenvectors of the area covered by given polygons. The vectors are sorted by their corresponding eigenvalue, largest first
std::tuple<Vec2f, Vec2f> compute_principal_components(const Polygons &polys)
{
Vec2f centroid_accumulator = Vec2f::Zero();
Vec2f second_moment_of_area_accumulator = Vec2f::Zero();
float second_moment_of_area_covariance_accumulator = 0.0f;
float area = 0.0f;
for (const Polygon &poly : polys) {
Vec2f p0 = unscaled(poly.first_point()).cast<float>();
for (size_t i = 2; i < poly.points.size(); i++) {
Vec2f p1 = unscaled(poly.points[i - 1]).cast<float>();
Vec2f p2 = unscaled(poly.points[i]).cast<float>();
float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f;
auto [triangle_area, first_moment_of_area, second_moment_area,
second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2);
area += sign * triangle_area;
centroid_accumulator += sign * first_moment_of_area;
second_moment_of_area_accumulator += sign * second_moment_area;
second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance;
}
}
if (area <= 0.0) {
return {Vec2d::Zero(), Vec2d::Zero()};
return {Vec2f::Zero(), Vec2f::Zero()};
}
Vec2d centroid = centroid_accumulator / area;
Vec2d variance = second_moment_of_area_accumulator / area - centroid.cwiseProduct(centroid);
Vec2f centroid = centroid_accumulator / area;
Vec2f variance = second_moment_of_area_accumulator / area - centroid.cwiseProduct(centroid);
double covariance = second_moment_of_area_covariance_accumulator / area - centroid.x() * centroid.y();
#if 0
std::cout << "area : " << area << std::endl;
@ -58,7 +102,7 @@ std::tuple<Vec2d, Vec2d> compute_principal_components(const Polygons &polys, con
std::cout << "covariance : " << covariance << std::endl;
#endif
if (abs(covariance) < EPSILON) {
std::tuple<Vec2d, Vec2d> result{Vec2d{variance.x(), 0.0}, Vec2d{0.0, variance.y()}};
std::tuple<Vec2f, Vec2f> result{Vec2f{variance.x(), 0.0}, Vec2f{0.0, variance.y()}};
if (variance.y() > variance.x()) {
return {std::get<1>(result), std::get<0>(result)};
} else
@ -72,12 +116,12 @@ std::tuple<Vec2d, Vec2d> compute_principal_components(const Polygons &polys, con
// Eigenvalues are solutions to det(C - lI) = 0, where l is the eigenvalue and I unit matrix
// Eigenvector for eigenvalue l is any vector v such that Cv = lv
double eigenvalue_a = 0.5 * (variance.x() + variance.y() +
sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4 * covariance * covariance));
double eigenvalue_b = 0.5 * (variance.x() + variance.y() -
sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4 * covariance * covariance));
Vec2d eigenvector_a{(eigenvalue_a - variance.y()) / covariance, 1.0};
Vec2d eigenvector_b{(eigenvalue_b - variance.y()) / covariance, 1.0};
float eigenvalue_a = 0.5f * (variance.x() + variance.y() +
sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4.0f * covariance * covariance));
float eigenvalue_b = 0.5f * (variance.x() + variance.y() -
sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4.0f * covariance * covariance));
Vec2f eigenvector_a{(eigenvalue_a - variance.y()) / covariance, 1.0f};
Vec2f eigenvector_b{(eigenvalue_b - variance.y()) / covariance, 1.0f};
#if 0
std::cout << "eigenvalue_a: " << eigenvalue_a << std::endl;

View File

@ -9,8 +9,15 @@
namespace Slic3r {
// returns triangle area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance
// none of the values is divided/normalized by area.
// The function computes intgeral over the area of the triangle, with function f(x,y) = x for first moments of area (y is analogous)
// f(x,y) = x^2 for second moment of area
// and f(x,y) = x*y for second moment of area covariance
std::tuple<float, Vec2f, Vec2f, float> compute_moments_of_area_of_triangle(const Vec2f &a, const Vec2f &b, const Vec2f &c);
// returns two eigenvectors of the area covered by given polygons. The vectors are sorted by their corresponding eigenvalue, largest first
std::tuple<Vec2d, Vec2d> compute_principal_components(const Polygons &polys, const double unscaled_resolution);
std::tuple<Vec2f, Vec2f> compute_principal_components(const Polygons &polys);
}

View File

@ -1201,7 +1201,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
}
// Invalidate just the supports step.
for (const PrintObjectStatus &print_object_status : print_objects_range) {
update_apply_status(print_object_status.print_object->invalidate_step(posSupportSpotsSearch));
update_apply_status(print_object_status.print_object->invalidate_step(posSupportMaterial));
}
if (supports_differ) {

View File

@ -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);
@ -534,7 +535,7 @@ void PrintConfigDef::init_fff_params()
def->category = L("Speed");
def->tooltip = L("This setting enables dynamic speed control on overhangs.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
def->set_default_value(new ConfigOptionBool(false));
def = this->add("overhang_overlap_levels", coPercents);
def->full_label = L("Overhang overlap levels");
@ -841,7 +842,8 @@ void PrintConfigDef::init_fff_params()
def = this->add("extra_perimeters_on_overhangs", coBool);
def->label = L("Extra perimeters on overhangs (Experimental)");
def->category = L("Layers and Perimeters");
def->tooltip = L("Create additional perimeter paths over steep overhangs and areas where bridges cannot be anchored.");
def->tooltip = L("Detect overhang areas where bridges cannot be anchored, and fill them with "
"extra perimeter paths. These paths are anchored to the nearby non-overhang area when possible.");
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(false));
@ -3271,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<SLAPillarConnectionMode>::get_enum_values();
def->enum_keys_map = &ConfigOptionEnum<SLAPillarConnectionMode>::get_enum_values();
def->enum_values = ConfigOptionEnum<SLAPillarConnectionMode>::get_enum_names();
def->enum_labels = ConfigOptionEnum<SLAPillarConnectionMode>::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;
@ -3613,90 +3828,12 @@ void PrintConfigDef::init_sla_params()
def->enum_labels = ConfigOptionEnum<sla::SupportTreeType>::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<SLAPillarConnectionMode>::get_enum_values();
def->enum_keys_map = &ConfigOptionEnum<SLAPillarConnectionMode>::get_enum_values();
def->enum_values = ConfigOptionEnum<SLAPillarConnectionMode>::get_enum_names();
def->enum_labels = ConfigOptionEnum<SLAPillarConnectionMode>::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));
init_sla_support_params("");
init_sla_support_params("branching");
def = this->add("support_enforcers_only", coBool);
def->label = L("Support only in enforced regions");
@ -3705,93 +3842,6 @@ void PrintConfigDef::init_sla_params()
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));
def = this->add("support_points_density_relative", coInt);
def->label = L("Support points density");
def->category = L("Supports");
@ -4759,6 +4809,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<ConfigOptionEnum<sla::SupportTreeType>>("support_tree_type");
std::string slatree = "";
if (suptreetype) {
auto ttype = static_cast<sla::SupportTreeType>(suptreetype->getInt());
switch (ttype) {
case sla::SupportTreeType::Branching: slatree = "branching"; break;
case sla::SupportTreeType::Organic: slatree = "organic"; break;
default:
;
}
}
return slatree;
}
} // namespace Slic3r
#include <cereal/types/polymorphic.hpp>

View File

@ -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<std::string> m_extruder_option_keys;
std::vector<std::string> 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))
// Generate only ground facing supports
((ConfigOptionBool, support_enforcers_only))
@ -892,6 +895,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<SLAPillarConnectionMode>, 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))
@ -1111,6 +1170,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

View File

@ -789,10 +789,10 @@ bool PrintObject::invalidate_step(PrintObjectStep step)
// propagate to dependent steps
if (step == posPerimeters) {
invalidated |= this->invalidate_steps({ posPrepareInfill, posInfill, posIroning, posEstimateCurledExtrusions });
invalidated |= this->invalidate_steps({ posPrepareInfill, posInfill, posIroning, posSupportSpotsSearch, posEstimateCurledExtrusions });
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
} else if (step == posPrepareInfill) {
invalidated |= this->invalidate_steps({ posInfill, posIroning });
invalidated |= this->invalidate_steps({ posInfill, posIroning, posSupportSpotsSearch });
} else if (step == posInfill) {
invalidated |= this->invalidate_steps({ posIroning, posSupportSpotsSearch });
invalidated |= m_print->invalidate_steps({ psSkirtBrim });

View File

@ -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<branchingtree::Node> m_pillars; // to put an index over them
// cache succesfull ground connections
mutable std::map<int, GroundConnection> m_gnd_connections;
mutable execution::SpinningMutex<ExecutionTBB> 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<size_t> 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<Vec3f> 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<size_t>& unroutable_pinheads() const
@ -113,15 +170,28 @@ public:
}
bool is_valid() const override { return !m_builder.ctl().stopcondition(); }
const std::vector<branchingtree::Node> & 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<double>(), tod = to.pos.cast<double>();
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<double>(),
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<double>(), get_radius(from)};
Vec3d init_dir = (to.pos - from.pos).cast<double>().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<double>(), get_radius(from)};
auto anchor = m_sm.cfg.ground_facing_only ?
std::optional<Anchor>{} : // 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<double>());
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<Vec3f> get_avoidance(const GroundConnection &conn,
float maxdist)
{
std::optional<Vec3f> ret;
if (conn) {
if (conn.path.size() > 1) {
ret = conn.path[1].pos.cast<float>();
} else {
Vec3f pbeg = conn.path[0].pos.cast<float>();
Vec3f pend = conn.pillar_base->pos.cast<float>();
pbeg.z() = std::max(pbeg.z() - maxdist, pend.z());
ret = pbeg;
}
}
return ret;
}
std::optional<Vec3f> BranchingTreeBuilder::suggest_avoidance(
const branchingtree::Node &from, float max_bridge_len) const
{
std::optional<Vec3f> 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<double>(), 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<float>(), 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();

View File

@ -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<unsigned> &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<unsigned long> 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<bool> canplace(needpillars, false);
std::vector<Vec3d> 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;
}

View File

@ -1,7 +1,7 @@
#ifndef LEGACYSUPPORTTREE_HPP
#define LEGACYSUPPORTTREE_HPP
#include "SupportTreeUtils.hpp"
#include "SupportTreeUtilsLegacy.hpp"
#include <libslic3r/SLA/SpatIndex.hpp>
#include <libslic3r/Execution/ExecutionTBB.hpp>
@ -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<unsigned, AABBMesh::hit_result> 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)

View File

@ -18,6 +18,8 @@
#include <boost/log/trivial.hpp>
#include <libslic3r/I18N.hpp>
#include <libnest2d/tools/benchmark.h>
//! 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<SupportTreeBuilder>(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.
}

View File

@ -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;
};
@ -116,11 +126,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)

View File

@ -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)

View File

@ -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;

View File

@ -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

View File

@ -6,10 +6,13 @@
#include <libslic3r/Execution/Execution.hpp>
#include <libslic3r/Optimize/NLoptOptimizer.hpp>
#include <libslic3r/Optimize/BruteforceOptimizer.hpp>
#include <libslic3r/MeshNormals.hpp>
#include <libslic3r/Geometry.hpp>
#include <libslic3r/SLA/SupportTreeBuilder.hpp>
#include <boost/variant.hpp>
#include <boost/container/small_vector.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r { namespace sla {
@ -23,36 +26,12 @@ 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<class I, class DoubleI = IntegerOnly<I>>
IntegerOnly<DoubleI> pairhash(I a, I b)
{
using std::ceil; using std::log2; using std::max; using std::min;
static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT);
static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT);
static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits;
I g = min(a, b), l = max(a, b);
// Assume the hash will fit into the output variable
assert((g ? (ceil(log2(g))) : 0) <= shift);
assert((l ? (ceil(log2(l))) : 0) <= shift);
return (DoubleI(g) << shift) + l;
}
// 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
template<size_t N>
class PointRing {
std::array<double, N> m_phis;
std::array<double, N - 1> m_phis;
// Two vectors that will be perpendicular to each other and to the
// axis. Values for a(X) and a(Y) are now arbitrary, a(Z) is just a
@ -72,7 +51,7 @@ class PointRing {
public:
PointRing(const Vec3d &n) : m_phis{linspace_array<N>(0., 2 * PI)}
PointRing(const Vec3d &n) : m_phis{linspace_array<N - 1>(0., 2 * PI)}
{
// We have to address the case when the direction vector v (same as
// dir) is coincident with one of the world axes. In this case two of
@ -91,7 +70,10 @@ public:
Vec3d get(size_t idx, const Vec3d &src, double r) const
{
double phi = m_phis[idx];
if (idx == 0)
return src;
double phi = m_phis[idx - 1];
double sinphi = std::sin(phi);
double cosphi = std::cos(phi);
@ -131,31 +113,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<size_t Samples = 8>
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<class Ex>
Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd)
using Beam = Beam_<>;
template<class Ex, size_t RayCount = Beam::SAMPLES>
Hit beam_mesh_hit(Ex policy,
const AABBMesh &mesh,
const Beam_<RayCount> &beam,
double sd)
{
Vec3d src = beam.src;
Vec3d dst = src + beam.dir;
@ -164,19 +155,19 @@ Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd)
Vec3d D = (dst - src);
Vec3d dir = D.normalized();
PointRing<Beam::SAMPLES> ring{dir};
PointRing<RayCount> ring{dir};
using Hit = AABBMesh::hit_result;
// Hit results
std::array<Hit, Beam::SAMPLES> hits;
std::array<Hit, RayCount> 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 +184,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());
}
@ -208,7 +199,10 @@ Hit pinhead_mesh_hit(Ex ex,
double width,
double sd)
{
static const size_t SAMPLES = 8;
// Support tree generation speed depends heavily on this value. 8 is almost
// ok, but to prevent rare cases of collision, 16 is necessary, which makes
// the algorithm run about 60% longer.
static const size_t SAMPLES = 16;
// Move away slightly from the touching point to avoid raycasting on the
// inner surface of the mesh.
@ -283,199 +277,12 @@ Hit pinhead_mesh_hit(Ex ex,
template<class Ex>
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<class Ex>
std::optional<DiffBridge> 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<AlgNLoptSubplex> 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<class Ex>
std::pair<bool, long> 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<DiffBridge> 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 +337,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 +352,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<AlgNLoptGenetic> solver(get_criteria(m.cfg).stop_score(w).max_iterations(100));
Optimizer<opt::AlgNLoptMLSL_Subplx> 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 +432,7 @@ std::optional<Head> 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 +440,335 @@ std::optional<Head> calculate_pinhead_placement(Ex policy,
return {};
}
template<class Ex>
std::pair<bool, long> 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<Junction, MaxExpectedJunctions> path;
std::optional<Pedestal> 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<class Fn>
constexpr bool IsWideningFn = std::is_invocable_r_v</*retval*/ double,
Fn,
Ball /*source*/,
Vec3d /*dir*/,
double /*length*/>;
// A widening function can determine how many ray samples should a beam contain
// (see in beam_mesh_hit)
template<class WFn> struct BeamSamples { static constexpr size_t Value = 8; };
template<class WFn> constexpr size_t BeamSamplesV = BeamSamples<remove_cvref_t<WFn>>::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<class Ex, class WideningFn,
class = std::enable_if_t<IsWideningFn<WideningFn>> >
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<WideningFn>;
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_<Samples> 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_<Samples> 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<class Ex>
std::pair<bool, long> 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<class Ex, class WideningFn,
class = std::enable_if_t<IsWideningFn<WideningFn>> >
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<AlgNLoptGenetic> 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<opt::AlgNLoptMLSL_Subplx> 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<double>::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<class Ex>
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<decltype(wfn)>, "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>,
"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<DefaultWideningModel> {
static constexpr size_t Value = 16;
};
template<class Ex>
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<class Ex>
@ -729,8 +788,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<AlgNLoptGenetic> solver(get_criteria(sm.cfg)
.stop_score(anchor.fullwidth())
@ -796,6 +854,92 @@ std::optional<Anchor> 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>();
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<Vec3f> 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<float, 2, 3> 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

View File

@ -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<class I, class DoubleI = IntegerOnly<I>>
IntegerOnly<DoubleI> pairhash(I a, I b)
{
using std::ceil; using std::log2; using std::max; using std::min;
static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT);
static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT);
static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits;
I g = min(a, b), l = max(a, b);
// Assume the hash will fit into the output variable
assert((g ? (ceil(log2(g))) : 0) <= shift);
assert((l ? (ceil(log2(l))) : 0) <= shift);
return (DoubleI(g) << shift) + l;
}
template<class Ex>
std::optional<DiffBridge> 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<AlgNLoptSubplex> 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<class Ex>
std::pair<bool, long> 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<DiffBridge> 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<class Ex>
std::pair<bool, long> 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<class Ex>
std::pair<bool, long> 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<AlgNLoptGenetic> 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

View File

@ -40,28 +40,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;
}
@ -822,6 +857,7 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector<t_conf
|| opt_key == "supports_enable"
|| opt_key == "support_tree_type"
|| opt_key == "support_object_elevation"
|| opt_key == "branchingsupport_object_elevation"
|| opt_key == "pad_around_object"
|| opt_key == "pad_around_object_everywhere"
|| opt_key == "slice_closing_radius"
@ -839,6 +875,7 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector<t_conf
|| opt_key == "support_pillar_diameter"
|| opt_key == "support_pillar_widening_factor"
|| opt_key == "support_small_pillar_diameter_percent"
|| opt_key == "support_max_weight_on_model"
|| opt_key == "support_max_bridges_on_pillar"
|| opt_key == "support_pillar_connection_mode"
|| opt_key == "support_buildplate_only"
@ -848,6 +885,24 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector<t_conf
|| opt_key == "support_max_bridge_length"
|| opt_key == "support_max_pillar_link_distance"
|| opt_key == "support_base_safety_distance"
|| opt_key == "branchingsupport_head_front_diameter"
|| opt_key == "branchingsupport_head_penetration"
|| opt_key == "branchingsupport_head_width"
|| opt_key == "branchingsupport_pillar_diameter"
|| opt_key == "branchingsupport_pillar_widening_factor"
|| opt_key == "branchingsupport_small_pillar_diameter_percent"
|| opt_key == "branchingsupport_max_weight_on_model"
|| opt_key == "branchingsupport_max_bridges_on_pillar"
|| opt_key == "branchingsupport_pillar_connection_mode"
|| opt_key == "branchingsupport_buildplate_only"
|| opt_key == "branchingsupport_base_diameter"
|| opt_key == "branchingsupport_base_height"
|| opt_key == "branchingsupport_critical_angle"
|| opt_key == "branchingsupport_max_bridge_length"
|| opt_key == "branchingsupport_max_pillar_link_distance"
|| opt_key == "branchingsupport_base_safety_distance"
|| opt_key == "pad_object_gap"
) {
steps.emplace_back(slaposSupportTree);

View File

@ -1,5 +1,6 @@
#include "SupportSpotsGenerator.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "ExtrusionEntity.hpp"
#include "ExtrusionEntityCollection.hpp"
@ -7,6 +8,7 @@
#include "Line.hpp"
#include "Point.hpp"
#include "Polygon.hpp"
#include "PrincipalComponents2D.hpp"
#include "Print.hpp"
#include "PrintBase.hpp"
#include "Tesselate.hpp"
@ -117,13 +119,14 @@ public:
size_t to_cell_index(const Vec3i &cell_coords) const
{
#ifdef DETAILED_DEBUG_LOGS
assert(cell_coords.x() >= 0);
assert(cell_coords.x() < cell_count.x());
assert(cell_coords.y() >= 0);
assert(cell_coords.y() < cell_count.y());
assert(cell_coords.z() >= 0);
assert(cell_coords.z() < cell_count.z());
#endif
return cell_coords.z() * cell_count.x() * cell_count.y() + cell_coords.y() * cell_count.x() + cell_coords.x();
}
@ -244,6 +247,7 @@ std::vector<ExtrusionLine> check_extrusion_entity_stability(const ExtrusionEntit
const float flow_width = get_flow_width(layer_region, entity->role());
// Compute only unsigned distance - prev_layer_lines can contain unconnected paths, thus the sign of the distance is unreliable
std::vector<ExtendedPoint> annotated_points = estimate_points_properties<true, true, false, false>(entity->as_polyline().points,
prev_layer_lines, flow_width,
params.bridge_distance);
@ -262,6 +266,7 @@ std::vector<ExtrusionLine> check_extrusion_entity_stability(const ExtrusionEntit
prev_layer_lines.get_line(curr_point.nearest_prev_layer_line) :
ExtrusionLine{};
// correctify the distance sign using slice polygons
float sign = (prev_layer_boundary.distance_from_lines<true>(curr_point.position) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f;
curr_point.distance *= sign;
@ -297,84 +302,39 @@ std::vector<ExtrusionLine> check_extrusion_entity_stability(const ExtrusionEntit
}
}
// returns triangle area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance
// none of the values is divided/normalized by area.
// The function computes intgeral over the area of the triangle, with function f(x,y) = x for first moments of area (y is analogous)
// f(x,y) = x^2 for second moment of area
// and f(x,y) = x*y for second moment of area covariance
std::tuple<float, Vec2f, Vec2f, float> compute_triangle_moments_of_area(const Vec2f &a, const Vec2f &b, const Vec2f &c)
{
// based on the following guide:
// Denote the vertices of S by a, b, c. Then the map
// g:(u,v)↦a+u(ba)+v(ca) ,
// which in coordinates appears as
// g:(u,v)↦{x(u,v)y(u,v)=a1+u(b1a1)+v(c1a1)=a2+u(b2a2)+v(c2a2) ,(1)
// obviously maps S bijectively onto S. Therefore the transformation formula for multiple integrals steps into action, and we obtain
// ∫Sf(x,y)d(x,y)=∫Sf(x(u,v),y(u,v))Jg(u,v) d(u,v) .
// In the case at hand the Jacobian determinant is a constant: From (1) we obtain
// Jg(u,v)=det[xuyuxvyv]=(b1a1)(c2a2)(c1a1)(b2a2) .
// Therefore we can write
// ∫Sf(x,y)d(x,y)=Jg∫10∫1u0f~(u,v) dv du ,
// where f~ denotes the pullback of f to S:
// f~(u,v):=f(x(u,v),y(u,v)) .
// Don't forget taking the absolute value of Jg!
float jacobian_determinant_abs = std::abs((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y()));
// coordinate transform: gx(u,v) = a.x + u * (b.x - a.x) + v * (c.x - a.x)
// coordinate transform: gy(u,v) = a.y + u * (b.y - a.y) + v * (c.y - a.y)
// second moment of area for x: f(x, y) = x^2;
// f(gx(u,v), gy(u,v)) = gx(u,v)^2 = ... (long expanded form)
// result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du
// integral_0^1 integral_0^(1 - u) (a + u (b - a) + v (c - a))^2 dv du = 1/12 (a^2 + a (b + c) + b^2 + b c + c^2)
Vec2f second_moment_of_area_xy = jacobian_determinant_abs *
(a.cwiseProduct(a) + b.cwiseProduct(b) + b.cwiseProduct(c) + c.cwiseProduct(c) +
a.cwiseProduct(b + c)) /
12.0f;
// second moment of area covariance : f(x, y) = x*y;
// f(gx(u,v), gy(u,v)) = gx(u,v)*gy(u,v) = ... (long expanded form)
//(a_1 + u * (b_1 - a_1) + v * (c_1 - a_1)) * (a_2 + u * (b_2 - a_2) + v * (c_2 - a_2))
// == (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2))
// intermediate result: integral_0^(1 - u) (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2)) dv =
// 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u - 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2
// b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) result = integral_0^1 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u -
// 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2 b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) du =
// 1/24 (a_2 (b_1 + c_1) + a_1 (2 a_2 + b_2 + c_2) + b_2 c_1 + b_1 c_2 + 2 b_1 b_2 + 2 c_1 c_2)
// result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du
float second_moment_of_area_covariance = jacobian_determinant_abs * (1.0f / 24.0f) *
(a.y() * (b.x() + c.x()) + a.x() * (2.0f * a.y() + b.y() + c.y()) + b.y() * c.x() +
b.x() * c.y() + 2.0f * b.x() * b.y() + 2.0f * c.x() * c.y());
float area = jacobian_determinant_abs * 0.5f;
Vec2f first_moment_of_area_xy = jacobian_determinant_abs * (a + b + c) / 6.0f;
return {area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance};
};
SliceConnection estimate_slice_connection(size_t slice_idx, const Layer *layer)
{
SliceConnection connection;
const LayerSlice &slice = layer->lslices_ex[slice_idx];
ExPolygon slice_poly = layer->lslices[slice_idx];
Polygons slice_polys = to_polygons(layer->lslices[slice_idx]);
BoundingBox slice_bb = get_extents(slice_polys);
const Layer *lower_layer = layer->lower_layer;
ExPolygons below_polys{};
for (const auto &link : slice.overlaps_below) { below_polys.push_back(lower_layer->lslices[link.slice_idx]); }
ExPolygons overlap = intersection_ex({slice_poly}, below_polys);
ExPolygons below{};
for (const auto &link : slice.overlaps_below) { below.push_back(lower_layer->lslices[link.slice_idx]); }
Polygons below_polys = to_polygons(below);
std::vector<Vec2f> triangles = triangulate_expolygons_2f(overlap);
for (size_t idx = 0; idx < triangles.size(); idx += 3) {
auto [area, first_moment_of_area, second_moment_area,
second_moment_of_area_covariance] = compute_triangle_moments_of_area(triangles[idx], triangles[idx + 1], triangles[idx + 2]);
connection.area += area;
connection.centroid_accumulator += Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), layer->print_z * area);
connection.second_moment_of_area_accumulator += second_moment_area;
connection.second_moment_of_area_covariance_accumulator += second_moment_of_area_covariance;
BoundingBox below_bb = get_extents(below_polys);
Polygons overlap = intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(slice_polys, below_bb),
ClipperUtils::clip_clipper_polygons_with_subject_bbox(below_polys, slice_bb));
for (const Polygon &poly : overlap) {
Vec2f p0 = unscaled(poly.first_point()).cast<float>();
for (size_t i = 2; i < poly.points.size(); i++) {
Vec2f p1 = unscaled(poly.points[i - 1]).cast<float>();
Vec2f p2 = unscaled(poly.points[i]).cast<float>();
float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f;
auto [area, first_moment_of_area, second_moment_area,
second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2);
connection.area += sign * area;
connection.centroid_accumulator += sign * Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), layer->print_z * area);
connection.second_moment_of_area_accumulator += sign * second_moment_area;
connection.second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance;
}
}
return connection;
@ -973,6 +933,9 @@ void estimate_malformations(LayerPtrs &layers, const Params &params)
std::vector<ExtrusionLine> current_layer_lines;
for (const LayerRegion *layer_region : l->regions()) {
for (const ExtrusionEntity *extrusion : layer_region->perimeters().flatten().entities) {
if (!extrusion->role().is_external_perimeter()) continue;
Points extrusion_pts;
extrusion->collect_points(extrusion_pts);
float flow_width = get_flow_width(layer_region, extrusion->role());

View File

@ -18,6 +18,8 @@
#include "PrintConfig.hpp"
#include "Utils.hpp"
#include <string_view>
#include <boost/log/trivial.hpp>
#include <tbb/parallel_for.h>
@ -26,6 +28,8 @@
namespace Slic3r::FFFTreeSupport
{
using namespace std::literals;
// or warning
// had to use a define beacuse the macro processing inside macro BOOST_LOG_TRIVIAL()
#define error_level_not_in_cache error
@ -336,7 +340,7 @@ const Polygons& TreeModelVolumes::getCollision(const coord_t orig_radius, LayerI
return (*result).get();
if (m_precalculated) {
BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate collision at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!";
tree_supports_show_error("Not precalculated Collision requested.", false);
tree_supports_show_error("Not precalculated Collision requested."sv, false);
}
const_cast<TreeModelVolumes*>(this)->calculateCollision(radius, layer_idx);
return getCollision(orig_radius, layer_idx, min_xy_dist);
@ -351,7 +355,7 @@ const Polygons& TreeModelVolumes::getCollisionHolefree(coord_t radius, LayerInde
return (*result).get();
if (m_precalculated) {
BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate collision holefree at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!";
tree_supports_show_error("Not precalculated Holefree Collision requested.", false);
tree_supports_show_error("Not precalculated Holefree Collision requested."sv, false);
}
const_cast<TreeModelVolumes*>(this)->calculateCollisionHolefree({ radius, layer_idx });
return getCollisionHolefree(radius, layer_idx);
@ -375,10 +379,10 @@ const Polygons& TreeModelVolumes::getAvoidance(const coord_t orig_radius, LayerI
if (m_precalculated) {
if (to_model) {
BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate Avoidance to model at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!";
tree_supports_show_error("Not precalculated Avoidance(to model) requested.", false);
tree_supports_show_error("Not precalculated Avoidance(to model) requested."sv, false);
} else {
BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate Avoidance at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!";
tree_supports_show_error("Not precalculated Avoidance(to buildplate) requested.", false);
tree_supports_show_error("Not precalculated Avoidance(to buildplate) requested."sv, false);
}
}
const_cast<TreeModelVolumes*>(this)->calculateAvoidance({ radius, layer_idx }, ! to_model, to_model);
@ -396,7 +400,7 @@ const Polygons& TreeModelVolumes::getPlaceableAreas(const coord_t orig_radius, L
return (*result).get();
if (m_precalculated) {
BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate Placeable Areas at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!";
tree_supports_show_error("Not precalculated Placeable areas requested.", false);
tree_supports_show_error("Not precalculated Placeable areas requested."sv, false);
}
const_cast<TreeModelVolumes*>(this)->calculatePlaceables(radius, layer_idx);
return getPlaceableAreas(orig_radius, layer_idx);
@ -422,7 +426,7 @@ const Polygons& TreeModelVolumes::getWallRestriction(const coord_t orig_radius,
tree_supports_show_error(
min_xy_dist ?
"Not precalculated Wall restriction of minimum xy distance requested )." :
"Not precalculated Wall restriction requested )."
"Not precalculated Wall restriction requested )."sv
, false);
}
const_cast<TreeModelVolumes*>(this)->calculateWallRestrictions({ radius, layer_idx });

View File

@ -58,6 +58,7 @@ enum class LineStatus
using LineInformation = std::vector<std::pair<Point, LineStatus>>;
using LineInformations = std::vector<LineInformation>;
using namespace std::literals;
static inline void validate_range(const Point &pt)
{
@ -191,10 +192,9 @@ static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> group_me
}
#endif
//todo Remove! Only relevant for public BETA!
static bool inline g_showed_critical_error = false;
static bool inline g_showed_performance_warning = false;
void tree_supports_show_error(std::string message, bool critical)
void tree_supports_show_error(std::string_view message, bool critical)
{ // todo Remove! ONLY FOR PUBLIC BETA!!
#if defined(_WIN32) && defined(TREE_SUPPORT_SHOW_ERRORS)
@ -204,7 +204,7 @@ void tree_supports_show_error(std::string message, bool critical)
bool show = (critical && !g_showed_critical_error) || (!critical && !g_showed_performance_warning);
(critical ? g_showed_critical_error : g_showed_performance_warning) = true;
if (show)
MessageBoxA(nullptr, std::string("TreeSupport_2 MOD detected an error while generating the tree support.\nPlease report this back to me with profile and model.\nRevision 5.0\n" + message + "\n" + bugtype).c_str(),
MessageBoxA(nullptr, std::string("TreeSupport_2 MOD detected an error while generating the tree support.\nPlease report this back to me with profile and model.\nRevision 5.0\n" + std::string(message) + "\n" + bugtype).c_str(),
"Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING);
#endif // WIN32
}
@ -572,7 +572,7 @@ static std::optional<std::pair<Point, size_t>> polyline_sample_next_point_at_dis
// In case a fixpoint is encountered, better aggressively overcompensate so the code does not become stuck here...
BOOST_LOG_TRIVIAL(warning) << "Tree Support: Encountered a fixpoint in polyline_sample_next_point_at_distance. This is expected to happen if the distance (currently " << next_distance <<
") is smaller than 100";
tree_supports_show_error("Encountered issue while placing tips. Some tips may be missing.", true);
tree_supports_show_error("Encountered issue while placing tips. Some tips may be missing."sv, true);
if (next_distance > 2 * current_distance)
// This case should never happen, but better safe than sorry.
break;
@ -759,7 +759,7 @@ static std::optional<std::pair<Point, size_t>> polyline_sample_next_point_at_dis
return do_final_difference ? diff(ret, collision_trimmed()) : union_(ret);
if (safe_step_size < 0 || last_step_offset_without_check < 0) {
BOOST_LOG_TRIVIAL(error) << "Offset increase got invalid parameter!";
tree_supports_show_error("Negative offset distance... How did you manage this ?", true);
tree_supports_show_error("Negative offset distance... How did you manage this ?"sv, true);
return do_final_difference ? diff(ret, collision_trimmed()) : union_(ret);
}
@ -951,7 +951,7 @@ static void generate_initial_areas(
bool safe_radius = p.second == LineStatus::TO_BP_SAFE || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE;
if (!mesh_config.support_rests_on_model && !to_bp) {
BOOST_LOG_TRIVIAL(warning) << "Tried to add an invalid support point";
tree_supports_show_error("Unable to add tip. Some overhang may not be supported correctly.", true);
tree_supports_show_error("Unable to add tip. Some overhang may not be supported correctly."sv, true);
return;
}
Polygons circle{ base_circle };
@ -1517,7 +1517,7 @@ static Point move_inside_if_outside(const Polygons &polygons, Point from, int di
if (area(check_layer_data) < tiny_area_threshold) {
BOOST_LOG_TRIVIAL(error) << "Lost area by doing catch up from " << ceil_radius_before << " to radius " <<
volumes.ceilRadius(config.getCollisionRadius(current_elem), settings.use_min_distance);
tree_supports_show_error("Area lost catching up radius. May not cause visible malformation.", true);
tree_supports_show_error("Area lost catching up radius. May not cause visible malformation."sv, true);
}
}
}
@ -1783,7 +1783,7 @@ static void increase_areas_one_layer(
"Parent " << &parent << ": Radius: " << config.getCollisionRadius(parent.state) << " at layer: " << layer_idx << " NextTarget: " << parent.state.layer_idx <<
" Distance to top: " << parent.state.distance_to_top << " Elephant foot increases " << parent.state.elephant_foot_increases << " use_min_xy_dist " << parent.state.use_min_xy_dist <<
" to buildplate " << parent.state.to_buildplate << " gracious " << parent.state.to_model_gracious << " safe " << parent.state.can_use_safe_radius << " until move " << parent.state.dont_move_until;
tree_supports_show_error("Potentially lost branch!", true);
tree_supports_show_error("Potentially lost branch!"sv, true);
} else
result = increase_single_area(volumes, config, settings, layer_idx, parent,
settings.increase_speed == slow_speed ? offset_slow : offset_fast, to_bp_data, to_model_data, inc_wo_collision, 0, mergelayer);
@ -2270,7 +2270,7 @@ static void create_layer_pathing(const TreeModelVolumes &volumes, const TreeSupp
if (elem.areas.to_bp_areas.empty() && elem.areas.to_model_areas.empty()) {
if (area(elem.areas.influence_areas) < tiny_area_threshold) {
BOOST_LOG_TRIVIAL(error) << "Insert Error of Influence area bypass on layer " << layer_idx - 1;
tree_supports_show_error("Insert error of area after bypassing merge.\n", true);
tree_supports_show_error("Insert error of area after bypassing merge.\n"sv, true);
}
// Move the area to output.
this_layer.emplace_back(elem.state, std::move(elem.parents), std::move(elem.areas.influence_areas));
@ -2303,7 +2303,7 @@ static void create_layer_pathing(const TreeModelVolumes &volumes, const TreeSupp
Polygons new_area = safe_union(elem.areas.influence_areas);
if (area(new_area) < tiny_area_threshold) {
BOOST_LOG_TRIVIAL(error) << "Insert Error of Influence area on layer " << layer_idx - 1 << ". Origin of " << elem.parents.size() << " areas. Was to bp " << elem.state.to_buildplate;
tree_supports_show_error("Insert error of area after merge.\n", true);
tree_supports_show_error("Insert error of area after merge.\n"sv, true);
}
this_layer.emplace_back(elem.state, std::move(elem.parents), std::move(new_area));
}
@ -2331,7 +2331,7 @@ static void set_points_on_areas(const SupportElement &elem, SupportElements *lay
// Based on the branch center point of the current layer, the point on the next (further up) layer is calculated.
if (! elem.state.result_on_layer_is_set()) {
BOOST_LOG_TRIVIAL(error) << "Uninitialized support element";
tree_supports_show_error("Uninitialized support element. A branch may be missing.\n", true);
tree_supports_show_error("Uninitialized support element. A branch may be missing.\n"sv, true);
return;
}
@ -2394,7 +2394,7 @@ static void set_to_model_contact_to_model_gracious(
// Could not find valid placement, even though it should exist => error handling
if (last_successfull_layer == nullptr) {
BOOST_LOG_TRIVIAL(warning) << "No valid placement found for to model gracious element on layer " << first_elem.state.layer_idx;
tree_supports_show_error("Could not fine valid placement on model! Just placing it down anyway. Could cause floating branches.", true);
tree_supports_show_error("Could not fine valid placement on model! Just placing it down anyway. Could cause floating branches."sv, true);
first_elem.state.to_model_gracious = false;
set_to_model_contact_simple(first_elem);
} else {
@ -2494,7 +2494,7 @@ static void create_nodes_from_area(
if (elem.state.to_buildplate) {
BOOST_LOG_TRIVIAL(error) << "Uninitialized Influence area targeting " << elem.state.target_position.x() << "," << elem.state.target_position.y() << ") "
"at target_height: " << elem.state.target_height << " layer: " << layer_idx;
tree_supports_show_error("Uninitialized support element! A branch could be missing or exist partially.", true);
tree_supports_show_error("Uninitialized support element! A branch could be missing or exist partially."sv, true);
}
// we dont need to remove yet the parents as they will have a lower dtt and also no result_on_layer set
elem.state.deleted = true;

View File

@ -17,7 +17,7 @@
#include "BoundingBox.hpp"
#include "Utils.hpp"
#define TREE_SUPPORT_SHOW_ERRORS
// #define TREE_SUPPORT_SHOW_ERRORS
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
// The various stages of the process can be weighted differently in the progress bar.
@ -576,8 +576,7 @@ public:
}
};
// todo Remove! ONLY FOR PUBLIC BETA!!
void tree_supports_show_error(std::string message, bool critical);
void tree_supports_show_error(std::string_view message, bool critical);
} // namespace FFFTreeSupport

View File

@ -385,6 +385,8 @@ inline IntegerOnly<I, I> fast_round_up(double a)
return a == 0.49999999999999994 ? I(0) : I(floor(a + 0.5));
}
template<class T> using SamePair = std::pair<T, T>;
} // namespace Slic3r
#endif // _libslic3r_h_

View File

@ -374,23 +374,43 @@ 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_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_enforcers_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_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);
@ -407,7 +427,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);

File diff suppressed because it is too large Load Diff

View File

@ -60,18 +60,25 @@ enum Technology {
T_ANY = ~0,
};
enum BundleLocation{
IN_VENDOR,
IN_ARCHIVE,
IN_RESOURCES
};
struct Bundle
{
std::unique_ptr<PresetBundle> preset_bundle;
VendorProfile* vendor_profile{ nullptr };
bool is_in_resources{ false };
//bool is_in_resources{ false };
BundleLocation location;
bool is_prusa_bundle{ false };
Bundle() = default;
Bundle(Bundle&& other);
// Returns false if not loaded. Reason for that is logged as boost::log error.
bool load(fs::path source_path, bool is_in_resources, bool is_prusa_bundle = false);
bool load(fs::path source_path, BundleLocation location, bool is_prusa_bundle = false);
const std::string& vendor_id() const { return vendor_profile->id; }
};
@ -84,74 +91,8 @@ struct BundleMap : std::map<std::string /* = vendor ID */, Bundle>
const Bundle& prusa_bundle() const;
};
struct Materials
{
Technology technology;
// use vector for the presets to purpose of save of presets sorting in the bundle
std::vector<const Preset*> presets;
// String is alias of material, size_t number of compatible counters
std::vector<std::pair<std::string, size_t>> compatibility_counter;
std::set<std::string> types;
std::set<const Preset*> printers;
struct Materials;
Materials(Technology technology) : technology(technology) {}
void push(const Preset *preset);
void add_printer(const Preset* preset);
void clear();
bool containts(const Preset *preset) const {
//return std::find(presets.begin(), presets.end(), preset) != presets.end();
return std::find_if(presets.begin(), presets.end(),
[preset](const Preset* element) { return element == preset; }) != presets.end();
}
bool get_omnipresent(const Preset* preset) {
return get_printer_counter(preset) == printers.size();
}
const std::vector<const Preset*> get_presets_by_alias(const std::string name) {
std::vector<const Preset*> ret_vec;
for (auto it = presets.begin(); it != presets.end(); ++it) {
if ((*it)->alias == name)
ret_vec.push_back((*it));
}
return ret_vec;
}
size_t get_printer_counter(const Preset* preset) {
for (auto it : compatibility_counter) {
if (it.first == preset->alias)
return it.second;
}
return 0;
}
const std::string& appconfig_section() const;
const std::string& get_type(const Preset *preset) const;
const std::string& get_vendor(const Preset *preset) const;
template<class F> void filter_presets(const Preset* printer, const std::string& type, const std::string& vendor, F cb) {
for (auto preset : presets) {
const Preset& prst = *(preset);
const Preset& prntr = *printer;
if ((printer == nullptr || is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) &&
(type.empty() || get_type(preset) == type) &&
(vendor.empty() || get_vendor(preset) == vendor)) {
cb(preset);
}
}
}
static const std::string UNKNOWN;
static const std::string& get_filament_type(const Preset *preset);
static const std::string& get_filament_vendor(const Preset *preset);
static const std::string& get_material_type(const Preset *preset);
static const std::string& get_material_vendor(const Preset *preset);
};
struct PrinterPickerEvent;
@ -344,6 +285,10 @@ struct PageMaterials: ConfigWizardPage
std::string empty_printers_label;
bool first_paint = { false };
static const std::string EMPTY;
static const std::string TEMPLATES;
// notify user first time they choose template profile
bool template_shown = { false };
bool notification_shown = { false };
int last_hovered_item = { -1 } ;
PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name);
@ -368,6 +313,82 @@ struct PageMaterials: ConfigWizardPage
virtual void on_activate() override;
};
struct Materials
{
Technology technology;
// use vector for the presets to purpose of save of presets sorting in the bundle
std::vector<const Preset*> presets;
// String is alias of material, size_t number of compatible counters
std::vector<std::pair<std::string, size_t>> compatibility_counter;
std::set<std::string> types;
std::set<const Preset*> printers;
Materials(Technology technology) : technology(technology) {}
void push(const Preset* preset);
void add_printer(const Preset* preset);
void clear();
bool containts(const Preset* preset) const {
//return std::find(presets.begin(), presets.end(), preset) != presets.end();
return std::find_if(presets.begin(), presets.end(),
[preset](const Preset* element) { return element == preset; }) != presets.end();
}
bool get_omnipresent(const Preset* preset) {
return get_printer_counter(preset) == printers.size();
}
const std::vector<const Preset*> get_presets_by_alias(const std::string name) {
std::vector<const Preset*> ret_vec;
for (auto it = presets.begin(); it != presets.end(); ++it) {
if ((*it)->alias == name)
ret_vec.push_back((*it));
}
return ret_vec;
}
size_t get_printer_counter(const Preset* preset) {
for (auto it : compatibility_counter) {
if (it.first == preset->alias)
return it.second;
}
return 0;
}
const std::string& appconfig_section() const;
const std::string& get_type(const Preset* preset) const;
const std::string& get_vendor(const Preset* preset) const;
template<class F> void filter_presets(const Preset* printer, const std::string& printer_name, const std::string& type, const std::string& vendor, F cb) {
for (auto preset : presets) {
const Preset& prst = *(preset);
const Preset& prntr = *printer;
if (((printer == nullptr && printer_name == PageMaterials::EMPTY) || (printer != nullptr && is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor)))) &&
(type.empty() || get_type(preset) == type) &&
(vendor.empty() || get_vendor(preset) == vendor) &&
prst.vendor && !prst.vendor->templates_profile) {
cb(preset);
}
else if ((printer == nullptr && printer_name == PageMaterials::TEMPLATES) && prst.vendor && prst.vendor->templates_profile &&
(type.empty() || get_type(preset) == type) &&
(vendor.empty() || get_vendor(preset) == vendor)) {
cb(preset);
}
}
}
static const std::string UNKNOWN;
static const std::string& get_filament_type(const Preset* preset);
static const std::string& get_filament_vendor(const Preset* preset);
static const std::string& get_material_type(const Preset* preset);
static const std::string& get_material_vendor(const Preset* preset);
};
struct PageCustom: ConfigWizardPage
{
PageCustom(ConfigWizard *parent);
@ -608,9 +629,11 @@ struct ConfigWizard::priv
std::unique_ptr<DynamicPrintConfig> custom_config; // Backing for custom printer definition
bool any_fff_selected; // Used to decide whether to display Filaments page
bool any_sla_selected; // Used to decide whether to display SLA Materials page
bool custom_printer_selected { false };
bool custom_printer_selected { false }; // New custom printer is requested
bool custom_printer_in_bundle { false }; // Older custom printer already exists when wizard starts
// Set to true if there are none FFF printers on the main FFF page. If true, only SLA printers are shown (not even custum printers)
bool only_sla_mode { false };
bool template_profile_selected { false }; // This bool has one purpose - to tell that template profile should be installed if its not (because it cannot be added to appconfig)
wxScrolledWindow *hscroll = nullptr;
wxBoxSizer *hscroll_sizer = nullptr;

View File

@ -163,6 +163,8 @@ void FileGet::priv::get_perform()
m_stopped = true;
fclose(file);
cancel = true;
if (m_written == 0)
std::remove(m_tmp_path.string().c_str());
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PAUSED);
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);

View File

@ -808,6 +808,7 @@ void GUI_App::post_init()
// Configuration is not compatible and reconfigure was refused by the user. Application is closing.
return;
CallAfter([this] {
// preset_updater->sync downloads profile updates on background so it must begin after config wizard finished.
bool cw_showed = this->config_wizard_startup();
this->preset_updater->sync(preset_bundle);
this->app_version_check(false);
@ -1248,7 +1249,7 @@ bool GUI_App::on_init_inner()
std::string evt_string = into_u8(evt.GetString());
if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) {
auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable);
this->plater_->get_notification_manager()->push_notification( notif_type
this->plater_->get_notification_manager()->push_version_notification( notif_type
, NotificationManager::NotificationLevel::ImportantNotificationLevel
, Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string)
, _u8L("See Releases page.")
@ -1281,7 +1282,8 @@ bool GUI_App::on_init_inner()
associate_gcode_files();
#endif // __WXMSW__
}
std::string delayed_error_load_presets;
// Suppress the '- default -' presets.
preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1");
try {
@ -1290,7 +1292,7 @@ bool GUI_App::on_init_inner()
// installation of a compatible system preset, thus nullifying the system preset substitutions.
init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent);
} catch (const std::exception &ex) {
show_error(nullptr, ex.what());
delayed_error_load_presets = ex.what();
}
#ifdef WIN32
@ -1307,6 +1309,9 @@ bool GUI_App::on_init_inner()
if (scrn && is_editor())
scrn->SetText(_L("Preparing settings tabs") + dots);
if (!delayed_error_load_presets.empty())
show_error(nullptr, delayed_error_load_presets);
mainframe = new MainFrame();
// hide settings tabs after first Layout
if (is_editor())
@ -3007,6 +3012,9 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage
wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null");
if (reason == ConfigWizard::RR_USER) {
// Cancel sync before starting wizard to prevent two downloads at same time
preset_updater->cancel_sync();
preset_updater->update_index_db();
if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED)
return false;
}
@ -3189,6 +3197,7 @@ bool GUI_App::check_updates(const bool verbose)
{
PresetUpdater::UpdateResult updater_result;
try {
preset_updater->update_index_db();
updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION);
if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) {
mainframe->Close();
@ -3300,6 +3309,19 @@ void GUI_App::on_version_read(wxCommandEvent& evt)
return;
}
if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) {
if (m_app_updater->get_triggered_by_user())
{
std::string text = (*Semver::parse(into_u8(evt.GetString())) == Semver())
? Slic3r::format(_u8L("Check for application update has failed."))
: Slic3r::format(_u8L("No new version is available. Latest release version is %1%."), evt.GetString());
this->plater_->get_notification_manager()->push_version_notification(NotificationType::NoNewReleaseAvailable
, NotificationManager::NotificationLevel::RegularNotificationLevel
, text
, std::string()
, std::function<bool(wxEvtHandler*)>()
);
}
return;
}
// notification

View File

@ -558,8 +558,8 @@ void MenuFactory::append_menu_items_add_volume(MenuType menu_type)
menu->Destroy(range_id);
if (wxGetApp().get_mode() == comSimple) {
append_menu_item_add_text(menu, ModelVolumeType::MODEL_PART, false);
append_menu_item_add_text(menu, ModelVolumeType::NEGATIVE_VOLUME, false);
//append_menu_item_add_text(menu, ModelVolumeType::MODEL_PART, false);
//append_menu_item_add_text(menu, ModelVolumeType::NEGATIVE_VOLUME, false);
append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_ENFORCER)].first), "",
[](wxCommandEvent&) { obj_list()->load_generic_subobject(L("Box"), ModelVolumeType::SUPPORT_ENFORCER); },

View File

@ -2480,6 +2480,9 @@ bool ObjectList::is_splittable(bool to_objects)
return false;
}
if (wxGetApp().get_mode() == comSimple)
return false; // suppress to split to parts for simple mode
ModelVolume* volume;
if (!get_volume_by_item(item, volume) || !volume)
return false;

View File

@ -47,7 +47,6 @@
#define ALLOW_ADD_FONT_BY_OS_SELECTOR
#define SHOW_WX_FONT_DESCRIPTOR // OS specific descriptor | file path --> in edit style <tree header>
#define SHOW_FONT_FILE_PROPERTY // ascent, descent, line gap, cache --> in advanced <tree header>
#define SHOW_FONT_COUNT // count of enumerated font --> in font combo box
#define SHOW_CONTAIN_3MF_FIX // when contain fix matrix --> show gray '3mf' next to close button
#define SHOW_OFFSET_DURING_DRAGGING // when drag with text over surface visualize used center
#define SHOW_IMGUI_ATLAS
@ -403,7 +402,7 @@ Transform3d priv::world_matrix(const GLVolume *gl_volume, const Model *model)
if (!fix.has_value())
return res;
return res * (*fix);
return res * fix->inverse();
}
Transform3d priv::world_matrix(const Selection &selection)
@ -861,6 +860,9 @@ void GLGizmoEmboss::on_set_state()
if (m_volume == nullptr) {
// reopen gizmo when new object is created
GLGizmoBase::m_state = GLGizmoBase::Off;
if (wxGetApp().get_mode() == comSimple)
// It's impossible to add a part in simple mode
return;
// start creating new object
create_volume(ModelVolumeType::MODEL_PART);
}
@ -1830,6 +1832,9 @@ void GLGizmoEmboss::init_font_name_texture() {
face.cancel = nullptr;
face.is_created = nullptr;
}
// Prepare filtration cache
m_face_names.hide = std::vector<bool>(m_face_names.faces.size(), {false});
}
void GLGizmoEmboss::draw_font_preview(FaceName& face, bool is_visible)
@ -1960,13 +1965,60 @@ void GLGizmoEmboss::draw_font_list()
};
}
// Code
const char *popup_id = "##font_list_popup";
const char *input_id = "##font_list_input";
ImGui::SetNextItemWidth(m_gui_cfg->input_width);
if (ImGui::BeginCombo("##font_selector", selected)) {
if (!m_face_names.is_init) init_face_names();
if (m_face_names.texture_id == 0) init_font_name_texture();
ImGuiInputTextFlags input_flags = ImGuiInputTextFlags_CharsUppercase;
// change color of hint to normal text
bool is_popup_open = ImGui::IsPopupOpen(popup_id);
if (!is_popup_open)
ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyleColorVec4(ImGuiCol_Text));
if (ImGui::InputTextWithHint(input_id, selected, &m_face_names.search, input_flags)) {
// update filtration result
m_face_names.hide = std::vector<bool>(m_face_names.faces.size(), {false});
for (FaceName &face : m_face_names.faces) {
size_t index = &face - &m_face_names.faces.front();
std::string name(face.wx_name.ToUTF8().data());
std::transform(name.begin(), name.end(), name.begin(), ::toupper);
// It should use C++ 20 feature https://en.cppreference.com/w/cpp/string/basic_string/starts_with
bool start_with = boost::starts_with(name, m_face_names.search);
m_face_names.hide[index] = !start_with;
}
}
if (!is_popup_open)
ImGui::PopStyleColor(); // revert changes for hint color
const bool is_input_text_active = ImGui::IsItemActive();
// is_input_text_activated
if (ImGui::IsItemActivated())
ImGui::OpenPopup(popup_id);
ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMin().x, ImGui::GetItemRectMax().y));
ImGui::SetNextWindowSize({2*m_gui_cfg->input_width, ImGui::GetTextLineHeight()*10});
ImGuiWindowFlags popup_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_ChildWindow;
if (ImGui::BeginPopup(popup_id, popup_flags))
{
bool set_selection_focus = false;
if (!m_face_names.is_init) {
init_face_names();
set_selection_focus = true;
}
if (m_face_names.texture_id == 0)
init_font_name_texture();
for (FaceName &face : m_face_names.faces) {
const wxString &wx_face_name = face.wx_name;
size_t index = &face - &m_face_names.faces.front();
// Filter for face names
if (m_face_names.hide[index])
continue;
ImGui::PushID(index);
ScopeGuard sg([]() { ImGui::PopID(); });
bool is_selected = (actual_face_name == wx_face_name);
@ -1978,24 +2030,32 @@ void GLGizmoEmboss::draw_font_list()
wxMessageBox(GUI::format(_L("Font face \"%1%\" can't be selected."), wx_face_name));
}
}
// tooltip as full name of font face
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", wx_face_name.ToUTF8().data());
if (is_selected) ImGui::SetItemDefaultFocus();
draw_font_preview(face, ImGui::IsItemVisible());
}
#ifdef SHOW_FONT_COUNT
ImGui::TextColored(ImGuiWrapper::COL_GREY_LIGHT, "Count %d",
static_cast<int>(m_face_names.names.size()));
#endif // SHOW_FONT_COUNT
ImGui::EndCombo();
} else if (m_face_names.is_init) {
// Just one after close combo box
// free texture and set id to zero
// on first draw set focus on selected font
if (set_selection_focus && is_selected)
ImGui::SetScrollHereY();
draw_font_preview(face, ImGui::IsItemVisible());
}
if (!ImGui::IsWindowFocused() ||
!is_input_text_active && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape))) {
// closing of popup
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
} else if (m_face_names.is_init) {
// Just one after close combo box
// free texture and set id to zero
m_face_names.is_init = false;
m_face_names.search.clear();
m_face_names.hide.clear();
// cancel all process for generation of texture
for (FaceName &face : m_face_names.faces)
if (face.cancel != nullptr) face.cancel->store(true);
if (face.cancel != nullptr)
face.cancel->store(true);
glsafe(::glDeleteTextures(1, &m_face_names.texture_id));
m_face_names.texture_id = 0;
}

View File

@ -270,6 +270,10 @@ private:
// hash created from enumerated font from OS
// check when new font was installed
size_t hash = 0;
// filtration pattern
std::string search = "";
std::vector<bool> hide; // result of filtration
} m_face_names;
static bool store(const Facenames &facenames);
static bool load(Facenames &facenames);

View File

@ -517,14 +517,6 @@ bool GLGizmoFdmSupports::has_backend_supports()
return done;
}
void GLGizmoFdmSupports::reslice_FDM_supports(bool postpone_error_messages) const {
wxGetApp().CallAfter(
[this, postpone_error_messages]() {
wxGetApp().plater()->reslice_FFF_until_step(posSupportSpotsSearch,
*m_c->selection_info()->model_object(), postpone_error_messages);
});
}
void GLGizmoFdmSupports::auto_generate()
{
ModelObject *mo = m_c->selection_info()->model_object();
@ -549,8 +541,10 @@ void GLGizmoFdmSupports::auto_generate()
}
}
this->waiting_for_autogenerated_supports = true;
wxGetApp().CallAfter([this]() { reslice_FDM_supports(); });
wxGetApp().CallAfter([this]() {
wxGetApp().plater()->reslice_FFF_until_step(posSupportSpotsSearch, *m_c->selection_info()->model_object(), false);
this->waiting_for_autogenerated_supports = true;
});
}
}

View File

@ -44,7 +44,6 @@ private:
bool waiting_for_autogenerated_supports = false;
bool has_backend_supports();
void reslice_FDM_supports(bool postpone_error_messages = false) const;
void auto_generate();
void apply_data_from_backend();
};

View File

@ -2246,6 +2246,29 @@ void NotificationManager::push_simplify_suggestion_notification(const std::stri
notification->object_id = object_id;
push_notification_data(std::move(notification), 0);
}
void NotificationManager::push_version_notification(NotificationType type, NotificationLevel level, const std::string& text, const std::string& hypertext, std::function<bool(wxEvtHandler*)> callback)
{
assert (type == NotificationType::NewAlphaAvailable
|| type == NotificationType::NewBetaAvailable
|| type == NotificationType::NoNewReleaseAvailable);
for (std::unique_ptr<PopNotification>& notification : m_pop_notifications) {
// NoNewReleaseAvailable must not show if alfa / beta is on.
NotificationType nttype = notification->get_type();
if (type == NotificationType::NoNewReleaseAvailable
&& (notification->get_type() == NotificationType::NewAlphaAvailable
|| notification->get_type() == NotificationType::NewBetaAvailable)) {
return;
}
// NoNewReleaseAvailable must close if alfa / beta is being push.
if (notification->get_type() == NotificationType::NoNewReleaseAvailable
&& (type == NotificationType::NewAlphaAvailable
|| type == NotificationType::NewBetaAvailable)) {
notification->close();
}
}
push_notification(type, level, text, hypertext, callback);
}
void NotificationManager::close_notification_of_type(const NotificationType type)
{
for (std::unique_ptr<PopNotification> &notification : m_pop_notifications) {

View File

@ -56,6 +56,7 @@ enum class NotificationType
// Like NewAppAvailable but with text and link for alpha / bet release
NewAlphaAvailable,
NewBetaAvailable,
NoNewReleaseAvailable,
// Notification on the start of PrusaSlicer, when updates of system profiles are detected.
// Contains a hyperlink to execute installation of the new system profiles.
PresetUpdateAvailable,
@ -191,6 +192,9 @@ public:
// Object warning with ObjectID, closes when object is deleted. ID used is of object not print like in slicing warning.
void push_simplify_suggestion_notification(const std::string& text, ObjectID object_id, const std::string& hypertext = "",
std::function<bool(wxEvtHandler*)> callback = std::function<bool(wxEvtHandler*)>());
// Could be either NewAlphaAvailable, NewBetaAvailable or NoNewReleaseAvailable - this function only makes sure only 1 is visible.
void push_version_notification(NotificationType type, NotificationLevel level, const std::string& text, const std::string& hypertext,
std::function<bool(wxEvtHandler*)> callback);
// Close object warnings, whose ObjectID is not in the list.
// living_oids is expected to be sorted.
void remove_simplify_suggestion_of_released_objects(const std::vector<ObjectID>& living_oids);

View File

@ -553,12 +553,12 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) :
const bool supports_enable = selection == _("None") ? false : true;
new_conf.set_key_value("supports_enable", new ConfigOptionBool(supports_enable));
new_conf.set_key_value("support_enforcers_only", new ConfigOptionBool(false));
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));
else if (selection == _("For support enforcers only")) {
new_conf.set_key_value("support_enforcers_only", new ConfigOptionBool(true));
}
@ -2643,7 +2643,6 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
if (model.looks_like_multipart_object()) {
//wxMessageDialog msg_dlg(q, _L(
MessageDialog msg_dlg(q, _L(
"This file contains several objects positioned at multiple heights.\n"
"Instead of considering them as multiple objects, should \n"
@ -2654,10 +2653,9 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
}
}
else if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) {
//wxMessageDialog msg_dlg(q, _L("This file cannot be loaded in a simple mode. Do you want to switch to an advanced mode?")+"\n",
if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) {
MessageDialog msg_dlg(q, _L("This file cannot be loaded in a simple mode. Do you want to switch to an advanced mode?")+"\n",
_L("Detected advanced data"), wxICON_WARNING | wxYES | wxNO);
_L("Detected advanced data"), wxICON_WARNING | wxYES | wxCANCEL);
if (msg_dlg.ShowModal() == wxID_YES) {
Slic3r::GUI::wxGetApp().save_mode(comAdvanced);
view3D->set_as_dirty();
@ -6147,7 +6145,7 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/*
break;
}
case ProjectDropDialog::LoadType::LoadGeometry: {
Plater::TakeSnapshot snapshot(this, _L("Import Object"));
// Plater::TakeSnapshot snapshot(this, _L("Import Object"));
load_files({ *it }, true, false);
break;
}

View File

@ -301,6 +301,11 @@ void PreferencesDialog::build()
L("Suppress \" - default - \" presets in the Print / Filament / Printer selections once there are any other valid presets available."),
app_config->get("no_defaults") == "1");
append_bool_option(m_optgroup_general, "no_templates",
L("Suppress \" Template \" filament presets"),
L("Suppress \" Template \" filament presets in configuration wizard and sidebar visibility."),
app_config->get("no_templates") == "1");
append_bool_option(m_optgroup_general, "show_incompatible_presets",
L("Show incompatible print and filament presets"),
L("When checked, the print and filament presets are shown in the preset editor "
@ -692,6 +697,8 @@ void PreferencesDialog::accept(wxEvent&)
DesktopIntegrationDialog::perform_desktop_integration(true);
#endif // __linux__
bool update_filament_sidebar = (m_values.find("no_templates") != m_values.end());
std::vector<std::string> options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled" };
for (const std::string& option : options_to_recreate_GUI) {
@ -761,6 +768,9 @@ void PreferencesDialog::accept(wxEvent&)
wxGetApp().update_ui_from_settings();
clear_cache();
if (update_filament_sidebar)
wxGetApp().plater()->sidebar().update_presets(Preset::Type::TYPE_FILAMENT);
}
void PreferencesDialog::revert(wxEvent&)

View File

@ -798,7 +798,7 @@ void PlaterPresetComboBox::show_edit_menu()
wxString PlaterPresetComboBox::get_preset_name(const Preset& preset)
{
std::string name = preset.alias.empty() ? preset.name : preset.alias;
std::string name = preset.alias.empty() ? preset.name : (preset.vendor && preset.vendor->templates_profile ? preset.name : preset.alias);
return from_u8(name + suffix(preset));
}
@ -836,6 +836,7 @@ void PlaterPresetComboBox::update()
null_icon_width = (wide_icons ? 3 : 2) * norm_icon_width + thin_space_icon_width + wide_space_icon_width;
std::map<wxString, wxBitmapBundle*> nonsys_presets;
std::map<wxString, wxBitmapBundle*> template_presets;
wxString selected_user_preset;
wxString tooltip;
@ -883,10 +884,18 @@ void PlaterPresetComboBox::update()
const std::string name = preset.alias.empty() ? preset.name : preset.alias;
if (preset.is_default || preset.is_system) {
Append(get_preset_name(preset), *bmp);
validate_selection(is_selected);
if (is_selected)
tooltip = from_u8(preset.name);
if (preset.vendor && preset.vendor->templates_profile) {
template_presets.emplace(get_preset_name(preset), bmp);
if (is_selected) {
selected_user_preset = get_preset_name(preset);
tooltip = from_u8(preset.name);
}
} else {
Append(get_preset_name(preset), *bmp);
validate_selection(is_selected);
if (is_selected)
tooltip = from_u8(preset.name);
}
}
else
{
@ -899,6 +908,8 @@ void PlaterPresetComboBox::update()
if (i + 1 == m_collection->num_default_presets())
set_label_marker(Append(separator(L("System presets")), NullBitmapBndl()));
}
if (!nonsys_presets.empty())
{
set_label_marker(Append(separator(L("User presets")), NullBitmapBndl()));
@ -908,6 +919,15 @@ void PlaterPresetComboBox::update()
}
}
const AppConfig* app_config = wxGetApp().app_config;
if (!template_presets.empty() && app_config->get("no_templates") == "0") {
set_label_marker(Append(separator(L("Template presets")), wxNullBitmap));
for (std::map<wxString, wxBitmapBundle*>::iterator it = template_presets.begin(); it != template_presets.end(); ++it) {
Append(it->first, *it->second);
validate_selection(it->first == selected_user_preset);
}
}
if (m_type == Preset::TYPE_PRINTER)
{
// add Physical printers, if any exists
@ -1046,6 +1066,8 @@ void TabPresetComboBox::update()
const std::deque<Preset>& presets = m_collection->get_presets();
std::map<wxString, std::pair<wxBitmapBundle*, bool>> nonsys_presets;
std::map<wxString, std::pair<wxBitmapBundle*, bool>> template_presets;
wxString selected = "";
if (!presets.front().is_visible)
set_label_marker(Append(separator(L("System presets")), NullBitmapBndl()));
@ -1078,11 +1100,19 @@ void TabPresetComboBox::update()
auto bmp = get_bmp(bitmap_key, main_icon_name, "lock_closed", is_enabled, preset.is_compatible, preset.is_system || preset.is_default);
assert(bmp);
if (preset.is_default || preset.is_system) {
int item_id = Append(get_preset_name(preset), *bmp);
if (!is_enabled)
set_label_marker(item_id, LABEL_ITEM_DISABLED);
validate_selection(i == idx_selected);
if (preset.is_default || preset.is_system) {
if (preset.vendor && preset.vendor->templates_profile) {
template_presets.emplace(get_preset_name(preset), std::pair<wxBitmapBundle*, bool>(bmp, is_enabled));
if (i == idx_selected)
selected = get_preset_name(preset);
} else {
int item_id = Append(get_preset_name(preset), *bmp);
if (!is_enabled)
set_label_marker(item_id, LABEL_ITEM_DISABLED);
validate_selection(i == idx_selected);
}
}
else
{
@ -1094,6 +1124,7 @@ void TabPresetComboBox::update()
if (i + 1 == m_collection->num_default_presets())
set_label_marker(Append(separator(L("System presets")), NullBitmapBndl()));
}
if (!nonsys_presets.empty())
{
set_label_marker(Append(separator(L("User presets")), NullBitmapBndl()));
@ -1105,7 +1136,19 @@ void TabPresetComboBox::update()
validate_selection(it->first == selected);
}
}
const AppConfig* app_config = wxGetApp().app_config;
if (!template_presets.empty() && app_config->get("no_templates") == "0") {
set_label_marker(Append(separator(L("Template presets")), wxNullBitmap));
for (std::map<wxString, std::pair<wxBitmapBundle*, bool>>::iterator it = template_presets.begin(); it != template_presets.end(); ++it) {
int item_id = Append(it->first, *it->second.first);
bool is_enabled = it->second.second;
if (!is_enabled)
set_label_marker(item_id, LABEL_ITEM_DISABLED);
validate_selection(it->first == selected);
}
}
if (m_type == Preset::TYPE_PRINTER)
{
// add Physical printers, if any exists

View File

@ -285,17 +285,17 @@ void SavePresetDialog::Item::Enable(bool enable /*= true*/)
// SavePresetDialog
//-----------------------------------------------
SavePresetDialog::SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix)
SavePresetDialog::SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix, bool template_filament)
: DPIDialog(parent, wxID_ANY, _L("Save preset"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING | wxRESIZE_BORDER)
{
build(std::vector<Preset::Type>{type}, suffix);
build(std::vector<Preset::Type>{type}, suffix, template_filament);
}
SavePresetDialog::SavePresetDialog(wxWindow* parent, std::vector<Preset::Type> types, std::string suffix, PresetBundle* preset_bundle/* = nullptr*/)
SavePresetDialog::SavePresetDialog(wxWindow* parent, std::vector<Preset::Type> types, std::string suffix, bool template_filament/* =false*/, PresetBundle* preset_bundle/* = nullptr*/)
: DPIDialog(parent, wxID_ANY, _L("Save presets"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING | wxRESIZE_BORDER),
m_preset_bundle(preset_bundle)
{
build(types, suffix);
build(types, suffix, template_filament);
}
SavePresetDialog::SavePresetDialog(wxWindow* parent, Preset::Type type, bool rename, const wxString& info_line_extention)
@ -312,7 +312,7 @@ SavePresetDialog::~SavePresetDialog()
}
}
void SavePresetDialog::build(std::vector<Preset::Type> types, std::string suffix)
void SavePresetDialog::build(std::vector<Preset::Type> types, std::string suffix, bool template_filament)
{
#if defined(__WXMSW__)
// ys_FIXME! temporary workaround for correct font scaling
@ -341,6 +341,15 @@ void SavePresetDialog::build(std::vector<Preset::Type> types, std::string suffix
btnOK->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(enable_ok_btn()); });
topSizer->Add(m_presets_sizer, 0, wxEXPAND | wxALL, BORDER_W);
// Add checkbox for Template filament saving
if (template_filament && types.size() == 1 && *types.begin() == Preset::Type::TYPE_FILAMENT) {
m_template_filament_checkbox = new wxCheckBox(this, wxID_ANY, _L("Save as profile derived from current printer only."));
wxBoxSizer* check_sizer = new wxBoxSizer(wxVERTICAL);
check_sizer->Add(m_template_filament_checkbox);
topSizer->Add(check_sizer, 0, wxEXPAND | wxALL, BORDER_W);
}
topSizer->Add(btns, 0, wxEXPAND | wxALL, BORDER_W);
SetSizer(topSizer);
@ -371,6 +380,15 @@ std::string SavePresetDialog::get_name(Preset::Type type)
return "";
}
bool SavePresetDialog::get_template_filament_checkbox()
{
if (m_template_filament_checkbox)
{
return m_template_filament_checkbox->GetValue();
}
return false;
}
bool SavePresetDialog::enable_ok_btn() const
{
for (const Item* item : m_items)

View File

@ -76,6 +76,7 @@ private:
wxStaticText* m_label {nullptr};
wxBoxSizer* m_radio_sizer {nullptr};
ActionType m_action {UndefAction};
wxCheckBox* m_template_filament_checkbox {nullptr};
std::string m_ph_printer_name;
std::string m_old_preset_name;
@ -88,8 +89,8 @@ public:
const wxString& get_info_line_extention() { return m_info_line_extention; }
SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix = "");
SavePresetDialog(wxWindow* parent, std::vector<Preset::Type> types, std::string suffix = "", PresetBundle* preset_bundle = nullptr);
SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix = "", bool template_filament = false);
SavePresetDialog(wxWindow* parent, std::vector<Preset::Type> types, std::string suffix = "", bool template_filament = false, PresetBundle* preset_bundle = nullptr);
SavePresetDialog(wxWindow* parent, Preset::Type type, bool rename, const wxString& info_line_extention);
~SavePresetDialog() override;
@ -105,12 +106,13 @@ public:
bool Layout() override;
bool is_for_rename() { return m_use_for_rename; }
bool get_template_filament_checkbox();
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
void on_sys_color_changed() override {}
private:
void build(std::vector<Preset::Type> types, std::string suffix = "");
void build(std::vector<Preset::Type> types, std::string suffix = "", bool template_filament = false);
void update_physical_printers(const std::string& preset_name);
void accept();
};

View File

@ -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"))
@ -3712,11 +3715,25 @@ void Tab::save_preset(std::string name /*= ""*/, bool detach)
// focus currently.is there anything better than this ?
//! m_treectrl->OnSetFocus();
auto& old_preset = m_presets->get_edited_preset();
bool from_template = false;
std::string edited_printer;
if (m_type == Preset::TYPE_FILAMENT && old_preset.vendor && old_preset.vendor->templates_profile)
{
//TODO: is this really the best way to get "printer_model" option of currently edited printer?
edited_printer = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt<ConfigOptionString>("printer_model")->serialize();
if (!edited_printer.empty())
from_template = true;
}
if (name.empty()) {
SavePresetDialog dlg(m_parent, m_type, detach ? _u8L("Detached") : "");
SavePresetDialog dlg(m_parent, m_type, detach ? _u8L("Detached") : "", from_template);
if (dlg.ShowModal() != wxID_OK)
return;
name = dlg.get_name();
if (from_template)
from_template = dlg.get_template_filament_checkbox();
}
if (detach && m_type == Preset::TYPE_PRINTER)
@ -3728,6 +3745,19 @@ void Tab::save_preset(std::string name /*= ""*/, bool detach)
if (detach && m_type == Preset::TYPE_PRINTER)
wxGetApp().mainframe->on_config_changed(m_config);
// Update compatible printers
if (from_template && !edited_printer.empty()) {
auto& new_preset = m_presets->get_edited_preset();
std::string cond = new_preset.compatible_printers_condition();
if (!cond.empty())
cond += " and ";
cond += "printer_model == \""+edited_printer+"\"";
new_preset.config.set("compatible_printers_condition", cond);
new_preset.save();
m_presets->save_current_preset(name, detach);
load_current_preset();
}
// Mark the print & filament enabled if they are compatible with the currently selected preset.
// If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a "red flag" icon showing that they are no more compatible.
m_preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
@ -4821,6 +4851,60 @@ void TabSLAMaterial::update()
wxGetApp().mainframe->on_config_changed(m_config);
}
static void add_options_into_line(ConfigOptionsGroupShp &optgroup,
const std::vector<SamePair<std::string>> &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<SamePair<std::string>> &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,42 +4917,14 @@ 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_enforcers_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");
build_sla_support_params({{"", "Default"}, {"branching", "Branching"}}, page);
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");
optgroup = page->new_optgroup(L("Automatic generation"));
optgroup->append_single_option_line("support_points_density_relative");

View File

@ -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<SamePair<std::string>> &methods,
const Slic3r::GUI::PageShp &page);
public:
TabSLAPrint(wxBookCtrlBase* parent) :
Tab(parent, _(L("Print Settings")), Slic3r::Preset::TYPE_SLA_PRINT) {}

View File

@ -323,6 +323,11 @@ void AppUpdater::priv::parse_version_string(const std::string& body)
return;
#endif // 0
BOOST_LOG_TRIVIAL(error) << "Could not find property tree in version file. Checking for application update has failed.";
// Lets send event with current version, this way if user triggered this check, it will notify him about no new version online.
std::string version = Semver().to_string();
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE);
evt->SetString(GUI::from_u8(version));
GUI::wxGetApp().QueueEvent(evt);
return;
}
std::string tree_string = body.substr(start);

View File

@ -12,14 +12,17 @@
#include <boost/filesystem/fstream.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/log/trivial.hpp>
#include <curl/curl.h>
#include <wx/app.h>
#include <wx/msgdlg.h>
#include <wx/progdlg.h>
#include "libslic3r/libslic3r.h"
#include "libslic3r/format.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/miniz_extension.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/UpdateDialogs.hpp"
@ -49,7 +52,7 @@ namespace Slic3r {
static const char *INDEX_FILENAME = "index.idx";
static const char *TMP_EXTENSION = ".download";
namespace {
void copy_file_fix(const fs::path &source, const fs::path &target)
{
BOOST_LOG_TRIVIAL(debug) << format("PresetUpdater: Copying %1% -> %2%", source, target);
@ -66,7 +69,21 @@ void copy_file_fix(const fs::path &source, const fs::path &target)
static constexpr const auto perms = fs::owner_read | fs::owner_write | fs::group_read | fs::others_read;
fs::permissions(target, perms);
}
std::string escape_string_url(const std::string& unescaped)
{
std::string ret_val;
CURL* curl = curl_easy_init();
if (curl) {
char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size());
if (decoded) {
ret_val = std::string(decoded);
curl_free(decoded);
}
curl_easy_cleanup(curl);
}
return ret_val;
}
}
struct Update
{
fs::path source;
@ -144,6 +161,7 @@ struct PresetUpdater::priv
std::string version_check_url;
fs::path cache_path;
fs::path cache_vendor_path;
fs::path rsrc_path;
fs::path vendor_path;
@ -155,19 +173,25 @@ struct PresetUpdater::priv
priv();
void set_download_prefs(AppConfig *app_config);
void set_download_prefs(const AppConfig *app_config);
bool get_file(const std::string &url, const fs::path &target_path) const;
void prune_tmps() const;
void sync_config(const VendorMap vendors);
void sync_config(const VendorMap vendors, const std::string& index_archive_url);
void check_install_indices() const;
Updates get_config_updates(const Semver& old_slic3r_version) const;
bool perform_updates(Updates &&updates, bool snapshot = true) const;
void set_waiting_updates(Updates u);
// checks existence and downloads resource to cache
void get_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const;
// checks existence and downloads resource to vendor or copy from cache to vendor
void get_or_copy_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const;
void update_index_db();
};
PresetUpdater::priv::priv()
: cache_path(fs::path(Slic3r::data_dir()) / "cache")
, cache_vendor_path(cache_path / "vendor")
, rsrc_path(fs::path(resources_dir()) / "profiles")
, vendor_path(fs::path(Slic3r::data_dir()) / "vendor")
, cancel(false)
@ -179,8 +203,13 @@ PresetUpdater::priv::priv()
index_db = Index::load_db();
}
void PresetUpdater::priv::update_index_db()
{
index_db = Index::load_db();
}
// Pull relevant preferences from AppConfig
void PresetUpdater::priv::set_download_prefs(AppConfig *app_config)
void PresetUpdater::priv::set_download_prefs(const AppConfig *app_config)
{
enabled_version_check = app_config->get("notify_release") != "none";
version_check_url = app_config->version_check_url();
@ -232,46 +261,208 @@ void PresetUpdater::priv::prune_tmps() const
}
}
void PresetUpdater::priv::get_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const
{
if (filename.empty() || vendor.empty())
return;
if (!boost::starts_with(url, "http://files.prusa3d.com/wp-content/uploads/repository/") &&
!boost::starts_with(url, "https://files.prusa3d.com/wp-content/uploads/repository/"))
{
throw Slic3r::CriticalException(GUI::format("URL outside prusa3d.com network: %1%", url));
}
std::string escaped_filename = escape_string_url(filename);
const fs::path file_in_vendor(vendor_path / (vendor + "/" + filename));
const fs::path file_in_rsrc(rsrc_path / (vendor + "/" + filename));
const fs::path file_in_cache(cache_path / (vendor + "/" + filename));
if (fs::exists(file_in_vendor)) { // Already in vendor. No need to do anything.
BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in vendor folder. No need to download.";
return;
}
if (fs::exists(file_in_rsrc)) { // In resources dir since installation. No need to do anything.
BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in resources folder. No need to download.";
return;
}
if (fs::exists(file_in_cache)) { // In cache/venodr_name/ dir. No need to do anything.
BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in cache folder. No need to download.";
return;
}
BOOST_LOG_TRIVIAL(info) << "Resources check could not find " << vendor << " / " << filename << " bed texture. Downloading.";
const auto resource_url = format("%1%%2%%3%", url, url.back() == '/' ? "" : "/", escaped_filename); // vendor should already be in url
if (!fs::exists(file_in_cache.parent_path()))
fs::create_directory(file_in_cache.parent_path());
get_file(resource_url, file_in_cache);
return;
}
void PresetUpdater::priv::get_or_copy_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const
{
if (filename.empty() || vendor.empty())
return;
std::string escaped_filename = escape_string_url(filename);
const fs::path file_in_vendor(vendor_path / (vendor + "/" + filename));
const fs::path file_in_rsrc(rsrc_path / (vendor + "/" + filename));
const fs::path file_in_cache(cache_path / (vendor + "/" + filename));
if (fs::exists(file_in_vendor)) { // Already in vendor. No need to do anything.
BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in vendor folder. No need to download.";
return;
}
if (fs::exists(file_in_rsrc)) { // In resources dir since installation. No need to do anything.
BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in resources folder. No need to download.";
return;
}
if (!fs::exists(file_in_cache)) { // No file to copy. Download it to straight to the vendor dir.
if (!boost::starts_with(url, "http://files.prusa3d.com/wp-content/uploads/repository/") &&
!boost::starts_with(url, "https://files.prusa3d.com/wp-content/uploads/repository/"))
{
throw Slic3r::CriticalException(GUI::format("URL outside prusa3d.com network: %1%", url));
}
BOOST_LOG_TRIVIAL(info) << "Downloading resources missing in cache directory: " << vendor << " / " << filename;
const auto resource_url = format("%1%%2%%3%", url, url.back() == '/' ? "" : "/", escaped_filename); // vendor should already be in url
if (!fs::exists(file_in_vendor.parent_path()))
fs::create_directory(file_in_vendor.parent_path());
get_file(resource_url, file_in_vendor);
return;
}
if (!fs::exists(file_in_vendor.parent_path())) // create vendor_name dir in vendor
fs::create_directory(file_in_vendor.parent_path());
BOOST_LOG_TRIVIAL(debug) << "Copiing: " << file_in_cache << " to " << file_in_vendor;
copy_file_fix(file_in_cache, file_in_vendor);
}
// Download vendor indices. Also download new bundles if an index indicates there's a new one available.
// Both are saved in cache.
void PresetUpdater::priv::sync_config(const VendorMap vendors)
void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string& index_archive_url)
{
BOOST_LOG_TRIVIAL(info) << "Syncing configuration cache";
if (!enabled_config_update) { return; }
// Donwload vendor preset bundles
// Download profiles archive zip
// dk: Do we want to return here on error? Or skip archive dwnld and unzip and work with previous run state cache / vendor? I think return.
// Any error here also doesnt show any info in UI. Do we want maybe notification?
fs::path archive_path(cache_path / "vendor_indices.zip");
if (index_archive_url.empty()) {
BOOST_LOG_TRIVIAL(error) << "Downloading profile archive failed - url has no value.";
return;
}
BOOST_LOG_TRIVIAL(info) << "Downloading vedor profiles archive zip from " << index_archive_url;
//check if idx_url is leading to our site
if (!boost::starts_with(index_archive_url, "http://files.prusa3d.com/wp-content/uploads/repository/") &&
!boost::starts_with(index_archive_url, "https://files.prusa3d.com/wp-content/uploads/repository/"))
{
BOOST_LOG_TRIVIAL(error) << "Unsafe url path for vedor profiles archive zip. Download is rejected.";
return;
}
if (!get_file(index_archive_url, archive_path)) {
BOOST_LOG_TRIVIAL(error) << "Download of vedor profiles archive zip failed.";
return;
}
if (cancel) {
return;
}
enum class VendorStatus
{
IN_ARCHIVE,
IN_CACHE,
NEW_VERSION,
INSTALLED
};
std::vector<std::pair<std::string, VendorStatus>> vendors_with_status;
// Unzip archive to cache / vendor
mz_zip_archive archive;
mz_zip_zero_struct(&archive);
if (!open_zip_reader(&archive, archive_path.string())) {
BOOST_LOG_TRIVIAL(error) << "Couldn't open zipped bundle.";
return;
} else {
mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
// loop the entries
mz_zip_archive_file_stat stat;
for (mz_uint i = 0; i < num_entries; ++i) {
if (mz_zip_reader_file_stat(&archive, i, &stat)) {
std::string name(stat.m_filename);
if (stat.m_uncomp_size > 0) {
std::string buffer((size_t)stat.m_uncomp_size, 0);
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0);
if (res == 0) {
BOOST_LOG_TRIVIAL(error) << "Failed to unzip " << stat.m_filename;
continue;
}
// create file from buffer
fs::path tmp_path(cache_vendor_path / (name + ".tmp"));
if (!fs::exists(tmp_path.parent_path())) {
BOOST_LOG_TRIVIAL(error) << "Failed to unzip file " << name << ". Directories are not supported. Skipping file.";
continue;
}
fs::path target_path(cache_vendor_path / name);
fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc);
file.write(buffer.c_str(), buffer.size());
file.close();
boost::system::error_code ec;
bool exists = fs::exists(tmp_path, ec);
if(!exists || ec) {
BOOST_LOG_TRIVIAL(error) << "Failed to find unzipped file at " << tmp_path << ". Terminating Preset updater synchorinzation." ;
close_zip_reader(&archive);
return;
}
fs::rename(tmp_path, target_path, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "Failed to rename unzipped file at " << tmp_path << ". Terminating Preset updater synchorinzation. Error message: " << ec.message();
close_zip_reader(&archive);
return;
}
// TODO: what if unexpected happens here (folder inside zip) - crash!
if (name.substr(name.size() - 3) == "idx")
vendors_with_status.emplace_back(name.substr(0, name.size() - 4), VendorStatus::IN_ARCHIVE); // asume for now its only in archive - if not, it will change later.
}
}
}
close_zip_reader(&archive);
}
// Update vendor preset bundles if in Vendor
// Over all indices from the cache directory:
for (auto &index : index_db) {
if (cancel) { return; }
if (cancel) {
return;
}
auto archive_it = std::find_if(vendors_with_status.begin(), vendors_with_status.end(),
[&index](const std::pair<std::string, VendorStatus>& element) { return element.first == index.vendor(); });
//assert(archive_it != vendors_with_status.end()); // this would mean there is a index for vendor that is missing in recently downloaded archive
const auto vendor_it = vendors.find(index.vendor());
if (vendor_it == vendors.end()) {
BOOST_LOG_TRIVIAL(warning) << "No such vendor: " << index.vendor();
// Not installed vendor yet we need to check missing thumbnails (of new printers)
BOOST_LOG_TRIVIAL(debug) << "No such vendor: " << index.vendor();
if (archive_it != vendors_with_status.end())
archive_it->second = VendorStatus::IN_CACHE;
continue;
}
if (archive_it != vendors_with_status.end())
archive_it->second = VendorStatus::INSTALLED;
const VendorProfile &vendor = vendor_it->second;
if (vendor.config_update_url.empty()) {
BOOST_LOG_TRIVIAL(info) << "Vendor has no config_update_url: " << vendor.name;
continue;
}
// Download a fresh index
BOOST_LOG_TRIVIAL(info) << "Downloading index for vendor: " << vendor.name;
const auto idx_url = vendor.config_update_url + "/" + INDEX_FILENAME;
const std::string idx_path = (cache_path / (vendor.id + ".idx")).string();
const std::string idx_path_temp = idx_path + "-update";
//check if idx_url is leading to our site
if (! boost::starts_with(idx_url, "http://files.prusa3d.com/wp-content/uploads/repository/") &&
! boost::starts_with(idx_url, "https://files.prusa3d.com/wp-content/uploads/repository/"))
{
BOOST_LOG_TRIVIAL(warning) << "unsafe url path for vendor \"" << vendor.name << "\" rejected: " << idx_url;
continue;
}
if (!get_file(idx_url, idx_path_temp)) { continue; }
if (cancel) { return; }
const std::string idx_path_temp = (cache_vendor_path / (vendor.id + ".idx")).string();
// Load the fresh index up
{
Index new_index;
@ -282,10 +473,11 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors)
continue;
}
if (new_index.version() < index.version()) {
BOOST_LOG_TRIVIAL(warning) << format("The downloaded index %1% for vendor %2% is older than the active one. Ignoring the downloaded index.", idx_path_temp, vendor.name);
BOOST_LOG_TRIVIAL(info) << format("The downloaded index %1% for vendor %2% is older than the active one. Ignoring the downloaded index.", idx_path_temp, vendor.name);
continue;
}
Slic3r::rename_file(idx_path_temp, idx_path);
copy_file_fix(idx_path_temp, idx_path);
//if we rename path we need to change it in Index object too or create the object again
//index = std::move(new_index);
try {
@ -315,12 +507,265 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors)
if (vendor.config_version >= recommended) { continue; }
// Download a fresh bundle
// vendors that are checked here, doesnt need to be checked again later
if (archive_it != vendors_with_status.end())
archive_it->second = VendorStatus::NEW_VERSION;
// Download recomended ini to cache
const auto path_in_cache = cache_path / (vendor.id + ".ini");
BOOST_LOG_TRIVIAL(info) << "Downloading new bundle for vendor: " << vendor.name;
const auto bundle_url = format("%1%/%2%.ini", vendor.config_update_url, recommended.to_string());
const auto bundle_path = cache_path / (vendor.id + ".ini");
if (! get_file(bundle_url, bundle_path)) { continue; }
if (cancel) { return; }
if (!get_file(bundle_url, bundle_path))
continue;
if (cancel)
return;
// vp is fully loaded to get all resources
VendorProfile vp;
try {
vp = VendorProfile::from_ini(bundle_path, true);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.id, bundle_path, e.what());
continue;
}
// check the fresh bundle for missing resources
// for that, the ini file must be parsed (done above)
for (const auto& model : vp.models) {
for (const std::string& res : { model.bed_texture, model.bed_model, model.thumbnail/*id +"_thumbnail.png"*/} ) {
if (! res.empty()) {
try
{
get_missing_resource(vp.id, res, vendor.config_update_url);
}
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(error) << "Failed to get " << res << " for " << vp.id << " " << model.id << ": " << e.what();
}
}
if (cancel)
return;
}
}
}
// Download missing thumbnails for not-installed vendors.
//for (const std::string& vendor : vendors_only_in_archive)
for (const std::pair<std::string, VendorStatus >& vendor : vendors_with_status) {
if (vendor.second == VendorStatus::IN_ARCHIVE) {
// index in archive and not in cache and not installed vendor
const auto idx_path_in_archive = cache_vendor_path / (vendor.first + ".idx");
const auto ini_path_in_archive = cache_vendor_path / (vendor.first + ".ini");
if (!fs::exists(idx_path_in_archive))
continue;
Index index;
try {
index.load(idx_path_in_archive);
}
catch (const std::exception& /* err */) {
BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path_in_archive, vendor.first);
continue;
}
const auto recommended_it = index.recommended();
if (recommended_it == index.end()) {
BOOST_LOG_TRIVIAL(error) << format("No recommended version for vendor: %1%, invalid index? (%2%)", vendor.first, idx_path_in_archive);
continue;
}
const auto recommended = recommended_it->config_version;
if (!fs::exists(ini_path_in_archive)){
// Download recommneded to vendor - we do not have any existing ini file so we have to use hardcoded url.
const std::string fixed_url = GUI::wxGetApp().app_config->profile_folder_url();
const auto bundle_url = format("%1%/%2%/%3%.ini", fixed_url, vendor.first, recommended.to_string());
if (!get_file(bundle_url, ini_path_in_archive))
continue;
} else {
// check existing ini version
// then download recommneded to vendor if needed
VendorProfile vp;
try {
vp = VendorProfile::from_ini(ini_path_in_archive, true);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what());
continue;
}
if (vp.config_version != recommended) {
const std::string fixed_url = GUI::wxGetApp().app_config->profile_folder_url();
const auto bundle_url = format("%1%/%2%/%3%.ini", fixed_url, vendor.first, recommended.to_string());
if (!get_file(bundle_url, ini_path_in_archive))
continue;
}
}
// check missing thumbnails
VendorProfile vp;
try {
vp = VendorProfile::from_ini(ini_path_in_archive, true);
}
catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what());
continue;
}
for (const auto& model : vp.models) {
if (!model.thumbnail.empty()) {
try
{
get_missing_resource(vp.id, model.thumbnail, vp.config_update_url);
}
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what();
}
}
if (cancel)
return;
}
} else if (vendor.second == VendorStatus::IN_CACHE) {
// find those where archive index recommends other version than index in cache and get it if not present
const auto idx_path_in_archive = cache_vendor_path / (vendor.first + ".idx");
const auto ini_path_in_archive = cache_vendor_path / (vendor.first + ".ini");
const auto idx_path_in_cache = cache_path / (vendor.first + ".idx");
if (!fs::exists(idx_path_in_archive) || !fs::exists(idx_path_in_cache))
continue;
// Compare index in cache and recetly downloaded one as part of zip archive
Index index_cache;
try {
index_cache.load(idx_path_in_cache);
}
catch (const std::exception& /* err */) {
BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path_in_cache, vendor.first);
continue;
}
const auto recommended_it_cache = index_cache.recommended();
if (recommended_it_cache == index_cache.end()) {
BOOST_LOG_TRIVIAL(error) << format("No recommended version for vendor: %1%, invalid index? (%2%)", vendor.first, idx_path_in_cache);
continue;
}
const auto recommended_cache = recommended_it_cache->config_version;
Index index_archive;
try {
index_archive.load(idx_path_in_archive);
}
catch (const std::exception& /* err */) {
BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path_in_archive, vendor.first);
continue;
}
const auto recommended_it_archive = index_archive.recommended();
if (recommended_it_archive == index_archive.end()) {
BOOST_LOG_TRIVIAL(error) << format("No recommended version for vendor: %1%, invalid index? (%2%)", vendor.first, idx_path_in_archive);
continue;
}
const auto recommended_archive = recommended_it_archive->config_version;
if (recommended_archive <= recommended_cache) {
// There isn't more recent recomended version online. This vendor is also not istalled.
// Thus only .ini is in resources and came with installation.
// And we expect all resources are present.
continue;
}
// Download new .ini if needed. So next time user runs Wizard, most recent profiles are shown & installed.
if (!fs::exists(ini_path_in_archive) || fs::is_empty(ini_path_in_archive)) {
// download recommneded to vendor
const fs::path ini_path_in_rsrc = rsrc_path / (vendor.first + ".ini");
if (!fs::exists(ini_path_in_rsrc)) {
// THIS SHOULD NOT HAPPEN
continue;
}
// Get download path from existing ini.
VendorProfile vp;
try {
vp = VendorProfile::from_ini(ini_path_in_rsrc, false);
}
catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_rsrc, e.what());
continue;
}
const auto bundle_url = format("%1%/%2%.ini", vp.config_update_url, recommended_archive.to_string());
if (!get_file(bundle_url, ini_path_in_archive)) {
BOOST_LOG_TRIVIAL(error) << format("Failed to open vendor .ini file when checking missing resources: %1%", ini_path_in_rsrc);
continue;
}
} else {
// Check existing ini version.
// Then download recommneded to vendor if needed.
VendorProfile vp;
try {
vp = VendorProfile::from_ini(ini_path_in_archive, false);
}
catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what());
continue;
}
if (vp.config_version != recommended_archive) {
const auto bundle_url = format("%1%/%2%.ini", vp.config_update_url, recommended_archive.to_string());
if (!get_file(bundle_url, ini_path_in_archive)) {
BOOST_LOG_TRIVIAL(error) << format("Failed to open vendor .ini file when checking missing resources: %1%", ini_path_in_archive);
continue;
}
}
}
if (!fs::exists(ini_path_in_archive)) {
BOOST_LOG_TRIVIAL(error) << "Resources check failed to find ini file for vendor: " << vendor.first;
continue;
}
// check missing thumbnails
VendorProfile vp;
try {
vp = VendorProfile::from_ini(ini_path_in_archive, true);
}
catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what());
continue;
}
for (const auto& model : vp.models) {
if (!model.thumbnail.empty()) {
try
{
get_missing_resource(vp.id, model.thumbnail, vp.config_update_url);
}
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what();
}
}
if (cancel)
return;
}
} else if (vendor.second == VendorStatus::INSTALLED || vendor.second == VendorStatus::NEW_VERSION) {
// Installed vendors need to check that no resource is missing. Do this only for files in vendor folder (not in resorces)
// VendorStatus::NEW_VERSION might seem like a mistake here since files are downloaded when preparing update higher in this function.
// But this is a check for ini file in vendor where resources might be still missing since last update.
const auto path_in_vendor = vendor_path / (vendor.first + ".ini");
if(!fs::exists(path_in_vendor))
continue;
VendorProfile vp;
try {
vp = VendorProfile::from_ini(path_in_vendor, true);
}
catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, path_in_vendor, e.what());
continue;
}
for (const auto& model : vp.models) {
for (const std::string& res : { model.bed_texture, model.bed_model, model.thumbnail }) {
if (!model.thumbnail.empty()) {
try
{
get_or_copy_missing_resource(vp.id, res, vp.config_update_url);
}
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what();
}
}
if (cancel)
return;
}
}
}
}
}
@ -370,8 +815,14 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
}
// Perform a basic load and check the version of the installed preset bundle.
auto vp = VendorProfile::from_ini(bundle_path, false);
VendorProfile vp;
try {
vp = VendorProfile::from_ini(bundle_path, false);
}
catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", idx.vendor(), bundle_path, e.what());
continue;
}
// Getting a recommended version from the latest index, wich may have been downloaded
// from the internet, or installed / updated from the installation resources.
auto recommended = idx.recommended();
@ -401,7 +852,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
bool current_not_supported = false; //if slcr is incompatible but situation is not downgrade, we do forced updated and this bool is information to do it
if (ver_current_found && !ver_current->is_current_slic3r_supported()){
if(ver_current->is_current_slic3r_downgrade()) {
if (ver_current->is_current_slic3r_downgrade()) {
// "Reconfigure" situation.
BOOST_LOG_TRIVIAL(warning) << "Current Slic3r incompatible with installed bundle: " << bundle_path.string();
updates.incompats.emplace_back(std::move(bundle_path), *ver_current, vp.name);
@ -443,10 +894,13 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
fs::path bundle_path_idx_to_install;
if (fs::exists(path_in_cache)) {
try {
VendorProfile new_vp = VendorProfile::from_ini(path_in_cache, false);
VendorProfile new_vp = VendorProfile::from_ini(path_in_cache, true);
if (new_vp.config_version == recommended->config_version) {
// The config bundle from the cache directory matches the recommended version of the index from the cache directory.
// This is the newest known recommended config. Use it.
if (!PresetUtils::vendor_profile_has_all_resources(new_vp)) {
BOOST_LOG_TRIVIAL(warning) << "Some resources are missing for update of vedor " << new_vp.id;
}
new_update = Update(std::move(path_in_cache), std::move(bundle_path), *recommended, vp.name, vp.changelog_url, current_not_supported);
// and install the config index from the cache into vendor's directory.
bundle_path_idx_to_install = idx.path();
@ -564,6 +1018,9 @@ bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons
BOOST_LOG_TRIVIAL(info) << format("Performing %1% updates", updates.updates.size());
wxProgressDialog progress_dialog(_L("Installing profiles"), _L("Installing profiles") , 100, nullptr, wxPD_AUTO_HIDE);
progress_dialog.Pulse();
for (const auto &update : updates.updates) {
BOOST_LOG_TRIVIAL(info) << '\t' << update;
@ -601,12 +1058,40 @@ bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons
for (const auto &name : bundle.obsolete_presets.sla_prints) { obsolete_remover("sla_print", name); }
for (const auto &name : bundle.obsolete_presets.sla_materials/*filaments*/) { obsolete_remover("sla_material", name); }
for (const auto &name : bundle.obsolete_presets.printers) { obsolete_remover("printer", name); }
// check if any resorces of installed bundle are missing. If so, new ones should be already downloaded at cache/vendor_id/
VendorProfile vp;
try {
vp = VendorProfile::from_ini(update.target, true);
}
catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1%, message: %2%", update.target, e.what());
continue;
}
progress_dialog.Update(1, GUI::format_wxstr(_L("Downloading resources for %1%."),vp.id));
progress_dialog.Pulse();
for (const auto& model : vp.models) {
for (const std::string& resource : { model.bed_texture, model.bed_model, model.thumbnail }) {
if (resource.empty())
continue;
try
{
get_or_copy_missing_resource(vp.id, resource, vp.config_update_url);
}
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(error) << "Failed to prepare " << resource << " for " << vp.id << " " << model.id << ": " << e.what();
}
}
}
}
progress_dialog.Destroy();
}
return true;
}
void PresetUpdater::priv::set_waiting_updates(Updates u)
{
waiting_updates = u;
@ -630,7 +1115,7 @@ PresetUpdater::~PresetUpdater()
}
}
void PresetUpdater::sync(PresetBundle *preset_bundle)
void PresetUpdater::sync(const PresetBundle *preset_bundle)
{
p->set_download_prefs(GUI::wxGetApp().app_config);
if (!p->enabled_version_check && !p->enabled_config_update) { return; }
@ -639,13 +1124,24 @@ void PresetUpdater::sync(PresetBundle *preset_bundle)
// Unfortunatelly as of C++11, it needs to be copied again
// into the closure (but perhaps the compiler can elide this).
VendorMap vendors = preset_bundle->vendors;
std::string index_archive_url = GUI::wxGetApp().app_config->index_archive_url();
p->thread = std::thread([this, vendors]() {
p->thread = std::thread([this, vendors, index_archive_url]() {
this->p->prune_tmps();
this->p->sync_config(std::move(vendors));
this->p->sync_config(std::move(vendors), index_archive_url);
});
}
void PresetUpdater::cancel_sync()
{
if (p && p->thread.joinable()) {
// This will stop transfers being done by the thread, if any.
// Cancelling takes some time, but should complete soon enough.
p->cancel = true;
p->thread.join();
}
}
void PresetUpdater::slic3r_update_notify()
{
if (! p->enabled_version_check)
@ -810,7 +1306,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3
return R_NOOP;
}
bool PresetUpdater::install_bundles_rsrc(std::vector<std::string> bundles, bool snapshot) const
bool PresetUpdater::install_bundles_rsrc_or_cache_vendor(std::vector<std::string> bundles, bool snapshot) const
{
Updates updates;
@ -818,8 +1314,66 @@ bool PresetUpdater::install_bundles_rsrc(std::vector<std::string> bundles, bool
for (const auto &bundle : bundles) {
auto path_in_rsrc = (p->rsrc_path / bundle).replace_extension(".ini");
auto path_in_cache_vendor = (p->cache_vendor_path / bundle).replace_extension(".ini");
auto path_in_vendors = (p->vendor_path / bundle).replace_extension(".ini");
updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", "");
bool is_in_rsrc = fs::exists(path_in_rsrc);
bool is_in_cache_vendor = fs::exists(path_in_cache_vendor) && !fs::is_empty(path_in_cache_vendor);
// find if in cache vendor is newer version than in resources
if (is_in_cache_vendor) {
Semver version_cache = Semver::zero();
try {
auto vp_cache = VendorProfile::from_ini(path_in_cache_vendor, false);
version_cache = vp_cache.config_version;
}
catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1%, message: %2%", path_in_cache_vendor, e.what());
// lets use file in resources
if (is_in_rsrc) {
updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", "");
}
continue;
}
Semver version_rsrc = Semver::zero();
try {
if (is_in_rsrc) {
auto vp = VendorProfile::from_ini(path_in_rsrc, false);
version_rsrc = vp.config_version;
}
}
catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1%, message: %2%", path_in_rsrc, e.what());
continue;
}
if (!is_in_rsrc || version_cache > version_rsrc) {
// in case we are installing from cache / vendor. we should also copy index to cache
// This needs to be done now bcs the current one would be missing this version on next start
// dk: Should we copy it to vendor dir too?
auto path_idx_cache_vendor(path_in_cache_vendor);
path_idx_cache_vendor.replace_extension(".idx");
auto path_idx_cache = (p->cache_path / bundle).replace_extension(".idx");
// DK: do this during perform_updates() too?
if (fs::exists(path_idx_cache_vendor))
copy_file_fix(path_idx_cache_vendor, path_idx_cache);
else // Should we dialog this?
BOOST_LOG_TRIVIAL(error) << GUI::format(_L("Couldn't locate idx file %1% when performing updates."), path_idx_cache_vendor.string());
updates.updates.emplace_back(std::move(path_in_cache_vendor), std::move(path_in_vendors), Version(), "", "");
} else {
if (is_in_rsrc)
updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", "");
}
} else {
if (! is_in_rsrc) {
// This should not happen. Instead of an assert, make it crash in Release mode too.
BOOST_LOG_TRIVIAL(error) << "Internal error in PresetUpdater! Terminating the application.";
std::terminate();
}
updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", "");
}
}
return p->perform_updates(std::move(updates), snapshot);
@ -857,4 +1411,9 @@ bool PresetUpdater::version_check_enabled() const
return p->enabled_version_check;
}
void PresetUpdater::update_index_db()
{
p->update_index_db();
}
}

View File

@ -26,7 +26,8 @@ public:
~PresetUpdater();
// If either version check or config updating is enabled, get the appropriate data in the background and cache it.
void sync(PresetBundle *preset_bundle);
void sync(const PresetBundle *preset_bundle);
void cancel_sync();
// If version check is enabled, check if chaced online slic3r version is newer, notify if so.
void slic3r_update_notify();
@ -52,9 +53,11 @@ public:
// Providing old slic3r version upgrade profiles on upgrade of an application even in case
// that the config index installed from the Internet is equal to the index contained in the installation package.
UpdateResult config_update(const Semver &old_slic3r_version, UpdateParams params) const;
void update_index_db();
// "Update" a list of bundles from resources (behaves like an online update).
bool install_bundles_rsrc(std::vector<std::string> bundles, bool snapshot = true) const;
// "Update" a list of bundles from resources or cache/vendor (behaves like an online update).
bool install_bundles_rsrc_or_cache_vendor(std::vector<std::string> bundles, bool snapshot = true) const;
void on_update_notification_confirm();

76
tests/data/U_overhang.obj Normal file
View File

@ -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

View File

@ -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.

View File

@ -1,4 +1,3 @@
#include <unordered_set>
#include <unordered_map>
#include <random>
#include <numeric>
@ -32,13 +31,6 @@ const char *const SUPPORT_TEST_MODELS[] = {
} // namespace
TEST_CASE("Pillar pairhash should be unique", "[SLASupportGeneration]") {
test_pairhash<int, int>();
test_pairhash<int, long>();
test_pairhash<unsigned, unsigned>();
test_pairhash<unsigned, unsigned long>();
}
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>();
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]") {

View File

@ -0,0 +1,377 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <unordered_set>
#include "libslic3r/Execution/ExecutionSeq.hpp"
#include "libslic3r/SLA/SupportTreeUtils.hpp"
#include "libslic3r/SLA/SupportTreeUtilsLegacy.hpp"
// Test pair hash for 'nums' random number pairs.
template <class I, class II> void test_pairhash()
{
const constexpr size_t nums = 1000;
I A[nums] = {0}, B[nums] = {0};
std::unordered_set<I> CH;
std::unordered_map<II, std::pair<I, I>> ints;
std::random_device rd;
std::mt19937 gen(rd());
const I Ibits = int(sizeof(I) * CHAR_BIT);
const II IIbits = int(sizeof(II) * CHAR_BIT);
int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits;
if (std::is_signed<I>::value) bits -= 1;
const I Imin = 0;
const I Imax = I(std::pow(2., bits) - 1);
std::uniform_int_distribution<I> 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<I, II>(a, b);
II hash_ba = Slic3r::sla::pairhash<I, II>(b, a);
REQUIRE(hash_ab == hash_ba);
auto it = ints.find(hash_ab);
if (it != ints.end()) {
REQUIRE((
(it->second.first == a && it->second.second == b) ||
(it->second.first == b && it->second.second == a)
));
} else
ints[hash_ab] = std::make_pair(a, b);
}
}
TEST_CASE("Pillar pairhash should be unique", "[suptreeutils]") {
test_pairhash<int, int>();
test_pairhash<int, long>();
test_pairhash<unsigned, unsigned>();
test_pairhash<unsigned, unsigned long>();
}
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);
}
}

View File

@ -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<sla::SupportPoint> 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));

View File

@ -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 <class I, class II> void test_pairhash()
{
const constexpr size_t nums = 1000;
I A[nums] = {0}, B[nums] = {0};
std::unordered_set<I> CH;
std::unordered_map<II, std::pair<I, I>> ints;
std::random_device rd;
std::mt19937 gen(rd());
const I Ibits = int(sizeof(I) * CHAR_BIT);
const II IIbits = int(sizeof(II) * CHAR_BIT);
int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits;
if (std::is_signed<I>::value) bits -= 1;
const I Imin = 0;
const I Imax = I(std::pow(2., bits) - 1);
std::uniform_int_distribution<I> 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<I, II>(a, b);
II hash_ba = sla::pairhash<I, II>(b, a);
REQUIRE(hash_ab == hash_ba);
auto it = ints.find(hash_ab);
if (it != ints.end()) {
REQUIRE((
(it->second.first == a && it->second.second == b) ||
(it->second.first == b && it->second.second == a)
));
} else
ints[hash_ab] = std::make_pair(a, b);
}
}
// SLA Raster test utils:
using TPixel = uint8_t;