From 3fcf194e39e93d36bc164acffbf7f4ad2c67fb6f Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Tue, 13 Mar 2018 12:39:57 +0100 Subject: [PATCH 01/38] ConfigWizard: Basic structure / WIP --- lib/Slic3r/GUI.pm | 20 +- lib/Slic3r/GUI/MainFrame.pm | 8 +- resources/icons/printers/MK2S.png | Bin 0 -> 58794 bytes resources/icons/printers/MK2SMM.png | Bin 0 -> 57081 bytes resources/icons/printers/MK3.png | Bin 0 -> 64194 bytes resources/profiles/PrusaResearch.ini | 8 +- xs/CMakeLists.txt | 5 + xs/src/libslic3r/Utils.hpp | 2 + xs/src/slic3r/GUI/AppConfig.cpp | 2 +- xs/src/slic3r/GUI/ConfigWizard.cpp | 433 +++++++++++++++++++++ xs/src/slic3r/GUI/ConfigWizard.hpp | 38 ++ xs/src/slic3r/GUI/ConfigWizard_private.hpp | 162 ++++++++ xs/src/slic3r/GUI/GUI.cpp | 15 + xs/src/slic3r/GUI/GUI.hpp | 4 + xs/src/slic3r/GUI/Preferences.cpp | 1 + xs/src/slic3r/GUI/Preset.hpp | 9 +- xs/src/slic3r/GUI/PresetBundle.cpp | 31 +- xs/src/slic3r/GUI/PresetBundle.hpp | 1 + xs/src/slic3r/Utils/PresetUpdate.cpp | 104 +++++ xs/src/slic3r/Utils/PresetUpdate.hpp | 38 ++ xs/xsp/GUI.xsp | 3 + xs/xsp/Utils_PresetUpdate.xsp | 18 + xs/xsp/my.map | 3 + 23 files changed, 881 insertions(+), 24 deletions(-) create mode 100644 resources/icons/printers/MK2S.png create mode 100644 resources/icons/printers/MK2SMM.png create mode 100644 resources/icons/printers/MK3.png create mode 100644 xs/src/slic3r/GUI/ConfigWizard.cpp create mode 100644 xs/src/slic3r/GUI/ConfigWizard.hpp create mode 100644 xs/src/slic3r/GUI/ConfigWizard_private.hpp create mode 100644 xs/src/slic3r/Utils/PresetUpdate.cpp create mode 100644 xs/src/slic3r/Utils/PresetUpdate.hpp create mode 100644 xs/xsp/Utils_PresetUpdate.xsp diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 4ec388c14..0c5e87429 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -101,6 +101,8 @@ sub OnInit { $self->{app_config}->set('version', $Slic3r::VERSION); $self->{app_config}->save; + my $slic3r_update_avail = $self->{app_config}->get("version_check") && $self->{app_config}->get("version_online") != $Slic3r::VERSION; + Slic3r::GUI::set_app_config($self->{app_config}); Slic3r::GUI::load_language(); @@ -111,6 +113,7 @@ sub OnInit { warn $@ . "\n"; show_error(undef, $@); } + # TODO: check previously downloaded updates $run_wizard = 1 if $self->{preset_bundle}->has_defauls_only; Slic3r::GUI::set_preset_bundle($self->{preset_bundle}); @@ -134,14 +137,19 @@ sub OnInit { $self->{app_config}->save if $self->{app_config}->dirty; }); - if ($run_wizard) { - # On OSX the UI was not initialized correctly if the wizard was called - # before the UI was up and running. - $self->CallAfter(sub { + # On OSX the UI was not initialized correctly if the wizard was called + # before the UI was up and running. + $self->CallAfter(sub { + if ($slic3r_update_avail) { + # TODO + } elsif ($run_wizard) { # Run the config wizard, don't offer the "reset user profile" checkbox. $self->{mainframe}->config_wizard(1); - }); - } + } + + # XXX: recreate_GUI ??? + Slic3r::PresetUpdater::download($self->{app_config}, $self->{preset_bundle}); + }); # The following event is emited by the C++ menu implementation of application language change. EVT_COMMAND($self, -1, $LANGUAGE_CHANGE_EVENT, sub{ diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index b2f51b9e1..68fc96339 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -95,7 +95,7 @@ sub new { }); $self->update_ui_from_settings; - + return $self; } @@ -643,6 +643,12 @@ sub config_wizard { my ($self, $fresh_start) = @_; # Exit wizard if there are unsaved changes and the user cancels the action. return unless $self->check_unsaved_changes; + + + # TODO: Offer "reset user profile" + Slic3r::GUI::open_config_wizard(); + return; + # Enumerate the profiles bundled with the Slic3r installation under resources/profiles. my $directory = Slic3r::resources_dir() . "/profiles"; my @profiles = (); diff --git a/resources/icons/printers/MK2S.png b/resources/icons/printers/MK2S.png new file mode 100644 index 0000000000000000000000000000000000000000..925447cf201911308d2a4788e6a0f8fd6f9db2c6 GIT binary patch literal 58794 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 DH{Xdt literal 0 HcmV?d00001 diff --git a/resources/icons/printers/MK2SMM.png b/resources/icons/printers/MK2SMM.png new file mode 100644 index 0000000000000000000000000000000000000000..d6ff161259d9752504ad959c87d0e8529ad97541 GIT binary patch literal 57081 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 literal 0 HcmV?d00001 diff --git a/resources/icons/printers/MK3.png b/resources/icons/printers/MK3.png new file mode 100644 index 0000000000000000000000000000000000000000..5279ba01e060d81738565c5fd99c11e06fbef540 GIT binary patch literal 64194 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�WrFjD4RdqUre@ogt*oytQM-njETd@mVx1yo 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

H`@`RyGv2V%cRm zB3AgRoS99FaA*JaJF3;LSMN3m=6GJltr65d8uPZ+g6jrBL}C8c*uw3>bL(4p&%yn* z1L!=|>Nlac8C>4j2 zL@Kf(2V*EpivaRK4Zkrq&<;=-W7uVvy}0;-3pn(tb!=~fSG@d1Ty?`NN4)smEEyf; zU2i#x-(2~h{PLGqKJt=d=iPVZ7eD{`k!h{QOs~7?qyPTyiNC0;6h)m#Ya%7-&UW>7 zJ8nys+fpe*mSwc+O`JD4Z;7IShBz0z|5T)rGHBkp3d+;96ql*WcDD<*Q!(p&@F*O( z!RqQ8b`13V<`5Ii$>@fKi^Jyu0u(cj{&P?K?a&qLKAx=DzQbl#^&pT30Wv>8TLi

#>L=X2CcPG;rmSx$cc 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

mM0xmn2E#!U}l&p`N$PZaZ@8&36buIP20?_pDBU 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 literal 0 HcmV?d00001 diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index fa4d48cda..0a1d26eaa 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -7,21 +7,25 @@ name = Prusa Research # This means, the server may force the Slic3r configuration to be downgraded. config_version = 0.1 # Where to get the updates from? -config_update_url = https://raw.githubusercontent.com/prusa3d/Slic3r-settings/master/live/PrusaResearch.ini +# TODO: proper URL +# config_update_url = https://raw.githubusercontent.com/prusa3d/Slic3r-settings/master/live/PrusaResearch.ini +config_update_url = https://gist.githubusercontent.com/vojtechkral/4d8fd4a3b8699a01ec892c264178461c/raw/e9187c3e15ceaf1a90f29b7c43cf3ccc746140f0/PrusaResearch.ini # The printer models will be shown by the Configuration Wizard in this order, # also the first model installed & the first nozzle installed will be activated after install. #TODO: One day we may differentiate variants of the nozzles / hot ends, #for example by the melt zone size, or whether the nozzle is hardened. [printer_model:MK3] +name = Original Prusa i3 MK3 variants = 0.4; 0.25; 0.6 [printer_model:MK2S] +name = Original Prusa i3 MK2S variants = 0.4; 0.25; 0.6 [printer_model:MK2SMM] # Printer model name will be shown by the installation wizard. -name = MK2S Multi Material +name = Original Prusa i3 MK2SMM variants = 0.4; 0.6 # All presets starting with asterisk, for example *common*, are intermediate and they will diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt index f28db8f92..e73c8429a 100644 --- a/xs/CMakeLists.txt +++ b/xs/CMakeLists.txt @@ -205,12 +205,16 @@ add_library(libslic3r_gui STATIC ${LIBDIR}/slic3r/GUI/BonjourDialog.hpp ${LIBDIR}/slic3r/Utils/ASCIIFolding.cpp ${LIBDIR}/slic3r/Utils/ASCIIFolding.hpp + ${LIBDIR}/slic3r/GUI/ConfigWizard.cpp + ${LIBDIR}/slic3r/GUI/ConfigWizard.hpp ${LIBDIR}/slic3r/Utils/Http.cpp ${LIBDIR}/slic3r/Utils/Http.hpp ${LIBDIR}/slic3r/Utils/OctoPrint.cpp ${LIBDIR}/slic3r/Utils/OctoPrint.hpp ${LIBDIR}/slic3r/Utils/Bonjour.cpp ${LIBDIR}/slic3r/Utils/Bonjour.hpp + ${LIBDIR}/slic3r/Utils/PresetUpdate.cpp + ${LIBDIR}/slic3r/Utils/PresetUpdate.hpp ) add_library(admesh STATIC @@ -356,6 +360,7 @@ set(XS_XSP_FILES ${XSP_DIR}/SurfaceCollection.xsp ${XSP_DIR}/TriangleMesh.xsp ${XSP_DIR}/Utils_OctoPrint.xsp + ${XSP_DIR}/Utils_PresetUpdate.xsp ${XSP_DIR}/XS.xsp ) foreach (file ${XS_XSP_FILES}) diff --git a/xs/src/libslic3r/Utils.hpp b/xs/src/libslic3r/Utils.hpp index 27e7fad6b..7e2fe3180 100644 --- a/xs/src/libslic3r/Utils.hpp +++ b/xs/src/libslic3r/Utils.hpp @@ -3,6 +3,8 @@ #include +#include "libslic3r.h" + namespace Slic3r { extern void set_logging_level(unsigned int level); diff --git a/xs/src/slic3r/GUI/AppConfig.cpp b/xs/src/slic3r/GUI/AppConfig.cpp index e32b645b4..4bdf88dda 100644 --- a/xs/src/slic3r/GUI/AppConfig.cpp +++ b/xs/src/slic3r/GUI/AppConfig.cpp @@ -42,7 +42,7 @@ void AppConfig::set_defaults() set("no_defaults", "1"); if (get("show_incompatible_presets").empty()) set("show_incompatible_presets", "0"); - // Version check is enabled by default in the config, but it is not implemented yet. + // Version check is enabled by default in the config, but it is not implemented yet. // XXX if (get("version_check").empty()) set("version_check", "1"); // Use OpenGL 1.1 even if OpenGL 2.0 is available. This is mainly to support some buggy Intel HD Graphics drivers. diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp new file mode 100644 index 000000000..3cad789b6 --- /dev/null +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -0,0 +1,433 @@ +#include "ConfigWizard_private.hpp" + +#include // XXX +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/Utils.hpp" +#include "PresetBundle.hpp" +#include "GUI.hpp" + +namespace fs = boost::filesystem; + +namespace Slic3r { +namespace GUI { + + +// Wizard page base + +ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname) : + wxPanel(parent), + parent(parent), + shortname(std::move(shortname)), + p_prev(nullptr), + p_next(nullptr) +{ + auto *sizer = new wxBoxSizer(wxVERTICAL); + + auto *text = new wxStaticText(this, wxID_ANY, std::move(title), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + auto font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + font.SetWeight(wxFONTWEIGHT_BOLD); + font.SetPointSize(14); + text->SetFont(font); + sizer->Add(text, 0, wxALIGN_LEFT, 0); + sizer->AddSpacer(10); + + content = new wxBoxSizer(wxVERTICAL); + sizer->Add(content, 1); + + SetSizer(sizer); + + this->Hide(); + + Bind(wxEVT_SIZE, [this](wxSizeEvent &event) { + this->Layout(); + event.Skip(); + }); +} + +ConfigWizardPage::~ConfigWizardPage() {} + +ConfigWizardPage* ConfigWizardPage::chain(ConfigWizardPage *page) +{ + if (p_next != nullptr) { p_next->p_prev = nullptr; } + p_next = page; + if (page != nullptr) { + if (page->p_prev != nullptr) { page->p_prev->p_next = nullptr; } + page->p_prev = this; + } + + return page; +} + +void ConfigWizardPage::append_text(wxString text) +{ + auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + widget->Wrap(CONTENT_WIDTH); + widget->SetMinSize(wxSize(CONTENT_WIDTH, -1)); + // content->Add(widget, 1, wxALIGN_LEFT | wxTOP | wxBOTTOM, 10); + content->Add(widget, 0, wxALIGN_LEFT | wxTOP | wxBOTTOM, 10); +} + +void ConfigWizardPage::append_widget(wxWindow *widget, int proportion) +{ + content->Add(widget, proportion, wxEXPAND | wxTOP | wxBOTTOM, 10); +} + +void ConfigWizardPage::append_spacer(int space) +{ + content->AddSpacer(space); +} + +bool ConfigWizardPage::Show(bool show) +{ + if (extra_buttons() != nullptr) { extra_buttons()->Show(show); } + return wxPanel::Show(show); +} + +void ConfigWizardPage::enable_next(bool enable) { parent->p->enable_next(enable); } + + +// Wizard pages + +PageWelcome::PageWelcome(ConfigWizard *parent, const PresetBundle &bundle) : + ConfigWizardPage(parent, _(L("Welcome to the Slic3r Configuration assistant")), _(L("Welcome"))), + others_buttons(new wxPanel(parent)), + variants_checked(0) +{ + append_text(_(L("Hello, welcome to Slic3r Prusa Edition! TODO: This text."))); + + const auto &vendors = bundle.vendors; + const auto vendor_prusa = std::find(vendors.cbegin(), vendors.cend(), VendorProfile("PrusaResearch")); + + // TODO: preload checkiness from app config + + if (vendor_prusa != vendors.cend()) { + const auto &models = vendor_prusa->models; + + auto *printer_picker = new wxPanel(this); + auto *printer_grid = new wxFlexGridSizer(models.size(), 0, 20); + printer_grid->SetFlexibleDirection(wxVERTICAL); + printer_picker->SetSizer(printer_grid); + + auto namefont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + namefont.SetWeight(wxFONTWEIGHT_BOLD); + + for (auto model = models.cbegin(); model != models.cend(); ++model) { + auto *panel = new wxPanel(printer_picker); + auto *sizer = new wxBoxSizer(wxVERTICAL); + panel->SetSizer(sizer); + + auto *title = new wxStaticText(panel, wxID_ANY, model->name, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + title->SetFont(namefont); + sizer->Add(title, 0, wxBOTTOM, 3); + + auto bitmap_file = wxString::Format("printers/%s.png", model->id); + wxBitmap bitmap(GUI::from_u8(Slic3r::var(bitmap_file.ToStdString())), wxBITMAP_TYPE_PNG); + auto *bitmap_widget = new wxStaticBitmap(panel, wxID_ANY, bitmap); + sizer->Add(bitmap_widget, 0, wxBOTTOM, 3); + + sizer->AddSpacer(20); + + for (const auto &variant : model->variants) { + auto *cbox = new wxCheckBox(panel, wxID_ANY, wxString::Format("%s %s %s", variant.name, _(L("mm")), _(L("nozzle")))); + sizer->Add(cbox, 0, wxBOTTOM, 3); + cbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { + this->variants_checked += event.IsChecked() ? 1 : -1; + this->on_variant_checked(); + }); + } + + printer_grid->Add(panel); + } + + append_widget(printer_picker); + } + + { + auto *sizer = new wxBoxSizer(wxHORIZONTAL); + auto *other_vendors = new wxButton(others_buttons, wxID_ANY, _(L("Other vendors"))); + auto *custom_setup = new wxButton(others_buttons, wxID_ANY, _(L("Custom setup"))); + + sizer->Add(other_vendors); + sizer->AddSpacer(BTN_SPACING); + sizer->Add(custom_setup); + + other_vendors->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->wizard_p()->on_other_vendors(); }); + custom_setup->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->wizard_p()->on_custom_setup(); }); + + others_buttons->SetSizer(sizer); + } +} + +void PageWelcome::on_page_set() +{ + chain(wizard_p()->page_update); + on_variant_checked(); +} + +void PageWelcome::on_variant_checked() +{ + enable_next(variants_checked > 0); +} + +PageUpdate::PageUpdate(ConfigWizard *parent) : + ConfigWizardPage(parent, _(L("Automatic updates")), _(L("Updates"))) +{ + append_text(_(L("TODO: text"))); + auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _(L("Check for Slic3r updates"))); + box_slic3r->SetValue(true); + append_widget(box_slic3r); + + append_text(_(L("TODO: text"))); + auto *box_presets = new wxCheckBox(this, wxID_ANY, _(L("Update built-in Presets automatically"))); + box_presets->SetValue(true); + append_widget(box_presets); +} + +void PageUpdate::presets_update_enable(bool enable) +{ + // TODO +} + +PageVendors::PageVendors(ConfigWizard *parent) : + ConfigWizardPage(parent, _(L("Other Vendors")), _(L("Other Vendors"))) +{} + +PageFirmware::PageFirmware(ConfigWizard *parent) : + ConfigWizardPage(parent, _(L("Firmware Type")), _(L("Firmware"))) +{} + +PageBedShape::PageBedShape(ConfigWizard *parent) : + ConfigWizardPage(parent, _(L("Bed Shape and Size")), _(L("Bed Shape"))) +{} + +PageDiameters::PageDiameters(ConfigWizard *parent) : + ConfigWizardPage(parent, _(L("Filament and Nozzle Diameter")), _(L("Print Diameters"))) +{} + +PageTemperatures::PageTemperatures(ConfigWizard *parent) : + ConfigWizardPage(parent, _(L("Bed and Extruder Temperature")), _(L("Temperatures"))) +{} + + +// Index + +ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) : + wxPanel(parent), + bg(GUI::from_u8(Slic3r::var("Slic3r_192px_transparent.png")), wxBITMAP_TYPE_PNG), + bullet_black(GUI::from_u8(Slic3r::var("bullet_black.png")), wxBITMAP_TYPE_PNG), + bullet_blue(GUI::from_u8(Slic3r::var("bullet_blue.png")), wxBITMAP_TYPE_PNG), + bullet_white(GUI::from_u8(Slic3r::var("bullet_white.png")), wxBITMAP_TYPE_PNG) +{ + SetMinSize(bg.GetSize()); + Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); + + wxClientDC dc(this); + text_height = dc.GetCharHeight(); +} + +void ConfigWizardIndex::load_items(ConfigWizardPage *firstpage) +{ + items.clear(); + item_active = items.cend(); + + for (auto *page = firstpage; page != nullptr; page = page->page_next()) { + items.emplace_back(page->shortname); + } + + Refresh(); +} + +void ConfigWizardIndex::set_active(ConfigWizardPage *page) +{ + item_active = std::find(items.cbegin(), items.cend(), page->shortname); + Refresh(); +} + +void ConfigWizardIndex::on_paint(wxPaintEvent & evt) +{ + enum { + MARGIN = 10, + SPACING = 5, + }; + + const auto size = GetClientSize(); + const auto h = size.GetHeight(); + const auto w = size.GetWidth(); + if (h == 0 || w == 0) { return; } + + wxPaintDC dc(this); + dc.DrawBitmap(bg, 0, h - bg.GetHeight(), false); + + const auto bullet_w = bullet_black.GetSize().GetWidth(); + const auto bullet_h = bullet_black.GetSize().GetHeight(); + const int yoff_icon = bullet_h < text_height ? (text_height - bullet_h) / 2 : 0; + const int yoff_text = bullet_h > text_height ? (bullet_h - text_height) / 2 : 0; + const int yinc = std::max(bullet_h, text_height) + SPACING; + + unsigned y = 0; + for (auto it = items.cbegin(); it != items.cend(); ++it) { + if (it < item_active) { dc.DrawBitmap(bullet_black, MARGIN, y + yoff_icon, false); } + if (it == item_active) { dc.DrawBitmap(bullet_blue, MARGIN, y + yoff_icon, false); } + if (it > item_active) { dc.DrawBitmap(bullet_white, MARGIN, y + yoff_icon, false); } + dc.DrawText(*it, MARGIN + bullet_w + SPACING, y + yoff_text); + y += yinc; + } +} + + + +// priv + +void ConfigWizard::priv::index_refresh() +{ + index->load_items(page_welcome); +} + +void ConfigWizard::priv::add_page(ConfigWizardPage *page) +{ + topsizer->Add(page, 0, wxEXPAND); + + auto *extra_buttons = page->extra_buttons(); + if (extra_buttons != nullptr) { + btnsizer->Prepend(extra_buttons, 0); + } +} + +void ConfigWizard::priv::set_page(ConfigWizardPage *page) +{ + if (page == nullptr) { return; } + if (page_current != nullptr) { page_current->Hide(); } + page_current = page; + enable_next(true); + + page->on_page_set(); + index->load_items(page_welcome); + index->set_active(page); + page->Show(); + + btn_prev->Enable(page->page_prev() != nullptr); + btn_next->Show(page->page_next() != nullptr); + btn_finish->Show(page->page_next() == nullptr); + + q->Layout(); +} + +void ConfigWizard::priv::enable_next(bool enable) +{ + btn_next->Enable(enable); + btn_finish->Enable(enable); +} + +void ConfigWizard::priv::on_other_vendors() +{ + page_welcome + ->chain(page_vendors) + ->chain(page_update); + set_page(page_vendors); +} + +void ConfigWizard::priv::on_custom_setup() +{ + page_welcome->chain(page_firmware); + page_temps->chain(page_update); + set_page(page_firmware); +} + +// Public + +ConfigWizard::ConfigWizard(wxWindow *parent, const PresetBundle &bundle) : + wxDialog(parent, wxID_ANY, _(L("Configuration Assistant")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + p(new priv(this)) +{ + p->index = new ConfigWizardIndex(this); + + auto *vsizer = new wxBoxSizer(wxVERTICAL); + p->topsizer = new wxBoxSizer(wxHORIZONTAL); + auto *hline = new wxStaticLine(this); + p->btnsizer = new wxBoxSizer(wxHORIZONTAL); + + p->topsizer->Add(p->index, 0, wxEXPAND); + p->topsizer->AddSpacer(INDEX_MARGIN); + + // TODO: btn labels vs default w/ icons ... use arrows from resources? (no apply icon) + // Also: http://docs.wxwidgets.org/3.0/page_stockitems.html + p->btn_prev = new wxButton(this, wxID_BACKWARD, _(L("< &Back"))); + p->btn_next = new wxButton(this, wxID_FORWARD, _(L("&Next >"))); + p->btn_finish = new wxButton(this, wxID_APPLY, _(L("&Finish"))); + p->btn_cancel = new wxButton(this, wxID_CANCEL); + p->btnsizer->AddStretchSpacer(); + p->btnsizer->Add(p->btn_prev, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_next, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); + + p->add_page(p->page_welcome = new PageWelcome(this, bundle)); + p->add_page(p->page_update = new PageUpdate(this)); + p->add_page(p->page_vendors = new PageVendors(this)); + p->add_page(p->page_firmware = new PageFirmware(this)); + p->add_page(p->page_bed = new PageBedShape(this)); + p->add_page(p->page_diams = new PageDiameters(this)); + p->add_page(p->page_temps = new PageTemperatures(this)); + p->index_refresh(); + + p->page_welcome->chain(p->page_update); + p->page_firmware + ->chain(p->page_bed) + ->chain(p->page_diams) + ->chain(p->page_temps); + + vsizer->Add(p->topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); + vsizer->Add(hline, 0, wxEXPAND); + vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); + + p->set_page(p->page_welcome); + SetSizerAndFit(vsizer); + SetMinSize(GetSize()); + + p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->go_prev(); }); + p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->go_next(); }); +} + +ConfigWizard::~ConfigWizard() {} + +void ConfigWizard::run(wxWindow *parent) +{ + PresetBundle bundle; + + const auto profiles_dir = fs::path(resources_dir()) / "profiles"; + for (fs::directory_iterator it(profiles_dir); it != fs::directory_iterator(); ++it) { + if (it->path().extension() == ".ini") { + bundle.load_configbundle(it->path().native(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); + } + } + + // XXX + for (const auto &vendor : bundle.vendors) { + std::cerr << "vendor: " << vendor.name << std::endl; + std::cerr << " URL: " << vendor.config_update_url << std::endl; + for (const auto &model : vendor.models) { + std::cerr << "\tmodel: " << model.id << " (" << model.name << ")" << std::endl; + for (const auto &variant : model.variants) { + std::cerr << "\t\tvariant: " << variant.name << std::endl; + } + } + } + + ConfigWizard wizard(parent, bundle); + wizard.ShowModal(); +} + + +} +} diff --git a/xs/src/slic3r/GUI/ConfigWizard.hpp b/xs/src/slic3r/GUI/ConfigWizard.hpp new file mode 100644 index 000000000..1b14e29be --- /dev/null +++ b/xs/src/slic3r/GUI/ConfigWizard.hpp @@ -0,0 +1,38 @@ +#ifndef slic3r_ConfigWizard_hpp_ +#define slic3r_ConfigWizard_hpp_ + +#include + +#include + +namespace Slic3r { + +class PresetBundle; + +namespace GUI { + + +class ConfigWizard: public wxDialog +{ +public: + ConfigWizard(wxWindow *parent, const PresetBundle &bundle); + ConfigWizard(ConfigWizard &&) = delete; + ConfigWizard(const ConfigWizard &) = delete; + ConfigWizard &operator=(ConfigWizard &&) = delete; + ConfigWizard &operator=(const ConfigWizard &) = delete; + ~ConfigWizard(); + + static void run(wxWindow *parent); +private: + struct priv; + std::unique_ptr p; + + friend class ConfigWizardPage; +}; + + + +} +} + +#endif diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp new file mode 100644 index 000000000..ba028c0e8 --- /dev/null +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -0,0 +1,162 @@ +#ifndef slic3r_ConfigWizard_private_hpp_ +#define slic3r_ConfigWizard_private_hpp_ + +#include "ConfigWizard.hpp" + +#include + +#include +#include +#include + + +namespace Slic3r { +namespace GUI { + + +enum { + DIALOG_MARGIN = 15, + INDEX_MARGIN = 40, + BTN_SPACING = 10, +}; + +struct ConfigWizardPage: wxPanel +{ + enum { + CONTENT_WIDTH = 500, + }; + + ConfigWizard *parent; + const wxString shortname; + wxBoxSizer *content; + + ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname); + + virtual ~ConfigWizardPage(); + + ConfigWizardPage *page_prev() const { return p_prev; } + ConfigWizardPage *page_next() const { return p_next; } + ConfigWizardPage* chain(ConfigWizardPage *page); + + void append_text(wxString text); + void append_widget(wxWindow *widget, int proportion = 0); + void append_spacer(int space); + + ConfigWizard::priv *wizard_p() const { return parent->p.get(); } + + virtual bool Show(bool show = true); + virtual bool Hide() { return Show(false); } + virtual wxPanel* extra_buttons() { return nullptr; } + virtual void on_page_set() {} + + void enable_next(bool enable); +private: + ConfigWizardPage *p_prev; + ConfigWizardPage *p_next; +}; + +struct PageWelcome: ConfigWizardPage +{ + wxPanel *others_buttons; + unsigned variants_checked; + + PageWelcome(ConfigWizard *parent, const PresetBundle &bundle); + + virtual wxPanel* extra_buttons() { return others_buttons; } + virtual void on_page_set(); + + void on_variant_checked(); +}; + +struct PageUpdate: ConfigWizardPage +{ + PageUpdate(ConfigWizard *parent); + + void presets_update_enable(bool enable); +}; + +struct PageVendors: ConfigWizardPage +{ + PageVendors(ConfigWizard *parent); +}; + +struct PageFirmware: ConfigWizardPage +{ + PageFirmware(ConfigWizard *parent); +}; + +struct PageBedShape: ConfigWizardPage +{ + PageBedShape(ConfigWizard *parent); +}; + +struct PageDiameters: ConfigWizardPage +{ + PageDiameters(ConfigWizard *parent); +}; + +struct PageTemperatures: ConfigWizardPage +{ + PageTemperatures(ConfigWizard *parent); +}; + + +class ConfigWizardIndex: public wxPanel +{ +public: + ConfigWizardIndex(wxWindow *parent); + + void load_items(ConfigWizardPage *firstpage); + void set_active(ConfigWizardPage *page); +private: + const wxBitmap bg; + const wxBitmap bullet_black; + const wxBitmap bullet_blue; + const wxBitmap bullet_white; + int text_height; + + std::vector items; + std::vector::const_iterator item_active; + + void on_paint(wxPaintEvent & evt); +}; + +struct ConfigWizard::priv +{ + ConfigWizard *q; + wxBoxSizer *topsizer = nullptr; + wxBoxSizer *btnsizer = nullptr; + ConfigWizardPage *page_current = nullptr; + ConfigWizardIndex *index = nullptr; + wxButton *btn_prev = nullptr; + wxButton *btn_next = nullptr; + wxButton *btn_finish = nullptr; + wxButton *btn_cancel = nullptr; + + PageWelcome *page_welcome = nullptr; + PageUpdate *page_update = nullptr; + PageVendors *page_vendors = nullptr; + PageFirmware *page_firmware = nullptr; + PageBedShape *page_bed = nullptr; + PageDiameters *page_diams = nullptr; + PageTemperatures *page_temps = nullptr; + + priv(ConfigWizard *q) : q(q) {} + + void add_page(ConfigWizardPage *page); + void index_refresh(); + void set_page(ConfigWizardPage *page); + void go_prev() { if (page_current != nullptr) { set_page(page_current->page_prev()); } } + void go_next() { if (page_current != nullptr) { set_page(page_current->page_next()); } } + void enable_next(bool enable); + + void on_other_vendors(); + void on_custom_setup(); +}; + + + +} +} + +#endif diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index 3eca4e707..53288067c 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -44,6 +44,7 @@ #include "TabIface.hpp" #include "AppConfig.hpp" #include "Utils.hpp" +#include "ConfigWizard.hpp" #include "Preferences.hpp" #include "PresetBundle.hpp" @@ -352,6 +353,20 @@ void add_debug_menu(wxMenuBar *menu, int event_language_change) //#endif } +void open_config_wizard() +{ + if (g_wxMainFrame == nullptr) { + throw std::runtime_error("Main frame not set"); + } + + // auto *wizard = new ConfigWizard(static_cast(g_wxMainFrame)); // FIXME: lifetime + + // wizard->run(); + ConfigWizard::run(g_wxMainFrame); + + // show_info(g_wxMainFrame, "After wizard", "After wizard"); +} + void open_preferences_dialog(int event_preferences) { auto dlg = new PreferencesDialog(g_wxMainFrame, event_preferences); diff --git a/xs/src/slic3r/GUI/GUI.hpp b/xs/src/slic3r/GUI/GUI.hpp index 362b15307..1f93e18e9 100644 --- a/xs/src/slic3r/GUI/GUI.hpp +++ b/xs/src/slic3r/GUI/GUI.hpp @@ -73,6 +73,7 @@ void break_to_debugger(); // Passing the wxWidgets GUI classes instantiated by the Perl part to C++. void set_wxapp(wxApp *app); void set_main_frame(wxFrame *main_frame); +// wxFrame* get_main_frame(); void set_tab_panel(wxNotebook *tab_panel); void set_app_config(AppConfig *app_config); void set_preset_bundle(PresetBundle *preset_bundle); @@ -84,6 +85,9 @@ wxColour* get_sys_label_clr(); void add_debug_menu(wxMenuBar *menu, int event_language_change); +// Opens the first-time configuration wizard +void open_config_wizard(); + // Create "Preferences" dialog after selecting menu "Preferences" in Perl part void open_preferences_dialog(int event_preferences); diff --git a/xs/src/slic3r/GUI/Preferences.cpp b/xs/src/slic3r/GUI/Preferences.cpp index 0009b17ec..6731cd394 100644 --- a/xs/src/slic3r/GUI/Preferences.cpp +++ b/xs/src/slic3r/GUI/Preferences.cpp @@ -14,6 +14,7 @@ void PreferencesDialog::build() m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; }; + // TODO // $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( // opt_id = > 'version_check', // type = > 'bool', diff --git a/xs/src/slic3r/GUI/Preset.hpp b/xs/src/slic3r/GUI/Preset.hpp index 3634c5dd9..78e15bada 100644 --- a/xs/src/slic3r/GUI/Preset.hpp +++ b/xs/src/slic3r/GUI/Preset.hpp @@ -40,7 +40,7 @@ public: struct PrinterModel { PrinterModel() {} - PrinterModel(const std::string &name) : name(name) {} + std::string id; std::string name; bool enabled = true; std::vector variants; @@ -51,11 +51,10 @@ public: return nullptr; } const PrinterVariant* variant(const std::string &name) const { return const_cast(this)->variant(name); } - - bool operator< (const PrinterModel &rhs) const { return this->name < rhs.name; } - bool operator==(const PrinterModel &rhs) const { return this->name == rhs.name; } }; - std::set models; + std::vector models; + + VendorProfile(std::string id) : id(std::move(id)) {} size_t num_variants() const { size_t n = 0; for (auto &model : models) n += model.variants.size(); return n; } diff --git a/xs/src/slic3r/GUI/PresetBundle.cpp b/xs/src/slic3r/GUI/PresetBundle.cpp index 7131bf771..241f0433e 100644 --- a/xs/src/slic3r/GUI/PresetBundle.cpp +++ b/xs/src/slic3r/GUI/PresetBundle.cpp @@ -4,6 +4,7 @@ #include "PresetBundle.hpp" #include "BitmapCache.hpp" +#include #include #include #include @@ -104,6 +105,7 @@ void PresetBundle::setup_directories() std::initializer_list paths = { data_dir, data_dir / "vendor", + data_dir / "cache", #ifdef SLIC3R_PROFILE_USE_PRESETS_SUBDIR // Store the print/filament/printer presets into a "presets" directory. data_dir / "presets", @@ -198,6 +200,7 @@ static inline std::string remove_ini_suffix(const std::string &name) // If the "vendor" section is missing, enable all models and variants of the particular vendor. void PresetBundle::load_installed_printers(const AppConfig &config) { + // TODO // m_storage } @@ -667,7 +670,7 @@ static void flatten_configbundle_hierarchy(boost::property_tree::ptree &tree) static void load_vendor_profile(const boost::property_tree::ptree &tree, VendorProfile &vendor_profile) { const std::string printer_model_key = "printer_model:"; - for (auto §ion : tree) + for (auto §ion : tree) { if (section.first == "vendor") { // Load the names of the active presets. for (auto &kvp : section.second) { @@ -682,7 +685,8 @@ static void load_vendor_profile(const boost::property_tree::ptree &tree, VendorP } } else if (boost::starts_with(section.first, printer_model_key)) { VendorProfile::PrinterModel model; - model.name = section.first.substr(printer_model_key.size()); + model.id = section.first.substr(printer_model_key.size()); + model.name = section.second.get("name", model.id); section.second.get("variants", ""); std::vector variants; if (Slic3r::unescape_strings_cstyle(section.second.get("variants", ""), variants)) { @@ -693,9 +697,10 @@ static void load_vendor_profile(const boost::property_tree::ptree &tree, VendorP } else { // Log error? } - if (! model.name.empty() && ! model.variants.empty()) - vendor_profile.models.insert(model); + if (! model.id.empty() && ! model.variants.empty()) + vendor_profile.models.push_back(std::move(model)); } + } } // Load a config bundle file, into presets and store the loaded presets into separate files @@ -719,12 +724,11 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla pt::ptree tree; boost::nowide::ifstream ifs(path); pt::read_ini(ifs, tree); - // Flatten the config bundle by applying the inheritance rules. Internal profiles (with names starting with '*') are removed. - flatten_configbundle_hierarchy(tree); const VendorProfile *vendor_profile = nullptr; - if (flags & LOAD_CFGBNDLE_SYSTEM) { - VendorProfile vp; + if (flags & (LOAD_CFGBNDLE_SYSTEM | LOAD_CFGBUNDLE_VENDOR_ONLY)) { + boost::filesystem::path fspath(path); + VendorProfile vp(fspath.stem().native()); load_vendor_profile(tree, vp); if (vp.name.empty()) throw std::runtime_error(std::string("Vendor Config Bundle is not valid: Missing vendor name key.")); @@ -732,6 +736,13 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla return 0; vendor_profile = &(*this->vendors.insert(vp).first); } + + if (flags & LOAD_CFGBUNDLE_VENDOR_ONLY) { + return 0; + } + + // 1.5) Flatten the config bundle by applying the inheritance rules. Internal profiles (with names starting with '*') are removed. + flatten_configbundle_hierarchy(tree); // 2) Parse the property_tree, extract the active preset names and the profiles, save them into local config files. std::vector loaded_prints; @@ -803,7 +814,9 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla section.first << "\" defines no printer variant, it will be ignored."; continue; } - auto it_model = vendor_profile->models.find(VendorProfile::PrinterModel(printer_model)); + auto it_model = std::find_if(vendor_profile->models.cbegin(), vendor_profile->models.cend(), + [&](const VendorProfile::PrinterModel &m) { return m.id == printer_model; } + ); if (it_model == vendor_profile->models.end()) { BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << section.first << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; diff --git a/xs/src/slic3r/GUI/PresetBundle.hpp b/xs/src/slic3r/GUI/PresetBundle.hpp index f481ba374..4949e0e03 100644 --- a/xs/src/slic3r/GUI/PresetBundle.hpp +++ b/xs/src/slic3r/GUI/PresetBundle.hpp @@ -81,6 +81,7 @@ public: LOAD_CFGBNDLE_RESET_USER_PROFILE = 2, // Load a system config bundle. LOAD_CFGBNDLE_SYSTEM = 4, + LOAD_CFGBUNDLE_VENDOR_ONLY = 8, }; // Load the config bundle, store it to the user profile directory by default. size_t load_configbundle(const std::string &path, unsigned int flags = LOAD_CFGBNDLE_SAVE); diff --git a/xs/src/slic3r/Utils/PresetUpdate.cpp b/xs/src/slic3r/Utils/PresetUpdate.cpp new file mode 100644 index 000000000..0e4c62af9 --- /dev/null +++ b/xs/src/slic3r/Utils/PresetUpdate.cpp @@ -0,0 +1,104 @@ +#include "PresetUpdate.hpp" + +#include // XXX +#include +#include +#include + +#include "libslic3r/Utils.hpp" +#include "slic3r/GUI/PresetBundle.hpp" +#include "slic3r/Utils/Http.hpp" + +namespace fs = boost::filesystem; + + +namespace Slic3r { + + +struct PresetUpdater::priv +{ + PresetBundle *bundle; + fs::path cache_path; + std::thread thread; + + priv(PresetBundle *bundle); + + void download(); +}; + + +PresetUpdater::priv::priv(PresetBundle *bundle) : + bundle(bundle), + cache_path(fs::path(Slic3r::data_dir()) / "cache") +{} + +void PresetUpdater::priv::download() +{ + std::cerr << "PresetUpdater::priv::download()" << std::endl; + + std::cerr << "Bundle vendors: " << bundle->vendors.size() << std::endl; + for (const auto &vendor : bundle->vendors) { + std::cerr << "vendor: " << vendor.name << std::endl; + std::cerr << " URL: " << vendor.config_update_url << std::endl; + + // TODO: Proper caching + + auto target_path = cache_path / vendor.id; + target_path += ".ini"; + std::cerr << "target_path: " << target_path << std::endl; + + Http::get(vendor.config_update_url) + .on_complete([&](std::string body, unsigned http_status) { + std::cerr << "Got ini: " << http_status << ", body: " << body.size() << std::endl; + fs::fstream file(target_path, std::ios::out | std::ios::binary | std::ios::trunc); + file.write(body.c_str(), body.size()); + }) + .on_error([](std::string body, std::string error, unsigned http_status) { + // TODO: what about errors? + std::cerr << "Error: " << http_status << ", " << error << std::endl; + }) + .perform_sync(); + } +} + +PresetUpdater::PresetUpdater(PresetBundle *preset_bundle) : p(new priv(preset_bundle)) {} + + +// Public + +PresetUpdater::~PresetUpdater() +{ + if (p && p->thread.joinable()) { + p->thread.detach(); + } +} + +void PresetUpdater::download(AppConfig *app_config, PresetBundle *preset_bundle) +{ + std::cerr << "PresetUpdater::download()" << std::endl; + + auto self = std::make_shared(preset_bundle); + auto thread = std::thread([self](){ + self->p->download(); + }); + self->p->thread = std::move(thread); +} + + +// TODO: remove +namespace Utils { + +void preset_update_check() +{ + std::cerr << "preset_update_check()" << std::endl; + + // TODO: + // 1. Get a version tag or the whole bundle from the web + // 2. Store into temporary location (?) + // 3. ??? + // 4. Profit! +} + +} + +} diff --git a/xs/src/slic3r/Utils/PresetUpdate.hpp b/xs/src/slic3r/Utils/PresetUpdate.hpp new file mode 100644 index 000000000..431134097 --- /dev/null +++ b/xs/src/slic3r/Utils/PresetUpdate.hpp @@ -0,0 +1,38 @@ +#ifndef slic3r_PresetUpdate_hpp_ +#define slic3r_PresetUpdate_hpp_ + +#include + +namespace Slic3r { + + +class AppConfig; +class PresetBundle; + +class PresetUpdater +{ +public: + PresetUpdater(PresetBundle *preset_bundle); + PresetUpdater(PresetUpdater &&) = delete; + PresetUpdater(const PresetUpdater &) = delete; + PresetUpdater &operator=(PresetUpdater &&) = delete; + PresetUpdater &operator=(const PresetUpdater &) = delete; + ~PresetUpdater(); + + static void download(AppConfig *app_config, PresetBundle *preset_bundle); +private: + struct priv; + std::unique_ptr p; +}; + + +// TODO: Remove +namespace Utils { + +void preset_update_check(); + +} + +} + +#endif diff --git a/xs/xsp/GUI.xsp b/xs/xsp/GUI.xsp index 1376ff164..5115eda64 100644 --- a/xs/xsp/GUI.xsp +++ b/xs/xsp/GUI.xsp @@ -54,6 +54,9 @@ int combochecklist_get_flags(SV *ui) void set_app_config(AppConfig *app_config) %code%{ Slic3r::GUI::set_app_config(app_config); %}; +void open_config_wizard() + %code%{ Slic3r::GUI::open_config_wizard(); %}; + void open_preferences_dialog(int preferences_event) %code%{ Slic3r::GUI::open_preferences_dialog(preferences_event); %}; diff --git a/xs/xsp/Utils_PresetUpdate.xsp b/xs/xsp/Utils_PresetUpdate.xsp new file mode 100644 index 000000000..3596b7c86 --- /dev/null +++ b/xs/xsp/Utils_PresetUpdate.xsp @@ -0,0 +1,18 @@ +%module{Slic3r::XS}; + +%{ +#include +#include "slic3r/Utils/PresetUpdate.hpp" +%} + +%name{Slic3r::PresetUpdater} class PresetUpdater { + static void download(PresetBundle *preset_bundle); +}; + + +# TODO: remove: + +%package{Slic3r::Utils}; + +void preset_update_check() + %code%{ Slic3r::Utils::preset_update_check(); %}; diff --git a/xs/xsp/my.map b/xs/xsp/my.map index 87a8d8d86..d817af052 100644 --- a/xs/xsp/my.map +++ b/xs/xsp/my.map @@ -235,6 +235,9 @@ PresetHints* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T TabIface* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T +# TODO: remove: +# ConfigWizard* O_OBJECT_SLIC3R +# Ref O_OBJECT_SLIC3R_T OctoPrint* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T From 57e47a3296c94e9ed879e5bf281aef69f1942409 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Wed, 28 Mar 2018 11:36:36 +0200 Subject: [PATCH 02/38] AppConfig: Support for vendor / model / variant enable state --- lib/Slic3r/GUI.pm | 24 ++- xs/CMakeLists.txt | 6 +- xs/src/perlglue.cpp | 1 + xs/src/slic3r/GUI/AppConfig.cpp | 75 +++++++++- xs/src/slic3r/GUI/AppConfig.hpp | 16 ++ xs/src/slic3r/Utils/PresetUpdate.cpp | 104 ------------- xs/src/slic3r/Utils/PresetUpdater.cpp | 137 ++++++++++++++++++ .../{PresetUpdate.hpp => PresetUpdater.hpp} | 12 +- xs/xsp/GUI_AppConfig.xsp | 1 + xs/xsp/Utils_PresetUpdate.xsp | 18 --- xs/xsp/Utils_PresetUpdater.xsp | 11 ++ xs/xsp/my.map | 4 + 12 files changed, 267 insertions(+), 142 deletions(-) delete mode 100644 xs/src/slic3r/Utils/PresetUpdate.cpp create mode 100644 xs/src/slic3r/Utils/PresetUpdater.cpp rename xs/src/slic3r/Utils/{PresetUpdate.hpp => PresetUpdater.hpp} (71%) delete mode 100644 xs/xsp/Utils_PresetUpdate.xsp create mode 100644 xs/xsp/Utils_PresetUpdater.xsp diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 0c5e87429..495dd9ecc 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -70,6 +70,8 @@ our $grey = Wx::Colour->new(200,200,200); our $LANGUAGE_CHANGE_EVENT = Wx::NewEventType; # 2) To inform about a change of Preferences. our $PREFERENCES_EVENT = Wx::NewEventType; +# To inform AppConfig about Slic3r version available online +our $VERSION_ONLINE_EVENT = Wx::NewEventType; sub OnInit { my ($self) = @_; @@ -101,7 +103,9 @@ sub OnInit { $self->{app_config}->set('version', $Slic3r::VERSION); $self->{app_config}->save; - my $slic3r_update_avail = $self->{app_config}->get("version_check") && $self->{app_config}->get("version_online") != $Slic3r::VERSION; + # my $version_check = $self->{app_config}->get('version_check'); + $self->{preset_updater} = Slic3r::PresetUpdater->new($VERSION_ONLINE_EVENT, $self->{app_config}); + my $slic3r_update = $self->{app_config}->slic3r_update_avail; Slic3r::GUI::set_app_config($self->{app_config}); Slic3r::GUI::load_language(); @@ -140,15 +144,17 @@ sub OnInit { # On OSX the UI was not initialized correctly if the wizard was called # before the UI was up and running. $self->CallAfter(sub { - if ($slic3r_update_avail) { + # XXX: recreate_GUI ??? + if ($slic3r_update) { # TODO - } elsif ($run_wizard) { + } + # XXX: ? + if ($run_wizard) { # Run the config wizard, don't offer the "reset user profile" checkbox. $self->{mainframe}->config_wizard(1); } - # XXX: recreate_GUI ??? - Slic3r::PresetUpdater::download($self->{app_config}, $self->{preset_bundle}); + $self->{preset_updater}->download($self->{preset_bundle}); }); # The following event is emited by the C++ menu implementation of application language change. @@ -161,6 +167,14 @@ sub OnInit { $self->update_ui_from_settings; }); + # The following event is emited by PresetUpdater (C++) + EVT_COMMAND($self, -1, $VERSION_ONLINE_EVENT, sub { + my ($self, $event) = @_; + my $version = $event->GetString; + $self->{app_config}->set('version_online', $version); + $self->{app_config}->save; + }); + return 1; } diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt index e73c8429a..3ce499d2b 100644 --- a/xs/CMakeLists.txt +++ b/xs/CMakeLists.txt @@ -213,8 +213,8 @@ add_library(libslic3r_gui STATIC ${LIBDIR}/slic3r/Utils/OctoPrint.hpp ${LIBDIR}/slic3r/Utils/Bonjour.cpp ${LIBDIR}/slic3r/Utils/Bonjour.hpp - ${LIBDIR}/slic3r/Utils/PresetUpdate.cpp - ${LIBDIR}/slic3r/Utils/PresetUpdate.hpp + ${LIBDIR}/slic3r/Utils/PresetUpdater.cpp + ${LIBDIR}/slic3r/Utils/PresetUpdater.hpp ) add_library(admesh STATIC @@ -360,7 +360,7 @@ set(XS_XSP_FILES ${XSP_DIR}/SurfaceCollection.xsp ${XSP_DIR}/TriangleMesh.xsp ${XSP_DIR}/Utils_OctoPrint.xsp - ${XSP_DIR}/Utils_PresetUpdate.xsp + ${XSP_DIR}/Utils_PresetUpdater.xsp ${XSP_DIR}/XS.xsp ) foreach (file ${XS_XSP_FILES}) diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp index d7c9a590a..9706ced2c 100644 --- a/xs/src/perlglue.cpp +++ b/xs/src/perlglue.cpp @@ -64,6 +64,7 @@ REGISTER_CLASS(PresetCollection, "GUI::PresetCollection"); REGISTER_CLASS(PresetBundle, "GUI::PresetBundle"); REGISTER_CLASS(PresetHints, "GUI::PresetHints"); REGISTER_CLASS(TabIface, "GUI::Tab"); +REGISTER_CLASS(PresetUpdater, "PresetUpdater"); REGISTER_CLASS(OctoPrint, "OctoPrint"); SV* ConfigBase__as_hash(ConfigBase* THIS) diff --git a/xs/src/slic3r/GUI/AppConfig.cpp b/xs/src/slic3r/GUI/AppConfig.cpp index 4bdf88dda..e951beb44 100644 --- a/xs/src/slic3r/GUI/AppConfig.cpp +++ b/xs/src/slic3r/GUI/AppConfig.cpp @@ -1,5 +1,3 @@ -#include - #include "../../libslic3r/libslic3r.h" #include "../../libslic3r/Utils.hpp" #include "AppConfig.hpp" @@ -9,15 +7,20 @@ #include #include #include +#include #include #include #include #include #include +#include namespace Slic3r { +static const std::string VENDOR_PREFIX = "vendor:"; +static const std::string MODEL_PREFIX = "model:"; + void AppConfig::reset() { m_storage.clear(); @@ -45,6 +48,8 @@ void AppConfig::set_defaults() // Version check is enabled by default in the config, but it is not implemented yet. // XXX if (get("version_check").empty()) set("version_check", "1"); + if (get("preset_update").empty()) + set("preset_update", "1"); // Use OpenGL 1.1 even if OpenGL 2.0 is available. This is mainly to support some buggy Intel HD Graphics drivers. // https://github.com/prusa3d/Slic3r/issues/233 if (get("use_legacy_opengl").empty()) @@ -67,6 +72,19 @@ void AppConfig::load() if (! data.empty()) // If there is a non-empty data, then it must be a top-level (without a section) config entry. m_storage[""][section.first] = data; + } else if (boost::starts_with(section.first, VENDOR_PREFIX)) { + // This is a vendor section listing enabled model / variants + const auto vendor_name = section.first.substr(VENDOR_PREFIX.size()); + auto &vendor = m_vendors[vendor_name]; + for (const auto &kvp : section.second) { + if (! boost::starts_with(kvp.first, MODEL_PREFIX)) { continue; } + const auto model_name = kvp.first.substr(MODEL_PREFIX.size()); + std::vector variants; + if (! unescape_strings_cstyle(kvp.second.data(), variants)) { continue; } + for (const auto &variant : variants) { + vendor[model_name].insert(variant); + } + } } else { // This must be a section name. Read the entries of a section. std::map &storage = m_storage[section.first]; @@ -96,10 +114,53 @@ void AppConfig::save() for (const std::pair &kvp : category.second) c << kvp.first << " = " << kvp.second << std::endl; } + // Write vendor sections + for (const auto &vendor : m_vendors) { + size_t size_sum = 0; + for (const auto &model : vendor.second) { size_sum += model.second.size(); } + if (size_sum == 0) { continue; } + + c << std::endl << "[" << VENDOR_PREFIX << vendor.first << "]" << std::endl; + + for (const auto &model : vendor.second) { + if (model.second.size() == 0) { continue; } + const std::vector variants(model.second.begin(), model.second.end()); + const auto escaped = escape_strings_cstyle(variants); + c << MODEL_PREFIX << model.first << " = " << escaped << std::endl; + } + } c.close(); m_dirty = false; } +bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const +{ + std::cerr << "AppConfig::get_variant(" << vendor << ", " << model << ", " << variant << ") "; + + const auto it_v = m_vendors.find(vendor); + if (it_v == m_vendors.end()) { return false; } + const auto it_m = it_v->second.find(model); + return it_m == it_v->second.end() ? false : it_m->second.find(variant) != it_m->second.end(); +} + +void AppConfig::set_variant(const std::string &vendor, const std::string &model, const std::string &variant, bool enable) +{ + if (enable) { + if (get_variant(vendor, model, variant)) { return; } + m_vendors[vendor][model].insert(variant); + } else { + auto it_v = m_vendors.find(vendor); + if (it_v == m_vendors.end()) { return; } + auto it_m = it_v->second.find(model); + if (it_m == it_v->second.end()) { return; } + auto it_var = it_m->second.find(variant); + if (it_var == it_m->second.end()) { return; } + it_m->second.erase(it_var); + } + // If we got here, there was an update + m_dirty = true; +} + std::string AppConfig::get_last_dir() const { const auto it = m_storage.find("recent"); @@ -156,6 +217,16 @@ void AppConfig::reset_selections() } } +bool AppConfig::version_check_enabled() const +{ + return get("version_check") == "1"; +} + +bool AppConfig::slic3r_update_avail() const +{ + return version_check_enabled() && get("version_online") != SLIC3R_VERSION; +} + std::string AppConfig::config_path() { return (boost::filesystem::path(Slic3r::data_dir()) / "slic3r.ini").make_preferred().string(); diff --git a/xs/src/slic3r/GUI/AppConfig.hpp b/xs/src/slic3r/GUI/AppConfig.hpp index 9b1d5a712..4ccd51304 100644 --- a/xs/src/slic3r/GUI/AppConfig.hpp +++ b/xs/src/slic3r/GUI/AppConfig.hpp @@ -1,9 +1,12 @@ #ifndef slic3r_AppConfig_hpp_ #define slic3r_AppConfig_hpp_ +#include #include #include +#include "libslic3r/Config.hpp" + namespace Slic3r { class AppConfig @@ -65,6 +68,13 @@ public: void clear_section(const std::string §ion) { m_storage[section].clear(); } + // TODO: remove / upgrade + // ConfigOptionStrings get_strings(const std::string §ion, const std::string &key) const; + // void set_strings(const std::string §ion, const std::string &key, const ConfigOptionStrings &value); + // TODO: + bool get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const; + void set_variant(const std::string &vendor, const std::string &model, const std::string &variant, bool enable); + // return recent/skein_directory or recent/config_directory or empty string. std::string get_last_dir() const; void update_config_dir(const std::string &dir); @@ -78,6 +88,10 @@ public: // the first non-default preset when called. void reset_selections(); + // Whether the Slic3r version available online differs from this one + bool version_check_enabled() const; + bool slic3r_update_avail() const; + // Get the default config path from Slic3r::data_dir(). static std::string config_path(); @@ -87,6 +101,8 @@ public: private: // Map of section, name -> value std::map> m_storage; + // Map of enabled vendors / models / variants + std::map>> m_vendors; // Has any value been modified since the config.ini has been last saved or loaded? bool m_dirty; }; diff --git a/xs/src/slic3r/Utils/PresetUpdate.cpp b/xs/src/slic3r/Utils/PresetUpdate.cpp deleted file mode 100644 index 0e4c62af9..000000000 --- a/xs/src/slic3r/Utils/PresetUpdate.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "PresetUpdate.hpp" - -#include // XXX -#include -#include -#include - -#include "libslic3r/Utils.hpp" -#include "slic3r/GUI/PresetBundle.hpp" -#include "slic3r/Utils/Http.hpp" - -namespace fs = boost::filesystem; - - -namespace Slic3r { - - -struct PresetUpdater::priv -{ - PresetBundle *bundle; - fs::path cache_path; - std::thread thread; - - priv(PresetBundle *bundle); - - void download(); -}; - - -PresetUpdater::priv::priv(PresetBundle *bundle) : - bundle(bundle), - cache_path(fs::path(Slic3r::data_dir()) / "cache") -{} - -void PresetUpdater::priv::download() -{ - std::cerr << "PresetUpdater::priv::download()" << std::endl; - - std::cerr << "Bundle vendors: " << bundle->vendors.size() << std::endl; - for (const auto &vendor : bundle->vendors) { - std::cerr << "vendor: " << vendor.name << std::endl; - std::cerr << " URL: " << vendor.config_update_url << std::endl; - - // TODO: Proper caching - - auto target_path = cache_path / vendor.id; - target_path += ".ini"; - std::cerr << "target_path: " << target_path << std::endl; - - Http::get(vendor.config_update_url) - .on_complete([&](std::string body, unsigned http_status) { - std::cerr << "Got ini: " << http_status << ", body: " << body.size() << std::endl; - fs::fstream file(target_path, std::ios::out | std::ios::binary | std::ios::trunc); - file.write(body.c_str(), body.size()); - }) - .on_error([](std::string body, std::string error, unsigned http_status) { - // TODO: what about errors? - std::cerr << "Error: " << http_status << ", " << error << std::endl; - }) - .perform_sync(); - } -} - -PresetUpdater::PresetUpdater(PresetBundle *preset_bundle) : p(new priv(preset_bundle)) {} - - -// Public - -PresetUpdater::~PresetUpdater() -{ - if (p && p->thread.joinable()) { - p->thread.detach(); - } -} - -void PresetUpdater::download(AppConfig *app_config, PresetBundle *preset_bundle) -{ - std::cerr << "PresetUpdater::download()" << std::endl; - - auto self = std::make_shared(preset_bundle); - auto thread = std::thread([self](){ - self->p->download(); - }); - self->p->thread = std::move(thread); -} - - -// TODO: remove -namespace Utils { - -void preset_update_check() -{ - std::cerr << "preset_update_check()" << std::endl; - - // TODO: - // 1. Get a version tag or the whole bundle from the web - // 2. Store into temporary location (?) - // 3. ??? - // 4. Profit! -} - -} - -} diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp new file mode 100644 index 000000000..11064d2f0 --- /dev/null +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -0,0 +1,137 @@ +#include "PresetUpdater.hpp" + +#include // XXX +#include +#include +#include +#include + +#include +#include + +#include "libslic3r/Utils.hpp" +#include "slic3r/GUI/GUI.hpp" +// #include "slic3r/GUI/AppConfig.hpp" +#include "slic3r/GUI/PresetBundle.hpp" +#include "slic3r/Utils/Http.hpp" + +namespace fs = boost::filesystem; + + +namespace Slic3r { + + +// TODO: proper URL +static const std::string SLIC3R_VERSION_URL = "https://gist.githubusercontent.com/vojtechkral/4d8fd4a3b8699a01ec892c264178461c/raw/e9187c3e15ceaf1a90f29b7c43cf3ccc746140f0/slic3rPE.version"; +enum { + SLIC3R_VERSION_BODY_MAX = 256, +}; + +struct PresetUpdater::priv +{ + int version_online_event; + bool version_check; + bool preset_update; + + fs::path cache_path; + bool cancel; + std::thread thread; + + priv(int event); + + void download(const std::set vendors) const; +}; + +PresetUpdater::priv::priv(int event) : + version_online_event(event), + version_check(false), + preset_update(false), + cache_path(fs::path(Slic3r::data_dir()) / "cache"), + cancel(false) +{} + +void PresetUpdater::priv::download(const std::set vendors) const +{ + std::cerr << "PresetUpdater::priv::download()" << std::endl; + + if (!version_check) { return; } + + // Download current Slic3r version + Http::get(SLIC3R_VERSION_URL) + .size_limit(SLIC3R_VERSION_BODY_MAX) + .on_progress([this](Http::Progress, bool &cancel) { + cancel = this->cancel; + }) + .on_complete([&](std::string body, unsigned http_status) { + boost::trim(body); + std::cerr << "Got version: " << http_status << ", body: \"" << body << '"' << std::endl; + wxCommandEvent* evt = new wxCommandEvent(version_online_event); + evt->SetString(body); + GUI::get_app()->QueueEvent(evt); + }) + .perform_sync(); + + if (!preset_update) { return; } + + // Donwload vendor preset bundles + std::cerr << "Bundle vendors: " << vendors.size() << std::endl; + for (const auto &vendor : vendors) { + std::cerr << "vendor: " << vendor.name << std::endl; + std::cerr << " URL: " << vendor.config_update_url << std::endl; + + if (cancel) { return; } + + // TODO: Proper caching + + auto target_path = cache_path / vendor.id; + target_path += ".ini"; + std::cerr << "target_path: " << target_path << std::endl; + + Http::get(vendor.config_update_url) + .on_progress([this](Http::Progress, bool &cancel) { + cancel = this->cancel; + }) + .on_complete([&](std::string body, unsigned http_status) { + std::cerr << "Got ini: " << http_status << ", body: " << body.size() << std::endl; + fs::fstream file(target_path, std::ios::out | std::ios::binary | std::ios::trunc); + file.write(body.c_str(), body.size()); + }) + .perform_sync(); + } +} + +PresetUpdater::PresetUpdater(int version_online_event, AppConfig *app_config) : + p(new priv(version_online_event)) +{ + p->preset_update = app_config->get("preset_update") == "1"; + // preset_update implies version_check: + p->version_check = p->preset_update || app_config->get("version_check") == "1"; +} + + +// Public + +PresetUpdater::~PresetUpdater() +{ + if (p && p->thread.joinable()) { + p->cancel = true; + p->thread.join(); + } +} + +void PresetUpdater::download(PresetBundle *preset_bundle) +{ + std::cerr << "PresetUpdater::download()" << std::endl; + + // Copy the whole vendors data for use in the background thread + // Unfortunatelly as of C++11, it needs to be copied again + // into the closure (but perhaps the compiler can elide this). + std::set vendors = preset_bundle->vendors; + + p->thread = std::move(std::thread([this, vendors]() { + this->p->download(std::move(vendors)); + })); +} + + +} diff --git a/xs/src/slic3r/Utils/PresetUpdate.hpp b/xs/src/slic3r/Utils/PresetUpdater.hpp similarity index 71% rename from xs/src/slic3r/Utils/PresetUpdate.hpp rename to xs/src/slic3r/Utils/PresetUpdater.hpp index 431134097..aafe9569b 100644 --- a/xs/src/slic3r/Utils/PresetUpdate.hpp +++ b/xs/src/slic3r/Utils/PresetUpdater.hpp @@ -12,27 +12,19 @@ class PresetBundle; class PresetUpdater { public: - PresetUpdater(PresetBundle *preset_bundle); + PresetUpdater(int version_online_event, AppConfig *app_config); PresetUpdater(PresetUpdater &&) = delete; PresetUpdater(const PresetUpdater &) = delete; PresetUpdater &operator=(PresetUpdater &&) = delete; PresetUpdater &operator=(const PresetUpdater &) = delete; ~PresetUpdater(); - static void download(AppConfig *app_config, PresetBundle *preset_bundle); + void download(PresetBundle *preset_bundle); private: struct priv; std::unique_ptr p; }; -// TODO: Remove -namespace Utils { - -void preset_update_check(); - } - -} - #endif diff --git a/xs/xsp/GUI_AppConfig.xsp b/xs/xsp/GUI_AppConfig.xsp index 08a88883d..de0e5a22b 100644 --- a/xs/xsp/GUI_AppConfig.xsp +++ b/xs/xsp/GUI_AppConfig.xsp @@ -43,4 +43,5 @@ void update_last_output_dir(char *dir); void reset_selections(); + bool slic3r_update_avail() const; }; diff --git a/xs/xsp/Utils_PresetUpdate.xsp b/xs/xsp/Utils_PresetUpdate.xsp deleted file mode 100644 index 3596b7c86..000000000 --- a/xs/xsp/Utils_PresetUpdate.xsp +++ /dev/null @@ -1,18 +0,0 @@ -%module{Slic3r::XS}; - -%{ -#include -#include "slic3r/Utils/PresetUpdate.hpp" -%} - -%name{Slic3r::PresetUpdater} class PresetUpdater { - static void download(PresetBundle *preset_bundle); -}; - - -# TODO: remove: - -%package{Slic3r::Utils}; - -void preset_update_check() - %code%{ Slic3r::Utils::preset_update_check(); %}; diff --git a/xs/xsp/Utils_PresetUpdater.xsp b/xs/xsp/Utils_PresetUpdater.xsp new file mode 100644 index 000000000..666379f02 --- /dev/null +++ b/xs/xsp/Utils_PresetUpdater.xsp @@ -0,0 +1,11 @@ +%module{Slic3r::XS}; + +%{ +#include +#include "slic3r/Utils/PresetUpdater.hpp" +%} + +%name{Slic3r::PresetUpdater} class PresetUpdater { + PresetUpdater(int version_online_event, AppConfig *app_config); + void download(PresetBundle* preset_bundle); +}; diff --git a/xs/xsp/my.map b/xs/xsp/my.map index d817af052..9c0f60c1d 100644 --- a/xs/xsp/my.map +++ b/xs/xsp/my.map @@ -239,6 +239,10 @@ Ref O_OBJECT_SLIC3R_T # ConfigWizard* O_OBJECT_SLIC3R # Ref O_OBJECT_SLIC3R_T +PresetUpdater* O_OBJECT_SLIC3R +Ref O_OBJECT_SLIC3R_T +Clone O_OBJECT_SLIC3R_T + OctoPrint* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T Clone O_OBJECT_SLIC3R_T From e53949f2c85e06adaf7be9a96873844a8862eab6 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Thu, 29 Mar 2018 17:54:43 +0200 Subject: [PATCH 03/38] Apply printer model / variant preferences when loading presets --- lib/Slic3r/GUI.pm | 2 +- lib/Slic3r/GUI/MainFrame.pm | 6 +- resources/profiles/PrusaResearch.ini | 2 + xs/src/slic3r/GUI/AppConfig.cpp | 8 +- xs/src/slic3r/GUI/AppConfig.hpp | 5 +- xs/src/slic3r/GUI/ConfigWizard.cpp | 154 ++++++++++++++++----- xs/src/slic3r/GUI/ConfigWizard.hpp | 5 +- xs/src/slic3r/GUI/ConfigWizard_private.hpp | 30 +++- xs/src/slic3r/GUI/GUI.cpp | 9 +- xs/src/slic3r/GUI/GUI.hpp | 2 +- xs/src/slic3r/GUI/Preset.cpp | 11 ++ xs/src/slic3r/GUI/Preset.hpp | 18 ++- xs/src/slic3r/GUI/PresetBundle.cpp | 20 ++- xs/src/slic3r/GUI/PresetBundle.hpp | 2 + xs/src/slic3r/Utils/PresetUpdater.cpp | 2 +- xs/xsp/GUI.xsp | 4 +- xs/xsp/GUI_Preset.xsp | 2 +- 17 files changed, 218 insertions(+), 64 deletions(-) diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 495dd9ecc..ed2f6dfc7 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -154,7 +154,7 @@ sub OnInit { $self->{mainframe}->config_wizard(1); } - $self->{preset_updater}->download($self->{preset_bundle}); + # $self->{preset_updater}->download($self->{preset_bundle}); }); # The following event is emited by the C++ menu implementation of application language change. diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index 68fc96339..4307375c8 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -646,7 +646,11 @@ sub config_wizard { # TODO: Offer "reset user profile" - Slic3r::GUI::open_config_wizard(); + Slic3r::GUI::open_config_wizard(wxTheApp->{preset_bundle}); + # Load the currently selected preset into the GUI, update the preset selection box. + foreach my $tab (values %{$self->{options_tabs}}) { # XXX: only if not cancelled? + $tab->load_current_preset; + } return; # Enumerate the profiles bundled with the Slic3r installation under resources/profiles. diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index 0a1d26eaa..6dc29a96e 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -999,6 +999,7 @@ default_print_profile = 0.15mm OPTIMAL MK3 [printer:Original Prusa i3 MK3 0.25 nozzle] inherits = *common* nozzle_diameter = 0.25 +printer_variant = 0.25 end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n retract_lift_below = 209 @@ -1009,6 +1010,7 @@ default_print_profile = 0.10mm DETAIL MK3 [printer:Original Prusa i3 MK3 0.6 nozzle] inherits = *common* nozzle_diameter = 0.6 +printer_variant = 0.6 end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n retract_lift_below = 209 diff --git a/xs/src/slic3r/GUI/AppConfig.cpp b/xs/src/slic3r/GUI/AppConfig.cpp index e951beb44..10a586e27 100644 --- a/xs/src/slic3r/GUI/AppConfig.cpp +++ b/xs/src/slic3r/GUI/AppConfig.cpp @@ -135,7 +135,7 @@ void AppConfig::save() bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const { - std::cerr << "AppConfig::get_variant(" << vendor << ", " << model << ", " << variant << ") "; + // std::cerr << "AppConfig::get_variant(" << vendor << ", " << model << ", " << variant << ") " << std::endl; const auto it_v = m_vendors.find(vendor); if (it_v == m_vendors.end()) { return false; } @@ -161,6 +161,12 @@ void AppConfig::set_variant(const std::string &vendor, const std::string &model, m_dirty = true; } +void AppConfig::set_vendors(const AppConfig &from) +{ + m_vendors = from.m_vendors; + m_dirty = true; +} + std::string AppConfig::get_last_dir() const { const auto it = m_storage.find("recent"); diff --git a/xs/src/slic3r/GUI/AppConfig.hpp b/xs/src/slic3r/GUI/AppConfig.hpp index 4ccd51304..e43ff51bf 100644 --- a/xs/src/slic3r/GUI/AppConfig.hpp +++ b/xs/src/slic3r/GUI/AppConfig.hpp @@ -68,12 +68,9 @@ public: void clear_section(const std::string §ion) { m_storage[section].clear(); } - // TODO: remove / upgrade - // ConfigOptionStrings get_strings(const std::string §ion, const std::string &key) const; - // void set_strings(const std::string §ion, const std::string &key, const ConfigOptionStrings &value); - // TODO: bool get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const; void set_variant(const std::string &vendor, const std::string &model, const std::string &variant, bool enable); + void set_vendors(const AppConfig &from); // return recent/skein_directory or recent/config_directory or empty string. std::string get_last_dir() const; diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index 3cad789b6..556b91a46 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -74,13 +74,17 @@ void ConfigWizardPage::append_text(wxString text) auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); widget->Wrap(CONTENT_WIDTH); widget->SetMinSize(wxSize(CONTENT_WIDTH, -1)); - // content->Add(widget, 1, wxALIGN_LEFT | wxTOP | wxBOTTOM, 10); content->Add(widget, 0, wxALIGN_LEFT | wxTOP | wxBOTTOM, 10); } -void ConfigWizardPage::append_widget(wxWindow *widget, int proportion) +void ConfigWizardPage::append_widget(wxWindow *widget, int proportion, int flag, int border) { - content->Add(widget, proportion, wxEXPAND | wxTOP | wxBOTTOM, 10); + content->Add(widget, proportion, flag, border); +} + +void ConfigWizardPage::append_sizer(wxSizer *sizer, int proportion) +{ + content->Add(sizer, proportion, wxEXPAND | wxTOP | wxBOTTOM, 10); } void ConfigWizardPage::append_spacer(int space) @@ -99,17 +103,19 @@ void ConfigWizardPage::enable_next(bool enable) { parent->p->enable_next(enable) // Wizard pages -PageWelcome::PageWelcome(ConfigWizard *parent, const PresetBundle &bundle) : +// PageWelcome::PageWelcome(ConfigWizard *parent, const PresetBundle &bundle) : +PageWelcome::PageWelcome(ConfigWizard *parent) : ConfigWizardPage(parent, _(L("Welcome to the Slic3r Configuration assistant")), _(L("Welcome"))), others_buttons(new wxPanel(parent)), variants_checked(0) { append_text(_(L("Hello, welcome to Slic3r Prusa Edition! TODO: This text."))); + const PresetBundle &bundle = wizard_p()->bundle_vendors; const auto &vendors = bundle.vendors; const auto vendor_prusa = std::find(vendors.cbegin(), vendors.cend(), VendorProfile("PrusaResearch")); - // TODO: preload checkiness from app config + const AppConfig &appconfig_vendors = wizard_p()->appconfig_vendors; if (vendor_prusa != vendors.cend()) { const auto &models = vendor_prusa->models; @@ -138,12 +144,20 @@ PageWelcome::PageWelcome(ConfigWizard *parent, const PresetBundle &bundle) : sizer->AddSpacer(20); + std::string model_id = model->id; + for (const auto &variant : model->variants) { + std::string variant_name = variant.name; auto *cbox = new wxCheckBox(panel, wxID_ANY, wxString::Format("%s %s %s", variant.name, _(L("mm")), _(L("nozzle")))); + bool enabled = appconfig_vendors.get_variant("PrusaResearch", model_id, variant_name); + variants_checked += enabled; + cbox->SetValue(enabled); sizer->Add(cbox, 0, wxBOTTOM, 3); - cbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { + cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { this->variants_checked += event.IsChecked() ? 1 : -1; this->on_variant_checked(); + AppConfig &appconfig_vendors = this->wizard_p()->appconfig_vendors; + appconfig_vendors.set_variant("PrusaResearch", model_id, variant_name, event.IsChecked()); }); } @@ -156,6 +170,7 @@ PageWelcome::PageWelcome(ConfigWizard *parent, const PresetBundle &bundle) : { auto *sizer = new wxBoxSizer(wxHORIZONTAL); auto *other_vendors = new wxButton(others_buttons, wxID_ANY, _(L("Other vendors"))); + other_vendors->Disable(); auto *custom_setup = new wxButton(others_buttons, wxID_ANY, _(L("Custom setup"))); sizer->Add(other_vendors); @@ -181,17 +196,24 @@ void PageWelcome::on_variant_checked() } PageUpdate::PageUpdate(ConfigWizard *parent) : - ConfigWizardPage(parent, _(L("Automatic updates")), _(L("Updates"))) + ConfigWizardPage(parent, _(L("Automatic updates")), _(L("Updates"))), + version_check(true), + preset_update(true) { - append_text(_(L("TODO: text"))); - auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _(L("Check for Slic3r updates"))); - box_slic3r->SetValue(true); - append_widget(box_slic3r); + const AppConfig *app_config = GUI::get_app_config(); + append_text(_(L("TODO: text"))); + auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _(L("Check for Slic3r updates"))); + box_slic3r->SetValue(app_config->get("version_check") == "1"); + append_widget(box_slic3r); + append_text(_(L("TODO: text"))); auto *box_presets = new wxCheckBox(this, wxID_ANY, _(L("Update built-in Presets automatically"))); - box_presets->SetValue(true); + box_presets->SetValue(app_config->get("preset_update") == "1"); append_widget(box_presets); + + box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); + box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); } void PageUpdate::presets_update_enable(bool enable) @@ -201,7 +223,42 @@ void PageUpdate::presets_update_enable(bool enable) PageVendors::PageVendors(ConfigWizard *parent) : ConfigWizardPage(parent, _(L("Other Vendors")), _(L("Other Vendors"))) -{} +{ + enum { + INDENT_SPACING = 30, + VERTICAL_SPACING = 10, + }; + + append_text(_(L("Other vendors! TODO: This text."))); + + const PresetBundle &bundle = wizard_p()->bundle_vendors; + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + for (const auto &vendor : bundle.vendors) { + if (vendor.id == "PrusaResearch") { continue; } + + auto *label_vendor = new wxStaticText(this, wxID_ANY, vendor.name); + label_vendor->SetFont(boldfont); + append_thing(label_vendor, 0, 0, 0); + + for (const auto &model : vendor.models) { + auto *label_model = new wxStaticText(this, wxID_ANY, model.name); + label_model->SetFont(boldfont); + append_thing(label_model, 0, wxLEFT, INDENT_SPACING); + + for (const auto &variant : model.variants) { + auto *cbox = new wxCheckBox(this, wxID_ANY, variant.name); + append_thing(cbox, 0, wxEXPAND | wxLEFT, 2 * INDENT_SPACING); + cbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { + // TODO + }); + } + } + + append_spacer(VERTICAL_SPACING); + } +} PageFirmware::PageFirmware(ConfigWizard *parent) : ConfigWizardPage(parent, _(L("Firmware Type")), _(L("Firmware"))) @@ -289,6 +346,31 @@ void ConfigWizardIndex::on_paint(wxPaintEvent & evt) // priv +void ConfigWizard::priv::load_vendors() +{ + const auto vendor_dir = fs::path(Slic3r::data_dir()) / "vendor"; + // const auto profiles_dir = fs::path(resources_dir()) / "profiles"; + for (fs::directory_iterator it(vendor_dir); it != fs::directory_iterator(); ++it) { + if (it->path().extension() == ".ini") { + bundle_vendors.load_configbundle(it->path().native(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); + } + } + + // XXX + // for (const auto &vendor : bundle_vendors.vendors) { + // std::cerr << "vendor: " << vendor.name << std::endl; + // std::cerr << " URL: " << vendor.config_update_url << std::endl; + // for (const auto &model : vendor.models) { + // std::cerr << "\tmodel: " << model.id << " (" << model.name << ")" << std::endl; + // for (const auto &variant : model.variants) { + // std::cerr << "\t\tvariant: " << variant.name << std::endl; + // } + // } + // } + + appconfig_vendors.set_vendors(*GUI::get_app_config()); +} + void ConfigWizard::priv::index_refresh() { index->load_items(page_welcome); @@ -344,12 +426,31 @@ void ConfigWizard::priv::on_custom_setup() set_page(page_firmware); } +void ConfigWizard::priv::on_finish() +{ + const bool is_custom_setup = page_welcome->page_next() == page_firmware; + + if (! is_custom_setup) { + AppConfig *app_config = GUI::get_app_config(); + app_config->set_vendors(appconfig_vendors); + + app_config->set("version_check", page_update->version_check ? "1" : "0"); + app_config->set("preset_update", page_update->preset_update ? "1" : "0"); + } else { + // TODO + } + + q->EndModal(wxID_OK); +} + // Public -ConfigWizard::ConfigWizard(wxWindow *parent, const PresetBundle &bundle) : +ConfigWizard::ConfigWizard(wxWindow *parent) : wxDialog(parent, wxID_ANY, _(L("Configuration Assistant")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), p(new priv(this)) { + p->load_vendors(); + p->index = new ConfigWizardIndex(this); auto *vsizer = new wxBoxSizer(wxVERTICAL); @@ -372,7 +473,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent, const PresetBundle &bundle) : p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); - p->add_page(p->page_welcome = new PageWelcome(this, bundle)); + p->add_page(p->page_welcome = new PageWelcome(this)); p->add_page(p->page_update = new PageUpdate(this)); p->add_page(p->page_vendors = new PageVendors(this)); p->add_page(p->page_firmware = new PageFirmware(this)); @@ -397,35 +498,24 @@ ConfigWizard::ConfigWizard(wxWindow *parent, const PresetBundle &bundle) : p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->go_prev(); }); p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->go_next(); }); + p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->on_finish(); }); } ConfigWizard::~ConfigWizard() {} -void ConfigWizard::run(wxWindow *parent) +void ConfigWizard::run(wxWindow *parent, PresetBundle *preset_bundle) { - PresetBundle bundle; - const auto profiles_dir = fs::path(resources_dir()) / "profiles"; for (fs::directory_iterator it(profiles_dir); it != fs::directory_iterator(); ++it) { if (it->path().extension() == ".ini") { - bundle.load_configbundle(it->path().native(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); + PresetBundle::install_vendor_configbundle(it->path()); } } - // XXX - for (const auto &vendor : bundle.vendors) { - std::cerr << "vendor: " << vendor.name << std::endl; - std::cerr << " URL: " << vendor.config_update_url << std::endl; - for (const auto &model : vendor.models) { - std::cerr << "\tmodel: " << model.id << " (" << model.name << ")" << std::endl; - for (const auto &variant : model.variants) { - std::cerr << "\t\tvariant: " << variant.name << std::endl; - } - } + ConfigWizard wizard(parent); + if (wizard.ShowModal() == wxID_OK) { + preset_bundle->load_presets(*GUI::get_app_config()); } - - ConfigWizard wizard(parent, bundle); - wizard.ShowModal(); } diff --git a/xs/src/slic3r/GUI/ConfigWizard.hpp b/xs/src/slic3r/GUI/ConfigWizard.hpp index 1b14e29be..a06388396 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard.hpp @@ -15,14 +15,15 @@ namespace GUI { class ConfigWizard: public wxDialog { public: - ConfigWizard(wxWindow *parent, const PresetBundle &bundle); + // ConfigWizard(wxWindow *parent, const PresetBundle &bundle); + ConfigWizard(wxWindow *parent); ConfigWizard(ConfigWizard &&) = delete; ConfigWizard(const ConfigWizard &) = delete; ConfigWizard &operator=(ConfigWizard &&) = delete; ConfigWizard &operator=(const ConfigWizard &) = delete; ~ConfigWizard(); - static void run(wxWindow *parent); + static void run(wxWindow *parent, PresetBundle *preset_bundle); private: struct priv; std::unique_ptr p; diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp index ba028c0e8..ab291d40f 100644 --- a/xs/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -9,6 +9,9 @@ #include #include +#include "AppConfig.hpp" +#include "PresetBundle.hpp" + namespace Slic3r { namespace GUI { @@ -34,14 +37,23 @@ struct ConfigWizardPage: wxPanel virtual ~ConfigWizardPage(); - ConfigWizardPage *page_prev() const { return p_prev; } - ConfigWizardPage *page_next() const { return p_next; } + ConfigWizardPage* page_prev() const { return p_prev; } + ConfigWizardPage* page_next() const { return p_next; } ConfigWizardPage* chain(ConfigWizardPage *page); void append_text(wxString text); - void append_widget(wxWindow *widget, int proportion = 0); + void append_widget(wxWindow *widget, int proportion = 0, int flag = wxEXPAND|wxTOP|wxBOTTOM, int border = 10); + + template + void append_thing(T *thing, int proportion = 0, int flag = wxEXPAND|wxTOP|wxBOTTOM, int border = 10) + { + content->Add(thing, proportion, flag, border); + } + + // TODO: remove: + void append_sizer(wxSizer *sizer, int proportion = 0); void append_spacer(int space); - + ConfigWizard::priv *wizard_p() const { return parent->p.get(); } virtual bool Show(bool show = true); @@ -60,7 +72,7 @@ struct PageWelcome: ConfigWizardPage wxPanel *others_buttons; unsigned variants_checked; - PageWelcome(ConfigWizard *parent, const PresetBundle &bundle); + PageWelcome(ConfigWizard *parent); virtual wxPanel* extra_buttons() { return others_buttons; } virtual void on_page_set(); @@ -70,6 +82,9 @@ struct PageWelcome: ConfigWizardPage struct PageUpdate: ConfigWizardPage { + bool version_check; + bool preset_update; + PageUpdate(ConfigWizard *parent); void presets_update_enable(bool enable); @@ -124,6 +139,9 @@ private: struct ConfigWizard::priv { ConfigWizard *q; + AppConfig appconfig_vendors; + PresetBundle bundle_vendors; + wxBoxSizer *topsizer = nullptr; wxBoxSizer *btnsizer = nullptr; ConfigWizardPage *page_current = nullptr; @@ -143,6 +161,7 @@ struct ConfigWizard::priv priv(ConfigWizard *q) : q(q) {} + void load_vendors(); void add_page(ConfigWizardPage *page); void index_refresh(); void set_page(ConfigWizardPage *page); @@ -152,6 +171,7 @@ struct ConfigWizard::priv void on_other_vendors(); void on_custom_setup(); + void on_finish(); }; diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index 53288067c..80e232287 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -353,18 +353,13 @@ void add_debug_menu(wxMenuBar *menu, int event_language_change) //#endif } -void open_config_wizard() +void open_config_wizard(PresetBundle *preset_bundle) { if (g_wxMainFrame == nullptr) { throw std::runtime_error("Main frame not set"); } - // auto *wizard = new ConfigWizard(static_cast(g_wxMainFrame)); // FIXME: lifetime - - // wizard->run(); - ConfigWizard::run(g_wxMainFrame); - - // show_info(g_wxMainFrame, "After wizard", "After wizard"); + ConfigWizard::run(g_wxMainFrame, preset_bundle); } void open_preferences_dialog(int event_preferences) diff --git a/xs/src/slic3r/GUI/GUI.hpp b/xs/src/slic3r/GUI/GUI.hpp index 1f93e18e9..98a124091 100644 --- a/xs/src/slic3r/GUI/GUI.hpp +++ b/xs/src/slic3r/GUI/GUI.hpp @@ -86,7 +86,7 @@ wxColour* get_sys_label_clr(); void add_debug_menu(wxMenuBar *menu, int event_language_change); // Opens the first-time configuration wizard -void open_config_wizard(); +void open_config_wizard(PresetBundle *preset_bundle); // Create "Preferences" dialog after selecting menu "Preferences" in Perl part void open_preferences_dialog(int event_preferences); diff --git a/xs/src/slic3r/GUI/Preset.cpp b/xs/src/slic3r/GUI/Preset.cpp index c437f8b41..40afca144 100644 --- a/xs/src/slic3r/GUI/Preset.cpp +++ b/xs/src/slic3r/GUI/Preset.cpp @@ -2,6 +2,7 @@ #include #include "Preset.hpp" +#include "AppConfig.hpp" #include #include @@ -175,6 +176,16 @@ bool Preset::update_compatible_with_printer(const Preset &active_printer, const return this->is_compatible = is_compatible_with_printer(active_printer, extra_config); } +void Preset::set_visible_from_appconfig(const AppConfig &app_config) +{ + if (vendor == nullptr) { return; } + const std::string &model = config.opt_string("printer_model"); + const std::string &variant = config.opt_string("printer_variant"); + if (model.empty() || variant.empty()) { return; } + is_visible = app_config.get_variant(vendor->id, model, variant); + std::cerr << vendor->id << " / " << model << " / " << variant << ": visible: " << is_visible << std::endl; +} + const std::vector& Preset::print_options() { static std::vector s_opts { diff --git a/xs/src/slic3r/GUI/Preset.hpp b/xs/src/slic3r/GUI/Preset.hpp index 78e15bada..075eed9af 100644 --- a/xs/src/slic3r/GUI/Preset.hpp +++ b/xs/src/slic3r/GUI/Preset.hpp @@ -13,6 +13,8 @@ class wxItemContainer; namespace Slic3r { +class AppConfig; + enum ConfigFileType { CONFIG_FILE_TYPE_UNKNOWN, @@ -35,14 +37,14 @@ public: PrinterVariant() {} PrinterVariant(const std::string &name) : name(name) {} std::string name; - bool enabled = true; + // bool enabled = true; // TODO: remove these? }; struct PrinterModel { PrinterModel() {} std::string id; std::string name; - bool enabled = true; + // bool enabled = true; std::vector variants; PrinterVariant* variant(const std::string &name) { for (auto &v : this->variants) @@ -85,7 +87,7 @@ public: bool is_external = false; // System preset is read-only. bool is_system = false; - // Preset is visible, if it is compatible with the active Printer. + // Preset is visible, if it is compatible with the active Printer. TODO: fix // Also the "default" preset is only visible, if it is the only preset in the list. bool is_visible = true; // Has this preset been modified? @@ -131,6 +133,9 @@ public: // Mark this preset as compatible if it is compatible with active_printer. bool update_compatible_with_printer(const Preset &active_printer, const DynamicPrintConfig *extra_config); + // Set is_visible according to application config + void set_visible_from_appconfig(const AppConfig &app_config); + // Resize the extruder specific fields, initialize them with the content of the 1st extruder. void set_num_extruders(unsigned int n) { set_num_extruders(this->config, n); } @@ -162,6 +167,13 @@ public: PresetCollection(Preset::Type type, const std::vector &keys); ~PresetCollection(); + typedef std::deque::iterator Iterator; + typedef std::deque::const_iterator ConstIterator; + Iterator begin() { return m_presets.begin() + 1; } + ConstIterator begin() const { return m_presets.begin() + 1; } + Iterator end() { return m_presets.end(); } + ConstIterator end() const { return m_presets.end(); } + void reset(bool delete_files); Preset::Type type() const { return m_type; } diff --git a/xs/src/slic3r/GUI/PresetBundle.cpp b/xs/src/slic3r/GUI/PresetBundle.cpp index 241f0433e..8645a4f2d 100644 --- a/xs/src/slic3r/GUI/PresetBundle.cpp +++ b/xs/src/slic3r/GUI/PresetBundle.cpp @@ -4,6 +4,7 @@ #include "PresetBundle.hpp" #include "BitmapCache.hpp" +#include // XXX #include #include #include @@ -105,7 +106,7 @@ void PresetBundle::setup_directories() std::initializer_list paths = { data_dir, data_dir / "vendor", - data_dir / "cache", + data_dir / "cache", // TODO: rename as vendor-cache? (Check usage elsewhere!) #ifdef SLIC3R_PROFILE_USE_PRESETS_SUBDIR // Store the print/filament/printer presets into a "presets" directory. data_dir / "presets", @@ -200,8 +201,12 @@ static inline std::string remove_ini_suffix(const std::string &name) // If the "vendor" section is missing, enable all models and variants of the particular vendor. void PresetBundle::load_installed_printers(const AppConfig &config) { - // TODO - // m_storage + std::cerr << "load_installed_printers()" << std::endl; + + for (auto &preset : printers) { + std::cerr << "preset: printer: " << preset.name << std::endl; + preset.set_visible_from_appconfig(config); + } } // Load selections (current print, current filaments, current printer) from config.ini @@ -221,6 +226,10 @@ void PresetBundle::load_selections(const AppConfig &config) break; this->set_filament_preset(i, remove_ini_suffix(config.get("presets", name))); } + + // Update visibility of presets based on application vendor / model / variant configuration. + this->load_installed_printers(config); + // Update visibility of presets based on their compatibility with the active printer. // Always try to select a compatible print and filament preset to the current printer preset, // as the application may have been closed with an active "external" preset, which does not @@ -708,6 +717,11 @@ static void load_vendor_profile(const boost::property_tree::ptree &tree, VendorP void PresetBundle::install_vendor_configbundle(const std::string &src_path0) { boost::filesystem::path src_path(src_path0); + install_vendor_configbundle(src_path); +} + +void PresetBundle::install_vendor_configbundle(const boost::filesystem::path &src_path) +{ boost::filesystem::copy_file(src_path, (boost::filesystem::path(data_dir()) / "vendor" / src_path.filename()).make_preferred(), boost::filesystem::copy_option::overwrite_if_exists); } diff --git a/xs/src/slic3r/GUI/PresetBundle.hpp b/xs/src/slic3r/GUI/PresetBundle.hpp index 4949e0e03..4189e6c46 100644 --- a/xs/src/slic3r/GUI/PresetBundle.hpp +++ b/xs/src/slic3r/GUI/PresetBundle.hpp @@ -5,6 +5,7 @@ #include "Preset.hpp" #include +#include namespace Slic3r { @@ -88,6 +89,7 @@ public: // Install the Vendor specific config bundle into user's directory. void install_vendor_configbundle(const std::string &src_path); + static void install_vendor_configbundle(const boost::filesystem::path &src_path); // Export a config bundle file containing all the presets and the names of the active presets. void export_configbundle(const std::string &path); // , const DynamicPrintConfig &settings); diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp index 11064d2f0..040d326b5 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.cpp +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -11,9 +11,9 @@ #include "libslic3r/Utils.hpp" #include "slic3r/GUI/GUI.hpp" -// #include "slic3r/GUI/AppConfig.hpp" #include "slic3r/GUI/PresetBundle.hpp" #include "slic3r/Utils/Http.hpp" +#include "slic3r/Utils/Semver.hpp" namespace fs = boost::filesystem; diff --git a/xs/xsp/GUI.xsp b/xs/xsp/GUI.xsp index 5115eda64..ad7f69a2d 100644 --- a/xs/xsp/GUI.xsp +++ b/xs/xsp/GUI.xsp @@ -54,8 +54,8 @@ int combochecklist_get_flags(SV *ui) void set_app_config(AppConfig *app_config) %code%{ Slic3r::GUI::set_app_config(app_config); %}; -void open_config_wizard() - %code%{ Slic3r::GUI::open_config_wizard(); %}; +void open_config_wizard(PresetBundle *preset_bundle) + %code%{ Slic3r::GUI::open_config_wizard(preset_bundle); %}; void open_preferences_dialog(int preferences_event) %code%{ Slic3r::GUI::open_preferences_dialog(preferences_event); %}; diff --git a/xs/xsp/GUI_Preset.xsp b/xs/xsp/GUI_Preset.xsp index 84efdde53..1187a1cf5 100644 --- a/xs/xsp/GUI_Preset.xsp +++ b/xs/xsp/GUI_Preset.xsp @@ -147,7 +147,7 @@ PresetCollection::arrayref() void install_vendor_configbundle(const char *path) %code%{ try { - THIS->install_vendor_configbundle(path); + THIS->install_vendor_configbundle(std::string(path)); } catch (std::exception& e) { croak("Installing a vendor config bundle %s failed:\n%s\n", path, e.what()); } From 8422cf93c0e550e75a40522c56b9570c72d32169 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Thu, 5 Apr 2018 16:10:44 +0200 Subject: [PATCH 04/38] ConfigWizard: Finalize custom setup --- xs/src/libslic3r/Config.hpp | 1 + xs/src/libslic3r/PrintConfig.cpp | 2 +- xs/src/slic3r/GUI/ConfigWizard.cpp | 246 +++++++++++++++------ xs/src/slic3r/GUI/ConfigWizard_private.hpp | 40 +++- 4 files changed, 212 insertions(+), 77 deletions(-) diff --git a/xs/src/libslic3r/Config.hpp b/xs/src/libslic3r/Config.hpp index 6eb307c5c..06db9efef 100644 --- a/xs/src/libslic3r/Config.hpp +++ b/xs/src/libslic3r/Config.hpp @@ -659,6 +659,7 @@ public: ConfigOptionPoints() : ConfigOptionVector() {} explicit ConfigOptionPoints(size_t n, const Pointf &value) : ConfigOptionVector(n, value) {} explicit ConfigOptionPoints(std::initializer_list il) : ConfigOptionVector(std::move(il)) {} + explicit ConfigOptionPoints(const std::vector &values) : ConfigOptionVector(values) {} static ConfigOptionType static_type() { return coPoints; } ConfigOptionType type() const override { return static_type(); } diff --git a/xs/src/libslic3r/PrintConfig.cpp b/xs/src/libslic3r/PrintConfig.cpp index 657e5a452..5795f044b 100644 --- a/xs/src/libslic3r/PrintConfig.cpp +++ b/xs/src/libslic3r/PrintConfig.cpp @@ -1620,7 +1620,7 @@ PrintConfigDef::PrintConfigDef() "temperature control commands in the output."); def->cli = "temperature=i@"; def->full_label = L("Temperature"); - def->max = 0; + def->min = 0; def->max = max_temp; def->default_value = new ConfigOptionInts { 200 }; diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index 556b91a46..edf958bd8 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -74,17 +74,7 @@ void ConfigWizardPage::append_text(wxString text) auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); widget->Wrap(CONTENT_WIDTH); widget->SetMinSize(wxSize(CONTENT_WIDTH, -1)); - content->Add(widget, 0, wxALIGN_LEFT | wxTOP | wxBOTTOM, 10); -} - -void ConfigWizardPage::append_widget(wxWindow *widget, int proportion, int flag, int border) -{ - content->Add(widget, proportion, flag, border); -} - -void ConfigWizardPage::append_sizer(wxSizer *sizer, int proportion) -{ - content->Add(sizer, proportion, wxEXPAND | wxTOP | wxBOTTOM, 10); + append(widget); } void ConfigWizardPage::append_spacer(int space) @@ -103,7 +93,6 @@ void ConfigWizardPage::enable_next(bool enable) { parent->p->enable_next(enable) // Wizard pages -// PageWelcome::PageWelcome(ConfigWizard *parent, const PresetBundle &bundle) : PageWelcome::PageWelcome(ConfigWizard *parent) : ConfigWizardPage(parent, _(L("Welcome to the Slic3r Configuration assistant")), _(L("Welcome"))), others_buttons(new wxPanel(parent)), @@ -164,13 +153,13 @@ PageWelcome::PageWelcome(ConfigWizard *parent) : printer_grid->Add(panel); } - append_widget(printer_picker); + append(printer_picker); } { auto *sizer = new wxBoxSizer(wxHORIZONTAL); auto *other_vendors = new wxButton(others_buttons, wxID_ANY, _(L("Other vendors"))); - other_vendors->Disable(); + // other_vendors->Disable(); // XXX auto *custom_setup = new wxButton(others_buttons, wxID_ANY, _(L("Custom setup"))); sizer->Add(other_vendors); @@ -201,36 +190,24 @@ PageUpdate::PageUpdate(ConfigWizard *parent) : preset_update(true) { const AppConfig *app_config = GUI::get_app_config(); - + append_text(_(L("TODO: text"))); auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _(L("Check for Slic3r updates"))); box_slic3r->SetValue(app_config->get("version_check") == "1"); - append_widget(box_slic3r); + append(box_slic3r); append_text(_(L("TODO: text"))); auto *box_presets = new wxCheckBox(this, wxID_ANY, _(L("Update built-in Presets automatically"))); box_presets->SetValue(app_config->get("preset_update") == "1"); - append_widget(box_presets); + append(box_presets); box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); } -void PageUpdate::presets_update_enable(bool enable) -{ - // TODO -} - PageVendors::PageVendors(ConfigWizard *parent) : ConfigWizardPage(parent, _(L("Other Vendors")), _(L("Other Vendors"))) { - enum { - INDENT_SPACING = 30, - VERTICAL_SPACING = 10, - }; - - append_text(_(L("Other vendors! TODO: This text."))); - const PresetBundle &bundle = wizard_p()->bundle_vendors; auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); boldfont.SetWeight(wxFONTWEIGHT_BOLD); @@ -240,16 +217,16 @@ PageVendors::PageVendors(ConfigWizard *parent) : auto *label_vendor = new wxStaticText(this, wxID_ANY, vendor.name); label_vendor->SetFont(boldfont); - append_thing(label_vendor, 0, 0, 0); + append(label_vendor, 0, 0, 0); for (const auto &model : vendor.models) { auto *label_model = new wxStaticText(this, wxID_ANY, model.name); label_model->SetFont(boldfont); - append_thing(label_model, 0, wxLEFT, INDENT_SPACING); + append(label_model, 0, wxLEFT, INDENT_SPACING); for (const auto &variant : model.variants) { auto *cbox = new wxCheckBox(this, wxID_ANY, variant.name); - append_thing(cbox, 0, wxEXPAND | wxLEFT, 2 * INDENT_SPACING); + append(cbox, 0, wxEXPAND | wxLEFT, 2 * INDENT_SPACING); cbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { // TODO }); @@ -260,21 +237,169 @@ PageVendors::PageVendors(ConfigWizard *parent) : } } + PageFirmware::PageFirmware(ConfigWizard *parent) : - ConfigWizardPage(parent, _(L("Firmware Type")), _(L("Firmware"))) -{} + ConfigWizardPage(parent, _(L("Firmware Type")), _(L("Firmware"))), + gcode_opt(print_config_def.options["gcode_flavor"]), + gcode_picker(nullptr) +{ + append_text(_(L("Choose the type of firmware used by your printer."))); + append_text(gcode_opt.tooltip); + + wxArrayString choices; + choices.Alloc(gcode_opt.enum_labels.size()); + for (const auto &label : gcode_opt.enum_labels) { + choices.Add(label); + } + + gcode_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); + const auto &enum_values = gcode_opt.enum_values; + auto needle = enum_values.cend(); + if (gcode_opt.default_value != nullptr) { + needle = std::find(enum_values.cbegin(), enum_values.cend(), gcode_opt.default_value->serialize()); + } + if (needle != enum_values.cend()) { + gcode_picker->SetSelection(needle - enum_values.cbegin()); + } else { + gcode_picker->SetSelection(0); + } + + append(gcode_picker); +} + +void PageFirmware::apply_custom_config(DynamicPrintConfig &config) +{ + ConfigOptionEnum opt; + + auto sel = gcode_picker->GetSelection(); + if (sel != wxNOT_FOUND && opt.deserialize(gcode_picker->GetString(sel).ToStdString())) { + config.set_key_value("gcode_flavor", &opt); + } +} PageBedShape::PageBedShape(ConfigWizard *parent) : - ConfigWizardPage(parent, _(L("Bed Shape and Size")), _(L("Bed Shape"))) -{} + ConfigWizardPage(parent, _(L("Bed Shape and Size")), _(L("Bed Shape"))), + shape_panel(new BedShapePanel(this)) +{ + append_text(_(L("Set the shape of your printer's bed."))); + + shape_panel->build_panel(wizard_p()->custom_config.option("bed_shape")); + append(shape_panel); +} + +void PageBedShape::apply_custom_config(DynamicPrintConfig &config) +{ + const auto points(shape_panel->GetValue()); + auto *opt = new ConfigOptionPoints(points); + config.set_key_value("bed_shape", opt); +} PageDiameters::PageDiameters(ConfigWizard *parent) : - ConfigWizardPage(parent, _(L("Filament and Nozzle Diameter")), _(L("Print Diameters"))) -{} + ConfigWizardPage(parent, _(L("Filament and Nozzle Diameters")), _(L("Print Diameters"))), + spin_nozzle(new wxSpinCtrlDouble(this, wxID_ANY)), + spin_filam(new wxSpinCtrlDouble(this, wxID_ANY)) +{ + spin_nozzle->SetDigits(2); + spin_nozzle->SetIncrement(0.1); + const auto &def_nozzle = print_config_def.options["nozzle_diameter"]; + auto *default_nozzle = dynamic_cast(def_nozzle.default_value); + spin_nozzle->SetValue(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5); + + spin_filam->SetDigits(2); + spin_filam->SetIncrement(0.25); + const auto &def_filam = print_config_def.options["filament_diameter"]; + auto *default_filam = dynamic_cast(def_filam.default_value); + spin_filam->SetValue(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0); + + append_text(_(L("Enter the diameter of your printer's hot end nozzle."))); + + auto *sizer_nozzle = new wxFlexGridSizer(3, 5, 5); + auto *text_nozzle = new wxStaticText(this, wxID_ANY, _(L("Nozzle Diameter:"))); + 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(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_nozzle); + + append_spacer(VERTICAL_SPACING); + + append_text(_(L("Enter the diameter of your filament."))); + append_text(_(L("Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average."))); + + auto *sizer_filam = new wxFlexGridSizer(3, 5, 5); + auto *text_filam = new wxStaticText(this, wxID_ANY, _(L("Filament Diameter:"))); + 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(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()); + config.set_key_value("nozzle_diameter", opt_nozzle); + auto *opt_filam = new ConfigOptionFloats(1, spin_filam->GetValue()); + config.set_key_value("filament_diameter", opt_filam); +} PageTemperatures::PageTemperatures(ConfigWizard *parent) : - ConfigWizardPage(parent, _(L("Bed and Extruder Temperature")), _(L("Temperatures"))) -{} + ConfigWizardPage(parent, _(L("Extruder and Bed Temperatures")), _(L("Temperatures"))), + spin_extr(new wxSpinCtrl(this, wxID_ANY)), + spin_bed(new wxSpinCtrl(this, wxID_ANY)) +{ + spin_extr->SetIncrement(5); + const auto &def_extr = print_config_def.options["temperature"]; + spin_extr->SetRange(def_extr.min, def_extr.max); + auto *default_extr = dynamic_cast(def_extr.default_value); + spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); + + spin_bed->SetIncrement(5); + const auto &def_bed = print_config_def.options["bed_temperature"]; + spin_bed->SetRange(def_bed.min, def_bed.max); + auto *default_bed = dynamic_cast(def_bed.default_value); + spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0); + + append_text(_(L("Enter the temperature needed for extruding your filament."))); + append_text(_(L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS."))); + + auto *sizer_extr = new wxFlexGridSizer(3, 5, 5); + auto *text_extr = new wxStaticText(this, wxID_ANY, _(L("Extrusion Temperature:"))); + auto *unit_extr = new wxStaticText(this, wxID_ANY, _(L("°C"))); + sizer_extr->AddGrowableCol(0, 1); + sizer_extr->Add(text_extr, 0, wxALIGN_CENTRE_VERTICAL); + sizer_extr->Add(spin_extr); + sizer_extr->Add(unit_extr, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_extr); + + append_spacer(VERTICAL_SPACING); + + append_text(_(L("Enter the bed temperature needed for getting your filament to stick to your heated bed."))); + append_text(_(L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed."))); + + auto *sizer_bed = new wxFlexGridSizer(3, 5, 5); + auto *text_bed = new wxStaticText(this, wxID_ANY, _(L("Bed Temperature:"))); + auto *unit_bed = new wxStaticText(this, wxID_ANY, _(L("°C"))); + sizer_bed->AddGrowableCol(0, 1); + sizer_bed->Add(text_bed, 0, wxALIGN_CENTRE_VERTICAL); + sizer_bed->Add(spin_bed); + sizer_bed->Add(unit_bed, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_bed); +} + +void PageTemperatures::apply_custom_config(DynamicPrintConfig &config) +{ + auto *opt_extr = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("temperature", opt_extr); + auto *opt_extr1st = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("first_layer_temperature", opt_extr1st); + auto *opt_bed = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("bed_temperature", opt_bed); + auto *opt_bed1st = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("first_layer_bed_temperature", opt_bed1st); +} // Index @@ -349,25 +474,12 @@ void ConfigWizardIndex::on_paint(wxPaintEvent & evt) void ConfigWizard::priv::load_vendors() { const auto vendor_dir = fs::path(Slic3r::data_dir()) / "vendor"; - // const auto profiles_dir = fs::path(resources_dir()) / "profiles"; for (fs::directory_iterator it(vendor_dir); it != fs::directory_iterator(); ++it) { if (it->path().extension() == ".ini") { bundle_vendors.load_configbundle(it->path().native(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); } } - // XXX - // for (const auto &vendor : bundle_vendors.vendors) { - // std::cerr << "vendor: " << vendor.name << std::endl; - // std::cerr << " URL: " << vendor.config_update_url << std::endl; - // for (const auto &model : vendor.models) { - // std::cerr << "\tmodel: " << model.id << " (" << model.name << ")" << std::endl; - // for (const auto &variant : model.variants) { - // std::cerr << "\t\tvariant: " << variant.name << std::endl; - // } - // } - // } - appconfig_vendors.set_vendors(*GUI::get_app_config()); } @@ -426,21 +538,22 @@ void ConfigWizard::priv::on_custom_setup() set_page(page_firmware); } -void ConfigWizard::priv::on_finish() +void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle) { const bool is_custom_setup = page_welcome->page_next() == page_firmware; if (! is_custom_setup) { - AppConfig *app_config = GUI::get_app_config(); app_config->set_vendors(appconfig_vendors); - app_config->set("version_check", page_update->version_check ? "1" : "0"); app_config->set("preset_update", page_update->preset_update ? "1" : "0"); + app_config->reset_selections(); // XXX: only on "fresh start"? + preset_bundle->load_presets(*app_config); } else { - // TODO + for (ConfigWizardPage *page = page_firmware; page != nullptr; page = page->page_next()) { + page->apply_custom_config(custom_config); + } + preset_bundle->load_config("My Settings", custom_config); } - - q->EndModal(wxID_OK); } // Public @@ -450,6 +563,10 @@ ConfigWizard::ConfigWizard(wxWindow *parent) : p(new priv(this)) { p->load_vendors(); + std::unique_ptr custom_config_defaults(DynamicPrintConfig::new_from_defaults_keys({ + "gcode_flavor", "bed_shape", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", + })); + p->custom_config.apply(*custom_config_defaults); p->index = new ConfigWizardIndex(this); @@ -461,10 +578,8 @@ ConfigWizard::ConfigWizard(wxWindow *parent) : p->topsizer->Add(p->index, 0, wxEXPAND); p->topsizer->AddSpacer(INDEX_MARGIN); - // TODO: btn labels vs default w/ icons ... use arrows from resources? (no apply icon) - // Also: http://docs.wxwidgets.org/3.0/page_stockitems.html - p->btn_prev = new wxButton(this, wxID_BACKWARD, _(L("< &Back"))); - p->btn_next = new wxButton(this, wxID_FORWARD, _(L("&Next >"))); + p->btn_prev = new wxButton(this, wxID_BACKWARD); + p->btn_next = new wxButton(this, wxID_FORWARD); p->btn_finish = new wxButton(this, wxID_APPLY, _(L("&Finish"))); p->btn_cancel = new wxButton(this, wxID_CANCEL); p->btnsizer->AddStretchSpacer(); @@ -498,7 +613,8 @@ ConfigWizard::ConfigWizard(wxWindow *parent) : p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->go_prev(); }); p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->go_next(); }); - p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->on_finish(); }); + // p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->on_finish(); }); + p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->EndModal(wxID_OK); }); } ConfigWizard::~ConfigWizard() {} @@ -514,7 +630,7 @@ void ConfigWizard::run(wxWindow *parent, PresetBundle *preset_bundle) ConfigWizard wizard(parent); if (wizard.ShowModal() == wxID_OK) { - preset_bundle->load_presets(*GUI::get_app_config()); + wizard.p->apply_config(GUI::get_app_config(), preset_bundle); } } diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp index ab291d40f..1d9519bc3 100644 --- a/xs/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -8,9 +8,13 @@ #include #include #include +#include +#include +#include "libslic3r/PrintConfig.hpp" #include "AppConfig.hpp" #include "PresetBundle.hpp" +#include "BedShapeDialog.hpp" namespace Slic3r { @@ -21,6 +25,8 @@ enum { DIALOG_MARGIN = 15, INDEX_MARGIN = 40, BTN_SPACING = 10, + INDENT_SPACING = 30, + VERTICAL_SPACING = 10, }; struct ConfigWizardPage: wxPanel @@ -41,17 +47,13 @@ struct ConfigWizardPage: wxPanel ConfigWizardPage* page_next() const { return p_next; } ConfigWizardPage* chain(ConfigWizardPage *page); - void append_text(wxString text); - void append_widget(wxWindow *widget, int proportion = 0, int flag = wxEXPAND|wxTOP|wxBOTTOM, int border = 10); - template - void append_thing(T *thing, int proportion = 0, int flag = wxEXPAND|wxTOP|wxBOTTOM, int border = 10) + void append(T *thing, int proportion = 0, int flag = wxEXPAND|wxTOP|wxBOTTOM, int border = 10) { content->Add(thing, proportion, flag, border); } - // TODO: remove: - void append_sizer(wxSizer *sizer, int proportion = 0); + void append_text(wxString text); void append_spacer(int space); ConfigWizard::priv *wizard_p() const { return parent->p.get(); } @@ -60,6 +62,7 @@ struct ConfigWizardPage: wxPanel virtual bool Hide() { return Show(false); } virtual wxPanel* extra_buttons() { return nullptr; } virtual void on_page_set() {} + virtual void apply_custom_config(DynamicPrintConfig &config) {} void enable_next(bool enable); private: @@ -84,10 +87,8 @@ struct PageUpdate: ConfigWizardPage { bool version_check; bool preset_update; - - PageUpdate(ConfigWizard *parent); - void presets_update_enable(bool enable); + PageUpdate(ConfigWizard *parent); }; struct PageVendors: ConfigWizardPage @@ -97,22 +98,37 @@ struct PageVendors: ConfigWizardPage struct PageFirmware: ConfigWizardPage { + const ConfigOptionDef &gcode_opt; + wxChoice *gcode_picker; + PageFirmware(ConfigWizard *parent); + virtual void apply_custom_config(DynamicPrintConfig &config); }; struct PageBedShape: ConfigWizardPage { + BedShapePanel *shape_panel; + PageBedShape(ConfigWizard *parent); + virtual void apply_custom_config(DynamicPrintConfig &config); }; struct PageDiameters: ConfigWizardPage { + wxSpinCtrlDouble *spin_nozzle; + wxSpinCtrlDouble *spin_filam; + PageDiameters(ConfigWizard *parent); + virtual void apply_custom_config(DynamicPrintConfig &config); }; struct PageTemperatures: ConfigWizardPage { + wxSpinCtrl *spin_extr; + wxSpinCtrl *spin_bed; + PageTemperatures(ConfigWizard *parent); + virtual void apply_custom_config(DynamicPrintConfig &config); }; @@ -133,7 +149,7 @@ private: std::vector items; std::vector::const_iterator item_active; - void on_paint(wxPaintEvent & evt); + void on_paint(wxPaintEvent &evt); }; struct ConfigWizard::priv @@ -141,6 +157,7 @@ struct ConfigWizard::priv ConfigWizard *q; AppConfig appconfig_vendors; PresetBundle bundle_vendors; + DynamicPrintConfig custom_config; wxBoxSizer *topsizer = nullptr; wxBoxSizer *btnsizer = nullptr; @@ -171,7 +188,8 @@ struct ConfigWizard::priv void on_other_vendors(); void on_custom_setup(); - void on_finish(); + + void apply_config(AppConfig *app_config, PresetBundle *preset_bundle); }; From d1c1dcbe8f380c12120228c7318f243633b27490 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Thu, 5 Apr 2018 18:30:03 +0200 Subject: [PATCH 05/38] ConfigWizard: Factor out a PrinterPicker widget, finalize other vendors page --- .../{MK2S.png => PrusaResearch_MK2S.png} | Bin .../{MK2SMM.png => PrusaResearch_MK2SMM.png} | Bin .../{MK3.png => PrusaResearch_MK3.png} | Bin xs/src/slic3r/GUI/ConfigWizard.cpp | 220 ++++++++++++------ xs/src/slic3r/GUI/ConfigWizard_private.hpp | 16 +- 5 files changed, 159 insertions(+), 77 deletions(-) rename resources/icons/printers/{MK2S.png => PrusaResearch_MK2S.png} (100%) rename resources/icons/printers/{MK2SMM.png => PrusaResearch_MK2SMM.png} (100%) rename resources/icons/printers/{MK3.png => PrusaResearch_MK3.png} (100%) diff --git a/resources/icons/printers/MK2S.png b/resources/icons/printers/PrusaResearch_MK2S.png similarity index 100% rename from resources/icons/printers/MK2S.png rename to resources/icons/printers/PrusaResearch_MK2S.png diff --git a/resources/icons/printers/MK2SMM.png b/resources/icons/printers/PrusaResearch_MK2SMM.png similarity index 100% rename from resources/icons/printers/MK2SMM.png rename to resources/icons/printers/PrusaResearch_MK2SMM.png diff --git a/resources/icons/printers/MK3.png b/resources/icons/printers/PrusaResearch_MK3.png similarity index 100% rename from resources/icons/printers/MK3.png rename to resources/icons/printers/PrusaResearch_MK3.png diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index edf958bd8..914ebb9a1 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -23,6 +23,86 @@ namespace Slic3r { namespace GUI { +// FIXME: scrolling + + +// Printer model picker GUI control + +struct PrinterPickerEvent : public wxEvent +{ + std::string vendor_id; + std::string model_id; + std::string variant_name; + bool enable; + + PrinterPickerEvent(wxEventType eventType, int winid, std::string vendor_id, std::string model_id, std::string variant_name, bool enable) : + wxEvent(winid, eventType), + vendor_id(std::move(vendor_id)), + model_id(std::move(model_id)), + variant_name(std::move(variant_name)), + enable(enable) + {} + + virtual wxEvent *Clone() const + { + return new PrinterPickerEvent(*this); + } +}; + +wxDEFINE_EVENT(EVT_PRINTER_PICK, PrinterPickerEvent); + +PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, const AppConfig &appconfig_vendors) : + wxPanel(parent), + variants_checked(0) +{ + const auto vendor_id = vendor.id; + const auto &models = vendor.models; + + auto *printer_grid = new wxFlexGridSizer(models.size(), 0, 20); + printer_grid->SetFlexibleDirection(wxVERTICAL); + SetSizer(printer_grid); + + auto namefont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + namefont.SetWeight(wxFONTWEIGHT_BOLD); + + for (auto model = models.cbegin(); model != models.cend(); ++model) { + auto *panel = new wxPanel(this); + auto *sizer = new wxBoxSizer(wxVERTICAL); + panel->SetSizer(sizer); + + auto *title = new wxStaticText(panel, wxID_ANY, model->name, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + title->SetFont(namefont); + sizer->Add(title, 0, wxBOTTOM, 3); + + auto bitmap_file = wxString::Format("printers/%s_%s.png", vendor.id, model->id); + wxBitmap bitmap(GUI::from_u8(Slic3r::var(bitmap_file.ToStdString())), wxBITMAP_TYPE_PNG); + auto *bitmap_widget = new wxStaticBitmap(panel, wxID_ANY, bitmap); + sizer->Add(bitmap_widget, 0, wxBOTTOM, 3); + + sizer->AddSpacer(20); + + const auto model_id = model->id; + + for (const auto &variant : model->variants) { + const auto variant_name = variant.name; + auto *cbox = new wxCheckBox(panel, wxID_ANY, wxString::Format("%s %s %s", variant.name, _(L("mm")), _(L("nozzle")))); + bool enabled = appconfig_vendors.get_variant("PrusaResearch", model_id, variant_name); + variants_checked += enabled; + cbox->SetValue(enabled); + sizer->Add(cbox, 0, wxBOTTOM, 3); + cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { + this->variants_checked += event.IsChecked() ? 1 : -1; + PrinterPickerEvent evt(EVT_PRINTER_PICK, this->GetId(), std::move(vendor_id), std::move(model_id), std::move(variant_name), event.IsChecked()); + this->AddPendingEvent(evt); + }); + } + + printer_grid->Add(panel); + } + +} + + // Wizard page base ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname) : @@ -95,8 +175,8 @@ void ConfigWizardPage::enable_next(bool enable) { parent->p->enable_next(enable) PageWelcome::PageWelcome(ConfigWizard *parent) : ConfigWizardPage(parent, _(L("Welcome to the Slic3r Configuration assistant")), _(L("Welcome"))), - others_buttons(new wxPanel(parent)), - variants_checked(0) + printer_picker(nullptr), + others_buttons(new wxPanel(parent)) { append_text(_(L("Hello, welcome to Slic3r Prusa Edition! TODO: This text."))); @@ -104,73 +184,34 @@ PageWelcome::PageWelcome(ConfigWizard *parent) : const auto &vendors = bundle.vendors; const auto vendor_prusa = std::find(vendors.cbegin(), vendors.cend(), VendorProfile("PrusaResearch")); - const AppConfig &appconfig_vendors = wizard_p()->appconfig_vendors; - if (vendor_prusa != vendors.cend()) { const auto &models = vendor_prusa->models; - auto *printer_picker = new wxPanel(this); - auto *printer_grid = new wxFlexGridSizer(models.size(), 0, 20); - printer_grid->SetFlexibleDirection(wxVERTICAL); - printer_picker->SetSizer(printer_grid); + AppConfig &appconfig_vendors = this->wizard_p()->appconfig_vendors; - auto namefont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - namefont.SetWeight(wxFONTWEIGHT_BOLD); - - for (auto model = models.cbegin(); model != models.cend(); ++model) { - auto *panel = new wxPanel(printer_picker); - auto *sizer = new wxBoxSizer(wxVERTICAL); - panel->SetSizer(sizer); - - auto *title = new wxStaticText(panel, wxID_ANY, model->name, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - title->SetFont(namefont); - sizer->Add(title, 0, wxBOTTOM, 3); - - auto bitmap_file = wxString::Format("printers/%s.png", model->id); - wxBitmap bitmap(GUI::from_u8(Slic3r::var(bitmap_file.ToStdString())), wxBITMAP_TYPE_PNG); - auto *bitmap_widget = new wxStaticBitmap(panel, wxID_ANY, bitmap); - sizer->Add(bitmap_widget, 0, wxBOTTOM, 3); - - sizer->AddSpacer(20); - - std::string model_id = model->id; - - for (const auto &variant : model->variants) { - std::string variant_name = variant.name; - auto *cbox = new wxCheckBox(panel, wxID_ANY, wxString::Format("%s %s %s", variant.name, _(L("mm")), _(L("nozzle")))); - bool enabled = appconfig_vendors.get_variant("PrusaResearch", model_id, variant_name); - variants_checked += enabled; - cbox->SetValue(enabled); - sizer->Add(cbox, 0, wxBOTTOM, 3); - cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { - this->variants_checked += event.IsChecked() ? 1 : -1; - this->on_variant_checked(); - AppConfig &appconfig_vendors = this->wizard_p()->appconfig_vendors; - appconfig_vendors.set_variant("PrusaResearch", model_id, variant_name, event.IsChecked()); - }); - } - - printer_grid->Add(panel); - } + printer_picker = new PrinterPicker(this, *vendor_prusa, appconfig_vendors); + printer_picker->Bind(EVT_PRINTER_PICK, [this, &appconfig_vendors](const PrinterPickerEvent &evt) { + appconfig_vendors.set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); + this->on_variant_checked(); + }); append(printer_picker); } - { - auto *sizer = new wxBoxSizer(wxHORIZONTAL); - auto *other_vendors = new wxButton(others_buttons, wxID_ANY, _(L("Other vendors"))); - // other_vendors->Disable(); // XXX - auto *custom_setup = new wxButton(others_buttons, wxID_ANY, _(L("Custom setup"))); + const size_t num_other_vendors = vendors.size() - (vendor_prusa != vendors.cend()); + auto *sizer = new wxBoxSizer(wxHORIZONTAL); + auto *other_vendors = new wxButton(others_buttons, wxID_ANY, _(L("Other vendors"))); + other_vendors->Enable(num_other_vendors > 0); + auto *custom_setup = new wxButton(others_buttons, wxID_ANY, _(L("Custom setup"))); - sizer->Add(other_vendors); - sizer->AddSpacer(BTN_SPACING); - sizer->Add(custom_setup); + sizer->Add(other_vendors); + sizer->AddSpacer(BTN_SPACING); + sizer->Add(custom_setup); - other_vendors->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->wizard_p()->on_other_vendors(); }); - custom_setup->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->wizard_p()->on_custom_setup(); }); + other_vendors->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->wizard_p()->on_other_vendors(); }); + custom_setup->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->wizard_p()->on_custom_setup(); }); - others_buttons->SetSizer(sizer); - } + others_buttons->SetSizer(sizer); } void PageWelcome::on_page_set() @@ -181,7 +222,7 @@ void PageWelcome::on_page_set() void PageWelcome::on_variant_checked() { - enable_next(variants_checked > 0); + enable_next(printer_picker != nullptr ? printer_picker->variants_checked > 0 : false); } PageUpdate::PageUpdate(ConfigWizard *parent) : @@ -208,35 +249,63 @@ PageUpdate::PageUpdate(ConfigWizard *parent) : PageVendors::PageVendors(ConfigWizard *parent) : ConfigWizardPage(parent, _(L("Other Vendors")), _(L("Other Vendors"))) { + append_text(_(L("Pick another vendor supported by Slic3r PE:"))); + const PresetBundle &bundle = wizard_p()->bundle_vendors; auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); boldfont.SetWeight(wxFONTWEIGHT_BOLD); + AppConfig &appconfig_vendors = this->wizard_p()->appconfig_vendors; + wxArrayString choices_vendors; + for (const auto &vendor : bundle.vendors) { if (vendor.id == "PrusaResearch") { continue; } - auto *label_vendor = new wxStaticText(this, wxID_ANY, vendor.name); - label_vendor->SetFont(boldfont); - append(label_vendor, 0, 0, 0); + auto *picker = new PrinterPicker(this, vendor, appconfig_vendors); + picker->Hide(); + pickers.push_back(picker); + choices_vendors.Add(vendor.name); - for (const auto &model : vendor.models) { - auto *label_model = new wxStaticText(this, wxID_ANY, model.name); - label_model->SetFont(boldfont); - append(label_model, 0, wxLEFT, INDENT_SPACING); + picker->Bind(EVT_PRINTER_PICK, [this, &appconfig_vendors](const PrinterPickerEvent &evt) { + appconfig_vendors.set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); + this->on_variant_checked(); + }); + } - for (const auto &variant : model.variants) { - auto *cbox = new wxCheckBox(this, wxID_ANY, variant.name); - append(cbox, 0, wxEXPAND | wxLEFT, 2 * INDENT_SPACING); - cbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { - // TODO - }); - } - } + auto *vendor_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices_vendors); + if (choices_vendors.GetCount() > 0) { + vendor_picker->SetSelection(0); + on_vendor_pick(0); + } - append_spacer(VERTICAL_SPACING); + vendor_picker->Bind(wxEVT_CHOICE, [this](wxCommandEvent &evt) { + this->on_vendor_pick(evt.GetInt()); + }); + + append(vendor_picker); + for (PrinterPicker *picker : pickers) { this->append(picker); } +} + +void PageVendors::on_page_set() +{ + on_variant_checked(); +} + +void PageVendors::on_vendor_pick(size_t i) +{ + for (PrinterPicker *picker : pickers) { picker->Hide(); } + if (i < pickers.size()) { + pickers[i]->Show(); + Layout(); } } +void PageVendors::on_variant_checked() +{ + size_t variants_checked = 0; + for (const PrinterPicker *picker : pickers) { variants_checked += picker->variants_checked; } + enable_next(variants_checked > 0); +} PageFirmware::PageFirmware(ConfigWizard *parent) : ConfigWizardPage(parent, _(L("Firmware Type")), _(L("Firmware"))), @@ -613,7 +682,6 @@ ConfigWizard::ConfigWizard(wxWindow *parent) : p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->go_prev(); }); p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->go_next(); }); - // p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->p->on_finish(); }); p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &evt) { this->EndModal(wxID_OK); }); } diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp index 1d9519bc3..9f9395975 100644 --- a/xs/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -29,6 +29,13 @@ enum { VERTICAL_SPACING = 10, }; +struct PrinterPicker: wxPanel +{ + unsigned variants_checked; + + PrinterPicker(wxWindow *parent, const VendorProfile &vendor, const AppConfig &appconfig_vendors); +}; + struct ConfigWizardPage: wxPanel { enum { @@ -72,8 +79,8 @@ private: struct PageWelcome: ConfigWizardPage { + PrinterPicker *printer_picker; wxPanel *others_buttons; - unsigned variants_checked; PageWelcome(ConfigWizard *parent); @@ -93,7 +100,14 @@ struct PageUpdate: ConfigWizardPage struct PageVendors: ConfigWizardPage { + std::vector pickers; + PageVendors(ConfigWizard *parent); + + virtual void on_page_set(); + + void on_vendor_pick(size_t i); + void on_variant_checked(); }; struct PageFirmware: ConfigWizardPage From 9dcec6662e788a8ae0f04043581a1a3c44d36006 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Fri, 6 Apr 2018 12:15:28 +0200 Subject: [PATCH 06/38] ConfigWizard: Other vendor sample data, minor fixes --- resources/icons/printers/BarBaz_M1.png | Bin 0 -> 10369 bytes resources/icons/printers/BarBaz_M2.png | Bin 0 -> 10369 bytes resources/icons/printers/BarBaz_M3.png | Bin 0 -> 10369 bytes resources/icons/printers/Foobar_M1.png | Bin 0 -> 9323 bytes resources/icons/printers/Foobar_M2.png | Bin 0 -> 9323 bytes resources/icons/printers/Foobar_M3.png | Bin 0 -> 9323 bytes resources/profiles/BarBaz.ini | 985 +++++++++++++++++++++ resources/profiles/Foobar.ini | 985 +++++++++++++++++++++ xs/src/slic3r/GUI/AppConfig.cpp | 2 - xs/src/slic3r/GUI/AppConfig.hpp | 10 +- xs/src/slic3r/GUI/ConfigWizard.cpp | 21 +- xs/src/slic3r/GUI/ConfigWizard_private.hpp | 9 +- xs/xsp/my.map | 3 - 13 files changed, 1990 insertions(+), 25 deletions(-) create mode 100644 resources/icons/printers/BarBaz_M1.png create mode 100644 resources/icons/printers/BarBaz_M2.png create mode 100644 resources/icons/printers/BarBaz_M3.png create mode 100644 resources/icons/printers/Foobar_M1.png create mode 100644 resources/icons/printers/Foobar_M2.png create mode 100644 resources/icons/printers/Foobar_M3.png create mode 100644 resources/profiles/BarBaz.ini create mode 100644 resources/profiles/Foobar.ini diff --git a/resources/icons/printers/BarBaz_M1.png b/resources/icons/printers/BarBaz_M1.png new file mode 100644 index 0000000000000000000000000000000000000000..5924cc88b9ac373d5dc802ac06b82d1ad9d5015c GIT binary patch literal 10369 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 CbaD>> literal 0 HcmV?d00001 diff --git a/resources/icons/printers/Foobar_M2.png b/resources/icons/printers/Foobar_M2.png new file mode 100644 index 0000000000000000000000000000000000000000..61a76a63d1d3b39f402aea981f7adc11e346b389 GIT binary patch literal 9323 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 CbaD>> literal 0 HcmV?d00001 diff --git a/resources/icons/printers/Foobar_M3.png b/resources/icons/printers/Foobar_M3.png new file mode 100644 index 0000000000000000000000000000000000000000..61a76a63d1d3b39f402aea981f7adc11e346b389 GIT binary patch literal 9323 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 CbaD>> literal 0 HcmV?d00001 diff --git a/resources/profiles/BarBaz.ini b/resources/profiles/BarBaz.ini new file mode 100644 index 000000000..83bb15684 --- /dev/null +++ b/resources/profiles/BarBaz.ini @@ -0,0 +1,985 @@ +# Print profiles for the Prusa Research printers. + +[vendor] +# Vendor name will be shown by the Config Wizard. +name = Bar Baz +# Configuration version of this file. Config file will only be installed, if the config_version differs. +# This means, the server may force the Slic3r configuration to be downgraded. +config_version = 0.1 +# Where to get the updates from? +config_update_url = https://example.com + +# The printer models will be shown by the Configuration Wizard in this order, +# also the first model installed & the first nozzle installed will be activated after install. +#TODO: One day we may differentiate variants of the nozzles / hot ends, +#for example by the melt zone size, or whether the nozzle is hardened. +[printer_model:M1] +name = Bar Baz Model 1 +variants = 0.4; 0.25; 0.6 + +[printer_model:M2] +name = Bar Baz Model 2 +variants = 0.4; 0.25; 0.6 + +[printer_model:M3] +# Printer model name will be shown by the installation wizard. +name = Bar Baz Model 3 +variants = 0.4; 0.6 + +# All presets starting with asterisk, for example *common*, are intermediate and they will +# not make it into the user interface. + +# Common print preset, mostly derived from MK2 single material with a 0.4mm nozzle. +# All other print presets will derive from the *common* print preset. +[print:*common*] +avoid_crossing_perimeters = 0 +bridge_acceleration = 1000 +bridge_angle = 0 +bridge_flow_ratio = 0.8 +bridge_speed = 20 +brim_width = 0 +clip_multipart_objects = 1 +compatible_printers = +complete_objects = 0 +default_acceleration = 1000 +dont_support_bridges = 1 +elefant_foot_compensation = 0 +ensure_vertical_shell_thickness = 1 +external_fill_pattern = rectilinear +external_perimeters_first = 0 +external_perimeter_extrusion_width = 0.45 +extra_perimeters = 0 +extruder_clearance_height = 20 +extruder_clearance_radius = 20 +extrusion_width = 0.45 +fill_angle = 45 +fill_density = 20% +fill_pattern = cubic +first_layer_acceleration = 1000 +first_layer_extrusion_width = 0.42 +first_layer_height = 0.2 +first_layer_speed = 30 +gap_fill_speed = 40 +gcode_comments = 0 +infill_every_layers = 1 +infill_extruder = 1 +infill_extrusion_width = 0.45 +infill_first = 0 +infill_only_where_needed = 0 +infill_overlap = 25% +interface_shells = 0 +max_print_speed = 100 +max_volumetric_extrusion_rate_slope_negative = 0 +max_volumetric_extrusion_rate_slope_positive = 0 +max_volumetric_speed = 0 +min_skirt_length = 4 +notes = +overhangs = 0 +only_retract_when_crossing_perimeters = 0 +ooze_prevention = 0 +output_filename_format = [input_filename_base].gcode +perimeters = 2 +perimeter_extruder = 1 +perimeter_extrusion_width = 0.45 +post_process = +print_settings_id = +raft_layers = 0 +resolution = 0 +seam_position = nearest +skirts = 1 +skirt_distance = 2 +skirt_height = 3 +small_perimeter_speed = 20 +solid_infill_below_area = 0 +solid_infill_every_layers = 0 +solid_infill_extruder = 1 +solid_infill_extrusion_width = 0.45 +spiral_vase = 0 +standby_temperature_delta = -5 +support_material = 0 +support_material_extruder = 0 +support_material_extrusion_width = 0.35 +support_material_interface_extruder = 0 +support_material_angle = 0 +support_material_buildplate_only = 0 +support_material_enforce_layers = 0 +support_material_contact_distance = 0.15 +support_material_interface_contact_loops = 0 +support_material_interface_layers = 2 +support_material_interface_spacing = 0.2 +support_material_interface_speed = 100% +support_material_pattern = rectilinear +support_material_spacing = 2 +support_material_speed = 50 +support_material_synchronize_layers = 0 +support_material_threshold = 45 +support_material_with_sheath = 0 +support_material_xy_spacing = 60% +thin_walls = 0 +top_infill_extrusion_width = 0.45 +top_solid_infill_speed = 40 +travel_speed = 180 +wipe_tower = 0 +wipe_tower_per_color_wipe = 20 +wipe_tower_width = 60 +wipe_tower_x = 180 +wipe_tower_y = 140 +xy_size_compensation = 0 + +# Print parameters common to a 0.25mm diameter nozzle. +[print:*0.25nozzle*] +external_perimeter_extrusion_width = 0.25 +extrusion_width = 0.25 +first_layer_extrusion_width = 0.25 +infill_extrusion_width = 0.25 +perimeter_extrusion_width = 0.25 +solid_infill_extrusion_width = 0.25 +top_infill_extrusion_width = 0.25 +support_material_extrusion_width = 0.18 +support_material_interface_layers = 0 +support_material_interface_spacing = 0.15 +support_material_spacing = 1 +support_material_xy_spacing = 150% + +# Print parameters common to a 0.6mm diameter nozzle. +[print:*0.6nozzle*] +external_perimeter_extrusion_width = 0.61 +extrusion_width = 0.67 +first_layer_extrusion_width = 0.65 +infill_extrusion_width = 0.7 +perimeter_extrusion_width = 0.65 +solid_infill_extrusion_width = 0.65 +top_infill_extrusion_width = 0.6 + +[print:*soluble_support*] +overhangs = 1 +skirts = 0 +support_material = 1 +support_material_contact_distance = 0 +support_material_extruder = 4 +support_material_extrusion_width = 0.45 +support_material_interface_extruder = 4 +support_material_interface_spacing = 0.1 +support_material_synchronize_layers = 1 +support_material_threshold = 80 +support_material_with_sheath = 1 +wipe_tower = 1 + +[print:*0.05mm*] +inherits = *common* +bottom_solid_layers = 10 +bridge_acceleration = 300 +bridge_flow_ratio = 0.7 +default_acceleration = 500 +external_perimeter_speed = 20 +fill_density = 20% +first_layer_acceleration = 500 +gap_fill_speed = 20 +infill_acceleration = 800 +infill_speed = 30 +max_print_speed = 80 +small_perimeter_speed = 15 +solid_infill_speed = 30 +support_material_extrusion_width = 0.3 +support_material_spacing = 1.5 +layer_height = 0.05 +perimeter_acceleration = 300 +perimeter_speed = 30 +perimeters = 3 +support_material_speed = 30 +top_solid_infill_speed = 20 +top_solid_layers = 15 + +[print:0.05mm ULTRADETAIL] +inherits = *0.05mm* +infill_extrusion_width = 0.5 + +[print:0.05mm ULTRADETAIL MK3] +inherits = *0.05mm* +fill_pattern = grid +top_infill_extrusion_width = 0.4 + +[print:0.05mm ULTRADETAIL 0.25 nozzle] +inherits = *0.05mm* +external_perimeter_extrusion_width = 0 +extrusion_width = 0.28 +fill_density = 20% +first_layer_extrusion_width = 0.3 +infill_extrusion_width = 0 +infill_speed = 20 +max_print_speed = 100 +perimeter_extrusion_width = 0 +perimeter_speed = 20 +small_perimeter_speed = 10 +solid_infill_extrusion_width = 0 +solid_infill_speed = 20 +support_material_speed = 20 +top_infill_extrusion_width = 0 + +[print:0.05mm ULTRADETAIL 0.25 nozzle MK3] +inherits = *0.05mm*; *0.25nozzle* +fill_pattern = grid +top_infill_extrusion_width = 0.4 + +[print:*0.10mm*] +inherits = *common* +bottom_solid_layers = 7 +bridge_flow_ratio = 0.7 +layer_height = 0.1 +perimeter_acceleration = 800 +top_solid_layers = 9 + +[print:0.10mm DETAIL] +inherits = *0.10mm* +external_perimeter_speed = 40 +infill_acceleration = 2000 +infill_speed = 60 +perimeter_speed = 50 +solid_infill_speed = 50 + +[print:0.10mm DETAIL MK3] +inherits = *0.10mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 200 +perimeter_speed = 45 +solid_infill_speed = 170 +top_infill_extrusion_width = 0.4 +top_solid_infill_speed = 50 + +[print:0.10mm DETAIL 0.25 nozzle] +inherits = *0.10mm* +bridge_acceleration = 600 +external_perimeter_speed = 20 +infill_acceleration = 1600 +infill_speed = 40 +perimeter_acceleration = 600 +perimeter_speed = 25 +small_perimeter_speed = 10 +solid_infill_speed = 40 +top_solid_infill_speed = 30 + +[print:0.10mm DETAIL 0.25 nozzle MK3] +inherits = *0.10mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 200 +perimeter_speed = 45 +solid_infill_speed = 170 +top_infill_extrusion_width = 0.4 +top_solid_infill_speed = 50 + +[print:0.10mm DETAIL 0.6 nozzle MK3] +inherits = *0.10mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 200 +perimeter_speed = 45 +solid_infill_speed = 170 +top_infill_extrusion_width = 0.4 +top_solid_infill_speed = 50 + +[print:*0.15mm*] +inherits = *common* +bottom_solid_layers = 5 +external_perimeter_speed = 40 +infill_acceleration = 2000 +infill_speed = 60 +layer_height = 0.15 +perimeter_acceleration = 800 +perimeter_speed = 50 +solid_infill_speed = 50 +top_infill_extrusion_width = 0.4 +top_solid_layers = 7 + +[print:0.15mm 100mms Linear Advance] +inherits = *0.15mm* +bridge_flow_ratio = 0.95 +external_perimeter_speed = 50 +infill_speed = 100 +max_print_speed = 150 +perimeter_speed = 60 +small_perimeter_speed = 30 +solid_infill_speed = 100 +support_material_speed = 60 +top_solid_infill_speed = 70 + +[print:0.15mm OPTIMAL] +inherits = *0.15mm* +top_infill_extrusion_width = 0.45 + +[print:0.15mm OPTIMAL 0.25 nozzle] +inherits = *0.15mm*; *0.25nozzle* +bridge_acceleration = 600 +bridge_flow_ratio = 0.7 +external_perimeter_speed = 20 +infill_acceleration = 1600 +infill_speed = 40 +perimeter_acceleration = 600 +perimeter_speed = 25 +small_perimeter_speed = 10 +solid_infill_speed = 40 +support_material_extrusion_width = 0.2 +top_solid_infill_speed = 30 + +[print:0.15mm OPTIMAL 0.6 nozzle] +inherits = *0.15mm*; *0.6nozzle* + +[print:0.15mm OPTIMAL MK3] +inherits = *0.15mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 170 +perimeter_speed = 45 +solid_infill_speed = 170 +top_solid_infill_speed = 50 + +[print:0.15mm OPTIMAL SOLUBLE FULL] +inherits = *0.15mm*; *soluble_support* +external_perimeter_speed = 25 +notes = Set your solluble extruder in Multiple Extruders > Support material/raft/skirt extruder & Support material/raft interface extruder +perimeter_speed = 40 +solid_infill_speed = 40 +top_infill_extrusion_width = 0.45 +top_solid_infill_speed = 30 +wipe_tower = 1 + +[print:0.15mm OPTIMAL SOLUBLE INTERFACE] +inherits = 0.15mm OPTIMAL SOLUBLE FULL +notes = Set your solluble extruder in Multiple Extruders > Support material/raft interface extruder +support_material_extruder = 0 +support_material_interface_layers = 3 +support_material_with_sheath = 0 +support_material_xy_spacing = 80% + +[print:0.15mm OPTIMAL 0.25 nozzle MK3] +inherits = *0.15mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 170 +perimeter_speed = 45 +solid_infill_speed = 170 +top_solid_infill_speed = 50 +[print:*0.20mm*] +inherits = *common* +bottom_solid_layers = 4 +bridge_flow_ratio = 0.95 +external_perimeter_speed = 40 +infill_acceleration = 2000 +infill_speed = 60 +layer_height = 0.2 +perimeter_acceleration = 800 +perimeter_speed = 50 +solid_infill_speed = 50 +top_infill_extrusion_width = 0.4 +top_solid_layers = 5 + +[print:0.15mm OPTIMAL 0.6 nozzle MK3] +inherits = *0.15mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 170 +perimeter_speed = 45 +solid_infill_speed = 170 +top_solid_infill_speed = 50 + +[print:0.20mm 100mms Linear Advance] +inherits = *0.20mm* +external_perimeter_speed = 50 +infill_speed = 100 +max_print_speed = 150 +perimeter_speed = 60 +small_perimeter_speed = 30 +solid_infill_speed = 100 +support_material_speed = 60 +top_solid_infill_speed = 70 + +[print:0.20mm FAST MK3] +inherits = *0.20mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 170 +perimeter_speed = 45 +solid_infill_speed = 170 +top_solid_infill_speed = 50 + +[print:0.20mm NORMAL] +inherits = *0.20mm* + +[print:0.20mm NORMAL 0.6 nozzle] +inherits = *0.20mm*; *0.6nozzle* + +[print:0.20mm NORMAL SOLUBLE FULL] +inherits = *0.20mm*; *soluble_support* +external_perimeter_speed = 30 +notes = Set your solluble extruder in Multiple Extruders > Support material/raft/skirt extruder & Support material/raft interface extruder +perimeter_speed = 40 +solid_infill_speed = 40 +top_solid_infill_speed = 30 + +[print:0.20mm NORMAL SOLUBLE INTERFACE] +inherits = 0.20mm NORMAL SOLUBLE FULL +notes = Set your solluble extruder in Multiple Extruders > Support material/raft interface extruder +support_material_extruder = 0 +support_material_interface_layers = 3 +support_material_with_sheath = 0 +support_material_xy_spacing = 80% + +[print:0.20mm FAST 0.6 nozzle MK3] +inherits = *0.20mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 170 +perimeter_speed = 45 +solid_infill_speed = 170 +top_solid_infill_speed = 50 + +[print:*0.35mm*] +inherits = *common* +bottom_solid_layers = 3 +external_perimeter_extrusion_width = 0.6 +external_perimeter_speed = 40 +first_layer_extrusion_width = 0.75 +infill_acceleration = 2000 +infill_speed = 60 +layer_height = 0.35 +perimeter_acceleration = 800 +perimeter_extrusion_width = 0.65 +perimeter_speed = 50 +solid_infill_extrusion_width = 0.65 +solid_infill_speed = 60 +top_solid_infill_speed = 50 +top_solid_layers = 4 + +[print:0.35mm FAST] +inherits = *0.35mm* +bridge_flow_ratio = 0.95 +first_layer_extrusion_width = 0.42 +perimeter_extrusion_width = 0.43 +solid_infill_extrusion_width = 0.7 +top_infill_extrusion_width = 0.43 + +[print:0.35mm FAST 0.6 nozzle] +inherits = *0.35mm*; *0.6nozzle* + +[print:0.35mm FAST sol full 0.6 nozzle] +inherits = *0.35mm*; *0.6nozzle*; *soluble_support* +external_perimeter_extrusion_width = 0.6 +external_perimeter_speed = 30 +notes = Set your solluble extruder in Multiple Extruders > Support material/raft interface extruder +perimeter_speed = 40 +support_material_extrusion_width = 0.55 +support_material_interface_layers = 3 +support_material_xy_spacing = 120% +top_infill_extrusion_width = 0.57 + +[print:0.35mm FAST sol int 0.6 nozzle] +inherits = 0.35mm FAST sol full 0.6 nozzle +support_material_extruder = 0 +support_material_interface_layers = 2 +support_material_with_sheath = 0 +support_material_xy_spacing = 150% + +[filament:*common*] +cooling = 1 +compatible_printers = +end_filament_gcode = "; Filament-specific end gcode" +extrusion_multiplier = 1 +filament_cost = 0 +filament_density = 0 +filament_diameter = 1.75 +filament_notes = "" +filament_settings_id = +filament_soluble = 0 +min_print_speed = 5 +slowdown_below_layer_time = 20 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}30{endif}; Filament gcode" + +[filament:*PLA*] +inherits = *common* +bed_temperature = 60 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +fan_always_on = 1 +fan_below_layer_time = 100 +filament_colour = #FF3232 +filament_max_volumetric_speed = 15 +filament_type = PLA +first_layer_bed_temperature = 60 +first_layer_temperature = 215 +max_fan_speed = 100 +min_fan_speed = 100 +temperature = 210 + +[filament:*PET*] +inherits = *common* +bed_temperature = 90 +bridge_fan_speed = 50 +disable_fan_first_layers = 3 +fan_always_on = 1 +fan_below_layer_time = 20 +filament_colour = #FF8000 +filament_max_volumetric_speed = 8 +filament_type = PET +first_layer_bed_temperature = 85 +first_layer_temperature = 230 +max_fan_speed = 50 +min_fan_speed = 30 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}45{endif}; Filament gcode" +temperature = 240 + +[filament:*ABS*] +inherits = *common* +bed_temperature = 110 +bridge_fan_speed = 30 +cooling = 0 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 20 +filament_colour = #3A80CA +filament_max_volumetric_speed = 11 +filament_type = ABS +first_layer_bed_temperature = 100 +first_layer_temperature = 255 +max_fan_speed = 30 +min_fan_speed = 20 +temperature = 255 + +[filament:*FLEX*] +inherits = *common* +bridge_fan_speed = 100 +cooling = 0 +disable_fan_first_layers = 1 +extrusion_multiplier = 1.2 +fan_always_on = 0 +fan_below_layer_time = 100 +filament_colour = #00CA0A +filament_max_volumetric_speed = 1.5 +filament_type = FLEX +first_layer_bed_temperature = 50 +first_layer_temperature = 240 +max_fan_speed = 90 +min_fan_speed = 70 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 240 + +[filament:ColorFabb Brass Bronze] +inherits = *PLA* +extrusion_multiplier = 1.2 +filament_colour = #804040 +filament_max_volumetric_speed = 10 + +[filament:ColorFabb HT] +inherits = *PET* +bed_temperature = 110 +bridge_fan_speed = 30 +cooling = 1 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 10 +first_layer_bed_temperature = 105 +first_layer_temperature = 270 +max_fan_speed = 20 +min_fan_speed = 10 +min_print_speed = 5 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}45{endif}; Filament gcode" +temperature = 270 + +[filament:ColorFabb PLA-PHA] +inherits = *PLA* + +[filament:ColorFabb Woodfil] +inherits = *PLA* +extrusion_multiplier = 1.2 +filament_colour = #804040 +filament_max_volumetric_speed = 10 +first_layer_temperature = 200 +min_print_speed = 5 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 200 + +[filament:ColorFabb XT] +inherits = *PET* +filament_type = PLA +first_layer_bed_temperature = 90 +first_layer_temperature = 260 +temperature = 270 + +[filament:ColorFabb XT-CF20] +inherits = *PET* +extrusion_multiplier = 1.2 +filament_colour = #804040 +filament_max_volumetric_speed = 1 +first_layer_bed_temperature = 90 +first_layer_temperature = 260 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}30{endif}; Filament gcode" +temperature = 260 + +[filament:ColorFabb nGen] +inherits = *PET* +bridge_fan_speed = 40 +fan_always_on = 0 +fan_below_layer_time = 10 +filament_type = NGEN +first_layer_temperature = 240 +max_fan_speed = 35 +min_fan_speed = 20 + +[filament:ColorFabb nGen flex] +inherits = *FLEX* +bed_temperature = 85 +bridge_fan_speed = 40 +cooling = 1 +disable_fan_first_layers = 3 +extrusion_multiplier = 1 +fan_below_layer_time = 10 +filament_max_volumetric_speed = 5 +first_layer_bed_temperature = 85 +first_layer_temperature = 260 +max_fan_speed = 35 +min_fan_speed = 20 +temperature = 260 + +[filament:E3D Edge] +inherits = *PET* +filament_notes = "List of manufacturers tested with standart PET print settings for MK2:\n\nE3D Edge\nFillamentum CPE GH100\nPlasty Mladeč PETG" + +[filament:E3D PC-ABS] +inherits = *ABS* +first_layer_temperature = 270 +temperature = 270 + +[filament:Fillamentum ABS] +inherits = *ABS* +first_layer_temperature = 240 +temperature = 240 + +[filament:Fillamentum ASA] +inherits = *ABS* +fan_always_on = 1 +first_layer_temperature = 265 +temperature = 265 + +[filament:Fillamentum CPE HG100 HM100] +inherits = *PET* +filament_notes = "CPE HG100 , CPE HM100" +first_layer_bed_temperature = 90 +first_layer_temperature = 275 +max_fan_speed = 50 +min_fan_speed = 50 +temperature = 275 + +[filament:Fillamentum Timberfil] +inherits = *PLA* +extrusion_multiplier = 1.2 +filament_colour = #804040 +filament_max_volumetric_speed = 10 +first_layer_temperature = 190 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 190 + +[filament:Generic ABS] +inherits = *ABS* +filament_notes = "List of materials tested with standart ABS print settings for MK2:\n\nEsun ABS\nFil-A-Gehr ABS\nHatchboxABS\nPlasty Mladeč ABS" + +[filament:Generic PET] +inherits = *PET* +filament_notes = "List of manufacturers tested with standart PET print settings for MK2:\n\nE3D Edge\nFillamentum CPE GH100\nPlasty Mladeč PETG" + +[filament:Generic PLA] +inherits = *PLA* +filament_notes = "List of materials tested with standart PLA print settings for MK2:\n\nDas Filament\nEsun PLA\nEUMAKERS PLA\nFiberlogy HD-PLA\nFillamentum PLA\nFloreon3D\nHatchbox PLA\nPlasty Mladeč PLA\nPrimavalue PLA\nProto pasta Matte Fiber\nVerbatim PLA\nVerbatim BVOH" + +[filament:Polymaker PC-Max] +inherits = *ABS* +bed_temperature = 115 +filament_colour = #3A80CA +first_layer_bed_temperature = 100 +first_layer_temperature = 270 +temperature = 270 + +[filament:Primavalue PVA] +inherits = *PLA* +cooling = 0 +fan_always_on = 0 +filament_colour = #FFFFD7 +filament_max_volumetric_speed = 10 +filament_notes = "List of materials tested with standart PVA print settings for MK2:\n\nPrimaSelect PVA+\nICE FILAMENTS PVA 'NAUGHTY NATURAL'\nVerbatim BVOH" +filament_soluble = 1 +filament_type = PVA +first_layer_temperature = 195 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 195 + +[filament:Prusa ABS] +inherits = *ABS* +filament_notes = "List of materials tested with standart ABS print settings for MK2:\n\nEsun ABS\nFil-A-Gehr ABS\nHatchboxABS\nPlasty Mladeč ABS" + +[filament:Prusa HIPS] +inherits = *ABS* +bridge_fan_speed = 50 +cooling = 1 +extrusion_multiplier = 0.9 +fan_always_on = 1 +fan_below_layer_time = 10 +filament_colour = #FFFFD7 +filament_soluble = 1 +filament_type = HIPS +first_layer_temperature = 220 +max_fan_speed = 20 +min_fan_speed = 20 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 220 + +[filament:Prusa PET] +inherits = *PET* +filament_notes = "List of manufacturers tested with standart PET print settings for MK2:\n\nE3D Edge\nFillamentum CPE GH100\nPlasty Mladeč PETG" + +[filament:Prusa PLA] +inherits = *PLA* +filament_notes = "List of materials tested with standart PLA print settings for MK2:\n\nDas Filament\nEsun PLA\nEUMAKERS PLA\nFiberlogy HD-PLA\nFillamentum PLA\nFloreon3D\nHatchbox PLA\nPlasty Mladeč PLA\nPrimavalue PLA\nProto pasta Matte Fiber\nVerbatim PLA\nVerbatim BVOH" + +[filament:SemiFlex or Flexfill 98A] +inherits = *FLEX* + +[filament:Taulman Bridge] +inherits = *common* +bed_temperature = 90 +bridge_fan_speed = 40 +cooling = 0 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 20 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 10 +filament_soluble = 0 +filament_type = PET +first_layer_bed_temperature = 60 +first_layer_temperature = 240 +max_fan_speed = 5 +min_fan_speed = 0 +min_print_speed = 5 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 250 + +[filament:Taulman T-Glase] +inherits = *PET* +bridge_fan_speed = 40 +cooling = 0 +fan_always_on = 0 +first_layer_bed_temperature = 90 +first_layer_temperature = 240 +max_fan_speed = 5 +min_fan_speed = 0 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}30{endif}; Filament gcode" + +[filament:Verbatim BVOH] +inherits = *common* +bed_temperature = 60 +bridge_fan_speed = 100 +cooling = 0 +disable_fan_first_layers = 1 +extrusion_multiplier = 1 +fan_always_on = 0 +fan_below_layer_time = 100 +filament_colour = #FFFFD7 +filament_max_volumetric_speed = 10 +filament_notes = "List of materials tested with standart PLA print settings for MK2:\n\nDas Filament\nEsun PLA\nEUMAKERS PLA\nFiberlogy HD-PLA\nFillamentum PLA\nFloreon3D\nHatchbox PLA\nPlasty Mladeč PLA\nPrimavalue PLA\nProto pasta Matte Fiber\nVerbatim PLA\nVerbatim BVOH" +filament_soluble = 1 +filament_type = PLA +first_layer_bed_temperature = 60 +first_layer_temperature = 215 +max_fan_speed = 100 +min_fan_speed = 100 +min_print_speed = 15 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 210 + +[filament:Verbatim PP] +inherits = *common* +bed_temperature = 100 +bridge_fan_speed = 100 +cooling = 1 +disable_fan_first_layers = 2 +extrusion_multiplier = 1 +fan_always_on = 1 +fan_below_layer_time = 100 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 5 +filament_notes = "List of materials tested with standart PLA print settings for MK2:\n\nEsun PLA\nFiberlogy HD-PLA\nFillamentum PLA\nFloreon3D\nHatchbox PLA\nPlasty Mladeč PLA\nPrimavalue PLA\nProto pasta Matte Fiber\nEUMAKERS PLA" +filament_type = PLA +first_layer_bed_temperature = 100 +first_layer_temperature = 220 +max_fan_speed = 100 +min_fan_speed = 100 +min_print_speed = 15 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 220 + +[printer:*common*] +bed_shape = 0x0,250x0,250x210,0x210 +before_layer_gcode = ;BEFORE_LAYER_CHANGE\n;[layer_z]\n\n +between_objects_gcode = +deretract_speed = 0 +end_gcode = G4 ; wait\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors +extruder_colour = #FFFF00 +extruder_offset = 0x0 +gcode_flavor = marlin +layer_gcode = ;AFTER_LAYER_CHANGE\n;[layer_z] +max_layer_height = 0.25 +min_layer_height = 0.07 +nozzle_diameter = 0.4 +octoprint_apikey = +octoprint_host = +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\n +printer_settings_id = +retract_before_travel = 1 +retract_before_wipe = 0% +retract_layer_change = 1 +retract_length = 0.8 +retract_length_toolchange = 4 +retract_lift = 0.6 +retract_lift_above = 0 +retract_lift_below = 199 +retract_restart_extra = 0 +retract_restart_extra_toolchange = 0 +retract_speed = 35 +serial_port = +serial_speed = 250000 +single_extruder_multi_material = 0 +start_gcode = M115 U3.1.0 ; tell printer latest fw version\nM201 X9000 Y9000 Z500 E10000 ; sets maximum accelerations, mm/sec^2\nM203 X500 Y500 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1500 T1500 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.2 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0 +toolchange_gcode = +use_firmware_retraction = 0 +use_relative_e_distances = 1 +use_volumetric_e = 0 +variable_layer_height = 1 +wipe = 1 +z_offset = 0 +printer_model = M2 +printer_variant = 0.4 +default_print_profile = 0.15mm OPTIMAL +default_filament_profile = Prusa PLA + +[printer:*multimaterial*] +inherits = *common* +deretract_speed = 50 +retract_before_travel = 3 +retract_before_wipe = 60% +retract_layer_change = 0 +retract_length = 4 +retract_lift = 0.6 +retract_lift_above = 0 +retract_lift_below = 199 +retract_restart_extra = 0 +retract_restart_extra_toolchange = 0 +retract_speed = 80 +single_extruder_multi_material = 1 +printer_model = M3 + +[printer:*mm-single*] +inherits = *multimaterial* +end_gcode = G1 E-4 F2100.00000\nG91\nG1 Z1 F7200.000\nG90\nG1 X245 Y1\nG1 X240 E4\nG1 F4000\nG1 X190 E2.7 \nG1 F4600\nG1 X110 E2.8\nG1 F5200\nG1 X40 E3 \nG1 E-15.0000 F5000\nG1 E-50.0000 F5400\nG1 E-15.0000 F3000\nG1 E-12.0000 F2000\nG1 F1600\nG1 X0 Y1 E3.0000\nG1 X50 Y1 E-5.0000\nG1 F2000\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-3.0000\nG4 S0\nM107 ; fan off\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nG28 X0 ; home X axis\nM84 ; disable motors\n\n +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN +start_gcode = M115 U3.1.0 ; tell printer latest fw version\nM201 X9000 Y9000 Z500 E10000 ; sets maximum accelerations, mm/sec^2\nM203 X500 Y500 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1500 T1500 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.2 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\n; Start G-Code sequence START\nT?\nM104 S[first_layer_temperature]\nM140 S[first_layer_bed_temperature]\nM109 S[first_layer_temperature]\nM190 S[first_layer_bed_temperature]\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nG28 W\nG80\nG92 E0.0\nM203 E100\nM92 E140\nG1 Z0.250 F7200.000\nG1 X50.0 E80.0 F1000.0\nG1 X160.0 E20.0 F1000.0\nG1 Z0.200 F7200.000\nG1 X220.0 E13 F1000.0\nG1 X240.0 E0 F1000.0\nG1 E-4 F1000.0\nG92 E0.0 + +[printer:*mm-multi*] +inherits = *multimaterial* +end_gcode = {if not has_wipe_tower}\n; Pull the filament into the cooling tubes.\nG1 E-4 F2100.00000\nG91\nG1 Z1 F7200.000\nG90\nG1 X245 Y1\nG1 X240 E4\nG1 F4000\nG1 X190 E2.7 \nG1 F4600\nG1 X110 E2.8\nG1 F5200\nG1 X40 E3 \nG1 E-15.0000 F5000\nG1 E-50.0000 F5400\nG1 E-15.0000 F3000\nG1 E-12.0000 F2000\nG1 F1600\nG1 X0 Y1 E3.0000\nG1 X50 Y1 E-5.0000\nG1 F2000\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-3.0000\nG4 S0\n{endif}\nM107 ; fan off\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nG28 X0 ; home X axis\nM84 ; disable motors +extruder_colour = #FFAA55;#5182DB;#4ECDD3;#FB7259 +nozzle_diameter = 0.4,0.4,0.4,0.4 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN +start_gcode = M115 U3.1.0 ; tell printer latest fw version\nM201 X9000 Y9000 Z500 E10000 ; sets maximum accelerations, mm/sec^2\nM203 X500 Y500 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1500 T1500 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.2 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\n; Start G-Code sequence START\nT[initial_tool]\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nG28 W\nG80\nG92 E0.0\nM203 E100 ; set max feedrate\nM92 E140 ; E-steps per filament milimeter\n{if not has_wipe_tower}\nG1 Z0.250 F7200.000\nG1 X50.0 E80.0 F1000.0\nG1 X160.0 E20.0 F1000.0\nG1 Z0.200 F7200.000\nG1 X220.0 E13 F1000.0\nG1 X240.0 E0 F1000.0\nG1 E-4 F1000.0\n{endif}\nG92 E0.0 +variable_layer_height = 0 + +[printer:Original Prusa i3 MK2] +inherits = *common* + +[printer:Original Prusa i3 MK2 0.25 nozzle] +inherits = *common* +max_layer_height = 0.1 +min_layer_height = 0.05 +nozzle_diameter = 0.25 +retract_length = 1 +retract_speed = 50 +variable_layer_height = 0 +printer_variant = 0.25 +default_print_profile = 0.10mm DETAIL 0.25 nozzle + +[printer:Original Prusa i3 MK2 0.6 nozzle] +inherits = *common* +max_layer_height = 0.35 +min_layer_height = 0.1 +nozzle_diameter = 0.6 +printer_variant = 0.6 + +[printer:Original Prusa i3 MK2 MM Single Mode] +inherits = *mm-single* + +[printer:Original Prusa i3 MK2 MM Single Mode 0.6 nozzle] +inherits = *mm-single* +nozzle_diameter = 0.6 +printer_variant = 0.6 + +[printer:Original Prusa i3 MK2 MultiMaterial] +inherits = *mm-multi* +nozzle_diameter = 0.4,0.4,0.4,0.4 + +[printer:Original Prusa i3 MK2 MultiMaterial 0.6 nozzle] +inherits = *mm-multi* +nozzle_diameter = 0.6,0.6,0.6,0.6 +printer_variant = 0.6 + +[printer:Original Prusa i3 MK3] +inherits = *common* +end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +retract_lift_below = 209 +start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} +printer_model = M1 +default_print_profile = 0.15mm OPTIMAL MK3 + +[printer:Original Prusa i3 MK3 0.25 nozzle] +inherits = *common* +nozzle_diameter = 0.25 +printer_variant = 0.25 +end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +retract_lift_below = 209 +start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} +printer_model = M1 +default_print_profile = 0.10mm DETAIL MK3 + +[printer:Original Prusa i3 MK3 0.6 nozzle] +inherits = *common* +nozzle_diameter = 0.6 +printer_variant = 0.6 +end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +retract_lift_below = 209 +start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} +printer_model = M1 +default_print_profile = 0.15mm OPTIMAL MK3 diff --git a/resources/profiles/Foobar.ini b/resources/profiles/Foobar.ini new file mode 100644 index 000000000..21681398a --- /dev/null +++ b/resources/profiles/Foobar.ini @@ -0,0 +1,985 @@ +# Print profiles for the Prusa Research printers. + +[vendor] +# Vendor name will be shown by the Config Wizard. +name = Foo Bar +# Configuration version of this file. Config file will only be installed, if the config_version differs. +# This means, the server may force the Slic3r configuration to be downgraded. +config_version = 0.1 +# Where to get the updates from? +config_update_url = https://example.com + +# The printer models will be shown by the Configuration Wizard in this order, +# also the first model installed & the first nozzle installed will be activated after install. +#TODO: One day we may differentiate variants of the nozzles / hot ends, +#for example by the melt zone size, or whether the nozzle is hardened. +[printer_model:M1] +name = Foo Bar Model 1 +variants = 0.4; 0.25; 0.6 + +[printer_model:M2] +name = Foo Bar Model 2 +variants = 0.4; 0.25; 0.6 + +[printer_model:M3] +# Printer model name will be shown by the installation wizard. +name = Foo Bar Model 3 +variants = 0.4; 0.6 + +# All presets starting with asterisk, for example *common*, are intermediate and they will +# not make it into the user interface. + +# Common print preset, mostly derived from MK2 single material with a 0.4mm nozzle. +# All other print presets will derive from the *common* print preset. +[print:*common*] +avoid_crossing_perimeters = 0 +bridge_acceleration = 1000 +bridge_angle = 0 +bridge_flow_ratio = 0.8 +bridge_speed = 20 +brim_width = 0 +clip_multipart_objects = 1 +compatible_printers = +complete_objects = 0 +default_acceleration = 1000 +dont_support_bridges = 1 +elefant_foot_compensation = 0 +ensure_vertical_shell_thickness = 1 +external_fill_pattern = rectilinear +external_perimeters_first = 0 +external_perimeter_extrusion_width = 0.45 +extra_perimeters = 0 +extruder_clearance_height = 20 +extruder_clearance_radius = 20 +extrusion_width = 0.45 +fill_angle = 45 +fill_density = 20% +fill_pattern = cubic +first_layer_acceleration = 1000 +first_layer_extrusion_width = 0.42 +first_layer_height = 0.2 +first_layer_speed = 30 +gap_fill_speed = 40 +gcode_comments = 0 +infill_every_layers = 1 +infill_extruder = 1 +infill_extrusion_width = 0.45 +infill_first = 0 +infill_only_where_needed = 0 +infill_overlap = 25% +interface_shells = 0 +max_print_speed = 100 +max_volumetric_extrusion_rate_slope_negative = 0 +max_volumetric_extrusion_rate_slope_positive = 0 +max_volumetric_speed = 0 +min_skirt_length = 4 +notes = +overhangs = 0 +only_retract_when_crossing_perimeters = 0 +ooze_prevention = 0 +output_filename_format = [input_filename_base].gcode +perimeters = 2 +perimeter_extruder = 1 +perimeter_extrusion_width = 0.45 +post_process = +print_settings_id = +raft_layers = 0 +resolution = 0 +seam_position = nearest +skirts = 1 +skirt_distance = 2 +skirt_height = 3 +small_perimeter_speed = 20 +solid_infill_below_area = 0 +solid_infill_every_layers = 0 +solid_infill_extruder = 1 +solid_infill_extrusion_width = 0.45 +spiral_vase = 0 +standby_temperature_delta = -5 +support_material = 0 +support_material_extruder = 0 +support_material_extrusion_width = 0.35 +support_material_interface_extruder = 0 +support_material_angle = 0 +support_material_buildplate_only = 0 +support_material_enforce_layers = 0 +support_material_contact_distance = 0.15 +support_material_interface_contact_loops = 0 +support_material_interface_layers = 2 +support_material_interface_spacing = 0.2 +support_material_interface_speed = 100% +support_material_pattern = rectilinear +support_material_spacing = 2 +support_material_speed = 50 +support_material_synchronize_layers = 0 +support_material_threshold = 45 +support_material_with_sheath = 0 +support_material_xy_spacing = 60% +thin_walls = 0 +top_infill_extrusion_width = 0.45 +top_solid_infill_speed = 40 +travel_speed = 180 +wipe_tower = 0 +wipe_tower_per_color_wipe = 20 +wipe_tower_width = 60 +wipe_tower_x = 180 +wipe_tower_y = 140 +xy_size_compensation = 0 + +# Print parameters common to a 0.25mm diameter nozzle. +[print:*0.25nozzle*] +external_perimeter_extrusion_width = 0.25 +extrusion_width = 0.25 +first_layer_extrusion_width = 0.25 +infill_extrusion_width = 0.25 +perimeter_extrusion_width = 0.25 +solid_infill_extrusion_width = 0.25 +top_infill_extrusion_width = 0.25 +support_material_extrusion_width = 0.18 +support_material_interface_layers = 0 +support_material_interface_spacing = 0.15 +support_material_spacing = 1 +support_material_xy_spacing = 150% + +# Print parameters common to a 0.6mm diameter nozzle. +[print:*0.6nozzle*] +external_perimeter_extrusion_width = 0.61 +extrusion_width = 0.67 +first_layer_extrusion_width = 0.65 +infill_extrusion_width = 0.7 +perimeter_extrusion_width = 0.65 +solid_infill_extrusion_width = 0.65 +top_infill_extrusion_width = 0.6 + +[print:*soluble_support*] +overhangs = 1 +skirts = 0 +support_material = 1 +support_material_contact_distance = 0 +support_material_extruder = 4 +support_material_extrusion_width = 0.45 +support_material_interface_extruder = 4 +support_material_interface_spacing = 0.1 +support_material_synchronize_layers = 1 +support_material_threshold = 80 +support_material_with_sheath = 1 +wipe_tower = 1 + +[print:*0.05mm*] +inherits = *common* +bottom_solid_layers = 10 +bridge_acceleration = 300 +bridge_flow_ratio = 0.7 +default_acceleration = 500 +external_perimeter_speed = 20 +fill_density = 20% +first_layer_acceleration = 500 +gap_fill_speed = 20 +infill_acceleration = 800 +infill_speed = 30 +max_print_speed = 80 +small_perimeter_speed = 15 +solid_infill_speed = 30 +support_material_extrusion_width = 0.3 +support_material_spacing = 1.5 +layer_height = 0.05 +perimeter_acceleration = 300 +perimeter_speed = 30 +perimeters = 3 +support_material_speed = 30 +top_solid_infill_speed = 20 +top_solid_layers = 15 + +[print:0.05mm ULTRADETAIL] +inherits = *0.05mm* +infill_extrusion_width = 0.5 + +[print:0.05mm ULTRADETAIL MK3] +inherits = *0.05mm* +fill_pattern = grid +top_infill_extrusion_width = 0.4 + +[print:0.05mm ULTRADETAIL 0.25 nozzle] +inherits = *0.05mm* +external_perimeter_extrusion_width = 0 +extrusion_width = 0.28 +fill_density = 20% +first_layer_extrusion_width = 0.3 +infill_extrusion_width = 0 +infill_speed = 20 +max_print_speed = 100 +perimeter_extrusion_width = 0 +perimeter_speed = 20 +small_perimeter_speed = 10 +solid_infill_extrusion_width = 0 +solid_infill_speed = 20 +support_material_speed = 20 +top_infill_extrusion_width = 0 + +[print:0.05mm ULTRADETAIL 0.25 nozzle MK3] +inherits = *0.05mm*; *0.25nozzle* +fill_pattern = grid +top_infill_extrusion_width = 0.4 + +[print:*0.10mm*] +inherits = *common* +bottom_solid_layers = 7 +bridge_flow_ratio = 0.7 +layer_height = 0.1 +perimeter_acceleration = 800 +top_solid_layers = 9 + +[print:0.10mm DETAIL] +inherits = *0.10mm* +external_perimeter_speed = 40 +infill_acceleration = 2000 +infill_speed = 60 +perimeter_speed = 50 +solid_infill_speed = 50 + +[print:0.10mm DETAIL MK3] +inherits = *0.10mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 200 +perimeter_speed = 45 +solid_infill_speed = 170 +top_infill_extrusion_width = 0.4 +top_solid_infill_speed = 50 + +[print:0.10mm DETAIL 0.25 nozzle] +inherits = *0.10mm* +bridge_acceleration = 600 +external_perimeter_speed = 20 +infill_acceleration = 1600 +infill_speed = 40 +perimeter_acceleration = 600 +perimeter_speed = 25 +small_perimeter_speed = 10 +solid_infill_speed = 40 +top_solid_infill_speed = 30 + +[print:0.10mm DETAIL 0.25 nozzle MK3] +inherits = *0.10mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 200 +perimeter_speed = 45 +solid_infill_speed = 170 +top_infill_extrusion_width = 0.4 +top_solid_infill_speed = 50 + +[print:0.10mm DETAIL 0.6 nozzle MK3] +inherits = *0.10mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 200 +perimeter_speed = 45 +solid_infill_speed = 170 +top_infill_extrusion_width = 0.4 +top_solid_infill_speed = 50 + +[print:*0.15mm*] +inherits = *common* +bottom_solid_layers = 5 +external_perimeter_speed = 40 +infill_acceleration = 2000 +infill_speed = 60 +layer_height = 0.15 +perimeter_acceleration = 800 +perimeter_speed = 50 +solid_infill_speed = 50 +top_infill_extrusion_width = 0.4 +top_solid_layers = 7 + +[print:0.15mm 100mms Linear Advance] +inherits = *0.15mm* +bridge_flow_ratio = 0.95 +external_perimeter_speed = 50 +infill_speed = 100 +max_print_speed = 150 +perimeter_speed = 60 +small_perimeter_speed = 30 +solid_infill_speed = 100 +support_material_speed = 60 +top_solid_infill_speed = 70 + +[print:0.15mm OPTIMAL] +inherits = *0.15mm* +top_infill_extrusion_width = 0.45 + +[print:0.15mm OPTIMAL 0.25 nozzle] +inherits = *0.15mm*; *0.25nozzle* +bridge_acceleration = 600 +bridge_flow_ratio = 0.7 +external_perimeter_speed = 20 +infill_acceleration = 1600 +infill_speed = 40 +perimeter_acceleration = 600 +perimeter_speed = 25 +small_perimeter_speed = 10 +solid_infill_speed = 40 +support_material_extrusion_width = 0.2 +top_solid_infill_speed = 30 + +[print:0.15mm OPTIMAL 0.6 nozzle] +inherits = *0.15mm*; *0.6nozzle* + +[print:0.15mm OPTIMAL MK3] +inherits = *0.15mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 170 +perimeter_speed = 45 +solid_infill_speed = 170 +top_solid_infill_speed = 50 + +[print:0.15mm OPTIMAL SOLUBLE FULL] +inherits = *0.15mm*; *soluble_support* +external_perimeter_speed = 25 +notes = Set your solluble extruder in Multiple Extruders > Support material/raft/skirt extruder & Support material/raft interface extruder +perimeter_speed = 40 +solid_infill_speed = 40 +top_infill_extrusion_width = 0.45 +top_solid_infill_speed = 30 +wipe_tower = 1 + +[print:0.15mm OPTIMAL SOLUBLE INTERFACE] +inherits = 0.15mm OPTIMAL SOLUBLE FULL +notes = Set your solluble extruder in Multiple Extruders > Support material/raft interface extruder +support_material_extruder = 0 +support_material_interface_layers = 3 +support_material_with_sheath = 0 +support_material_xy_spacing = 80% + +[print:0.15mm OPTIMAL 0.25 nozzle MK3] +inherits = *0.15mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 170 +perimeter_speed = 45 +solid_infill_speed = 170 +top_solid_infill_speed = 50 +[print:*0.20mm*] +inherits = *common* +bottom_solid_layers = 4 +bridge_flow_ratio = 0.95 +external_perimeter_speed = 40 +infill_acceleration = 2000 +infill_speed = 60 +layer_height = 0.2 +perimeter_acceleration = 800 +perimeter_speed = 50 +solid_infill_speed = 50 +top_infill_extrusion_width = 0.4 +top_solid_layers = 5 + +[print:0.15mm OPTIMAL 0.6 nozzle MK3] +inherits = *0.15mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 170 +perimeter_speed = 45 +solid_infill_speed = 170 +top_solid_infill_speed = 50 + +[print:0.20mm 100mms Linear Advance] +inherits = *0.20mm* +external_perimeter_speed = 50 +infill_speed = 100 +max_print_speed = 150 +perimeter_speed = 60 +small_perimeter_speed = 30 +solid_infill_speed = 100 +support_material_speed = 60 +top_solid_infill_speed = 70 + +[print:0.20mm FAST MK3] +inherits = *0.20mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 170 +perimeter_speed = 45 +solid_infill_speed = 170 +top_solid_infill_speed = 50 + +[print:0.20mm NORMAL] +inherits = *0.20mm* + +[print:0.20mm NORMAL 0.6 nozzle] +inherits = *0.20mm*; *0.6nozzle* + +[print:0.20mm NORMAL SOLUBLE FULL] +inherits = *0.20mm*; *soluble_support* +external_perimeter_speed = 30 +notes = Set your solluble extruder in Multiple Extruders > Support material/raft/skirt extruder & Support material/raft interface extruder +perimeter_speed = 40 +solid_infill_speed = 40 +top_solid_infill_speed = 30 + +[print:0.20mm NORMAL SOLUBLE INTERFACE] +inherits = 0.20mm NORMAL SOLUBLE FULL +notes = Set your solluble extruder in Multiple Extruders > Support material/raft interface extruder +support_material_extruder = 0 +support_material_interface_layers = 3 +support_material_with_sheath = 0 +support_material_xy_spacing = 80% + +[print:0.20mm FAST 0.6 nozzle MK3] +inherits = *0.20mm* +bridge_speed = 30 +external_perimeter_speed = 35 +fill_pattern = grid +infill_acceleration = 1500 +infill_speed = 170 +max_print_speed = 170 +perimeter_speed = 45 +solid_infill_speed = 170 +top_solid_infill_speed = 50 + +[print:*0.35mm*] +inherits = *common* +bottom_solid_layers = 3 +external_perimeter_extrusion_width = 0.6 +external_perimeter_speed = 40 +first_layer_extrusion_width = 0.75 +infill_acceleration = 2000 +infill_speed = 60 +layer_height = 0.35 +perimeter_acceleration = 800 +perimeter_extrusion_width = 0.65 +perimeter_speed = 50 +solid_infill_extrusion_width = 0.65 +solid_infill_speed = 60 +top_solid_infill_speed = 50 +top_solid_layers = 4 + +[print:0.35mm FAST] +inherits = *0.35mm* +bridge_flow_ratio = 0.95 +first_layer_extrusion_width = 0.42 +perimeter_extrusion_width = 0.43 +solid_infill_extrusion_width = 0.7 +top_infill_extrusion_width = 0.43 + +[print:0.35mm FAST 0.6 nozzle] +inherits = *0.35mm*; *0.6nozzle* + +[print:0.35mm FAST sol full 0.6 nozzle] +inherits = *0.35mm*; *0.6nozzle*; *soluble_support* +external_perimeter_extrusion_width = 0.6 +external_perimeter_speed = 30 +notes = Set your solluble extruder in Multiple Extruders > Support material/raft interface extruder +perimeter_speed = 40 +support_material_extrusion_width = 0.55 +support_material_interface_layers = 3 +support_material_xy_spacing = 120% +top_infill_extrusion_width = 0.57 + +[print:0.35mm FAST sol int 0.6 nozzle] +inherits = 0.35mm FAST sol full 0.6 nozzle +support_material_extruder = 0 +support_material_interface_layers = 2 +support_material_with_sheath = 0 +support_material_xy_spacing = 150% + +[filament:*common*] +cooling = 1 +compatible_printers = +end_filament_gcode = "; Filament-specific end gcode" +extrusion_multiplier = 1 +filament_cost = 0 +filament_density = 0 +filament_diameter = 1.75 +filament_notes = "" +filament_settings_id = +filament_soluble = 0 +min_print_speed = 5 +slowdown_below_layer_time = 20 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}30{endif}; Filament gcode" + +[filament:*PLA*] +inherits = *common* +bed_temperature = 60 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +fan_always_on = 1 +fan_below_layer_time = 100 +filament_colour = #FF3232 +filament_max_volumetric_speed = 15 +filament_type = PLA +first_layer_bed_temperature = 60 +first_layer_temperature = 215 +max_fan_speed = 100 +min_fan_speed = 100 +temperature = 210 + +[filament:*PET*] +inherits = *common* +bed_temperature = 90 +bridge_fan_speed = 50 +disable_fan_first_layers = 3 +fan_always_on = 1 +fan_below_layer_time = 20 +filament_colour = #FF8000 +filament_max_volumetric_speed = 8 +filament_type = PET +first_layer_bed_temperature = 85 +first_layer_temperature = 230 +max_fan_speed = 50 +min_fan_speed = 30 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}45{endif}; Filament gcode" +temperature = 240 + +[filament:*ABS*] +inherits = *common* +bed_temperature = 110 +bridge_fan_speed = 30 +cooling = 0 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 20 +filament_colour = #3A80CA +filament_max_volumetric_speed = 11 +filament_type = ABS +first_layer_bed_temperature = 100 +first_layer_temperature = 255 +max_fan_speed = 30 +min_fan_speed = 20 +temperature = 255 + +[filament:*FLEX*] +inherits = *common* +bridge_fan_speed = 100 +cooling = 0 +disable_fan_first_layers = 1 +extrusion_multiplier = 1.2 +fan_always_on = 0 +fan_below_layer_time = 100 +filament_colour = #00CA0A +filament_max_volumetric_speed = 1.5 +filament_type = FLEX +first_layer_bed_temperature = 50 +first_layer_temperature = 240 +max_fan_speed = 90 +min_fan_speed = 70 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 240 + +[filament:ColorFabb Brass Bronze] +inherits = *PLA* +extrusion_multiplier = 1.2 +filament_colour = #804040 +filament_max_volumetric_speed = 10 + +[filament:ColorFabb HT] +inherits = *PET* +bed_temperature = 110 +bridge_fan_speed = 30 +cooling = 1 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 10 +first_layer_bed_temperature = 105 +first_layer_temperature = 270 +max_fan_speed = 20 +min_fan_speed = 10 +min_print_speed = 5 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}45{endif}; Filament gcode" +temperature = 270 + +[filament:ColorFabb PLA-PHA] +inherits = *PLA* + +[filament:ColorFabb Woodfil] +inherits = *PLA* +extrusion_multiplier = 1.2 +filament_colour = #804040 +filament_max_volumetric_speed = 10 +first_layer_temperature = 200 +min_print_speed = 5 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 200 + +[filament:ColorFabb XT] +inherits = *PET* +filament_type = PLA +first_layer_bed_temperature = 90 +first_layer_temperature = 260 +temperature = 270 + +[filament:ColorFabb XT-CF20] +inherits = *PET* +extrusion_multiplier = 1.2 +filament_colour = #804040 +filament_max_volumetric_speed = 1 +first_layer_bed_temperature = 90 +first_layer_temperature = 260 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}30{endif}; Filament gcode" +temperature = 260 + +[filament:ColorFabb nGen] +inherits = *PET* +bridge_fan_speed = 40 +fan_always_on = 0 +fan_below_layer_time = 10 +filament_type = NGEN +first_layer_temperature = 240 +max_fan_speed = 35 +min_fan_speed = 20 + +[filament:ColorFabb nGen flex] +inherits = *FLEX* +bed_temperature = 85 +bridge_fan_speed = 40 +cooling = 1 +disable_fan_first_layers = 3 +extrusion_multiplier = 1 +fan_below_layer_time = 10 +filament_max_volumetric_speed = 5 +first_layer_bed_temperature = 85 +first_layer_temperature = 260 +max_fan_speed = 35 +min_fan_speed = 20 +temperature = 260 + +[filament:E3D Edge] +inherits = *PET* +filament_notes = "List of manufacturers tested with standart PET print settings for MK2:\n\nE3D Edge\nFillamentum CPE GH100\nPlasty Mladeč PETG" + +[filament:E3D PC-ABS] +inherits = *ABS* +first_layer_temperature = 270 +temperature = 270 + +[filament:Fillamentum ABS] +inherits = *ABS* +first_layer_temperature = 240 +temperature = 240 + +[filament:Fillamentum ASA] +inherits = *ABS* +fan_always_on = 1 +first_layer_temperature = 265 +temperature = 265 + +[filament:Fillamentum CPE HG100 HM100] +inherits = *PET* +filament_notes = "CPE HG100 , CPE HM100" +first_layer_bed_temperature = 90 +first_layer_temperature = 275 +max_fan_speed = 50 +min_fan_speed = 50 +temperature = 275 + +[filament:Fillamentum Timberfil] +inherits = *PLA* +extrusion_multiplier = 1.2 +filament_colour = #804040 +filament_max_volumetric_speed = 10 +first_layer_temperature = 190 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 190 + +[filament:Generic ABS] +inherits = *ABS* +filament_notes = "List of materials tested with standart ABS print settings for MK2:\n\nEsun ABS\nFil-A-Gehr ABS\nHatchboxABS\nPlasty Mladeč ABS" + +[filament:Generic PET] +inherits = *PET* +filament_notes = "List of manufacturers tested with standart PET print settings for MK2:\n\nE3D Edge\nFillamentum CPE GH100\nPlasty Mladeč PETG" + +[filament:Generic PLA] +inherits = *PLA* +filament_notes = "List of materials tested with standart PLA print settings for MK2:\n\nDas Filament\nEsun PLA\nEUMAKERS PLA\nFiberlogy HD-PLA\nFillamentum PLA\nFloreon3D\nHatchbox PLA\nPlasty Mladeč PLA\nPrimavalue PLA\nProto pasta Matte Fiber\nVerbatim PLA\nVerbatim BVOH" + +[filament:Polymaker PC-Max] +inherits = *ABS* +bed_temperature = 115 +filament_colour = #3A80CA +first_layer_bed_temperature = 100 +first_layer_temperature = 270 +temperature = 270 + +[filament:Primavalue PVA] +inherits = *PLA* +cooling = 0 +fan_always_on = 0 +filament_colour = #FFFFD7 +filament_max_volumetric_speed = 10 +filament_notes = "List of materials tested with standart PVA print settings for MK2:\n\nPrimaSelect PVA+\nICE FILAMENTS PVA 'NAUGHTY NATURAL'\nVerbatim BVOH" +filament_soluble = 1 +filament_type = PVA +first_layer_temperature = 195 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 195 + +[filament:Prusa ABS] +inherits = *ABS* +filament_notes = "List of materials tested with standart ABS print settings for MK2:\n\nEsun ABS\nFil-A-Gehr ABS\nHatchboxABS\nPlasty Mladeč ABS" + +[filament:Prusa HIPS] +inherits = *ABS* +bridge_fan_speed = 50 +cooling = 1 +extrusion_multiplier = 0.9 +fan_always_on = 1 +fan_below_layer_time = 10 +filament_colour = #FFFFD7 +filament_soluble = 1 +filament_type = HIPS +first_layer_temperature = 220 +max_fan_speed = 20 +min_fan_speed = 20 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 220 + +[filament:Prusa PET] +inherits = *PET* +filament_notes = "List of manufacturers tested with standart PET print settings for MK2:\n\nE3D Edge\nFillamentum CPE GH100\nPlasty Mladeč PETG" + +[filament:Prusa PLA] +inherits = *PLA* +filament_notes = "List of materials tested with standart PLA print settings for MK2:\n\nDas Filament\nEsun PLA\nEUMAKERS PLA\nFiberlogy HD-PLA\nFillamentum PLA\nFloreon3D\nHatchbox PLA\nPlasty Mladeč PLA\nPrimavalue PLA\nProto pasta Matte Fiber\nVerbatim PLA\nVerbatim BVOH" + +[filament:SemiFlex or Flexfill 98A] +inherits = *FLEX* + +[filament:Taulman Bridge] +inherits = *common* +bed_temperature = 90 +bridge_fan_speed = 40 +cooling = 0 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 20 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 10 +filament_soluble = 0 +filament_type = PET +first_layer_bed_temperature = 60 +first_layer_temperature = 240 +max_fan_speed = 5 +min_fan_speed = 0 +min_print_speed = 5 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 250 + +[filament:Taulman T-Glase] +inherits = *PET* +bridge_fan_speed = 40 +cooling = 0 +fan_always_on = 0 +first_layer_bed_temperature = 90 +first_layer_temperature = 240 +max_fan_speed = 5 +min_fan_speed = 0 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}30{endif}; Filament gcode" + +[filament:Verbatim BVOH] +inherits = *common* +bed_temperature = 60 +bridge_fan_speed = 100 +cooling = 0 +disable_fan_first_layers = 1 +extrusion_multiplier = 1 +fan_always_on = 0 +fan_below_layer_time = 100 +filament_colour = #FFFFD7 +filament_max_volumetric_speed = 10 +filament_notes = "List of materials tested with standart PLA print settings for MK2:\n\nDas Filament\nEsun PLA\nEUMAKERS PLA\nFiberlogy HD-PLA\nFillamentum PLA\nFloreon3D\nHatchbox PLA\nPlasty Mladeč PLA\nPrimavalue PLA\nProto pasta Matte Fiber\nVerbatim PLA\nVerbatim BVOH" +filament_soluble = 1 +filament_type = PLA +first_layer_bed_temperature = 60 +first_layer_temperature = 215 +max_fan_speed = 100 +min_fan_speed = 100 +min_print_speed = 15 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 210 + +[filament:Verbatim PP] +inherits = *common* +bed_temperature = 100 +bridge_fan_speed = 100 +cooling = 1 +disable_fan_first_layers = 2 +extrusion_multiplier = 1 +fan_always_on = 1 +fan_below_layer_time = 100 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 5 +filament_notes = "List of materials tested with standart PLA print settings for MK2:\n\nEsun PLA\nFiberlogy HD-PLA\nFillamentum PLA\nFloreon3D\nHatchbox PLA\nPlasty Mladeč PLA\nPrimavalue PLA\nProto pasta Matte Fiber\nEUMAKERS PLA" +filament_type = PLA +first_layer_bed_temperature = 100 +first_layer_temperature = 220 +max_fan_speed = 100 +min_fan_speed = 100 +min_print_speed = 15 +start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" +temperature = 220 + +[printer:*common*] +bed_shape = 0x0,250x0,250x210,0x210 +before_layer_gcode = ;BEFORE_LAYER_CHANGE\n;[layer_z]\n\n +between_objects_gcode = +deretract_speed = 0 +end_gcode = G4 ; wait\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors +extruder_colour = #FFFF00 +extruder_offset = 0x0 +gcode_flavor = marlin +layer_gcode = ;AFTER_LAYER_CHANGE\n;[layer_z] +max_layer_height = 0.25 +min_layer_height = 0.07 +nozzle_diameter = 0.4 +octoprint_apikey = +octoprint_host = +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\n +printer_settings_id = +retract_before_travel = 1 +retract_before_wipe = 0% +retract_layer_change = 1 +retract_length = 0.8 +retract_length_toolchange = 4 +retract_lift = 0.6 +retract_lift_above = 0 +retract_lift_below = 199 +retract_restart_extra = 0 +retract_restart_extra_toolchange = 0 +retract_speed = 35 +serial_port = +serial_speed = 250000 +single_extruder_multi_material = 0 +start_gcode = M115 U3.1.0 ; tell printer latest fw version\nM201 X9000 Y9000 Z500 E10000 ; sets maximum accelerations, mm/sec^2\nM203 X500 Y500 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1500 T1500 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.2 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0 +toolchange_gcode = +use_firmware_retraction = 0 +use_relative_e_distances = 1 +use_volumetric_e = 0 +variable_layer_height = 1 +wipe = 1 +z_offset = 0 +printer_model = M2 +printer_variant = 0.4 +default_print_profile = 0.15mm OPTIMAL +default_filament_profile = Prusa PLA + +[printer:*multimaterial*] +inherits = *common* +deretract_speed = 50 +retract_before_travel = 3 +retract_before_wipe = 60% +retract_layer_change = 0 +retract_length = 4 +retract_lift = 0.6 +retract_lift_above = 0 +retract_lift_below = 199 +retract_restart_extra = 0 +retract_restart_extra_toolchange = 0 +retract_speed = 80 +single_extruder_multi_material = 1 +printer_model = M3 + +[printer:*mm-single*] +inherits = *multimaterial* +end_gcode = G1 E-4 F2100.00000\nG91\nG1 Z1 F7200.000\nG90\nG1 X245 Y1\nG1 X240 E4\nG1 F4000\nG1 X190 E2.7 \nG1 F4600\nG1 X110 E2.8\nG1 F5200\nG1 X40 E3 \nG1 E-15.0000 F5000\nG1 E-50.0000 F5400\nG1 E-15.0000 F3000\nG1 E-12.0000 F2000\nG1 F1600\nG1 X0 Y1 E3.0000\nG1 X50 Y1 E-5.0000\nG1 F2000\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-3.0000\nG4 S0\nM107 ; fan off\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nG28 X0 ; home X axis\nM84 ; disable motors\n\n +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN +start_gcode = M115 U3.1.0 ; tell printer latest fw version\nM201 X9000 Y9000 Z500 E10000 ; sets maximum accelerations, mm/sec^2\nM203 X500 Y500 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1500 T1500 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.2 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\n; Start G-Code sequence START\nT?\nM104 S[first_layer_temperature]\nM140 S[first_layer_bed_temperature]\nM109 S[first_layer_temperature]\nM190 S[first_layer_bed_temperature]\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nG28 W\nG80\nG92 E0.0\nM203 E100\nM92 E140\nG1 Z0.250 F7200.000\nG1 X50.0 E80.0 F1000.0\nG1 X160.0 E20.0 F1000.0\nG1 Z0.200 F7200.000\nG1 X220.0 E13 F1000.0\nG1 X240.0 E0 F1000.0\nG1 E-4 F1000.0\nG92 E0.0 + +[printer:*mm-multi*] +inherits = *multimaterial* +end_gcode = {if not has_wipe_tower}\n; Pull the filament into the cooling tubes.\nG1 E-4 F2100.00000\nG91\nG1 Z1 F7200.000\nG90\nG1 X245 Y1\nG1 X240 E4\nG1 F4000\nG1 X190 E2.7 \nG1 F4600\nG1 X110 E2.8\nG1 F5200\nG1 X40 E3 \nG1 E-15.0000 F5000\nG1 E-50.0000 F5400\nG1 E-15.0000 F3000\nG1 E-12.0000 F2000\nG1 F1600\nG1 X0 Y1 E3.0000\nG1 X50 Y1 E-5.0000\nG1 F2000\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-3.0000\nG4 S0\n{endif}\nM107 ; fan off\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nG28 X0 ; home X axis\nM84 ; disable motors +extruder_colour = #FFAA55;#5182DB;#4ECDD3;#FB7259 +nozzle_diameter = 0.4,0.4,0.4,0.4 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN +start_gcode = M115 U3.1.0 ; tell printer latest fw version\nM201 X9000 Y9000 Z500 E10000 ; sets maximum accelerations, mm/sec^2\nM203 X500 Y500 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1500 T1500 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.2 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\n; Start G-Code sequence START\nT[initial_tool]\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nG28 W\nG80\nG92 E0.0\nM203 E100 ; set max feedrate\nM92 E140 ; E-steps per filament milimeter\n{if not has_wipe_tower}\nG1 Z0.250 F7200.000\nG1 X50.0 E80.0 F1000.0\nG1 X160.0 E20.0 F1000.0\nG1 Z0.200 F7200.000\nG1 X220.0 E13 F1000.0\nG1 X240.0 E0 F1000.0\nG1 E-4 F1000.0\n{endif}\nG92 E0.0 +variable_layer_height = 0 + +[printer:Original Prusa i3 MK2] +inherits = *common* + +[printer:Original Prusa i3 MK2 0.25 nozzle] +inherits = *common* +max_layer_height = 0.1 +min_layer_height = 0.05 +nozzle_diameter = 0.25 +retract_length = 1 +retract_speed = 50 +variable_layer_height = 0 +printer_variant = 0.25 +default_print_profile = 0.10mm DETAIL 0.25 nozzle + +[printer:Original Prusa i3 MK2 0.6 nozzle] +inherits = *common* +max_layer_height = 0.35 +min_layer_height = 0.1 +nozzle_diameter = 0.6 +printer_variant = 0.6 + +[printer:Original Prusa i3 MK2 MM Single Mode] +inherits = *mm-single* + +[printer:Original Prusa i3 MK2 MM Single Mode 0.6 nozzle] +inherits = *mm-single* +nozzle_diameter = 0.6 +printer_variant = 0.6 + +[printer:Original Prusa i3 MK2 MultiMaterial] +inherits = *mm-multi* +nozzle_diameter = 0.4,0.4,0.4,0.4 + +[printer:Original Prusa i3 MK2 MultiMaterial 0.6 nozzle] +inherits = *mm-multi* +nozzle_diameter = 0.6,0.6,0.6,0.6 +printer_variant = 0.6 + +[printer:Original Prusa i3 MK3] +inherits = *common* +end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +retract_lift_below = 209 +start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} +printer_model = M1 +default_print_profile = 0.15mm OPTIMAL MK3 + +[printer:Original Prusa i3 MK3 0.25 nozzle] +inherits = *common* +nozzle_diameter = 0.25 +printer_variant = 0.25 +end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +retract_lift_below = 209 +start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} +printer_model = M1 +default_print_profile = 0.10mm DETAIL MK3 + +[printer:Original Prusa i3 MK3 0.6 nozzle] +inherits = *common* +nozzle_diameter = 0.6 +printer_variant = 0.6 +end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +retract_lift_below = 209 +start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} +printer_model = M1 +default_print_profile = 0.15mm OPTIMAL MK3 diff --git a/xs/src/slic3r/GUI/AppConfig.cpp b/xs/src/slic3r/GUI/AppConfig.cpp index 10a586e27..9e5ce5f1b 100644 --- a/xs/src/slic3r/GUI/AppConfig.cpp +++ b/xs/src/slic3r/GUI/AppConfig.cpp @@ -135,8 +135,6 @@ void AppConfig::save() bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const { - // std::cerr << "AppConfig::get_variant(" << vendor << ", " << model << ", " << variant << ") " << std::endl; - const auto it_v = m_vendors.find(vendor); if (it_v == m_vendors.end()) { return false; } const auto it_m = it_v->second.find(model); diff --git a/xs/src/slic3r/GUI/AppConfig.hpp b/xs/src/slic3r/GUI/AppConfig.hpp index e43ff51bf..7aac95fd6 100644 --- a/xs/src/slic3r/GUI/AppConfig.hpp +++ b/xs/src/slic3r/GUI/AppConfig.hpp @@ -68,9 +68,9 @@ public: void clear_section(const std::string §ion) { m_storage[section].clear(); } - bool get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const; - void set_variant(const std::string &vendor, const std::string &model, const std::string &variant, bool enable); - void set_vendors(const AppConfig &from); + bool get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const; + void set_variant(const std::string &vendor, const std::string &model, const std::string &variant, bool enable); + void set_vendors(const AppConfig &from); // return recent/skein_directory or recent/config_directory or empty string. std::string get_last_dir() const; @@ -86,8 +86,8 @@ public: void reset_selections(); // Whether the Slic3r version available online differs from this one - bool version_check_enabled() const; - bool slic3r_update_avail() const; + bool version_check_enabled() const; + bool slic3r_update_avail() const; // Get the default config path from Slic3r::data_dir(). static std::string config_path(); diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index 914ebb9a1..ce51d7641 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -1,6 +1,5 @@ #include "ConfigWizard_private.hpp" -#include // XXX #include #include #include @@ -23,9 +22,6 @@ namespace Slic3r { namespace GUI { -// FIXME: scrolling - - // Printer model picker GUI control struct PrinterPickerEvent : public wxEvent @@ -296,7 +292,7 @@ void PageVendors::on_vendor_pick(size_t i) for (PrinterPicker *picker : pickers) { picker->Hide(); } if (i < pickers.size()) { pickers[i]->Show(); - Layout(); + wizard_p()->layout_fit(); } } @@ -352,7 +348,7 @@ PageBedShape::PageBedShape(ConfigWizard *parent) : { append_text(_(L("Set the shape of your printer's bed."))); - shape_panel->build_panel(wizard_p()->custom_config.option("bed_shape")); + shape_panel->build_panel(wizard_p()->custom_config->option("bed_shape")); append(shape_panel); } @@ -583,7 +579,13 @@ void ConfigWizard::priv::set_page(ConfigWizardPage *page) btn_next->Show(page->page_next() != nullptr); btn_finish->Show(page->page_next() == nullptr); + layout_fit(); +} + +void ConfigWizard::priv::layout_fit() +{ q->Layout(); + q->Fit(); } void ConfigWizard::priv::enable_next(bool enable) @@ -619,9 +621,9 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese preset_bundle->load_presets(*app_config); } else { for (ConfigWizardPage *page = page_firmware; page != nullptr; page = page->page_next()) { - page->apply_custom_config(custom_config); + page->apply_custom_config(*custom_config); } - preset_bundle->load_config("My Settings", custom_config); + preset_bundle->load_config("My Settings", *custom_config); } } @@ -632,10 +634,9 @@ ConfigWizard::ConfigWizard(wxWindow *parent) : p(new priv(this)) { p->load_vendors(); - std::unique_ptr custom_config_defaults(DynamicPrintConfig::new_from_defaults_keys({ + p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ "gcode_flavor", "bed_shape", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", })); - p->custom_config.apply(*custom_config_defaults); p->index = new ConfigWizardIndex(this); diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp index 9f9395975..d32a609be 100644 --- a/xs/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -22,6 +22,8 @@ namespace GUI { enum { + CONTENT_WIDTH = 500, + DIALOG_MARGIN = 15, INDEX_MARGIN = 40, BTN_SPACING = 10, @@ -38,10 +40,6 @@ struct PrinterPicker: wxPanel struct ConfigWizardPage: wxPanel { - enum { - CONTENT_WIDTH = 500, - }; - ConfigWizard *parent; const wxString shortname; wxBoxSizer *content; @@ -171,7 +169,7 @@ struct ConfigWizard::priv ConfigWizard *q; AppConfig appconfig_vendors; PresetBundle bundle_vendors; - DynamicPrintConfig custom_config; + std::unique_ptr custom_config; wxBoxSizer *topsizer = nullptr; wxBoxSizer *btnsizer = nullptr; @@ -196,6 +194,7 @@ struct ConfigWizard::priv void add_page(ConfigWizardPage *page); void index_refresh(); void set_page(ConfigWizardPage *page); + void layout_fit(); void go_prev() { if (page_current != nullptr) { set_page(page_current->page_prev()); } } void go_next() { if (page_current != nullptr) { set_page(page_current->page_next()); } } void enable_next(bool enable); diff --git a/xs/xsp/my.map b/xs/xsp/my.map index 9c0f60c1d..c1ca58827 100644 --- a/xs/xsp/my.map +++ b/xs/xsp/my.map @@ -235,9 +235,6 @@ PresetHints* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T TabIface* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T -# TODO: remove: -# ConfigWizard* O_OBJECT_SLIC3R -# Ref O_OBJECT_SLIC3R_T PresetUpdater* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T From 90a8ef8e9f208dce144cea4252173079fd0f830c Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Fri, 6 Apr 2018 13:18:12 +0200 Subject: [PATCH 07/38] Cleanup --- lib/Slic3r/GUI/MainFrame.pm | 44 +--------------------- resources/profiles/PrusaResearch.ini | 5 --- xs/src/slic3r/GUI/ConfigWizard.cpp | 15 +++++--- xs/src/slic3r/GUI/ConfigWizard.hpp | 3 +- xs/src/slic3r/GUI/ConfigWizard_private.hpp | 4 +- xs/src/slic3r/GUI/GUI.cpp | 4 +- xs/src/slic3r/GUI/GUI.hpp | 5 +-- xs/src/slic3r/GUI/Preset.cpp | 1 - xs/src/slic3r/GUI/Preset.hpp | 5 +-- xs/src/slic3r/GUI/PresetBundle.cpp | 6 +-- xs/xsp/GUI.xsp | 4 +- 11 files changed, 23 insertions(+), 73 deletions(-) diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index 4307375c8..402cb8051 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -644,48 +644,8 @@ sub config_wizard { # Exit wizard if there are unsaved changes and the user cancels the action. return unless $self->check_unsaved_changes; - - # TODO: Offer "reset user profile" - Slic3r::GUI::open_config_wizard(wxTheApp->{preset_bundle}); - # Load the currently selected preset into the GUI, update the preset selection box. - foreach my $tab (values %{$self->{options_tabs}}) { # XXX: only if not cancelled? - $tab->load_current_preset; - } - return; - - # Enumerate the profiles bundled with the Slic3r installation under resources/profiles. - my $directory = Slic3r::resources_dir() . "/profiles"; - my @profiles = (); - if (opendir(DIR, Slic3r::encode_path($directory))) { - while (my $file = readdir(DIR)) { - if ($file =~ /\.ini$/) { - $file =~ s/\.ini$//; - push @profiles, Slic3r::decode_path($file); - } - } - closedir(DIR); - } - # Open the wizard. - if (my $result = Slic3r::GUI::ConfigWizard->new($self, \@profiles, $fresh_start)->run) { - eval { - if ($result->{reset_user_profile}) { - wxTheApp->{preset_bundle}->reset(1); - } - if (defined $result->{config}) { - # Load and save the settings into print, filament and printer presets. - wxTheApp->{preset_bundle}->load_config('My Settings', $result->{config}); - } else { - # Wizard returned a name of a preset bundle bundled with the installation. Unpack it. - wxTheApp->{preset_bundle}->install_vendor_configbundle($directory . '/' . $result->{preset_name} . '.ini'); - # Reset the print / filament / printer selections, so that following line will select some sensible defaults. - if ($fresh_start) { - wxTheApp->{app_config}->reset_selections; - } - # Reload all presets after the vendor config bundle has been installed. - wxTheApp->{preset_bundle}->load_presets(wxTheApp->{app_config}); - } - }; - Slic3r::GUI::catch_error($self) and return; + # TODO: Offer "reset user profile" ??? + if (Slic3r::GUI::open_config_wizard(wxTheApp->{preset_bundle})) { # Load the currently selected preset into the GUI, update the preset selection box. foreach my $tab (values %{$self->{options_tabs}}) { $tab->load_current_preset; diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index 6dc29a96e..b654b4039 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -1017,8 +1017,3 @@ retract_lift_below = 209 start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} printer_model = MK3 default_print_profile = 0.15mm OPTIMAL MK3 - -[presets] -print = 0.15mm OPTIMAL MK3 -printer = Original Prusa i3 MK3 -filament = Prusa PLA diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index ce51d7641..032b5e903 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -412,16 +412,16 @@ void PageDiameters::apply_custom_config(DynamicPrintConfig &config) PageTemperatures::PageTemperatures(ConfigWizard *parent) : ConfigWizardPage(parent, _(L("Extruder and Bed Temperatures")), _(L("Temperatures"))), - spin_extr(new wxSpinCtrl(this, wxID_ANY)), - spin_bed(new wxSpinCtrl(this, wxID_ANY)) + spin_extr(new wxSpinCtrlDouble(this, wxID_ANY)), + spin_bed(new wxSpinCtrlDouble(this, wxID_ANY)) { - spin_extr->SetIncrement(5); + spin_extr->SetIncrement(5.0); const auto &def_extr = print_config_def.options["temperature"]; spin_extr->SetRange(def_extr.min, def_extr.max); auto *default_extr = dynamic_cast(def_extr.default_value); spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); - spin_bed->SetIncrement(5); + spin_bed->SetIncrement(5.0); const auto &def_bed = print_config_def.options["bed_temperature"]; spin_bed->SetRange(def_bed.min, def_bed.max); auto *default_bed = dynamic_cast(def_bed.default_value); @@ -541,7 +541,7 @@ void ConfigWizard::priv::load_vendors() const auto vendor_dir = fs::path(Slic3r::data_dir()) / "vendor"; for (fs::directory_iterator it(vendor_dir); it != fs::directory_iterator(); ++it) { if (it->path().extension() == ".ini") { - bundle_vendors.load_configbundle(it->path().native(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); + bundle_vendors.load_configbundle(it->path().string(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); } } @@ -688,7 +688,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent) : ConfigWizard::~ConfigWizard() {} -void ConfigWizard::run(wxWindow *parent, PresetBundle *preset_bundle) +bool ConfigWizard::run(wxWindow *parent, PresetBundle *preset_bundle) { const auto profiles_dir = fs::path(resources_dir()) / "profiles"; for (fs::directory_iterator it(profiles_dir); it != fs::directory_iterator(); ++it) { @@ -700,6 +700,9 @@ void ConfigWizard::run(wxWindow *parent, PresetBundle *preset_bundle) ConfigWizard wizard(parent); if (wizard.ShowModal() == wxID_OK) { wizard.p->apply_config(GUI::get_app_config(), preset_bundle); + return true; + } else { + return false; } } diff --git a/xs/src/slic3r/GUI/ConfigWizard.hpp b/xs/src/slic3r/GUI/ConfigWizard.hpp index a06388396..40ecf09a1 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard.hpp @@ -15,7 +15,6 @@ namespace GUI { class ConfigWizard: public wxDialog { public: - // ConfigWizard(wxWindow *parent, const PresetBundle &bundle); ConfigWizard(wxWindow *parent); ConfigWizard(ConfigWizard &&) = delete; ConfigWizard(const ConfigWizard &) = delete; @@ -23,7 +22,7 @@ public: ConfigWizard &operator=(const ConfigWizard &) = delete; ~ConfigWizard(); - static void run(wxWindow *parent, PresetBundle *preset_bundle); + static bool run(wxWindow *parent, PresetBundle *preset_bundle); private: struct priv; std::unique_ptr p; diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp index d32a609be..652328aaa 100644 --- a/xs/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -136,8 +136,8 @@ struct PageDiameters: ConfigWizardPage struct PageTemperatures: ConfigWizardPage { - wxSpinCtrl *spin_extr; - wxSpinCtrl *spin_bed; + wxSpinCtrlDouble *spin_extr; + wxSpinCtrlDouble *spin_bed; PageTemperatures(ConfigWizard *parent); virtual void apply_custom_config(DynamicPrintConfig &config); diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index 80e232287..310860e53 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -353,13 +353,13 @@ void add_debug_menu(wxMenuBar *menu, int event_language_change) //#endif } -void open_config_wizard(PresetBundle *preset_bundle) +bool open_config_wizard(PresetBundle *preset_bundle) { if (g_wxMainFrame == nullptr) { throw std::runtime_error("Main frame not set"); } - ConfigWizard::run(g_wxMainFrame, preset_bundle); + return ConfigWizard::run(g_wxMainFrame, preset_bundle); } void open_preferences_dialog(int event_preferences) diff --git a/xs/src/slic3r/GUI/GUI.hpp b/xs/src/slic3r/GUI/GUI.hpp index 98a124091..321c97d36 100644 --- a/xs/src/slic3r/GUI/GUI.hpp +++ b/xs/src/slic3r/GUI/GUI.hpp @@ -73,7 +73,6 @@ void break_to_debugger(); // Passing the wxWidgets GUI classes instantiated by the Perl part to C++. void set_wxapp(wxApp *app); void set_main_frame(wxFrame *main_frame); -// wxFrame* get_main_frame(); void set_tab_panel(wxNotebook *tab_panel); void set_app_config(AppConfig *app_config); void set_preset_bundle(PresetBundle *preset_bundle); @@ -85,8 +84,8 @@ wxColour* get_sys_label_clr(); void add_debug_menu(wxMenuBar *menu, int event_language_change); -// Opens the first-time configuration wizard -void open_config_wizard(PresetBundle *preset_bundle); +// Opens the first-time configuration wizard, returns true if wizard is finished & accepted. +bool open_config_wizard(PresetBundle *preset_bundle); // Create "Preferences" dialog after selecting menu "Preferences" in Perl part void open_preferences_dialog(int event_preferences); diff --git a/xs/src/slic3r/GUI/Preset.cpp b/xs/src/slic3r/GUI/Preset.cpp index 40afca144..66836074e 100644 --- a/xs/src/slic3r/GUI/Preset.cpp +++ b/xs/src/slic3r/GUI/Preset.cpp @@ -183,7 +183,6 @@ void Preset::set_visible_from_appconfig(const AppConfig &app_config) const std::string &variant = config.opt_string("printer_variant"); if (model.empty() || variant.empty()) { return; } is_visible = app_config.get_variant(vendor->id, model, variant); - std::cerr << vendor->id << " / " << model << " / " << variant << ": visible: " << is_visible << std::endl; } const std::vector& Preset::print_options() diff --git a/xs/src/slic3r/GUI/Preset.hpp b/xs/src/slic3r/GUI/Preset.hpp index 075eed9af..4f734b85e 100644 --- a/xs/src/slic3r/GUI/Preset.hpp +++ b/xs/src/slic3r/GUI/Preset.hpp @@ -37,14 +37,12 @@ public: PrinterVariant() {} PrinterVariant(const std::string &name) : name(name) {} std::string name; - // bool enabled = true; // TODO: remove these? }; struct PrinterModel { PrinterModel() {} std::string id; std::string name; - // bool enabled = true; std::vector variants; PrinterVariant* variant(const std::string &name) { for (auto &v : this->variants) @@ -87,7 +85,8 @@ public: bool is_external = false; // System preset is read-only. bool is_system = false; - // Preset is visible, if it is compatible with the active Printer. TODO: fix + // Preset is visible, if it is associated with a printer model / variant that is enabled in the AppConfig + // or if it has no printer model / variant association. // Also the "default" preset is only visible, if it is the only preset in the list. bool is_visible = true; // Has this preset been modified? diff --git a/xs/src/slic3r/GUI/PresetBundle.cpp b/xs/src/slic3r/GUI/PresetBundle.cpp index 8645a4f2d..bd9e35ca8 100644 --- a/xs/src/slic3r/GUI/PresetBundle.cpp +++ b/xs/src/slic3r/GUI/PresetBundle.cpp @@ -4,7 +4,6 @@ #include "PresetBundle.hpp" #include "BitmapCache.hpp" -#include // XXX #include #include #include @@ -201,10 +200,7 @@ static inline std::string remove_ini_suffix(const std::string &name) // If the "vendor" section is missing, enable all models and variants of the particular vendor. void PresetBundle::load_installed_printers(const AppConfig &config) { - std::cerr << "load_installed_printers()" << std::endl; - for (auto &preset : printers) { - std::cerr << "preset: printer: " << preset.name << std::endl; preset.set_visible_from_appconfig(config); } } @@ -742,7 +738,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla const VendorProfile *vendor_profile = nullptr; if (flags & (LOAD_CFGBNDLE_SYSTEM | LOAD_CFGBUNDLE_VENDOR_ONLY)) { boost::filesystem::path fspath(path); - VendorProfile vp(fspath.stem().native()); + VendorProfile vp(fspath.stem().string()); load_vendor_profile(tree, vp); if (vp.name.empty()) throw std::runtime_error(std::string("Vendor Config Bundle is not valid: Missing vendor name key.")); diff --git a/xs/xsp/GUI.xsp b/xs/xsp/GUI.xsp index ad7f69a2d..fcf465a29 100644 --- a/xs/xsp/GUI.xsp +++ b/xs/xsp/GUI.xsp @@ -54,8 +54,8 @@ int combochecklist_get_flags(SV *ui) void set_app_config(AppConfig *app_config) %code%{ Slic3r::GUI::set_app_config(app_config); %}; -void open_config_wizard(PresetBundle *preset_bundle) - %code%{ Slic3r::GUI::open_config_wizard(preset_bundle); %}; +bool open_config_wizard(PresetBundle *preset_bundle) + %code%{ RETVAL=Slic3r::GUI::open_config_wizard(preset_bundle); %}; void open_preferences_dialog(int preferences_event) %code%{ Slic3r::GUI::open_preferences_dialog(preferences_event); %}; From 57f6601c9d6b506ebc3aacdb580c59d3c90633d2 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Mon, 9 Apr 2018 10:41:34 +0200 Subject: [PATCH 08/38] ConfigWizard: Fix logo rendering --- xs/src/slic3r/GUI/ConfigWizard.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index 032b5e903..f13448c37 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -477,10 +477,22 @@ ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) : bullet_white(GUI::from_u8(Slic3r::var("bullet_white.png")), wxBITMAP_TYPE_PNG) { SetMinSize(bg.GetSize()); - Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); wxClientDC dc(this); text_height = dc.GetCharHeight(); + + // Add logo bitmap. + // This could be done in on_paint() along with the index labels, but I've found it tricky + // to get the bitmap rendered well on all platforms with transparent background. + // In some cases it didn't work at all. And so wxStaticBitmap is used here instead, + // because it has all the platform quirks figured out. + auto *sizer = new wxBoxSizer(wxVERTICAL); + auto *logo = new wxStaticBitmap(this, wxID_ANY, bg); + sizer->AddStretchSpacer(); + sizer->Add(logo); + SetSizer(sizer); + + Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); } void ConfigWizardIndex::load_items(ConfigWizardPage *firstpage) @@ -509,12 +521,9 @@ void ConfigWizardIndex::on_paint(wxPaintEvent & evt) }; const auto size = GetClientSize(); - const auto h = size.GetHeight(); - const auto w = size.GetWidth(); - if (h == 0 || w == 0) { return; } + if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; } wxPaintDC dc(this); - dc.DrawBitmap(bg, 0, h - bg.GetHeight(), false); const auto bullet_w = bullet_black.GetSize().GetWidth(); const auto bullet_h = bullet_black.GetSize().GetHeight(); From b8a06d728ad523b5fa1542840c02e9dc67e0403e Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Mon, 9 Apr 2018 16:24:34 +0200 Subject: [PATCH 09/38] Fixes in 2DBed --- xs/src/slic3r/GUI/2DBed.cpp | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/xs/src/slic3r/GUI/2DBed.cpp b/xs/src/slic3r/GUI/2DBed.cpp index c5d68400d..6d788cf34 100644 --- a/xs/src/slic3r/GUI/2DBed.cpp +++ b/xs/src/slic3r/GUI/2DBed.cpp @@ -1,4 +1,4 @@ -#include "2DBed.hpp"; +#include "2DBed.hpp" #include #include "BoundingBox.hpp" @@ -66,7 +66,7 @@ void Bed_2D::repaint() shift.y - (cbb.max.y - GetSize().GetHeight())); // draw bed fill - dc.SetBrush(*new wxBrush(*new wxColour(255, 255, 255), wxSOLID)); + dc.SetBrush(wxBrush(wxColour(255, 255, 255), wxSOLID)); wxPointList pt_list; for (auto pt: m_bed_shape) { @@ -87,7 +87,7 @@ void Bed_2D::repaint() } polylines = intersection_pl(polylines, bed_polygon); - dc.SetPen(*new wxPen(*new wxColour(230, 230, 230), 1, wxSOLID)); + dc.SetPen(wxPen(wxColour(230, 230, 230), 1, wxSOLID)); for (auto pl : polylines) { for (size_t i = 0; i < pl.points.size()-1; i++){ @@ -98,8 +98,8 @@ void Bed_2D::repaint() } // draw bed contour - dc.SetPen(*new wxPen(*new wxColour(0, 0, 0), 1, wxSOLID)); - dc.SetBrush(*new wxBrush(*new wxColour(0, 0, 0), wxTRANSPARENT)); + dc.SetPen(wxPen(wxColour(0, 0, 0), 1, wxSOLID)); + dc.SetBrush(wxBrush(wxColour(0, 0, 0), wxTRANSPARENT)); dc.DrawPolygon(&pt_list, 0, 0); auto origin_px = to_pixels(Pointf(0, 0)); @@ -108,7 +108,7 @@ void Bed_2D::repaint() auto axes_len = 50; auto arrow_len = 6; auto arrow_angle = Geometry::deg2rad(45.0); - dc.SetPen(*new wxPen(*new wxColour(255, 0, 0), 2, wxSOLID)); // red + dc.SetPen(wxPen(wxColour(255, 0, 0), 2, wxSOLID)); // red auto x_end = Pointf(origin_px.x + axes_len, origin_px.y); dc.DrawLine(wxPoint(origin_px.x, origin_px.y), wxPoint(x_end.x, x_end.y)); for (auto angle : { -arrow_angle, arrow_angle }){ @@ -118,7 +118,7 @@ void Bed_2D::repaint() dc.DrawLine(wxPoint(x_end.x, x_end.y), wxPoint(end.x, end.y)); } - dc.SetPen(*new wxPen(*new wxColour(0, 255, 0), 2, wxSOLID)); // green + dc.SetPen(wxPen(wxColour(0, 255, 0), 2, wxSOLID)); // green auto y_end = Pointf(origin_px.x, origin_px.y - axes_len); dc.DrawLine(wxPoint(origin_px.x, origin_px.y), wxPoint(y_end.x, y_end.y)); for (auto angle : { -arrow_angle, arrow_angle }) { @@ -129,19 +129,23 @@ void Bed_2D::repaint() } // draw origin - dc.SetPen(*new wxPen(*new wxColour(0, 0, 0), 1, wxSOLID)); - dc.SetBrush(*new wxBrush(*new wxColour(0, 0, 0), wxSOLID)); + dc.SetPen(wxPen(wxColour(0, 0, 0), 1, wxSOLID)); + dc.SetBrush(wxBrush(wxColour(0, 0, 0), wxSOLID)); dc.DrawCircle(origin_px.x, origin_px.y, 3); - dc.SetTextForeground(*new wxColour(0, 0, 0)); - dc.SetFont(*new wxFont(10, wxDEFAULT, wxNORMAL, wxNORMAL)); - dc.DrawText("(0,0)", origin_px.x + 1, origin_px.y + 2); + static const auto origin_label = wxString("(0,0)"); + dc.SetTextForeground(wxColour(0, 0, 0)); + dc.SetFont(wxFont(10, wxDEFAULT, wxNORMAL, wxNORMAL)); + auto extent = dc.GetTextExtent(origin_label); + const auto origin_label_x = origin_px.x <= cw / 2 ? origin_px.x + 1 : origin_px.x - 1 - extent.GetWidth(); + const auto origin_label_y = origin_px.y <= ch / 2 ? origin_px.y + 1 : origin_px.y - 1 - extent.GetHeight(); + dc.DrawText(origin_label, origin_label_x, origin_label_y); // draw current position if (m_pos!= Pointf(0, 0)) { auto pos_px = to_pixels(m_pos); - dc.SetPen(*new wxPen(*new wxColour(200, 0, 0), 2, wxSOLID)); - dc.SetBrush(*new wxBrush(*new wxColour(200, 0, 0), wxTRANSPARENT)); + dc.SetPen(wxPen(wxColour(200, 0, 0), 2, wxSOLID)); + dc.SetBrush(wxBrush(wxColour(200, 0, 0), wxTRANSPARENT)); dc.DrawCircle(pos_px.x, pos_px.y, 5); dc.DrawLine(pos_px.x - 15, pos_px.y, pos_px.x + 15, pos_px.y); From 26511deec0f4b86ee1f7597281f4553fbe91bca2 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Mon, 9 Apr 2018 16:39:50 +0200 Subject: [PATCH 10/38] Add '-alpha' suffix to data directory for now --- xs/src/libslic3r/utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xs/src/libslic3r/utils.cpp b/xs/src/libslic3r/utils.cpp index 34b9eaa9f..14736468a 100644 --- a/xs/src/libslic3r/utils.cpp +++ b/xs/src/libslic3r/utils.cpp @@ -119,7 +119,7 @@ static std::string g_data_dir; void set_data_dir(const std::string &dir) { - g_data_dir = dir; + g_data_dir = dir + "-alpha"; // FIXME: Resolve backcompat problems } const std::string& data_dir() From 32c4cddb91f91a35dd468c3f394911a0b7ad952b Mon Sep 17 00:00:00 2001 From: bubnikv Date: Mon, 9 Apr 2018 17:03:37 +0200 Subject: [PATCH 11/38] Ported the AboutDialog to C++, thanks @alexrj for the work. New "configuration" menu over the snapshots, user preferences etc. --- lib/Slic3r/GUI.pm | 8 -- lib/Slic3r/GUI/AboutDialog.pm | 122 -------------------------- lib/Slic3r/GUI/MainFrame.pm | 14 +-- xs/CMakeLists.txt | 6 +- xs/lib/Slic3r/XS.pm | 2 +- xs/src/libslic3r/FileParserError.hpp | 4 + xs/src/slic3r/Config/Snapshot.cpp | 36 ++++++++ xs/src/slic3r/Config/Snapshot.hpp | 7 ++ xs/src/slic3r/Config/Version.cpp | 47 +++++++++- xs/src/slic3r/Config/Version.hpp | 12 ++- xs/src/slic3r/GUI/AboutDialog.cpp | 125 +++++++++++++++++++++++++++ xs/src/slic3r/GUI/AboutDialog.hpp | 36 ++++++++ xs/src/slic3r/GUI/GUI.cpp | 74 +++++++++++----- xs/src/slic3r/GUI/GUI.hpp | 13 +-- xs/xsp/GUI.xsp | 12 +-- 15 files changed, 336 insertions(+), 182 deletions(-) delete mode 100644 lib/Slic3r/GUI/AboutDialog.pm create mode 100644 xs/src/slic3r/GUI/AboutDialog.cpp create mode 100644 xs/src/slic3r/GUI/AboutDialog.hpp diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 4ec388c14..88e4745d1 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -7,7 +7,6 @@ use File::Basename qw(basename); use FindBin; use List::Util qw(first); use Slic3r::GUI::2DBed; -use Slic3r::GUI::AboutDialog; use Slic3r::GUI::BedShapeDialog; use Slic3r::GUI::ConfigWizard; use Slic3r::GUI::Controller; @@ -191,13 +190,6 @@ sub recreate_GUI{ } } -sub about { - my ($self) = @_; - my $about = Slic3r::GUI::AboutDialog->new(undef); - $about->ShowModal; - $about->Destroy; -} - sub system_info { my ($self) = @_; my $slic3r_info = Slic3r::slic3r_info(format => 'html'); diff --git a/lib/Slic3r/GUI/AboutDialog.pm b/lib/Slic3r/GUI/AboutDialog.pm deleted file mode 100644 index 0879ea35b..000000000 --- a/lib/Slic3r/GUI/AboutDialog.pm +++ /dev/null @@ -1,122 +0,0 @@ -package Slic3r::GUI::AboutDialog; -use strict; -use warnings; -use utf8; - -use Wx qw(:font :html :misc :dialog :sizer :systemsettings :frame :id); -use Wx::Event qw(EVT_HTML_LINK_CLICKED EVT_LEFT_DOWN EVT_BUTTON); -use Wx::Print; -use Wx::Html; -use base 'Wx::Dialog'; - -sub new { - my $class = shift; - my ($parent) = @_; - my $self = $class->SUPER::new($parent, -1, 'About Slic3r', wxDefaultPosition, [600, 340], wxCAPTION); - - $self->SetBackgroundColour(Wx::wxWHITE); - my $hsizer = Wx::BoxSizer->new(wxHORIZONTAL); - $self->SetSizer($hsizer); - - # logo - my $logo = Slic3r::GUI::AboutDialog::Logo->new($self, -1, wxDefaultPosition, wxDefaultSize); - $logo->SetBackgroundColour(Wx::wxWHITE); - $hsizer->Add($logo, 0, wxEXPAND | wxLEFT | wxRIGHT, 30); - - my $vsizer = Wx::BoxSizer->new(wxVERTICAL); - $hsizer->Add($vsizer, 1, wxEXPAND, 0); - - # title - my $title = Wx::StaticText->new($self, -1, $Slic3r::FORK_NAME, wxDefaultPosition, wxDefaultSize); - my $title_font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - $title_font->SetWeight(wxFONTWEIGHT_BOLD); - $title_font->SetFamily(wxFONTFAMILY_ROMAN); - $title_font->SetPointSize(24); - $title->SetFont($title_font); - $vsizer->Add($title, 0, wxALIGN_LEFT | wxTOP, 30); - - # version - my $version = Wx::StaticText->new($self, -1, "Version $Slic3r::VERSION", wxDefaultPosition, wxDefaultSize); - my $version_font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - $version_font->SetPointSize(&Wx::wxMSW ? 9 : 11); - $version->SetFont($version_font); - $vsizer->Add($version, 0, wxALIGN_LEFT | wxBOTTOM, 10); - - # text - my $text = - '' . - '' . - '' . - 'Copyright © 2016 Vojtech Bubnik, Prusa Research.
' . - 'Copyright © 2011-2016 Alessandro Ranellucci.
' . - 'Slic3r is licensed under the ' . - 'GNU Affero General Public License, version 3.' . - '


' . - 'Contributions by Henrik Brix Andersen, Nicolas Dandrimont, Mark Hindess, Petr Ledvina, Y. Sapir, Mike Sheldrake and numerous others. ' . - 'Manual by Gary Hodgson. Inspired by the RepRap community.
' . - 'Slic3r logo designed by Corey Daniels, Silk Icon Set designed by Mark James. ' . - '
' . - '' . - ''; - my $html = Wx::HtmlWindow->new($self, -1, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_NEVER); - my $font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - my $size = &Wx::wxMSW ? 8 : 10; - $html->SetFonts($font->GetFaceName, $font->GetFaceName, [$size, $size, $size, $size, $size, $size, $size]); - $html->SetBorders(2); - $html->SetPage($text); - $vsizer->Add($html, 1, wxEXPAND | wxALIGN_LEFT | wxRIGHT | wxBOTTOM, 20); - EVT_HTML_LINK_CLICKED($self, $html, \&link_clicked); - - my $buttons = $self->CreateStdDialogButtonSizer(wxOK); - $self->SetEscapeId(wxID_CLOSE); - EVT_BUTTON($self, wxID_CLOSE, sub { - $self->EndModal(wxID_CLOSE); - $self->Close; - }); - $vsizer->Add($buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3); - - EVT_LEFT_DOWN($self, sub { $self->Close }); - EVT_LEFT_DOWN($logo, sub { $self->Close }); - - return $self; -} - -sub link_clicked { - my ($self, $event) = @_; - - Wx::LaunchDefaultBrowser($event->GetLinkInfo->GetHref); - $event->Skip(0); -} - -package Slic3r::GUI::AboutDialog::Logo; -use Wx qw(:bitmap :dc); -use Wx::Event qw(EVT_PAINT); -use base 'Wx::Panel'; - -sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - - $self->{logo} = Wx::Bitmap->new(Slic3r::var("Slic3r_192px.png"), wxBITMAP_TYPE_PNG); - $self->SetMinSize(Wx::Size->new($self->{logo}->GetWidth, $self->{logo}->GetHeight)); - - EVT_PAINT($self, \&repaint); - - return $self; -} - -sub repaint { - my ($self, $event) = @_; - - my $dc = Wx::PaintDC->new($self); - $dc->SetBackgroundMode(wxTRANSPARENT); - - my $size = $self->GetSize; - my $logo_w = $self->{logo}->GetWidth; - my $logo_h = $self->{logo}->GetHeight; - $dc->DrawBitmap($self->{logo}, ($size->GetWidth - $logo_w) / 2, ($size->GetHeight - $logo_h) / 2, 1); - - $event->Skip; -} - -1; diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index b2f51b9e1..31124e432 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -237,12 +237,6 @@ sub _init_menubar { $self->repair_stl; }, undef, 'wrench.png'); $fileMenu->AppendSeparator(); - # Cmd+, is standard on OS X - what about other operating systems? - $self->_append_menu_item($fileMenu, L("Preferences…\tCtrl+,"), L('Application preferences'), sub { - # Opening the C++ preferences dialog. - Slic3r::GUI::open_preferences_dialog($self->{preferences_event}); - }, wxID_PREFERENCES); - $fileMenu->AppendSeparator(); $self->_append_menu_item($fileMenu, L("&Quit"), L('Quit Slic3r'), sub { $self->Close(0); }, wxID_EXIT); @@ -348,7 +342,7 @@ sub _init_menubar { Wx::LaunchDefaultBrowser('http://github.com/prusa3d/slic3r/issues/new'); }); $self->_append_menu_item($helpMenu, L("&About Slic3r"), L('Show about dialog'), sub { - wxTheApp->about; + Slic3r::GUI::about; }); } @@ -362,11 +356,9 @@ sub _init_menubar { $menubar->Append($self->{object_menu}, L("&Object")) if $self->{object_menu}; $menubar->Append($windowMenu, L("&Window")); $menubar->Append($self->{viewMenu}, L("&View")) if $self->{viewMenu}; - # Add an optional debug menu - # (Select application language from the list of installed languages) - Slic3r::GUI::add_debug_menu($menubar, $self->{lang_ch_event}); + # Add a configuration menu. + Slic3r::GUI::add_config_menu($menubar, $self->{preferences_event}, $self->{lang_ch_event}); $menubar->Append($helpMenu, L("&Help")); - # Add an optional debug menu. In production code, the add_debug_menu() call should do nothing. $self->SetMenuBar($menubar); } } diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt index 84f169e57..b9a6dbfee 100644 --- a/xs/CMakeLists.txt +++ b/xs/CMakeLists.txt @@ -170,6 +170,8 @@ add_library(libslic3r STATIC ) add_library(libslic3r_gui STATIC + ${LIBDIR}/slic3r/GUI/AboutDialog.cpp + ${LIBDIR}/slic3r/GUI/AboutDialog.hpp ${LIBDIR}/slic3r/GUI/AppConfig.cpp ${LIBDIR}/slic3r/GUI/AppConfig.hpp ${LIBDIR}/slic3r/GUI/BitmapCache.cpp @@ -533,13 +535,13 @@ if (SLIC3R_PRUSACONTROL) set(wxWidgets_UseAlienWx 1) if (wxWidgets_UseAlienWx) set(AlienWx_DEBUG 1) - find_package(AlienWx REQUIRED COMPONENTS base core adv) + find_package(AlienWx REQUIRED COMPONENTS base core adv html) include_directories(${AlienWx_INCLUDE_DIRS}) #add_compile_options(${AlienWx_CXX_FLAGS}) add_definitions(${AlienWx_DEFINITIONS}) set(wxWidgets_LIBRARIES ${AlienWx_LIBRARIES}) else () - find_package(wxWidgets REQUIRED COMPONENTS base core adv) + find_package(wxWidgets REQUIRED COMPONENTS base core adv html) include(${wxWidgets_USE_FILE}) endif () add_definitions(-DSLIC3R_GUI -DSLIC3R_PRUS) diff --git a/xs/lib/Slic3r/XS.pm b/xs/lib/Slic3r/XS.pm index 47a584343..06eb041df 100644 --- a/xs/lib/Slic3r/XS.pm +++ b/xs/lib/Slic3r/XS.pm @@ -12,7 +12,7 @@ our $VERSION = '0.01'; BEGIN { if ($^O eq 'MSWin32') { eval "use Wx"; -# eval "use Wx::Html"; + eval "use Wx::Html"; eval "use Wx::Print"; # because of some Wx bug, thread creation fails if we don't have this (looks like Wx::Printout is hard-coded in some thread cleanup code) } } diff --git a/xs/src/libslic3r/FileParserError.hpp b/xs/src/libslic3r/FileParserError.hpp index 82a6b328e..3f560fa4f 100644 --- a/xs/src/libslic3r/FileParserError.hpp +++ b/xs/src/libslic3r/FileParserError.hpp @@ -4,6 +4,7 @@ #include "libslic3r.h" #include +#include #include namespace Slic3r { @@ -15,6 +16,9 @@ public: file_parser_error(const std::string &msg, const std::string &file, unsigned long line = 0) : std::runtime_error(format_what(msg, file, line)), m_message(msg), m_filename(file), m_line(line) {} + file_parser_error(const std::string &msg, const boost::filesystem::path &file, unsigned long line = 0) : + std::runtime_error(format_what(msg, file.string(), line)), + m_message(msg), m_filename(file.string()), m_line(line) {} // gcc 3.4.2 complains about lack of throw specifier on compiler // generated dtor ~file_parser_error() throw() {} diff --git a/xs/src/slic3r/Config/Snapshot.cpp b/xs/src/slic3r/Config/Snapshot.cpp index 559e4c63c..91f02ab25 100644 --- a/xs/src/slic3r/Config/Snapshot.cpp +++ b/xs/src/slic3r/Config/Snapshot.cpp @@ -205,6 +205,22 @@ size_t SnapshotDB::load_db() return m_snapshots.size(); } +void SnapshotDB::update_slic3r_versions(std::vector &index_db) +{ + for (Snapshot &snapshot : m_snapshots) { + for (Snapshot::VendorConfig &vendor_config : snapshot.vendor_configs) { + auto it = std::find_if(index_db.begin(), index_db.end(), [&vendor_config](const Index &idx) { return idx.vendor() == vendor_config.name; }); + if (it != index_db.end()) { + Index::const_iterator it_version = it->find(vendor_config.version); + if (it_version != it->end()) { + vendor_config.min_slic3r_version = it_version->min_slic3r_version; + vendor_config.max_slic3r_version = it_version->max_slic3r_version; + } + } + } + } +} + static void copy_config_dir_single_level(const boost::filesystem::path &path_src, const boost::filesystem::path &path_dst) { if (! boost::filesystem::is_directory(path_dst) && @@ -303,6 +319,26 @@ boost::filesystem::path SnapshotDB::create_db_dir() return snapshots_dir; } +SnapshotDB& SnapshotDB::singleton() +{ + static SnapshotDB instance; + bool loaded = false; + if (! loaded) { + try { + loaded = true; + // Load the snapshot database. + instance.load_db(); + // Load the vendor specific configuration indices. + std::vector index_db = Index::load_db(); + // Update the min / max slic3r versions compatible with the configurations stored inside the snapshots + // based on the min / max slic3r versions defined by the vendor specific config indices. + instance.update_slic3r_versions(index_db); + } catch (std::exception &ex) { + } + } + return instance; +} + } // namespace Config } // namespace GUI } // namespace Slic3r diff --git a/xs/src/slic3r/Config/Snapshot.hpp b/xs/src/slic3r/Config/Snapshot.hpp index 358797bf7..1d02c8650 100644 --- a/xs/src/slic3r/Config/Snapshot.hpp +++ b/xs/src/slic3r/Config/Snapshot.hpp @@ -6,6 +6,7 @@ #include +#include "Version.hpp" #include "../Utils/Semver.hpp" namespace Slic3r { @@ -15,6 +16,8 @@ class AppConfig; namespace GUI { namespace Config { +class Version; + // A snapshot contains: // Slic3r.ini // vendor/ @@ -75,12 +78,16 @@ public: class SnapshotDB { public: + // Initialize the SnapshotDB singleton instance. Load the database if it has not been loaded yet. + static SnapshotDB& singleton(); + typedef std::vector::const_iterator const_iterator; // Load the snapshot database from the snapshots directory. // If the snapshot directory or its parent does not exist yet, it will be created. // Returns a number of snapshots loaded. size_t load_db(); + void update_slic3r_versions(std::vector &index_db); // Create a snapshot directory, copy the vendor config bundles, user print/filament/printer profiles, // create an index. diff --git a/xs/src/slic3r/Config/Version.cpp b/xs/src/slic3r/Config/Version.cpp index 1102f3149..cc961829d 100644 --- a/xs/src/slic3r/Config/Version.cpp +++ b/xs/src/slic3r/Config/Version.cpp @@ -6,6 +6,8 @@ #include "../../libslic3r/libslic3r.h" #include "../../libslic3r/Config.hpp" +#include "../../libslic3r/FileParserError.hpp" +#include "../../libslic3r/Utils.hpp" namespace Slic3r { namespace GUI { @@ -62,11 +64,12 @@ inline std::string unquote_version_comment(char *value, char *end, const std::st return svalue; } -size_t Index::load(const std::string &path) +size_t Index::load(const boost::filesystem::path &path) { m_configs.clear(); + m_vendor = path.stem().string(); - boost::nowide::ifstream ifs(path); + boost::nowide::ifstream ifs(path.string()); std::string line; size_t idx_line = 0; Version ver; @@ -96,7 +99,7 @@ size_t Index::load(const std::string &path) if (semver) throw file_parser_error("Key cannot be a semantic version", path, idx_line); // Verify validity of the key / value pair. - std::string svalue = unquote_value(left_trim(++ value), end, path, idx_line); + std::string svalue = unquote_value(left_trim(++ value), end, path.string(), idx_line); if (key == "min_sic3r_version" || key == "max_slic3r_version") { if (! svalue.empty()) semver = Semver::parse(key); @@ -113,13 +116,24 @@ size_t Index::load(const std::string &path) if (! semver) throw file_parser_error("Invalid semantic version", path, idx_line); ver.config_version = *semver; - ver.comment = (end <= key_end) ? "" : unquote_version_comment(value, end, path, idx_line); + ver.comment = (end <= key_end) ? "" : unquote_version_comment(value, end, path.string(), idx_line); m_configs.emplace_back(ver); } + // Sort the configs by their version. + std::sort(m_configs.begin(), m_configs.end(), [](const Version &v1, const Version &v2) { return v1.config_version < v2.config_version; }); return m_configs.size(); } +Index::const_iterator Index::find(const Semver &ver) +{ + Version key; + key.config_version = ver; + auto it = std::lower_bound(m_configs.begin(), m_configs.end(), key, + [](const Version &v1, const Version &v2) { return v1.config_version < v2.config_version; }); + return (it == m_configs.end() || it->config_version == ver) ? it : m_configs.end(); +} + Index::const_iterator Index::recommended() const { int idx = -1; @@ -131,6 +145,31 @@ Index::const_iterator Index::recommended() const return highest; } +std::vector Index::load_db() +{ + boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); + boost::filesystem::path vendor_dir = data_dir / "vendor"; + + std::vector index_db; + std::string errors_cummulative; + for (auto &dir_entry : boost::filesystem::directory_iterator(vendor_dir)) + if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".idx")) { + Index idx; + try { + idx.load(dir_entry.path()); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; + continue; + } + index_db.emplace_back(std::move(idx)); + } + + if (! errors_cummulative.empty()) + throw std::runtime_error(errors_cummulative); + return index_db; +} + } // namespace Config } // namespace GUI } // namespace Slic3r diff --git a/xs/src/slic3r/Config/Version.hpp b/xs/src/slic3r/Config/Version.hpp index 7af1d4b5b..43512e82f 100644 --- a/xs/src/slic3r/Config/Version.hpp +++ b/xs/src/slic3r/Config/Version.hpp @@ -4,6 +4,8 @@ #include #include +#include + #include "../../libslic3r/FileParserError.hpp" #include "../Utils/Semver.hpp" @@ -54,17 +56,25 @@ public: typedef std::vector::const_iterator const_iterator; // Read a config index file in the simple format described in the Index class comment. // Throws Slic3r::file_parser_error and the standard std file access exceptions. - size_t load(const std::string &path); + size_t load(const boost::filesystem::path &path); + + const std::string& vendor() const { return m_vendor; } const_iterator begin() const { return m_configs.begin(); } const_iterator end() const { return m_configs.end(); } + const_iterator find(const Semver &ver); const std::vector& configs() const { return m_configs; } // Finds a recommended config to be installed for the current Slic3r version. // Returns configs().end() if such version does not exist in the index. This shall never happen // if the index is valid. const_iterator recommended() const; + // Load all vendor specific indices. + // Throws Slic3r::file_parser_error and the standard std file access exceptions. + static std::vector load_db(); + private: + std::string m_vendor; std::vector m_configs; }; diff --git a/xs/src/slic3r/GUI/AboutDialog.cpp b/xs/src/slic3r/GUI/AboutDialog.cpp new file mode 100644 index 000000000..49cfff2bd --- /dev/null +++ b/xs/src/slic3r/GUI/AboutDialog.cpp @@ -0,0 +1,125 @@ +#include "AboutDialog.hpp" + +#include "../../libslic3r/Utils.hpp" + +namespace Slic3r { +namespace GUI { + +AboutDialogLogo::AboutDialogLogo(wxWindow* parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) +{ + this->SetBackgroundColour(*wxWHITE); + this->logo = wxBitmap(from_u8(Slic3r::var("Slic3r_192px.png")), wxBITMAP_TYPE_PNG); + this->SetMinSize(this->logo.GetSize()); + + this->Bind(wxEVT_PAINT, &AboutDialogLogo::onRepaint, this); +} + +void AboutDialogLogo::onRepaint(wxEvent &event) +{ + wxPaintDC dc(this); + dc.SetBackgroundMode(wxTRANSPARENT); + + wxSize size = this->GetSize(); + int logo_w = this->logo.GetWidth(); + int logo_h = this->logo.GetHeight(); + dc.DrawBitmap(this->logo, (size.GetWidth() - logo_w)/2, (size.GetHeight() - logo_h)/2, true); + + event.Skip(); +} + +AboutDialog::AboutDialog() + : wxDialog(NULL, wxID_ANY, _(L("About Slic3r")), wxDefaultPosition, wxSize(600, 340), wxCAPTION) +{ + this->SetBackgroundColour(*wxWHITE); + + wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL); + this->SetSizer(hsizer); + + // logo + AboutDialogLogo* logo = new AboutDialogLogo(this); + hsizer->Add(logo, 0, wxEXPAND | wxLEFT | wxRIGHT, 30); + + wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL); + hsizer->Add(vsizer, 1, wxEXPAND, 0); + + // title + { + wxStaticText* title = new wxStaticText(this, wxID_ANY, "Slic3r Prusa Edition", wxDefaultPosition, wxDefaultSize); + wxFont title_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + title_font.SetWeight(wxFONTWEIGHT_BOLD); + title_font.SetFamily(wxFONTFAMILY_ROMAN); + title_font.SetPointSize(24); + title->SetFont(title_font); + vsizer->Add(title, 0, wxALIGN_LEFT | wxTOP, 30); + } + + // version + { + std::string version_string = _(L("Version ")) + std::string(SLIC3R_VERSION); + wxStaticText* version = new wxStaticText(this, wxID_ANY, version_string.c_str(), wxDefaultPosition, wxDefaultSize); + wxFont version_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + #ifdef __WXMSW__ + version_font.SetPointSize(9); + #else + version_font.SetPointSize(11); + #endif + version->SetFont(version_font); + vsizer->Add(version, 0, wxALIGN_LEFT | wxBOTTOM, 10); + } + + // text + wxHtmlWindow* html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_NEVER); + { + wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + #ifdef __WXMSW__ + int size[] = {8,8,8,8,8,8,8}; + #else + int size[] = {11,11,11,11,11,11,11}; + #endif + html->SetFonts(font.GetFaceName(), font.GetFaceName(), size); + html->SetBorders(2); + const char* text = + "" + "" + "" + "Copyright © 2016-2018 Prusa Research.
" + "Copyright © 2011-2017 Alessandro Ranellucci.
" + "Slic3r is licensed under the " + "GNU Affero General Public License, version 3." + "


" + "Contributions by Henrik Brix Andersen, Nicolas Dandrimont, Mark Hindess, Petr Ledvina, Joseph Lenox, Y. Sapir, Mike Sheldrake, Vojtech Bubnik and numerous others. " + "Manual by Gary Hodgson. Inspired by the RepRap community.
" + "Slic3r logo designed by Corey Daniels, Silk Icon Set designed by Mark James. " + "
" + "" + ""; + html->SetPage(text); + vsizer->Add(html, 1, wxEXPAND | wxALIGN_LEFT | wxRIGHT | wxBOTTOM, 20); + html->Bind(wxEVT_HTML_LINK_CLICKED, &AboutDialog::onLinkClicked, this); + } + + wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxCLOSE); + this->SetEscapeId(wxID_CLOSE); + this->Bind(wxEVT_BUTTON, &AboutDialog::onCloseDialog, this, wxID_CLOSE); + vsizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3); + + this->Bind(wxEVT_LEFT_DOWN, &AboutDialog::onCloseDialog, this); + logo->Bind(wxEVT_LEFT_DOWN, &AboutDialog::onCloseDialog, this); + html->Bind(wxEVT_LEFT_DOWN, &AboutDialog::onCloseDialog, this); +} + +void AboutDialog::onLinkClicked(wxHtmlLinkEvent &event) +{ + wxLaunchDefaultBrowser(event.GetLinkInfo().GetHref()); + event.Skip(false); +} + +void AboutDialog::onCloseDialog(wxEvent &) +{ + this->EndModal(wxID_CLOSE); + this->Close(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/xs/src/slic3r/GUI/AboutDialog.hpp b/xs/src/slic3r/GUI/AboutDialog.hpp new file mode 100644 index 000000000..01f7564c5 --- /dev/null +++ b/xs/src/slic3r/GUI/AboutDialog.hpp @@ -0,0 +1,36 @@ +#ifndef slic3r_GUI_AboutDialog_hpp_ +#define slic3r_GUI_AboutDialog_hpp_ + +#include "GUI.hpp" + +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +class AboutDialogLogo : public wxPanel +{ +public: + AboutDialogLogo(wxWindow* parent); + +private: + wxBitmap logo; + void onRepaint(wxEvent &event); +}; + +class AboutDialog : public wxDialog +{ +public: + AboutDialog(); + +private: + void onLinkClicked(wxHtmlLinkEvent &event); + void onCloseDialog(wxEvent &); +}; + +} // namespace GUI +} // namespace Slic3r + +#endif diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index 3eca4e707..48d56ff11 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -42,6 +42,7 @@ #include "Tab.hpp" #include "TabIface.hpp" +#include "AboutDialog.hpp" #include "AppConfig.hpp" #include "Utils.hpp" #include "Preferences.hpp" @@ -330,32 +331,56 @@ void get_installed_languages(wxArrayString & names, } } -void add_debug_menu(wxMenuBar *menu, int event_language_change) +enum ConfigMenuIDs { + ConfigMenuWizard, + ConfigMenuSnapshots, + ConfigMenuTakeSnapshot, + ConfigMenuUpdate, + ConfigMenuPreferences, + ConfigMenuLanguage, + ConfigMenuCnt, +}; + +void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_language_change) { -//#if 0 auto local_menu = new wxMenu(); - local_menu->Append(wxWindow::NewControlId(1), _(L("Change Application Language"))); - local_menu->Bind(wxEVT_MENU, [event_language_change](wxEvent&){ - wxArrayString names; - wxArrayLong identifiers; - get_installed_languages(names, identifiers); - if (select_language(names, identifiers)){ - save_language(); - show_info(g_wxTabPanel, _(L("Application will be restarted")), _(L("Attention!"))); - if (event_language_change > 0) { - wxCommandEvent event(event_language_change); - g_wxApp->ProcessEvent(event); + wxWindowID config_id_base = wxWindow::NewControlId((int)ConfigMenuCnt); + + // Cmd+, is standard on OS X - what about other operating systems? + local_menu->Append(config_id_base + ConfigMenuWizard, _(L("Configuration Wizard\u2026")), _(L("Run configuration wizard"))); + local_menu->Append(config_id_base + ConfigMenuSnapshots, _(L("Configuration Snapshots\u2026")), _(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"))); + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuPreferences, _(L("Preferences\u2026\tCtrl+,")), _(L("Application preferences"))); + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuLanguage, _(L("Change Application Language"))); + local_menu->Bind(wxEVT_MENU, [config_id_base, event_language_change, event_preferences_changed](wxEvent &event){ + switch (event.GetId() - config_id_base) { + case ConfigMenuPreferences: + { + auto dlg = new PreferencesDialog(g_wxMainFrame, event_preferences_changed); + dlg->ShowModal(); + break; + } + case ConfigMenuLanguage: + { + wxArrayString names; + wxArrayLong identifiers; + get_installed_languages(names, identifiers); + if (select_language(names, identifiers)) { + save_language(); + show_info(g_wxTabPanel, _(L("Application will be restarted")), _(L("Attention!"))); + if (event_language_change > 0) { + wxCommandEvent event(event_language_change); + g_wxApp->ProcessEvent(event); + } } + break; + } } }); - menu->Append(local_menu, _(L("&Localization"))); -//#endif -} - -void open_preferences_dialog(int event_preferences) -{ - auto dlg = new PreferencesDialog(g_wxMainFrame, event_preferences); - dlg->ShowModal(); + menu->Append(local_menu, _(L("&Configuration"))); } void create_preset_tabs(bool no_controller, int event_value_change, int event_presets_changed) @@ -737,4 +762,11 @@ int get_export_option(wxFileDialog* dlg) return 0; } +void about() +{ + AboutDialog dlg; + dlg.ShowModal(); + dlg.Destroy(); +} + } } diff --git a/xs/src/slic3r/GUI/GUI.hpp b/xs/src/slic3r/GUI/GUI.hpp index 362b15307..0cbdf8729 100644 --- a/xs/src/slic3r/GUI/GUI.hpp +++ b/xs/src/slic3r/GUI/GUI.hpp @@ -82,10 +82,7 @@ wxApp* get_app(); wxColour* get_modified_label_clr(); wxColour* get_sys_label_clr(); -void add_debug_menu(wxMenuBar *menu, int event_language_change); - -// Create "Preferences" dialog after selecting menu "Preferences" in Perl part -void open_preferences_dialog(int event_preferences); +void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_language_change); // Create a new preset tab (print, filament and printer), void create_preset_tabs(bool no_controller, int event_value_change, int event_presets_changed); @@ -134,7 +131,11 @@ ConfigOptionsGroup* get_optgroup(); void add_export_option(wxFileDialog* dlg, const std::string& format); int get_export_option(wxFileDialog* dlg); -} -} + +// Display an About dialog +void about(); + +} // namespace GUI +} // namespace Slic3r #endif diff --git a/xs/xsp/GUI.xsp b/xs/xsp/GUI.xsp index 1376ff164..c8d76adba 100644 --- a/xs/xsp/GUI.xsp +++ b/xs/xsp/GUI.xsp @@ -9,6 +9,9 @@ %package{Slic3r::GUI}; +void about() + %code{% Slic3r::GUI::about(); %}; + void disable_screensaver() %code{% Slic3r::GUI::disable_screensaver(); %}; @@ -32,9 +35,9 @@ void set_main_frame(SV *ui) void set_tab_panel(SV *ui) %code%{ Slic3r::GUI::set_tab_panel((wxNotebook*)wxPli_sv_2_object(aTHX_ ui, "Wx::Notebook")); %}; - -void add_debug_menu(SV *ui, int event_language_change) - %code%{ Slic3r::GUI::add_debug_menu((wxMenuBar*)wxPli_sv_2_object(aTHX_ ui, "Wx::MenuBar"), event_language_change); %}; + +void add_config_menu(SV *ui, int event_preferences_changed, int event_language_change) + %code%{ Slic3r::GUI::add_config_menu((wxMenuBar*)wxPli_sv_2_object(aTHX_ ui, "Wx::MenuBar"), event_preferences_changed, event_language_change); %}; void create_preset_tabs(bool no_controller, int event_value_change, int event_presets_changed) %code%{ Slic3r::GUI::create_preset_tabs(no_controller, event_value_change, event_presets_changed); %}; @@ -54,9 +57,6 @@ int combochecklist_get_flags(SV *ui) void set_app_config(AppConfig *app_config) %code%{ Slic3r::GUI::set_app_config(app_config); %}; -void open_preferences_dialog(int preferences_event) - %code%{ Slic3r::GUI::open_preferences_dialog(preferences_event); %}; - void set_preset_bundle(PresetBundle *preset_bundle) %code%{ Slic3r::GUI::set_preset_bundle(preset_bundle); %}; From 0694fad01626578cea4828ba19749f38e8d5e146 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Tue, 10 Apr 2018 16:27:42 +0200 Subject: [PATCH 12/38] Initial implementation of the config snapshot dialog. --- xs/CMakeLists.txt | 6 +- xs/src/slic3r/Config/Snapshot.cpp | 8 +- xs/src/slic3r/Config/Snapshot.hpp | 2 +- xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp | 110 +++++++++++++++++++++ xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp | 30 ++++++ xs/src/slic3r/GUI/GUI.cpp | 14 +++ xs/src/slic3r/Utils/Semver.hpp | 16 ++- xs/src/slic3r/Utils/Time.cpp | 44 ++++++--- xs/src/slic3r/Utils/Time.hpp | 5 +- 9 files changed, 213 insertions(+), 22 deletions(-) create mode 100644 xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp create mode 100644 xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt index b9a6dbfee..e01f11b26 100644 --- a/xs/CMakeLists.txt +++ b/xs/CMakeLists.txt @@ -176,6 +176,8 @@ add_library(libslic3r_gui STATIC ${LIBDIR}/slic3r/GUI/AppConfig.hpp ${LIBDIR}/slic3r/GUI/BitmapCache.cpp ${LIBDIR}/slic3r/GUI/BitmapCache.hpp + ${LIBDIR}/slic3r/GUI/ConfigSnapshotDialog.cpp + ${LIBDIR}/slic3r/GUI/ConfigSnapshotDialog.hpp ${LIBDIR}/slic3r/GUI/3DScene.cpp ${LIBDIR}/slic3r/GUI/3DScene.hpp ${LIBDIR}/slic3r/GUI/GLShader.cpp @@ -218,6 +220,8 @@ add_library(libslic3r_gui STATIC ${LIBDIR}/slic3r/Utils/OctoPrint.hpp ${LIBDIR}/slic3r/Utils/Bonjour.cpp ${LIBDIR}/slic3r/Utils/Bonjour.hpp + ${LIBDIR}/slic3r/Utils/Time.cpp + ${LIBDIR}/slic3r/Utils/Time.hpp ) add_library(admesh STATIC @@ -408,7 +412,7 @@ if(APPLE) # Ignore undefined symbols of the perl interpreter, they will be found in the caller image. target_link_libraries(XS "-undefined dynamic_lookup") endif() -target_link_libraries(XS libslic3r libslic3r_gui admesh miniz clipper nowide polypartition poly2tri) +target_link_libraries(XS libslic3r libslic3r_gui admesh miniz clipper nowide polypartition poly2tri semver) if(SLIC3R_PROFILE) target_link_libraries(XS Shiny) endif() diff --git a/xs/src/slic3r/Config/Snapshot.cpp b/xs/src/slic3r/Config/Snapshot.cpp index 91f02ab25..33c7ef82f 100644 --- a/xs/src/slic3r/Config/Snapshot.cpp +++ b/xs/src/slic3r/Config/Snapshot.cpp @@ -241,7 +241,7 @@ static void delete_existing_ini_files(const boost::filesystem::path &path) boost::filesystem::remove(dir_entry.path()); } -const Snapshot& SnapshotDB::make_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment) +const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment) { boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); boost::filesystem::path snapshot_db_dir = SnapshotDB::create_db_dir(); @@ -267,8 +267,10 @@ const Snapshot& SnapshotDB::make_snapshot(const AppConfig &app_config, Snapshot: } // Vendor specific config bundles and installed printers. + boost::filesystem::path snapshot_dir = snapshot_db_dir / snapshot.id; + boost::filesystem::create_directory(snapshot_dir); + // Backup the presets. - boost::filesystem::path snapshot_dir = snapshot_db_dir / snapshot.id; for (const char *subdir : { "print", "filament", "printer", "vendor" }) copy_config_dir_single_level(data_dir / subdir, snapshot_dir / subdir); snapshot.save_ini((snapshot_dir / "snapshot.ini").string()); @@ -322,7 +324,7 @@ boost::filesystem::path SnapshotDB::create_db_dir() SnapshotDB& SnapshotDB::singleton() { static SnapshotDB instance; - bool loaded = false; + static bool loaded = false; if (! loaded) { try { loaded = true; diff --git a/xs/src/slic3r/Config/Snapshot.hpp b/xs/src/slic3r/Config/Snapshot.hpp index 1d02c8650..3c7754273 100644 --- a/xs/src/slic3r/Config/Snapshot.hpp +++ b/xs/src/slic3r/Config/Snapshot.hpp @@ -91,7 +91,7 @@ public: // Create a snapshot directory, copy the vendor config bundles, user print/filament/printer profiles, // create an index. - const Snapshot& make_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment); + const Snapshot& take_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment); void restore_snapshot(const std::string &id, AppConfig &app_config); void restore_snapshot(const Snapshot &snapshot, AppConfig &app_config); diff --git a/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp b/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp new file mode 100644 index 000000000..a4e4d846b --- /dev/null +++ b/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp @@ -0,0 +1,110 @@ +#include "ConfigSnapshotDialog.hpp" + +#include "../Config/Snapshot.hpp" +#include "../Utils/Time.hpp" + +#include "../../libslic3r/Utils.hpp" + +namespace Slic3r { +namespace GUI { + +static std::string generate_html_row(const Config::Snapshot &snapshot, bool row_even) +{ + // Start by declaring a row with an alternating background color. + std::string text = ""; + text += ""; +// text += _(L("ID:")) + " " + snapshot.id + "
"; + text += _(L("time captured:")) + " " + Utils::format_local_date_time(snapshot.time_captured) + "
"; + text += _(L("slic3r version:")) + " " + snapshot.slic3r_version_captured.to_string() + "
"; + if (! snapshot.comment.empty()) + text += _(L("user comment:")) + " " + snapshot.comment + "
"; +// text += "reason: " + snapshot.reason + "
"; + text += "print: " + snapshot.print + "
"; + text += "filaments: " + snapshot.filaments.front() + "
"; + text += "printer: " + snapshot.printer + "
"; + + for (const Config::Snapshot::VendorConfig &vc : snapshot.vendor_configs) { + text += "vendor: " + vc.name + ", ver: " + vc.version.to_string() + ", min slic3r ver: " + vc.min_slic3r_version.to_string() + ", max slic3r ver: " + vc.max_slic3r_version.to_string() + "
"; + } + + text += "

Activate

"; + text += ""; + text += ""; + return text; +} + +static std::string generate_html_page(const Config::SnapshotDB &snapshot_db) +{ + std::string text = + "" + "" + ""; + text += ""; + for (size_t i_row = 0; i_row < snapshot_db.snapshots().size(); ++ i_row) { + const Config::Snapshot &snapshot = snapshot_db.snapshots()[i_row]; + text += generate_html_row(snapshot, i_row & 1); + } + text += + "
" + "
" + "" + ""; + return text; +} + +ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db) + : wxDialog(NULL, wxID_ANY, _(L("Configuration Snapshots")), wxDefaultPosition, wxSize(600, 500), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxMAXIMIZE_BOX) +{ + this->SetBackgroundColour(*wxWHITE); + + wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL); + this->SetSizer(vsizer); + + // text + wxHtmlWindow* html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO); + { + wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + #ifdef __WXMSW__ + int size[] = {8,8,8,8,8,8,8}; + #else + int size[] = {11,11,11,11,11,11,11}; + #endif + html->SetFonts(font.GetFaceName(), font.GetFaceName(), size); + html->SetBorders(2); + std::string text = generate_html_page(snapshot_db); + FILE *file = ::fopen("d:\\temp\\configsnapshotdialog.html", "wt"); + fwrite(text.data(), 1, text.size(), file); + fclose(file); + html->SetPage(text.c_str()); + vsizer->Add(html, 1, wxEXPAND | wxALIGN_LEFT | wxRIGHT | wxBOTTOM, 0); + html->Bind(wxEVT_HTML_LINK_CLICKED, &ConfigSnapshotDialog::onLinkClicked, this); + } + + wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxCLOSE); + this->SetEscapeId(wxID_CLOSE); + this->Bind(wxEVT_BUTTON, &ConfigSnapshotDialog::onCloseDialog, this, wxID_CLOSE); + vsizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3); + +/* + this->Bind(wxEVT_LEFT_DOWN, &ConfigSnapshotDialog::onCloseDialog, this); + logo->Bind(wxEVT_LEFT_DOWN, &ConfigSnapshotDialog::onCloseDialog, this); + html->Bind(wxEVT_LEFT_DOWN, &ConfigSnapshotDialog::onCloseDialog, this); +*/ +} + +void ConfigSnapshotDialog::onLinkClicked(wxHtmlLinkEvent &event) +{ + wxLaunchDefaultBrowser(event.GetLinkInfo().GetHref()); + event.Skip(false); +} + +void ConfigSnapshotDialog::onCloseDialog(wxEvent &) +{ + this->EndModal(wxID_CLOSE); + this->Close(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp b/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp new file mode 100644 index 000000000..70187ee99 --- /dev/null +++ b/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp @@ -0,0 +1,30 @@ +#ifndef slic3r_GUI_ConfigSnapshotDialog_hpp_ +#define slic3r_GUI_ConfigSnapshotDialog_hpp_ + +#include "GUI.hpp" + +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +namespace Config { + class SnapshotDB; +} + +class ConfigSnapshotDialog : public wxDialog +{ +public: + ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db); + +private: + void onLinkClicked(wxHtmlLinkEvent &event); + void onCloseDialog(wxEvent &); +}; + +} // namespace GUI +} // namespace Slic3r + +#endif /* slic3r_GUI_ConfigSnapshotDialog_hpp_ */ diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index 48d56ff11..0a163bf28 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -44,10 +44,13 @@ #include "TabIface.hpp" #include "AboutDialog.hpp" #include "AppConfig.hpp" +#include "ConfigSnapshotDialog.hpp" #include "Utils.hpp" #include "Preferences.hpp" #include "PresetBundle.hpp" +#include "../Config/Snapshot.hpp" + namespace Slic3r { namespace GUI { #if __APPLE__ @@ -357,6 +360,17 @@ void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_l local_menu->Append(config_id_base + ConfigMenuLanguage, _(L("Change Application Language"))); local_menu->Bind(wxEVT_MENU, [config_id_base, event_language_change, event_preferences_changed](wxEvent &event){ switch (event.GetId() - config_id_base) { + case ConfigMenuTakeSnapshot: + // Take a configuration snapshot. + Slic3r::GUI::Config::SnapshotDB::singleton().take_snapshot(*g_AppConfig, Slic3r::GUI::Config::Snapshot::SNAPSHOT_USER, ""); + break; + case ConfigMenuSnapshots: + { + ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton()); + dlg.ShowModal(); + dlg.Destroy(); + break; + } case ConfigMenuPreferences: { auto dlg = new PreferencesDialog(g_wxMainFrame, event_preferences_changed); diff --git a/xs/src/slic3r/Utils/Semver.hpp b/xs/src/slic3r/Utils/Semver.hpp index 7fc3b8033..8aa8cc53f 100644 --- a/xs/src/slic3r/Utils/Semver.hpp +++ b/xs/src/slic3r/Utils/Semver.hpp @@ -46,11 +46,23 @@ public: return Semver(ver); } - Semver(Semver &&other) { *this = std::move(other); } - Semver(const Semver &other) { *this = other; } + Semver(Semver &&other) : ver(other.ver) + { + other.ver.major = other.ver.minor = other.ver.patch = 0; + other.ver.metadata = other.ver.prerelease = nullptr; + } + + Semver(const Semver &other) : ver(other.ver) + { + if (other.ver.metadata != nullptr) + std::strcpy(ver.metadata, other.ver.metadata); + if (other.ver.prerelease != nullptr) + std::strcpy(ver.prerelease, other.ver.prerelease); + } Semver &operator=(Semver &&other) { + ::semver_free(&ver); ver = other.ver; other.ver.major = other.ver.minor = other.ver.patch = 0; other.ver.metadata = other.ver.prerelease = nullptr; diff --git a/xs/src/slic3r/Utils/Time.cpp b/xs/src/slic3r/Utils/Time.cpp index c4123c7bb..a2b2328af 100644 --- a/xs/src/slic3r/Utils/Time.cpp +++ b/xs/src/slic3r/Utils/Time.cpp @@ -1,13 +1,18 @@ #include "Time.hpp" +#ifdef WIN32 + #define WIN32_LEAN_AND_MEAN + #include + #undef WIN32_LEAN_AND_MEAN +#endif /* WIN32 */ + namespace Slic3r { namespace Utils { time_t parse_time_ISO8601Z(const std::string &sdate) { - int y, M, d, h, m; - float s; - if (sscanf(sdate.c_str(), "%d-%d-%dT%d:%d:%fZ", &y, &M, &d, &h, &m, &s) != 6) + int y, M, d, h, m, s; + if (sscanf(sdate.c_str(), "%04d%02d%02dT%02d%02d%02dZ", &y, &M, &d, &h, &m, &s) != 6) return (time_t)-1; struct tm tms; tms.tm_year = y - 1900; // Year since 1900 @@ -15,7 +20,7 @@ time_t parse_time_ISO8601Z(const std::string &sdate) tms.tm_mday = d; // 1-31 tms.tm_hour = h; // 0-23 tms.tm_min = m; // 0-59 - tms.tm_sec = (int)s; // 0-61 (0-60 in C++11) + tms.tm_sec = s; // 0-61 (0-60 in C++11) return mktime(&tms); } @@ -23,21 +28,34 @@ std::string format_time_ISO8601Z(time_t time) { struct tm tms; #ifdef WIN32 - gmtime_s(time, &tms); + gmtime_s(&tms, &time); #else - gmtime_r(&tms, time); + gmtime_r(&time, &tms); #endif char buf[128]; - sprintf(buf, "%d-%d-%dT%d:%d:%fZ", - tms.tm_year + 1900 - tms.tm_mon + 1 - tms.tm_mday - tms.tm_hour - tms.tm_min + sprintf(buf, "%04d%02d%02dT%02d%02d%02dZ", + tms.tm_year + 1900, + tms.tm_mon + 1, + tms.tm_mday, + tms.tm_hour, + tms.tm_min, tms.tm_sec); return buf; } +std::string format_local_date_time(time_t time) +{ + struct tm tms; +#ifdef WIN32 + localtime_s(&tms, &time); +#else + localtime_r(&time, &tms); +#endif + char buf[80]; + strftime(buf, 80, "%x %X", &tms); + return buf; +} + time_t get_current_time_utc() { #ifdef WIN32 @@ -59,5 +77,3 @@ time_t get_current_time_utc() }; // namespace Utils }; // namespace Slic3r - -#endif /* slic3r_Utils_Time_hpp_ */ diff --git a/xs/src/slic3r/Utils/Time.hpp b/xs/src/slic3r/Utils/Time.hpp index 6b2fbf893..7b670bd3e 100644 --- a/xs/src/slic3r/Utils/Time.hpp +++ b/xs/src/slic3r/Utils/Time.hpp @@ -13,8 +13,11 @@ namespace Utils { extern time_t parse_time_ISO8601Z(const std::string &s); extern std::string format_time_ISO8601Z(time_t time); +// Format the date and time from an UTC time according to the active locales and a local time zone. +extern std::string format_local_date_time(time_t time); + // There is no gmtime() on windows. -time_t get_current_time_utc(); +extern time_t get_current_time_utc(); }; // namespace Utils }; // namespace Slic3r From da2878958bfb23ae65b3ee4789844530d35260d9 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Wed, 11 Apr 2018 12:21:15 +0200 Subject: [PATCH 13/38] Wizard runs from the new Config menu, snapshots could be rolled back / forward. --- lib/Slic3r/GUI.pm | 5 +- lib/Slic3r/GUI/Controller.pm | 4 +- lib/Slic3r/GUI/MainFrame.pm | 47 ++---------- resources/profiles/PrusaResearch.ini | 2 +- xs/src/slic3r/Config/Snapshot.cpp | 46 ++++++++++-- xs/src/slic3r/Config/Snapshot.hpp | 6 ++ xs/src/slic3r/GUI/AppConfig.hpp | 3 + xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp | 21 ++++-- xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp | 4 ++ xs/src/slic3r/GUI/GUI.cpp | 84 ++++++++++++++++++---- xs/src/slic3r/GUI/GUI.hpp | 10 ++- xs/src/slic3r/GUI/Tab.cpp | 12 ++-- xs/xsp/GUI.xsp | 7 +- 13 files changed, 165 insertions(+), 86 deletions(-) diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index bb56fa886..b28b84df3 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -8,7 +8,6 @@ use FindBin; use List::Util qw(first); use Slic3r::GUI::2DBed; use Slic3r::GUI::BedShapeDialog; -use Slic3r::GUI::ConfigWizard; use Slic3r::GUI::Controller; use Slic3r::GUI::Controller::ManualControlDialog; use Slic3r::GUI::Controller::PrinterPanel; @@ -150,7 +149,7 @@ sub OnInit { # XXX: ? if ($run_wizard) { # Run the config wizard, don't offer the "reset user profile" checkbox. - $self->{mainframe}->config_wizard(1); + Slic3r::GUI::config_wizard(1); } # $self->{preset_updater}->download($self->{preset_bundle}); @@ -207,7 +206,7 @@ sub recreate_GUI{ # before the UI was up and running. $self->CallAfter(sub { # Run the config wizard, don't offer the "reset user profile" checkbox. - $self->{mainframe}->config_wizard(1); + Slic3r::GUI::config_wizard(1); }); } } diff --git a/lib/Slic3r/GUI/Controller.pm b/lib/Slic3r/GUI/Controller.pm index 6aa7b34cb..f7d90c796 100644 --- a/lib/Slic3r/GUI/Controller.pm +++ b/lib/Slic3r/GUI/Controller.pm @@ -7,7 +7,7 @@ use strict; use warnings; use utf8; -use Wx qw(wxTheApp :frame :id :misc :sizer :bitmap :button :icon :dialog); +use Wx qw(wxTheApp :frame :id :misc :sizer :bitmap :button :icon :dialog wxBORDER_NONE); use Wx::Event qw(EVT_CLOSE EVT_LEFT_DOWN EVT_MENU); use base qw(Wx::ScrolledWindow Class::Accessor); use List::Util qw(first); @@ -34,7 +34,7 @@ sub new { # button for adding new printer panels { my $btn = $self->{btn_add} = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new(Slic3r::var("add.png"), wxBITMAP_TYPE_PNG), - wxDefaultPosition, wxDefaultSize, Wx::wxBORDER_NONE); + wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); $btn->SetToolTipString("Add printer…") if $btn->can('SetToolTipString'); diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index e5b27d1dc..886c8f419 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -81,7 +81,7 @@ sub new { # declare events EVT_CLOSE($self, sub { my (undef, $event) = @_; - if ($event->CanVeto && !$self->check_unsaved_changes) { + if ($event->CanVeto && !Slic3r::GUI::check_unsaved_changes) { $event->Veto; return; } @@ -313,11 +313,6 @@ sub _init_menubar { # Help menu my $helpMenu = Wx::Menu->new; { - $self->_append_menu_item($helpMenu, L("&Configuration ").$Slic3r::GUI::ConfigWizard::wizard."…", L("Run Configuration ").$Slic3r::GUI::ConfigWizard::wizard, sub { - # Run the config wizard, offer the "reset user profile" checkbox. - $self->config_wizard(0); - }); - $helpMenu->AppendSeparator(); $self->_append_menu_item($helpMenu, L("Prusa 3D Drivers"), L('Open the Prusa3D drivers download page in your browser'), sub { Wx::LaunchDefaultBrowser('http://www.prusa3d.com/drivers/'); }); @@ -554,7 +549,7 @@ sub export_config { sub load_config_file { my ($self, $file) = @_; if (!$file) { - return unless $self->check_unsaved_changes; + return unless Slic3r::GUI::check_unsaved_changes; my $dlg = Wx::FileDialog->new($self, L('Select configuration to load:'), $last_config ? dirname($last_config) : wxTheApp->{app_config}->get_last_dir, "config.ini", @@ -573,7 +568,7 @@ sub load_config_file { sub export_configbundle { my ($self) = @_; - return unless $self->check_unsaved_changes; + return unless Slic3r::GUI::check_unsaved_changes; # validate current configuration in case it's dirty eval { wxTheApp->{preset_bundle}->full_config->validate; }; Slic3r::GUI::catch_error($self) and return; @@ -597,7 +592,7 @@ sub export_configbundle { # but that behavior was not documented and likely buggy. sub load_configbundle { my ($self, $file, $reset_user_profile) = @_; - return unless $self->check_unsaved_changes; + return unless Slic3r::GUI::check_unsaved_changes; if (!$file) { my $dlg = Wx::FileDialog->new($self, L('Select configuration to load:'), $last_config ? dirname($last_config) : wxTheApp->{app_config}->get_last_dir, @@ -631,40 +626,6 @@ sub load_config { $self->{plater}->on_config_change($config) if $self->{plater}; } -sub config_wizard { - my ($self, $fresh_start) = @_; - # Exit wizard if there are unsaved changes and the user cancels the action. - return unless $self->check_unsaved_changes; - - # TODO: Offer "reset user profile" ??? - if (Slic3r::GUI::open_config_wizard(wxTheApp->{preset_bundle})) { - # Load the currently selected preset into the GUI, update the preset selection box. - foreach my $tab (values %{$self->{options_tabs}}) { - $tab->load_current_preset; - } - } -} - -# 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. -sub check_unsaved_changes { - my $self = shift; - - my @dirty = (); - foreach my $tab (values %{$self->{options_tabs}}) { - push @dirty, $tab->title if $tab->current_preset_is_dirty; - } - - if (@dirty) { - my $titles = join ', ', @dirty; - my $confirm = Wx::MessageDialog->new($self, L("You have unsaved changes ").($titles).L(". Discard changes and continue anyway?"), - L('Unsaved Presets'), wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); - return $confirm->ShowModal == wxID_YES; - } - - return 1; -} - sub select_tab { my ($self, $tab) = @_; $self->{tabpanel}->SetSelection($tab); diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index b654b4039..cf82855cf 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -5,7 +5,7 @@ name = Prusa Research # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the Slic3r configuration to be downgraded. -config_version = 0.1 +config_version = 0.1.0 # Where to get the updates from? # TODO: proper URL # config_update_url = https://raw.githubusercontent.com/prusa3d/Slic3r-settings/master/live/PrusaResearch.ini diff --git a/xs/src/slic3r/Config/Snapshot.cpp b/xs/src/slic3r/Config/Snapshot.cpp index 33c7ef82f..9fabbe013 100644 --- a/xs/src/slic3r/Config/Snapshot.cpp +++ b/xs/src/slic3r/Config/Snapshot.cpp @@ -1,5 +1,6 @@ #include "Snapshot.hpp" #include "../GUI/AppConfig.hpp" +#include "../GUI/PresetBundle.hpp" #include "../Utils/Time.hpp" #include @@ -172,6 +173,14 @@ void Snapshot::export_selections(AppConfig &config) const config.set("presets", "printer", printer); } +void Snapshot::export_vendor_configs(AppConfig &config) const +{ + std::map>> vendors; + for (const VendorConfig &vc : vendor_configs) + vendors[vc.name] = vc.models_variants_installed; + config.set_vendors(std::move(vendors)); +} + size_t SnapshotDB::load_db() { boost::filesystem::path snapshots_dir = SnapshotDB::create_db_dir(); @@ -199,7 +208,8 @@ size_t SnapshotDB::load_db() } m_snapshots.emplace_back(std::move(snapshot)); } - + // Sort the snapshots by their date/time. + std::sort(m_snapshots.begin(), m_snapshots.end(), [](const Snapshot &s1, const Snapshot &s2) { return s1.time_captured < s2.time_captured; }); if (! errors_cummulative.empty()) throw std::runtime_error(errors_cummulative); return m_snapshots.size(); @@ -266,6 +276,30 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: snapshot.filaments.emplace_back(app_config.get("presets", name)); } // Vendor specific config bundles and installed printers. + for (const std::pair>> &vendor : app_config.vendors()) { + Snapshot::VendorConfig cfg; + cfg.name = vendor.first; + cfg.models_variants_installed = vendor.second; + // Read the active config bundle, parse the config version. + PresetBundle bundle; + bundle.load_configbundle((data_dir / "vendor" / (cfg.name + ".ini")).string(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); + for (const VendorProfile &vp : bundle.vendors) + if (vp.id == cfg.name) + cfg.version = *Semver::parse(vp.config_version); + // Fill-in the min/max slic3r version from the config index, if possible. + try { + // Load the config index for the vendor. + Index index; + index.load(data_dir / "vendor" / (cfg.name + ".idx")); + auto it = index.find(cfg.version); + if (it != index.end()) { + cfg.min_slic3r_version = it->min_slic3r_version; + cfg.max_slic3r_version = it->max_slic3r_version; + } + } catch (const std::runtime_error &err) { + } + snapshot.vendor_configs.emplace_back(std::move(cfg)); + } boost::filesystem::path snapshot_dir = snapshot_db_dir / snapshot.id; boost::filesystem::create_directory(snapshot_dir); @@ -274,6 +308,7 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: for (const char *subdir : { "print", "filament", "printer", "vendor" }) copy_config_dir_single_level(data_dir / subdir, snapshot_dir / subdir); snapshot.save_ini((snapshot_dir / "snapshot.ini").string()); + assert(m_snapshots.empty() || m_snapshots.back().time_captured <= snapshot.time_captured); m_snapshots.emplace_back(std::move(snapshot)); return m_snapshots.back(); } @@ -293,18 +328,15 @@ void SnapshotDB::restore_snapshot(const Snapshot &snapshot, AppConfig &app_confi boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); boost::filesystem::path snapshot_db_dir = SnapshotDB::create_db_dir(); boost::filesystem::path snapshot_dir = snapshot_db_dir / snapshot.id; - // Remove existing ini files and restore the ini files from the snapshot. for (const char *subdir : { "print", "filament", "printer", "vendor" }) { delete_existing_ini_files(data_dir / subdir); copy_config_dir_single_level(snapshot_dir / subdir, data_dir / subdir); } - - // Update app_config from the snapshot. + // Update AppConfig with the selections of the print / filament / printer profiles + // and about the installed printer types and variants. snapshot.export_selections(app_config); - - // Store information about the snapshot. - + snapshot.export_vendor_configs(app_config); } boost::filesystem::path SnapshotDB::create_db_dir() diff --git a/xs/src/slic3r/Config/Snapshot.hpp b/xs/src/slic3r/Config/Snapshot.hpp index 3c7754273..a7b8a5aa5 100644 --- a/xs/src/slic3r/Config/Snapshot.hpp +++ b/xs/src/slic3r/Config/Snapshot.hpp @@ -1,6 +1,8 @@ #ifndef slic3r_GUI_Snapshot_ #define slic3r_GUI_Snapshot_ +#include +#include #include #include @@ -17,6 +19,7 @@ namespace GUI { namespace Config { class Version; +class Index; // A snapshot contains: // Slic3r.ini @@ -42,6 +45,7 @@ public: // Export the print / filament / printer selections to be activated into the AppConfig. void export_selections(AppConfig &config) const; + void export_vendor_configs(AppConfig &config) const; // ID of a snapshot should equal to the name of the snapshot directory. // The ID contains the date/time, reason and comment to be human readable. @@ -70,6 +74,8 @@ public: Semver min_slic3r_version = Semver::zero(); // Maximum Slic3r version compatible with this vendor configuration, or empty. Semver max_slic3r_version = Semver::inf(); + // Which printer models of this vendor were installed, and which variants of the models? + std::map> models_variants_installed; }; // List of vendor configs contained in this snapshot. std::vector vendor_configs; diff --git a/xs/src/slic3r/GUI/AppConfig.hpp b/xs/src/slic3r/GUI/AppConfig.hpp index 7aac95fd6..40b3a12fd 100644 --- a/xs/src/slic3r/GUI/AppConfig.hpp +++ b/xs/src/slic3r/GUI/AppConfig.hpp @@ -71,6 +71,9 @@ public: bool get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const; void set_variant(const std::string &vendor, const std::string &model, const std::string &variant, bool enable); void set_vendors(const AppConfig &from); + void set_vendors(const std::map>> &vendors) { m_vendors = vendors; m_dirty = true; } + void set_vendors(std::map>> &&vendors) { m_vendors = std::move(vendors); m_dirty = true; } + const std::map>> vendors() const { return m_vendors; } // return recent/skein_directory or recent/config_directory or empty string. std::string get_last_dir() const; diff --git a/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp b/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp index a4e4d846b..730b97a32 100644 --- a/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp +++ b/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp @@ -26,7 +26,19 @@ static std::string generate_html_row(const Config::Snapshot &snapshot, bool row_ text += "printer: " + snapshot.printer + "
"; for (const Config::Snapshot::VendorConfig &vc : snapshot.vendor_configs) { - text += "vendor: " + vc.name + ", ver: " + vc.version.to_string() + ", min slic3r ver: " + vc.min_slic3r_version.to_string() + ", max slic3r ver: " + vc.max_slic3r_version.to_string() + "
"; + text += "vendor: " + vc.name + ", ver: " + vc.version.to_string() + ", min slic3r ver: " + vc.min_slic3r_version.to_string(); + if (vc.max_slic3r_version != Semver::inf()) + text += ", max slic3r ver: " + vc.max_slic3r_version.to_string(); + text += "
"; + for (const std::pair> &model : vc.models_variants_installed) { + text += "model: " + model.first + ", variants: "; + for (const std::string &variant : model.second) { + if (&variant != &*model.second.begin()) + text += ", "; + text += variant; + } + text += "
"; + } } text += "

Activate

"; @@ -43,7 +55,7 @@ static std::string generate_html_page(const Config::SnapshotDB &snapshot_db) ""; text += ""; for (size_t i_row = 0; i_row < snapshot_db.snapshots().size(); ++ i_row) { - const Config::Snapshot &snapshot = snapshot_db.snapshots()[i_row]; + const Config::Snapshot &snapshot = snapshot_db.snapshots()[snapshot_db.snapshots().size() - i_row - 1]; text += generate_html_row(snapshot, i_row & 1); } text += @@ -96,8 +108,9 @@ ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db void ConfigSnapshotDialog::onLinkClicked(wxHtmlLinkEvent &event) { - wxLaunchDefaultBrowser(event.GetLinkInfo().GetHref()); - event.Skip(false); + m_snapshot_to_activate = event.GetLinkInfo().GetHref(); + this->EndModal(wxID_CLOSE); + this->Close(); } void ConfigSnapshotDialog::onCloseDialog(wxEvent &) diff --git a/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp b/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp index 70187ee99..0d1109615 100644 --- a/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp +++ b/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp @@ -19,9 +19,13 @@ class ConfigSnapshotDialog : public wxDialog public: ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db); + const std::string& snapshot_to_activate() const { return m_snapshot_to_activate; } + private: void onLinkClicked(wxHtmlLinkEvent &event); void onCloseDialog(wxEvent &); + + std::string m_snapshot_to_activate; }; } // namespace GUI diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index dcb21f644..cef56b892 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -349,9 +349,17 @@ void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_l { auto local_menu = new wxMenu(); wxWindowID config_id_base = wxWindow::NewControlId((int)ConfigMenuCnt); - + + // A different naming convention is used for the Wizard on Windows vs. OSX & GTK. +#if WIN32 + std::string config_wizard_menu = _(L("Configuration Wizard")); + std::string config_wizard_tooltip = _(L("Run configuration wizard")); +#else + std::string config_wizard_menu = _(L("Configuration Assistant")); + std::string config_wizard_tooltip = _(L("Run configuration Assistant")); +#endif // Cmd+, is standard on OS X - what about other operating systems? - local_menu->Append(config_id_base + ConfigMenuWizard, _(L("Configuration Wizard\u2026")), _(L("Run configuration wizard"))); + local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_menu + "\u2026", config_wizard_tooltip); local_menu->Append(config_id_base + ConfigMenuSnapshots, _(L("Configuration Snapshots\u2026")), _(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"))); @@ -361,21 +369,35 @@ void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_l local_menu->Append(config_id_base + ConfigMenuLanguage, _(L("Change Application Language"))); local_menu->Bind(wxEVT_MENU, [config_id_base, event_language_change, event_preferences_changed](wxEvent &event){ switch (event.GetId() - config_id_base) { + case ConfigMenuWizard: + config_wizard(0); + break; case ConfigMenuTakeSnapshot: // Take a configuration snapshot. - Slic3r::GUI::Config::SnapshotDB::singleton().take_snapshot(*g_AppConfig, Slic3r::GUI::Config::Snapshot::SNAPSHOT_USER, ""); + if (check_unsaved_changes()) { + wxTextEntryDialog dlg(nullptr, _(L("Taking configuration snapshot")), _(L("Snapshot name"))); + if (dlg.ShowModal() == wxID_OK) + Slic3r::GUI::Config::SnapshotDB::singleton().take_snapshot( + *g_AppConfig, Slic3r::GUI::Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); + } break; case ConfigMenuSnapshots: - { - ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton()); - dlg.ShowModal(); - dlg.Destroy(); + if (check_unsaved_changes()) { + ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton()); + dlg.ShowModal(); + if (! dlg.snapshot_to_activate().empty()) { + Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *g_AppConfig); + g_PresetBundle->load_presets(*g_AppConfig); + // Load the currently selected preset into the GUI, update the preset selection box. + for (Tab *tab : g_tabs_list) + tab->load_current_preset(); + } + } break; - } case ConfigMenuPreferences: { - auto dlg = new PreferencesDialog(g_wxMainFrame, event_preferences_changed); - dlg->ShowModal(); + PreferencesDialog dlg(g_wxMainFrame, event_preferences_changed); + dlg.ShowModal(); break; } case ConfigMenuLanguage: @@ -398,13 +420,45 @@ void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_l menu->Append(local_menu, _(L("&Configuration"))); } -bool open_config_wizard(PresetBundle *preset_bundle) +// 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. +bool check_unsaved_changes() { - if (g_wxMainFrame == nullptr) { - throw std::runtime_error("Main frame not set"); - } + std::string dirty; + for (Tab *tab : g_tabs_list) + if (tab->current_preset_is_dirty()) + if (dirty.empty()) + dirty = tab->name(); + else + dirty += std::string(", ") + tab->name(); + if (dirty.empty()) + // No changes, the application may close or reload presets. + return true; + // Ask the user. + auto dialog = new wxMessageDialog(g_wxMainFrame, + _(L("You have unsaved changes ")) + dirty + _(L(". Discard changes and continue anyway?")), + _(L("Unsaved Presets")), + wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); + return dialog->ShowModal() == wxID_YES; +} - return ConfigWizard::run(g_wxMainFrame, preset_bundle); +bool config_wizard(bool fresh_start) +{ + if (g_wxMainFrame == nullptr) + throw std::runtime_error("Main frame not set"); + + // Exit wizard if there are unsaved changes and the user cancels the action. + if (! check_unsaved_changes()) + return false; + + // TODO: Offer "reset user profile" ??? + if (! ConfigWizard::run(g_wxMainFrame, g_PresetBundle)) + return false; + + // Load the currently selected preset into the GUI, update the preset selection box. + for (Tab *tab : g_tabs_list) + tab->load_current_preset(); + return true; } void open_preferences_dialog(int event_preferences) diff --git a/xs/src/slic3r/GUI/GUI.hpp b/xs/src/slic3r/GUI/GUI.hpp index 938eed498..2a3667eb3 100644 --- a/xs/src/slic3r/GUI/GUI.hpp +++ b/xs/src/slic3r/GUI/GUI.hpp @@ -82,13 +82,17 @@ wxApp* get_app(); wxColour* get_modified_label_clr(); wxColour* get_sys_label_clr(); -void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_language_change); +extern void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_language_change); + +// 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. +extern bool check_unsaved_changes(); // Opens the first-time configuration wizard, returns true if wizard is finished & accepted. -bool open_config_wizard(PresetBundle *preset_bundle); +extern bool config_wizard(bool fresh_start); // Create "Preferences" dialog after selecting menu "Preferences" in Perl part -void open_preferences_dialog(int event_preferences); +extern void open_preferences_dialog(int event_preferences); // Create a new preset tab (print, filament and printer), void create_preset_tabs(bool no_controller, int event_value_change, int event_presets_changed); diff --git a/xs/src/slic3r/GUI/Tab.cpp b/xs/src/slic3r/GUI/Tab.cpp index babff7d80..940987536 100644 --- a/xs/src/slic3r/GUI/Tab.cpp +++ b/xs/src/slic3r/GUI/Tab.cpp @@ -2061,9 +2061,9 @@ wxSizer* Tab::compatible_printers_widget(wxWindow* parent, wxCheckBox** checkbox presets.Add(preset.name); } - auto dlg = new wxMultiChoiceDialog(parent, - _(L("Select the printers this profile is compatible with.")), - _(L("Compatible printers")), presets); + wxMultiChoiceDialog dlg(parent, + _(L("Select the printers this profile is compatible with.")), + _(L("Compatible printers")), presets); // # Collect and set indices of printers marked as compatible. wxArrayInt selections; auto *compatible_printers = dynamic_cast(m_config->option("compatible_printers")); @@ -2075,12 +2075,12 @@ wxSizer* Tab::compatible_printers_widget(wxWindow* parent, wxCheckBox** checkbox selections.Add(idx); break; } - dlg->SetSelections(selections); + dlg.SetSelections(selections); std::vector value; // Show the dialog. - if (dlg->ShowModal() == wxID_OK) { + if (dlg.ShowModal() == wxID_OK) { selections.Clear(); - selections = dlg->GetSelections(); + selections = dlg.GetSelections(); for (auto idx : selections) value.push_back(presets[idx].ToStdString()); if (value.empty()) { diff --git a/xs/xsp/GUI.xsp b/xs/xsp/GUI.xsp index 8dba13caf..964f350b9 100644 --- a/xs/xsp/GUI.xsp +++ b/xs/xsp/GUI.xsp @@ -57,8 +57,11 @@ int combochecklist_get_flags(SV *ui) void set_app_config(AppConfig *app_config) %code%{ Slic3r::GUI::set_app_config(app_config); %}; -bool open_config_wizard(PresetBundle *preset_bundle) - %code%{ RETVAL=Slic3r::GUI::open_config_wizard(preset_bundle); %}; +bool check_unsaved_changes() + %code%{ RETVAL=Slic3r::GUI::check_unsaved_changes(); %}; + +bool config_wizard(int fresh_start) + %code%{ RETVAL=Slic3r::GUI::config_wizard(fresh_start != 0); %}; void open_preferences_dialog(int preferences_event) %code%{ Slic3r::GUI::open_preferences_dialog(preferences_event); %}; From aaa8f133c00baba3ccff4e2115436254483a1fe0 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Wed, 11 Apr 2018 15:17:41 +0200 Subject: [PATCH 14/38] Fixed parsing of the config index. --- resources/profiles/PrusaResearch.idx | 13 +++++++ xs/src/semver/semver.c | 2 +- xs/src/slic3r/Config/Snapshot.cpp | 24 ++++++++++--- xs/src/slic3r/Config/Version.cpp | 54 ++++++++++++++++++---------- xs/src/slic3r/Utils/Semver.hpp | 10 +++--- 5 files changed, 74 insertions(+), 29 deletions(-) create mode 100644 resources/profiles/PrusaResearch.idx diff --git a/resources/profiles/PrusaResearch.idx b/resources/profiles/PrusaResearch.idx new file mode 100644 index 000000000..28f17f10a --- /dev/null +++ b/resources/profiles/PrusaResearch.idx @@ -0,0 +1,13 @@ +# This is an example configuration version index. +# The index contains version numbers +min_slic3r_version =1.39.0 +0.2.0-alpha "some test comment" +max_slic3r_version= 1.39.4 +0.1.0 another test comment + +# some empty lines + +# version without a comment +min_slic3r_version = 1.0.0 +max_slic3r_version = 1.1.0 +0.0.1 diff --git a/xs/src/semver/semver.c b/xs/src/semver/semver.c index 29bc1868d..599217f89 100644 --- a/xs/src/semver/semver.c +++ b/xs/src/semver/semver.c @@ -200,7 +200,7 @@ semver_parse_version (const char *str, semver_t *ver) { slice = next + 1; } - return 0; + return (index == 3) ? 0 : -1; } static int diff --git a/xs/src/slic3r/Config/Snapshot.cpp b/xs/src/slic3r/Config/Snapshot.cpp index 9fabbe013..eeb5b6ac5 100644 --- a/xs/src/slic3r/Config/Snapshot.cpp +++ b/xs/src/slic3r/Config/Snapshot.cpp @@ -57,6 +57,7 @@ void Snapshot::load_ini(const std::string &path) // Parse snapshot.ini std::string group_name_vendor = "Vendor:"; std::string key_filament = "filament"; + std::string key_prefix_model = "model_"; for (auto §ion : tree) { if (section.first == "snapshot") { // Parse the common section. @@ -107,10 +108,7 @@ void Snapshot::load_ini(const std::string &path) VendorConfig vc; vc.name = section.first.substr(group_name_vendor.size()); for (auto &kvp : section.second) { - if (boost::starts_with(kvp.first, "model_")) { - //model:MK2S = 0.4;xxx - //model:MK3 = 0.4;xxx - } else if (kvp.first == "version" || kvp.first == "min_slic3r_version" || kvp.first == "max_slic3r_version") { + if (kvp.first == "version" || kvp.first == "min_slic3r_version" || kvp.first == "max_slic3r_version") { // Version of the vendor specific config bundle bundled with this snapshot. auto semver = Semver::parse(kvp.second.data()); if (! semver) @@ -121,8 +119,16 @@ void Snapshot::load_ini(const std::string &path) vc.min_slic3r_version = *semver; else vc.max_slic3r_version = *semver; - } + } else if (boost::starts_with(kvp.first, key_prefix_model) && kvp.first.size() > key_prefix_model.size()) { + // Parse the printer variants installed for the current model. + auto &set_variants = vc.models_variants_installed[kvp.first.substr(key_prefix_model.size())]; + std::vector variants; + if (unescape_strings_cstyle(kvp.second.data(), variants)) + for (auto &variant : variants) + set_variants.insert(std::move(variant)); + } } + this->vendor_configs.emplace_back(std::move(vc)); } } } @@ -155,6 +161,14 @@ void Snapshot::save_ini(const std::string &path) c << "version = " << vc.version.to_string() << std::endl; c << "min_slic3r_version = " << vc.min_slic3r_version.to_string() << std::endl; c << "max_slic3r_version = " << vc.max_slic3r_version.to_string() << std::endl; + // Export installed printer models and their variants. + for (const auto &model : vc.models_variants_installed) { + if (model.second.size() == 0) + continue; + const std::vector variants(model.second.begin(), model.second.end()); + const auto escaped = escape_strings_cstyle(variants); + c << "model_" << model.first << " = " << escaped << std::endl; + } } c.close(); } diff --git a/xs/src/slic3r/Config/Version.cpp b/xs/src/slic3r/Config/Version.cpp index cc961829d..95b3caf1a 100644 --- a/xs/src/slic3r/Config/Version.cpp +++ b/xs/src/slic3r/Config/Version.cpp @@ -40,12 +40,13 @@ inline std::string unquote_value(char *value, char *end, const std::string &path if (value == end) { // Empty string is a valid string. } else if (*value == '"') { - if (++ value < -- end || *end != '"') + if (++ value > -- end || *end != '"') throw file_parser_error("String not enquoted correctly", path, idx_line); *end = 0; if (! unescape_string_cstyle(value, svalue)) throw file_parser_error("Invalid escape sequence inside a quoted string", path, idx_line); - } + } else + svalue.assign(value, end); return svalue; } @@ -55,12 +56,13 @@ inline std::string unquote_version_comment(char *value, char *end, const std::st if (value == end) { // Empty string is a valid string. } else if (*value == '"') { - if (++ value < -- end || *end != '"') + if (++ value > -- end || *end != '"') throw file_parser_error("Version comment not enquoted correctly", path, idx_line); *end = 0; if (! unescape_string_cstyle(value, svalue)) throw file_parser_error("Invalid escape sequence inside a quoted version comment", path, idx_line); - } + } else + svalue.assign(value, end); return svalue; } @@ -77,41 +79,55 @@ size_t Index::load(const boost::filesystem::path &path) ++ idx_line; // Skip the initial white spaces. char *key = left_trim(const_cast(line.data())); + if (*key == '#') + // Skip a comment line. + continue; // Right trim the line. char *end = right_trim(key); + if (key == end) + // Skip an empty line. + continue; // Keyword may only contain alphanumeric characters. Semantic version may in addition contain "+.-". char *key_end = key; - bool maybe_semver = false; - for (;; ++ key) { - if (strchr("+.-", *key) != nullptr) - maybe_semver = true; - else if (! std::isalnum(*key)) - break; + bool maybe_semver = true; + for (; *key_end != 0; ++ key_end) { + if (std::isalnum(*key_end) || strchr("+.-", *key_end) != nullptr) { + // It may be a semver. + } else if (*key_end == '_') { + // Cannot be a semver, but it may be a key. + maybe_semver = false; + } else + // End of semver or keyword. + break; } - if (*key != 0 && *key != ' ' && *key != '\t' && *key != '=') + if (*key_end != 0 && *key_end != ' ' && *key_end != '\t' && *key_end != '=') throw file_parser_error("Invalid keyword or semantic version", path, idx_line); - *key_end = 0; + char *value = left_trim(key_end); + bool key_value_pair = *value == '='; + if (key_value_pair) + value = left_trim(value + 1); + *key_end = 0; boost::optional semver; if (maybe_semver) semver = Semver::parse(key); - char *value = left_trim(key_end); - if (*value == '=') { + if (key_value_pair) { if (semver) - throw file_parser_error("Key cannot be a semantic version", path, idx_line); + throw file_parser_error("Key cannot be a semantic version", path, idx_line);\ // Verify validity of the key / value pair. - std::string svalue = unquote_value(left_trim(++ value), end, path.string(), idx_line); - if (key == "min_sic3r_version" || key == "max_slic3r_version") { + std::string svalue = unquote_value(value, end, path.string(), idx_line); + if (strcmp(key, "min_slic3r_version") == 0 || strcmp(key, "max_slic3r_version") == 0) { if (! svalue.empty()) - semver = Semver::parse(key); + semver = Semver::parse(svalue); if (! semver) throw file_parser_error(std::string(key) + " must referece a valid semantic version", path, idx_line); - if (key == "min_sic3r_version") + if (strcmp(key, "min_slic3r_version") == 0) ver.min_slic3r_version = *semver; else ver.max_slic3r_version = *semver; } else { // Ignore unknown keys, as there may come new keys in the future. } + continue; } if (! semver) throw file_parser_error("Invalid semantic version", path, idx_line); diff --git a/xs/src/slic3r/Utils/Semver.hpp b/xs/src/slic3r/Utils/Semver.hpp index 8aa8cc53f..bd8e9b758 100644 --- a/xs/src/slic3r/Utils/Semver.hpp +++ b/xs/src/slic3r/Utils/Semver.hpp @@ -55,9 +55,9 @@ public: Semver(const Semver &other) : ver(other.ver) { if (other.ver.metadata != nullptr) - std::strcpy(ver.metadata, other.ver.metadata); + ver.metadata = strdup(other.ver.metadata); if (other.ver.prerelease != nullptr) - std::strcpy(ver.prerelease, other.ver.prerelease); + ver.prerelease = strdup(other.ver.prerelease); } Semver &operator=(Semver &&other) @@ -73,8 +73,10 @@ public: { ::semver_free(&ver); ver = other.ver; - if (other.ver.metadata != nullptr) { std::strcpy(ver.metadata, other.ver.metadata); } - if (other.ver.prerelease != nullptr) { std::strcpy(ver.prerelease, other.ver.prerelease); } + if (other.ver.metadata != nullptr) + ver.metadata = strdup(other.ver.metadata); + if (other.ver.prerelease != nullptr) + ver.prerelease = strdup(other.ver.prerelease); return *this; } From 31ea03feb0af7376a35aa3b3428684b1e59d4a15 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Wed, 11 Apr 2018 13:12:08 +0200 Subject: [PATCH 15/38] ConfigWizard: Make bundle installation more intelligent, fixes --- xs/src/slic3r/Config/Snapshot.cpp | 4 +- xs/src/slic3r/GUI/AppConfig.cpp | 19 +++++++ xs/src/slic3r/GUI/AppConfig.hpp | 3 ++ xs/src/slic3r/GUI/ConfigWizard.cpp | 9 ++-- xs/src/slic3r/GUI/Preset.cpp | 75 ++++++++++++++++++++++++++- xs/src/slic3r/GUI/Preset.hpp | 11 +++- xs/src/slic3r/GUI/PresetBundle.cpp | 43 +-------------- xs/src/slic3r/Utils/PresetUpdater.cpp | 32 +++++++----- xs/src/slic3r/Utils/PresetUpdater.hpp | 4 +- xs/src/slic3r/Utils/Semver.hpp | 19 ++++++- xs/xsp/GUI.xsp | 8 ++- 11 files changed, 159 insertions(+), 68 deletions(-) diff --git a/xs/src/slic3r/Config/Snapshot.cpp b/xs/src/slic3r/Config/Snapshot.cpp index eeb5b6ac5..b6c356576 100644 --- a/xs/src/slic3r/Config/Snapshot.cpp +++ b/xs/src/slic3r/Config/Snapshot.cpp @@ -275,7 +275,7 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: // Snapshot header. snapshot.time_captured = Slic3r::Utils::get_current_time_utc(); snapshot.id = Slic3r::Utils::format_time_ISO8601Z(snapshot.time_captured); - snapshot.slic3r_version_captured = *Semver::parse(SLIC3R_VERSION); + snapshot.slic3r_version_captured = *Semver::parse(SLIC3R_VERSION); // XXX: have Semver Slic3r version snapshot.comment = comment; snapshot.reason = reason; // Active presets at the time of the snapshot. @@ -299,7 +299,7 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: bundle.load_configbundle((data_dir / "vendor" / (cfg.name + ".ini")).string(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); for (const VendorProfile &vp : bundle.vendors) if (vp.id == cfg.name) - cfg.version = *Semver::parse(vp.config_version); + cfg.version = vp.config_version; // Fill-in the min/max slic3r version from the config index, if possible. try { // Load the config index for the vendor. diff --git a/xs/src/slic3r/GUI/AppConfig.cpp b/xs/src/slic3r/GUI/AppConfig.cpp index 9e5ce5f1b..ee77f877a 100644 --- a/xs/src/slic3r/GUI/AppConfig.cpp +++ b/xs/src/slic3r/GUI/AppConfig.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -228,9 +229,27 @@ bool AppConfig::version_check_enabled() const bool AppConfig::slic3r_update_avail() const { + // FIXME: Update with Semver + // TODO: probably need to move semver to libslic3r return version_check_enabled() && get("version_online") != SLIC3R_VERSION; } +Semver AppConfig::get_slic3r_version() const +{ + // TODO: move to Semver c-tor (???) + auto res = Semver::parse(get("version")); + if (! res) { + throw std::runtime_error(std::string("Could not parse Slic3r version string in application config.")); + } else { + return *res; + } +} + +void AppConfig::set_slic3r_version(const Semver &version) +{ + set("version", version.to_string()); +} + std::string AppConfig::config_path() { return (boost::filesystem::path(Slic3r::data_dir()) / "slic3r.ini").make_preferred().string(); diff --git a/xs/src/slic3r/GUI/AppConfig.hpp b/xs/src/slic3r/GUI/AppConfig.hpp index 40b3a12fd..cac2759f1 100644 --- a/xs/src/slic3r/GUI/AppConfig.hpp +++ b/xs/src/slic3r/GUI/AppConfig.hpp @@ -6,6 +6,7 @@ #include #include "libslic3r/Config.hpp" +#include "slic3r/Utils/Semver.hpp" namespace Slic3r { @@ -91,6 +92,8 @@ public: // Whether the Slic3r version available online differs from this one bool version_check_enabled() const; bool slic3r_update_avail() const; + Semver get_slic3r_version() const; + void set_slic3r_version(const Semver &version); // Get the default config path from Slic3r::data_dir(). static std::string config_path(); diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index f13448c37..0c2a9dd59 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -15,6 +15,7 @@ #include "libslic3r/Utils.hpp" #include "PresetBundle.hpp" #include "GUI.hpp" +#include "slic3r/Utils/PresetUpdater.hpp" namespace fs = boost::filesystem; @@ -699,12 +700,8 @@ ConfigWizard::~ConfigWizard() {} bool ConfigWizard::run(wxWindow *parent, PresetBundle *preset_bundle) { - const auto profiles_dir = fs::path(resources_dir()) / "profiles"; - for (fs::directory_iterator it(profiles_dir); it != fs::directory_iterator(); ++it) { - if (it->path().extension() == ".ini") { - PresetBundle::install_vendor_configbundle(it->path()); - } - } + // FIXME: this should be done always at app startup + PresetUpdater::init_vendors(); ConfigWizard wizard(parent); if (wizard.ShowModal() == wxID_OK) { diff --git a/xs/src/slic3r/GUI/Preset.cpp b/xs/src/slic3r/GUI/Preset.cpp index 66836074e..39bfbd398 100644 --- a/xs/src/slic3r/GUI/Preset.cpp +++ b/xs/src/slic3r/GUI/Preset.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -24,14 +25,16 @@ #include "../../libslic3r/Utils.hpp" #include "../../libslic3r/PlaceholderParser.hpp" +using boost::property_tree::ptree; + namespace Slic3r { -ConfigFileType guess_config_file_type(const boost::property_tree::ptree &tree) +ConfigFileType guess_config_file_type(const ptree &tree) { size_t app_config = 0; size_t bundle = 0; size_t config = 0; - for (const boost::property_tree::ptree::value_type &v : tree) { + for (const ptree::value_type &v : tree) { if (v.second.empty()) { if (v.first == "background_processing" || v.first == "last_output_path" || @@ -59,6 +62,74 @@ ConfigFileType guess_config_file_type(const boost::property_tree::ptree &tree) (bundle > config) ? CONFIG_FILE_TYPE_CONFIG_BUNDLE : CONFIG_FILE_TYPE_CONFIG; } + +VendorProfile VendorProfile::from_ini(const boost::filesystem::path &path, bool load_all) +{ + ptree tree; + boost::filesystem::ifstream ifs(path); + boost::property_tree::read_ini(ifs, tree); + return VendorProfile::from_ini(tree, path, load_all); +} + +VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem::path &path, bool load_all) +{ + static const std::string printer_model_key = "printer_model:"; + const std::string id = path.stem().string(); + VendorProfile res(id); + + auto get_or_throw = [&](const ptree &tree, const std::string &key) -> ptree::const_assoc_iterator + { + auto res = tree.find(key); + if (res == tree.not_found()) { + throw std::runtime_error((boost::format("Vendor Config Bundle `%1%` is not valid: Missing secion or key: `%2%`.") % id % key).str()); + } + return res; + }; + + const auto &vendor_section = get_or_throw(tree, "vendor")->second; + res.name = get_or_throw(vendor_section, "name")->second.data(); + + auto config_version_str = get_or_throw(vendor_section, "config_version")->second.data(); + auto config_version = Semver::parse(config_version_str); + if (! config_version) { + throw std::runtime_error((boost::format("Vendor Config Bundle `%1%` is not valid: Cannot parse config_version: `%2%`.") % id % config_version_str).str()); + } else { + res.config_version = std::move(*config_version); + } + + auto config_update_url = vendor_section.find("config_update_url"); + if (config_update_url != vendor_section.not_found()) { + res.config_update_url = config_update_url->second.data(); + } + + if (! load_all) { + return res; + } + + for (auto §ion : tree) { + if (boost::starts_with(section.first, printer_model_key)) { + VendorProfile::PrinterModel model; + model.id = section.first.substr(printer_model_key.size()); + model.name = section.second.get("name", model.id); + section.second.get("variants", ""); + std::vector variants; + if (Slic3r::unescape_strings_cstyle(section.second.get("variants", ""), variants)) { + for (const std::string &variant_name : variants) { + if (model.variant(variant_name) == nullptr) + model.variants.emplace_back(VendorProfile::PrinterVariant(variant_name)); + } + } else { + // Log error? // XXX + } + if (! model.id.empty() && ! model.variants.empty()) + res.models.push_back(std::move(model)); + } + } + + return res; +} + + // Suffix to be added to a modified preset name in the combo box. static std::string g_suffix_modified = " (modified)"; const std::string& Preset::suffix_modified() diff --git a/xs/src/slic3r/GUI/Preset.hpp b/xs/src/slic3r/GUI/Preset.hpp index 4f734b85e..d6ccfd450 100644 --- a/xs/src/slic3r/GUI/Preset.hpp +++ b/xs/src/slic3r/GUI/Preset.hpp @@ -3,8 +3,12 @@ #include +#include +#include + #include "../../libslic3r/libslic3r.h" #include "../../libslic3r/PrintConfig.hpp" +#include "slic3r/Utils/Semver.hpp" class wxBitmap; class wxChoice; @@ -30,7 +34,7 @@ class VendorProfile public: std::string name; std::string id; - std::string config_version; + Semver config_version; std::string config_update_url; struct PrinterVariant { @@ -54,7 +58,10 @@ public: }; std::vector models; - VendorProfile(std::string id) : id(std::move(id)) {} + VendorProfile(std::string id) : id(std::move(id)), config_version(0, 0, 0) {} + + static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true); + static VendorProfile from_ini(const boost::property_tree::ptree &tree, const boost::filesystem::path &path, bool load_all=true); size_t num_variants() const { size_t n = 0; for (auto &model : models) n += model.variants.size(); return n; } diff --git a/xs/src/slic3r/GUI/PresetBundle.cpp b/xs/src/slic3r/GUI/PresetBundle.cpp index bd9e35ca8..ad27bf8c6 100644 --- a/xs/src/slic3r/GUI/PresetBundle.cpp +++ b/xs/src/slic3r/GUI/PresetBundle.cpp @@ -105,7 +105,7 @@ void PresetBundle::setup_directories() std::initializer_list paths = { data_dir, data_dir / "vendor", - data_dir / "cache", // TODO: rename as vendor-cache? (Check usage elsewhere!) + data_dir / "cache", #ifdef SLIC3R_PROFILE_USE_PRESETS_SUBDIR // Store the print/filament/printer presets into a "presets" directory. data_dir / "presets", @@ -672,42 +672,6 @@ static void flatten_configbundle_hierarchy(boost::property_tree::ptree &tree) flatten_configbundle_hierarchy(tree, "printer"); } -static void load_vendor_profile(const boost::property_tree::ptree &tree, VendorProfile &vendor_profile) -{ - const std::string printer_model_key = "printer_model:"; - for (auto §ion : tree) { - if (section.first == "vendor") { - // Load the names of the active presets. - for (auto &kvp : section.second) { - if (kvp.first == "name") - vendor_profile.name = kvp.second.data(); - else if (kvp.first == "id") - vendor_profile.id = kvp.second.data(); - else if (kvp.first == "config_version") - vendor_profile.config_version = kvp.second.data(); - else if (kvp.first == "config_update_url") - vendor_profile.config_update_url = kvp.second.data(); - } - } else if (boost::starts_with(section.first, printer_model_key)) { - VendorProfile::PrinterModel model; - model.id = section.first.substr(printer_model_key.size()); - model.name = section.second.get("name", model.id); - section.second.get("variants", ""); - std::vector variants; - if (Slic3r::unescape_strings_cstyle(section.second.get("variants", ""), variants)) { - for (const std::string &variant_name : variants) { - if (model.variant(variant_name) == nullptr) - model.variants.emplace_back(VendorProfile::PrinterVariant(variant_name)); - } - } else { - // Log error? - } - if (! model.id.empty() && ! model.variants.empty()) - vendor_profile.models.push_back(std::move(model)); - } - } -} - // Load a config bundle file, into presets and store the loaded presets into separate files // of the local configuration directory. void PresetBundle::install_vendor_configbundle(const std::string &src_path0) @@ -738,10 +702,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla const VendorProfile *vendor_profile = nullptr; if (flags & (LOAD_CFGBNDLE_SYSTEM | LOAD_CFGBUNDLE_VENDOR_ONLY)) { boost::filesystem::path fspath(path); - VendorProfile vp(fspath.stem().string()); - load_vendor_profile(tree, vp); - if (vp.name.empty()) - throw std::runtime_error(std::string("Vendor Config Bundle is not valid: Missing vendor name key.")); + auto vp = VendorProfile::from_ini(tree, fspath.stem().string()); if (vp.num_variants() == 0) return 0; vendor_profile = &(*this->vendors.insert(vp).first); diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp index 040d326b5..a16a6d889 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.cpp +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -1,9 +1,8 @@ #include "PresetUpdater.hpp" -#include // XXX #include #include -#include +#include #include #include @@ -22,6 +21,7 @@ namespace Slic3r { // TODO: proper URL +// TODO: Actually, use index static const std::string SLIC3R_VERSION_URL = "https://gist.githubusercontent.com/vojtechkral/4d8fd4a3b8699a01ec892c264178461c/raw/e9187c3e15ceaf1a90f29b7c43cf3ccc746140f0/slic3rPE.version"; enum { SLIC3R_VERSION_BODY_MAX = 256, @@ -52,8 +52,6 @@ PresetUpdater::priv::priv(int event) : void PresetUpdater::priv::download(const std::set vendors) const { - std::cerr << "PresetUpdater::priv::download()" << std::endl; - if (!version_check) { return; } // Download current Slic3r version @@ -64,7 +62,6 @@ void PresetUpdater::priv::download(const std::set vendors) const }) .on_complete([&](std::string body, unsigned http_status) { boost::trim(body); - std::cerr << "Got version: " << http_status << ", body: \"" << body << '"' << std::endl; wxCommandEvent* evt = new wxCommandEvent(version_online_event); evt->SetString(body); GUI::get_app()->QueueEvent(evt); @@ -74,25 +71,19 @@ void PresetUpdater::priv::download(const std::set vendors) const if (!preset_update) { return; } // Donwload vendor preset bundles - std::cerr << "Bundle vendors: " << vendors.size() << std::endl; for (const auto &vendor : vendors) { - std::cerr << "vendor: " << vendor.name << std::endl; - std::cerr << " URL: " << vendor.config_update_url << std::endl; - if (cancel) { return; } // TODO: Proper caching auto target_path = cache_path / vendor.id; target_path += ".ini"; - std::cerr << "target_path: " << target_path << std::endl; Http::get(vendor.config_update_url) .on_progress([this](Http::Progress, bool &cancel) { cancel = this->cancel; }) .on_complete([&](std::string body, unsigned http_status) { - std::cerr << "Got ini: " << http_status << ", body: " << body.size() << std::endl; fs::fstream file(target_path, std::ios::out | std::ios::binary | std::ios::trunc); file.write(body.c_str(), body.size()); }) @@ -121,7 +112,6 @@ PresetUpdater::~PresetUpdater() void PresetUpdater::download(PresetBundle *preset_bundle) { - std::cerr << "PresetUpdater::download()" << std::endl; // Copy the whole vendors data for use in the background thread // Unfortunatelly as of C++11, it needs to be copied again @@ -133,5 +123,23 @@ void PresetUpdater::download(PresetBundle *preset_bundle) })); } +void PresetUpdater::init_vendors() +{ + const auto vendors_rources = fs::path(resources_dir()) / "profiles"; + const auto vendors_data = fs::path(Slic3r::data_dir()) / "vendor"; + + for (fs::directory_iterator it(vendors_rources); it != fs::directory_iterator(); ++it) { + if (it->path().extension() == ".ini") { + auto vp = VendorProfile::from_ini(it->path(), false); + const auto path_in_data = vendors_data / it->path().filename(); + + if (! fs::exists(path_in_data) || VendorProfile::from_ini(path_in_data, false).config_version < vp.config_version) { + // FIXME: update vendor bundle properly when snapshotting is ready + PresetBundle::install_vendor_configbundle(it->path()); + } + } + } +} + } diff --git a/xs/src/slic3r/Utils/PresetUpdater.hpp b/xs/src/slic3r/Utils/PresetUpdater.hpp index aafe9569b..b10c61784 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.hpp +++ b/xs/src/slic3r/Utils/PresetUpdater.hpp @@ -19,7 +19,9 @@ public: PresetUpdater &operator=(const PresetUpdater &) = delete; ~PresetUpdater(); - void download(PresetBundle *preset_bundle); + void download(PresetBundle *preset_bundle); // XXX + + static void init_vendors(); private: struct priv; std::unique_ptr p; diff --git a/xs/src/slic3r/Utils/Semver.hpp b/xs/src/slic3r/Utils/Semver.hpp index bd8e9b758..2ac2ba700 100644 --- a/xs/src/slic3r/Utils/Semver.hpp +++ b/xs/src/slic3r/Utils/Semver.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -10,6 +11,7 @@ namespace Slic3r { +// FIXME:: operators=: leak, return class Semver { @@ -18,9 +20,22 @@ public: struct Minor { const int i; Minor(int i) : i(i) {} }; struct Patch { const int i; Patch(int i) : i(i) {} }; + Semver(int major, int minor, int patch, + boost::optional metadata = boost::none, + boost::optional prerelease = boost::none) + { + ver.major = major; + ver.minor = minor; + ver.patch = patch; + ver.metadata = metadata ? std::strcpy(ver.metadata, metadata->c_str()) : nullptr; + ver.prerelease = prerelease ? std::strcpy(ver.prerelease, prerelease->c_str()) : nullptr; + } + + // TODO: throwing ctor ??? + static boost::optional parse(const std::string &str) { - semver_t ver; + semver_t ver = semver_zero(); if (::semver_parse(str.c_str(), &ver) == 0) { return Semver(ver); } else { @@ -121,6 +136,8 @@ private: semver_t ver; Semver(semver_t ver) : ver(ver) {} + + static semver_t semver_zero() { return { 0, 0, 0, nullptr, nullptr }; } }; diff --git a/xs/xsp/GUI.xsp b/xs/xsp/GUI.xsp index 964f350b9..46e4ace83 100644 --- a/xs/xsp/GUI.xsp +++ b/xs/xsp/GUI.xsp @@ -61,7 +61,13 @@ bool check_unsaved_changes() %code%{ RETVAL=Slic3r::GUI::check_unsaved_changes(); %}; bool config_wizard(int fresh_start) - %code%{ RETVAL=Slic3r::GUI::config_wizard(fresh_start != 0); %}; + %code%{ + try { + RETVAL = Slic3r::GUI::config_wizard(fresh_start != 0); + } catch (std::exception& e) { + croak("%s\n", e.what()); + } + %}; void open_preferences_dialog(int preferences_event) %code%{ Slic3r::GUI::open_preferences_dialog(preferences_event); %}; From 12b3132b1a55459b550f144b70b7ae548014c751 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Wed, 11 Apr 2018 15:20:38 +0200 Subject: [PATCH 16/38] Perform init_vendors at startup --- lib/Slic3r/GUI.pm | 2 ++ xs/src/slic3r/GUI/ConfigWizard.cpp | 3 --- xs/xsp/Utils_PresetUpdater.xsp | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index b28b84df3..2082728cc 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -101,6 +101,8 @@ sub OnInit { $self->{app_config}->set('version', $Slic3r::VERSION); $self->{app_config}->save; + Slic3r::PresetUpdater::init_vendors(); + # my $version_check = $self->{app_config}->get('version_check'); $self->{preset_updater} = Slic3r::PresetUpdater->new($VERSION_ONLINE_EVENT, $self->{app_config}); my $slic3r_update = $self->{app_config}->slic3r_update_avail; diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index 0c2a9dd59..52a896704 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -700,9 +700,6 @@ ConfigWizard::~ConfigWizard() {} bool ConfigWizard::run(wxWindow *parent, PresetBundle *preset_bundle) { - // FIXME: this should be done always at app startup - PresetUpdater::init_vendors(); - ConfigWizard wizard(parent); if (wizard.ShowModal() == wxID_OK) { wizard.p->apply_config(GUI::get_app_config(), preset_bundle); diff --git a/xs/xsp/Utils_PresetUpdater.xsp b/xs/xsp/Utils_PresetUpdater.xsp index 666379f02..1cb9f1c39 100644 --- a/xs/xsp/Utils_PresetUpdater.xsp +++ b/xs/xsp/Utils_PresetUpdater.xsp @@ -8,4 +8,5 @@ %name{Slic3r::PresetUpdater} class PresetUpdater { PresetUpdater(int version_online_event, AppConfig *app_config); void download(PresetBundle* preset_bundle); + static void init_vendors(); }; From b030791384a312d8368941fb7bc01c65e1fd6dea Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Wed, 11 Apr 2018 17:07:27 +0200 Subject: [PATCH 17/38] Semver fixes, misc fixes --- resources/profiles/BarBaz.ini | 2 +- resources/profiles/Foobar.ini | 2 +- xs/src/semver/semver.c | 18 ++++++++++++++++++ xs/src/semver/semver.h | 3 +++ xs/src/slic3r/GUI/AboutDialog.cpp | 2 +- xs/src/slic3r/GUI/GUI.cpp | 8 ++++---- xs/src/slic3r/Utils/Semver.hpp | 30 +++++------------------------- xs/src/slic3r/Utils/Time.cpp | 3 ++- 8 files changed, 35 insertions(+), 33 deletions(-) diff --git a/resources/profiles/BarBaz.ini b/resources/profiles/BarBaz.ini index 83bb15684..7c8cd3a6b 100644 --- a/resources/profiles/BarBaz.ini +++ b/resources/profiles/BarBaz.ini @@ -5,7 +5,7 @@ name = Bar Baz # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the Slic3r configuration to be downgraded. -config_version = 0.1 +config_version = 0.1.0 # Where to get the updates from? config_update_url = https://example.com diff --git a/resources/profiles/Foobar.ini b/resources/profiles/Foobar.ini index 21681398a..571aa8bb8 100644 --- a/resources/profiles/Foobar.ini +++ b/resources/profiles/Foobar.ini @@ -5,7 +5,7 @@ name = Foo Bar # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the Slic3r configuration to be downgraded. -config_version = 0.1 +config_version = 0.1.0 # Where to get the updates from? config_update_url = https://example.com diff --git a/xs/src/semver/semver.c b/xs/src/semver/semver.c index 599217f89..3e4a30e3a 100644 --- a/xs/src/semver/semver.c +++ b/xs/src/semver/semver.c @@ -615,3 +615,21 @@ semver_numeric (semver_t *x) { return num; } + +static char *semver_strdup(const char *src) { + if (src == NULL) return NULL; + size_t len = strlen(src) + 1; + char *res = malloc(len); + return res != NULL ? (char *) memcpy(res, src, len) : NULL; +} + +semver_t +semver_copy(const semver_t *ver) { + semver_t res = *ver; + if (ver->metadata != NULL) { + res.metadata = strdup(ver->metadata); + } + if (ver->prerelease != NULL) { + res.prerelease = strdup(ver->prerelease); + } +} \ No newline at end of file diff --git a/xs/src/semver/semver.h b/xs/src/semver/semver.h index 1b48670ca..7251f51e3 100644 --- a/xs/src/semver/semver.h +++ b/xs/src/semver/semver.h @@ -98,6 +98,9 @@ semver_is_valid (const char *s); int semver_clean (char *s); +semver_t +semver_copy(const semver_t *ver); + #ifdef __cplusplus } #endif diff --git a/xs/src/slic3r/GUI/AboutDialog.cpp b/xs/src/slic3r/GUI/AboutDialog.cpp index 49cfff2bd..664bbd1bb 100644 --- a/xs/src/slic3r/GUI/AboutDialog.cpp +++ b/xs/src/slic3r/GUI/AboutDialog.cpp @@ -56,7 +56,7 @@ AboutDialog::AboutDialog() // version { - std::string version_string = _(L("Version ")) + std::string(SLIC3R_VERSION); + auto version_string = _(L("Version ")) + std::string(SLIC3R_VERSION); wxStaticText* version = new wxStaticText(this, wxID_ANY, version_string.c_str(), wxDefaultPosition, wxDefaultSize); wxFont version_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); #ifdef __WXMSW__ diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index cef56b892..d70b47840 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -352,11 +352,11 @@ void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_l // A different naming convention is used for the Wizard on Windows vs. OSX & GTK. #if WIN32 - std::string config_wizard_menu = _(L("Configuration Wizard")); - std::string config_wizard_tooltip = _(L("Run configuration wizard")); + auto config_wizard_menu = _(L("Configuration Wizard")); + auto config_wizard_tooltip = _(L("Run configuration wizard")); #else - std::string config_wizard_menu = _(L("Configuration Assistant")); - std::string config_wizard_tooltip = _(L("Run configuration Assistant")); + auto config_wizard_menu = _(L("Configuration Assistant")); + auto config_wizard_tooltip = _(L("Run configuration Assistant")); #endif // Cmd+, is standard on OS X - what about other operating systems? local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_menu + "\u2026", config_wizard_tooltip); diff --git a/xs/src/slic3r/Utils/Semver.hpp b/xs/src/slic3r/Utils/Semver.hpp index 2ac2ba700..a1f4a92e8 100644 --- a/xs/src/slic3r/Utils/Semver.hpp +++ b/xs/src/slic3r/Utils/Semver.hpp @@ -43,11 +43,7 @@ public: } } - static const Semver zero() - { - static semver_t ver = { 0, 0, 0, nullptr, nullptr }; - return Semver(ver); - } + static const Semver zero() { return Semver(semver_zero()); } static const Semver inf() { @@ -61,37 +57,21 @@ public: return Semver(ver); } - Semver(Semver &&other) : ver(other.ver) - { - other.ver.major = other.ver.minor = other.ver.patch = 0; - other.ver.metadata = other.ver.prerelease = nullptr; - } - - Semver(const Semver &other) : ver(other.ver) - { - if (other.ver.metadata != nullptr) - ver.metadata = strdup(other.ver.metadata); - if (other.ver.prerelease != nullptr) - ver.prerelease = strdup(other.ver.prerelease); - } + Semver(Semver &&other) : ver(other.ver) { other.ver = semver_zero(); } + Semver(const Semver &other) : ver(::semver_copy(&other.ver)) {} Semver &operator=(Semver &&other) { ::semver_free(&ver); ver = other.ver; - other.ver.major = other.ver.minor = other.ver.patch = 0; - other.ver.metadata = other.ver.prerelease = nullptr; + other.ver = semver_zero(); return *this; } Semver &operator=(const Semver &other) { ::semver_free(&ver); - ver = other.ver; - if (other.ver.metadata != nullptr) - ver.metadata = strdup(other.ver.metadata); - if (other.ver.prerelease != nullptr) - ver.prerelease = strdup(other.ver.prerelease); + ver = ::semver_copy(&other.ver); return *this; } diff --git a/xs/src/slic3r/Utils/Time.cpp b/xs/src/slic3r/Utils/Time.cpp index a2b2328af..f38c4b407 100644 --- a/xs/src/slic3r/Utils/Time.cpp +++ b/xs/src/slic3r/Utils/Time.cpp @@ -71,7 +71,8 @@ time_t get_current_time_utc() tm.tm_isdst = -1; return mktime(&tm); #else - return gmtime(); + const time_t current_local = time(nullptr); + return mktime(gmtime(¤t_local)); #endif } From 9ab38f416d20d0ca8fc33c08d80e2be07fc878ff Mon Sep 17 00:00:00 2001 From: bubnikv Date: Thu, 12 Apr 2018 11:24:03 +0200 Subject: [PATCH 18/38] Improvement of the snapshot dialog, fixed storing of the snapshot "reason" field. --- xs/src/slic3r/Config/Snapshot.cpp | 22 ++++++++++++++- xs/src/slic3r/Config/Snapshot.hpp | 2 ++ xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp | 31 +++++++++++++++++----- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/xs/src/slic3r/Config/Snapshot.cpp b/xs/src/slic3r/Config/Snapshot.cpp index eeb5b6ac5..c50021e2e 100644 --- a/xs/src/slic3r/Config/Snapshot.cpp +++ b/xs/src/slic3r/Config/Snapshot.cpp @@ -133,6 +133,21 @@ void Snapshot::load_ini(const std::string &path) } } +static std::string reason_string(const Snapshot::Reason reason) +{ + switch (reason) { + case Snapshot::SNAPSHOT_UPGRADE: + return "upgrade"; + case Snapshot::SNAPSHOT_DOWNGRADE: + return "downgrade"; + case Snapshot::SNAPSHOT_USER: + return "user"; + case Snapshot::SNAPSHOT_UNKNOWN: + default: + return "unknown"; + } +} + void Snapshot::save_ini(const std::string &path) { boost::nowide::ofstream c; @@ -145,7 +160,7 @@ void Snapshot::save_ini(const std::string &path) c << "time_captured = " << Slic3r::Utils::format_time_ISO8601Z(this->time_captured) << std::endl; c << "slic3r_version_captured = " << this->slic3r_version_captured.to_string() << std::endl; c << "comment = " << this->comment << std::endl; - c << "reason = " << this->reason << std::endl; + c << "reason = " << reason_string(this->reason) << std::endl; // Export the active presets at the time of the snapshot. c << std::endl << "[presets]" << std::endl; @@ -294,6 +309,11 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: Snapshot::VendorConfig cfg; cfg.name = vendor.first; cfg.models_variants_installed = vendor.second; + for (auto it = cfg.models_variants_installed.begin(); it != cfg.models_variants_installed.end();) + if (it->second.empty()) + cfg.models_variants_installed.erase(it ++); + else + ++ it; // Read the active config bundle, parse the config version. PresetBundle bundle; bundle.load_configbundle((data_dir / "vendor" / (cfg.name + ".ini")).string(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); diff --git a/xs/src/slic3r/Config/Snapshot.hpp b/xs/src/slic3r/Config/Snapshot.hpp index a7b8a5aa5..8f27027a4 100644 --- a/xs/src/slic3r/Config/Snapshot.hpp +++ b/xs/src/slic3r/Config/Snapshot.hpp @@ -57,6 +57,8 @@ public: std::string comment; Reason reason; + std::string format_reason() const; + // Active presets at the time of the snapshot. std::string print; std::vector filaments; diff --git a/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp b/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp index 730b97a32..5bc8b1012 100644 --- a/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp +++ b/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp @@ -8,6 +8,21 @@ namespace Slic3r { namespace GUI { +static std::string format_reason(const Config::Snapshot::Reason reason) +{ + switch (reason) { + case Config::Snapshot::SNAPSHOT_UPGRADE: + return std::string(_(L("Upgrade"))); + case Config::Snapshot::SNAPSHOT_DOWNGRADE: + return std::string(_(L("Downgrade"))); + case Config::Snapshot::SNAPSHOT_USER: + return std::string(_(L("User"))); + case Config::Snapshot::SNAPSHOT_UNKNOWN: + default: + return std::string(_(L("Unknown"))); + } +} + static std::string generate_html_row(const Config::Snapshot &snapshot, bool row_even) { // Start by declaring a row with an alternating background color. @@ -15,11 +30,15 @@ static std::string generate_html_row(const Config::Snapshot &snapshot, bool row_ text += row_even ? "#FFFFFF" : "#C0C0C0"; text += "\">"; text += ""; text += ""; text += ""; return text; } -static std::string generate_html_page(const Config::SnapshotDB &snapshot_db) +static std::string generate_html_page(const Config::SnapshotDB &snapshot_db, const std::string &on_snapshot) { std::string text = "" @@ -75,7 +79,7 @@ static std::string generate_html_page(const Config::SnapshotDB &snapshot_db) text += "
"; -// text += _(L("ID:")) + " " + snapshot.id + "
"; - text += _(L("time captured:")) + " " + Utils::format_local_date_time(snapshot.time_captured) + "
"; - text += _(L("slic3r version:")) + " " + snapshot.slic3r_version_captured.to_string() + "
"; + // Format the row header. + text += std::string("") + Utils::format_local_date_time(snapshot.time_captured) + ": " + format_reason(snapshot.reason); if (! snapshot.comment.empty()) - text += _(L("user comment:")) + " " + snapshot.comment + "
"; + text += " (" + snapshot.comment + ")"; + text += "

"; + // End of row header. +// text += _(L("ID:")) + " " + snapshot.id + "
"; + // text += _(L("time captured:")) + " " + Utils::format_local_date_time(snapshot.time_captured) + "
"; + text += _(L("slic3r version:")) + " " + snapshot.slic3r_version_captured.to_string() + "
"; // text += "reason: " + snapshot.reason + "
"; text += "print: " + snapshot.print + "
"; text += "filaments: " + snapshot.filaments.front() + "
"; @@ -79,9 +98,9 @@ ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db { wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); #ifdef __WXMSW__ - int size[] = {8,8,8,8,8,8,8}; + int size[] = {8,8,8,8,11,11,11}; #else - int size[] = {11,11,11,11,11,11,11}; + int size[] = {11,11,11,11,14,14,14}; #endif html->SetFonts(font.GetFaceName(), font.GetFaceName(), size); html->SetBorders(2); From b49b59cbb25ee8903837e34af1da1f647ef62c84 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Thu, 12 Apr 2018 20:04:48 +0200 Subject: [PATCH 19/38] Configuration update application at startup --- lib/Slic3r/GUI.pm | 18 +- xs/src/semver/semver.c | 3 +- xs/src/slic3r/Config/Version.cpp | 7 +- xs/src/slic3r/Config/Version.hpp | 2 +- xs/src/slic3r/GUI/AppConfig.hpp | 9 +- xs/src/slic3r/GUI/ConfigWizard.cpp | 63 +++++-- xs/src/slic3r/GUI/ConfigWizard_private.hpp | 12 +- xs/src/slic3r/GUI/Preset.cpp | 7 + xs/src/slic3r/GUI/Preset.hpp | 3 +- xs/src/slic3r/GUI/PresetBundle.cpp | 3 +- xs/src/slic3r/Utils/PresetUpdater.cpp | 208 +++++++++++++++++++-- xs/src/slic3r/Utils/PresetUpdater.hpp | 2 +- xs/src/slic3r/Utils/Semver.hpp | 3 +- xs/xsp/Utils_PresetUpdater.xsp | 2 +- 14 files changed, 288 insertions(+), 54 deletions(-) diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 2082728cc..b592c4289 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -101,11 +101,14 @@ sub OnInit { $self->{app_config}->set('version', $Slic3r::VERSION); $self->{app_config}->save; - Slic3r::PresetUpdater::init_vendors(); - # my $version_check = $self->{app_config}->get('version_check'); $self->{preset_updater} = Slic3r::PresetUpdater->new($VERSION_ONLINE_EVENT, $self->{app_config}); - my $slic3r_update = $self->{app_config}->slic3r_update_avail; + eval { $self->{preset_updater}->config_update() }; + if ($@) { + warn $@ . "\n"; + fatal_error(undef, $@); + } + # my $slic3r_update = $self->{app_config}->slic3r_update_avail; Slic3r::GUI::set_app_config($self->{app_config}); Slic3r::GUI::load_language(); @@ -123,6 +126,7 @@ sub OnInit { Slic3r::GUI::set_preset_bundle($self->{preset_bundle}); # application frame + print STDERR "Creating main frame...\n"; Wx::Image::FindHandlerType(wxBITMAP_TYPE_PNG) || Wx::Image::AddHandler(Wx::PNGHandler->new); $self->{mainframe} = my $frame = Slic3r::GUI::MainFrame->new( # If set, the "Controller" tab for the control of the printer over serial line and the serial port settings are hidden. @@ -145,9 +149,9 @@ sub OnInit { # before the UI was up and running. $self->CallAfter(sub { # XXX: recreate_GUI ??? - if ($slic3r_update) { - # TODO - } + # if ($slic3r_update) { + # # TODO + # } # XXX: ? if ($run_wizard) { # Run the config wizard, don't offer the "reset user profile" checkbox. @@ -159,6 +163,7 @@ sub OnInit { # The following event is emited by the C++ menu implementation of application language change. EVT_COMMAND($self, -1, $LANGUAGE_CHANGE_EVENT, sub{ + print STDERR "LANGUAGE_CHANGE_EVENT\n"; $self->recreate_GUI; }); @@ -179,6 +184,7 @@ sub OnInit { } sub recreate_GUI{ + print STDERR "recreate_GUI\n"; my ($self) = @_; my $topwindow = $self->GetTopWindow(); $self->{mainframe} = my $frame = Slic3r::GUI::MainFrame->new( diff --git a/xs/src/semver/semver.c b/xs/src/semver/semver.c index 3e4a30e3a..0285fe40e 100644 --- a/xs/src/semver/semver.c +++ b/xs/src/semver/semver.c @@ -632,4 +632,5 @@ semver_copy(const semver_t *ver) { if (ver->prerelease != NULL) { res.prerelease = strdup(ver->prerelease); } -} \ No newline at end of file + return res; +} diff --git a/xs/src/slic3r/Config/Version.cpp b/xs/src/slic3r/Config/Version.cpp index 95b3caf1a..5430e569c 100644 --- a/xs/src/slic3r/Config/Version.cpp +++ b/xs/src/slic3r/Config/Version.cpp @@ -141,7 +141,7 @@ size_t Index::load(const boost::filesystem::path &path) return m_configs.size(); } -Index::const_iterator Index::find(const Semver &ver) +Index::const_iterator Index::find(const Semver &ver) const { Version key; key.config_version = ver; @@ -163,12 +163,11 @@ Index::const_iterator Index::recommended() const std::vector Index::load_db() { - boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); - boost::filesystem::path vendor_dir = data_dir / "vendor"; + boost::filesystem::path cache_dir = boost::filesystem::path(Slic3r::data_dir()) / "cache"; std::vector index_db; std::string errors_cummulative; - for (auto &dir_entry : boost::filesystem::directory_iterator(vendor_dir)) + for (auto &dir_entry : boost::filesystem::directory_iterator(cache_dir)) if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".idx")) { Index idx; try { diff --git a/xs/src/slic3r/Config/Version.hpp b/xs/src/slic3r/Config/Version.hpp index 43512e82f..c010a1748 100644 --- a/xs/src/slic3r/Config/Version.hpp +++ b/xs/src/slic3r/Config/Version.hpp @@ -62,7 +62,7 @@ public: const_iterator begin() const { return m_configs.begin(); } const_iterator end() const { return m_configs.end(); } - const_iterator find(const Semver &ver); + const_iterator find(const Semver &ver) const; const std::vector& configs() const { return m_configs; } // Finds a recommended config to be installed for the current Slic3r version. // Returns configs().end() if such version does not exist in the index. This shall never happen diff --git a/xs/src/slic3r/GUI/AppConfig.hpp b/xs/src/slic3r/GUI/AppConfig.hpp index cac2759f1..ffda083ec 100644 --- a/xs/src/slic3r/GUI/AppConfig.hpp +++ b/xs/src/slic3r/GUI/AppConfig.hpp @@ -69,12 +69,13 @@ public: void clear_section(const std::string §ion) { m_storage[section].clear(); } + typedef std::map>> VendorMap; bool get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const; void set_variant(const std::string &vendor, const std::string &model, const std::string &variant, bool enable); void set_vendors(const AppConfig &from); - void set_vendors(const std::map>> &vendors) { m_vendors = vendors; m_dirty = true; } - void set_vendors(std::map>> &&vendors) { m_vendors = std::move(vendors); m_dirty = true; } - const std::map>> vendors() const { return m_vendors; } + void set_vendors(const VendorMap &vendors) { m_vendors = vendors; m_dirty = true; } + void set_vendors(VendorMap &&vendors) { m_vendors = std::move(vendors); m_dirty = true; } + const VendorMap& vendors() const { return m_vendors; } // return recent/skein_directory or recent/config_directory or empty string. std::string get_last_dir() const; @@ -105,7 +106,7 @@ private: // Map of section, name -> value std::map> m_storage; // Map of enabled vendors / models / variants - std::map>> m_vendors; + VendorMap m_vendors; // Has any value been modified since the config.ini has been last saved or loaded? bool m_dirty; }; diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index 52a896704..f353ab7f7 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -1,8 +1,8 @@ #include "ConfigWizard_private.hpp" +#include // XXX #include #include -#include #include #include @@ -17,7 +17,6 @@ #include "GUI.hpp" #include "slic3r/Utils/PresetUpdater.hpp" -namespace fs = boost::filesystem; namespace Slic3r { namespace GUI { @@ -62,25 +61,25 @@ PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, cons auto namefont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); namefont.SetWeight(wxFONTWEIGHT_BOLD); - for (auto model = models.cbegin(); model != models.cend(); ++model) { + for (const auto &model : models) { auto *panel = new wxPanel(this); auto *sizer = new wxBoxSizer(wxVERTICAL); panel->SetSizer(sizer); - auto *title = new wxStaticText(panel, wxID_ANY, model->name, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + auto *title = new wxStaticText(panel, wxID_ANY, model.name, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); title->SetFont(namefont); sizer->Add(title, 0, wxBOTTOM, 3); - auto bitmap_file = wxString::Format("printers/%s_%s.png", vendor.id, model->id); + auto bitmap_file = wxString::Format("printers/%s_%s.png", vendor.id, model.id); wxBitmap bitmap(GUI::from_u8(Slic3r::var(bitmap_file.ToStdString())), wxBITMAP_TYPE_PNG); auto *bitmap_widget = new wxStaticBitmap(panel, wxID_ANY, bitmap); sizer->Add(bitmap_widget, 0, wxBOTTOM, 3); sizer->AddSpacer(20); - const auto model_id = model->id; + const auto model_id = model.id; - for (const auto &variant : model->variants) { + for (const auto &variant : model.variants) { const auto variant_name = variant.name; auto *cbox = new wxCheckBox(panel, wxID_ANY, wxString::Format("%s %s %s", variant.name, _(L("mm")), _(L("nozzle")))); bool enabled = appconfig_vendors.get_variant("PrusaResearch", model_id, variant_name); @@ -177,16 +176,18 @@ PageWelcome::PageWelcome(ConfigWizard *parent) : { append_text(_(L("Hello, welcome to Slic3r Prusa Edition! TODO: This text."))); - const PresetBundle &bundle = wizard_p()->bundle_vendors; - const auto &vendors = bundle.vendors; - const auto vendor_prusa = std::find(vendors.cbegin(), vendors.cend(), VendorProfile("PrusaResearch")); + // const PresetBundle &bundle = wizard_p()->bundle_vendors; + // const auto &vendors = bundle.vendors; + const auto &vendors = wizard_p()->vendors; + // const auto vendor_prusa = std::find(vendors.cbegin(), vendors.cend(), VendorProfile("PrusaResearch")); + const auto vendor_prusa = vendors.find("PrusaResearch"); if (vendor_prusa != vendors.cend()) { - const auto &models = vendor_prusa->models; + const auto &models = vendor_prusa->second.models; AppConfig &appconfig_vendors = this->wizard_p()->appconfig_vendors; - printer_picker = new PrinterPicker(this, *vendor_prusa, appconfig_vendors); + printer_picker = new PrinterPicker(this, vendor_prusa->second, appconfig_vendors); printer_picker->Bind(EVT_PRINTER_PICK, [this, &appconfig_vendors](const PrinterPickerEvent &evt) { appconfig_vendors.set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); this->on_variant_checked(); @@ -248,14 +249,17 @@ PageVendors::PageVendors(ConfigWizard *parent) : { append_text(_(L("Pick another vendor supported by Slic3r PE:"))); - const PresetBundle &bundle = wizard_p()->bundle_vendors; + // const PresetBundle &bundle = wizard_p()->bundle_vendors; + // const auto &vendors = wizard_p()->vendors; auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); boldfont.SetWeight(wxFONTWEIGHT_BOLD); AppConfig &appconfig_vendors = this->wizard_p()->appconfig_vendors; wxArrayString choices_vendors; - for (const auto &vendor : bundle.vendors) { + // for (const auto &vendor : vendors) { + for (const auto vendor_pair : wizard_p()->vendors) { + const auto &vendor = vendor_pair.second; if (vendor.id == "PrusaResearch") { continue; } auto *picker = new PrinterPicker(this, vendor, appconfig_vendors); @@ -549,9 +553,25 @@ void ConfigWizardIndex::on_paint(wxPaintEvent & evt) void ConfigWizard::priv::load_vendors() { const auto vendor_dir = fs::path(Slic3r::data_dir()) / "vendor"; + const auto rsrc_vendor_dir = fs::path(resources_dir()) / "profiles"; + + // Load vendors from the "vendors" directory in datadir for (fs::directory_iterator it(vendor_dir); it != fs::directory_iterator(); ++it) { if (it->path().extension() == ".ini") { - bundle_vendors.load_configbundle(it->path().string(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); + auto vp = VendorProfile::from_ini(it->path()); + vendors[vp.id] = std::move(vp); + } + } + + // Additionally load up vendors from the application resources directory, but only those not seen in the datadir + for (fs::directory_iterator it(rsrc_vendor_dir); it != fs::directory_iterator(); ++it) { + if (it->path().extension() == ".ini") { + const auto id = it->path().stem().string(); + if (vendors.find(id) == vendors.end()) { + auto vp = VendorProfile::from_ini(it->path()); + vendors_rsrc[vp.id] = it->path(); + vendors[vp.id] = std::move(vp); + } } } @@ -624,6 +644,19 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese const bool is_custom_setup = page_welcome->page_next() == page_firmware; if (! is_custom_setup) { + const auto enabled_vendors = appconfig_vendors.vendors(); + for (const auto &vendor_rsrc : vendors_rsrc) { + const auto vendor = enabled_vendors.find(vendor_rsrc.first); + if (vendor == enabled_vendors.end()) { continue; } + + size_t size_sum = 0; + for (const auto &model : vendor->second) { size_sum += model.second.size(); } + if (size_sum == 0) { continue; } + + // This vendor needs to be installed + PresetBundle::install_vendor_configbundle(vendor_rsrc.second); + } + app_config->set_vendors(appconfig_vendors); app_config->set("version_check", page_update->version_check ? "1" : "0"); app_config->set("preset_update", page_update->preset_update ? "1" : "0"); diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp index 652328aaa..137b276b8 100644 --- a/xs/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -4,6 +4,9 @@ #include "ConfigWizard.hpp" #include +#include +#include +#include #include #include @@ -13,13 +16,16 @@ #include "libslic3r/PrintConfig.hpp" #include "AppConfig.hpp" -#include "PresetBundle.hpp" +#include "Preset.hpp" +// #include "PresetBundle.hpp" #include "BedShapeDialog.hpp" +namespace fs = boost::filesystem; namespace Slic3r { namespace GUI { +// typedef std::unordered_map VendorMap; enum { CONTENT_WIDTH = 500, @@ -168,7 +174,9 @@ struct ConfigWizard::priv { ConfigWizard *q; AppConfig appconfig_vendors; - PresetBundle bundle_vendors; + // PresetBundle bundle_vendors; + std::unordered_map vendors; + std::unordered_map vendors_rsrc; std::unique_ptr custom_config; wxBoxSizer *topsizer = nullptr; diff --git a/xs/src/slic3r/GUI/Preset.cpp b/xs/src/slic3r/GUI/Preset.cpp index 39bfbd398..26f0ff594 100644 --- a/xs/src/slic3r/GUI/Preset.cpp +++ b/xs/src/slic3r/GUI/Preset.cpp @@ -5,6 +5,8 @@ #include "AppConfig.hpp" #include +#include +#include #include #include #include @@ -75,6 +77,11 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem { static const std::string printer_model_key = "printer_model:"; const std::string id = path.stem().string(); + + if (! boost::filesystem::exists(path)) { + throw std::runtime_error((boost::format("Cannot load Vendor Config Bundle `%1%`: File not found: `%2%`.") % id % path).str()); + } + VendorProfile res(id); auto get_or_throw = [&](const ptree &tree, const std::string &key) -> ptree::const_assoc_iterator diff --git a/xs/src/slic3r/GUI/Preset.hpp b/xs/src/slic3r/GUI/Preset.hpp index d6ccfd450..8855bf1e7 100644 --- a/xs/src/slic3r/GUI/Preset.hpp +++ b/xs/src/slic3r/GUI/Preset.hpp @@ -58,7 +58,8 @@ public: }; std::vector models; - VendorProfile(std::string id) : id(std::move(id)), config_version(0, 0, 0) {} + VendorProfile() {} + VendorProfile(std::string id) : id(std::move(id)) {} static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true); static VendorProfile from_ini(const boost::property_tree::ptree &tree, const boost::filesystem::path &path, bool load_all=true); diff --git a/xs/src/slic3r/GUI/PresetBundle.cpp b/xs/src/slic3r/GUI/PresetBundle.cpp index ad27bf8c6..d1d66df60 100644 --- a/xs/src/slic3r/GUI/PresetBundle.cpp +++ b/xs/src/slic3r/GUI/PresetBundle.cpp @@ -701,8 +701,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla const VendorProfile *vendor_profile = nullptr; if (flags & (LOAD_CFGBNDLE_SYSTEM | LOAD_CFGBUNDLE_VENDOR_ONLY)) { - boost::filesystem::path fspath(path); - auto vp = VendorProfile::from_ini(tree, fspath.stem().string()); + auto vp = VendorProfile::from_ini(tree, path); if (vp.num_variants() == 0) return 0; vendor_profile = &(*this->vendors.insert(vp).first); diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp index a16a6d889..28b977321 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.cpp +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -1,21 +1,32 @@ #include "PresetUpdater.hpp" +#include // XXX +#include #include +#include #include #include #include #include #include +#include #include "libslic3r/Utils.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/PresetBundle.hpp" #include "slic3r/Utils/Http.hpp" -#include "slic3r/Utils/Semver.hpp" +#include "slic3r/Config/Version.hpp" +#include "slic3r/Config/Snapshot.hpp" namespace fs = boost::filesystem; +using Slic3r::GUI::Config::Index; +using Slic3r::GUI::Config::Version; +using Slic3r::GUI::Config::Snapshot; +using Slic3r::GUI::Config::SnapshotDB; +// XXX: Prevent incomplete file downloads: download a tmp file, then move +// Delete incomplete ones on startup. namespace Slic3r { @@ -27,26 +38,55 @@ enum { SLIC3R_VERSION_BODY_MAX = 256, }; + +struct Update +{ + fs::path source; + fs::path target; + Version version; + + Update(const fs::path &source, fs::path &&target, const Version &version) : + source(source), + target(std::move(target)), + version(version) + {} + + std::string name() { return source.stem().string(); } +}; + +typedef std::vector Updates; + + struct PresetUpdater::priv { int version_online_event; + AppConfig *app_config; bool version_check; bool preset_update; fs::path cache_path; + fs::path rsrc_path; + fs::path vendor_path; + bool cancel; std::thread thread; - priv(int event); + priv(int event, AppConfig *app_config); void download(const std::set vendors) const; + + void check_install_indices() const; + Updates config_update() const; }; -PresetUpdater::priv::priv(int event) : +PresetUpdater::priv::priv(int event, AppConfig *app_config) : version_online_event(event), + app_config(app_config), version_check(false), preset_update(false), cache_path(fs::path(Slic3r::data_dir()) / "cache"), + rsrc_path(fs::path(resources_dir()) / "profiles"), + vendor_path(fs::path(Slic3r::data_dir()) / "vendor"), cancel(false) {} @@ -91,11 +131,119 @@ void PresetUpdater::priv::download(const std::set vendors) const } } +void PresetUpdater::priv::check_install_indices() const +{ + for (fs::directory_iterator it(rsrc_path); it != fs::directory_iterator(); ++it) { + const auto &path = it->path(); + if (path.extension() == ".idx") { + const auto path_in_cache = cache_path / path.filename(); + + // TODO: compare versions + if (! fs::exists(path_in_cache)) { + fs::copy_file(path, path_in_cache, fs::copy_option::overwrite_if_exists); + } + } + } +} + +Updates PresetUpdater::priv::config_update() const +{ + priv::check_install_indices(); + const auto index_db = Index::load_db(); // TODO: Keep in Snapshots singleton? + + Updates updates; + + for (const auto idx : index_db) { + const auto bundle_path = vendor_path / (idx.vendor() + ".ini"); + + // If the bundle doesn't exist at all, update from resources + // if (! fs::exists(bundle_path)) { + // auto path_in_rsrc = rsrc_path / (idx.vendor() + ".ini"); + + // // Otherwise load it and check for chached updates + // const auto rsrc_vp = VendorProfile::from_ini(path_in_rsrc, false); + + // const auto rsrc_ver = idx.find(rsrc_vp.config_version); + // if (rsrc_ver == idx.end()) { + // // TODO: throw + // } + + // if (fs::exists(path_in_rsrc)) { + // updates.emplace_back(bundle_path, std::move(path_in_rsrc), *rsrc_ver); + // } else { + // // XXX: ??? + // } + + // continue; + // } + + if (! fs::exists(bundle_path)) { + continue; + } + + // Perform a basic load and check the version + const auto vp = VendorProfile::from_ini(bundle_path, false); + + const auto ver_current = idx.find(vp.config_version); + if (ver_current == idx.end()) { + // TODO: throw + } + + const auto recommended = idx.recommended(); + if (recommended == idx.end()) { + // TODO: throw + } + + if (! ver_current->is_current_slic3r_supported()) { + + // TODO: Downgrade situation + + } else if (recommended->config_version > ver_current->config_version) { + // Config bundle update situation + + auto path_in_cache = cache_path / (idx.vendor() + ".ini"); + const auto cached_vp = VendorProfile::from_ini(path_in_cache, false); + if (cached_vp.config_version == recommended->config_version) { + updates.emplace_back(bundle_path, std::move(path_in_cache), *ver_current); + } else { + // XXX: ??? + } + } + } + + // Check for bundles that don't have an index + // for (fs::directory_iterator it(rsrc_path); it != fs::directory_iterator(); ++it) { + // if (it->path().extension() == ".ini") { + // const auto &path = it->path(); + // const auto vendor_id = path.stem().string(); + + // const auto needle = std::find_if(index_db.begin(), index_db.end(), [&vendor_id](const Index &idx) { + // return idx.vendor() == vendor_id; + // }); + // if (needle != index_db.end()) { + // continue; + // } + + // auto vp = VendorProfile::from_ini(path, false); + // auto path_in_data = vendor_path / path.filename(); + + // if (! fs::exists(path_in_data)) { + // Version version; + // version.config_version = vp.config_version; + // updates.emplace_back(path, std::move(path_in_data), version); + // } + // } + // } + + return updates; +} + + PresetUpdater::PresetUpdater(int version_online_event, AppConfig *app_config) : - p(new priv(version_online_event)) + p(new priv(version_online_event, app_config)) { p->preset_update = app_config->get("preset_update") == "1"; - // preset_update implies version_check: + // preset_update implies version_check: // XXX: not any more p->version_check = p->preset_update || app_config->get("version_check") == "1"; } @@ -123,19 +271,49 @@ void PresetUpdater::download(PresetBundle *preset_bundle) })); } -void PresetUpdater::init_vendors() +void PresetUpdater::config_update() { - const auto vendors_rources = fs::path(resources_dir()) / "profiles"; - const auto vendors_data = fs::path(Slic3r::data_dir()) / "vendor"; + const auto updates = p->config_update(); + if (updates.size() > 0) { + const auto msg = _(L("Configuration update is available. Would you like to install it?")); - for (fs::directory_iterator it(vendors_rources); it != fs::directory_iterator(); ++it) { - if (it->path().extension() == ".ini") { - auto vp = VendorProfile::from_ini(it->path(), false); - const auto path_in_data = vendors_data / it->path().filename(); + auto ext_msg = _(L( + "Note that a full configuration snapshot will be created first. It can then be restored at any time " + "should there be a problem with the new version.\n\n" + "Updated configuration bundles:\n" + )); - if (! fs::exists(path_in_data) || VendorProfile::from_ini(path_in_data, false).config_version < vp.config_version) { - // FIXME: update vendor bundle properly when snapshotting is ready - PresetBundle::install_vendor_configbundle(it->path()); + for (const auto &update : updates) { + ext_msg += update.target.stem().string() + " " + update.version.config_version.to_string(); + if (! update.version.comment.empty()) { + ext_msg += std::string(" (") + update.version.comment + ")"; + } + ext_msg += "\n"; + } + + wxMessageDialog dlg(NULL, msg, _(L("Configuration update")), wxYES_NO|wxCENTRE); + dlg.SetExtendedMessage(ext_msg); + const auto res = dlg.ShowModal(); + std::cerr << "After modal" << std::endl; + if (res == wxID_YES) { + // User gave clearance, updates are go + + // TODO: Comment? + SnapshotDB::singleton().take_snapshot(*p->app_config, Snapshot::SNAPSHOT_UPGRADE, ""); + + for (const auto &update : updates) { + fs::copy_file(update.source, update.target, fs::copy_option::overwrite_if_exists); + + PresetBundle bundle; + bundle.load_configbundle(update.target.string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); + + auto preset_remover = [](const Preset &preset) { + fs::remove(preset.file); + }; + + for (const auto &preset : bundle.prints) { preset_remover(preset); } + for (const auto &preset : bundle.filaments) { preset_remover(preset); } + for (const auto &preset : bundle.printers) { preset_remover(preset); } } } } diff --git a/xs/src/slic3r/Utils/PresetUpdater.hpp b/xs/src/slic3r/Utils/PresetUpdater.hpp index b10c61784..8fd6e4528 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.hpp +++ b/xs/src/slic3r/Utils/PresetUpdater.hpp @@ -21,7 +21,7 @@ public: void download(PresetBundle *preset_bundle); // XXX - static void init_vendors(); + void config_update(); private: struct priv; std::unique_ptr p; diff --git a/xs/src/slic3r/Utils/Semver.hpp b/xs/src/slic3r/Utils/Semver.hpp index a1f4a92e8..2c27ce982 100644 --- a/xs/src/slic3r/Utils/Semver.hpp +++ b/xs/src/slic3r/Utils/Semver.hpp @@ -11,7 +11,6 @@ namespace Slic3r { -// FIXME:: operators=: leak, return class Semver { @@ -20,6 +19,8 @@ public: struct Minor { const int i; Minor(int i) : i(i) {} }; struct Patch { const int i; Patch(int i) : i(i) {} }; + Semver() : ver(semver_zero()) {} + Semver(int major, int minor, int patch, boost::optional metadata = boost::none, boost::optional prerelease = boost::none) diff --git a/xs/xsp/Utils_PresetUpdater.xsp b/xs/xsp/Utils_PresetUpdater.xsp index 1cb9f1c39..4c1a637e4 100644 --- a/xs/xsp/Utils_PresetUpdater.xsp +++ b/xs/xsp/Utils_PresetUpdater.xsp @@ -8,5 +8,5 @@ %name{Slic3r::PresetUpdater} class PresetUpdater { PresetUpdater(int version_online_event, AppConfig *app_config); void download(PresetBundle* preset_bundle); - static void init_vendors(); + void config_update(); }; From 82890ec815080898c601222b8e370d4475a3af2e Mon Sep 17 00:00:00 2001 From: bubnikv Date: Fri, 13 Apr 2018 14:49:33 +0200 Subject: [PATCH 20/38] Removed some obsolete Perl binding. Added Version Index "version" method. Implemented automatic selection of default_print_profile and default_filament_profile, when the print / filament profiles are not compatible with the selected printer profile. Fixed selection of a printer profile, if the currently selected printer profile becomes invisible. --- lib/Slic3r/GUI/ConfigWizard.pm | 458 --------------------- resources/profiles/PrusaResearch.idx | 3 + xs/src/perlglue.cpp | 1 - xs/src/semver/semver.c | 7 +- xs/src/slic3r/Config/Version.cpp | 9 + xs/src/slic3r/Config/Version.hpp | 3 + xs/src/slic3r/GUI/ConfigWizard.cpp | 11 +- xs/src/slic3r/GUI/ConfigWizard.hpp | 2 +- xs/src/slic3r/GUI/ConfigWizard_private.hpp | 2 +- xs/src/slic3r/GUI/GUI.cpp | 2 +- xs/src/slic3r/GUI/Preset.cpp | 41 +- xs/src/slic3r/GUI/Preset.hpp | 45 +- xs/src/slic3r/GUI/PresetBundle.cpp | 59 ++- xs/src/slic3r/GUI/Tab.cpp | 5 +- xs/src/slic3r/GUI/Tab.hpp | 2 +- xs/xsp/GUI_Preset.xsp | 42 -- xs/xsp/my.map | 2 - xs/xsp/typemap.xspt | 2 - 18 files changed, 146 insertions(+), 550 deletions(-) delete mode 100644 lib/Slic3r/GUI/ConfigWizard.pm diff --git a/lib/Slic3r/GUI/ConfigWizard.pm b/lib/Slic3r/GUI/ConfigWizard.pm deleted file mode 100644 index a32d345ed..000000000 --- a/lib/Slic3r/GUI/ConfigWizard.pm +++ /dev/null @@ -1,458 +0,0 @@ -# The config wizard is executed when the Slic3r is first started. -# The wizard helps the user to specify the 3D printer properties. - -package Slic3r::GUI::ConfigWizard; -use strict; -use warnings; -use utf8; - -use Wx; -use base 'Wx::Wizard'; - -# adhere to various human interface guidelines -our $wizard = 'Wizard'; -$wizard = 'Assistant' if &Wx::wxMAC || &Wx::wxGTK; - -sub new { - my ($class, $parent, $presets, $fresh_start) = @_; - my $self = $class->SUPER::new($parent, -1, "Configuration $wizard"); - - # initialize an empty repository - $self->{config} = Slic3r::Config->new; - - my $welcome_page = Slic3r::GUI::ConfigWizard::Page::Welcome->new($self, $fresh_start); - $self->add_page($welcome_page); - $self->add_page(Slic3r::GUI::ConfigWizard::Page::Firmware->new($self)); - $self->add_page(Slic3r::GUI::ConfigWizard::Page::Bed->new($self)); - $self->add_page(Slic3r::GUI::ConfigWizard::Page::Nozzle->new($self)); - $self->add_page(Slic3r::GUI::ConfigWizard::Page::Filament->new($self)); - $self->add_page(Slic3r::GUI::ConfigWizard::Page::Temperature->new($self)); - $self->add_page(Slic3r::GUI::ConfigWizard::Page::BedTemperature->new($self)); - $self->add_page(Slic3r::GUI::ConfigWizard::Page::Finished->new($self)); - - $_->build_index for @{$self->{pages}}; - - $welcome_page->set_selection_presets([@{$presets}, 'Other']); - - return $self; -} - -sub add_page { - my ($self, $page) = @_; - - my $n = push @{$self->{pages}}, $page; - # add first page to the page area sizer - $self->GetPageAreaSizer->Add($page) if $n == 1; - # link pages - $self->{pages}[$n-2]->set_next_page($page) if $n >= 2; - $page->set_previous_page($self->{pages}[$n-2]) if $n >= 2; -} - -sub run { - my ($self) = @_; - my $result; - if (Wx::Wizard::RunWizard($self, $self->{pages}[0])) { - my $preset_name = $self->{pages}[0]->{preset_name}; - $result = { - preset_name => $preset_name, - reset_user_profile => $self->{pages}[0]->{reset_user_profile} - }; - if ($preset_name eq 'Other') { - # it would be cleaner to have these defined inside each page class, - # in some event getting called before leaving the page - # set first_layer_height + layer_height based on nozzle_diameter - my $nozzle = $self->{config}->nozzle_diameter; - $self->{config}->set('first_layer_height', $nozzle->[0]); - $self->{config}->set('layer_height', $nozzle->[0] - 0.1); - - # set first_layer_temperature to temperature + 5 - $self->{config}->set('first_layer_temperature', [$self->{config}->temperature->[0] + 5]); - - # set first_layer_bed_temperature to temperature + 5 - $self->{config}->set('first_layer_bed_temperature', - [ ($self->{config}->bed_temperature->[0] > 0) ? ($self->{config}->bed_temperature->[0] + 5) : 0 ]); - $result->{config} = $self->{config}; - } - } - $self->Destroy; - return $result; -} - -package Slic3r::GUI::ConfigWizard::Index; -use Wx qw(:bitmap :dc :font :misc :sizer :systemsettings :window); -use Wx::Event qw(EVT_ERASE_BACKGROUND EVT_PAINT); -use base 'Wx::Panel'; - -sub new { - my $class = shift; - my ($parent, $title) = @_; - my $self = $class->SUPER::new($parent); - - push @{$self->{titles}}, $title; - $self->{own_index} = 0; - - $self->{bullets}->{before} = Wx::Bitmap->new(Slic3r::var("bullet_black.png"), wxBITMAP_TYPE_PNG); - $self->{bullets}->{own} = Wx::Bitmap->new(Slic3r::var("bullet_blue.png"), wxBITMAP_TYPE_PNG); - $self->{bullets}->{after} = Wx::Bitmap->new(Slic3r::var("bullet_white.png"), wxBITMAP_TYPE_PNG); - - $self->{background} = Wx::Bitmap->new(Slic3r::var("Slic3r_192px_transparent.png"), wxBITMAP_TYPE_PNG); - $self->SetMinSize(Wx::Size->new($self->{background}->GetWidth, $self->{background}->GetHeight)); - - EVT_PAINT($self, \&repaint); - - return $self; -} - -sub repaint { - my ($self, $event) = @_; - my $size = $self->GetClientSize; - my $gap = 5; - - my $dc = Wx::PaintDC->new($self); - $dc->SetBackgroundMode(wxTRANSPARENT); - $dc->SetFont($self->GetFont); - $dc->SetTextForeground($self->GetForegroundColour); - - my $background_h = $self->{background}->GetHeight; - my $background_w = $self->{background}->GetWidth; - $dc->DrawBitmap($self->{background}, ($size->GetWidth - $background_w) / 2, ($size->GetHeight - $background_h) / 2, 1); - - my $label_h = $self->{bullets}->{own}->GetHeight; - $label_h = $dc->GetCharHeight if $dc->GetCharHeight > $label_h; - my $label_w = $size->GetWidth; - - my $i = 0; - foreach (@{$self->{titles}}) { - my $bullet = $self->{bullets}->{own}; - $bullet = $self->{bullets}->{before} if $i < $self->{own_index}; - $bullet = $self->{bullets}->{after} if $i > $self->{own_index}; - - $dc->SetTextForeground(Wx::Colour->new(128, 128, 128)) if $i > $self->{own_index}; - $dc->DrawLabel($_, $bullet, Wx::Rect->new(0, $i * ($label_h + $gap), $label_w, $label_h)); - # Only show the first bullet if this is the only wizard page to be displayed. - last if $i == 0 && $self->{just_welcome}; - $i++; - } - - $event->Skip; -} - -sub prepend_title { - my $self = shift; - my ($title) = @_; - - unshift @{$self->{titles}}, $title; - $self->{own_index}++; - $self->Refresh; -} - -sub append_title { - my $self = shift; - my ($title) = @_; - - push @{$self->{titles}}, $title; - $self->Refresh; -} - -package Slic3r::GUI::ConfigWizard::Page; -use Wx qw(:font :misc :sizer :staticline :systemsettings); -use base 'Wx::WizardPage'; - -sub new { - my $class = shift; - my ($parent, $title, $short_title) = @_; - my $self = $class->SUPER::new($parent); - - my $sizer = Wx::FlexGridSizer->new(0, 2, 10, 10); - $sizer->AddGrowableCol(1, 1); - $sizer->AddGrowableRow(1, 1); - $sizer->AddStretchSpacer(0); - $self->SetSizer($sizer); - - # title - my $text = Wx::StaticText->new($self, -1, $title, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - my $bold_font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - $bold_font->SetWeight(wxFONTWEIGHT_BOLD); - $bold_font->SetPointSize(14); - $text->SetFont($bold_font); - $sizer->Add($text, 0, wxALIGN_LEFT, 0); - - # index - $self->{short_title} = $short_title ? $short_title : $title; - $self->{index} = Slic3r::GUI::ConfigWizard::Index->new($self, $self->{short_title}); - $sizer->Add($self->{index}, 1, wxEXPAND | wxTOP | wxRIGHT, 10); - - # contents - $self->{width} = 430; - $self->{vsizer} = Wx::BoxSizer->new(wxVERTICAL); - $sizer->Add($self->{vsizer}, 1); - - return $self; -} - -sub append_text { - my $self = shift; - my ($text) = @_; - - my $para = Wx::StaticText->new($self, -1, $text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - $para->Wrap($self->{width}); - $para->SetMinSize([$self->{width}, -1]); - $self->{vsizer}->Add($para, 0, wxALIGN_LEFT | wxTOP | wxBOTTOM, 10); -} - -sub append_option { - my $self = shift; - my ($full_key) = @_; - - # populate repository with the factory default - my ($opt_key, $opt_index) = split /#/, $full_key, 2; - $self->config->apply(Slic3r::Config::new_from_defaults_keys([$opt_key])); - - # draw the control - my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new( - parent => $self, - title => '', - config => $self->config, - full_labels => 1, - ); - $optgroup->append_single_option_line($opt_key, $opt_index); - $self->{vsizer}->Add($optgroup->sizer, 0, wxEXPAND | wxTOP | wxBOTTOM, 10); -} - -sub append_panel { - my ($self, $panel) = @_; - $self->{vsizer}->Add($panel, 0, wxEXPAND | wxTOP | wxBOTTOM, 10); -} - -sub set_previous_page { - my $self = shift; - my ($previous_page) = @_; - $self->{previous_page} = $previous_page; -} - -sub GetPrev { - my $self = shift; - return $self->{previous_page}; -} - -sub set_next_page { - my $self = shift; - my ($next_page) = @_; - $self->{next_page} = $next_page; -} - -sub GetNext { - my $self = shift; - return $self->{next_page}; -} - -sub get_short_title { - my $self = shift; - return $self->{short_title}; -} - -sub build_index { - my $self = shift; - - my $page = $self; - $self->{index}->prepend_title($page->get_short_title) while ($page = $page->GetPrev); - $page = $self; - $self->{index}->append_title($page->get_short_title) while ($page = $page->GetNext); -} - -sub config { - my ($self) = @_; - return $self->GetParent->{config}; -} - -package Slic3r::GUI::ConfigWizard::Page::Welcome; -use base 'Slic3r::GUI::ConfigWizard::Page'; -use Wx qw(:misc :sizer wxID_FORWARD); -use Wx::Event qw(EVT_ACTIVATE EVT_CHOICE EVT_CHECKBOX); - -sub new { - my ($class, $parent, $fresh_start) = @_; - my $self = $class->SUPER::new($parent, "Welcome to the Slic3r Configuration $wizard", 'Welcome'); - $self->{full_wizard_workflow} = 1; - $self->{reset_user_profile} = 0; - - # Test for the existence of the old config path. - my $message_has_legacy; - { - my $datadir = Slic3r::data_dir; - if ($datadir =~ /Slic3rPE/) { - # Check for existence of the legacy Slic3r directory. - my $datadir_legacy = substr $datadir, 0, -2; - my $dir_enc = Slic3r::encode_path($datadir_legacy); - if (-e $dir_enc && -d $dir_enc && - -e ($dir_enc . '/print') && -d ($dir_enc . '/print') && - -e ($dir_enc . '/filament') && -d ($dir_enc . '/filament') && - -e ($dir_enc . '/printer') && -d ($dir_enc . '/printer') && - -e ($dir_enc . '/slic3r.ini')) { - $message_has_legacy = "Starting with Slic3r 1.38.4, the user profile directory has been renamed to $datadir. You may consider closing Slic3r and renaming $datadir_legacy to $datadir."; - } - } - } - - $self->append_text('Hello, welcome to Slic3r Prusa Edition! This '.lc($wizard).' helps you with the initial configuration; just a few settings and you will be ready to print.'); - $self->append_text('Please select your printer vendor and printer type. If your printer is not listed, you may try your luck and select a similar one. If you select "Other", this ' . lc($wizard) . ' will let you set the basic 3D printer parameters.'); - $self->append_text($message_has_legacy) if defined $message_has_legacy; - # To import an existing configuration instead, cancel this '.lc($wizard).' and use the Open Config menu item found in the File menu.'); - $self->append_text('If you received a configuration file or a config bundle from your 3D printer vendor, cancel this '.lc($wizard).' and use the "File->Load Config" or "File->Load Config Bundle" menu.'); - - $self->{choice} = my $choice = Wx::Choice->new($self, -1, wxDefaultPosition, wxDefaultSize, []); - $self->{vsizer}->Add($choice, 0, wxEXPAND | wxTOP | wxBOTTOM, 10); - if (! $fresh_start) { - $self->{reset_checkbox} = Wx::CheckBox->new($self, -1, "Reset user profile, install from scratch"); - $self->{vsizer}->Add($self->{reset_checkbox}, 0, wxEXPAND | wxTOP | wxBOTTOM, 10); - } - - EVT_CHOICE($parent, $choice, sub { - my $sel = $self->{choice}->GetStringSelection; - $self->{preset_name} = $sel; - $self->set_full_wizard_workflow(($sel eq 'Other') || ($sel eq '')); - }); - - if (! $fresh_start) { - EVT_CHECKBOX($self, $self->{reset_checkbox}, sub { - $self->{reset_user_profile} = $self->{reset_checkbox}->GetValue(); - }); - } - - EVT_ACTIVATE($parent, sub { - $self->set_full_wizard_workflow($self->{preset_name} eq 'Other'); - }); - - return $self; -} - -sub set_full_wizard_workflow { - my ($self, $full_workflow) = @_; - $self->{full_wizard_workflow} = $full_workflow; - $self->{index}->{just_welcome} = !$full_workflow; - $self->{index}->Refresh; - my $next_button = $self->GetParent->FindWindow(wxID_FORWARD); - $next_button->SetLabel($full_workflow ? "&Next >" : "&Finish"); -} - -# Set the preset names, select the first item. -sub set_selection_presets { - my ($self, $names) = @_; - $self->{choice}->Append($names); - $self->{choice}->SetSelection(0); - $self->{preset_name} = $names->[0]; -} - -sub GetNext { - my $self = shift; - return $self->{full_wizard_workflow} ? $self->{next_page} : undef; -} - -package Slic3r::GUI::ConfigWizard::Page::Firmware; -use base 'Slic3r::GUI::ConfigWizard::Page'; - -sub new { - my $class = shift; - my ($parent) = @_; - my $self = $class->SUPER::new($parent, 'Firmware Type'); - - $self->append_text('Choose the type of firmware used by your printer, then click Next.'); - $self->append_option('gcode_flavor'); - - return $self; -} - -package Slic3r::GUI::ConfigWizard::Page::Bed; -use base 'Slic3r::GUI::ConfigWizard::Page'; - -sub new { - my $class = shift; - my ($parent) = @_; - my $self = $class->SUPER::new($parent, 'Bed Size'); - - $self->append_text('Set the shape of your printer\'s bed, then click Next.'); - - $self->config->apply(Slic3r::Config::new_from_defaults_keys(['bed_shape'])); - $self->{bed_shape_panel} = my $panel = Slic3r::GUI::BedShapePanel->new($self, $self->config->bed_shape); - $self->{bed_shape_panel}->on_change(sub { - $self->config->set('bed_shape', $self->{bed_shape_panel}->GetValue); - }); - $self->append_panel($self->{bed_shape_panel}); - return $self; -} - -package Slic3r::GUI::ConfigWizard::Page::Nozzle; -use base 'Slic3r::GUI::ConfigWizard::Page'; - -sub new { - my $class = shift; - my ($parent) = @_; - my $self = $class->SUPER::new($parent, 'Nozzle Diameter'); - - $self->append_text('Enter the diameter of your printer\'s hot end nozzle, then click Next.'); - $self->append_option('nozzle_diameter#0'); - - return $self; -} - -package Slic3r::GUI::ConfigWizard::Page::Filament; -use base 'Slic3r::GUI::ConfigWizard::Page'; - -sub new { - my $class = shift; - my ($parent) = @_; - my $self = $class->SUPER::new($parent, 'Filament Diameter'); - - $self->append_text('Enter the diameter of your filament, then click Next.'); - $self->append_text('Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average.'); - $self->append_option('filament_diameter#0'); - - return $self; -} - -package Slic3r::GUI::ConfigWizard::Page::Temperature; -use base 'Slic3r::GUI::ConfigWizard::Page'; - -sub new { - my $class = shift; - my ($parent) = @_; - my $self = $class->SUPER::new($parent, 'Extrusion Temperature'); - - $self->append_text('Enter the temperature needed for extruding your filament, then click Next.'); - $self->append_text('A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS.'); - $self->append_option('temperature#0'); - - return $self; -} - -package Slic3r::GUI::ConfigWizard::Page::BedTemperature; -use base 'Slic3r::GUI::ConfigWizard::Page'; - -sub new { - my $class = shift; - my ($parent) = @_; - my $self = $class->SUPER::new($parent, 'Bed Temperature'); - - $self->append_text('Enter the bed temperature needed for getting your filament to stick to your heated bed, then click Next.'); - $self->append_text('A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed.'); - $self->append_option('bed_temperature#0'); - - return $self; -} - -package Slic3r::GUI::ConfigWizard::Page::Finished; -use base 'Slic3r::GUI::ConfigWizard::Page'; - -sub new { - my $class = shift; - my ($parent) = @_; - my $self = $class->SUPER::new($parent, 'Congratulations!', 'Finish'); - - $self->append_text("You have successfully completed the Slic3r Configuration $wizard. " . - 'Slic3r is now configured for your printer and filament.'); - $self->append_text('To close this '.lc($wizard).' and apply the newly created configuration, click Finish.'); - - return $self; -} - -1; diff --git a/resources/profiles/PrusaResearch.idx b/resources/profiles/PrusaResearch.idx index 28f17f10a..3bc2aeffd 100644 --- a/resources/profiles/PrusaResearch.idx +++ b/resources/profiles/PrusaResearch.idx @@ -1,6 +1,9 @@ # This is an example configuration version index. # The index contains version numbers min_slic3r_version =1.39.0 +max_slic3r_version= 1.39.5 +1.1.1 +1.1.0 0.2.0-alpha "some test comment" max_slic3r_version= 1.39.4 0.1.0 another test comment diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp index 9706ced2c..205eec218 100644 --- a/xs/src/perlglue.cpp +++ b/xs/src/perlglue.cpp @@ -62,7 +62,6 @@ REGISTER_CLASS(GLVolumeCollection, "GUI::_3DScene::GLVolume::Collection"); REGISTER_CLASS(Preset, "GUI::Preset"); REGISTER_CLASS(PresetCollection, "GUI::PresetCollection"); REGISTER_CLASS(PresetBundle, "GUI::PresetBundle"); -REGISTER_CLASS(PresetHints, "GUI::PresetHints"); REGISTER_CLASS(TabIface, "GUI::Tab"); REGISTER_CLASS(PresetUpdater, "PresetUpdater"); REGISTER_CLASS(OctoPrint, "OctoPrint"); diff --git a/xs/src/semver/semver.c b/xs/src/semver/semver.c index 3e4a30e3a..68d18af09 100644 --- a/xs/src/semver/semver.c +++ b/xs/src/semver/semver.c @@ -175,6 +175,9 @@ semver_parse_version (const char *str, semver_t *ver) { slice = (char *) str; index = 0; + // non mandatory + ver->patch = 0; + while (slice != NULL && index++ < 4) { next = strchr(slice, DELIMITER[0]); if (next == NULL) @@ -200,7 +203,8 @@ semver_parse_version (const char *str, semver_t *ver) { slice = next + 1; } - return (index == 3) ? 0 : -1; + // Major and minor versions are mandatory, patch version is not mandatory. + return (index == 2 || index == 3) ? 0 : -1; } static int @@ -632,4 +636,5 @@ semver_copy(const semver_t *ver) { if (ver->prerelease != NULL) { res.prerelease = strdup(ver->prerelease); } + return res; } \ No newline at end of file diff --git a/xs/src/slic3r/Config/Version.cpp b/xs/src/slic3r/Config/Version.cpp index 95b3caf1a..a80b0b6e9 100644 --- a/xs/src/slic3r/Config/Version.cpp +++ b/xs/src/slic3r/Config/Version.cpp @@ -141,6 +141,15 @@ size_t Index::load(const boost::filesystem::path &path) return m_configs.size(); } +Semver Index::version() const +{ + Semver ver = Semver::zero(); + for (const Version &cv : m_configs) + if (cv.config_version >= ver) + ver = cv.config_version; + return ver; +} + Index::const_iterator Index::find(const Semver &ver) { Version key; diff --git a/xs/src/slic3r/Config/Version.hpp b/xs/src/slic3r/Config/Version.hpp index 43512e82f..c4243ca75 100644 --- a/xs/src/slic3r/Config/Version.hpp +++ b/xs/src/slic3r/Config/Version.hpp @@ -59,6 +59,9 @@ public: size_t load(const boost::filesystem::path &path); const std::string& vendor() const { return m_vendor; } + // Returns version of the index as the highest version of all the configs. + // If there is no config, Semver::zero() is returned. + Semver version() const; const_iterator begin() const { return m_configs.begin(); } const_iterator end() const { return m_configs.end(); } diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index 52a896704..30d1bf4e3 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -619,7 +619,7 @@ void ConfigWizard::priv::on_custom_setup() set_page(page_firmware); } -void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle) +void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, bool fresh_start) { const bool is_custom_setup = page_welcome->page_next() == page_firmware; @@ -627,7 +627,8 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese app_config->set_vendors(appconfig_vendors); app_config->set("version_check", page_update->version_check ? "1" : "0"); app_config->set("preset_update", page_update->preset_update ? "1" : "0"); - app_config->reset_selections(); // XXX: only on "fresh start"? + if (fresh_start) + app_config->reset_selections(); preset_bundle->load_presets(*app_config); } else { for (ConfigWizardPage *page = page_firmware; page != nullptr; page = page->page_next()) { @@ -635,6 +636,8 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese } preset_bundle->load_config("My Settings", *custom_config); } + // Update the selections from the compatibilty. + preset_bundle->export_selections(*app_config); } // Public @@ -698,11 +701,11 @@ ConfigWizard::ConfigWizard(wxWindow *parent) : ConfigWizard::~ConfigWizard() {} -bool ConfigWizard::run(wxWindow *parent, PresetBundle *preset_bundle) +bool ConfigWizard::run(wxWindow *parent, PresetBundle *preset_bundle, bool fresh_start) { ConfigWizard wizard(parent); if (wizard.ShowModal() == wxID_OK) { - wizard.p->apply_config(GUI::get_app_config(), preset_bundle); + wizard.p->apply_config(GUI::get_app_config(), preset_bundle, fresh_start); return true; } else { return false; diff --git a/xs/src/slic3r/GUI/ConfigWizard.hpp b/xs/src/slic3r/GUI/ConfigWizard.hpp index 40ecf09a1..4e791e279 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard.hpp @@ -22,7 +22,7 @@ public: ConfigWizard &operator=(const ConfigWizard &) = delete; ~ConfigWizard(); - static bool run(wxWindow *parent, PresetBundle *preset_bundle); + static bool run(wxWindow *parent, PresetBundle *preset_bundle, bool fresh_start); private: struct priv; std::unique_ptr p; diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp index 652328aaa..6881f6f55 100644 --- a/xs/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -202,7 +202,7 @@ struct ConfigWizard::priv void on_other_vendors(); void on_custom_setup(); - void apply_config(AppConfig *app_config, PresetBundle *preset_bundle); + void apply_config(AppConfig *app_config, PresetBundle *preset_bundle, bool fresh_start); }; diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index d70b47840..036e08993 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -452,7 +452,7 @@ bool config_wizard(bool fresh_start) return false; // TODO: Offer "reset user profile" ??? - if (! ConfigWizard::run(g_wxMainFrame, g_PresetBundle)) + if (! ConfigWizard::run(g_wxMainFrame, g_PresetBundle, fresh_start)) return false; // Load the currently selected preset into the GUI, update the preset selection box. diff --git a/xs/src/slic3r/GUI/Preset.cpp b/xs/src/slic3r/GUI/Preset.cpp index 39bfbd398..853c229ff 100644 --- a/xs/src/slic3r/GUI/Preset.cpp +++ b/xs/src/slic3r/GUI/Preset.cpp @@ -521,18 +521,6 @@ size_t PresetCollection::first_visible_idx() const return idx; } -// Return index of the first compatible preset. Certainly at least the '- default -' preset shall be compatible. -size_t PresetCollection::first_compatible_idx() const -{ - size_t idx = m_default_suppressed ? 1 : 0; - for (; idx < this->m_presets.size(); ++ idx) - if (m_presets[idx].is_compatible) - break; - if (idx == this->m_presets.size()) - idx = 0; - return idx; -} - void PresetCollection::set_default_suppressed(bool default_suppressed) { if (m_default_suppressed != default_suppressed) { @@ -541,7 +529,7 @@ void PresetCollection::set_default_suppressed(bool default_suppressed) } } -void PresetCollection::update_compatible_with_printer(const Preset &active_printer, bool select_other_if_incompatible) +size_t PresetCollection::update_compatible_with_printer_internal(const Preset &active_printer, bool unselect_if_incompatible) { DynamicPrintConfig config; config.set_key_value("printer_preset", new ConfigOptionString(active_printer.name)); @@ -552,14 +540,12 @@ void PresetCollection::update_compatible_with_printer(const Preset &active_print Preset &preset_selected = m_presets[idx_preset]; Preset &preset_edited = selected ? m_edited_preset : preset_selected; if (! preset_edited.update_compatible_with_printer(active_printer, &config) && - selected && select_other_if_incompatible) + selected && unselect_if_incompatible) m_idx_selected = (size_t)-1; if (selected) preset_selected.is_compatible = preset_edited.is_compatible; } - if (m_idx_selected == (size_t)-1) - // Find some other compatible preset, or the "-- default --" preset. - this->select_preset(first_compatible_idx()); + return m_idx_selected; } // Save the preset under a new name. If the name is different from the old one, @@ -689,8 +675,8 @@ bool PresetCollection::select_preset_by_name(const std::string &name_w_suffix, b // 1) Try to find the preset by its name. auto it = this->find_preset_internal(name); size_t idx = 0; - if (it != m_presets.end() && it->name == name) - // Preset found by its name. + if (it != m_presets.end() && it->name == name && it->is_visible) + // Preset found by its name and it is visible. idx = it - m_presets.begin(); else { // Find the first visible preset. @@ -711,6 +697,23 @@ bool PresetCollection::select_preset_by_name(const std::string &name_w_suffix, b return false; } +bool PresetCollection::select_preset_by_name_strict(const std::string &name) +{ + // 1) Try to find the preset by its name. + auto it = this->find_preset_internal(name); + size_t idx = (size_t)-1; + if (it != m_presets.end() && it->name == name && it->is_visible) + // Preset found by its name. + idx = it - m_presets.begin(); + // 2) Select the new preset. + if (idx != (size_t)-1) { + this->select_preset(idx); + return true; + } + m_idx_selected = idx; + return false; +} + std::string PresetCollection::name() const { switch (this->type()) { diff --git a/xs/src/slic3r/GUI/Preset.hpp b/xs/src/slic3r/GUI/Preset.hpp index d6ccfd450..08caf4d4e 100644 --- a/xs/src/slic3r/GUI/Preset.hpp +++ b/xs/src/slic3r/GUI/Preset.hpp @@ -18,6 +18,7 @@ class wxItemContainer; namespace Slic3r { class AppConfig; +class PresetBundle; enum ConfigFileType { @@ -243,19 +244,49 @@ public: { return const_cast(this)->find_preset(name, first_visible_if_not_found); } size_t first_visible_idx() const; - size_t first_compatible_idx() const; + // Return index of the first compatible preset. Certainly at least the '- default -' preset shall be compatible. + // If one of the prefered_alternates is compatible, select it. + template + size_t first_compatible_idx(PreferedCondition prefered_condition) const + { + size_t i = m_default_suppressed ? 1 : 0; + size_t n = this->m_presets.size(); + size_t i_compatible = n; + for (; i < n; ++ i) + if (m_presets[i].is_compatible) { + if (prefered_condition(m_presets[i].name)) + return i; + if (i_compatible == n) + // Store the first compatible profile into i_compatible. + i_compatible = i; + } + return (i_compatible == n) ? 0 : i_compatible; + } + // Return index of the first compatible preset. Certainly at least the '- default -' preset shall be compatible. + size_t first_compatible_idx() const { return this->first_compatible_idx([](const std::string&){return true;}); } + // Return index of the first visible preset. Certainly at least the '- default -' preset shall be visible. // Return the first visible preset. Certainly at least the '- default -' preset shall be visible. Preset& first_visible() { return this->preset(this->first_visible_idx()); } const Preset& first_visible() const { return this->preset(this->first_visible_idx()); } Preset& first_compatible() { return this->preset(this->first_compatible_idx()); } + template + Preset& first_compatible(PreferedCondition prefered_condition) { return this->preset(this->first_compatible_idx(prefered_condition)); } const Preset& first_compatible() const { return this->preset(this->first_compatible_idx()); } // Return number of presets including the "- default -" preset. size_t size() const { return this->m_presets.size(); } // For Print / Filament presets, disable those, which are not compatible with the printer. - void update_compatible_with_printer(const Preset &active_printer, bool select_other_if_incompatible); + template + void update_compatible_with_printer(const Preset &active_printer, bool select_other_if_incompatible, PreferedCondition prefered_condition) + { + if (this->update_compatible_with_printer_internal(active_printer, select_other_if_incompatible) == (size_t)-1) + // Find some other compatible preset, or the "-- default --" preset. + this->select_preset(this->first_compatible_idx(prefered_condition)); + } + void update_compatible_with_printer(const Preset &active_printer, bool select_other_if_incompatible) + { this->update_compatible_with_printer(active_printer, select_other_if_incompatible, [](const std::string&){return true;}); } size_t num_visible() const { return std::count_if(m_presets.begin(), m_presets.end(), [](const Preset &preset){return preset.is_visible;}); } @@ -291,6 +322,11 @@ public: // Generate a file path from a profile name. Add the ".ini" suffix if it is missing. std::string path_from_name(const std::string &new_name) const; +protected: + // Select a preset, if it exists. If it does not exist, select an invalid (-1) index. + // This is a temporary state, which shall be fixed immediately by the following step. + bool select_preset_by_name_strict(const std::string &name); + private: PresetCollection(); PresetCollection(const PresetCollection &other); @@ -308,6 +344,8 @@ private: std::deque::const_iterator find_preset_internal(const std::string &name) const { return const_cast(this)->find_preset_internal(name); } + size_t update_compatible_with_printer_internal(const Preset &active_printer, bool unselect_if_incompatible); + static std::vector dirty_options(const Preset *edited, const Preset *reference); // Type of this PresetCollection: TYPE_PRINT, TYPE_FILAMENT or TYPE_PRINTER. @@ -333,6 +371,9 @@ private: wxBitmap *m_bitmap_main_frame; // Path to the directory to store the config files into. std::string m_dir_path; + + // to access select_preset_by_name_strict() + friend class PresetBundle; }; } // namespace Slic3r diff --git a/xs/src/slic3r/GUI/PresetBundle.cpp b/xs/src/slic3r/GUI/PresetBundle.cpp index ad27bf8c6..244915864 100644 --- a/xs/src/slic3r/GUI/PresetBundle.cpp +++ b/xs/src/slic3r/GUI/PresetBundle.cpp @@ -209,22 +209,34 @@ void PresetBundle::load_installed_printers(const AppConfig &config) // This is done just once on application start up. void PresetBundle::load_selections(const AppConfig &config) { - prints.select_preset_by_name(remove_ini_suffix(config.get("presets", "print")), true); - filaments.select_preset_by_name(remove_ini_suffix(config.get("presets", "filament")), true); - printers.select_preset_by_name(remove_ini_suffix(config.get("presets", "printer")), true); + // Update visibility of presets based on application vendor / model / variant configuration. + this->load_installed_printers(config); + + // Parse the initial print / filament / printer profile names. + std::string initial_print_profile_name = remove_ini_suffix(config.get("presets", "print")); + std::vector initial_filament_profile_names; + std::string initial_printer_profile_name = remove_ini_suffix(config.get("presets", "printer")); + auto *nozzle_diameter = dynamic_cast(printers.get_selected_preset().config.option("nozzle_diameter")); size_t num_extruders = nozzle_diameter->values.size(); - this->set_filament_preset(0, filaments.get_selected_preset().name); + initial_filament_profile_names.emplace_back(remove_ini_suffix(config.get("presets", "filament"))); + this->set_filament_preset(0, initial_filament_profile_names.back()); for (unsigned int i = 1; i < (unsigned int)num_extruders; ++ i) { char name[64]; sprintf(name, "filament_%d", i); if (! config.has("presets", name)) break; - this->set_filament_preset(i, remove_ini_suffix(config.get("presets", name))); + initial_filament_profile_names.emplace_back(remove_ini_suffix(config.get("presets", name))); + this->set_filament_preset(i, initial_filament_profile_names.back()); } - // Update visibility of presets based on application vendor / model / variant configuration. - this->load_installed_printers(config); + // Activate print / filament / printer profiles from the config. + // If the printer profile enumerated by the config are not visible, select an alternate preset. + // Do not select alternate profiles for the print / filament profiles as those presets + // will be selected by the following call of this->update_compatible_with_printer(true). + prints.select_preset_by_name_strict(initial_print_profile_name); + filaments.select_preset_by_name_strict(initial_filament_profile_names.front()); + printers.select_preset_by_name(initial_printer_profile_name, true); // Update visibility of presets based on their compatibility with the active printer. // Always try to select a compatible print and filament preset to the current printer preset, @@ -861,14 +873,35 @@ void PresetBundle::update_multi_material_filament_presets() void PresetBundle::update_compatible_with_printer(bool select_other_if_incompatible) { - this->prints.update_compatible_with_printer(this->printers.get_edited_preset(), select_other_if_incompatible); - this->filaments.update_compatible_with_printer(this->printers.get_edited_preset(), select_other_if_incompatible); + const Preset &printer_preset = this->printers.get_edited_preset(); + const std::string &prefered_print_profile = printer_preset.config.opt_string("default_print_profile"); + const std::vector &prefered_filament_profiles = printer_preset.config.option("default_filament_profile")->values; + prefered_print_profile.empty() ? + this->prints.update_compatible_with_printer(printer_preset, select_other_if_incompatible) : + this->prints.update_compatible_with_printer(printer_preset, select_other_if_incompatible, + [&prefered_print_profile](const std::string& profile_name){ return profile_name == prefered_print_profile; }); + prefered_filament_profiles.empty() ? + this->filaments.update_compatible_with_printer(printer_preset, select_other_if_incompatible) : + this->filaments.update_compatible_with_printer(printer_preset, select_other_if_incompatible, + [&prefered_filament_profiles](const std::string& profile_name) + { return std::find(prefered_filament_profiles.begin(), prefered_filament_profiles.end(), profile_name) != prefered_filament_profiles.end(); }); if (select_other_if_incompatible) { // Verify validity of the current filament presets. - for (std::string &filament_name : this->filament_presets) { - Preset *preset = this->filaments.find_preset(filament_name, false); - if (preset == nullptr || ! preset->is_compatible) - filament_name = this->filaments.first_compatible().name; + this->filament_presets.front() = this->filaments.get_edited_preset().name; + for (size_t idx = 1; idx < this->filament_presets.size(); ++ idx) { + std::string &filament_name = this->filament_presets[idx]; + Preset *preset = this->filaments.find_preset(filament_name, false); + if (preset == nullptr || ! preset->is_compatible) { + // Pick a compatible profile. If there are prefered_filament_profiles, use them. + if (prefered_filament_profiles.empty()) + filament_name = this->filaments.first_compatible().name; + else { + const std::string &preferred = (idx < prefered_filament_profiles.size()) ? + prefered_filament_profiles[idx] : prefered_filament_profiles.front(); + filament_name = this->filaments.first_compatible( + [&preferred](const std::string& profile_name){ return profile_name == preferred; }).name; + } + } } } } diff --git a/xs/src/slic3r/GUI/Tab.cpp b/xs/src/slic3r/GUI/Tab.cpp index 940987536..ed2b4b951 100644 --- a/xs/src/slic3r/GUI/Tab.cpp +++ b/xs/src/slic3r/GUI/Tab.cpp @@ -540,7 +540,8 @@ void Tab::load_key_value(std::string opt_key, boost::any value) change_opt_value(*m_config, opt_key, value); // Mark the print & filament enabled if they are compatible with the currently selected preset. if (opt_key.compare("compatible_printers") == 0) { - m_preset_bundle->update_compatible_with_printer(0); + // Don't select another profile if this profile happens to become incompatible. + m_preset_bundle->update_compatible_with_printer(false); } m_presets->update_dirty_ui(m_presets_choice); on_presets_changed(); @@ -1772,7 +1773,7 @@ void Tab::rebuild_page_tree() // Called by the UI combo box when the user switches profiles. // Select a preset by a name.If !defined(name), then the default preset is selected. // If the current profile is modified, user is asked to save the changes. -void Tab::select_preset(std::string preset_name /*= ""*/) +void Tab::select_preset(const std::string &preset_name /*= ""*/) { std::string name = preset_name; auto force = false; diff --git a/xs/src/slic3r/GUI/Tab.hpp b/xs/src/slic3r/GUI/Tab.hpp index f39ff728c..bd9672bb2 100644 --- a/xs/src/slic3r/GUI/Tab.hpp +++ b/xs/src/slic3r/GUI/Tab.hpp @@ -143,7 +143,7 @@ public: void create_preset_tab(PresetBundle *preset_bundle); void load_current_preset(); void rebuild_page_tree(); - void select_preset(std::string preset_name = ""); + void select_preset(const std::string &preset_name = ""); bool may_discard_current_dirty_preset(PresetCollection* presets = nullptr, std::string new_printer_name = ""); wxSizer* compatible_printers_widget(wxWindow* parent, wxCheckBox** checkbox, wxButton** btn); diff --git a/xs/xsp/GUI_Preset.xsp b/xs/xsp/GUI_Preset.xsp index 1187a1cf5..d0d4057b2 100644 --- a/xs/xsp/GUI_Preset.xsp +++ b/xs/xsp/GUI_Preset.xsp @@ -44,9 +44,6 @@ Ref find_preset(char *name, bool first_visible_if_not_found = false) %code%{ RETVAL = THIS->find_preset(name, first_visible_if_not_found); %}; - bool current_is_dirty(); - std::vector current_dirty_options(); - void update_tab_ui(SV *ui, bool show_incompatible) %code%{ auto cb = (wxBitmapComboBox*)wxPli_sv_2_object( aTHX_ ui, "Wx::BitmapComboBox" ); THIS->update_tab_ui(cb, show_incompatible); %}; @@ -55,30 +52,6 @@ %code%{ auto cb = (wxBitmapComboBox*)wxPli_sv_2_object( aTHX_ ui, "Wx::BitmapComboBox" ); THIS->update_platter_ui(cb); %}; - bool update_dirty_ui(SV *ui) - %code%{ RETVAL = THIS->update_dirty_ui((wxBitmapComboBox*)wxPli_sv_2_object(aTHX_ ui, "Wx::BitmapComboBox")); %}; - - void select_preset(int idx); - bool select_preset_by_name(char *name) %code%{ RETVAL = THIS->select_preset_by_name(name, true); %}; - void discard_current_changes(); - - void save_current_preset(char *new_name) - %code%{ - try { - THIS->save_current_preset(new_name); - } catch (std::exception& e) { - croak("Error saving a preset %s:\n%s\n", new_name, e.what()); - } - %}; - void delete_current_preset() - %code%{ - try { - THIS->delete_current_preset(); - } catch (std::exception& e) { - croak("Error deleting a preset file %s:\n%s\n", THIS->get_selected_preset().file.c_str(), e.what()); - } - %}; - %{ SV* @@ -173,9 +146,6 @@ PresetCollection::arrayref() std::vector filament_presets() %code%{ RETVAL = THIS->filament_presets; %}; void set_filament_preset(int idx, const char *name); - void update_multi_material_filament_presets(); - - void update_compatible_with_printer(bool select_other_if_incompatible); Clone full_config() %code%{ RETVAL = THIS->full_config(); %}; @@ -183,15 +153,3 @@ PresetCollection::arrayref() %code%{ auto cb = (wxBitmapComboBox*)wxPli_sv_2_object(aTHX_ ui, "Wx::BitmapComboBox"); THIS->update_platter_filament_ui(extruder_idx, cb); %}; }; - -%name{Slic3r::GUI::PresetHints} class PresetHints { - PresetHints(); - ~PresetHints(); - - static std::string cooling_description(Preset *preset) - %code%{ RETVAL = PresetHints::cooling_description(*preset); %}; - static std::string maximum_volumetric_flow_description(PresetBundle *preset) - %code%{ RETVAL = PresetHints::maximum_volumetric_flow_description(*preset); %}; - static std::string recommended_thin_wall_thickness(PresetBundle *preset) - %code%{ RETVAL = PresetHints::recommended_thin_wall_thickness(*preset); %}; -}; diff --git a/xs/xsp/my.map b/xs/xsp/my.map index c1ca58827..79b71143b 100644 --- a/xs/xsp/my.map +++ b/xs/xsp/my.map @@ -231,8 +231,6 @@ PresetCollection* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T PresetBundle* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T -PresetHints* O_OBJECT_SLIC3R -Ref O_OBJECT_SLIC3R_T TabIface* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt index 0214a158d..a2bca2c7b 100644 --- a/xs/xsp/typemap.xspt +++ b/xs/xsp/typemap.xspt @@ -208,8 +208,6 @@ %typemap{Ref}{simple}; %typemap{PresetBundle*}; %typemap{Ref}{simple}; -%typemap{PresetHints*}; -%typemap{Ref}{simple}; %typemap{TabIface*}; %typemap{Ref}{simple}; From 7dbb2ed6a3f97b585ed7341a9199bfd40627da6b Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Fri, 13 Apr 2018 15:08:58 +0200 Subject: [PATCH 21/38] Configuration updates downloading --- lib/Slic3r/GUI.pm | 5 +- resources/profiles/PrusaResearch.idx | 3 - resources/profiles/PrusaResearch.ini | 3 +- xs/src/libslic3r/utils.cpp | 2 +- xs/src/slic3r/Config/Version.cpp | 4 +- xs/src/slic3r/GUI/AppConfig.cpp | 6 +- xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp | 3 - xs/src/slic3r/GUI/ConfigWizard.cpp | 3 +- xs/src/slic3r/GUI/ConfigWizard_private.hpp | 6 +- xs/src/slic3r/Utils/PresetUpdater.cpp | 224 ++++++++++++--------- xs/src/slic3r/Utils/PresetUpdater.hpp | 4 +- xs/xsp/Utils_PresetUpdater.xsp | 4 +- 12 files changed, 148 insertions(+), 119 deletions(-) diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index b592c4289..40cfcad89 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -103,7 +103,7 @@ sub OnInit { # my $version_check = $self->{app_config}->get('version_check'); $self->{preset_updater} = Slic3r::PresetUpdater->new($VERSION_ONLINE_EVENT, $self->{app_config}); - eval { $self->{preset_updater}->config_update() }; + eval { $self->{preset_updater}->config_update($self->{app_config}) }; if ($@) { warn $@ . "\n"; fatal_error(undef, $@); @@ -158,7 +158,8 @@ sub OnInit { Slic3r::GUI::config_wizard(1); } - # $self->{preset_updater}->download($self->{preset_bundle}); + # TODO: call periodically? + $self->{preset_updater}->sync($self->{app_config}, $self->{preset_bundle}); }); # The following event is emited by the C++ menu implementation of application language change. diff --git a/resources/profiles/PrusaResearch.idx b/resources/profiles/PrusaResearch.idx index 28f17f10a..837bc7bac 100644 --- a/resources/profiles/PrusaResearch.idx +++ b/resources/profiles/PrusaResearch.idx @@ -1,8 +1,5 @@ # This is an example configuration version index. # The index contains version numbers -min_slic3r_version =1.39.0 -0.2.0-alpha "some test comment" -max_slic3r_version= 1.39.4 0.1.0 another test comment # some empty lines diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index cf82855cf..0ca094880 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -8,8 +8,7 @@ name = Prusa Research config_version = 0.1.0 # Where to get the updates from? # TODO: proper URL -# config_update_url = https://raw.githubusercontent.com/prusa3d/Slic3r-settings/master/live/PrusaResearch.ini -config_update_url = https://gist.githubusercontent.com/vojtechkral/4d8fd4a3b8699a01ec892c264178461c/raw/e9187c3e15ceaf1a90f29b7c43cf3ccc746140f0/PrusaResearch.ini +config_update_url = https://raw.githubusercontent.com/vojtechkral/slic3r-settings-tmp/master/PrusaResearch # The printer models will be shown by the Configuration Wizard in this order, # also the first model installed & the first nozzle installed will be activated after install. diff --git a/xs/src/libslic3r/utils.cpp b/xs/src/libslic3r/utils.cpp index 703d5ff66..733757e25 100644 --- a/xs/src/libslic3r/utils.cpp +++ b/xs/src/libslic3r/utils.cpp @@ -119,7 +119,7 @@ static std::string g_data_dir; void set_data_dir(const std::string &dir) { - g_data_dir = dir + "-alpha"; // FIXME: Resolve backcompat problems + g_data_dir = dir; } const std::string& data_dir() diff --git a/xs/src/slic3r/Config/Version.cpp b/xs/src/slic3r/Config/Version.cpp index 5430e569c..b1abc5c63 100644 --- a/xs/src/slic3r/Config/Version.cpp +++ b/xs/src/slic3r/Config/Version.cpp @@ -153,10 +153,10 @@ Index::const_iterator Index::find(const Semver &ver) const Index::const_iterator Index::recommended() const { int idx = -1; - const_iterator highest = m_configs.end(); + const_iterator highest = this->end(); for (const_iterator it = this->begin(); it != this->end(); ++ it) if (it->is_current_slic3r_supported() && - (highest == this->end() || highest->max_slic3r_version < it->max_slic3r_version)) + (highest == this->end() || highest->config_version < it->config_version)) highest = it; return highest; } diff --git a/xs/src/slic3r/GUI/AppConfig.cpp b/xs/src/slic3r/GUI/AppConfig.cpp index ee77f877a..d0f2f1019 100644 --- a/xs/src/slic3r/GUI/AppConfig.cpp +++ b/xs/src/slic3r/GUI/AppConfig.cpp @@ -46,11 +46,15 @@ void AppConfig::set_defaults() set("no_defaults", "1"); if (get("show_incompatible_presets").empty()) set("show_incompatible_presets", "0"); - // Version check is enabled by default in the config, but it is not implemented yet. // XXX + if (get("version_check").empty()) set("version_check", "1"); + // TODO: proper URL + if (get("version_check_url").empty()) + set("version_check_url", "https://gist.githubusercontent.com/vojtechkral/4d8fd4a3b8699a01ec892c264178461c/raw/e9187c3e15ceaf1a90f29b7c43cf3ccc746140f0/slic3rPE.version"); if (get("preset_update").empty()) set("preset_update", "1"); + // Use OpenGL 1.1 even if OpenGL 2.0 is available. This is mainly to support some buggy Intel HD Graphics drivers. // https://github.com/prusa3d/Slic3r/issues/233 if (get("use_legacy_opengl").empty()) diff --git a/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp b/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp index 730b97a32..8739b8fa2 100644 --- a/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp +++ b/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp @@ -86,9 +86,6 @@ ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db html->SetFonts(font.GetFaceName(), font.GetFaceName(), size); html->SetBorders(2); std::string text = generate_html_page(snapshot_db); - FILE *file = ::fopen("d:\\temp\\configsnapshotdialog.html", "wt"); - fwrite(text.data(), 1, text.size(), file); - fclose(file); html->SetPage(text.c_str()); vsizer->Add(html, 1, wxEXPAND | wxALIGN_LEFT | wxRIGHT | wxBOTTOM, 0); html->Bind(wxEVT_HTML_LINK_CLICKED, &ConfigSnapshotDialog::onLinkClicked, this); diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index f353ab7f7..5b49fc025 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -660,7 +660,8 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese app_config->set_vendors(appconfig_vendors); app_config->set("version_check", page_update->version_check ? "1" : "0"); app_config->set("preset_update", page_update->preset_update ? "1" : "0"); - app_config->reset_selections(); // XXX: only on "fresh start"? + app_config->reset_selections(); + // ^ TODO: replace with appropriate printer selection preset_bundle->load_presets(*app_config); } else { for (ConfigWizardPage *page = page_firmware; page != nullptr; page = page->page_next()) { diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp index 137b276b8..c93e0a80d 100644 --- a/xs/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -17,7 +17,6 @@ #include "libslic3r/PrintConfig.hpp" #include "AppConfig.hpp" #include "Preset.hpp" -// #include "PresetBundle.hpp" #include "BedShapeDialog.hpp" namespace fs = boost::filesystem; @@ -173,9 +172,8 @@ private: struct ConfigWizard::priv { ConfigWizard *q; - AppConfig appconfig_vendors; - // PresetBundle bundle_vendors; - std::unordered_map vendors; + AppConfig appconfig_vendors; // TODO: use order-preserving container + std::unordered_map vendors; // TODO: just set? std::unordered_map vendors_rsrc; std::unique_ptr custom_config; diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp index 28b977321..257c7f552 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.cpp +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -25,19 +27,17 @@ using Slic3r::GUI::Config::Version; using Slic3r::GUI::Config::Snapshot; using Slic3r::GUI::Config::SnapshotDB; -// XXX: Prevent incomplete file downloads: download a tmp file, then move -// Delete incomplete ones on startup. namespace Slic3r { -// TODO: proper URL -// TODO: Actually, use index -static const std::string SLIC3R_VERSION_URL = "https://gist.githubusercontent.com/vojtechkral/4d8fd4a3b8699a01ec892c264178461c/raw/e9187c3e15ceaf1a90f29b7c43cf3ccc746140f0/slic3rPE.version"; enum { SLIC3R_VERSION_BODY_MAX = 256, }; +static const char *INDEX_FILENAME = "index.idx"; +static const char *TMP_EXTENSION = ".download"; + struct Update { @@ -45,7 +45,7 @@ struct Update fs::path target; Version version; - Update(const fs::path &source, fs::path &&target, const Version &version) : + Update(fs::path &&source, const fs::path &target, const Version &version) : source(source), target(std::move(target)), version(version) @@ -60,9 +60,11 @@ typedef std::vector Updates; struct PresetUpdater::priv { int version_online_event; - AppConfig *app_config; - bool version_check; - bool preset_update; + std::vector index_db; + + bool enabled_version_check; + bool enabled_config_update; + std::string version_check_url; fs::path cache_path; fs::path rsrc_path; @@ -73,7 +75,11 @@ struct PresetUpdater::priv priv(int event, AppConfig *app_config); - void download(const std::set vendors) const; + void set_download_prefs(AppConfig *app_config); + bool get_file(const std::string &url, const fs::path &target_path) const; + void prune_tmps() const; + void sync_version() const; + void sync_config(const std::set vendors) const; void check_install_indices() const; Updates config_update() const; @@ -81,21 +87,62 @@ struct PresetUpdater::priv PresetUpdater::priv::priv(int event, AppConfig *app_config) : version_online_event(event), - app_config(app_config), - version_check(false), - preset_update(false), cache_path(fs::path(Slic3r::data_dir()) / "cache"), rsrc_path(fs::path(resources_dir()) / "profiles"), vendor_path(fs::path(Slic3r::data_dir()) / "vendor"), cancel(false) -{} - -void PresetUpdater::priv::download(const std::set vendors) const { - if (!version_check) { return; } + set_download_prefs(app_config); + check_install_indices(); + index_db = std::move(Index::load_db()); +} - // Download current Slic3r version - Http::get(SLIC3R_VERSION_URL) +void PresetUpdater::priv::set_download_prefs(AppConfig *app_config) +{ + enabled_version_check = app_config->get("version_check") == "1"; + version_check_url = app_config->get("version_check_url"); + enabled_config_update = app_config->get("preset_update") == "1"; +} + +bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &target_path) const +{ + std::cerr << "get_file(): " << url << " -> " << target_path << std::endl; + + // TODO: Proper caching + + bool res = false; + fs::path tmp_path = target_path; + tmp_path += TMP_EXTENSION; + + Http::get(url) + .on_progress([this](Http::Progress, bool &cancel) { + if (cancel) { cancel = true; } + }) + .on_complete([&](std::string body, unsigned http_status) { + fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc); + file.write(body.c_str(), body.size()); + fs::rename(tmp_path, target_path); + res = true; + }) + .perform_sync(); + + return res; +} + +void PresetUpdater::priv::prune_tmps() const +{ + for (fs::directory_iterator it(cache_path); it != fs::directory_iterator(); ++it) { + if (it->path().extension() == TMP_EXTENSION) { + fs::remove(it->path()); + } + } +} + +void PresetUpdater::priv::sync_version() const +{ + if (! enabled_version_check) { return; } + + Http::get(version_check_url) .size_limit(SLIC3R_VERSION_BODY_MAX) .on_progress([this](Http::Progress, bool &cancel) { cancel = this->cancel; @@ -107,27 +154,55 @@ void PresetUpdater::priv::download(const std::set vendors) const GUI::get_app()->QueueEvent(evt); }) .perform_sync(); +} - if (!preset_update) { return; } +void PresetUpdater::priv::sync_config(const std::set vendors) const +{ + std::cerr << "sync_config()" << std::endl; + + if (!enabled_config_update) { return; } // Donwload vendor preset bundles - for (const auto &vendor : vendors) { + for (const auto &index : index_db) { if (cancel) { return; } - // TODO: Proper caching + std::cerr << "Index: " << index.vendor() << std::endl; - auto target_path = cache_path / vendor.id; - target_path += ".ini"; + const auto vendor_it = vendors.find(VendorProfile(index.vendor())); + if (vendor_it == vendors.end()) { continue; } - Http::get(vendor.config_update_url) - .on_progress([this](Http::Progress, bool &cancel) { - cancel = this->cancel; - }) - .on_complete([&](std::string body, unsigned http_status) { - fs::fstream file(target_path, std::ios::out | std::ios::binary | std::ios::trunc); - file.write(body.c_str(), body.size()); - }) - .perform_sync(); + const VendorProfile &vendor = *vendor_it; + if (vendor.config_update_url.empty()) { continue; } + + // Download a fresh index + const auto idx_url = vendor.config_update_url + "/" + INDEX_FILENAME; + const auto idx_path = cache_path / (vendor.id + ".idx"); + if (! get_file(idx_url, idx_path)) { continue; } + if (cancel) { return; } + + std::cerr << "Got a new index: " << idx_path << std::endl; + + // Load the fresh index up + Index new_index; + new_index.load(idx_path); + + // See if a there's a new version to download + const auto recommended_it = new_index.recommended(); + if (recommended_it == new_index.end()) { continue; } + const auto recommended = recommended_it->config_version; + + std::cerr << "Current vendor version: " << vendor.config_version.to_string() << std::endl; + std::cerr << "Recommended version:\t" << recommended.to_string() << std::endl; + + if (vendor.config_version >= recommended) { continue; } + + // Download a fresh bundle + const auto bundle_url = (boost::format("%1%/%2%.ini") % vendor.config_update_url % recommended.to_string()).str(); + const auto bundle_path = cache_path / (vendor.id + ".ini"); + if (! get_file(bundle_url, bundle_path)) { continue; } + if (cancel) { return; } + + std::cerr << "Got a new bundle: " << bundle_path << std::endl; } } @@ -148,35 +223,11 @@ void PresetUpdater::priv::check_install_indices() const Updates PresetUpdater::priv::config_update() const { - priv::check_install_indices(); - const auto index_db = Index::load_db(); // TODO: Keep in Snapshots singleton? - Updates updates; for (const auto idx : index_db) { const auto bundle_path = vendor_path / (idx.vendor() + ".ini"); - // If the bundle doesn't exist at all, update from resources - // if (! fs::exists(bundle_path)) { - // auto path_in_rsrc = rsrc_path / (idx.vendor() + ".ini"); - - // // Otherwise load it and check for chached updates - // const auto rsrc_vp = VendorProfile::from_ini(path_in_rsrc, false); - - // const auto rsrc_ver = idx.find(rsrc_vp.config_version); - // if (rsrc_ver == idx.end()) { - // // TODO: throw - // } - - // if (fs::exists(path_in_rsrc)) { - // updates.emplace_back(bundle_path, std::move(path_in_rsrc), *rsrc_ver); - // } else { - // // XXX: ??? - // } - - // continue; - // } - if (! fs::exists(bundle_path)) { continue; } @@ -186,12 +237,12 @@ Updates PresetUpdater::priv::config_update() const const auto ver_current = idx.find(vp.config_version); if (ver_current == idx.end()) { - // TODO: throw + // TODO: throw / ignore ? } const auto recommended = idx.recommended(); if (recommended == idx.end()) { - // TODO: throw + throw std::runtime_error((boost::format("Invalid index: `%1%`") % idx.vendor()).str()); } if (! ver_current->is_current_slic3r_supported()) { @@ -202,50 +253,26 @@ Updates PresetUpdater::priv::config_update() const // Config bundle update situation auto path_in_cache = cache_path / (idx.vendor() + ".ini"); + if (! fs::exists(path_in_cache)) { + continue; + } + const auto cached_vp = VendorProfile::from_ini(path_in_cache, false); if (cached_vp.config_version == recommended->config_version) { - updates.emplace_back(bundle_path, std::move(path_in_cache), *ver_current); + updates.emplace_back(std::move(path_in_cache), bundle_path, *recommended); } else { - // XXX: ??? + // XXX: ? } } } - // Check for bundles that don't have an index - // for (fs::directory_iterator it(rsrc_path); it != fs::directory_iterator(); ++it) { - // if (it->path().extension() == ".ini") { - // const auto &path = it->path(); - // const auto vendor_id = path.stem().string(); - - // const auto needle = std::find_if(index_db.begin(), index_db.end(), [&vendor_id](const Index &idx) { - // return idx.vendor() == vendor_id; - // }); - // if (needle != index_db.end()) { - // continue; - // } - - // auto vp = VendorProfile::from_ini(path, false); - // auto path_in_data = vendor_path / path.filename(); - - // if (! fs::exists(path_in_data)) { - // Version version; - // version.config_version = vp.config_version; - // updates.emplace_back(path, std::move(path_in_data), version); - // } - // } - // } - return updates; } PresetUpdater::PresetUpdater(int version_online_event, AppConfig *app_config) : p(new priv(version_online_event, app_config)) -{ - p->preset_update = app_config->get("preset_update") == "1"; - // preset_update implies version_check: // XXX: not any more - p->version_check = p->preset_update || app_config->get("version_check") == "1"; -} +{} // Public @@ -258,8 +285,10 @@ PresetUpdater::~PresetUpdater() } } -void PresetUpdater::download(PresetBundle *preset_bundle) +void PresetUpdater::sync(AppConfig *app_config, PresetBundle *preset_bundle) { + p->set_download_prefs(app_config); + if (!p->enabled_version_check && !p->enabled_config_update) { return; } // Copy the whole vendors data for use in the background thread // Unfortunatelly as of C++11, it needs to be copied again @@ -267,12 +296,16 @@ void PresetUpdater::download(PresetBundle *preset_bundle) std::set vendors = preset_bundle->vendors; p->thread = std::move(std::thread([this, vendors]() { - this->p->download(std::move(vendors)); + this->p->prune_tmps(); + this->p->sync_version(); + this->p->sync_config(std::move(vendors)); })); } -void PresetUpdater::config_update() +void PresetUpdater::config_update(AppConfig *app_config) { + if (! p->enabled_config_update) { return; } + const auto updates = p->config_update(); if (updates.size() > 0) { const auto msg = _(L("Configuration update is available. Would you like to install it?")); @@ -298,12 +331,11 @@ void PresetUpdater::config_update() if (res == wxID_YES) { // User gave clearance, updates are go - // TODO: Comment? - SnapshotDB::singleton().take_snapshot(*p->app_config, Snapshot::SNAPSHOT_UPGRADE, ""); + SnapshotDB::singleton().take_snapshot(*app_config, Snapshot::SNAPSHOT_UPGRADE, ""); for (const auto &update : updates) { fs::copy_file(update.source, update.target, fs::copy_option::overwrite_if_exists); - + PresetBundle bundle; bundle.load_configbundle(update.target.string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); diff --git a/xs/src/slic3r/Utils/PresetUpdater.hpp b/xs/src/slic3r/Utils/PresetUpdater.hpp index 8fd6e4528..966dd1464 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.hpp +++ b/xs/src/slic3r/Utils/PresetUpdater.hpp @@ -19,9 +19,9 @@ public: PresetUpdater &operator=(const PresetUpdater &) = delete; ~PresetUpdater(); - void download(PresetBundle *preset_bundle); // XXX + void sync(AppConfig *app_config, PresetBundle *preset_bundle); - void config_update(); + void config_update(AppConfig *app_config); private: struct priv; std::unique_ptr p; diff --git a/xs/xsp/Utils_PresetUpdater.xsp b/xs/xsp/Utils_PresetUpdater.xsp index 4c1a637e4..3a4d55a01 100644 --- a/xs/xsp/Utils_PresetUpdater.xsp +++ b/xs/xsp/Utils_PresetUpdater.xsp @@ -7,6 +7,6 @@ %name{Slic3r::PresetUpdater} class PresetUpdater { PresetUpdater(int version_online_event, AppConfig *app_config); - void download(PresetBundle* preset_bundle); - void config_update(); + void sync(AppConfig *app_config, PresetBundle* preset_bundle); + void config_update(AppConfig *app_config); }; From 6d25ed2b00447ca3b0ed48a9d0ec288ee8b24503 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Fri, 13 Apr 2018 16:15:30 +0200 Subject: [PATCH 22/38] Version's compatibility with Slic3r extended with pre-release compatibility check: A release Slic3r is not compatible with alpha and beta configs, a beta Slic3r is not compatible with alpha configs, but is compatible with beta configs etc. --- xs/src/slic3r/Config/Version.cpp | 120 +++++++++++++++++++++++++++++++ xs/src/slic3r/Config/Version.hpp | 2 +- xs/src/slic3r/Utils/Semver.hpp | 7 ++ 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/xs/src/slic3r/Config/Version.cpp b/xs/src/slic3r/Config/Version.cpp index a80b0b6e9..8344e2822 100644 --- a/xs/src/slic3r/Config/Version.cpp +++ b/xs/src/slic3r/Config/Version.cpp @@ -15,11 +15,131 @@ namespace Config { static boost::optional s_current_slic3r_semver = Semver::parse(SLIC3R_VERSION); +// Optimized lexicographic compare of two pre-release versions, ignoring the numeric suffix. +static int compare_prerelease(const char *p1, const char *p2) +{ + for (;;) { + char c1 = *p1 ++; + char c2 = *p2 ++; + bool a1 = std::isalpha(c1) && c1 != 0; + bool a2 = std::isalpha(c2) && c2 != 0; + if (a1) { + if (a2) { + if (c1 != c2) + return (c1 < c2) ? -1 : 1; + } else + return 1; + } else { + if (a2) + return -1; + else + return 0; + } + } + // This shall never happen. + return 0; +} + +bool Version::is_slic3r_supported(const Semver &slic3r_version) const +{ + if (! slic3r_version.in_range(min_slic3r_version, max_slic3r_version)) + return false; + // Now verify, whether the configuration pre-release status is compatible with the Slic3r's pre-release status. + // Alpha Slic3r will happily load any configuration, while beta Slic3r will ignore alpha configurations etc. + const char *prerelease_slic3r = slic3r_version.prerelease(); + const char *prerelease_config = this->config_version.prerelease(); + if (prerelease_config == nullptr) + // Released config is always supported. + return true; + else if (prerelease_slic3r == nullptr) + // Released slic3r only supports released configs. + return false; + // Compare the pre-release status of Slic3r against the config. + // If the prerelease status of slic3r is lexicographically lower or equal + // to the prerelease status of the config, accept it. + return compare_prerelease(prerelease_slic3r, prerelease_config) != 1; +} + bool Version::is_current_slic3r_supported() const { return this->is_slic3r_supported(*s_current_slic3r_semver); } +#if 0 +//TODO: This test should be moved to a unit test, once we have C++ unit tests in place. +static int version_test() +{ + Version v; + v.config_version = *Semver::parse("1.1.2"); + v.min_slic3r_version = *Semver::parse("1.38.0"); + v.max_slic3r_version = Semver::inf(); + assert(v.is_slic3r_supported(*Semver::parse("1.38.0"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.38.0-alpha"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.37.0-alpha"))); + // Test the prerelease status. + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-rc2"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0"))); + v.config_version = *Semver::parse("1.1.2-alpha"); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-rc2"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0"))); + v.config_version = *Semver::parse("1.1.2-alpha1"); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-rc2"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0"))); + v.config_version = *Semver::parse("1.1.2-beta"); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-rc"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-rc2"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0"))); + v.config_version = *Semver::parse("1.1.2-rc"); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-rc"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-rc2"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0"))); + v.config_version = *Semver::parse("1.1.2-rc2"); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-rc"))); + assert(v.is_slic3r_supported(*Semver::parse("1.39.0-rc2"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.39.0"))); + // Test the upper boundary. + v.config_version = *Semver::parse("1.1.2"); + v.max_slic3r_version = *Semver::parse("1.39.3-beta1"); + assert(v.is_slic3r_supported(*Semver::parse("1.38.0"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.38.0-alpha"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.38.0-alpha1"))); + assert(! v.is_slic3r_supported(*Semver::parse("1.37.0-alpha"))); + return 0; +} +static int version_test_run = version_test(); +#endif + inline char* left_trim(char *c) { for (; *c == ' ' || *c == '\t'; ++ c); diff --git a/xs/src/slic3r/Config/Version.hpp b/xs/src/slic3r/Config/Version.hpp index c4243ca75..fb45c17eb 100644 --- a/xs/src/slic3r/Config/Version.hpp +++ b/xs/src/slic3r/Config/Version.hpp @@ -27,7 +27,7 @@ struct Version // Single comment line. std::string comment; - bool is_slic3r_supported(const Semver &slicer_version) const { return slicer_version.in_range(min_slic3r_version, max_slic3r_version); } + bool is_slic3r_supported(const Semver &slicer_version) const; bool is_current_slic3r_supported() const; }; diff --git a/xs/src/slic3r/Utils/Semver.hpp b/xs/src/slic3r/Utils/Semver.hpp index a1f4a92e8..538a3f144 100644 --- a/xs/src/slic3r/Utils/Semver.hpp +++ b/xs/src/slic3r/Utils/Semver.hpp @@ -77,6 +77,13 @@ public: ~Semver() { ::semver_free(&ver); } + // const accessors + int major() const { return ver.major; } + int minor() const { return ver.minor; } + int patch() const { return ver.patch; } + const char* prerelease() const { return ver.prerelease; } + const char* metadata() const { return ver.metadata; } + // Comparison bool operator<(const Semver &b) const { return ::semver_compare(ver, b.ver) == -1; } bool operator<=(const Semver &b) const { return ::semver_compare(ver, b.ver) <= 0; } From c733e3151b2d15fdf1507c8417c2a96898d1d019 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Mon, 16 Apr 2018 16:52:11 +0200 Subject: [PATCH 23/38] Updating: Detect legacy datadir, remove conflicting presets --- lib/Slic3r/GUI.pm | 34 ++---- xs/src/libslic3r/libslic3r.h | 2 +- xs/src/slic3r/Config/Snapshot.hpp | 2 +- xs/src/slic3r/GUI/AppConfig.cpp | 5 +- xs/src/slic3r/GUI/AppConfig.hpp | 6 +- xs/src/slic3r/GUI/ConfigWizard.cpp | 120 +++++++++++++++------ xs/src/slic3r/GUI/ConfigWizard.hpp | 3 +- xs/src/slic3r/GUI/ConfigWizard_private.hpp | 24 ++++- xs/src/slic3r/GUI/GUI.cpp | 48 +++++++-- xs/src/slic3r/GUI/GUI.hpp | 9 +- xs/src/slic3r/Utils/PresetUpdater.cpp | 60 +++++++---- xs/src/slic3r/Utils/PresetUpdater.hpp | 3 +- xs/src/slic3r/Utils/Semver.hpp | 2 - xs/xsp/GUI.xsp | 7 +- xs/xsp/my.map | 1 - xs/xsp/typemap.xspt | 2 + 16 files changed, 232 insertions(+), 96 deletions(-) diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 40cfcad89..473fc6b90 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -95,14 +95,15 @@ sub OnInit { warn $@ . "\n"; fatal_error(undef, $@); } - my $run_wizard = ! $self->{app_config}->exists; + my $app_conf_exists = $self->{app_config}->exists; # load settings - $self->{app_config}->load if ! $run_wizard; + $self->{app_config}->load if $app_conf_exists; $self->{app_config}->set('version', $Slic3r::VERSION); $self->{app_config}->save; # my $version_check = $self->{app_config}->get('version_check'); $self->{preset_updater} = Slic3r::PresetUpdater->new($VERSION_ONLINE_EVENT, $self->{app_config}); + Slic3r::GUI::set_preset_updater($self->{preset_updater}); eval { $self->{preset_updater}->config_update($self->{app_config}) }; if ($@) { warn $@ . "\n"; @@ -120,8 +121,6 @@ sub OnInit { warn $@ . "\n"; show_error(undef, $@); } - # TODO: check previously downloaded updates - $run_wizard = 1 if $self->{preset_bundle}->has_defauls_only; Slic3r::GUI::set_preset_bundle($self->{preset_bundle}); @@ -148,16 +147,8 @@ sub OnInit { # On OSX the UI was not initialized correctly if the wizard was called # before the UI was up and running. $self->CallAfter(sub { - # XXX: recreate_GUI ??? - # if ($slic3r_update) { - # # TODO - # } - # XXX: ? - if ($run_wizard) { - # Run the config wizard, don't offer the "reset user profile" checkbox. - Slic3r::GUI::config_wizard(1); - } - + Slic3r::GUI::config_wizard_startup($app_conf_exists); + # TODO: call periodically? $self->{preset_updater}->sync($self->{app_config}, $self->{preset_bundle}); }); @@ -209,15 +200,12 @@ sub recreate_GUI{ $self->{app_config}->save if $self->{app_config}->dirty; }); - my $run_wizard = 1 if $self->{preset_bundle}->has_defauls_only; - if ($run_wizard) { - # On OSX the UI was not initialized correctly if the wizard was called - # before the UI was up and running. - $self->CallAfter(sub { - # Run the config wizard, don't offer the "reset user profile" checkbox. - Slic3r::GUI::config_wizard(1); - }); - } + # On OSX the UI was not initialized correctly if the wizard was called + # before the UI was up and running. + $self->CallAfter(sub { + # Run the config wizard, don't offer the "reset user profile" checkbox. + Slic3r::GUI::config_wizard_startup(1); + }); } sub system_info { diff --git a/xs/src/libslic3r/libslic3r.h b/xs/src/libslic3r/libslic3r.h index 0f192c37c..4aef4d5c1 100644 --- a/xs/src/libslic3r/libslic3r.h +++ b/xs/src/libslic3r/libslic3r.h @@ -14,7 +14,7 @@ #include #define SLIC3R_FORK_NAME "Slic3r Prusa Edition" -#define SLIC3R_VERSION "1.39.0" +#define SLIC3R_VERSION "1.40.0" #define SLIC3R_BUILD "UNKNOWN" typedef int32_t coord_t; diff --git a/xs/src/slic3r/Config/Snapshot.hpp b/xs/src/slic3r/Config/Snapshot.hpp index a7b8a5aa5..584a37400 100644 --- a/xs/src/slic3r/Config/Snapshot.hpp +++ b/xs/src/slic3r/Config/Snapshot.hpp @@ -97,7 +97,7 @@ public: // Create a snapshot directory, copy the vendor config bundles, user print/filament/printer profiles, // create an index. - const Snapshot& take_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment); + const Snapshot& take_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment = ""); void restore_snapshot(const std::string &id, AppConfig &app_config); void restore_snapshot(const Snapshot &snapshot, AppConfig &app_config); diff --git a/xs/src/slic3r/GUI/AppConfig.cpp b/xs/src/slic3r/GUI/AppConfig.cpp index d0f2f1019..5104078b1 100644 --- a/xs/src/slic3r/GUI/AppConfig.cpp +++ b/xs/src/slic3r/GUI/AppConfig.cpp @@ -98,6 +98,10 @@ void AppConfig::load() } } + // Figure out if datadir has legacy presets + auto ini_ver = Semver::parse(get("version")); + m_legacy_datadir = ini_ver ? *ini_ver < Semver(1, 40, 0) : true; + // Override missing or keys with their defaults. this->set_defaults(); m_dirty = false; @@ -240,7 +244,6 @@ bool AppConfig::slic3r_update_avail() const Semver AppConfig::get_slic3r_version() const { - // TODO: move to Semver c-tor (???) auto res = Semver::parse(get("version")); if (! res) { throw std::runtime_error(std::string("Could not parse Slic3r version string in application config.")); diff --git a/xs/src/slic3r/GUI/AppConfig.hpp b/xs/src/slic3r/GUI/AppConfig.hpp index ffda083ec..88ba0a662 100644 --- a/xs/src/slic3r/GUI/AppConfig.hpp +++ b/xs/src/slic3r/GUI/AppConfig.hpp @@ -13,7 +13,7 @@ namespace Slic3r { class AppConfig { public: - AppConfig() : m_dirty(false) { this->reset(); } + AppConfig() : m_dirty(false), m_legacy_datadir(false) { this->reset(); } // Clear and reset to defaults. void reset(); @@ -98,6 +98,8 @@ public: // Get the default config path from Slic3r::data_dir(). static std::string config_path(); + + bool legacy_datadir() const { return m_legacy_datadir; } // Does the config file exist? static bool exists(); @@ -109,6 +111,8 @@ private: VendorMap m_vendors; // Has any value been modified since the config.ini has been last saved or loaded? bool m_dirty; + // Whether the existing version is before system profiles & configuration updating + bool m_legacy_datadir; }; }; // namespace Slic3r diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index 5b49fc025..a059d234b 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -1,8 +1,8 @@ #include "ConfigWizard_private.hpp" -#include // XXX #include #include +#include #include #include @@ -17,6 +17,7 @@ #include "GUI.hpp" #include "slic3r/Utils/PresetUpdater.hpp" +// TODO: Wizard vs Assistant namespace Slic3r { namespace GUI { @@ -54,48 +55,80 @@ PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, cons const auto vendor_id = vendor.id; const auto &models = vendor.models; + auto *sizer = new wxBoxSizer(wxVERTICAL); + auto *printer_grid = new wxFlexGridSizer(models.size(), 0, 20); printer_grid->SetFlexibleDirection(wxVERTICAL); - SetSizer(printer_grid); + sizer->Add(printer_grid); auto namefont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); namefont.SetWeight(wxFONTWEIGHT_BOLD); for (const auto &model : models) { auto *panel = new wxPanel(this); - auto *sizer = new wxBoxSizer(wxVERTICAL); - panel->SetSizer(sizer); + auto *col_sizer = new wxBoxSizer(wxVERTICAL); + panel->SetSizer(col_sizer); auto *title = new wxStaticText(panel, wxID_ANY, model.name, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); title->SetFont(namefont); - sizer->Add(title, 0, wxBOTTOM, 3); + col_sizer->Add(title, 0, wxBOTTOM, 3); auto bitmap_file = wxString::Format("printers/%s_%s.png", vendor.id, model.id); wxBitmap bitmap(GUI::from_u8(Slic3r::var(bitmap_file.ToStdString())), wxBITMAP_TYPE_PNG); auto *bitmap_widget = new wxStaticBitmap(panel, wxID_ANY, bitmap); - sizer->Add(bitmap_widget, 0, wxBOTTOM, 3); + col_sizer->Add(bitmap_widget, 0, wxBOTTOM, 3); - sizer->AddSpacer(20); + col_sizer->AddSpacer(20); const auto model_id = model.id; for (const auto &variant : model.variants) { - const auto variant_name = variant.name; - auto *cbox = new wxCheckBox(panel, wxID_ANY, wxString::Format("%s %s %s", variant.name, _(L("mm")), _(L("nozzle")))); - bool enabled = appconfig_vendors.get_variant("PrusaResearch", model_id, variant_name); + const auto label = wxString::Format("%s %s %s", variant.name, _(L("mm")), _(L("nozzle"))); + auto *cbox = new Checkbox(panel, label, model_id, variant.name); + const size_t idx = cboxes.size(); + cboxes.push_back(cbox); + bool enabled = appconfig_vendors.get_variant("PrusaResearch", model_id, variant.name); variants_checked += enabled; cbox->SetValue(enabled); - sizer->Add(cbox, 0, wxBOTTOM, 3); - cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { - this->variants_checked += event.IsChecked() ? 1 : -1; - PrinterPickerEvent evt(EVT_PRINTER_PICK, this->GetId(), std::move(vendor_id), std::move(model_id), std::move(variant_name), event.IsChecked()); - this->AddPendingEvent(evt); + col_sizer->Add(cbox, 0, wxBOTTOM, 3); + cbox->Bind(wxEVT_CHECKBOX, [this, idx](wxCommandEvent &event) { + if (idx >= this->cboxes.size()) { return; } + this->on_checkbox(this->cboxes[idx], event.IsChecked()); }); } printer_grid->Add(panel); } + auto *all_none_sizer = new wxBoxSizer(wxHORIZONTAL); + auto *sel_all = new wxButton(this, wxID_ANY, _(L("Select all"))); + auto *sel_none = new wxButton(this, wxID_ANY, _(L("Select none"))); + sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(true); }); + sel_none->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(false); }); + all_none_sizer->AddStretchSpacer(); + all_none_sizer->Add(sel_all); + all_none_sizer->Add(sel_none); + sizer->AddStretchSpacer(); + sizer->Add(all_none_sizer, 0, wxEXPAND); + + SetSizer(sizer); +} + +void PrinterPicker::select_all(bool select) +{ + for (const auto &cb : cboxes) { + if (cb->GetValue() != select) { + cb->SetValue(select); + on_checkbox(cb, select); + } + } +} + +void PrinterPicker::on_checkbox(const Checkbox *cbox, bool checked) +{ + variants_checked += checked ? 1 : -1; + PrinterPickerEvent evt(EVT_PRINTER_PICK, GetId(), vendor_id, cbox->model, cbox->variant, checked); + AddPendingEvent(evt); } @@ -176,15 +209,10 @@ PageWelcome::PageWelcome(ConfigWizard *parent) : { append_text(_(L("Hello, welcome to Slic3r Prusa Edition! TODO: This text."))); - // const PresetBundle &bundle = wizard_p()->bundle_vendors; - // const auto &vendors = bundle.vendors; const auto &vendors = wizard_p()->vendors; - // const auto vendor_prusa = std::find(vendors.cbegin(), vendors.cend(), VendorProfile("PrusaResearch")); const auto vendor_prusa = vendors.find("PrusaResearch"); if (vendor_prusa != vendors.cend()) { - const auto &models = vendor_prusa->second.models; - AppConfig &appconfig_vendors = this->wizard_p()->appconfig_vendors; printer_picker = new PrinterPicker(this, vendor_prusa->second, appconfig_vendors); @@ -550,6 +578,17 @@ void ConfigWizardIndex::on_paint(wxPaintEvent & evt) // priv +static const std::unordered_map> legacy_preset_map {{ + { "Original Prusa i3 MK2.ini", std::make_pair("MK2S", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode.ini", std::make_pair("MK2S", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, + { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2S", "0.4") }, + { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, + { "Original Prusa i3 MK2 0.25 nozzle.ini", std::make_pair("MK2S", "0.25") }, + { "Original Prusa i3 MK2 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, + { "Original Prusa i3 MK3.ini", std::make_pair("MK3", "0.4") }, +}}; + void ConfigWizard::priv::load_vendors() { const auto vendor_dir = fs::path(Slic3r::data_dir()) / "vendor"; @@ -569,13 +608,28 @@ void ConfigWizard::priv::load_vendors() const auto id = it->path().stem().string(); if (vendors.find(id) == vendors.end()) { auto vp = VendorProfile::from_ini(it->path()); - vendors_rsrc[vp.id] = it->path(); + vendors_rsrc[vp.id] = it->path().filename().string(); vendors[vp.id] = std::move(vp); } } } - appconfig_vendors.set_vendors(*GUI::get_app_config()); + // Load up the set of vendors / models / variants the user has had enabled up till now + const AppConfig *app_config = GUI::get_app_config(); + if (! app_config->legacy_datadir()) { + appconfig_vendors.set_vendors(*app_config); + } else { + // In case of legacy datadir, try to guess the preference based on the printer preset files that are present + const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; + for (fs::directory_iterator it(printer_dir); it != fs::directory_iterator(); ++it) { + auto needle = legacy_preset_map.find(it->path().filename().string()); + if (needle == legacy_preset_map.end()) { continue; } + + const auto &model = needle->second.first; + const auto &variant = needle->second.second; + appconfig_vendors.set_variant("PrusaResearch", model, variant, true); + } + } } void ConfigWizard::priv::index_refresh() @@ -639,22 +693,28 @@ void ConfigWizard::priv::on_custom_setup() set_page(page_firmware); } -void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle) +void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, PresetUpdater *updater) { const bool is_custom_setup = page_welcome->page_next() == page_firmware; if (! is_custom_setup) { const auto enabled_vendors = appconfig_vendors.vendors(); + + // Install bundles from resources if needed: + std::vector install_bundles; for (const auto &vendor_rsrc : vendors_rsrc) { const auto vendor = enabled_vendors.find(vendor_rsrc.first); if (vendor == enabled_vendors.end()) { continue; } - + size_t size_sum = 0; for (const auto &model : vendor->second) { size_sum += model.second.size(); } if (size_sum == 0) { continue; } // This vendor needs to be installed - PresetBundle::install_vendor_configbundle(vendor_rsrc.second); + install_bundles.emplace_back(vendor_rsrc.second); + } + if (install_bundles.size() > 0) { + updater->install_bundles_rsrc(app_config, std::move(install_bundles)); } app_config->set_vendors(appconfig_vendors); @@ -732,14 +792,10 @@ ConfigWizard::ConfigWizard(wxWindow *parent) : ConfigWizard::~ConfigWizard() {} -bool ConfigWizard::run(wxWindow *parent, PresetBundle *preset_bundle) +void ConfigWizard::run(PresetBundle *preset_bundle, PresetUpdater *updater) { - ConfigWizard wizard(parent); - if (wizard.ShowModal() == wxID_OK) { - wizard.p->apply_config(GUI::get_app_config(), preset_bundle); - return true; - } else { - return false; + if (ShowModal() == wxID_OK) { + p->apply_config(GUI::get_app_config(), preset_bundle, updater); } } diff --git a/xs/src/slic3r/GUI/ConfigWizard.hpp b/xs/src/slic3r/GUI/ConfigWizard.hpp index 40ecf09a1..b34467011 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard.hpp @@ -8,6 +8,7 @@ namespace Slic3r { class PresetBundle; +class PresetUpdater; namespace GUI { @@ -22,7 +23,7 @@ public: ConfigWizard &operator=(const ConfigWizard &) = delete; ~ConfigWizard(); - static bool run(wxWindow *parent, PresetBundle *preset_bundle); + void run(PresetBundle *preset_bundle, PresetUpdater *updater); private: struct priv; std::unique_ptr p; diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp index c93e0a80d..8aab1cc19 100644 --- a/xs/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -15,6 +15,7 @@ #include #include "libslic3r/PrintConfig.hpp" +#include "slic3r/Utils/PresetUpdater.hpp" #include "AppConfig.hpp" #include "Preset.hpp" #include "BedShapeDialog.hpp" @@ -24,8 +25,6 @@ namespace fs = boost::filesystem; namespace Slic3r { namespace GUI { -// typedef std::unordered_map VendorMap; - enum { CONTENT_WIDTH = 500, @@ -38,9 +37,26 @@ enum { struct PrinterPicker: wxPanel { + struct Checkbox : wxCheckBox + { + Checkbox(wxWindow *parent, const wxString &label, const std::string &model, const std::string &variant) : + wxCheckBox(parent, wxID_ANY, label), + model(model), + variant(variant) + {} + + std::string model; + std::string variant; + }; + + const std::string vendor_id; + std::vector cboxes; unsigned variants_checked; PrinterPicker(wxWindow *parent, const VendorProfile &vendor, const AppConfig &appconfig_vendors); + + void select_all(bool select); + void on_checkbox(const Checkbox *cbox, bool checked); }; struct ConfigWizardPage: wxPanel @@ -174,7 +190,7 @@ struct ConfigWizard::priv ConfigWizard *q; AppConfig appconfig_vendors; // TODO: use order-preserving container std::unordered_map vendors; // TODO: just set? - std::unordered_map vendors_rsrc; + std::unordered_map vendors_rsrc; std::unique_ptr custom_config; wxBoxSizer *topsizer = nullptr; @@ -208,7 +224,7 @@ struct ConfigWizard::priv void on_other_vendors(); void on_custom_setup(); - void apply_config(AppConfig *app_config, PresetBundle *preset_bundle); + void apply_config(AppConfig *app_config, PresetBundle *preset_bundle, PresetUpdater *updater); }; diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index d70b47840..916c407af 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include "wxExtensions.hpp" @@ -50,6 +51,7 @@ #include "Preferences.hpp" #include "PresetBundle.hpp" +#include "../Utils/PresetUpdater.hpp" #include "../Config/Snapshot.hpp" namespace Slic3r { namespace GUI { @@ -179,6 +181,7 @@ wxFrame *g_wxMainFrame = nullptr; wxNotebook *g_wxTabPanel = nullptr; AppConfig *g_AppConfig = nullptr; PresetBundle *g_PresetBundle= nullptr; +PresetUpdater *g_PresetUpdater = nullptr; std::vector g_tabs_list; @@ -212,6 +215,11 @@ void set_preset_bundle(PresetBundle *preset_bundle) g_PresetBundle = preset_bundle; } +void set_preset_updater(PresetUpdater *updater) +{ + g_PresetUpdater = updater; +} + std::vector& get_tabs_list() { return g_tabs_list; @@ -442,23 +450,51 @@ bool check_unsaved_changes() return dialog->ShowModal() == wxID_YES; } -bool config_wizard(bool fresh_start) +void config_wizard_startup(bool app_config_exists) +{ + if (! app_config_exists || g_PresetBundle->has_defauls_only()) { + config_wizard(true); + } else if (g_AppConfig->legacy_datadir()) { + // Looks like user has legacy pre-vendorbundle data directory, + // explain what this is and run the wizard + + const auto msg = _(L("Configuration update")); + const auto ext_msg = _(L( + "Slic3r PE now uses an updated configuration structure.\n\n" + + "So called 'System presets' have been introduced, which hold the built-in default settings for various " + "printers. These System presets cannot be modified, instead, users now may create their" + "own presets inheriting settings from one of the System presets.\n" + "An inheriting preset may either inherit a particular value from its parent or override it with a customized value.\n\n" + + // TODO: Assistant vs Wizard + "Please proceed with the Configuration wizard that follows to set up the new presets " + "and to choose whether to enable automatic preset updates." + )); + wxMessageDialog dlg(NULL, msg, _(L("Configuration update")), wxOK|wxCENTRE); + dlg.SetExtendedMessage(ext_msg); + const auto res = dlg.ShowModal(); + + config_wizard(true); + } +} + +void config_wizard(bool fresh_start) // TODO: fresh_start useful ? { if (g_wxMainFrame == nullptr) throw std::runtime_error("Main frame not set"); // Exit wizard if there are unsaved changes and the user cancels the action. if (! check_unsaved_changes()) - return false; + return; - // TODO: Offer "reset user profile" ??? - if (! ConfigWizard::run(g_wxMainFrame, g_PresetBundle)) - return false; + // TODO: Offer "reset user profile" ??? + ConfigWizard wizard(g_wxMainFrame); + wizard.run(g_PresetBundle, g_PresetUpdater); // Load the currently selected preset into the GUI, update the preset selection box. for (Tab *tab : g_tabs_list) tab->load_current_preset(); - return true; } void open_preferences_dialog(int event_preferences) diff --git a/xs/src/slic3r/GUI/GUI.hpp b/xs/src/slic3r/GUI/GUI.hpp index 2a3667eb3..6a23ed4eb 100644 --- a/xs/src/slic3r/GUI/GUI.hpp +++ b/xs/src/slic3r/GUI/GUI.hpp @@ -25,6 +25,7 @@ namespace Slic3r { class PresetBundle; class PresetCollection; class AppConfig; +class PresetUpdater; class DynamicPrintConfig; class TabIface; @@ -76,6 +77,7 @@ void set_main_frame(wxFrame *main_frame); void set_tab_panel(wxNotebook *tab_panel); void set_app_config(AppConfig *app_config); void set_preset_bundle(PresetBundle *preset_bundle); +void set_preset_updater(PresetUpdater *updater); AppConfig* get_app_config(); wxApp* get_app(); @@ -88,8 +90,11 @@ extern void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int // to notify the user whether he is aware that some preset changes will be lost. extern bool check_unsaved_changes(); -// Opens the first-time configuration wizard, returns true if wizard is finished & accepted. -extern bool config_wizard(bool fresh_start); +// Checks if configuration wizard needs to run, calls config_wizard if so +extern void config_wizard_startup(bool app_config_exists); + +// Opens the configuration wizard, returns true if wizard is finished & accepted. +extern void config_wizard(bool fresh_start); // Create "Preferences" dialog after selecting menu "Preferences" in Perl part extern void open_preferences_dialog(int event_preferences); diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp index 257c7f552..3291af7e0 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.cpp +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -51,6 +51,11 @@ struct Update version(version) {} + Update(fs::path &&source, fs::path &&target) : + source(source), + target(std::move(target)) + {} + std::string name() { return source.stem().string(); } }; @@ -83,6 +88,7 @@ struct PresetUpdater::priv void check_install_indices() const; Updates config_update() const; + void perform_updates(AppConfig *app_config, Updates &&updates) const; }; PresetUpdater::priv::priv(int event, AppConfig *app_config) : @@ -121,6 +127,7 @@ bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &targe .on_complete([&](std::string body, unsigned http_status) { fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc); file.write(body.c_str(), body.size()); + file.close(); fs::rename(tmp_path, target_path); res = true; }) @@ -269,6 +276,26 @@ Updates PresetUpdater::priv::config_update() const return updates; } +void PresetUpdater::priv::perform_updates(AppConfig *app_config, Updates &&updates) const +{ + SnapshotDB::singleton().take_snapshot(*app_config, Snapshot::SNAPSHOT_UPGRADE); + + for (const auto &update : updates) { + fs::copy_file(update.source, update.target, fs::copy_option::overwrite_if_exists); + + PresetBundle bundle; + bundle.load_configbundle(update.target.string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); + + auto preset_remover = [](const Preset &preset) { + fs::remove(preset.file); + }; + + for (const auto &preset : bundle.prints) { preset_remover(preset); } + for (const auto &preset : bundle.filaments) { preset_remover(preset); } + for (const auto &preset : bundle.printers) { preset_remover(preset); } + } +} + PresetUpdater::PresetUpdater(int version_online_event, AppConfig *app_config) : p(new priv(version_online_event, app_config)) @@ -306,7 +333,7 @@ void PresetUpdater::config_update(AppConfig *app_config) { if (! p->enabled_config_update) { return; } - const auto updates = p->config_update(); + auto updates = p->config_update(); if (updates.size() > 0) { const auto msg = _(L("Configuration update is available. Would you like to install it?")); @@ -330,26 +357,23 @@ void PresetUpdater::config_update(AppConfig *app_config) std::cerr << "After modal" << std::endl; if (res == wxID_YES) { // User gave clearance, updates are go - - SnapshotDB::singleton().take_snapshot(*app_config, Snapshot::SNAPSHOT_UPGRADE, ""); - - for (const auto &update : updates) { - fs::copy_file(update.source, update.target, fs::copy_option::overwrite_if_exists); - - PresetBundle bundle; - bundle.load_configbundle(update.target.string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); - - auto preset_remover = [](const Preset &preset) { - fs::remove(preset.file); - }; - - for (const auto &preset : bundle.prints) { preset_remover(preset); } - for (const auto &preset : bundle.filaments) { preset_remover(preset); } - for (const auto &preset : bundle.printers) { preset_remover(preset); } - } + p->perform_updates(app_config, std::move(updates)); } } } +void PresetUpdater::install_bundles_rsrc(AppConfig *app_config, std::vector &&bundles) +{ + Updates updates; + + for (const auto &bundle : bundles) { + auto path_in_rsrc = p->rsrc_path / bundle; + auto path_in_vendors = p->vendor_path / bundle; + updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors)); + } + + p->perform_updates(app_config, std::move(updates)); +} + } diff --git a/xs/src/slic3r/Utils/PresetUpdater.hpp b/xs/src/slic3r/Utils/PresetUpdater.hpp index 966dd1464..1499570db 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.hpp +++ b/xs/src/slic3r/Utils/PresetUpdater.hpp @@ -2,6 +2,7 @@ #define slic3r_PresetUpdate_hpp_ #include +#include namespace Slic3r { @@ -20,8 +21,8 @@ public: ~PresetUpdater(); void sync(AppConfig *app_config, PresetBundle *preset_bundle); - void config_update(AppConfig *app_config); + void install_bundles_rsrc(AppConfig *app_config, std::vector &&bundles); private: struct priv; std::unique_ptr p; diff --git a/xs/src/slic3r/Utils/Semver.hpp b/xs/src/slic3r/Utils/Semver.hpp index 2c27ce982..3e9276be6 100644 --- a/xs/src/slic3r/Utils/Semver.hpp +++ b/xs/src/slic3r/Utils/Semver.hpp @@ -32,8 +32,6 @@ public: ver.prerelease = prerelease ? std::strcpy(ver.prerelease, prerelease->c_str()) : nullptr; } - // TODO: throwing ctor ??? - static boost::optional parse(const std::string &str) { semver_t ver = semver_zero(); diff --git a/xs/xsp/GUI.xsp b/xs/xsp/GUI.xsp index 46e4ace83..0d9f0b62e 100644 --- a/xs/xsp/GUI.xsp +++ b/xs/xsp/GUI.xsp @@ -60,10 +60,10 @@ void set_app_config(AppConfig *app_config) bool check_unsaved_changes() %code%{ RETVAL=Slic3r::GUI::check_unsaved_changes(); %}; -bool config_wizard(int fresh_start) +void config_wizard_startup(int app_config_exists) %code%{ try { - RETVAL = Slic3r::GUI::config_wizard(fresh_start != 0); + Slic3r::GUI::config_wizard_startup(app_config_exists != 0); } catch (std::exception& e) { croak("%s\n", e.what()); } @@ -75,6 +75,9 @@ void open_preferences_dialog(int preferences_event) void set_preset_bundle(PresetBundle *preset_bundle) %code%{ Slic3r::GUI::set_preset_bundle(preset_bundle); %}; +void set_preset_updater(PresetUpdater* updater) + %code%{ Slic3r::GUI::set_preset_updater(updater); %}; + void add_frequently_changed_parameters(SV *ui_parent, SV *ui_sizer, SV *ui_p_sizer) %code%{ Slic3r::GUI::add_frequently_changed_parameters((wxWindow*)wxPli_sv_2_object(aTHX_ ui_parent, "Wx::Window"), (wxBoxSizer*)wxPli_sv_2_object(aTHX_ ui_sizer, "Wx::BoxSizer"), diff --git a/xs/xsp/my.map b/xs/xsp/my.map index c1ca58827..393338f3b 100644 --- a/xs/xsp/my.map +++ b/xs/xsp/my.map @@ -238,7 +238,6 @@ Ref O_OBJECT_SLIC3R_T PresetUpdater* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T -Clone O_OBJECT_SLIC3R_T OctoPrint* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt index 0214a158d..2539811b3 100644 --- a/xs/xsp/typemap.xspt +++ b/xs/xsp/typemap.xspt @@ -208,6 +208,8 @@ %typemap{Ref}{simple}; %typemap{PresetBundle*}; %typemap{Ref}{simple}; +%typemap{PresetUpdater*}; +%typemap{Ref}{simple}; %typemap{PresetHints*}; %typemap{Ref}{simple}; %typemap{TabIface*}; From d26c8e5336bbc92b79875141f43182ccb7461616 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Mon, 16 Apr 2018 17:43:23 +0200 Subject: [PATCH 24/38] Fix: Avoid the infamous `major` & `minor` macros on GCC --- xs/src/slic3r/Utils/Semver.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xs/src/slic3r/Utils/Semver.hpp b/xs/src/slic3r/Utils/Semver.hpp index 762f5bd70..bf3c78964 100644 --- a/xs/src/slic3r/Utils/Semver.hpp +++ b/xs/src/slic3r/Utils/Semver.hpp @@ -77,8 +77,8 @@ public: ~Semver() { ::semver_free(&ver); } // const accessors - int major() const { return ver.major; } - int minor() const { return ver.minor; } + int maj() const { return ver.major; } + int min() const { return ver.minor; } int patch() const { return ver.patch; } const char* prerelease() const { return ver.prerelease; } const char* metadata() const { return ver.metadata; } From 37cf839b2779bb5f14b0cdf7a408956e580e80cd Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Mon, 16 Apr 2018 18:33:33 +0200 Subject: [PATCH 25/38] ConfigWizard: Fix regression --- xs/src/slic3r/GUI/ConfigWizard.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index cd1ed64cb..467c13e16 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -50,9 +50,9 @@ wxDEFINE_EVENT(EVT_PRINTER_PICK, PrinterPickerEvent); PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, const AppConfig &appconfig_vendors) : wxPanel(parent), + vendor_id(vendor.id), variants_checked(0) { - const auto vendor_id = vendor.id; const auto &models = vendor.models; auto *sizer = new wxBoxSizer(wxVERTICAL); From b506aa11fab60fa5413fc82d3615e15a7a9624ea Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Tue, 17 Apr 2018 10:28:32 +0200 Subject: [PATCH 26/38] PresetUpdater: Fix: Compare versions when installing indices --- xs/src/slic3r/Utils/PresetUpdater.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp index 3291af7e0..dc3b95d79 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.cpp +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -220,9 +220,16 @@ void PresetUpdater::priv::check_install_indices() const if (path.extension() == ".idx") { const auto path_in_cache = cache_path / path.filename(); - // TODO: compare versions if (! fs::exists(path_in_cache)) { fs::copy_file(path, path_in_cache, fs::copy_option::overwrite_if_exists); + } else { + Index idx_rsrc, idx_cache; + idx_rsrc.load(path); + idx_cache.load(path_in_cache); + + if (idx_cache.version() < idx_rsrc.version()) { + fs::copy_file(path, path_in_cache, fs::copy_option::overwrite_if_exists); + } } } } @@ -267,8 +274,6 @@ Updates PresetUpdater::priv::config_update() const const auto cached_vp = VendorProfile::from_ini(path_in_cache, false); if (cached_vp.config_version == recommended->config_version) { updates.emplace_back(std::move(path_in_cache), bundle_path, *recommended); - } else { - // XXX: ? } } } From 6286c9ee7c8cd2f03b4de3a38e26d96f8c7108ec Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Tue, 17 Apr 2018 11:54:59 +0200 Subject: [PATCH 27/38] ConfigWizard & updating: Fixes & cleanups --- resources/profiles/PrusaResearch.idx | 1 - xs/src/slic3r/GUI/ConfigWizard.cpp | 34 ++++++++++++++++++++------- xs/src/slic3r/GUI/ConfigWizard.hpp | 2 ++ xs/src/slic3r/GUI/GUI.cpp | 11 ++------- xs/src/slic3r/Utils/PresetUpdater.cpp | 6 ++--- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/resources/profiles/PrusaResearch.idx b/resources/profiles/PrusaResearch.idx index 3bc2aeffd..b43e26663 100644 --- a/resources/profiles/PrusaResearch.idx +++ b/resources/profiles/PrusaResearch.idx @@ -1,7 +1,6 @@ # This is an example configuration version index. # The index contains version numbers min_slic3r_version =1.39.0 -max_slic3r_version= 1.39.5 1.1.1 1.1.0 0.2.0-alpha "some test comment" diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index 467c13e16..00f8399e6 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -203,7 +203,7 @@ void ConfigWizardPage::enable_next(bool enable) { parent->p->enable_next(enable) // Wizard pages PageWelcome::PageWelcome(ConfigWizard *parent) : - ConfigWizardPage(parent, _(L("Welcome to the Slic3r Configuration assistant")), _(L("Welcome"))), + ConfigWizardPage(parent, wxString::Format(_(L("Welcome to the Slic3r %s")), ConfigWizard::name()), _(L("Welcome"))), printer_picker(nullptr), others_buttons(new wxPanel(parent)) { @@ -257,16 +257,26 @@ PageUpdate::PageUpdate(ConfigWizard *parent) : preset_update(true) { const AppConfig *app_config = GUI::get_app_config(); + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); - append_text(_(L("TODO: text"))); - auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _(L("Check for Slic3r updates"))); + auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _(L("Check for application updates"))); box_slic3r->SetValue(app_config->get("version_check") == "1"); append(box_slic3r); + append_text(_(L("If enabled, Slic3r checks for new versions of Slic3r PE online. When a new version becomes available a notification is displayed at the next application startup (never during program usage). This is only a notification mechanisms, no automatic installation is done."))); + + append_spacer(VERTICAL_SPACING); - append_text(_(L("TODO: text"))); auto *box_presets = new wxCheckBox(this, wxID_ANY, _(L("Update built-in Presets automatically"))); box_presets->SetValue(app_config->get("preset_update") == "1"); append(box_presets); + append_text(_(L("If enabled, Slic3r downloads updates of built-in system presets in the background. These updates are downloaded into a separate temporary location. When a new preset version becomes available it is offered at application startup."))); + const auto text_bold = _(L("Updates are never applied without user's consent and never overwrite user's customized settings.")); + auto *label_bold = new wxStaticText(this, wxID_ANY, text_bold); + label_bold->SetFont(boldfont); + label_bold->Wrap(CONTENT_WIDTH); + append(label_bold); + append_text(_(L("Additionally a backup snapshot of the whole configuration is created before an update is applied."))); box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); @@ -277,15 +287,12 @@ PageVendors::PageVendors(ConfigWizard *parent) : { append_text(_(L("Pick another vendor supported by Slic3r PE:"))); - // const PresetBundle &bundle = wizard_p()->bundle_vendors; - // const auto &vendors = wizard_p()->vendors; auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); boldfont.SetWeight(wxFONTWEIGHT_BOLD); AppConfig &appconfig_vendors = this->wizard_p()->appconfig_vendors; wxArrayString choices_vendors; - // for (const auto &vendor : vendors) { for (const auto vendor_pair : wizard_p()->vendors) { const auto &vendor = vendor_pair.second; if (vendor.id == "PrusaResearch") { continue; } @@ -737,7 +744,7 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese // Public ConfigWizard::ConfigWizard(wxWindow *parent, bool fresh_start) : - wxDialog(parent, wxID_ANY, _(L("Configuration Assistant")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + wxDialog(parent, wxID_ANY, name(), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), p(new priv(this)) { p->fresh_start = fresh_start; @@ -804,5 +811,16 @@ void ConfigWizard::run(PresetBundle *preset_bundle, PresetUpdater *updater) } +const wxString& ConfigWizard::name() +{ + // A different naming convention is used for the Wizard on Windows vs. OSX & GTK. +#if WIN32 + static const wxString config_wizard_name = _(L("Configuration Wizard")); +#else + static const wxString config_wizard_name = _(L("Configuration Assistant")); +#endif + return config_wizard_name; +} + } } diff --git a/xs/src/slic3r/GUI/ConfigWizard.hpp b/xs/src/slic3r/GUI/ConfigWizard.hpp index d1d994ac1..66c8b3a95 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard.hpp @@ -24,6 +24,8 @@ public: ~ConfigWizard(); void run(PresetBundle *preset_bundle, PresetUpdater *updater); + + static const wxString& name(); private: struct priv; std::unique_ptr p; diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index 96d137165..bfdd46287 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -377,16 +377,9 @@ void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_l auto local_menu = new wxMenu(); wxWindowID config_id_base = wxWindow::NewControlId((int)ConfigMenuCnt); - // A different naming convention is used for the Wizard on Windows vs. OSX & GTK. -#if WIN32 - auto config_wizard_menu = _(L("Configuration Wizard")); - auto config_wizard_tooltip = _(L("Run configuration wizard")); -#else - auto config_wizard_menu = _(L("Configuration Assistant")); - auto config_wizard_tooltip = _(L("Run configuration Assistant")); -#endif + const auto config_wizard_tooltip = wxString::Format(_(L("Run %s")), ConfigWizard::name()); // Cmd+, is standard on OS X - what about other operating systems? - local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_menu + "\u2026", config_wizard_tooltip); + local_menu->Append(config_id_base + ConfigMenuWizard, ConfigWizard::name() + "\u2026", config_wizard_tooltip); local_menu->Append(config_id_base + ConfigMenuSnapshots, _(L("Configuration Snapshots\u2026")), _(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"))); diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp index dc3b95d79..939be863c 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.cpp +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -114,8 +115,6 @@ bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &targe { std::cerr << "get_file(): " << url << " -> " << target_path << std::endl; - // TODO: Proper caching - bool res = false; fs::path tmp_path = target_path; tmp_path += TMP_EXTENSION; @@ -251,7 +250,8 @@ Updates PresetUpdater::priv::config_update() const const auto ver_current = idx.find(vp.config_version); if (ver_current == idx.end()) { - // TODO: throw / ignore ? + BOOST_LOG_TRIVIAL(warning) << boost::format("Preset bundle (`%1%`) version not found in index: %2%") % idx.vendor() % vp.config_version.to_string(); + continue; } const auto recommended = idx.recommended(); From df03b8e4e891918b95d9accad90c2e071ce088dd Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Tue, 17 Apr 2018 16:59:53 +0200 Subject: [PATCH 28/38] PresetUpdater: Notify about Slic3r updates --- lib/Slic3r/GUI.pm | 11 ++- xs/src/slic3r/GUI/AppConfig.cpp | 29 +------ xs/src/slic3r/GUI/AppConfig.hpp | 6 -- xs/src/slic3r/GUI/ConfigWizard.cpp | 2 +- xs/src/slic3r/GUI/PresetBundle.cpp | 13 --- xs/src/slic3r/GUI/PresetBundle.hpp | 4 - xs/src/slic3r/Utils/PresetUpdater.cpp | 116 ++++++++++++++++++++++---- xs/src/slic3r/Utils/PresetUpdater.hpp | 9 +- xs/xsp/GUI_AppConfig.xsp | 1 - xs/xsp/GUI_Preset.xsp | 8 -- xs/xsp/Utils_PresetUpdater.xsp | 7 +- 11 files changed, 119 insertions(+), 87 deletions(-) diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 473fc6b90..5ff8fdde1 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -87,6 +87,7 @@ sub OnInit { $self->{app_config} = Slic3r::GUI::AppConfig->new; $self->{preset_bundle} = Slic3r::GUI::PresetBundle->new; + Slic3r::GUI::set_app_config($self->{app_config}); # just checking for existence of Slic3r::data_dir is not enough: it may be an empty directory # supplied as argument to --datadir; in that case we should still run the wizard @@ -102,16 +103,18 @@ sub OnInit { $self->{app_config}->save; # my $version_check = $self->{app_config}->get('version_check'); - $self->{preset_updater} = Slic3r::PresetUpdater->new($VERSION_ONLINE_EVENT, $self->{app_config}); + $self->{preset_updater} = Slic3r::PresetUpdater->new($VERSION_ONLINE_EVENT); Slic3r::GUI::set_preset_updater($self->{preset_updater}); - eval { $self->{preset_updater}->config_update($self->{app_config}) }; + eval { + $self->{preset_updater}->slic3r_update_notify(); + $self->{preset_updater}->config_update(); + }; if ($@) { warn $@ . "\n"; fatal_error(undef, $@); } # my $slic3r_update = $self->{app_config}->slic3r_update_avail; - Slic3r::GUI::set_app_config($self->{app_config}); Slic3r::GUI::load_language(); # Suppress the '- default -' presets. @@ -150,7 +153,7 @@ sub OnInit { Slic3r::GUI::config_wizard_startup($app_conf_exists); # TODO: call periodically? - $self->{preset_updater}->sync($self->{app_config}, $self->{preset_bundle}); + $self->{preset_updater}->sync($self->{preset_bundle}); }); # The following event is emited by the C++ menu implementation of application language change. diff --git a/xs/src/slic3r/GUI/AppConfig.cpp b/xs/src/slic3r/GUI/AppConfig.cpp index 5104078b1..100b2d69b 100644 --- a/xs/src/slic3r/GUI/AppConfig.cpp +++ b/xs/src/slic3r/GUI/AppConfig.cpp @@ -51,7 +51,7 @@ void AppConfig::set_defaults() set("version_check", "1"); // TODO: proper URL if (get("version_check_url").empty()) - set("version_check_url", "https://gist.githubusercontent.com/vojtechkral/4d8fd4a3b8699a01ec892c264178461c/raw/e9187c3e15ceaf1a90f29b7c43cf3ccc746140f0/slic3rPE.version"); + set("version_check_url", "https://gist.githubusercontent.com/vojtechkral/4d8fd4a3b8699a01ec892c264178461c/raw/2f05a64db19e45a7f8fe2cedeff555d544af679b/slic3rPE.version"); if (get("preset_update").empty()) set("preset_update", "1"); @@ -230,33 +230,6 @@ void AppConfig::reset_selections() } } -bool AppConfig::version_check_enabled() const -{ - return get("version_check") == "1"; -} - -bool AppConfig::slic3r_update_avail() const -{ - // FIXME: Update with Semver - // TODO: probably need to move semver to libslic3r - return version_check_enabled() && get("version_online") != SLIC3R_VERSION; -} - -Semver AppConfig::get_slic3r_version() const -{ - auto res = Semver::parse(get("version")); - if (! res) { - throw std::runtime_error(std::string("Could not parse Slic3r version string in application config.")); - } else { - return *res; - } -} - -void AppConfig::set_slic3r_version(const Semver &version) -{ - set("version", version.to_string()); -} - std::string AppConfig::config_path() { return (boost::filesystem::path(Slic3r::data_dir()) / "slic3r.ini").make_preferred().string(); diff --git a/xs/src/slic3r/GUI/AppConfig.hpp b/xs/src/slic3r/GUI/AppConfig.hpp index 88ba0a662..6dcfec046 100644 --- a/xs/src/slic3r/GUI/AppConfig.hpp +++ b/xs/src/slic3r/GUI/AppConfig.hpp @@ -90,12 +90,6 @@ public: // the first non-default preset when called. void reset_selections(); - // Whether the Slic3r version available online differs from this one - bool version_check_enabled() const; - bool slic3r_update_avail() const; - Semver get_slic3r_version() const; - void set_slic3r_version(const Semver &version); - // Get the default config path from Slic3r::data_dir(). static std::string config_path(); diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index 00f8399e6..c4ca7a886 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -721,7 +721,7 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese install_bundles.emplace_back(vendor_rsrc.second); } if (install_bundles.size() > 0) { - updater->install_bundles_rsrc(app_config, std::move(install_bundles)); + updater->install_bundles_rsrc(std::move(install_bundles)); } app_config->set_vendors(appconfig_vendors); diff --git a/xs/src/slic3r/GUI/PresetBundle.cpp b/xs/src/slic3r/GUI/PresetBundle.cpp index beec4fe07..6f0754a03 100644 --- a/xs/src/slic3r/GUI/PresetBundle.cpp +++ b/xs/src/slic3r/GUI/PresetBundle.cpp @@ -695,19 +695,6 @@ static void flatten_configbundle_hierarchy(boost::property_tree::ptree &tree) flatten_configbundle_hierarchy(tree, "printer"); } -// Load a config bundle file, into presets and store the loaded presets into separate files -// of the local configuration directory. -void PresetBundle::install_vendor_configbundle(const std::string &src_path0) -{ - boost::filesystem::path src_path(src_path0); - install_vendor_configbundle(src_path); -} - -void PresetBundle::install_vendor_configbundle(const boost::filesystem::path &src_path) -{ - boost::filesystem::copy_file(src_path, (boost::filesystem::path(data_dir()) / "vendor" / src_path.filename()).make_preferred(), boost::filesystem::copy_option::overwrite_if_exists); -} - // Load a config bundle file, into presets and store the loaded presets into separate files // of the local configuration directory. size_t PresetBundle::load_configbundle(const std::string &path, unsigned int flags) diff --git a/xs/src/slic3r/GUI/PresetBundle.hpp b/xs/src/slic3r/GUI/PresetBundle.hpp index 651b95701..27add21ee 100644 --- a/xs/src/slic3r/GUI/PresetBundle.hpp +++ b/xs/src/slic3r/GUI/PresetBundle.hpp @@ -92,10 +92,6 @@ public: // Load the config bundle, store it to the user profile directory by default. size_t load_configbundle(const std::string &path, unsigned int flags = LOAD_CFGBNDLE_SAVE); - // Install the Vendor specific config bundle into user's directory. - void install_vendor_configbundle(const std::string &src_path); - static void install_vendor_configbundle(const boost::filesystem::path &src_path); - // Export a config bundle file containing all the presets and the names of the active presets. void export_configbundle(const std::string &path); // , const DynamicPrintConfig &settings); diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp index 939be863c..439a619c6 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.cpp +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -14,7 +14,15 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include "libslic3r/libslic3r.h" #include "libslic3r/Utils.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/PresetBundle.hpp" @@ -40,6 +48,63 @@ static const char *INDEX_FILENAME = "index.idx"; static const char *TMP_EXTENSION = ".download"; +struct UpdateNotification : wxDialog +{ + // If this dialog gets any more complex, it should probably be factored out... + + enum { + CONTENT_WIDTH = 400, + BORDER = 30, + SPACING = 15, + }; + + wxCheckBox *cbox; + + UpdateNotification(const Semver &ver_current, const Semver &ver_online) : wxDialog(nullptr, wxID_ANY, _(L("Update available"))) + { + auto *topsizer = new wxBoxSizer(wxHORIZONTAL); + auto *sizer = new wxBoxSizer(wxVERTICAL); + + const auto url = wxString::Format("https://github.com/prusa3d/Slic3r/releases/tag/version_%s", ver_online.to_string()); + auto *link = new wxHyperlinkCtrl(this, wxID_ANY, url, url); + + auto *text = new wxStaticText(this, wxID_ANY, + _(L("New version of Slic3r PE is available. To download, follow the link below."))); + const auto link_width = link->GetSize().GetWidth(); + text->Wrap(CONTENT_WIDTH > link_width ? CONTENT_WIDTH : link_width); + sizer->Add(text); + sizer->AddSpacer(SPACING); + + auto *versions = new wxFlexGridSizer(2, 0, SPACING); + versions->Add(new wxStaticText(this, wxID_ANY, _(L("Current version:")))); + versions->Add(new wxStaticText(this, wxID_ANY, ver_current.to_string())); + versions->Add(new wxStaticText(this, wxID_ANY, _(L("New version:")))); + versions->Add(new wxStaticText(this, wxID_ANY, ver_online.to_string())); + sizer->Add(versions); + sizer->AddSpacer(SPACING); + + sizer->Add(link); + sizer->AddSpacer(2*SPACING); + + cbox = new wxCheckBox(this, wxID_ANY, _(L("Don't notify about new versions any more"))); + sizer->Add(cbox); + sizer->AddSpacer(SPACING); + + auto *ok = new wxButton(this, wxID_OK); + ok->SetFocus(); + sizer->Add(ok, 0, wxALIGN_CENTRE_HORIZONTAL); + + auto *logo = new wxStaticBitmap(this, wxID_ANY, wxBitmap(GUI::from_u8(Slic3r::var("Slic3r_192px.png")), wxBITMAP_TYPE_PNG)); + + topsizer->Add(logo, 0, wxALL, BORDER); + topsizer->Add(sizer, 0, wxALL, BORDER); + + SetSizerAndFit(topsizer); + } + + bool disable_version_check() const { return cbox->GetValue(); } +}; + struct Update { fs::path source; @@ -79,7 +144,7 @@ struct PresetUpdater::priv bool cancel; std::thread thread; - priv(int event, AppConfig *app_config); + priv(int version_online_event); void set_download_prefs(AppConfig *app_config); bool get_file(const std::string &url, const fs::path &target_path) const; @@ -89,17 +154,17 @@ struct PresetUpdater::priv void check_install_indices() const; Updates config_update() const; - void perform_updates(AppConfig *app_config, Updates &&updates) const; + void perform_updates(Updates &&updates) const; }; -PresetUpdater::priv::priv(int event, AppConfig *app_config) : - version_online_event(event), +PresetUpdater::priv::priv(int version_online_event) : + version_online_event(version_online_event), cache_path(fs::path(Slic3r::data_dir()) / "cache"), rsrc_path(fs::path(resources_dir()) / "profiles"), vendor_path(fs::path(Slic3r::data_dir()) / "vendor"), cancel(false) { - set_download_prefs(app_config); + set_download_prefs(GUI::get_app_config()); check_install_indices(); index_db = std::move(Index::load_db()); } @@ -281,9 +346,9 @@ Updates PresetUpdater::priv::config_update() const return updates; } -void PresetUpdater::priv::perform_updates(AppConfig *app_config, Updates &&updates) const +void PresetUpdater::priv::perform_updates(Updates &&updates) const { - SnapshotDB::singleton().take_snapshot(*app_config, Snapshot::SNAPSHOT_UPGRADE); + SnapshotDB::singleton().take_snapshot(*GUI::get_app_config(), Snapshot::SNAPSHOT_UPGRADE); for (const auto &update : updates) { fs::copy_file(update.source, update.target, fs::copy_option::overwrite_if_exists); @@ -302,8 +367,8 @@ void PresetUpdater::priv::perform_updates(AppConfig *app_config, Updates &&updat } -PresetUpdater::PresetUpdater(int version_online_event, AppConfig *app_config) : - p(new priv(version_online_event, app_config)) +PresetUpdater::PresetUpdater(int version_online_event) : + p(new priv(version_online_event)) {} @@ -317,9 +382,9 @@ PresetUpdater::~PresetUpdater() } } -void PresetUpdater::sync(AppConfig *app_config, PresetBundle *preset_bundle) +void PresetUpdater::sync(PresetBundle *preset_bundle) { - p->set_download_prefs(app_config); + p->set_download_prefs(GUI::get_app_config()); if (!p->enabled_version_check && !p->enabled_config_update) { return; } // Copy the whole vendors data for use in the background thread @@ -334,7 +399,28 @@ void PresetUpdater::sync(AppConfig *app_config, PresetBundle *preset_bundle) })); } -void PresetUpdater::config_update(AppConfig *app_config) +void PresetUpdater::slic3r_update_notify() +{ + if (! p->enabled_version_check) { return; } + + auto* app_config = GUI::get_app_config(); + const auto ver_slic3r = Semver::parse(SLIC3R_VERSION); + const auto ver_online = Semver::parse(app_config->get("version_online")); + if (! ver_slic3r) { + throw std::runtime_error("Could not parse Slic3r version string: " SLIC3R_VERSION); + } + + if (ver_online && *ver_online > *ver_slic3r) { + UpdateNotification notification(*ver_slic3r, *ver_online); + notification.ShowModal(); + if (notification.disable_version_check()) { + app_config->set("version_check", "0"); + p->enabled_version_check = false; + } + } +} + +void PresetUpdater::config_update() const { if (! p->enabled_config_update) { return; } @@ -362,12 +448,12 @@ void PresetUpdater::config_update(AppConfig *app_config) std::cerr << "After modal" << std::endl; if (res == wxID_YES) { // User gave clearance, updates are go - p->perform_updates(app_config, std::move(updates)); + p->perform_updates(std::move(updates)); } } } -void PresetUpdater::install_bundles_rsrc(AppConfig *app_config, std::vector &&bundles) +void PresetUpdater::install_bundles_rsrc(std::vector &&bundles) { Updates updates; @@ -377,7 +463,7 @@ void PresetUpdater::install_bundles_rsrc(AppConfig *app_config, std::vectorperform_updates(app_config, std::move(updates)); + p->perform_updates(std::move(updates)); } diff --git a/xs/src/slic3r/Utils/PresetUpdater.hpp b/xs/src/slic3r/Utils/PresetUpdater.hpp index 1499570db..a53ed86eb 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.hpp +++ b/xs/src/slic3r/Utils/PresetUpdater.hpp @@ -13,16 +13,17 @@ class PresetBundle; class PresetUpdater { public: - PresetUpdater(int version_online_event, AppConfig *app_config); + PresetUpdater(int version_online_event); PresetUpdater(PresetUpdater &&) = delete; PresetUpdater(const PresetUpdater &) = delete; PresetUpdater &operator=(PresetUpdater &&) = delete; PresetUpdater &operator=(const PresetUpdater &) = delete; ~PresetUpdater(); - void sync(AppConfig *app_config, PresetBundle *preset_bundle); - void config_update(AppConfig *app_config); - void install_bundles_rsrc(AppConfig *app_config, std::vector &&bundles); + void sync(PresetBundle *preset_bundle); + void slic3r_update_notify(); + void config_update() const; + void install_bundles_rsrc(std::vector &&bundles); private: struct priv; std::unique_ptr p; diff --git a/xs/xsp/GUI_AppConfig.xsp b/xs/xsp/GUI_AppConfig.xsp index de0e5a22b..08a88883d 100644 --- a/xs/xsp/GUI_AppConfig.xsp +++ b/xs/xsp/GUI_AppConfig.xsp @@ -43,5 +43,4 @@ void update_last_output_dir(char *dir); void reset_selections(); - bool slic3r_update_avail() const; }; diff --git a/xs/xsp/GUI_Preset.xsp b/xs/xsp/GUI_Preset.xsp index ecf6f5a22..2c63db10c 100644 --- a/xs/xsp/GUI_Preset.xsp +++ b/xs/xsp/GUI_Preset.xsp @@ -117,14 +117,6 @@ PresetCollection::arrayref() croak("Loading of a config bundle %s failed:\n%s\n", path, e.what()); } %}; - void install_vendor_configbundle(const char *path) - %code%{ - try { - THIS->install_vendor_configbundle(std::string(path)); - } catch (std::exception& e) { - croak("Installing a vendor config bundle %s failed:\n%s\n", path, e.what()); - } - %}; void export_configbundle(char *path) %code%{ try { diff --git a/xs/xsp/Utils_PresetUpdater.xsp b/xs/xsp/Utils_PresetUpdater.xsp index 3a4d55a01..53c3aa985 100644 --- a/xs/xsp/Utils_PresetUpdater.xsp +++ b/xs/xsp/Utils_PresetUpdater.xsp @@ -6,7 +6,8 @@ %} %name{Slic3r::PresetUpdater} class PresetUpdater { - PresetUpdater(int version_online_event, AppConfig *app_config); - void sync(AppConfig *app_config, PresetBundle* preset_bundle); - void config_update(AppConfig *app_config); + PresetUpdater(int version_online_event); + void sync(PresetBundle* preset_bundle); + void slic3r_update_notify(); + void config_update(); }; From c884f3b213981b89d7f76a6532e961477705727c Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Tue, 17 Apr 2018 17:11:56 +0200 Subject: [PATCH 29/38] Display app update notification with the main frame --- lib/Slic3r/GUI.pm | 11 ++--------- xs/src/slic3r/Utils/PresetUpdater.cpp | 9 +++++++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 5ff8fdde1..10375a9dc 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -102,18 +102,13 @@ sub OnInit { $self->{app_config}->set('version', $Slic3r::VERSION); $self->{app_config}->save; - # my $version_check = $self->{app_config}->get('version_check'); $self->{preset_updater} = Slic3r::PresetUpdater->new($VERSION_ONLINE_EVENT); Slic3r::GUI::set_preset_updater($self->{preset_updater}); - eval { - $self->{preset_updater}->slic3r_update_notify(); - $self->{preset_updater}->config_update(); - }; + eval { $self->{preset_updater}->config_update(); }; if ($@) { warn $@ . "\n"; fatal_error(undef, $@); } - # my $slic3r_update = $self->{app_config}->slic3r_update_avail; Slic3r::GUI::load_language(); @@ -139,7 +134,6 @@ sub OnInit { ); $self->SetTopWindow($frame); - #EVT_IDLE($frame, sub { EVT_IDLE($self->{mainframe}, sub { while (my $cb = shift @cb) { $cb->(); @@ -151,8 +145,7 @@ sub OnInit { # before the UI was up and running. $self->CallAfter(sub { Slic3r::GUI::config_wizard_startup($app_conf_exists); - - # TODO: call periodically? + $self->{preset_updater}->slic3r_update_notify(); $self->{preset_updater}->sync($self->{preset_bundle}); }); diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp index 439a619c6..2140a2c91 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.cpp +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -86,7 +86,7 @@ struct UpdateNotification : wxDialog sizer->Add(link); sizer->AddSpacer(2*SPACING); - cbox = new wxCheckBox(this, wxID_ANY, _(L("Don't notify about new versions any more"))); + cbox = new wxCheckBox(this, wxID_ANY, _(L("Don't notify about new releases any more"))); sizer->Add(cbox); sizer->AddSpacer(SPACING); @@ -136,6 +136,7 @@ struct PresetUpdater::priv bool enabled_version_check; bool enabled_config_update; std::string version_check_url; + bool had_config_update; fs::path cache_path; fs::path rsrc_path; @@ -159,6 +160,7 @@ struct PresetUpdater::priv PresetUpdater::priv::priv(int version_online_event) : version_online_event(version_online_event), + had_config_update(false), cache_path(fs::path(Slic3r::data_dir()) / "cache"), rsrc_path(fs::path(resources_dir()) / "profiles"), vendor_path(fs::path(Slic3r::data_dir()) / "vendor"), @@ -401,7 +403,8 @@ void PresetUpdater::sync(PresetBundle *preset_bundle) void PresetUpdater::slic3r_update_notify() { - if (! p->enabled_version_check) { return; } + if (! p->enabled_version_check || p->had_config_update) { return; } + // ^ We don't want to bother the user with updates multiple times, put off till next time. auto* app_config = GUI::get_app_config(); const auto ver_slic3r = Semver::parse(SLIC3R_VERSION); @@ -450,6 +453,8 @@ void PresetUpdater::config_update() const // User gave clearance, updates are go p->perform_updates(std::move(updates)); } + + p->had_config_update = true; } } From 81c6ad3ab72be74752e802f83515a41112428eab Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Wed, 18 Apr 2018 11:40:43 +0200 Subject: [PATCH 30/38] ConfigWizard: Add reset option, fixes Fix mock vendors --- resources/profiles/BarBaz.ini | 44 +++++++++++----------- resources/profiles/Foobar.ini | 44 +++++++++++----------- xs/src/slic3r/GUI/ConfigWizard.cpp | 32 ++++++++++++---- xs/src/slic3r/GUI/ConfigWizard.hpp | 2 +- xs/src/slic3r/GUI/ConfigWizard_private.hpp | 9 +++-- xs/src/slic3r/GUI/GUI.cpp | 33 ++++++++-------- xs/src/slic3r/GUI/GUI.hpp | 2 +- xs/src/slic3r/Utils/PresetUpdater.cpp | 12 +++--- xs/src/slic3r/Utils/PresetUpdater.hpp | 2 +- 9 files changed, 102 insertions(+), 78 deletions(-) diff --git a/resources/profiles/BarBaz.ini b/resources/profiles/BarBaz.ini index 7c8cd3a6b..ed2686cdc 100644 --- a/resources/profiles/BarBaz.ini +++ b/resources/profiles/BarBaz.ini @@ -1,4 +1,4 @@ -# Print profiles for the Prusa Research printers. +# Print profiles for the BarBaz Research printers. [vendor] # Vendor name will be shown by the Config Wizard. @@ -735,11 +735,11 @@ first_layer_temperature = 195 start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" temperature = 195 -[filament:Prusa ABS] +[filament:BarBaz ABS] inherits = *ABS* filament_notes = "List of materials tested with standart ABS print settings for MK2:\n\nEsun ABS\nFil-A-Gehr ABS\nHatchboxABS\nPlasty Mladeč ABS" -[filament:Prusa HIPS] +[filament:BarBaz HIPS] inherits = *ABS* bridge_fan_speed = 50 cooling = 1 @@ -755,11 +755,11 @@ min_fan_speed = 20 start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" temperature = 220 -[filament:Prusa PET] +[filament:BarBaz PET] inherits = *PET* filament_notes = "List of manufacturers tested with standart PET print settings for MK2:\n\nE3D Edge\nFillamentum CPE GH100\nPlasty Mladeč PETG" -[filament:Prusa PLA] +[filament:BarBaz PLA] inherits = *PLA* filament_notes = "List of materials tested with standart PLA print settings for MK2:\n\nDas Filament\nEsun PLA\nEUMAKERS PLA\nFiberlogy HD-PLA\nFillamentum PLA\nFloreon3D\nHatchbox PLA\nPlasty Mladeč PLA\nPrimavalue PLA\nProto pasta Matte Fiber\nVerbatim PLA\nVerbatim BVOH" @@ -855,7 +855,7 @@ min_layer_height = 0.07 nozzle_diameter = 0.4 octoprint_apikey = octoprint_host = -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\n +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_BarBaz3D\nPRINTER_MODEL_MK2\n printer_settings_id = retract_before_travel = 1 retract_before_wipe = 0% @@ -882,7 +882,7 @@ z_offset = 0 printer_model = M2 printer_variant = 0.4 default_print_profile = 0.15mm OPTIMAL -default_filament_profile = Prusa PLA +default_filament_profile = BarBaz PLA [printer:*multimaterial*] inherits = *common* @@ -903,7 +903,7 @@ printer_model = M3 [printer:*mm-single*] inherits = *multimaterial* end_gcode = G1 E-4 F2100.00000\nG91\nG1 Z1 F7200.000\nG90\nG1 X245 Y1\nG1 X240 E4\nG1 F4000\nG1 X190 E2.7 \nG1 F4600\nG1 X110 E2.8\nG1 F5200\nG1 X40 E3 \nG1 E-15.0000 F5000\nG1 E-50.0000 F5400\nG1 E-15.0000 F3000\nG1 E-12.0000 F2000\nG1 F1600\nG1 X0 Y1 E3.0000\nG1 X50 Y1 E-5.0000\nG1 F2000\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-3.0000\nG4 S0\nM107 ; fan off\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nG28 X0 ; home X axis\nM84 ; disable motors\n\n -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_BarBaz3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN start_gcode = M115 U3.1.0 ; tell printer latest fw version\nM201 X9000 Y9000 Z500 E10000 ; sets maximum accelerations, mm/sec^2\nM203 X500 Y500 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1500 T1500 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.2 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\n; Start G-Code sequence START\nT?\nM104 S[first_layer_temperature]\nM140 S[first_layer_bed_temperature]\nM109 S[first_layer_temperature]\nM190 S[first_layer_bed_temperature]\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nG28 W\nG80\nG92 E0.0\nM203 E100\nM92 E140\nG1 Z0.250 F7200.000\nG1 X50.0 E80.0 F1000.0\nG1 X160.0 E20.0 F1000.0\nG1 Z0.200 F7200.000\nG1 X220.0 E13 F1000.0\nG1 X240.0 E0 F1000.0\nG1 E-4 F1000.0\nG92 E0.0 [printer:*mm-multi*] @@ -911,14 +911,14 @@ inherits = *multimaterial* end_gcode = {if not has_wipe_tower}\n; Pull the filament into the cooling tubes.\nG1 E-4 F2100.00000\nG91\nG1 Z1 F7200.000\nG90\nG1 X245 Y1\nG1 X240 E4\nG1 F4000\nG1 X190 E2.7 \nG1 F4600\nG1 X110 E2.8\nG1 F5200\nG1 X40 E3 \nG1 E-15.0000 F5000\nG1 E-50.0000 F5400\nG1 E-15.0000 F3000\nG1 E-12.0000 F2000\nG1 F1600\nG1 X0 Y1 E3.0000\nG1 X50 Y1 E-5.0000\nG1 F2000\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-3.0000\nG4 S0\n{endif}\nM107 ; fan off\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nG28 X0 ; home X axis\nM84 ; disable motors extruder_colour = #FFAA55;#5182DB;#4ECDD3;#FB7259 nozzle_diameter = 0.4,0.4,0.4,0.4 -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_BarBaz3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN start_gcode = M115 U3.1.0 ; tell printer latest fw version\nM201 X9000 Y9000 Z500 E10000 ; sets maximum accelerations, mm/sec^2\nM203 X500 Y500 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1500 T1500 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.2 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\n; Start G-Code sequence START\nT[initial_tool]\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nG28 W\nG80\nG92 E0.0\nM203 E100 ; set max feedrate\nM92 E140 ; E-steps per filament milimeter\n{if not has_wipe_tower}\nG1 Z0.250 F7200.000\nG1 X50.0 E80.0 F1000.0\nG1 X160.0 E20.0 F1000.0\nG1 Z0.200 F7200.000\nG1 X220.0 E13 F1000.0\nG1 X240.0 E0 F1000.0\nG1 E-4 F1000.0\n{endif}\nG92 E0.0 variable_layer_height = 0 -[printer:Original Prusa i3 MK2] +[printer:BarBaz i3 MK2] inherits = *common* -[printer:Original Prusa i3 MK2 0.25 nozzle] +[printer:BarBaz i3 MK2 0.25 nozzle] inherits = *common* max_layer_height = 0.1 min_layer_height = 0.05 @@ -929,56 +929,56 @@ variable_layer_height = 0 printer_variant = 0.25 default_print_profile = 0.10mm DETAIL 0.25 nozzle -[printer:Original Prusa i3 MK2 0.6 nozzle] +[printer:BarBaz i3 MK2 0.6 nozzle] inherits = *common* max_layer_height = 0.35 min_layer_height = 0.1 nozzle_diameter = 0.6 printer_variant = 0.6 -[printer:Original Prusa i3 MK2 MM Single Mode] +[printer:BarBaz i3 MK2 MM Single Mode] inherits = *mm-single* -[printer:Original Prusa i3 MK2 MM Single Mode 0.6 nozzle] +[printer:BarBaz i3 MK2 MM Single Mode 0.6 nozzle] inherits = *mm-single* nozzle_diameter = 0.6 printer_variant = 0.6 -[printer:Original Prusa i3 MK2 MultiMaterial] +[printer:BarBaz i3 MK2 MultiMaterial] inherits = *mm-multi* nozzle_diameter = 0.4,0.4,0.4,0.4 -[printer:Original Prusa i3 MK2 MultiMaterial 0.6 nozzle] +[printer:BarBaz i3 MK2 MultiMaterial 0.6 nozzle] inherits = *mm-multi* nozzle_diameter = 0.6,0.6,0.6,0.6 printer_variant = 0.6 -[printer:Original Prusa i3 MK3] +[printer:BarBaz i3 MK3] inherits = *common* end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_BarBaz3D\nPRINTER_MODEL_MK3\n retract_lift_below = 209 start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} printer_model = M1 default_print_profile = 0.15mm OPTIMAL MK3 -[printer:Original Prusa i3 MK3 0.25 nozzle] +[printer:BarBaz i3 MK3 0.25 nozzle] inherits = *common* nozzle_diameter = 0.25 printer_variant = 0.25 end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_BarBaz3D\nPRINTER_MODEL_MK3\n retract_lift_below = 209 start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} printer_model = M1 default_print_profile = 0.10mm DETAIL MK3 -[printer:Original Prusa i3 MK3 0.6 nozzle] +[printer:BarBaz i3 MK3 0.6 nozzle] inherits = *common* nozzle_diameter = 0.6 printer_variant = 0.6 end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_BarBaz3D\nPRINTER_MODEL_MK3\n retract_lift_below = 209 start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} printer_model = M1 diff --git a/resources/profiles/Foobar.ini b/resources/profiles/Foobar.ini index 571aa8bb8..6f31401ea 100644 --- a/resources/profiles/Foobar.ini +++ b/resources/profiles/Foobar.ini @@ -1,4 +1,4 @@ -# Print profiles for the Prusa Research printers. +# Print profiles for the Foobar Research printers. [vendor] # Vendor name will be shown by the Config Wizard. @@ -735,11 +735,11 @@ first_layer_temperature = 195 start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" temperature = 195 -[filament:Prusa ABS] +[filament:Foobar ABS] inherits = *ABS* filament_notes = "List of materials tested with standart ABS print settings for MK2:\n\nEsun ABS\nFil-A-Gehr ABS\nHatchboxABS\nPlasty Mladeč ABS" -[filament:Prusa HIPS] +[filament:Foobar HIPS] inherits = *ABS* bridge_fan_speed = 50 cooling = 1 @@ -755,11 +755,11 @@ min_fan_speed = 20 start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}200{else}10{endif}; Filament gcode" temperature = 220 -[filament:Prusa PET] +[filament:Foobar PET] inherits = *PET* filament_notes = "List of manufacturers tested with standart PET print settings for MK2:\n\nE3D Edge\nFillamentum CPE GH100\nPlasty Mladeč PETG" -[filament:Prusa PLA] +[filament:Foobar PLA] inherits = *PLA* filament_notes = "List of materials tested with standart PLA print settings for MK2:\n\nDas Filament\nEsun PLA\nEUMAKERS PLA\nFiberlogy HD-PLA\nFillamentum PLA\nFloreon3D\nHatchbox PLA\nPlasty Mladeč PLA\nPrimavalue PLA\nProto pasta Matte Fiber\nVerbatim PLA\nVerbatim BVOH" @@ -855,7 +855,7 @@ min_layer_height = 0.07 nozzle_diameter = 0.4 octoprint_apikey = octoprint_host = -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\n +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_Foobar3D\nPRINTER_MODEL_MK2\n printer_settings_id = retract_before_travel = 1 retract_before_wipe = 0% @@ -882,7 +882,7 @@ z_offset = 0 printer_model = M2 printer_variant = 0.4 default_print_profile = 0.15mm OPTIMAL -default_filament_profile = Prusa PLA +default_filament_profile = Foobar PLA [printer:*multimaterial*] inherits = *common* @@ -903,7 +903,7 @@ printer_model = M3 [printer:*mm-single*] inherits = *multimaterial* end_gcode = G1 E-4 F2100.00000\nG91\nG1 Z1 F7200.000\nG90\nG1 X245 Y1\nG1 X240 E4\nG1 F4000\nG1 X190 E2.7 \nG1 F4600\nG1 X110 E2.8\nG1 F5200\nG1 X40 E3 \nG1 E-15.0000 F5000\nG1 E-50.0000 F5400\nG1 E-15.0000 F3000\nG1 E-12.0000 F2000\nG1 F1600\nG1 X0 Y1 E3.0000\nG1 X50 Y1 E-5.0000\nG1 F2000\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-3.0000\nG4 S0\nM107 ; fan off\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nG28 X0 ; home X axis\nM84 ; disable motors\n\n -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_Foobar3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN start_gcode = M115 U3.1.0 ; tell printer latest fw version\nM201 X9000 Y9000 Z500 E10000 ; sets maximum accelerations, mm/sec^2\nM203 X500 Y500 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1500 T1500 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.2 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\n; Start G-Code sequence START\nT?\nM104 S[first_layer_temperature]\nM140 S[first_layer_bed_temperature]\nM109 S[first_layer_temperature]\nM190 S[first_layer_bed_temperature]\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nG28 W\nG80\nG92 E0.0\nM203 E100\nM92 E140\nG1 Z0.250 F7200.000\nG1 X50.0 E80.0 F1000.0\nG1 X160.0 E20.0 F1000.0\nG1 Z0.200 F7200.000\nG1 X220.0 E13 F1000.0\nG1 X240.0 E0 F1000.0\nG1 E-4 F1000.0\nG92 E0.0 [printer:*mm-multi*] @@ -911,14 +911,14 @@ inherits = *multimaterial* end_gcode = {if not has_wipe_tower}\n; Pull the filament into the cooling tubes.\nG1 E-4 F2100.00000\nG91\nG1 Z1 F7200.000\nG90\nG1 X245 Y1\nG1 X240 E4\nG1 F4000\nG1 X190 E2.7 \nG1 F4600\nG1 X110 E2.8\nG1 F5200\nG1 X40 E3 \nG1 E-15.0000 F5000\nG1 E-50.0000 F5400\nG1 E-15.0000 F3000\nG1 E-12.0000 F2000\nG1 F1600\nG1 X0 Y1 E3.0000\nG1 X50 Y1 E-5.0000\nG1 F2000\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-5.0000\nG1 F2400\nG1 X0 Y1 E5.0000\nG1 X50 Y1 E-3.0000\nG4 S0\n{endif}\nM107 ; fan off\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nG28 X0 ; home X axis\nM84 ; disable motors extruder_colour = #FFAA55;#5182DB;#4ECDD3;#FB7259 nozzle_diameter = 0.4,0.4,0.4,0.4 -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_Foobar3D\nPRINTER_MODEL_MK2\nPRINTER_HAS_BOWDEN start_gcode = M115 U3.1.0 ; tell printer latest fw version\nM201 X9000 Y9000 Z500 E10000 ; sets maximum accelerations, mm/sec^2\nM203 X500 Y500 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1500 T1500 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.2 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\n; Start G-Code sequence START\nT[initial_tool]\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nG28 W\nG80\nG92 E0.0\nM203 E100 ; set max feedrate\nM92 E140 ; E-steps per filament milimeter\n{if not has_wipe_tower}\nG1 Z0.250 F7200.000\nG1 X50.0 E80.0 F1000.0\nG1 X160.0 E20.0 F1000.0\nG1 Z0.200 F7200.000\nG1 X220.0 E13 F1000.0\nG1 X240.0 E0 F1000.0\nG1 E-4 F1000.0\n{endif}\nG92 E0.0 variable_layer_height = 0 -[printer:Original Prusa i3 MK2] +[printer:Foobar i3 MK2] inherits = *common* -[printer:Original Prusa i3 MK2 0.25 nozzle] +[printer:Foobar i3 MK2 0.25 nozzle] inherits = *common* max_layer_height = 0.1 min_layer_height = 0.05 @@ -929,56 +929,56 @@ variable_layer_height = 0 printer_variant = 0.25 default_print_profile = 0.10mm DETAIL 0.25 nozzle -[printer:Original Prusa i3 MK2 0.6 nozzle] +[printer:Foobar i3 MK2 0.6 nozzle] inherits = *common* max_layer_height = 0.35 min_layer_height = 0.1 nozzle_diameter = 0.6 printer_variant = 0.6 -[printer:Original Prusa i3 MK2 MM Single Mode] +[printer:Foobar i3 MK2 MM Single Mode] inherits = *mm-single* -[printer:Original Prusa i3 MK2 MM Single Mode 0.6 nozzle] +[printer:Foobar i3 MK2 MM Single Mode 0.6 nozzle] inherits = *mm-single* nozzle_diameter = 0.6 printer_variant = 0.6 -[printer:Original Prusa i3 MK2 MultiMaterial] +[printer:Foobar i3 MK2 MultiMaterial] inherits = *mm-multi* nozzle_diameter = 0.4,0.4,0.4,0.4 -[printer:Original Prusa i3 MK2 MultiMaterial 0.6 nozzle] +[printer:Foobar i3 MK2 MultiMaterial 0.6 nozzle] inherits = *mm-multi* nozzle_diameter = 0.6,0.6,0.6,0.6 printer_variant = 0.6 -[printer:Original Prusa i3 MK3] +[printer:Foobar i3 MK3] inherits = *common* end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_Foobar3D\nPRINTER_MODEL_MK3\n retract_lift_below = 209 start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} printer_model = M1 default_print_profile = 0.15mm OPTIMAL MK3 -[printer:Original Prusa i3 MK3 0.25 nozzle] +[printer:Foobar i3 MK3 0.25 nozzle] inherits = *common* nozzle_diameter = 0.25 printer_variant = 0.25 end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_Foobar3D\nPRINTER_MODEL_MK3\n retract_lift_below = 209 start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} printer_model = M1 default_print_profile = 0.10mm DETAIL MK3 -[printer:Original Prusa i3 MK3 0.6 nozzle] +[printer:Foobar i3 MK3 0.6 nozzle] inherits = *common* nozzle_diameter = 0.6 printer_variant = 0.6 end_gcode = G4 ; wait\nM221 S100\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y200; home X axis\nM84 ; disable motors -printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MK3\n +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_Foobar3D\nPRINTER_MODEL_MK3\n retract_lift_below = 209 start_gcode = M115 U3.1.1-RC5 ; tell printer latest fw version\nM201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2\nM203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm/sec\nM204 S1250 T1250 ; sets acceleration (S) and retract acceleration (T)\nM205 X10 Y10 Z0.4 E2.5 ; sets the jerk limits, mm/sec\nM205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec\nM83 ; extruder relative mode\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nG1 Y-3.0 F1000.0 ; go outside print area\nG92 E0.0\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E12.5 F1000.0 ; intro line\nG92 E0.0\nM221 S{if layer_height==0.05}100{else}95{endif} printer_model = M1 diff --git a/xs/src/slic3r/GUI/ConfigWizard.cpp b/xs/src/slic3r/GUI/ConfigWizard.cpp index c4ca7a886..0eee234db 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.cpp +++ b/xs/src/slic3r/GUI/ConfigWizard.cpp @@ -17,7 +17,6 @@ #include "GUI.hpp" #include "slic3r/Utils/PresetUpdater.hpp" -// TODO: Wizard vs Assistant namespace Slic3r { namespace GUI { @@ -205,9 +204,18 @@ void ConfigWizardPage::enable_next(bool enable) { parent->p->enable_next(enable) PageWelcome::PageWelcome(ConfigWizard *parent) : ConfigWizardPage(parent, wxString::Format(_(L("Welcome to the Slic3r %s")), ConfigWizard::name()), _(L("Welcome"))), printer_picker(nullptr), - others_buttons(new wxPanel(parent)) + others_buttons(new wxPanel(parent)), + cbox_reset(new wxCheckBox(this, wxID_ANY, _(L("Remove user profiles - install from scratch (a snapshot will be taken beforehand)")))) { - append_text(_(L("Hello, welcome to Slic3r Prusa Edition! TODO: This text."))); + if (wizard_p()->flag_startup && wizard_p()->flag_empty_datadir) { + wxString::Format(_(L("Run %s")), ConfigWizard::name()); + append_text(wxString::Format( + _(L("Hello, welcome to Slic3r Prusa Edition! This %s helps you with the initial configuration; just a few settings and you will be ready to print.")), + ConfigWizard::name()) + ); + } else { + append(cbox_reset); + } const auto &vendors = wizard_p()->vendors; const auto vendor_prusa = vendors.find("PrusaResearch"); @@ -720,14 +728,22 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese // This vendor needs to be installed install_bundles.emplace_back(vendor_rsrc.second); } + + // If the datadir was empty don't take a snapshot (it would just be an empty snapshot) + const bool snapshot = !flag_empty_datadir || page_welcome->reset_user_profile(); if (install_bundles.size() > 0) { - updater->install_bundles_rsrc(std::move(install_bundles)); + // Install bundles from resources. + updater->install_bundles_rsrc(std::move(install_bundles), snapshot); + } + + if (page_welcome->reset_user_profile()) { + preset_bundle->reset(true); } app_config->set_vendors(appconfig_vendors); app_config->set("version_check", page_update->version_check ? "1" : "0"); app_config->set("preset_update", page_update->preset_update ? "1" : "0"); - if (fresh_start) + if (flag_startup) app_config->reset_selections(); // ^ TODO: replace with appropriate printer selection preset_bundle->load_presets(*app_config); @@ -743,11 +759,13 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese // Public -ConfigWizard::ConfigWizard(wxWindow *parent, bool fresh_start) : +ConfigWizard::ConfigWizard(wxWindow *parent, bool startup, bool empty_datadir) : wxDialog(parent, wxID_ANY, name(), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), p(new priv(this)) { - p->fresh_start = fresh_start; + p->flag_startup = startup; + p->flag_empty_datadir = empty_datadir; + p->load_vendors(); p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ "gcode_flavor", "bed_shape", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", diff --git a/xs/src/slic3r/GUI/ConfigWizard.hpp b/xs/src/slic3r/GUI/ConfigWizard.hpp index 66c8b3a95..eeb64c57f 100644 --- a/xs/src/slic3r/GUI/ConfigWizard.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard.hpp @@ -16,7 +16,7 @@ namespace GUI { class ConfigWizard: public wxDialog { public: - ConfigWizard(wxWindow *parent, bool fresh_start); + ConfigWizard(wxWindow *parent, bool startup, bool empty_datadir); ConfigWizard(ConfigWizard &&) = delete; ConfigWizard(const ConfigWizard &) = delete; ConfigWizard &operator=(ConfigWizard &&) = delete; diff --git a/xs/src/slic3r/GUI/ConfigWizard_private.hpp b/xs/src/slic3r/GUI/ConfigWizard_private.hpp index faabca812..8e43ac3ac 100644 --- a/xs/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/xs/src/slic3r/GUI/ConfigWizard_private.hpp @@ -100,12 +100,14 @@ struct PageWelcome: ConfigWizardPage { PrinterPicker *printer_picker; wxPanel *others_buttons; + wxCheckBox *cbox_reset; PageWelcome(ConfigWizard *parent); virtual wxPanel* extra_buttons() { return others_buttons; } virtual void on_page_set(); + bool reset_user_profile() const { return cbox_reset->GetValue(); } void on_variant_checked(); }; @@ -188,9 +190,10 @@ private: struct ConfigWizard::priv { ConfigWizard *q; - bool fresh_start; - AppConfig appconfig_vendors; // TODO: use order-preserving container - std::unordered_map vendors; // TODO: just set? + bool flag_startup; + bool flag_empty_datadir; + AppConfig appconfig_vendors; + std::unordered_map vendors; std::unordered_map vendors_rsrc; std::unique_ptr custom_config; diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index bfdd46287..7de82cb5f 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -390,7 +390,7 @@ void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_l local_menu->Bind(wxEVT_MENU, [config_id_base, event_language_change, event_preferences_changed](wxEvent &event){ switch (event.GetId() - config_id_base) { case ConfigMenuWizard: - config_wizard(0); + config_wizard(false, false); break; case ConfigMenuTakeSnapshot: // Take a configuration snapshot. @@ -465,33 +465,35 @@ bool check_unsaved_changes() void config_wizard_startup(bool app_config_exists) { if (! app_config_exists || g_PresetBundle->has_defauls_only()) { - config_wizard(true); + config_wizard(true, true); } else if (g_AppConfig->legacy_datadir()) { // Looks like user has legacy pre-vendorbundle data directory, // explain what this is and run the wizard const auto msg = _(L("Configuration update")); - const auto ext_msg = _(L( - "Slic3r PE now uses an updated configuration structure.\n\n" + const auto ext_msg = wxString::Format( + _(L( + "Slic3r PE now uses an updated configuration structure.\n\n" - "So called 'System presets' have been introduced, which hold the built-in default settings for various " - "printers. These System presets cannot be modified, instead, users now may create their" - "own presets inheriting settings from one of the System presets.\n" - "An inheriting preset may either inherit a particular value from its parent or override it with a customized value.\n\n" + "So called 'System presets' have been introduced, which hold the built-in default settings for various " + "printers. These System presets cannot be modified, instead, users now may create their" + "own presets inheriting settings from one of the System presets.\n" + "An inheriting preset may either inherit a particular value from its parent or override it with a customized value.\n\n" - // TODO: Assistant vs Wizard - "Please proceed with the Configuration wizard that follows to set up the new presets " - "and to choose whether to enable automatic preset updates." - )); + "Please proceed with the %s that follows to set up the new presets " + "and to choose whether to enable automatic preset updates." + )), + ConfigWizard::name() + ); wxMessageDialog dlg(NULL, msg, _(L("Configuration update")), wxOK|wxCENTRE); dlg.SetExtendedMessage(ext_msg); const auto res = dlg.ShowModal(); - config_wizard(true); + config_wizard(true, false); } } -void config_wizard(bool fresh_start) // TODO: fresh_start useful ? +void config_wizard(bool startup, bool empty_datadir) { if (g_wxMainFrame == nullptr) throw std::runtime_error("Main frame not set"); @@ -500,8 +502,7 @@ void config_wizard(bool fresh_start) // TODO: fresh_start useful ? if (! check_unsaved_changes()) return; - // TODO: Offer "reset user profile" ??? - ConfigWizard wizard(g_wxMainFrame, fresh_start); + ConfigWizard wizard(g_wxMainFrame, startup, empty_datadir); wizard.run(g_PresetBundle, g_PresetUpdater); // Load the currently selected preset into the GUI, update the preset selection box. diff --git a/xs/src/slic3r/GUI/GUI.hpp b/xs/src/slic3r/GUI/GUI.hpp index 6ee9d4f54..92a6e6ebb 100644 --- a/xs/src/slic3r/GUI/GUI.hpp +++ b/xs/src/slic3r/GUI/GUI.hpp @@ -97,7 +97,7 @@ extern bool check_unsaved_changes(); extern void config_wizard_startup(bool app_config_exists); // Opens the configuration wizard, returns true if wizard is finished & accepted. -extern void config_wizard(bool fresh_start); +extern void config_wizard(bool startup, bool empty_datadir); // Create "Preferences" dialog after selecting menu "Preferences" in Perl part extern void open_preferences_dialog(int event_preferences); diff --git a/xs/src/slic3r/Utils/PresetUpdater.cpp b/xs/src/slic3r/Utils/PresetUpdater.cpp index 2140a2c91..bf9e1eb44 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.cpp +++ b/xs/src/slic3r/Utils/PresetUpdater.cpp @@ -155,7 +155,7 @@ struct PresetUpdater::priv void check_install_indices() const; Updates config_update() const; - void perform_updates(Updates &&updates) const; + void perform_updates(Updates &&updates, bool snapshot = true) const; }; PresetUpdater::priv::priv(int version_online_event) : @@ -348,9 +348,11 @@ Updates PresetUpdater::priv::config_update() const return updates; } -void PresetUpdater::priv::perform_updates(Updates &&updates) const +void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) const { - SnapshotDB::singleton().take_snapshot(*GUI::get_app_config(), Snapshot::SNAPSHOT_UPGRADE); + if (snapshot) { + SnapshotDB::singleton().take_snapshot(*GUI::get_app_config(), Snapshot::SNAPSHOT_UPGRADE); + } for (const auto &update : updates) { fs::copy_file(update.source, update.target, fs::copy_option::overwrite_if_exists); @@ -458,7 +460,7 @@ void PresetUpdater::config_update() const } } -void PresetUpdater::install_bundles_rsrc(std::vector &&bundles) +void PresetUpdater::install_bundles_rsrc(std::vector &&bundles, bool snapshot) { Updates updates; @@ -468,7 +470,7 @@ void PresetUpdater::install_bundles_rsrc(std::vector &&bundles) updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors)); } - p->perform_updates(std::move(updates)); + p->perform_updates(std::move(updates), snapshot); } diff --git a/xs/src/slic3r/Utils/PresetUpdater.hpp b/xs/src/slic3r/Utils/PresetUpdater.hpp index a53ed86eb..8fe3e021d 100644 --- a/xs/src/slic3r/Utils/PresetUpdater.hpp +++ b/xs/src/slic3r/Utils/PresetUpdater.hpp @@ -23,7 +23,7 @@ public: void sync(PresetBundle *preset_bundle); void slic3r_update_notify(); void config_update() const; - void install_bundles_rsrc(std::vector &&bundles); + void install_bundles_rsrc(std::vector &&bundles, bool snapshot = true); private: struct priv; std::unique_ptr p; From 0711f84ea0277dd503abe49cf0b58ddeb2d030f2 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Wed, 18 Apr 2018 12:33:07 +0200 Subject: [PATCH 31/38] Add version check & preset update options to Preferences --- xs/src/slic3r/GUI/Preferences.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/xs/src/slic3r/GUI/Preferences.cpp b/xs/src/slic3r/GUI/Preferences.cpp index 6731cd394..4c60577d5 100644 --- a/xs/src/slic3r/GUI/Preferences.cpp +++ b/xs/src/slic3r/GUI/Preferences.cpp @@ -9,7 +9,7 @@ void PreferencesDialog::build() { auto app_config = get_app_config(); m_optgroup = std::make_shared(this, _(L("General"))); - m_optgroup->label_width = 200; + m_optgroup->label_width = 400; m_optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value){ m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; }; @@ -49,6 +49,22 @@ void PreferencesDialog::build() option = Option (def,"background_processing"); m_optgroup->append_single_option_line(option); + // Please keep in sync with ConfigWizard + def.label = L("Check for application updates"); + def.type = coBool; + def.tooltip = L("If enabled, Slic3r checks for new versions of Slic3r PE online. When a new version becomes available a notification is displayed at the next application startup (never during program usage). This is only a notification mechanisms, no automatic installation is done."); + def.default_value = new ConfigOptionBool(app_config->get("version_check") == "1"); + option = Option (def, "version_check"); + m_optgroup->append_single_option_line(option); + + // Please keep in sync with ConfigWizard + def.label = L("Update built-in Presets automatically"); + def.type = coBool; + def.tooltip = L("If enabled, Slic3r downloads updates of built-in system presets in the background. These updates are downloaded into a separate temporary location. When a new preset version becomes available it is offered at application startup."); + def.default_value = new ConfigOptionBool(app_config->get("preset_update") == "1"); + option = Option (def, "preset_update"); + m_optgroup->append_single_option_line(option); + def.label = L("Disable USB/serial connection"); def.type = coBool; def.tooltip = L("Disable communication with the printer over a serial / USB cable. " From fa97a867519ef5dc91bf7fab4c7c5d3a104f0c50 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Wed, 18 Apr 2018 13:35:51 +0200 Subject: [PATCH 32/38] Implemented merging of system profiles from various vendors. --- xs/src/slic3r/GUI/Preset.cpp | 23 +++++++++++++++++++++ xs/src/slic3r/GUI/Preset.hpp | 3 +++ xs/src/slic3r/GUI/PresetBundle.cpp | 33 +++++++++++++++++++++++++++++- xs/src/slic3r/GUI/PresetBundle.hpp | 4 +++- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/xs/src/slic3r/GUI/Preset.cpp b/xs/src/slic3r/GUI/Preset.cpp index 7fa9c998d..15dcb2fbb 100644 --- a/xs/src/slic3r/GUI/Preset.cpp +++ b/xs/src/slic3r/GUI/Preset.cpp @@ -790,6 +790,29 @@ bool PresetCollection::select_preset_by_name_strict(const std::string &name) return false; } +// Merge one vendor's presets with the other vendor's presets, report duplicates. +std::vector PresetCollection::merge_presets(PresetCollection &&other, const std::set &new_vendors) +{ + std::vector duplicates; + for (Preset &preset : other.m_presets) { + if (preset.is_default || preset.is_external) + continue; + Preset key(m_type, preset.name); + auto it = std::lower_bound(m_presets.begin() + 1, m_presets.end(), key); + if (it == m_presets.end() || it->name != preset.name) { + if (preset.vendor != nullptr) { + // Re-assign a pointer to the vendor structure in the new PresetBundle. + auto it = new_vendors.find(*preset.vendor); + assert(it != new_vendors.end()); + preset.vendor = &(*it); + } + this->m_presets.emplace(it, std::move(preset)); + } else + duplicates.emplace_back(std::move(preset.name)); + } + return duplicates; +} + std::string PresetCollection::name() const { switch (this->type()) { diff --git a/xs/src/slic3r/GUI/Preset.hpp b/xs/src/slic3r/GUI/Preset.hpp index 480d6b178..e80f138ee 100644 --- a/xs/src/slic3r/GUI/Preset.hpp +++ b/xs/src/slic3r/GUI/Preset.hpp @@ -336,6 +336,9 @@ protected: // This is a temporary state, which shall be fixed immediately by the following step. bool select_preset_by_name_strict(const std::string &name); + // Merge one vendor's presets with the other vendor's presets, report duplicates. + std::vector merge_presets(PresetCollection &&other, const std::set &new_vendors); + private: PresetCollection(); PresetCollection(const PresetCollection &other); diff --git a/xs/src/slic3r/GUI/PresetBundle.cpp b/xs/src/slic3r/GUI/PresetBundle.cpp index 6f0754a03..717e7a6f0 100644 --- a/xs/src/slic3r/GUI/PresetBundle.cpp +++ b/xs/src/slic3r/GUI/PresetBundle.cpp @@ -178,6 +178,7 @@ std::string PresetBundle::load_system_presets() // Here the vendor specific read only Config Bundles are stored. boost::filesystem::path dir = (boost::filesystem::path(data_dir()) / "vendor").make_preferred(); std::string errors_cummulative; + bool first = true; for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini")) { std::string name = dir_entry.path().filename().string(); @@ -185,7 +186,25 @@ std::string PresetBundle::load_system_presets() name.erase(name.size() - 4); try { // Load the config bundle, flatten it. - this->load_configbundle(dir_entry.path().string(), LOAD_CFGBNDLE_SYSTEM); + if (first) { + // Reset this PresetBundle and load the first vendor config. + this->load_configbundle(dir_entry.path().string(), LOAD_CFGBNDLE_SYSTEM); + first = false; + } else { + // Load the other vendor configs, merge them with this PresetBundle. + // Report duplicate profiles. + PresetBundle other; + other.load_configbundle(dir_entry.path().string(), LOAD_CFGBNDLE_SYSTEM); + std::vector duplicates = this->merge_presets(std::move(other)); + if (! duplicates.empty()) { + errors_cummulative += "Vendor configuration file " + name + " contains the following presets with names used by other vendors: "; + for (size_t i = 0; i < duplicates.size(); ++ i) { + if (i > 0) + errors_cummulative += ", "; + errors_cummulative += duplicates[i]; + } + } + } } catch (const std::runtime_error &err) { errors_cummulative += err.what(); errors_cummulative += "\n"; @@ -194,6 +213,18 @@ std::string PresetBundle::load_system_presets() return errors_cummulative; } +// Merge one vendor's presets with the other vendor's presets, report duplicates. +std::vector PresetBundle::merge_presets(PresetBundle &&other) +{ + this->vendors.insert(other.vendors.begin(), other.vendors.end()); + std::vector duplicate_prints = this->prints .merge_presets(std::move(other.prints), this->vendors); + std::vector duplicate_filaments = this->filaments.merge_presets(std::move(other.filaments), this->vendors); + std::vector duplicate_printers = this->printers .merge_presets(std::move(other.printers), this->vendors); + append(duplicate_prints, std::move(duplicate_filaments)); + append(duplicate_prints, std::move(duplicate_printers)); + return duplicate_prints; +} + static inline std::string remove_ini_suffix(const std::string &name) { std::string out = name; diff --git a/xs/src/slic3r/GUI/PresetBundle.hpp b/xs/src/slic3r/GUI/PresetBundle.hpp index 27add21ee..9f2afbead 100644 --- a/xs/src/slic3r/GUI/PresetBundle.hpp +++ b/xs/src/slic3r/GUI/PresetBundle.hpp @@ -116,10 +116,12 @@ public: // preset if the current print or filament preset is not compatible. void update_compatible_with_printer(bool select_other_if_incompatible); - static bool parse_color(const std::string &scolor, unsigned char *rgb_out); + static bool parse_color(const std::string &scolor, unsigned char *rgb_out); private: std::string load_system_presets(); + // Merge one vendor's presets with the other vendor's presets, report duplicates. + std::vector merge_presets(PresetBundle &&other); // Set the "enabled" flag for printer vendors, printer models and printer variants // based on the user configuration. From c3c9ebdd12616aaa79ea0e6d2a06d3db06d36f31 Mon Sep 17 00:00:00 2001 From: Vojtech Kral Date: Wed, 18 Apr 2018 18:48:32 +0200 Subject: [PATCH 33/38] Fix wxPerl warning annoyance --- lib/Slic3r.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index e55e24a7a..19915ddd8 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -277,5 +277,6 @@ sub system_info # this package declaration prevents an ugly fatal warning to be emitted when # spawning a new thread package GLUquadricObjPtr; +package Wx::Printout; 1; From ab397e5ce1d27b82006516876fa84022b4b5cd92 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Fri, 20 Apr 2018 10:26:23 +0200 Subject: [PATCH 34/38] Added SnapshotDB::snapshot_with_vendor_preset() utility function to find out whether there has ever been a snapshot taken with a given configuration version. Implemented an "on snapshot" flag, which indicates, whether the current state equals to some snapshot. If so, a new snapshot is not taken in upgrade / downgrade case. --- xs/src/slic3r/Config/Snapshot.cpp | 124 ++++++++++++++++++++- xs/src/slic3r/Config/Snapshot.hpp | 17 ++- xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp | 38 +++---- xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp | 4 +- xs/src/slic3r/GUI/GUI.cpp | 15 ++- 5 files changed, 168 insertions(+), 30 deletions(-) diff --git a/xs/src/slic3r/Config/Snapshot.cpp b/xs/src/slic3r/Config/Snapshot.cpp index c82169308..18329aa5c 100644 --- a/xs/src/slic3r/Config/Snapshot.cpp +++ b/xs/src/slic3r/Config/Snapshot.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -81,6 +82,8 @@ void Snapshot::load_ini(const std::string &path) this->reason = SNAPSHOT_UPGRADE; else if (rsn == "downgrade") this->reason = SNAPSHOT_DOWNGRADE; + else if (rsn == "before_rollback") + this->reason = SNAPSHOT_BEFORE_ROLLBACK; else if (rsn == "user") this->reason = SNAPSHOT_USER; else @@ -131,6 +134,9 @@ void Snapshot::load_ini(const std::string &path) this->vendor_configs.emplace_back(std::move(vc)); } } + // Sort the vendors lexicographically. + std::sort(this->vendor_configs.begin(), this->vendor_configs.begin(), + [](const VendorConfig &cfg1, const VendorConfig &cfg2) { return cfg1.name < cfg2.name; }); } static std::string reason_string(const Snapshot::Reason reason) @@ -140,6 +146,8 @@ static std::string reason_string(const Snapshot::Reason reason) return "upgrade"; case Snapshot::SNAPSHOT_DOWNGRADE: return "downgrade"; + case Snapshot::SNAPSHOT_BEFORE_ROLLBACK: + return "before_rollback"; case Snapshot::SNAPSHOT_USER: return "user"; case Snapshot::SNAPSHOT_UNKNOWN: @@ -210,6 +218,74 @@ void Snapshot::export_vendor_configs(AppConfig &config) const config.set_vendors(std::move(vendors)); } +// Perform a deep compare of the active print / filament / printer / vendor directories. +// Return true if the content of the current print / filament / printer / vendor directories +// matches the state stored in this snapshot. +bool Snapshot::equal_to_active(const AppConfig &app_config) const +{ + // 1) Check, whether this snapshot contains the same set of active vendors, printer models and variants + // as app_config. + { + std::set matched; + for (const VendorConfig &vc : this->vendor_configs) { + auto it_vendor_models_variants = app_config.vendors().find(vc.name); + if (it_vendor_models_variants == app_config.vendors().end() || + it_vendor_models_variants->second != vc.models_variants_installed) + // There are more vendors enabled in the snapshot than currently installed. + return false; + matched.insert(vc.name); + } + for (const std::pair>> &v : app_config.vendors()) + if (matched.find(v.first) == matched.end() && ! v.second.empty()) + // There are more vendors currently installed than enabled in the snapshot. + return false; + } + + // 2) Check, whether this snapshot references the same set of ini files as the current state. + boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); + boost::filesystem::path snapshot_dir = boost::filesystem::path(Slic3r::data_dir()) / SLIC3R_SNAPSHOTS_DIR / this->id; + for (const char *subdir : { "print", "filament", "printer", "vendor" }) { + boost::filesystem::path path1 = data_dir / subdir; + boost::filesystem::path path2 = snapshot_dir / subdir; + std::vector files1, files2; + for (auto &dir_entry : boost::filesystem::directory_iterator(path1)) + if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini")) + files1.emplace_back(dir_entry.path().filename().string()); + for (auto &dir_entry : boost::filesystem::directory_iterator(path2)) + if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini")) + files2.emplace_back(dir_entry.path().filename().string()); + std::sort(files1.begin(), files1.end()); + std::sort(files2.begin(), files2.end()); + if (files1 != files2) + return false; + for (const std::string &filename : files1) { + FILE *f1 = boost::nowide::fopen((path1 / filename).string().c_str(), "rb"); + FILE *f2 = boost::nowide::fopen((path2 / filename).string().c_str(), "rb"); + bool same = true; + if (f1 && f2) { + char buf1[4096]; + char buf2[4096]; + do { + size_t r1 = fread(buf1, 1, 4096, f1); + size_t r2 = fread(buf2, 1, 4096, f2); + if (r1 != r2 || memcmp(buf1, buf2, r1)) { + same = false; + break; + } + } while (! feof(f1) || ! feof(f2)); + } else + same = false; + if (f1) + fclose(f1); + if (f2) + fclose(f2); + if (! same) + return false; + } + } + return true; +} + size_t SnapshotDB::load_db() { boost::filesystem::path snapshots_dir = SnapshotDB::create_db_dir(); @@ -347,12 +423,12 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: return m_snapshots.back(); } -void SnapshotDB::restore_snapshot(const std::string &id, AppConfig &app_config) +const Snapshot& SnapshotDB::restore_snapshot(const std::string &id, AppConfig &app_config) { for (const Snapshot &snapshot : m_snapshots) if (snapshot.id == id) { this->restore_snapshot(snapshot, app_config); - return; + return snapshot; } throw std::runtime_error(std::string("Snapshot with id " + id + " was not found.")); } @@ -373,6 +449,50 @@ void SnapshotDB::restore_snapshot(const Snapshot &snapshot, AppConfig &app_confi snapshot.export_vendor_configs(app_config); } +bool SnapshotDB::is_on_snapshot(AppConfig &app_config) const +{ + // Is the "on_snapshot" configuration value set? + std::string on_snapshot = app_config.get("on_snapshot"); + if (on_snapshot.empty()) + // No, we are not on a snapshot. + return false; + // Is the "on_snapshot" equal to the current configuration state? + auto it_snapshot = this->snapshot(on_snapshot); + if (it_snapshot != this->end() && it_snapshot->equal_to_active(app_config)) + // Yes, we are on the snapshot. + return true; + // No, we are no more on a snapshot. Reset the state. + app_config.set("on_snapshot", ""); + return false; +} + +SnapshotDB::const_iterator SnapshotDB::snapshot_with_vendor_preset(const std::string &vendor_name, const Semver &config_version) +{ + auto it_found = m_snapshots.end(); + Snapshot::VendorConfig key; + key.name = vendor_name; + for (auto it = m_snapshots.begin(); it != m_snapshots.end(); ++ it) { + const Snapshot &snapshot = *it; + auto it_vendor_config = std::lower_bound(snapshot.vendor_configs.begin(), snapshot.vendor_configs.end(), + key, [](const Snapshot::VendorConfig &cfg1, const Snapshot::VendorConfig &cfg2) { return cfg1.name < cfg2.name; }); + if (it_vendor_config != snapshot.vendor_configs.end() && it_vendor_config->name == vendor_name && + config_version == it_vendor_config->version) { + // Vendor config found with the correct version. + // Save it, but continue searching, as we want the newest snapshot. + it_found = it; + } + } + return it_found; +} + +SnapshotDB::const_iterator SnapshotDB::snapshot(const std::string &id) const +{ + for (const_iterator it = m_snapshots.begin(); it != m_snapshots.end(); ++ it) + if (it->id == id) + return it; + return m_snapshots.end(); +} + boost::filesystem::path SnapshotDB::create_db_dir() { boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); diff --git a/xs/src/slic3r/Config/Snapshot.hpp b/xs/src/slic3r/Config/Snapshot.hpp index a7e70bb4b..77aee3e21 100644 --- a/xs/src/slic3r/Config/Snapshot.hpp +++ b/xs/src/slic3r/Config/Snapshot.hpp @@ -34,6 +34,7 @@ public: SNAPSHOT_UNKNOWN, SNAPSHOT_UPGRADE, SNAPSHOT_DOWNGRADE, + SNAPSHOT_BEFORE_ROLLBACK, SNAPSHOT_USER, }; @@ -47,6 +48,11 @@ public: void export_selections(AppConfig &config) const; void export_vendor_configs(AppConfig &config) const; + // Perform a deep compare of the active print / filament / printer / vendor directories. + // Return true if the content of the current print / filament / printer / vendor directories + // matches the state stored in this snapshot. + bool equal_to_active(const AppConfig &app_config) const; + // ID of a snapshot should equal to the name of the snapshot directory. // The ID contains the date/time, reason and comment to be human readable. std::string id; @@ -79,7 +85,7 @@ public: // Which printer models of this vendor were installed, and which variants of the models? std::map> models_variants_installed; }; - // List of vendor configs contained in this snapshot. + // List of vendor configs contained in this snapshot, sorted lexicographically. std::vector vendor_configs; }; @@ -100,17 +106,24 @@ public: // Create a snapshot directory, copy the vendor config bundles, user print/filament/printer profiles, // create an index. const Snapshot& take_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment = ""); - void restore_snapshot(const std::string &id, AppConfig &app_config); + const Snapshot& restore_snapshot(const std::string &id, AppConfig &app_config); void restore_snapshot(const Snapshot &snapshot, AppConfig &app_config); + // Test whether the AppConfig's on_snapshot variable points to an existing snapshot, and the existing snapshot + // matches the current state. If it does not match the current state, the AppConfig's "on_snapshot" ID is reset. + bool is_on_snapshot(AppConfig &app_config) const; + // Finds the newest snapshot, which contains a config bundle for vendor_name with config_version. + const_iterator snapshot_with_vendor_preset(const std::string &vendor_name, const Semver &config_version); const_iterator begin() const { return m_snapshots.begin(); } const_iterator end() const { return m_snapshots.end(); } + const_iterator snapshot(const std::string &id) const; const std::vector& snapshots() const { return m_snapshots; } private: // Create the snapshots directory if it does not exist yet. static boost::filesystem::path create_db_dir(); + // Snapshots are sorted by their date/time, oldest first. std::vector m_snapshots; }; diff --git a/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp b/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp index 0e0df15f2..99af707e1 100644 --- a/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp +++ b/xs/src/slic3r/GUI/ConfigSnapshotDialog.cpp @@ -15,6 +15,8 @@ static std::string format_reason(const Config::Snapshot::Reason reason) return std::string(_(L("Upgrade"))); case Config::Snapshot::SNAPSHOT_DOWNGRADE: return std::string(_(L("Downgrade"))); + case Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK: + return std::string(_(L("Before roll back"))); case Config::Snapshot::SNAPSHOT_USER: return std::string(_(L("User"))); case Config::Snapshot::SNAPSHOT_UNKNOWN: @@ -23,34 +25,35 @@ static std::string format_reason(const Config::Snapshot::Reason reason) } } -static std::string generate_html_row(const Config::Snapshot &snapshot, bool row_even) +static std::string generate_html_row(const Config::Snapshot &snapshot, bool row_even, bool snapshot_active) { // Start by declaring a row with an alternating background color. std::string text = "
"; // Format the row header. - text += std::string("") + Utils::format_local_date_time(snapshot.time_captured) + ": " + format_reason(snapshot.reason); + text += std::string("") + (snapshot_active ? _(L("Active: ")) : "") + + Utils::format_local_date_time(snapshot.time_captured) + ": " + format_reason(snapshot.reason); if (! snapshot.comment.empty()) text += " (" + snapshot.comment + ")"; text += "
"; // End of row header. // text += _(L("ID:")) + " " + snapshot.id + "
"; // text += _(L("time captured:")) + " " + Utils::format_local_date_time(snapshot.time_captured) + "
"; - text += _(L("slic3r version:")) + " " + snapshot.slic3r_version_captured.to_string() + "
"; + text += _(L("slic3r version")) + ": " + snapshot.slic3r_version_captured.to_string() + "
"; // text += "reason: " + snapshot.reason + "
"; - text += "print: " + snapshot.print + "
"; - text += "filaments: " + snapshot.filaments.front() + "
"; - text += "printer: " + snapshot.printer + "
"; + text += _(L("print")) + ": " + snapshot.print + "
"; + text += _(L("filaments")) + ": " + snapshot.filaments.front() + "
"; + text += _(L("printer")) + ": " + snapshot.printer + "
"; for (const Config::Snapshot::VendorConfig &vc : snapshot.vendor_configs) { - text += "vendor: " + vc.name + ", ver: " + vc.version.to_string() + ", min slic3r ver: " + vc.min_slic3r_version.to_string(); + text += _(L("vendor")) + ": " + vc.name + ", ver: " + vc.version.to_string() + ", min slic3r ver: " + vc.min_slic3r_version.to_string(); if (vc.max_slic3r_version != Semver::inf()) text += ", max slic3r ver: " + vc.max_slic3r_version.to_string(); text += "
"; for (const std::pair> &model : vc.models_variants_installed) { - text += "model: " + model.first + ", variants: "; + text += _(L("model")) + ": " + model.first + ", " + _(L("variants")) + ": "; for (const std::string &variant : model.second) { if (&variant != &*model.second.begin()) text += ", "; @@ -60,13 +63,14 @@ static std::string generate_html_row(const Config::Snapshot &snapshot, bool row_ } } - text += "

Activate

"; + if (! snapshot_active) + text += "

" + _(L("Activate")) + "

"; text += "
"; for (size_t i_row = 0; i_row < snapshot_db.snapshots().size(); ++ i_row) { const Config::Snapshot &snapshot = snapshot_db.snapshots()[snapshot_db.snapshots().size() - i_row - 1]; - text += generate_html_row(snapshot, i_row & 1); + text += generate_html_row(snapshot, i_row & 1, snapshot.id == on_snapshot); } text += "
" @@ -85,7 +89,7 @@ static std::string generate_html_page(const Config::SnapshotDB &snapshot_db) return text; } -ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db) +ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db, const std::string &on_snapshot) : wxDialog(NULL, wxID_ANY, _(L("Configuration Snapshots")), wxDefaultPosition, wxSize(600, 500), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxMAXIMIZE_BOX) { this->SetBackgroundColour(*wxWHITE); @@ -104,7 +108,7 @@ ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db #endif html->SetFonts(font.GetFaceName(), font.GetFaceName(), size); html->SetBorders(2); - std::string text = generate_html_page(snapshot_db); + std::string text = generate_html_page(snapshot_db, on_snapshot); html->SetPage(text.c_str()); vsizer->Add(html, 1, wxEXPAND | wxALIGN_LEFT | wxRIGHT | wxBOTTOM, 0); html->Bind(wxEVT_HTML_LINK_CLICKED, &ConfigSnapshotDialog::onLinkClicked, this); @@ -114,12 +118,6 @@ ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db this->SetEscapeId(wxID_CLOSE); this->Bind(wxEVT_BUTTON, &ConfigSnapshotDialog::onCloseDialog, this, wxID_CLOSE); vsizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3); - -/* - this->Bind(wxEVT_LEFT_DOWN, &ConfigSnapshotDialog::onCloseDialog, this); - logo->Bind(wxEVT_LEFT_DOWN, &ConfigSnapshotDialog::onCloseDialog, this); - html->Bind(wxEVT_LEFT_DOWN, &ConfigSnapshotDialog::onCloseDialog, this); -*/ } void ConfigSnapshotDialog::onLinkClicked(wxHtmlLinkEvent &event) diff --git a/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp b/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp index 0d1109615..943601e73 100644 --- a/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp +++ b/xs/src/slic3r/GUI/ConfigSnapshotDialog.hpp @@ -17,14 +17,14 @@ namespace Config { class ConfigSnapshotDialog : public wxDialog { public: - ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db); - + ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db, const std::string &id); const std::string& snapshot_to_activate() const { return m_snapshot_to_activate; } private: void onLinkClicked(wxHtmlLinkEvent &event); void onCloseDialog(wxEvent &); + // If set, it contains a snapshot ID to be restored after the dialog closes. std::string m_snapshot_to_activate; }; diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index 7de82cb5f..88c3f421b 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -397,16 +397,23 @@ void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_l if (check_unsaved_changes()) { wxTextEntryDialog dlg(nullptr, _(L("Taking configuration snapshot")), _(L("Snapshot name"))); if (dlg.ShowModal() == wxID_OK) - Slic3r::GUI::Config::SnapshotDB::singleton().take_snapshot( - *g_AppConfig, Slic3r::GUI::Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); + g_AppConfig->set("on_snapshot", + Slic3r::GUI::Config::SnapshotDB::singleton().take_snapshot( + *g_AppConfig, Slic3r::GUI::Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()).id); } break; case ConfigMenuSnapshots: if (check_unsaved_changes()) { - ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton()); + std::string on_snapshot; + if (Config::SnapshotDB::singleton().is_on_snapshot(*g_AppConfig)) + on_snapshot = g_AppConfig->get("on_snapshot"); + ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); dlg.ShowModal(); if (! dlg.snapshot_to_activate().empty()) { - Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *g_AppConfig); + if (! Config::SnapshotDB::singleton().is_on_snapshot(*g_AppConfig)) + Config::SnapshotDB::singleton().take_snapshot(*g_AppConfig, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK); + g_AppConfig->set("on_snapshot", + Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *g_AppConfig).id); g_PresetBundle->load_presets(*g_AppConfig); // Load the currently selected preset into the GUI, update the preset selection box. for (Tab *tab : g_tabs_list) From 6c627be4c18cc747df9040ed3bceb2bc14823f0b Mon Sep 17 00:00:00 2001 From: bubnikv Date: Wed, 25 Apr 2018 10:37:31 +0200 Subject: [PATCH 35/38] New cooling logic to equalize extrusion velocity. The old behavior caused bad outer surface print quality on Prusa i3 MK3 --- xs/src/libslic3r/GCode/CoolingBuffer.cpp | 479 +++++++++++++++-------- 1 file changed, 306 insertions(+), 173 deletions(-) diff --git a/xs/src/libslic3r/GCode/CoolingBuffer.cpp b/xs/src/libslic3r/GCode/CoolingBuffer.cpp index cd2baeffb..121cbb017 100644 --- a/xs/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/xs/src/libslic3r/GCode/CoolingBuffer.cpp @@ -30,6 +30,174 @@ void CoolingBuffer::reset() m_current_pos[4] = float(m_gcodegen.config().travel_speed.value); } +struct CoolingLine +{ + enum Type { + TYPE_SET_TOOL = 1 << 0, + TYPE_EXTRUDE_END = 1 << 1, + TYPE_BRIDGE_FAN_START = 1 << 2, + TYPE_BRIDGE_FAN_END = 1 << 3, + TYPE_G0 = 1 << 4, + TYPE_G1 = 1 << 5, + TYPE_ADJUSTABLE = 1 << 6, + TYPE_EXTERNAL_PERIMETER = 1 << 7, + // The line sets a feedrate. + TYPE_HAS_F = 1 << 8, + TYPE_WIPE = 1 << 9, + TYPE_G4 = 1 << 10, + TYPE_G92 = 1 << 11, + }; + + CoolingLine(unsigned int type, size_t line_start, size_t line_end) : + type(type), line_start(line_start), line_end(line_end), + length(0.f), time(0.f), time_max(0.f), slowdown(false) {} + + bool adjustable(bool slowdown_external_perimeters) const { + return (this->type & TYPE_ADJUSTABLE) && + (! (this->type & TYPE_EXTERNAL_PERIMETER) || slowdown_external_perimeters) && + this->time < this->time_max; + } + + bool adjustable() const { + return (this->type & TYPE_ADJUSTABLE) && this->time < this->time_max; + } + + size_t type; + // Start of this line at the G-code snippet. + size_t line_start; + // End of this line at the G-code snippet. + size_t line_end; + // XY Euclidian length of this segment. + float length; + // Current feedrate, possibly adjusted. + float feedrate; + // Current duration of this segment. + float time; + // Maximum duration of this segment. + float time_max; + // If marked with the "slowdown" flag, the line has been slowed down. + bool slowdown; +}; + +// Calculate the required per extruder time stretches. +struct PerExtruderAdjustments +{ + // Calculate the total elapsed time per this extruder, adjusted for the slowdown. + float elapsed_time_total() { + float time_total = 0.f; + for (const CoolingLine &line : lines) + time_total += line.time; + return time_total; + } + // Calculate the maximum time when slowing down. + float maximum_time(bool slowdown_external_perimeters) { + float time_total = 0.f; + for (const CoolingLine &line : lines) + if (line.adjustable(slowdown_external_perimeters)) { + if (line.time_max == FLT_MAX) + return FLT_MAX; + else + time_total += line.time_max; + } else + time_total += line.time; + return time_total; + } + // Calculate the non-adjustable part of the total time. + float non_adjustable_time(bool slowdown_external_perimeters) { + float time_total = 0.f; + for (const CoolingLine &line : lines) + if (! line.adjustable(slowdown_external_perimeters)) + time_total += line.time; + return time_total; + } + float slow_down_maximum(bool slowdown_external_perimeters) { + float time_total = 0.f; + for (CoolingLine &line : lines) { + if (line.adjustable(slowdown_external_perimeters)) { + assert(line.time_max >= 0.f && line.time_max < FLT_MAX); + line.slowdown = true; + line.time = line.time_max; + line.feedrate = line.length / line.time; + } + time_total += line.time; + } + return time_total; + } + float slow_down_proportional(float factor, bool slowdown_external_perimeters) { + assert(factor >= 1.f); + float time_total = 0.f; + for (CoolingLine &line : lines) { + if (line.adjustable(slowdown_external_perimeters)) { + line.slowdown = true; + line.time = std::min(line.time_max, line.time * factor); + line.feedrate = line.length / line.time; + } + time_total += line.time; + } + return time_total; + } + + bool operator<(const PerExtruderAdjustments &rhs) const { return this->extruder_id < rhs.extruder_id; } + + // Sort the lines, adjustable first, higher feedrate first. + void sort_lines_by_decreasing_feedrate() { + std::sort(lines.begin(), lines.end(), [](const CoolingLine &l1, const CoolingLine &l2) { + bool adj1 = l1.adjustable(); + bool adj2 = l2.adjustable(); + return (adj1 == adj2) ? l1.feedrate > l2.feedrate : adj1; + }); + for (n_lines_adjustable = 0; n_lines_adjustable < lines.size(); ++ n_lines_adjustable) + if ((this->lines[n_lines_adjustable].type & CoolingLine::TYPE_ADJUSTABLE) == 0) + break; + time_non_adjustable = 0.f; + for (size_t i = n_lines_adjustable; i < lines.size(); ++ i) + time_non_adjustable += lines[i].time; + } + + // Calculate the maximum time when slowing down. + float time_stretch_when_slowing_down_to(float min_feedrate) { + float time_stretch = 0.f; + if (this->min_print_speed < min_feedrate + EPSILON) { + for (size_t i = 0; i < n_lines_adjustable; ++ i) { + const CoolingLine &line = lines[i]; + if (line.feedrate > min_feedrate) + time_stretch += line.time * (line.feedrate / min_feedrate - 1.f); + } + } + return time_stretch; + } + + void slow_down_to(float min_feedrate) { + if (this->min_print_speed < min_feedrate + EPSILON) { + for (size_t i = 0; i < n_lines_adjustable; ++ i) { + CoolingLine &line = lines[i]; + if (line.feedrate > min_feedrate) { + line.time *= std::max(1.f, line.feedrate / min_feedrate); + line.feedrate = min_feedrate; + line.slowdown = true; + } + } + } + } + + // Extruder, for which the G-code will be adjusted. + unsigned int extruder_id = 0; + // Minimum print speed allowed for this extruder. + float min_print_speed = 0.f; + + // Parsed lines. + std::vector lines; + // The following two values are set by sort_lines_by_decreasing_feedrate(): + // Number of adjustable lines, at the start of lines. + size_t n_lines_adjustable = 0; + // Non-adjustable time of lines starting with n_lines_adjustable. + float time_non_adjustable = 0; + + // Temporaries for processing the slow down. Both thresholds go from 0 to n_lines_adjustable. + size_t idx_line_begin = 0; + size_t idx_line_end = 0; +}; + #define EXTRUDER_CONFIG(OPT) config.OPT.get_at(m_current_extruder) std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_id) @@ -38,125 +206,23 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ const std::vector &extruders = m_gcodegen.writer().extruders(); const size_t num_extruders = extruders.size(); - // Calculate the required per extruder time stretches. - struct Adjustment { - Adjustment(unsigned int extruder_id = 0) : extruder_id(extruder_id) {} - // Calculate the total elapsed time per this extruder, adjusted for the slowdown. - float elapsed_time_total() { - float time_total = 0.f; - for (const Line &line : lines) - time_total += line.time; - return time_total; - } - // Calculate the maximum time when slowing down. - float maximum_time(bool slowdown_external_perimeters) { - float time_total = 0.f; - for (const Line &line : lines) - if (line.adjustable(slowdown_external_perimeters)) { - if (line.time_max == FLT_MAX) - return FLT_MAX; - else - time_total += line.time_max; - } else - time_total += line.time; - return time_total; - } - // Calculate the non-adjustable part of the total time. - float non_adjustable_time(bool slowdown_external_perimeters) { - float time_total = 0.f; - for (const Line &line : lines) - if (! line.adjustable(slowdown_external_perimeters)) - time_total += line.time; - return time_total; - } - float slow_down_maximum(bool slowdown_external_perimeters) { - float time_total = 0.f; - for (Line &line : lines) { - if (line.adjustable(slowdown_external_perimeters)) { - assert(line.time_max >= 0.f && line.time_max < FLT_MAX); - line.slowdown = true; - line.time = line.time_max; - } - time_total += line.time; - } - return time_total; - } - float slow_down_proportional(float factor, bool slowdown_external_perimeters) { - assert(factor >= 1.f); - float time_total = 0.f; - for (Line &line : lines) { - if (line.adjustable(slowdown_external_perimeters)) { - line.slowdown = true; - line.time = std::min(line.time_max, line.time * factor); - } - time_total += line.time; - } - return time_total; - } - - bool operator<(const Adjustment &rhs) const { return this->extruder_id < rhs.extruder_id; } - - struct Line - { - enum Type { - TYPE_SET_TOOL = 1 << 0, - TYPE_EXTRUDE_END = 1 << 1, - TYPE_BRIDGE_FAN_START = 1 << 2, - TYPE_BRIDGE_FAN_END = 1 << 3, - TYPE_G0 = 1 << 4, - TYPE_G1 = 1 << 5, - TYPE_ADJUSTABLE = 1 << 6, - TYPE_EXTERNAL_PERIMETER = 1 << 7, - // The line sets a feedrate. - TYPE_HAS_F = 1 << 8, - TYPE_WIPE = 1 << 9, - TYPE_G4 = 1 << 10, - TYPE_G92 = 1 << 11, - }; - - Line(unsigned int type, size_t line_start, size_t line_end) : - type(type), line_start(line_start), line_end(line_end), - length(0.f), time(0.f), time_max(0.f), slowdown(false) {} - - bool adjustable(bool slowdown_external_perimeters) const { - return (this->type & TYPE_ADJUSTABLE) && - (! (this->type & TYPE_EXTERNAL_PERIMETER) || slowdown_external_perimeters) && - this->time < this->time_max; - } - - size_t type; - // Start of this line at the G-code snippet. - size_t line_start; - // End of this line at the G-code snippet. - size_t line_end; - // XY Euclidian length of this segment. - float length; - // Current duration of this segment. - float time; - // Maximum duration of this segment. - float time_max; - // If marked with the "slowdown" flag, the line has been slowed down. - bool slowdown; - }; - - // Extruder, for which the G-code will be adjusted. - unsigned int extruder_id; - // Parsed lines. - std::vector lines; - }; - std::vector adjustments(num_extruders, Adjustment()); - for (size_t i = 0; i < num_extruders; ++ i) - adjustments[i].extruder_id = extruders[i].id(); - const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); + std::vector per_extruder_adjustments(num_extruders); + std::vector map_extruder_to_per_extruder_adjustment(num_extruders, 0); + for (size_t i = 0; i < num_extruders; ++ i) { + unsigned int extruder_id = extruders[i].id(); + per_extruder_adjustments[i].extruder_id = extruder_id; + per_extruder_adjustments[i].min_print_speed = config.min_print_speed.get_at(i); + map_extruder_to_per_extruder_adjustment[extruder_id] = i; + } + const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); // Parse the layer G-code for the moves, which could be adjusted. { - float min_print_speed = float(EXTRUDER_CONFIG(min_print_speed)); - auto adjustment = std::lower_bound(adjustments.begin(), adjustments.end(), Adjustment(m_current_extruder)); + PerExtruderAdjustments *adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[m_current_extruder]]; unsigned int initial_extruder = m_current_extruder; const char *line_start = gcode.c_str(); const char *line_end = line_start; const char extrusion_axis = config.get_extrusion_axis()[0]; - // Index of an existing Adjustment::Line of the current adjustment, which holds the feedrate setting command + // Index of an existing CoolingLine of the current adjustment, which holds the feedrate setting command // for a sequence of extrusion moves. size_t active_speed_modifier = size_t(-1); for (; *line_start != 0; line_start = line_end) { @@ -164,16 +230,16 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ ++ line_end; // sline will not contain the trailing '\n'. std::string sline(line_start, line_end); - // Adjustment::Line will contain the trailing '\n'. + // CoolingLine will contain the trailing '\n'. if (*line_end == '\n') ++ line_end; - Adjustment::Line line(0, line_start - gcode.c_str(), line_end - gcode.c_str()); + CoolingLine line(0, line_start - gcode.c_str(), line_end - gcode.c_str()); if (boost::starts_with(sline, "G0 ")) - line.type = Adjustment::Line::TYPE_G0; + line.type = CoolingLine::TYPE_G0; else if (boost::starts_with(sline, "G1 ")) - line.type = Adjustment::Line::TYPE_G1; + line.type = CoolingLine::TYPE_G1; else if (boost::starts_with(sline, "G92 ")) - line.type = Adjustment::Line::TYPE_G92; + line.type = CoolingLine::TYPE_G92; if (line.type) { // G0, G1 or G92 // Parse the G-code line. @@ -192,9 +258,9 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ if (axis == 4) { // Convert mm/min to mm/sec. new_pos[4] /= 60.f; - if ((line.type & Adjustment::Line::TYPE_G92) == 0) + if ((line.type & CoolingLine::TYPE_G92) == 0) // This is G0 or G1 line and it sets the feedrate. This mark is used for reducing the duplicate F calls. - line.type |= Adjustment::Line::TYPE_HAS_F; + line.type |= CoolingLine::TYPE_HAS_F; } } // Skip this word. @@ -203,14 +269,14 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ bool external_perimeter = boost::contains(sline, ";_EXTERNAL_PERIMETER"); bool wipe = boost::contains(sline, ";_WIPE"); if (external_perimeter) - line.type |= Adjustment::Line::TYPE_EXTERNAL_PERIMETER; + line.type |= CoolingLine::TYPE_EXTERNAL_PERIMETER; if (wipe) - line.type |= Adjustment::Line::TYPE_WIPE; + line.type |= CoolingLine::TYPE_WIPE; if (boost::contains(sline, ";_EXTRUDE_SET_SPEED") && ! wipe) { - line.type |= Adjustment::Line::TYPE_ADJUSTABLE; + line.type |= CoolingLine::TYPE_ADJUSTABLE; active_speed_modifier = adjustment->lines.size(); } - if ((line.type & Adjustment::Line::TYPE_G92) == 0) { + if ((line.type & CoolingLine::TYPE_G92) == 0) { // G0 or G1. Calculate the duration. if (config.use_relative_e_distances.value) // Reset extruder accumulator. @@ -227,15 +293,17 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ // Movement in the extruder axis. line.length = std::abs(dif[3]); } - if (line.length > 0) - line.time = line.length / new_pos[4]; // current F + if (line.length > 0) { + line.feedrate = new_pos[4]; // current F + line.time = line.length / line.feedrate; + } line.time_max = line.time; - if ((line.type & Adjustment::Line::TYPE_ADJUSTABLE) || active_speed_modifier != size_t(-1)) - line.time_max = (min_print_speed == 0.f) ? FLT_MAX : std::max(line.time, line.length / min_print_speed); - if (active_speed_modifier < adjustment->lines.size() && (line.type & Adjustment::Line::TYPE_G1)) { + if ((line.type & CoolingLine::TYPE_ADJUSTABLE) || active_speed_modifier != size_t(-1)) + line.time_max = (adjustment->min_print_speed == 0.f) ? FLT_MAX : std::max(line.time, line.length / adjustment->min_print_speed); + if (active_speed_modifier < adjustment->lines.size() && (line.type & CoolingLine::TYPE_G1)) { // Inside the ";_EXTRUDE_SET_SPEED" blocks, there must not be a G1 Fxx entry. - assert((line.type & Adjustment::Line::TYPE_HAS_F) == 0); - Adjustment::Line &sm = adjustment->lines[active_speed_modifier]; + assert((line.type & CoolingLine::TYPE_HAS_F) == 0); + CoolingLine &sm = adjustment->lines[active_speed_modifier]; sm.length += line.length; sm.time += line.time; if (sm.time_max != FLT_MAX) { @@ -250,24 +318,23 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ } m_current_pos = std::move(new_pos); } else if (boost::starts_with(sline, ";_EXTRUDE_END")) { - line.type = Adjustment::Line::TYPE_EXTRUDE_END; + line.type = CoolingLine::TYPE_EXTRUDE_END; active_speed_modifier = size_t(-1); } else if (boost::starts_with(sline, toolchange_prefix)) { // Switch the tool. - line.type = Adjustment::Line::TYPE_SET_TOOL; + line.type = CoolingLine::TYPE_SET_TOOL; unsigned int new_extruder = (unsigned int)atoi(sline.c_str() + toolchange_prefix.size()); if (new_extruder != m_current_extruder) { m_current_extruder = new_extruder; - min_print_speed = float(EXTRUDER_CONFIG(min_print_speed)); - adjustment = std::lower_bound(adjustments.begin(), adjustments.end(), Adjustment(m_current_extruder)); + adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[m_current_extruder]]; } } else if (boost::starts_with(sline, ";_BRIDGE_FAN_START")) { - line.type = Adjustment::Line::TYPE_BRIDGE_FAN_START; + line.type = CoolingLine::TYPE_BRIDGE_FAN_START; } else if (boost::starts_with(sline, ";_BRIDGE_FAN_END")) { - line.type = Adjustment::Line::TYPE_BRIDGE_FAN_END; + line.type = CoolingLine::TYPE_BRIDGE_FAN_END; } else if (boost::starts_with(sline, "G4 ")) { // Parse the wait time. - line.type = Adjustment::Line::TYPE_G4; + line.type = CoolingLine::TYPE_G4; size_t pos_S = sline.find('S', 3); size_t pos_P = sline.find('P', 3); line.time = line.time_max = float( @@ -281,18 +348,19 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ } // Sort the extruders by the increasing slowdown_below_layer_time. - std::vector by_slowdown_layer_time; - by_slowdown_layer_time.reserve(num_extruders); + std::vector extruder_by_slowdown_time; + extruder_by_slowdown_time.reserve(num_extruders); // Only insert entries, which are adjustable (have cooling enabled and non-zero stretchable time). // Collect total print time of non-adjustable extruders. float elapsed_time_total_non_adjustable = 0.f; for (size_t i = 0; i < num_extruders; ++ i) { - if (config.cooling.get_at(extruders[i].id())) - by_slowdown_layer_time.emplace_back(i); - else - elapsed_time_total_non_adjustable += adjustments[i].elapsed_time_total(); + if (config.cooling.get_at(extruders[i].id())) { + extruder_by_slowdown_time.emplace_back(i); + per_extruder_adjustments[i].sort_lines_by_decreasing_feedrate(); + } else + elapsed_time_total_non_adjustable += per_extruder_adjustments[i].elapsed_time_total(); } - std::sort(by_slowdown_layer_time.begin(), by_slowdown_layer_time.end(), + std::sort(extruder_by_slowdown_time.begin(), extruder_by_slowdown_time.end(), [&config, &extruders](const size_t idx1, const size_t idx2){ return config.slowdown_below_layer_time.get_at(extruders[idx1].id()) < config.slowdown_below_layer_time.get_at(extruders[idx2].id()); @@ -303,18 +371,18 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ { // Elapsed time for the already adjusted extruders. float elapsed_time_total0 = elapsed_time_total_non_adjustable; - for (size_t i_by_slowdown_layer_time = 0; i_by_slowdown_layer_time < by_slowdown_layer_time.size(); ++ i_by_slowdown_layer_time) { - // Idx in adjustments. - size_t idx = by_slowdown_layer_time[i_by_slowdown_layer_time]; - // Macro to sum or adjust all sections starting with i_by_slowdown_layer_time. + for (size_t i_extruder_by_slowdown_time = 0; i_extruder_by_slowdown_time < extruder_by_slowdown_time.size(); ++ i_extruder_by_slowdown_time) { + // Idx in per_extruder_adjustments. + size_t idx = extruder_by_slowdown_time[i_extruder_by_slowdown_time]; + // Macro to sum or adjust all sections starting with i_extruder_by_slowdown_time. #define FORALL_UNPROCESSED(ACCUMULATOR, ACTION) \ ACCUMULATOR = elapsed_time_total0;\ - for (size_t j = i_by_slowdown_layer_time; j < by_slowdown_layer_time.size(); ++ j) \ - ACCUMULATOR += adjustments[by_slowdown_layer_time[j]].ACTION + for (size_t j = i_extruder_by_slowdown_time; j < extruder_by_slowdown_time.size(); ++ j) \ + ACCUMULATOR += per_extruder_adjustments[extruder_by_slowdown_time[j]].ACTION // Calculate the current adjusted elapsed_time_total over the non-finalized extruders. float total; FORALL_UNPROCESSED(total, elapsed_time_total()); - float slowdown_below_layer_time = float(config.slowdown_below_layer_time.get_at(adjustments[idx].extruder_id)) * 1.001f; + float slowdown_below_layer_time = float(config.slowdown_below_layer_time.get_at(per_extruder_adjustments[idx].extruder_id)) * 1.001f; if (total > slowdown_below_layer_time) { // The current total time is above the minimum threshold of the rest of the extruders, don't adjust anything. } else { @@ -323,8 +391,9 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ float max_time; FORALL_UNPROCESSED(max_time, maximum_time(true)); if (max_time > slowdown_below_layer_time) { - // By slowing every possible movement, the layer time could be reached. Now decide - // whether the external perimeters shall be slowed down as well. + // By slowing every possible movement, the layer time could be reached. +#if 0 + // Now decide, whether the external perimeters shall be slowed down as well. float max_time_nep; FORALL_UNPROCESSED(max_time_nep, maximum_time(false)); if (max_time_nep > slowdown_below_layer_time) { @@ -355,34 +424,98 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ break; } } +#else + // Slow down. Try to equalize the feedrates. + std::vector by_min_print_speed; + by_min_print_speed.reserve(extruder_by_slowdown_time.size() - i_extruder_by_slowdown_time); + for (size_t j = i_extruder_by_slowdown_time; j < extruder_by_slowdown_time.size(); ++ j) + by_min_print_speed.emplace_back(&per_extruder_adjustments[extruder_by_slowdown_time[j]]); + // Find the next highest adjustable feedrate among the extruders. + float feedrate = 0; + for (PerExtruderAdjustments *adj : by_min_print_speed) + if (adj->idx_line_begin < adj->n_lines_adjustable && adj->lines[adj->idx_line_begin].feedrate > feedrate) + feedrate = adj->lines[adj->idx_line_begin].feedrate; + if (feedrate == 0) + // No adjustable line is left. + break; + // Sort by min_print_speed, maximum speed first. + std::sort(by_min_print_speed.begin(), by_min_print_speed.end(), + [](const PerExtruderAdjustments *p1, const PerExtruderAdjustments *p2){ return p1->min_print_speed > p2->min_print_speed; }); + // Slow down, fast moves first. + for (;;) { + // For each extruder, find the span of lines with a feedrate close to feedrate. + for (PerExtruderAdjustments *adj : by_min_print_speed) { + for (adj->idx_line_end = adj->idx_line_begin; + adj->idx_line_end < adj->n_lines_adjustable && adj->lines[adj->idx_line_end].feedrate > feedrate - EPSILON; + ++ adj->idx_line_end) ; + } + // Find the next highest adjustable feedrate among the extruders. + float feedrate_next = 0.f; + for (PerExtruderAdjustments *adj : by_min_print_speed) + if (adj->idx_line_end < adj->n_lines_adjustable && adj->lines[adj->idx_line_end].feedrate > feedrate_next) + feedrate_next = adj->lines[adj->idx_line_end].feedrate; + // Slow down, limited by max(feedrate_next, min_print_speed). + for (auto adj = by_min_print_speed.begin(); adj != by_min_print_speed.end();) { + float feedrate_limit = std::max(feedrate_next, (*adj)->min_print_speed); + float time_stretch = slowdown_below_layer_time - total; + float time_stretch_max = 0.f; + std::pair time_stretched(0.f, 0.f); + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + time_stretch_max += (*it)->time_stretch_when_slowing_down_to(feedrate_limit); + bool done = false; + if (time_stretch_max > time_stretch) { + feedrate_limit = feedrate - (feedrate - feedrate_limit) * time_stretch / time_stretch_max; + done = true; + } + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + (*it)->slow_down_to(feedrate_limit); + if (done) { + // Break from two levels of loops. + feedrate_next = 0.f; + break; + } + // Skip the other extruders with nearly the same min_print_speed, as they have been processed already. + auto next = adj; + for (++ next; next != by_min_print_speed.end() && (*next)->min_print_speed > (*adj)->min_print_speed - EPSILON; ++ next); + adj = next; + } + if (feedrate_next == 0.f) + // There are no other extrusions available for slow down. + break; + for (PerExtruderAdjustments *adj : by_min_print_speed) { + adj->idx_line_begin = adj->idx_line_end; + feedrate = feedrate_next; + } + } +#endif } else { // Slow down to maximum possible. FORALL_UNPROCESSED(total, slow_down_maximum(true)); } } #undef FORALL_UNPROCESSED - // Sum the final elapsed time for all extruders up to i_by_slowdown_layer_time. - if (i_by_slowdown_layer_time + 1 == by_slowdown_layer_time.size()) + // Sum the final elapsed time for all extruders up to i_extruder_by_slowdown_time. + if (i_extruder_by_slowdown_time + 1 == extruder_by_slowdown_time.size()) // Optimization for single extruder prints. elapsed_time_total0 = total; else - elapsed_time_total0 += adjustments[idx].elapsed_time_total(); + elapsed_time_total0 += per_extruder_adjustments[idx].elapsed_time_total(); } elapsed_time_total = elapsed_time_total0; } // Transform the G-code. // First sort the adjustment lines by their position in the source G-code. - std::vector lines; + std::vector lines; { size_t n_lines = 0; - for (const Adjustment &adj : adjustments) + for (const PerExtruderAdjustments &adj : per_extruder_adjustments) n_lines += adj.lines.size(); lines.reserve(n_lines); - for (const Adjustment &adj : adjustments) - for (const Adjustment::Line &line : adj.lines) + for (const PerExtruderAdjustments &adj : per_extruder_adjustments) + for (const CoolingLine &line : adj.lines) lines.emplace_back(&line); - std::sort(lines.begin(), lines.end(), [](const Adjustment::Line *ln1, const Adjustment::Line *ln2) { return ln1->line_start < ln2->line_start; } ); + std::sort(lines.begin(), lines.end(), [](const CoolingLine *ln1, const CoolingLine *ln2) { return ln1->line_start < ln2->line_start; } ); } // Second generate the adjusted G-code. std::string new_gcode; @@ -425,27 +558,27 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ const char *pos = gcode.c_str(); int current_feedrate = 0; - for (const Adjustment::Line *line : lines) { + for (const CoolingLine *line : lines) { const char *line_start = gcode.c_str() + line->line_start; const char *line_end = gcode.c_str() + line->line_end; if (line_start > pos) new_gcode.append(pos, line_start - pos); - if (line->type & Adjustment::Line::TYPE_SET_TOOL) { + if (line->type & CoolingLine::TYPE_SET_TOOL) { unsigned int new_extruder = (unsigned int)atoi(line_start + toolchange_prefix.size()); if (new_extruder != m_current_extruder) { m_current_extruder = new_extruder; change_extruder_set_fan(); } new_gcode.append(line_start, line_end - line_start); - } else if (line->type & Adjustment::Line::TYPE_BRIDGE_FAN_START) { + } else if (line->type & CoolingLine::TYPE_BRIDGE_FAN_START) { if (bridge_fan_control) new_gcode += m_gcodegen.writer().set_fan(bridge_fan_speed, true); - } else if (line->type & Adjustment::Line::TYPE_BRIDGE_FAN_END) { + } else if (line->type & CoolingLine::TYPE_BRIDGE_FAN_END) { if (bridge_fan_control) new_gcode += m_gcodegen.writer().set_fan(fan_speed, true); - } else if (line->type & Adjustment::Line::TYPE_EXTRUDE_END) { + } else if (line->type & CoolingLine::TYPE_EXTRUDE_END) { // Just remove this comment. - } else if (line->type & (Adjustment::Line::TYPE_ADJUSTABLE | Adjustment::Line::TYPE_EXTERNAL_PERIMETER | Adjustment::Line::TYPE_WIPE | Adjustment::Line::TYPE_HAS_F)) { + } else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) { // Find the start of a comment, or roll to the end of line. const char *end = line_start; for (; end < line_end && *end != ';'; ++ end); @@ -456,14 +589,14 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ assert(fpos != nullptr); if (line->slowdown) { modify = true; - new_feedrate = int(floor(60. * (line->length / line->time) + 0.5)); + new_feedrate = int(floor(60. * line->feedrate + 0.5)); } else { new_feedrate = atoi(fpos); if (new_feedrate != current_feedrate) { // Append the line without the comment. new_gcode.append(line_start, end - line_start); current_feedrate = new_feedrate; - } else if ((line->type & (Adjustment::Line::TYPE_ADJUSTABLE | Adjustment::Line::TYPE_EXTERNAL_PERIMETER | Adjustment::Line::TYPE_WIPE)) || line->length == 0.) { + } else if ((line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE)) || line->length == 0.) { // Feedrate does not change and this line does not move the print head. Skip the complete G-code line including the G-code comment. end = line_end; } else { @@ -497,13 +630,13 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ } // Process the rest of the line. if (end < line_end) { - if (line->type & (Adjustment::Line::TYPE_ADJUSTABLE | Adjustment::Line::TYPE_EXTERNAL_PERIMETER | Adjustment::Line::TYPE_WIPE)) { + if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE)) { // Process comments, remove ";_EXTRUDE_SET_SPEED", ";_EXTERNAL_PERIMETER", ";_WIPE" std::string comment(end, line_end); boost::replace_all(comment, ";_EXTRUDE_SET_SPEED", ""); - if (line->type & Adjustment::Line::TYPE_EXTERNAL_PERIMETER) + if (line->type & CoolingLine::TYPE_EXTERNAL_PERIMETER) boost::replace_all(comment, ";_EXTERNAL_PERIMETER", ""); - if (line->type & Adjustment::Line::TYPE_WIPE) + if (line->type & CoolingLine::TYPE_WIPE) boost::replace_all(comment, ";_WIPE", ""); new_gcode += comment; } else { From 269770bbbc373f1462631f918b0bb894db7422a1 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Wed, 25 Apr 2018 22:06:44 +0200 Subject: [PATCH 36/38] Fix of a new cooling logic. --- t/cooling.t | 6 +-- xs/src/libslic3r/GCode/CoolingBuffer.cpp | 60 ++++++++++++++++-------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/t/cooling.t b/t/cooling.t index ee4f6abea..f69b7e8a8 100644 --- a/t/cooling.t +++ b/t/cooling.t @@ -2,7 +2,7 @@ use Test::More; use strict; use warnings; -plan tests => 15; +plan tests => 14; BEGIN { use FindBin; @@ -203,8 +203,8 @@ $config->set('disable_fan_first_layers', [ 0 ]); ok $all_below, 'slowdown_below_layer_time is honored'; # check that all layers have at least one unaltered external perimeter speed - my $external = all { $_ > 0 } values %layer_external; - ok $external, 'slowdown_below_layer_time does not alter external perimeters'; +# my $external = all { $_ > 0 } values %layer_external; +# ok $external, 'slowdown_below_layer_time does not alter external perimeters'; } __END__ diff --git a/xs/src/libslic3r/GCode/CoolingBuffer.cpp b/xs/src/libslic3r/GCode/CoolingBuffer.cpp index 121cbb017..ca9fc6555 100644 --- a/xs/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/xs/src/libslic3r/GCode/CoolingBuffer.cpp @@ -102,6 +102,13 @@ struct PerExtruderAdjustments time_total += line.time; return time_total; } + float adjustable_time(bool slowdown_external_perimeters) { + float time_total = 0.f; + for (const CoolingLine &line : lines) + if (line.adjustable(slowdown_external_perimeters)) + time_total += line.time; + return time_total; + } // Calculate the non-adjustable part of the total time. float non_adjustable_time(bool slowdown_external_perimeters) { float time_total = 0.f; @@ -207,11 +214,14 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ const size_t num_extruders = extruders.size(); std::vector per_extruder_adjustments(num_extruders); - std::vector map_extruder_to_per_extruder_adjustment(num_extruders, 0); + unsigned int id_extruder_max = 0; + for (const Extruder &ex : extruders) + id_extruder_max = std::max(ex.id(), id_extruder_max); + std::vector map_extruder_to_per_extruder_adjustment(id_extruder_max + 1, 0); for (size_t i = 0; i < num_extruders; ++ i) { unsigned int extruder_id = extruders[i].id(); per_extruder_adjustments[i].extruder_id = extruder_id; - per_extruder_adjustments[i].min_print_speed = config.min_print_speed.get_at(i); + per_extruder_adjustments[i].min_print_speed = config.min_print_speed.get_at(extruder_id); map_extruder_to_per_extruder_adjustment[extruder_id] = i; } const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); @@ -442,6 +452,7 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ std::sort(by_min_print_speed.begin(), by_min_print_speed.end(), [](const PerExtruderAdjustments *p1, const PerExtruderAdjustments *p2){ return p1->min_print_speed > p2->min_print_speed; }); // Slow down, fast moves first. + float time_stretch = slowdown_below_layer_time - total; for (;;) { // For each extruder, find the span of lines with a feedrate close to feedrate. for (PerExtruderAdjustments *adj : by_min_print_speed) { @@ -457,23 +468,34 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ // Slow down, limited by max(feedrate_next, min_print_speed). for (auto adj = by_min_print_speed.begin(); adj != by_min_print_speed.end();) { float feedrate_limit = std::max(feedrate_next, (*adj)->min_print_speed); - float time_stretch = slowdown_below_layer_time - total; - float time_stretch_max = 0.f; - std::pair time_stretched(0.f, 0.f); - for (auto it = adj; it != by_min_print_speed.end(); ++ it) - time_stretch_max += (*it)->time_stretch_when_slowing_down_to(feedrate_limit); - bool done = false; - if (time_stretch_max > time_stretch) { - feedrate_limit = feedrate - (feedrate - feedrate_limit) * time_stretch / time_stretch_max; - done = true; - } - for (auto it = adj; it != by_min_print_speed.end(); ++ it) - (*it)->slow_down_to(feedrate_limit); - if (done) { - // Break from two levels of loops. - feedrate_next = 0.f; - break; - } + if (feedrate_limit == 0.f) { + float adjustable_time = 0.f; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + adjustable_time += (*it)->adjustable_time(true); + float ratio = (adjustable_time + time_stretch) / adjustable_time; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + (*it)->slow_down_proportional(ratio, true); + // Break from two levels of loops. + feedrate_next = 0.f; + break; + } else { + float time_stretch_max = 0.f; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + time_stretch_max += (*it)->time_stretch_when_slowing_down_to(feedrate_limit); + bool done = false; + if (time_stretch_max > time_stretch) { + feedrate_limit = feedrate - (feedrate - feedrate_limit) * time_stretch / time_stretch_max; + done = true; + } + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + (*it)->slow_down_to(feedrate_limit); + if (done) { + // Break from two levels of loops. + feedrate_next = 0.f; + break; + } + time_stretch -= time_stretch_max; + } // Skip the other extruders with nearly the same min_print_speed, as they have been processed already. auto next = adj; for (++ next; next != by_min_print_speed.end() && (*next)->min_print_speed > (*adj)->min_print_speed - EPSILON; ++ next); From cbaf0ccc51133db07b47e1de0177b5db4fae2a02 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Wed, 25 Apr 2018 22:54:52 +0200 Subject: [PATCH 37/38] Refactored cooling logic for readibility and maintainability. --- t/cooling.t | 1 + xs/src/libslic3r/GCode/CoolingBuffer.cpp | 753 ++++++++++++----------- xs/src/libslic3r/GCode/CoolingBuffer.hpp | 26 +- 3 files changed, 430 insertions(+), 350 deletions(-) diff --git a/t/cooling.t b/t/cooling.t index f69b7e8a8..2f444cf9d 100644 --- a/t/cooling.t +++ b/t/cooling.t @@ -79,6 +79,7 @@ $config->set('disable_fan_first_layers', [ 0 ]); "G1 X50 F2500\n" . "G1 F3000;_EXTRUDE_SET_SPEED\n" . "G1 X100 E1\n" . + ";_EXTRUDE_END\n" . "G1 E4 F400", # Print time of $gcode. my $print_time = 50 / (2500 / 60) + 100 / (3000 / 60) + 4 / (400 / 60); diff --git a/xs/src/libslic3r/GCode/CoolingBuffer.cpp b/xs/src/libslic3r/GCode/CoolingBuffer.cpp index ca9fc6555..141d197ca 100644 --- a/xs/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/xs/src/libslic3r/GCode/CoolingBuffer.cpp @@ -89,8 +89,9 @@ struct PerExtruderAdjustments time_total += line.time; return time_total; } - // Calculate the maximum time when slowing down. - float maximum_time(bool slowdown_external_perimeters) { + // Calculate the total elapsed time when slowing down + // to the minimum extrusion feed rate defined for the current material. + float maximum_time_after_slowdown(bool slowdown_external_perimeters) { float time_total = 0.f; for (const CoolingLine &line : lines) if (line.adjustable(slowdown_external_perimeters)) { @@ -102,6 +103,7 @@ struct PerExtruderAdjustments time_total += line.time; return time_total; } + // Calculate the adjustable part of the total time. float adjustable_time(bool slowdown_external_perimeters) { float time_total = 0.f; for (const CoolingLine &line : lines) @@ -117,7 +119,9 @@ struct PerExtruderAdjustments time_total += line.time; return time_total; } - float slow_down_maximum(bool slowdown_external_perimeters) { + // Slow down the adjustable extrusions to the minimum feedrate allowed for the current extruder material. + // Used by both proportional and non-proportional slow down. + float slowdown_to_minimum_feedrate(bool slowdown_external_perimeters) { float time_total = 0.f; for (CoolingLine &line : lines) { if (line.adjustable(slowdown_external_perimeters)) { @@ -130,6 +134,8 @@ struct PerExtruderAdjustments } return time_total; } + // Slow down each adjustable G-code line proportionally by a factor. + // Used by the proportional slow down. float slow_down_proportional(float factor, bool slowdown_external_perimeters) { assert(factor >= 1.f); float time_total = 0.f; @@ -144,9 +150,8 @@ struct PerExtruderAdjustments return time_total; } - bool operator<(const PerExtruderAdjustments &rhs) const { return this->extruder_id < rhs.extruder_id; } - // Sort the lines, adjustable first, higher feedrate first. + // Used by non-proportional slow down. void sort_lines_by_decreasing_feedrate() { std::sort(lines.begin(), lines.end(), [](const CoolingLine &l1, const CoolingLine &l2) { bool adj1 = l1.adjustable(); @@ -161,34 +166,41 @@ struct PerExtruderAdjustments time_non_adjustable += lines[i].time; } - // Calculate the maximum time when slowing down. - float time_stretch_when_slowing_down_to(float min_feedrate) { + // Calculate the maximum time stretch when slowing down to min_feedrate. + // Slowdown to min_feedrate shall be allowed for this extruder's material. + // Used by non-proportional slow down. + float time_stretch_when_slowing_down_to_feedrate(float min_feedrate) { float time_stretch = 0.f; - if (this->min_print_speed < min_feedrate + EPSILON) { - for (size_t i = 0; i < n_lines_adjustable; ++ i) { - const CoolingLine &line = lines[i]; - if (line.feedrate > min_feedrate) - time_stretch += line.time * (line.feedrate / min_feedrate - 1.f); - } + assert(this->min_print_speed < min_feedrate + EPSILON); + for (size_t i = 0; i < n_lines_adjustable; ++ i) { + const CoolingLine &line = lines[i]; + if (line.feedrate > min_feedrate) + time_stretch += line.time * (line.feedrate / min_feedrate - 1.f); } return time_stretch; } - void slow_down_to(float min_feedrate) { - if (this->min_print_speed < min_feedrate + EPSILON) { - for (size_t i = 0; i < n_lines_adjustable; ++ i) { - CoolingLine &line = lines[i]; - if (line.feedrate > min_feedrate) { - line.time *= std::max(1.f, line.feedrate / min_feedrate); - line.feedrate = min_feedrate; - line.slowdown = true; - } + // Slow down all adjustable lines down to min_feedrate. + // Slowdown to min_feedrate shall be allowed for this extruder's material. + // Used by non-proportional slow down. + void slow_down_to_feedrate(float min_feedrate) { + assert(this->min_print_speed < min_feedrate + EPSILON); + for (size_t i = 0; i < n_lines_adjustable; ++ i) { + CoolingLine &line = lines[i]; + if (line.feedrate > min_feedrate) { + line.time *= std::max(1.f, line.feedrate / min_feedrate); + line.feedrate = min_feedrate; + line.slowdown = true; } } } // Extruder, for which the G-code will be adjusted. unsigned int extruder_id = 0; + // Is the cooling slow down logic enabled for this extruder's material? + bool cooling_slow_down_enabled = false; + // Slow down the print down to min_print_speed if the total layer time is below slowdown_below_layer_time. + float slowdown_below_layer_time = 0.f; // Minimum print speed allowed for this extruder. float min_print_speed = 0.f; @@ -199,335 +211,387 @@ struct PerExtruderAdjustments size_t n_lines_adjustable = 0; // Non-adjustable time of lines starting with n_lines_adjustable. float time_non_adjustable = 0; + // Current total time for this extruder. + float time_total = 0; + // Maximum time for this extruder, when the maximum slow down is applied. + float time_maximum = 0; // Temporaries for processing the slow down. Both thresholds go from 0 to n_lines_adjustable. size_t idx_line_begin = 0; size_t idx_line_end = 0; }; -#define EXTRUDER_CONFIG(OPT) config.OPT.get_at(m_current_extruder) - std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_id) +{ + std::vector per_extruder_adjustments = this->parse_layer_gcode(gcode, m_current_pos); + float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); + return this->apply_layer_cooldown(gcode, layer_id, layer_time_stretched, per_extruder_adjustments); +} + +// Parse the layer G-code for the moves, which could be adjusted. +// Return the list of parsed lines, bucketed by an extruder. +std::vector CoolingBuffer::parse_layer_gcode(const std::string &gcode, std::vector ¤t_pos) const { const FullPrintConfig &config = m_gcodegen.config(); const std::vector &extruders = m_gcodegen.writer().extruders(); - const size_t num_extruders = extruders.size(); - - std::vector per_extruder_adjustments(num_extruders); - unsigned int id_extruder_max = 0; + unsigned int num_extruders = 0; for (const Extruder &ex : extruders) - id_extruder_max = std::max(ex.id(), id_extruder_max); - std::vector map_extruder_to_per_extruder_adjustment(id_extruder_max + 1, 0); - for (size_t i = 0; i < num_extruders; ++ i) { - unsigned int extruder_id = extruders[i].id(); - per_extruder_adjustments[i].extruder_id = extruder_id; - per_extruder_adjustments[i].min_print_speed = config.min_print_speed.get_at(extruder_id); + num_extruders = std::max(ex.id() + 1, num_extruders); + + std::vector per_extruder_adjustments(extruders.size()); + std::vector map_extruder_to_per_extruder_adjustment(num_extruders, 0); + for (size_t i = 0; i < extruders.size(); ++ i) { + PerExtruderAdjustments &adj = per_extruder_adjustments[i]; + unsigned int extruder_id = extruders[i].id(); + adj.extruder_id = extruder_id; + adj.cooling_slow_down_enabled = config.cooling.get_at(extruder_id); + adj.slowdown_below_layer_time = config.slowdown_below_layer_time.get_at(extruder_id); + adj.min_print_speed = config.min_print_speed.get_at(extruder_id); map_extruder_to_per_extruder_adjustment[extruder_id] = i; } - const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); - // Parse the layer G-code for the moves, which could be adjusted. + + const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); + unsigned int current_extruder = m_current_extruder; + PerExtruderAdjustments *adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[current_extruder]]; + const char *line_start = gcode.c_str(); + const char *line_end = line_start; + const char extrusion_axis = config.get_extrusion_axis()[0]; + // Index of an existing CoolingLine of the current adjustment, which holds the feedrate setting command + // for a sequence of extrusion moves. + size_t active_speed_modifier = size_t(-1); + + for (; *line_start != 0; line_start = line_end) { - PerExtruderAdjustments *adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[m_current_extruder]]; - unsigned int initial_extruder = m_current_extruder; - const char *line_start = gcode.c_str(); - const char *line_end = line_start; - const char extrusion_axis = config.get_extrusion_axis()[0]; - // Index of an existing CoolingLine of the current adjustment, which holds the feedrate setting command - // for a sequence of extrusion moves. - size_t active_speed_modifier = size_t(-1); - for (; *line_start != 0; line_start = line_end) { - while (*line_end != '\n' && *line_end != 0) - ++ line_end; - // sline will not contain the trailing '\n'. - std::string sline(line_start, line_end); - // CoolingLine will contain the trailing '\n'. - if (*line_end == '\n') - ++ line_end; - CoolingLine line(0, line_start - gcode.c_str(), line_end - gcode.c_str()); - if (boost::starts_with(sline, "G0 ")) - line.type = CoolingLine::TYPE_G0; - else if (boost::starts_with(sline, "G1 ")) - line.type = CoolingLine::TYPE_G1; - else if (boost::starts_with(sline, "G92 ")) - line.type = CoolingLine::TYPE_G92; - if (line.type) { - // G0, G1 or G92 - // Parse the G-code line. - std::vector new_pos(m_current_pos); - const char *c = sline.data() + 3; - for (;;) { - // Skip whitespaces. - for (; *c == ' ' || *c == '\t'; ++ c); - if (*c == 0 || *c == ';') - break; - // Parse the axis. - size_t axis = (*c >= 'X' && *c <= 'Z') ? (*c - 'X') : - (*c == extrusion_axis) ? 3 : (*c == 'F') ? 4 : size_t(-1); - if (axis != size_t(-1)) { - new_pos[axis] = float(atof(++c)); - if (axis == 4) { - // Convert mm/min to mm/sec. - new_pos[4] /= 60.f; - if ((line.type & CoolingLine::TYPE_G92) == 0) - // This is G0 or G1 line and it sets the feedrate. This mark is used for reducing the duplicate F calls. - line.type |= CoolingLine::TYPE_HAS_F; - } - } - // Skip this word. - for (; *c != ' ' && *c != '\t' && *c != 0; ++ c); - } - bool external_perimeter = boost::contains(sline, ";_EXTERNAL_PERIMETER"); - bool wipe = boost::contains(sline, ";_WIPE"); - if (external_perimeter) - line.type |= CoolingLine::TYPE_EXTERNAL_PERIMETER; - if (wipe) - line.type |= CoolingLine::TYPE_WIPE; - if (boost::contains(sline, ";_EXTRUDE_SET_SPEED") && ! wipe) { - line.type |= CoolingLine::TYPE_ADJUSTABLE; - active_speed_modifier = adjustment->lines.size(); - } - if ((line.type & CoolingLine::TYPE_G92) == 0) { - // G0 or G1. Calculate the duration. - if (config.use_relative_e_distances.value) - // Reset extruder accumulator. - m_current_pos[3] = 0.f; - float dif[4]; - for (size_t i = 0; i < 4; ++ i) - dif[i] = new_pos[i] - m_current_pos[i]; - float dxy2 = dif[0] * dif[0] + dif[1] * dif[1]; - float dxyz2 = dxy2 + dif[2] * dif[2]; - if (dxyz2 > 0.f) { - // Movement in xyz, calculate time from the xyz Euclidian distance. - line.length = sqrt(dxyz2); - } else if (std::abs(dif[3]) > 0.f) { - // Movement in the extruder axis. - line.length = std::abs(dif[3]); + while (*line_end != '\n' && *line_end != 0) + ++ line_end; + // sline will not contain the trailing '\n'. + std::string sline(line_start, line_end); + // CoolingLine will contain the trailing '\n'. + if (*line_end == '\n') + ++ line_end; + CoolingLine line(0, line_start - gcode.c_str(), line_end - gcode.c_str()); + if (boost::starts_with(sline, "G0 ")) + line.type = CoolingLine::TYPE_G0; + else if (boost::starts_with(sline, "G1 ")) + line.type = CoolingLine::TYPE_G1; + else if (boost::starts_with(sline, "G92 ")) + line.type = CoolingLine::TYPE_G92; + if (line.type) { + // G0, G1 or G92 + // Parse the G-code line. + std::vector new_pos(current_pos); + const char *c = sline.data() + 3; + for (;;) { + // Skip whitespaces. + for (; *c == ' ' || *c == '\t'; ++ c); + if (*c == 0 || *c == ';') + break; + // Parse the axis. + size_t axis = (*c >= 'X' && *c <= 'Z') ? (*c - 'X') : + (*c == extrusion_axis) ? 3 : (*c == 'F') ? 4 : size_t(-1); + if (axis != size_t(-1)) { + new_pos[axis] = float(atof(++c)); + if (axis == 4) { + // Convert mm/min to mm/sec. + new_pos[4] /= 60.f; + if ((line.type & CoolingLine::TYPE_G92) == 0) + // This is G0 or G1 line and it sets the feedrate. This mark is used for reducing the duplicate F calls. + line.type |= CoolingLine::TYPE_HAS_F; } - if (line.length > 0) { - line.feedrate = new_pos[4]; // current F - line.time = line.length / line.feedrate; - } - line.time_max = line.time; - if ((line.type & CoolingLine::TYPE_ADJUSTABLE) || active_speed_modifier != size_t(-1)) - line.time_max = (adjustment->min_print_speed == 0.f) ? FLT_MAX : std::max(line.time, line.length / adjustment->min_print_speed); - if (active_speed_modifier < adjustment->lines.size() && (line.type & CoolingLine::TYPE_G1)) { - // Inside the ";_EXTRUDE_SET_SPEED" blocks, there must not be a G1 Fxx entry. - assert((line.type & CoolingLine::TYPE_HAS_F) == 0); - CoolingLine &sm = adjustment->lines[active_speed_modifier]; - sm.length += line.length; - sm.time += line.time; - if (sm.time_max != FLT_MAX) { - if (line.time_max == FLT_MAX) - sm.time_max = FLT_MAX; - else - sm.time_max += line.time_max; - } - // Don't store this line. - line.type = 0; - } - } - m_current_pos = std::move(new_pos); - } else if (boost::starts_with(sline, ";_EXTRUDE_END")) { - line.type = CoolingLine::TYPE_EXTRUDE_END; - active_speed_modifier = size_t(-1); - } else if (boost::starts_with(sline, toolchange_prefix)) { - // Switch the tool. - line.type = CoolingLine::TYPE_SET_TOOL; - unsigned int new_extruder = (unsigned int)atoi(sline.c_str() + toolchange_prefix.size()); - if (new_extruder != m_current_extruder) { - m_current_extruder = new_extruder; - adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[m_current_extruder]]; } - } else if (boost::starts_with(sline, ";_BRIDGE_FAN_START")) { - line.type = CoolingLine::TYPE_BRIDGE_FAN_START; - } else if (boost::starts_with(sline, ";_BRIDGE_FAN_END")) { - line.type = CoolingLine::TYPE_BRIDGE_FAN_END; - } else if (boost::starts_with(sline, "G4 ")) { - // Parse the wait time. - line.type = CoolingLine::TYPE_G4; - size_t pos_S = sline.find('S', 3); - size_t pos_P = sline.find('P', 3); - line.time = line.time_max = float( - (pos_S > 0) ? atof(sline.c_str() + pos_S + 1) : - (pos_P > 0) ? atof(sline.c_str() + pos_P + 1) * 0.001 : 0.); + // Skip this word. + for (; *c != ' ' && *c != '\t' && *c != 0; ++ c); } - if (line.type != 0) - adjustment->lines.emplace_back(std::move(line)); - } - m_current_extruder = initial_extruder; + bool external_perimeter = boost::contains(sline, ";_EXTERNAL_PERIMETER"); + bool wipe = boost::contains(sline, ";_WIPE"); + if (external_perimeter) + line.type |= CoolingLine::TYPE_EXTERNAL_PERIMETER; + if (wipe) + line.type |= CoolingLine::TYPE_WIPE; + if (boost::contains(sline, ";_EXTRUDE_SET_SPEED") && ! wipe) { + line.type |= CoolingLine::TYPE_ADJUSTABLE; + active_speed_modifier = adjustment->lines.size(); + } + if ((line.type & CoolingLine::TYPE_G92) == 0) { + // G0 or G1. Calculate the duration. + if (config.use_relative_e_distances.value) + // Reset extruder accumulator. + current_pos[3] = 0.f; + float dif[4]; + for (size_t i = 0; i < 4; ++ i) + dif[i] = new_pos[i] - current_pos[i]; + float dxy2 = dif[0] * dif[0] + dif[1] * dif[1]; + float dxyz2 = dxy2 + dif[2] * dif[2]; + if (dxyz2 > 0.f) { + // Movement in xyz, calculate time from the xyz Euclidian distance. + line.length = sqrt(dxyz2); + } else if (std::abs(dif[3]) > 0.f) { + // Movement in the extruder axis. + line.length = std::abs(dif[3]); + } + if (line.length > 0) { + line.feedrate = new_pos[4]; // current F + line.time = line.length / line.feedrate; + } + line.time_max = line.time; + if ((line.type & CoolingLine::TYPE_ADJUSTABLE) || active_speed_modifier != size_t(-1)) + line.time_max = (adjustment->min_print_speed == 0.f) ? FLT_MAX : std::max(line.time, line.length / adjustment->min_print_speed); + if (active_speed_modifier < adjustment->lines.size() && (line.type & CoolingLine::TYPE_G1)) { + // Inside the ";_EXTRUDE_SET_SPEED" blocks, there must not be a G1 Fxx entry. + assert((line.type & CoolingLine::TYPE_HAS_F) == 0); + CoolingLine &sm = adjustment->lines[active_speed_modifier]; + sm.length += line.length; + sm.time += line.time; + if (sm.time_max != FLT_MAX) { + if (line.time_max == FLT_MAX) + sm.time_max = FLT_MAX; + else + sm.time_max += line.time_max; + } + // Don't store this line. + line.type = 0; + } + } + current_pos = std::move(new_pos); + } else if (boost::starts_with(sline, ";_EXTRUDE_END")) { + line.type = CoolingLine::TYPE_EXTRUDE_END; + active_speed_modifier = size_t(-1); + } else if (boost::starts_with(sline, toolchange_prefix)) { + // Switch the tool. + line.type = CoolingLine::TYPE_SET_TOOL; + unsigned int new_extruder = (unsigned int)atoi(sline.c_str() + toolchange_prefix.size()); + if (new_extruder != current_extruder) { + current_extruder = new_extruder; + adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[current_extruder]]; + } + } else if (boost::starts_with(sline, ";_BRIDGE_FAN_START")) { + line.type = CoolingLine::TYPE_BRIDGE_FAN_START; + } else if (boost::starts_with(sline, ";_BRIDGE_FAN_END")) { + line.type = CoolingLine::TYPE_BRIDGE_FAN_END; + } else if (boost::starts_with(sline, "G4 ")) { + // Parse the wait time. + line.type = CoolingLine::TYPE_G4; + size_t pos_S = sline.find('S', 3); + size_t pos_P = sline.find('P', 3); + line.time = line.time_max = float( + (pos_S > 0) ? atof(sline.c_str() + pos_S + 1) : + (pos_P > 0) ? atof(sline.c_str() + pos_P + 1) * 0.001 : 0.); + } + if (line.type != 0) + adjustment->lines.emplace_back(std::move(line)); } - // Sort the extruders by the increasing slowdown_below_layer_time. - std::vector extruder_by_slowdown_time; - extruder_by_slowdown_time.reserve(num_extruders); + return per_extruder_adjustments; +} + +// Slow down an extruder range proportionally down to slowdown_below_layer_time. +// Return the total time for the complete layer. +static inline float extruder_range_slow_down_proportional( + std::vector::iterator it_begin, + std::vector::iterator it_end, + // Elapsed time for the extruders already processed. + float elapsed_time_total0, + // Initial total elapsed time before slow down. + float elapsed_time_before_slowdown, + // Target time for the complete layer (all extruders applied). + float slowdown_below_layer_time) +{ + // Total layer time after the slow down has been applied. + float total_after_slowdown = elapsed_time_before_slowdown; + // Now decide, whether the external perimeters shall be slowed down as well. + float max_time_nep = elapsed_time_total0; + for (auto it = it_begin; it != it_end; ++ it) + max_time_nep += (*it)->maximum_time_after_slowdown(false); + if (max_time_nep > slowdown_below_layer_time) { + // It is sufficient to slow down the non-external perimeter moves to reach the target layer time. + // Slow down the non-external perimeters proportionally. + float non_adjustable_time = elapsed_time_total0; + for (auto it = it_begin; it != it_end; ++ it) + non_adjustable_time += (*it)->non_adjustable_time(false); + // The following step is a linear programming task due to the minimum movement speeds of the print moves. + // Run maximum 5 iterations until a good enough approximation is reached. + for (size_t iter = 0; iter < 5; ++ iter) { + float factor = (slowdown_below_layer_time - non_adjustable_time) / (total_after_slowdown - non_adjustable_time); + assert(factor > 1.f); + total_after_slowdown = elapsed_time_total0; + for (auto it = it_begin; it != it_end; ++ it) + total_after_slowdown += (*it)->slow_down_proportional(factor, false); + if (total_after_slowdown > 0.95f * slowdown_below_layer_time) + break; + } + } else { + // Slow down everything. First slow down the non-external perimeters to maximum. + for (auto it = it_begin; it != it_end; ++ it) + (*it)->slowdown_to_minimum_feedrate(false); + // Slow down the external perimeters proportionally. + float non_adjustable_time = elapsed_time_total0; + for (auto it = it_begin; it != it_end; ++ it) + non_adjustable_time += (*it)->non_adjustable_time(true); + for (size_t iter = 0; iter < 5; ++ iter) { + float factor = (slowdown_below_layer_time - non_adjustable_time) / (total_after_slowdown - non_adjustable_time); + assert(factor > 1.f); + total_after_slowdown = elapsed_time_total0; + for (auto it = it_begin; it != it_end; ++ it) + total_after_slowdown += (*it)->slow_down_proportional(factor, true); + if (total_after_slowdown > 0.95f * slowdown_below_layer_time) + break; + } + } + return total_after_slowdown; +} + +// Slow down an extruder range to slowdown_below_layer_time. +// Return the total time for the complete layer. +static inline void extruder_range_slow_down_non_proportional( + std::vector::iterator it_begin, + std::vector::iterator it_end, + float time_stretch) +{ + // Slow down. Try to equalize the feedrates. + std::vector by_min_print_speed(it_begin, it_end); + // Find the next highest adjustable feedrate among the extruders. + float feedrate = 0; + for (PerExtruderAdjustments *adj : by_min_print_speed) { + adj->idx_line_begin = 0; + adj->idx_line_end = 0; + assert(adj->idx_line_begin < adj->n_lines_adjustable); + if (adj->lines[adj->idx_line_begin].feedrate > feedrate) + feedrate = adj->lines[adj->idx_line_begin].feedrate; + } + assert(feedrate > 0.f); + // Sort by min_print_speed, maximum speed first. + std::sort(by_min_print_speed.begin(), by_min_print_speed.end(), + [](const PerExtruderAdjustments *p1, const PerExtruderAdjustments *p2){ return p1->min_print_speed > p2->min_print_speed; }); + // Slow down, fast moves first. + for (;;) { + // For each extruder, find the span of lines with a feedrate close to feedrate. + for (PerExtruderAdjustments *adj : by_min_print_speed) { + for (adj->idx_line_end = adj->idx_line_begin; + adj->idx_line_end < adj->n_lines_adjustable && adj->lines[adj->idx_line_end].feedrate > feedrate - EPSILON; + ++ adj->idx_line_end) ; + } + // Find the next highest adjustable feedrate among the extruders. + float feedrate_next = 0.f; + for (PerExtruderAdjustments *adj : by_min_print_speed) + if (adj->idx_line_end < adj->n_lines_adjustable && adj->lines[adj->idx_line_end].feedrate > feedrate_next) + feedrate_next = adj->lines[adj->idx_line_end].feedrate; + // Slow down, limited by max(feedrate_next, min_print_speed). + for (auto adj = by_min_print_speed.begin(); adj != by_min_print_speed.end();) { + // Slow down at most by time_stretch. + if ((*adj)->min_print_speed == 0.f) { + // All the adjustable speeds are now lowered to the same speed, + // and the minimum speed is set to zero. + float time_adjustable = 0.f; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + time_adjustable += (*it)->adjustable_time(true); + float rate = (time_adjustable + time_stretch) / time_adjustable; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + (*it)->slow_down_proportional(rate, true); + return; + } else { + float feedrate_limit = std::max(feedrate_next, (*adj)->min_print_speed); + bool done = false; + float time_stretch_max = 0.f; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + time_stretch_max += (*it)->time_stretch_when_slowing_down_to_feedrate(feedrate_limit); + if (time_stretch_max >= time_stretch) { + feedrate_limit = feedrate - (feedrate - feedrate_limit) * time_stretch / time_stretch_max; + done = true; + } else + time_stretch -= time_stretch_max; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + (*it)->slow_down_to_feedrate(feedrate_limit); + if (done) + return; + } + // Skip the other extruders with nearly the same min_print_speed, as they have been processed already. + auto next = adj; + for (++ next; next != by_min_print_speed.end() && (*next)->min_print_speed > (*adj)->min_print_speed - EPSILON; ++ next); + adj = next; + } + if (feedrate_next == 0.f) + // There are no other extrusions available for slow down. + break; + for (PerExtruderAdjustments *adj : by_min_print_speed) { + adj->idx_line_begin = adj->idx_line_end; + feedrate = feedrate_next; + } + } +} + +// Calculate slow down for all the extruders. +float CoolingBuffer::calculate_layer_slowdown(std::vector &per_extruder_adjustments) +{ + // Sort the extruders by an increasing slowdown_below_layer_time. + // The layers with a lower slowdown_below_layer_time are slowed down + // together with all the other layers with slowdown_below_layer_time above. + std::vector by_slowdown_time; + by_slowdown_time.reserve(per_extruder_adjustments.size()); // Only insert entries, which are adjustable (have cooling enabled and non-zero stretchable time). // Collect total print time of non-adjustable extruders. - float elapsed_time_total_non_adjustable = 0.f; - for (size_t i = 0; i < num_extruders; ++ i) { - if (config.cooling.get_at(extruders[i].id())) { - extruder_by_slowdown_time.emplace_back(i); - per_extruder_adjustments[i].sort_lines_by_decreasing_feedrate(); + float elapsed_time_total0 = 0.f; + for (PerExtruderAdjustments &adj : per_extruder_adjustments) { + // Curren total time for this extruder. + adj.time_total = adj.elapsed_time_total(); + // Maximum time for this extruder, when all extrusion moves are slowed down to min_extrusion_speed. + adj.time_maximum = adj.maximum_time_after_slowdown(true); + if (adj.cooling_slow_down_enabled) { + by_slowdown_time.emplace_back(&adj); + if (! m_cooling_logic_proportional) + // sorts the lines, also sets adj.time_non_adjustable + adj.sort_lines_by_decreasing_feedrate(); } else - elapsed_time_total_non_adjustable += per_extruder_adjustments[i].elapsed_time_total(); + elapsed_time_total0 += adj.elapsed_time_total(); } - std::sort(extruder_by_slowdown_time.begin(), extruder_by_slowdown_time.end(), - [&config, &extruders](const size_t idx1, const size_t idx2){ - return config.slowdown_below_layer_time.get_at(extruders[idx1].id()) < - config.slowdown_below_layer_time.get_at(extruders[idx2].id()); - }); + std::sort(by_slowdown_time.begin(), by_slowdown_time.end(), + [](const PerExtruderAdjustments *adj1, const PerExtruderAdjustments *adj2) + { return adj1->slowdown_below_layer_time < adj2->slowdown_below_layer_time; }); - // Elapsed time after adjustment. - float elapsed_time_total = 0.f; - { - // Elapsed time for the already adjusted extruders. - float elapsed_time_total0 = elapsed_time_total_non_adjustable; - for (size_t i_extruder_by_slowdown_time = 0; i_extruder_by_slowdown_time < extruder_by_slowdown_time.size(); ++ i_extruder_by_slowdown_time) { - // Idx in per_extruder_adjustments. - size_t idx = extruder_by_slowdown_time[i_extruder_by_slowdown_time]; - // Macro to sum or adjust all sections starting with i_extruder_by_slowdown_time. - #define FORALL_UNPROCESSED(ACCUMULATOR, ACTION) \ - ACCUMULATOR = elapsed_time_total0;\ - for (size_t j = i_extruder_by_slowdown_time; j < extruder_by_slowdown_time.size(); ++ j) \ - ACCUMULATOR += per_extruder_adjustments[extruder_by_slowdown_time[j]].ACTION - // Calculate the current adjusted elapsed_time_total over the non-finalized extruders. - float total; - FORALL_UNPROCESSED(total, elapsed_time_total()); - float slowdown_below_layer_time = float(config.slowdown_below_layer_time.get_at(per_extruder_adjustments[idx].extruder_id)) * 1.001f; - if (total > slowdown_below_layer_time) { - // The current total time is above the minimum threshold of the rest of the extruders, don't adjust anything. + for (auto cur_begin = by_slowdown_time.begin(); cur_begin != by_slowdown_time.end(); ++ cur_begin) { + PerExtruderAdjustments &adj = *(*cur_begin); + // Calculate the current adjusted elapsed_time_total over the non-finalized extruders. + float total = elapsed_time_total0; + for (auto it = cur_begin; it != by_slowdown_time.end(); ++ it) + total += (*it)->time_total; + float slowdown_below_layer_time = adj.slowdown_below_layer_time * 1.001f; + if (total > slowdown_below_layer_time) { + // The current total time is above the minimum threshold of the rest of the extruders, don't adjust anything. + } else { + // Adjust this and all the following (higher config.slowdown_below_layer_time) extruders. + // Sum maximum slow down time as if everything was slowed down including the external perimeters. + float max_time = elapsed_time_total0; + for (auto it = cur_begin; it != by_slowdown_time.end(); ++ it) + max_time += (*it)->time_maximum; + if (max_time > slowdown_below_layer_time) { + if (m_cooling_logic_proportional) + extruder_range_slow_down_proportional(cur_begin, by_slowdown_time.end(), elapsed_time_total0, total, slowdown_below_layer_time); + else + extruder_range_slow_down_non_proportional(cur_begin, by_slowdown_time.end(), slowdown_below_layer_time - total); } else { - // Adjust this and all the following (higher config.slowdown_below_layer_time) extruders. - // Sum maximum slow down time as if everything was slowed down including the external perimeters. - float max_time; - FORALL_UNPROCESSED(max_time, maximum_time(true)); - if (max_time > slowdown_below_layer_time) { - // By slowing every possible movement, the layer time could be reached. -#if 0 - // Now decide, whether the external perimeters shall be slowed down as well. - float max_time_nep; - FORALL_UNPROCESSED(max_time_nep, maximum_time(false)); - if (max_time_nep > slowdown_below_layer_time) { - // It is sufficient to slow down the non-external perimeter moves to reach the target layer time. - // Slow down the non-external perimeters proportionally. - float non_adjustable_time; - FORALL_UNPROCESSED(non_adjustable_time, non_adjustable_time(false)); - // The following step is a linear programming task due to the minimum movement speeds of the print moves. - // Run maximum 5 iterations until a good enough approximation is reached. - for (size_t iter = 0; iter < 5; ++ iter) { - float factor = (slowdown_below_layer_time - non_adjustable_time) / (total - non_adjustable_time); - assert(factor > 1.f); - FORALL_UNPROCESSED(total, slow_down_proportional(factor, false)); - if (total > 0.95f * slowdown_below_layer_time) - break; - } - } else { - // Slow down everything. First slow down the non-external perimeters to maximum. - FORALL_UNPROCESSED(total, slow_down_maximum(false)); - // Slow down the external perimeters proportionally. - float non_adjustable_time; - FORALL_UNPROCESSED(non_adjustable_time, non_adjustable_time(true)); - for (size_t iter = 0; iter < 5; ++ iter) { - float factor = (slowdown_below_layer_time - non_adjustable_time) / (total - non_adjustable_time); - assert(factor > 1.f); - FORALL_UNPROCESSED(total, slow_down_proportional(factor, true)); - if (total > 0.95f * slowdown_below_layer_time) - break; - } - } -#else - // Slow down. Try to equalize the feedrates. - std::vector by_min_print_speed; - by_min_print_speed.reserve(extruder_by_slowdown_time.size() - i_extruder_by_slowdown_time); - for (size_t j = i_extruder_by_slowdown_time; j < extruder_by_slowdown_time.size(); ++ j) - by_min_print_speed.emplace_back(&per_extruder_adjustments[extruder_by_slowdown_time[j]]); - // Find the next highest adjustable feedrate among the extruders. - float feedrate = 0; - for (PerExtruderAdjustments *adj : by_min_print_speed) - if (adj->idx_line_begin < adj->n_lines_adjustable && adj->lines[adj->idx_line_begin].feedrate > feedrate) - feedrate = adj->lines[adj->idx_line_begin].feedrate; - if (feedrate == 0) - // No adjustable line is left. - break; - // Sort by min_print_speed, maximum speed first. - std::sort(by_min_print_speed.begin(), by_min_print_speed.end(), - [](const PerExtruderAdjustments *p1, const PerExtruderAdjustments *p2){ return p1->min_print_speed > p2->min_print_speed; }); - // Slow down, fast moves first. - float time_stretch = slowdown_below_layer_time - total; - for (;;) { - // For each extruder, find the span of lines with a feedrate close to feedrate. - for (PerExtruderAdjustments *adj : by_min_print_speed) { - for (adj->idx_line_end = adj->idx_line_begin; - adj->idx_line_end < adj->n_lines_adjustable && adj->lines[adj->idx_line_end].feedrate > feedrate - EPSILON; - ++ adj->idx_line_end) ; - } - // Find the next highest adjustable feedrate among the extruders. - float feedrate_next = 0.f; - for (PerExtruderAdjustments *adj : by_min_print_speed) - if (adj->idx_line_end < adj->n_lines_adjustable && adj->lines[adj->idx_line_end].feedrate > feedrate_next) - feedrate_next = adj->lines[adj->idx_line_end].feedrate; - // Slow down, limited by max(feedrate_next, min_print_speed). - for (auto adj = by_min_print_speed.begin(); adj != by_min_print_speed.end();) { - float feedrate_limit = std::max(feedrate_next, (*adj)->min_print_speed); - if (feedrate_limit == 0.f) { - float adjustable_time = 0.f; - for (auto it = adj; it != by_min_print_speed.end(); ++ it) - adjustable_time += (*it)->adjustable_time(true); - float ratio = (adjustable_time + time_stretch) / adjustable_time; - for (auto it = adj; it != by_min_print_speed.end(); ++ it) - (*it)->slow_down_proportional(ratio, true); - // Break from two levels of loops. - feedrate_next = 0.f; - break; - } else { - float time_stretch_max = 0.f; - for (auto it = adj; it != by_min_print_speed.end(); ++ it) - time_stretch_max += (*it)->time_stretch_when_slowing_down_to(feedrate_limit); - bool done = false; - if (time_stretch_max > time_stretch) { - feedrate_limit = feedrate - (feedrate - feedrate_limit) * time_stretch / time_stretch_max; - done = true; - } - for (auto it = adj; it != by_min_print_speed.end(); ++ it) - (*it)->slow_down_to(feedrate_limit); - if (done) { - // Break from two levels of loops. - feedrate_next = 0.f; - break; - } - time_stretch -= time_stretch_max; - } - // Skip the other extruders with nearly the same min_print_speed, as they have been processed already. - auto next = adj; - for (++ next; next != by_min_print_speed.end() && (*next)->min_print_speed > (*adj)->min_print_speed - EPSILON; ++ next); - adj = next; - } - if (feedrate_next == 0.f) - // There are no other extrusions available for slow down. - break; - for (PerExtruderAdjustments *adj : by_min_print_speed) { - adj->idx_line_begin = adj->idx_line_end; - feedrate = feedrate_next; - } - } -#endif - } else { - // Slow down to maximum possible. - FORALL_UNPROCESSED(total, slow_down_maximum(true)); - } + // Slow down to maximum possible. + for (auto it = cur_begin; it != by_slowdown_time.end(); ++ it) + (*it)->slowdown_to_minimum_feedrate(true); } - #undef FORALL_UNPROCESSED - // Sum the final elapsed time for all extruders up to i_extruder_by_slowdown_time. - if (i_extruder_by_slowdown_time + 1 == extruder_by_slowdown_time.size()) - // Optimization for single extruder prints. - elapsed_time_total0 = total; - else - elapsed_time_total0 += per_extruder_adjustments[idx].elapsed_time_total(); } - elapsed_time_total = elapsed_time_total0; + elapsed_time_total0 += adj.elapsed_time_total(); } - // Transform the G-code. - // First sort the adjustment lines by their position in the source G-code. + return elapsed_time_total0; +} + +// Apply slow down over G-code lines stored in per_extruder_adjustments, enable fan if needed. +// Returns the adjusted G-code. +std::string CoolingBuffer::apply_layer_cooldown( + // Source G-code for the current layer. + const std::string &gcode, + // ID of the current layer, used to disable fan for the first n layers. + size_t layer_id, + // Total time of this layer after slow down, used to control the fan. + float layer_time, + // Per extruder list of G-code lines and their cool down attributes. + std::vector &per_extruder_adjustments) +{ + // First sort the adjustment lines by of multiple extruders by their position in the source G-code. std::vector lines; { size_t n_lines = 0; @@ -545,8 +609,9 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ int fan_speed = -1; bool bridge_fan_control = false; int bridge_fan_speed = 0; - auto change_extruder_set_fan = [ this, layer_id, elapsed_time_total, &new_gcode, &fan_speed, &bridge_fan_control, &bridge_fan_speed ]() { + auto change_extruder_set_fan = [ this, layer_id, layer_time, &new_gcode, &fan_speed, &bridge_fan_control, &bridge_fan_speed ]() { const FullPrintConfig &config = m_gcodegen.config(); +#define EXTRUDER_CONFIG(OPT) config.OPT.get_at(m_current_extruder) int min_fan_speed = EXTRUDER_CONFIG(min_fan_speed); int fan_speed_new = EXTRUDER_CONFIG(fan_always_on) ? min_fan_speed : 0; if (layer_id >= EXTRUDER_CONFIG(disable_fan_first_layers)) { @@ -554,17 +619,18 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ float slowdown_below_layer_time = float(EXTRUDER_CONFIG(slowdown_below_layer_time)); float fan_below_layer_time = float(EXTRUDER_CONFIG(fan_below_layer_time)); if (EXTRUDER_CONFIG(cooling)) { - if (elapsed_time_total < slowdown_below_layer_time) { + if (layer_time < slowdown_below_layer_time) { // Layer time very short. Enable the fan to a full throttle. fan_speed_new = max_fan_speed; - } else if (elapsed_time_total < fan_below_layer_time) { + } else if (layer_time < fan_below_layer_time) { // Layer time quite short. Enable the fan proportionally according to the current layer time. - assert(elapsed_time_total >= slowdown_below_layer_time); - double t = (elapsed_time_total - slowdown_below_layer_time) / (fan_below_layer_time - slowdown_below_layer_time); + assert(layer_time >= slowdown_below_layer_time); + double t = (layer_time - slowdown_below_layer_time) / (fan_below_layer_time - slowdown_below_layer_time); fan_speed_new = int(floor(t * min_fan_speed + (1. - t) * max_fan_speed) + 0.5); } } bridge_fan_speed = EXTRUDER_CONFIG(bridge_fan_speed); +#undef EXTRUDER_CONFIG bridge_fan_control = bridge_fan_speed > fan_speed_new; } else { bridge_fan_control = false; @@ -576,10 +642,11 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ new_gcode += m_gcodegen.writer().set_fan(fan_speed); } }; - change_extruder_set_fan(); - const char *pos = gcode.c_str(); - int current_feedrate = 0; + const char *pos = gcode.c_str(); + int current_feedrate = 0; + const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); + change_extruder_set_fan(); for (const CoolingLine *line : lines) { const char *line_start = gcode.c_str() + line->line_start; const char *line_end = gcode.c_str() + line->line_end; @@ -602,9 +669,9 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ // Just remove this comment. } else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) { // Find the start of a comment, or roll to the end of line. - const char *end = line_start; - for (; end < line_end && *end != ';'; ++ end); - // Find the 'F' word. + const char *end = line_start; + for (; end < line_end && *end != ';'; ++ end); + // Find the 'F' word. const char *fpos = strstr(line_start + 2, " F") + 2; int new_feedrate = current_feedrate; bool modify = false; @@ -643,7 +710,7 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ new_gcode.append(line_start, f - line_start + 1); } // Skip the non-whitespaces of the F parameter up the comment or end of line. - for (; fpos != end && *fpos != ' ' && *fpos != ';' && *fpos != '\n'; ++fpos); + for (; fpos != end && *fpos != ' ' && *fpos != ';' && *fpos != '\n'; ++fpos); // Append the rest of the line without the comment. if (fpos < end) new_gcode.append(fpos, end - fpos); @@ -653,21 +720,21 @@ std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_ // Process the rest of the line. if (end < line_end) { if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE)) { - // Process comments, remove ";_EXTRUDE_SET_SPEED", ";_EXTERNAL_PERIMETER", ";_WIPE" - std::string comment(end, line_end); - boost::replace_all(comment, ";_EXTRUDE_SET_SPEED", ""); + // Process comments, remove ";_EXTRUDE_SET_SPEED", ";_EXTERNAL_PERIMETER", ";_WIPE" + std::string comment(end, line_end); + boost::replace_all(comment, ";_EXTRUDE_SET_SPEED", ""); if (line->type & CoolingLine::TYPE_EXTERNAL_PERIMETER) boost::replace_all(comment, ";_EXTERNAL_PERIMETER", ""); if (line->type & CoolingLine::TYPE_WIPE) boost::replace_all(comment, ";_WIPE", ""); - new_gcode += comment; - } else { - // Just attach the rest of the source line. - new_gcode.append(end, line_end - end); - } + new_gcode += comment; + } else { + // Just attach the rest of the source line. + new_gcode.append(end, line_end - end); + } } } else { - new_gcode.append(line_start, line_end - line_start); + new_gcode.append(line_start, line_end - line_start); } pos = line_end; } diff --git a/xs/src/libslic3r/GCode/CoolingBuffer.hpp b/xs/src/libslic3r/GCode/CoolingBuffer.hpp index f85c470b3..bf4b082e2 100644 --- a/xs/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/xs/src/libslic3r/GCode/CoolingBuffer.hpp @@ -9,13 +9,17 @@ namespace Slic3r { class GCode; class Layer; +class PerExtruderAdjustments; -/* -A standalone G-code filter, to control cooling of the print. -The G-code is processed per layer. Once a layer is collected, fan start / stop commands are edited -and the print is modified to stretch over a minimum layer time. -*/ - +// A standalone G-code filter, to control cooling of the print. +// The G-code is processed per layer. Once a layer is collected, fan start / stop commands are edited +// and the print is modified to stretch over a minimum layer time. +// +// The simple it sounds, the actual implementation is significantly more complex. +// Namely, for a multi-extruder print, each material may require a different cooling logic. +// For example, some materials may not like to print too slowly, while with some materials +// we may slow down significantly. +// class CoolingBuffer { public: CoolingBuffer(GCode &gcodegen); @@ -25,7 +29,12 @@ public: GCode* gcodegen() { return &m_gcodegen; } private: - CoolingBuffer& operator=(const CoolingBuffer&); + CoolingBuffer& operator=(const CoolingBuffer&) = delete; + std::vector parse_layer_gcode(const std::string &gcode, std::vector ¤t_pos) const; + float calculate_layer_slowdown(std::vector &per_extruder_adjustments); + // Apply slow down over G-code lines stored in per_extruder_adjustments, enable fan if needed. + // Returns the adjusted G-code. + std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); GCode& m_gcodegen; std::string m_gcode; @@ -34,6 +43,9 @@ private: std::vector m_axis; std::vector m_current_pos; unsigned int m_current_extruder; + + // Old logic: proportional. + bool m_cooling_logic_proportional = false; }; } From 25d47c1da8bac02d5b1408d9a39ba7356b14fc56 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Thu, 26 Apr 2018 18:56:16 +0200 Subject: [PATCH 38/38] Fix of the new cooling logic. --- xs/src/libslic3r/GCode/CoolingBuffer.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/xs/src/libslic3r/GCode/CoolingBuffer.cpp b/xs/src/libslic3r/GCode/CoolingBuffer.cpp index 141d197ca..b786f9bce 100644 --- a/xs/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/xs/src/libslic3r/GCode/CoolingBuffer.cpp @@ -50,7 +50,7 @@ struct CoolingLine CoolingLine(unsigned int type, size_t line_start, size_t line_end) : type(type), line_start(line_start), line_end(line_end), - length(0.f), time(0.f), time_max(0.f), slowdown(false) {} + length(0.f), feedrate(0.f), time(0.f), time_max(0.f), slowdown(false) {} bool adjustable(bool slowdown_external_perimeters) const { return (this->type & TYPE_ADJUSTABLE) && @@ -158,9 +158,9 @@ struct PerExtruderAdjustments bool adj2 = l2.adjustable(); return (adj1 == adj2) ? l1.feedrate > l2.feedrate : adj1; }); - for (n_lines_adjustable = 0; n_lines_adjustable < lines.size(); ++ n_lines_adjustable) - if ((this->lines[n_lines_adjustable].type & CoolingLine::TYPE_ADJUSTABLE) == 0) - break; + for (n_lines_adjustable = 0; + n_lines_adjustable < lines.size() && this->lines[n_lines_adjustable].adjustable(); + ++ n_lines_adjustable); time_non_adjustable = 0.f; for (size_t i = n_lines_adjustable; i < lines.size(); ++ i) time_non_adjustable += lines[i].time; @@ -329,10 +329,10 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: // Movement in the extruder axis. line.length = std::abs(dif[3]); } - if (line.length > 0) { - line.feedrate = new_pos[4]; // current F - line.time = line.length / line.feedrate; - } + line.feedrate = new_pos[4]; + assert((line.type & CoolingLine::TYPE_ADJUSTABLE) == 0 || line.feedrate > 0.f); + if (line.length > 0) + line.time = line.length / line.feedrate; line.time_max = line.time; if ((line.type & CoolingLine::TYPE_ADJUSTABLE) || active_speed_modifier != size_t(-1)) line.time_max = (adjustment->min_print_speed == 0.f) ? FLT_MAX : std::max(line.time, line.length / adjustment->min_print_speed); @@ -340,6 +340,7 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: // Inside the ";_EXTRUDE_SET_SPEED" blocks, there must not be a G1 Fxx entry. assert((line.type & CoolingLine::TYPE_HAS_F) == 0); CoolingLine &sm = adjustment->lines[active_speed_modifier]; + assert(sm.feedrate > 0.f); sm.length += line.length; sm.time += line.time; if (sm.time_max != FLT_MAX) {