From 4293a68aaa2966a2e708de905594f312e45f72bd Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Thu, 4 Mar 2021 14:50:59 +0100 Subject: [PATCH 01/17] Reverting to old rotation optimizer object-function. Keep the performance optimizations though --- src/libslic3r/SLA/Rotfinder.cpp | 223 ++++--------------------- src/slic3r/GUI/Jobs/RotoptimizeJob.cpp | 14 -- 2 files changed, 31 insertions(+), 206 deletions(-) diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index 702690c19..6caea2393 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -13,33 +13,11 @@ #include +#include + namespace Slic3r { namespace sla { -inline bool is_on_floor(const SLAPrintObject &mo) -{ - auto opt_elevation = mo.config().support_object_elevation.getFloat(); - auto opt_padaround = mo.config().pad_around_object.getBool(); - - return opt_elevation < EPSILON || opt_padaround; -} - -// Find transformed mesh ground level without copy and with parallel reduce. -double find_ground_level(const TriangleMesh &mesh, - const Transform3d & tr, - size_t threads) -{ - size_t vsize = mesh.its.vertices.size(); - - auto minfn = [](double a, double b) { return std::min(a, b); }; - - auto accessfn = [&mesh, &tr] (size_t vi) { - return (tr * mesh.its.vertices[vi].template cast()).z(); - }; - - double zmin = std::numeric_limits::max(); - size_t granularity = vsize / threads; - return ccr_par::reduce(size_t(0), vsize, zmin, minfn, accessfn, granularity); -} +namespace { // Get the vertices of a triangle directly in an array of 3 points std::array get_triangle_vertices(const TriangleMesh &mesh, @@ -74,33 +52,13 @@ struct Facestats { } }; -inline const Vec3d DOWN = {0., 0., -1.}; -constexpr double POINTS_PER_UNIT_AREA = 1.; - -// The score function for a particular face -inline double get_score(const Facestats &fc) -{ - // Simply get the angle (acos of dot product) between the face normal and - // the DOWN vector. - double phi = 1. - std::acos(fc.normal.dot(DOWN)) / PI; - - // Only consider faces that have have slopes below 90 deg: - phi = phi * (phi > 0.5); - - // Make the huge slopes more significant than the smaller slopes - phi = phi * phi * phi; - - // Multiply with the area of the current face - return fc.area * POINTS_PER_UNIT_AREA * phi; -} - template double sum_score(AccessFn &&accessfn, size_t facecount, size_t Nthreads) { double initv = 0.; - auto mergefn = std::plus{}; - size_t grainsize = facecount / Nthreads; - size_t from = 0, to = facecount; + auto mergefn = [](double a, double b) { return a + b; }; + size_t grainsize = facecount / Nthreads; + size_t from = 0, to = facecount; return ccr_par::reduce(from, to, initv, mergefn, accessfn, grainsize); } @@ -112,36 +70,18 @@ double get_model_supportedness(const TriangleMesh &mesh, const Transform3d &tr) auto accessfn = [&mesh, &tr](size_t fi) { Facestats fc{get_transformed_triangle(mesh, tr, fi)}; - return get_score(fc); + + // We should score against the alignment with the reference planes + return std::abs(fc.normal.dot(Vec3d::UnitX())) + + std::abs(fc.normal.dot(Vec3d::UnitY())) + + std::abs(fc.normal.dot(Vec3d::UnitZ())); }; size_t facecount = mesh.its.indices.size(); size_t Nthreads = std::thread::hardware_concurrency(); - return sum_score(accessfn, facecount, Nthreads) / facecount; -} + double S = sum_score(accessfn, facecount, Nthreads); -double get_model_supportedness_onfloor(const TriangleMesh &mesh, - const Transform3d & tr) -{ - if (mesh.its.vertices.empty()) return std::nan(""); - - size_t Nthreads = std::thread::hardware_concurrency(); - - double zmin = find_ground_level(mesh, tr, Nthreads); - double zlvl = zmin + 0.1; // Set up a slight tolerance from z level - - auto accessfn = [&mesh, &tr, zlvl](size_t fi) { - std::array tri = get_transformed_triangle(mesh, tr, fi); - Facestats fc{tri}; - - if (tri[0].z() <= zlvl && tri[1].z() <= zlvl && tri[2].z() <= zlvl) - return -fc.area * POINTS_PER_UNIT_AREA; - - return get_score(fc); - }; - - size_t facecount = mesh.its.indices.size(); - return sum_score(accessfn, facecount, Nthreads) / facecount; + return S / facecount; } using XYRotation = std::array; @@ -155,88 +95,7 @@ Transform3d to_transform3d(const XYRotation &rot) return rt; } -XYRotation from_transform3d(const Transform3d &tr) -{ - Vec3d rot3d = Geometry::Transformation {tr}.get_rotation(); - return {rot3d.x(), rot3d.y()}; -} - -// Find the best score from a set of function inputs. Evaluate for every point. -template -std::array find_min_score(Fn &&fn, It from, It to, StopCond &&stopfn) -{ - std::array ret = {}; - - double score = std::numeric_limits::max(); - - size_t Nthreads = std::thread::hardware_concurrency(); - size_t dist = std::distance(from, to); - std::vector scores(dist, score); - - ccr_par::for_each(size_t(0), dist, [&stopfn, &scores, &fn, &from](size_t i) { - if (stopfn()) return; - - scores[i] = fn(*(from + i)); - }, dist / Nthreads); - - auto it = std::min_element(scores.begin(), scores.end()); - - if (it != scores.end()) ret = *(from + std::distance(scores.begin(), it)); - - return ret; -} - -// collect the rotations for each face of the convex hull -std::vector get_chull_rotations(const TriangleMesh &mesh, size_t max_count) -{ - TriangleMesh chull = mesh.convex_hull_3d(); - chull.require_shared_vertices(); - double chull2d_area = chull.convex_hull().area(); - double area_threshold = chull2d_area / (scaled(1e3) * scaled(1.)); - - size_t facecount = chull.its.indices.size(); - - struct RotArea { XYRotation rot; double area; }; - - auto inputs = reserve_vector(facecount); - - auto rotcmp = [](const RotArea &r1, const RotArea &r2) { - double xdiff = r1.rot[X] - r2.rot[X], ydiff = r1.rot[Y] - r2.rot[Y]; - return std::abs(xdiff) < EPSILON ? ydiff < 0. : xdiff < 0.; - }; - - auto eqcmp = [](const XYRotation &r1, const XYRotation &r2) { - double xdiff = r1[X] - r2[X], ydiff = r1[Y] - r2[Y]; - return std::abs(xdiff) < EPSILON && std::abs(ydiff) < EPSILON; - }; - - for (size_t fi = 0; fi < facecount; ++fi) { - Facestats fc{get_triangle_vertices(chull, fi)}; - - if (fc.area > area_threshold) { - auto q = Eigen::Quaterniond{}.FromTwoVectors(fc.normal, DOWN); - XYRotation rot = from_transform3d(Transform3d::Identity() * q); - RotArea ra = {rot, fc.area}; - - auto it = std::lower_bound(inputs.begin(), inputs.end(), ra, rotcmp); - - if (it == inputs.end() || !eqcmp(it->rot, rot)) - inputs.insert(it, ra); - } - } - - inputs.shrink_to_fit(); - if (!max_count) max_count = inputs.size(); - std::sort(inputs.begin(), inputs.end(), - [](const RotArea &ra, const RotArea &rb) { - return ra.area > rb.area; - }); - - auto ret = reserve_vector(std::min(max_count, inputs.size())); - for (const RotArea &ra : inputs) ret.emplace_back(ra.rot); - - return ret; -} +} // namespace Vec2d find_best_rotation(const SLAPrintObject & po, float accuracy, @@ -267,45 +126,26 @@ Vec2d find_best_rotation(const SLAPrintObject & po, statuscb(unsigned(++status * 100.0/max_tries) ); }; - // Different search methods have to be used depending on the model elevation - if (is_on_floor(po)) { + // Preparing the optimizer. + size_t gridsize = std::sqrt(max_tries); + opt::Optimizer solver(opt::StopCriteria{} + .max_iterations(max_tries) + .stop_condition(stopcond), + gridsize); - std::vector inputs = get_chull_rotations(mesh, max_tries); - max_tries = inputs.size(); + // We are searching rotations around only two axes x, y. Thus the + // problem becomes a 2 dimensional optimization task. + // We can specify the bounds for a dimension in the following way: + auto bounds = opt::bounds({ {-PI/2, PI/2}, {-PI/2, PI/2} }); - // If the model can be placed on the bed directly, we only need to - // check the 3D convex hull face rotations. - - auto objfn = [&mesh, &statusfn](const XYRotation &rot) { + auto result = solver.to_max().optimize( + [&mesh, &statusfn] (const XYRotation &rot) + { statusfn(); - Transform3d tr = to_transform3d(rot); - return get_model_supportedness_onfloor(mesh, tr); - }; + return get_model_supportedness(mesh, to_transform3d(rot)); + }, opt::initvals({0., 0.}), bounds); - rot = find_min_score<2>(objfn, inputs.begin(), inputs.end(), stopcond); - } else { - // Preparing the optimizer. - size_t gridsize = std::sqrt(max_tries); // 2D grid has gridsize^2 calls - opt::Optimizer solver(opt::StopCriteria{} - .max_iterations(max_tries) - .stop_condition(stopcond), - gridsize); - - // We are searching rotations around only two axes x, y. Thus the - // problem becomes a 2 dimensional optimization task. - // We can specify the bounds for a dimension in the following way: - auto bounds = opt::bounds({ {-PI, PI}, {-PI, PI} }); - - auto result = solver.to_min().optimize( - [&mesh, &statusfn] (const XYRotation &rot) - { - statusfn(); - return get_model_supportedness(mesh, to_transform3d(rot)); - }, opt::initvals({0., 0.}), bounds); - - // Save the result and fck off - rot = result.optimum; - } + rot = result.optimum; return {rot[0], rot[1]}; } @@ -315,8 +155,7 @@ double get_model_supportedness(const SLAPrintObject &po, const Transform3d &tr) TriangleMesh mesh = po.model_object()->raw_mesh(); mesh.require_shared_vertices(); - return is_on_floor(po) ? get_model_supportedness_onfloor(mesh, tr) : - get_model_supportedness(mesh, tr); + return get_model_supportedness(mesh, tr); } }} // namespace Slic3r::sla diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index 978ccf8fc..ac737cafb 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -21,20 +21,6 @@ void RotoptimizeJob::process() if (!o || !po) return; - TriangleMesh mesh = o->raw_mesh(); - mesh.require_shared_vertices(); - -// for (auto inst : o->instances) { -// Transform3d tr = Transform3d::Identity(); -// tr.rotate(Eigen::AngleAxisd(inst->get_rotation(Z), Vec3d::UnitZ())); -// tr.rotate(Eigen::AngleAxisd(inst->get_rotation(Y), Vec3d::UnitY())); -// tr.rotate(Eigen::AngleAxisd(inst->get_rotation(X), Vec3d::UnitX())); - -// double score = sla::get_model_supportedness(*po, tr); - -// std::cout << "Model supportedness before: " << score << std::endl; -// } - Vec2d r = sla::find_best_rotation(*po, 0.75f, [this](unsigned s) { if (s < 100) From 7760d3fbc43254327ed06b9db4b6853c60c4aa0b Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Wed, 17 Mar 2021 19:42:58 +0100 Subject: [PATCH 02/17] Add new execution framework Inspired by std::execution --- src/libslic3r/CMakeLists.txt | 3 + src/libslic3r/Execution/Execution.hpp | 100 +++++++++++++++++++ src/libslic3r/Execution/ExecutionSeq.hpp | 84 ++++++++++++++++ src/libslic3r/Execution/ExecutionTBB.hpp | 77 +++++++++++++++ src/libslic3r/MTUtils.hpp | 9 +- src/libslic3r/SLA/Concurrency.hpp | 120 ++++------------------- 6 files changed, 285 insertions(+), 108 deletions(-) create mode 100644 src/libslic3r/Execution/Execution.hpp create mode 100644 src/libslic3r/Execution/ExecutionSeq.hpp create mode 100644 src/libslic3r/Execution/ExecutionTBB.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 4a762f7e1..2abe94656 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -219,6 +219,9 @@ add_library(libslic3r STATIC SimplifyMeshImpl.hpp SimplifyMesh.cpp MarchingSquares.hpp + Execution/Execution.hpp + Execution/ExecutionSeq.hpp + Execution/ExecutionTBB.hpp Optimize/Optimizer.hpp Optimize/NLoptOptimizer.hpp Optimize/BruteforceOptimizer.hpp diff --git a/src/libslic3r/Execution/Execution.hpp b/src/libslic3r/Execution/Execution.hpp new file mode 100644 index 000000000..809cc45d3 --- /dev/null +++ b/src/libslic3r/Execution/Execution.hpp @@ -0,0 +1,100 @@ +#ifndef EXECUTION_HPP +#define EXECUTION_HPP + +#include +#include +#include +#include + +#include "libslic3r/libslic3r.h" + +namespace Slic3r { + +template +using remove_cvref_t = std::remove_reference_t>; + +// Override for valid execution policies +template struct IsExecutionPolicy_ : public std::false_type {}; + +template constexpr bool IsExecutionPolicy = + IsExecutionPolicy_>::value; + +template +using ExecutionPolicyOnly = std::enable_if_t, T>; + +namespace execution { + +// This struct needs to be specialized for each execution policy. +// See ExecutionSeq.hpp and ExecutionTBB.hpp for example. +template struct Traits {}; + +template using AsTraits = Traits>; + +// Each execution policy should declare two types of mutexes. A a spin lock and +// a blocking mutex. +template using SpinningMutex = typename Traits::SpinningMutex; +template using BlockingMutex = typename Traits::BlockingMutex; + +// Query the available threads for concurrency. +template > +size_t max_concurrency(const EP &ep) +{ + return AsTraits::max_concurrency(ep); +} + +// foreach loop with the execution policy passed as argument. Granularity can +// be specified explicitly. max_concurrency() can be used for optimal results. +template> +void for_each(const EP &ep, It from, It to, Fn &&fn, size_t granularity = 1) +{ + AsTraits::for_each(ep, from, to, std::forward(fn), granularity); +} + +// A reduce operation with the execution policy passed as argument. +// mergefn has T(const T&, const T&) signature +// accessfn has T(I) signature if I is an integral type and +// T(const I::value_type &) if I is an iterator type. +template > +T reduce(const EP & ep, + I from, + I to, + const T & init, + MergeFn && mergefn, + AccessFn &&accessfn, + size_t granularity = 1) +{ + return AsTraits::reduce(ep, from, to, init, + std::forward(mergefn), + std::forward(accessfn), + granularity); +} + +// An overload of reduce method to be used with iterators as 'from' and 'to' +// arguments. +template, + class = IteratorOnly > +T reduce(const EP &ep, + I from, + I to, + const T & init, + MergeFn &&mergefn, + size_t granularity = 1) +{ + return reduce( + ep, from, to, init, std::forward(mergefn), + [](typename I::value_type &i) { return i; }, granularity); +} + +} // namespace execution_policy +} // namespace Slic3r + +#endif // EXECUTION_HPP diff --git a/src/libslic3r/Execution/ExecutionSeq.hpp b/src/libslic3r/Execution/ExecutionSeq.hpp new file mode 100644 index 000000000..321d65631 --- /dev/null +++ b/src/libslic3r/Execution/ExecutionSeq.hpp @@ -0,0 +1,84 @@ +#ifndef EXECUTIONSEQ_HPP +#define EXECUTIONSEQ_HPP + +#ifdef PRUSASLICER_USE_EXECUTION_STD // Conflicts with our version of TBB +#include +#endif + +#include "Execution.hpp" + +namespace Slic3r { + +// Execution policy implementing dummy sequential algorithms +struct ExecutionSeq {}; + +template<> struct IsExecutionPolicy_ : public std::true_type {}; + +static constexpr ExecutionSeq ex_seq = {}; + +template struct IsSequentialEP_ { static constexpr bool value = false; }; + +template<> struct IsSequentialEP_: public std::true_type {}; +#ifdef PRUSASLICER_USE_EXECUTION_STD +template<> struct IsExecutionPolicy_: public std::true_type {}; +template<> struct IsSequentialEP_: public std::true_type {}; +#endif + +template +constexpr bool IsSequentialEP = IsSequentialEP_>::value; + +template +using SequentialEPOnly = std::enable_if_t, R>; + +template +struct execution::Traits> { +private: + struct _Mtx { inline void lock() {} inline void unlock() {} }; + + template + static IteratorOnly loop_(It from, It to, Fn &&fn) + { + for (auto it = from; it != to; ++it) fn(*it); + } + + template + static IntegerOnly loop_(I from, I to, Fn &&fn) + { + for (I i = from; i < to; ++i) fn(i); + } + +public: + using SpinningMutex = _Mtx; + using BlockingMutex = _Mtx; + + template + static void for_each(const EP &, + It from, + It to, + Fn &&fn, + size_t /* ignore granularity */ = 1) + { + loop_(from, to, std::forward(fn)); + } + + template + static T reduce(const EP &, + I from, + I to, + const T & init, + MergeFn &&mergefn, + AccessFn &&access, + size_t /*granularity*/ = 1 + ) + { + T acc = init; + loop_(from, to, [&](auto &i) { acc = mergefn(acc, access(i)); }); + return acc; + } + + static size_t max_concurrency(const EP &) { return 1; } +}; + +} // namespace Slic3r + +#endif // EXECUTIONSEQ_HPP diff --git a/src/libslic3r/Execution/ExecutionTBB.hpp b/src/libslic3r/Execution/ExecutionTBB.hpp new file mode 100644 index 000000000..cf6373c46 --- /dev/null +++ b/src/libslic3r/Execution/ExecutionTBB.hpp @@ -0,0 +1,77 @@ +#ifndef EXECUTIONTBB_HPP +#define EXECUTIONTBB_HPP + +#include +#include +#include +#include +#include + +#include "Execution.hpp" + +namespace Slic3r { + +struct ExecutionTBB {}; +template<> struct IsExecutionPolicy_ : public std::true_type {}; + +// Execution policy using Intel TBB library under the hood. +static constexpr ExecutionTBB ex_tbb = {}; + +template<> struct execution::Traits { +private: + + template + static IteratorOnly loop_(const tbb::blocked_range &range, Fn &&fn) + { + for (auto &el : range) fn(el); + } + + template + static IntegerOnly loop_(const tbb::blocked_range &range, Fn &&fn) + { + for (I i = range.begin(); i < range.end(); ++i) fn(i); + } + +public: + using SpinningMutex = tbb::spin_mutex; + using BlockingMutex = tbb::mutex; + + template + static void for_each(const ExecutionTBB &, + It from, It to, Fn &&fn, size_t granularity) + { + tbb::parallel_for(tbb::blocked_range{from, to, granularity}, + [&fn](const auto &range) { + loop_(range, std::forward(fn)); + }); + } + + template + static T reduce(const ExecutionTBB &, + I from, + I to, + const T &init, + MergeFn &&mergefn, + AccessFn &&access, + size_t granularity = 1 + ) + { + return tbb::parallel_reduce( + tbb::blocked_range{from, to, granularity}, init, + [&](const auto &range, T subinit) { + T acc = subinit; + loop_(range, [&](auto &i) { acc = mergefn(acc, access(i)); }); + return acc; + }, + std::forward(mergefn)); + } + + static size_t max_concurrency(const ExecutionTBB &) + { + return tbb::this_task_arena::max_concurrency(); + } +}; + +} + +#endif // EXECUTIONTBB_HPP diff --git a/src/libslic3r/MTUtils.hpp b/src/libslic3r/MTUtils.hpp index 555cfe501..7b903f66c 100644 --- a/src/libslic3r/MTUtils.hpp +++ b/src/libslic3r/MTUtils.hpp @@ -106,13 +106,8 @@ template bool all_of(const C &container) }); } -template struct remove_cvref -{ - using type = - typename std::remove_cv::type>::type; -}; - -template using remove_cvref_t = typename remove_cvref::type; +template +using remove_cvref_t = std::remove_reference_t>; /// Exactly like Matlab https://www.mathworks.com/help/matlab/ref/linspace.html template> diff --git a/src/libslic3r/SLA/Concurrency.hpp b/src/libslic3r/SLA/Concurrency.hpp index 8ff0ff809..7299101b3 100644 --- a/src/libslic3r/SLA/Concurrency.hpp +++ b/src/libslic3r/SLA/Concurrency.hpp @@ -1,16 +1,10 @@ #ifndef SLA_CONCURRENCY_H #define SLA_CONCURRENCY_H -#include -#include -#include -#include -#include +// FIXME: Deprecated -#include -#include - -#include +#include +#include namespace Slic3r { namespace sla { @@ -23,124 +17,48 @@ template struct _ccr {}; template<> struct _ccr { - using SpinningMutex = tbb::spin_mutex; - using BlockingMutex = tbb::mutex; - - template - static IteratorOnly loop_(const tbb::blocked_range &range, Fn &&fn) - { - for (auto &el : range) fn(el); - } - - template - static IntegerOnly loop_(const tbb::blocked_range &range, Fn &&fn) - { - for (I i = range.begin(); i < range.end(); ++i) fn(i); - } + using SpinningMutex = execution::SpinningMutex; + using BlockingMutex = execution::BlockingMutex; template static void for_each(It from, It to, Fn &&fn, size_t granularity = 1) { - tbb::parallel_for(tbb::blocked_range{from, to, granularity}, - [&fn](const auto &range) { - loop_(range, std::forward(fn)); - }); + execution::for_each(ex_tbb, from, to, std::forward(fn), granularity); } - template - static T reduce(I from, - I to, - const T &init, - MergeFn &&mergefn, - AccessFn &&access, - size_t granularity = 1 - ) + template + static auto reduce(Args&&...args) { - return tbb::parallel_reduce( - tbb::blocked_range{from, to, granularity}, init, - [&](const auto &range, T subinit) { - T acc = subinit; - loop_(range, [&](auto &i) { acc = mergefn(acc, access(i)); }); - return acc; - }, - std::forward(mergefn)); - } - - template - static IteratorOnly reduce(I from, - I to, - const T & init, - MergeFn &&mergefn, - size_t granularity = 1) - { - return reduce( - from, to, init, std::forward(mergefn), - [](typename I::value_type &i) { return i; }, granularity); + return execution::reduce(ex_tbb, std::forward(args)...); } static size_t max_concurreny() { - return tbb::this_task_arena::max_concurrency(); + return execution::max_concurrency(ex_tbb); } }; template<> struct _ccr { -private: - struct _Mtx { inline void lock() {} inline void unlock() {} }; - -public: - using SpinningMutex = _Mtx; - using BlockingMutex = _Mtx; - - template - static IteratorOnly loop_(It from, It to, Fn &&fn) - { - for (auto it = from; it != to; ++it) fn(*it); - } - - template - static IntegerOnly loop_(I from, I to, Fn &&fn) - { - for (I i = from; i < to; ++i) fn(i); - } + using SpinningMutex = execution::SpinningMutex; + using BlockingMutex = execution::BlockingMutex; template - static void for_each(It from, - It to, - Fn &&fn, - size_t /* ignore granularity */ = 1) + static void for_each(It from, It to, Fn &&fn, size_t granularity = 1) { - loop_(from, to, std::forward(fn)); + execution::for_each(ex_seq, from, to, std::forward(fn), granularity); } - template - static T reduce(I from, - I to, - const T & init, - MergeFn &&mergefn, - AccessFn &&access, - size_t /*granularity*/ = 1 - ) + template + static auto reduce(Args&&...args) { - T acc = init; - loop_(from, to, [&](auto &i) { acc = mergefn(acc, access(i)); }); - return acc; + return execution::reduce(ex_seq, std::forward(args)...); } - template - static IteratorOnly reduce(I from, - I to, - const T &init, - MergeFn &&mergefn, - size_t /*granularity*/ = 1 - ) + static size_t max_concurreny() { - return reduce(from, to, init, std::forward(mergefn), - [](typename I::value_type &i) { return i; }); + return execution::max_concurrency(ex_seq); } - - static size_t max_concurreny() { return 1; } }; using ccr = _ccr; From de8bb00fa93379b04654209898a0fabfbe354cce Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Thu, 4 Mar 2021 18:15:13 +0100 Subject: [PATCH 03/17] Speed up rotation optimizer - No float to double conversion - Solving issue of random (very similar) results due to the parallel summation of floats --- src/libslic3r/SLA/Rotfinder.cpp | 85 +++++++++++++++++---------------- src/libslic3r/SLA/Rotfinder.hpp | 2 +- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index 6caea2393..f410eb0e1 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -1,7 +1,9 @@ #include #include -#include + +#include +#include #include @@ -20,66 +22,58 @@ namespace Slic3r { namespace sla { namespace { // Get the vertices of a triangle directly in an array of 3 points -std::array get_triangle_vertices(const TriangleMesh &mesh, +std::array get_triangle_vertices(const TriangleMesh &mesh, size_t faceidx) { const auto &face = mesh.its.indices[faceidx]; - return {Vec3d{mesh.its.vertices[face(0)].cast()}, - Vec3d{mesh.its.vertices[face(1)].cast()}, - Vec3d{mesh.its.vertices[face(2)].cast()}}; + return {mesh.its.vertices[face(0)], + mesh.its.vertices[face(1)], + mesh.its.vertices[face(2)]}; } -std::array get_transformed_triangle(const TriangleMesh &mesh, - const Transform3d & tr, +std::array get_transformed_triangle(const TriangleMesh &mesh, + const Transform3f & tr, size_t faceidx) { const auto &tri = get_triangle_vertices(mesh, faceidx); return {tr * tri[0], tr * tri[1], tr * tri[2]}; } -// Get area and normal of a triangle -struct Facestats { - Vec3d normal; - double area; - - explicit Facestats(const std::array &triangle) - { - Vec3d U = triangle[1] - triangle[0]; - Vec3d V = triangle[2] - triangle[0]; - Vec3d C = U.cross(V); - normal = C.normalized(); - area = 0.5 * C.norm(); - } -}; - -template -double sum_score(AccessFn &&accessfn, size_t facecount, size_t Nthreads) +template Vec<3, T> normal(const std::array, 3> &tri) { - double initv = 0.; - auto mergefn = [](double a, double b) { return a + b; }; - size_t grainsize = facecount / Nthreads; - size_t from = 0, to = facecount; + Vec<3, T> U = tri[1] - tri[0]; + Vec<3, T> V = tri[2] - tri[0]; + return U.cross(V).normalized(); +} - return ccr_par::reduce(from, to, initv, mergefn, accessfn, grainsize); +template +T sum_score(AccessFn &&accessfn, size_t facecount, size_t Nthreads) +{ + T initv = 0.; + auto mergefn = [](T a, T b) { return a + b; }; + size_t grainsize = facecount / Nthreads; + size_t from = 0, to = facecount; + + return execution::reduce(ex_seq, from, to, initv, mergefn, accessfn, grainsize); } // Try to guess the number of support points needed to support a mesh -double get_model_supportedness(const TriangleMesh &mesh, const Transform3d &tr) +double get_model_supportedness(const TriangleMesh &mesh, const Transform3f &tr) { if (mesh.its.vertices.empty()) return std::nan(""); auto accessfn = [&mesh, &tr](size_t fi) { - Facestats fc{get_transformed_triangle(mesh, tr, fi)}; + Vec3f n = normal(get_transformed_triangle(mesh, tr, fi)); // We should score against the alignment with the reference planes - return std::abs(fc.normal.dot(Vec3d::UnitX())) + - std::abs(fc.normal.dot(Vec3d::UnitY())) + - std::abs(fc.normal.dot(Vec3d::UnitZ())); + return scaled(std::abs(n.dot(Vec3f::UnitX())) + + std::abs(n.dot(Vec3f::UnitY())) + + std::abs(n.dot(Vec3f::UnitZ()))); }; size_t facecount = mesh.its.indices.size(); size_t Nthreads = std::thread::hardware_concurrency(); - double S = sum_score(accessfn, facecount, Nthreads); + double S = unscaled(sum_score(accessfn, facecount, Nthreads)); return S / facecount; } @@ -87,11 +81,12 @@ double get_model_supportedness(const TriangleMesh &mesh, const Transform3d &tr) using XYRotation = std::array; // prepare the rotation transformation -Transform3d to_transform3d(const XYRotation &rot) +Transform3f to_transform3f(const XYRotation &rot) { - Transform3d rt = Transform3d::Identity(); - rt.rotate(Eigen::AngleAxisd(rot[1], Vec3d::UnitY())); - rt.rotate(Eigen::AngleAxisd(rot[0], Vec3d::UnitX())); + Transform3f rt = Transform3f::Identity(); + rt.rotate(Eigen::AngleAxisf(float(rot[1]), Vec3f::UnitY())); + rt.rotate(Eigen::AngleAxisf(float(rot[0]), Vec3f::UnitX())); + return rt; } @@ -138,19 +133,27 @@ Vec2d find_best_rotation(const SLAPrintObject & po, // We can specify the bounds for a dimension in the following way: auto bounds = opt::bounds({ {-PI/2, PI/2}, {-PI/2, PI/2} }); + Benchmark bench; + + bench.start(); auto result = solver.to_max().optimize( [&mesh, &statusfn] (const XYRotation &rot) { statusfn(); - return get_model_supportedness(mesh, to_transform3d(rot)); + return get_model_supportedness(mesh, to_transform3f(rot)); }, opt::initvals({0., 0.}), bounds); + bench.stop(); rot = result.optimum; + std::cout << "Optimum score: " << result.score << std::endl; + std::cout << "Optimum rotation: " << result.optimum[0] << " " << result.optimum[1] << std::endl; + std::cout << "Optimization took: " << bench.getElapsedSec() << " seconds" << std::endl; + return {rot[0], rot[1]}; } -double get_model_supportedness(const SLAPrintObject &po, const Transform3d &tr) +double get_model_supportedness(const SLAPrintObject &po, const Transform3f &tr) { TriangleMesh mesh = po.model_object()->raw_mesh(); mesh.require_shared_vertices(); diff --git a/src/libslic3r/SLA/Rotfinder.hpp b/src/libslic3r/SLA/Rotfinder.hpp index 96561a890..a6fde2c9d 100644 --- a/src/libslic3r/SLA/Rotfinder.hpp +++ b/src/libslic3r/SLA/Rotfinder.hpp @@ -35,7 +35,7 @@ Vec2d find_best_rotation( ); double get_model_supportedness(const SLAPrintObject &mesh, - const Transform3d & tr); + const Transform3f & tr); } // namespace sla } // namespace Slic3r From 46fd722f3ce3776b6807ba07679b333455ee1a93 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Thu, 18 Mar 2021 09:37:46 +0100 Subject: [PATCH 04/17] Unite cancel callback and status function --- src/libslic3r/SLA/Rotfinder.cpp | 15 +++++++++------ src/libslic3r/SLA/Rotfinder.hpp | 12 ++++++------ src/slic3r/GUI/Jobs/RotoptimizeJob.cpp | 11 +++++------ 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index f410eb0e1..79be5e1ec 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -92,10 +92,9 @@ Transform3f to_transform3f(const XYRotation &rot) } // namespace -Vec2d find_best_rotation(const SLAPrintObject & po, - float accuracy, - std::function statuscb, - std::function stopcond) +Vec2d find_best_rotation(const SLAPrintObject & po, + float accuracy, + std::function statuscb) { static const unsigned MAX_TRIES = 1000; @@ -108,7 +107,7 @@ Vec2d find_best_rotation(const SLAPrintObject & po, mesh.require_shared_vertices(); // To keep track of the number of iterations - unsigned status = 0; + int status = 0; // The maximum number of iterations auto max_tries = unsigned(accuracy * MAX_TRIES); @@ -118,7 +117,11 @@ Vec2d find_best_rotation(const SLAPrintObject & po, auto statusfn = [&statuscb, &status, &max_tries] { // report status - statuscb(unsigned(++status * 100.0/max_tries) ); + statuscb(++status * 100.0/max_tries); + }; + + auto stopcond = [&statuscb] { + return ! statuscb(-1); }; // Preparing the optimizer. diff --git a/src/libslic3r/SLA/Rotfinder.hpp b/src/libslic3r/SLA/Rotfinder.hpp index a6fde2c9d..2b92c52b8 100644 --- a/src/libslic3r/SLA/Rotfinder.hpp +++ b/src/libslic3r/SLA/Rotfinder.hpp @@ -19,19 +19,19 @@ namespace sla { * @param accuracy The optimization accuracy from 0.0f to 1.0f. Currently, * the nlopt genetic optimizer is used and the number of iterations is * accuracy * 100000. This can change in the future. - * @param statuscb A status indicator callback called with the unsigned + * @param statuscb A status indicator callback called with the int * argument spanning from 0 to 100. May not reach 100 if the optimization finds - * an optimum before max iterations are reached. - * @param stopcond A function that if returns true, the search process will be - * terminated and the best solution found will be returned. + * an optimum before max iterations are reached. It should return a boolean + * signaling if the operation may continue (true) or not (false). A status + * value lower than 0 shall not update the status but still return a valid + * continuation indicator. * * @return Returns the rotations around each axis (x, y, z) */ Vec2d find_best_rotation( const SLAPrintObject& modelobj, float accuracy = 1.0f, - std::function statuscb = [] (unsigned) {}, - std::function stopcond = [] () { return false; } + std::function statuscb = [] (int) { return true; } ); double get_model_supportedness(const SLAPrintObject &mesh, diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index ac737cafb..7e1bfaeeb 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -21,13 +21,12 @@ void RotoptimizeJob::process() if (!o || !po) return; - Vec2d r = sla::find_best_rotation(*po, 0.75f, - [this](unsigned s) { - if (s < 100) - update_status(int(s), _(L("Searching for optimal orientation"))); - }, - [this] () { return was_canceled(); }); + Vec2d r = sla::find_best_rotation(*po, 0.75f, [this](int s) { + if (s > 0 && s < 100) + update_status(s, _(L("Searching for optimal orientation"))); + return !was_canceled(); + }); double mindist = 6.0; // FIXME From 4eb13a407f03fed8899436e9dd738fe636596270 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Thu, 18 Mar 2021 09:38:21 +0100 Subject: [PATCH 05/17] Extend execution framework with convenience functions --- src/libslic3r/Execution/Execution.hpp | 42 +++++++++++++++++++++++---- tests/sla_print/sla_print_tests.cpp | 2 +- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/Execution/Execution.hpp b/src/libslic3r/Execution/Execution.hpp index 809cc45d3..e4bad9f23 100644 --- a/src/libslic3r/Execution/Execution.hpp +++ b/src/libslic3r/Execution/Execution.hpp @@ -10,6 +10,7 @@ namespace Slic3r { +// Borrowed from C++20 template using remove_cvref_t = std::remove_reference_t>; @@ -31,7 +32,7 @@ template struct Traits {}; template using AsTraits = Traits>; // Each execution policy should declare two types of mutexes. A a spin lock and -// a blocking mutex. +// a blocking mutex. These types should satisfy the BasicLockable concept. template using SpinningMutex = typename Traits::SpinningMutex; template using BlockingMutex = typename Traits::BlockingMutex; @@ -75,13 +76,12 @@ T reduce(const EP & ep, } // An overload of reduce method to be used with iterators as 'from' and 'to' -// arguments. +// arguments. Access functor is omitted here. template, - class = IteratorOnly > + class = ExecutionPolicyOnly > T reduce(const EP &ep, I from, I to, @@ -91,7 +91,39 @@ T reduce(const EP &ep, { return reduce( ep, from, to, init, std::forward(mergefn), - [](typename I::value_type &i) { return i; }, granularity); + [](const auto &i) { return i; }, granularity); +} + +template> +T accumulate(const EP & ep, + I from, + I to, + const T & init, + AccessFn &&accessfn, + size_t granularity = 1) +{ + return reduce(ep, from, to, init, std::plus{}, + std::forward(accessfn), granularity); +} + + +template > +T accumulate(const EP &ep, + I from, + I to, + const T & init, + size_t granularity = 1) +{ + return reduce( + ep, from, to, init, std::plus{}, [](const auto &i) { return i; }, + granularity); } } // namespace execution_policy diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index bdd5731dc..59c841468 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -248,7 +248,7 @@ TEST_CASE("Test concurrency") double ref = std::accumulate(vals.begin(), vals.end(), 0.); - double s = sla::ccr_par::reduce(vals.begin(), vals.end(), 0., std::plus{}); + double s = execution::accumulate(ex_tbb, vals.begin(), vals.end(), 0.); REQUIRE(s == Approx(ref)); } From 0194094afa1459629c8fa308500e57421c19603d Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Thu, 18 Mar 2021 20:20:01 +0100 Subject: [PATCH 06/17] Method selection implemented --- src/libslic3r/SLA/Rotfinder.cpp | 14 +---- src/libslic3r/SLA/Rotfinder.hpp | 10 ++-- src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp | 75 ++++++++++++++++++++++++- src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp | 34 +++++++++++ src/slic3r/GUI/Jobs/RotoptimizeJob.cpp | 23 +++++++- src/slic3r/GUI/Jobs/RotoptimizeJob.hpp | 43 +++++++++++++- 6 files changed, 181 insertions(+), 18 deletions(-) diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index 79be5e1ec..eb54b02dc 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -92,9 +92,9 @@ Transform3f to_transform3f(const XYRotation &rot) } // namespace -Vec2d find_best_rotation(const SLAPrintObject & po, - float accuracy, - std::function statuscb) +Vec2d find_best_misalignment_rotation(const SLAPrintObject & po, + float accuracy, + std::function statuscb) { static const unsigned MAX_TRIES = 1000; @@ -156,12 +156,4 @@ Vec2d find_best_rotation(const SLAPrintObject & po, return {rot[0], rot[1]}; } -double get_model_supportedness(const SLAPrintObject &po, const Transform3f &tr) -{ - TriangleMesh mesh = po.model_object()->raw_mesh(); - mesh.require_shared_vertices(); - - return get_model_supportedness(mesh, tr); -} - }} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/Rotfinder.hpp b/src/libslic3r/SLA/Rotfinder.hpp index 2b92c52b8..56884565f 100644 --- a/src/libslic3r/SLA/Rotfinder.hpp +++ b/src/libslic3r/SLA/Rotfinder.hpp @@ -9,9 +9,12 @@ namespace Slic3r { class SLAPrintObject; +class TriangleMesh; namespace sla { +using RotOptimizeStatusCB = std::function; + /** * The function should find the best rotation for SLA upside down printing. * @@ -28,14 +31,13 @@ namespace sla { * * @return Returns the rotations around each axis (x, y, z) */ -Vec2d find_best_rotation( +Vec2d find_best_misalignment_rotation( const SLAPrintObject& modelobj, float accuracy = 1.0f, - std::function statuscb = [] (int) { return true; } + RotOptimizeStatusCB statuscb = [] (int) { return true; } ); -double get_model_supportedness(const SLAPrintObject &mesh, - const Transform3f & tr); + } // namespace sla } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index c5060a88e..b44bc01f0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -7,9 +7,10 @@ #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Plater.hpp" #include "libslic3r/PresetBundle.hpp" -#include "libslic3r/SLA/Rotfinder.hpp" +#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp" namespace Slic3r { namespace GUI { @@ -204,6 +205,23 @@ void GLGizmoRotate3D::on_render_input_window(float x, float y, float bottom_limi { if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) return; + + RotoptimzeWindow popup{m_imgui, m_rotoptimizewin_state, {x, y, bottom_limit}}; +} + +void GLGizmoRotate3D::load_rotoptimize_state() +{ + std::string accuracy_str = + wxGetApp().app_config->get("rotoptimize", "accuracy"); + + std::string method_str = + wxGetApp().app_config->get("rotoptimize", "method_id"); + + if (!accuracy_str.empty()) + m_rotoptimizewin_state.accuracy = std::stof(accuracy_str); + + if (!method_str.empty()) + m_rotoptimizewin_state.method_id = std::stoi(method_str); } void GLGizmoRotate::render_circle() const @@ -436,6 +454,9 @@ GLGizmoRotate3D::GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_fil { m_gizmos[i].set_group_id(i); } + + std::cout << "Load rotopt state" << std::endl; + load_rotoptimize_state(); } bool GLGizmoRotate3D::on_init() @@ -492,5 +513,57 @@ void GLGizmoRotate3D::on_render() const m_gizmos[Z].render(); } +const char * GLGizmoRotate3D::RotoptimzeWindow::options[RotoptimizeJob::get_methods_count()]; +bool GLGizmoRotate3D::RotoptimzeWindow::options_valid = false; + +GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, + State & state, + const Alignment &alignment) + : m_imgui{imgui} +{ + imgui->begin(_L("Optimize orientation"), ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoCollapse); + + // adjust window position to avoid overlap the view toolbar + float win_h = ImGui::GetWindowHeight(); + float x = alignment.x, y = alignment.y; + y = std::min(y, alignment.bottom_limit - win_h); + ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); + + ImGui::PushItemWidth(200.f); + + size_t methods_cnt = RotoptimizeJob::get_methods_count(); + if (!options_valid) { + for (size_t i = 0; i < methods_cnt; ++i) + options[i] = RotoptimizeJob::get_method_names()[i].c_str(); + + options_valid = true; + } + + int citem = state.method_id; + if (ImGui::Combo(_L("Choose method").c_str(), &citem, options, methods_cnt) ) { + state.method_id = citem; + wxGetApp().app_config->set("rotoptimize", "method_id", std::to_string(state.method_id)); + } + + float accuracy = state.accuracy; + if (imgui->slider_float(_L("Accuracy/Speed"), &accuracy, 0.01f, 1.f, "%.1f")) { + state.accuracy = accuracy; + wxGetApp().app_config->set("rotoptimize", "accuracy", std::to_string(state.accuracy)); + } + + ImGui::Separator(); + + if ( imgui->button(_L("Optimize")) ) { + wxGetApp().plater()->optimize_rotation(); + } +} + +GLGizmoRotate3D::RotoptimzeWindow::~RotoptimzeWindow() +{ + m_imgui->end(); +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp index 126c97b1d..c18d0eefd 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp @@ -2,6 +2,7 @@ #define slic3r_GLGizmoRotate_hpp_ #include "GLGizmoBase.hpp" +#include "../Jobs/RotoptimizeJob.hpp" namespace Slic3r { @@ -136,6 +137,39 @@ protected: } void on_render_input_window(float x, float y, float bottom_limit) override; + +private: + + class RotoptimzeWindow { + ImGuiWrapper *m_imgui = nullptr; + + static const char * options []; + static bool options_valid; + + public: + + struct State { + float accuracy = 1.f; + int method_id = 0; + }; + + struct Alignment { float x, y, bottom_limit; }; + + RotoptimzeWindow(ImGuiWrapper * imgui, + State & state, + const Alignment &bottom_limit); + + ~RotoptimzeWindow(); + + RotoptimzeWindow(const RotoptimzeWindow&) = delete; + RotoptimzeWindow(RotoptimzeWindow &&) = delete; + RotoptimzeWindow& operator=(const RotoptimzeWindow &) = delete; + RotoptimzeWindow& operator=(RotoptimzeWindow &&) = delete; + }; + + RotoptimzeWindow::State m_rotoptimizewin_state = {}; + + void load_rotoptimize_state(); }; } // namespace GUI diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index 7e1bfaeeb..2e83bbf96 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -8,8 +8,29 @@ #include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "libslic3r/AppConfig.hpp" + namespace Slic3r { namespace GUI { +void RotoptimizeJob::prepare() +{ + std::string accuracy_str = + wxGetApp().app_config->get("rotoptimize", "accuracy"); + + std::string method_str = + wxGetApp().app_config->get("rotoptimize", "method_id"); + + if (!accuracy_str.empty()) + m_accuracy = std::stof(accuracy_str); + + if (!method_str.empty()) + m_method_id = std::stoi(method_str); + + m_accuracy = std::max(0.f, std::min(m_accuracy, 1.f)); + m_method_id = std::max(size_t(0), std::min(get_methods_count() - 1, m_method_id)); +} + void RotoptimizeJob::process() { int obj_idx = m_plater->get_selected_object_idx(); @@ -21,7 +42,7 @@ void RotoptimizeJob::process() if (!o || !po) return; - Vec2d r = sla::find_best_rotation(*po, 0.75f, [this](int s) { + Vec2d r = Methods[m_method_id].findfn(*po, m_accuracy, [this](int s) { if (s > 0 && s < 100) update_status(s, _(L("Searching for optimal orientation"))); diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp index 06688b52d..357036440 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp @@ -3,17 +3,58 @@ #include "PlaterJob.hpp" -namespace Slic3r { namespace GUI { +#include "libslic3r/SLA/Rotfinder.hpp" + +namespace Slic3r { + +class SLAPrintObject; + +namespace GUI { class RotoptimizeJob : public PlaterJob { + using FindFn = std::function; + + struct FindMethod { std::string name; FindFn findfn; }; + + static inline const FindMethod Methods[] = { + { L("Best misalignment"), sla::find_best_misalignment_rotation }, + { L("Least supports"), sla::find_best_misalignment_rotation } + }; + + size_t m_method_id = 0; + float m_accuracy = 0.75; +protected: + + void prepare() override; + public: + RotoptimizeJob(std::shared_ptr pri, Plater *plater) : PlaterJob{std::move(pri), plater} {} void process() override; void finalize() override; + + static constexpr size_t get_methods_count() { return std::size(Methods); } + static const auto & get_method_names() + { + static bool m_method_names_valid = false; + static std::array m_method_names; + + if (!m_method_names_valid) { + + for (size_t i = 0; i < std::size(Methods); ++i) + m_method_names[i] = _utf8(Methods[i].name); + + m_method_names_valid = true; + } + + return m_method_names; + } }; }} // namespace Slic3r::GUI From f3e3aabec79866df5351e8117b151068d766f1d0 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Fri, 19 Mar 2021 10:01:50 +0100 Subject: [PATCH 07/17] Least supports optimization revived. Fix missing include on Win32 Cleanup benchmarking code --- src/libslic3r/SLA/Rotfinder.cpp | 291 +++++++++++++++++++++++-- src/libslic3r/SLA/Rotfinder.hpp | 6 +- src/slic3r/GUI/Jobs/RotoptimizeJob.hpp | 2 +- tests/sla_print/sla_print_tests.cpp | 1 + 4 files changed, 281 insertions(+), 19 deletions(-) diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index eb54b02dc..c97cf5010 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -11,16 +11,16 @@ #include "libslic3r/PrintConfig.hpp" #include -#include "Model.hpp" #include -#include - namespace Slic3r { namespace sla { namespace { +inline const Vec3f DOWN = {0.f, 0.f, -1.f}; +constexpr double POINTS_PER_UNIT_AREA = 1.f; + // Get the vertices of a triangle directly in an array of 3 points std::array get_triangle_vertices(const TriangleMesh &mesh, size_t faceidx) @@ -54,11 +54,11 @@ T sum_score(AccessFn &&accessfn, size_t facecount, size_t Nthreads) size_t grainsize = facecount / Nthreads; size_t from = 0, to = facecount; - return execution::reduce(ex_seq, from, to, initv, mergefn, accessfn, grainsize); + return execution::reduce(ex_tbb, from, to, initv, mergefn, accessfn, grainsize); } // Try to guess the number of support points needed to support a mesh -double get_model_supportedness(const TriangleMesh &mesh, const Transform3f &tr) +double get_misalginment_score(const TriangleMesh &mesh, const Transform3f &tr) { if (mesh.its.vertices.empty()) return std::nan(""); @@ -78,6 +78,100 @@ double get_model_supportedness(const TriangleMesh &mesh, const Transform3f &tr) return S / facecount; } +// Get area and normal of a triangle +struct Facestats { + Vec3f normal; + double area; + + explicit Facestats(const std::array &triangle) + { + Vec3f U = triangle[1] - triangle[0]; + Vec3f V = triangle[2] - triangle[0]; + Vec3f C = U.cross(V); + normal = C.normalized(); + area = 0.5 * C.norm(); + } +}; + +// The score function for a particular face +inline double get_supportedness_score(const Facestats &fc) +{ + // Simply get the angle (acos of dot product) between the face normal and + // the DOWN vector. + float phi = 1. - std::acos(fc.normal.dot(DOWN)) / float(PI); + + // Only consider faces that have have slopes below 90 deg: + phi = phi * (phi > 0.5); + + // Make the huge slopes more significant than the smaller slopes + phi = phi * phi * phi; + + // Multiply with the area of the current face + return fc.area * POINTS_PER_UNIT_AREA * phi; +} + +// Try to guess the number of support points needed to support a mesh +double get_supportedness_score(const TriangleMesh &mesh, const Transform3f &tr) +{ + if (mesh.its.vertices.empty()) return std::nan(""); + + auto accessfn = [&mesh, &tr](size_t fi) { + Facestats fc{get_transformed_triangle(mesh, tr, fi)}; + + return get_supportedness_score(fc); + }; + + size_t facecount = mesh.its.indices.size(); + size_t Nthreads = std::thread::hardware_concurrency(); + double S = unscaled(sum_score(accessfn, facecount, Nthreads)); + + return S / facecount; +} + +// Find transformed mesh ground level without copy and with parallel reduce. +float find_ground_level(const TriangleMesh &mesh, + const Transform3f & tr, + size_t threads) +{ + size_t vsize = mesh.its.vertices.size(); + + auto minfn = [](float a, float b) { return std::min(a, b); }; + + auto accessfn = [&mesh, &tr] (size_t vi) { + return (tr * mesh.its.vertices[vi]).z(); + }; + + auto zmin = std::numeric_limits::max(); + size_t granularity = vsize / threads; + return execution::reduce(ex_tbb, size_t(0), vsize, zmin, minfn, accessfn, granularity); +} + +float get_supportedness_onfloor_score(const TriangleMesh &mesh, + const Transform3f & tr) +{ + if (mesh.its.vertices.empty()) return std::nan(""); + + size_t Nthreads = std::thread::hardware_concurrency(); + + float zmin = find_ground_level(mesh, tr, Nthreads); + float zlvl = zmin + 0.1f; // Set up a slight tolerance from z level + + auto accessfn = [&mesh, &tr, zlvl](size_t fi) { + std::array tri = get_transformed_triangle(mesh, tr, fi); + Facestats fc{tri}; + + if (tri[0].z() <= zlvl && tri[1].z() <= zlvl && tri[2].z() <= zlvl) + return -fc.area * POINTS_PER_UNIT_AREA; + + return get_supportedness_score(fc); + }; + + size_t facecount = mesh.its.indices.size(); + double S = unscaled(sum_score(accessfn, facecount, Nthreads)); + + return S / facecount; +} + using XYRotation = std::array; // prepare the rotation transformation @@ -90,13 +184,107 @@ Transform3f to_transform3f(const XYRotation &rot) return rt; } +XYRotation from_transform3f(const Transform3f &tr) +{ + Vec3d rot3 = Geometry::Transformation{tr.cast()}.get_rotation(); + return {rot3.x(), rot3.y()}; +} + +inline bool is_on_floor(const SLAPrintObject &mo) +{ + auto opt_elevation = mo.config().support_object_elevation.getFloat(); + auto opt_padaround = mo.config().pad_around_object.getBool(); + + return opt_elevation < EPSILON || opt_padaround; +} + +// collect the rotations for each face of the convex hull +std::vector get_chull_rotations(const TriangleMesh &mesh, size_t max_count) +{ + TriangleMesh chull = mesh.convex_hull_3d(); + chull.require_shared_vertices(); + double chull2d_area = chull.convex_hull().area(); + double area_threshold = chull2d_area / (scaled(1e3) * scaled(1.)); + + size_t facecount = chull.its.indices.size(); + + struct RotArea { XYRotation rot; double area; }; + + auto inputs = reserve_vector(facecount); + + auto rotcmp = [](const RotArea &r1, const RotArea &r2) { + double xdiff = r1.rot[X] - r2.rot[X], ydiff = r1.rot[Y] - r2.rot[Y]; + return std::abs(xdiff) < EPSILON ? ydiff < 0. : xdiff < 0.; + }; + + auto eqcmp = [](const XYRotation &r1, const XYRotation &r2) { + double xdiff = r1[X] - r2[X], ydiff = r1[Y] - r2[Y]; + return std::abs(xdiff) < EPSILON && std::abs(ydiff) < EPSILON; + }; + + for (size_t fi = 0; fi < facecount; ++fi) { + Facestats fc{get_triangle_vertices(chull, fi)}; + + if (fc.area > area_threshold) { + auto q = Eigen::Quaternionf{}.FromTwoVectors(fc.normal, DOWN); + XYRotation rot = from_transform3f(Transform3f::Identity() * q); + RotArea ra = {rot, fc.area}; + + auto it = std::lower_bound(inputs.begin(), inputs.end(), ra, rotcmp); + + if (it == inputs.end() || !eqcmp(it->rot, rot)) + inputs.insert(it, ra); + } + } + + inputs.shrink_to_fit(); + if (!max_count) max_count = inputs.size(); + std::sort(inputs.begin(), inputs.end(), + [](const RotArea &ra, const RotArea &rb) { + return ra.area > rb.area; + }); + + auto ret = reserve_vector(std::min(max_count, inputs.size())); + for (const RotArea &ra : inputs) ret.emplace_back(ra.rot); + + return ret; +} + +// Find the best score from a set of function inputs. Evaluate for every point. +template +std::array find_min_score(Fn &&fn, It from, It to, StopCond &&stopfn) +{ + std::array ret = {}; + + double score = std::numeric_limits::max(); + + size_t Nthreads = std::thread::hardware_concurrency(); + size_t dist = std::distance(from, to); + std::vector scores(dist, score); + + execution::for_each( + ex_tbb, size_t(0), dist, [&stopfn, &scores, &fn, &from](size_t i) { + if (stopfn()) return; + + scores[i] = fn(*(from + i)); + }, + dist / Nthreads); + + auto it = std::min_element(scores.begin(), scores.end()); + + if (it != scores.end()) + ret = *(from + std::distance(scores.begin(), it)); + + return ret; +} + } // namespace -Vec2d find_best_misalignment_rotation(const SLAPrintObject & po, - float accuracy, - std::function statuscb) +Vec2d find_best_misalignment_rotation(const SLAPrintObject & po, + float accuracy, + RotOptimizeStatusCB statuscb) { - static const unsigned MAX_TRIES = 1000; + static constexpr unsigned MAX_TRIES = 1000; // return value XYRotation rot; @@ -136,22 +324,91 @@ Vec2d find_best_misalignment_rotation(const SLAPrintObject & po, // We can specify the bounds for a dimension in the following way: auto bounds = opt::bounds({ {-PI/2, PI/2}, {-PI/2, PI/2} }); - Benchmark bench; - - bench.start(); auto result = solver.to_max().optimize( [&mesh, &statusfn] (const XYRotation &rot) { statusfn(); - return get_model_supportedness(mesh, to_transform3f(rot)); + return get_misalginment_score(mesh, to_transform3f(rot)); }, opt::initvals({0., 0.}), bounds); - bench.stop(); rot = result.optimum; - std::cout << "Optimum score: " << result.score << std::endl; - std::cout << "Optimum rotation: " << result.optimum[0] << " " << result.optimum[1] << std::endl; - std::cout << "Optimization took: " << bench.getElapsedSec() << " seconds" << std::endl; + return {rot[0], rot[1]}; +} + +Vec2d find_least_supports_rotation(const SLAPrintObject & po, + float accuracy, + RotOptimizeStatusCB statuscb) +{ + static const unsigned MAX_TRIES = 1000; + + // return value + XYRotation rot; + + // We will use only one instance of this converted mesh to examine different + // rotations + TriangleMesh mesh = po.model_object()->raw_mesh(); + mesh.require_shared_vertices(); + + // To keep track of the number of iterations + unsigned status = 0; + + // The maximum number of iterations + auto max_tries = unsigned(accuracy * MAX_TRIES); + + // call status callback with zero, because we are at the start + statuscb(status); + + auto statusfn = [&statuscb, &status, &max_tries] { + // report status + statuscb(unsigned(++status * 100.0/max_tries) ); + }; + + auto stopcond = [&statuscb] { + return ! statuscb(-1); + }; + + // Different search methods have to be used depending on the model elevation + if (is_on_floor(po)) { + + std::vector inputs = get_chull_rotations(mesh, max_tries); + max_tries = inputs.size(); + + // If the model can be placed on the bed directly, we only need to + // check the 3D convex hull face rotations. + + auto objfn = [&mesh, &statusfn](const XYRotation &rot) { + statusfn(); + Transform3f tr = to_transform3f(rot); + return get_supportedness_onfloor_score(mesh, tr); + }; + + rot = find_min_score<2>(objfn, inputs.begin(), inputs.end(), stopcond); + + } else { + + // Preparing the optimizer. + size_t gridsize = std::sqrt(max_tries); // 2D grid has gridsize^2 calls + opt::Optimizer solver(opt::StopCriteria{} + .max_iterations(max_tries) + .stop_condition(stopcond), + gridsize); + + // We are searching rotations around only two axes x, y. Thus the + // problem becomes a 2 dimensional optimization task. + // We can specify the bounds for a dimension in the following way: + auto bounds = opt::bounds({ {-PI, PI}, {-PI, PI} }); + + auto result = solver.to_min().optimize( + [&mesh, &statusfn] (const XYRotation &rot) + { + statusfn(); + return get_supportedness_score(mesh, to_transform3f(rot)); + }, opt::initvals({0., 0.}), bounds); + + // Save the result + rot = result.optimum; + } return {rot[0], rot[1]}; } diff --git a/src/libslic3r/SLA/Rotfinder.hpp b/src/libslic3r/SLA/Rotfinder.hpp index 56884565f..c0007944a 100644 --- a/src/libslic3r/SLA/Rotfinder.hpp +++ b/src/libslic3r/SLA/Rotfinder.hpp @@ -37,7 +37,11 @@ Vec2d find_best_misalignment_rotation( RotOptimizeStatusCB statuscb = [] (int) { return true; } ); - +Vec2d find_least_supports_rotation( + const SLAPrintObject& modelobj, + float accuracy = 1.0f, + RotOptimizeStatusCB statuscb = [] (int) { return true; } + ); } // namespace sla } // namespace Slic3r diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp index 357036440..dfec2d6a6 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp @@ -21,7 +21,7 @@ class RotoptimizeJob : public PlaterJob static inline const FindMethod Methods[] = { { L("Best misalignment"), sla::find_best_misalignment_rotation }, - { L("Least supports"), sla::find_best_misalignment_rotation } + { L("Least supports"), sla::find_least_supports_rotation } }; size_t m_method_id = 0; diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index 59c841468..1f98463cc 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include "sla_test_utils.hpp" From e7f5c61bb8f908f1cc11110cd2d3b803914386cf Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Fri, 19 Mar 2021 18:37:07 +0100 Subject: [PATCH 08/17] Remove leftover debug message --- src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index b44bc01f0..a60c74f30 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -455,7 +455,6 @@ GLGizmoRotate3D::GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_fil m_gizmos[i].set_group_id(i); } - std::cout << "Load rotopt state" << std::endl; load_rotoptimize_state(); } From 33eec05f0257d321d8651d9d9dd31027af969c9b Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Fri, 19 Mar 2021 18:45:55 +0100 Subject: [PATCH 09/17] Tolerate corrupted appconfig settings for auto rotation --- src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index a60c74f30..b16932746 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -217,11 +217,18 @@ void GLGizmoRotate3D::load_rotoptimize_state() std::string method_str = wxGetApp().app_config->get("rotoptimize", "method_id"); - if (!accuracy_str.empty()) - m_rotoptimizewin_state.accuracy = std::stof(accuracy_str); + if (!accuracy_str.empty()) { + float accuracy = std::stof(accuracy_str); + accuracy = std::max(0.f, std::min(accuracy, 1.f)); - if (!method_str.empty()) - m_rotoptimizewin_state.method_id = std::stoi(method_str); + m_rotoptimizewin_state.accuracy = accuracy; + } + + if (!method_str.empty()) { + int method_id = std::stoi(method_str); + if (method_id < int(RotoptimizeJob::get_methods_count())) + m_rotoptimizewin_state.method_id = method_id; + } } void GLGizmoRotate::render_circle() const From 4a9768cc7f27568bb9a4d98b52739e76e95e6a2c Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Fri, 19 Mar 2021 18:48:10 +0100 Subject: [PATCH 10/17] Change configuration bank name for SLA auto rotation --- src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp | 8 ++++---- src/slic3r/GUI/Jobs/RotoptimizeJob.cpp | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index b16932746..197800797 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -212,10 +212,10 @@ void GLGizmoRotate3D::on_render_input_window(float x, float y, float bottom_limi void GLGizmoRotate3D::load_rotoptimize_state() { std::string accuracy_str = - wxGetApp().app_config->get("rotoptimize", "accuracy"); + wxGetApp().app_config->get("sla_auto_rotate", "accuracy"); std::string method_str = - wxGetApp().app_config->get("rotoptimize", "method_id"); + wxGetApp().app_config->get("sla_auto_rotate", "method_id"); if (!accuracy_str.empty()) { float accuracy = std::stof(accuracy_str); @@ -550,13 +550,13 @@ GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, int citem = state.method_id; if (ImGui::Combo(_L("Choose method").c_str(), &citem, options, methods_cnt) ) { state.method_id = citem; - wxGetApp().app_config->set("rotoptimize", "method_id", std::to_string(state.method_id)); + wxGetApp().app_config->set("sla_auto_rotate", "method_id", std::to_string(state.method_id)); } float accuracy = state.accuracy; if (imgui->slider_float(_L("Accuracy/Speed"), &accuracy, 0.01f, 1.f, "%.1f")) { state.accuracy = accuracy; - wxGetApp().app_config->set("rotoptimize", "accuracy", std::to_string(state.accuracy)); + wxGetApp().app_config->set("sla_auto_rotate", "accuracy", std::to_string(state.accuracy)); } ImGui::Separator(); diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index 2e83bbf96..05b141131 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -16,10 +16,10 @@ namespace Slic3r { namespace GUI { void RotoptimizeJob::prepare() { std::string accuracy_str = - wxGetApp().app_config->get("rotoptimize", "accuracy"); + wxGetApp().app_config->get("sla_auto_rotate", "accuracy"); std::string method_str = - wxGetApp().app_config->get("rotoptimize", "method_id"); + wxGetApp().app_config->get("sla_auto_rotate", "method_id"); if (!accuracy_str.empty()) m_accuracy = std::stof(accuracy_str); From 5443f77489e5f673dd77b7f78e30b7600295fd95 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Fri, 26 Mar 2021 18:20:31 +0100 Subject: [PATCH 11/17] Increase performance of "best misalignment" method --- src/libslic3r/SLA/Rotfinder.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index c97cf5010..89de1cf83 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -63,12 +63,14 @@ double get_misalginment_score(const TriangleMesh &mesh, const Transform3f &tr) if (mesh.its.vertices.empty()) return std::nan(""); auto accessfn = [&mesh, &tr](size_t fi) { - Vec3f n = normal(get_transformed_triangle(mesh, tr, fi)); + auto triangle = get_transformed_triangle(mesh, tr, fi); + Vec3f U = triangle[1] - triangle[0]; + Vec3f V = triangle[2] - triangle[0]; + Vec3f C = U.cross(V); // We should score against the alignment with the reference planes - return scaled(std::abs(n.dot(Vec3f::UnitX())) + - std::abs(n.dot(Vec3f::UnitY())) + - std::abs(n.dot(Vec3f::UnitZ()))); + return scaled(std::abs(C.dot(Vec3f::UnitX())) + + std::abs(C.dot(Vec3f::UnitY()))); }; size_t facecount = mesh.its.indices.size(); From 773116b777fa771420c884f7ec38b03c7587c340 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Fri, 26 Mar 2021 18:22:53 +0100 Subject: [PATCH 12/17] Allow auto-rotation of objects not completely inside bed. Don't use SLAPrintObject as the input for optimization. Use ModelObject and pass the print config to the optimization in RotoptimizeJob::prepare() --- src/libslic3r/SLA/Rotfinder.cpp | 37 ++++++++++++++--------- src/libslic3r/SLA/Rotfinder.hpp | 42 ++++++++++++++++++++------ src/slic3r/GUI/Jobs/RotoptimizeJob.cpp | 24 ++++++++++----- src/slic3r/GUI/Jobs/RotoptimizeJob.hpp | 11 ++++--- 4 files changed, 77 insertions(+), 37 deletions(-) diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index 89de1cf83..6e8a0ce6b 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "libslic3r/SLAPrint.hpp" #include "libslic3r/PrintConfig.hpp" @@ -192,10 +193,10 @@ XYRotation from_transform3f(const Transform3f &tr) return {rot3.x(), rot3.y()}; } -inline bool is_on_floor(const SLAPrintObject &mo) +inline bool is_on_floor(const SLAPrintObjectConfig &cfg) { - auto opt_elevation = mo.config().support_object_elevation.getFloat(); - auto opt_padaround = mo.config().pad_around_object.getBool(); + auto opt_elevation = cfg.support_object_elevation.getFloat(); + auto opt_padaround = cfg.pad_around_object.getBool(); return opt_elevation < EPSILON || opt_padaround; } @@ -282,9 +283,8 @@ std::array find_min_score(Fn &&fn, It from, It to, StopCond &&stopfn) } // namespace -Vec2d find_best_misalignment_rotation(const SLAPrintObject & po, - float accuracy, - RotOptimizeStatusCB statuscb) +Vec2d find_best_misalignment_rotation(const ModelObject & mo, + const RotOptimizeParams ¶ms) { static constexpr unsigned MAX_TRIES = 1000; @@ -293,14 +293,16 @@ Vec2d find_best_misalignment_rotation(const SLAPrintObject & po, // We will use only one instance of this converted mesh to examine different // rotations - TriangleMesh mesh = po.model_object()->raw_mesh(); + TriangleMesh mesh = mo.raw_mesh(); mesh.require_shared_vertices(); // To keep track of the number of iterations int status = 0; // The maximum number of iterations - auto max_tries = unsigned(accuracy * MAX_TRIES); + auto max_tries = unsigned(params.accuracy() * MAX_TRIES); + + auto &statuscb = params.statuscb(); // call status callback with zero, because we are at the start statuscb(status); @@ -338,9 +340,8 @@ Vec2d find_best_misalignment_rotation(const SLAPrintObject & po, return {rot[0], rot[1]}; } -Vec2d find_least_supports_rotation(const SLAPrintObject & po, - float accuracy, - RotOptimizeStatusCB statuscb) +Vec2d find_least_supports_rotation(const ModelObject & mo, + const RotOptimizeParams ¶ms) { static const unsigned MAX_TRIES = 1000; @@ -349,14 +350,16 @@ Vec2d find_least_supports_rotation(const SLAPrintObject & po, // We will use only one instance of this converted mesh to examine different // rotations - TriangleMesh mesh = po.model_object()->raw_mesh(); + TriangleMesh mesh = mo.raw_mesh(); mesh.require_shared_vertices(); // To keep track of the number of iterations unsigned status = 0; // The maximum number of iterations - auto max_tries = unsigned(accuracy * MAX_TRIES); + auto max_tries = unsigned(params.accuracy() * MAX_TRIES); + + auto &statuscb = params.statuscb(); // call status callback with zero, because we are at the start statuscb(status); @@ -370,8 +373,14 @@ Vec2d find_least_supports_rotation(const SLAPrintObject & po, return ! statuscb(-1); }; + SLAPrintObjectConfig pocfg; + if (params.print_config()) + pocfg.apply(*params.print_config(), true); + + pocfg.apply(mo.config.get()); + // Different search methods have to be used depending on the model elevation - if (is_on_floor(po)) { + if (is_on_floor(pocfg)) { std::vector inputs = get_chull_rotations(mesh, max_tries); max_tries = inputs.size(); diff --git a/src/libslic3r/SLA/Rotfinder.hpp b/src/libslic3r/SLA/Rotfinder.hpp index c0007944a..77a39016d 100644 --- a/src/libslic3r/SLA/Rotfinder.hpp +++ b/src/libslic3r/SLA/Rotfinder.hpp @@ -8,13 +8,39 @@ namespace Slic3r { +class ModelObject; class SLAPrintObject; class TriangleMesh; +class DynamicPrintConfig; namespace sla { using RotOptimizeStatusCB = std::function; +class RotOptimizeParams { + float m_accuracy = 1.; + const DynamicPrintConfig *m_print_config = nullptr; + RotOptimizeStatusCB m_statuscb = [](int) { return true; }; + +public: + + RotOptimizeParams &accuracy(float a) { m_accuracy = a; return *this; } + RotOptimizeParams &print_config(const DynamicPrintConfig *c) + { + m_print_config = c; + return *this; + } + RotOptimizeParams &statucb(RotOptimizeStatusCB cb) + { + m_statuscb = std::move(cb); + return *this; + } + + float accuracy() const { return m_accuracy; } + const DynamicPrintConfig * print_config() const { return m_print_config; } + const RotOptimizeStatusCB &statuscb() const { return m_statuscb; } +}; + /** * The function should find the best rotation for SLA upside down printing. * @@ -31,17 +57,13 @@ using RotOptimizeStatusCB = std::function; * * @return Returns the rotations around each axis (x, y, z) */ -Vec2d find_best_misalignment_rotation( - const SLAPrintObject& modelobj, - float accuracy = 1.0f, - RotOptimizeStatusCB statuscb = [] (int) { return true; } - ); +Vec2d find_best_misalignment_rotation(const ModelObject &modelobj, + const RotOptimizeParams & = {}); -Vec2d find_least_supports_rotation( - const SLAPrintObject& modelobj, - float accuracy = 1.0f, - RotOptimizeStatusCB statuscb = [] (int) { return true; } - ); +Vec2d find_least_supports_rotation(const ModelObject &modelobj, + const RotOptimizeParams & = {}); + +double find_Z_fit_to_bed_rotation(const ModelObject &mo, const BoundingBox &bed); } // namespace sla } // namespace Slic3r diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index 05b141131..04144112e 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -7,6 +7,7 @@ #include "libslic3r/SLAPrint.hpp" #include "slic3r/GUI/Plater.hpp" +#include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "libslic3r/AppConfig.hpp" @@ -29,25 +30,32 @@ void RotoptimizeJob::prepare() m_accuracy = std::max(0.f, std::min(m_accuracy, 1.f)); m_method_id = std::max(size_t(0), std::min(get_methods_count() - 1, m_method_id)); + + m_default_print_cfg = wxGetApp().preset_bundle->full_config(); } void RotoptimizeJob::process() { int obj_idx = m_plater->get_selected_object_idx(); - if (obj_idx < 0 || int(m_plater->sla_print().objects().size()) <= obj_idx) + if (obj_idx < 0) return; ModelObject *o = m_plater->model().objects[size_t(obj_idx)]; - const SLAPrintObject *po = m_plater->sla_print().objects()[size_t(obj_idx)]; - if (!o || !po) return; + if (!o) return; - Vec2d r = Methods[m_method_id].findfn(*po, m_accuracy, [this](int s) { - if (s > 0 && s < 100) - update_status(s, _(L("Searching for optimal orientation"))); + auto params = + sla::RotOptimizeParams{} + .accuracy(m_accuracy) + .print_config(&m_default_print_cfg) + .statucb([this](int s) { + if (s > 0 && s < 100) + update_status(s, _(L("Searching for optimal orientation"))); - return !was_canceled(); - }); + return !was_canceled(); + }); + + Vec2d r = Methods[m_method_id].findfn(*o, params); double mindist = 6.0; // FIXME diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp index dfec2d6a6..cb9a96b56 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp @@ -4,18 +4,16 @@ #include "PlaterJob.hpp" #include "libslic3r/SLA/Rotfinder.hpp" +#include "libslic3r/PrintConfig.hpp" namespace Slic3r { -class SLAPrintObject; - namespace GUI { class RotoptimizeJob : public PlaterJob { - using FindFn = std::function; + using FindFn = std::function; struct FindMethod { std::string name; FindFn findfn; }; @@ -26,6 +24,9 @@ class RotoptimizeJob : public PlaterJob size_t m_method_id = 0; float m_accuracy = 0.75; + + DynamicPrintConfig m_default_print_cfg; + protected: void prepare() override; From 804758dfed11543548fe0198ac691308da37fe1c Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Wed, 31 Mar 2021 14:49:22 +0200 Subject: [PATCH 13/17] Remove accuracy slicer No practical use --- src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index 197800797..b22bc080e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -553,12 +553,6 @@ GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, wxGetApp().app_config->set("sla_auto_rotate", "method_id", std::to_string(state.method_id)); } - float accuracy = state.accuracy; - if (imgui->slider_float(_L("Accuracy/Speed"), &accuracy, 0.01f, 1.f, "%.1f")) { - state.accuracy = accuracy; - wxGetApp().app_config->set("sla_auto_rotate", "accuracy", std::to_string(state.accuracy)); - } - ImGui::Separator(); if ( imgui->button(_L("Optimize")) ) { From 649dfca8d6b0a1a70407ec71762aa35069e2b46d Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Wed, 31 Mar 2021 14:50:24 +0200 Subject: [PATCH 14/17] Allow rotation of multiple selected items. Disable auto positioning --- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 30 +++++++++---- src/slic3r/GUI/Jobs/ArrangeJob.hpp | 11 ++++- src/slic3r/GUI/Jobs/RotoptimizeJob.cpp | 61 ++++++++++++++++---------- src/slic3r/GUI/Jobs/RotoptimizeJob.hpp | 13 +++++- src/slic3r/GUI/Plater.cpp | 26 ++++++----- src/slic3r/GUI/Plater.hpp | 2 +- 6 files changed, 99 insertions(+), 44 deletions(-) diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 3f1207b47..391c1fe28 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -78,7 +78,7 @@ void ArrangeJob::prepare_all() { for (ModelObject *obj: m_plater->model().objects) for (ModelInstance *mi : obj->instances) { ArrangePolygons & cont = mi->printable ? m_selected : m_unprintable; - cont.emplace_back(get_arrange_poly(PtrWrapper{mi}, m_plater)); + cont.emplace_back(get_arrange_poly(mi, m_plater)); } if (auto wti = get_wipe_tower_arrangepoly(*m_plater)) @@ -111,7 +111,7 @@ void ArrangeJob::prepare_selected() { for (size_t i = 0; i < inst_sel.size(); ++i) { ArrangePolygon &&ap = - get_arrange_poly(PtrWrapper{mo->instances[i]}, m_plater); + get_arrange_poly(mo->instances[i], m_plater); ArrangePolygons &cont = mo->instances[i]->printable ? (inst_sel[i] ? m_selected : @@ -161,12 +161,7 @@ void ArrangeJob::process() { static const auto arrangestr = _(L("Arranging")); - const GLCanvas3D::ArrangeSettings &settings = - static_cast(m_plater->canvas3D())->get_arrange_settings(); - - arrangement::ArrangeParams params; - params.allow_rotations = settings.enable_rotation; - params.min_obj_distance = scaled(settings.distance); + arrangement::ArrangeParams params = get_arrange_params(m_plater); auto count = unsigned(m_selected.size() + m_unprintable.size()); Points bedpts = get_bed_shape(*m_plater->config()); @@ -235,4 +230,23 @@ double bed_stride(const Plater *plater) { return scaled((1. + LOGICAL_BED_GAP) * bedwidth); } +template<> +arrangement::ArrangePolygon get_arrange_poly(ModelInstance *inst, + const Plater * plater) +{ + return get_arrange_poly(PtrWrapper{inst}, plater); +} + +arrangement::ArrangeParams get_arrange_params(Plater *p) +{ + const GLCanvas3D::ArrangeSettings &settings = + static_cast(p->canvas3D())->get_arrange_settings(); + + arrangement::ArrangeParams params; + params.allow_rotations = settings.enable_rotation; + params.min_obj_distance = scaled(settings.distance); + + return params; +} + }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.hpp b/src/slic3r/GUI/Jobs/ArrangeJob.hpp index 38aafb52c..7fa4b927e 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.hpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.hpp @@ -4,7 +4,11 @@ #include "PlaterJob.hpp" #include "libslic3r/Arrange.hpp" -namespace Slic3r { namespace GUI { +namespace Slic3r { + +class ModelInstance; + +namespace GUI { class ArrangeJob : public PlaterJob { @@ -89,6 +93,11 @@ arrangement::ArrangePolygon get_arrange_poly(T obj, const Plater *plater) return ap; } +template<> +arrangement::ArrangePolygon get_arrange_poly(ModelInstance *inst, + const Plater * plater); + +arrangement::ArrangeParams get_arrange_params(Plater *p); }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index 04144112e..a670affef 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -32,36 +32,59 @@ void RotoptimizeJob::prepare() m_method_id = std::max(size_t(0), std::min(get_methods_count() - 1, m_method_id)); m_default_print_cfg = wxGetApp().preset_bundle->full_config(); + + const auto &sel = m_plater->get_selection().get_content(); + + m_selected_object_ids.clear(); + m_selected_object_ids.reserve(sel.size()); + for (auto &[obj_idx, ignore] : sel) + m_selected_object_ids.emplace_back(obj_idx); } void RotoptimizeJob::process() { - int obj_idx = m_plater->get_selected_object_idx(); - if (obj_idx < 0) - return; - - ModelObject *o = m_plater->model().objects[size_t(obj_idx)]; - - if (!o) return; - + int prev_status = 0; auto params = sla::RotOptimizeParams{} .accuracy(m_accuracy) .print_config(&m_default_print_cfg) - .statucb([this](int s) { + .statucb([this, &prev_status](int s) + { if (s > 0 && s < 100) - update_status(s, _(L("Searching for optimal orientation"))); + update_status(prev_status + s / m_selected_object_ids.size(), + _(L("Searching for optimal orientation"))); return !was_canceled(); }); - Vec2d r = Methods[m_method_id].findfn(*o, params); - double mindist = 6.0; // FIXME + for (ObjRot &objrot : m_selected_object_ids) { + ModelObject *o = m_plater->model().objects[size_t(objrot.idx)]; + if (!o) continue; + + if (Methods[m_method_id].findfn) + objrot.rot = Methods[m_method_id].findfn(*o, params); + + prev_status += 100 / m_selected_object_ids.size(); + + if (was_canceled()) break; + } + + update_status(100, was_canceled() ? _(L("Orientation search canceled.")) : + _(L("Orientation found."))); +} + +void RotoptimizeJob::finalize() +{ + if (was_canceled()) return; + + for (const ObjRot &objrot : m_selected_object_ids) { + ModelObject *o = m_plater->model().objects[size_t(objrot.idx)]; + if (!o) continue; - if (!was_canceled()) { for(ModelInstance * oi : o->instances) { - oi->set_rotation({r[X], r[Y], 0.}); + if (objrot.rot) + oi->set_rotation({objrot.rot->x(), objrot.rot->y(), 0.}); auto trmatrix = oi->get_transformation().get_matrix(); Polygon trchull = o->convex_hull_2d(trmatrix); @@ -77,19 +100,13 @@ void RotoptimizeJob::process() oi->set_rotation(rt); } - m_plater->find_new_position(o->instances, scaled(mindist)); - // Correct the z offset of the object which was corrupted be // the rotation o->ensure_on_bed(); + +// m_plater->find_new_position(o->instances); } - update_status(100, was_canceled() ? _(L("Orientation search canceled.")) : - _(L("Orientation found."))); -} - -void RotoptimizeJob::finalize() -{ if (!was_canceled()) m_plater->update(); diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp index cb9a96b56..1535d0fa6 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp @@ -19,7 +19,9 @@ class RotoptimizeJob : public PlaterJob static inline const FindMethod Methods[] = { { L("Best misalignment"), sla::find_best_misalignment_rotation }, - { L("Least supports"), sla::find_least_supports_rotation } + { L("Least supports"), sla::find_least_supports_rotation }, + // Just a min area bounding box that is done for all methods anyway. + { L("Z axis only"), nullptr } }; size_t m_method_id = 0; @@ -27,6 +29,15 @@ class RotoptimizeJob : public PlaterJob DynamicPrintConfig m_default_print_cfg; + struct ObjRot + { + size_t idx; + std::optional rot; + ObjRot(size_t id): idx{id}, rot{} {} + }; + + std::vector m_selected_object_ids; + protected: void prepare() override; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a640d6e28..e0a5031c2 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2669,32 +2669,36 @@ void Plater::priv::mirror(Axis axis) view3D->mirror_selection(axis); } -void Plater::find_new_position(const ModelInstancePtrs &instances, - coord_t min_d) +void Plater::find_new_position(const ModelInstancePtrs &instances) { arrangement::ArrangePolygons movable, fixed; + arrangement::ArrangeParams arr_params = get_arrange_params(this); for (const ModelObject *mo : p->model.objects) - for (const ModelInstance *inst : mo->instances) { + for (ModelInstance *inst : mo->instances) { auto it = std::find(instances.begin(), instances.end(), inst); - auto arrpoly = inst->get_arrange_polygon(); + auto arrpoly = get_arrange_poly(inst, this); if (it == instances.end()) fixed.emplace_back(std::move(arrpoly)); - else + else { + arrpoly.setter = [it](const arrangement::ArrangePolygon &p) { + if (p.is_arranged() && p.bed_idx == 0) { + Vec2d t = p.translation.cast(); + (*it)->apply_arrange_result(t, p.rotation); + } + }; movable.emplace_back(std::move(arrpoly)); + } } if (auto wt = get_wipe_tower_arrangepoly(*this)) fixed.emplace_back(*wt); - arrangement::arrange(movable, fixed, get_bed_shape(*config()), - arrangement::ArrangeParams{min_d}); + arrangement::arrange(movable, fixed, get_bed_shape(*config()), arr_params); - for (size_t i = 0; i < instances.size(); ++i) - if (movable[i].bed_idx == 0) - instances[i]->apply_arrange_result(movable[i].translation.cast(), - movable[i].rotation); + for (auto & m : movable) + m.apply(); } void Plater::priv::split_object() diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index ff81dad26..16590ed9b 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -258,7 +258,7 @@ public: BoundingBoxf bed_shape_bb() const; void arrange(); - void find_new_position(const ModelInstancePtrs &instances, coord_t min_d); + void find_new_position(const ModelInstancePtrs &instances); void set_current_canvas_as_dirty(); void unbind_canvas_event_handlers(); From 1663787b96138cf4f8cc182932b5016cecf8ca2a Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Wed, 31 Mar 2021 15:21:22 +0200 Subject: [PATCH 15/17] Better naming of gui controls --- src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp | 2 +- src/slic3r/GUI/Jobs/RotoptimizeJob.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index b22bc080e..6b6905e4d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -548,7 +548,7 @@ GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, } int citem = state.method_id; - if (ImGui::Combo(_L("Choose method").c_str(), &citem, options, methods_cnt) ) { + if (ImGui::Combo(_L("Choose goal").c_str(), &citem, options, methods_cnt) ) { state.method_id = citem; wxGetApp().app_config->set("sla_auto_rotate", "method_id", std::to_string(state.method_id)); } diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp index 1535d0fa6..3144f3c3e 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp @@ -18,7 +18,7 @@ class RotoptimizeJob : public PlaterJob struct FindMethod { std::string name; FindFn findfn; }; static inline const FindMethod Methods[] = { - { L("Best misalignment"), sla::find_best_misalignment_rotation }, + { L("Best surface quality"), sla::find_best_misalignment_rotation }, { L("Least supports"), sla::find_least_supports_rotation }, // Just a min area bounding box that is done for all methods anyway. { L("Z axis only"), nullptr } From bed3321324265ec8209e9ae41d207ac5e2e6f982 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Wed, 31 Mar 2021 16:04:16 +0200 Subject: [PATCH 16/17] Small improvement to "least supports" method --- src/libslic3r/SLA/Rotfinder.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index 6e8a0ce6b..b84921279 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -103,8 +103,8 @@ inline double get_supportedness_score(const Facestats &fc) // the DOWN vector. float phi = 1. - std::acos(fc.normal.dot(DOWN)) / float(PI); - // Only consider faces that have have slopes below 90 deg: - phi = phi * (phi > 0.5); + // Only consider faces that have slopes below 90 deg: + phi = phi * (phi >= 0.5f); // Make the huge slopes more significant than the smaller slopes phi = phi * phi * phi; From 9b47fb512ee4c62b7478ad637a69e8d4d883d726 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Wed, 31 Mar 2021 16:31:53 +0200 Subject: [PATCH 17/17] Remove right click menu item for "optimize orientation" --- src/slic3r/GUI/GUI_Factories.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 0bd523b26..01ea53ad3 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -858,10 +858,6 @@ void MenuFactory::create_sla_object_menu() []() { return plater()->can_split(true); }, m_parent); m_sla_object_menu.AppendSeparator(); - - // Add the automatic rotation sub-menu - append_menu_item(&m_sla_object_menu, wxID_ANY, _L("Optimize orientation"), _L("Optimize the rotation of the object for better print results."), - [](wxCommandEvent&) { plater()->optimize_rotation(); }); } void MenuFactory::create_part_menu()