diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 211a2c2e7..34c0efd01 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -169,9 +169,11 @@ set(SLIC3R_GUI_SOURCES GUI/PrintHostDialogs.cpp GUI/PrintHostDialogs.hpp GUI/Jobs/Job.hpp - GUI/Jobs/Job.cpp - GUI/Jobs/PlaterJob.hpp - GUI/Jobs/PlaterJob.cpp + GUI/Jobs/Worker.hpp + GUI/Jobs/BoostThreadWorker.hpp + GUI/Jobs/BoostThreadWorker.cpp + GUI/Jobs/BusyCursorJob.hpp + GUI/Jobs/PlaterWorker.hpp GUI/Jobs/ArrangeJob.hpp GUI/Jobs/ArrangeJob.cpp GUI/Jobs/RotoptimizeJob.hpp @@ -183,6 +185,8 @@ set(SLIC3R_GUI_SOURCES GUI/Jobs/ProgressIndicator.hpp GUI/Jobs/NotificationProgressIndicator.hpp GUI/Jobs/NotificationProgressIndicator.cpp + GUI/Jobs/ThreadSafeQueue.hpp + GUI/Jobs/SLAImportDialog.hpp GUI/ProgressStatusBar.hpp GUI/ProgressStatusBar.cpp GUI/Mouse3DController.cpp diff --git a/src/slic3r/GUI/GUI_Init.cpp b/src/slic3r/GUI/GUI_Init.cpp index 92223a767..000199f93 100644 --- a/src/slic3r/GUI/GUI_Init.cpp +++ b/src/slic3r/GUI/GUI_Init.cpp @@ -9,6 +9,8 @@ #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/MainFrame.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/I18N.hpp" + // To show a message box if GUI initialization ends up with an exception thrown. #include diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index a234a19ff..700b1c17e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -540,11 +540,12 @@ GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, ImVec2 button_sz = {btn_txt_sz.x + padding.x, btn_txt_sz.y + padding.y}; ImGui::SetCursorPosX(padding.x + sz.x - button_sz.x); - if (wxGetApp().plater()->is_any_job_running()) + if (!wxGetApp().plater()->get_ui_job_worker().is_idle()) imgui->disabled_begin(true); if ( imgui->button(btn_txt) ) { - wxGetApp().plater()->optimize_rotation(); + replace_job(wxGetApp().plater()->get_ui_job_worker(), + std::make_unique()); } imgui->disabled_end(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp index af1ecf548..448efd98a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp @@ -2,7 +2,6 @@ #define slic3r_GLGizmoRotate_hpp_ #include "GLGizmoBase.hpp" -#include "../Jobs/RotoptimizeJob.hpp" namespace Slic3r { namespace GUI { diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 2771f9d27..3c7dad0a6 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -162,49 +162,43 @@ void ArrangeJob::prepare() wxGetKeyState(WXK_SHIFT) ? prepare_selected() : prepare_all(); } -void ArrangeJob::on_exception(const std::exception_ptr &eptr) +void ArrangeJob::process(Ctl &ctl) { - try { - if (eptr) - std::rethrow_exception(eptr); - } catch (libnest2d::GeometryException &) { - show_error(m_plater, _(L("Could not arrange model objects! " - "Some geometries may be invalid."))); - } catch (std::exception &) { - PlaterJob::on_exception(eptr); - } -} + static const auto arrangestr = _u8L("Arranging"); -void ArrangeJob::process() -{ - static const auto arrangestr = _(L("Arranging")); + ctl.update_status(0, arrangestr); + ctl.call_on_main_thread([this]{ prepare(); }).wait();; arrangement::ArrangeParams params = get_arrange_params(m_plater); - auto count = unsigned(m_selected.size() + m_unprintable.size()); + auto count = unsigned(m_selected.size() + m_unprintable.size()); Points bedpts = get_bed_shape(*m_plater->config()); - - params.stopcondition = [this]() { return was_canceled(); }; - - params.progressind = [this, count](unsigned st) { + + params.stopcondition = [&ctl]() { return ctl.was_canceled(); }; + + params.progressind = [this, count, &ctl](unsigned st) { st += m_unprintable.size(); - if (st > 0) update_status(int(count - st), arrangestr); + if (st > 0) ctl.update_status(int(count - st) * 100 / status_range(), arrangestr); }; + ctl.update_status(0, arrangestr); + arrangement::arrange(m_selected, m_unselected, bedpts, params); - params.progressind = [this, count](unsigned st) { - if (st > 0) update_status(int(count - st), arrangestr); + params.progressind = [this, count, &ctl](unsigned st) { + if (st > 0) ctl.update_status(int(count - st) * 100 / status_range(), arrangestr); }; arrangement::arrange(m_unprintable, {}, bedpts, params); // finalize just here. - update_status(int(count), - was_canceled() ? _(L("Arranging canceled.")) - : _(L("Arranging done."))); + ctl.update_status(int(count) * 100 / status_range(), ctl.was_canceled() ? + _u8L("Arranging canceled.") : + _u8L("Arranging done.")); } +ArrangeJob::ArrangeJob() : m_plater{wxGetApp().plater()} {} + static std::string concat_strings(const std::set &strings, const std::string &delim = "\n") { @@ -215,10 +209,21 @@ static std::string concat_strings(const std::set &strings, }); } -void ArrangeJob::finalize() { - // Ignore the arrange result if aborted. - if (was_canceled()) return; - +void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) { + try { + if (eptr) + std::rethrow_exception(eptr); + } catch (libnest2d::GeometryException &) { + show_error(m_plater, _(L("Could not arrange model objects! " + "Some geometries may be invalid."))); + eptr = nullptr; + } catch(...) { + eptr = std::current_exception(); + } + + if (canceled || eptr) + return; + // Unprintable items go to the last virtual bed int beds = 0; @@ -250,8 +255,6 @@ void ArrangeJob::finalize() { _L("Arrangement ignored the following objects which can't fit into a single bed:\n%s"), concat_strings(names, "\n"))); } - - Job::finalize(); } std::optional diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.hpp b/src/slic3r/GUI/Jobs/ArrangeJob.hpp index a5ecc0c83..106cc57dd 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.hpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.hpp @@ -1,7 +1,9 @@ #ifndef ARRANGEJOB_HPP #define ARRANGEJOB_HPP -#include "PlaterJob.hpp" +#include + +#include "Job.hpp" #include "libslic3r/Arrange.hpp" namespace Slic3r { @@ -10,13 +12,16 @@ class ModelInstance; namespace GUI { -class ArrangeJob : public PlaterJob +class Plater; + +class ArrangeJob : public Job { using ArrangePolygon = arrangement::ArrangePolygon; using ArrangePolygons = arrangement::ArrangePolygons; ArrangePolygons m_selected, m_unselected, m_unprintable; std::vector m_unarranged; + Plater *m_plater; // clear m_selected and m_unselected, reserve space for next usage void clear_input(); @@ -30,25 +35,20 @@ class ArrangeJob : public PlaterJob ArrangePolygon get_arrange_poly_(ModelInstance *mi); -protected: - - void prepare() override; - - void on_exception(const std::exception_ptr &) override; - - void process() override; - public: - ArrangeJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater} - {} - int status_range() const override + void prepare(); + + void process(Ctl &ctl) override; + + ArrangeJob(); + + int status_range() const { return int(m_selected.size() + m_unprintable.size()); } - void finalize() override; + void finalize(bool canceled, std::exception_ptr &e) override; }; std::optional get_wipe_tower_arrangepoly(const Plater &); diff --git a/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp b/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp new file mode 100644 index 000000000..a4f22be55 --- /dev/null +++ b/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp @@ -0,0 +1,181 @@ +#include + +#include "BoostThreadWorker.hpp" + +namespace Slic3r { namespace GUI { + +void BoostThreadWorker::WorkerMessage::deliver(BoostThreadWorker &runner) +{ + switch(MsgType(get_type())) { + case Empty: break; + case Status: { + auto info = boost::get(m_data); + if (runner.get_pri()) { + runner.get_pri()->set_progress(info.status); + runner.get_pri()->set_status_text(info.msg.c_str()); + } + break; + } + case Finalize: { + auto& entry = boost::get(m_data); + entry.job->finalize(entry.canceled, entry.eptr); + + // Unhandled exceptions are rethrown without mercy. + if (entry.eptr) + std::rethrow_exception(entry.eptr); + + break; + } + case MainThreadCall: { + auto &calldata = boost::get(m_data); + calldata.fn(); + calldata.promise.set_value(); + + break; + } + } +} + +void BoostThreadWorker::run() +{ + bool stop = false; + while (!stop) { + m_input_queue + .consume_one(BlockingWait{0, &m_running}, [this, &stop](JobEntry &e) { + if (!e.job) + stop = true; + else { + m_canceled.store(false); + + try { + e.job->process(*this); + } catch (...) { + e.eptr = std::current_exception(); + } + + e.canceled = m_canceled.load(); + m_output_queue.push(std::move(e)); // finalization message + } + m_running.store(false); + }); + }; +} + +void BoostThreadWorker::update_status(int st, const std::string &msg) +{ + m_output_queue.push(st, msg); +} + +std::future BoostThreadWorker::call_on_main_thread(std::function fn) +{ + MainThreadCallData cbdata{std::move(fn), {}}; + std::future future = cbdata.promise.get_future(); + + m_output_queue.push(std::move(cbdata)); + + return future; +} + +BoostThreadWorker::BoostThreadWorker(std::shared_ptr pri, + boost::thread::attributes &attribs, + const char * name) + : m_progress(std::move(pri)), m_name{name} +{ + if (m_progress) + m_progress->set_cancel_callback([this](){ cancel(); }); + + m_thread = create_thread(attribs, [this] { this->run(); }); + + std::string nm{name}; + if (!nm.empty()) set_thread_name(m_thread, name); +} + +constexpr int ABORT_WAIT_MAX_MS = 10000; + +BoostThreadWorker::~BoostThreadWorker() +{ + bool joined = false; + try { + cancel_all(); + wait_for_idle(ABORT_WAIT_MAX_MS); + m_input_queue.push(JobEntry{nullptr}); + joined = join(ABORT_WAIT_MAX_MS); + } catch(...) {} + + if (!joined) + BOOST_LOG_TRIVIAL(error) + << "Could not join worker thread '" << m_name << "'"; +} + +bool BoostThreadWorker::join(int timeout_ms) +{ + if (!m_thread.joinable()) + return true; + + if (timeout_ms <= 0) { + m_thread.join(); + } + else if (m_thread.try_join_for(boost::chrono::milliseconds(timeout_ms))) { + return true; + } + else + return false; + + return true; +} + +void BoostThreadWorker::process_events() +{ + while (m_output_queue.consume_one([this](WorkerMessage &msg) { + msg.deliver(*this); + })); +} + +bool BoostThreadWorker::wait_for_current_job(unsigned timeout_ms) +{ + bool ret = true; + + if (!is_idle()) { + bool was_finish = false; + bool timeout_reached = false; + while (!timeout_reached && !was_finish) { + timeout_reached = + !m_output_queue.consume_one(BlockingWait{timeout_ms}, + [this, &was_finish]( + WorkerMessage &msg) { + msg.deliver(*this); + if (msg.get_type() == + WorkerMessage::Finalize) + was_finish = true; + }); + } + + ret = !timeout_reached; + } + + return ret; +} + +bool BoostThreadWorker::wait_for_idle(unsigned timeout_ms) +{ + bool timeout_reached = false; + while (!timeout_reached && !is_idle()) { + timeout_reached = !m_output_queue + .consume_one(BlockingWait{timeout_ms}, + [this](WorkerMessage &msg) { + msg.deliver(*this); + }); + } + + return !timeout_reached; +} + +bool BoostThreadWorker::push(std::unique_ptr job) +{ + if (job) + m_input_queue.push(JobEntry{std::move(job)}); + + return bool{job}; +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/BoostThreadWorker.hpp b/src/slic3r/GUI/Jobs/BoostThreadWorker.hpp new file mode 100644 index 000000000..2fe39c0a7 --- /dev/null +++ b/src/slic3r/GUI/Jobs/BoostThreadWorker.hpp @@ -0,0 +1,140 @@ +#ifndef BOOSTTHREADWORKER_HPP +#define BOOSTTHREADWORKER_HPP + +#include + +#include "Worker.hpp" + +#include +#include + +#include "ThreadSafeQueue.hpp" + +namespace Slic3r { namespace GUI { + +// An implementation of the Worker interface which uses the boost::thread +// API and two thread safe message queues to communicate with the main thread +// back and forth. The queue from the main thread to the worker thread holds the +// job entries that will be performed on the worker. The other queue holds messages +// from the worker to the main thread. These messages include status updates, +// finishing operation and arbitrary functiors that need to be performed +// on the main thread during the jobs execution, like displaying intermediate +// results. +class BoostThreadWorker : public Worker, private Job::Ctl +{ + struct JobEntry // Goes into worker and also out of worker as a finalize msg + { + std::unique_ptr job; + bool canceled = false; + std::exception_ptr eptr = nullptr; + }; + + // A message data for status updates. Only goes from worker to main thread. + struct StatusInfo { int status; std::string msg; }; + + // An arbitrary callback to be called on the main thread. Only from worker + // to main thread. + struct MainThreadCallData + { + std::function fn; + std::promise promise; + }; + + struct EmptyMessage {}; + + class WorkerMessage + { + public: + enum MsgType { Empty, Status, Finalize, MainThreadCall }; + + private: + boost::variant m_data; + + public: + WorkerMessage() = default; + WorkerMessage(int s, std::string txt) + : m_data{StatusInfo{s, std::move(txt)}} + {} + WorkerMessage(JobEntry &&entry) : m_data{std::move(entry)} {} + WorkerMessage(MainThreadCallData fn) : m_data{std::move(fn)} {} + + int get_type () const { return m_data.which(); } + + void deliver(BoostThreadWorker &runner); + }; + + using JobQueue = ThreadSafeQueueSPSC; + using MessageQueue = ThreadSafeQueueSPSC; + + boost::thread m_thread; + std::atomic m_running{false}, m_canceled{false}; + std::shared_ptr m_progress; + JobQueue m_input_queue; // from main thread to worker + MessageQueue m_output_queue; // form worker to main thread + std::string m_name; + + void run(); + + bool join(int timeout_ms = 0); + +protected: + // Implement Job::Ctl interface: + + void update_status(int st, const std::string &msg = "") override; + + bool was_canceled() const override { return m_canceled.load(); } + + std::future call_on_main_thread(std::function fn) override; + +public: + explicit BoostThreadWorker(std::shared_ptr pri, + boost::thread::attributes & attr, + const char * name = ""); + + explicit BoostThreadWorker(std::shared_ptr pri, + boost::thread::attributes && attr, + const char * name = "") + : BoostThreadWorker{std::move(pri), attr, name} + {} + + explicit BoostThreadWorker(std::shared_ptr pri, + const char * name = "") + : BoostThreadWorker{std::move(pri), {}, name} + {} + + ~BoostThreadWorker(); + + BoostThreadWorker(const BoostThreadWorker &) = delete; + BoostThreadWorker(BoostThreadWorker &&) = delete; + BoostThreadWorker &operator=(const BoostThreadWorker &) = delete; + BoostThreadWorker &operator=(BoostThreadWorker &&) = delete; + + bool push(std::unique_ptr job) override; + + bool is_idle() const override + { + // The assumption is that jobs can only be queued from a single main + // thread from which this method is also called. And the output + // messages are also processed only in this calling thread. In that + // case, if the input queue is empty, it will remain so during this + // function call. If the worker thread is also not running and the + // output queue is already processed, we can safely say that the + // worker is dormant. + return m_input_queue.empty() && !m_running.load() && m_output_queue.empty(); + } + + void cancel() override { m_canceled.store(true); } + void cancel_all() override { m_input_queue.clear(); cancel(); } + + ProgressIndicator * get_pri() { return m_progress.get(); } + const ProgressIndicator * get_pri() const { return m_progress.get(); } + + void process_events() override; + bool wait_for_current_job(unsigned timeout_ms = 0) override; + bool wait_for_idle(unsigned timeout_ms = 0) override; + +}; + +}} // namespace Slic3r::GUI + +#endif // BOOSTTHREADWORKER_HPP diff --git a/src/slic3r/GUI/Jobs/BusyCursorJob.hpp b/src/slic3r/GUI/Jobs/BusyCursorJob.hpp new file mode 100644 index 000000000..530213b1d --- /dev/null +++ b/src/slic3r/GUI/Jobs/BusyCursorJob.hpp @@ -0,0 +1,48 @@ +#ifndef BUSYCURSORJOB_HPP +#define BUSYCURSORJOB_HPP + +#include "Job.hpp" + +#include + +namespace Slic3r { namespace GUI { + +struct CursorSetterRAII +{ + Job::Ctl &ctl; + CursorSetterRAII(Job::Ctl &c) : ctl{c} + { + ctl.call_on_main_thread([] { wxBeginBusyCursor(); }); + } + ~CursorSetterRAII() + { + ctl.call_on_main_thread([] { wxEndBusyCursor(); }); + } +}; + +template +class BusyCursored: public Job { + JobSubclass m_job; + +public: + template + BusyCursored(Args &&...args) : m_job{std::forward(args)...} + {} + + void process(Ctl &ctl) override + { + CursorSetterRAII cursor_setter{ctl}; + m_job.process(ctl); + } + + void finalize(bool canceled, std::exception_ptr &eptr) override + { + m_job.finalize(canceled, eptr); + } +}; + + +} +} + +#endif // BUSYCURSORJOB_HPP diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp index 870f31f2f..c7d69eb50 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.cpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Model.hpp" #include "libslic3r/ClipperUtils.hpp" +#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" @@ -102,8 +103,12 @@ void FillBedJob::prepare() p.translation(X) -= p.bed_idx * stride; } -void FillBedJob::process() +void FillBedJob::process(Ctl &ctl) { + auto statustxt = _u8L("Filling bed"); + ctl.call_on_main_thread([this] { prepare(); }).wait(); + ctl.update_status(0, statustxt); + if (m_object_idx == -1 || m_selected.empty()) return; const GLCanvas3D::ArrangeSettings &settings = @@ -114,13 +119,13 @@ void FillBedJob::process() params.min_obj_distance = scaled(settings.distance); bool do_stop = false; - params.stopcondition = [this, &do_stop]() { - return was_canceled() || do_stop; + params.stopcondition = [&ctl, &do_stop]() { + return ctl.was_canceled() || do_stop; }; - params.progressind = [this](unsigned st) { + params.progressind = [this, &ctl, &statustxt](unsigned st) { if (st > 0) - update_status(int(m_status_range - st), _(L("Filling bed"))); + ctl.update_status(int(m_status_range - st) * 100 / status_range(), statustxt); }; params.on_packed = [&do_stop] (const ArrangePolygon &ap) { @@ -130,15 +135,18 @@ void FillBedJob::process() arrangement::arrange(m_selected, m_unselected, m_bedpts, params); // finalize just here. - update_status(m_status_range, was_canceled() ? - _(L("Bed filling canceled.")) : - _(L("Bed filling done."))); + ctl.update_status(100, ctl.was_canceled() ? + _u8L("Bed filling canceled.") : + _u8L("Bed filling done.")); } -void FillBedJob::finalize() +FillBedJob::FillBedJob() : m_plater{wxGetApp().plater()} {} + +void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr) { // Ignore the arrange result if aborted. - if (was_canceled()) return; + if (canceled || eptr) + return; if (m_object_idx == -1) return; @@ -167,8 +175,6 @@ void FillBedJob::finalize() m_plater->sidebar() .obj_list()->increase_object_instances(m_object_idx, size_t(added_cnt)); } - - Job::finalize(); } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/FillBedJob.hpp b/src/slic3r/GUI/Jobs/FillBedJob.hpp index bf407656d..b1417bbbd 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.hpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.hpp @@ -7,9 +7,9 @@ namespace Slic3r { namespace GUI { class Plater; -class FillBedJob : public PlaterJob +class FillBedJob : public Job { - int m_object_idx = -1; + int m_object_idx = -1; using ArrangePolygon = arrangement::ArrangePolygon; using ArrangePolygons = arrangement::ArrangePolygons; @@ -20,23 +20,20 @@ class FillBedJob : public PlaterJob Points m_bedpts; int m_status_range = 0; - -protected: - - void prepare() override; - void process() override; + Plater *m_plater; public: - FillBedJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater} - {} + void prepare(); + void process(Ctl &ctl) override; - int status_range() const override + FillBedJob(); + + int status_range() const /*override*/ { return m_status_range; } - void finalize() override; + void finalize(bool canceled, std::exception_ptr &e) override; }; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/Job.cpp b/src/slic3r/GUI/Jobs/Job.cpp deleted file mode 100644 index 9d0d4bc80..000000000 --- a/src/slic3r/GUI/Jobs/Job.cpp +++ /dev/null @@ -1,158 +0,0 @@ -#include -#include - -#include "Job.hpp" -#include -#include - -namespace Slic3r { - -void GUI::Job::run(std::exception_ptr &eptr) -{ - m_running.store(true); - try { - process(); - } catch (...) { - eptr = std::current_exception(); - } - - m_running.store(false); - - // ensure to call the last status to finalize the job - update_status(status_range(), ""); -} - -void GUI::Job::update_status(int st, const wxString &msg) -{ - auto evt = new wxThreadEvent(wxEVT_THREAD, m_thread_evt_id); - evt->SetInt(st); - evt->SetString(msg); - wxQueueEvent(this, evt); -} - -GUI::Job::Job(std::shared_ptr pri) - : m_progress(std::move(pri)) -{ - m_thread_evt_id = wxNewId(); - - Bind(wxEVT_THREAD, [this](const wxThreadEvent &evt) { - if (m_finalizing) return; - - auto msg = evt.GetString(); - if (!msg.empty() && !m_worker_error) - m_progress->set_status_text(msg.ToUTF8().data()); - - if (m_finalized) return; - - m_progress->set_progress(evt.GetInt()); - if (evt.GetInt() == status_range() || m_worker_error) { - // set back the original range and cancel callback - m_progress->set_range(m_range); - // Make sure progress indicators get the last value of their range - // to make sure they close, fade out, whathever - m_progress->set_progress(m_range); - m_progress->set_cancel_callback(); - wxEndBusyCursor(); - - if (m_worker_error) { - m_finalized = true; - m_progress->set_status_text(""); - m_progress->set_progress(m_range); - on_exception(m_worker_error); - } - else { - // This is an RAII solution to remember that finalization is - // running. The run method calls update_status(status_range(), "") - // at the end, which queues up a call to this handler in all cases. - // If process also calls update_status with maxed out status arg - // it will call this handler twice. It is not a problem unless - // yield is called inside the finilize() method, which would - // jump out of finalize and call this handler again. - struct Finalizing { - bool &flag; - Finalizing (bool &f): flag(f) { flag = true; } - ~Finalizing() { flag = false; } - } fin(m_finalizing); - - finalize(); - } - - // dont do finalization again for the same process - m_finalized = true; - } - }, m_thread_evt_id); -} - -void GUI::Job::start() -{ // Start the job. No effect if the job is already running - if (!m_running.load()) { - prepare(); - - // Save the current status indicatior range and push the new one - m_range = m_progress->get_range(); - m_progress->set_range(status_range()); - - // init cancellation flag and set the cancel callback - m_canceled.store(false); - m_progress->set_cancel_callback( - [this]() { m_canceled.store(true); }); - - m_finalized = false; - m_finalizing = false; - - // Changing cursor to busy - wxBeginBusyCursor(); - - try { // Execute the job - m_worker_error = nullptr; - m_thread = create_thread([this] { this->run(m_worker_error); }); - } catch (std::exception &) { - update_status(status_range(), - _(L("ERROR: not enough resources to " - "execute a new job."))); - } - - // The state changes will be undone when the process hits the - // last status value, in the status update handler (see ctor) - } -} - -bool GUI::Job::join(int timeout_ms) -{ - if (!m_thread.joinable()) return true; - - if (timeout_ms <= 0) - m_thread.join(); - else if (!m_thread.try_join_for(boost::chrono::milliseconds(timeout_ms))) - return false; - - return true; -} - -void GUI::ExclusiveJobGroup::start(size_t jid) { - assert(jid < m_jobs.size()); - stop_all(); - m_jobs[jid]->start(); -} - -void GUI::ExclusiveJobGroup::join_all(int wait_ms) -{ - std::vector aborted(m_jobs.size(), false); - - for (size_t jid = 0; jid < m_jobs.size(); ++jid) - aborted[jid] = m_jobs[jid]->join(wait_ms); - - if (!std::all_of(aborted.begin(), aborted.end(), [](bool t) { return t; })) - BOOST_LOG_TRIVIAL(error) << "Could not abort a job!"; -} - -bool GUI::ExclusiveJobGroup::is_any_running() const -{ - return std::any_of(m_jobs.begin(), m_jobs.end(), - [](const std::unique_ptr &j) { - return j->is_running(); - }); -} - -} - diff --git a/src/slic3r/GUI/Jobs/Job.hpp b/src/slic3r/GUI/Jobs/Job.hpp index 8243ce943..824c0b830 100644 --- a/src/slic3r/GUI/Jobs/Job.hpp +++ b/src/slic3r/GUI/Jobs/Job.hpp @@ -3,119 +3,53 @@ #include #include +#include #include "libslic3r/libslic3r.h" - -#include - #include "ProgressIndicator.hpp" -#include - -#include - namespace Slic3r { namespace GUI { -// A class to handle UI jobs like arranging and optimizing rotation. -// These are not instant jobs, the user has to be informed about their -// state in the status progress indicator. On the other hand they are -// separated from the background slicing process. Ideally, these jobs should -// run when the background process is not running. -// -// TODO: A mechanism would be useful for blocking the plater interactions: -// objects would be frozen for the user. In case of arrange, an animation -// could be shown, or with the optimize orientations, partial results -// could be displayed. -class Job : public wxEvtHandler -{ - int m_range = 100; - int m_thread_evt_id = wxID_ANY; - boost::thread m_thread; - std::atomic m_running{false}, m_canceled{false}; - bool m_finalized = false, m_finalizing = false; - std::shared_ptr m_progress; - std::exception_ptr m_worker_error = nullptr; - - void run(std::exception_ptr &); - -protected: - // status range for a particular job - virtual int status_range() const { return 100; } - - // status update, to be used from the work thread (process() method) - void update_status(int st, const wxString &msg = ""); +// A class representing a job that is to be run in the background, not blocking +// the main thread. Running it is up to a Worker object (see Worker interface) +class Job { +public: - bool was_canceled() const { return m_canceled.load(); } + // A controller interface that informs the job about cancellation and + // makes it possible for the job to advertise its status. + class Ctl { + public: + virtual ~Ctl() = default; - // Launched just before start(), a job can use it to prepare internals - virtual void prepare() {} + // status update, to be used from the work thread (process() method) + virtual void update_status(int st, const std::string &msg = "") = 0; - // The method where the actual work of the job should be defined. - virtual void process() = 0; - - // Launched when the job is finished. It refreshes the 3Dscene by def. - virtual void finalize() { m_finalized = true; } + // Returns true if the job was asked to cancel itself. + virtual bool was_canceled() const = 0; + // Execute a functor on the main thread. Note that the exact time of + // execution is hard to determine. This can be used to make modifications + // on the UI, like displaying some intermediate results or modify the + // cursor. + // This function returns a std::future object which enables the + // caller to optionally wait for the main thread to finish the function call. + virtual std::future call_on_main_thread(std::function fn) = 0; + }; + + virtual ~Job() = default; + + // The method where the actual work of the job should be defined. This is + // run on the worker thread. + virtual void process(Ctl &ctl) = 0; + + // Launched when the job is finished on the UI thread. + // If the job was cancelled, the first parameter will have a true value. // Exceptions occuring in process() are redirected from the worker thread - // into the main (UI) thread. This method is called from the main thread and - // can be overriden to handle these exceptions. - virtual void on_exception(const std::exception_ptr &eptr) - { - if (eptr) std::rethrow_exception(eptr); - } - -public: - Job(std::shared_ptr pri); - - bool is_finalized() const { return m_finalized; } - - Job(const Job &) = delete; - Job(Job &&) = delete; - Job &operator=(const Job &) = delete; - Job &operator=(Job &&) = delete; - - void start(); - - // To wait for the running job and join the threads. False is - // returned if the timeout has been reached and the job is still - // running. Call cancel() before this fn if you want to explicitly - // end the job. - bool join(int timeout_ms = 0); - - bool is_running() const { return m_running.load(); } - void cancel() { m_canceled.store(true); } -}; - -// Jobs defined inside the group class will be managed so that only one can -// run at a time. Also, the background process will be stopped if a job is -// started. -class ExclusiveJobGroup -{ - static const int ABORT_WAIT_MAX_MS = 10000; - - std::vector> m_jobs; - -protected: - virtual void before_start() {} - -public: - virtual ~ExclusiveJobGroup() = default; - - size_t add_job(std::unique_ptr &&job) - { - m_jobs.emplace_back(std::move(job)); - return m_jobs.size() - 1; - } - - void start(size_t jid); - - void cancel_all() { for (auto& j : m_jobs) j->cancel(); } - - void join_all(int wait_ms = 0); - - void stop_all() { cancel_all(); join_all(ABORT_WAIT_MAX_MS); } - - bool is_any_running() const; + // into the main (UI) thread. This method receives the exception and can + // handle it properly. Assign nullptr to this second argument before + // function return to prevent further action. Leaving it with a non-null + // value will result in rethrowing by the worker. + virtual void finalize(bool /*canceled*/, std::exception_ptr &) {} }; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp index cb7170568..f398f7333 100644 --- a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp +++ b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp @@ -12,11 +12,15 @@ void NotificationProgressIndicator::set_range(int range) void NotificationProgressIndicator::set_cancel_callback(CancelFn fn) { - m_nm->progress_indicator_set_cancel_callback(std::move(fn)); + m_cancelfn = std::move(fn); + m_nm->progress_indicator_set_cancel_callback(m_cancelfn); } void NotificationProgressIndicator::set_progress(int pr) { + if (!pr) + set_cancel_callback(m_cancelfn); + m_nm->progress_indicator_set_progress(pr); } diff --git a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.hpp b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.hpp index 6b03af69d..b31cb7f7c 100644 --- a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.hpp +++ b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.hpp @@ -9,7 +9,7 @@ class NotificationManager; class NotificationProgressIndicator: public ProgressIndicator { NotificationManager *m_nm = nullptr; - + CancelFn m_cancelfn; public: explicit NotificationProgressIndicator(NotificationManager *nm); diff --git a/src/slic3r/GUI/Jobs/PlaterJob.cpp b/src/slic3r/GUI/Jobs/PlaterJob.cpp deleted file mode 100644 index 4af205d41..000000000 --- a/src/slic3r/GUI/Jobs/PlaterJob.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "PlaterJob.hpp" -#include "slic3r/GUI/GUI.hpp" -#include "slic3r/GUI/Plater.hpp" - -namespace Slic3r { namespace GUI { - -void PlaterJob::on_exception(const std::exception_ptr &eptr) -{ - try { - if (eptr) - std::rethrow_exception(eptr); - } catch (std::exception &e) { - show_error(m_plater, _(L("An unexpected error occured")) + ": "+ e.what()); - } -} - -}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/PlaterJob.hpp b/src/slic3r/GUI/Jobs/PlaterJob.hpp deleted file mode 100644 index fcf0a54b8..000000000 --- a/src/slic3r/GUI/Jobs/PlaterJob.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef PLATERJOB_HPP -#define PLATERJOB_HPP - -#include "Job.hpp" - -namespace Slic3r { namespace GUI { - -class Plater; - -class PlaterJob : public Job { -protected: - Plater *m_plater; - - void on_exception(const std::exception_ptr &) override; - -public: - - PlaterJob(std::shared_ptr pri, Plater *plater): - Job{std::move(pri)}, m_plater{plater} {} -}; - -}} // namespace Slic3r::GUI - -#endif // PLATERJOB_HPP diff --git a/src/slic3r/GUI/Jobs/PlaterWorker.hpp b/src/slic3r/GUI/Jobs/PlaterWorker.hpp new file mode 100644 index 000000000..573590272 --- /dev/null +++ b/src/slic3r/GUI/Jobs/PlaterWorker.hpp @@ -0,0 +1,127 @@ +#ifndef PLATERWORKER_HPP +#define PLATERWORKER_HPP + +#include + +#include "Worker.hpp" +#include "BusyCursorJob.hpp" + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" + +namespace Slic3r { namespace GUI { + +class Plater; + +template +class PlaterWorker: public Worker { + WorkerSubclass m_w; + Plater *m_plater; + + class PlaterJob : public Job { + std::unique_ptr m_job; + Plater *m_plater; + + public: + void process(Ctl &c) override + { + // Ensure that wxWidgets processing wakes up to handle outgoing + // messages in plater's wxIdle handler. Otherwise it might happen + // that the message will only be processed when an event like mouse + // move comes along which might be too late. + struct WakeUpCtl: Ctl { + Ctl &ctl; + WakeUpCtl(Ctl &c) : ctl{c} {} + + void update_status(int st, const std::string &msg = "") override + { + wxWakeUpIdle(); + ctl.update_status(st, msg); + } + + bool was_canceled() const override { return ctl.was_canceled(); } + + std::future call_on_main_thread(std::function fn) override + { + wxWakeUpIdle(); + return ctl.call_on_main_thread(std::move(fn)); + } + + } wctl{c}; + + CursorSetterRAII busycursor{wctl}; + m_job->process(wctl); + } + + void finalize(bool canceled, std::exception_ptr &eptr) override + { + m_job->finalize(canceled, eptr); + + if (eptr) try { + std::rethrow_exception(eptr); + } catch (std::exception &e) { + show_error(m_plater, _L("An unexpected error occured: ") + e.what()); + eptr = nullptr; + } + } + + PlaterJob(Plater *p, std::unique_ptr j) + : m_job{std::move(j)}, m_plater{p} + { + // TODO: decide if disabling slice button during UI job is what we + // want. + // if (m_plater) + // m_plater->sidebar().enable_buttons(false); + } + + ~PlaterJob() override + { + // TODO: decide if disabling slice button during UI job is what we want. + + // Reload scene ensures that the slice button gets properly + // enabled or disabled after the job finishes, depending on the + // state of slicing. This might be an overkill but works for now. + // if (m_plater) + // m_plater->canvas3D()->reload_scene(false); + } + }; + +public: + + template + PlaterWorker(Plater *plater, WorkerArgs &&...args) + : m_w{std::forward(args)...}, m_plater{plater} + { + // Ensure that messages from the worker thread to the UI thread are + // processed continuously. + plater->Bind(wxEVT_IDLE, [this](wxIdleEvent &) { + process_events(); + }); + } + + // Always package the job argument into a PlaterJob + bool push(std::unique_ptr job) override + { + return m_w.push(std::make_unique(m_plater, std::move(job))); + } + + bool is_idle() const override { return m_w.is_idle(); } + void cancel() override { m_w.cancel(); } + void cancel_all() override { m_w.cancel_all(); } + void process_events() override { m_w.process_events(); } + bool wait_for_current_job(unsigned timeout_ms = 0) override + { + return m_w.wait_for_current_job(timeout_ms); + } + bool wait_for_idle(unsigned timeout_ms = 0) override + { + return m_w.wait_for_idle(timeout_ms); + } +}; + +}} // namespace Slic3r::GUI + +#endif // PLATERJOB_HPP diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index 95821a674..e88d24fcd 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -12,6 +12,8 @@ #include "slic3r/GUI/GUI_App.hpp" #include "libslic3r/AppConfig.hpp" +#include + namespace Slic3r { namespace GUI { void RotoptimizeJob::prepare() @@ -45,20 +47,23 @@ void RotoptimizeJob::prepare() } } -void RotoptimizeJob::process() +void RotoptimizeJob::process(Ctl &ctl) { int prev_status = 0; + auto statustxt = _u8L("Searching for optimal orientation"); + ctl.update_status(0, statustxt); + auto params = sla::RotOptimizeParams{} .accuracy(m_accuracy) .print_config(&m_default_print_cfg) - .statucb([this, &prev_status](int s) + .statucb([this, &prev_status, &ctl, &statustxt](int s) { if (s > 0 && s < 100) - update_status(prev_status + s / m_selected_object_ids.size(), - _(L("Searching for optimal orientation"))); + ctl.update_status(prev_status + s / m_selected_object_ids.size(), + statustxt); - return !was_canceled(); + return !ctl.was_canceled(); }); @@ -71,16 +76,20 @@ void RotoptimizeJob::process() prev_status += 100 / m_selected_object_ids.size(); - if (was_canceled()) break; + if (ctl.was_canceled()) break; } - update_status(100, was_canceled() ? _(L("Orientation search canceled.")) : - _(L("Orientation found."))); + ctl.update_status(100, ctl.was_canceled() ? + _u8L("Orientation search canceled.") : + _u8L("Orientation found.")); } -void RotoptimizeJob::finalize() +RotoptimizeJob::RotoptimizeJob() : m_plater{wxGetApp().plater()} { prepare(); } + +void RotoptimizeJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (was_canceled()) return; + if (canceled || eptr) + return; for (const ObjRot &objrot : m_selected_object_ids) { ModelObject *o = m_plater->model().objects[size_t(objrot.idx)]; @@ -111,10 +120,8 @@ void RotoptimizeJob::finalize() // m_plater->find_new_position(o->instances); } - if (!was_canceled()) + if (!canceled) m_plater->update(); - - Job::finalize(); } }} diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp index cdb367f23..71a28deb7 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp @@ -1,16 +1,17 @@ #ifndef ROTOPTIMIZEJOB_HPP #define ROTOPTIMIZEJOB_HPP -#include "PlaterJob.hpp" +#include "Job.hpp" #include "libslic3r/SLA/Rotfinder.hpp" #include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/I18N.hpp" -namespace Slic3r { +namespace Slic3r { namespace GUI { -namespace GUI { +class Plater; -class RotoptimizeJob : public PlaterJob +class RotoptimizeJob : public Job { using FindFn = std::function; @@ -44,19 +45,16 @@ class RotoptimizeJob : public PlaterJob }; std::vector m_selected_object_ids; - -protected: - - void prepare() override; - void process() override; + Plater *m_plater; public: - RotoptimizeJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater} - {} + void prepare(); + void process(Ctl &ctl) override; - void finalize() override; + RotoptimizeJob(); + + void finalize(bool canceled, std::exception_ptr &) override; static constexpr size_t get_methods_count() { return std::size(Methods); } diff --git a/src/slic3r/GUI/Jobs/SLAImportDialog.hpp b/src/slic3r/GUI/Jobs/SLAImportDialog.hpp new file mode 100644 index 000000000..7dbecff2a --- /dev/null +++ b/src/slic3r/GUI/Jobs/SLAImportDialog.hpp @@ -0,0 +1,114 @@ +#ifndef SLAIMPORTDIALOG_HPP +#define SLAIMPORTDIALOG_HPP + +#include "SLAImportJob.hpp" + +#include +#include +#include +#include +#include + +#include "libslic3r/AppConfig.hpp" +#include "slic3r/GUI/I18N.hpp" + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" + +//#include "libslic3r/Model.hpp" +//#include "libslic3r/PresetBundle.hpp" + +namespace Slic3r { namespace GUI { + +class SLAImportDialog: public wxDialog, public SLAImportJobView { + wxFilePickerCtrl *m_filepicker; + wxComboBox *m_import_dropdown, *m_quality_dropdown; + +public: + SLAImportDialog(Plater *plater) + : wxDialog{plater, wxID_ANY, "Import SLA archive"} + { + auto szvert = new wxBoxSizer{wxVERTICAL}; + auto szfilepck = new wxBoxSizer{wxHORIZONTAL}; + + m_filepicker = new wxFilePickerCtrl(this, wxID_ANY, + from_u8(wxGetApp().app_config->get_last_dir()), _(L("Choose SLA archive:")), + "SL1 / SL1S archive files (*.sl1, *.sl1s, *.zip)|*.sl1;*.SL1;*.sl1s;*.SL1S;*.zip;*.ZIP", + wxDefaultPosition, wxDefaultSize, wxFLP_DEFAULT_STYLE | wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + szfilepck->Add(new wxStaticText(this, wxID_ANY, _L("Import file") + ": "), 0, wxALIGN_CENTER); + szfilepck->Add(m_filepicker, 1); + szvert->Add(szfilepck, 0, wxALL | wxEXPAND, 5); + + auto szchoices = new wxBoxSizer{wxHORIZONTAL}; + + static const std::vector inp_choices = { + _(L("Import model and profile")), + _(L("Import profile only")), + _(L("Import model only")) + }; + + m_import_dropdown = new wxComboBox( + this, wxID_ANY, inp_choices[0], wxDefaultPosition, wxDefaultSize, + inp_choices.size(), inp_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); + + szchoices->Add(m_import_dropdown); + szchoices->Add(new wxStaticText(this, wxID_ANY, _L("Quality") + ": "), 0, wxALIGN_CENTER | wxALL, 5); + + static const std::vector qual_choices = { + _(L("Accurate")), + _(L("Balanced")), + _(L("Quick")) + }; + + m_quality_dropdown = new wxComboBox( + this, wxID_ANY, qual_choices[0], wxDefaultPosition, wxDefaultSize, + qual_choices.size(), qual_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); + szchoices->Add(m_quality_dropdown); + + m_import_dropdown->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) { + if (get_selection() == Sel::profileOnly) + m_quality_dropdown->Disable(); + else m_quality_dropdown->Enable(); + }); + + szvert->Add(szchoices, 0, wxALL, 5); + szvert->AddStretchSpacer(1); + auto szbtn = new wxBoxSizer(wxHORIZONTAL); + szbtn->Add(new wxButton{this, wxID_CANCEL}); + szbtn->Add(new wxButton{this, wxID_OK}); + szvert->Add(szbtn, 0, wxALIGN_RIGHT | wxALL, 5); + + SetSizerAndFit(szvert); + } + + Sel get_selection() const override + { + int sel = m_import_dropdown->GetSelection(); + return Sel(std::min(int(Sel::modelOnly), std::max(0, sel))); + } + + Vec2i get_marchsq_windowsize() const override + { + enum { Accurate, Balanced, Fast}; + + switch(m_quality_dropdown->GetSelection()) + { + case Fast: return {8, 8}; + case Balanced: return {4, 4}; + default: + case Accurate: + return {2, 2}; + } + } + + std::string get_path() const override + { + return m_filepicker->GetPath().ToUTF8().data(); + } +}; + +}} // namespace Slic3r::GUI + +#endif // SLAIMPORTDIALOG_HPP diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.cpp b/src/slic3r/GUI/Jobs/SLAImportJob.cpp index 0d42cec2d..1bb8cdf6c 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -3,7 +3,6 @@ #include "libslic3r/Format/SL1.hpp" #include "slic3r/GUI/GUI.hpp" -#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/NotificationManager.hpp" @@ -11,104 +10,10 @@ #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" -#include -#include -#include #include -#include namespace Slic3r { namespace GUI { -enum class Sel { modelAndProfile, profileOnly, modelOnly}; - -class ImportDlg: public wxDialog { - wxFilePickerCtrl *m_filepicker; - wxComboBox *m_import_dropdown, *m_quality_dropdown; - -public: - ImportDlg(Plater *plater) - : wxDialog{plater, wxID_ANY, "Import SLA archive"} - { - auto szvert = new wxBoxSizer{wxVERTICAL}; - auto szfilepck = new wxBoxSizer{wxHORIZONTAL}; - - m_filepicker = new wxFilePickerCtrl(this, wxID_ANY, - from_u8(wxGetApp().app_config->get_last_dir()), _(L("Choose SLA archive:")), - "SL1 / SL1S archive files (*.sl1, *.sl1s, *.zip)|*.sl1;*.SL1;*.sl1s;*.SL1S;*.zip;*.ZIP", - wxDefaultPosition, wxDefaultSize, wxFLP_DEFAULT_STYLE | wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - szfilepck->Add(new wxStaticText(this, wxID_ANY, _L("Import file") + ": "), 0, wxALIGN_CENTER); - szfilepck->Add(m_filepicker, 1); - szvert->Add(szfilepck, 0, wxALL | wxEXPAND, 5); - - auto szchoices = new wxBoxSizer{wxHORIZONTAL}; - - static const std::vector inp_choices = { - _(L("Import model and profile")), - _(L("Import profile only")), - _(L("Import model only")) - }; - - m_import_dropdown = new wxComboBox( - this, wxID_ANY, inp_choices[0], wxDefaultPosition, wxDefaultSize, - inp_choices.size(), inp_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); - - szchoices->Add(m_import_dropdown); - szchoices->Add(new wxStaticText(this, wxID_ANY, _L("Quality") + ": "), 0, wxALIGN_CENTER | wxALL, 5); - - static const std::vector qual_choices = { - _(L("Accurate")), - _(L("Balanced")), - _(L("Quick")) - }; - - m_quality_dropdown = new wxComboBox( - this, wxID_ANY, qual_choices[0], wxDefaultPosition, wxDefaultSize, - qual_choices.size(), qual_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); - szchoices->Add(m_quality_dropdown); - - m_import_dropdown->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) { - if (get_selection() == Sel::profileOnly) - m_quality_dropdown->Disable(); - else m_quality_dropdown->Enable(); - }); - - szvert->Add(szchoices, 0, wxALL, 5); - szvert->AddStretchSpacer(1); - auto szbtn = new wxBoxSizer(wxHORIZONTAL); - szbtn->Add(new wxButton{this, wxID_CANCEL}); - szbtn->Add(new wxButton{this, wxID_OK}); - szvert->Add(szbtn, 0, wxALIGN_RIGHT | wxALL, 5); - - SetSizerAndFit(szvert); - } - - Sel get_selection() const - { - int sel = m_import_dropdown->GetSelection(); - return Sel(std::min(int(Sel::modelOnly), std::max(0, sel))); - } - - Vec2i get_marchsq_windowsize() const - { - enum { Accurate, Balanced, Fast}; - - switch(m_quality_dropdown->GetSelection()) - { - case Fast: return {8, 8}; - case Balanced: return {4, 4}; - default: - case Accurate: - return {2, 2}; - } - } - - wxString get_path() const - { - return m_filepicker->GetPath(); - } -}; - class SLAImportJob::priv { public: Plater *plater; @@ -122,23 +27,28 @@ public: std::string err; ConfigSubstitutions config_substitutions; - ImportDlg import_dlg; + const SLAImportJobView * import_dlg; - priv(Plater *plt) : plater{plt}, import_dlg{plt} {} + priv(Plater *plt, const SLAImportJobView *view) : plater{plt}, import_dlg{view} {} }; -SLAImportJob::SLAImportJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater}, p{std::make_unique(plater)} -{} +SLAImportJob::SLAImportJob(const SLAImportJobView *view) + : p{std::make_unique(wxGetApp().plater(), view)} +{ + prepare(); +} SLAImportJob::~SLAImportJob() = default; -void SLAImportJob::process() +void SLAImportJob::process(Ctl &ctl) { - auto progr = [this](int s) { + auto statustxt = _u8L("Importing SLA archive"); + ctl.update_status(0, statustxt); + + auto progr = [&ctl, &statustxt](int s) { if (s < 100) - update_status(int(s), _(L("Importing SLA archive"))); - return !was_canceled(); + ctl.update_status(int(s), statustxt); + return !ctl.was_canceled(); }; if (p->path.empty()) return; @@ -161,15 +71,15 @@ void SLAImportJob::process() p->err = ex.what(); } - update_status(100, was_canceled() ? _(L("Importing canceled.")) : - _(L("Importing done."))); + ctl.update_status(100, ctl.was_canceled() ? _u8L("Importing canceled.") : + _u8L("Importing done.")); } void SLAImportJob::reset() { p->sel = Sel::modelAndProfile; p->mesh = {}; - p->profile = m_plater->sla_print().full_print_config(); + p->profile = p->plater->sla_print().full_print_config(); p->win = {2, 2}; p->path.Clear(); } @@ -178,22 +88,19 @@ void SLAImportJob::prepare() { reset(); - if (p->import_dlg.ShowModal() == wxID_OK) { - auto path = p->import_dlg.get_path(); - auto nm = wxFileName(path); - p->path = !nm.Exists(wxFILE_EXISTS_REGULAR) ? "" : nm.GetFullPath(); - p->sel = p->import_dlg.get_selection(); - p->win = p->import_dlg.get_marchsq_windowsize(); - p->config_substitutions.clear(); - } else { - p->path = ""; - } + auto path = p->import_dlg->get_path(); + auto nm = wxFileName(path); + p->path = !nm.Exists(wxFILE_EXISTS_REGULAR) ? "" : nm.GetFullPath(); + p->sel = p->import_dlg->get_selection(); + p->win = p->import_dlg->get_marchsq_windowsize(); + p->config_substitutions.clear(); } -void SLAImportJob::finalize() +void SLAImportJob::finalize(bool canceled, std::exception_ptr &eptr) { // Ignore the arrange result if aborted. - if (was_canceled()) return; + if (canceled || eptr) + return; if (!p->err.empty()) { show_error(p->plater, p->err); @@ -204,7 +111,7 @@ void SLAImportJob::finalize() std::string name = wxFileName(p->path).GetName().ToUTF8().data(); if (p->profile.empty()) { - m_plater->get_notification_manager()->push_notification( + p->plater->get_notification_manager()->push_notification( NotificationType::CustomNotification, NotificationManager::NotificationLevel::WarningNotificationLevel, _L("The imported SLA archive did not contain any presets. " @@ -213,7 +120,7 @@ void SLAImportJob::finalize() if (p->sel != Sel::modelOnly) { if (p->profile.empty()) - p->profile = m_plater->sla_print().full_print_config(); + p->profile = p->plater->sla_print().full_print_config(); const ModelObjectPtrs& objects = p->plater->model().objects; for (auto object : objects) diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.hpp b/src/slic3r/GUI/Jobs/SLAImportJob.hpp index c2ca10ef6..b2aea8bf8 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.hpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.hpp @@ -1,22 +1,37 @@ #ifndef SLAIMPORTJOB_HPP #define SLAIMPORTJOB_HPP -#include "PlaterJob.hpp" +#include "Job.hpp" + +#include "libslic3r/Point.hpp" namespace Slic3r { namespace GUI { -class SLAImportJob : public PlaterJob { +class SLAImportJobView { +public: + enum Sel { modelAndProfile, profileOnly, modelOnly}; + + virtual ~SLAImportJobView() = default; + + virtual Sel get_selection() const = 0; + virtual Vec2i get_marchsq_windowsize() const = 0; + virtual std::string get_path() const = 0; +}; + +class Plater; + +class SLAImportJob : public Job { class priv; std::unique_ptr p; - -protected: - void prepare() override; - void process() override; - void finalize() override; + using Sel = SLAImportJobView::Sel; public: - SLAImportJob(std::shared_ptr pri, Plater *plater); + void prepare(); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &) override; + + SLAImportJob(const SLAImportJobView *); ~SLAImportJob(); void reset(); diff --git a/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp b/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp new file mode 100644 index 000000000..d40049013 --- /dev/null +++ b/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp @@ -0,0 +1,123 @@ +#ifndef THREADSAFEQUEUE_HPP +#define THREADSAFEQUEUE_HPP + +#include +#include +#include +#include +#include + +namespace Slic3r { namespace GUI { + +// Helper structure for overloads of ThreadSafeQueueSPSC::consume_one() +// to block if the queue is empty. +struct BlockingWait +{ + // Timeout to wait for the arrival of new element into the queue. + unsigned timeout_ms = 0; + + // An optional atomic flag to set true if an incoming element gets + // consumed. The flag will be atomically set to true when popping the + // front of the queue. + std::atomic *pop_flag = nullptr; +}; + +// A thread safe queue for one producer and one consumer. +template class Container = std::deque, + class... ContainerArgs> +class ThreadSafeQueueSPSC +{ + std::queue> m_queue; + mutable std::mutex m_mutex; + std::condition_variable m_cond_var; +public: + + // Consume one element, block if the queue is empty. + template bool consume_one(const BlockingWait &blkw, Fn &&fn) + { + static_assert(!std::is_reference_v, ""); + static_assert(std::is_default_constructible_v, ""); + static_assert(std::is_move_assignable_v || std::is_copy_assignable_v, ""); + + T el; + { + std::unique_lock lk{m_mutex}; + + auto pred = [this]{ return !m_queue.empty(); }; + if (blkw.timeout_ms > 0) { + auto timeout = std::chrono::milliseconds(blkw.timeout_ms); + if (!m_cond_var.wait_for(lk, timeout, pred)) + return false; + } + else + m_cond_var.wait(lk, pred); + + if constexpr (std::is_move_assignable_v) + el = std::move(m_queue.front()); + else + el = m_queue.front(); + + m_queue.pop(); + + if (blkw.pop_flag) + // The optional flag is set before the lock us unlocked. + blkw.pop_flag->store(true); + } + + fn(el); + return true; + } + + // Consume one element, return true if consumed, false if queue was empty. + template bool consume_one(Fn &&fn) + { + T el; + { + std::unique_lock lk{m_mutex}; + if (!m_queue.empty()) { + if constexpr (std::is_move_assignable_v) + el = std::move(m_queue.front()); + else + el = m_queue.front(); + + m_queue.pop(); + } else + return false; + } + + fn(el); + + return true; + } + + // Push element into the queue. + template void push(TArgs&&...el) + { + std::lock_guard lk{m_mutex}; + m_queue.emplace(std::forward(el)...); + m_cond_var.notify_one(); + } + + bool empty() const + { + std::lock_guard lk{m_mutex}; + return m_queue.empty(); + } + + size_t size() const + { + std::lock_guard lk{m_mutex}; + return m_queue.size(); + } + + void clear() + { + std::lock_guard lk{m_mutex}; + while (!m_queue.empty()) m_queue.pop(); + } +}; + +}} // namespace Slic3r::GUI + +#endif // THREADSAFEQUEUE_HPP diff --git a/src/slic3r/GUI/Jobs/Worker.hpp b/src/slic3r/GUI/Jobs/Worker.hpp new file mode 100644 index 000000000..0bc7bc086 --- /dev/null +++ b/src/slic3r/GUI/Jobs/Worker.hpp @@ -0,0 +1,119 @@ +#ifndef PRUSALSICER_WORKER_HPP +#define PRUSALSICER_WORKER_HPP + +#include + +#include "Job.hpp" + +namespace Slic3r { namespace GUI { + +// An interface of a worker that runs jobs on a dedicated worker thread, one +// after the other. It is assumed that every method of this class is called +// from the same main thread. +class Worker { +public: + // Queue up a new job after the current one. This call does not block. + // Returns false if the job gets discarded. + virtual bool push(std::unique_ptr job) = 0; + + // Returns true if no job is running, the job queue is empty and no job + // message is left to be processed. This means that nothing is left to + // finalize or take care of in the main thread. + virtual bool is_idle() const = 0; + + // Ask the current job gracefully to cancel. This call is not blocking and + // the job may or may not cancel eventually, depending on its + // implementation. Note that it is not trivial to kill a thread forcefully + // and we don't need that. + virtual void cancel() = 0; + + // This method will delete the queued jobs and cancel the current one. + virtual void cancel_all() = 0; + + // Needs to be called continuously to process events (like status update + // or finalizing of jobs) in the main thread. This can be done e.g. in a + // wxIdle handler. + virtual void process_events() = 0; + + // Wait until the current job finishes. Timeout will only be considered + // if not zero. Returns false if timeout is reached but the job has not + // finished. + virtual bool wait_for_current_job(unsigned timeout_ms = 0) = 0; + + // Wait until the whole job queue finishes. Timeout will only be considered + // if not zero. Returns false only if timeout is reached but the worker has + // not reached the idle state. + virtual bool wait_for_idle(unsigned timeout_ms = 0) = 0; + + // The destructor shall properly close the worker thread. + virtual ~Worker() = default; +}; + +template constexpr bool IsProcessFn = std::is_invocable_v; +template constexpr bool IsFinishFn = std::is_invocable_v; + +// Helper function to use the worker with arbitrary functors. +template>, + class = std::enable_if_t> > +bool queue_job(Worker &w, ProcessFn fn, FinishFn finishfn) +{ + struct LambdaJob: Job { + ProcessFn fn; + FinishFn finishfn; + + LambdaJob(ProcessFn pfn, FinishFn ffn) + : fn{std::move(pfn)}, finishfn{std::move(ffn)} + {} + + void process(Ctl &ctl) override { fn(ctl); } + void finalize(bool canceled, std::exception_ptr &eptr) override + { + finishfn(canceled, eptr); + } + }; + + auto j = std::make_unique(std::move(fn), std::move(finishfn)); + return w.push(std::move(j)); +} + +template>> +bool queue_job(Worker &w, ProcessFn fn) +{ + return queue_job(w, std::move(fn), [](bool, std::exception_ptr &) {}); +} + +inline bool queue_job(Worker &w, std::unique_ptr j) +{ + return w.push(std::move(j)); +} + +// Replace the current job queue with a new job. The signature is the same +// as for queue_job(). This cancels all jobs and +// will not wait. The new job will begin after the queue cancels properly. +// Note that this can be called from the UI thread and will not block it if +// the jobs take longer to cancel. +template bool replace_job(Worker &w, Args&& ...args) +{ + w.cancel_all(); + return queue_job(w, std::forward(args)...); +} + +// Cancel the current job and wait for it to actually be stopped. +inline bool stop_current_job(Worker &w, unsigned timeout_ms = 0) +{ + w.cancel(); + return w.wait_for_current_job(timeout_ms); +} + +// Cancel all pending jobs including current one and wait until the worker +// becomes idle. +inline bool stop_queue(Worker &w, unsigned timeout_ms = 0) +{ + w.cancel_all(); + return w.wait_for_idle(timeout_ms); +} + +}} // namespace Slic3r::GUI + +#endif // WORKER_HPP diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index a88509527..c61c0e6d2 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -574,7 +574,7 @@ void MainFrame::shutdown() #endif // _WIN32 if (m_plater != nullptr) { - m_plater->stop_jobs(); + m_plater->get_ui_job_worker().cancel_all(); // Unbinding of wxWidgets event handling in canvases needs to be done here because on MAC, // when closing the application using Command+Q, a mouse event is triggered after this lambda is completed, @@ -1211,7 +1211,7 @@ void MainFrame::init_menubar_as_editor() append_menu_item(import_menu, wxID_ANY, _L("Import SL1 / SL1S Archive") + dots, _L("Load an SL1 / Sl1S archive"), [this](wxCommandEvent&) { if (m_plater) m_plater->import_sl1_archive(); }, "import_plater", nullptr, - [this](){return m_plater != nullptr && !m_plater->is_any_job_running(); }, this); + [this](){return m_plater != nullptr && m_plater->get_ui_job_worker().is_idle(); }, this); import_menu->AppendSeparator(); append_menu_item(import_menu, wxID_ANY, _L("Import &Config") + dots + "\tCtrl+L", _L("Load exported configuration file"), diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 461eb02e2..7a9390971 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -73,7 +73,10 @@ #include "Jobs/FillBedJob.hpp" #include "Jobs/RotoptimizeJob.hpp" #include "Jobs/SLAImportJob.hpp" +#include "Jobs/SLAImportDialog.hpp" #include "Jobs/NotificationProgressIndicator.hpp" +#include "Jobs/PlaterWorker.hpp" +#include "Jobs/BoostThreadWorker.hpp" #include "BackgroundSlicingProcess.hpp" #include "PrintHostDialogs.hpp" #include "ConfigWizard.hpp" @@ -1638,54 +1641,12 @@ struct Plater::priv BackgroundSlicingProcess background_process; bool suppressed_backround_processing_update { false }; - // Jobs defined inside the group class will be managed so that only one can - // run at a time. Also, the background process will be stopped if a job is - // started. It is up the the plater to ensure that the background slicing - // can't be restarted while a ui job is still running. - class Jobs: public ExclusiveJobGroup - { - priv *m; - size_t m_arrange_id, m_fill_bed_id, m_rotoptimize_id, m_sla_import_id; - std::shared_ptr m_pri; - - void before_start() override { m->background_process.stop(); } - - public: - Jobs(priv *_m) : - m(_m), - m_pri{std::make_shared(m->notification_manager.get())} - { - m_arrange_id = add_job(std::make_unique(m_pri, m->q)); - m_fill_bed_id = add_job(std::make_unique(m_pri, m->q)); - m_rotoptimize_id = add_job(std::make_unique(m_pri, m->q)); - m_sla_import_id = add_job(std::make_unique(m_pri, m->q)); - } - - void arrange() - { - m->take_snapshot(_L("Arrange")); - start(m_arrange_id); - } - - void fill_bed() - { - m->take_snapshot(_L("Fill bed")); - start(m_fill_bed_id); - } - - void optimize_rotation() - { - m->take_snapshot(_L("Optimize Rotation")); - start(m_rotoptimize_id); - } - - void import_sla_arch() - { - m->take_snapshot(_L("Import SLA archive")); - start(m_sla_import_id); - } - - } m_ui_jobs; + // TODO: A mechanism would be useful for blocking the plater interactions: + // objects would be frozen for the user. In case of arrange, an animation + // could be shown, or with the optimize orientations, partial results + // could be displayed. + PlaterWorker m_worker; + SLAImportDialog * m_sla_import_dlg; bool delayed_scene_refresh; std::string delayed_error_message; @@ -1990,7 +1951,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) })) , sidebar(new Sidebar(q)) , notification_manager(std::make_unique(q)) - , m_ui_jobs(this) + , m_worker{q, std::make_unique(notification_manager.get()), "ui_worker"} + , m_sla_import_dlg{new SLAImportDialog{q}} , delayed_scene_refresh(false) , view_toolbar(GLToolbar::Radio, "View") , collapse_toolbar(GLToolbar::Normal, "Collapse") @@ -2926,7 +2888,7 @@ void Plater::priv::remove(size_t obj_idx) if (view3D->is_layers_editing_enabled()) view3D->enable_layers_editing(false); - m_ui_jobs.cancel_all(); + m_worker.cancel_all(); model.delete_object(obj_idx); update(); // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. @@ -2941,7 +2903,7 @@ void Plater::priv::delete_object_from_model(size_t obj_idx) if (! model.objects[obj_idx]->name.empty()) snapshot_label += ": " + wxString::FromUTF8(model.objects[obj_idx]->name.c_str()); Plater::TakeSnapshot snapshot(q, snapshot_label); - m_ui_jobs.cancel_all(); + m_worker.cancel_all(); model.delete_object(obj_idx); update(); object_list_changed(); @@ -2959,7 +2921,7 @@ void Plater::priv::delete_all_objects_from_model() view3D->get_canvas3d()->reset_sequential_print_clearance(); - m_ui_jobs.cancel_all(); + m_worker.cancel_all(); // Stop and reset the Print content. background_process.reset(); @@ -2991,7 +2953,7 @@ void Plater::priv::reset() view3D->get_canvas3d()->reset_sequential_print_clearance(); - m_ui_jobs.cancel_all(); + m_worker.cancel_all(); // Stop and reset the Print content. this->background_process.reset(); @@ -3292,7 +3254,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // Restart background processing thread based on a bitmask of UpdateBackgroundProcessReturnState. bool Plater::priv::restart_background_process(unsigned int state) { - if (m_ui_jobs.is_any_running()) { + if (!m_worker.is_idle()) { // Avoid a race condition return false; } @@ -3920,7 +3882,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) { if (evt.status.percent >= -1) { - if (m_ui_jobs.is_any_running()) { + if (!m_worker.is_idle()) { // Avoid a race condition return; } @@ -4609,7 +4571,7 @@ bool Plater::priv::can_simplify() const bool Plater::priv::can_increase_instances() const { - if (m_ui_jobs.is_any_running() + if (!m_worker.is_idle() || q->canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; @@ -4619,7 +4581,7 @@ bool Plater::priv::can_increase_instances() const bool Plater::priv::can_decrease_instances() const { - if (m_ui_jobs.is_any_running() + if (!m_worker.is_idle() || q->canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; @@ -4639,7 +4601,7 @@ bool Plater::priv::can_split_to_volumes() const bool Plater::priv::can_arrange() const { - return !model.objects.empty() && !m_ui_jobs.is_any_running(); + return !model.objects.empty() && m_worker.is_idle(); } bool Plater::priv::can_layers_editing() const @@ -5093,8 +5055,11 @@ void Plater::add_model(bool imperial_units/* = false*/) void Plater::import_sl1_archive() { - if (!p->m_ui_jobs.is_any_running()) - p->m_ui_jobs.import_sla_arch(); + auto &w = get_ui_job_worker(); + if (w.is_idle() && p->m_sla_import_dlg->ShowModal() == wxID_OK) { + p->take_snapshot(_L("Import SLA archive")); + replace_job(w, std::make_unique(p->m_sla_import_dlg)); + } } void Plater::extract_config_from_project() @@ -5376,12 +5341,9 @@ bool Plater::load_files(const wxArrayString& filenames) void Plater::update() { p->update(); } -void Plater::stop_jobs() { p->m_ui_jobs.stop_all(); } +Worker &Plater::get_ui_job_worker() { return p->m_worker; } -bool Plater::is_any_job_running() const -{ - return p->m_ui_jobs.is_any_running(); -} +const Worker &Plater::get_ui_job_worker() const { return p->m_worker; } void Plater::update_ui_from_settings() { p->update_ui_from_settings(); } @@ -5422,7 +5384,7 @@ void Plater::remove_selected() return; Plater::TakeSnapshot snapshot(this, _L("Delete Selected Objects")); - p->m_ui_jobs.cancel_all(); + get_ui_job_worker().cancel_all(); p->view3D->delete_selected(); } @@ -5531,8 +5493,11 @@ void Plater::set_number_of_copies(/*size_t num*/) void Plater::fill_bed_with_instances() { - if (!p->m_ui_jobs.is_any_running()) - p->m_ui_jobs.fill_bed(); + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_L("Fill bed")); + replace_job(w, std::make_unique()); + } } bool Plater::is_selection_empty() const @@ -5933,8 +5898,14 @@ void Plater::reslice() if (canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) return; - // Stop arrange and (or) optimize rotation tasks. - this->stop_jobs(); + // Stop the running (and queued) UI jobs and only proceed if they actually + // get stopped. + unsigned timeout_ms = 10000; + if (!stop_queue(this->get_ui_job_worker(), timeout_ms)) { + BOOST_LOG_TRIVIAL(error) << "Could not stop UI job within " + << timeout_ms << " milliseconds timeout!"; + return; + } if (printer_technology() == ptSLA) { for (auto& object : model().objects) @@ -6398,8 +6369,11 @@ GLCanvas3D* Plater::get_current_canvas3D() void Plater::arrange() { - if (!p->m_ui_jobs.is_any_running()) - p->m_ui_jobs.arrange(); + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_L("Arrange")); + replace_job(w, std::make_unique()); + } } void Plater::set_current_canvas_as_dirty() @@ -6570,7 +6544,6 @@ void Plater::suppress_background_process(const bool stop_background_process) void Plater::mirror(Axis axis) { p->mirror(axis); } void Plater::split_object() { p->split_object(); } void Plater::split_volume() { p->split_volume(); } -void Plater::optimize_rotation() { if (!p->m_ui_jobs.is_any_running()) p->m_ui_jobs.optimize_rotation(); } void Plater::update_menus() { p->menus.update(); } void Plater::show_action_buttons(const bool ready_to_slice) const { p->show_action_buttons(ready_to_slice); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index d3eead4ed..b0c5a354d 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -14,6 +14,7 @@ #include "libslic3r/BoundingBox.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" #include "Jobs/Job.hpp" +#include "Jobs/Worker.hpp" #include "Search.hpp" class wxButton; @@ -177,8 +178,41 @@ public: const wxString& get_last_loaded_gcode() const { return m_last_loaded_gcode; } void update(); - void stop_jobs(); - bool is_any_job_running() const; + + // Get the worker handling the UI jobs (arrange, fill bed, etc...) + // Here is an example of starting up an ad-hoc job: + // queue_job( + // get_ui_job_worker(), + // [](Job::Ctl &ctl) { + // // Executed in the worker thread + // + // CursorSetterRAII cursor_setter{ctl}; + // std::string msg = "Running"; + // + // ctl.update_status(0, msg); + // for (int i = 0; i < 100; i++) { + // usleep(100000); + // if (ctl.was_canceled()) break; + // ctl.update_status(i + 1, msg); + // } + // ctl.update_status(100, msg); + // }, + // [](bool, std::exception_ptr &e) { + // // Executed in UI thread after the work is done + // + // try { + // if (e) std::rethrow_exception(e); + // } catch (std::exception &e) { + // BOOST_LOG_TRIVIAL(error) << e.what(); + // } + // e = nullptr; + // }); + // This would result in quick run of the progress indicator notification + // from 0 to 100. Use replace_job() instead of queue_job() to cancel all + // pending jobs. + Worker& get_ui_job_worker(); + const Worker & get_ui_job_worker() const; + void select_view(const std::string& direction); void select_view_3D(const std::string& name); @@ -302,7 +336,6 @@ public: void mirror(Axis axis); void split_object(); void split_volume(); - void optimize_rotation(); bool can_delete() const; bool can_delete_all() const; diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index e1ac113ae..be1b645d7 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -1,6 +1,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp + slic3r_jobs_tests.cpp ) target_link_libraries(${_TEST_NAME}_tests test_common libslic3r_gui libslic3r) diff --git a/tests/slic3rutils/slic3r_jobs_tests.cpp b/tests/slic3rutils/slic3r_jobs_tests.cpp new file mode 100644 index 000000000..39682ff98 --- /dev/null +++ b/tests/slic3rutils/slic3r_jobs_tests.cpp @@ -0,0 +1,148 @@ +#include "catch2/catch.hpp" + +#include +#include + +#include "slic3r/GUI/Jobs/BoostThreadWorker.hpp" +#include "slic3r/GUI/Jobs/ProgressIndicator.hpp" + +//#include + +struct Progress: Slic3r::ProgressIndicator { + int range = 100; + int pr = 0; + std::string statustxt; + void set_range(int r) override { range = r; } + void set_cancel_callback(CancelFn = CancelFn()) override {} + void set_progress(int p) override { pr = p; } + void set_status_text(const char *txt) override { statustxt = txt; } + int get_range() const override { return range; } +}; + +TEST_CASE("nullptr job should be ignored", "[Jobs]") { + Slic3r::GUI::BoostThreadWorker worker{std::make_unique()}; + worker.push(nullptr); + + REQUIRE(worker.is_idle()); +} + +TEST_CASE("State should not be idle while running a job", "[Jobs]") { + using namespace Slic3r; + using namespace Slic3r::GUI; + BoostThreadWorker worker{std::make_unique(), "worker_thread"}; + + queue_job(worker, [&worker](Job::Ctl &ctl) { + ctl.call_on_main_thread([&worker] { + REQUIRE(!worker.is_idle()); + }).wait(); + }); + + worker.wait_for_idle(); + + REQUIRE(worker.is_idle()); +} + +TEST_CASE("Status messages should be received by the main thread during job execution", "[Jobs]") { + using namespace Slic3r; + using namespace Slic3r::GUI; + auto pri = std::make_shared(); + BoostThreadWorker worker{pri}; + + queue_job(worker, [](Job::Ctl &ctl){ + for (int s = 0; s <= 100; ++s) { + ctl.update_status(s, "Running"); + } + }); + + worker.wait_for_idle(); + + REQUIRE(pri->pr == 100); + REQUIRE(pri->statustxt == "Running"); +} + +TEST_CASE("Cancellation should be recognized be the worker", "[Jobs]") { + using namespace Slic3r; + using namespace Slic3r::GUI; + + auto pri = std::make_shared(); + BoostThreadWorker worker{pri}; + + queue_job( + worker, + [](Job::Ctl &ctl) { + for (int s = 0; s <= 100; ++s) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + ctl.update_status(s, "Running"); + if (ctl.was_canceled()) break; + } + }, + [](bool cancelled, std::exception_ptr &) { // finalize + REQUIRE(cancelled == true); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + worker.cancel(); + + worker.wait_for_current_job(); + + REQUIRE(pri->pr != 100); +} + +TEST_CASE("cancel_all should remove all pending jobs", "[Jobs]") { + using namespace Slic3r; + using namespace Slic3r::GUI; + + auto pri = std::make_shared(); + BoostThreadWorker worker{pri}; + + std::array jobres = {false}; + + queue_job(worker, [&jobres](Job::Ctl &) { + jobres[0] = true; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }); + queue_job(worker, [&jobres](Job::Ctl &) { + jobres[1] = true; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }); + queue_job(worker, [&jobres](Job::Ctl &) { + jobres[2] = true; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }); + queue_job(worker, [&jobres](Job::Ctl &) { + jobres[3] = true; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }); + + std::this_thread::sleep_for(std::chrono::microseconds(500)); + worker.cancel_all(); + + REQUIRE(jobres[0] == true); + REQUIRE(jobres[1] == false); + REQUIRE(jobres[2] == false); + REQUIRE(jobres[3] == false); +} + +TEST_CASE("Exception should be properly forwarded to finalize()", "[Jobs]") { + using namespace Slic3r; + using namespace Slic3r::GUI; + + auto pri = std::make_shared(); + BoostThreadWorker worker{pri}; + + queue_job( + worker, [](Job::Ctl &) { throw std::runtime_error("test"); }, + [](bool /*canceled*/, std::exception_ptr &eptr) { + REQUIRE(eptr != nullptr); + try { + std::rethrow_exception(eptr); + } catch (std::runtime_error &e) { + REQUIRE(std::string(e.what()) == "test"); + } + + eptr = nullptr; + }); + + worker.wait_for_idle(); + REQUIRE(worker.is_idle()); +}