Merge branch 'tm_ui_job_rework_3' into dev

This commit is contained in:
tamasmeszaros 2022-01-11 11:41:58 +01:00
commit cbcda3b0b5
30 changed files with 1297 additions and 612 deletions

View File

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

View File

@ -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 <wx/msgdlg.h>

View File

@ -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<RotoptimizeJob>());
}
imgui->disabled_end();

View File

@ -2,7 +2,6 @@
#define slic3r_GLGizmoRotate_hpp_
#include "GLGizmoBase.hpp"
#include "../Jobs/RotoptimizeJob.hpp"
namespace Slic3r {
namespace GUI {

View File

@ -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());
Points bedpts = get_bed_shape(*m_plater->config());
params.stopcondition = [this]() { return was_canceled(); };
params.stopcondition = [&ctl]() { return ctl.was_canceled(); };
params.progressind = [this, count](unsigned st) {
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<std::string> &strings,
const std::string &delim = "\n")
{
@ -215,9 +209,20 @@ static std::string concat_strings(const std::set<std::string> &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<arrangement::ArrangePolygon>

View File

@ -1,7 +1,9 @@
#ifndef ARRANGEJOB_HPP
#define ARRANGEJOB_HPP
#include "PlaterJob.hpp"
#include <optional>
#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<ModelInstance*> 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<ProgressIndicator> 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<arrangement::ArrangePolygon> get_wipe_tower_arrangepoly(const Plater &);

View File

@ -0,0 +1,181 @@
#include <exception>
#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<StatusInfo>(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<JobEntry>(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<MainThreadCallData >(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<void> BoostThreadWorker::call_on_main_thread(std::function<void ()> fn)
{
MainThreadCallData cbdata{std::move(fn), {}};
std::future<void> future = cbdata.promise.get_future();
m_output_queue.push(std::move(cbdata));
return future;
}
BoostThreadWorker::BoostThreadWorker(std::shared_ptr<ProgressIndicator> 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> job)
{
if (job)
m_input_queue.push(JobEntry{std::move(job)});
return bool{job};
}
}} // namespace Slic3r::GUI

View File

@ -0,0 +1,140 @@
#ifndef BOOSTTHREADWORKER_HPP
#define BOOSTTHREADWORKER_HPP
#include <boost/variant.hpp>
#include "Worker.hpp"
#include <libslic3r/Thread.hpp>
#include <boost/log/trivial.hpp>
#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> 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<void()> fn;
std::promise<void> promise;
};
struct EmptyMessage {};
class WorkerMessage
{
public:
enum MsgType { Empty, Status, Finalize, MainThreadCall };
private:
boost::variant<EmptyMessage, StatusInfo, JobEntry, MainThreadCallData> 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<JobEntry>;
using MessageQueue = ThreadSafeQueueSPSC<WorkerMessage>;
boost::thread m_thread;
std::atomic<bool> m_running{false}, m_canceled{false};
std::shared_ptr<ProgressIndicator> 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<void> call_on_main_thread(std::function<void()> fn) override;
public:
explicit BoostThreadWorker(std::shared_ptr<ProgressIndicator> pri,
boost::thread::attributes & attr,
const char * name = "");
explicit BoostThreadWorker(std::shared_ptr<ProgressIndicator> pri,
boost::thread::attributes && attr,
const char * name = "")
: BoostThreadWorker{std::move(pri), attr, name}
{}
explicit BoostThreadWorker(std::shared_ptr<ProgressIndicator> 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> 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

View File

@ -0,0 +1,48 @@
#ifndef BUSYCURSORJOB_HPP
#define BUSYCURSORJOB_HPP
#include "Job.hpp"
#include <wx/utils.h>
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 JobSubclass>
class BusyCursored: public Job {
JobSubclass m_job;
public:
template<class... Args>
BusyCursored(Args &&...args) : m_job{std::forward<Args>(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

View File

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

View File

@ -7,7 +7,7 @@ namespace Slic3r { namespace GUI {
class Plater;
class FillBedJob : public PlaterJob
class FillBedJob : public Job
{
int m_object_idx = -1;
@ -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<ProgressIndicator> 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

View File

@ -1,158 +0,0 @@
#include <algorithm>
#include <exception>
#include "Job.hpp"
#include <libslic3r/Thread.hpp>
#include <boost/log/trivial.hpp>
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<ProgressIndicator> 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<bool> 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<GUI::Job> &j) {
return j->is_running();
});
}
}

View File

@ -3,119 +3,53 @@
#include <atomic>
#include <exception>
#include <future>
#include "libslic3r/libslic3r.h"
#include <slic3r/GUI/I18N.hpp>
#include "ProgressIndicator.hpp"
#include <wx/event.h>
#include <boost/thread.hpp>
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<bool> m_running{false}, m_canceled{false};
bool m_finalized = false, m_finalizing = false;
std::shared_ptr<ProgressIndicator> m_progress;
std::exception_ptr m_worker_error = nullptr;
// 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:
void run(std::exception_ptr &);
protected:
// status range for a particular job
virtual int status_range() const { return 100; }
// 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;
// status update, to be used from the work thread (process() method)
void update_status(int st, const wxString &msg = "");
virtual void update_status(int st, const std::string &msg = "") = 0;
bool was_canceled() const { return m_canceled.load(); }
// Returns true if the job was asked to cancel itself.
virtual bool was_canceled() const = 0;
// Launched just before start(), a job can use it to prepare internals
virtual void prepare() {}
// 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<void> object which enables the
// caller to optionally wait for the main thread to finish the function call.
virtual std::future<void> call_on_main_thread(std::function<void()> fn) = 0;
};
// The method where the actual work of the job should be defined.
virtual void process() = 0;
virtual ~Job() = default;
// Launched when the job is finished. It refreshes the 3Dscene by def.
virtual void finalize() { m_finalized = true; }
// 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<ProgressIndicator> 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<std::unique_ptr<GUI::Job>> m_jobs;
protected:
virtual void before_start() {}
public:
virtual ~ExclusiveJobGroup() = default;
size_t add_job(std::unique_ptr<GUI::Job> &&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

View File

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

View File

@ -9,7 +9,7 @@ class NotificationManager;
class NotificationProgressIndicator: public ProgressIndicator {
NotificationManager *m_nm = nullptr;
CancelFn m_cancelfn;
public:
explicit NotificationProgressIndicator(NotificationManager *nm);

View File

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

View File

@ -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<ProgressIndicator> pri, Plater *plater):
Job{std::move(pri)}, m_plater{plater} {}
};
}} // namespace Slic3r::GUI
#endif // PLATERJOB_HPP

View File

@ -0,0 +1,127 @@
#ifndef PLATERWORKER_HPP
#define PLATERWORKER_HPP
#include <map>
#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 WorkerSubclass>
class PlaterWorker: public Worker {
WorkerSubclass m_w;
Plater *m_plater;
class PlaterJob : public Job {
std::unique_ptr<Job> 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<void> call_on_main_thread(std::function<void()> 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<Job> 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<class... WorkerArgs>
PlaterWorker(Plater *plater, WorkerArgs &&...args)
: m_w{std::forward<WorkerArgs>(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> job) override
{
return m_w.push(std::make_unique<PlaterJob>(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

View File

@ -12,6 +12,8 @@
#include "slic3r/GUI/GUI_App.hpp"
#include "libslic3r/AppConfig.hpp"
#include <slic3r/GUI/I18N.hpp>
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();
}
}}

View File

@ -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<Vec2d(const ModelObject & mo,
const sla::RotOptimizeParams &params)>;
@ -44,19 +45,16 @@ class RotoptimizeJob : public PlaterJob
};
std::vector<ObjRot> m_selected_object_ids;
protected:
void prepare() override;
void process() override;
Plater *m_plater;
public:
RotoptimizeJob(std::shared_ptr<ProgressIndicator> 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); }

View File

@ -0,0 +1,114 @@
#ifndef SLAIMPORTDIALOG_HPP
#define SLAIMPORTDIALOG_HPP
#include "SLAImportJob.hpp"
#include <wx/dialog.h>
#include <wx/stattext.h>
#include <wx/combobox.h>
#include <wx/filename.h>
#include <wx/filepicker.h>
#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<wxString> 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<wxString> 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

View File

@ -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 <wx/dialog.h>
#include <wx/stattext.h>
#include <wx/combobox.h>
#include <wx/filename.h>
#include <wx/filepicker.h>
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<wxString> 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<wxString> 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<ProgressIndicator> pri, Plater *plater)
: PlaterJob{std::move(pri), plater}, p{std::make_unique<priv>(plater)}
{}
SLAImportJob::SLAImportJob(const SLAImportJobView *view)
: p{std::make_unique<priv>(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 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->sel = p->import_dlg->get_selection();
p->win = p->import_dlg->get_marchsq_windowsize();
p->config_substitutions.clear();
} else {
p->path = "";
}
}
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)

View File

@ -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<priv> p;
protected:
void prepare() override;
void process() override;
void finalize() override;
using Sel = SLAImportJobView::Sel;
public:
SLAImportJob(std::shared_ptr<ProgressIndicator> pri, Plater *plater);
void prepare();
void process(Ctl &ctl) override;
void finalize(bool canceled, std::exception_ptr &) override;
SLAImportJob(const SLAImportJobView *);
~SLAImportJob();
void reset();

View File

@ -0,0 +1,123 @@
#ifndef THREADSAFEQUEUE_HPP
#define THREADSAFEQUEUE_HPP
#include <type_traits>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>
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<bool> *pop_flag = nullptr;
};
// A thread safe queue for one producer and one consumer.
template<class T,
template<class, class...> class Container = std::deque,
class... ContainerArgs>
class ThreadSafeQueueSPSC
{
std::queue<T, Container<T, ContainerArgs...>> m_queue;
mutable std::mutex m_mutex;
std::condition_variable m_cond_var;
public:
// Consume one element, block if the queue is empty.
template<class Fn> bool consume_one(const BlockingWait &blkw, Fn &&fn)
{
static_assert(!std::is_reference_v<T>, "");
static_assert(std::is_default_constructible_v<T>, "");
static_assert(std::is_move_assignable_v<T> || std::is_copy_assignable_v<T>, "");
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<T>)
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<class Fn> bool consume_one(Fn &&fn)
{
T el;
{
std::unique_lock lk{m_mutex};
if (!m_queue.empty()) {
if constexpr (std::is_move_assignable_v<T>)
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<class...TArgs> void push(TArgs&&...el)
{
std::lock_guard lk{m_mutex};
m_queue.emplace(std::forward<TArgs>(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

View File

@ -0,0 +1,119 @@
#ifndef PRUSALSICER_WORKER_HPP
#define PRUSALSICER_WORKER_HPP
#include <memory>
#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> 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<class Fn> constexpr bool IsProcessFn = std::is_invocable_v<Fn, Job::Ctl&>;
template<class Fn> constexpr bool IsFinishFn = std::is_invocable_v<Fn, bool, std::exception_ptr&>;
// Helper function to use the worker with arbitrary functors.
template<class ProcessFn, class FinishFn,
class = std::enable_if_t<IsProcessFn<ProcessFn>>,
class = std::enable_if_t<IsFinishFn<FinishFn>> >
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<LambdaJob>(std::move(fn), std::move(finishfn));
return w.push(std::move(j));
}
template<class ProcessFn, class = std::enable_if_t<IsProcessFn<ProcessFn>>>
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<Job> 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<class...Args> bool replace_job(Worker &w, Args&& ...args)
{
w.cancel_all();
return queue_job(w, std::forward<Args>(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

View File

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

View File

@ -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<NotificationProgressIndicator> m_pri;
void before_start() override { m->background_process.stop(); }
public:
Jobs(priv *_m) :
m(_m),
m_pri{std::make_shared<NotificationProgressIndicator>(m->notification_manager.get())}
{
m_arrange_id = add_job(std::make_unique<ArrangeJob>(m_pri, m->q));
m_fill_bed_id = add_job(std::make_unique<FillBedJob>(m_pri, m->q));
m_rotoptimize_id = add_job(std::make_unique<RotoptimizeJob>(m_pri, m->q));
m_sla_import_id = add_job(std::make_unique<SLAImportJob>(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<BoostThreadWorker> 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<NotificationManager>(q))
, m_ui_jobs(this)
, m_worker{q, std::make_unique<NotificationProgressIndicator>(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<SLAImportJob>(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<FillBedJob>());
}
}
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<ArrangeJob>());
}
}
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); }

View File

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

View File

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

View File

@ -0,0 +1,148 @@
#include "catch2/catch.hpp"
#include <chrono>
#include <thread>
#include "slic3r/GUI/Jobs/BoostThreadWorker.hpp"
#include "slic3r/GUI/Jobs/ProgressIndicator.hpp"
//#include <boost/thread/thread.hpp>
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<Progress>()};
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<Progress>(), "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<Progress>();
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<Progress>();
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<Progress>();
BoostThreadWorker worker{pri};
std::array<bool, 4> 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<Progress>();
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());
}