Fixed conflicts after merge with master

This commit is contained in:
enricoturri1966 2021-05-10 10:25:57 +02:00
commit f786d9c96e
174 changed files with 9811 additions and 5525 deletions

View file

@ -194,7 +194,7 @@ bool Bed3D::set_shape(const Pointfs& shape, const std::string& custom_texture, c
const BoundingBox& bed_bbox = poly.contour.bounding_box();
calc_gridlines(poly, bed_bbox);
m_polygon = offset_ex(poly.contour, (float)bed_bbox.radius() * 1.7f, jtRound, scale_(0.5))[0].contour;
m_polygon = offset(poly.contour, (float)bed_bbox.radius() * 1.7f, jtRound, scale_(0.5))[0];
reset();
m_texture.reset();

View file

@ -145,7 +145,7 @@ void BackgroundSlicingProcess::process_fff()
// Passing the timestamp
evt.SetInt((int)(m_fff_print->step_state_with_timestamp(PrintStep::psSlicingFinished).timestamp));
wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, evt.Clone());
m_fff_print->export_gcode(m_temp_output_path, m_gcode_result, m_thumbnail_cb);
m_fff_print->export_gcode(m_temp_output_path, m_gcode_result, [this](const ThumbnailsParams& params) { return this->render_thumbnails(params); });
if (this->set_step_started(bspsGCodeFinalize)) {
if (! m_export_path.empty()) {
wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id));
@ -221,21 +221,14 @@ void BackgroundSlicingProcess::process_sla()
const std::string export_path = m_sla_print->print_statistics().finalize_output_path(m_export_path);
ThumbnailsList thumbnails = this->render_thumbnails(
ThumbnailsParams{current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, true, true, true});
Zipper zipper(export_path);
m_sla_archive.export_print(zipper, *m_sla_print);
if (m_thumbnail_cb != nullptr)
{
ThumbnailsList thumbnails;
m_thumbnail_cb(thumbnails, current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, true, true, true);
// m_thumbnail_cb(thumbnails, current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, false, true, true); // renders also supports and pad
for (const ThumbnailData& data : thumbnails)
{
if (data.is_valid())
write_thumbnail(zipper, data);
}
}
m_sla_archive.export_print(zipper, *m_sla_print); // true, false, true, true); // renders also supports and pad
for (const ThumbnailData& data : thumbnails)
if (data.is_valid())
write_thumbnail(zipper, data);
zipper.finalize();
m_print->set_status(100, (boost::format(_utf8(L("Masked SLA file exported to %1%"))) % export_path).str());
@ -362,16 +355,19 @@ bool BackgroundSlicingProcess::start()
return true;
}
// To be called on the UI thread.
bool BackgroundSlicingProcess::stop()
{
// m_print->state_mutex() shall NOT be held. Unfortunately there is no interface to test for it.
std::unique_lock<std::mutex> lck(m_mutex);
if (m_state == STATE_INITIAL) {
// this->m_export_path.clear();
// m_export_path.clear();
return false;
}
// assert(this->running());
if (m_state == STATE_STARTED || m_state == STATE_RUNNING) {
// Cancel any task planned by the background thread on UI thread.
cancel_ui_task(m_ui_task);
m_print->cancel();
// Wait until the background processing stops by being canceled.
m_condition.wait(lck, [this](){ return m_state == STATE_CANCELED; });
@ -383,7 +379,7 @@ bool BackgroundSlicingProcess::stop()
m_state = STATE_IDLE;
m_print->set_cancel_callback([](){});
}
// this->m_export_path.clear();
// m_export_path.clear();
return true;
}
@ -396,7 +392,7 @@ bool BackgroundSlicingProcess::reset()
return stopped;
}
// To be called by Print::apply() through the Print::m_cancel_callback to stop the background
// To be called by Print::apply() on the UI thread through the Print::m_cancel_callback to stop the background
// processing before changing any data of running or finalized milestones.
// This function shall not trigger any UI update through the wxWidgets event.
void BackgroundSlicingProcess::stop_internal()
@ -408,6 +404,8 @@ void BackgroundSlicingProcess::stop_internal()
std::unique_lock<std::mutex> lck(m_mutex);
assert(m_state == STATE_STARTED || m_state == STATE_RUNNING || m_state == STATE_FINISHED || m_state == STATE_CANCELED);
if (m_state == STATE_STARTED || m_state == STATE_RUNNING) {
// Cancel any task planned by the background thread on UI thread.
cancel_ui_task(m_ui_task);
// At this point of time the worker thread may be blocking on m_print->state_mutex().
// Set the print state to canceled before unlocking the state_mutex(), so when the worker thread wakes up,
// it throws the CanceledException().
@ -424,6 +422,60 @@ void BackgroundSlicingProcess::stop_internal()
m_print->set_cancel_callback([](){});
}
// Execute task from background thread on the UI thread. Returns true if processed, false if cancelled.
bool BackgroundSlicingProcess::execute_ui_task(std::function<void()> task)
{
bool running = false;
if (m_mutex.try_lock()) {
// Cancellation is either not in process, or already canceled and waiting for us to finish.
// There must be no UI task planned.
assert(! m_ui_task);
if (! m_print->canceled()) {
running = true;
m_ui_task = std::make_shared<UITask>();
}
m_mutex.unlock();
} else {
// Cancellation is in process.
}
bool result = false;
if (running) {
std::shared_ptr<UITask> ctx = m_ui_task;
GUI::wxGetApp().mainframe->m_plater->CallAfter([task, ctx]() {
// Running on the UI thread, thus ctx->state does not need to be guarded with mutex against ::cancel_ui_task().
assert(ctx->state == UITask::Planned || ctx->state == UITask::Canceled);
if (ctx->state == UITask::Planned) {
task();
std::unique_lock<std::mutex> lck(ctx->mutex);
ctx->state = UITask::Finished;
}
// Wake up the worker thread from the UI thread.
ctx->condition.notify_all();
});
{
std::unique_lock<std::mutex> lock(ctx->mutex);
ctx->condition.wait(lock, [&ctx]{ return ctx->state == UITask::Finished || ctx->state == UITask::Canceled; });
}
result = ctx->state == UITask::Finished;
m_ui_task.reset();
}
return result;
}
// To be called on the UI thread from ::stop() and ::stop_internal().
void BackgroundSlicingProcess::cancel_ui_task(std::shared_ptr<UITask> task)
{
if (task) {
std::unique_lock<std::mutex> lck(task->mutex);
task->state = UITask::Canceled;
lck.unlock();
task->condition.notify_all();
}
}
bool BackgroundSlicingProcess::empty() const
{
assert(m_print != nullptr);
@ -444,7 +496,7 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model &model, const Dyn
assert(config.opt_enum<PrinterTechnology>("printer_technology") == m_print->technology());
Print::ApplyStatus invalidated = m_print->apply(model, config);
if ((invalidated & PrintBase::APPLY_STATUS_INVALIDATED) != 0 && m_print->technology() == ptFFF &&
!this->m_fff_print->is_step_done(psGCodeExport)) {
!m_fff_print->is_step_done(psGCodeExport)) {
// Some FFF status was invalidated, and the G-code was not exported yet.
// Let the G-code preview UI know that the final G-code preview is not valid.
// In addition, this early memory deallocation reduces memory footprint.
@ -546,19 +598,14 @@ void BackgroundSlicingProcess::prepare_upload()
} else {
m_upload_job.upload_data.upload_path = m_sla_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string());
ThumbnailsList thumbnails = this->render_thumbnails(
ThumbnailsParams{current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, true, true, true});
// true, false, true, true); // renders also supports and pad
Zipper zipper{source_path.string()};
m_sla_archive.export_print(zipper, *m_sla_print, m_upload_job.upload_data.upload_path.string());
if (m_thumbnail_cb != nullptr)
{
ThumbnailsList thumbnails;
m_thumbnail_cb(thumbnails, current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, true, true, true);
// m_thumbnail_cb(thumbnails, current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, false, true, true); // renders also supports and pad
for (const ThumbnailData& data : thumbnails)
{
if (data.is_valid())
write_thumbnail(zipper, data);
}
}
for (const ThumbnailData& data : thumbnails)
if (data.is_valid())
write_thumbnail(zipper, data);
zipper.finalize();
}
@ -569,4 +616,13 @@ void BackgroundSlicingProcess::prepare_upload()
GUI::wxGetApp().printhost_job_queue().enqueue(std::move(m_upload_job));
}
// Executed by the background thread, to start a task on the UI thread.
ThumbnailsList BackgroundSlicingProcess::render_thumbnails(const ThumbnailsParams &params)
{
ThumbnailsList thumbnails;
if (m_thumbnail_cb)
this->execute_ui_task([this, &params, &thumbnails](){ thumbnails = m_thumbnail_cb(params); });
return thumbnails;
}
}; // namespace Slic3r

View file

@ -212,6 +212,23 @@ private:
std::condition_variable m_condition;
State m_state = STATE_INITIAL;
// For executing tasks from the background thread on UI thread synchronously (waiting for result) using wxWidgets CallAfter().
// When the background proces is canceled, the UITask has to be invalidated as well, so that it will not be
// executed on the UI thread referencing invalid data.
struct UITask {
enum State {
Planned,
Finished,
Canceled,
};
State state = Planned;
std::mutex mutex;
std::condition_variable condition;
};
// Only one UI task may be planned by the background thread to be executed on the UI thread, as the background
// thread is blocking until the UI thread calculation finishes.
std::shared_ptr<UITask> m_ui_task;
PrintState<BackgroundSlicingProcessStep, bspsCount> m_step_state;
mutable tbb::mutex m_step_state_mutex;
bool set_step_started(BackgroundSlicingProcessStep step);
@ -222,6 +239,12 @@ private:
// If the background processing stop was requested, throw CanceledException.
void throw_if_canceled() const { if (m_print->canceled()) throw CanceledException(); }
void prepare_upload();
// To be executed at the background thread.
ThumbnailsList render_thumbnails(const ThumbnailsParams &params);
// Execute task from background thread on the UI thread synchronously. Returns true if processed, false if cancelled before executing the task.
bool execute_ui_task(std::function<void()> task);
// To be called from inside m_mutex to cancel a planned UI task.
static void cancel_ui_task(std::shared_ptr<BackgroundSlicingProcess::UITask> task);
// wxWidgets command ID to be sent to the plater to inform that the slicing is finished, and the G-code export will continue.
int m_event_slicing_completed_id = 0;

View file

@ -45,7 +45,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
// layer_height shouldn't be equal to zero
if (config->opt_float("layer_height") < EPSILON)
{
const wxString msg_text = _(L("Zero layer height is not valid.\n\nThe layer height will be reset to 0.01."));
const wxString msg_text = _(L("Layer height is not valid.\n\nThe layer height will be reset to 0.01."));
wxMessageDialog dialog(nullptr, msg_text, _(L("Layer height")), wxICON_WARNING | wxOK);
DynamicPrintConfig new_conf = *config;
is_msg_dlg_already_exist = true;
@ -55,9 +55,9 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
is_msg_dlg_already_exist = false;
}
if (fabs(config->option<ConfigOptionFloatOrPercent>("first_layer_height")->value - 0) < EPSILON)
if (config->option<ConfigOptionFloatOrPercent>("first_layer_height")->value < EPSILON)
{
const wxString msg_text = _(L("Zero first layer height is not valid.\n\nThe first layer height will be reset to 0.01."));
const wxString msg_text = _(L("First layer height is not valid.\n\nThe first layer height will be reset to 0.01."));
wxMessageDialog dialog(nullptr, msg_text, _(L("First layer height")), wxICON_WARNING | wxOK);
DynamicPrintConfig new_conf = *config;
is_msg_dlg_already_exist = true;

View file

@ -26,14 +26,18 @@
#include <wx/wupdlock.h>
#include <wx/debug.h>
#include "libslic3r/Platform.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Config.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "GUI_Utils.hpp"
#include "GUI_ObjectManipulation.hpp"
#include "Field.hpp"
#include "DesktopIntegrationDialog.hpp"
#include "slic3r/Config/Snapshot.hpp"
#include "slic3r/Utils/PresetUpdater.hpp"
#include "format.hpp"
#if defined(__linux__) && defined(__WXGTK3__)
#define wxLinux_gtk3 true
@ -450,7 +454,6 @@ void ConfigWizardPage::append_spacer(int space)
content->AddSpacer(space);
}
// Wizard pages
PageWelcome::PageWelcome(ConfigWizard *parent)
@ -469,9 +472,21 @@ PageWelcome::PageWelcome(ConfigWizard *parent)
, cbox_reset(append(
new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)"))
))
, cbox_integrate(append(
new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (Sets this binary to be searchable by the system)."))
))
{
welcome_text->Hide();
cbox_reset->Hide();
#ifdef __linux__
if (!DesktopIntegrationDialog::is_integrated())
cbox_integrate->Show(true);
else
cbox_integrate->Hide();
#else
cbox_integrate->Hide();
#endif
}
void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason)
@ -479,6 +494,14 @@ void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason)
const bool data_empty = run_reason == ConfigWizard::RR_DATA_EMPTY;
welcome_text->Show(data_empty);
cbox_reset->Show(!data_empty);
#ifdef __linux__
if (!DesktopIntegrationDialog::is_integrated())
cbox_integrate->Show(true);
else
cbox_integrate->Hide();
#else
cbox_integrate->Hide();
#endif
}
@ -1361,20 +1384,39 @@ void PageBedShape::apply_custom_config(DynamicPrintConfig &config)
config.set_key_value("bed_custom_model", new ConfigOptionString(custom_model));
}
static void focus_event(wxFocusEvent& e, wxTextCtrl* ctrl, double def_value)
{
e.Skip();
wxString str = ctrl->GetValue();
// Replace the first occurence of comma in decimal number.
bool was_replace = str.Replace(",", ".", false) > 0;
double val = 0.0;
if (!str.ToCDouble(&val)) {
if (val == 0.0)
val = def_value;
ctrl->SetValue(double_to_string(val));
show_error(nullptr, _L("Invalid numeric input."));
ctrl->SetFocus();
}
else if (was_replace)
ctrl->SetValue(double_to_string(val));
}
PageDiameters::PageDiameters(ConfigWizard *parent)
: ConfigWizardPage(parent, _L("Filament and Nozzle Diameters"), _L("Print Diameters"), 1)
, spin_nozzle(new wxSpinCtrlDouble(this, wxID_ANY))
, spin_filam(new wxSpinCtrlDouble(this, wxID_ANY))
, diam_nozzle(new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord)))
, diam_filam (new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord)))
{
spin_nozzle->SetDigits(2);
spin_nozzle->SetIncrement(0.1);
auto *default_nozzle = print_config_def.get("nozzle_diameter")->get_default_value<ConfigOptionFloats>();
spin_nozzle->SetValue(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5);
wxString value = double_to_string(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5);
diam_nozzle->SetValue(value);
spin_filam->SetDigits(2);
spin_filam->SetIncrement(0.25);
auto *default_filam = print_config_def.get("filament_diameter")->get_default_value<ConfigOptionFloats>();
spin_filam->SetValue(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0);
value = double_to_string(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0);
diam_filam->SetValue(value);
diam_nozzle->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_nozzle, 0.5); }, diam_nozzle->GetId());
diam_filam ->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_filam , 3.0); }, diam_filam->GetId());
append_text(_L("Enter the diameter of your printer's hot end nozzle."));
@ -1383,7 +1425,7 @@ PageDiameters::PageDiameters(ConfigWizard *parent)
auto *unit_nozzle = new wxStaticText(this, wxID_ANY, _L("mm"));
sizer_nozzle->AddGrowableCol(0, 1);
sizer_nozzle->Add(text_nozzle, 0, wxALIGN_CENTRE_VERTICAL);
sizer_nozzle->Add(spin_nozzle);
sizer_nozzle->Add(diam_nozzle);
sizer_nozzle->Add(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL);
append(sizer_nozzle);
@ -1397,16 +1439,21 @@ PageDiameters::PageDiameters(ConfigWizard *parent)
auto *unit_filam = new wxStaticText(this, wxID_ANY, _L("mm"));
sizer_filam->AddGrowableCol(0, 1);
sizer_filam->Add(text_filam, 0, wxALIGN_CENTRE_VERTICAL);
sizer_filam->Add(spin_filam);
sizer_filam->Add(diam_filam);
sizer_filam->Add(unit_filam, 0, wxALIGN_CENTRE_VERTICAL);
append(sizer_filam);
}
void PageDiameters::apply_custom_config(DynamicPrintConfig &config)
{
auto *opt_nozzle = new ConfigOptionFloats(1, spin_nozzle->GetValue());
double val = 0.0;
diam_nozzle->GetValue().ToCDouble(&val);
auto *opt_nozzle = new ConfigOptionFloats(1, val);
config.set_key_value("nozzle_diameter", opt_nozzle);
auto *opt_filam = new ConfigOptionFloats(1, spin_filam->GetValue());
val = 0.0;
diam_filam->GetValue().ToCDouble(&val);
auto * opt_filam = new ConfigOptionFloats(1, val);
config.set_key_value("filament_diameter", opt_filam);
auto set_extrusion_width = [&config, opt_nozzle](const char *key, double dmr) {
@ -2373,6 +2420,12 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese
}
}
#ifdef __linux__
// Desktop integration on Linux
if (page_welcome->integrate_desktop())
DesktopIntegrationDialog::perform_desktop_integration();
#endif
// Decide whether to create snapshot based on run_reason and the reset profile checkbox
bool snapshot = true;
Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE;
@ -2490,7 +2543,6 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese
// Update the selections from the compatibilty.
preset_bundle->export_selections(*app_config);
}
void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add)
{
const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla;

View file

@ -45,7 +45,6 @@ public:
bool run(RunReason reason, StartPage start_page = SP_WELCOME);
static const wxString& name(const bool from_menu = false);
protected:
void on_dpi_changed(const wxRect &suggested_rect) override ;

View file

@ -227,10 +227,12 @@ struct PageWelcome: ConfigWizardPage
{
wxStaticText *welcome_text;
wxCheckBox *cbox_reset;
wxCheckBox *cbox_integrate;
PageWelcome(ConfigWizard *parent);
bool reset_user_profile() const { return cbox_reset != nullptr ? cbox_reset->GetValue() : false; }
bool integrate_desktop() const { return cbox_integrate != nullptr ? cbox_integrate->GetValue() : false; }
virtual void set_run_reason(ConfigWizard::RunReason run_reason) override;
};
@ -449,8 +451,8 @@ struct PageBedShape: ConfigWizardPage
struct PageDiameters: ConfigWizardPage
{
wxSpinCtrlDouble *spin_nozzle;
wxSpinCtrlDouble *spin_filam;
wxTextCtrl *diam_nozzle;
wxTextCtrl *diam_filam;
PageDiameters(ConfigWizard *parent);
virtual void apply_custom_config(DynamicPrintConfig &config);
@ -615,7 +617,9 @@ struct ConfigWizard::priv
void apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater);
// #ys_FIXME_alise
void update_presets_in_config(const std::string& section, const std::string& alias_key, bool add);
#ifdef __linux__
void perform_desktop_integration() const;
#endif
bool check_fff_selected(); // Used to decide whether to display Filaments page
bool check_sla_selected(); // Used to decide whether to display SLA Materials page

View file

@ -0,0 +1,408 @@
#ifdef __linux__
#include "DesktopIntegrationDialog.hpp"
#include "GUI_App.hpp"
#include "format.hpp"
#include "I18N.hpp"
#include "NotificationManager.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Platform.hpp"
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <wx/filename.h>
#include <wx/stattext.h>
namespace Slic3r {
namespace GUI {
namespace integrate_desktop_internal{
// Disects path strings stored in system variable divided by ':' and adds into vector
static void resolve_path_from_var(const std::string& var, std::vector<std::string>& paths)
{
wxString wxdirs;
if (! wxGetEnv(boost::nowide::widen(var), &wxdirs) || wxdirs.empty() )
return;
std::string dirs = boost::nowide::narrow(wxdirs);
for (size_t i = dirs.find(':'); i != std::string::npos; i = dirs.find(':'))
{
paths.push_back(dirs.substr(0, i));
if (dirs.size() > i+1)
dirs = dirs.substr(i+1);
}
paths.push_back(dirs);
}
// Return true if directory in path p+dir_name exists
static bool contains_path_dir(const std::string& p, const std::string& dir_name)
{
if (p.empty() || dir_name.empty())
return false;
boost::filesystem::path path(p + (p[p.size()-1] == '/' ? "" : "/") + dir_name);
if (boost::filesystem::exists(path) && boost::filesystem::is_directory(path)) {
//BOOST_LOG_TRIVIAL(debug) << path.string() << " " << std::oct << boost::filesystem::status(path).permissions();
return true; //boost::filesystem::status(path).permissions() & boost::filesystem::owner_write;
} else
BOOST_LOG_TRIVIAL(debug) << path.string() << " doesnt exists";
return false;
}
// Creates directory in path if not exists yet
static void create_dir(const boost::filesystem::path& path)
{
if (boost::filesystem::exists(path))
return;
BOOST_LOG_TRIVIAL(debug)<< "creating " << path.string();
boost::system::error_code ec;
boost::filesystem::create_directory(path, ec);
if (ec)
BOOST_LOG_TRIVIAL(error)<< "create directory failed: " << ec.message();
}
// Starts at basic_path (excluded) and creates all directories in dir_path
static void create_path(const std::string& basic_path, const std::string& dir_path)
{
if (basic_path.empty() || dir_path.empty())
return;
boost::filesystem::path path(basic_path);
std::string dirs = dir_path;
for (size_t i = dirs.find('/'); i != std::string::npos; i = dirs.find('/'))
{
std::string dir = dirs.substr(0, i);
path = boost::filesystem::path(path.string() +"/"+ dir);
create_dir(path);
dirs = dirs.substr(i+1);
}
path = boost::filesystem::path(path.string() +"/"+ dirs);
create_dir(path);
}
// Calls our internal copy_file function to copy file at icon_path to dest_path
static bool copy_icon(const std::string& icon_path, const std::string& dest_path)
{
BOOST_LOG_TRIVIAL(debug) <<"icon from "<< icon_path;
BOOST_LOG_TRIVIAL(debug) <<"icon to "<< dest_path;
std::string error_message;
auto cfr = copy_file(icon_path, dest_path, error_message, false);
if (cfr) {
BOOST_LOG_TRIVIAL(debug) << "Copy icon fail(" << cfr << "): " << error_message;
return false;
}
BOOST_LOG_TRIVIAL(debug) << "Copy icon success.";
return true;
}
// Creates new file filled with data.
static bool create_desktop_file(const std::string& path, const std::string& data)
{
BOOST_LOG_TRIVIAL(debug) <<".desktop to "<< path;
std::ofstream output(path);
output << data;
struct stat buffer;
if (stat(path.c_str(), &buffer) == 0)
{
BOOST_LOG_TRIVIAL(debug) << "Desktop file created.";
return true;
}
BOOST_LOG_TRIVIAL(debug) << "Desktop file NOT created.";
return false;
}
} // namespace integratec_desktop_internal
// methods that actually do / undo desktop integration. Static to be accesible from anywhere.
bool DesktopIntegrationDialog::is_integrated()
{
const char *appimage_env = std::getenv("APPIMAGE");
if (!appimage_env)
return false;
const AppConfig *app_config = wxGetApp().app_config;
std::string path(app_config->get("desktop_integration_app_path"));
BOOST_LOG_TRIVIAL(debug) << "Desktop integration desktop file path: " << path;
if (path.empty())
return false;
// confirmation that PrusaSlicer.desktop exists
struct stat buffer;
return (stat (path.c_str(), &buffer) == 0);
}
bool DesktopIntegrationDialog::integration_possible()
{
const char *appimage_env = std::getenv("APPIMAGE");
if (!appimage_env)
return false;
return true;
}
void DesktopIntegrationDialog::perform_desktop_integration()
{
BOOST_LOG_TRIVIAL(debug) << "performing desktop integration";
// Path to appimage
const char *appimage_env = std::getenv("APPIMAGE");
std::string appimage_path;
if (appimage_env) {
try {
appimage_path = boost::filesystem::canonical(boost::filesystem::path(appimage_env)).string();
} catch (std::exception &) {
}
} else {
// not appimage - not performing
BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - not Appimage executable.";
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationFail);
return;
}
// Find directories icons and applications
// $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored.
// If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used.
// $XDG_DATA_DIRS defines the preference-ordered set of base directories to search for data files in addition to the $XDG_DATA_HOME base directory.
// The directories in $XDG_DATA_DIRS should be seperated with a colon ':'.
// If $XDG_DATA_DIRS is either not set or empty, a value equal to /usr/local/share/:/usr/share/ should be used.
std::vector<std::string>target_candidates;
integrate_desktop_internal::resolve_path_from_var("XDG_DATA_HOME", target_candidates);
integrate_desktop_internal::resolve_path_from_var("XDG_DATA_DIRS", target_candidates);
AppConfig *app_config = wxGetApp().app_config;
// suffix string to create different desktop file for alpha, beta.
std::string version_suffix;
std::string name_suffix;
std::string version(SLIC3R_VERSION);
if (version.find("alpha") != std::string::npos)
{
version_suffix = "-alpha";
name_suffix = " - alpha";
}else if (version.find("beta") != std::string::npos)
{
version_suffix = "-beta";
name_suffix = " - beta";
}
// theme path to icon destination
std::string icon_theme_path;
std::string icon_theme_dirs;
if (platform_flavor() == PlatformFlavor::LinuxOnChromium) {
icon_theme_path = "hicolor/96x96/apps/";
icon_theme_dirs = "/hicolor/96x96/apps";
}
std::string target_dir_icons;
std::string target_dir_desktop;
// slicer icon
// iterate thru target_candidates to find icons folder
for (size_t i = 0; i < target_candidates.size(); ++i) {
// Copy icon PrusaSlicer.png from resources_dir()/icons to target_dir_icons/icons/
if (integrate_desktop_internal::contains_path_dir(target_candidates[i], "icons")) {
target_dir_icons = target_candidates[i];
std::string icon_path = GUI::format("%1%/icons/PrusaSlicer.png",resources_dir());
std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer%3%.png", target_dir_icons, icon_theme_path, version_suffix);
if (integrate_desktop_internal::copy_icon(icon_path, dest_path))
break; // success
else
target_dir_icons.clear(); // copying failed
// if all failed - try creating default home folder
if (i == target_candidates.size() - 1) {
// create $HOME/.local/share
integrate_desktop_internal::create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/icons" + icon_theme_dirs);
// copy icon
target_dir_icons = GUI::format("%1%/.local/share",wxFileName::GetHomeDir());
std::string icon_path = GUI::format("%1%/icons/PrusaSlicer.png",resources_dir());
std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer%3%.png", target_dir_icons, icon_theme_path, version_suffix);
if (!integrate_desktop_internal::contains_path_dir(target_dir_icons, "icons")
|| !integrate_desktop_internal::copy_icon(icon_path, dest_path)) {
// every attempt failed - icon wont be present
target_dir_icons.clear();
}
}
}
}
if(target_dir_icons.empty()) {
BOOST_LOG_TRIVIAL(error) << "Copying PrusaSlicer icon to icons directory failed.";
} else
// save path to icon
app_config->set("desktop_integration_icon_slicer_path", GUI::format("%1%/icons/%2%PrusaSlicer%3%.png", target_dir_icons, icon_theme_path, version_suffix));
// desktop file
// iterate thru target_candidates to find applications folder
for (size_t i = 0; i < target_candidates.size(); ++i)
{
if (integrate_desktop_internal::contains_path_dir(target_candidates[i], "applications")) {
target_dir_desktop = target_candidates[i];
// Write slicer desktop file
std::string desktop_file = GUI::format(
"[Desktop Entry]\n"
"Name=PrusaSlicer%1%\n"
"GenericName=3D Printing Software\n"
"Icon=PrusaSlicer%2%\n"
"Exec=%3% %%F\n"
"Terminal=false\n"
"Type=Application\n"
"MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;application/x-amf;\n"
"Categories=Graphics;3DGraphics;Engineering;\n"
"Keywords=3D;Printing;Slicer;slice;3D;printer;convert;gcode;stl;obj;amf;SLA\n"
"StartupNotify=false\n"
"StartupWMClass=prusa-slicer", name_suffix, version_suffix, appimage_path);
std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix);
if (integrate_desktop_internal::create_desktop_file(path, desktop_file)){
BOOST_LOG_TRIVIAL(debug) << "PrusaSlicer.desktop file installation success.";
break;
} else {
// write failed - try another path
BOOST_LOG_TRIVIAL(error) << "PrusaSlicer.desktop file installation failed.";
target_dir_desktop.clear();
}
// if all failed - try creating default home folder
if (i == target_candidates.size() - 1) {
// create $HOME/.local/share
integrate_desktop_internal::create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/applications");
// create desktop file
target_dir_desktop = GUI::format("%1%/.local/share",wxFileName::GetHomeDir());
std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix);
if (integrate_desktop_internal::contains_path_dir(target_dir_desktop, "applications")) {
if (!integrate_desktop_internal::create_desktop_file(path, desktop_file)) {
// Desktop file not written - end desktop integration
BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not create desktop file";
return;
}
} else {
// Desktop file not written - end desktop integration
BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not find applications directory";
return;
}
}
}
}
if(target_dir_desktop.empty()) {
// Desktop file not written - end desktop integration
BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not find applications directory";
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationFail);
return;
}
// save path to desktop file
app_config->set("desktop_integration_app_path", GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix));
// Repeat for Gcode viewer - use same paths as for slicer files
// Icon
if (!target_dir_icons.empty())
{
std::string icon_path = GUI::format("%1%/icons/PrusaSlicer-gcodeviewer_192px.png",resources_dir());
std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer-gcodeviewer%3%.png", target_dir_icons, icon_theme_path, version_suffix);
if (integrate_desktop_internal::copy_icon(icon_path, dest_path))
// save path to icon
app_config->set("desktop_integration_icon_viewer_path", dest_path);
else
BOOST_LOG_TRIVIAL(error) << "Copying Gcode Viewer icon to icons directory failed.";
}
// Desktop file
std::string desktop_file = GUI::format(
"[Desktop Entry]\n"
"Name=Prusa Gcode Viewer%1%\n"
"GenericName=3D Printing Software\n"
"Icon=PrusaSlicer-gcodeviewer%2%\n"
"Exec=%3% --gcodeviwer %%F\n"
"Terminal=false\n"
"Type=Application\n"
"MimeType=text/x.gcode;\n"
"Categories=Graphics;3DGraphics;\n"
"Keywords=3D;Printing;Slicer;\n"
"StartupNotify=false", name_suffix, version_suffix, appimage_path);
std::string desktop_path = GUI::format("%1%/applications/PrusaSlicerGcodeViewer%2%.desktop", target_dir_desktop, version_suffix);
if (integrate_desktop_internal::create_desktop_file(desktop_path, desktop_file))
// save path to desktop file
app_config->set("desktop_integration_app_viewer_path", desktop_path);
else {
BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could create gcode viewer desktop file";
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationFail);
}
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationSuccess);
}
void DesktopIntegrationDialog::undo_desktop_intgration()
{
const char *appimage_env = std::getenv("APPIMAGE");
if (!appimage_env) {
BOOST_LOG_TRIVIAL(error) << "Undo desktop integration failed - not Appimage executable.";
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::UndoDesktopIntegrationFail);
return;
}
const AppConfig *app_config = wxGetApp().app_config;
// slicer .desktop
std::string path = std::string(app_config->get("desktop_integration_app_path"));
if (!path.empty()) {
BOOST_LOG_TRIVIAL(debug) << "removing " << path;
std::remove(path.c_str());
}
// slicer icon
path = std::string(app_config->get("desktop_integration_icon_slicer_path"));
if (!path.empty()) {
BOOST_LOG_TRIVIAL(debug) << "removing " << path;
std::remove(path.c_str());
}
// gcode viwer .desktop
path = std::string(app_config->get("desktop_integration_app_viewer_path"));
if (!path.empty()) {
BOOST_LOG_TRIVIAL(debug) << "removing " << path;
std::remove(path.c_str());
}
// gcode viewer icon
path = std::string(app_config->get("desktop_integration_icon_viewer_path"));
if (!path.empty()) {
BOOST_LOG_TRIVIAL(debug) << "removing " << path;
std::remove(path.c_str());
}
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::UndoDesktopIntegrationSuccess);
}
DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent)
: wxDialog(parent, wxID_ANY, _(L("Desktop Integration")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
{
bool can_undo = DesktopIntegrationDialog::is_integrated();
wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL);
wxString text = _L("Desktop Integration sets this binary to be searchable by the system.\n\nPress \"Perform\" to proceed.");
if (can_undo)
text += "\nPress \"Undo\" to remove previous integration.";
vbox->Add(
new wxStaticText( this, wxID_ANY, text),
// , wxDefaultPosition, wxSize(100,50), wxTE_MULTILINE),
1, // make vertically stretchable
wxEXPAND | // make horizontally stretchable
wxALL, // and make border all around
10 ); // set border width to 10
wxBoxSizer *btn_szr = new wxBoxSizer(wxHORIZONTAL);
wxButton *btn_perform = new wxButton(this, wxID_ANY, _L("Perform"));
btn_szr->Add(btn_perform, 0, wxALL, 10);
btn_perform->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { DesktopIntegrationDialog::perform_desktop_integration(); EndModal(wxID_ANY); });
if (can_undo){
wxButton *btn_undo = new wxButton(this, wxID_ANY, _L("Undo"));
btn_szr->Add(btn_undo, 0, wxALL, 10);
btn_undo->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { DesktopIntegrationDialog::undo_desktop_intgration(); EndModal(wxID_ANY); });
}
wxButton *btn_cancel = new wxButton(this, wxID_ANY, _L("Cancel"));
btn_szr->Add(btn_cancel, 0, wxALL, 10);
btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_ANY); });
vbox->Add(btn_szr, 0, wxALIGN_CENTER);
SetSizerAndFit(vbox);
}
DesktopIntegrationDialog::~DesktopIntegrationDialog()
{
}
} // namespace GUI
} // namespace Slic3r
#endif // __linux__

View file

@ -0,0 +1,39 @@
#ifdef __linux__
#ifndef slic3r_DesktopIntegrationDialog_hpp_
#define slic3r_DesktopIntegrationDialog_hpp_
#include <wx/dialog.h>
namespace Slic3r {
namespace GUI {
class DesktopIntegrationDialog : public wxDialog
{
public:
DesktopIntegrationDialog(wxWindow *parent);
DesktopIntegrationDialog(DesktopIntegrationDialog &&) = delete;
DesktopIntegrationDialog(const DesktopIntegrationDialog &) = delete;
DesktopIntegrationDialog &operator=(DesktopIntegrationDialog &&) = delete;
DesktopIntegrationDialog &operator=(const DesktopIntegrationDialog &) = delete;
~DesktopIntegrationDialog();
// methods that actually do / undo desktop integration. Static to be accesible from anywhere.
// returns true if path to PrusaSlicer.desktop is stored in App Config and existence of desktop file.
// Does not check if desktop file leads to this binary or existence of icons and viewer desktop file.
static bool is_integrated();
// true if appimage
static bool integration_possible();
// Creates Desktop files and icons for both PrusaSlicer and GcodeViewer.
// Stores paths into App Config.
// Rewrites if files already existed.
static void perform_desktop_integration();
// Deletes Desktop files and icons for both PrusaSlicer and GcodeViewer at paths stored in App Config.
static void undo_desktop_intgration();
private:
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_DesktopIntegrationDialog_hpp_
#endif // __linux__

View file

@ -17,6 +17,7 @@
#include "GLCanvas3D.hpp"
#include "GLToolbar.hpp"
#include "GUI_Preview.hpp"
#include "GUI_ObjectManipulation.hpp"
#include <imgui/imgui_internal.h>
#include <GL/glew.h>
@ -143,6 +144,9 @@ bool GCodeViewer::Path::matches(const GCodeProcessor::MoveVertex& move) const
case EMoveType::Custom_GCode:
case EMoveType::Retract:
case EMoveType::Unretract:
#if ENABLE_SEAMS_VISUALIZATION
case EMoveType::Seam:
#endif // ENABLE_SEAMS_VISUALIZATION
case EMoveType::Extrude: {
// use rounding to reduce the number of generated paths
#if ENABLE_SPLITTED_VERTEX_BUFFER
@ -540,6 +544,9 @@ const std::vector<GCodeViewer::Color> GCodeViewer::Extrusion_Role_Colors {{
const std::vector<GCodeViewer::Color> GCodeViewer::Options_Colors {{
{ 0.803f, 0.135f, 0.839f }, // Retractions
{ 0.287f, 0.679f, 0.810f }, // Unretractions
#if ENABLE_SEAMS_VISUALIZATION
{ 0.900f, 0.900f, 0.900f }, // Seams
#endif // ENABLE_SEAMS_VISUALIZATION
{ 0.758f, 0.744f, 0.389f }, // ToolChanges
{ 0.856f, 0.582f, 0.546f }, // ColorChanges
{ 0.322f, 0.942f, 0.512f }, // PausePrints
@ -582,11 +589,20 @@ GCodeViewer::GCodeViewer()
case EMoveType::Pause_Print:
case EMoveType::Custom_GCode:
case EMoveType::Retract:
#if ENABLE_SEAMS_VISUALIZATION
case EMoveType::Unretract:
case EMoveType::Seam: {
buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Point;
buffer.vertices.format = VBuffer::EFormat::Position;
break;
}
#else
case EMoveType::Unretract: {
buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Point;
buffer.vertices.format = VBuffer::EFormat::Position;
break;
}
#endif // ENABLE_SEAMS_VISUALIZATION
case EMoveType::Wipe:
case EMoveType::Extrude: {
buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Triangle;
@ -672,13 +688,13 @@ void GCodeViewer::load(const GCodeProcessor::Result& gcode_result, const Print&
wxGetApp().plater()->set_bed_shape(bed_shape, texture, model, gcode_result.bed_shape.empty());
}
m_time_statistics = gcode_result.time_statistics;
m_print_statistics = gcode_result.print_statistics;
if (m_time_estimate_mode != PrintEstimatedTimeStatistics::ETimeMode::Normal) {
float time = m_time_statistics.modes[static_cast<size_t>(m_time_estimate_mode)].time;
if (m_time_estimate_mode != PrintEstimatedStatistics::ETimeMode::Normal) {
float time = m_print_statistics.modes[static_cast<size_t>(m_time_estimate_mode)].time;
if (time == 0.0f ||
short_time(get_time_dhms(time)) == short_time(get_time_dhms(m_time_statistics.modes[static_cast<size_t>(PrintEstimatedTimeStatistics::ETimeMode::Normal)].time)))
m_time_estimate_mode = PrintEstimatedTimeStatistics::ETimeMode::Normal;
short_time(get_time_dhms(time)) == short_time(get_time_dhms(m_print_statistics.modes[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time)))
m_time_estimate_mode = PrintEstimatedStatistics::ETimeMode::Normal;
}
}
@ -773,7 +789,7 @@ void GCodeViewer::reset()
m_layers.reset();
m_layers_z_range = { 0, 0 };
m_roles = std::vector<ExtrusionRole>();
m_time_statistics.reset();
m_print_statistics.reset();
#if ENABLE_GCODE_WINDOW
m_sequential_view.gcode_window.reset();
#endif // ENABLE_GCODE_WINDOW
@ -796,10 +812,18 @@ void GCodeViewer::render() const
case EMoveType::Pause_Print:
case EMoveType::Custom_GCode:
case EMoveType::Retract:
#if ENABLE_SEAMS_VISUALIZATION
case EMoveType::Unretract:
case EMoveType::Seam: {
buffer.shader = wxGetApp().is_glsl_version_greater_or_equal_to(1, 20) ? "options_120" : "options_110";
break;
}
#else
case EMoveType::Unretract: {
buffer.shader = wxGetApp().is_glsl_version_greater_or_equal_to(1, 20) ? "options_120" : "options_110";
break;
}
#endif // ENABLE_SEAMS_VISUALIZATION
case EMoveType::Wipe:
case EMoveType::Extrude: {
buffer.shader = "gouraud_light";
@ -938,6 +962,9 @@ unsigned int GCodeViewer::get_options_visibility_flags() const
flags = set_flag(flags, static_cast<unsigned int>(Preview::OptionType::Wipe), is_toolpath_move_type_visible(EMoveType::Wipe));
flags = set_flag(flags, static_cast<unsigned int>(Preview::OptionType::Retractions), is_toolpath_move_type_visible(EMoveType::Retract));
flags = set_flag(flags, static_cast<unsigned int>(Preview::OptionType::Unretractions), is_toolpath_move_type_visible(EMoveType::Unretract));
#if ENABLE_SEAMS_VISUALIZATION
flags = set_flag(flags, static_cast<unsigned int>(Preview::OptionType::Seams), is_toolpath_move_type_visible(EMoveType::Seam));
#endif // ENABLE_SEAMS_VISUALIZATION
flags = set_flag(flags, static_cast<unsigned int>(Preview::OptionType::ToolChanges), is_toolpath_move_type_visible(EMoveType::Tool_change));
flags = set_flag(flags, static_cast<unsigned int>(Preview::OptionType::ColorChanges), is_toolpath_move_type_visible(EMoveType::Color_change));
flags = set_flag(flags, static_cast<unsigned int>(Preview::OptionType::PausePrints), is_toolpath_move_type_visible(EMoveType::Pause_Print));
@ -958,6 +985,9 @@ void GCodeViewer::set_options_visibility_from_flags(unsigned int flags)
set_toolpath_move_type_visible(EMoveType::Wipe, is_flag_set(static_cast<unsigned int>(Preview::OptionType::Wipe)));
set_toolpath_move_type_visible(EMoveType::Retract, is_flag_set(static_cast<unsigned int>(Preview::OptionType::Retractions)));
set_toolpath_move_type_visible(EMoveType::Unretract, is_flag_set(static_cast<unsigned int>(Preview::OptionType::Unretractions)));
#if ENABLE_SEAMS_VISUALIZATION
set_toolpath_move_type_visible(EMoveType::Seam, is_flag_set(static_cast<unsigned int>(Preview::OptionType::Seams)));
#endif // ENABLE_SEAMS_VISUALIZATION
set_toolpath_move_type_visible(EMoveType::Tool_change, is_flag_set(static_cast<unsigned int>(Preview::OptionType::ToolChanges)));
set_toolpath_move_type_visible(EMoveType::Color_change, is_flag_set(static_cast<unsigned int>(Preview::OptionType::ColorChanges)));
set_toolpath_move_type_visible(EMoveType::Pause_Print, is_flag_set(static_cast<unsigned int>(Preview::OptionType::PausePrints)));
@ -1595,13 +1625,15 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result)
#if ENABLE_REDUCED_TOOLPATHS_SEGMENT_CAPS
const std::array<IBufferType, 8> first_seg_v_offsets = convert_vertices_offset(vbuffer_size, { 0, 1, 2, 3, 4, 5, 6, 7 });
const std::array<IBufferType, 8> non_first_seg_v_offsets = convert_vertices_offset(vbuffer_size, { -4, 0, -2, 1, 2, 3, 4, 5 });
#endif // ENABLE_REDUCED_TOOLPATHS_SEGMENT_CAPS
bool is_first_segment = (last_path.vertices_count() == 1);
if (is_first_segment || vbuffer_size == 0) {
#else
if (last_path.vertices_count() == 1 || vbuffer_size == 0) {
#endif // ENABLE_REDUCED_TOOLPATHS_SEGMENT_CAPS
// 1st segment or restart into a new vertex buffer
// ===============================================
#if ENABLE_REDUCED_TOOLPATHS_SEGMENT_CAPS
if (last_path.vertices_count() == 1)
if (is_first_segment)
// starting cap triangles
append_starting_cap_triangles(indices, first_seg_v_offsets);
#endif // ENABLE_REDUCED_TOOLPATHS_SEGMENT_CAPS
@ -1679,7 +1711,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result)
#if ENABLE_REDUCED_TOOLPATHS_SEGMENT_CAPS
if (next != nullptr && (curr.type != next->type || !last_path.matches(*next)))
// ending cap triangles
append_ending_cap_triangles(indices, non_first_seg_v_offsets);
append_ending_cap_triangles(indices, is_first_segment ? first_seg_v_offsets : non_first_seg_v_offsets);
#endif // ENABLE_REDUCED_TOOLPATHS_SEGMENT_CAPS
last_path.sub_paths.back().last = { ibuffer_id, indices.size() - 1, move_id, curr.position };
@ -1714,7 +1746,11 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result)
// for the gcode viewer we need to take in account all moves to correctly size the printbed
m_paths_bounding_box.merge(move.position.cast<double>());
else {
#if ENABLE_START_GCODE_VISUALIZATION
if (move.type == EMoveType::Extrude && move.extrusion_role != erCustom && move.width != 0.0f && move.height != 0.0f)
#else
if (move.type == EMoveType::Extrude && move.width != 0.0f && move.height != 0.0f)
#endif // ENABLE_START_GCODE_VISUALIZATION
m_paths_bounding_box.merge(move.position.cast<double>());
}
}
@ -3163,6 +3199,9 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool
case EMoveType::Custom_GCode: { color = Options_Colors[static_cast<unsigned int>(EOptionsColors::CustomGCodes)]; break; }
case EMoveType::Retract: { color = Options_Colors[static_cast<unsigned int>(EOptionsColors::Retractions)]; break; }
case EMoveType::Unretract: { color = Options_Colors[static_cast<unsigned int>(EOptionsColors::Unretractions)]; break; }
#if ENABLE_SEAMS_VISUALIZATION
case EMoveType::Seam: { color = Options_Colors[static_cast<unsigned int>(EOptionsColors::Seams)]; break; }
#endif // ENABLE_SEAMS_VISUALIZATION
case EMoveType::Extrude: {
if (!top_layer_only ||
m_sequential_view.current.last == global_endpoints.last ||
@ -4013,14 +4052,25 @@ void GCodeViewer::render_legend() const
if (!m_legend_enabled)
return;
#if ENABLE_SCROLLABLE_LEGEND
const Size cnv_size = wxGetApp().plater()->get_current_canvas3D()->get_canvas_size();
#endif // ENABLE_SCROLLABLE_LEGEND
ImGuiWrapper& imgui = *wxGetApp().imgui();
imgui.set_next_window_pos(0.0f, 0.0f, ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::SetNextWindowBgAlpha(0.6f);
#if ENABLE_SCROLLABLE_LEGEND
const float max_height = 0.75f * static_cast<float>(cnv_size.get_height());
const float child_height = 0.3333f * max_height;
ImGui::SetNextWindowSizeConstraints({ 0.0f, 0.0f }, { -1.0f, max_height });
imgui.begin(std::string("Legend"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove);
#else
imgui.begin(std::string("Legend"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
#endif // ENABLE_SCROLLABLE_LEGEND
enum class EItemType : unsigned char
{
@ -4030,95 +4080,126 @@ void GCodeViewer::render_legend() const
Line
};
const PrintEstimatedTimeStatistics::Mode& time_mode = m_time_statistics.modes[static_cast<size_t>(m_time_estimate_mode)];
const PrintEstimatedStatistics::Mode& time_mode = m_print_statistics.modes[static_cast<size_t>(m_time_estimate_mode)];
#if ENABLE_SCROLLABLE_LEGEND
bool show_estimated_time = time_mode.time > 0.0f && (m_view_type == EViewType::FeatureType ||
(m_view_type == EViewType::ColorPrint && !time_mode.custom_gcode_times.empty()));
#endif // ENABLE_SCROLLABLE_LEGEND
float icon_size = ImGui::GetTextLineHeight();
float percent_bar_size = 2.0f * ImGui::GetTextLineHeight();
const float icon_size = ImGui::GetTextLineHeight();
const float percent_bar_size = 2.0f * ImGui::GetTextLineHeight();
auto append_item = [this, draw_list, icon_size, percent_bar_size, &imgui](EItemType type, const Color& color, const std::string& label,
bool visible = true, const std::string& time = "", float percent = 0.0f, float max_percent = 0.0f, const std::array<float, 2>& offsets = { 0.0f, 0.0f },
bool imperial_units = wxGetApp().app_config->get("use_inches") == "1";
#if ENABLE_SCROLLABLE_LEGEND
auto append_item = [this, icon_size, percent_bar_size, &imgui, imperial_units](EItemType type, const Color& color, const std::string& label,
#else
auto append_item = [this, draw_list, icon_size, percent_bar_size, &imgui, imperial_units](EItemType type, const Color& color, const std::string& label,
#endif // ENABLE_SCROLLABLE_LEGEND
bool visible = true, const std::string& time = "", float percent = 0.0f, float max_percent = 0.0f, const std::array<float, 4>& offsets = { 0.0f, 0.0f, 0.0f, 0.0f },
double used_filament_m = 0.0, double used_filament_g = 0.0,
std::function<void()> callback = nullptr) {
if (!visible)
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3333f);
ImVec2 pos = ImGui::GetCursorScreenPos();
switch (type) {
default:
case EItemType::Rect: {
draw_list->AddRectFilled({ pos.x + 1.0f, pos.y + 1.0f }, { pos.x + icon_size - 1.0f, pos.y + icon_size - 1.0f },
ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }));
break;
}
case EItemType::Circle: {
ImVec2 center(0.5f * (pos.x + pos.x + icon_size), 0.5f * (pos.y + pos.y + icon_size));
if (m_buffers[buffer_id(EMoveType::Retract)].shader == "options_120") {
draw_list->AddCircleFilled(center, 0.5f * icon_size,
ImGui::GetColorU32({ 0.5f * color[0], 0.5f * color[1], 0.5f * color[2], 1.0f }), 16);
float radius = 0.5f * icon_size;
draw_list->AddCircleFilled(center, radius, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 16);
radius = 0.5f * icon_size * 0.01f * 33.0f;
draw_list->AddCircleFilled(center, radius, ImGui::GetColorU32({ 0.5f * color[0], 0.5f * color[1], 0.5f * color[2], 1.0f }), 16);
}
else
draw_list->AddCircleFilled(center, 0.5f * icon_size, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 16);
if (!visible)
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3333f);
break;
}
case EItemType::Hexagon: {
ImVec2 center(0.5f * (pos.x + pos.x + icon_size), 0.5f * (pos.y + pos.y + icon_size));
draw_list->AddNgonFilled(center, 0.5f * icon_size, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 6);
break;
}
case EItemType::Line: {
draw_list->AddLine({ pos.x + 1, pos.y + icon_size - 1 }, { pos.x + icon_size - 1, pos.y + 1 }, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 3.0f);
break;
}
}
// draw text
ImGui::Dummy({ icon_size, icon_size });
ImGui::SameLine();
if (callback != nullptr) {
if (ImGui::MenuItem(label.c_str()))
callback();
else {
// show tooltip
if (ImGui::IsItemHovered()) {
if (!visible)
ImGui::PopStyleVar();
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND);
ImGui::BeginTooltip();
imgui.text(visible ? _u8L("Click to hide") : _u8L("Click to show"));
ImGui::EndTooltip();
ImGui::PopStyleColor();
if (!visible)
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3333f);
// to avoid the tooltip to change size when moving the mouse
wxGetApp().plater()->get_current_canvas3D()->set_as_dirty();
wxGetApp().plater()->get_current_canvas3D()->request_extra_frame();
}
}
if (!time.empty()) {
ImGui::SameLine(offsets[0]);
imgui.text(time);
ImGui::SameLine(offsets[1]);
pos = ImGui::GetCursorScreenPos();
float width = std::max(1.0f, percent_bar_size * percent / max_percent);
draw_list->AddRectFilled({ pos.x, pos.y + 2.0f }, { pos.x + width, pos.y + icon_size - 2.0f },
ImGui::GetColorU32(ImGuiWrapper::COL_ORANGE_LIGHT));
ImGui::Dummy({ percent_bar_size, icon_size });
ImGui::SameLine();
char buf[64];
::sprintf(buf, "%.1f%%", 100.0f * percent);
ImGui::TextUnformatted((percent > 0.0f) ? buf : "");
}
#if ENABLE_SCROLLABLE_LEGEND
ImDrawList* draw_list = ImGui::GetWindowDrawList();
#endif // ENABLE_SCROLLABLE_LEGEND
ImVec2 pos = ImGui::GetCursorScreenPos();
switch (type) {
default:
case EItemType::Rect: {
draw_list->AddRectFilled({ pos.x + 1.0f, pos.y + 1.0f }, { pos.x + icon_size - 1.0f, pos.y + icon_size - 1.0f },
ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }));
break;
}
case EItemType::Circle: {
ImVec2 center(0.5f * (pos.x + pos.x + icon_size), 0.5f * (pos.y + pos.y + icon_size));
if (m_buffers[buffer_id(EMoveType::Retract)].shader == "options_120") {
draw_list->AddCircleFilled(center, 0.5f * icon_size,
ImGui::GetColorU32({ 0.5f * color[0], 0.5f * color[1], 0.5f * color[2], 1.0f }), 16);
float radius = 0.5f * icon_size;
draw_list->AddCircleFilled(center, radius, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 16);
radius = 0.5f * icon_size * 0.01f * 33.0f;
draw_list->AddCircleFilled(center, radius, ImGui::GetColorU32({ 0.5f * color[0], 0.5f * color[1], 0.5f * color[2], 1.0f }), 16);
}
else
imgui.text(label);
draw_list->AddCircleFilled(center, 0.5f * icon_size, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 16);
if (!visible)
ImGui::PopStyleVar();
break;
}
case EItemType::Hexagon: {
ImVec2 center(0.5f * (pos.x + pos.x + icon_size), 0.5f * (pos.y + pos.y + icon_size));
draw_list->AddNgonFilled(center, 0.5f * icon_size, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 6);
break;
}
case EItemType::Line: {
draw_list->AddLine({ pos.x + 1, pos.y + icon_size - 1 }, { pos.x + icon_size - 1, pos.y + 1 }, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 3.0f);
break;
}
}
// draw text
ImGui::Dummy({ icon_size, icon_size });
ImGui::SameLine();
if (callback != nullptr) {
if (ImGui::MenuItem(label.c_str()))
callback();
else {
// show tooltip
if (ImGui::IsItemHovered()) {
if (!visible)
ImGui::PopStyleVar();
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND);
ImGui::BeginTooltip();
imgui.text(visible ? _u8L("Click to hide") : _u8L("Click to show"));
ImGui::EndTooltip();
ImGui::PopStyleColor();
if (!visible)
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3333f);
// to avoid the tooltip to change size when moving the mouse
wxGetApp().plater()->get_current_canvas3D()->set_as_dirty();
wxGetApp().plater()->get_current_canvas3D()->request_extra_frame();
}
}
if (!time.empty()) {
ImGui::SameLine(offsets[0]);
imgui.text(time);
ImGui::SameLine(offsets[1]);
pos = ImGui::GetCursorScreenPos();
const float width = std::max(1.0f, percent_bar_size * percent / max_percent);
draw_list->AddRectFilled({ pos.x, pos.y + 2.0f }, { pos.x + width, pos.y + icon_size - 2.0f },
ImGui::GetColorU32(ImGuiWrapper::COL_ORANGE_LIGHT));
ImGui::Dummy({ percent_bar_size, icon_size });
ImGui::SameLine();
char buf[64];
::sprintf(buf, "%.1f%%", 100.0f * percent);
ImGui::TextUnformatted((percent > 0.0f) ? buf : "");
ImGui::SameLine(offsets[2]);
::sprintf(buf, imperial_units ? "%.2f in" : "%.2f m", used_filament_m);
imgui.text(buf);
ImGui::SameLine(offsets[3]);
::sprintf(buf, "%.2f g", used_filament_g);
imgui.text(buf);
}
}
else {
imgui.text(label);
if (used_filament_m > 0.0) {
char buf[64];
ImGui::SameLine(offsets[0]);
::sprintf(buf, imperial_units ? "%.2f in" : "%.2f m", used_filament_m);
imgui.text(buf);
ImGui::SameLine(offsets[1]);
::sprintf(buf, "%.2f g", used_filament_g);
imgui.text(buf);
}
}
if (!visible)
ImGui::PopStyleVar();
};
auto append_range = [append_item](const Extrusions::Range& range, unsigned int decimals) {
@ -4136,19 +4217,20 @@ void GCodeViewer::render_legend() const
append_range_item(0, range.min, decimals);
}
else {
float step_size = range.step_size();
const float step_size = range.step_size();
for (int i = static_cast<int>(Range_Colors.size()) - 1; i >= 0; --i) {
append_range_item(i, range.min + static_cast<float>(i) * step_size, decimals);
}
}
};
auto append_headers = [&imgui](const std::array<std::string, 3>& texts, const std::array<float, 2>& offsets) {
imgui.text(texts[0]);
ImGui::SameLine(offsets[0]);
imgui.text(texts[1]);
ImGui::SameLine(offsets[1]);
imgui.text(texts[2]);
auto append_headers = [&imgui](const std::array<std::string, 5>& texts, const std::array<float, 4>& offsets) {
size_t i = 0;
for (; i < offsets.size(); i++) {
imgui.text(texts[i]);
ImGui::SameLine(offsets[i]);
}
imgui.text(texts[i]);
ImGui::Separator();
};
@ -4161,11 +4243,12 @@ void GCodeViewer::render_legend() const
};
auto calculate_offsets = [max_width](const std::vector<std::string>& labels, const std::vector<std::string>& times,
const std::array<std::string, 2>& titles, float extra_size = 0.0f) {
const std::array<std::string, 4>& titles, float extra_size = 0.0f) {
const ImGuiStyle& style = ImGui::GetStyle();
std::array<float, 2> ret = { 0.0f, 0.0f };
std::array<float, 4> ret = { 0.0f, 0.0f, 0.0f, 0.0f };
ret[0] = max_width(labels, titles[0], extra_size) + 3.0f * style.ItemSpacing.x;
ret[1] = ret[0] + max_width(times, titles[1]) + style.ItemSpacing.x;
for (size_t i = 1; i < titles.size(); i++)
ret[i] = ret[i-1] + max_width(times, titles[i]) + style.ItemSpacing.x;
return ret;
};
@ -4185,8 +4268,8 @@ void GCodeViewer::render_legend() const
if (lower_b == zs.end())
continue;
double current_z = *lower_b;
double previous_z = (lower_b == zs.begin()) ? 0.0 : *(--lower_b);
const double current_z = *lower_b;
const double previous_z = (lower_b == zs.begin()) ? 0.0 : *(--lower_b);
// to avoid duplicate values, check adding values
if (ret.empty() || !(ret.back().second.first == previous_z && ret.back().second.second == current_z))
@ -4216,16 +4299,27 @@ void GCodeViewer::render_legend() const
return _u8L("from") + " " + std::string(buf1) + " " + _u8L("to") + " " + std::string(buf2) + " " + _u8L("mm");
};
auto role_time_and_percent = [ time_mode](ExtrusionRole role) {
auto role_time_and_percent = [time_mode](ExtrusionRole role) {
auto it = std::find_if(time_mode.roles_times.begin(), time_mode.roles_times.end(), [role](const std::pair<ExtrusionRole, float>& item) { return role == item.first; });
return (it != time_mode.roles_times.end()) ? std::make_pair(it->second, it->second / time_mode.time) : std::make_pair(0.0f, 0.0f);
};
auto used_filament_per_role = [this, imperial_units](ExtrusionRole role) {
auto it = m_print_statistics.used_filaments_per_role.find(role);
if (it == m_print_statistics.used_filaments_per_role.end())
return std::make_pair(0.0, 0.0);
double koef = imperial_units ? 1000.0 / ObjectManipulation::in_to_mm : 1.0;
return std::make_pair(it->second.first * koef, it->second.second);
};
// data used to properly align items in columns when showing time
std::array<float, 2> offsets = { 0.0f, 0.0f };
std::array<float, 4> offsets = { 0.0f, 0.0f, 0.0f, 0.0f };
std::vector<std::string> labels;
std::vector<std::string> times;
std::vector<float> percents;
std::vector<double> used_filaments_m;
std::vector<double> used_filaments_g;
float max_percent = 0.0f;
if (m_view_type == EViewType::FeatureType) {
@ -4238,10 +4332,73 @@ void GCodeViewer::render_legend() const
times.push_back((time > 0.0f) ? short_time(get_time_dhms(time)) : "");
percents.push_back(percent);
max_percent = std::max(max_percent, percent);
auto [used_filament_m, used_filament_g] = used_filament_per_role(role);
used_filaments_m.push_back(used_filament_m);
used_filaments_g.push_back(used_filament_g);
}
}
offsets = calculate_offsets(labels, times, { _u8L("Feature type"), _u8L("Time") }, icon_size);
std::string longest_percentage_string;
for (double item : percents) {
char buffer[64];
::sprintf(buffer, "%.2f %%", item);
if (::strlen(buffer) > longest_percentage_string.length())
longest_percentage_string = buffer;
}
longest_percentage_string += " ";
if (_u8L("Percentage").length() > longest_percentage_string.length())
longest_percentage_string = _u8L("Percentage");
std::string longest_used_filament_string;
for (double item : used_filaments_m) {
char buffer[64];
::sprintf(buffer, imperial_units ? "%.2f in" : "%.2f m", item);
if (::strlen(buffer) > longest_used_filament_string.length())
longest_used_filament_string = buffer;
}
offsets = calculate_offsets(labels, times, { _u8L("Feature type"), _u8L("Time"), longest_percentage_string, longest_used_filament_string }, icon_size);
}
// get used filament (meters and grams) from used volume in respect to the active extruder
auto get_used_filament_from_volume = [imperial_units](double volume, int extruder_id) {
const std::vector<std::string>& filament_presets = wxGetApp().preset_bundle->filament_presets;
const PresetCollection& filaments = wxGetApp().preset_bundle->filaments;
double koef = imperial_units ? 1.0/ObjectManipulation::in_to_mm : 0.001;
std::pair<double, double> ret = { 0.0, 0.0 };
if (const Preset* filament_preset = filaments.find_preset(filament_presets[extruder_id], false)) {
double filament_radius = 0.5 * filament_preset->config.opt_float("filament_diameter", 0);
double s = PI * sqr(filament_radius);
ret.first = volume / s * koef;
double filament_density = filament_preset->config.opt_float("filament_density", 0);
ret.second = volume * filament_density * 0.001;
}
return ret;
};
if (m_view_type == EViewType::Tool) {
// calculate used filaments data
for (size_t extruder_id : m_extruder_ids) {
if (m_print_statistics.volumes_per_extruder.find(extruder_id) == m_print_statistics.volumes_per_extruder.end())
continue;
double volume = m_print_statistics.volumes_per_extruder.at(extruder_id);
auto [used_filament_m, used_filament_g] = get_used_filament_from_volume(volume, extruder_id);
used_filaments_m.push_back(used_filament_m);
used_filaments_g.push_back(used_filament_g);
}
std::string longest_used_filament_string;
for (double item : used_filaments_m) {
char buffer[64];
::sprintf(buffer, imperial_units ? "%.2f in" : "%.2f m", item);
if (::strlen(buffer) > longest_used_filament_string.length())
longest_used_filament_string = buffer;
}
offsets = calculate_offsets(labels, times, { "Extruder NNN", longest_used_filament_string }, icon_size);
}
// extrusion paths section -> title
@ -4249,7 +4406,7 @@ void GCodeViewer::render_legend() const
{
case EViewType::FeatureType:
{
append_headers({ _u8L("Feature type"), _u8L("Time"), _u8L("Percentage") }, offsets);
append_headers({ _u8L("Feature type"), _u8L("Time"), _u8L("Percentage"), _u8L("Used filament") }, offsets);
break;
}
case EViewType::Height: { imgui.title(_u8L("Height (mm)")); break; }
@ -4258,7 +4415,11 @@ void GCodeViewer::render_legend() const
case EViewType::FanSpeed: { imgui.title(_u8L("Fan Speed (%)")); break; }
case EViewType::Temperature: { imgui.title(_u8L("Temperature (°C)")); break; }
case EViewType::VolumetricRate: { imgui.title(_u8L("Volumetric flow rate (mm³/s)")); break; }
case EViewType::Tool: { imgui.title(_u8L("Tool")); break; }
case EViewType::Tool:
{
append_headers({ _u8L("Tool"), _u8L("Used filament") }, offsets);
break;
}
case EViewType::ColorPrint: { imgui.title(_u8L("Color Print")); break; }
default: { break; }
}
@ -4272,9 +4433,9 @@ void GCodeViewer::render_legend() const
ExtrusionRole role = m_roles[i];
if (role >= erCount)
continue;
bool visible = is_visible(role);
const bool visible = is_visible(role);
append_item(EItemType::Rect, Extrusion_Role_Colors[static_cast<unsigned int>(role)], labels[i],
visible, times[i], percents[i], max_percent, offsets, [this, role, visible]() {
visible, times[i], percents[i], max_percent, offsets, used_filaments_m[i], used_filaments_g[i], [this, role, visible]() {
Extrusions* extrusions = const_cast<Extrusions*>(&m_extrusions);
extrusions->role_visibility_flags = visible ? extrusions->role_visibility_flags & ~(1 << role) : extrusions->role_visibility_flags | (1 << role);
// update buffers' render paths
@ -4296,16 +4457,33 @@ void GCodeViewer::render_legend() const
case EViewType::Tool:
{
// shows only extruders actually used
for (unsigned char i : m_extruder_ids) {
append_item(EItemType::Rect, m_tool_colors[i], _u8L("Extruder") + " " + std::to_string(i + 1));
size_t i = 0;
for (unsigned char extruder_id : m_extruder_ids) {
append_item(EItemType::Rect, m_tool_colors[extruder_id], _u8L("Extruder") + " " + std::to_string(extruder_id + 1),
true, "", 0.0f, 0.0f, offsets, used_filaments_m[i], used_filaments_g[i]);
i++;
}
break;
}
case EViewType::ColorPrint:
{
#if ENABLE_SCROLLABLE_LEGEND
const std::vector<CustomGCode::Item>& custom_gcode_per_print_z = wxGetApp().plater()->model().custom_gcode_per_print_z.gcodes;
size_t total_items = 1;
for (unsigned char i : m_extruder_ids) {
total_items += color_print_ranges(i, custom_gcode_per_print_z).size();
}
const bool need_scrollable = static_cast<float>(total_items) * (icon_size + ImGui::GetStyle().ItemSpacing.y) > child_height;
// add scrollable region, if needed
if (need_scrollable)
ImGui::BeginChild("color_prints", { -1.0f, child_height }, false);
#else
const std::vector<CustomGCode::Item>& custom_gcode_per_print_z = wxGetApp().plater()->model().custom_gcode_per_print_z.gcodes;
#endif // ENABLE_SCROLLABLE_LEGEND
if (m_extruders_count == 1) { // single extruder use case
std::vector<std::pair<Color, std::pair<double, double>>> cp_values = color_print_ranges(0, custom_gcode_per_print_z);
const std::vector<std::pair<Color, std::pair<double, double>>> cp_values = color_print_ranges(0, custom_gcode_per_print_z);
const int items_cnt = static_cast<int>(cp_values.size());
if (items_cnt == 0) { // There are no color changes, but there are some pause print or custom Gcode
append_item(EItemType::Rect, m_tool_colors.front(), _u8L("Default color"));
@ -4328,7 +4506,7 @@ void GCodeViewer::render_legend() const
else { // multi extruder use case
// shows only extruders actually used
for (unsigned char i : m_extruder_ids) {
std::vector<std::pair<Color, std::pair<double, double>>> cp_values = color_print_ranges(i, custom_gcode_per_print_z);
const std::vector<std::pair<Color, std::pair<double, double>>> cp_values = color_print_ranges(i, custom_gcode_per_print_z);
const int items_cnt = static_cast<int>(cp_values.size());
if (items_cnt == 0) { // There are no color changes, but there are some pause print or custom Gcode
append_item(EItemType::Rect, m_tool_colors[i], _u8L("Extruder") + " " + std::to_string(i + 1) + " " + _u8L("default color"));
@ -4354,6 +4532,10 @@ void GCodeViewer::render_legend() const
}
}
}
#if ENABLE_SCROLLABLE_LEGEND
if (need_scrollable)
ImGui::EndChild();
#endif // ENABLE_SCROLLABLE_LEGEND
break;
}
@ -4379,10 +4561,11 @@ void GCodeViewer::render_legend() const
Color color1;
Color color2;
Times times;
std::pair<double, double> used_filament {0.0f, 0.0f};
};
using PartialTimes = std::vector<PartialTime>;
auto generate_partial_times = [this](const TimesList& times) {
auto generate_partial_times = [this, get_used_filament_from_volume](const TimesList& times, const std::vector<double>& used_filaments) {
PartialTimes items;
std::vector<CustomGCode::Item> custom_gcode_per_print_z = wxGetApp().plater()->model().custom_gcode_per_print_z.gcodes;
@ -4392,6 +4575,7 @@ void GCodeViewer::render_legend() const
last_color[i] = m_tool_colors[i];
}
int last_extruder_id = 1;
int color_change_idx = 0;
for (const auto& time_rec : times) {
switch (time_rec.first)
{
@ -4407,14 +4591,14 @@ void GCodeViewer::render_legend() const
case CustomGCode::ColorChange: {
auto it = std::find_if(custom_gcode_per_print_z.begin(), custom_gcode_per_print_z.end(), [time_rec](const CustomGCode::Item& item) { return item.type == time_rec.first; });
if (it != custom_gcode_per_print_z.end()) {
items.push_back({ PartialTime::EType::Print, it->extruder, last_color[it->extruder - 1], Color(), time_rec.second });
items.push_back({ PartialTime::EType::Print, it->extruder, last_color[it->extruder - 1], Color(), time_rec.second, get_used_filament_from_volume(used_filaments[color_change_idx++], it->extruder-1) });
items.push_back({ PartialTime::EType::ColorChange, it->extruder, last_color[it->extruder - 1], decode_color(it->color), time_rec.second });
last_color[it->extruder - 1] = decode_color(it->color);
last_extruder_id = it->extruder;
custom_gcode_per_print_z.erase(it);
}
else
items.push_back({ PartialTime::EType::Print, last_extruder_id, last_color[last_extruder_id - 1], Color(), time_rec.second });
items.push_back({ PartialTime::EType::Print, last_extruder_id, last_color[last_extruder_id - 1], Color(), time_rec.second, get_used_filament_from_volume(used_filaments[color_change_idx++], last_extruder_id -1) });
break;
}
@ -4425,7 +4609,7 @@ void GCodeViewer::render_legend() const
return items;
};
auto append_color_change = [&imgui](const Color& color1, const Color& color2, const std::array<float, 2>& offsets, const Times& times) {
auto append_color_change = [&imgui](const Color& color1, const Color& color2, const std::array<float, 4>& offsets, const Times& times) {
imgui.text(_u8L("Color change"));
ImGui::SameLine();
@ -4444,7 +4628,7 @@ void GCodeViewer::render_legend() const
imgui.text(short_time(get_time_dhms(times.second - times.first)));
};
auto append_print = [&imgui](const Color& color, const std::array<float, 2>& offsets, const Times& times) {
auto append_print = [&imgui, imperial_units](const Color& color, const std::array<float, 4>& offsets, const Times& times, std::pair<double, double> used_filament) {
imgui.text(_u8L("Print"));
ImGui::SameLine();
@ -4460,9 +4644,19 @@ void GCodeViewer::render_legend() const
imgui.text(short_time(get_time_dhms(times.second)));
ImGui::SameLine(offsets[1]);
imgui.text(short_time(get_time_dhms(times.first)));
if (used_filament.first > 0.0f) {
char buffer[64];
ImGui::SameLine(offsets[2]);
::sprintf(buffer, imperial_units ? "%.2f in" : "%.2f m", used_filament.first);
imgui.text(buffer);
ImGui::SameLine(offsets[3]);
::sprintf(buffer, "%.2f g", used_filament.second);
imgui.text(buffer);
}
};
PartialTimes partial_times = generate_partial_times(time_mode.custom_gcode_times);
PartialTimes partial_times = generate_partial_times(time_mode.custom_gcode_times, m_print_statistics.volumes_per_color_change);
if (!partial_times.empty()) {
labels.clear();
times.clear();
@ -4476,15 +4670,34 @@ void GCodeViewer::render_legend() const
}
times.push_back(short_time(get_time_dhms(item.times.second)));
}
offsets = calculate_offsets(labels, times, { _u8L("Event"), _u8L("Remaining time") }, 2.0f * icon_size);
std::string longest_used_filament_string;
for (const PartialTime& item : partial_times) {
if (item.used_filament.first > 0.0f) {
char buffer[64];
::sprintf(buffer, imperial_units ? "%.2f in" : "%.2f m", item.used_filament.first);
if (::strlen(buffer) > longest_used_filament_string.length())
longest_used_filament_string = buffer;
}
}
offsets = calculate_offsets(labels, times, { _u8L("Event"), _u8L("Remaining time"), _u8L("Duration"), longest_used_filament_string }, 2.0f * icon_size);
ImGui::Spacing();
append_headers({ _u8L("Event"), _u8L("Remaining time"), _u8L("Duration") }, offsets);
append_headers({ _u8L("Event"), _u8L("Remaining time"), _u8L("Duration"), _u8L("Used filament") }, offsets);
#if ENABLE_SCROLLABLE_LEGEND
const bool need_scrollable = static_cast<float>(partial_times.size()) * (icon_size + ImGui::GetStyle().ItemSpacing.y) > child_height;
if (need_scrollable)
// add scrollable region
ImGui::BeginChild("events", { -1.0f, child_height }, false);
#endif // ENABLE_SCROLLABLE_LEGEND
for (const PartialTime& item : partial_times) {
switch (item.type)
{
case PartialTime::EType::Print: {
append_print(item.color1, offsets, item.times);
append_print(item.color1, offsets, item.times, item.used_filament);
break;
}
case PartialTime::EType::Pause: {
@ -4499,6 +4712,11 @@ void GCodeViewer::render_legend() const
}
}
}
#if ENABLE_SCROLLABLE_LEGEND
if (need_scrollable)
ImGui::EndChild();
#endif // ENABLE_SCROLLABLE_LEGEND
}
}
@ -4557,7 +4775,12 @@ void GCodeViewer::render_legend() const
available(EMoveType::Pause_Print) ||
available(EMoveType::Retract) ||
available(EMoveType::Tool_change) ||
#if ENABLE_SEAMS_VISUALIZATION
available(EMoveType::Unretract) ||
available(EMoveType::Seam);
#else
available(EMoveType::Unretract);
#endif // ENABLE_SEAMS_VISUALIZATION
};
auto add_option = [this, append_item](EMoveType move_type, EOptionsColors color, const std::string& text) {
@ -4575,6 +4798,9 @@ void GCodeViewer::render_legend() const
// items
add_option(EMoveType::Retract, EOptionsColors::Retractions, _u8L("Retractions"));
add_option(EMoveType::Unretract, EOptionsColors::Unretractions, _u8L("Deretractions"));
#if ENABLE_SEAMS_VISUALIZATION
add_option(EMoveType::Seam, EOptionsColors::Seams, _u8L("Seams"));
#endif // ENABLE_SEAMS_VISUALIZATION
add_option(EMoveType::Tool_change, EOptionsColors::ToolChanges, _u8L("Tool changes"));
add_option(EMoveType::Color_change, EOptionsColors::ColorChanges, _u8L("Color changes"));
add_option(EMoveType::Pause_Print, EOptionsColors::PausePrints, _u8L("Print pauses"));
@ -4634,10 +4860,14 @@ void GCodeViewer::render_legend() const
}
// total estimated printing time section
#if ENABLE_SCROLLABLE_LEGEND
if (show_estimated_time) {
#else
if (time_mode.time > 0.0f && (m_view_type == EViewType::FeatureType ||
(m_view_type == EViewType::ColorPrint && !time_mode.custom_gcode_times.empty()))) {
ImGui::Spacing();
#endif // ENABLE_SCROLLABLE_LEGEND
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Separator, { 1.0f, 1.0f, 1.0f, 1.0f });
ImGui::Separator();
@ -4647,12 +4877,12 @@ void GCodeViewer::render_legend() const
ImGui::AlignTextToFramePadding();
switch (m_time_estimate_mode)
{
case PrintEstimatedTimeStatistics::ETimeMode::Normal:
case PrintEstimatedStatistics::ETimeMode::Normal:
{
imgui.text(_u8L("Estimated printing time") + " [" + _u8L("Normal mode") + "]:");
break;
}
case PrintEstimatedTimeStatistics::ETimeMode::Stealth:
case PrintEstimatedStatistics::ETimeMode::Stealth:
{
imgui.text(_u8L("Estimated printing time") + " [" + _u8L("Stealth mode") + "]:");
break;
@ -4662,18 +4892,18 @@ void GCodeViewer::render_legend() const
ImGui::SameLine();
imgui.text(short_time(get_time_dhms(time_mode.time)));
auto show_mode_button = [this, &imgui](const wxString& label, PrintEstimatedTimeStatistics::ETimeMode mode) {
auto show_mode_button = [this, &imgui](const wxString& label, PrintEstimatedStatistics::ETimeMode mode) {
bool show = false;
for (size_t i = 0; i < m_time_statistics.modes.size(); ++i) {
for (size_t i = 0; i < m_print_statistics.modes.size(); ++i) {
if (i != static_cast<size_t>(mode) &&
short_time(get_time_dhms(m_time_statistics.modes[static_cast<size_t>(mode)].time)) != short_time(get_time_dhms(m_time_statistics.modes[i].time))) {
short_time(get_time_dhms(m_print_statistics.modes[static_cast<size_t>(mode)].time)) != short_time(get_time_dhms(m_print_statistics.modes[i].time))) {
show = true;
break;
}
}
if (show && m_time_statistics.modes[static_cast<size_t>(mode)].roles_times.size() > 0) {
if (show && m_print_statistics.modes[static_cast<size_t>(mode)].roles_times.size() > 0) {
if (imgui.button(label)) {
*const_cast<PrintEstimatedTimeStatistics::ETimeMode*>(&m_time_estimate_mode) = mode;
*const_cast<PrintEstimatedStatistics::ETimeMode*>(&m_time_estimate_mode) = mode;
wxGetApp().plater()->get_current_canvas3D()->set_as_dirty();
wxGetApp().plater()->get_current_canvas3D()->request_extra_frame();
}
@ -4681,12 +4911,12 @@ void GCodeViewer::render_legend() const
};
switch (m_time_estimate_mode) {
case PrintEstimatedTimeStatistics::ETimeMode::Normal: {
show_mode_button(_L("Show stealth mode"), PrintEstimatedTimeStatistics::ETimeMode::Stealth);
case PrintEstimatedStatistics::ETimeMode::Normal: {
show_mode_button(_L("Show stealth mode"), PrintEstimatedStatistics::ETimeMode::Stealth);
break;
}
case PrintEstimatedTimeStatistics::ETimeMode::Stealth: {
show_mode_button(_L("Show normal mode"), PrintEstimatedTimeStatistics::ETimeMode::Normal);
case PrintEstimatedStatistics::ETimeMode::Stealth: {
show_mode_button(_L("Show normal mode"), PrintEstimatedStatistics::ETimeMode::Normal);
break;
}
default : { assert(false); break; }

View file

@ -46,6 +46,9 @@ class GCodeViewer
{
Retractions,
Unretractions,
#if ENABLE_SEAMS_VISUALIZATION
Seams,
#endif // ENABLE_SEAMS_VISUALIZATION
ToolChanges,
ColorChanges,
PausePrints,
@ -693,8 +696,8 @@ private:
Shells m_shells;
EViewType m_view_type{ EViewType::FeatureType };
bool m_legend_enabled{ true };
PrintEstimatedTimeStatistics m_time_statistics;
PrintEstimatedTimeStatistics::ETimeMode m_time_estimate_mode{ PrintEstimatedTimeStatistics::ETimeMode::Normal };
PrintEstimatedStatistics m_print_statistics;
PrintEstimatedStatistics::ETimeMode m_time_estimate_mode{ PrintEstimatedStatistics::ETimeMode::Normal };
#if ENABLE_GCODE_VIEWER_STATISTICS
Statistics m_statistics;
#endif // ENABLE_GCODE_VIEWER_STATISTICS

View file

@ -1,7 +1,6 @@
#include "libslic3r/libslic3r.h"
#include "GLCanvas3D.hpp"
#include "admesh/stl.h"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/GCode/ThumbnailData.hpp"
@ -1672,8 +1671,10 @@ void GLCanvas3D::render()
if (m_picking_enabled)
m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast<coord_t>());
_render_current_gizmo();
// sidebar hints need to be rendered before the gizmos because the depth buffer
// could be invalidated by the following gizmo render methods
_render_selection_sidebar_hints();
_render_current_gizmo();
#if ENABLE_RENDER_PICKING_PASS
}
#endif // ENABLE_RENDER_PICKING_PASS
@ -1712,6 +1713,11 @@ void GLCanvas3D::render()
}
#endif // ENABLE_RENDER_STATISTICS
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
if (wxGetApp().is_editor() && wxGetApp().plater()->is_view3D_shown())
wxGetApp().plater()->render_project_state_debug_window();
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
#if ENABLE_CAMERA_STATISTICS
camera.debug_render();
#endif // ENABLE_CAMERA_STATISTICS
@ -3146,7 +3152,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
}
if (m_gizmos.on_mouse(evt)) {
if (wxWindow::FindFocus() != this->m_canvas)
if (wxWindow::FindFocus() != m_canvas)
// Grab keyboard focus for input in gizmo dialogs.
m_canvas->SetFocus();
@ -3169,7 +3175,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
m_mouse.set_move_start_threshold_position_2D_as_invalid();
}
if (evt.ButtonDown() && wxWindow::FindFocus() != this->m_canvas)
if (evt.ButtonDown() && wxWindow::FindFocus() != m_canvas)
// Grab keyboard focus on any mouse click event.
m_canvas->SetFocus();
@ -4829,8 +4835,16 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h)
if (m_canvas == nullptr && m_context == nullptr)
return;
#if ENABLE_SCROLLABLE_LEGEND
const std::array<unsigned int, 2> new_size = { w, h };
if (m_old_size == new_size)
return;
m_old_size = new_size;
#endif // ENABLE_SCROLLABLE_LEGEND
auto *imgui = wxGetApp().imgui();
imgui->set_display_size((float)w, (float)h);
imgui->set_display_size(static_cast<float>(w), static_cast<float>(h));
const float font_size = 1.5f * wxGetApp().em_unit();
#if ENABLE_RETINA_GL
imgui->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor());
@ -4838,6 +4852,10 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h)
imgui->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f);
#endif
#if ENABLE_SCROLLABLE_LEGEND
this->request_extra_frame();
#endif // ENABLE_SCROLLABLE_LEGEND
// ensures that this canvas is current
_set_current();
}
@ -4878,8 +4896,7 @@ void GLCanvas3D::_update_camera_zoom(double zoom)
void GLCanvas3D::_refresh_if_shown_on_screen()
{
if (_is_shown_on_screen())
{
if (_is_shown_on_screen()) {
const Size& cnv_size = get_canvas_size();
_resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height());
@ -5048,8 +5065,9 @@ void GLCanvas3D::_render_background() const
if (!m_volumes.empty())
use_error_color &= _is_any_volume_outside();
else {
BoundingBoxf3 test_volume = (m_config != nullptr) ? print_volume(*m_config) : BoundingBoxf3();
use_error_color &= (test_volume.radius() > 0.0) ? !test_volume.contains(m_gcode_viewer.get_paths_bounding_box()) : false;
const BoundingBoxf3 test_volume = (m_config != nullptr) ? print_volume(*m_config) : BoundingBoxf3();
const BoundingBoxf3& paths_volume = m_gcode_viewer.get_paths_bounding_box();
use_error_color &= (test_volume.radius() > 0.0 && paths_volume.radius() > 0.0) ? !test_volume.contains(paths_volume) : false;
}
}
@ -5983,7 +6001,7 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c
{
if (layerm->slices.surfaces.empty())
continue;
const PrintRegionConfig& cfg = layerm->region()->config();
const PrintRegionConfig& cfg = layerm->region().config();
if (cfg.perimeter_extruder.value == m_selected_extruder ||
cfg.infill_extruder.value == m_selected_extruder ||
cfg.solid_infill_extruder.value == m_selected_extruder ) {
@ -6006,7 +6024,7 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c
for (const LayerRegion *layerm : layer->regions()) {
if (is_selected_separate_extruder)
{
const PrintRegionConfig& cfg = layerm->region()->config();
const PrintRegionConfig& cfg = layerm->region().config();
if (cfg.perimeter_extruder.value != m_selected_extruder ||
cfg.infill_extruder.value != m_selected_extruder ||
cfg.solid_infill_extruder.value != m_selected_extruder)
@ -6014,7 +6032,7 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c
}
if (ctxt.has_perimeters)
_3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy,
volume(idx_layer, layerm->region()->config().perimeter_extruder.value, 0));
volume(idx_layer, layerm->region().config().perimeter_extruder.value, 0));
if (ctxt.has_infill) {
for (const ExtrusionEntity *ee : layerm->fills.entities) {
// fill represents infill extrusions of a single island.
@ -6023,8 +6041,8 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c
_3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy,
volume(idx_layer,
is_solid_infill(fill->entities.front()->role()) ?
layerm->region()->config().solid_infill_extruder :
layerm->region()->config().infill_extruder,
layerm->region().config().solid_infill_extruder :
layerm->region().config().infill_extruder,
1));
}
}
@ -6249,7 +6267,7 @@ void GLCanvas3D::_load_sla_shells()
#else
v.indexed_vertex_array.load_mesh(mesh);
#endif // ENABLE_SMOOTH_NORMALS
v.indexed_vertex_array.finalize_geometry(this->m_initialized);
v.indexed_vertex_array.finalize_geometry(m_initialized);
v.shader_outside_printer_detection_enabled = outside_printer_detection_enabled;
v.composite_id.volume_id = volume_id;
v.set_instance_offset(unscale(instance.shift.x(), instance.shift.y(), 0));

View file

@ -471,6 +471,10 @@ private:
Model* m_model;
BackgroundSlicingProcess *m_process;
#if ENABLE_SCROLLABLE_LEGEND
std::array<unsigned int, 2> m_old_size{ 0, 0 };
#endif // ENABLE_SCROLLABLE_LEGEND
// Screen is only refreshed from the OnIdle handler if it is dirty.
bool m_dirty;
bool m_initialized;

View file

@ -11,6 +11,7 @@
#include <exception>
#include <cstdlib>
#include <regex>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp>
#include <boost/lexical_cast.hpp>
@ -68,6 +69,7 @@
#include "UnsavedChangesDialog.hpp"
#include "SavePresetDialog.hpp"
#include "PrintHostDialogs.hpp"
#include "DesktopIntegrationDialog.hpp"
#include "BitmapCache.hpp"
@ -244,10 +246,11 @@ private:
// credits infornation
credits = title + " " +
_L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n\n" +
_L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" +
_L("Developed by Prusa Research.")+ "\n\n" +
title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + "\n\n" +
_L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" +
_L("Artwork model by Nora Al-Badri and Jan Nikolai Nelles");
_L("Artwork model by M Boyer");
title_font = version_font = credits_font = init_font;
}
@ -632,8 +635,17 @@ void GUI_App::post_init()
//FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config.
this->mainframe->load_config_file(this->init_params->load_configs.back());
// If loading a 3MF file, the config is loaded from the last one.
if (! this->init_params->input_files.empty())
this->plater()->load_files(this->init_params->input_files, true, true);
if (!this->init_params->input_files.empty()) {
const std::vector<size_t> res = this->plater()->load_files(this->init_params->input_files, true, true);
if (!res.empty() && this->init_params->input_files.size() == 1) {
// Update application titlebar when opening a project file
const std::string& filename = this->init_params->input_files.front();
if (boost::algorithm::iends_with(filename, ".amf") ||
boost::algorithm::iends_with(filename, ".amf.xml") ||
boost::algorithm::iends_with(filename, ".3mf"))
this->plater()->set_project_filename(filename);
}
}
if (! this->init_params->extra_config.empty())
this->mainframe->load_config(this->init_params->extra_config);
}
@ -904,6 +916,14 @@ bool GUI_App::on_init_inner()
}
else
load_current_presets();
#if ENABLE_PROJECT_DIRTY_STATE
if (plater_ != nullptr) {
plater_->reset_project_dirty_initial_presets();
plater_->update_project_dirty_from_presets();
}
#endif // ENABLE_PROJECT_DIRTY_STATE
mainframe->Show(true);
obj_list()->set_min_height();
@ -1623,6 +1643,10 @@ void GUI_App::add_config_menu(wxMenuBar *menu)
local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots"));
local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot"));
local_menu->Append(config_id_base + ConfigMenuUpdate, _L("Check for updates"), _L("Check for configuration updates"));
#ifdef __linux__
if (DesktopIntegrationDialog::integration_possible())
local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration"));
#endif
local_menu->AppendSeparator();
}
local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots +
@ -1663,9 +1687,18 @@ void GUI_App::add_config_menu(wxMenuBar *menu)
case ConfigMenuUpdate:
check_updates(true);
break;
#ifdef __linux__
case ConfigMenuDesktopIntegration:
show_desktop_integration_dialog();
break;
#endif
case ConfigMenuTakeSnapshot:
// Take a configuration snapshot.
#if ENABLE_PROJECT_DIRTY_STATE
if (check_and_save_current_preset_changes()) {
#else
if (check_unsaved_changes()) {
#endif // ENABLE_PROJECT_DIRTY_STATE
wxTextEntryDialog dlg(nullptr, _L("Taking configuration snapshot"), _L("Snapshot name"));
// set current normal font for dialog children,
@ -1680,7 +1713,11 @@ void GUI_App::add_config_menu(wxMenuBar *menu)
}
break;
case ConfigMenuSnapshots:
#if ENABLE_PROJECT_DIRTY_STATE
if (check_and_save_current_preset_changes()) {
#else
if (check_unsaved_changes()) {
#endif // ENABLE_PROJECT_DIRTY_STATE
std::string on_snapshot;
if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config))
on_snapshot = app_config->get("on_snapshot");
@ -1781,8 +1818,57 @@ void GUI_App::add_config_menu(wxMenuBar *menu)
menu->Append(local_menu, _L("&Configuration"));
}
#if ENABLE_PROJECT_DIRTY_STATE
bool GUI_App::has_unsaved_preset_changes() const
{
PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology();
for (const Tab* const tab : tabs_list) {
if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty())
return true;
}
return false;
}
bool GUI_App::has_current_preset_changes() const
{
PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology();
for (const Tab* const tab : tabs_list) {
if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty())
return true;
}
return false;
}
void GUI_App::update_saved_preset_from_current_preset()
{
PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology();
for (Tab* tab : tabs_list) {
if (tab->supports_printer_technology(printer_technology))
tab->update_saved_preset_from_current_preset();
}
}
std::vector<std::pair<unsigned int, std::string>> GUI_App::get_selected_presets() const
{
std::vector<std::pair<unsigned int, std::string>> ret;
PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology();
for (Tab* tab : tabs_list) {
if (tab->supports_printer_technology(printer_technology)) {
const PresetCollection* presets = tab->get_presets();
ret.push_back({ static_cast<unsigned int>(presets->type()), presets->get_selected_preset_name() });
}
}
return ret;
}
#endif // ENABLE_PROJECT_DIRTY_STATE
// This is called when closing the application, when loading a config file or when starting the config wizard
// to notify the user whether he is aware that some preset changes will be lost.
#if ENABLE_PROJECT_DIRTY_STATE
bool GUI_App::check_and_save_current_preset_changes(const wxString& header)
{
if (this->plater()->model().objects.empty() && has_current_preset_changes()) {
#else
bool GUI_App::check_unsaved_changes(const wxString &header)
{
PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology();
@ -1794,8 +1880,8 @@ bool GUI_App::check_unsaved_changes(const wxString &header)
break;
}
if (has_unsaved_changes)
{
if (has_unsaved_changes) {
#endif // ENABLE_PROJECT_DIRTY_STATE
UnsavedChangesDialog dlg(header);
if (wxGetApp().app_config->get("default_action_on_close_application") == "none" && dlg.ShowModal() == wxID_CANCEL)
return false;
@ -2055,6 +2141,15 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage
return res;
}
void GUI_App::show_desktop_integration_dialog()
{
#ifdef __linux__
//wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null");
DesktopIntegrationDialog dialog(mainframe);
dialog.ShowModal();
#endif //__linux__
}
#if ENABLE_THUMBNAIL_GENERATOR_DEBUG
void GUI_App::gcode_thumbnails_debug()
{

View file

@ -76,6 +76,7 @@ enum ConfigMenuIDs {
ConfigMenuSnapshots,
ConfigMenuTakeSnapshot,
ConfigMenuUpdate,
ConfigMenuDesktopIntegration,
ConfigMenuPreferences,
ConfigMenuModeSimple,
ConfigMenuModeAdvanced,
@ -209,7 +210,15 @@ public:
void update_mode();
void add_config_menu(wxMenuBar *menu);
bool check_unsaved_changes(const wxString &header = wxString());
#if ENABLE_PROJECT_DIRTY_STATE
bool has_unsaved_preset_changes() const;
bool has_current_preset_changes() const;
void update_saved_preset_from_current_preset();
std::vector<std::pair<unsigned int, std::string>> get_selected_presets() const;
bool check_and_save_current_preset_changes(const wxString& header = wxString());
#else
bool check_unsaved_changes(const wxString& header = wxString());
#endif // ENABLE_PROJECT_DIRTY_STATE
bool check_print_host_queue();
bool checked_tab(Tab* tab);
void load_current_presets(bool check_printer_presets = true);
@ -268,6 +277,7 @@ public:
void open_web_page_localized(const std::string &http_address);
bool run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page = ConfigWizard::SP_WELCOME);
void show_desktop_integration_dialog();
#if ENABLE_THUMBNAIL_GENERATOR_DEBUG
// temporary and debug only -> extract thumbnails from selected gcode and save them as png files

View file

@ -7,6 +7,9 @@
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "Plater.hpp"
#if ENABLE_PROJECT_DIRTY_STATE
#include "MainFrame.hpp"
#endif // ENABLE_PROJECT_DIRTY_STATE
#include "OptionsGroup.hpp"
#include "Tab.hpp"
@ -1457,12 +1460,15 @@ void ObjectList::load_shape_object(const std::string& type_name)
if (obj_idx < 0)
return;
take_snapshot(_(L("Add Shape")));
take_snapshot(_L("Add Shape"));
// Create mesh
BoundingBoxf3 bb;
TriangleMesh mesh = create_mesh(type_name, bb);
load_mesh_object(mesh, _(L("Shape")) + "-" + _(type_name));
load_mesh_object(mesh, _L("Shape") + "-" + _(type_name));
#if ENABLE_PROJECT_DIRTY_STATE
wxGetApp().mainframe->update_title();
#endif // ENABLE_PROJECT_DIRTY_STATE
}
void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center)
@ -2467,28 +2473,22 @@ void ObjectList::unselect_objects()
m_prevent_list_events = false;
}
void ObjectList::select_current_object(int idx)
void ObjectList::select_object_item(bool is_msr_gizmo)
{
m_prevent_list_events = true;
UnselectAll();
if (idx >= 0)
Select(m_objects_model->GetItemById(idx));
part_selection_changed();
m_prevent_list_events = false;
}
if (wxDataViewItem item = GetSelection()) {
ItemType type = m_objects_model->GetItemType(item);
bool is_volume_item = type == itVolume || type == itSettings && m_objects_model->GetItemType(m_objects_model->GetParent(item)) == itVolume;
if (is_msr_gizmo && is_volume_item || type == itObject)
return;
void ObjectList::select_current_volume(int idx, int vol_idx)
{
if (vol_idx < 0) {
select_current_object(idx);
return;
if (wxDataViewItem obj_item = m_objects_model->GetTopParent(item)) {
m_prevent_list_events = true;
UnselectAll();
Select(obj_item);
part_selection_changed();
m_prevent_list_events = false;
}
}
m_prevent_list_events = true;
UnselectAll();
if (idx >= 0)
Select(m_objects_model->GetItemByVolumeId(idx, vol_idx));
part_selection_changed();
m_prevent_list_events = false;
}
static void update_selection(wxDataViewItemArray& sels, ObjectList::SELECTION_MODE mode, ObjectDataViewModel* model)
@ -2893,7 +2893,8 @@ void ObjectList::update_selections()
{
const auto item = GetSelection();
if (selection.is_single_full_object()) {
if (m_objects_model->GetItemType(m_objects_model->GetParent(item)) & itObject)
if (m_objects_model->GetItemType(m_objects_model->GetParent(item)) & itObject &&
m_objects_model->GetObjectIdByItem(item) == selection.get_object_idx() )
return;
sels.Add(m_objects_model->GetItemById(selection.get_object_idx()));
}

View file

@ -291,10 +291,9 @@ public:
// #ys_FIXME_to_delete
// Unselect all objects in the list on c++ side
void unselect_objects();
// Select current object in the list on c++ side
void select_current_object(int idx);
// Select current volume in the list on c++ side
void select_current_volume(int idx, int vol_idx);
// Select object item in the ObjectList, when some gizmo is activated
// "is_msr_gizmo" indicates if Move/Scale/Rotate gizmo was activated
void select_object_item(bool is_msr_gizmo);
// Remove objects/sub-object from the list
void remove();

View file

@ -235,7 +235,7 @@ bool Preview::init(wxWindow* parent, Model* model)
_L("Ironing") + "|1|" +
_L("Bridge infill") + "|1|" +
_L("Gap fill") + "|1|" +
_L("Skirt") + "|1|" +
_L("Skirt/Brim") + "|1|" +
_L("Support material") + "|1|" +
_L("Support material interface") + "|1|" +
_L("Wipe tower") + "|1|" +
@ -250,6 +250,9 @@ bool Preview::init(wxWindow* parent, Model* model)
get_option_type_string(OptionType::Wipe) + "|0|" +
get_option_type_string(OptionType::Retractions) + "|0|" +
get_option_type_string(OptionType::Unretractions) + "|0|" +
#if ENABLE_SEAMS_VISUALIZATION
get_option_type_string(OptionType::Seams) + "|0|" +
#endif // ENABLE_SEAMS_VISUALIZATION
get_option_type_string(OptionType::ToolChanges) + "|0|" +
get_option_type_string(OptionType::ColorChanges) + "|0|" +
get_option_type_string(OptionType::PausePrints) + "|0|" +
@ -640,7 +643,7 @@ void Preview::update_layers_slider(const std::vector<double>& layers_z, bool kee
if (sla_print_technology)
m_layers_slider->SetLayersTimes(plater->sla_print().print_statistics().layers_times);
else {
auto print_mode_stat = m_gcode_result->time_statistics.modes.front();
auto print_mode_stat = m_gcode_result->print_statistics.modes.front();
m_layers_slider->SetLayersTimes(print_mode_stat.layers_times, print_mode_stat.time);
}
@ -1008,6 +1011,9 @@ wxString Preview::get_option_type_string(OptionType type) const
case OptionType::Wipe: { return _L("Wipe"); }
case OptionType::Retractions: { return _L("Retractions"); }
case OptionType::Unretractions: { return _L("Deretractions"); }
#if ENABLE_SEAMS_VISUALIZATION
case OptionType::Seams: { return _L("Seams"); }
#endif // ENABLE_SEAMS_VISUALIZATION
case OptionType::ToolChanges: { return _L("Tool changes"); }
case OptionType::ColorChanges: { return _L("Color changes"); }
case OptionType::PausePrints: { return _L("Print pauses"); }

View file

@ -116,6 +116,9 @@ public:
Wipe,
Retractions,
Unretractions,
#if ENABLE_SEAMS_VISUALIZATION
Seams,
#endif // ENABLE_SEAMS_VISUALIZATION
ToolChanges,
ColorChanges,
PausePrints,

View file

@ -32,6 +32,42 @@ GLGizmoPainterBase::GLGizmoPainterBase(GLCanvas3D& parent, const std::string& ic
#if ENABLE_PROJECT_DIRTY_STATE
// port of 948bc382655993721d93d3b9fce9b0186fcfb211
void GLGizmoPainterBase::activate_internal_undo_redo_stack(bool activate)
{
Plater* plater = wxGetApp().plater();
// Following is needed to prevent taking an extra snapshot when the activation of
// the internal stack happens when the gizmo is already active (such as open gizmo,
// close gizmo, undo, start painting). The internal stack does not activate on the
// undo, because that would obliterate all future of the main stack (user would
// have to close the gizmo himself, he has no access to main undo/redo after the
// internal stack opens). We don't want the "entering" snapshot taken in this case,
// because there already is one.
std::string last_snapshot_name;
plater->undo_redo_topmost_string_getter(plater->can_undo(), last_snapshot_name);
if (activate && !m_internal_stack_active) {
std::string str = get_painter_type() == PainterGizmoType::FDM_SUPPORTS
? _u8L("Entering Paint-on supports")
: _u8L("Entering Seam painting");
if (last_snapshot_name != str)
Plater::TakeSnapshot(plater, str);
plater->enter_gizmos_stack();
m_internal_stack_active = true;
}
if (!activate && m_internal_stack_active) {
plater->leave_gizmos_stack();
std::string str = get_painter_type() == PainterGizmoType::SEAM
? _u8L("Leaving Seam painting")
: _u8L("Leaving Paint-on supports");
if (last_snapshot_name != str)
Plater::TakeSnapshot(plater, str);
m_internal_stack_active = false;
}
}
#else
void GLGizmoPainterBase::activate_internal_undo_redo_stack(bool activate)
{
if (activate && ! m_internal_stack_active) {
@ -51,6 +87,7 @@ void GLGizmoPainterBase::activate_internal_undo_redo_stack(bool activate)
m_internal_stack_active = false;
}
}
#endif // ENABLE_PROJECT_DIRTY_STATE

View file

@ -45,18 +45,18 @@ bool GLGizmoSlaSupports::on_init()
{
m_shortcut_key = WXK_CONTROL_L;
m_desc["head_diameter"] = _(L("Head diameter")) + ": ";
m_desc["lock_supports"] = _(L("Lock supports under new islands"));
m_desc["remove_selected"] = _(L("Remove selected points"));
m_desc["remove_all"] = _(L("Remove all points"));
m_desc["apply_changes"] = _(L("Apply changes"));
m_desc["discard_changes"] = _(L("Discard changes"));
m_desc["minimal_distance"] = _(L("Minimal points distance")) + ": ";
m_desc["points_density"] = _(L("Support points density")) + ": ";
m_desc["auto_generate"] = _(L("Auto-generate points"));
m_desc["manual_editing"] = _(L("Manual editing"));
m_desc["clipping_of_view"] = _(L("Clipping of view"))+ ": ";
m_desc["reset_direction"] = _(L("Reset direction"));
m_desc["head_diameter"] = _L("Head diameter") + ": ";
m_desc["lock_supports"] = _L("Lock supports under new islands");
m_desc["remove_selected"] = _L("Remove selected points");
m_desc["remove_all"] = _L("Remove all points");
m_desc["apply_changes"] = _L("Apply changes");
m_desc["discard_changes"] = _L("Discard changes");
m_desc["minimal_distance"] = _L("Minimal points distance") + ": ";
m_desc["points_density"] = _L("Support points density") + ": ";
m_desc["auto_generate"] = _L("Auto-generate points");
m_desc["manual_editing"] = _L("Manual editing");
m_desc["clipping_of_view"] = _L("Clipping of view")+ ": ";
m_desc["reset_direction"] = _L("Reset direction");
return true;
}
@ -372,7 +372,7 @@ bool GLGizmoSlaSupports::gizmo_event(SLAGizmoEventType action, const Vec2d& mous
if (m_selection_empty) {
std::pair<Vec3f, Vec3f> pos_and_normal;
if (unproject_on_mesh(mouse_position, pos_and_normal)) { // we got an intersection
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Add support point")));
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Add support point"));
m_editing_cache.emplace_back(sla::SupportPoint(pos_and_normal.first, m_new_point_head_diameter/2.f, false), false, pos_and_normal.second);
m_parent.set_as_dirty();
m_wait_for_up_event = true;
@ -512,7 +512,7 @@ void GLGizmoSlaSupports::delete_selected_points(bool force)
std::abort();
}
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Delete support point")));
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Delete support point"));
for (unsigned int idx=0; idx<m_editing_cache.size(); ++idx) {
if (m_editing_cache[idx].selected && (!m_editing_cache[idx].support_point.is_new_island || !m_lock_unique_islands || force)) {
@ -692,7 +692,7 @@ RENDER_AGAIN:
cache_entry.support_point.head_front_radius = m_old_point_head_diameter / 2.f;
float backup = m_new_point_head_diameter;
m_new_point_head_diameter = m_old_point_head_diameter;
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Change point head diameter")));
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Change point head diameter"));
m_new_point_head_diameter = backup;
for (auto& cache_entry : m_editing_cache)
if (cache_entry.selected)
@ -760,7 +760,7 @@ RENDER_AGAIN:
if (slider_released) {
mo->config.set("support_points_minimal_distance", m_minimal_point_distance_stash);
mo->config.set("support_points_density_relative", (int)m_density_stash);
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Support parameter change")));
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Support parameter change"));
mo->config.set("support_points_minimal_distance", minimal_point_distance);
mo->config.set("support_points_density_relative", (int)density);
wxGetApp().obj_list()->update_and_show_object_settings_item();
@ -867,10 +867,9 @@ bool GLGizmoSlaSupports::on_is_selectable() const
std::string GLGizmoSlaSupports::on_get_name() const
{
return (_(L("SLA Support Points")) + " [L]").ToUTF8().data();
return (_L("SLA Support Points") + " [L]").ToUTF8().data();
}
CommonGizmosDataID GLGizmoSlaSupports::on_get_requirements() const
{
return CommonGizmosDataID(
@ -895,7 +894,11 @@ void GLGizmoSlaSupports::on_set_state()
// data are not yet available, the CallAfter will postpone taking the
// snapshot until they are. No, it does not feel right.
wxGetApp().CallAfter([]() {
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("SLA gizmo turned on")));
#if ENABLE_PROJECT_DIRTY_STATE
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Entering SLA gizmo"));
#else
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("SLA gizmo turned on"));
#endif // ENABLE_PROJECT_DIRTY_STATE
});
}
@ -909,8 +912,8 @@ void GLGizmoSlaSupports::on_set_state()
wxGetApp().CallAfter([this]() {
// Following is called through CallAfter, because otherwise there was a problem
// on OSX with the wxMessageDialog being shown several times when clicked into.
wxMessageDialog dlg(GUI::wxGetApp().mainframe, _(L("Do you want to save your manually "
"edited support points?")) + "\n",_(L("Save changes?")), wxICON_QUESTION | wxYES | wxNO);
wxMessageDialog dlg(GUI::wxGetApp().mainframe, _L("Do you want to save your manually "
"edited support points?") + "\n",_L("Save changes?"), wxICON_QUESTION | wxYES | wxNO);
if (dlg.ShowModal() == wxID_YES)
editing_mode_apply_changes();
else
@ -922,7 +925,11 @@ void GLGizmoSlaSupports::on_set_state()
else {
// we are actually shutting down
disable_editing_mode(); // so it is not active next time the gizmo opens
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("SLA gizmo turned off")));
#if ENABLE_PROJECT_DIRTY_STATE
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Leaving SLA gizmo"));
#else
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("SLA gizmo turned off"));
#endif // ENABLE_PROJECT_DIRTY_STATE
m_normal_cache.clear();
m_old_mo_id = -1;
}
@ -953,7 +960,7 @@ void GLGizmoSlaSupports::on_stop_dragging()
&& backup.support_point.pos != m_point_before_drag.support_point.pos) // and it was moved, not just selected
{
m_editing_cache[m_hover_id] = m_point_before_drag;
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Move support point")));
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Move support point"));
m_editing_cache[m_hover_id] = backup;
}
}
@ -1046,7 +1053,7 @@ void GLGizmoSlaSupports::editing_mode_apply_changes()
disable_editing_mode(); // this leaves the editing mode undo/redo stack and must be done before the snapshot is taken
if (unsaved_changes()) {
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Support points edit")));
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Support points edit"));
m_normal_cache.clear();
for (const CacheEntry& ce : m_editing_cache)
@ -1125,14 +1132,14 @@ void GLGizmoSlaSupports::get_data_from_backend()
void GLGizmoSlaSupports::auto_generate()
{
wxMessageDialog dlg(GUI::wxGetApp().plater(),
_(L("Autogeneration will erase all manually edited points.")) + "\n\n" +
_(L("Are you sure you want to do it?")) + "\n",
_(L("Warning")), wxICON_WARNING | wxYES | wxNO);
_L("Autogeneration will erase all manually edited points.") + "\n\n" +
_L("Are you sure you want to do it?") + "\n",
_L("Warning"), wxICON_WARNING | wxYES | wxNO);
ModelObject* mo = m_c->selection_info()->model_object();
if (mo->sla_points_status != sla::PointsStatus::UserModified || m_normal_cache.empty() || dlg.ShowModal() == wxID_YES) {
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Autogenerate support points")));
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points"));
wxGetApp().CallAfter([this]() { reslice_SLA_supports(); });
mo->sla_points_status = sla::PointsStatus::Generating;
}
@ -1180,7 +1187,7 @@ bool GLGizmoSlaSupports::unsaved_changes() const
}
SlaGizmoHelpDialog::SlaGizmoHelpDialog()
: wxDialog(nullptr, wxID_ANY, _(L("SLA gizmo keyboard shortcuts")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
: wxDialog(nullptr, wxID_ANY, _L("SLA gizmo keyboard shortcuts"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
{
SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
const wxString ctrl = GUI::shortkey_ctrl_prefix();
@ -1191,7 +1198,7 @@ SlaGizmoHelpDialog::SlaGizmoHelpDialog()
const wxFont& font = wxGetApp().small_font();
const wxFont& bold_font = wxGetApp().bold_font();
auto note_text = new wxStaticText(this, wxID_ANY, _(L("Note: some shortcuts work in (non)editing mode only.")));
auto note_text = new wxStaticText(this, wxID_ANY, _L("Note: some shortcuts work in (non)editing mode only."));
note_text->SetFont(font);
auto vsizer = new wxBoxSizer(wxVERTICAL);
@ -1209,21 +1216,21 @@ SlaGizmoHelpDialog::SlaGizmoHelpDialog()
vsizer->AddSpacer(20);
std::vector<std::pair<wxString, wxString>> shortcuts;
shortcuts.push_back(std::make_pair(_(L("Left click")), _(L("Add point"))));
shortcuts.push_back(std::make_pair(_(L("Right click")), _(L("Remove point"))));
shortcuts.push_back(std::make_pair(_(L("Drag")), _(L("Move point"))));
shortcuts.push_back(std::make_pair(ctrl+_(L("Left click")), _(L("Add point to selection"))));
shortcuts.push_back(std::make_pair(alt+_(L("Left click")), _(L("Remove point from selection"))));
shortcuts.push_back(std::make_pair(wxString("Shift+")+_(L("Drag")), _(L("Select by rectangle"))));
shortcuts.push_back(std::make_pair(alt+_(L("Drag")), _(L("Deselect by rectangle"))));
shortcuts.push_back(std::make_pair(ctrl+"A", _(L("Select all points"))));
shortcuts.push_back(std::make_pair("Delete", _(L("Remove selected points"))));
shortcuts.push_back(std::make_pair(ctrl+_(L("Mouse wheel")), _(L("Move clipping plane"))));
shortcuts.push_back(std::make_pair("R", _(L("Reset clipping plane"))));
shortcuts.push_back(std::make_pair("Enter", _(L("Apply changes"))));
shortcuts.push_back(std::make_pair("Esc", _(L("Discard changes"))));
shortcuts.push_back(std::make_pair("M", _(L("Switch to editing mode"))));
shortcuts.push_back(std::make_pair("A", _(L("Auto-generate points"))));
shortcuts.push_back(std::make_pair(_L("Left click"), _L("Add point")));
shortcuts.push_back(std::make_pair(_L("Right click"), _L("Remove point")));
shortcuts.push_back(std::make_pair(_L("Drag"), _L("Move point")));
shortcuts.push_back(std::make_pair(ctrl+_L("Left click"), _L("Add point to selection")));
shortcuts.push_back(std::make_pair(alt+_L("Left click"), _L("Remove point from selection")));
shortcuts.push_back(std::make_pair(wxString("Shift+")+_L("Drag"), _L("Select by rectangle")));
shortcuts.push_back(std::make_pair(alt+_(L("Drag")), _L("Deselect by rectangle")));
shortcuts.push_back(std::make_pair(ctrl+"A", _L("Select all points")));
shortcuts.push_back(std::make_pair("Delete", _L("Remove selected points")));
shortcuts.push_back(std::make_pair(ctrl+_L("Mouse wheel"), _L("Move clipping plane")));
shortcuts.push_back(std::make_pair("R", _L("Reset clipping plane")));
shortcuts.push_back(std::make_pair("Enter", _L("Apply changes")));
shortcuts.push_back(std::make_pair("Esc", _L("Discard changes")));
shortcuts.push_back(std::make_pair("M", _L("Switch to editing mode")));
shortcuts.push_back(std::make_pair("A", _L("Auto-generate points")));
for (const auto& pair : shortcuts) {
auto shortcut = new wxStaticText(this, wxID_ANY, pair.first);

View file

@ -5,6 +5,7 @@
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI_ObjectManipulation.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
@ -1084,8 +1085,10 @@ void GLGizmosManager::update_on_off_state(const Vec2d& mouse_pos)
return;
size_t idx = get_gizmo_idx_from_mouse(mouse_pos);
if (idx != Undefined && m_gizmos[idx]->is_activable() && m_hover == idx)
if (idx != Undefined && m_gizmos[idx]->is_activable() && m_hover == idx) {
activate_gizmo(m_current == idx ? Undefined : (EType)idx);
wxGetApp().obj_list()->select_object_item((EType)idx <= Rotate);
}
}
std::string GLGizmosManager::update_hover_state(const Vec2d& mouse_pos)

View file

@ -488,8 +488,7 @@ bool ImGuiWrapper::undo_redo_list(const ImVec2& size, const bool is_undo, bool (
int i=0;
const char* item_text;
while (items_getter(is_undo, i, &item_text))
{
while (items_getter(is_undo, i, &item_text)) {
ImGui::Selectable(item_text, i < hovered);
if (ImGui::IsItemHovered()) {

View file

@ -206,7 +206,14 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S
// declare events
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& event) {
#if ENABLE_PROJECT_DIRTY_STATE
if (m_plater != nullptr)
m_plater->save_project_if_dirty();
if (event.CanVeto() && !wxGetApp().check_and_save_current_preset_changes()) {
#else
if (event.CanVeto() && !wxGetApp().check_unsaved_changes()) {
#endif // ENABLE_PROJECT_DIRTY_STATE
event.Veto();
return;
}
@ -487,8 +494,14 @@ void MainFrame::update_title()
// m_plater->get_project_filename() produces file name including path, but excluding extension.
// Don't try to remove the extension, it would remove part of the file name after the last dot!
wxString project = from_path(into_path(m_plater->get_project_filename()).filename());
#if ENABLE_PROJECT_DIRTY_STATE
wxString dirty_marker = (!m_plater->model().objects.empty() && m_plater->is_project_dirty()) ? "*" : "";
if (!dirty_marker.empty() || !project.empty())
title = dirty_marker + project + " - ";
#else
if (!project.empty())
title += (project + " - ");
#endif // ENABLE_PROJECT_DIRTY_STATE
}
std::string build_id = wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID;
@ -672,10 +685,36 @@ bool MainFrame::can_start_new_project() const
return (m_plater != nullptr) && (!m_plater->get_project_filename(".3mf").IsEmpty() || !m_plater->model().objects.empty());
}
#if ENABLE_PROJECT_DIRTY_STATE
bool MainFrame::can_save() const
{
return (m_plater != nullptr) && !m_plater->model().objects.empty() && !m_plater->get_project_filename().empty() && m_plater->is_project_dirty();
}
bool MainFrame::can_save_as() const
{
return (m_plater != nullptr) && !m_plater->model().objects.empty();
}
void MainFrame::save_project()
{
save_project_as(m_plater->get_project_filename(".3mf"));
}
void MainFrame::save_project_as(const wxString& filename)
{
bool ret = (m_plater != nullptr) ? m_plater->export_3mf(into_path(filename)) : false;
if (ret) {
// wxGetApp().update_saved_preset_from_current_preset();
m_plater->reset_project_dirty_after_save();
}
}
#else
bool MainFrame::can_save() const
{
return (m_plater != nullptr) && !m_plater->model().objects.empty();
}
#endif // ENABLE_PROJECT_DIRTY_STATE
bool MainFrame::can_export_model() const
{
@ -984,16 +1023,27 @@ void MainFrame::init_menubar_as_editor()
Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(m_recent_projects.GetCount() > 0); }, recent_projects_submenu->GetId());
#if ENABLE_PROJECT_DIRTY_STATE
append_menu_item(fileMenu, wxID_ANY, _L("&Save Project") + "\tCtrl+S", _L("Save current project file"),
[this](wxCommandEvent&) { save_project(); }, "save", nullptr,
[this](){return m_plater != nullptr && can_save(); }, this);
#else
append_menu_item(fileMenu, wxID_ANY, _L("&Save Project") + "\tCtrl+S", _L("Save current project file"),
[this](wxCommandEvent&) { if (m_plater) m_plater->export_3mf(into_path(m_plater->get_project_filename(".3mf"))); }, "save", nullptr,
[this](){return m_plater != nullptr && can_save(); }, this);
#endif // ENABLE_PROJECT_DIRTY_STATE
#ifdef __APPLE__
append_menu_item(fileMenu, wxID_ANY, _L("Save Project &as") + dots + "\tCtrl+Shift+S", _L("Save current project file as"),
#else
append_menu_item(fileMenu, wxID_ANY, _L("Save Project &as") + dots + "\tCtrl+Alt+S", _L("Save current project file as"),
#endif // __APPLE__
#if ENABLE_PROJECT_DIRTY_STATE
[this](wxCommandEvent&) { save_project_as(); }, "save", nullptr,
[this](){return m_plater != nullptr && can_save_as(); }, this);
#else
[this](wxCommandEvent&) { if (m_plater) m_plater->export_3mf(); }, "save", nullptr,
[this](){return m_plater != nullptr && can_save(); }, this);
#endif // ENABLE_PROJECT_DIRTY_STATE
fileMenu->AppendSeparator();
@ -1483,10 +1533,10 @@ void MainFrame::repair_stl()
output_file = dlg.GetPath();
}
auto tmesh = new Slic3r::TriangleMesh();
tmesh->ReadSTLFile(input_file.ToUTF8().data());
tmesh->repair();
tmesh->WriteOBJFile(output_file.ToUTF8().data());
Slic3r::TriangleMesh tmesh;
tmesh.ReadSTLFile(input_file.ToUTF8().data());
tmesh.repair();
tmesh.WriteOBJFile(output_file.ToUTF8().data());
Slic3r::GUI::show_info(this, L("Your file was repaired."), L("Repair"));
}
@ -1518,7 +1568,11 @@ void MainFrame::export_config()
// Load a config file containing a Print, Filament & Printer preset.
void MainFrame::load_config_file()
{
#if ENABLE_PROJECT_DIRTY_STATE
if (!wxGetApp().check_and_save_current_preset_changes())
#else
if (!wxGetApp().check_unsaved_changes())
#endif // ENABLE_PROJECT_DIRTY_STATE
return;
wxFileDialog dlg(this, _L("Select configuration to load:"),
!m_last_config.IsEmpty() ? get_dir_name(m_last_config) : wxGetApp().app_config->get_last_dir(),
@ -1547,7 +1601,11 @@ bool MainFrame::load_config_file(const std::string &path)
void MainFrame::export_configbundle(bool export_physical_printers /*= false*/)
{
#if ENABLE_PROJECT_DIRTY_STATE
if (!wxGetApp().check_and_save_current_preset_changes())
#else
if (!wxGetApp().check_unsaved_changes())
#endif // ENABLE_PROJECT_DIRTY_STATE
return;
// validate current configuration in case it's dirty
auto err = wxGetApp().preset_bundle->full_config().validate();
@ -1579,7 +1637,11 @@ void MainFrame::export_configbundle(bool export_physical_printers /*= false*/)
// but that behavior was not documented and likely buggy.
void MainFrame::load_configbundle(wxString file/* = wxEmptyString, const bool reset_user_profile*/)
{
#if ENABLE_PROJECT_DIRTY_STATE
if (!wxGetApp().check_and_save_current_preset_changes())
#else
if (!wxGetApp().check_unsaved_changes())
#endif // ENABLE_PROJECT_DIRTY_STATE
return;
if (file.IsEmpty()) {
wxFileDialog dlg(this, _L("Select configuration to load:"),

View file

@ -91,7 +91,9 @@ class MainFrame : public DPIFrame
void on_value_changed(wxCommandEvent&);
bool can_start_new_project() const;
#if !ENABLE_PROJECT_DIRTY_STATE
bool can_save() const;
#endif // !ENABLE_PROJECT_DIRTY_STATE
bool can_export_model() const;
bool can_export_toolpaths() const;
bool can_export_supports() const;
@ -184,6 +186,13 @@ public:
// Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig
void on_config_changed(DynamicPrintConfig* cfg) const ;
#if ENABLE_PROJECT_DIRTY_STATE
bool can_save() const;
bool can_save_as() const;
void save_project();
void save_project_as(const wxString& filename = wxString());
#endif // ENABLE_PROJECT_DIRTY_STATE
void add_to_recent_projects(const wxString& filename);
PrintHostQueueDialog* printhost_queue_dlg() { return m_printhost_queue_dlg; }

View file

@ -82,7 +82,13 @@ enum class NotificationType
// Notification emitted by Print::validate
PrintValidateWarning,
// Notification telling user to quit SLA supports manual editing
QuitSLAManualMode
QuitSLAManualMode,
// Desktop integration basic info
DesktopIntegrationSuccess,
DesktopIntegrationFail,
UndoDesktopIntegrationSuccess,
UndoDesktopIntegrationFail
};
class NotificationManager
@ -514,6 +520,14 @@ private:
"To export the G-code correctly, check the \"Color Change G-code\" in \"Printer Settings > Custom G-code\"") },
{NotificationType::EmptyAutoColorChange, NotificationLevel::RegularNotification, 10,
_u8L("This model doesn't allow to automatically add the color changes") },
{NotificationType::DesktopIntegrationSuccess, NotificationLevel::RegularNotification, 10,
_u8L("Desktop integration was successful.") },
{NotificationType::DesktopIntegrationFail, NotificationLevel::WarningNotification, 10,
_u8L("Desktop integration failed.") },
{NotificationType::UndoDesktopIntegrationSuccess, NotificationLevel::RegularNotification, 10,
_u8L("Undo desktop integration was successful.") },
{NotificationType::UndoDesktopIntegrationFail, NotificationLevel::WarningNotification, 10,
_u8L("Undo desktop integration failed.") },
//{NotificationType::NewAppAvailable, NotificationLevel::ImportantNotification, 20, _u8L("New vesion of PrusaSlicer is available.", _u8L("Download page.") },
//{NotificationType::LoadingFailed, NotificationLevel::RegularNotification, 20, _u8L("Loading of model has Failed") },
//{NotificationType::DeviceEjected, NotificationLevel::RegularNotification, 10, _u8L("Removable device has been safely ejected")} // if we want changeble text (like here name of device), we need to do it as CustomNotification

View file

@ -228,7 +228,7 @@ public:
int config_type() const throw() { return m_config_type; }
const t_opt_map& opt_map() const throw() { return m_opt_map; }
void set_config_category_and_type(const wxString &category, int type) { this->m_config_category = category; this->m_config_type = type; }
void set_config_category_and_type(const wxString &category, int type) { m_config_category = category; m_config_type = type; }
void set_config(DynamicPrintConfig* config) { m_config = config; m_modelconfig = nullptr; }
Option get_option(const std::string& opt_key, int opt_index = -1);
Line create_single_option_line(const std::string& title, const wxString& path = wxEmptyString, int idx = -1) /*const*/{

View file

@ -81,6 +81,9 @@
#include "InstanceCheck.hpp"
#include "NotificationManager.hpp"
#include "PresetComboBoxes.hpp"
#if ENABLE_PROJECT_DIRTY_STATE
#include "ProjectDirtyStateManager.hpp"
#endif // ENABLE_PROJECT_DIRTY_STATE
#ifdef __APPLE__
#include "Gizmos/GLGizmosManager.hpp"
@ -976,7 +979,6 @@ void Sidebar::sys_color_changed()
for (PlaterPresetComboBox* combo : p->combos_filament)
combo->msw_rescale();
p->frequently_changed_parameters->msw_rescale();
p->object_list->msw_rescale();
p->object_list->sys_color_changed();
p->object_manipulation->sys_color_changed();
@ -1173,10 +1175,10 @@ void Sidebar::update_sliced_info_sizer()
new_label += format_wxstr(":\n - %1%\n - %2%", _L("objects"), _L("wipe tower"));
wxString info_text = is_wipe_tower ?
wxString::Format("%.2f \n%.2f \n%.2f", ps.total_used_filament / /*1000*/koef,
(ps.total_used_filament - ps.total_wipe_tower_filament) / /*1000*/koef,
ps.total_wipe_tower_filament / /*1000*/koef) :
wxString::Format("%.2f", ps.total_used_filament / /*1000*/koef);
wxString::Format("%.2f \n%.2f \n%.2f", ps.total_used_filament / koef,
(ps.total_used_filament - ps.total_wipe_tower_filament) / koef,
ps.total_wipe_tower_filament / koef) :
wxString::Format("%.2f", ps.total_used_filament / koef);
p->sliced_info->SetTextAndShow(siFilament_m, info_text, new_label);
koef = imperial_units ? pow(ObjectManipulation::mm_to_in, 3) : 1.0f;
@ -1204,7 +1206,7 @@ void Sidebar::update_sliced_info_sizer()
filament_weight = ps.total_weight;
else {
double filament_density = filament_preset->config.opt_float("filament_density", 0);
filament_weight = filament.second * filament_density * 2.4052f * 0.001; // assumes 1.75mm filament diameter;
filament_weight = filament.second * filament_density/* *2.4052f*/ * 0.001; // assumes 1.75mm filament diameter;
new_label += "\n - " + format_wxstr(_L("Filament at extruder %1%"), filament.first + 1);
info_text += wxString::Format("\n%.2f", filament_weight);
@ -1358,7 +1360,8 @@ void Sidebar::update_ui_from_settings()
update_sliced_info_sizer();
// update Cut gizmo, if it's open
p->plater->canvas3D()->update_gizmos_on_off_state();
p->plater->canvas3D()->request_extra_frame();
p->plater->set_current_canvas_as_dirty();
p->plater->get_current_canvas3D()->request_extra_frame();
}
std::vector<PlaterPresetComboBox*>& Sidebar::combos_filament()
@ -1396,7 +1399,13 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi
this->MSWUpdateDragImageOnLeave();
#endif // WIN32
#if ENABLE_PROJECT_DIRTY_STATE
bool res = (m_plater != nullptr) ? m_plater->load_files(filenames) : false;
wxGetApp().mainframe->update_title();
return res;
#else
return (m_plater != nullptr) ? m_plater->load_files(filenames) : false;
#endif // ENABLE_PROJECT_DIRTY_STATE
}
// State to manage showing after export notifications and device ejecting
@ -1440,6 +1449,10 @@ struct Plater::priv
Preview *preview;
NotificationManager* notification_manager { nullptr };
#if ENABLE_PROJECT_DIRTY_STATE
ProjectDirtyStateManager dirty_state;
#endif // ENABLE_PROJECT_DIRTY_STATE
BackgroundSlicingProcess background_process;
bool suppressed_backround_processing_update { false };
@ -1510,6 +1523,31 @@ struct Plater::priv
priv(Plater *q, MainFrame *main_frame);
~priv();
#if ENABLE_PROJECT_DIRTY_STATE
bool is_project_dirty() const { return dirty_state.is_dirty(); }
void update_project_dirty_from_presets() { dirty_state.update_from_presets(); }
bool save_project_if_dirty() {
if (dirty_state.is_dirty()) {
MainFrame* mainframe = wxGetApp().mainframe;
if (mainframe->can_save_as()) {
wxMessageDialog dlg(mainframe, _L("Do you want to save the changes to the current project ?"), wxString(SLIC3R_APP_NAME), wxYES_NO | wxCANCEL);
int res = dlg.ShowModal();
if (res == wxID_YES)
mainframe->save_project_as(wxGetApp().plater()->get_project_filename());
else if (res == wxID_CANCEL)
return false;
}
}
return true;
}
void reset_project_dirty_after_save() { dirty_state.reset_after_save(); }
void reset_project_dirty_initial_presets() { dirty_state.reset_initial_presets(); }
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
void render_project_state_debug_window() const { dirty_state.render_debug_window(); }
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
#endif // ENABLE_PROJECT_DIRTY_STATE
enum class UpdateParams {
FORCE_FULL_SCREEN_REFRESH = 1,
FORCE_BACKGROUND_PROCESSING_UPDATE = 2,
@ -1593,8 +1631,8 @@ struct Plater::priv
void redo();
void undo_redo_to(size_t time_to_load);
void suppress_snapshots() { this->m_prevent_snapshots++; }
void allow_snapshots() { this->m_prevent_snapshots--; }
void suppress_snapshots() { m_prevent_snapshots++; }
void allow_snapshots() { m_prevent_snapshots--; }
void process_validation_warning(const std::string& warning) const;
@ -1689,7 +1727,7 @@ struct Plater::priv
bool can_split(bool to_objects) const;
void generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background);
void generate_thumbnails(ThumbnailsList& thumbnails, const Vec2ds& sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background);
ThumbnailsList generate_thumbnails(const ThumbnailsParams& params);
void bring_instance_forward() const;
@ -1765,15 +1803,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
background_process.set_fff_print(&fff_print);
background_process.set_sla_print(&sla_print);
background_process.set_gcode_result(&gcode_result);
background_process.set_thumbnail_cb([this](ThumbnailsList& thumbnails, const Vec2ds& sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background)
{
std::packaged_task<void(ThumbnailsList&, const Vec2ds&, bool, bool, bool, bool)> task([this](ThumbnailsList& thumbnails, const Vec2ds& sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) {
generate_thumbnails(thumbnails, sizes, printable_only, parts_only, show_bed, transparent_background);
});
std::future<void> result = task.get_future();
wxTheApp->CallAfter([&]() { task(thumbnails, sizes, printable_only, parts_only, show_bed, transparent_background); });
result.wait();
});
background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params); });
background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED);
background_process.set_finished_event(EVT_PROCESS_COMPLETED);
background_process.set_export_began_event(EVT_EXPORT_BEGAN);
@ -3818,17 +3848,17 @@ void Plater::priv::generate_thumbnail(ThumbnailData& data, unsigned int w, unsig
view3D->get_canvas3d()->render_thumbnail(data, w, h, printable_only, parts_only, show_bed, transparent_background);
}
void Plater::priv::generate_thumbnails(ThumbnailsList& thumbnails, const Vec2ds& sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background)
ThumbnailsList Plater::priv::generate_thumbnails(const ThumbnailsParams& params)
{
thumbnails.clear();
for (const Vec2d& size : sizes)
{
ThumbnailsList thumbnails;
for (const Vec2d& size : params.sizes) {
thumbnails.push_back(ThumbnailData());
Point isize(size); // round to ints
generate_thumbnail(thumbnails.back(), isize.x(), isize.y(), printable_only, parts_only, show_bed, transparent_background);
generate_thumbnail(thumbnails.back(), isize.x(), isize.y(), params.printable_only, params.parts_only, params.show_bed, params.transparent_background);
if (!thumbnails.back().is_valid())
thumbnails.pop_back();
}
return thumbnails;
}
wxString Plater::priv::get_project_filename(const wxString& extension) const
@ -4217,9 +4247,9 @@ int Plater::priv::get_active_snapshot_index()
void Plater::priv::take_snapshot(const std::string& snapshot_name)
{
if (this->m_prevent_snapshots > 0)
if (m_prevent_snapshots > 0)
return;
assert(this->m_prevent_snapshots >= 0);
assert(m_prevent_snapshots >= 0);
UndoRedo::SnapshotData snapshot_data;
snapshot_data.printer_technology = this->printer_technology;
if (this->view3D->is_layers_editing_enabled())
@ -4249,6 +4279,11 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name)
}
this->undo_redo_stack().take_snapshot(snapshot_name, model, view3D->get_canvas3d()->get_selection(), view3D->get_canvas3d()->get_gizmos_manager(), snapshot_data);
this->undo_redo_stack().release_least_recently_used();
#if ENABLE_PROJECT_DIRTY_STATE
dirty_state.update_from_undo_redo_stack(ProjectDirtyStateManager::UpdateType::TakeSnapshot);
#endif // ENABLE_PROJECT_DIRTY_STATE
// Save the last active preset name of a particular printer technology.
((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name();
BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info();
@ -4289,8 +4324,13 @@ void Plater::priv::undo_redo_to(std::vector<UndoRedo::Snapshot>::const_iterator
if (printer_technology_changed) {
// Switching the printer technology when jumping forwards / backwards in time. Switch to the last active printer profile of the other type.
std::string s_pt = (it_snapshot->snapshot_data.printer_technology == ptFFF) ? "FFF" : "SLA";
#if ENABLE_PROJECT_DIRTY_STATE
if (!wxGetApp().check_and_save_current_preset_changes(format_wxstr(_L(
"%1% printer was active at the time the target Undo / Redo snapshot was taken. Switching to %1% printer requires reloading of %1% presets."), s_pt)))
#else
if (! wxGetApp().check_unsaved_changes(format_wxstr(_L(
"%1% printer was active at the time the target Undo / Redo snapshot was taken. Switching to %1% printer requires reloading of %1% presets."), s_pt)))
#endif // ENABLE_PROJECT_DIRTY_STATE
// Don't switch the profiles.
return;
}
@ -4379,6 +4419,10 @@ void Plater::priv::undo_redo_to(std::vector<UndoRedo::Snapshot>::const_iterator
if (! view3D->is_layers_editing_enabled() && this->layers_height_allowed() && new_variable_layer_editing_active)
view3D->get_canvas3d()->force_main_toolbar_left_action(view3D->get_canvas3d()->get_main_toolbar_item_id("layersediting"));
}
#if ENABLE_PROJECT_DIRTY_STATE
dirty_state.update_from_undo_redo_stack(ProjectDirtyStateManager::UpdateType::UndoRedoTo);
#endif // ENABLE_PROJECT_DIRTY_STATE
}
void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bool /* temp_snapshot_was_taken */)
@ -4462,9 +4506,16 @@ Plater::Plater(wxWindow *parent, MainFrame *main_frame)
// Initialization performed in the private c-tor
}
Plater::~Plater()
{
}
#if ENABLE_PROJECT_DIRTY_STATE
bool Plater::is_project_dirty() const { return p->is_project_dirty(); }
void Plater::update_project_dirty_from_presets() { p->update_project_dirty_from_presets(); }
bool Plater::save_project_if_dirty() { return p->save_project_if_dirty(); }
void Plater::reset_project_dirty_after_save() { p->reset_project_dirty_after_save(); }
void Plater::reset_project_dirty_initial_presets() { p->reset_project_dirty_initial_presets(); }
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
void Plater::render_project_state_debug_window() const { p->render_project_state_debug_window(); }
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
#endif // ENABLE_PROJECT_DIRTY_STATE
Sidebar& Plater::sidebar() { return *p->sidebar; }
Model& Plater::model() { return p->model; }
@ -4475,12 +4526,30 @@ SLAPrint& Plater::sla_print() { return p->sla_print; }
void Plater::new_project()
{
#if ENABLE_PROJECT_DIRTY_STATE
if (!p->save_project_if_dirty())
return;
#endif // ENABLE_PROJECT_DIRTY_STATE
p->select_view_3D("3D");
#if ENABLE_PROJECT_DIRTY_STATE
take_snapshot(_L("New Project"));
Plater::SuppressSnapshots suppress(this);
reset();
reset_project_dirty_initial_presets();
update_project_dirty_from_presets();
#else
wxPostEvent(p->view3D->get_wxglcanvas(), SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL));
#endif // ENABLE_PROJECT_DIRTY_STATE
}
void Plater::load_project()
{
#if ENABLE_PROJECT_DIRTY_STATE
if (!p->save_project_if_dirty())
return;
#endif // ENABLE_PROJECT_DIRTY_STATE
// Ask user for a project file name.
wxString input_file;
wxGetApp().load_project(this, input_file);
@ -4504,8 +4573,16 @@ void Plater::load_project(const wxString& filename)
std::vector<size_t> res = load_files(input_paths);
// if res is empty no data has been loaded
#if ENABLE_PROJECT_DIRTY_STATE
if (!res.empty()) {
p->set_project_filename(filename);
reset_project_dirty_initial_presets();
update_project_dirty_from_presets();
}
#else
if (!res.empty())
p->set_project_filename(filename);
#endif // ENABLE_PROJECT_DIRTY_STATE
}
void Plater::add_model(bool imperial_units/* = false*/)
@ -4536,7 +4613,13 @@ void Plater::add_model(bool imperial_units/* = false*/)
}
Plater::TakeSnapshot snapshot(this, snapshot_label);
#if ENABLE_PROJECT_DIRTY_STATE
std::vector<size_t> res = load_files(paths, true, false, imperial_units);
if (!res.empty())
wxGetApp().mainframe->update_title();
#else
load_files(paths, true, false, imperial_units);
#endif // ENABLE_PROJECT_DIRTY_STATE
}
void Plater::import_sl1_archive()
@ -5095,6 +5178,30 @@ void Plater::export_stl(bool extended, bool selection_only)
if (selection_only && (obj_idx == -1 || selection.is_wipe_tower()))
return;
// Following lambda generates a combined mesh for export with normals pointing outwards.
auto mesh_to_export = [](const ModelObject* mo, bool instances) -> TriangleMesh {
TriangleMesh mesh;
for (const ModelVolume *v : mo->volumes)
if (v->is_model_part()) {
TriangleMesh vol_mesh(v->mesh());
vol_mesh.repair();
vol_mesh.transform(v->get_matrix(), true);
mesh.merge(vol_mesh);
}
mesh.repair();
if (instances) {
TriangleMesh vols_mesh(mesh);
mesh = TriangleMesh();
for (const ModelInstance *i : mo->instances) {
TriangleMesh m = vols_mesh;
m.transform(i->get_matrix(), true);
mesh.merge(m);
}
}
mesh.repair();
return mesh;
};
TriangleMesh mesh;
if (p->printer_technology == ptFFF) {
if (selection_only) {
@ -5102,20 +5209,21 @@ void Plater::export_stl(bool extended, bool selection_only)
if (selection.get_mode() == Selection::Instance)
{
if (selection.is_single_full_object())
mesh = model_object->mesh();
mesh = mesh_to_export(model_object, true);
else
mesh = model_object->full_raw_mesh();
mesh = mesh_to_export(model_object, false);
}
else
{
const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin());
mesh = model_object->volumes[volume->volume_idx()]->mesh();
mesh.transform(volume->get_volume_transformation().get_matrix());
mesh.transform(volume->get_volume_transformation().get_matrix(), true);
mesh.translate(-model_object->origin_translation.cast<float>());
}
}
else {
mesh = p->model.mesh();
for (const ModelObject *o : p->model.objects)
mesh.merge(mesh_to_export(o, true));
}
}
else
@ -5226,24 +5334,39 @@ void Plater::export_amf()
}
}
#if ENABLE_PROJECT_DIRTY_STATE
bool Plater::export_3mf(const boost::filesystem::path& output_path)
#else
void Plater::export_3mf(const boost::filesystem::path& output_path)
#endif // ENABLE_PROJECT_DIRTY_STATE
{
if (p->model.objects.empty()
|| canvas3D()->get_gizmos_manager().is_in_editing_mode(true))
#if ENABLE_PROJECT_DIRTY_STATE
return false;
#else
return;
#endif // ENABLE_PROJECT_DIRTY_STATE
wxString path;
bool export_config = true;
if (output_path.empty())
{
if (output_path.empty()) {
path = p->get_export_file(FT_3MF);
#if ENABLE_PROJECT_DIRTY_STATE
if (path.empty()) { return false; }
#else
if (path.empty()) { return; }
#endif // ENABLE_PROJECT_DIRTY_STATE
}
else
path = from_path(output_path);
if (!path.Lower().EndsWith(".3mf"))
#if ENABLE_PROJECT_DIRTY_STATE
return false;
#else
return;
#endif // ENABLE_PROJECT_DIRTY_STATE
DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure();
const std::string path_u8 = into_u8(path);
@ -5251,6 +5374,19 @@ void Plater::export_3mf(const boost::filesystem::path& output_path)
bool full_pathnames = wxGetApp().app_config->get("export_sources_full_pathnames") == "1";
ThumbnailData thumbnail_data;
p->generate_thumbnail(thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, false, true, true, true);
#if ENABLE_PROJECT_DIRTY_STATE
bool ret = Slic3r::store_3mf(path_u8.c_str(), &p->model, export_config ? &cfg : nullptr, full_pathnames, &thumbnail_data);
if (ret) {
// Success
p->statusbar()->set_status_text(format_wxstr(_L("3MF file exported to %s"), path));
p->set_project_filename(path);
}
else {
// Failure
p->statusbar()->set_status_text(format_wxstr(_L("Error exporting 3MF file %s"), path));
}
return ret;
#else
if (Slic3r::store_3mf(path_u8.c_str(), &p->model, export_config ? &cfg : nullptr, full_pathnames, &thumbnail_data)) {
// Success
p->statusbar()->set_status_text(format_wxstr(_L("3MF file exported to %s"), path));
@ -5260,6 +5396,7 @@ void Plater::export_3mf(const boost::filesystem::path& output_path)
// Failure
p->statusbar()->set_status_text(format_wxstr(_L("Error exporting 3MF file %s"), path));
}
#endif // ENABLE_PROJECT_DIRTY_STATE
}
void Plater::reload_from_disk()
@ -5716,7 +5853,7 @@ wxString Plater::get_project_filename(const wxString& extension) const
void Plater::set_project_filename(const wxString& filename)
{
return p->set_project_filename(filename);
p->set_project_filename(filename);
}
bool Plater::is_export_gcode_scheduled() const
@ -6143,6 +6280,9 @@ bool Plater::can_mirror() const { return p->can_mirror(); }
bool Plater::can_split(bool to_objects) const { return p->can_split(to_objects); }
const UndoRedo::Stack& Plater::undo_redo_stack_main() const { return p->undo_redo_stack_main(); }
void Plater::clear_undo_redo_stack_main() { p->undo_redo_stack_main().clear(); }
#if ENABLE_PROJECT_DIRTY_STATE
const UndoRedo::Stack& Plater::undo_redo_stack_active() const { return p->undo_redo_stack(); }
#endif // ENABLE_PROJECT_DIRTY_STATE
void Plater::enter_gizmos_stack() { p->enter_gizmos_stack(); }
void Plater::leave_gizmos_stack() { p->leave_gizmos_stack(); }
bool Plater::inside_snapshot_capture() { return p->inside_snapshot_capture(); }

View file

@ -128,7 +128,18 @@ public:
Plater(const Plater &) = delete;
Plater &operator=(Plater &&) = delete;
Plater &operator=(const Plater &) = delete;
~Plater();
~Plater() = default;
#if ENABLE_PROJECT_DIRTY_STATE
bool is_project_dirty() const;
void update_project_dirty_from_presets();
bool save_project_if_dirty();
void reset_project_dirty_after_save();
void reset_project_dirty_initial_presets();
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
void render_project_state_debug_window() const;
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
#endif // ENABLE_PROJECT_DIRTY_STATE
Sidebar& sidebar();
Model& model();
@ -198,7 +209,11 @@ public:
void export_gcode(bool prefer_removable);
void export_stl(bool extended = false, bool selection_only = false);
void export_amf();
#if ENABLE_PROJECT_DIRTY_STATE
bool export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path());
#else
void export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path());
#endif // ENABLE_PROJECT_DIRTY_STATE
void reload_from_disk();
void reload_all_from_disk();
bool has_toolpaths_to_export() const;
@ -228,6 +243,9 @@ public:
// For the memory statistics.
const Slic3r::UndoRedo::Stack& undo_redo_stack_main() const;
void clear_undo_redo_stack_main();
#if ENABLE_PROJECT_DIRTY_STATE
const Slic3r::UndoRedo::Stack& undo_redo_stack_active() const;
#endif // ENABLE_PROJECT_DIRTY_STATE
// Enter / leave the Gizmos specific Undo / Redo stack. To be used by the SLA support point editing gizmo.
void enter_gizmos_stack();
void leave_gizmos_stack();

View file

@ -134,7 +134,7 @@ void PresetComboBox::OnSelect(wxCommandEvent& evt)
auto marker = reinterpret_cast<Marker>(this->GetClientData(selected_item));
if (marker >= LABEL_ITEM_DISABLED && marker < LABEL_ITEM_MAX)
this->SetSelection(this->m_last_selected);
this->SetSelection(m_last_selected);
else if (on_selection_changed && (m_last_selected != selected_item || m_collection->current_is_dirty())) {
m_last_selected = selected_item;
on_selection_changed(selected_item);
@ -698,7 +698,7 @@ void PlaterPresetComboBox::OnSelect(wxCommandEvent &evt)
auto marker = reinterpret_cast<Marker>(this->GetClientData(selected_item));
if (marker >= LABEL_ITEM_MARKER && marker < LABEL_ITEM_MAX) {
this->SetSelection(this->m_last_selected);
this->SetSelection(m_last_selected);
evt.StopPropagation();
if (marker == LABEL_ITEM_MARKER)
return;
@ -715,8 +715,8 @@ void PlaterPresetComboBox::OnSelect(wxCommandEvent &evt)
}
return;
}
else if (marker == LABEL_ITEM_PHYSICAL_PRINTER || this->m_last_selected != selected_item || m_collection->current_is_dirty())
this->m_last_selected = selected_item;
else if (marker == LABEL_ITEM_PHYSICAL_PRINTER || m_last_selected != selected_item || m_collection->current_is_dirty())
m_last_selected = selected_item;
evt.Skip();
}
@ -973,7 +973,7 @@ void TabPresetComboBox::OnSelect(wxCommandEvent &evt)
auto marker = reinterpret_cast<Marker>(this->GetClientData(selected_item));
if (marker >= LABEL_ITEM_DISABLED && marker < LABEL_ITEM_MAX) {
this->SetSelection(this->m_last_selected);
this->SetSelection(m_last_selected);
if (marker == LABEL_ITEM_WIZARD_PRINTERS)
wxTheApp->CallAfter([this]() {
wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_PRINTERS);

View file

@ -86,7 +86,8 @@ std::string PresetHints::maximum_volumetric_flow_description(const PresetBundle
// Print config values
double layer_height = print_config.opt_float("layer_height");
double first_layer_height = print_config.get_abs_value("first_layer_height", layer_height);
assert(! print_config.option<ConfigOptionFloatOrPercent>("first_layer_height")->percent);
double first_layer_height = print_config.opt_float("first_layer_height");
double support_material_speed = print_config.opt_float("support_material_speed");
double support_material_interface_speed = print_config.get_abs_value("support_material_interface_speed", support_material_speed);
double bridge_speed = print_config.opt_float("bridge_speed");

View file

@ -0,0 +1,412 @@
#include "libslic3r/libslic3r.h"
#include "ProjectDirtyStateManager.hpp"
#include "ImGuiWrapper.hpp"
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "I18N.hpp"
#include "Plater.hpp"
#include "../Utils/UndoRedo.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <algorithm>
#include <assert.h>
#if ENABLE_PROJECT_DIRTY_STATE
namespace Slic3r {
namespace GUI {
enum class EStackType
{
Main,
Gizmo
};
// returns the current active snapshot (the topmost snapshot in the undo part of the stack) in the given stack
static const UndoRedo::Snapshot* get_active_snapshot(const UndoRedo::Stack& stack) {
const std::vector<UndoRedo::Snapshot>& snapshots = stack.snapshots();
const size_t active_snapshot_time = stack.active_snapshot_time();
const auto it = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(active_snapshot_time));
const int idx = it - snapshots.begin() - 1;
const Slic3r::UndoRedo::Snapshot* ret = (0 <= idx && (size_t)idx < snapshots.size() - 1) ?
&snapshots[idx] : nullptr;
assert(ret != nullptr);
return ret;
}
// returns the last saveable snapshot (the topmost snapshot in the undo part of the stack that can be saved) in the given stack
static const UndoRedo::Snapshot* get_last_saveable_snapshot(EStackType type, const UndoRedo::Stack& stack,
const ProjectDirtyStateManager::DirtyState::Gizmos& gizmos, size_t last_save_main) {
// returns true if the given snapshot is not saveable
auto skip_main = [&gizmos, last_save_main, &stack](const UndoRedo::Snapshot& snapshot) {
auto is_gizmo_with_modifications = [&gizmos, &stack](const UndoRedo::Snapshot& snapshot) {
if (boost::starts_with(snapshot.name, _utf8("Entering"))) {
if (gizmos.current)
return true;
std::string topmost_redo;
wxGetApp().plater()->undo_redo_topmost_string_getter(false, topmost_redo);
if (boost::starts_with(topmost_redo, _utf8("Leaving"))) {
const std::vector<UndoRedo::Snapshot>& snapshots = stack.snapshots();
const UndoRedo::Snapshot* leaving_snapshot = &(*std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(snapshot.timestamp + 1)));
if (gizmos.is_used_and_modified(*leaving_snapshot))
return true;
}
}
return false;
};
if (snapshot.name == _utf8("New Project"))
return true;
else if (snapshot.name == _utf8("Reset Project"))
return true;
else if (boost::starts_with(snapshot.name, _utf8("Load Project:")))
return true;
else if (boost::starts_with(snapshot.name, _utf8("Selection")))
return true;
else if (boost::starts_with(snapshot.name, _utf8("Entering"))) {
if (last_save_main != snapshot.timestamp + 1 && !is_gizmo_with_modifications(snapshot))
return true;
}
else if (boost::starts_with(snapshot.name, _utf8("Leaving"))) {
if (last_save_main != snapshot.timestamp && !gizmos.is_used_and_modified(snapshot))
return true;
}
return false;
};
// returns true if the given snapshot is not saveable
auto skip_gizmo = [](const UndoRedo::Snapshot& snapshot) {
// put here any needed condition to skip the snapshot
return false;
};
const UndoRedo::Snapshot* curr = get_active_snapshot(stack);
const std::vector<UndoRedo::Snapshot>& snapshots = stack.snapshots();
size_t shift = 1;
while (curr->timestamp > 0 && ((type == EStackType::Main && skip_main(*curr)) || (type == EStackType::Gizmo && skip_gizmo(*curr)))) {
const UndoRedo::Snapshot* temp = curr;
curr = &(*std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(curr->timestamp - shift)));
shift = (curr == temp) ? shift + 1 : 1;
}
if (type == EStackType::Main) {
if (boost::starts_with(curr->name, _utf8("Entering")) && last_save_main == curr->timestamp + 1) {
std::string topmost_redo;
wxGetApp().plater()->undo_redo_topmost_string_getter(false, topmost_redo);
if (boost::starts_with(topmost_redo, _utf8("Leaving")))
curr = &(*std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(curr->timestamp + 1)));
}
}
return curr->timestamp > 0 ? curr : nullptr;
}
// returns the name of the gizmo contained in the given string
static std::string extract_gizmo_name(const std::string& s) {
static const std::array<std::string, 2> prefixes = { _utf8("Entering"), _utf8("Leaving") };
std::string ret;
for (const std::string& prefix : prefixes) {
if (boost::starts_with(s, prefix))
ret = s.substr(prefix.length() + 1);
if (!ret.empty())
break;
}
return ret;
}
void ProjectDirtyStateManager::DirtyState::Gizmos::add_used(const UndoRedo::Snapshot& snapshot)
{
const std::string name = extract_gizmo_name(snapshot.name);
auto it = used.find(name);
if (it == used.end())
it = used.insert({ name, { {} } }).first;
it->second.modified_timestamps.push_back(snapshot.timestamp);
}
void ProjectDirtyStateManager::DirtyState::Gizmos::remove_obsolete_used(const Slic3r::UndoRedo::Stack& main_stack)
{
const std::vector<UndoRedo::Snapshot>& snapshots = main_stack.snapshots();
for (auto& item : used) {
auto it = item.second.modified_timestamps.begin();
while (it != item.second.modified_timestamps.end()) {
size_t timestamp = *it;
auto snapshot_it = std::find_if(snapshots.begin(), snapshots.end(), [timestamp](const Slic3r::UndoRedo::Snapshot& snapshot) { return snapshot.timestamp == timestamp; });
if (snapshot_it == snapshots.end())
it = item.second.modified_timestamps.erase(it);
else
++it;
}
}
}
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
bool ProjectDirtyStateManager::DirtyState::Gizmos::any_used_modified() const
{
for (auto& [name, gizmo] : used) {
if (!gizmo.modified_timestamps.empty())
return true;
}
return false;
}
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
// returns true if the given snapshot is contained in any of the gizmos caches
bool ProjectDirtyStateManager::DirtyState::Gizmos::is_used_and_modified(const UndoRedo::Snapshot& snapshot) const
{
for (const auto& item : used) {
for (size_t i : item.second.modified_timestamps) {
if (i == snapshot.timestamp)
return true;
}
}
return false;
}
void ProjectDirtyStateManager::DirtyState::Gizmos::reset()
{
used.clear();
}
void ProjectDirtyStateManager::update_from_undo_redo_stack(UpdateType type)
{
if (!wxGetApp().initialized())
return;
const Plater* plater = wxGetApp().plater();
const UndoRedo::Stack& main_stack = plater->undo_redo_stack_main();
const UndoRedo::Stack& active_stack = plater->undo_redo_stack_active();
if (&main_stack == &active_stack)
update_from_undo_redo_main_stack(type, main_stack);
else
update_from_undo_redo_gizmo_stack(type, active_stack);
wxGetApp().mainframe->update_title();
}
void ProjectDirtyStateManager::update_from_presets()
{
m_state.presets = false;
std::vector<std::pair<unsigned int, std::string>> selected_presets = wxGetApp().get_selected_presets();
for (const auto& [type, name] : selected_presets) {
m_state.presets |= !m_initial_presets[type].empty() && m_initial_presets[type] != name;
}
m_state.presets |= wxGetApp().has_unsaved_preset_changes();
wxGetApp().mainframe->update_title();
}
void ProjectDirtyStateManager::reset_after_save()
{
const Plater* plater = wxGetApp().plater();
const UndoRedo::Stack& main_stack = plater->undo_redo_stack_main();
const UndoRedo::Stack& active_stack = plater->undo_redo_stack_active();
if (&main_stack == &active_stack) {
const UndoRedo::Snapshot* saveable_snapshot = get_last_saveable_snapshot(EStackType::Main, main_stack, m_state.gizmos, m_last_save.main);
m_last_save.main = (saveable_snapshot != nullptr) ? saveable_snapshot->timestamp : 0;
}
else {
const UndoRedo::Snapshot* main_active_snapshot = get_active_snapshot(main_stack);
if (boost::starts_with(main_active_snapshot->name, _utf8("Entering"))) {
if (m_state.gizmos.current)
m_last_save.main = main_active_snapshot->timestamp + 1;
}
const UndoRedo::Snapshot* saveable_snapshot = get_last_saveable_snapshot(EStackType::Gizmo, active_stack, m_state.gizmos, m_last_save.main);
m_last_save.gizmo = saveable_snapshot->timestamp;
}
reset_initial_presets();
m_state.reset();
wxGetApp().mainframe->update_title();
}
void ProjectDirtyStateManager::reset_initial_presets()
{
m_initial_presets = std::array<std::string, Preset::TYPE_COUNT>();
std::vector<std::pair<unsigned int, std::string>> selected_presets = wxGetApp().get_selected_presets();
for (const auto& [type, name] : selected_presets) {
m_initial_presets[type] = name;
}
}
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
void ProjectDirtyStateManager::render_debug_window() const
{
ImGuiWrapper& imgui = *wxGetApp().imgui();
auto color = [](bool value) {
return value ? ImVec4(1.0f, 0.49f, 0.216f, 1.0f) /* orange */: ImVec4(1.0f, 1.0f, 1.0f, 1.0f) /* white */;
};
auto bool_to_text = [](bool value) {
return value ? "true" : "false";
};
auto append_bool_item = [color, bool_to_text, &imgui](const std::string& name, bool value) {
imgui.text_colored(color(value), name);
ImGui::SameLine();
imgui.text_colored(color(value), bool_to_text(value));
};
auto append_int_item = [&imgui](const std::string& name, int value) {
imgui.text(name);
ImGui::SameLine();
imgui.text(std::to_string(value));
};
auto append_snapshot_item = [&imgui](const std::string& label, const UndoRedo::Snapshot* snapshot) {
imgui.text(label);
ImGui::SameLine(100);
if (snapshot != nullptr)
imgui.text(snapshot->name + " (" + std::to_string(snapshot->timestamp) + ")");
else
imgui.text("-");
};
imgui.begin(std::string("Project dirty state statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
if (ImGui::CollapsingHeader("Dirty state", ImGuiTreeNodeFlags_DefaultOpen)) {
append_bool_item("Overall:", is_dirty());
ImGui::Separator();
append_bool_item("Plater:", m_state.plater);
append_bool_item("Presets:", m_state.presets);
append_bool_item("Current gizmo:", m_state.gizmos.current);
}
if (ImGui::CollapsingHeader("Last save timestamps", ImGuiTreeNodeFlags_DefaultOpen)) {
append_int_item("Main:", m_last_save.main);
append_int_item("Current gizmo:", m_last_save.gizmo);
}
const UndoRedo::Stack& main_stack = wxGetApp().plater()->undo_redo_stack_main();
const UndoRedo::Snapshot* main_active_snapshot = get_active_snapshot(main_stack);
const UndoRedo::Snapshot* main_last_saveable_snapshot = get_last_saveable_snapshot(EStackType::Main, main_stack, m_state.gizmos, m_last_save.main);
const std::vector<UndoRedo::Snapshot>& main_snapshots = main_stack.snapshots();
if (ImGui::CollapsingHeader("Main snapshots", ImGuiTreeNodeFlags_DefaultOpen)) {
append_snapshot_item("Active:", main_active_snapshot);
append_snapshot_item("Last saveable:", main_last_saveable_snapshot);
}
if (ImGui::CollapsingHeader("Main undo/redo stack", ImGuiTreeNodeFlags_DefaultOpen)) {
for (const UndoRedo::Snapshot& snapshot : main_snapshots) {
bool active = main_active_snapshot->timestamp == snapshot.timestamp;
imgui.text_colored(color(active), snapshot.name);
ImGui::SameLine(150);
imgui.text_colored(color(active), " (" + std::to_string(snapshot.timestamp) + ")");
if (&snapshot == main_last_saveable_snapshot) {
ImGui::SameLine();
imgui.text_colored(color(active), " (S)");
}
if (m_last_save.main > 0 && m_last_save.main == snapshot.timestamp) {
ImGui::SameLine();
imgui.text_colored(color(active), " (LS)");
}
}
}
const UndoRedo::Stack& active_stack = wxGetApp().plater()->undo_redo_stack_active();
if (&active_stack != &main_stack) {
if (ImGui::CollapsingHeader("Gizmo undo/redo stack", ImGuiTreeNodeFlags_DefaultOpen)) {
const UndoRedo::Snapshot* active_active_snapshot = get_active_snapshot(active_stack);
const std::vector<UndoRedo::Snapshot>& active_snapshots = active_stack.snapshots();
for (const UndoRedo::Snapshot& snapshot : active_snapshots) {
bool active = active_active_snapshot->timestamp == snapshot.timestamp;
imgui.text_colored(color(active), snapshot.name);
ImGui::SameLine(150);
imgui.text_colored(color(active), " (" + std::to_string(snapshot.timestamp) + ")");
}
}
}
if (m_state.gizmos.any_used_modified()) {
if (ImGui::CollapsingHeader("Gizmos", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(10.0f);
for (const auto& [name, gizmo] : m_state.gizmos.used) {
if (!gizmo.modified_timestamps.empty()) {
if (ImGui::CollapsingHeader(name.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
std::string modified_timestamps;
for (size_t i = 0; i < gizmo.modified_timestamps.size(); ++i) {
if (i > 0)
modified_timestamps += " | ";
modified_timestamps += std::to_string(gizmo.modified_timestamps[i]);
}
imgui.text(modified_timestamps);
}
}
}
ImGui::Unindent(10.0f);
}
}
imgui.end();
}
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
void ProjectDirtyStateManager::update_from_undo_redo_main_stack(UpdateType type, const Slic3r::UndoRedo::Stack& stack)
{
m_state.plater = false;
if (type == UpdateType::TakeSnapshot) {
if (m_last_save.main != 0) {
const std::vector<UndoRedo::Snapshot>& snapshots = stack.snapshots();
auto snapshot_it = std::find_if(snapshots.begin(), snapshots.end(), [this](const Slic3r::UndoRedo::Snapshot& snapshot) { return snapshot.timestamp == m_last_save.main; });
if (snapshot_it == snapshots.end())
m_last_save.main = 0;
}
m_state.gizmos.remove_obsolete_used(stack);
}
const UndoRedo::Snapshot* active_snapshot = get_active_snapshot(stack);
if (active_snapshot->name == _utf8("New Project") ||
active_snapshot->name == _utf8("Reset Project") ||
boost::starts_with(active_snapshot->name, _utf8("Load Project:")))
return;
if (boost::starts_with(active_snapshot->name, _utf8("Entering"))) {
if (type == UpdateType::UndoRedoTo) {
std::string topmost_redo;
wxGetApp().plater()->undo_redo_topmost_string_getter(false, topmost_redo);
if (boost::starts_with(topmost_redo, _utf8("Leaving"))) {
const std::vector<UndoRedo::Snapshot>& snapshots = stack.snapshots();
const UndoRedo::Snapshot* leaving_snapshot = &(*std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(active_snapshot->timestamp + 1)));
if (m_state.gizmos.is_used_and_modified(*leaving_snapshot)) {
m_state.plater = (leaving_snapshot != nullptr && leaving_snapshot->timestamp != m_last_save.main);
return;
}
}
}
m_state.gizmos.current = false;
m_last_save.gizmo = 0;
}
else if (boost::starts_with(active_snapshot->name, _utf8("Leaving"))) {
if (m_state.gizmos.current)
m_state.gizmos.add_used(*active_snapshot);
m_state.gizmos.current = false;
m_last_save.gizmo = 0;
}
const UndoRedo::Snapshot* last_saveable_snapshot = get_last_saveable_snapshot(EStackType::Main, stack, m_state.gizmos, m_last_save.main);
m_state.plater = (last_saveable_snapshot != nullptr && last_saveable_snapshot->timestamp != m_last_save.main);
}
void ProjectDirtyStateManager::update_from_undo_redo_gizmo_stack(UpdateType type, const Slic3r::UndoRedo::Stack& stack)
{
m_state.gizmos.current = false;
const UndoRedo::Snapshot* active_snapshot = get_active_snapshot(stack);
if (active_snapshot->name == "Gizmos-Initial")
return;
const UndoRedo::Snapshot* last_saveable_snapshot = get_last_saveable_snapshot(EStackType::Gizmo, stack, m_state.gizmos, m_last_save.main);
m_state.gizmos.current = (last_saveable_snapshot != nullptr && last_saveable_snapshot->timestamp != m_last_save.gizmo);
}
} // namespace GUI
} // namespace Slic3r
#endif // ENABLE_PROJECT_DIRTY_STATE

View file

@ -0,0 +1,96 @@
#ifndef slic3r_ProjectDirtyStateManager_hpp_
#define slic3r_ProjectDirtyStateManager_hpp_
#include "libslic3r/Preset.hpp"
#if ENABLE_PROJECT_DIRTY_STATE
namespace Slic3r {
namespace UndoRedo {
class Stack;
struct Snapshot;
} // namespace UndoRedo
namespace GUI {
class ProjectDirtyStateManager
{
public:
enum class UpdateType : unsigned char
{
TakeSnapshot,
UndoRedoTo
};
struct DirtyState
{
struct Gizmos
{
struct Gizmo
{
std::vector<size_t> modified_timestamps;
};
bool current{ false };
std::map<std::string, Gizmo> used;
void add_used(const UndoRedo::Snapshot& snapshot);
void remove_obsolete_used(const Slic3r::UndoRedo::Stack& main_stack);
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
bool any_used_modified() const;
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
bool is_used_and_modified(const UndoRedo::Snapshot& snapshot) const;
void reset();
};
bool plater{ false };
bool presets{ false };
Gizmos gizmos;
bool is_dirty() const { return plater || presets || gizmos.current; }
void reset() {
plater = false;
presets = false;
gizmos.current = false;
}
};
private:
struct LastSaveTimestamps
{
size_t main{ 0 };
size_t gizmo{ 0 };
void reset() {
main = 0;
gizmo = 0;
}
};
DirtyState m_state;
LastSaveTimestamps m_last_save;
// keeps track of initial selected presets
std::array<std::string, Preset::TYPE_COUNT> m_initial_presets;
public:
bool is_dirty() const { return m_state.is_dirty(); }
void update_from_undo_redo_stack(UpdateType type);
void update_from_presets();
void reset_after_save();
void reset_initial_presets();
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
void render_debug_window() const;
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
private:
void update_from_undo_redo_main_stack(UpdateType type, const Slic3r::UndoRedo::Stack& stack);
void update_from_undo_redo_gizmo_stack(UpdateType type, const Slic3r::UndoRedo::Stack& stack);
};
} // namespace GUI
} // namespace Slic3r
#endif // ENABLE_PROJECT_DIRTY_STATE
#endif // slic3r_ProjectDirtyStateManager_hpp_

View file

@ -1178,7 +1178,7 @@ void Selection::render_center(bool gizmo_is_dragging) const
if (!m_valid || is_empty() || m_quadric == nullptr)
return;
Vec3d center = gizmo_is_dragging ? m_cache.dragging_center : get_bounding_box().center();
const Vec3d center = gizmo_is_dragging ? m_cache.dragging_center : get_bounding_box().center();
glsafe(::glDisable(GL_DEPTH_TEST));
@ -1247,7 +1247,7 @@ void Selection::render_sidebar_hints(const std::string& sidebar_field) const
} else {
glsafe(::glTranslated(center(0), center(1), center(2)));
if (requires_local_axes()) {
Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true);
const Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true);
glsafe(::glMultMatrixd(orient_matrix.data()));
}
}
@ -1935,7 +1935,7 @@ void Selection::render_sidebar_layers_hints(const std::string& sidebar_field) co
if (pos == std::string::npos)
return;
double min_z = std::stod(field.substr(pos + 1));
const double min_z = std::stod(field.substr(pos + 1));
// extract type
field = field.substr(0, pos);
@ -1943,7 +1943,7 @@ void Selection::render_sidebar_layers_hints(const std::string& sidebar_field) co
if (pos == std::string::npos)
return;
int type = std::stoi(field.substr(pos + 1));
const int type = std::stoi(field.substr(pos + 1));
const BoundingBoxf3& box = get_bounding_box();
@ -1954,8 +1954,8 @@ void Selection::render_sidebar_layers_hints(const std::string& sidebar_field) co
// view dependend order of rendering to keep correct transparency
bool camera_on_top = wxGetApp().plater()->get_camera().is_looking_downward();
float z1 = camera_on_top ? min_z : max_z;
float z2 = camera_on_top ? max_z : min_z;
const float z1 = camera_on_top ? min_z : max_z;
const float z2 = camera_on_top ? max_z : min_z;
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDisable(GL_CULL_FACE));

View file

@ -940,7 +940,7 @@ void Tab::update_visibility()
page->update_visibility(m_mode, page.get() == m_active_page);
rebuild_page_tree();
if (this->m_type == Preset::TYPE_SLA_PRINT)
if (m_type == Preset::TYPE_SLA_PRINT)
update_description_lines();
Layout();
@ -1217,9 +1217,8 @@ void Tab::apply_config_from_cache()
// to update number of "filament" selection boxes when the number of extruders change.
void Tab::on_presets_changed()
{
if (wxGetApp().plater() == nullptr) {
if (wxGetApp().plater() == nullptr)
return;
}
// Instead of PostEvent (EVT_TAB_PRESETS_CHANGED) just call update_presets
wxGetApp().plater()->sidebar().update_presets(m_type);
@ -1237,6 +1236,10 @@ void Tab::on_presets_changed()
// clear m_dependent_tabs after first update from select_preset()
// to avoid needless preset loading from update() function
m_dependent_tabs.clear();
#if ENABLE_PROJECT_DIRTY_STATE
wxGetApp().plater()->update_project_dirty_from_presets();
#endif // ENABLE_PROJECT_DIRTY_STATE
}
void Tab::build_preset_description_line(ConfigOptionsGroup* optgroup)
@ -2113,10 +2116,16 @@ wxSizer* Tab::description_line_widget(wxWindow* parent, ogStaticText* *StaticTex
return sizer;
}
#if ENABLE_PROJECT_DIRTY_STATE
bool Tab::saved_preset_is_dirty() const { return m_presets->saved_is_dirty(); }
void Tab::update_saved_preset_from_current_preset() { m_presets->update_saved_preset_from_current_preset(); }
bool Tab::current_preset_is_dirty() const { return m_presets->current_is_dirty(); }
#else
bool Tab::current_preset_is_dirty()
{
return m_presets->current_is_dirty();
}
#endif // ENABLE_PROJECT_DIRTY_STATE
void TabPrinter::build()
{
@ -3141,8 +3150,8 @@ void Tab::select_preset(std::string preset_name, bool delete_current /*=false*/,
if (preset_name.empty()) {
if (delete_current) {
// Find an alternate preset to be selected after the current preset is deleted.
const std::deque<Preset> &presets = this->m_presets->get_presets();
size_t idx_current = this->m_presets->get_idx_selected();
const std::deque<Preset> &presets = m_presets->get_presets();
size_t idx_current = m_presets->get_idx_selected();
// Find the next visible preset.
size_t idx_new = idx_current + 1;
if (idx_new < presets.size())

View file

@ -270,7 +270,11 @@ public:
Preset::Type type() const { return m_type; }
// The tab is already constructed.
bool completed() const { return m_completed; }
virtual bool supports_printer_technology(const PrinterTechnology tech) = 0;
#if ENABLE_PROJECT_DIRTY_STATE
virtual bool supports_printer_technology(const PrinterTechnology tech) const = 0;
#else
virtual bool supports_printer_technology(const PrinterTechnology tech) = 0;
#endif // ENABLE_PROJECT_DIRTY_STATE
void create_preset_tab();
void add_scaled_button(wxWindow* parent, ScalableButton** btn, const std::string& icon_name,
@ -333,7 +337,13 @@ public:
Field* get_field(const t_config_option_key &opt_key, Page** selected_page, int opt_index = -1);
void toggle_option(const std::string& opt_key, bool toggle, int opt_index = -1);
wxSizer* description_line_widget(wxWindow* parent, ogStaticText** StaticText, wxString text = wxEmptyString);
#if ENABLE_PROJECT_DIRTY_STATE
bool current_preset_is_dirty() const;
bool saved_preset_is_dirty() const;
void update_saved_preset_from_current_preset();
#else
bool current_preset_is_dirty();
#endif // ENABLE_PROJECT_DIRTY_STATE
DynamicPrintConfig* get_config() { return m_config; }
PresetCollection* get_presets() { return m_presets; }
@ -377,8 +387,8 @@ class TabPrint : public Tab
{
public:
TabPrint(wxNotebook* parent) :
// Tab(parent, _(L("Print Settings")), L("print")) {}
Tab(parent, _(L("Print Settings")), Slic3r::Preset::TYPE_PRINT) {}
// Tab(parent, _L("Print Settings"), L("print")) {}
Tab(parent, _L("Print Settings"), Slic3r::Preset::TYPE_PRINT) {}
~TabPrint() {}
void build() override;
@ -387,7 +397,11 @@ public:
void toggle_options() override;
void update() override;
void clear_pages() override;
bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptFFF; }
#if ENABLE_PROJECT_DIRTY_STATE
bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; }
#else
bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptFFF; }
#endif // ENABLE_PROJECT_DIRTY_STATE
private:
ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr;
@ -407,8 +421,8 @@ private:
std::map<std::string, wxCheckBox*> m_overrides_options;
public:
TabFilament(wxNotebook* parent) :
// Tab(parent, _(L("Filament Settings")), L("filament")) {}
Tab(parent, _(L("Filament Settings")), Slic3r::Preset::TYPE_FILAMENT) {}
// Tab(parent, _L("Filament Settings"), L("filament")) {}
Tab(parent, _L("Filament Settings"), Slic3r::Preset::TYPE_FILAMENT) {}
~TabFilament() {}
void build() override;
@ -417,7 +431,11 @@ public:
void toggle_options() override;
void update() override;
void clear_pages() override;
bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptFFF; }
#if ENABLE_PROJECT_DIRTY_STATE
bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; }
#else
bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptFFF; }
#endif // ENABLE_PROJECT_DIRTY_STATE
};
class TabPrinter : public Tab
@ -450,7 +468,7 @@ public:
// TabPrinter(wxNotebook* parent) : Tab(parent, _(L("Printer Settings")), L("printer")) {}
TabPrinter(wxNotebook* parent) :
Tab(parent, _(L("Printer Settings")), Slic3r::Preset::TYPE_PRINTER) {}
Tab(parent, _L("Printer Settings"), Slic3r::Preset::TYPE_PRINTER) {}
~TabPrinter() {}
void build() override;
@ -472,7 +490,11 @@ public:
void init_options_list() override;
void msw_rescale() override;
void sys_color_changed() override;
bool supports_printer_technology(const PrinterTechnology /* tech */) override { return true; }
#if ENABLE_PROJECT_DIRTY_STATE
bool supports_printer_technology(const PrinterTechnology /* tech */) const override { return true; }
#else
bool supports_printer_technology(const PrinterTechnology /* tech */) override { return true; }
#endif // ENABLE_PROJECT_DIRTY_STATE
wxSizer* create_bed_shape_widget(wxWindow* parent);
void cache_extruder_cnt();
@ -483,8 +505,8 @@ class TabSLAMaterial : public Tab
{
public:
TabSLAMaterial(wxNotebook* parent) :
// Tab(parent, _(L("Material Settings")), L("sla_material")) {}
Tab(parent, _(L("Material Settings")), Slic3r::Preset::TYPE_SLA_MATERIAL) {}
// Tab(parent, _L("Material Settings"), L("sla_material")) {}
Tab(parent, _L("Material Settings"), Slic3r::Preset::TYPE_SLA_MATERIAL) {}
~TabSLAMaterial() {}
void build() override;
@ -492,15 +514,19 @@ public:
void toggle_options() override {};
void update() override;
void init_options_list() override;
bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptSLA; }
#if ENABLE_PROJECT_DIRTY_STATE
bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptSLA; }
#else
bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptSLA; }
#endif // ENABLE_PROJECT_DIRTY_STATE
};
class TabSLAPrint : public Tab
{
public:
TabSLAPrint(wxNotebook* parent) :
// Tab(parent, _(L("Print Settings")), L("sla_print")) {}
Tab(parent, _(L("Print Settings")), Slic3r::Preset::TYPE_SLA_PRINT) {}
// Tab(parent, _L("Print Settings"), L("sla_print")) {}
Tab(parent, _L("Print Settings"), Slic3r::Preset::TYPE_SLA_PRINT) {}
~TabSLAPrint() {}
ogStaticText* m_support_object_elevation_description_line = nullptr;
@ -511,7 +537,11 @@ public:
void toggle_options() override;
void update() override;
void clear_pages() override;
bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptSLA; }
#if ENABLE_PROJECT_DIRTY_STATE
bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptSLA; }
#else
bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptSLA; }
#endif // ENABLE_PROJECT_DIRTY_STATE
};
} // GUI

View file

@ -1694,6 +1694,9 @@ void DiffPresetDialog::update_compatibility(const std::string& preset_name, Pres
technology_changed = old_printer_technology != new_printer_technology;
}
// select preset
presets->select_preset_by_name(preset_name, false);
// Mark the print & filament enabled if they are compatible with the currently selected preset.
// The following method should not discard changes of current print or filament presets on change of a printer profile,
// if they are compatible with the current printer.