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;
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());
@ -199,6 +130,9 @@ 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.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;
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);
if (nroot.left >= 0) traverse(pc, nroot.left, fn);
if (nroot.right >= 0) traverse(pc, nroot.right, fn);
} 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()) {
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);
}
}
explicit NLoptOpt(StopCriteria stopcr = {}) : m_stopcr(stopcr) {}
assert(false);
return {};
}
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 AlgNLoptCobyla = detail::NLoptAlg<NLOPT_LN_COBYLA>;
using AlgNLoptDIRECT = detail::NLoptAlg<NLOPT_GN_DIRECT>;
using AlgNLoptMLSL = detail::NLoptAlg<NLOPT_GN_MLSL>;
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)
{
// 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();
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;
// 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)
{
// 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};
};
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;
// 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 (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();
}
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()
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,6 +170,19 @@ 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,
@ -121,7 +191,7 @@ bool BranchingTreeBuilder::add_bridge(const branchingtree::Node &from,
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
@ -369,7 +372,6 @@ void DefaultSupportTree::add_pinheads()
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());
@ -438,8 +440,7 @@ void DefaultSupportTree::add_pinheads()
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);
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
@ -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);
@ -599,7 +600,7 @@ void DefaultSupportTree::routing_to_ground()
// 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);
}
}
}

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;
@ -77,11 +77,19 @@ struct SupportTreeConfig
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,6 +67,14 @@ 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;
@ -77,7 +85,6 @@ struct Head: public SupportTreeNode {
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;
@ -104,20 +111,20 @@ 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;
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,15 +155,15 @@ 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];
@ -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.
@ -291,193 +285,6 @@ Hit pinhead_mesh_hit(Ex ex,
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};
}
inline double distance(const SupportPoint &a, const SupportPoint &b)
{
return (a.pos - b.pos).norm();
@ -554,19 +361,17 @@ bool optimize_pinhead_placement(Ex policy,
// 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);
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,
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 Junction &j,
const Vec3d &dir,
double end_r)
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,
// 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 &j,
const Junction &source,
WideningFn &&wideningfn,
const Vec3d &init_dir = DOWN)
{
constexpr unsigned MaxIterationsGlobal = 5000;
constexpr unsigned MaxIterationsLocal = 100;
constexpr double RelScoreDiff = 0.05;
const auto gndlvl = ground_level(sm);
// 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)
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 criteria_loc = criteria;
criteria_loc.max_iterations(MaxIterationsLocal);
criteria_loc.abs_score_diff(EPSILON);
criteria_loc.rel_score_diff(RelScoreDiff);
Optimizer<opt::AlgNLoptMLSL_Subplx> solver(criteria);
solver.set_loc_criteria(criteria_loc);
solver.seed(0); // require repeatability
// 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 downdst = j.pos.z() - ground_level(sm);
double gndlvl = ground_level(sm);
auto wfn = [end_radius, gndlvl](const Ball &src, const Vec3d &dir, double len) {
if (len < EPSILON)
return src.R;
auto res = connect_to_ground(policy, builder, sm, j, init_dir, end_radius);
if (res.first)
return res;
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;
// 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);
return r;
};
Optimizer<AlgNLoptGenetic> solver(get_criteria(sm.cfg).stop_score(1e6));
solver.seed(0); // we want deterministic behavior
static_assert(IsWideningFn<decltype(wfn)>, "Not a widening function");
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} })
);
return deepsearch_ground_connection(policy, sm, source, wfn, init_dir);
}
Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized();
struct DefaultWideningModel {
static constexpr double WIDENING_SCALE = 0.02;
const SupportableMesh &sm;
return connect_to_ground(policy, builder, sm, j, bridgedir, end_radius);
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,6 +40,9 @@ sla::SupportTreeConfig make_support_cfg(const SLAPrintObjectConfig& c)
scfg.enabled = c.supports_enable.getBool();
scfg.tree_type = c.support_tree_type.value;
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;
@ -62,6 +65,38 @@ sla::SupportTreeConfig make_support_cfg(const SLAPrintObjectConfig& c)
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);
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;
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;
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);

View File

@ -73,10 +73,10 @@ using Config::SnapshotDB;
// Configuration data structures extensions needed for the wizard
bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bundle)
bool Bundle::load(fs::path source_path, BundleLocation location, bool ais_prusa_bundle)
{
this->preset_bundle = std::make_unique<PresetBundle>();
this->is_in_resources = ais_in_resources;
this->location = location;
this->is_prusa_bundle = ais_prusa_bundle;
std::string path_string = source_path.string();
@ -104,7 +104,7 @@ bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bu
Bundle::Bundle(Bundle &&other)
: preset_bundle(std::move(other.preset_bundle))
, vendor_profile(other.vendor_profile)
, is_in_resources(other.is_in_resources)
, location(other.location)
, is_prusa_bundle(other.is_prusa_bundle)
{
other.vendor_profile = nullptr;
@ -115,25 +115,35 @@ BundleMap BundleMap::load()
BundleMap res;
const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred();
const auto archive_dir = (boost::filesystem::path(Slic3r::data_dir()) / "cache" / "vendor").make_preferred();
const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
// Load Prusa bundle from the datadir/vendor directory or from datadir/cache/vendor (archive) or from resources/profiles.
auto prusa_bundle_path = (vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini");
auto prusa_bundle_rsrc = false;
BundleLocation prusa_bundle_loc = BundleLocation::IN_VENDOR;
if (! boost::filesystem::exists(prusa_bundle_path)) {
prusa_bundle_path = (archive_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini");
prusa_bundle_loc = BundleLocation::IN_ARCHIVE;
}
if (!boost::filesystem::exists(prusa_bundle_path)) {
prusa_bundle_path = (rsrc_vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini");
prusa_bundle_rsrc = true;
prusa_bundle_loc = BundleLocation::IN_RESOURCES;
}
{
Bundle prusa_bundle;
if (prusa_bundle.load(std::move(prusa_bundle_path), prusa_bundle_rsrc, true))
if (prusa_bundle.load(std::move(prusa_bundle_path), prusa_bundle_loc, true))
res.emplace(PresetBundle::PRUSA_BUNDLE, std::move(prusa_bundle));
}
// Load the other bundles in the datadir/vendor directory
// and then additionally from resources/profiles.
bool is_in_resources = false;
for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) {
for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) {
// and then additionally from datadir/cache/vendor (archive) and resources/profiles.
// Should we concider case where archive has older profiles than resources (shouldnt happen)?
typedef std::pair<const fs::path&, BundleLocation> DirData;
std::vector<DirData> dir_list { {vendor_dir, BundleLocation::IN_VENDOR}, {archive_dir, BundleLocation::IN_ARCHIVE}, {rsrc_vendor_dir, BundleLocation::IN_RESOURCES} };
for ( auto dir : dir_list) {
if (!fs::exists(dir.first))
continue;
for (const auto &dir_entry : boost::filesystem::directory_iterator(dir.first)) {
if (Slic3r::is_ini_file(dir_entry)) {
std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".ini" part
@ -141,12 +151,10 @@ BundleMap BundleMap::load()
if (res.find(id) != res.end()) { continue; }
Bundle bundle;
if (bundle.load(dir_entry.path(), is_in_resources))
if (bundle.load(dir_entry.path(), dir.second))
res.emplace(std::move(id), std::move(bundle));
}
}
is_in_resources = true;
}
return res;
@ -220,29 +228,41 @@ PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxSt
bool is_variants = false;
const fs::path vendor_dir_path = (fs::path(Slic3r::data_dir()) / "vendor").make_preferred();
const fs::path cache_dir_path = (fs::path(Slic3r::data_dir()) / "cache").make_preferred();
const fs::path rsrc_dir_path = (fs::path(resources_dir()) / "profiles").make_preferred();
for (const auto &model : models) {
if (! filter(model)) { continue; }
wxBitmap bitmap;
int bitmap_width = 0;
auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width)->bool {
if (wxFileExists(bitmap_file)) {
auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width) {
bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG);
bitmap_width = bitmap.GetWidth();
return true;
}
return false;
};
if (!load_bitmap(GUI::from_u8(Slic3r::data_dir() + "/vendor/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) {
if (!load_bitmap(GUI::from_u8(Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) {
bool found = false;
for (const fs::path& res : { rsrc_dir_path / vendor.id / model.thumbnail
, vendor_dir_path / vendor.id / model.thumbnail
, cache_dir_path / vendor.id / model.thumbnail })
{
if (!fs::exists(res))
continue;
load_bitmap(GUI::from_u8(res.string()), bitmap, bitmap_width);
found = true;
break;
}
if (!found) {
BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead")
% (Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png")
% model.thumbnail
% vendor.id
% model.id;
load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width);
}
}
auto *title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT);
wxStaticText* title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT);
title->SetFont(font_name);
const int wrap_width = std::max((int)MODEL_MIN_WRAP, bitmap_width);
title->Wrap(wrap_width);
@ -255,7 +275,7 @@ PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxSt
titles.push_back(title);
auto *bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap);
wxStaticBitmap* bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap);
bitmaps.push_back(bitmap_widget);
auto *variants_panel = new wxPanel(this);
@ -278,7 +298,7 @@ PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxSt
is_variants = true;
}
auto *cbox = new Checkbox(variants_panel, label, model_id, variant.name);
Checkbox* cbox = new Checkbox(variants_panel, label, model_id, variant.name);
i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox);
const bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name);
@ -620,6 +640,7 @@ void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason)
const std::string PageMaterials::EMPTY;
const std::string PageMaterials::TEMPLATES = "templates";
PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name)
: ConfigWizardPage(parent, std::move(title), std::move(shortname))
@ -727,6 +748,11 @@ void PageMaterials::reload_presets()
clear();
list_printer->append(_L("(All)"), &EMPTY);
const AppConfig* app_config = wxGetApp().app_config;
if (materials->technology == T_FFF && app_config->get("no_templates") == "0")
list_printer->append(_L("(Templates)"), &TEMPLATES);
//list_printer->SetLabelMarkup("<b>bald</b>");
for (const Preset* printer : materials->printers) {
list_printer->append(printer->name, &printer->name);
@ -758,8 +784,12 @@ void PageMaterials::set_compatible_printers_html_window(const std::vector<std::s
const auto text_clr = wxGetApp().get_label_clr_default();
const auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue()));
const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue()));
wxString first_line = format_wxstr(_L("%1% marked with <b>*</b> are <b>not</b> compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials"));
wxString text;
if (materials->technology == T_FFF && template_shown) {
text = format_wxstr(_L("%1% visible for <b>(\"Template\")</b> printer are universal profiles available for all printers. These might not be compatible with your printer."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials"));
} else {
wxString first_line = format_wxstr(_L("%1% marked with <b>*</b> are <b>not</b> compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials"));
if (all_printers) {
wxString second_line = format_wxstr(_L("All installed printers are compatible with the selected %1%."), materials->technology == T_FFF ? _L("filament") : _L("SLA material"));
text = wxString::Format(
@ -780,7 +810,8 @@ void PageMaterials::set_compatible_printers_html_window(const std::vector<std::s
, first_line
, second_line
);
} else {
}
else {
wxString second_line;
if (!printer_names.empty())
second_line = (materials->technology == T_FFF ?
@ -819,6 +850,8 @@ void PageMaterials::set_compatible_printers_html_window(const std::vector<std::s
"</html>"
);
}
}
wxFont font = get_default_font_for_dpi(this, get_dpi_for_window(this));
const int fs = font.GetPointSize();
@ -863,6 +896,8 @@ void PageMaterials::on_material_highlighted(int sel_material)
std::vector<std::string> names;
for (const Preset* printer : materials->printers) {
for (const Preset* material : matching_materials) {
if (material->vendor && material->vendor->templates_profile)
continue;
if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) {
names.push_back(printer->name);
break;
@ -880,6 +915,8 @@ void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected
wxArrayInt sel_printers;
int sel_printers_count = list_printer->GetSelections(sel_printers);
bool templates_available = list_printer->size() > 1 && list_printer->get_data(1) == TEMPLATES;
// Does our wxWidgets version support operator== for wxArrayInt ?
// https://github.com/prusa3d/PrusaSlicer/issues/5152#issuecomment-787208614
#if wxCHECK_VERSION(3, 1, 1)
@ -895,13 +932,14 @@ void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected
};
if (!are_equal(sel_printers, sel_printers_prev)) {
#endif
template_shown = false;
// Refresh type list
list_type->Clear();
list_type->append(_L("(All)"), &EMPTY);
if (sel_printers_count > 0) {
if (sel_printers_count > 1) {
// If all is selected with other printers
// unselect "all" or all printers depending on last value
// same with "templates"
if (sel_printers[0] == 0 && sel_printers_count > 1) {
if (last_selected_printer == 0) {
list_printer->SetSelection(wxNOT_FOUND);
@ -911,7 +949,18 @@ void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected
sel_printers_count = list_printer->GetSelections(sel_printers);
}
}
if (sel_printers[0] != 0) {
if (materials->technology == T_FFF && templates_available && (sel_printers[0] == 1 || sel_printers[1] == 1) && sel_printers_count > 1) {
if (last_selected_printer == 1) {
list_printer->SetSelection(wxNOT_FOUND);
list_printer->SetSelection(1);
}
else if (last_selected_printer != 0) {
list_printer->SetSelection(1, false);
sel_printers_count = list_printer->GetSelections(sel_printers);
}
}
}
if (sel_printers_count > 0 && sel_printers[0] != 0 && ((materials->technology == T_FFF && templates_available && sel_printers[0] != 1) || materials->technology != T_FFF || !templates_available)) {
for (int i = 0; i < sel_printers_count; i++) {
const std::string& printer_name = list_printer->get_data(sel_printers[i]);
const Preset* printer = nullptr;
@ -921,20 +970,35 @@ void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected
break;
}
}
materials->filter_presets(printer, EMPTY, EMPTY, [this](const Preset* p) {
materials->filter_presets(printer, printer_name, EMPTY, EMPTY, [this](const Preset* p) {
const std::string& type = this->materials->get_type(p);
if (list_type->find(type) == wxNOT_FOUND) {
list_type->append(type, &type);
}
});
}
} else {
}
else if (sel_printers_count > 0 && last_selected_printer == 0) {
//clear selection except "ALL"
list_printer->SetSelection(wxNOT_FOUND);
list_printer->SetSelection(0);
sel_printers_count = list_printer->GetSelections(sel_printers);
materials->filter_presets(nullptr, EMPTY, EMPTY, [this](const Preset* p) {
materials->filter_presets(nullptr, EMPTY, EMPTY, EMPTY, [this](const Preset* p) {
const std::string& type = this->materials->get_type(p);
if (list_type->find(type) == wxNOT_FOUND) {
list_type->append(type, &type);
}
});
}
else if (materials->technology == T_FFF && templates_available && sel_printers_count > 0 && last_selected_printer == 1) {
//clear selection except "TEMPLATES"
list_printer->SetSelection(wxNOT_FOUND);
list_printer->SetSelection(1);
sel_printers_count = list_printer->GetSelections(sel_printers);
template_shown = true;
materials->filter_presets(nullptr, TEMPLATES, EMPTY, EMPTY,
[this](const Preset* p) {
const std::string& type = this->materials->get_type(p);
if (list_type->find(type) == wxNOT_FOUND) {
list_type->append(type, &type);
@ -942,7 +1006,6 @@ void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected
});
}
sort_list_data(list_type, true, true);
}
sel_printers_prev = sel_printers;
sel_type = 0;
@ -971,7 +1034,7 @@ void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected
break;
}
}
materials->filter_presets(printer, type, EMPTY, [this](const Preset* p) {
materials->filter_presets(printer, printer_name, type, EMPTY, [this](const Preset* p) {
const std::string& vendor = this->materials->get_vendor(p);
if (list_vendor->find(vendor) == wxNOT_FOUND) {
list_vendor->append(vendor, &vendor);
@ -996,7 +1059,7 @@ void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected
if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) {
const std::string& type = list_type->get_data(sel_type);
const std::string& vendor = list_vendor->get_data(sel_vendor);
// finst printer preset
// first printer preset
std::vector<ProfilePrintData> to_list;
for (int i = 0; i < sel_printers_count; i++) {
const std::string& printer_name = list_printer->get_data(sel_printers[i]);
@ -1007,15 +1070,14 @@ void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected
break;
}
}
materials->filter_presets(printer, type, vendor, [this, &to_list](const Preset* p) {
materials->filter_presets(printer, printer_name, type, vendor, [this, &to_list](const Preset* p) {
const std::string& section = materials->appconfig_section();
bool checked = wizard_p()->appconfig_new.has(section, p->name);
bool was_checked = false;
int cur_i = list_profile->find(p->alias);
if (cur_i == wxNOT_FOUND) {
cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) ? "" : " *"), &p->alias);
cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) || template_shown ? "" : " *"), &p->alias);
to_list.emplace_back(p->alias, materials->get_omnipresent(p), checked);
}
else {
@ -1053,10 +1115,15 @@ void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool mat
std::vector<std::reference_wrapper<const std::string>> prusa_profiles;
std::vector<std::reference_wrapper<const std::string>> other_profiles;
bool add_TEMPLATES_item = false;
for (int i = 0 ; i < list->size(); ++i) {
const std::string& data = list->get_data(i);
if (data == EMPTY) // do not sort <all> item
continue;
if (data == TEMPLATES) {// do not sort <templates> item
add_TEMPLATES_item = true;
continue;
}
if (!material_type_ordering && data.find("Prusa") != std::string::npos)
prusa_profiles.push_back(data);
else
@ -1095,10 +1162,13 @@ void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool mat
list->Clear();
if (add_All_item)
list->append(_L("(All)"), &EMPTY);
if (materials->technology == T_FFF && add_TEMPLATES_item)
list->append(_L("(Templates)"), &TEMPLATES);
for (const auto& item : prusa_profiles)
list->append(item, &const_cast<std::string&>(item.get()));
for (const auto& item : other_profiles)
list->append(item, &const_cast<std::string&>(item.get()));
}
void PageMaterials::sort_list_data(PresetList* list, const std::vector<ProfilePrintData>& data)
@ -1125,11 +1195,11 @@ void PageMaterials::sort_list_data(PresetList* list, const std::vector<ProfilePr
});
list->Clear();
for (size_t i = 0; i < prusa_profiles.size(); ++i) {
list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent ? "" : " *"), &const_cast<std::string&>(prusa_profiles[i].name.get()));
list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent || template_shown ? "" : " *"), &const_cast<std::string&>(prusa_profiles[i].name.get()));
list->Check(i, prusa_profiles[i].checked);
}
for (size_t i = 0; i < other_profiles.size(); ++i) {
list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent ? "" : " *"), &const_cast<std::string&>(other_profiles[i].name.get()));
list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent || template_shown ? "" : " *"), &const_cast<std::string&>(other_profiles[i].name.get()));
list->Check(i + prusa_profiles.size(), other_profiles[i].checked);
}
}
@ -1139,6 +1209,15 @@ void PageMaterials::select_material(int i)
const bool checked = list_profile->IsChecked(i);
const std::string& alias_key = list_profile->get_data(i);
if (checked && template_shown && !notification_shown) {
notification_shown = true;
wxString message = _L("You have selected template filament. Please note that these filaments are available for all printers but are NOT certain to be compatible with your printer. Do you still wish to have this filament selected?\n(This message won't be displayed again.)");
MessageDialog msg(this, message, _L("Notice"), wxYES_NO);
if (msg.ShowModal() == wxID_NO) {
list_profile->Check(i, false);
return;
}
}
wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked);
}
@ -1543,6 +1622,8 @@ PageVendors::PageVendors(ConfigWizard *parent)
for (const auto &pair : wizard_p()->bundles) {
const VendorProfile *vendor = pair.second.vendor_profile;
if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; }
if (vendor && vendor->templates_profile)
continue;
auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name);
cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) {
@ -2199,11 +2280,17 @@ void ConfigWizard::priv::load_pages()
// Filaments & Materials
if (any_fff_selected) { index->add_page(page_filaments); }
// Filaments page if only custom printer is selected
const AppConfig* app_config = wxGetApp().app_config;
if (!any_fff_selected && (custom_printer_selected || custom_printer_in_bundle) && (app_config->get("no_templates") == "0")) {
update_materials(T_ANY);
index->add_page(page_filaments);
}
}
if (any_sla_selected) { index->add_page(page_sla_materials); }
// there should to be selected at least one printer
btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected);
btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected || custom_printer_in_bundle);
index->add_page(page_update);
index->add_page(page_downloader);
@ -2265,6 +2352,13 @@ void ConfigWizard::priv::load_vendors()
}
}
for (const auto& printer : wxGetApp().preset_bundle->printers) {
if (!printer.is_default && !printer.is_system && printer.is_visible) {
custom_printer_in_bundle = true;
break;
}
}
// Initialize the is_visible flag in printer Presets
for (auto &pair : bundles) {
pair.second.preset_bundle->load_installed_printers(appconfig_new);
@ -2386,7 +2480,7 @@ void ConfigWizard::priv::set_run_reason(RunReason run_reason)
void ConfigWizard::priv::update_materials(Technology technology)
{
if (any_fff_selected && (technology & T_FFF)) {
if ((any_fff_selected || custom_printer_in_bundle || custom_printer_selected) && (technology & T_FFF)) {
filaments.clear();
aliases_fff.clear();
// Iterate filaments in all bundles
@ -2409,11 +2503,22 @@ void ConfigWizard::priv::update_materials(Technology technology)
filaments.add_printer(&printer);
}
}
// template filament bundle has no printers - filament would be never added
if(pair.second.vendor_profile&& pair.second.vendor_profile->templates_profile && pair.second.preset_bundle->printers.begin() == pair.second.preset_bundle->printers.end())
{
if (!filaments.containts(&filament)) {
filaments.push(&filament);
if (!filament.alias.empty())
aliases_fff[filament.alias].insert(filament.name);
}
}
}
}
// count compatible printers
for (const auto& preset : filaments.presets) {
// skip template filaments
if (preset->vendor && preset->vendor->templates_profile)
continue;
const auto filter = [preset](const std::pair<std::string, size_t> element) {
return preset->alias == element.first;
@ -2421,17 +2526,19 @@ void ConfigWizard::priv::update_materials(Technology technology)
if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) {
continue;
}
// find all aliases (except templates)
std::vector<size_t> idx_with_same_alias;
for (size_t i = 0; i < filaments.presets.size(); ++i) {
if (preset->alias == filaments.presets[i]->alias)
if (preset->alias == filaments.presets[i]->alias && ((filaments.presets[i]->vendor && !filaments.presets[i]->vendor->templates_profile) || !filaments.presets[i]->vendor))
idx_with_same_alias.push_back(i);
}
// check compatibility with each printer
size_t counter = 0;
for (const auto& printer : filaments.printers) {
if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF)
continue;
bool compatible = false;
// Test otrher materials with same alias
// Test other materials with same alias
for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) {
const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]);
const Preset& prntr = *printer;
@ -2694,6 +2801,21 @@ bool ConfigWizard::priv::check_and_install_missing_materials(Technology technolo
has_material = true;
break;
}
// find if preset.first is part of the templates profile (up is searching if preset.first is part of printer vendor preset)
for (const auto& bp : bundles) {
if (!bp.second.preset_bundle->vendors.empty() && bp.second.preset_bundle->vendors.begin()->second.templates_profile) {
const PresetCollection& template_materials = bp.second.preset_bundle->materials(technology);
const Preset* template_material = template_materials.find_preset(preset.first, false);
if (template_material && is_compatible_with_printer(PresetWithVendorProfile(*template_material, &bp.second.preset_bundle->vendors.begin()->second), PresetWithVendorProfile(printer, nullptr))) {
has_material = true;
break;
}
}
}
if (has_material)
break;
}
}
if (! has_material)
@ -2702,6 +2824,27 @@ bool ConfigWizard::priv::check_and_install_missing_materials(Technology technolo
}
}
}
// todo: just workaround so template_profile_selected wont get to false after this function is called for SLA
// this will work unltil there are no SLA template filaments
if (technology == ptFFF) {
// template_profile_selected check
template_profile_selected = false;
for (const auto& bp : bundles) {
if (!bp.second.preset_bundle->vendors.empty() && bp.second.preset_bundle->vendors.begin()->second.templates_profile) {
for (const auto& preset : appconfig_presets) {
const PresetCollection& template_materials = bp.second.preset_bundle->materials(technology);
const Preset* template_material = template_materials.find_preset(preset.first, false);
if (template_material){
template_profile_selected = true;
break;
}
}
if (template_profile_selected) {
break;
}
}
}
}
assert(printer_models_without_material.empty() || only_for_model_id.empty() || only_for_model_id == (*printer_models_without_material.begin())->id);
return printer_models_without_material;
};
@ -2852,10 +2995,10 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese
if (!check_unsaved_preset_changes)
act_btns |= ActionButtons::SAVE;
// Install bundles from resources if needed:
// Install bundles from resources or cache / vendor if needed:
std::vector<std::string> install_bundles;
for (const auto &pair : bundles) {
if (! pair.second.is_in_resources) { continue; }
if (pair.second.location == BundleLocation::IN_VENDOR) { continue; }
if (pair.second.is_prusa_bundle) {
// Always install Prusa bundle, because it has a lot of filaments/materials
@ -2865,7 +3008,14 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese
}
const auto vendor = enabled_vendors.find(pair.first);
if (vendor == enabled_vendors.end()) { continue; }
if (vendor == enabled_vendors.end()) {
// vendor not found
// if templates vendor and needs to be installed, add it
// then continue
if (template_profile_selected && pair.second.vendor_profile && pair.second.vendor_profile->templates_profile)
install_bundles.emplace_back(pair.first);
continue;
}
size_t size_sum = 0;
for (const auto &model : vendor->second) { size_sum += model.second.size(); }
@ -2915,12 +3065,14 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese
return false;
if (install_bundles.size() > 0) {
// Install bundles from resources.
// Install bundles from resources or cache / vendor.
// Don't create snapshot - we've already done that above if applicable.
if (! updater->install_bundles_rsrc(std::move(install_bundles), false))
bool install_result = updater->install_bundles_rsrc_or_cache_vendor(std::move(install_bundles), false);
if (!install_result)
return false;
} else {
BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources";
BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources or cache / vendor";
}
if (page_welcome->reset_user_profile()) {

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.")
@ -1282,6 +1283,7 @@ bool GUI_App::on_init_inner()
#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();
// on first draw set focus on selected font
if (set_selection_focus && is_selected)
ImGui::SetScrollHereY();
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();
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()
}
}
wxGetApp().CallAfter([this]() {
wxGetApp().plater()->reslice_FFF_until_step(posSupportSpotsSearch, *m_c->selection_info()->model_object(), false);
this->waiting_for_autogenerated_supports = true;
wxGetApp().CallAfter([this]() { reslice_FDM_supports(); });
});
}
}

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,11 +884,19 @@ void PlaterPresetComboBox::update()
const std::string name = preset.alias.empty() ? preset.name : preset.alias;
if (preset.is_default || preset.is_system) {
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
{
nonsys_presets.emplace(get_preset_name(preset), bmp);
@ -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()));
@ -1079,11 +1101,19 @@ void TabPresetComboBox::update()
assert(bmp);
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
{
std::pair<wxBitmapBundle*, bool> pair(bmp, is_enabled);
@ -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()));
@ -1106,6 +1137,18 @@ void TabPresetComboBox::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, 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

@ -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,45 +261,207 @@ 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
{
@ -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();
@ -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,7 +1058,35 @@ 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;
@ -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,9 +1314,67 @@ 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");
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();
@ -53,8 +54,10 @@ public:
// 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;
// "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;
void update_index_db();
// "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;