Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/prusa3d/PrusaSlicer.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/slic3r')
-rw-r--r--src/slic3r/AppController.cpp167
-rw-r--r--src/slic3r/AppController.hpp263
-rw-r--r--src/slic3r/AppControllerWx.cpp302
-rw-r--r--src/slic3r/CMakeLists.txt106
-rw-r--r--src/slic3r/Config/Snapshot.cpp532
-rw-r--r--src/slic3r/Config/Snapshot.hpp129
-rw-r--r--src/slic3r/Config/Version.cpp319
-rw-r--r--src/slic3r/Config/Version.hpp88
-rw-r--r--src/slic3r/GUI/2DBed.cpp183
-rw-r--r--src/slic3r/GUI/2DBed.hpp52
-rw-r--r--src/slic3r/GUI/3DScene.cpp2205
-rw-r--r--src/slic3r/GUI/3DScene.hpp603
-rw-r--r--src/slic3r/GUI/AboutDialog.cpp136
-rw-r--r--src/slic3r/GUI/AboutDialog.hpp36
-rw-r--r--src/slic3r/GUI/AppConfig.cpp266
-rw-r--r--src/slic3r/GUI/AppConfig.hpp140
-rw-r--r--src/slic3r/GUI/BackgroundSlicingProcess.cpp167
-rw-r--r--src/slic3r/GUI/BackgroundSlicingProcess.hpp91
-rw-r--r--src/slic3r/GUI/BedShapeDialog.cpp343
-rw-r--r--src/slic3r/GUI/BedShapeDialog.hpp56
-rw-r--r--src/slic3r/GUI/BitmapCache.cpp172
-rw-r--r--src/slic3r/GUI/BitmapCache.hpp44
-rw-r--r--src/slic3r/GUI/BonjourDialog.cpp200
-rw-r--r--src/slic3r/GUI/BonjourDialog.hpp49
-rw-r--r--src/slic3r/GUI/ButtonsDescription.cpp84
-rw-r--r--src/slic3r/GUI/ButtonsDescription.hpp27
-rw-r--r--src/slic3r/GUI/ConfigExceptions.hpp15
-rw-r--r--src/slic3r/GUI/ConfigSnapshotDialog.cpp140
-rw-r--r--src/slic3r/GUI/ConfigSnapshotDialog.hpp34
-rw-r--r--src/slic3r/GUI/ConfigWizard.cpp915
-rw-r--r--src/slic3r/GUI/ConfigWizard.hpp50
-rw-r--r--src/slic3r/GUI/ConfigWizard_private.hpp241
-rw-r--r--src/slic3r/GUI/Field.cpp784
-rw-r--r--src/slic3r/GUI/Field.hpp466
-rw-r--r--src/slic3r/GUI/FirmwareDialog.cpp846
-rw-r--r--src/slic3r/GUI/FirmwareDialog.hpp31
-rw-r--r--src/slic3r/GUI/GLCanvas3D.cpp5522
-rw-r--r--src/slic3r/GUI/GLCanvas3D.hpp765
-rw-r--r--src/slic3r/GUI/GLCanvas3DManager.cpp819
-rw-r--r--src/slic3r/GUI/GLCanvas3DManager.hpp192
-rw-r--r--src/slic3r/GUI/GLGizmo.cpp1503
-rw-r--r--src/slic3r/GUI/GLGizmo.hpp366
-rw-r--r--src/slic3r/GUI/GLShader.cpp256
-rw-r--r--src/slic3r/GUI/GLShader.hpp41
-rw-r--r--src/slic3r/GUI/GLTexture.cpp199
-rw-r--r--src/slic3r/GUI/GLTexture.hpp60
-rw-r--r--src/slic3r/GUI/GLToolbar.cpp722
-rw-r--r--src/slic3r/GUI/GLToolbar.hpp175
-rw-r--r--src/slic3r/GUI/GUI.cpp1402
-rw-r--r--src/slic3r/GUI/GUI.hpp260
-rw-r--r--src/slic3r/GUI/GUI_ObjectParts.cpp2041
-rw-r--r--src/slic3r/GUI/GUI_ObjectParts.hpp147
-rw-r--r--src/slic3r/GUI/LambdaObjectDialog.cpp199
-rw-r--r--src/slic3r/GUI/LambdaObjectDialog.hpp40
-rw-r--r--src/slic3r/GUI/MsgDialog.cpp88
-rw-r--r--src/slic3r/GUI/MsgDialog.hpp67
-rw-r--r--src/slic3r/GUI/OptionsGroup.cpp545
-rw-r--r--src/slic3r/GUI/OptionsGroup.hpp271
-rw-r--r--src/slic3r/GUI/Preferences.cpp134
-rw-r--r--src/slic3r/GUI/Preferences.hpp31
-rw-r--r--src/slic3r/GUI/Preset.cpp1019
-rw-r--r--src/slic3r/GUI/Preset.hpp444
-rw-r--r--src/slic3r/GUI/PresetBundle.cpp1401
-rw-r--r--src/slic3r/GUI/PresetBundle.hpp168
-rw-r--r--src/slic3r/GUI/PresetHints.cpp278
-rw-r--r--src/slic3r/GUI/PresetHints.hpp30
-rw-r--r--src/slic3r/GUI/ProgressIndicator.hpp70
-rw-r--r--src/slic3r/GUI/ProgressStatusBar.cpp152
-rw-r--r--src/slic3r/GUI/ProgressStatusBar.hpp68
-rw-r--r--src/slic3r/GUI/RammingChart.cpp279
-rw-r--r--src/slic3r/GUI/RammingChart.hpp115
-rw-r--r--src/slic3r/GUI/Tab.cpp3033
-rw-r--r--src/slic3r/GUI/Tab.hpp385
-rw-r--r--src/slic3r/GUI/TabIface.cpp20
-rw-r--r--src/slic3r/GUI/TabIface.hpp41
-rw-r--r--src/slic3r/GUI/UpdateDialogs.cpp196
-rw-r--r--src/slic3r/GUI/UpdateDialogs.hpp81
-rw-r--r--src/slic3r/GUI/Widget.hpp16
-rw-r--r--src/slic3r/GUI/WipeTowerDialog.cpp338
-rw-r--r--src/slic3r/GUI/WipeTowerDialog.hpp90
-rw-r--r--src/slic3r/GUI/callback.hpp30
-rw-r--r--src/slic3r/GUI/wxExtensions.cpp1662
-rw-r--r--src/slic3r/GUI/wxExtensions.hpp773
-rw-r--r--src/slic3r/GUI/wxinit.h25
-rw-r--r--src/slic3r/Utils/ASCIIFolding.cpp1954
-rw-r--r--src/slic3r/Utils/ASCIIFolding.hpp15
-rw-r--r--src/slic3r/Utils/Bonjour.cpp781
-rw-r--r--src/slic3r/Utils/Bonjour.hpp64
-rw-r--r--src/slic3r/Utils/Duet.cpp279
-rw-r--r--src/slic3r/Utils/Duet.hpp47
-rw-r--r--src/slic3r/Utils/FixModelByWin10.cpp402
-rw-r--r--src/slic3r/Utils/FixModelByWin10.hpp26
-rw-r--r--src/slic3r/Utils/HexFile.cpp106
-rw-r--r--src/slic3r/Utils/HexFile.hpp33
-rw-r--r--src/slic3r/Utils/Http.cpp451
-rw-r--r--src/slic3r/Utils/Http.hpp115
-rw-r--r--src/slic3r/Utils/OctoPrint.cpp173
-rw-r--r--src/slic3r/Utils/OctoPrint.hpp42
-rw-r--r--src/slic3r/Utils/PresetUpdater.cpp633
-rw-r--r--src/slic3r/Utils/PresetUpdater.hpp42
-rw-r--r--src/slic3r/Utils/PrintHost.cpp23
-rw-r--r--src/slic3r/Utils/PrintHost.hpp35
-rw-r--r--src/slic3r/Utils/PrintHostSendDialog.cpp52
-rw-r--r--src/slic3r/Utils/PrintHostSendDialog.hpp38
-rw-r--r--src/slic3r/Utils/Semver.hpp151
-rw-r--r--src/slic3r/Utils/Serial.cpp495
-rw-r--r--src/slic3r/Utils/Serial.hpp82
-rw-r--r--src/slic3r/Utils/Time.cpp80
-rw-r--r--src/slic3r/Utils/Time.hpp25
109 files changed, 42985 insertions, 0 deletions
diff --git a/src/slic3r/AppController.cpp b/src/slic3r/AppController.cpp
new file mode 100644
index 000000000..4a36b5d7f
--- /dev/null
+++ b/src/slic3r/AppController.cpp
@@ -0,0 +1,167 @@
+#include "AppController.hpp"
+
+#include <future>
+#include <chrono>
+#include <sstream>
+#include <cstdarg>
+#include <thread>
+#include <unordered_map>
+
+#include <slic3r/GUI/GUI.hpp>
+#include <ModelArrange.hpp>
+#include <slic3r/GUI/PresetBundle.hpp>
+
+#include <Geometry.hpp>
+#include <PrintConfig.hpp>
+#include <Print.hpp>
+#include <Model.hpp>
+#include <Utils.hpp>
+
+namespace Slic3r {
+
+class AppControllerBoilerplate::PriData {
+public:
+ std::mutex m;
+ std::thread::id ui_thread;
+
+ inline explicit PriData(std::thread::id uit): ui_thread(uit) {}
+};
+
+AppControllerBoilerplate::AppControllerBoilerplate()
+ :pri_data_(new PriData(std::this_thread::get_id())) {}
+
+AppControllerBoilerplate::~AppControllerBoilerplate() {
+ pri_data_.reset();
+}
+
+bool AppControllerBoilerplate::is_main_thread() const
+{
+ return pri_data_->ui_thread == std::this_thread::get_id();
+}
+
+namespace GUI {
+PresetBundle* get_preset_bundle();
+}
+
+AppControllerBoilerplate::ProgresIndicatorPtr
+AppControllerBoilerplate::global_progress_indicator() {
+ ProgresIndicatorPtr ret;
+
+ pri_data_->m.lock();
+ ret = global_progressind_;
+ pri_data_->m.unlock();
+
+ return ret;
+}
+
+void AppControllerBoilerplate::global_progress_indicator(
+ AppControllerBoilerplate::ProgresIndicatorPtr gpri)
+{
+ pri_data_->m.lock();
+ global_progressind_ = gpri;
+ pri_data_->m.unlock();
+}
+
+void ProgressIndicator::message_fmt(
+ const std::string &fmtstr, ...) {
+ std::stringstream ss;
+ va_list args;
+ va_start(args, fmtstr);
+
+ auto fmt = fmtstr.begin();
+
+ while (*fmt != '\0') {
+ if (*fmt == 'd') {
+ int i = va_arg(args, int);
+ ss << i << '\n';
+ } else if (*fmt == 'c') {
+ // note automatic conversion to integral type
+ int c = va_arg(args, int);
+ ss << static_cast<char>(c) << '\n';
+ } else if (*fmt == 'f') {
+ double d = va_arg(args, double);
+ ss << d << '\n';
+ }
+ ++fmt;
+ }
+
+ va_end(args);
+ message(ss.str());
+}
+
+void AppController::arrange_model()
+{
+ using Coord = libnest2d::TCoord<libnest2d::PointImpl>;
+
+ if(arranging_.load()) return;
+
+ // to prevent UI reentrancies
+ arranging_.store(true);
+
+ unsigned count = 0;
+ for(auto obj : model_->objects) count += obj->instances.size();
+
+ auto pind = global_progress_indicator();
+
+ float pmax = 1.0;
+
+ if(pind) {
+ pmax = pind->max();
+
+ // Set the range of the progress to the object count
+ pind->max(count);
+
+ pind->on_cancel([this](){
+ arranging_.store(false);
+ });
+ }
+
+ auto dist = print_ctl()->config().min_object_distance();
+
+ // Create the arranger config
+ auto min_obj_distance = static_cast<Coord>(dist/SCALING_FACTOR);
+
+ auto& bedpoints = print_ctl()->config().bed_shape.values;
+ Polyline bed; bed.points.reserve(bedpoints.size());
+ for(auto& v : bedpoints)
+ bed.append(Point::new_scale(v(0), v(1)));
+
+ if(pind) pind->update(0, L("Arranging objects..."));
+
+ try {
+ arr::BedShapeHint hint;
+ // TODO: from Sasha from GUI
+ hint.type = arr::BedShapeType::WHO_KNOWS;
+
+ arr::arrange(*model_,
+ min_obj_distance,
+ bed,
+ hint,
+ false, // create many piles not just one pile
+ [this, pind, count](unsigned rem) {
+ if(pind)
+ pind->update(count - rem, L("Arranging objects..."));
+
+ process_events();
+ }, [this] () { return !arranging_.load(); });
+ } catch(std::exception& e) {
+ std::cerr << e.what() << std::endl;
+ report_issue(IssueType::ERR,
+ L("Could not arrange model objects! "
+ "Some geometries may be invalid."),
+ L("Exception occurred"));
+ }
+
+ // Restore previous max value
+ if(pind) {
+ pind->max(pmax);
+ pind->update(0, arranging_.load() ? L("Arranging done.") :
+ L("Arranging canceled."));
+
+ pind->on_cancel(/*remove cancel function*/);
+ }
+
+ arranging_.store(false);
+}
+
+}
diff --git a/src/slic3r/AppController.hpp b/src/slic3r/AppController.hpp
new file mode 100644
index 000000000..71472835e
--- /dev/null
+++ b/src/slic3r/AppController.hpp
@@ -0,0 +1,263 @@
+#ifndef APPCONTROLLER_HPP
+#define APPCONTROLLER_HPP
+
+#include <string>
+#include <vector>
+#include <memory>
+#include <atomic>
+#include <iostream>
+
+#include "GUI/ProgressIndicator.hpp"
+
+#include <PrintConfig.hpp>
+
+namespace Slic3r {
+
+class Model;
+class Print;
+class PrintObject;
+class PrintConfig;
+class ProgressStatusBar;
+class DynamicPrintConfig;
+
+/**
+ * @brief A boilerplate class for creating application logic. It should provide
+ * features as issue reporting and progress indication, etc...
+ *
+ * The lower lever UI independent classes can be manipulated with a subclass
+ * of this controller class. We can also catch any exceptions that lower level
+ * methods could throw and display appropriate errors and warnings.
+ *
+ * Note that the outer and the inner interface of this class is free from any
+ * UI toolkit dependencies. We can implement it with any UI framework or make it
+ * a cli client.
+ */
+class AppControllerBoilerplate {
+public:
+
+ /// A Progress indicator object smart pointer
+ using ProgresIndicatorPtr = std::shared_ptr<ProgressIndicator>;
+
+private:
+ class PriData; // Some structure to store progress indication data
+
+ // Pimpl data for thread safe progress indication features
+ std::unique_ptr<PriData> pri_data_;
+
+public:
+
+ AppControllerBoilerplate();
+ ~AppControllerBoilerplate();
+
+ using Path = std::string;
+ using PathList = std::vector<Path>;
+
+ /// Common runtime issue types
+ enum class IssueType {
+ INFO,
+ WARN,
+ WARN_Q, // Warning with a question to continue
+ ERR,
+ FATAL
+ };
+
+ /**
+ * @brief Query some paths from the user.
+ *
+ * It should display a file chooser dialog in case of a UI application.
+ * @param title Title of a possible query dialog.
+ * @param extensions Recognized file extensions.
+ * @return Returns a list of paths choosed by the user.
+ */
+ PathList query_destination_paths(
+ const std::string& title,
+ const std::string& extensions) const;
+
+ /**
+ * @brief Same as query_destination_paths but works for directories only.
+ */
+ PathList query_destination_dirs(
+ const std::string& title) const;
+
+ /**
+ * @brief Same as query_destination_paths but returns only one path.
+ */
+ Path query_destination_path(
+ const std::string& title,
+ const std::string& extensions,
+ const std::string& hint = "") const;
+
+ /**
+ * @brief Report an issue to the user be it fatal or recoverable.
+ *
+ * In a UI app this should display some message dialog.
+ *
+ * @param issuetype The type of the runtime issue.
+ * @param description A somewhat longer description of the issue.
+ * @param brief A very brief description. Can be used for message dialog
+ * title.
+ */
+ bool report_issue(IssueType issuetype,
+ const std::string& description,
+ const std::string& brief);
+
+ bool report_issue(IssueType issuetype,
+ const std::string& description);
+
+ /**
+ * @brief Return the global progress indicator for the current controller.
+ * Can be empty as well.
+ *
+ * Only one thread should use the global indicator at a time.
+ */
+ ProgresIndicatorPtr global_progress_indicator();
+
+ void global_progress_indicator(ProgresIndicatorPtr gpri);
+
+ /**
+ * @brief A predicate telling the caller whether it is the thread that
+ * created the AppConroller object itself. This probably means that the
+ * execution is in the UI thread. Otherwise it returns false meaning that
+ * some worker thread called this function.
+ * @return Return true for the same caller thread that created this
+ * object and false for every other.
+ */
+ bool is_main_thread() const;
+
+ /**
+ * @brief The frontend supports asynch execution.
+ *
+ * A Graphic UI will support this, a CLI may not. This can be used in
+ * subclass methods to decide whether to start threads for block free UI.
+ *
+ * Note that even a progress indicator's update called regularly can solve
+ * the blocking UI problem in some cases even when an event loop is present.
+ * This is how wxWidgets gauge work but creating a separate thread will make
+ * the UI even more fluent.
+ *
+ * @return true if a job or method can be executed asynchronously, false
+ * otherwise.
+ */
+ bool supports_asynch() const;
+
+ void process_events();
+
+protected:
+
+ /**
+ * @brief Create a new progress indicator and return a smart pointer to it.
+ * @param statenum The number of states for the given procedure.
+ * @param title The title of the procedure.
+ * @param firstmsg The message for the first subtask to be displayed.
+ * @return Smart pointer to the created object.
+ */
+ ProgresIndicatorPtr create_progress_indicator(
+ unsigned statenum,
+ const std::string& title,
+ const std::string& firstmsg) const;
+
+ ProgresIndicatorPtr create_progress_indicator(
+ unsigned statenum,
+ const std::string& title) const;
+
+ // This is a global progress indicator placeholder. In the Slic3r UI it can
+ // contain the progress indicator on the statusbar.
+ ProgresIndicatorPtr global_progressind_;
+};
+
+#if 0
+/**
+ * @brief Implementation of the printing logic.
+ */
+class PrintController: public AppControllerBoilerplate {
+ Print *print_ = nullptr;
+public:
+
+ // Must be public for perl to use it
+ explicit inline PrintController(Print *print): print_(print) {}
+
+ PrintController(const PrintController&) = delete;
+ PrintController(PrintController&&) = delete;
+
+ using Ptr = std::unique_ptr<PrintController>;
+
+ inline static Ptr create(Print *print) {
+ return PrintController::Ptr( new PrintController(print) );
+ }
+
+ void slice() {}
+ void slice_to_png() {}
+
+ const PrintConfig& config() const;
+};
+#else
+class PrintController: public AppControllerBoilerplate {
+public:
+ using Ptr = std::unique_ptr<PrintController>;
+ explicit inline PrintController(Print *print){}
+ inline static Ptr create(Print *print) {
+ return PrintController::Ptr( new PrintController(print) );
+ }
+ void slice() {}
+ void slice_to_png() {}
+ const PrintConfig& config() const { static PrintConfig cfg; return cfg; }
+};
+#endif
+
+/**
+ * @brief Top level controller.
+ */
+class AppController: public AppControllerBoilerplate {
+ Model *model_ = nullptr;
+ PrintController::Ptr printctl;
+ std::atomic<bool> arranging_;
+public:
+
+ /**
+ * @brief Get the print controller object.
+ *
+ * @return Return a raw pointer instead of a smart one for perl to be able
+ * to use this function and access the print controller.
+ */
+ PrintController * print_ctl() { return printctl.get(); }
+
+ /**
+ * @brief Set a model object.
+ *
+ * @param model A raw pointer to the model object. This can be used from
+ * perl.
+ */
+ void set_model(Model *model) { model_ = model; }
+
+ /**
+ * @brief Set the print object from perl.
+ *
+ * This will create a print controller that will then be accessible from
+ * perl.
+ * @param print A print object which can be a perl-ish extension as well.
+ */
+ void set_print(Print *print) {
+ printctl = PrintController::create(print);
+ }
+
+ /**
+ * @brief Set up a global progress indicator.
+ *
+ * In perl we have a progress indicating status bar on the bottom of the
+ * window which is defined and created in perl. We can pass the ID-s of the
+ * gauge and the statusbar id and make a wrapper implementation of the
+ * ProgressIndicator interface so we can use this GUI widget from C++.
+ *
+ * This function should be called from perl.
+ *
+ * @param gauge_id The ID of the gague widget of the status bar.
+ * @param statusbar_id The ID of the status bar.
+ */
+ void set_global_progress_indicator(ProgressStatusBar *prs);
+
+ void arrange_model();
+};
+
+}
+
+#endif // APPCONTROLLER_HPP
diff --git a/src/slic3r/AppControllerWx.cpp b/src/slic3r/AppControllerWx.cpp
new file mode 100644
index 000000000..4d67d5f66
--- /dev/null
+++ b/src/slic3r/AppControllerWx.cpp
@@ -0,0 +1,302 @@
+#include "AppController.hpp"
+
+#include <thread>
+#include <future>
+
+#include <slic3r/GUI/GUI.hpp>
+#include <slic3r/GUI/ProgressStatusBar.hpp>
+
+#include <wx/app.h>
+#include <wx/filedlg.h>
+#include <wx/msgdlg.h>
+#include <wx/progdlg.h>
+#include <wx/gauge.h>
+#include <wx/statusbr.h>
+#include <wx/event.h>
+
+// This source file implements the UI dependent methods of the AppControllers.
+// It will be clear what is needed to be reimplemented in case of a UI framework
+// change or a CLI client creation. In this particular case we use wxWidgets to
+// implement everything.
+
+namespace Slic3r {
+
+bool AppControllerBoilerplate::supports_asynch() const
+{
+ return true;
+}
+
+void AppControllerBoilerplate::process_events()
+{
+ wxYieldIfNeeded();
+}
+
+AppControllerBoilerplate::PathList
+AppControllerBoilerplate::query_destination_paths(
+ const std::string &title,
+ const std::string &extensions) const
+{
+
+ wxFileDialog dlg(wxTheApp->GetTopWindow(), _(title) );
+ dlg.SetWildcard(extensions);
+
+ dlg.ShowModal();
+
+ wxArrayString paths;
+ dlg.GetPaths(paths);
+
+ PathList ret(paths.size(), "");
+ for(auto& p : paths) ret.push_back(p.ToStdString());
+
+ return ret;
+}
+
+AppControllerBoilerplate::Path
+AppControllerBoilerplate::query_destination_path(
+ const std::string &title,
+ const std::string &extensions,
+ const std::string& hint) const
+{
+ wxFileDialog dlg(wxTheApp->GetTopWindow(), _(title) );
+ dlg.SetWildcard(extensions);
+
+ dlg.SetFilename(hint);
+
+ Path ret;
+
+ if(dlg.ShowModal() == wxID_OK) {
+ ret = Path(dlg.GetPath());
+ }
+
+ return ret;
+}
+
+bool AppControllerBoilerplate::report_issue(IssueType issuetype,
+ const std::string &description,
+ const std::string &brief)
+{
+ auto icon = wxICON_INFORMATION;
+ auto style = wxOK|wxCENTRE;
+ switch(issuetype) {
+ case IssueType::INFO: break;
+ case IssueType::WARN: icon = wxICON_WARNING; break;
+ case IssueType::WARN_Q: icon = wxICON_WARNING; style |= wxCANCEL; break;
+ case IssueType::ERR:
+ case IssueType::FATAL: icon = wxICON_ERROR;
+ }
+
+ auto ret = wxMessageBox(_(description), _(brief), icon | style);
+ return ret != wxCANCEL;
+}
+
+bool AppControllerBoilerplate::report_issue(
+ AppControllerBoilerplate::IssueType issuetype,
+ const std::string &description)
+{
+ return report_issue(issuetype, description, std::string());
+}
+
+wxDEFINE_EVENT(PROGRESS_STATUS_UPDATE_EVENT, wxCommandEvent);
+
+namespace {
+
+/*
+ * A simple thread safe progress dialog implementation that can be used from
+ * the main thread as well.
+ */
+class GuiProgressIndicator:
+ public ProgressIndicator, public wxEvtHandler {
+
+ wxProgressDialog gauge_;
+ using Base = ProgressIndicator;
+ wxString message_;
+ int range_; wxString title_;
+ bool is_asynch_ = false;
+
+ const int id_ = wxWindow::NewControlId();
+
+ // status update handler
+ void _state( wxCommandEvent& evt) {
+ unsigned st = evt.GetInt();
+ message_ = evt.GetString();
+ _state(st);
+ }
+
+ // Status update implementation
+ void _state( unsigned st) {
+ if(!gauge_.IsShown()) gauge_.ShowModal();
+ Base::state(st);
+ if(!gauge_.Update(static_cast<int>(st), message_)) {
+ cancel();
+ }
+ }
+
+public:
+
+ /// Setting whether it will be used from the UI thread or some worker thread
+ inline void asynch(bool is) { is_asynch_ = is; }
+
+ /// Get the mode of parallel operation.
+ inline bool asynch() const { return is_asynch_; }
+
+ inline GuiProgressIndicator(int range, const wxString& title,
+ const wxString& firstmsg) :
+ gauge_(title, firstmsg, range, wxTheApp->GetTopWindow(),
+ wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT),
+
+ message_(firstmsg),
+ range_(range), title_(title)
+ {
+ Base::max(static_cast<float>(range));
+ Base::states(static_cast<unsigned>(range));
+
+ Bind(PROGRESS_STATUS_UPDATE_EVENT,
+ &GuiProgressIndicator::_state,
+ this, id_);
+ }
+
+ virtual void state(float val) override {
+ state(static_cast<unsigned>(val));
+ }
+
+ void state(unsigned st) {
+ // send status update event
+ if(is_asynch_) {
+ auto evt = new wxCommandEvent(PROGRESS_STATUS_UPDATE_EVENT, id_);
+ evt->SetInt(st);
+ evt->SetString(message_);
+ wxQueueEvent(this, evt);
+ } else _state(st);
+ }
+
+ virtual void message(const std::string & msg) override {
+ message_ = _(msg);
+ }
+
+ virtual void messageFmt(const std::string& fmt, ...) {
+ va_list arglist;
+ va_start(arglist, fmt);
+ message_ = wxString::Format(_(fmt), arglist);
+ va_end(arglist);
+ }
+
+ virtual void title(const std::string & title) override {
+ title_ = _(title);
+ }
+};
+}
+
+AppControllerBoilerplate::ProgresIndicatorPtr
+AppControllerBoilerplate::create_progress_indicator(
+ unsigned statenum,
+ const std::string& title,
+ const std::string& firstmsg) const
+{
+ auto pri =
+ std::make_shared<GuiProgressIndicator>(statenum, title, firstmsg);
+
+ // We set up the mode of operation depending of the creator thread's
+ // identity
+ pri->asynch(!is_main_thread());
+
+ return pri;
+}
+
+AppControllerBoilerplate::ProgresIndicatorPtr
+AppControllerBoilerplate::create_progress_indicator(
+ unsigned statenum, const std::string &title) const
+{
+ return create_progress_indicator(statenum, title, std::string());
+}
+
+namespace {
+
+class Wrapper: public ProgressIndicator, public wxEvtHandler {
+ ProgressStatusBar *sbar_;
+ using Base = ProgressIndicator;
+ wxString message_;
+ AppControllerBoilerplate& ctl_;
+
+ void showProgress(bool show = true) {
+ sbar_->show_progress(show);
+ }
+
+ void _state(unsigned st) {
+ if( st <= ProgressIndicator::max() ) {
+ Base::state(st);
+ sbar_->set_status_text(message_);
+ sbar_->set_progress(st);
+ }
+ }
+
+ // status update handler
+ void _state( wxCommandEvent& evt) {
+ unsigned st = evt.GetInt(); _state(st);
+ }
+
+ const int id_ = wxWindow::NewControlId();
+
+public:
+
+ inline Wrapper(ProgressStatusBar *sbar,
+ AppControllerBoilerplate& ctl):
+ sbar_(sbar), ctl_(ctl)
+ {
+ Base::max(static_cast<float>(sbar_->get_range()));
+ Base::states(static_cast<unsigned>(sbar_->get_range()));
+
+ Bind(PROGRESS_STATUS_UPDATE_EVENT,
+ &Wrapper::_state,
+ this, id_);
+ }
+
+ virtual void state(float val) override {
+ state(unsigned(val));
+ }
+
+ virtual void max(float val) override {
+ if(val > 1.0) {
+ sbar_->set_range(static_cast<int>(val));
+ ProgressIndicator::max(val);
+ }
+ }
+
+ void state(unsigned st) {
+ if(!ctl_.is_main_thread()) {
+ auto evt = new wxCommandEvent(PROGRESS_STATUS_UPDATE_EVENT, id_);
+ evt->SetInt(st);
+ wxQueueEvent(this, evt);
+ } else {
+ _state(st);
+ }
+ }
+
+ virtual void message(const std::string & msg) override {
+ message_ = _(msg);
+ }
+
+ virtual void message_fmt(const std::string& fmt, ...) override {
+ va_list arglist;
+ va_start(arglist, fmt);
+ message_ = wxString::Format(_(fmt), arglist);
+ va_end(arglist);
+ }
+
+ virtual void title(const std::string & /*title*/) override {}
+
+ virtual void on_cancel(CancelFn fn) override {
+ sbar_->set_cancel_callback(fn);
+ Base::on_cancel(fn);
+ }
+
+};
+}
+
+void AppController::set_global_progress_indicator(ProgressStatusBar *prsb)
+{
+ if(prsb) {
+ global_progress_indicator(std::make_shared<Wrapper>(prsb, *this));
+ }
+}
+
+}
diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
new file mode 100644
index 000000000..e4dfd35ea
--- /dev/null
+++ b/src/slic3r/CMakeLists.txt
@@ -0,0 +1,106 @@
+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/BackgroundSlicingProcess.cpp
+ ${LIBDIR}/slic3r/GUI/BackgroundSlicingProcess.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
+ ${LIBDIR}/slic3r/GUI/GLShader.hpp
+ ${LIBDIR}/slic3r/GUI/GLCanvas3D.hpp
+ ${LIBDIR}/slic3r/GUI/GLCanvas3D.cpp
+ ${LIBDIR}/slic3r/GUI/GLCanvas3DManager.hpp
+ ${LIBDIR}/slic3r/GUI/GLCanvas3DManager.cpp
+ ${LIBDIR}/slic3r/GUI/GLGizmo.hpp
+ ${LIBDIR}/slic3r/GUI/GLGizmo.cpp
+ ${LIBDIR}/slic3r/GUI/GLTexture.hpp
+ ${LIBDIR}/slic3r/GUI/GLTexture.cpp
+ ${LIBDIR}/slic3r/GUI/GLToolbar.hpp
+ ${LIBDIR}/slic3r/GUI/GLToolbar.cpp
+ ${LIBDIR}/slic3r/GUI/Preferences.cpp
+ ${LIBDIR}/slic3r/GUI/Preferences.hpp
+ ${LIBDIR}/slic3r/GUI/Preset.cpp
+ ${LIBDIR}/slic3r/GUI/Preset.hpp
+ ${LIBDIR}/slic3r/GUI/PresetBundle.cpp
+ ${LIBDIR}/slic3r/GUI/PresetBundle.hpp
+ ${LIBDIR}/slic3r/GUI/PresetHints.cpp
+ ${LIBDIR}/slic3r/GUI/PresetHints.hpp
+ ${LIBDIR}/slic3r/GUI/GUI.cpp
+ ${LIBDIR}/slic3r/GUI/GUI.hpp
+ ${LIBDIR}/slic3r/GUI/GUI_ObjectParts.cpp
+ ${LIBDIR}/slic3r/GUI/GUI_ObjectParts.hpp
+ ${LIBDIR}/slic3r/GUI/LambdaObjectDialog.cpp
+ ${LIBDIR}/slic3r/GUI/LambdaObjectDialog.hpp
+ ${LIBDIR}/slic3r/GUI/Tab.cpp
+ ${LIBDIR}/slic3r/GUI/Tab.hpp
+ ${LIBDIR}/slic3r/GUI/TabIface.cpp
+ ${LIBDIR}/slic3r/GUI/TabIface.hpp
+ ${LIBDIR}/slic3r/GUI/Field.cpp
+ ${LIBDIR}/slic3r/GUI/Field.hpp
+ ${LIBDIR}/slic3r/GUI/OptionsGroup.cpp
+ ${LIBDIR}/slic3r/GUI/OptionsGroup.hpp
+ ${LIBDIR}/slic3r/GUI/BedShapeDialog.cpp
+ ${LIBDIR}/slic3r/GUI/BedShapeDialog.hpp
+ ${LIBDIR}/slic3r/GUI/2DBed.cpp
+ ${LIBDIR}/slic3r/GUI/2DBed.hpp
+ ${LIBDIR}/slic3r/GUI/wxExtensions.cpp
+ ${LIBDIR}/slic3r/GUI/wxExtensions.hpp
+ ${LIBDIR}/slic3r/GUI/WipeTowerDialog.cpp
+ ${LIBDIR}/slic3r/GUI/WipeTowerDialog.hpp
+ ${LIBDIR}/slic3r/GUI/RammingChart.cpp
+ ${LIBDIR}/slic3r/GUI/RammingChart.hpp
+ ${LIBDIR}/slic3r/GUI/BonjourDialog.cpp
+ ${LIBDIR}/slic3r/GUI/BonjourDialog.hpp
+ ${LIBDIR}/slic3r/GUI/ButtonsDescription.cpp
+ ${LIBDIR}/slic3r/GUI/ButtonsDescription.hpp
+ ${LIBDIR}/slic3r/Config/Snapshot.cpp
+ ${LIBDIR}/slic3r/Config/Snapshot.hpp
+ ${LIBDIR}/slic3r/Config/Version.cpp
+ ${LIBDIR}/slic3r/Config/Version.hpp
+ ${LIBDIR}/slic3r/Utils/ASCIIFolding.cpp
+ ${LIBDIR}/slic3r/Utils/ASCIIFolding.hpp
+ ${LIBDIR}/slic3r/Utils/Serial.cpp
+ ${LIBDIR}/slic3r/Utils/Serial.hpp
+ ${LIBDIR}/slic3r/GUI/ConfigWizard.cpp
+ ${LIBDIR}/slic3r/GUI/ConfigWizard.hpp
+ ${LIBDIR}/slic3r/GUI/MsgDialog.cpp
+ ${LIBDIR}/slic3r/GUI/MsgDialog.hpp
+ ${LIBDIR}/slic3r/GUI/UpdateDialogs.cpp
+ ${LIBDIR}/slic3r/GUI/UpdateDialogs.hpp
+ ${LIBDIR}/slic3r/GUI/FirmwareDialog.cpp
+ ${LIBDIR}/slic3r/GUI/FirmwareDialog.hpp
+ ${LIBDIR}/slic3r/GUI/ProgressIndicator.hpp
+ ${LIBDIR}/slic3r/GUI/ProgressStatusBar.hpp
+ ${LIBDIR}/slic3r/GUI/ProgressStatusBar.cpp
+ ${LIBDIR}/slic3r/Utils/Http.cpp
+ ${LIBDIR}/slic3r/Utils/Http.hpp
+ ${LIBDIR}/slic3r/Utils/FixModelByWin10.cpp
+ ${LIBDIR}/slic3r/Utils/FixModelByWin10.hpp
+ ${LIBDIR}/slic3r/Utils/PrintHostSendDialog.cpp
+ ${LIBDIR}/slic3r/Utils/PrintHostSendDialog.hpp
+ ${LIBDIR}/slic3r/Utils/OctoPrint.cpp
+ ${LIBDIR}/slic3r/Utils/OctoPrint.hpp
+ ${LIBDIR}/slic3r/Utils/Duet.cpp
+ ${LIBDIR}/slic3r/Utils/Duet.hpp
+ ${LIBDIR}/slic3r/Utils/PrintHost.cpp
+ ${LIBDIR}/slic3r/Utils/PrintHost.hpp
+ ${LIBDIR}/slic3r/Utils/Bonjour.cpp
+ ${LIBDIR}/slic3r/Utils/Bonjour.hpp
+ ${LIBDIR}/slic3r/Utils/PresetUpdater.cpp
+ ${LIBDIR}/slic3r/Utils/PresetUpdater.hpp
+ ${LIBDIR}/slic3r/Utils/Time.cpp
+ ${LIBDIR}/slic3r/Utils/Time.hpp
+ ${LIBDIR}/slic3r/Utils/HexFile.cpp
+ ${LIBDIR}/slic3r/Utils/HexFile.hpp
+ ${LIBDIR}/slic3r/AppController.hpp
+ ${LIBDIR}/slic3r/AppController.cpp
+ ${LIBDIR}/slic3r/AppControllerWx.cpp
+)
+
+target_link_libraries(libslic3r_gui libslic3r avrdude)
diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp
new file mode 100644
index 000000000..704fbcfa1
--- /dev/null
+++ b/src/slic3r/Config/Snapshot.cpp
@@ -0,0 +1,532 @@
+#include "Snapshot.hpp"
+#include "../GUI/AppConfig.hpp"
+#include "../GUI/PresetBundle.hpp"
+#include "../Utils/Time.hpp"
+
+#include <time.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/nowide/cstdio.hpp>
+#include <boost/nowide/fstream.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+#include <boost/property_tree/ptree.hpp>
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/Config.hpp"
+#include "../../libslic3r/FileParserError.hpp"
+#include "../../libslic3r/Utils.hpp"
+
+#define SLIC3R_SNAPSHOTS_DIR "snapshots"
+#define SLIC3R_SNAPSHOT_FILE "snapshot.ini"
+
+namespace Slic3r {
+namespace GUI {
+namespace Config {
+
+void Snapshot::clear()
+{
+ this->id.clear();
+ this->time_captured = 0;
+ this->slic3r_version_captured = Semver::invalid();
+ this->comment.clear();
+ this->reason = SNAPSHOT_UNKNOWN;
+ this->print.clear();
+ this->filaments.clear();
+ this->printer.clear();
+}
+
+void Snapshot::load_ini(const std::string &path)
+{
+ this->clear();
+
+ auto throw_on_parse_error = [&path](const std::string &msg) {
+ throw file_parser_error(std::string("Failed loading the snapshot file. Reason: ") + msg, path);
+ };
+
+ // Load the snapshot.ini file.
+ boost::property_tree::ptree tree;
+ try {
+ boost::nowide::ifstream ifs(path);
+ boost::property_tree::read_ini(ifs, tree);
+ } catch (const std::ifstream::failure &err) {
+ throw file_parser_error(std::string("The snapshot file cannot be loaded. Reason: ") + err.what(), path);
+ } catch (const std::runtime_error &err) {
+ throw_on_parse_error(err.what());
+ }
+
+ // Parse snapshot.ini
+ std::string group_name_vendor = "Vendor:";
+ std::string key_filament = "filament";
+ std::string key_prefix_model = "model_";
+ for (auto &section : tree) {
+ if (section.first == "snapshot") {
+ // Parse the common section.
+ for (auto &kvp : section.second) {
+ if (kvp.first == "id")
+ this->id = kvp.second.data();
+ else if (kvp.first == "time_captured") {
+ this->time_captured = Slic3r::Utils::parse_time_ISO8601Z(kvp.second.data());
+ if (this->time_captured == (time_t)-1)
+ throw_on_parse_error("invalid timestamp");
+ } else if (kvp.first == "slic3r_version_captured") {
+ auto semver = Semver::parse(kvp.second.data());
+ if (! semver)
+ throw_on_parse_error("invalid slic3r_version_captured semver");
+ this->slic3r_version_captured = *semver;
+ } else if (kvp.first == "comment") {
+ this->comment = kvp.second.data();
+ } else if (kvp.first == "reason") {
+ std::string rsn = kvp.second.data();
+ if (rsn == "upgrade")
+ 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
+ this->reason = SNAPSHOT_UNKNOWN;
+ }
+ }
+ } else if (section.first == "presets") {
+ // Load the names of the active presets.
+ for (auto &kvp : section.second) {
+ if (kvp.first == "print") {
+ this->print = kvp.second.data();
+ } else if (boost::starts_with(kvp.first, "filament")) {
+ int idx = 0;
+ if (kvp.first == "filament" || sscanf(kvp.first.c_str(), "filament_%d", &idx) == 1) {
+ if (int(this->filaments.size()) <= idx)
+ this->filaments.resize(idx + 1, std::string());
+ this->filaments[idx] = kvp.second.data();
+ }
+ } else if (kvp.first == "printer") {
+ this->printer = kvp.second.data();
+ }
+ }
+ } else if (boost::starts_with(section.first, group_name_vendor) && section.first.size() > group_name_vendor.size()) {
+ // Vendor specific section.
+ VendorConfig vc;
+ vc.name = section.first.substr(group_name_vendor.size());
+ for (auto &kvp : section.second) {
+ 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)
+ throw_on_parse_error("invalid " + kvp.first + " format for " + section.first);
+ if (kvp.first == "version")
+ vc.version.config_version = *semver;
+ else if (kvp.first == "min_slic3r_version")
+ vc.version.min_slic3r_version = *semver;
+ else
+ vc.version.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<std::string> 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));
+ }
+ }
+ // 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)
+{
+ switch (reason) {
+ case Snapshot::SNAPSHOT_UPGRADE:
+ 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:
+ default:
+ return "unknown";
+ }
+}
+
+void Snapshot::save_ini(const std::string &path)
+{
+ boost::nowide::ofstream c;
+ c.open(path, std::ios::out | std::ios::trunc);
+ c << "# " << Slic3r::header_slic3r_generated() << std::endl;
+
+ // Export the common "snapshot".
+ c << std::endl << "[snapshot]" << std::endl;
+ c << "id = " << this->id << std::endl;
+ 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 = " << reason_string(this->reason) << std::endl;
+
+ // Export the active presets at the time of the snapshot.
+ c << std::endl << "[presets]" << std::endl;
+ c << "print = " << this->print << std::endl;
+ c << "filament = " << this->filaments.front() << std::endl;
+ for (size_t i = 1; i < this->filaments.size(); ++ i)
+ c << "filament_" << std::to_string(i) << " = " << this->filaments[i] << std::endl;
+ c << "printer = " << this->printer << std::endl;
+
+ // Export the vendor configs.
+ for (const VendorConfig &vc : this->vendor_configs) {
+ c << std::endl << "[Vendor:" << vc.name << "]" << std::endl;
+ c << "version = " << vc.version.config_version.to_string() << std::endl;
+ c << "min_slic3r_version = " << vc.version.min_slic3r_version.to_string() << std::endl;
+ c << "max_slic3r_version = " << vc.version.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<std::string> variants(model.second.begin(), model.second.end());
+ const auto escaped = escape_strings_cstyle(variants);
+ c << "model_" << model.first << " = " << escaped << std::endl;
+ }
+ }
+ c.close();
+}
+
+void Snapshot::export_selections(AppConfig &config) const
+{
+ assert(filaments.size() >= 1);
+ config.clear_section("presets");
+ config.set("presets", "print", print);
+ config.set("presets", "filament", filaments.front());
+ for (int i = 1; i < filaments.size(); ++i) {
+ char name[64];
+ sprintf(name, "filament_%d", i);
+ config.set("presets", name, filaments[i]);
+ }
+ config.set("presets", "printer", printer);
+}
+
+void Snapshot::export_vendor_configs(AppConfig &config) const
+{
+ std::map<std::string, std::map<std::string, std::set<std::string>>> vendors;
+ for (const VendorConfig &vc : vendor_configs)
+ vendors[vc.name] = vc.models_variants_installed;
+ 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<std::string> 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<std::string, std::map<std::string, std::set<std::string>>> &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<std::string> 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();
+
+ m_snapshots.clear();
+
+ // Walk over the snapshot directories and load their index.
+ std::string errors_cummulative;
+ for (auto &dir_entry : boost::filesystem::directory_iterator(snapshots_dir))
+ if (boost::filesystem::is_directory(dir_entry.status())) {
+ // Try to read "snapshot.ini".
+ boost::filesystem::path path_ini = dir_entry.path() / SLIC3R_SNAPSHOT_FILE;
+ Snapshot snapshot;
+ try {
+ snapshot.load_ini(path_ini.string());
+ } catch (const std::runtime_error &err) {
+ errors_cummulative += err.what();
+ errors_cummulative += "\n";
+ continue;
+ }
+ // Check that the name of the snapshot directory matches the snapshot id stored in the snapshot.ini file.
+ if (dir_entry.path().filename().string() != snapshot.id) {
+ errors_cummulative += std::string("Snapshot ID ") + snapshot.id + " does not match the snapshot directory " + dir_entry.path().filename().string() + "\n";
+ continue;
+ }
+ 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();
+}
+
+void SnapshotDB::update_slic3r_versions(std::vector<Index> &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.config_version);
+ if (it_version != it->end()) {
+ vendor_config.version.min_slic3r_version = it_version->min_slic3r_version;
+ vendor_config.version.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) &&
+ ! boost::filesystem::create_directory(path_dst))
+ throw std::runtime_error(std::string("Slic3r was unable to create a directory at ") + path_dst.string());
+
+ for (auto &dir_entry : boost::filesystem::directory_iterator(path_src))
+ if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini"))
+ boost::filesystem::copy_file(dir_entry.path(), path_dst / dir_entry.path().filename(), boost::filesystem::copy_option::overwrite_if_exists);
+}
+
+static void delete_existing_ini_files(const boost::filesystem::path &path)
+{
+ if (! boost::filesystem::is_directory(path))
+ return;
+ for (auto &dir_entry : boost::filesystem::directory_iterator(path))
+ if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini"))
+ boost::filesystem::remove(dir_entry.path());
+}
+
+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();
+
+ // 1) Prepare the snapshot structure.
+ Snapshot 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); // XXX: have Semver Slic3r version
+ snapshot.comment = comment;
+ snapshot.reason = reason;
+ // Active presets at the time of the snapshot.
+ snapshot.print = app_config.get("presets", "print");
+ snapshot.filaments.emplace_back(app_config.get("presets", "filament"));
+ snapshot.printer = app_config.get("presets", "printer");
+ for (unsigned int i = 1; i < 1000; ++ i) {
+ char name[64];
+ sprintf(name, "filament_%d", i);
+ if (! app_config.has("presets", name))
+ break;
+ snapshot.filaments.emplace_back(app_config.get("presets", name));
+ }
+ // Vendor specific config bundles and installed printers.
+ for (const std::pair<std::string, std::map<std::string, std::set<std::string>>> &vendor : app_config.vendors()) {
+ 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);
+ for (const VendorProfile &vp : bundle.vendors)
+ if (vp.id == cfg.name)
+ cfg.version.config_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.
+ Index index;
+ index.load(data_dir / "vendor" / (cfg.name + ".idx"));
+ auto it = index.find(cfg.version.config_version);
+ if (it != index.end()) {
+ cfg.version.min_slic3r_version = it->min_slic3r_version;
+ cfg.version.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);
+
+ // Backup the presets.
+ 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();
+}
+
+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 snapshot;
+ }
+ throw std::runtime_error(std::string("Snapshot with id " + id + " was not found."));
+}
+
+void SnapshotDB::restore_snapshot(const Snapshot &snapshot, AppConfig &app_config)
+{
+ 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 AppConfig with the selections of the print / filament / printer profiles
+ // and about the installed printer types and variants.
+ snapshot.export_selections(app_config);
+ 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.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());
+ boost::filesystem::path snapshots_dir = data_dir / SLIC3R_SNAPSHOTS_DIR;
+ for (const boost::filesystem::path &path : { data_dir, snapshots_dir }) {
+ boost::filesystem::path subdir = path;
+ subdir.make_preferred();
+ if (! boost::filesystem::is_directory(subdir) &&
+ ! boost::filesystem::create_directory(subdir))
+ throw std::runtime_error(std::string("Slic3r was unable to create a directory at ") + subdir.string());
+ }
+ return snapshots_dir;
+}
+
+SnapshotDB& SnapshotDB::singleton()
+{
+ static SnapshotDB instance;
+ static bool loaded = false;
+ if (! loaded) {
+ try {
+ loaded = true;
+ // Load the snapshot database.
+ instance.load_db();
+ // Load the vendor specific configuration indices.
+ std::vector<Index> 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/src/slic3r/Config/Snapshot.hpp b/src/slic3r/Config/Snapshot.hpp
new file mode 100644
index 000000000..a916dfe92
--- /dev/null
+++ b/src/slic3r/Config/Snapshot.hpp
@@ -0,0 +1,129 @@
+#ifndef slic3r_GUI_Snapshot_
+#define slic3r_GUI_Snapshot_
+
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <boost/filesystem.hpp>
+
+#include "Version.hpp"
+#include "../Utils/Semver.hpp"
+
+namespace Slic3r {
+
+class AppConfig;
+
+namespace GUI {
+namespace Config {
+
+class Index;
+
+// A snapshot contains:
+// Slic3r.ini
+// vendor/
+// print/
+// filament/
+// printer/
+class Snapshot
+{
+public:
+ enum Reason {
+ SNAPSHOT_UNKNOWN,
+ SNAPSHOT_UPGRADE,
+ SNAPSHOT_DOWNGRADE,
+ SNAPSHOT_BEFORE_ROLLBACK,
+ SNAPSHOT_USER,
+ };
+
+ Snapshot() { clear(); }
+
+ void clear();
+ void load_ini(const std::string &path);
+ void save_ini(const std::string &path);
+
+ // 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;
+
+ // 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;
+ std::time_t time_captured;
+ // Which Slic3r version captured this snapshot?
+ Semver slic3r_version_captured = Semver::invalid();
+ // Comment entered by the user at the start of the snapshot capture.
+ std::string comment;
+ Reason reason;
+
+ std::string format_reason() const;
+
+ // Active presets at the time of the snapshot.
+ std::string print;
+ std::vector<std::string> filaments;
+ std::string printer;
+
+ // Annotation of the vendor configuration stored in the snapshot.
+ // This information is displayed to the user and used to decide compatibility
+ // of the configuration stored in the snapshot with the running Slic3r version.
+ struct VendorConfig {
+ // Name of the vendor contained in this snapshot.
+ std::string name;
+ // Version of the vendor config contained in this snapshot, along with compatibility data.
+ Version version;
+ // Which printer models of this vendor were installed, and which variants of the models?
+ std::map<std::string, std::set<std::string>> models_variants_installed;
+ };
+ // List of vendor configs contained in this snapshot, sorted lexicographically.
+ std::vector<VendorConfig> vendor_configs;
+};
+
+class SnapshotDB
+{
+public:
+ // Initialize the SnapshotDB singleton instance. Load the database if it has not been loaded yet.
+ static SnapshotDB& singleton();
+
+ typedef std::vector<Snapshot>::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> &index_db);
+
+ // 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& 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<Snapshot>& 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<Snapshot> m_snapshots;
+};
+
+} // namespace Config
+} // namespace GUI
+} // namespace Slic3r
+
+#endif /* slic3r_GUI_Snapshot_ */
diff --git a/src/slic3r/Config/Version.cpp b/src/slic3r/Config/Version.cpp
new file mode 100644
index 000000000..a85322eca
--- /dev/null
+++ b/src/slic3r/Config/Version.cpp
@@ -0,0 +1,319 @@
+#include "Version.hpp"
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/nowide/fstream.hpp>
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/Config.hpp"
+#include "../../libslic3r/FileParserError.hpp"
+#include "../../libslic3r/Utils.hpp"
+
+namespace Slic3r {
+namespace GUI {
+namespace Config {
+
+static const Semver s_current_slic3r_semver(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);
+ return c;
+}
+
+inline char* right_trim(char *start)
+{
+ char *end = start + strlen(start) - 1;
+ for (; end >= start && (*end == ' ' || *end == '\t'); -- end);
+ *(++ end) = 0;
+ return end;
+}
+
+inline std::string unquote_value(char *value, char *end, const std::string &path, int idx_line)
+{
+ std::string svalue;
+ if (value == end) {
+ // Empty string is a valid string.
+ } else if (*value == '"') {
+ 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;
+}
+
+inline std::string unquote_version_comment(char *value, char *end, const std::string &path, int idx_line)
+{
+ std::string svalue;
+ if (value == end) {
+ // Empty string is a valid string.
+ } else if (*value == '"') {
+ 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;
+}
+
+size_t Index::load(const boost::filesystem::path &path)
+{
+ m_configs.clear();
+ m_vendor = path.stem().string();
+
+ boost::nowide::ifstream ifs(path.string());
+ std::string line;
+ size_t idx_line = 0;
+ Version ver;
+ while (std::getline(ifs, line)) {
+ ++ idx_line;
+ // Skip the initial white spaces.
+ char *key = left_trim(const_cast<char*>(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 = 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_end != 0 && *key_end != ' ' && *key_end != '\t' && *key_end != '=')
+ throw file_parser_error("Invalid keyword or semantic version", path, idx_line);
+ 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> semver;
+ if (maybe_semver)
+ semver = Semver::parse(key);
+ if (key_value_pair) {
+ 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(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(svalue);
+ if (! semver)
+ throw file_parser_error(std::string(key) + " must referece a valid semantic version", path, idx_line);
+ 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);
+ ver.config_version = *semver;
+ 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();
+}
+
+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) const
+{
+ 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;
+ 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->config_version < it->config_version))
+ highest = it;
+ return highest;
+}
+
+std::vector<Index> Index::load_db()
+{
+ boost::filesystem::path cache_dir = boost::filesystem::path(Slic3r::data_dir()) / "cache";
+
+ std::vector<Index> index_db;
+ std::string errors_cummulative;
+ 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 {
+ 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/src/slic3r/Config/Version.hpp b/src/slic3r/Config/Version.hpp
new file mode 100644
index 000000000..acb0ae460
--- /dev/null
+++ b/src/slic3r/Config/Version.hpp
@@ -0,0 +1,88 @@
+#ifndef slic3r_GUI_ConfigIndex_
+#define slic3r_GUI_ConfigIndex_
+
+#include <string>
+#include <vector>
+
+#include <boost/filesystem.hpp>
+
+#include "../../libslic3r/FileParserError.hpp"
+#include "../Utils/Semver.hpp"
+
+namespace Slic3r {
+namespace GUI {
+namespace Config {
+
+// Configuration bundle version.
+struct Version
+{
+ // Version of this config.
+ Semver config_version = Semver::invalid();
+ // Minimum Slic3r version, for which this config is applicable.
+ Semver min_slic3r_version = Semver::zero();
+ // Maximum Slic3r version, for which this config is recommended.
+ // Slic3r should read older configuration and upgrade to a newer format,
+ // but likely there has been a better configuration published, using the new features.
+ Semver max_slic3r_version = Semver::inf();
+ // Single comment line.
+ std::string comment;
+
+ bool is_slic3r_supported(const Semver &slicer_version) const;
+ bool is_current_slic3r_supported() const;
+};
+
+// Index of vendor specific config bundle versions and Slic3r compatibilities.
+// The index is being downloaded from the internet, also an initial version of the index
+// is contained in the Slic3r installation.
+//
+// The index has a simple format:
+//
+// min_sic3r_version =
+// max_slic3r_version =
+// config_version "comment"
+// config_version "comment"
+// ...
+// min_slic3r_version =
+// max_slic3r_version =
+// config_version comment
+// config_version comment
+// ...
+//
+// The min_slic3r_version, max_slic3r_version keys are applied to the config versions below,
+// empty slic3r version means an open interval.
+class Index
+{
+public:
+ typedef std::vector<Version>::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 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(); }
+ const_iterator find(const Semver &ver) const;
+ const std::vector<Version>& 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<Index> load_db();
+
+private:
+ std::string m_vendor;
+ std::vector<Version> m_configs;
+};
+
+} // namespace Config
+} // namespace GUI
+} // namespace Slic3r
+
+#endif /* slic3r_GUI_ConfigIndex_ */
diff --git a/src/slic3r/GUI/2DBed.cpp b/src/slic3r/GUI/2DBed.cpp
new file mode 100644
index 000000000..e19f839cd
--- /dev/null
+++ b/src/slic3r/GUI/2DBed.cpp
@@ -0,0 +1,183 @@
+#include "2DBed.hpp"
+
+#include <wx/dcbuffer.h>
+#include "BoundingBox.hpp"
+#include "Geometry.hpp"
+#include "ClipperUtils.hpp"
+
+namespace Slic3r {
+namespace GUI {
+
+void Bed_2D::repaint()
+{
+ wxAutoBufferedPaintDC dc(this);
+ auto cw = GetSize().GetWidth();
+ auto ch = GetSize().GetHeight();
+ // when canvas is not rendered yet, size is 0, 0
+ if (cw == 0) return ;
+
+ if (m_user_drawn_background) {
+ // On all systems the AutoBufferedPaintDC() achieves double buffering.
+ // On MacOS the background is erased, on Windows the background is not erased
+ // and on Linux / GTK the background is erased to gray color.
+ // Fill DC with the background on Windows & Linux / GTK.
+ auto color = wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); //GetSystemColour
+ dc.SetPen(*new wxPen(color, 1, wxPENSTYLE_SOLID));
+ dc.SetBrush(*new wxBrush(color, wxBRUSHSTYLE_SOLID));
+ auto rect = GetUpdateRegion().GetBox();
+ dc.DrawRectangle(rect.GetLeft(), rect.GetTop(), rect.GetWidth(), rect.GetHeight());
+ }
+
+ // turn cw and ch from sizes to max coordinates
+ cw--;
+ ch--;
+
+ auto cbb = BoundingBoxf(Vec2d(0, 0),Vec2d(cw, ch));
+ // leave space for origin point
+ cbb.min(0) += 4;
+ cbb.max -= Vec2d(4., 4.);
+
+ // leave space for origin label
+ cbb.max(1) -= 13;
+
+ // read new size
+ cw = cbb.size()(0);
+ ch = cbb.size()(1);
+
+ auto ccenter = cbb.center();
+
+ // get bounding box of bed shape in G - code coordinates
+ auto bed_shape = m_bed_shape;
+ auto bed_polygon = Polygon::new_scale(m_bed_shape);
+ auto bb = BoundingBoxf(m_bed_shape);
+ bb.merge(Vec2d(0, 0)); // origin needs to be in the visible area
+ auto bw = bb.size()(0);
+ auto bh = bb.size()(1);
+ auto bcenter = bb.center();
+
+ // calculate the scaling factor for fitting bed shape in canvas area
+ auto sfactor = std::min(cw/bw, ch/bh);
+ auto shift = Vec2d(
+ ccenter(0) - bcenter(0) * sfactor,
+ ccenter(1) - bcenter(1) * sfactor
+ );
+ m_scale_factor = sfactor;
+ m_shift = Vec2d(shift(0) + cbb.min(0),
+ shift(1) - (cbb.max(1) - GetSize().GetHeight()));
+
+ // draw bed fill
+ dc.SetBrush(wxBrush(wxColour(255, 255, 255), wxSOLID));
+ wxPointList pt_list;
+ for (auto pt: m_bed_shape)
+ {
+ Point pt_pix = to_pixels(pt);
+ pt_list.push_back(new wxPoint(pt_pix(0), pt_pix(1)));
+ }
+ dc.DrawPolygon(&pt_list, 0, 0);
+
+ // draw grid
+ auto step = 10; // 1cm grid
+ Polylines polylines;
+ for (auto x = bb.min(0) - fmod(bb.min(0), step) + step; x < bb.max(0); x += step) {
+ polylines.push_back(Polyline::new_scale({ Vec2d(x, bb.min(1)), Vec2d(x, bb.max(1)) }));
+ }
+ for (auto y = bb.min(1) - fmod(bb.min(1), step) + step; y < bb.max(1); y += step) {
+ polylines.push_back(Polyline::new_scale({ Vec2d(bb.min(0), y), Vec2d(bb.max(0), y) }));
+ }
+ polylines = intersection_pl(polylines, bed_polygon);
+
+ dc.SetPen(wxPen(wxColour(230, 230, 230), 1, wxSOLID));
+ for (auto pl : polylines)
+ {
+ for (size_t i = 0; i < pl.points.size()-1; i++){
+ Point pt1 = to_pixels(unscale(pl.points[i]));
+ Point pt2 = to_pixels(unscale(pl.points[i+1]));
+ dc.DrawLine(pt1(0), pt1(1), pt2(0), pt2(1));
+ }
+ }
+
+ // draw bed contour
+ 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(Vec2d(0, 0));
+
+ // draw axes
+ auto axes_len = 50;
+ auto arrow_len = 6;
+ auto arrow_angle = Geometry::deg2rad(45.0);
+ dc.SetPen(wxPen(wxColour(255, 0, 0), 2, wxSOLID)); // red
+ auto x_end = Vec2d(origin_px(0) + axes_len, origin_px(1));
+ dc.DrawLine(wxPoint(origin_px(0), origin_px(1)), wxPoint(x_end(0), x_end(1)));
+ for (auto angle : { -arrow_angle, arrow_angle }){
+ auto end = Eigen::Translation2d(x_end) * Eigen::Rotation2Dd(angle) * Eigen::Translation2d(- x_end) * Eigen::Vector2d(x_end(0) - arrow_len, x_end(1));
+ dc.DrawLine(wxPoint(x_end(0), x_end(1)), wxPoint(end(0), end(1)));
+ }
+
+ dc.SetPen(wxPen(wxColour(0, 255, 0), 2, wxSOLID)); // green
+ auto y_end = Vec2d(origin_px(0), origin_px(1) - axes_len);
+ dc.DrawLine(wxPoint(origin_px(0), origin_px(1)), wxPoint(y_end(0), y_end(1)));
+ for (auto angle : { -arrow_angle, arrow_angle }) {
+ auto end = Eigen::Translation2d(y_end) * Eigen::Rotation2Dd(angle) * Eigen::Translation2d(- y_end) * Eigen::Vector2d(y_end(0), y_end(1) + arrow_len);
+ dc.DrawLine(wxPoint(y_end(0), y_end(1)), wxPoint(end(0), end(1)));
+ }
+
+ // draw origin
+ dc.SetPen(wxPen(wxColour(0, 0, 0), 1, wxSOLID));
+ dc.SetBrush(wxBrush(wxColour(0, 0, 0), wxSOLID));
+ dc.DrawCircle(origin_px(0), origin_px(1), 3);
+
+ 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(0) <= cw / 2 ? origin_px(0) + 1 : origin_px(0) - 1 - extent.GetWidth();
+ const auto origin_label_y = origin_px(1) <= ch / 2 ? origin_px(1) + 1 : origin_px(1) - 1 - extent.GetHeight();
+ dc.DrawText(origin_label, origin_label_x, origin_label_y);
+
+ // draw current position
+ if (m_pos!= Vec2d(0, 0)) {
+ auto pos_px = to_pixels(m_pos);
+ dc.SetPen(wxPen(wxColour(200, 0, 0), 2, wxSOLID));
+ dc.SetBrush(wxBrush(wxColour(200, 0, 0), wxTRANSPARENT));
+ dc.DrawCircle(pos_px(0), pos_px(1), 5);
+
+ dc.DrawLine(pos_px(0) - 15, pos_px(1), pos_px(0) + 15, pos_px(1));
+ dc.DrawLine(pos_px(0), pos_px(1) - 15, pos_px(0), pos_px(1) + 15);
+ }
+
+ m_painted = true;
+}
+
+// convert G - code coordinates into pixels
+Point Bed_2D::to_pixels(Vec2d point){
+ auto p = point * m_scale_factor + m_shift;
+ return Point(p(0), GetSize().GetHeight() - p(1));
+}
+
+void Bed_2D::mouse_event(wxMouseEvent event){
+ if (!m_interactive) return;
+ if (!m_painted) return;
+
+ auto pos = event.GetPosition();
+ auto point = to_units(Point(pos.x, pos.y));
+ if (event.LeftDown() || event.Dragging()) {
+ if (m_on_move)
+ m_on_move(point) ;
+ Refresh();
+ }
+}
+
+// convert pixels into G - code coordinates
+Vec2d Bed_2D::to_units(Point point){
+ return (Vec2d(point(0), GetSize().GetHeight() - point(1)) - m_shift) * (1. / m_scale_factor);
+}
+
+void Bed_2D::set_pos(Vec2d pos){
+ m_pos = pos;
+ Refresh();
+}
+
+} // GUI
+} // Slic3r \ No newline at end of file
diff --git a/src/slic3r/GUI/2DBed.hpp b/src/slic3r/GUI/2DBed.hpp
new file mode 100644
index 000000000..d7a7f4260
--- /dev/null
+++ b/src/slic3r/GUI/2DBed.hpp
@@ -0,0 +1,52 @@
+#ifndef slic3r_2DBed_hpp_
+#define slic3r_2DBed_hpp_
+
+#include <wx/wx.h>
+#include "Config.hpp"
+
+namespace Slic3r {
+namespace GUI {
+
+class Bed_2D : public wxPanel
+{
+ bool m_user_drawn_background = true;
+
+ bool m_painted = false;
+ bool m_interactive = false;
+ double m_scale_factor;
+ Vec2d m_shift = Vec2d::Zero();
+ Vec2d m_pos = Vec2d::Zero();
+ std::function<void(Vec2d)> m_on_move = nullptr;
+
+ Point to_pixels(Vec2d point);
+ Vec2d to_units(Point point);
+ void repaint();
+ void mouse_event(wxMouseEvent event);
+ void set_pos(Vec2d pos);
+
+public:
+ Bed_2D(wxWindow* parent)
+ {
+ Create(parent, wxID_ANY, wxDefaultPosition, wxSize(250, -1), wxTAB_TRAVERSAL);
+// m_user_drawn_background = $^O ne 'darwin';
+#ifdef __APPLE__
+ m_user_drawn_background = false;
+#endif /*__APPLE__*/
+ Bind(wxEVT_PAINT, ([this](wxPaintEvent e) { repaint(); }));
+// EVT_ERASE_BACKGROUND($self, sub{}) if $self->{user_drawn_background};
+// Bind(EVT_MOUSE_EVENTS, ([this](wxMouseEvent event){/*mouse_event()*/; }));
+ Bind(wxEVT_LEFT_DOWN, ([this](wxMouseEvent event){ mouse_event(event); }));
+ Bind(wxEVT_MOTION, ([this](wxMouseEvent event){ mouse_event(event); }));
+ Bind(wxEVT_SIZE, ([this](wxSizeEvent e) { Refresh(); }));
+ }
+ ~Bed_2D(){}
+
+ std::vector<Vec2d> m_bed_shape;
+
+};
+
+
+} // GUI
+} // Slic3r
+
+#endif /* slic3r_2DBed_hpp_ */
diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp
new file mode 100644
index 000000000..e6f038042
--- /dev/null
+++ b/src/slic3r/GUI/3DScene.cpp
@@ -0,0 +1,2205 @@
+#include <GL/glew.h>
+
+#include "3DScene.hpp"
+
+#include "../../libslic3r/ExtrusionEntity.hpp"
+#include "../../libslic3r/ExtrusionEntityCollection.hpp"
+#include "../../libslic3r/Geometry.hpp"
+#include "../../libslic3r/GCode/PreviewData.hpp"
+#include "../../libslic3r/Print.hpp"
+#include "../../libslic3r/Slicing.hpp"
+#include "../../slic3r/GUI/PresetBundle.hpp"
+#include "GCode/Analyzer.hpp"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <utility>
+#include <assert.h>
+
+#include <boost/log/trivial.hpp>
+
+#include <tbb/parallel_for.h>
+#include <tbb/spin_mutex.h>
+
+#include <Eigen/Dense>
+
+#include "GUI.hpp"
+
+namespace Slic3r {
+
+void GLIndexedVertexArray::load_mesh_flat_shading(const TriangleMesh &mesh)
+{
+ assert(triangle_indices.empty() && vertices_and_normals_interleaved_size == 0);
+ assert(quad_indices.empty() && triangle_indices_size == 0);
+ assert(vertices_and_normals_interleaved.size() % 6 == 0 && quad_indices_size == vertices_and_normals_interleaved.size());
+
+ this->vertices_and_normals_interleaved.reserve(this->vertices_and_normals_interleaved.size() + 3 * 3 * 2 * mesh.facets_count());
+
+ for (int i = 0; i < mesh.stl.stats.number_of_facets; ++ i) {
+ const stl_facet &facet = mesh.stl.facet_start[i];
+ for (int j = 0; j < 3; ++ j)
+ this->push_geometry(facet.vertex[j](0), facet.vertex[j](1), facet.vertex[j](2), facet.normal(0), facet.normal(1), facet.normal(2));
+ }
+}
+
+void GLIndexedVertexArray::load_mesh_full_shading(const TriangleMesh &mesh)
+{
+ assert(triangle_indices.empty() && vertices_and_normals_interleaved_size == 0);
+ assert(quad_indices.empty() && triangle_indices_size == 0);
+ assert(vertices_and_normals_interleaved.size() % 6 == 0 && quad_indices_size == vertices_and_normals_interleaved.size());
+
+ this->vertices_and_normals_interleaved.reserve(this->vertices_and_normals_interleaved.size() + 3 * 3 * 2 * mesh.facets_count());
+
+ unsigned int vertices_count = 0;
+ for (int i = 0; i < mesh.stl.stats.number_of_facets; ++i) {
+ const stl_facet &facet = mesh.stl.facet_start[i];
+ for (int j = 0; j < 3; ++j)
+ this->push_geometry(facet.vertex[j](0), facet.vertex[j](1), facet.vertex[j](2), facet.normal(0), facet.normal(1), facet.normal(2));
+
+ this->push_triangle(vertices_count, vertices_count + 1, vertices_count + 2);
+ vertices_count += 3;
+ }
+}
+
+void GLIndexedVertexArray::finalize_geometry(bool use_VBOs)
+{
+ assert(this->vertices_and_normals_interleaved_VBO_id == 0);
+ assert(this->triangle_indices_VBO_id == 0);
+ assert(this->quad_indices_VBO_id == 0);
+
+ this->setup_sizes();
+
+ if (use_VBOs) {
+ if (! empty()) {
+ glGenBuffers(1, &this->vertices_and_normals_interleaved_VBO_id);
+ glBindBuffer(GL_ARRAY_BUFFER, this->vertices_and_normals_interleaved_VBO_id);
+ glBufferData(GL_ARRAY_BUFFER, this->vertices_and_normals_interleaved.size() * 4, this->vertices_and_normals_interleaved.data(), GL_STATIC_DRAW);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ this->vertices_and_normals_interleaved.clear();
+ }
+ if (! this->triangle_indices.empty()) {
+ glGenBuffers(1, &this->triangle_indices_VBO_id);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->triangle_indices_VBO_id);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->triangle_indices.size() * 4, this->triangle_indices.data(), GL_STATIC_DRAW);
+ this->triangle_indices.clear();
+ }
+ if (! this->quad_indices.empty()) {
+ glGenBuffers(1, &this->quad_indices_VBO_id);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->quad_indices_VBO_id);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->quad_indices.size() * 4, this->quad_indices.data(), GL_STATIC_DRAW);
+ this->quad_indices.clear();
+ }
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ }
+ this->shrink_to_fit();
+}
+
+void GLIndexedVertexArray::release_geometry()
+{
+ if (this->vertices_and_normals_interleaved_VBO_id)
+ glDeleteBuffers(1, &this->vertices_and_normals_interleaved_VBO_id);
+ if (this->triangle_indices_VBO_id)
+ glDeleteBuffers(1, &this->triangle_indices_VBO_id);
+ if (this->quad_indices_VBO_id)
+ glDeleteBuffers(1, &this->quad_indices_VBO_id);
+ this->clear();
+ this->shrink_to_fit();
+}
+
+void GLIndexedVertexArray::render() const
+{
+ if (this->vertices_and_normals_interleaved_VBO_id) {
+ glBindBuffer(GL_ARRAY_BUFFER, this->vertices_and_normals_interleaved_VBO_id);
+ glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), (const void*)(3 * sizeof(float)));
+ glNormalPointer(GL_FLOAT, 6 * sizeof(float), nullptr);
+ } else {
+ glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), this->vertices_and_normals_interleaved.data() + 3);
+ glNormalPointer(GL_FLOAT, 6 * sizeof(float), this->vertices_and_normals_interleaved.data());
+ }
+ glEnableClientState(GL_VERTEX_ARRAY);
+ glEnableClientState(GL_NORMAL_ARRAY);
+
+ if (this->indexed()) {
+ if (this->vertices_and_normals_interleaved_VBO_id) {
+ // Render using the Vertex Buffer Objects.
+ if (this->triangle_indices_size > 0) {
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->triangle_indices_VBO_id);
+ glDrawElements(GL_TRIANGLES, GLsizei(this->triangle_indices_size), GL_UNSIGNED_INT, nullptr);
+ }
+ if (this->quad_indices_size > 0) {
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->quad_indices_VBO_id);
+ glDrawElements(GL_QUADS, GLsizei(this->quad_indices_size), GL_UNSIGNED_INT, nullptr);
+ }
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ } else {
+ // Render in an immediate mode.
+ if (! this->triangle_indices.empty())
+ glDrawElements(GL_TRIANGLES, GLsizei(this->triangle_indices_size), GL_UNSIGNED_INT, this->triangle_indices.data());
+ if (! this->quad_indices.empty())
+ glDrawElements(GL_QUADS, GLsizei(this->quad_indices_size), GL_UNSIGNED_INT, this->quad_indices.data());
+ }
+ } else
+ glDrawArrays(GL_TRIANGLES, 0, GLsizei(this->vertices_and_normals_interleaved_size / 6));
+
+ if (this->vertices_and_normals_interleaved_VBO_id)
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDisableClientState(GL_VERTEX_ARRAY);
+ glDisableClientState(GL_NORMAL_ARRAY);
+}
+
+void GLIndexedVertexArray::render(
+ const std::pair<size_t, size_t> &tverts_range,
+ const std::pair<size_t, size_t> &qverts_range) const
+{
+ assert(this->indexed());
+ if (! this->indexed())
+ return;
+
+ if (this->vertices_and_normals_interleaved_VBO_id) {
+ // Render using the Vertex Buffer Objects.
+ glBindBuffer(GL_ARRAY_BUFFER, this->vertices_and_normals_interleaved_VBO_id);
+ glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), (const void*)(3 * sizeof(float)));
+ glNormalPointer(GL_FLOAT, 6 * sizeof(float), nullptr);
+ glEnableClientState(GL_VERTEX_ARRAY);
+ glEnableClientState(GL_NORMAL_ARRAY);
+ if (this->triangle_indices_size > 0) {
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->triangle_indices_VBO_id);
+ glDrawElements(GL_TRIANGLES, GLsizei(std::min(this->triangle_indices_size, tverts_range.second - tverts_range.first)), GL_UNSIGNED_INT, (const void*)(tverts_range.first * 4));
+ }
+ if (this->quad_indices_size > 0) {
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->quad_indices_VBO_id);
+ glDrawElements(GL_QUADS, GLsizei(std::min(this->quad_indices_size, qverts_range.second - qverts_range.first)), GL_UNSIGNED_INT, (const void*)(qverts_range.first * 4));
+ }
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ } else {
+ // Render in an immediate mode.
+ glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), this->vertices_and_normals_interleaved.data() + 3);
+ glNormalPointer(GL_FLOAT, 6 * sizeof(float), this->vertices_and_normals_interleaved.data());
+ glEnableClientState(GL_VERTEX_ARRAY);
+ glEnableClientState(GL_NORMAL_ARRAY);
+ if (! this->triangle_indices.empty())
+ glDrawElements(GL_TRIANGLES, GLsizei(std::min(this->triangle_indices_size, tverts_range.second - tverts_range.first)), GL_UNSIGNED_INT, (const void*)(this->triangle_indices.data() + tverts_range.first));
+ if (! this->quad_indices.empty())
+ glDrawElements(GL_QUADS, GLsizei(std::min(this->quad_indices_size, qverts_range.second - qverts_range.first)), GL_UNSIGNED_INT, (const void*)(this->quad_indices.data() + qverts_range.first));
+ }
+
+ glDisableClientState(GL_VERTEX_ARRAY);
+ glDisableClientState(GL_NORMAL_ARRAY);
+}
+
+const float GLVolume::SELECTED_COLOR[4] = { 0.0f, 1.0f, 0.0f, 1.0f };
+const float GLVolume::HOVER_COLOR[4] = { 0.4f, 0.9f, 0.1f, 1.0f };
+const float GLVolume::OUTSIDE_COLOR[4] = { 0.0f, 0.38f, 0.8f, 1.0f };
+const float GLVolume::SELECTED_OUTSIDE_COLOR[4] = { 0.19f, 0.58f, 1.0f, 1.0f };
+
+GLVolume::GLVolume(float r, float g, float b, float a)
+ : m_offset(Vec3d::Zero())
+ , m_rotation(0.0)
+ , m_scaling_factor(1.0)
+ , m_world_matrix(Transform3f::Identity())
+ , m_world_matrix_dirty(true)
+ , m_transformed_bounding_box_dirty(true)
+ , m_transformed_convex_hull_bounding_box_dirty(true)
+ , m_convex_hull(nullptr)
+ , composite_id(-1)
+ , select_group_id(-1)
+ , drag_group_id(-1)
+ , extruder_id(0)
+ , selected(false)
+ , is_active(true)
+ , zoom_to_volumes(true)
+ , shader_outside_printer_detection_enabled(false)
+ , is_outside(false)
+ , hover(false)
+ , is_modifier(false)
+ , is_wipe_tower(false)
+ , is_extrusion_path(false)
+ , tverts_range(0, size_t(-1))
+ , qverts_range(0, size_t(-1))
+{
+ color[0] = r;
+ color[1] = g;
+ color[2] = b;
+ color[3] = a;
+ set_render_color(r, g, b, a);
+}
+
+void GLVolume::set_render_color(float r, float g, float b, float a)
+{
+ render_color[0] = r;
+ render_color[1] = g;
+ render_color[2] = b;
+ render_color[3] = a;
+}
+
+void GLVolume::set_render_color(const float* rgba, unsigned int size)
+{
+ size = std::min((unsigned int)4, size);
+ for (int i = 0; i < size; ++i)
+ {
+ render_color[i] = rgba[i];
+ }
+}
+
+void GLVolume::set_render_color()
+{
+ if (selected)
+ set_render_color(is_outside ? SELECTED_OUTSIDE_COLOR : SELECTED_COLOR, 4);
+ else if (hover)
+ set_render_color(HOVER_COLOR, 4);
+ else if (is_outside && shader_outside_printer_detection_enabled)
+ set_render_color(OUTSIDE_COLOR, 4);
+ else
+ set_render_color(color, 4);
+}
+
+double GLVolume::get_rotation()
+{
+ return m_rotation;
+}
+
+void GLVolume::set_rotation(double rotation)
+{
+ if (m_rotation != rotation)
+ {
+ m_rotation = rotation;
+ m_world_matrix_dirty = true;
+ m_transformed_bounding_box_dirty = true;
+ m_transformed_convex_hull_bounding_box_dirty = true;
+ }
+}
+
+const Vec3d& GLVolume::get_offset() const
+{
+ return m_offset;
+}
+
+void GLVolume::set_offset(const Vec3d& offset)
+{
+ if (m_offset != offset)
+ {
+ m_offset = offset;
+ m_world_matrix_dirty = true;
+ m_transformed_bounding_box_dirty = true;
+ m_transformed_convex_hull_bounding_box_dirty = true;
+ }
+}
+
+void GLVolume::set_scaling_factor(double factor)
+{
+ if (m_scaling_factor != factor)
+ {
+ m_scaling_factor = factor;
+ m_world_matrix_dirty = true;
+ m_transformed_bounding_box_dirty = true;
+ m_transformed_convex_hull_bounding_box_dirty = true;
+ }
+}
+
+void GLVolume::set_convex_hull(const TriangleMesh& convex_hull)
+{
+ m_convex_hull = &convex_hull;
+}
+
+void GLVolume::set_select_group_id(const std::string& select_by)
+{
+ if (select_by == "object")
+ select_group_id = object_idx() * 1000000;
+ else if (select_by == "volume")
+ select_group_id = object_idx() * 1000000 + volume_idx() * 1000;
+ else if (select_by == "instance")
+ select_group_id = composite_id;
+}
+
+void GLVolume::set_drag_group_id(const std::string& drag_by)
+{
+ if (drag_by == "object")
+ drag_group_id = object_idx() * 1000;
+ else if (drag_by == "instance")
+ drag_group_id = object_idx() * 1000 + instance_idx();
+}
+
+const Transform3f& GLVolume::world_matrix() const
+{
+ if (m_world_matrix_dirty)
+ {
+ m_world_matrix = Transform3f::Identity();
+ m_world_matrix.translate(m_offset.cast<float>());
+ m_world_matrix.rotate(Eigen::AngleAxisf((float)m_rotation, Vec3f::UnitZ()));
+ m_world_matrix.scale((float)m_scaling_factor);
+ m_world_matrix_dirty = false;
+ }
+ return m_world_matrix;
+}
+
+const BoundingBoxf3& GLVolume::transformed_bounding_box() const
+{
+ if (m_transformed_bounding_box_dirty)
+ {
+ m_transformed_bounding_box = bounding_box.transformed(world_matrix().cast<double>());
+ m_transformed_bounding_box_dirty = false;
+ }
+
+ return m_transformed_bounding_box;
+}
+
+const BoundingBoxf3& GLVolume::transformed_convex_hull_bounding_box() const
+{
+ if (m_transformed_convex_hull_bounding_box_dirty)
+ {
+ if ((m_convex_hull != nullptr) && (m_convex_hull->stl.stats.number_of_facets > 0))
+ m_transformed_convex_hull_bounding_box = m_convex_hull->transformed_bounding_box(world_matrix().cast<double>());
+ else
+ m_transformed_convex_hull_bounding_box = bounding_box.transformed(world_matrix().cast<double>());
+
+ m_transformed_convex_hull_bounding_box_dirty = false;
+ }
+
+ return m_transformed_convex_hull_bounding_box;
+}
+
+void GLVolume::set_range(double min_z, double max_z)
+{
+ this->qverts_range.first = 0;
+ this->qverts_range.second = this->indexed_vertex_array.quad_indices_size;
+ this->tverts_range.first = 0;
+ this->tverts_range.second = this->indexed_vertex_array.triangle_indices_size;
+ if (! this->print_zs.empty()) {
+ // The Z layer range is specified.
+ // First test whether the Z span of this object is not out of (min_z, max_z) completely.
+ if (this->print_zs.front() > max_z || this->print_zs.back() < min_z) {
+ this->qverts_range.second = 0;
+ this->tverts_range.second = 0;
+ } else {
+ // Then find the lowest layer to be displayed.
+ size_t i = 0;
+ for (; i < this->print_zs.size() && this->print_zs[i] < min_z; ++ i);
+ if (i == this->print_zs.size()) {
+ // This shall not happen.
+ this->qverts_range.second = 0;
+ this->tverts_range.second = 0;
+ } else {
+ // Remember start of the layer.
+ this->qverts_range.first = this->offsets[i * 2];
+ this->tverts_range.first = this->offsets[i * 2 + 1];
+ // Some layers are above $min_z. Which?
+ for (; i < this->print_zs.size() && this->print_zs[i] <= max_z; ++ i);
+ if (i < this->print_zs.size()) {
+ this->qverts_range.second = this->offsets[i * 2];
+ this->tverts_range.second = this->offsets[i * 2 + 1];
+ }
+ }
+ }
+ }
+}
+
+void GLVolume::render() const
+{
+ if (!is_active)
+ return;
+
+ ::glCullFace(GL_BACK);
+ ::glPushMatrix();
+ ::glTranslated(m_offset(0), m_offset(1), m_offset(2));
+ ::glRotated(m_rotation * 180.0 / (double)PI, 0.0, 0.0, 1.0);
+ ::glScaled(m_scaling_factor, m_scaling_factor, m_scaling_factor);
+ if (this->indexed_vertex_array.indexed())
+ this->indexed_vertex_array.render(this->tverts_range, this->qverts_range);
+ else
+ this->indexed_vertex_array.render();
+ ::glPopMatrix();
+}
+
+void GLVolume::render_using_layer_height() const
+{
+ if (!is_active)
+ return;
+
+ GLint current_program_id;
+ glGetIntegerv(GL_CURRENT_PROGRAM, &current_program_id);
+
+ if ((layer_height_texture_data.shader_id > 0) && (layer_height_texture_data.shader_id != current_program_id))
+ glUseProgram(layer_height_texture_data.shader_id);
+
+ GLint z_to_texture_row_id = (layer_height_texture_data.shader_id > 0) ? glGetUniformLocation(layer_height_texture_data.shader_id, "z_to_texture_row") : -1;
+ GLint z_texture_row_to_normalized_id = (layer_height_texture_data.shader_id > 0) ? glGetUniformLocation(layer_height_texture_data.shader_id, "z_texture_row_to_normalized") : -1;
+ GLint z_cursor_id = (layer_height_texture_data.shader_id > 0) ? glGetUniformLocation(layer_height_texture_data.shader_id, "z_cursor") : -1;
+ GLint z_cursor_band_width_id = (layer_height_texture_data.shader_id > 0) ? glGetUniformLocation(layer_height_texture_data.shader_id, "z_cursor_band_width") : -1;
+ GLint world_matrix_id = (layer_height_texture_data.shader_id > 0) ? glGetUniformLocation(layer_height_texture_data.shader_id, "volume_world_matrix") : -1;
+
+ if (z_to_texture_row_id >= 0)
+ glUniform1f(z_to_texture_row_id, (GLfloat)layer_height_texture_z_to_row_id());
+
+ if (z_texture_row_to_normalized_id >= 0)
+ glUniform1f(z_texture_row_to_normalized_id, (GLfloat)(1.0f / layer_height_texture_height()));
+
+ if (z_cursor_id >= 0)
+ glUniform1f(z_cursor_id, (GLfloat)(layer_height_texture_data.print_object->model_object()->bounding_box().max(2) * layer_height_texture_data.z_cursor_relative));
+
+ if (z_cursor_band_width_id >= 0)
+ glUniform1f(z_cursor_band_width_id, (GLfloat)layer_height_texture_data.edit_band_width);
+
+ if (world_matrix_id >= 0)
+ ::glUniformMatrix4fv(world_matrix_id, 1, GL_FALSE, (const GLfloat*)world_matrix().data());
+
+ GLsizei w = (GLsizei)layer_height_texture_width();
+ GLsizei h = (GLsizei)layer_height_texture_height();
+ GLsizei half_w = w / 2;
+ GLsizei half_h = h / 2;
+
+ ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ glBindTexture(GL_TEXTURE_2D, layer_height_texture_data.texture_id);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, half_w, half_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, layer_height_texture_data_ptr_level0());
+ glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, half_w, half_h, GL_RGBA, GL_UNSIGNED_BYTE, layer_height_texture_data_ptr_level1());
+
+ render();
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ if ((current_program_id > 0) && (layer_height_texture_data.shader_id != current_program_id))
+ glUseProgram(current_program_id);
+}
+
+void GLVolume::render_VBOs(int color_id, int detection_id, int worldmatrix_id) const
+{
+ if (!is_active)
+ return;
+
+ if (!indexed_vertex_array.vertices_and_normals_interleaved_VBO_id)
+ return;
+
+ if (layer_height_texture_data.can_use())
+ {
+ ::glDisableClientState(GL_VERTEX_ARRAY);
+ ::glDisableClientState(GL_NORMAL_ARRAY);
+ render_using_layer_height();
+ ::glEnableClientState(GL_VERTEX_ARRAY);
+ ::glEnableClientState(GL_NORMAL_ARRAY);
+ return;
+ }
+
+ GLsizei n_triangles = GLsizei(std::min(indexed_vertex_array.triangle_indices_size, tverts_range.second - tverts_range.first));
+ GLsizei n_quads = GLsizei(std::min(indexed_vertex_array.quad_indices_size, qverts_range.second - qverts_range.first));
+ if (n_triangles + n_quads == 0)
+ {
+ ::glDisableClientState(GL_VERTEX_ARRAY);
+ ::glDisableClientState(GL_NORMAL_ARRAY);
+
+ if (color_id >= 0)
+ {
+ float color[4];
+ ::memcpy((void*)color, (const void*)render_color, 4 * sizeof(float));
+ ::glUniform4fv(color_id, 1, (const GLfloat*)color);
+ }
+ else
+ ::glColor4f(render_color[0], render_color[1], render_color[2], render_color[3]);
+
+ if (detection_id != -1)
+ ::glUniform1i(detection_id, shader_outside_printer_detection_enabled ? 1 : 0);
+
+ if (worldmatrix_id != -1)
+ ::glUniformMatrix4fv(worldmatrix_id, 1, GL_FALSE, (const GLfloat*)world_matrix().data());
+
+ render();
+
+ ::glEnableClientState(GL_VERTEX_ARRAY);
+ ::glEnableClientState(GL_NORMAL_ARRAY);
+
+ return;
+ }
+
+ if (color_id >= 0)
+ ::glUniform4fv(color_id, 1, (const GLfloat*)render_color);
+ else
+ ::glColor4f(render_color[0], render_color[1], render_color[2], render_color[3]);
+
+ if (detection_id != -1)
+ ::glUniform1i(detection_id, shader_outside_printer_detection_enabled ? 1 : 0);
+
+ if (worldmatrix_id != -1)
+ ::glUniformMatrix4fv(worldmatrix_id, 1, GL_FALSE, (const GLfloat*)world_matrix().data());
+
+ ::glBindBuffer(GL_ARRAY_BUFFER, indexed_vertex_array.vertices_and_normals_interleaved_VBO_id);
+ ::glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), (const void*)(3 * sizeof(float)));
+ ::glNormalPointer(GL_FLOAT, 6 * sizeof(float), nullptr);
+
+ ::glPushMatrix();
+ ::glTranslated(m_offset(0), m_offset(1), m_offset(2));
+ ::glRotated(m_rotation * 180.0 / (double)PI, 0.0, 0.0, 1.0);
+ ::glScaled(m_scaling_factor, m_scaling_factor, m_scaling_factor);
+
+ if (n_triangles > 0)
+ {
+ ::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexed_vertex_array.triangle_indices_VBO_id);
+ ::glDrawElements(GL_TRIANGLES, n_triangles, GL_UNSIGNED_INT, (const void*)(tverts_range.first * 4));
+ }
+ if (n_quads > 0)
+ {
+ ::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexed_vertex_array.quad_indices_VBO_id);
+ ::glDrawElements(GL_QUADS, n_quads, GL_UNSIGNED_INT, (const void*)(qverts_range.first * 4));
+ }
+
+ ::glPopMatrix();
+}
+
+void GLVolume::render_legacy() const
+{
+ assert(!indexed_vertex_array.vertices_and_normals_interleaved_VBO_id);
+ if (!is_active)
+ return;
+
+ GLsizei n_triangles = GLsizei(std::min(indexed_vertex_array.triangle_indices_size, tverts_range.second - tverts_range.first));
+ GLsizei n_quads = GLsizei(std::min(indexed_vertex_array.quad_indices_size, qverts_range.second - qverts_range.first));
+ if (n_triangles + n_quads == 0)
+ {
+ ::glDisableClientState(GL_VERTEX_ARRAY);
+ ::glDisableClientState(GL_NORMAL_ARRAY);
+
+ ::glColor4f(render_color[0], render_color[1], render_color[2], render_color[3]);
+ render();
+
+ ::glEnableClientState(GL_VERTEX_ARRAY);
+ ::glEnableClientState(GL_NORMAL_ARRAY);
+
+ return;
+ }
+
+ ::glColor4f(render_color[0], render_color[1], render_color[2], render_color[3]);
+ ::glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), indexed_vertex_array.vertices_and_normals_interleaved.data() + 3);
+ ::glNormalPointer(GL_FLOAT, 6 * sizeof(float), indexed_vertex_array.vertices_and_normals_interleaved.data());
+
+ ::glPushMatrix();
+ ::glTranslated(m_offset(0), m_offset(1), m_offset(2));
+ ::glRotated(m_rotation * 180.0 / (double)PI, 0.0, 0.0, 1.0);
+ ::glScaled(m_scaling_factor, m_scaling_factor, m_scaling_factor);
+
+ if (n_triangles > 0)
+ ::glDrawElements(GL_TRIANGLES, n_triangles, GL_UNSIGNED_INT, indexed_vertex_array.triangle_indices.data() + tverts_range.first);
+
+ if (n_quads > 0)
+ ::glDrawElements(GL_QUADS, n_quads, GL_UNSIGNED_INT, indexed_vertex_array.quad_indices.data() + qverts_range.first);
+
+ ::glPopMatrix();
+}
+
+double GLVolume::layer_height_texture_z_to_row_id() const
+{
+ return (this->layer_height_texture.get() == nullptr) ? 0.0 : double(this->layer_height_texture->cells - 1) / (double(this->layer_height_texture->width) * this->layer_height_texture_data.print_object->model_object()->bounding_box().max(2));
+}
+
+void GLVolume::generate_layer_height_texture(const PrintObject *print_object, bool force)
+{
+ LayersTexture *tex = this->layer_height_texture.get();
+ if (tex == nullptr)
+ // No layer_height_texture is assigned to this GLVolume, therefore the layer height texture cannot be filled.
+ return;
+
+ // Always try to update the layer height profile.
+ bool update = print_object->update_layer_height_profile(const_cast<ModelObject*>(print_object->model_object())->layer_height_profile) || force;
+ // Update if the layer height profile was changed, or when the texture is not valid.
+ if (! update && ! tex->data.empty() && tex->cells > 0)
+ // Texture is valid, don't update.
+ return;
+
+ if (tex->data.empty()) {
+ tex->width = 1024;
+ tex->height = 1024;
+ tex->levels = 2;
+ tex->data.assign(tex->width * tex->height * 5, 0);
+ }
+
+ SlicingParameters slicing_params = print_object->slicing_parameters();
+ bool level_of_detail_2nd_level = true;
+ tex->cells = Slic3r::generate_layer_height_texture(
+ slicing_params,
+ Slic3r::generate_object_layers(slicing_params, print_object->model_object()->layer_height_profile),
+ tex->data.data(), tex->height, tex->width, level_of_detail_2nd_level);
+}
+
+// 512x512 bitmaps are supported everywhere, but that may not be sufficent for super large print volumes.
+#define LAYER_HEIGHT_TEXTURE_WIDTH 1024
+#define LAYER_HEIGHT_TEXTURE_HEIGHT 1024
+
+std::vector<int> GLVolumeCollection::load_object(
+ const ModelObject *model_object,
+ int obj_idx,
+ const std::vector<int> &instance_idxs,
+ const std::string &color_by,
+ const std::string &select_by,
+ const std::string &drag_by,
+ bool use_VBOs)
+{
+ static float colors[4][4] = {
+ { 1.0f, 1.0f, 0.0f, 1.f },
+ { 1.0f, 0.5f, 0.5f, 1.f },
+ { 0.5f, 1.0f, 0.5f, 1.f },
+ { 0.5f, 0.5f, 1.0f, 1.f }
+ };
+
+ // Object will have a single common layer height texture for all volumes.
+ std::shared_ptr<LayersTexture> layer_height_texture = std::make_shared<LayersTexture>();
+
+ std::vector<int> volumes_idx;
+ for (int volume_idx = 0; volume_idx < int(model_object->volumes.size()); ++ volume_idx) {
+ const ModelVolume *model_volume = model_object->volumes[volume_idx];
+
+ int extruder_id = -1;
+ if (model_volume->is_model_part())
+ {
+ extruder_id = model_volume->config.has("extruder") ? model_volume->config.option("extruder")->getInt() : 0;
+ if (extruder_id == 0)
+ extruder_id = model_object->config.has("extruder") ? model_object->config.option("extruder")->getInt() : 0;
+ }
+
+ for (int instance_idx : instance_idxs) {
+ const ModelInstance *instance = model_object->instances[instance_idx];
+ TriangleMesh mesh = model_volume->mesh;
+ volumes_idx.push_back(int(this->volumes.size()));
+ float color[4];
+ memcpy(color, colors[((color_by == "volume") ? volume_idx : obj_idx) % 4], sizeof(float) * 3);
+ if (model_volume->is_support_blocker()) {
+ color[0] = 1.0f;
+ color[1] = 0.2f;
+ color[2] = 0.2f;
+ } else if (model_volume->is_support_enforcer()) {
+ color[0] = 0.2f;
+ color[1] = 0.2f;
+ color[2] = 1.0f;
+ }
+ color[3] = model_volume->is_model_part() ? 1.f : 0.5f;
+ this->volumes.emplace_back(new GLVolume(color));
+ GLVolume &v = *this->volumes.back();
+ if (use_VBOs)
+ v.indexed_vertex_array.load_mesh_full_shading(mesh);
+ else
+ v.indexed_vertex_array.load_mesh_flat_shading(mesh);
+
+ // finalize_geometry() clears the vertex arrays, therefore the bounding box has to be computed before finalize_geometry().
+ v.bounding_box = v.indexed_vertex_array.bounding_box();
+ v.indexed_vertex_array.finalize_geometry(use_VBOs);
+ v.composite_id = obj_idx * 1000000 + volume_idx * 1000 + instance_idx;
+ v.set_select_group_id(select_by);
+ v.set_drag_group_id(drag_by);
+ if (model_volume->is_model_part())
+ {
+ v.set_convex_hull(model_volume->get_convex_hull());
+ v.layer_height_texture = layer_height_texture;
+ if (extruder_id != -1)
+ v.extruder_id = extruder_id;
+ }
+ v.is_modifier = ! model_volume->is_model_part();
+ v.shader_outside_printer_detection_enabled = model_volume->is_model_part();
+#if ENABLE_MODELINSTANCE_3D_OFFSET
+ v.set_offset(instance->get_offset());
+#else
+ v.set_offset(Vec3d(instance->offset(0), instance->offset(1), 0.0));
+#endif // ENABLE_MODELINSTANCE_3D_OFFSET
+ v.set_rotation(instance->rotation);
+ v.set_scaling_factor(instance->scaling_factor);
+ }
+ }
+
+ return volumes_idx;
+}
+
+
+int GLVolumeCollection::load_wipe_tower_preview(
+ int obj_idx, float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool use_VBOs, bool size_unknown, float brim_width)
+{
+ if (depth < 0.01f)
+ return int(this->volumes.size() - 1);
+ if (height == 0.0f)
+ height = 0.1f;
+ Point origin_of_rotation(0.f, 0.f);
+ TriangleMesh mesh;
+ float color[4] = { 0.5f, 0.5f, 0.0f, 1.f };
+
+ // In case we don't know precise dimensions of the wipe tower yet, we'll draw the box with different color with one side jagged:
+ if (size_unknown) {
+ color[0] = 0.9f;
+ color[1] = 0.6f;
+
+ depth = std::max(depth, 10.f); // Too narrow tower would interfere with the teeth. The estimate is not precise anyway.
+ float min_width = 30.f;
+ // We'll now create the box with jagged edge. y-coordinates of the pre-generated model are shifted so that the front
+ // edge has y=0 and centerline of the back edge has y=depth:
+ Pointf3s points;
+ std::vector<Vec3crd> facets;
+ float out_points_idx[][3] = {{0, -depth, 0}, {0, 0, 0}, {38.453, 0, 0}, {61.547, 0, 0}, {100, 0, 0}, {100, -depth, 0}, {55.7735, -10, 0}, {44.2265, 10, 0},
+ {38.453, 0, 1}, {0, 0, 1}, {0, -depth, 1}, {100, -depth, 1}, {100, 0, 1}, {61.547, 0, 1}, {55.7735, -10, 1}, {44.2265, 10, 1}};
+ int out_facets_idx[][3] = {{0, 1, 2}, {3, 4, 5}, {6, 5, 0}, {3, 5, 6}, {6, 2, 7}, {6, 0, 2}, {8, 9, 10}, {11, 12, 13}, {10, 11, 14}, {14, 11, 13}, {15, 8, 14},
+ {8, 10, 14}, {3, 12, 4}, {3, 13, 12}, {6, 13, 3}, {6, 14, 13}, {7, 14, 6}, {7, 15, 14}, {2, 15, 7}, {2, 8, 15}, {1, 8, 2}, {1, 9, 8},
+ {0, 9, 1}, {0, 10, 9}, {5, 10, 0}, {5, 11, 10}, {4, 11, 5}, {4, 12, 11}};
+ for (int i=0;i<16;++i)
+ points.push_back(Vec3d(out_points_idx[i][0] / (100.f/min_width), out_points_idx[i][1] + depth, out_points_idx[i][2]));
+ for (int i=0;i<28;++i)
+ facets.push_back(Vec3crd(out_facets_idx[i][0], out_facets_idx[i][1], out_facets_idx[i][2]));
+ TriangleMesh tooth_mesh(points, facets);
+
+ // We have the mesh ready. It has one tooth and width of min_width. We will now append several of these together until we are close to
+ // the required width of the block. Than we can scale it precisely.
+ size_t n = std::max(1, int(width/min_width)); // How many shall be merged?
+ for (size_t i=0;i<n;++i) {
+ mesh.merge(tooth_mesh);
+ tooth_mesh.translate(min_width, 0.f, 0.f);
+ }
+
+ mesh.scale(Vec3d(width/(n*min_width), 1.f, height)); // Scaling to proper width
+ }
+ else
+ mesh = make_cube(width, depth, height);
+
+ // We'll make another mesh to show the brim (fixed layer height):
+ TriangleMesh brim_mesh = make_cube(width+2.f*brim_width, depth+2.f*brim_width, 0.2f);
+ brim_mesh.translate(-brim_width, -brim_width, 0.f);
+ mesh.merge(brim_mesh);
+
+ mesh.rotate(rotation_angle, &origin_of_rotation); // rotates the box according to the config rotation setting
+
+ this->volumes.emplace_back(new GLVolume(color));
+ GLVolume &v = *this->volumes.back();
+
+ if (use_VBOs)
+ v.indexed_vertex_array.load_mesh_full_shading(mesh);
+ else
+ v.indexed_vertex_array.load_mesh_flat_shading(mesh);
+
+ v.set_offset(Vec3d(pos_x, pos_y, 0.0));
+
+ // finalize_geometry() clears the vertex arrays, therefore the bounding box has to be computed before finalize_geometry().
+ v.bounding_box = v.indexed_vertex_array.bounding_box();
+ v.indexed_vertex_array.finalize_geometry(use_VBOs);
+ v.composite_id = obj_idx * 1000000;
+ v.select_group_id = obj_idx * 1000000;
+ v.drag_group_id = obj_idx * 1000;
+ v.is_wipe_tower = true;
+ v.shader_outside_printer_detection_enabled = ! size_unknown;
+ return int(this->volumes.size() - 1);
+}
+
+void GLVolumeCollection::render_VBOs() const
+{
+ ::glEnable(GL_BLEND);
+ ::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ ::glCullFace(GL_BACK);
+ ::glEnableClientState(GL_VERTEX_ARRAY);
+ ::glEnableClientState(GL_NORMAL_ARRAY);
+
+ GLint current_program_id;
+ ::glGetIntegerv(GL_CURRENT_PROGRAM, &current_program_id);
+ GLint color_id = (current_program_id > 0) ? glGetUniformLocation(current_program_id, "uniform_color") : -1;
+ GLint print_box_min_id = (current_program_id > 0) ? glGetUniformLocation(current_program_id, "print_box.min") : -1;
+ GLint print_box_max_id = (current_program_id > 0) ? glGetUniformLocation(current_program_id, "print_box.max") : -1;
+ GLint print_box_detection_id = (current_program_id > 0) ? glGetUniformLocation(current_program_id, "print_box.volume_detection") : -1;
+ GLint print_box_worldmatrix_id = (current_program_id > 0) ? glGetUniformLocation(current_program_id, "print_box.volume_world_matrix") : -1;
+
+ if (print_box_min_id != -1)
+ ::glUniform3fv(print_box_min_id, 1, (const GLfloat*)print_box_min);
+
+ if (print_box_max_id != -1)
+ ::glUniform3fv(print_box_max_id, 1, (const GLfloat*)print_box_max);
+
+ for (GLVolume *volume : this->volumes)
+ {
+ if (volume->layer_height_texture_data.can_use())
+ volume->generate_layer_height_texture(volume->layer_height_texture_data.print_object, false);
+ else
+ volume->set_render_color();
+
+ volume->render_VBOs(color_id, print_box_detection_id, print_box_worldmatrix_id);
+ }
+
+ ::glBindBuffer(GL_ARRAY_BUFFER, 0);
+ ::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+
+ ::glDisableClientState(GL_VERTEX_ARRAY);
+ ::glDisableClientState(GL_NORMAL_ARRAY);
+
+ ::glDisable(GL_BLEND);
+}
+
+void GLVolumeCollection::render_legacy() const
+{
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ glCullFace(GL_BACK);
+ glEnableClientState(GL_VERTEX_ARRAY);
+ glEnableClientState(GL_NORMAL_ARRAY);
+
+ for (GLVolume *volume : this->volumes)
+ {
+ volume->set_render_color();
+ volume->render_legacy();
+ }
+
+ glDisableClientState(GL_VERTEX_ARRAY);
+ glDisableClientState(GL_NORMAL_ARRAY);
+
+ glDisable(GL_BLEND);
+}
+
+bool GLVolumeCollection::check_outside_state(const DynamicPrintConfig* config, ModelInstance::EPrintVolumeState* out_state)
+{
+ if (config == nullptr)
+ return false;
+
+ const ConfigOptionPoints* opt = dynamic_cast<const ConfigOptionPoints*>(config->option("bed_shape"));
+ if (opt == nullptr)
+ return false;
+
+ BoundingBox bed_box_2D = get_extents(Polygon::new_scale(opt->values));
+ BoundingBoxf3 print_volume(Vec3d(unscale<double>(bed_box_2D.min(0)), unscale<double>(bed_box_2D.min(1)), 0.0), Vec3d(unscale<double>(bed_box_2D.max(0)), unscale<double>(bed_box_2D.max(1)), config->opt_float("max_print_height")));
+ // Allow the objects to protrude below the print bed
+ print_volume.min(2) = -1e10;
+
+ ModelInstance::EPrintVolumeState state = ModelInstance::PVS_Inside;
+ bool all_contained = true;
+
+ for (GLVolume* volume : this->volumes)
+ {
+ if ((volume != nullptr) && !volume->is_modifier && (!volume->is_wipe_tower || (volume->is_wipe_tower && volume->shader_outside_printer_detection_enabled)))
+ {
+ const BoundingBoxf3& bb = volume->transformed_convex_hull_bounding_box();
+ bool contained = print_volume.contains(bb);
+ all_contained &= contained;
+
+ volume->is_outside = !contained;
+
+ if ((state == ModelInstance::PVS_Inside) && volume->is_outside)
+ state = ModelInstance::PVS_Fully_Outside;
+
+ if ((state == ModelInstance::PVS_Fully_Outside) && volume->is_outside && print_volume.intersects(bb))
+ state = ModelInstance::PVS_Partly_Outside;
+ }
+ }
+
+ if (out_state != nullptr)
+ *out_state = state;
+
+ return all_contained;
+}
+
+void GLVolumeCollection::reset_outside_state()
+{
+ for (GLVolume* volume : this->volumes)
+ {
+ if (volume != nullptr)
+ volume->is_outside = false;
+ }
+}
+
+void GLVolumeCollection::update_colors_by_extruder(const DynamicPrintConfig* config)
+{
+ static const float inv_255 = 1.0f / 255.0f;
+
+ struct Color
+ {
+ std::string text;
+ unsigned char rgb[3];
+
+ Color()
+ : text("")
+ {
+ rgb[0] = 255;
+ rgb[1] = 255;
+ rgb[2] = 255;
+ }
+
+ void set(const std::string& text, unsigned char* rgb)
+ {
+ this->text = text;
+ ::memcpy((void*)this->rgb, (const void*)rgb, 3 * sizeof(unsigned char));
+ }
+ };
+
+ if (config == nullptr)
+ return;
+
+ const ConfigOptionStrings* extruders_opt = dynamic_cast<const ConfigOptionStrings*>(config->option("extruder_colour"));
+ if (extruders_opt == nullptr)
+ return;
+
+ const ConfigOptionStrings* filamemts_opt = dynamic_cast<const ConfigOptionStrings*>(config->option("filament_colour"));
+ if (filamemts_opt == nullptr)
+ return;
+
+ unsigned int colors_count = std::max((unsigned int)extruders_opt->values.size(), (unsigned int)filamemts_opt->values.size());
+ if (colors_count == 0)
+ return;
+
+ std::vector<Color> colors(colors_count);
+
+ unsigned char rgb[3];
+ for (unsigned int i = 0; i < colors_count; ++i)
+ {
+ const std::string& txt_color = config->opt_string("extruder_colour", i);
+ if (PresetBundle::parse_color(txt_color, rgb))
+ {
+ colors[i].set(txt_color, rgb);
+ }
+ else
+ {
+ const std::string& txt_color = config->opt_string("filament_colour", i);
+ if (PresetBundle::parse_color(txt_color, rgb))
+ colors[i].set(txt_color, rgb);
+ }
+ }
+
+ for (GLVolume* volume : volumes)
+ {
+ if ((volume == nullptr) || volume->is_modifier || volume->is_wipe_tower)
+ continue;
+
+ int extruder_id = volume->extruder_id - 1;
+ if ((extruder_id < 0) || ((unsigned int)colors.size() <= extruder_id))
+ extruder_id = 0;
+
+ const Color& color = colors[extruder_id];
+ if (!color.text.empty())
+ {
+ for (int i = 0; i < 3; ++i)
+ {
+ volume->color[i] = (float)color.rgb[i] * inv_255;
+ }
+ }
+ }
+}
+
+void GLVolumeCollection::set_select_by(const std::string& select_by)
+{
+ for (GLVolume *vol : this->volumes)
+ {
+ if (vol != nullptr)
+ vol->set_select_group_id(select_by);
+ }
+}
+
+void GLVolumeCollection::set_drag_by(const std::string& drag_by)
+{
+ for (GLVolume *vol : this->volumes)
+ {
+ if (vol != nullptr)
+ vol->set_drag_group_id(drag_by);
+ }
+}
+
+std::vector<double> GLVolumeCollection::get_current_print_zs(bool active_only) const
+{
+ // Collect layer top positions of all volumes.
+ std::vector<double> print_zs;
+ for (GLVolume *vol : this->volumes)
+ {
+ if (!active_only || vol->is_active)
+ append(print_zs, vol->print_zs);
+ }
+ std::sort(print_zs.begin(), print_zs.end());
+
+ // Replace intervals of layers with similar top positions with their average value.
+ int n = int(print_zs.size());
+ int k = 0;
+ for (int i = 0; i < n;) {
+ int j = i + 1;
+ coordf_t zmax = print_zs[i] + EPSILON;
+ for (; j < n && print_zs[j] <= zmax; ++ j) ;
+ print_zs[k ++] = (j > i + 1) ? (0.5 * (print_zs[i] + print_zs[j - 1])) : print_zs[i];
+ i = j;
+ }
+ if (k < n)
+ print_zs.erase(print_zs.begin() + k, print_zs.end());
+
+ return print_zs;
+}
+
+// caller is responsible for supplying NO lines with zero length
+static void thick_lines_to_indexed_vertex_array(
+ const Lines &lines,
+ const std::vector<double> &widths,
+ const std::vector<double> &heights,
+ bool closed,
+ double top_z,
+ GLIndexedVertexArray &volume)
+{
+ assert(! lines.empty());
+ if (lines.empty())
+ return;
+
+#define LEFT 0
+#define RIGHT 1
+#define TOP 2
+#define BOTTOM 3
+
+ // right, left, top, bottom
+ int idx_prev[4] = { -1, -1, -1, -1 };
+ double bottom_z_prev = 0.;
+ Vec2d b1_prev(Vec2d::Zero());
+ Vec2d v_prev(Vec2d::Zero());
+ int idx_initial[4] = { -1, -1, -1, -1 };
+ double width_initial = 0.;
+ double bottom_z_initial = 0.0;
+
+ // loop once more in case of closed loops
+ size_t lines_end = closed ? (lines.size() + 1) : lines.size();
+ for (size_t ii = 0; ii < lines_end; ++ ii) {
+ size_t i = (ii == lines.size()) ? 0 : ii;
+ const Line &line = lines[i];
+ double len = unscale<double>(line.length());
+ double inv_len = 1.0 / len;
+ double bottom_z = top_z - heights[i];
+ double middle_z = 0.5 * (top_z + bottom_z);
+ double width = widths[i];
+
+ bool is_first = (ii == 0);
+ bool is_last = (ii == lines_end - 1);
+ bool is_closing = closed && is_last;
+
+ Vec2d v = unscale(line.vector());
+ v *= inv_len;
+
+ Vec2d a = unscale(line.a);
+ Vec2d b = unscale(line.b);
+ Vec2d a1 = a;
+ Vec2d a2 = a;
+ Vec2d b1 = b;
+ Vec2d b2 = b;
+ {
+ double dist = 0.5 * width; // scaled
+ double dx = dist * v(0);
+ double dy = dist * v(1);
+ a1 += Vec2d(+dy, -dx);
+ a2 += Vec2d(-dy, +dx);
+ b1 += Vec2d(+dy, -dx);
+ b2 += Vec2d(-dy, +dx);
+ }
+
+ // calculate new XY normals
+ Vector n = line.normal();
+ Vec3d xy_right_normal = unscale(n(0), n(1), 0);
+ xy_right_normal *= inv_len;
+
+ int idx_a[4];
+ int idx_b[4];
+ int idx_last = int(volume.vertices_and_normals_interleaved.size() / 6);
+
+ bool bottom_z_different = bottom_z_prev != bottom_z;
+ bottom_z_prev = bottom_z;
+
+ if (!is_first && bottom_z_different)
+ {
+ // Found a change of the layer thickness -> Add a cap at the end of the previous segment.
+ volume.push_quad(idx_b[BOTTOM], idx_b[LEFT], idx_b[TOP], idx_b[RIGHT]);
+ }
+
+ // Share top / bottom vertices if possible.
+ if (is_first) {
+ idx_a[TOP] = idx_last++;
+ volume.push_geometry(a(0), a(1), top_z , 0., 0., 1.);
+ } else {
+ idx_a[TOP] = idx_prev[TOP];
+ }
+
+ if (is_first || bottom_z_different) {
+ // Start of the 1st line segment or a change of the layer thickness while maintaining the print_z.
+ idx_a[BOTTOM] = idx_last ++;
+ volume.push_geometry(a(0), a(1), bottom_z, 0., 0., -1.);
+ idx_a[LEFT ] = idx_last ++;
+ volume.push_geometry(a2(0), a2(1), middle_z, -xy_right_normal(0), -xy_right_normal(1), -xy_right_normal(2));
+ idx_a[RIGHT] = idx_last ++;
+ volume.push_geometry(a1(0), a1(1), middle_z, xy_right_normal(0), xy_right_normal(1), xy_right_normal(2));
+ }
+ else {
+ idx_a[BOTTOM] = idx_prev[BOTTOM];
+ }
+
+ if (is_first) {
+ // Start of the 1st line segment.
+ width_initial = width;
+ bottom_z_initial = bottom_z;
+ memcpy(idx_initial, idx_a, sizeof(int) * 4);
+ } else {
+ // Continuing a previous segment.
+ // Share left / right vertices if possible.
+ double v_dot = v_prev.dot(v);
+ bool sharp = v_dot < 0.707; // sin(45 degrees)
+ if (sharp) {
+ if (!bottom_z_different)
+ {
+ // Allocate new left / right points for the start of this segment as these points will receive their own normals to indicate a sharp turn.
+ idx_a[RIGHT] = idx_last++;
+ volume.push_geometry(a1(0), a1(1), middle_z, xy_right_normal(0), xy_right_normal(1), xy_right_normal(2));
+ idx_a[LEFT] = idx_last++;
+ volume.push_geometry(a2(0), a2(1), middle_z, -xy_right_normal(0), -xy_right_normal(1), -xy_right_normal(2));
+ }
+ }
+ if (v_dot > 0.9) {
+ if (!bottom_z_different)
+ {
+ // The two successive segments are nearly collinear.
+ idx_a[LEFT ] = idx_prev[LEFT];
+ idx_a[RIGHT] = idx_prev[RIGHT];
+ }
+ }
+ else if (!sharp) {
+ if (!bottom_z_different)
+ {
+ // Create a sharp corner with an overshot and average the left / right normals.
+ // At the crease angle of 45 degrees, the overshot at the corner will be less than (1-1/cos(PI/8)) = 8.2% over an arc.
+ Vec2d intersection(Vec2d::Zero());
+ Geometry::ray_ray_intersection(b1_prev, v_prev, a1, v, intersection);
+ a1 = intersection;
+ a2 = 2. * a - intersection;
+ assert((a - a1).norm() < width);
+ assert((a - a2).norm() < width);
+ float *n_left_prev = volume.vertices_and_normals_interleaved.data() + idx_prev[LEFT ] * 6;
+ float *p_left_prev = n_left_prev + 3;
+ float *n_right_prev = volume.vertices_and_normals_interleaved.data() + idx_prev[RIGHT] * 6;
+ float *p_right_prev = n_right_prev + 3;
+ p_left_prev [0] = float(a2(0));
+ p_left_prev [1] = float(a2(1));
+ p_right_prev[0] = float(a1(0));
+ p_right_prev[1] = float(a1(1));
+ xy_right_normal(0) += n_right_prev[0];
+ xy_right_normal(1) += n_right_prev[1];
+ xy_right_normal *= 1. / xy_right_normal.norm();
+ n_left_prev [0] = float(-xy_right_normal(0));
+ n_left_prev [1] = float(-xy_right_normal(1));
+ n_right_prev[0] = float( xy_right_normal(0));
+ n_right_prev[1] = float( xy_right_normal(1));
+ idx_a[LEFT ] = idx_prev[LEFT ];
+ idx_a[RIGHT] = idx_prev[RIGHT];
+ }
+ }
+ else if (cross2(v_prev, v) > 0.) {
+ // Right turn. Fill in the right turn wedge.
+ volume.push_triangle(idx_prev[RIGHT], idx_a [RIGHT], idx_prev[TOP] );
+ volume.push_triangle(idx_prev[RIGHT], idx_prev[BOTTOM], idx_a [RIGHT] );
+ } else {
+ // Left turn. Fill in the left turn wedge.
+ volume.push_triangle(idx_prev[LEFT], idx_prev[TOP], idx_a [LEFT] );
+ volume.push_triangle(idx_prev[LEFT], idx_a [LEFT], idx_prev[BOTTOM]);
+ }
+ if (is_closing) {
+ if (!sharp) {
+ if (!bottom_z_different)
+ {
+ // Closing a loop with smooth transition. Unify the closing left / right vertices.
+ memcpy(volume.vertices_and_normals_interleaved.data() + idx_initial[LEFT ] * 6, volume.vertices_and_normals_interleaved.data() + idx_prev[LEFT ] * 6, sizeof(float) * 6);
+ memcpy(volume.vertices_and_normals_interleaved.data() + idx_initial[RIGHT] * 6, volume.vertices_and_normals_interleaved.data() + idx_prev[RIGHT] * 6, sizeof(float) * 6);
+ volume.vertices_and_normals_interleaved.erase(volume.vertices_and_normals_interleaved.end() - 12, volume.vertices_and_normals_interleaved.end());
+ // Replace the left / right vertex indices to point to the start of the loop.
+ for (size_t u = volume.quad_indices.size() - 16; u < volume.quad_indices.size(); ++ u) {
+ if (volume.quad_indices[u] == idx_prev[LEFT])
+ volume.quad_indices[u] = idx_initial[LEFT];
+ else if (volume.quad_indices[u] == idx_prev[RIGHT])
+ volume.quad_indices[u] = idx_initial[RIGHT];
+ }
+ }
+ }
+ // This is the last iteration, only required to solve the transition.
+ break;
+ }
+ }
+
+ // Only new allocate top / bottom vertices, if not closing a loop.
+ if (is_closing) {
+ idx_b[TOP] = idx_initial[TOP];
+ } else {
+ idx_b[TOP] = idx_last ++;
+ volume.push_geometry(b(0), b(1), top_z , 0., 0., 1.);
+ }
+
+ if (is_closing && (width == width_initial) && (bottom_z == bottom_z_initial)) {
+ idx_b[BOTTOM] = idx_initial[BOTTOM];
+ } else {
+ idx_b[BOTTOM] = idx_last ++;
+ volume.push_geometry(b(0), b(1), bottom_z, 0., 0., -1.);
+ }
+ // Generate new vertices for the end of this line segment.
+ idx_b[LEFT ] = idx_last ++;
+ volume.push_geometry(b2(0), b2(1), middle_z, -xy_right_normal(0), -xy_right_normal(1), -xy_right_normal(2));
+ idx_b[RIGHT ] = idx_last ++;
+ volume.push_geometry(b1(0), b1(1), middle_z, xy_right_normal(0), xy_right_normal(1), xy_right_normal(2));
+
+ memcpy(idx_prev, idx_b, 4 * sizeof(int));
+ bottom_z_prev = bottom_z;
+ b1_prev = b1;
+ v_prev = v;
+
+ if (bottom_z_different && (closed || (!is_first && !is_last)))
+ {
+ // Found a change of the layer thickness -> Add a cap at the beginning of this segment.
+ volume.push_quad(idx_a[BOTTOM], idx_a[RIGHT], idx_a[TOP], idx_a[LEFT]);
+ }
+
+ if (! closed) {
+ // Terminate open paths with caps.
+ if (is_first)
+ volume.push_quad(idx_a[BOTTOM], idx_a[RIGHT], idx_a[TOP], idx_a[LEFT]);
+ // We don't use 'else' because both cases are true if we have only one line.
+ if (is_last)
+ volume.push_quad(idx_b[BOTTOM], idx_b[LEFT], idx_b[TOP], idx_b[RIGHT]);
+ }
+
+ // Add quads for a straight hollow tube-like segment.
+ // bottom-right face
+ volume.push_quad(idx_a[BOTTOM], idx_b[BOTTOM], idx_b[RIGHT], idx_a[RIGHT]);
+ // top-right face
+ volume.push_quad(idx_a[RIGHT], idx_b[RIGHT], idx_b[TOP], idx_a[TOP]);
+ // top-left face
+ volume.push_quad(idx_a[TOP], idx_b[TOP], idx_b[LEFT], idx_a[LEFT]);
+ // bottom-left face
+ volume.push_quad(idx_a[LEFT], idx_b[LEFT], idx_b[BOTTOM], idx_a[BOTTOM]);
+ }
+
+#undef LEFT
+#undef RIGHT
+#undef TOP
+#undef BOTTOM
+}
+
+// caller is responsible for supplying NO lines with zero length
+static void thick_lines_to_indexed_vertex_array(const Lines3& lines,
+ const std::vector<double>& widths,
+ const std::vector<double>& heights,
+ bool closed,
+ GLIndexedVertexArray& volume)
+{
+ assert(!lines.empty());
+ if (lines.empty())
+ return;
+
+#define LEFT 0
+#define RIGHT 1
+#define TOP 2
+#define BOTTOM 3
+
+ // left, right, top, bottom
+ int idx_initial[4] = { -1, -1, -1, -1 };
+ int idx_prev[4] = { -1, -1, -1, -1 };
+ double z_prev = 0.0;
+ Vec3d n_right_prev = Vec3d::Zero();
+ Vec3d n_top_prev = Vec3d::Zero();
+ Vec3d unit_v_prev = Vec3d::Zero();
+ double width_initial = 0.0;
+
+ // new vertices around the line endpoints
+ // left, right, top, bottom
+ Vec3d a[4] = { Vec3d::Zero(), Vec3d::Zero(), Vec3d::Zero(), Vec3d::Zero() };
+ Vec3d b[4] = { Vec3d::Zero(), Vec3d::Zero(), Vec3d::Zero(), Vec3d::Zero() };
+
+ // loop once more in case of closed loops
+ size_t lines_end = closed ? (lines.size() + 1) : lines.size();
+ for (size_t ii = 0; ii < lines_end; ++ii)
+ {
+ size_t i = (ii == lines.size()) ? 0 : ii;
+
+ const Line3& line = lines[i];
+ double height = heights[i];
+ double width = widths[i];
+
+ Vec3d unit_v = unscale(line.vector()).normalized();
+
+ Vec3d n_top = Vec3d::Zero();
+ Vec3d n_right = Vec3d::Zero();
+ Vec3d unit_positive_z(0.0, 0.0, 1.0);
+
+ if ((line.a(0) == line.b(0)) && (line.a(1) == line.b(1)))
+ {
+ // vertical segment
+ n_right = (line.a(2) < line.b(2)) ? Vec3d(-1.0, 0.0, 0.0) : Vec3d(1.0, 0.0, 0.0);
+ n_top = Vec3d(0.0, 1.0, 0.0);
+ }
+ else
+ {
+ // generic segment
+ n_right = unit_v.cross(unit_positive_z).normalized();
+ n_top = n_right.cross(unit_v).normalized();
+ }
+
+ Vec3d rl_displacement = 0.5 * width * n_right;
+ Vec3d tb_displacement = 0.5 * height * n_top;
+ Vec3d l_a = unscale(line.a);
+ Vec3d l_b = unscale(line.b);
+
+ a[RIGHT] = l_a + rl_displacement;
+ a[LEFT] = l_a - rl_displacement;
+ a[TOP] = l_a + tb_displacement;
+ a[BOTTOM] = l_a - tb_displacement;
+ b[RIGHT] = l_b + rl_displacement;
+ b[LEFT] = l_b - rl_displacement;
+ b[TOP] = l_b + tb_displacement;
+ b[BOTTOM] = l_b - tb_displacement;
+
+ Vec3d n_bottom = -n_top;
+ Vec3d n_left = -n_right;
+
+ int idx_a[4];
+ int idx_b[4];
+ int idx_last = int(volume.vertices_and_normals_interleaved.size() / 6);
+
+ bool z_different = (z_prev != l_a(2));
+ z_prev = l_b(2);
+
+ // Share top / bottom vertices if possible.
+ if (ii == 0)
+ {
+ idx_a[TOP] = idx_last++;
+ volume.push_geometry(a[TOP], n_top);
+ }
+ else
+ idx_a[TOP] = idx_prev[TOP];
+
+ if ((ii == 0) || z_different)
+ {
+ // Start of the 1st line segment or a change of the layer thickness while maintaining the print_z.
+ idx_a[BOTTOM] = idx_last++;
+ volume.push_geometry(a[BOTTOM], n_bottom);
+ idx_a[LEFT] = idx_last++;
+ volume.push_geometry(a[LEFT], n_left);
+ idx_a[RIGHT] = idx_last++;
+ volume.push_geometry(a[RIGHT], n_right);
+ }
+ else
+ idx_a[BOTTOM] = idx_prev[BOTTOM];
+
+ if (ii == 0)
+ {
+ // Start of the 1st line segment.
+ width_initial = width;
+ ::memcpy(idx_initial, idx_a, sizeof(int) * 4);
+ }
+ else
+ {
+ // Continuing a previous segment.
+ // Share left / right vertices if possible.
+ double v_dot = unit_v_prev.dot(unit_v);
+ bool is_sharp = v_dot < 0.707; // sin(45 degrees)
+ bool is_right_turn = n_top_prev.dot(unit_v_prev.cross(unit_v)) > 0.0;
+
+ if (is_sharp)
+ {
+ // Allocate new left / right points for the start of this segment as these points will receive their own normals to indicate a sharp turn.
+ idx_a[RIGHT] = idx_last++;
+ volume.push_geometry(a[RIGHT], n_right);
+ idx_a[LEFT] = idx_last++;
+ volume.push_geometry(a[LEFT], n_left);
+ }
+
+ if (v_dot > 0.9)
+ {
+ // The two successive segments are nearly collinear.
+ idx_a[LEFT] = idx_prev[LEFT];
+ idx_a[RIGHT] = idx_prev[RIGHT];
+ }
+ else if (!is_sharp)
+ {
+ // Create a sharp corner with an overshot and average the left / right normals.
+ // At the crease angle of 45 degrees, the overshot at the corner will be less than (1-1/cos(PI/8)) = 8.2% over an arc.
+
+ // averages normals
+ Vec3d average_n_right = 0.5 * (n_right + n_right_prev).normalized();
+ Vec3d average_n_left = -average_n_right;
+ Vec3d average_rl_displacement = 0.5 * width * average_n_right;
+
+ // updates vertices around a
+ a[RIGHT] = l_a + average_rl_displacement;
+ a[LEFT] = l_a - average_rl_displacement;
+
+ // updates previous line normals
+ float* normal_left_prev = volume.vertices_and_normals_interleaved.data() + idx_prev[LEFT] * 6;
+ normal_left_prev[0] = float(average_n_left(0));
+ normal_left_prev[1] = float(average_n_left(1));
+ normal_left_prev[2] = float(average_n_left(2));
+
+ float* normal_right_prev = volume.vertices_and_normals_interleaved.data() + idx_prev[RIGHT] * 6;
+ normal_right_prev[0] = float(average_n_right(0));
+ normal_right_prev[1] = float(average_n_right(1));
+ normal_right_prev[2] = float(average_n_right(2));
+
+ // updates previous line's vertices around b
+ float* b_left_prev = normal_left_prev + 3;
+ b_left_prev[0] = float(a[LEFT](0));
+ b_left_prev[1] = float(a[LEFT](1));
+ b_left_prev[2] = float(a[LEFT](2));
+
+ float* b_right_prev = normal_right_prev + 3;
+ b_right_prev[0] = float(a[RIGHT](0));
+ b_right_prev[1] = float(a[RIGHT](1));
+ b_right_prev[2] = float(a[RIGHT](2));
+
+ idx_a[LEFT] = idx_prev[LEFT];
+ idx_a[RIGHT] = idx_prev[RIGHT];
+ }
+ else if (is_right_turn)
+ {
+ // Right turn. Fill in the right turn wedge.
+ volume.push_triangle(idx_prev[RIGHT], idx_a[RIGHT], idx_prev[TOP]);
+ volume.push_triangle(idx_prev[RIGHT], idx_prev[BOTTOM], idx_a[RIGHT]);
+ }
+ else
+ {
+ // Left turn. Fill in the left turn wedge.
+ volume.push_triangle(idx_prev[LEFT], idx_prev[TOP], idx_a[LEFT]);
+ volume.push_triangle(idx_prev[LEFT], idx_a[LEFT], idx_prev[BOTTOM]);
+ }
+
+ if (ii == lines.size())
+ {
+ if (!is_sharp)
+ {
+ // Closing a loop with smooth transition. Unify the closing left / right vertices.
+ ::memcpy(volume.vertices_and_normals_interleaved.data() + idx_initial[LEFT] * 6, volume.vertices_and_normals_interleaved.data() + idx_prev[LEFT] * 6, sizeof(float) * 6);
+ ::memcpy(volume.vertices_and_normals_interleaved.data() + idx_initial[RIGHT] * 6, volume.vertices_and_normals_interleaved.data() + idx_prev[RIGHT] * 6, sizeof(float) * 6);
+ volume.vertices_and_normals_interleaved.erase(volume.vertices_and_normals_interleaved.end() - 12, volume.vertices_and_normals_interleaved.end());
+ // Replace the left / right vertex indices to point to the start of the loop.
+ for (size_t u = volume.quad_indices.size() - 16; u < volume.quad_indices.size(); ++u)
+ {
+ if (volume.quad_indices[u] == idx_prev[LEFT])
+ volume.quad_indices[u] = idx_initial[LEFT];
+ else if (volume.quad_indices[u] == idx_prev[RIGHT])
+ volume.quad_indices[u] = idx_initial[RIGHT];
+ }
+ }
+
+ // This is the last iteration, only required to solve the transition.
+ break;
+ }
+ }
+
+ // Only new allocate top / bottom vertices, if not closing a loop.
+ if (closed && (ii + 1 == lines.size()))
+ idx_b[TOP] = idx_initial[TOP];
+ else
+ {
+ idx_b[TOP] = idx_last++;
+ volume.push_geometry(b[TOP], n_top);
+ }
+
+ if (closed && (ii + 1 == lines.size()) && (width == width_initial))
+ idx_b[BOTTOM] = idx_initial[BOTTOM];
+ else
+ {
+ idx_b[BOTTOM] = idx_last++;
+ volume.push_geometry(b[BOTTOM], n_bottom);
+ }
+
+ // Generate new vertices for the end of this line segment.
+ idx_b[LEFT] = idx_last++;
+ volume.push_geometry(b[LEFT], n_left);
+ idx_b[RIGHT] = idx_last++;
+ volume.push_geometry(b[RIGHT], n_right);
+
+ ::memcpy(idx_prev, idx_b, 4 * sizeof(int));
+ n_right_prev = n_right;
+ n_top_prev = n_top;
+ unit_v_prev = unit_v;
+
+ if (!closed)
+ {
+ // Terminate open paths with caps.
+ if (i == 0)
+ volume.push_quad(idx_a[BOTTOM], idx_a[RIGHT], idx_a[TOP], idx_a[LEFT]);
+
+ // We don't use 'else' because both cases are true if we have only one line.
+ if (i + 1 == lines.size())
+ volume.push_quad(idx_b[BOTTOM], idx_b[LEFT], idx_b[TOP], idx_b[RIGHT]);
+ }
+
+ // Add quads for a straight hollow tube-like segment.
+ // bottom-right face
+ volume.push_quad(idx_a[BOTTOM], idx_b[BOTTOM], idx_b[RIGHT], idx_a[RIGHT]);
+ // top-right face
+ volume.push_quad(idx_a[RIGHT], idx_b[RIGHT], idx_b[TOP], idx_a[TOP]);
+ // top-left face
+ volume.push_quad(idx_a[TOP], idx_b[TOP], idx_b[LEFT], idx_a[LEFT]);
+ // bottom-left face
+ volume.push_quad(idx_a[LEFT], idx_b[LEFT], idx_b[BOTTOM], idx_a[BOTTOM]);
+ }
+
+#undef LEFT
+#undef RIGHT
+#undef TOP
+#undef BOTTOM
+}
+
+static void point_to_indexed_vertex_array(const Vec3crd& point,
+ double width,
+ double height,
+ GLIndexedVertexArray& volume)
+{
+ // builds a double piramid, with vertices on the local axes, around the point
+
+ Vec3d center = unscale(point);
+
+ double scale_factor = 1.0;
+ double w = scale_factor * width;
+ double h = scale_factor * height;
+
+ // new vertices ids
+ int idx_last = int(volume.vertices_and_normals_interleaved.size() / 6);
+ int idxs[6];
+ for (int i = 0; i < 6; ++i)
+ {
+ idxs[i] = idx_last + i;
+ }
+
+ Vec3d displacement_x(w, 0.0, 0.0);
+ Vec3d displacement_y(0.0, w, 0.0);
+ Vec3d displacement_z(0.0, 0.0, h);
+
+ Vec3d unit_x(1.0, 0.0, 0.0);
+ Vec3d unit_y(0.0, 1.0, 0.0);
+ Vec3d unit_z(0.0, 0.0, 1.0);
+
+ // vertices
+ volume.push_geometry(center - displacement_x, -unit_x); // idxs[0]
+ volume.push_geometry(center + displacement_x, unit_x); // idxs[1]
+ volume.push_geometry(center - displacement_y, -unit_y); // idxs[2]
+ volume.push_geometry(center + displacement_y, unit_y); // idxs[3]
+ volume.push_geometry(center - displacement_z, -unit_z); // idxs[4]
+ volume.push_geometry(center + displacement_z, unit_z); // idxs[5]
+
+ // top piramid faces
+ volume.push_triangle(idxs[0], idxs[2], idxs[5]);
+ volume.push_triangle(idxs[2], idxs[1], idxs[5]);
+ volume.push_triangle(idxs[1], idxs[3], idxs[5]);
+ volume.push_triangle(idxs[3], idxs[0], idxs[5]);
+
+ // bottom piramid faces
+ volume.push_triangle(idxs[2], idxs[0], idxs[4]);
+ volume.push_triangle(idxs[1], idxs[2], idxs[4]);
+ volume.push_triangle(idxs[3], idxs[1], idxs[4]);
+ volume.push_triangle(idxs[0], idxs[3], idxs[4]);
+}
+
+void _3DScene::thick_lines_to_verts(
+ const Lines &lines,
+ const std::vector<double> &widths,
+ const std::vector<double> &heights,
+ bool closed,
+ double top_z,
+ GLVolume &volume)
+{
+ thick_lines_to_indexed_vertex_array(lines, widths, heights, closed, top_z, volume.indexed_vertex_array);
+}
+
+void _3DScene::thick_lines_to_verts(const Lines3& lines,
+ const std::vector<double>& widths,
+ const std::vector<double>& heights,
+ bool closed,
+ GLVolume& volume)
+{
+ thick_lines_to_indexed_vertex_array(lines, widths, heights, closed, volume.indexed_vertex_array);
+}
+
+static void thick_point_to_verts(const Vec3crd& point,
+ double width,
+ double height,
+ GLVolume& volume)
+{
+ point_to_indexed_vertex_array(point, width, height, volume.indexed_vertex_array);
+}
+
+// Fill in the qverts and tverts with quads and triangles for the extrusion_path.
+void _3DScene::extrusionentity_to_verts(const ExtrusionPath &extrusion_path, float print_z, GLVolume &volume)
+{
+ Lines lines = extrusion_path.polyline.lines();
+ std::vector<double> widths(lines.size(), extrusion_path.width);
+ std::vector<double> heights(lines.size(), extrusion_path.height);
+ thick_lines_to_verts(lines, widths, heights, false, print_z, volume);
+}
+
+// Fill in the qverts and tverts with quads and triangles for the extrusion_path.
+void _3DScene::extrusionentity_to_verts(const ExtrusionPath &extrusion_path, float print_z, const Point &copy, GLVolume &volume)
+{
+ Polyline polyline = extrusion_path.polyline;
+ polyline.remove_duplicate_points();
+ polyline.translate(copy);
+ Lines lines = polyline.lines();
+ std::vector<double> widths(lines.size(), extrusion_path.width);
+ std::vector<double> heights(lines.size(), extrusion_path.height);
+ thick_lines_to_verts(lines, widths, heights, false, print_z, volume);
+}
+
+// Fill in the qverts and tverts with quads and triangles for the extrusion_loop.
+void _3DScene::extrusionentity_to_verts(const ExtrusionLoop &extrusion_loop, float print_z, const Point &copy, GLVolume &volume)
+{
+ Lines lines;
+ std::vector<double> widths;
+ std::vector<double> heights;
+ for (const ExtrusionPath &extrusion_path : extrusion_loop.paths) {
+ Polyline polyline = extrusion_path.polyline;
+ polyline.remove_duplicate_points();
+ polyline.translate(copy);
+ Lines lines_this = polyline.lines();
+ append(lines, lines_this);
+ widths.insert(widths.end(), lines_this.size(), extrusion_path.width);
+ heights.insert(heights.end(), lines_this.size(), extrusion_path.height);
+ }
+ thick_lines_to_verts(lines, widths, heights, true, print_z, volume);
+}
+
+// Fill in the qverts and tverts with quads and triangles for the extrusion_multi_path.
+void _3DScene::extrusionentity_to_verts(const ExtrusionMultiPath &extrusion_multi_path, float print_z, const Point &copy, GLVolume &volume)
+{
+ Lines lines;
+ std::vector<double> widths;
+ std::vector<double> heights;
+ for (const ExtrusionPath &extrusion_path : extrusion_multi_path.paths) {
+ Polyline polyline = extrusion_path.polyline;
+ polyline.remove_duplicate_points();
+ polyline.translate(copy);
+ Lines lines_this = polyline.lines();
+ append(lines, lines_this);
+ widths.insert(widths.end(), lines_this.size(), extrusion_path.width);
+ heights.insert(heights.end(), lines_this.size(), extrusion_path.height);
+ }
+ thick_lines_to_verts(lines, widths, heights, false, print_z, volume);
+}
+
+void _3DScene::extrusionentity_to_verts(const ExtrusionEntityCollection &extrusion_entity_collection, float print_z, const Point &copy, GLVolume &volume)
+{
+ for (const ExtrusionEntity *extrusion_entity : extrusion_entity_collection.entities)
+ extrusionentity_to_verts(extrusion_entity, print_z, copy, volume);
+}
+
+void _3DScene::extrusionentity_to_verts(const ExtrusionEntity *extrusion_entity, float print_z, const Point &copy, GLVolume &volume)
+{
+ if (extrusion_entity != nullptr) {
+ auto *extrusion_path = dynamic_cast<const ExtrusionPath*>(extrusion_entity);
+ if (extrusion_path != nullptr)
+ extrusionentity_to_verts(*extrusion_path, print_z, copy, volume);
+ else {
+ auto *extrusion_loop = dynamic_cast<const ExtrusionLoop*>(extrusion_entity);
+ if (extrusion_loop != nullptr)
+ extrusionentity_to_verts(*extrusion_loop, print_z, copy, volume);
+ else {
+ auto *extrusion_multi_path = dynamic_cast<const ExtrusionMultiPath*>(extrusion_entity);
+ if (extrusion_multi_path != nullptr)
+ extrusionentity_to_verts(*extrusion_multi_path, print_z, copy, volume);
+ else {
+ auto *extrusion_entity_collection = dynamic_cast<const ExtrusionEntityCollection*>(extrusion_entity);
+ if (extrusion_entity_collection != nullptr)
+ extrusionentity_to_verts(*extrusion_entity_collection, print_z, copy, volume);
+ else {
+ throw std::runtime_error("Unexpected extrusion_entity type in to_verts()");
+ }
+ }
+ }
+ }
+ }
+}
+
+void _3DScene::polyline3_to_verts(const Polyline3& polyline, double width, double height, GLVolume& volume)
+{
+ Lines3 lines = polyline.lines();
+ std::vector<double> widths(lines.size(), width);
+ std::vector<double> heights(lines.size(), height);
+ thick_lines_to_verts(lines, widths, heights, false, volume);
+}
+
+void _3DScene::point3_to_verts(const Vec3crd& point, double width, double height, GLVolume& volume)
+{
+ thick_point_to_verts(point, width, height, volume);
+}
+
+GUI::GLCanvas3DManager _3DScene::s_canvas_mgr;
+
+void _3DScene::init_gl()
+{
+ s_canvas_mgr.init_gl();
+}
+
+std::string _3DScene::get_gl_info(bool format_as_html, bool extensions)
+{
+ return s_canvas_mgr.get_gl_info(format_as_html, extensions);
+}
+
+bool _3DScene::use_VBOs()
+{
+ return s_canvas_mgr.use_VBOs();
+}
+
+bool _3DScene::add_canvas(wxGLCanvas* canvas)
+{
+ return s_canvas_mgr.add(canvas);
+}
+
+bool _3DScene::remove_canvas(wxGLCanvas* canvas)
+{
+ return s_canvas_mgr.remove(canvas);
+}
+
+void _3DScene::remove_all_canvases()
+{
+ s_canvas_mgr.remove_all();
+}
+
+bool _3DScene::init(wxGLCanvas* canvas)
+{
+ return s_canvas_mgr.init(canvas);
+}
+
+void _3DScene::set_as_dirty(wxGLCanvas* canvas)
+{
+ s_canvas_mgr.set_as_dirty(canvas);
+}
+
+unsigned int _3DScene::get_volumes_count(wxGLCanvas* canvas)
+{
+ return s_canvas_mgr.get_volumes_count(canvas);
+}
+
+void _3DScene::reset_volumes(wxGLCanvas* canvas)
+{
+ s_canvas_mgr.reset_volumes(canvas);
+}
+
+void _3DScene::deselect_volumes(wxGLCanvas* canvas)
+{
+ s_canvas_mgr.deselect_volumes(canvas);
+}
+
+void _3DScene::select_volume(wxGLCanvas* canvas, unsigned int id)
+{
+ s_canvas_mgr.select_volume(canvas, id);
+}
+
+void _3DScene::update_volumes_selection(wxGLCanvas* canvas, const std::vector<int>& selections)
+{
+ s_canvas_mgr.update_volumes_selection(canvas, selections);
+}
+
+int _3DScene::check_volumes_outside_state(wxGLCanvas* canvas, const DynamicPrintConfig* config)
+{
+ return s_canvas_mgr.check_volumes_outside_state(canvas, config);
+}
+
+bool _3DScene::move_volume_up(wxGLCanvas* canvas, unsigned int id)
+{
+ return s_canvas_mgr.move_volume_up(canvas, id);
+}
+
+bool _3DScene::move_volume_down(wxGLCanvas* canvas, unsigned int id)
+{
+ return s_canvas_mgr.move_volume_down(canvas, id);
+}
+
+void _3DScene::set_objects_selections(wxGLCanvas* canvas, const std::vector<int>& selections)
+{
+ s_canvas_mgr.set_objects_selections(canvas, selections);
+}
+
+void _3DScene::set_config(wxGLCanvas* canvas, DynamicPrintConfig* config)
+{
+ s_canvas_mgr.set_config(canvas, config);
+}
+
+void _3DScene::set_print(wxGLCanvas* canvas, Print* print)
+{
+ s_canvas_mgr.set_print(canvas, print);
+}
+
+void _3DScene::set_model(wxGLCanvas* canvas, Model* model)
+{
+ s_canvas_mgr.set_model(canvas, model);
+}
+
+void _3DScene::set_bed_shape(wxGLCanvas* canvas, const Pointfs& shape)
+{
+ s_canvas_mgr.set_bed_shape(canvas, shape);
+}
+
+void _3DScene::set_auto_bed_shape(wxGLCanvas* canvas)
+{
+ s_canvas_mgr.set_auto_bed_shape(canvas);
+}
+
+BoundingBoxf3 _3DScene::get_volumes_bounding_box(wxGLCanvas* canvas)
+{
+ return s_canvas_mgr.get_volumes_bounding_box(canvas);
+}
+
+void _3DScene::set_axes_length(wxGLCanvas* canvas, float length)
+{
+ s_canvas_mgr.set_axes_length(canvas, length);
+}
+
+void _3DScene::set_cutting_plane(wxGLCanvas* canvas, float z, const ExPolygons& polygons)
+{
+ s_canvas_mgr.set_cutting_plane(canvas, z, polygons);
+}
+
+void _3DScene::set_color_by(wxGLCanvas* canvas, const std::string& value)
+{
+ s_canvas_mgr.set_color_by(canvas, value);
+}
+
+void _3DScene::set_select_by(wxGLCanvas* canvas, const std::string& value)
+{
+ s_canvas_mgr.set_select_by(canvas, value);
+}
+
+void _3DScene::set_drag_by(wxGLCanvas* canvas, const std::string& value)
+{
+ s_canvas_mgr.set_drag_by(canvas, value);
+}
+
+std::string _3DScene::get_select_by(wxGLCanvas* canvas)
+{
+ return s_canvas_mgr.get_select_by(canvas);
+}
+
+bool _3DScene::is_layers_editing_enabled(wxGLCanvas* canvas)
+{
+ return s_canvas_mgr.is_layers_editing_enabled(canvas);
+}
+
+bool _3DScene::is_layers_editing_allowed(wxGLCanvas* canvas)
+{
+ return s_canvas_mgr.is_layers_editing_allowed(canvas);
+}
+
+bool _3DScene::is_shader_enabled(wxGLCanvas* canvas)
+{
+ return s_canvas_mgr.is_shader_enabled(canvas);
+}
+
+bool _3DScene::is_reload_delayed(wxGLCanvas* canvas)
+{
+ return s_canvas_mgr.is_reload_delayed(canvas);
+}
+
+void _3DScene::enable_layers_editing(wxGLCanvas* canvas, bool enable)
+{
+ s_canvas_mgr.enable_layers_editing(canvas, enable);
+}
+
+void _3DScene::enable_warning_texture(wxGLCanvas* canvas, bool enable)
+{
+ s_canvas_mgr.enable_warning_texture(canvas, enable);
+}
+
+void _3DScene::enable_legend_texture(wxGLCanvas* canvas, bool enable)
+{
+ s_canvas_mgr.enable_legend_texture(canvas, enable);
+}
+
+void _3DScene::enable_picking(wxGLCanvas* canvas, bool enable)
+{
+ s_canvas_mgr.enable_picking(canvas, enable);
+}
+
+void _3DScene::enable_moving(wxGLCanvas* canvas, bool enable)
+{
+ s_canvas_mgr.enable_moving(canvas, enable);
+}
+
+void _3DScene::enable_gizmos(wxGLCanvas* canvas, bool enable)
+{
+ s_canvas_mgr.enable_gizmos(canvas, enable);
+}
+
+void _3DScene::enable_toolbar(wxGLCanvas* canvas, bool enable)
+{
+ s_canvas_mgr.enable_toolbar(canvas, enable);
+}
+
+void _3DScene::enable_shader(wxGLCanvas* canvas, bool enable)
+{
+ s_canvas_mgr.enable_shader(canvas, enable);
+}
+
+void _3DScene::enable_force_zoom_to_bed(wxGLCanvas* canvas, bool enable)
+{
+ s_canvas_mgr.enable_force_zoom_to_bed(canvas, enable);
+}
+
+void _3DScene::enable_dynamic_background(wxGLCanvas* canvas, bool enable)
+{
+ s_canvas_mgr.enable_dynamic_background(canvas, enable);
+}
+
+void _3DScene::allow_multisample(wxGLCanvas* canvas, bool allow)
+{
+ s_canvas_mgr.allow_multisample(canvas, allow);
+}
+
+void _3DScene::enable_toolbar_item(wxGLCanvas* canvas, const std::string& name, bool enable)
+{
+ s_canvas_mgr.enable_toolbar_item(canvas, name, enable);
+}
+
+bool _3DScene::is_toolbar_item_pressed(wxGLCanvas* canvas, const std::string& name)
+{
+ return s_canvas_mgr.is_toolbar_item_pressed(canvas, name);
+}
+
+void _3DScene::zoom_to_bed(wxGLCanvas* canvas)
+{
+ s_canvas_mgr.zoom_to_bed(canvas);
+}
+
+void _3DScene::zoom_to_volumes(wxGLCanvas* canvas)
+{
+ s_canvas_mgr.zoom_to_volumes(canvas);
+}
+
+void _3DScene::select_view(wxGLCanvas* canvas, const std::string& direction)
+{
+ s_canvas_mgr.select_view(canvas, direction);
+}
+
+void _3DScene::set_viewport_from_scene(wxGLCanvas* canvas, wxGLCanvas* other)
+{
+ s_canvas_mgr.set_viewport_from_scene(canvas, other);
+}
+
+void _3DScene::update_volumes_colors_by_extruder(wxGLCanvas* canvas)
+{
+ s_canvas_mgr.update_volumes_colors_by_extruder(canvas);
+}
+
+void _3DScene::update_gizmos_data(wxGLCanvas* canvas)
+{
+ s_canvas_mgr.update_gizmos_data(canvas);
+}
+
+void _3DScene::render(wxGLCanvas* canvas)
+{
+ s_canvas_mgr.render(canvas);
+}
+
+std::vector<double> _3DScene::get_current_print_zs(wxGLCanvas* canvas, bool active_only)
+{
+ return s_canvas_mgr.get_current_print_zs(canvas, active_only);
+}
+
+void _3DScene::set_toolpaths_range(wxGLCanvas* canvas, double low, double high)
+{
+ s_canvas_mgr.set_toolpaths_range(canvas, low, high);
+}
+
+void _3DScene::register_on_viewport_changed_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_viewport_changed_callback(canvas, callback);
+}
+
+void _3DScene::register_on_double_click_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_double_click_callback(canvas, callback);
+}
+
+void _3DScene::register_on_right_click_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_right_click_callback(canvas, callback);
+}
+
+void _3DScene::register_on_select_object_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_select_object_callback(canvas, callback);
+}
+
+void _3DScene::register_on_model_update_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_model_update_callback(canvas, callback);
+}
+
+void _3DScene::register_on_remove_object_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_remove_object_callback(canvas, callback);
+}
+
+void _3DScene::register_on_arrange_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_arrange_callback(canvas, callback);
+}
+
+void _3DScene::register_on_rotate_object_left_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_rotate_object_left_callback(canvas, callback);
+}
+
+void _3DScene::register_on_rotate_object_right_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_rotate_object_right_callback(canvas, callback);
+}
+
+void _3DScene::register_on_scale_object_uniformly_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_scale_object_uniformly_callback(canvas, callback);
+}
+
+void _3DScene::register_on_increase_objects_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_increase_objects_callback(canvas, callback);
+}
+
+void _3DScene::register_on_decrease_objects_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_decrease_objects_callback(canvas, callback);
+}
+
+void _3DScene::register_on_instance_moved_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_instance_moved_callback(canvas, callback);
+}
+
+void _3DScene::register_on_wipe_tower_moved_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_wipe_tower_moved_callback(canvas, callback);
+}
+
+void _3DScene::register_on_enable_action_buttons_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_enable_action_buttons_callback(canvas, callback);
+}
+
+void _3DScene::register_on_gizmo_scale_uniformly_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_gizmo_scale_uniformly_callback(canvas, callback);
+}
+
+void _3DScene::register_on_gizmo_rotate_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_gizmo_rotate_callback(canvas, callback);
+}
+
+void _3DScene::register_on_gizmo_flatten_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_gizmo_flatten_callback(canvas, callback);
+}
+
+void _3DScene::register_on_update_geometry_info_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_on_update_geometry_info_callback(canvas, callback);
+}
+
+void _3DScene::register_action_add_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_add_callback(canvas, callback);
+}
+
+void _3DScene::register_action_delete_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_delete_callback(canvas, callback);
+}
+
+void _3DScene::register_action_deleteall_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_deleteall_callback(canvas, callback);
+}
+
+void _3DScene::register_action_arrange_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_arrange_callback(canvas, callback);
+}
+
+void _3DScene::register_action_more_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_more_callback(canvas, callback);
+}
+
+void _3DScene::register_action_fewer_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_fewer_callback(canvas, callback);
+}
+
+void _3DScene::register_action_split_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_split_callback(canvas, callback);
+}
+
+void _3DScene::register_action_cut_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_cut_callback(canvas, callback);
+}
+
+void _3DScene::register_action_settings_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_settings_callback(canvas, callback);
+}
+
+void _3DScene::register_action_layersediting_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_layersediting_callback(canvas, callback);
+}
+
+void _3DScene::register_action_selectbyparts_callback(wxGLCanvas* canvas, void* callback)
+{
+ s_canvas_mgr.register_action_selectbyparts_callback(canvas, callback);
+}
+
+static inline int hex_digit_to_int(const char c)
+{
+ return
+ (c >= '0' && c <= '9') ? int(c - '0') :
+ (c >= 'A' && c <= 'F') ? int(c - 'A') + 10 :
+ (c >= 'a' && c <= 'f') ? int(c - 'a') + 10 : -1;
+}
+
+static inline std::vector<float> parse_colors(const std::vector<std::string> &scolors)
+{
+ std::vector<float> output(scolors.size() * 4, 1.f);
+ for (size_t i = 0; i < scolors.size(); ++ i) {
+ const std::string &scolor = scolors[i];
+ const char *c = scolor.data() + 1;
+ if (scolor.size() == 7 && scolor.front() == '#') {
+ for (size_t j = 0; j < 3; ++j) {
+ int digit1 = hex_digit_to_int(*c ++);
+ int digit2 = hex_digit_to_int(*c ++);
+ if (digit1 == -1 || digit2 == -1)
+ break;
+ output[i * 4 + j] = float(digit1 * 16 + digit2) / 255.f;
+ }
+ }
+ }
+ return output;
+}
+
+std::vector<int> _3DScene::load_object(wxGLCanvas* canvas, const ModelObject* model_object, int obj_idx, std::vector<int> instance_idxs)
+{
+ return s_canvas_mgr.load_object(canvas, model_object, obj_idx, instance_idxs);
+}
+
+std::vector<int> _3DScene::load_object(wxGLCanvas* canvas, const Model* model, int obj_idx)
+{
+ return s_canvas_mgr.load_object(canvas, model, obj_idx);
+}
+
+int _3DScene::get_first_volume_id(wxGLCanvas* canvas, int obj_idx)
+{
+ return s_canvas_mgr.get_first_volume_id(canvas, obj_idx);
+}
+
+int _3DScene::get_in_object_volume_id(wxGLCanvas* canvas, int scene_vol_idx)
+{
+ return s_canvas_mgr.get_in_object_volume_id(canvas, scene_vol_idx);
+}
+
+void _3DScene::reload_scene(wxGLCanvas* canvas, bool force)
+{
+ s_canvas_mgr.reload_scene(canvas, force);
+}
+
+void _3DScene::load_gcode_preview(wxGLCanvas* canvas, const GCodePreviewData* preview_data, const std::vector<std::string>& str_tool_colors)
+{
+ s_canvas_mgr.load_gcode_preview(canvas, preview_data, str_tool_colors);
+}
+
+void _3DScene::load_preview(wxGLCanvas* canvas, const std::vector<std::string>& str_tool_colors)
+{
+ s_canvas_mgr.load_preview(canvas, str_tool_colors);
+}
+
+void _3DScene::reset_legend_texture()
+{
+ s_canvas_mgr.reset_legend_texture();
+}
+
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp
new file mode 100644
index 000000000..f2d1c0786
--- /dev/null
+++ b/src/slic3r/GUI/3DScene.hpp
@@ -0,0 +1,603 @@
+#ifndef slic3r_3DScene_hpp_
+#define slic3r_3DScene_hpp_
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/Point.hpp"
+#include "../../libslic3r/Line.hpp"
+#include "../../libslic3r/TriangleMesh.hpp"
+#include "../../libslic3r/Utils.hpp"
+#include "../../libslic3r/Model.hpp"
+#include "../../slic3r/GUI/GLCanvas3DManager.hpp"
+
+class wxBitmap;
+class wxWindow;
+
+namespace Slic3r {
+
+class Print;
+class PrintObject;
+class Model;
+class ModelObject;
+class GCodePreviewData;
+class DynamicPrintConfig;
+class ExtrusionPath;
+class ExtrusionMultiPath;
+class ExtrusionLoop;
+class ExtrusionEntity;
+class ExtrusionEntityCollection;
+
+// A container for interleaved arrays of 3D vertices and normals,
+// possibly indexed by triangles and / or quads.
+class GLIndexedVertexArray {
+public:
+ GLIndexedVertexArray() :
+ vertices_and_normals_interleaved_VBO_id(0),
+ triangle_indices_VBO_id(0),
+ quad_indices_VBO_id(0)
+ { this->setup_sizes(); }
+ GLIndexedVertexArray(const GLIndexedVertexArray &rhs) :
+ vertices_and_normals_interleaved(rhs.vertices_and_normals_interleaved),
+ triangle_indices(rhs.triangle_indices),
+ quad_indices(rhs.quad_indices),
+ vertices_and_normals_interleaved_VBO_id(0),
+ triangle_indices_VBO_id(0),
+ quad_indices_VBO_id(0)
+ { this->setup_sizes(); }
+ GLIndexedVertexArray(GLIndexedVertexArray &&rhs) :
+ vertices_and_normals_interleaved(std::move(rhs.vertices_and_normals_interleaved)),
+ triangle_indices(std::move(rhs.triangle_indices)),
+ quad_indices(std::move(rhs.quad_indices)),
+ vertices_and_normals_interleaved_VBO_id(0),
+ triangle_indices_VBO_id(0),
+ quad_indices_VBO_id(0)
+ { this->setup_sizes(); }
+
+ GLIndexedVertexArray& operator=(const GLIndexedVertexArray &rhs)
+ {
+ assert(vertices_and_normals_interleaved_VBO_id == 0);
+ assert(triangle_indices_VBO_id == 0);
+ assert(triangle_indices_VBO_id == 0);
+ this->vertices_and_normals_interleaved = rhs.vertices_and_normals_interleaved;
+ this->triangle_indices = rhs.triangle_indices;
+ this->quad_indices = rhs.quad_indices;
+ this->setup_sizes();
+ return *this;
+ }
+
+ GLIndexedVertexArray& operator=(GLIndexedVertexArray &&rhs)
+ {
+ assert(vertices_and_normals_interleaved_VBO_id == 0);
+ assert(triangle_indices_VBO_id == 0);
+ assert(triangle_indices_VBO_id == 0);
+ this->vertices_and_normals_interleaved = std::move(rhs.vertices_and_normals_interleaved);
+ this->triangle_indices = std::move(rhs.triangle_indices);
+ this->quad_indices = std::move(rhs.quad_indices);
+ this->setup_sizes();
+ return *this;
+ }
+
+ // Vertices and their normals, interleaved to be used by void glInterleavedArrays(GL_N3F_V3F, 0, x)
+ std::vector<float> vertices_and_normals_interleaved;
+ std::vector<int> triangle_indices;
+ std::vector<int> quad_indices;
+
+ // When the geometry data is loaded into the graphics card as Vertex Buffer Objects,
+ // the above mentioned std::vectors are cleared and the following variables keep their original length.
+ size_t vertices_and_normals_interleaved_size;
+ size_t triangle_indices_size;
+ size_t quad_indices_size;
+
+ // IDs of the Vertex Array Objects, into which the geometry has been loaded.
+ // Zero if the VBOs are not used.
+ unsigned int vertices_and_normals_interleaved_VBO_id;
+ unsigned int triangle_indices_VBO_id;
+ unsigned int quad_indices_VBO_id;
+
+ void load_mesh_flat_shading(const TriangleMesh &mesh);
+ void load_mesh_full_shading(const TriangleMesh &mesh);
+
+ inline bool has_VBOs() const { return vertices_and_normals_interleaved_VBO_id != 0; }
+
+ inline void reserve(size_t sz) {
+ this->vertices_and_normals_interleaved.reserve(sz * 6);
+ this->triangle_indices.reserve(sz * 3);
+ this->quad_indices.reserve(sz * 4);
+ }
+
+ inline void push_geometry(float x, float y, float z, float nx, float ny, float nz) {
+ if (this->vertices_and_normals_interleaved.size() + 6 > this->vertices_and_normals_interleaved.capacity())
+ this->vertices_and_normals_interleaved.reserve(next_highest_power_of_2(this->vertices_and_normals_interleaved.size() + 6));
+ this->vertices_and_normals_interleaved.push_back(nx);
+ this->vertices_and_normals_interleaved.push_back(ny);
+ this->vertices_and_normals_interleaved.push_back(nz);
+ this->vertices_and_normals_interleaved.push_back(x);
+ this->vertices_and_normals_interleaved.push_back(y);
+ this->vertices_and_normals_interleaved.push_back(z);
+ };
+
+ inline void push_geometry(double x, double y, double z, double nx, double ny, double nz) {
+ push_geometry(float(x), float(y), float(z), float(nx), float(ny), float(nz));
+ }
+
+ inline void push_geometry(const Vec3d& p, const Vec3d& n) {
+ push_geometry(p(0), p(1), p(2), n(0), n(1), n(2));
+ }
+
+ inline void push_triangle(int idx1, int idx2, int idx3) {
+ if (this->triangle_indices.size() + 3 > this->vertices_and_normals_interleaved.capacity())
+ this->triangle_indices.reserve(next_highest_power_of_2(this->triangle_indices.size() + 3));
+ this->triangle_indices.push_back(idx1);
+ this->triangle_indices.push_back(idx2);
+ this->triangle_indices.push_back(idx3);
+ };
+
+ inline void push_quad(int idx1, int idx2, int idx3, int idx4) {
+ if (this->quad_indices.size() + 4 > this->vertices_and_normals_interleaved.capacity())
+ this->quad_indices.reserve(next_highest_power_of_2(this->quad_indices.size() + 4));
+ this->quad_indices.push_back(idx1);
+ this->quad_indices.push_back(idx2);
+ this->quad_indices.push_back(idx3);
+ this->quad_indices.push_back(idx4);
+ };
+
+ // Finalize the initialization of the geometry & indices,
+ // upload the geometry and indices to OpenGL VBO objects
+ // and shrink the allocated data, possibly relasing it if it has been loaded into the VBOs.
+ void finalize_geometry(bool use_VBOs);
+ // Release the geometry data, release OpenGL VBOs.
+ void release_geometry();
+ // Render either using an immediate mode, or the VBOs.
+ void render() const;
+ void render(const std::pair<size_t, size_t> &tverts_range, const std::pair<size_t, size_t> &qverts_range) const;
+
+ // Is there any geometry data stored?
+ bool empty() const { return vertices_and_normals_interleaved_size == 0; }
+
+ // Is this object indexed, or is it just a set of triangles?
+ bool indexed() const { return ! this->empty() && this->triangle_indices_size + this->quad_indices_size > 0; }
+
+ void clear() {
+ this->vertices_and_normals_interleaved.clear();
+ this->triangle_indices.clear();
+ this->quad_indices.clear();
+ this->setup_sizes();
+ }
+
+ // Shrink the internal storage to tighly fit the data stored.
+ void shrink_to_fit() {
+ if (! this->has_VBOs())
+ this->setup_sizes();
+ this->vertices_and_normals_interleaved.shrink_to_fit();
+ this->triangle_indices.shrink_to_fit();
+ this->quad_indices.shrink_to_fit();
+ }
+
+ BoundingBoxf3 bounding_box() const {
+ BoundingBoxf3 bbox;
+ if (! this->vertices_and_normals_interleaved.empty()) {
+ bbox.defined = true;
+ bbox.min(0) = bbox.max(0) = this->vertices_and_normals_interleaved[3];
+ bbox.min(1) = bbox.max(1) = this->vertices_and_normals_interleaved[4];
+ bbox.min(2) = bbox.max(2) = this->vertices_and_normals_interleaved[5];
+ for (size_t i = 9; i < this->vertices_and_normals_interleaved.size(); i += 6) {
+ const float *verts = this->vertices_and_normals_interleaved.data() + i;
+ bbox.min(0) = std::min<coordf_t>(bbox.min(0), verts[0]);
+ bbox.min(1) = std::min<coordf_t>(bbox.min(1), verts[1]);
+ bbox.min(2) = std::min<coordf_t>(bbox.min(2), verts[2]);
+ bbox.max(0) = std::max<coordf_t>(bbox.max(0), verts[0]);
+ bbox.max(1) = std::max<coordf_t>(bbox.max(1), verts[1]);
+ bbox.max(2) = std::max<coordf_t>(bbox.max(2), verts[2]);
+ }
+ }
+ return bbox;
+ }
+
+private:
+ inline void setup_sizes() {
+ vertices_and_normals_interleaved_size = this->vertices_and_normals_interleaved.size();
+ triangle_indices_size = this->triangle_indices.size();
+ quad_indices_size = this->quad_indices.size();
+ }
+};
+
+class LayersTexture
+{
+public:
+ LayersTexture() : width(0), height(0), levels(0), cells(0) {}
+
+ // Texture data
+ std::vector<char> data;
+ // Width of the texture, top level.
+ size_t width;
+ // Height of the texture, top level.
+ size_t height;
+ // For how many levels of detail is the data allocated?
+ size_t levels;
+ // Number of texture cells allocated for the height texture.
+ size_t cells;
+};
+
+class GLVolume {
+ struct LayerHeightTextureData
+ {
+ // ID of the layer height texture
+ unsigned int texture_id;
+ // ID of the shader used to render with the layer height texture
+ unsigned int shader_id;
+ // The print object to update when generating the layer height texture
+ const PrintObject* print_object;
+
+ float z_cursor_relative;
+ float edit_band_width;
+
+ LayerHeightTextureData() { reset(); }
+
+ void reset()
+ {
+ texture_id = 0;
+ shader_id = 0;
+ print_object = nullptr;
+ z_cursor_relative = 0.0f;
+ edit_band_width = 0.0f;
+ }
+
+ bool can_use() const { return (texture_id > 0) && (shader_id > 0) && (print_object != nullptr); }
+ };
+
+public:
+ static const float SELECTED_COLOR[4];
+ static const float HOVER_COLOR[4];
+ static const float OUTSIDE_COLOR[4];
+ static const float SELECTED_OUTSIDE_COLOR[4];
+
+ GLVolume(float r = 1.f, float g = 1.f, float b = 1.f, float a = 1.f);
+ GLVolume(const float *rgba) : GLVolume(rgba[0], rgba[1], rgba[2], rgba[3]) {}
+
+private:
+ // Offset of the volume to be rendered.
+ Vec3d m_offset;
+ // Rotation around Z axis of the volume to be rendered.
+ double m_rotation;
+ // Scale factor of the volume to be rendered.
+ double m_scaling_factor;
+ // World matrix of the volume to be rendered.
+ mutable Transform3f m_world_matrix;
+ // Whether or not is needed to recalculate the world matrix.
+ mutable bool m_world_matrix_dirty;
+ // Bounding box of this volume, in unscaled coordinates.
+ mutable BoundingBoxf3 m_transformed_bounding_box;
+ // Whether or not is needed to recalculate the transformed bounding box.
+ mutable bool m_transformed_bounding_box_dirty;
+ // Pointer to convex hull of the original mesh, if any.
+ const TriangleMesh* m_convex_hull;
+ // Bounding box of this volume, in unscaled coordinates.
+ mutable BoundingBoxf3 m_transformed_convex_hull_bounding_box;
+ // Whether or not is needed to recalculate the transformed convex hull bounding box.
+ mutable bool m_transformed_convex_hull_bounding_box_dirty;
+
+public:
+
+ // Bounding box of this volume, in unscaled coordinates.
+ BoundingBoxf3 bounding_box;
+ // Color of the triangles / quads held by this volume.
+ float color[4];
+ // Color used to render this volume.
+ float render_color[4];
+ // An ID containing the object ID, volume ID and instance ID.
+ int composite_id;
+ // An ID for group selection. It may be the same for all meshes of all object instances, or for just a single object instance.
+ int select_group_id;
+ // An ID for group dragging. It may be the same for all meshes of all object instances, or for just a single object instance.
+ int drag_group_id;
+ // An ID containing the extruder ID (used to select color).
+ int extruder_id;
+ // Is this object selected?
+ bool selected;
+ // Whether or not this volume is active for rendering
+ bool is_active;
+ // Whether or not to use this volume when applying zoom_to_volumes()
+ bool zoom_to_volumes;
+ // Wheter or not this volume is enabled for outside print volume detection in shader.
+ bool shader_outside_printer_detection_enabled;
+ // Wheter or not this volume is outside print volume.
+ bool is_outside;
+ // Boolean: Is mouse over this object?
+ bool hover;
+ // Wheter or not this volume has been generated from a modifier
+ bool is_modifier;
+ // Wheter or not this volume has been generated from the wipe tower
+ bool is_wipe_tower;
+ // Wheter or not this volume has been generated from an extrusion path
+ bool is_extrusion_path;
+
+ // Interleaved triangles & normals with indexed triangles & quads.
+ GLIndexedVertexArray indexed_vertex_array;
+ // Ranges of triangle and quad indices to be rendered.
+ std::pair<size_t, size_t> tverts_range;
+ std::pair<size_t, size_t> qverts_range;
+
+ // If the qverts or tverts contain thick extrusions, then offsets keeps pointers of the starts
+ // of the extrusions per layer.
+ std::vector<coordf_t> print_zs;
+ // Offset into qverts & tverts, or offsets into indices stored into an OpenGL name_index_buffer.
+ std::vector<size_t> offsets;
+
+ void set_render_color(float r, float g, float b, float a);
+ void set_render_color(const float* rgba, unsigned int size);
+ // Sets render color in dependence of current state
+ void set_render_color();
+
+ double get_rotation();
+ void set_rotation(double rotation);
+
+ const Vec3d& get_offset() const;
+ void set_offset(const Vec3d& offset);
+
+ void set_scaling_factor(double factor);
+
+ void set_convex_hull(const TriangleMesh& convex_hull);
+
+ void set_select_group_id(const std::string& select_by);
+ void set_drag_group_id(const std::string& drag_by);
+
+ int object_idx() const { return this->composite_id / 1000000; }
+ int volume_idx() const { return (this->composite_id / 1000) % 1000; }
+ int instance_idx() const { return this->composite_id % 1000; }
+
+ const Transform3f& world_matrix() const;
+ const BoundingBoxf3& transformed_bounding_box() const;
+ const BoundingBoxf3& transformed_convex_hull_bounding_box() const;
+
+ bool empty() const { return this->indexed_vertex_array.empty(); }
+ bool indexed() const { return this->indexed_vertex_array.indexed(); }
+
+ void set_range(coordf_t low, coordf_t high);
+ void render() const;
+ void render_using_layer_height() const;
+ void render_VBOs(int color_id, int detection_id, int worldmatrix_id) const;
+ void render_legacy() const;
+
+ void finalize_geometry(bool use_VBOs) { this->indexed_vertex_array.finalize_geometry(use_VBOs); }
+ void release_geometry() { this->indexed_vertex_array.release_geometry(); }
+
+ /************************************************ Layer height texture ****************************************************/
+ std::shared_ptr<LayersTexture> layer_height_texture;
+ // Data to render this volume using the layer height texture
+ LayerHeightTextureData layer_height_texture_data;
+
+ bool has_layer_height_texture() const
+ { return this->layer_height_texture.get() != nullptr; }
+ size_t layer_height_texture_width() const
+ { return (this->layer_height_texture.get() == nullptr) ? 0 : this->layer_height_texture->width; }
+ size_t layer_height_texture_height() const
+ { return (this->layer_height_texture.get() == nullptr) ? 0 : this->layer_height_texture->height; }
+ size_t layer_height_texture_cells() const
+ { return (this->layer_height_texture.get() == nullptr) ? 0 : this->layer_height_texture->cells; }
+ void* layer_height_texture_data_ptr_level0() const {
+ return (layer_height_texture.get() == nullptr) ? 0 :
+ (void*)layer_height_texture->data.data();
+ }
+ void* layer_height_texture_data_ptr_level1() const {
+ return (layer_height_texture.get() == nullptr) ? 0 :
+ (void*)(layer_height_texture->data.data() + layer_height_texture->width * layer_height_texture->height * 4);
+ }
+ double layer_height_texture_z_to_row_id() const;
+ void generate_layer_height_texture(const PrintObject *print_object, bool force);
+
+ void set_layer_height_texture_data(unsigned int texture_id, unsigned int shader_id, const PrintObject* print_object, float z_cursor_relative, float edit_band_width)
+ {
+ layer_height_texture_data.texture_id = texture_id;
+ layer_height_texture_data.shader_id = shader_id;
+ layer_height_texture_data.print_object = print_object;
+ layer_height_texture_data.z_cursor_relative = z_cursor_relative;
+ layer_height_texture_data.edit_band_width = edit_band_width;
+ }
+
+ void reset_layer_height_texture_data() { layer_height_texture_data.reset(); }
+};
+
+class GLVolumeCollection
+{
+ // min and max vertex of the print box volume
+ float print_box_min[3];
+ float print_box_max[3];
+
+public:
+ std::vector<GLVolume*> volumes;
+
+ GLVolumeCollection() {};
+ ~GLVolumeCollection() { clear(); };
+
+ std::vector<int> load_object(
+ const ModelObject *model_object,
+ int obj_idx,
+ const std::vector<int> &instance_idxs,
+ const std::string &color_by,
+ const std::string &select_by,
+ const std::string &drag_by,
+ bool use_VBOs);
+
+ int load_wipe_tower_preview(
+ int obj_idx, float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool use_VBOs, bool size_unknown, float brim_width);
+
+ // Render the volumes by OpenGL.
+ void render_VBOs() const;
+ void render_legacy() const;
+
+ // Finalize the initialization of the geometry & indices,
+ // upload the geometry and indices to OpenGL VBO objects
+ // and shrink the allocated data, possibly relasing it if it has been loaded into the VBOs.
+ void finalize_geometry(bool use_VBOs) { for (auto *v : volumes) v->finalize_geometry(use_VBOs); }
+ // Release the geometry data assigned to the volumes.
+ // If OpenGL VBOs were allocated, an OpenGL context has to be active to release them.
+ void release_geometry() { for (auto *v : volumes) v->release_geometry(); }
+ // Clear the geometry
+ void clear() { for (auto *v : volumes) delete v; volumes.clear(); }
+
+ bool empty() const { return volumes.empty(); }
+ void set_range(double low, double high) { for (GLVolume *vol : this->volumes) vol->set_range(low, high); }
+
+ void set_print_box(float min_x, float min_y, float min_z, float max_x, float max_y, float max_z) {
+ print_box_min[0] = min_x; print_box_min[1] = min_y; print_box_min[2] = min_z;
+ print_box_max[0] = max_x; print_box_max[1] = max_y; print_box_max[2] = max_z;
+ }
+
+ // returns true if all the volumes are completely contained in the print volume
+ // returns the containment state in the given out_state, if non-null
+ bool check_outside_state(const DynamicPrintConfig* config, ModelInstance::EPrintVolumeState* out_state);
+ void reset_outside_state();
+
+ void update_colors_by_extruder(const DynamicPrintConfig* config);
+
+ void set_select_by(const std::string& select_by);
+ void set_drag_by(const std::string& drag_by);
+
+ // Returns a vector containing the sorted list of all the print_zs of the volumes contained in this collection
+ std::vector<double> get_current_print_zs(bool active_only) const;
+
+private:
+ GLVolumeCollection(const GLVolumeCollection &other);
+ GLVolumeCollection& operator=(const GLVolumeCollection &);
+};
+
+class _3DScene
+{
+ static GUI::GLCanvas3DManager s_canvas_mgr;
+
+public:
+ static void init_gl();
+ static std::string get_gl_info(bool format_as_html, bool extensions);
+ static bool use_VBOs();
+
+ static bool add_canvas(wxGLCanvas* canvas);
+ static bool remove_canvas(wxGLCanvas* canvas);
+ static void remove_all_canvases();
+
+ static bool init(wxGLCanvas* canvas);
+
+ static void set_as_dirty(wxGLCanvas* canvas);
+
+ static unsigned int get_volumes_count(wxGLCanvas* canvas);
+ static void reset_volumes(wxGLCanvas* canvas);
+ static void deselect_volumes(wxGLCanvas* canvas);
+ static void select_volume(wxGLCanvas* canvas, unsigned int id);
+ static void update_volumes_selection(wxGLCanvas* canvas, const std::vector<int>& selections);
+ static int check_volumes_outside_state(wxGLCanvas* canvas, const DynamicPrintConfig* config);
+ static bool move_volume_up(wxGLCanvas* canvas, unsigned int id);
+ static bool move_volume_down(wxGLCanvas* canvas, unsigned int id);
+
+ static void set_objects_selections(wxGLCanvas* canvas, const std::vector<int>& selections);
+
+ static void set_config(wxGLCanvas* canvas, DynamicPrintConfig* config);
+ static void set_print(wxGLCanvas* canvas, Print* print);
+ static void set_model(wxGLCanvas* canvas, Model* model);
+
+ static void set_bed_shape(wxGLCanvas* canvas, const Pointfs& shape);
+ static void set_auto_bed_shape(wxGLCanvas* canvas);
+
+ static BoundingBoxf3 get_volumes_bounding_box(wxGLCanvas* canvas);
+
+ static void set_axes_length(wxGLCanvas* canvas, float length);
+
+ static void set_cutting_plane(wxGLCanvas* canvas, float z, const ExPolygons& polygons);
+
+ static void set_color_by(wxGLCanvas* canvas, const std::string& value);
+ static void set_select_by(wxGLCanvas* canvas, const std::string& value);
+ static void set_drag_by(wxGLCanvas* canvas, const std::string& value);
+
+ static std::string get_select_by(wxGLCanvas* canvas);
+
+ static bool is_layers_editing_enabled(wxGLCanvas* canvas);
+ static bool is_layers_editing_allowed(wxGLCanvas* canvas);
+ static bool is_shader_enabled(wxGLCanvas* canvas);
+
+ static bool is_reload_delayed(wxGLCanvas* canvas);
+
+ static void enable_layers_editing(wxGLCanvas* canvas, bool enable);
+ static void enable_warning_texture(wxGLCanvas* canvas, bool enable);
+ static void enable_legend_texture(wxGLCanvas* canvas, bool enable);
+ static void enable_picking(wxGLCanvas* canvas, bool enable);
+ static void enable_moving(wxGLCanvas* canvas, bool enable);
+ static void enable_gizmos(wxGLCanvas* canvas, bool enable);
+ static void enable_toolbar(wxGLCanvas* canvas, bool enable);
+ static void enable_shader(wxGLCanvas* canvas, bool enable);
+ static void enable_force_zoom_to_bed(wxGLCanvas* canvas, bool enable);
+ static void enable_dynamic_background(wxGLCanvas* canvas, bool enable);
+ static void allow_multisample(wxGLCanvas* canvas, bool allow);
+
+ static void enable_toolbar_item(wxGLCanvas* canvas, const std::string& name, bool enable);
+ static bool is_toolbar_item_pressed(wxGLCanvas* canvas, const std::string& name);
+
+ static void zoom_to_bed(wxGLCanvas* canvas);
+ static void zoom_to_volumes(wxGLCanvas* canvas);
+ static void select_view(wxGLCanvas* canvas, const std::string& direction);
+ static void set_viewport_from_scene(wxGLCanvas* canvas, wxGLCanvas* other);
+
+ static void update_volumes_colors_by_extruder(wxGLCanvas* canvas);
+ static void update_gizmos_data(wxGLCanvas* canvas);
+
+ static void render(wxGLCanvas* canvas);
+
+ static std::vector<double> get_current_print_zs(wxGLCanvas* canvas, bool active_only);
+ static void set_toolpaths_range(wxGLCanvas* canvas, double low, double high);
+
+ static void register_on_viewport_changed_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_double_click_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_right_click_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_select_object_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_model_update_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_remove_object_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_arrange_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_rotate_object_left_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_rotate_object_right_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_scale_object_uniformly_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_increase_objects_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_decrease_objects_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_instance_moved_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_wipe_tower_moved_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_enable_action_buttons_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_gizmo_scale_uniformly_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_gizmo_rotate_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_gizmo_flatten_callback(wxGLCanvas* canvas, void* callback);
+ static void register_on_update_geometry_info_callback(wxGLCanvas* canvas, void* callback);
+
+ static void register_action_add_callback(wxGLCanvas* canvas, void* callback);
+ static void register_action_delete_callback(wxGLCanvas* canvas, void* callback);
+ static void register_action_deleteall_callback(wxGLCanvas* canvas, void* callback);
+ static void register_action_arrange_callback(wxGLCanvas* canvas, void* callback);
+ static void register_action_more_callback(wxGLCanvas* canvas, void* callback);
+ static void register_action_fewer_callback(wxGLCanvas* canvas, void* callback);
+ static void register_action_split_callback(wxGLCanvas* canvas, void* callback);
+ static void register_action_cut_callback(wxGLCanvas* canvas, void* callback);
+ static void register_action_settings_callback(wxGLCanvas* canvas, void* callback);
+ static void register_action_layersediting_callback(wxGLCanvas* canvas, void* callback);
+ static void register_action_selectbyparts_callback(wxGLCanvas* canvas, void* callback);
+
+ static std::vector<int> load_object(wxGLCanvas* canvas, const ModelObject* model_object, int obj_idx, std::vector<int> instance_idxs);
+ static std::vector<int> load_object(wxGLCanvas* canvas, const Model* model, int obj_idx);
+
+ static int get_first_volume_id(wxGLCanvas* canvas, int obj_idx);
+ static int get_in_object_volume_id(wxGLCanvas* canvas, int scene_vol_idx);
+
+ static void reload_scene(wxGLCanvas* canvas, bool force);
+
+ static void load_gcode_preview(wxGLCanvas* canvas, const GCodePreviewData* preview_data, const std::vector<std::string>& str_tool_colors);
+ static void load_preview(wxGLCanvas* canvas, const std::vector<std::string>& str_tool_colors);
+
+ static void reset_legend_texture();
+
+ static void thick_lines_to_verts(const Lines& lines, const std::vector<double>& widths, const std::vector<double>& heights, bool closed, double top_z, GLVolume& volume);
+ static void thick_lines_to_verts(const Lines3& lines, const std::vector<double>& widths, const std::vector<double>& heights, bool closed, GLVolume& volume);
+ static void extrusionentity_to_verts(const ExtrusionPath& extrusion_path, float print_z, GLVolume& volume);
+ static void extrusionentity_to_verts(const ExtrusionPath& extrusion_path, float print_z, const Point& copy, GLVolume& volume);
+ static void extrusionentity_to_verts(const ExtrusionLoop& extrusion_loop, float print_z, const Point& copy, GLVolume& volume);
+ static void extrusionentity_to_verts(const ExtrusionMultiPath& extrusion_multi_path, float print_z, const Point& copy, GLVolume& volume);
+ static void extrusionentity_to_verts(const ExtrusionEntityCollection& extrusion_entity_collection, float print_z, const Point& copy, GLVolume& volume);
+ static void extrusionentity_to_verts(const ExtrusionEntity* extrusion_entity, float print_z, const Point& copy, GLVolume& volume);
+ static void polyline3_to_verts(const Polyline3& polyline, double width, double height, GLVolume& volume);
+ static void point3_to_verts(const Vec3crd& point, double width, double height, GLVolume& volume);
+};
+
+}
+
+#endif
diff --git a/src/slic3r/GUI/AboutDialog.cpp b/src/slic3r/GUI/AboutDialog.cpp
new file mode 100644
index 000000000..0fed8d175
--- /dev/null
+++ b/src/slic3r/GUI/AboutDialog.cpp
@@ -0,0 +1,136 @@
+#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, wxDefaultSize, wxCAPTION)
+{
+ wxColour bgr_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
+ SetBackgroundColour(bgr_clr);
+ wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL);
+
+ auto main_sizer = new wxBoxSizer(wxVERTICAL);
+ main_sizer->Add(hsizer, 0, wxEXPAND | wxALL, 20);
+
+ // logo
+ wxBitmap logo_bmp = wxBitmap(from_u8(Slic3r::var("Slic3r_192px.png")), wxBITMAP_TYPE_PNG);
+ auto *logo = new wxStaticBitmap(this, wxID_ANY, std::move(logo_bmp));
+ hsizer->Add(logo, 1, wxALIGN_CENTRE_VERTICAL | wxEXPAND | wxTOP | wxBOTTOM, 35);
+
+ wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL);
+#ifdef __WXMSW__
+ int proportion = 2;
+#else
+ int proportion = 3;
+#endif
+ hsizer->Add(vsizer, proportion, wxEXPAND|wxLEFT, 20);
+
+ // 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, 10);
+ }
+
+ // 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__
+ 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_AUTO/*NEVER*/);
+ {
+ wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
+ const auto text_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
+ auto text_clr_str = wxString::Format(wxT("#%02X%02X%02X"), text_clr.Red(), text_clr.Green(), text_clr.Blue());
+ auto bgr_clr_str = wxString::Format(wxT("#%02X%02X%02X"), bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue());
+
+ const int fs = font.GetPointSize()-1;
+ int size[] = {fs,fs,fs,fs,fs,fs,fs};
+ html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
+ html->SetBorders(2);
+ const auto text = wxString::Format(
+ "<html>"
+ "<body bgcolor= %s link= %s>"
+ "<font color=%s>"
+ "Copyright &copy; 2016-2018 Prusa Research. <br />"
+ "Copyright &copy; 2011-2017 Alessandro Ranellucci. <br />"
+ "<a href=\"http://slic3r.org/\">Slic3r</a> is licensed under the "
+ "<a href=\"http://www.gnu.org/licenses/agpl-3.0.html\">GNU Affero General Public License, version 3</a>."
+ "<br /><br />"
+ "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. <br />"
+ "Slic3r logo designed by Corey Daniels, <a href=\"http://www.famfamfam.com/lab/icons/silk/\">Silk Icon Set</a> designed by Mark James. "
+ "</font>"
+ "</body>"
+ "</html>", bgr_clr_str, text_clr_str, text_clr_str);
+ html->SetPage(text);
+ vsizer->Add(html, 1, wxEXPAND | wxBOTTOM, 10);
+ 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);
+
+ SetSizer(main_sizer);
+ main_sizer->SetSizeHints(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/src/slic3r/GUI/AboutDialog.hpp b/src/slic3r/GUI/AboutDialog.hpp
new file mode 100644
index 000000000..01f7564c5
--- /dev/null
+++ b/src/slic3r/GUI/AboutDialog.hpp
@@ -0,0 +1,36 @@
+#ifndef slic3r_GUI_AboutDialog_hpp_
+#define slic3r_GUI_AboutDialog_hpp_
+
+#include "GUI.hpp"
+
+#include <wx/wx.h>
+#include <wx/intl.h>
+#include <wx/html/htmlwin.h>
+
+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/src/slic3r/GUI/AppConfig.cpp b/src/slic3r/GUI/AppConfig.cpp
new file mode 100644
index 000000000..d7307cc32
--- /dev/null
+++ b/src/slic3r/GUI/AppConfig.cpp
@@ -0,0 +1,266 @@
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/Utils.hpp"
+#include "AppConfig.hpp"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <utility>
+#include <assert.h>
+#include <vector>
+#include <stdexcept>
+
+#include <boost/filesystem.hpp>
+#include <boost/nowide/cenv.hpp>
+#include <boost/nowide/fstream.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+
+namespace Slic3r {
+
+static const std::string VENDOR_PREFIX = "vendor:";
+static const std::string MODEL_PREFIX = "model:";
+static const std::string VERSION_CHECK_URL = "https://raw.githubusercontent.com/prusa3d/Slic3r-settings/master/live/Slic3rPE.version";
+
+void AppConfig::reset()
+{
+ m_storage.clear();
+ set_defaults();
+};
+
+// Override missing or keys with their defaults.
+void AppConfig::set_defaults()
+{
+ // Reset the empty fields to defaults.
+ if (get("autocenter").empty())
+ set("autocenter", "0");
+ // Disable background processing by default as it is not stable.
+ if (get("background_processing").empty())
+ set("background_processing", "0");
+ // If set, the "Controller" tab for the control of the printer over serial line and the serial port settings are hidden.
+ // By default, Prusa has the controller hidden.
+ if (get("no_controller").empty())
+ set("no_controller", "1");
+ // If set, the "- default -" selections of print/filament/printer are suppressed, if there is a valid preset available.
+ if (get("no_defaults").empty())
+ set("no_defaults", "1");
+ if (get("show_incompatible_presets").empty())
+ set("show_incompatible_presets", "0");
+
+ 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())
+ set("use_legacy_opengl", "0");
+
+ if (get("remember_output_path").empty())
+ set("remember_output_path", "1");
+
+ // Remove legacy window positions/sizes
+ erase("", "main_frame_maximized");
+ erase("", "main_frame_pos");
+ erase("", "main_frame_size");
+ erase("", "object_settings_maximized");
+ erase("", "object_settings_pos");
+ erase("", "object_settings_size");
+}
+
+void AppConfig::load()
+{
+ // 1) Read the complete config file into a boost::property_tree.
+ namespace pt = boost::property_tree;
+ pt::ptree tree;
+ boost::nowide::ifstream ifs(AppConfig::config_path());
+ pt::read_ini(ifs, tree);
+
+ // 2) Parse the property_tree, extract the sections and key / value pairs.
+ for (const auto &section : tree) {
+ if (section.second.empty()) {
+ // This may be a top level (no section) entry, or an empty section.
+ std::string data = section.second.data();
+ 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<std::string> 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<std::string, std::string> &storage = m_storage[section.first];
+ for (auto &kvp : section.second)
+ storage[kvp.first] = kvp.second.data();
+ }
+ }
+
+ // Figure out if datadir has legacy presets
+ auto ini_ver = Semver::parse(get("version"));
+ m_legacy_datadir = false;
+ if (ini_ver) {
+ m_orig_version = *ini_ver;
+ // Make 1.40.0 alphas compare well
+ ini_ver->set_metadata(boost::none);
+ ini_ver->set_prerelease(boost::none);
+ m_legacy_datadir = ini_ver < Semver(1, 40, 0);
+ }
+
+ // Override missing or keys with their defaults.
+ this->set_defaults();
+ m_dirty = false;
+}
+
+void AppConfig::save()
+{
+ boost::nowide::ofstream c;
+ c.open(AppConfig::config_path(), std::ios::out | std::ios::trunc);
+ c << "# " << Slic3r::header_slic3r_generated() << std::endl;
+ // Make sure the "no" category is written first.
+ for (const std::pair<std::string, std::string> &kvp : m_storage[""])
+ c << kvp.first << " = " << kvp.second << std::endl;
+ // Write the other categories.
+ for (const auto category : m_storage) {
+ if (category.first.empty())
+ continue;
+ c << std::endl << "[" << category.first << "]" << std::endl;
+ for (const std::pair<std::string, std::string> &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<std::string> 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
+{
+ 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;
+}
+
+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");
+ if (it != m_storage.end()) {
+ {
+ const auto it2 = it->second.find("skein_directory");
+ if (it2 != it->second.end() && ! it2->second.empty())
+ return it2->second;
+ }
+ {
+ const auto it2 = it->second.find("config_directory");
+ if (it2 != it->second.end() && ! it2->second.empty())
+ return it2->second;
+ }
+ }
+ return std::string();
+}
+
+void AppConfig::update_config_dir(const std::string &dir)
+{
+ this->set("recent", "config_directory", dir);
+}
+
+void AppConfig::update_skein_dir(const std::string &dir)
+{
+ this->set("recent", "skein_directory", dir);
+}
+
+std::string AppConfig::get_last_output_dir(const std::string &alt) const
+{
+ const auto it = m_storage.find("");
+ if (it != m_storage.end()) {
+ const auto it2 = it->second.find("last_output_path");
+ const auto it3 = it->second.find("remember_output_path");
+ if (it2 != it->second.end() && it3 != it->second.end() && ! it2->second.empty() && it3->second == "1")
+ return it2->second;
+ }
+ return alt;
+}
+
+void AppConfig::update_last_output_dir(const std::string &dir)
+{
+ this->set("", "last_output_path", dir);
+}
+
+void AppConfig::reset_selections()
+{
+ auto it = m_storage.find("presets");
+ if (it != m_storage.end()) {
+ it->second.erase("print");
+ it->second.erase("filament");
+ it->second.erase("sla_material");
+ it->second.erase("printer");
+ m_dirty = true;
+ }
+}
+
+std::string AppConfig::config_path()
+{
+ return (boost::filesystem::path(Slic3r::data_dir()) / "slic3r.ini").make_preferred().string();
+}
+
+std::string AppConfig::version_check_url() const
+{
+ auto from_settings = get("version_check_url");
+ return from_settings.empty() ? VERSION_CHECK_URL : from_settings;
+}
+
+bool AppConfig::exists()
+{
+ return boost::filesystem::exists(AppConfig::config_path());
+}
+
+}; // namespace Slic3r
diff --git a/src/slic3r/GUI/AppConfig.hpp b/src/slic3r/GUI/AppConfig.hpp
new file mode 100644
index 000000000..5af635a12
--- /dev/null
+++ b/src/slic3r/GUI/AppConfig.hpp
@@ -0,0 +1,140 @@
+#ifndef slic3r_AppConfig_hpp_
+#define slic3r_AppConfig_hpp_
+
+#include <set>
+#include <map>
+#include <string>
+
+#include "libslic3r/Config.hpp"
+#include "slic3r/Utils/Semver.hpp"
+
+namespace Slic3r {
+
+class AppConfig
+{
+public:
+ AppConfig() :
+ m_dirty(false),
+ m_orig_version(Semver::invalid()),
+ m_legacy_datadir(false)
+ {
+ this->reset();
+ }
+
+ // Clear and reset to defaults.
+ void reset();
+ // Override missing or keys with their defaults.
+ void set_defaults();
+
+ // Load the slic3r.ini from a user profile directory (or a datadir, if configured).
+ void load();
+ // Store the slic3r.ini into a user profile directory (or a datadir, if configured).
+ void save();
+
+ // Does this config need to be saved?
+ bool dirty() const { return m_dirty; }
+
+ // Const accessor, it will return false if a section or a key does not exist.
+ bool get(const std::string &section, const std::string &key, std::string &value) const
+ {
+ value.clear();
+ auto it = m_storage.find(section);
+ if (it == m_storage.end())
+ return false;
+ auto it2 = it->second.find(key);
+ if (it2 == it->second.end())
+ return false;
+ value = it2->second;
+ return true;
+ }
+ std::string get(const std::string &section, const std::string &key) const
+ { std::string value; this->get(section, key, value); return value; }
+ std::string get(const std::string &key) const
+ { std::string value; this->get("", key, value); return value; }
+ void set(const std::string &section, const std::string &key, const std::string &value)
+ {
+ std::string &old = m_storage[section][key];
+ if (old != value) {
+ old = value;
+ m_dirty = true;
+ }
+ }
+ void set(const std::string &key, const std::string &value)
+ { this->set("", key, value); }
+ bool has(const std::string &section, const std::string &key) const
+ {
+ auto it = m_storage.find(section);
+ if (it == m_storage.end())
+ return false;
+ auto it2 = it->second.find(key);
+ return it2 != it->second.end() && ! it2->second.empty();
+ }
+ bool has(const std::string &key) const
+ { return this->has("", key); }
+
+ void erase(const std::string &section, const std::string &key)
+ {
+ auto it = m_storage.find(section);
+ if (it != m_storage.end()) {
+ it->second.erase(key);
+ }
+ }
+
+ void clear_section(const std::string &section)
+ { m_storage[section].clear(); }
+
+ typedef std::map<std::string, std::map<std::string, std::set<std::string>>> 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 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;
+ void update_config_dir(const std::string &dir);
+ void update_skein_dir(const std::string &dir);
+
+ std::string get_last_output_dir(const std::string &alt) const;
+ void update_last_output_dir(const std::string &dir);
+
+ // reset the current print / filament / printer selections, so that
+ // the PresetBundle::load_selections(const AppConfig &config) call will select
+ // the first non-default preset when called.
+ void reset_selections();
+
+ // Get the default config path from Slic3r::data_dir().
+ static std::string config_path();
+
+ // Returns true if the user's data directory comes from before Slic3r 1.40.0 (no updating)
+ bool legacy_datadir() const { return m_legacy_datadir; }
+ void set_legacy_datadir(bool value) { m_legacy_datadir = value; }
+
+ // Get the Slic3r version check url.
+ // This returns a hardcoded string unless it is overriden by "version_check_url" in the ini file.
+ std::string version_check_url() const;
+
+ // Returns the original Slic3r version found in the ini file before it was overwritten
+ // by the current version
+ Semver orig_version() const { return m_orig_version; }
+
+ // Does the config file exist?
+ static bool exists();
+
+private:
+ // Map of section, name -> value
+ std::map<std::string, std::map<std::string, std::string>> m_storage;
+ // Map of enabled vendors / models / variants
+ VendorMap m_vendors;
+ // Has any value been modified since the config.ini has been last saved or loaded?
+ bool m_dirty;
+ // Original version found in the ini file before it was overwritten
+ Semver m_orig_version;
+ // Whether the existing version is before system profiles & configuration updating
+ bool m_legacy_datadir;
+};
+
+}; // namespace Slic3r
+
+#endif /* slic3r_AppConfig_hpp_ */
diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp
new file mode 100644
index 000000000..99997e390
--- /dev/null
+++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp
@@ -0,0 +1,167 @@
+#include "BackgroundSlicingProcess.hpp"
+#include "GUI.hpp"
+
+#include <wx/event.h>
+#include <wx/panel.h>
+#include <wx/stdpaths.h>
+
+// Print now includes tbb, and tbb includes Windows. This breaks compilation of wxWidgets if included before wx.
+#include "../../libslic3r/Print.hpp"
+#include "../../libslic3r/Utils.hpp"
+#include "../../libslic3r/GCode/PostProcessor.hpp"
+
+//#undef NDEBUG
+#include <cassert>
+#include <stdexcept>
+
+#include <boost/format.hpp>
+#include <boost/nowide/cstdio.hpp>
+
+namespace Slic3r {
+
+namespace GUI {
+ extern wxPanel *g_wxPlater;
+};
+
+BackgroundSlicingProcess::BackgroundSlicingProcess()
+{
+ m_temp_output_path = wxStandardPaths::Get().GetTempDir().utf8_str().data();
+ m_temp_output_path += (boost::format(".%1%.gcode") % get_current_pid()).str();
+}
+
+BackgroundSlicingProcess::~BackgroundSlicingProcess()
+{
+ this->stop();
+ this->join_background_thread();
+ boost::nowide::remove(m_temp_output_path.c_str());
+}
+
+void BackgroundSlicingProcess::thread_proc()
+{
+ std::unique_lock<std::mutex> lck(m_mutex);
+ // Let the caller know we are ready to run the background processing task.
+ m_state = STATE_IDLE;
+ lck.unlock();
+ m_condition.notify_one();
+ for (;;) {
+ assert(m_state == STATE_IDLE || m_state == STATE_CANCELED || m_state == STATE_FINISHED);
+ // Wait until a new task is ready to be executed, or this thread should be finished.
+ lck.lock();
+ m_condition.wait(lck, [this](){ return m_state == STATE_STARTED || m_state == STATE_EXIT; });
+ if (m_state == STATE_EXIT)
+ // Exiting this thread.
+ break;
+ // Process the background slicing task.
+ m_state = STATE_RUNNING;
+ lck.unlock();
+ std::string error;
+ try {
+ assert(m_print != nullptr);
+ m_print->process();
+ if (! m_print->canceled()) {
+ wxQueueEvent(GUI::g_wxPlater, new wxCommandEvent(m_event_sliced_id));
+ m_print->export_gcode(m_temp_output_path, m_gcode_preview_data);
+ if (! m_print->canceled() && ! m_output_path.empty()) {
+ if (copy_file(m_temp_output_path, m_output_path) != 0)
+ throw std::runtime_error("Copying of the temporary G-code to the output G-code failed");
+ m_print->set_status(95, "Running post-processing scripts");
+ run_post_process_scripts(m_output_path, m_print->config());
+ }
+ }
+ } catch (CanceledException &ex) {
+ // Canceled, this is all right.
+ assert(m_print->canceled());
+ } catch (std::exception &ex) {
+ error = ex.what();
+ } catch (...) {
+ error = "Unknown C++ exception.";
+ }
+ lck.lock();
+ m_state = m_print->canceled() ? STATE_CANCELED : STATE_FINISHED;
+ wxCommandEvent evt(m_event_finished_id);
+ evt.SetString(error);
+ evt.SetInt(m_print->canceled() ? -1 : (error.empty() ? 1 : 0));
+ wxQueueEvent(GUI::g_wxPlater, evt.Clone());
+ m_print->restart();
+ lck.unlock();
+ // Let the UI thread wake up if it is waiting for the background task to finish.
+ m_condition.notify_one();
+ // Let the UI thread see the result.
+ }
+ m_state = STATE_EXITED;
+ lck.unlock();
+ // End of the background processing thread. The UI thread should join m_thread now.
+}
+
+void BackgroundSlicingProcess::join_background_thread()
+{
+ std::unique_lock<std::mutex> lck(m_mutex);
+ if (m_state == STATE_INITIAL) {
+ // Worker thread has not been started yet.
+ assert(! m_thread.joinable());
+ } else {
+ assert(m_state == STATE_IDLE);
+ assert(m_thread.joinable());
+ // Notify the worker thread to exit.
+ m_state = STATE_EXIT;
+ lck.unlock();
+ m_condition.notify_one();
+ // Wait until the worker thread exits.
+ m_thread.join();
+ }
+}
+
+bool BackgroundSlicingProcess::start()
+{
+ std::unique_lock<std::mutex> lck(m_mutex);
+ if (m_state == STATE_INITIAL) {
+ // The worker thread is not running yet. Start it.
+ assert(! m_thread.joinable());
+ m_thread = std::thread([this]{this->thread_proc();});
+ // Wait until the worker thread is ready to execute the background processing task.
+ m_condition.wait(lck, [this](){ return m_state == STATE_IDLE; });
+ }
+ assert(m_state == STATE_IDLE || this->running());
+ if (this->running())
+ // The background processing thread is already running.
+ return false;
+ if (! this->idle())
+ throw std::runtime_error("Cannot start a background task, the worker thread is not idle.");
+ m_state = STATE_STARTED;
+ lck.unlock();
+ m_condition.notify_one();
+ return true;
+}
+
+bool BackgroundSlicingProcess::stop()
+{
+ std::unique_lock<std::mutex> lck(m_mutex);
+ if (m_state == STATE_INITIAL) {
+ this->m_output_path.clear();
+ return false;
+ }
+ assert(this->running());
+ if (m_state == STATE_STARTED || m_state == STATE_RUNNING) {
+ m_print->cancel();
+ // Wait until the background processing stops by being canceled.
+ m_condition.wait(lck, [this](){ return m_state == STATE_CANCELED; });
+ // In the "Canceled" state. Reset the state to "Idle".
+ m_state = STATE_IDLE;
+ } else if (m_state == STATE_FINISHED || m_state == STATE_CANCELED) {
+ // In the "Finished" or "Canceled" state. Reset the state to "Idle".
+ m_state = STATE_IDLE;
+ }
+ this->m_output_path.clear();
+ return true;
+}
+
+// Apply config over the print. Returns false, if the new config values caused any of the already
+// processed steps to be invalidated, therefore the task will need to be restarted.
+bool BackgroundSlicingProcess::apply_config(const DynamicPrintConfig &config)
+{
+ this->stop();
+ bool invalidated = m_print->apply_config(config);
+ return invalidated;
+}
+
+}; // namespace Slic3r
diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp
new file mode 100644
index 000000000..cc7a6db30
--- /dev/null
+++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp
@@ -0,0 +1,91 @@
+#ifndef slic3r_GUI_BackgroundSlicingProcess_hpp_
+#define slic3r_GUI_BackgroundSlicingProcess_hpp_
+
+#include <string>
+#include <condition_variable>
+#include <mutex>
+#include <thread>
+
+namespace Slic3r {
+
+class DynamicPrintConfig;
+class GCodePreviewData;
+class Print;
+
+// Support for the GUI background processing (Slicing and G-code generation).
+// As of now this class is not declared in Slic3r::GUI due to the Perl bindings limits.
+class BackgroundSlicingProcess
+{
+public:
+ BackgroundSlicingProcess();
+ // Stop the background processing and finalize the bacgkround processing thread, remove temp files.
+ ~BackgroundSlicingProcess();
+
+ void set_print(Print *print) { m_print = print; }
+ void set_gcode_preview_data(GCodePreviewData *gpd) { m_gcode_preview_data = gpd; }
+ // The following wxCommandEvent will be sent to the UI thread / Platter window, when the slicing is finished
+ // and the background processing will transition into G-code export.
+ // The wxCommandEvent is sent to the UI thread asynchronously without waiting for the event to be processed.
+ void set_sliced_event(int event_id) { m_event_sliced_id = event_id; }
+ // The following wxCommandEvent will be sent to the UI thread / Platter window, when the G-code export is finished.
+ // The wxCommandEvent is sent to the UI thread asynchronously without waiting for the event to be processed.
+ void set_finished_event(int event_id) { m_event_finished_id = event_id; }
+
+ // Set the output path of the G-code.
+ void set_output_path(const std::string &path) { m_output_path = path; }
+ // Start the background processing. Returns false if the background processing was already running.
+ bool start();
+ // Cancel the background processing. Returns false if the background processing was not running.
+ // A stopped background processing may be restarted with start().
+ bool stop();
+
+ // Apply config over the print. Returns false, if the new config values caused any of the already
+ // processed steps to be invalidated, therefore the task will need to be restarted.
+ bool apply_config(const DynamicPrintConfig &config);
+
+ enum State {
+ // m_thread is not running yet, or it did not reach the STATE_IDLE yet (it does not wait on the condition yet).
+ STATE_INITIAL = 0,
+ // m_thread is waiting for the task to execute.
+ STATE_IDLE,
+ STATE_STARTED,
+ // m_thread is executing a task.
+ STATE_RUNNING,
+ // m_thread finished executing a task, and it is waiting until the UI thread picks up the results.
+ STATE_FINISHED,
+ // m_thread finished executing a task, the task has been canceled by the UI thread, therefore the UI thread will not be notified.
+ STATE_CANCELED,
+ // m_thread exited the loop and it is going to finish. The UI thread should join on m_thread.
+ STATE_EXIT,
+ STATE_EXITED,
+ };
+ State state() const { return m_state; }
+ bool idle() const { return m_state == STATE_IDLE; }
+ bool running() const { return m_state == STATE_STARTED || m_state == STATE_RUNNING || m_state == STATE_FINISHED || m_state == STATE_CANCELED; }
+
+private:
+ void thread_proc();
+ void join_background_thread();
+
+ Print *m_print = nullptr;
+ // Data structure, to which the G-code export writes its annotations.
+ GCodePreviewData *m_gcode_preview_data = nullptr;
+ std::string m_temp_output_path;
+ std::string m_output_path;
+ // Thread, on which the background processing is executed. The thread will always be present
+ // and ready to execute the slicing process.
+ std::thread m_thread;
+ // Mutex and condition variable to synchronize m_thread with the UI thread.
+ std::mutex m_mutex;
+ std::condition_variable m_condition;
+ State m_state = STATE_INITIAL;
+
+ // wxWidgets command ID to be sent to the platter to inform that the slicing is finished, and the G-code export will continue.
+ int m_event_sliced_id = 0;
+ // wxWidgets command ID to be sent to the platter to inform that the task finished.
+ int m_event_finished_id = 0;
+};
+
+}; // namespace Slic3r
+
+#endif /* slic3r_GUI_BackgroundSlicingProcess_hpp_ */
diff --git a/src/slic3r/GUI/BedShapeDialog.cpp b/src/slic3r/GUI/BedShapeDialog.cpp
new file mode 100644
index 000000000..e04f2b370
--- /dev/null
+++ b/src/slic3r/GUI/BedShapeDialog.cpp
@@ -0,0 +1,343 @@
+#include "BedShapeDialog.hpp"
+
+#include <wx/sizer.h>
+#include <wx/statbox.h>
+#include <wx/wx.h>
+#include "Polygon.hpp"
+#include "BoundingBox.hpp"
+#include <wx/numformatter.h>
+#include "Model.hpp"
+#include "boost/nowide/iostream.hpp"
+
+#include <algorithm>
+
+namespace Slic3r {
+namespace GUI {
+
+void BedShapeDialog::build_dialog(ConfigOptionPoints* default_pt)
+{
+ m_panel = new BedShapePanel(this);
+ m_panel->build_panel(default_pt);
+
+ auto main_sizer = new wxBoxSizer(wxVERTICAL);
+ main_sizer->Add(m_panel, 1, wxEXPAND);
+ main_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10);
+
+ SetSizer(main_sizer);
+ SetMinSize(GetSize());
+ main_sizer->SetSizeHints(this);
+
+ // needed to actually free memory
+ this->Bind(wxEVT_CLOSE_WINDOW, ([this](wxCloseEvent e){
+ EndModal(wxID_OK);
+ Destroy();
+ }));
+}
+
+void BedShapePanel::build_panel(ConfigOptionPoints* default_pt)
+{
+// on_change(nullptr);
+
+ auto box = new wxStaticBox(this, wxID_ANY, _(L("Shape")));
+ auto sbsizer = new wxStaticBoxSizer(box, wxVERTICAL);
+
+ // shape options
+ m_shape_options_book = new wxChoicebook(this, wxID_ANY, wxDefaultPosition, wxSize(300, -1), wxCHB_TOP);
+ sbsizer->Add(m_shape_options_book);
+
+ auto optgroup = init_shape_options_page(_(L("Rectangular")));
+ ConfigOptionDef def;
+ def.type = coPoints;
+ def.default_value = new ConfigOptionPoints{ Vec2d(200, 200) };
+ def.label = L("Size");
+ def.tooltip = L("Size in X and Y of the rectangular plate.");
+ Option option(def, "rect_size");
+ optgroup->append_single_option_line(option);
+
+ def.type = coPoints;
+ def.default_value = new ConfigOptionPoints{ Vec2d(0, 0) };
+ def.label = L("Origin");
+ def.tooltip = L("Distance of the 0,0 G-code coordinate from the front left corner of the rectangle.");
+ option = Option(def, "rect_origin");
+ optgroup->append_single_option_line(option);
+
+ optgroup = init_shape_options_page(_(L("Circular")));
+ def.type = coFloat;
+ def.default_value = new ConfigOptionFloat(200);
+ def.sidetext = L("mm");
+ def.label = L("Diameter");
+ def.tooltip = L("Diameter of the print bed. It is assumed that origin (0,0) is located in the center.");
+ option = Option(def, "diameter");
+ optgroup->append_single_option_line(option);
+
+ optgroup = init_shape_options_page(_(L("Custom")));
+ Line line{ "", "" };
+ line.full_width = 1;
+ line.widget = [this](wxWindow* parent) {
+ auto btn = new wxButton(parent, wxID_ANY, _(L("Load shape from STL...")), wxDefaultPosition, wxDefaultSize);
+
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(btn);
+
+ btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e)
+ {
+ load_stl();
+ }));
+
+ return sizer;
+ };
+ optgroup->append_line(line);
+
+ Bind(wxEVT_CHOICEBOOK_PAGE_CHANGED, ([this](wxCommandEvent e)
+ {
+ update_shape();
+ }));
+
+ // right pane with preview canvas
+ m_canvas = new Bed_2D(this);
+ m_canvas->m_bed_shape = default_pt->values;
+
+ // main sizer
+ auto top_sizer = new wxBoxSizer(wxHORIZONTAL);
+ top_sizer->Add(sbsizer, 0, wxEXPAND | wxLeft | wxTOP | wxBOTTOM, 10);
+ if (m_canvas)
+ top_sizer->Add(m_canvas, 1, wxEXPAND | wxALL, 10) ;
+
+ SetSizerAndFit(top_sizer);
+
+ set_shape(default_pt);
+ update_preview();
+}
+
+#define SHAPE_RECTANGULAR 0
+#define SHAPE_CIRCULAR 1
+#define SHAPE_CUSTOM 2
+
+// Called from the constructor.
+// Create a panel for a rectangular / circular / custom bed shape.
+ConfigOptionsGroupShp BedShapePanel::init_shape_options_page(wxString title){
+
+ auto panel = new wxPanel(m_shape_options_book);
+ ConfigOptionsGroupShp optgroup;
+ optgroup = std::make_shared<ConfigOptionsGroup>(panel, _(L("Settings")));
+
+ optgroup->label_width = 100;
+ optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value){
+ update_shape();
+ };
+
+ m_optgroups.push_back(optgroup);
+ panel->SetSizerAndFit(optgroup->sizer);
+ m_shape_options_book->AddPage(panel, title);
+
+ return optgroup;
+}
+
+// Called from the constructor.
+// Set the initial bed shape from a list of points.
+// Deduce the bed shape type(rect, circle, custom)
+// This routine shall be smart enough if the user messes up
+// with the list of points in the ini file directly.
+void BedShapePanel::set_shape(ConfigOptionPoints* points)
+{
+ auto polygon = Polygon::new_scale(points->values);
+
+ // is this a rectangle ?
+ if (points->size() == 4) {
+ auto lines = polygon.lines();
+ if (lines[0].parallel_to(lines[2]) && lines[1].parallel_to(lines[3])) {
+ // okay, it's a rectangle
+ // find origin
+ coordf_t x_min, x_max, y_min, y_max;
+ x_max = x_min = points->values[0](0);
+ y_max = y_min = points->values[0](1);
+ for (auto pt : points->values)
+ {
+ x_min = std::min(x_min, pt(0));
+ x_max = std::max(x_max, pt(0));
+ y_min = std::min(y_min, pt(1));
+ y_max = std::max(y_max, pt(1));
+ }
+
+ auto origin = new ConfigOptionPoints{ Vec2d(-x_min, -y_min) };
+
+ m_shape_options_book->SetSelection(SHAPE_RECTANGULAR);
+ auto optgroup = m_optgroups[SHAPE_RECTANGULAR];
+ optgroup->set_value("rect_size", new ConfigOptionPoints{ Vec2d(x_max - x_min, y_max - y_min) });//[x_max - x_min, y_max - y_min]);
+ optgroup->set_value("rect_origin", origin);
+ update_shape();
+ return;
+ }
+ }
+
+ // is this a circle ?
+ {
+ // Analyze the array of points.Do they reside on a circle ?
+ auto center = polygon.bounding_box().center();
+ std::vector<double> vertex_distances;
+ double avg_dist = 0;
+ for (auto pt: polygon.points)
+ {
+ double distance = (pt - center).cast<double>().norm();
+ vertex_distances.push_back(distance);
+ avg_dist += distance;
+ }
+
+ avg_dist /= vertex_distances.size();
+ bool defined_value = true;
+ for (auto el: vertex_distances)
+ {
+ if (abs(el - avg_dist) > 10 * SCALED_EPSILON)
+ defined_value = false;
+ break;
+ }
+ if (defined_value) {
+ // all vertices are equidistant to center
+ m_shape_options_book->SetSelection(SHAPE_CIRCULAR);
+ auto optgroup = m_optgroups[SHAPE_CIRCULAR];
+ boost::any ret = wxNumberFormatter::ToString(unscale<double>(avg_dist * 2), 0);
+ optgroup->set_value("diameter", ret);
+ update_shape();
+ return;
+ }
+ }
+
+ if (points->size() < 3) {
+ // Invalid polygon.Revert to default bed dimensions.
+ m_shape_options_book->SetSelection(SHAPE_RECTANGULAR);
+ auto optgroup = m_optgroups[SHAPE_RECTANGULAR];
+ optgroup->set_value("rect_size", new ConfigOptionPoints{ Vec2d(200, 200) });
+ optgroup->set_value("rect_origin", new ConfigOptionPoints{ Vec2d(0, 0) });
+ update_shape();
+ return;
+ }
+
+ // This is a custom bed shape, use the polygon provided.
+ m_shape_options_book->SetSelection(SHAPE_CUSTOM);
+ // Copy the polygon to the canvas, make a copy of the array.
+ m_canvas->m_bed_shape = points->values;
+ update_shape();
+}
+
+void BedShapePanel::update_preview()
+{
+ if (m_canvas) m_canvas->Refresh();
+ Refresh();
+}
+
+// Update the bed shape from the dialog fields.
+void BedShapePanel::update_shape()
+{
+ auto page_idx = m_shape_options_book->GetSelection();
+ if (page_idx == SHAPE_RECTANGULAR) {
+ Vec2d rect_size(Vec2d::Zero());
+ Vec2d rect_origin(Vec2d::Zero());
+ try{
+ rect_size = boost::any_cast<Vec2d>(m_optgroups[SHAPE_RECTANGULAR]->get_value("rect_size")); }
+ catch (const std::exception &e){
+ return;
+ }
+ try{
+ rect_origin = boost::any_cast<Vec2d>(m_optgroups[SHAPE_RECTANGULAR]->get_value("rect_origin"));
+ }
+ catch (const std::exception &e){
+ return;}
+
+ auto x = rect_size(0);
+ auto y = rect_size(1);
+ // empty strings or '-' or other things
+ if (x == 0 || y == 0) return;
+ double x0 = 0.0;
+ double y0 = 0.0;
+ double x1 = x;
+ double y1 = y;
+
+ auto dx = rect_origin(0);
+ auto dy = rect_origin(1);
+
+ x0 -= dx;
+ x1 -= dx;
+ y0 -= dy;
+ y1 -= dy;
+ m_canvas->m_bed_shape = { Vec2d(x0, y0),
+ Vec2d(x1, y0),
+ Vec2d(x1, y1),
+ Vec2d(x0, y1)};
+ }
+ else if(page_idx == SHAPE_CIRCULAR) {
+ double diameter;
+ try{
+ diameter = boost::any_cast<double>(m_optgroups[SHAPE_CIRCULAR]->get_value("diameter"));
+ }
+ catch (const std::exception &e){
+ return;
+ }
+ if (diameter == 0.0) return ;
+ auto r = diameter / 2;
+ auto twopi = 2 * PI;
+ auto edges = 60;
+ std::vector<Vec2d> points;
+ for (size_t i = 1; i <= 60; ++i){
+ auto angle = i * twopi / edges;
+ points.push_back(Vec2d(r*cos(angle), r*sin(angle)));
+ }
+ m_canvas->m_bed_shape = points;
+ }
+
+// $self->{on_change}->();
+ update_preview();
+}
+
+// Loads an stl file, projects it to the XY plane and calculates a polygon.
+void BedShapePanel::load_stl()
+{
+ t_file_wild_card vec_FILE_WILDCARDS = get_file_wild_card();
+ std::vector<std::string> file_types = { "known", "stl", "obj", "amf", "3mf", "prusa" };
+ wxString MODEL_WILDCARD;
+ for (auto file_type: file_types)
+ MODEL_WILDCARD += vec_FILE_WILDCARDS.at(file_type) + "|";
+
+ auto dialog = new wxFileDialog(this, _(L("Choose a file to import bed shape from (STL/OBJ/AMF/3MF/PRUSA):")), "", "",
+ MODEL_WILDCARD, wxFD_OPEN | wxFD_FILE_MUST_EXIST);
+ if (dialog->ShowModal() != wxID_OK) {
+ dialog->Destroy();
+ return;
+ }
+ wxArrayString input_file;
+ dialog->GetPaths(input_file);
+ dialog->Destroy();
+
+ std::string file_name = input_file[0].ToStdString();
+
+ Model model;
+ try {
+ model = Model::read_from_file(file_name);
+ }
+ catch (std::exception &e) {
+ auto msg = _(L("Error! ")) + file_name + " : " + e.what() + ".";
+ show_error(this, msg);
+ exit(1);
+ }
+
+ auto mesh = model.mesh();
+ auto expolygons = mesh.horizontal_projection();
+
+ if (expolygons.size() == 0) {
+ show_error(this, _(L("The selected file contains no geometry.")));
+ return;
+ }
+ if (expolygons.size() > 1) {
+ show_error(this, _(L("The selected file contains several disjoint areas. This is not supported.")));
+ return;
+ }
+
+ auto polygon = expolygons[0].contour;
+ std::vector<Vec2d> points;
+ for (auto pt : polygon.points)
+ points.push_back(unscale(pt));
+ m_canvas->m_bed_shape = points;
+ update_preview();
+}
+
+} // GUI
+} // Slic3r
diff --git a/src/slic3r/GUI/BedShapeDialog.hpp b/src/slic3r/GUI/BedShapeDialog.hpp
new file mode 100644
index 000000000..d8ba5a912
--- /dev/null
+++ b/src/slic3r/GUI/BedShapeDialog.hpp
@@ -0,0 +1,56 @@
+#ifndef slic3r_BedShapeDialog_hpp_
+#define slic3r_BedShapeDialog_hpp_
+// The bed shape dialog.
+// The dialog opens from Print Settins tab->Bed Shape : Set...
+
+#include "OptionsGroup.hpp"
+#include "2DBed.hpp"
+
+
+#include <wx/dialog.h>
+#include <wx/choicebk.h>
+
+namespace Slic3r {
+namespace GUI {
+
+using ConfigOptionsGroupShp = std::shared_ptr<ConfigOptionsGroup>;
+class BedShapePanel : public wxPanel
+{
+ wxChoicebook* m_shape_options_book;
+ Bed_2D* m_canvas;
+
+ std::vector <ConfigOptionsGroupShp> m_optgroups;
+
+public:
+ BedShapePanel(wxWindow* parent) : wxPanel(parent, wxID_ANY){}
+ ~BedShapePanel(){}
+
+ void build_panel(ConfigOptionPoints* default_pt);
+
+ ConfigOptionsGroupShp init_shape_options_page(wxString title);
+ void set_shape(ConfigOptionPoints* points);
+ void update_preview();
+ void update_shape();
+ void load_stl();
+
+ // Returns the resulting bed shape polygon. This value will be stored to the ini file.
+ std::vector<Vec2d> GetValue() { return m_canvas->m_bed_shape; }
+};
+
+class BedShapeDialog : public wxDialog
+{
+ BedShapePanel* m_panel;
+public:
+ BedShapeDialog(wxWindow* parent) : wxDialog(parent, wxID_ANY, _(L("Bed Shape")),
+ wxDefaultPosition, wxSize(350, 700), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER){}
+ ~BedShapeDialog(){ }
+
+ void build_dialog(ConfigOptionPoints* default_pt);
+ std::vector<Vec2d> GetValue() { return m_panel->GetValue(); }
+};
+
+} // GUI
+} // Slic3r
+
+
+#endif /* slic3r_BedShapeDialog_hpp_ */
diff --git a/src/slic3r/GUI/BitmapCache.cpp b/src/slic3r/GUI/BitmapCache.cpp
new file mode 100644
index 000000000..93853458e
--- /dev/null
+++ b/src/slic3r/GUI/BitmapCache.cpp
@@ -0,0 +1,172 @@
+#include "BitmapCache.hpp"
+
+#if ! defined(WIN32) && ! defined(__APPLE__)
+#define BROKEN_ALPHA
+#endif
+
+#ifdef BROKEN_ALPHA
+ #include <wx/mstream.h>
+ #include <wx/rawbmp.h>
+#endif /* BROKEN_ALPHA */
+
+namespace Slic3r { namespace GUI {
+
+void BitmapCache::clear()
+{
+ for (std::pair<const std::string, wxBitmap*> &bitmap : m_map)
+ delete bitmap.second;
+}
+
+static wxBitmap wxImage_to_wxBitmap_with_alpha(wxImage &&image)
+{
+#ifdef BROKEN_ALPHA
+ wxMemoryOutputStream stream;
+ image.SaveFile(stream, wxBITMAP_TYPE_PNG);
+ wxStreamBuffer *buf = stream.GetOutputStreamBuffer();
+ return wxBitmap::NewFromPNGData(buf->GetBufferStart(), buf->GetBufferSize());
+#else
+ return wxBitmap(std::move(image));
+#endif
+}
+
+wxBitmap* BitmapCache::insert(const std::string &bitmap_key, size_t width, size_t height)
+{
+ wxBitmap *bitmap = nullptr;
+ auto it = m_map.find(bitmap_key);
+ if (it == m_map.end()) {
+ bitmap = new wxBitmap(width, height);
+ m_map[bitmap_key] = bitmap;
+ } else {
+ bitmap = it->second;
+ if (bitmap->GetWidth() != width || bitmap->GetHeight() != height)
+ bitmap->Create(width, height);
+ }
+#ifndef BROKEN_ALPHA
+ bitmap->UseAlpha();
+#endif
+ return bitmap;
+}
+
+wxBitmap* BitmapCache::insert(const std::string &bitmap_key, const wxBitmap &bmp)
+{
+ wxBitmap *bitmap = nullptr;
+ auto it = m_map.find(bitmap_key);
+ if (it == m_map.end()) {
+ bitmap = new wxBitmap(bmp);
+ m_map[bitmap_key] = bitmap;
+ } else {
+ bitmap = it->second;
+ *bitmap = bmp;
+ }
+ return bitmap;
+}
+
+wxBitmap* BitmapCache::insert(const std::string &bitmap_key, const wxBitmap &bmp, const wxBitmap &bmp2)
+{
+ // Copying the wxBitmaps is cheap as the bitmap's content is reference counted.
+ const wxBitmap bmps[2] = { bmp, bmp2 };
+ return this->insert(bitmap_key, bmps, bmps + 2);
+}
+
+wxBitmap* BitmapCache::insert(const std::string &bitmap_key, const wxBitmap &bmp, const wxBitmap &bmp2, const wxBitmap &bmp3)
+{
+ // Copying the wxBitmaps is cheap as the bitmap's content is reference counted.
+ const wxBitmap bmps[3] = { bmp, bmp2, bmp3 };
+ return this->insert(bitmap_key, bmps, bmps + 3);
+}
+
+wxBitmap* BitmapCache::insert(const std::string &bitmap_key, const wxBitmap *begin, const wxBitmap *end)
+{
+ size_t width = 0;
+ size_t height = 0;
+ for (const wxBitmap *bmp = begin; bmp != end; ++ bmp) {
+ width += bmp->GetWidth();
+ height = std::max<size_t>(height, bmp->GetHeight());
+ }
+
+#ifdef BROKEN_ALPHA
+
+ wxImage image(width, height);
+ image.InitAlpha();
+ // Fill in with a white color.
+ memset(image.GetData(), 0x0ff, width * height * 3);
+ // Fill in with full transparency.
+ memset(image.GetAlpha(), 0, width * height);
+ size_t x = 0;
+ for (const wxBitmap *bmp = begin; bmp != end; ++ bmp) {
+ if (bmp->GetWidth() > 0) {
+ if (bmp->GetDepth() == 32) {
+ wxAlphaPixelData data(*const_cast<wxBitmap*>(bmp));
+ data.UseAlpha();
+ if (data) {
+ for (int r = 0; r < bmp->GetHeight(); ++ r) {
+ wxAlphaPixelData::Iterator src(data);
+ src.Offset(data, 0, r);
+ unsigned char *dst_pixels = image.GetData() + (x + r * width) * 3;
+ unsigned char *dst_alpha = image.GetAlpha() + x + r * width;
+ for (int c = 0; c < bmp->GetWidth(); ++ c, ++ src) {
+ *dst_pixels ++ = src.Red();
+ *dst_pixels ++ = src.Green();
+ *dst_pixels ++ = src.Blue();
+ *dst_alpha ++ = src.Alpha();
+ }
+ }
+ }
+ } else if (bmp->GetDepth() == 24) {
+ wxNativePixelData data(*const_cast<wxBitmap*>(bmp));
+ if (data) {
+ for (int r = 0; r < bmp->GetHeight(); ++ r) {
+ wxNativePixelData::Iterator src(data);
+ src.Offset(data, 0, r);
+ unsigned char *dst_pixels = image.GetData() + (x + r * width) * 3;
+ unsigned char *dst_alpha = image.GetAlpha() + x + r * width;
+ for (int c = 0; c < bmp->GetWidth(); ++ c, ++ src) {
+ *dst_pixels ++ = src.Red();
+ *dst_pixels ++ = src.Green();
+ *dst_pixels ++ = src.Blue();
+ *dst_alpha ++ = wxALPHA_OPAQUE;
+ }
+ }
+ }
+ }
+ }
+ x += bmp->GetWidth();
+ }
+ return this->insert(bitmap_key, wxImage_to_wxBitmap_with_alpha(std::move(image)));
+
+#else
+
+ wxBitmap *bitmap = this->insert(bitmap_key, width, height);
+ wxMemoryDC memDC;
+ memDC.SelectObject(*bitmap);
+ memDC.SetBackground(*wxTRANSPARENT_BRUSH);
+ memDC.Clear();
+ size_t x = 0;
+ for (const wxBitmap *bmp = begin; bmp != end; ++ bmp) {
+ if (bmp->GetWidth() > 0)
+ memDC.DrawBitmap(*bmp, x, 0, true);
+ x += bmp->GetWidth();
+ }
+ memDC.SelectObject(wxNullBitmap);
+ return bitmap;
+
+#endif
+}
+
+wxBitmap BitmapCache::mksolid(size_t width, size_t height, unsigned char r, unsigned char g, unsigned char b, unsigned char transparency)
+{
+ wxImage image(width, height);
+ image.InitAlpha();
+ unsigned char* imgdata = image.GetData();
+ unsigned char* imgalpha = image.GetAlpha();
+ for (size_t i = 0; i < width * height; ++ i) {
+ *imgdata ++ = r;
+ *imgdata ++ = g;
+ *imgdata ++ = b;
+ *imgalpha ++ = transparency;
+ }
+ return wxImage_to_wxBitmap_with_alpha(std::move(image));
+}
+
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/BitmapCache.hpp b/src/slic3r/GUI/BitmapCache.hpp
new file mode 100644
index 000000000..bec9a7ad2
--- /dev/null
+++ b/src/slic3r/GUI/BitmapCache.hpp
@@ -0,0 +1,44 @@
+#ifndef SLIC3R_GUI_BITMAP_CACHE_HPP
+#define SLIC3R_GUI_BITMAP_CACHE_HPP
+
+#include <wx/wxprec.h>
+#ifndef WX_PRECOMP
+ #include <wx/wx.h>
+#endif
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/Config.hpp"
+
+#include "GUI.hpp"
+
+namespace Slic3r { namespace GUI {
+
+class BitmapCache
+{
+public:
+ BitmapCache() {}
+ ~BitmapCache() { clear(); }
+ void clear();
+
+ wxBitmap* find(const std::string &name) { auto it = m_map.find(name); return (it == m_map.end()) ? nullptr : it->second; }
+ const wxBitmap* find(const std::string &name) const { return const_cast<BitmapCache*>(this)->find(name); }
+
+ wxBitmap* insert(const std::string &name, size_t width, size_t height);
+ wxBitmap* insert(const std::string &name, const wxBitmap &bmp);
+ wxBitmap* insert(const std::string &name, const wxBitmap &bmp, const wxBitmap &bmp2);
+ wxBitmap* insert(const std::string &name, const wxBitmap &bmp, const wxBitmap &bmp2, const wxBitmap &bmp3);
+ wxBitmap* insert(const std::string &name, const std::vector<wxBitmap> &bmps) { return this->insert(name, &bmps.front(), &bmps.front() + bmps.size()); }
+ wxBitmap* insert(const std::string &name, const wxBitmap *begin, const wxBitmap *end);
+
+ static wxBitmap mksolid(size_t width, size_t height, unsigned char r, unsigned char g, unsigned char b, unsigned char transparency);
+ static wxBitmap mksolid(size_t width, size_t height, const unsigned char rgb[3]) { return mksolid(width, height, rgb[0], rgb[1], rgb[2], wxALPHA_OPAQUE); }
+ static wxBitmap mkclear(size_t width, size_t height) { return mksolid(width, height, 0, 0, 0, wxALPHA_TRANSPARENT); }
+
+private:
+ std::map<std::string, wxBitmap*> m_map;
+};
+
+} // GUI
+} // Slic3r
+
+#endif /* SLIC3R_GUI_BITMAP_CACHE_HPP */
diff --git a/src/slic3r/GUI/BonjourDialog.cpp b/src/slic3r/GUI/BonjourDialog.cpp
new file mode 100644
index 000000000..11cfea642
--- /dev/null
+++ b/src/slic3r/GUI/BonjourDialog.cpp
@@ -0,0 +1,200 @@
+#include "slic3r/Utils/Bonjour.hpp" // On Windows, boost needs to be included before wxWidgets headers
+
+#include "BonjourDialog.hpp"
+
+#include <set>
+#include <mutex>
+
+#include <wx/sizer.h>
+#include <wx/button.h>
+#include <wx/listctrl.h>
+#include <wx/stattext.h>
+#include <wx/timer.h>
+
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/Utils/Bonjour.hpp"
+
+
+namespace Slic3r {
+
+
+struct BonjourReplyEvent : public wxEvent
+{
+ BonjourReply reply;
+
+ BonjourReplyEvent(wxEventType eventType, int winid, BonjourReply &&reply) :
+ wxEvent(winid, eventType),
+ reply(std::move(reply))
+ {}
+
+ virtual wxEvent *Clone() const
+ {
+ return new BonjourReplyEvent(*this);
+ }
+};
+
+wxDEFINE_EVENT(EVT_BONJOUR_REPLY, BonjourReplyEvent);
+
+wxDECLARE_EVENT(EVT_BONJOUR_COMPLETE, wxCommandEvent);
+wxDEFINE_EVENT(EVT_BONJOUR_COMPLETE, wxCommandEvent);
+
+class ReplySet: public std::set<BonjourReply> {};
+
+struct LifetimeGuard
+{
+ std::mutex mutex;
+ BonjourDialog *dialog;
+
+ LifetimeGuard(BonjourDialog *dialog) : dialog(dialog) {}
+};
+
+
+BonjourDialog::BonjourDialog(wxWindow *parent) :
+ wxDialog(parent, wxID_ANY, _(L("Network lookup"))),
+ list(new wxListView(this, wxID_ANY, wxDefaultPosition, wxSize(800, 300))),
+ replies(new ReplySet),
+ label(new wxStaticText(this, wxID_ANY, "")),
+ timer(new wxTimer()),
+ timer_state(0)
+{
+ wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
+
+ vsizer->Add(label, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 10);
+
+ list->SetSingleStyle(wxLC_SINGLE_SEL);
+ list->SetSingleStyle(wxLC_SORT_DESCENDING);
+ list->AppendColumn(_(L("Address")), wxLIST_FORMAT_LEFT, 50);
+ list->AppendColumn(_(L("Hostname")), wxLIST_FORMAT_LEFT, 100);
+ list->AppendColumn(_(L("Service name")), wxLIST_FORMAT_LEFT, 200);
+ list->AppendColumn(_(L("OctoPrint version")), wxLIST_FORMAT_LEFT, 50);
+
+ vsizer->Add(list, 1, wxEXPAND | wxALL, 10);
+
+ wxBoxSizer *button_sizer = new wxBoxSizer(wxHORIZONTAL);
+ button_sizer->Add(new wxButton(this, wxID_OK, "OK"), 0, wxALL, 10);
+ button_sizer->Add(new wxButton(this, wxID_CANCEL, "Cancel"), 0, wxALL, 10);
+ // ^ Note: The Ok/Cancel labels are translated by wxWidgets
+
+ vsizer->Add(button_sizer, 0, wxALIGN_CENTER);
+ SetSizerAndFit(vsizer);
+
+ Bind(EVT_BONJOUR_REPLY, &BonjourDialog::on_reply, this);
+
+ Bind(EVT_BONJOUR_COMPLETE, [this](wxCommandEvent &) {
+ this->timer_state = 0;
+ });
+
+ Bind(wxEVT_TIMER, &BonjourDialog::on_timer, this);
+}
+
+BonjourDialog::~BonjourDialog()
+{
+ // Needed bacuse of forward defs
+}
+
+bool BonjourDialog::show_and_lookup()
+{
+ Show(); // Because we need GetId() to work before ShowModal()
+
+ timer->Stop();
+ timer->SetOwner(this);
+ timer_state = 1;
+ timer->Start(1000);
+ wxTimerEvent evt_dummy;
+ on_timer(evt_dummy);
+
+ // The background thread needs to queue messages for this dialog
+ // and for that it needs a valid pointer to it (mandated by the wxWidgets API).
+ // Here we put the pointer under a shared_ptr and protect it by a mutex,
+ // so that both threads can access it safely.
+ auto dguard = std::make_shared<LifetimeGuard>(this);
+
+ bonjour = std::move(Bonjour("octoprint")
+ .set_retries(3)
+ .set_timeout(4)
+ .on_reply([dguard](BonjourReply &&reply) {
+ std::lock_guard<std::mutex> lock_guard(dguard->mutex);
+ auto dialog = dguard->dialog;
+ if (dialog != nullptr) {
+ auto evt = new BonjourReplyEvent(EVT_BONJOUR_REPLY, dialog->GetId(), std::move(reply));
+ wxQueueEvent(dialog, evt);
+ }
+ })
+ .on_complete([dguard]() {
+ std::lock_guard<std::mutex> lock_guard(dguard->mutex);
+ auto dialog = dguard->dialog;
+ if (dialog != nullptr) {
+ auto evt = new wxCommandEvent(EVT_BONJOUR_COMPLETE, dialog->GetId());
+ wxQueueEvent(dialog, evt);
+ }
+ })
+ .lookup()
+ );
+
+ bool res = ShowModal() == wxID_OK && list->GetFirstSelected() >= 0;
+ {
+ // Tell the background thread the dialog is going away...
+ std::lock_guard<std::mutex> lock_guard(dguard->mutex);
+ dguard->dialog = nullptr;
+ }
+ return res;
+}
+
+wxString BonjourDialog::get_selected() const
+{
+ auto sel = list->GetFirstSelected();
+ return sel >= 0 ? list->GetItemText(sel) : wxString();
+}
+
+
+// Private
+
+void BonjourDialog::on_reply(BonjourReplyEvent &e)
+{
+ if (replies->find(e.reply) != replies->end()) {
+ // We already have this reply
+ return;
+ }
+
+ replies->insert(std::move(e.reply));
+
+ auto selected = get_selected();
+ list->DeleteAllItems();
+
+ // The whole list is recreated so that we benefit from it already being sorted in the set.
+ // (And also because wxListView's sorting API is bananas.)
+ for (const auto &reply : *replies) {
+ auto item = list->InsertItem(0, reply.full_address);
+ list->SetItem(item, 1, reply.hostname);
+ list->SetItem(item, 2, reply.service_name);
+ list->SetItem(item, 3, reply.version);
+ }
+
+ for (int i = 0; i < 4; i++) {
+ this->list->SetColumnWidth(i, wxLIST_AUTOSIZE);
+ if (this->list->GetColumnWidth(i) < 100) { this->list->SetColumnWidth(i, 100); }
+ }
+
+ if (!selected.IsEmpty()) {
+ // Attempt to preserve selection
+ auto hit = list->FindItem(-1, selected);
+ if (hit >= 0) { list->SetItemState(hit, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); }
+ }
+}
+
+void BonjourDialog::on_timer(wxTimerEvent &)
+{
+ const auto search_str = _(L("Searching for devices"));
+
+ if (timer_state > 0) {
+ const std::string dots(timer_state, '.');
+ label->SetLabel(wxString::Format("%s %s", search_str, dots));
+ timer_state = (timer_state) % 3 + 1;
+ } else {
+ label->SetLabel(wxString::Format("%s: %s", search_str, _(L("Finished"))+"."));
+ timer->Stop();
+ }
+}
+
+
+}
diff --git a/src/slic3r/GUI/BonjourDialog.hpp b/src/slic3r/GUI/BonjourDialog.hpp
new file mode 100644
index 000000000..e3f53790b
--- /dev/null
+++ b/src/slic3r/GUI/BonjourDialog.hpp
@@ -0,0 +1,49 @@
+#ifndef slic3r_BonjourDialog_hpp_
+#define slic3r_BonjourDialog_hpp_
+
+#include <memory>
+
+#include <wx/dialog.h>
+
+class wxListView;
+class wxStaticText;
+class wxTimer;
+class wxTimerEvent;
+
+
+namespace Slic3r {
+
+class Bonjour;
+class BonjourReplyEvent;
+class ReplySet;
+
+
+class BonjourDialog: public wxDialog
+{
+public:
+ BonjourDialog(wxWindow *parent);
+ BonjourDialog(BonjourDialog &&) = delete;
+ BonjourDialog(const BonjourDialog &) = delete;
+ BonjourDialog &operator=(BonjourDialog &&) = delete;
+ BonjourDialog &operator=(const BonjourDialog &) = delete;
+ ~BonjourDialog();
+
+ bool show_and_lookup();
+ wxString get_selected() const;
+private:
+ wxListView *list;
+ std::unique_ptr<ReplySet> replies;
+ wxStaticText *label;
+ std::shared_ptr<Bonjour> bonjour;
+ std::unique_ptr<wxTimer> timer;
+ unsigned timer_state;
+
+ void on_reply(BonjourReplyEvent &);
+ void on_timer(wxTimerEvent &);
+};
+
+
+
+}
+
+#endif
diff --git a/src/slic3r/GUI/ButtonsDescription.cpp b/src/slic3r/GUI/ButtonsDescription.cpp
new file mode 100644
index 000000000..5739fc90e
--- /dev/null
+++ b/src/slic3r/GUI/ButtonsDescription.cpp
@@ -0,0 +1,84 @@
+#include "ButtonsDescription.hpp"
+#include <wx/sizer.h>
+#include <wx/stattext.h>
+#include <wx/statbmp.h>
+#include <wx/clrpicker.h>
+
+#include "GUI.hpp"
+
+namespace Slic3r {
+namespace GUI {
+
+ButtonsDescription::ButtonsDescription(wxWindow* parent, t_icon_descriptions* icon_descriptions) :
+ wxDialog(parent, wxID_ANY, _(L("Buttons And Text Colors Description")), wxDefaultPosition, wxDefaultSize),
+ m_icon_descriptions(icon_descriptions)
+{
+ auto grid_sizer = new wxFlexGridSizer(3, 20, 20);
+
+ auto main_sizer = new wxBoxSizer(wxVERTICAL);
+ main_sizer->Add(grid_sizer, 0, wxEXPAND | wxALL, 20);
+
+ // Icon description
+ for (auto pair : *m_icon_descriptions)
+ {
+ auto icon = new wxStaticBitmap(this, wxID_ANY, *pair.first);
+ grid_sizer->Add(icon, -1, wxALIGN_CENTRE_VERTICAL);
+
+ std::istringstream f(pair.second);
+ std::string s;
+ getline(f, s, ';');
+ auto description = new wxStaticText(this, wxID_ANY, _(s));
+ grid_sizer->Add(description, -1, wxALIGN_CENTRE_VERTICAL);
+ getline(f, s, ';');
+ description = new wxStaticText(this, wxID_ANY, _(s));
+ grid_sizer->Add(description, -1, wxALIGN_CENTRE_VERTICAL | wxEXPAND);
+ }
+
+ // Text color description
+ auto sys_label = new wxStaticText(this, wxID_ANY, _(L("Value is the same as the system value")));
+ sys_label->SetForegroundColour(get_label_clr_sys());
+ auto sys_colour = new wxColourPickerCtrl(this, wxID_ANY, get_label_clr_sys());
+ sys_colour->Bind(wxEVT_COLOURPICKER_CHANGED, ([sys_colour, sys_label](wxCommandEvent e)
+ {
+ sys_label->SetForegroundColour(sys_colour->GetColour());
+ sys_label->Refresh();
+ }));
+ size_t t= 0;
+ while (t < 3){
+ grid_sizer->Add(new wxStaticText(this, wxID_ANY, ""), -1, wxALIGN_CENTRE_VERTICAL | wxEXPAND);
+ ++t;
+ }
+ grid_sizer->Add(0, -1, wxALIGN_CENTRE_VERTICAL);
+ grid_sizer->Add(sys_colour, -1, wxALIGN_CENTRE_VERTICAL);
+ grid_sizer->Add(sys_label, -1, wxALIGN_CENTRE_VERTICAL | wxEXPAND);
+
+ auto mod_label = new wxStaticText(this, wxID_ANY, _(L("Value was changed and is not equal to the system value or the last saved preset")));
+ mod_label->SetForegroundColour(get_label_clr_modified());
+ auto mod_colour = new wxColourPickerCtrl(this, wxID_ANY, get_label_clr_modified());
+ mod_colour->Bind(wxEVT_COLOURPICKER_CHANGED, ([mod_colour, mod_label](wxCommandEvent e)
+ {
+ mod_label->SetForegroundColour(mod_colour->GetColour());
+ mod_label->Refresh();
+ }));
+ grid_sizer->Add(0, -1, wxALIGN_CENTRE_VERTICAL);
+ grid_sizer->Add(mod_colour, -1, wxALIGN_CENTRE_VERTICAL);
+ grid_sizer->Add(mod_label, -1, wxALIGN_CENTRE_VERTICAL | wxEXPAND);
+
+
+ auto buttons = CreateStdDialogButtonSizer(wxOK|wxCANCEL);
+ main_sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10);
+
+ wxButton* btn = static_cast<wxButton*>(FindWindowById(wxID_OK, this));
+ btn->Bind(wxEVT_BUTTON, [sys_colour, mod_colour, this](wxCommandEvent&) {
+ set_label_clr_sys(sys_colour->GetColour());
+ set_label_clr_modified(mod_colour->GetColour());
+ EndModal(wxID_OK);
+ });
+
+ SetSizer(main_sizer);
+ main_sizer->SetSizeHints(this);
+}
+
+} // GUI
+} // Slic3r
+
diff --git a/src/slic3r/GUI/ButtonsDescription.hpp b/src/slic3r/GUI/ButtonsDescription.hpp
new file mode 100644
index 000000000..4858eaaea
--- /dev/null
+++ b/src/slic3r/GUI/ButtonsDescription.hpp
@@ -0,0 +1,27 @@
+#ifndef slic3r_ButtonsDescription_hpp
+#define slic3r_ButtonsDescription_hpp
+
+#include <wx/dialog.h>
+#include <vector>
+
+namespace Slic3r {
+namespace GUI {
+
+using t_icon_descriptions = std::vector<std::pair<wxBitmap*, std::string>>;
+
+class ButtonsDescription : public wxDialog
+{
+ t_icon_descriptions* m_icon_descriptions;
+public:
+ ButtonsDescription(wxWindow* parent, t_icon_descriptions* icon_descriptions);
+ ~ButtonsDescription(){}
+
+
+};
+
+} // GUI
+} // Slic3r
+
+
+#endif
+
diff --git a/src/slic3r/GUI/ConfigExceptions.hpp b/src/slic3r/GUI/ConfigExceptions.hpp
new file mode 100644
index 000000000..9038d3445
--- /dev/null
+++ b/src/slic3r/GUI/ConfigExceptions.hpp
@@ -0,0 +1,15 @@
+#include <exception>
+namespace Slic3r {
+
+class ConfigError : public std::runtime_error {
+using std::runtime_error::runtime_error;
+};
+
+namespace GUI {
+
+class ConfigGUITypeError : public ConfigError {
+using ConfigError::ConfigError;
+};
+}
+
+}
diff --git a/src/slic3r/GUI/ConfigSnapshotDialog.cpp b/src/slic3r/GUI/ConfigSnapshotDialog.cpp
new file mode 100644
index 000000000..efcbf05bd
--- /dev/null
+++ b/src/slic3r/GUI/ConfigSnapshotDialog.cpp
@@ -0,0 +1,140 @@
+#include "ConfigSnapshotDialog.hpp"
+
+#include "../Config/Snapshot.hpp"
+#include "../Utils/Time.hpp"
+
+#include "../../libslic3r/Utils.hpp"
+
+namespace Slic3r {
+namespace GUI {
+
+static wxString format_reason(const Config::Snapshot::Reason reason)
+{
+ switch (reason) {
+ case Config::Snapshot::SNAPSHOT_UPGRADE:
+ return wxString(_(L("Upgrade")));
+ case Config::Snapshot::SNAPSHOT_DOWNGRADE:
+ return wxString(_(L("Downgrade")));
+ case Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK:
+ return wxString(_(L("Before roll back")));
+ case Config::Snapshot::SNAPSHOT_USER:
+ return wxString(_(L("User")));
+ case Config::Snapshot::SNAPSHOT_UNKNOWN:
+ default:
+ return wxString(_(L("Unknown")));
+ }
+}
+
+static wxString generate_html_row(const Config::Snapshot &snapshot, bool row_even, bool snapshot_active)
+{
+ // Start by declaring a row with an alternating background color.
+ wxString text = "<tr bgcolor=\"";
+ text += snapshot_active ? "#B3FFCB" : (row_even ? "#FFFFFF" : "#D5D5D5");
+ text += "\">";
+ text += "<td>";
+ // Format the row header.
+ text += wxString("<font size=\"5\"><b>") + (snapshot_active ? _(L("Active: ")) : "") +
+ Utils::format_local_date_time(snapshot.time_captured) + ": " + format_reason(snapshot.reason);
+ if (! snapshot.comment.empty())
+ text += " (" + snapshot.comment + ")";
+ text += "</b></font><br>";
+ // End of row header.
+ text += _(L("slic3r version")) + ": " + snapshot.slic3r_version_captured.to_string() + "<br>";
+ text += _(L("print")) + ": " + snapshot.print + "<br>";
+ text += _(L("filaments")) + ": " + snapshot.filaments.front() + "<br>";
+ text += _(L("printer")) + ": " + snapshot.printer + "<br>";
+
+ bool compatible = true;
+ for (const Config::Snapshot::VendorConfig &vc : snapshot.vendor_configs) {
+ text += _(L("vendor")) + ": " + vc.name +", " + _(L("version")) + ": " + vc.version.config_version.to_string() +
+ ", " + _(L("min slic3r version")) + ": " + vc.version.min_slic3r_version.to_string();
+ if (vc.version.max_slic3r_version != Semver::inf())
+ text += ", " + _(L("max slic3r version")) + ": " + vc.version.max_slic3r_version.to_string();
+ text += "<br>";
+ for (const std::pair<std::string, std::set<std::string>> &model : vc.models_variants_installed) {
+ text += _(L("model")) + ": " + model.first + ", " + _(L("variants")) + ": ";
+ for (const std::string &variant : model.second) {
+ if (&variant != &*model.second.begin())
+ text += ", ";
+ text += variant;
+ }
+ text += "<br>";
+ }
+ if (! vc.version.is_current_slic3r_supported()) { compatible = false; }
+ }
+
+ if (! compatible) {
+ text += "<p align=\"right\">" + _(L("Incompatible with this Slic3r")) + "</p>";
+ }
+ else if (! snapshot_active)
+ text += "<p align=\"right\"><a href=\"" + snapshot.id + "\">" + _(L("Activate")) + "</a></p>";
+ text += "</td>";
+ text += "</tr>";
+ return text;
+}
+
+static wxString generate_html_page(const Config::SnapshotDB &snapshot_db, const wxString &on_snapshot)
+{
+ wxString text =
+ "<html>"
+ "<body bgcolor=\"#ffffff\" cellspacing=\"2\" cellpadding=\"0\" border=\"0\" link=\"#800000\">"
+ "<font color=\"#000000\">";
+ text += "<table style=\"width:100%\">";
+ 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, snapshot.id == on_snapshot);
+ }
+ text +=
+ "</table>"
+ "</font>"
+ "</body>"
+ "</html>";
+ return text;
+}
+
+ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db, const wxString &on_snapshot)
+ : 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,11,11,11};
+ #else
+ int size[] = {11,11,11,11,14,14,14};
+ #endif
+ html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
+ html->SetBorders(2);
+ wxString text = generate_html_page(snapshot_db, on_snapshot);
+ html->SetPage(text);
+ 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);
+}
+
+void ConfigSnapshotDialog::onLinkClicked(wxHtmlLinkEvent &event)
+{
+ m_snapshot_to_activate = event.GetLinkInfo().GetHref();
+ this->EndModal(wxID_CLOSE);
+ this->Close();
+}
+
+void ConfigSnapshotDialog::onCloseDialog(wxEvent &)
+{
+ this->EndModal(wxID_CLOSE);
+ this->Close();
+}
+
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/ConfigSnapshotDialog.hpp b/src/slic3r/GUI/ConfigSnapshotDialog.hpp
new file mode 100644
index 000000000..f43fb8ed1
--- /dev/null
+++ b/src/slic3r/GUI/ConfigSnapshotDialog.hpp
@@ -0,0 +1,34 @@
+#ifndef slic3r_GUI_ConfigSnapshotDialog_hpp_
+#define slic3r_GUI_ConfigSnapshotDialog_hpp_
+
+#include "GUI.hpp"
+
+#include <wx/wx.h>
+#include <wx/intl.h>
+#include <wx/html/htmlwin.h>
+
+namespace Slic3r {
+namespace GUI {
+
+namespace Config {
+ class SnapshotDB;
+}
+
+class ConfigSnapshotDialog : public wxDialog
+{
+public:
+ ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db, const wxString &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;
+};
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif /* slic3r_GUI_ConfigSnapshotDialog_hpp_ */
diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp
new file mode 100644
index 000000000..e784d8525
--- /dev/null
+++ b/src/slic3r/GUI/ConfigWizard.cpp
@@ -0,0 +1,915 @@
+#include "ConfigWizard_private.hpp"
+
+#include <algorithm>
+#include <utility>
+#include <unordered_map>
+#include <boost/format.hpp>
+#include <boost/log/trivial.hpp>
+
+#include <wx/settings.h>
+#include <wx/stattext.h>
+#include <wx/textctrl.h>
+#include <wx/dcclient.h>
+#include <wx/statbmp.h>
+#include <wx/checkbox.h>
+#include <wx/statline.h>
+
+#include "libslic3r/Utils.hpp"
+#include "PresetBundle.hpp"
+#include "GUI.hpp"
+#include "slic3r/Utils/PresetUpdater.hpp"
+
+
+namespace Slic3r {
+namespace GUI {
+
+
+// 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),
+ vendor_id(vendor.id),
+ variants_checked(0)
+{
+ const auto &models = vendor.models;
+
+ auto *sizer = new wxBoxSizer(wxVERTICAL);
+
+ auto *printer_grid = new wxFlexGridSizer(models.size(), 0, 20);
+ printer_grid->SetFlexibleDirection(wxVERTICAL | wxHORIZONTAL);
+ sizer->Add(printer_grid);
+
+ auto namefont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
+ namefont.SetWeight(wxFONTWEIGHT_BOLD);
+
+ // wxGrid appends widgets by rows, but we need to construct them in columns.
+ // These vectors are used to hold the elements so that they can be appended in the right order.
+ std::vector<wxStaticText*> titles;
+ std::vector<wxStaticBitmap*> bitmaps;
+ std::vector<wxPanel*> variants_panels;
+
+ for (const auto &model : models) {
+ 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 *title = new wxStaticText(this, wxID_ANY, model.name, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT);
+ title->SetFont(namefont);
+ title->Wrap(std::max((int)MODEL_MIN_WRAP, bitmap.GetWidth()));
+ titles.push_back(title);
+
+ auto *bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap);
+ bitmaps.push_back(bitmap_widget);
+
+ auto *variants_panel = new wxPanel(this);
+ auto *variants_sizer = new wxBoxSizer(wxVERTICAL);
+ variants_panel->SetSizer(variants_sizer);
+ const auto model_id = model.id;
+
+ bool default_variant = true; // Mark the first variant as default in the GUI
+ for (const auto &variant : model.variants) {
+ const auto label = wxString::Format("%s %s %s %s", variant.name, _(L("mm")), _(L("nozzle")),
+ (default_variant ? _(L("(default)")) : wxString()));
+ default_variant = false;
+ auto *cbox = new Checkbox(variants_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);
+ variants_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());
+ });
+ }
+
+ variants_panels.push_back(variants_panel);
+ }
+
+ for (auto title : titles) { printer_grid->Add(title, 0, wxBOTTOM, 3); }
+ for (auto bitmap : bitmaps) { printer_grid->Add(bitmap, 0, wxBOTTOM, 20); }
+ for (auto vp : variants_panels) { printer_grid->Add(vp); }
+
+ 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::select_one(size_t i, bool select)
+{
+ if (i < cboxes.size() && cboxes[i]->GetValue() != select) {
+ cboxes[i]->SetValue(select);
+ on_checkbox(cboxes[i], 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);
+}
+
+
+// Wizard page base
+
+ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname) :
+ wxPanel(parent->p->hscroll),
+ 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(WRAP_WIDTH);
+ widget->SetMinSize(wxSize(WRAP_WIDTH, -1));
+ append(widget);
+}
+
+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, bool check_first_variant) :
+ ConfigWizardPage(parent, wxString::Format(_(L("Welcome to the Slic3r %s")), ConfigWizard::name()), _(L("Welcome"))),
+ printer_picker(nullptr),
+ others_buttons(new wxPanel(parent)),
+ cbox_reset(nullptr)
+{
+ if (wizard_p()->run_reason == ConfigWizard::RR_DATA_EMPTY) {
+ 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 {
+ cbox_reset = new wxCheckBox(this, wxID_ANY, _(L("Remove user profiles - install from scratch (a snapshot will be taken beforehand)")));
+ append(cbox_reset);
+ }
+
+ const auto &vendors = wizard_p()->vendors;
+ const auto vendor_prusa = vendors.find("PrusaResearch");
+
+ if (vendor_prusa != vendors.cend()) {
+ AppConfig &appconfig_vendors = this->wizard_p()->appconfig_vendors;
+
+ printer_picker = new PrinterPicker(this, vendor_prusa->second, appconfig_vendors);
+ if (check_first_variant) {
+ // Select the default (first) model/variant on the Prusa vendor
+ printer_picker->select_one(0, true);
+ }
+ 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);
+ }
+
+ 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);
+
+ 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(printer_picker != nullptr ? printer_picker->variants_checked > 0 : false);
+}
+
+PageUpdate::PageUpdate(ConfigWizard *parent) :
+ ConfigWizardPage(parent, _(L("Automatic updates")), _(L("Updates"))),
+ version_check(true),
+ preset_update(true)
+{
+ const AppConfig *app_config = GUI::get_app_config();
+ auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
+ boldfont.SetWeight(wxFONTWEIGHT_BOLD);
+
+ 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);
+
+ 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(WRAP_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(); });
+}
+
+PageVendors::PageVendors(ConfigWizard *parent) :
+ ConfigWizardPage(parent, _(L("Other Vendors")), _(L("Other Vendors")))
+{
+ append_text(_(L("Pick another vendor supported by Slic3r PE:")));
+
+ 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_pair : wizard_p()->vendors) {
+ const auto &vendor = vendor_pair.second;
+ if (vendor.id == "PrusaResearch") { continue; }
+
+ auto *picker = new PrinterPicker(this, vendor, appconfig_vendors);
+ picker->Hide();
+ pickers.push_back(picker);
+ choices_vendors.Add(vendor.name);
+
+ 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();
+ });
+ }
+
+ 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);
+ }
+
+ 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();
+ wizard_p()->layout_fit();
+ }
+}
+
+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"))),
+ 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)
+{
+ auto sel = gcode_picker->GetSelection();
+ if (sel >= 0 && sel < gcode_opt.enum_labels.size()) {
+ auto *opt = new ConfigOptionEnum<GCodeFlavor>(static_cast<GCodeFlavor>(sel));
+ config.set_key_value("gcode_flavor", opt);
+ }
+}
+
+PageBedShape::PageBedShape(ConfigWizard *parent) :
+ 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<ConfigOptionPoints>("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 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<const ConfigOptionFloats*>(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<const ConfigOptionFloats*>(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("Extruder and Bed Temperatures")), _(L("Temperatures"))),
+ spin_extr(new wxSpinCtrlDouble(this, wxID_ANY)),
+ spin_bed(new wxSpinCtrlDouble(this, wxID_ANY))
+{
+ 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<const ConfigOptionInts*>(def_extr.default_value);
+ spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200);
+
+ 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<const ConfigOptionInts*>(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
+
+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());
+
+ 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)
+{
+ 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();
+ if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; }
+
+ wxPaintDC dc(this);
+
+ 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
+
+static const std::unordered_map<std::string, std::pair<std::string, std::string>> 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("MK2SMM", "0.4") },
+ { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") },
+ { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2SMM", "0.4") },
+ { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2SMM", "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";
+ 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") {
+ try {
+ auto vp = VendorProfile::from_ini(it->path());
+ vendors[vp.id] = std::move(vp);
+ }
+ catch (const std::exception& e) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Error loading vendor bundle %1%: %2%") % it->path() % e.what();
+ }
+
+ }
+ }
+
+ // 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()) {
+ try {
+ auto vp = VendorProfile::from_ini(it->path());
+ vendors_rsrc[vp.id] = it->path().filename().string();
+ vendors[vp.id] = std::move(vp);
+ }
+ catch (const std::exception& e) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Error loading vendor bundle %1%: %2%") % it->path() % e.what();
+ }
+ }
+ }
+ }
+
+ // 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()
+{
+ index->load_items(page_welcome);
+}
+
+void ConfigWizard::priv::add_page(ConfigWizardPage *page)
+{
+ hscroll_sizer->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);
+
+ layout_fit();
+}
+
+void ConfigWizard::priv::layout_fit()
+{
+ q->Layout();
+ q->Fit();
+}
+
+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);
+}
+
+void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const 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<std::string> 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
+ install_bundles.emplace_back(vendor_rsrc.second);
+ }
+
+ // Decide whether to create snapshot based on run_reason and the reset profile checkbox
+ bool snapshot = true;
+ switch (run_reason) {
+ case ConfigWizard::RR_DATA_EMPTY: snapshot = false; break;
+ case ConfigWizard::RR_DATA_LEGACY: snapshot = true; break;
+ case ConfigWizard::RR_DATA_INCOMPAT: snapshot = false; break; // In this case snapshot is done by PresetUpdater with the appropriate reason
+ case ConfigWizard::RR_USER: snapshot = page_welcome->reset_user_profile(); break;
+ }
+ if (install_bundles.size() > 0) {
+ // Install bundles from resources.
+ updater->install_bundles_rsrc(std::move(install_bundles), snapshot);
+ } else {
+ BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources";
+ }
+
+ if (page_welcome->reset_user_profile()) {
+ BOOST_LOG_TRIVIAL(info) << "Resetting user profiles...";
+ 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");
+ app_config->reset_selections();
+ preset_bundle->load_presets(*app_config);
+ } else {
+ 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);
+ }
+ // Update the selections from the compatibilty.
+ preset_bundle->export_selections(*app_config);
+}
+
+// Public
+
+ConfigWizard::ConfigWizard(wxWindow *parent, RunReason reason) :
+ wxDialog(parent, wxID_ANY, name(), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER),
+ p(new priv(this))
+{
+ p->run_reason = reason;
+
+ p->load_vendors();
+ p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({
+ "gcode_flavor", "bed_shape", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature",
+ }));
+
+ p->index = new ConfigWizardIndex(this);
+
+ auto *vsizer = new wxBoxSizer(wxVERTICAL);
+ auto *topsizer = new wxBoxSizer(wxHORIZONTAL);
+ auto *hline = new wxStaticLine(this);
+ p->btnsizer = new wxBoxSizer(wxHORIZONTAL);
+
+ // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling.
+ // Later, we compare that to the size of the current screen and set minimum width based on that (see below).
+ p->hscroll = new wxScrolledWindow(this);
+ p->hscroll_sizer = new wxBoxSizer(wxHORIZONTAL);
+ p->hscroll->SetSizer(p->hscroll_sizer);
+
+ topsizer->Add(p->index, 0, wxEXPAND);
+ topsizer->AddSpacer(INDEX_MARGIN);
+ topsizer->Add(p->hscroll, 1, wxEXPAND);
+
+ p->btn_prev = new wxButton(this, wxID_NONE, _(L("< &Back")));
+ p->btn_next = new wxButton(this, wxID_NONE, _(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, reason == RR_DATA_EMPTY || reason == RR_DATA_LEGACY));
+ 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(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);
+ SetSizer(vsizer);
+ SetSizerAndFit(vsizer);
+
+ // We can now enable scrolling on hscroll
+ p->hscroll->SetScrollRate(30, 30);
+ // Compare current ("ideal") wizard size with the size of the current screen.
+ // If the screen is smaller, resize wizrad to match, which will enable scrollbars.
+ auto wizard_size = GetSize();
+ unsigned width, height;
+ if (GUI::get_current_screen_size(this, width, height)) {
+ wizard_size.SetWidth(std::min(wizard_size.GetWidth(), (int)(width - 2 * DIALOG_MARGIN)));
+ wizard_size.SetHeight(std::min(wizard_size.GetHeight(), (int)(height - 2 * DIALOG_MARGIN)));
+ SetMinSize(wizard_size);
+ }
+ Fit();
+
+ 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->EndModal(wxID_OK); });
+}
+
+ConfigWizard::~ConfigWizard() {}
+
+bool ConfigWizard::run(PresetBundle *preset_bundle, const PresetUpdater *updater)
+{
+ BOOST_LOG_TRIVIAL(info) << "Running ConfigWizard, reason: " << p->run_reason;
+ if (ShowModal() == wxID_OK) {
+ auto *app_config = GUI::get_app_config();
+ p->apply_config(app_config, preset_bundle, updater);
+ app_config->set_legacy_datadir(false);
+ BOOST_LOG_TRIVIAL(info) << "ConfigWizard applied";
+ return true;
+ } else {
+ BOOST_LOG_TRIVIAL(info) << "ConfigWizard cancelled";
+ return false;
+ }
+}
+
+
+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/src/slic3r/GUI/ConfigWizard.hpp b/src/slic3r/GUI/ConfigWizard.hpp
new file mode 100644
index 000000000..73fce7cd2
--- /dev/null
+++ b/src/slic3r/GUI/ConfigWizard.hpp
@@ -0,0 +1,50 @@
+#ifndef slic3r_ConfigWizard_hpp_
+#define slic3r_ConfigWizard_hpp_
+
+#include <memory>
+
+#include <wx/dialog.h>
+
+namespace Slic3r {
+
+class PresetBundle;
+class PresetUpdater;
+
+namespace GUI {
+
+
+class ConfigWizard: public wxDialog
+{
+public:
+ // Why is the Wizard run
+ enum RunReason {
+ RR_DATA_EMPTY, // No or empty datadir
+ RR_DATA_LEGACY, // Pre-updating datadir
+ RR_DATA_INCOMPAT, // Incompatible datadir - Slic3r downgrade situation
+ RR_USER, // User requested the Wizard from the menus
+ };
+
+ ConfigWizard(wxWindow *parent, RunReason run_reason);
+ ConfigWizard(ConfigWizard &&) = delete;
+ ConfigWizard(const ConfigWizard &) = delete;
+ ConfigWizard &operator=(ConfigWizard &&) = delete;
+ ConfigWizard &operator=(const ConfigWizard &) = delete;
+ ~ConfigWizard();
+
+ // Run the Wizard. Return whether it was completed.
+ bool run(PresetBundle *preset_bundle, const PresetUpdater *updater);
+
+ static const wxString& name();
+private:
+ struct priv;
+ std::unique_ptr<priv> p;
+
+ friend class ConfigWizardPage;
+};
+
+
+
+}
+}
+
+#endif
diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp
new file mode 100644
index 000000000..2c8f23cd3
--- /dev/null
+++ b/src/slic3r/GUI/ConfigWizard_private.hpp
@@ -0,0 +1,241 @@
+#ifndef slic3r_ConfigWizard_private_hpp_
+#define slic3r_ConfigWizard_private_hpp_
+
+#include "ConfigWizard.hpp"
+
+#include <vector>
+#include <set>
+#include <unordered_map>
+#include <boost/filesystem.hpp>
+
+#include <wx/sizer.h>
+#include <wx/panel.h>
+#include <wx/button.h>
+#include <wx/choice.h>
+#include <wx/spinctrl.h>
+
+#include "libslic3r/PrintConfig.hpp"
+#include "slic3r/Utils/PresetUpdater.hpp"
+#include "AppConfig.hpp"
+#include "Preset.hpp"
+#include "BedShapeDialog.hpp"
+
+namespace fs = boost::filesystem;
+
+namespace Slic3r {
+namespace GUI {
+
+enum {
+ WRAP_WIDTH = 500,
+ MODEL_MIN_WRAP = 150,
+
+ DIALOG_MARGIN = 15,
+ INDEX_MARGIN = 40,
+ BTN_SPACING = 10,
+ INDENT_SPACING = 30,
+ VERTICAL_SPACING = 10,
+};
+
+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<Checkbox*> cboxes;
+ unsigned variants_checked;
+
+ PrinterPicker(wxWindow *parent, const VendorProfile &vendor, const AppConfig &appconfig_vendors);
+
+ void select_all(bool select);
+ void select_one(size_t i, bool select);
+ void on_checkbox(const Checkbox *cbox, bool checked);
+};
+
+struct ConfigWizardPage: wxPanel
+{
+ 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);
+
+ template<class T>
+ void append(T *thing, int proportion = 0, int flag = wxEXPAND|wxTOP|wxBOTTOM, int border = 10)
+ {
+ content->Add(thing, proportion, flag, border);
+ }
+
+ void append_text(wxString text);
+ 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() {}
+ virtual void apply_custom_config(DynamicPrintConfig &config) {}
+
+ void enable_next(bool enable);
+private:
+ ConfigWizardPage *p_prev;
+ ConfigWizardPage *p_next;
+};
+
+struct PageWelcome: ConfigWizardPage
+{
+ PrinterPicker *printer_picker;
+ wxPanel *others_buttons;
+ wxCheckBox *cbox_reset;
+
+ PageWelcome(ConfigWizard *parent, bool check_first_variant);
+
+ virtual wxPanel* extra_buttons() { return others_buttons; }
+ virtual void on_page_set();
+
+ bool reset_user_profile() const { return cbox_reset != nullptr ? cbox_reset->GetValue() : false; }
+ void on_variant_checked();
+};
+
+struct PageUpdate: ConfigWizardPage
+{
+ bool version_check;
+ bool preset_update;
+
+ PageUpdate(ConfigWizard *parent);
+};
+
+struct PageVendors: ConfigWizardPage
+{
+ std::vector<PrinterPicker*> pickers;
+
+ PageVendors(ConfigWizard *parent);
+
+ virtual void on_page_set();
+
+ void on_vendor_pick(size_t i);
+ void on_variant_checked();
+};
+
+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
+{
+ wxSpinCtrlDouble *spin_extr;
+ wxSpinCtrlDouble *spin_bed;
+
+ PageTemperatures(ConfigWizard *parent);
+ virtual void apply_custom_config(DynamicPrintConfig &config);
+};
+
+
+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<wxString> items;
+ std::vector<wxString>::const_iterator item_active;
+
+ void on_paint(wxPaintEvent &evt);
+};
+
+struct ConfigWizard::priv
+{
+ ConfigWizard *q;
+ ConfigWizard::RunReason run_reason;
+ AppConfig appconfig_vendors;
+ std::unordered_map<std::string, VendorProfile> vendors;
+ std::unordered_map<std::string, std::string> vendors_rsrc;
+ std::unique_ptr<DynamicPrintConfig> custom_config;
+
+ wxScrolledWindow *hscroll = nullptr;
+ wxBoxSizer *hscroll_sizer = 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 load_vendors();
+ 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);
+
+ void on_other_vendors();
+ void on_custom_setup();
+
+ void apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater);
+};
+
+
+
+}
+}
+
+#endif
diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp
new file mode 100644
index 000000000..f143e8bc6
--- /dev/null
+++ b/src/slic3r/GUI/Field.cpp
@@ -0,0 +1,784 @@
+#include "GUI.hpp"//"slic3r_gui.hpp"
+#include "Field.hpp"
+
+//#include <wx/event.h>
+#include <regex>
+#include <wx/numformatter.h>
+#include <wx/tooltip.h>
+#include "PrintConfig.hpp"
+#include <boost/algorithm/string/predicate.hpp>
+
+namespace Slic3r { namespace GUI {
+
+ wxString double_to_string(double const value)
+ {
+ if (value - int(value) == 0)
+ return wxString::Format(_T("%i"), int(value));
+ else {
+ int precision = 4;
+ for (size_t p = 1; p < 4; p++)
+ {
+ double cur_val = pow(10, p)*value;
+ if (cur_val - int(cur_val) == 0) {
+ precision = p;
+ break;
+ }
+ }
+ return wxNumberFormatter::ToString(value, precision, wxNumberFormatter::Style_None);
+ }
+ }
+
+ void Field::PostInitialize(){
+ auto color = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
+ m_Undo_btn = new MyButton(m_parent, wxID_ANY, "", wxDefaultPosition,wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER);
+ m_Undo_to_sys_btn = new MyButton(m_parent, wxID_ANY, "", wxDefaultPosition,wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER);
+ if (wxMSW) {
+ m_Undo_btn->SetBackgroundColour(color);
+ m_Undo_to_sys_btn->SetBackgroundColour(color);
+ }
+ m_Undo_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent){ on_back_to_initial_value(); }));
+ m_Undo_to_sys_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent){ on_back_to_sys_value(); }));
+
+ //set default bitmap
+ wxBitmap bmp;
+ bmp.LoadFile(from_u8(var("bullet_white.png")), wxBITMAP_TYPE_PNG);
+ set_undo_bitmap(&bmp);
+ set_undo_to_sys_bitmap(&bmp);
+
+ switch (m_opt.type)
+ {
+ case coPercents:
+ case coFloats:
+ case coStrings:
+ case coBools:
+ case coInts: {
+ auto tag_pos = m_opt_id.find("#");
+ if (tag_pos != std::string::npos)
+ m_opt_idx = stoi(m_opt_id.substr(tag_pos + 1, m_opt_id.size()));
+ break;
+ }
+ default:
+ break;
+ }
+
+ BUILD();
+ }
+
+ void Field::on_kill_focus(wxEvent& event) {
+ // Without this, there will be nasty focus bugs on Windows.
+ // Also, docs for wxEvent::Skip() say "In general, it is recommended to skip all
+ // non-command events to allow the default handling to take place."
+ event.Skip();
+ // call the registered function if it is available
+ if (m_on_kill_focus!=nullptr)
+ m_on_kill_focus();
+ }
+ void Field::on_change_field()
+ {
+// std::cerr << "calling Field::_on_change \n";
+ if (m_on_change != nullptr && !m_disable_change_event)
+ m_on_change(m_opt_id, get_value());
+ }
+
+ void Field::on_back_to_initial_value(){
+ if (m_back_to_initial_value != nullptr && m_is_modified_value)
+ m_back_to_initial_value(m_opt_id);
+ }
+
+ void Field::on_back_to_sys_value(){
+ if (m_back_to_sys_value != nullptr && m_is_nonsys_value)
+ m_back_to_sys_value(m_opt_id);
+ }
+
+ wxString Field::get_tooltip_text(const wxString& default_string)
+ {
+ wxString tooltip_text("");
+ wxString tooltip = _(m_opt.tooltip);
+ if (tooltip.length() > 0)
+ tooltip_text = tooltip + "\n" + _(L("default value")) + "\t: " +
+ (boost::iends_with(m_opt_id, "_gcode") ? "\n" : "") + default_string +
+ (boost::iends_with(m_opt_id, "_gcode") ? "" : "\n") +
+ _(L("parameter name")) + "\t: " + m_opt_id;
+
+ return tooltip_text;
+ }
+
+ bool Field::is_matched(const std::string& string, const std::string& pattern)
+ {
+ std::regex regex_pattern(pattern, std::regex_constants::icase); // use ::icase to make the matching case insensitive like /i in perl
+ return std::regex_match(string, regex_pattern);
+ }
+
+ void Field::get_value_by_opt_type(wxString& str)
+ {
+ switch (m_opt.type){
+ case coInt:
+ m_value = wxAtoi(str);
+ break;
+ case coPercent:
+ case coPercents:
+ case coFloats:
+ case coFloat:{
+ if (m_opt.type == coPercent && str.Last() == '%')
+ str.RemoveLast();
+ else if (str.Last() == '%') {
+ wxString label = m_Label->GetLabel();
+ if (label.Last() == '\n') label.RemoveLast();
+ while (label.Last() == ' ') label.RemoveLast();
+ if (label.Last() == ':') label.RemoveLast();
+ show_error(m_parent, wxString::Format(_(L("%s doesn't support percentage")), label));
+ set_value(double_to_string(m_opt.min), true);
+ m_value = double(m_opt.min);
+ break;
+ }
+ double val;
+ if(!str.ToCDouble(&val))
+ {
+ show_error(m_parent, _(L("Input value contains incorrect symbol(s).\nUse, please, only digits")));
+ set_value(double_to_string(val), true);
+ }
+ if (m_opt.min > val || val > m_opt.max)
+ {
+ show_error(m_parent, _(L("Input value is out of range")));
+ if (m_opt.min > val) val = m_opt.min;
+ if (val > m_opt.max) val = m_opt.max;
+ set_value(double_to_string(val), true);
+ }
+ m_value = val;
+ break; }
+ case coString:
+ case coStrings:
+ case coFloatOrPercent:
+ m_value = str.ToStdString();
+ break;
+ default:
+ break;
+ }
+ }
+
+ void TextCtrl::BUILD() {
+ auto size = wxSize(wxDefaultSize);
+ if (m_opt.height >= 0) size.SetHeight(m_opt.height);
+ if (m_opt.width >= 0) size.SetWidth(m_opt.width);
+
+ wxString text_value = wxString("");
+
+ switch (m_opt.type) {
+ case coFloatOrPercent:
+ {
+ text_value = double_to_string(m_opt.default_value->getFloat());
+ if (static_cast<const ConfigOptionFloatOrPercent*>(m_opt.default_value)->percent)
+ text_value += "%";
+ break;
+ }
+ case coPercent:
+ {
+ text_value = wxString::Format(_T("%i"), int(m_opt.default_value->getFloat()));
+ text_value += "%";
+ break;
+ }
+ case coPercents:
+ case coFloats:
+ case coFloat:
+ {
+ double val = m_opt.type == coFloats ?
+ static_cast<const ConfigOptionFloats*>(m_opt.default_value)->get_at(m_opt_idx) :
+ m_opt.type == coFloat ?
+ m_opt.default_value->getFloat() :
+ static_cast<const ConfigOptionPercents*>(m_opt.default_value)->get_at(m_opt_idx);
+ text_value = double_to_string(val);
+ break;
+ }
+ case coString:
+ text_value = static_cast<const ConfigOptionString*>(m_opt.default_value)->value;
+ break;
+ case coStrings:
+ {
+ const ConfigOptionStrings *vec = static_cast<const ConfigOptionStrings*>(m_opt.default_value);
+ if (vec == nullptr || vec->empty()) break; //for the case of empty default value
+ text_value = vec->get_at(m_opt_idx);
+ break;
+ }
+ default:
+ break;
+ }
+
+ auto temp = new wxTextCtrl(m_parent, wxID_ANY, text_value, wxDefaultPosition, size, (m_opt.multiline ? wxTE_MULTILINE : 0));
+
+ temp->SetToolTip(get_tooltip_text(text_value));
+
+ temp->Bind(wxEVT_LEFT_DOWN, ([temp](wxEvent& event)
+ {
+ //! to allow the default handling
+ event.Skip();
+ //! eliminating the g-code pop up text description
+ bool flag = false;
+#ifdef __WXGTK__
+ // I have no idea why, but on GTK flag works in other way
+ flag = true;
+#endif // __WXGTK__
+ temp->GetToolTip()->Enable(flag);
+ }), temp->GetId());
+
+#if !defined(__WXGTK__)
+ temp->Bind(wxEVT_KILL_FOCUS, ([this, temp](wxEvent& e)
+ {
+ e.Skip();// on_kill_focus(e);
+ temp->GetToolTip()->Enable(true);
+ }), temp->GetId());
+#endif // __WXGTK__
+
+ temp->Bind(wxEVT_TEXT, ([this](wxCommandEvent& evt)
+ {
+#ifdef __WXGTK__
+ if (bChangedValueEvent)
+#endif //__WXGTK__
+ on_change_field();
+ }), temp->GetId());
+
+#ifdef __WXGTK__
+ // to correct value updating on GTK we should:
+ // call on_change_field() on wxEVT_KEY_UP instead of wxEVT_TEXT
+ // and prevent value updating on wxEVT_KEY_DOWN
+ temp->Bind(wxEVT_KEY_DOWN, &TextCtrl::change_field_value, this);
+ temp->Bind(wxEVT_KEY_UP, &TextCtrl::change_field_value, this);
+#endif //__WXGTK__
+
+ // select all text using Ctrl+A
+ temp->Bind(wxEVT_CHAR, ([temp](wxKeyEvent& event)
+ {
+ if (wxGetKeyState(wxKeyCode('A')) && wxGetKeyState(WXK_CONTROL))
+ temp->SetSelection(-1, -1); //select all
+ event.Skip();
+ }));
+
+ // recast as a wxWindow to fit the calling convention
+ window = dynamic_cast<wxWindow*>(temp);
+ }
+
+ boost::any& TextCtrl::get_value()
+ {
+ wxString ret_str = static_cast<wxTextCtrl*>(window)->GetValue();
+ get_value_by_opt_type(ret_str);
+
+ return m_value;
+ }
+
+ void TextCtrl::enable() { dynamic_cast<wxTextCtrl*>(window)->Enable(); dynamic_cast<wxTextCtrl*>(window)->SetEditable(true); }
+ void TextCtrl::disable() { dynamic_cast<wxTextCtrl*>(window)->Disable(); dynamic_cast<wxTextCtrl*>(window)->SetEditable(false); }
+
+#ifdef __WXGTK__
+ void TextCtrl::change_field_value(wxEvent& event)
+ {
+ if (bChangedValueEvent = event.GetEventType()==wxEVT_KEY_UP)
+ on_change_field();
+ event.Skip();
+ };
+#endif //__WXGTK__
+
+void CheckBox::BUILD() {
+ auto size = wxSize(wxDefaultSize);
+ if (m_opt.height >= 0) size.SetHeight(m_opt.height);
+ if (m_opt.width >= 0) size.SetWidth(m_opt.width);
+
+ bool check_value = m_opt.type == coBool ?
+ m_opt.default_value->getBool() : m_opt.type == coBools ?
+ static_cast<const ConfigOptionBools*>(m_opt.default_value)->get_at(m_opt_idx) :
+ false;
+
+ auto temp = new wxCheckBox(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size);
+ temp->SetValue(check_value);
+ if (m_opt.readonly) temp->Disable();
+
+ temp->Bind(wxEVT_CHECKBOX, ([this](wxCommandEvent e) { on_change_field(); }), temp->GetId());
+
+ temp->SetToolTip(get_tooltip_text(check_value ? "true" : "false"));
+
+ // recast as a wxWindow to fit the calling convention
+ window = dynamic_cast<wxWindow*>(temp);
+}
+
+boost::any& CheckBox::get_value()
+{
+// boost::any m_value;
+ bool value = dynamic_cast<wxCheckBox*>(window)->GetValue();
+ if (m_opt.type == coBool)
+ m_value = static_cast<bool>(value);
+ else
+ m_value = static_cast<unsigned char>(value);
+ return m_value;
+}
+
+int undef_spin_val = -9999; //! Probably, It's not necessary
+
+void SpinCtrl::BUILD() {
+ auto size = wxSize(wxDefaultSize);
+ if (m_opt.height >= 0) size.SetHeight(m_opt.height);
+ if (m_opt.width >= 0) size.SetWidth(m_opt.width);
+
+ wxString text_value = wxString("");
+ int default_value = 0;
+
+ switch (m_opt.type) {
+ case coInt:
+ default_value = m_opt.default_value->getInt();
+ text_value = wxString::Format(_T("%i"), default_value);
+ break;
+ case coInts:
+ {
+ const ConfigOptionInts *vec = static_cast<const ConfigOptionInts*>(m_opt.default_value);
+ if (vec == nullptr || vec->empty()) break;
+ for (size_t id = 0; id < vec->size(); ++id)
+ {
+ default_value = vec->get_at(id);
+ text_value += wxString::Format(_T("%i"), default_value);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ const int min_val = m_opt.min == INT_MIN ? 0: m_opt.min;
+ const int max_val = m_opt.max < 2147483647 ? m_opt.max : 2147483647;
+
+ auto temp = new wxSpinCtrl(m_parent, wxID_ANY, text_value, wxDefaultPosition, size,
+ 0, min_val, max_val, default_value);
+
+// temp->Bind(wxEVT_SPINCTRL, ([this](wxCommandEvent e) { tmp_value = undef_spin_val; on_change_field(); }), temp->GetId());
+// temp->Bind(wxEVT_KILL_FOCUS, ([this](wxEvent& e) { tmp_value = undef_spin_val; on_kill_focus(e); }), temp->GetId());
+ temp->Bind(wxEVT_TEXT, ([this](wxCommandEvent e)
+ {
+// # On OSX / Cocoa, wxSpinCtrl::GetValue() doesn't return the new value
+// # when it was changed from the text control, so the on_change callback
+// # gets the old one, and on_kill_focus resets the control to the old value.
+// # As a workaround, we get the new value from $event->GetString and store
+// # here temporarily so that we can return it from $self->get_value
+ std::string value = e.GetString().utf8_str().data();
+ if (is_matched(value, "^\\d+$"))
+ tmp_value = std::stoi(value);
+ on_change_field();
+// # We don't reset tmp_value here because _on_change might put callbacks
+// # in the CallAfter queue, and we want the tmp value to be available from
+// # them as well.
+ }), temp->GetId());
+
+ temp->SetToolTip(get_tooltip_text(text_value));
+
+ // recast as a wxWindow to fit the calling convention
+ window = dynamic_cast<wxWindow*>(temp);
+}
+
+void Choice::BUILD() {
+ auto size = wxSize(wxDefaultSize);
+ if (m_opt.height >= 0) size.SetHeight(m_opt.height);
+ if (m_opt.width >= 0) size.SetWidth(m_opt.width);
+
+ wxComboBox* temp;
+ if (!m_opt.gui_type.empty() && m_opt.gui_type.compare("select_open") != 0)
+ temp = new wxComboBox(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size);
+ else
+ temp = new wxComboBox(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, NULL, wxCB_READONLY);
+
+ // recast as a wxWindow to fit the calling convention
+ window = dynamic_cast<wxWindow*>(temp);
+
+ if (m_opt.enum_labels.empty() && m_opt.enum_values.empty()){
+ }
+ else{
+ for (auto el : m_opt.enum_labels.empty() ? m_opt.enum_values : m_opt.enum_labels){
+ const wxString& str = _(el);//m_opt_id == "support" ? _(el) : el;
+ temp->Append(str);
+ }
+ set_selection();
+ }
+ temp->Bind(wxEVT_TEXT, ([this](wxCommandEvent e) { on_change_field(); }), temp->GetId());
+ temp->Bind(wxEVT_COMBOBOX, ([this](wxCommandEvent e) { on_change_field(); }), temp->GetId());
+
+ temp->SetToolTip(get_tooltip_text(temp->GetValue()));
+}
+
+void Choice::set_selection()
+{
+ wxString text_value = wxString("");
+ switch (m_opt.type){
+ case coFloat:
+ case coPercent: {
+ double val = m_opt.default_value->getFloat();
+ text_value = val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 1);
+ size_t idx = 0;
+ for (auto el : m_opt.enum_values)
+ {
+ if (el.compare(text_value) == 0)
+ break;
+ ++idx;
+ }
+// if (m_opt.type == coPercent) text_value += "%";
+ idx == m_opt.enum_values.size() ?
+ dynamic_cast<wxComboBox*>(window)->SetValue(text_value) :
+ dynamic_cast<wxComboBox*>(window)->SetSelection(idx);
+ break;
+ }
+ case coEnum:{
+ int id_value = static_cast<const ConfigOptionEnum<SeamPosition>*>(m_opt.default_value)->value; //!!
+ dynamic_cast<wxComboBox*>(window)->SetSelection(id_value);
+ break;
+ }
+ case coInt:{
+ int val = m_opt.default_value->getInt(); //!!
+ text_value = wxString::Format(_T("%i"), int(val));
+ size_t idx = 0;
+ for (auto el : m_opt.enum_values)
+ {
+ if (el.compare(text_value) == 0)
+ break;
+ ++idx;
+ }
+ idx == m_opt.enum_values.size() ?
+ dynamic_cast<wxComboBox*>(window)->SetValue(text_value) :
+ dynamic_cast<wxComboBox*>(window)->SetSelection(idx);
+ break;
+ }
+ case coStrings:{
+ text_value = static_cast<const ConfigOptionStrings*>(m_opt.default_value)->get_at(m_opt_idx);
+
+ size_t idx = 0;
+ for (auto el : m_opt.enum_values)
+ {
+ if (el.compare(text_value) == 0)
+ break;
+ ++idx;
+ }
+ idx == m_opt.enum_values.size() ?
+ dynamic_cast<wxComboBox*>(window)->SetValue(text_value) :
+ dynamic_cast<wxComboBox*>(window)->SetSelection(idx);
+ break;
+ }
+ }
+}
+
+void Choice::set_value(const std::string& value, bool change_event) //! Redundant?
+{
+ m_disable_change_event = !change_event;
+
+ size_t idx=0;
+ for (auto el : m_opt.enum_values)
+ {
+ if (el.compare(value) == 0)
+ break;
+ ++idx;
+ }
+
+ idx == m_opt.enum_values.size() ?
+ dynamic_cast<wxComboBox*>(window)->SetValue(value) :
+ dynamic_cast<wxComboBox*>(window)->SetSelection(idx);
+
+ m_disable_change_event = false;
+}
+
+void Choice::set_value(const boost::any& value, bool change_event)
+{
+ m_disable_change_event = !change_event;
+
+ switch (m_opt.type){
+ case coInt:
+ case coFloat:
+ case coPercent:
+ case coString:
+ case coStrings:{
+ wxString text_value;
+ if (m_opt.type == coInt)
+ text_value = wxString::Format(_T("%i"), int(boost::any_cast<int>(value)));
+ else
+ text_value = boost::any_cast<wxString>(value);
+ auto idx = 0;
+ for (auto el : m_opt.enum_values)
+ {
+ if (el.compare(text_value) == 0)
+ break;
+ ++idx;
+ }
+ idx == m_opt.enum_values.size() ?
+ dynamic_cast<wxComboBox*>(window)->SetValue(text_value) :
+ dynamic_cast<wxComboBox*>(window)->SetSelection(idx);
+ break;
+ }
+ case coEnum:{
+ int val = boost::any_cast<int>(value);
+ if (m_opt_id.compare("external_fill_pattern") == 0)
+ {
+ if (!m_opt.enum_values.empty()){
+ std::string key;
+ t_config_enum_values map_names = ConfigOptionEnum<InfillPattern>::get_enum_values();
+ for (auto it : map_names) {
+ if (val == it.second) {
+ key = it.first;
+ break;
+ }
+ }
+
+ size_t idx = 0;
+ for (auto el : m_opt.enum_values)
+ {
+ if (el.compare(key) == 0)
+ break;
+ ++idx;
+ }
+
+ val = idx == m_opt.enum_values.size() ? 0 : idx;
+ }
+ else
+ val = 0;
+ }
+ dynamic_cast<wxComboBox*>(window)->SetSelection(val);
+ break;
+ }
+ default:
+ break;
+ }
+
+ m_disable_change_event = false;
+}
+
+//! it's needed for _update_serial_ports()
+void Choice::set_values(const std::vector<std::string>& values)
+{
+ if (values.empty())
+ return;
+ m_disable_change_event = true;
+
+// # it looks that Clear() also clears the text field in recent wxWidgets versions,
+// # but we want to preserve it
+ auto ww = dynamic_cast<wxComboBox*>(window);
+ auto value = ww->GetValue();
+ ww->Clear();
+ ww->Append("");
+ for (auto el : values)
+ ww->Append(wxString(el));
+ ww->SetValue(value);
+
+ m_disable_change_event = false;
+}
+
+boost::any& Choice::get_value()
+{
+// boost::any m_value;
+ wxString ret_str = static_cast<wxComboBox*>(window)->GetValue();
+
+ // options from right panel
+ std::vector <std::string> right_panel_options{ "support", "scale_unit" };
+ for (auto rp_option: right_panel_options)
+ if (m_opt_id == rp_option)
+ return m_value = boost::any(ret_str);
+
+ if (m_opt.type != coEnum)
+ /*m_value = */get_value_by_opt_type(ret_str);
+ else
+ {
+ int ret_enum = static_cast<wxComboBox*>(window)->GetSelection();
+ if (m_opt_id.compare("external_fill_pattern") == 0)
+ {
+ if (!m_opt.enum_values.empty()){
+ std::string key = m_opt.enum_values[ret_enum];
+ t_config_enum_values map_names = ConfigOptionEnum<InfillPattern>::get_enum_values();
+ int value = map_names.at(key);
+
+ m_value = static_cast<InfillPattern>(value);
+ }
+ else
+ m_value = static_cast<InfillPattern>(0);
+ }
+ if (m_opt_id.compare("fill_pattern") == 0)
+ m_value = static_cast<InfillPattern>(ret_enum);
+ else if (m_opt_id.compare("gcode_flavor") == 0)
+ m_value = static_cast<GCodeFlavor>(ret_enum);
+ else if (m_opt_id.compare("support_material_pattern") == 0)
+ m_value = static_cast<SupportMaterialPattern>(ret_enum);
+ else if (m_opt_id.compare("seam_position") == 0)
+ m_value = static_cast<SeamPosition>(ret_enum);
+ else if (m_opt_id.compare("host_type") == 0)
+ m_value = static_cast<PrintHostType>(ret_enum);
+ }
+
+ return m_value;
+}
+
+void ColourPicker::BUILD()
+{
+ auto size = wxSize(wxDefaultSize);
+ if (m_opt.height >= 0) size.SetHeight(m_opt.height);
+ if (m_opt.width >= 0) size.SetWidth(m_opt.width);
+
+ wxString clr(static_cast<const ConfigOptionStrings*>(m_opt.default_value)->get_at(m_opt_idx));
+ auto temp = new wxColourPickerCtrl(m_parent, wxID_ANY, clr, wxDefaultPosition, size);
+
+ // // recast as a wxWindow to fit the calling convention
+ window = dynamic_cast<wxWindow*>(temp);
+
+ temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([this](wxCommandEvent e) { on_change_field(); }), temp->GetId());
+
+ temp->SetToolTip(get_tooltip_text(clr));
+}
+
+boost::any& ColourPicker::get_value(){
+// boost::any m_value;
+
+ auto colour = static_cast<wxColourPickerCtrl*>(window)->GetColour();
+ auto clr_str = wxString::Format(wxT("#%02X%02X%02X"), colour.Red(), colour.Green(), colour.Blue());
+ m_value = clr_str.ToStdString();
+
+ return m_value;
+}
+
+void PointCtrl::BUILD()
+{
+ auto size = wxSize(wxDefaultSize);
+ if (m_opt.height >= 0) size.SetHeight(m_opt.height);
+ if (m_opt.width >= 0) size.SetWidth(m_opt.width);
+
+ auto temp = new wxBoxSizer(wxHORIZONTAL);
+ // $self->wxSizer($sizer);
+ //
+ wxSize field_size(40, -1);
+
+ auto default_pt = static_cast<const ConfigOptionPoints*>(m_opt.default_value)->values.at(0);
+ double val = default_pt(0);
+ wxString X = val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 2, wxNumberFormatter::Style_None);
+ val = default_pt(1);
+ wxString Y = val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 2, wxNumberFormatter::Style_None);
+
+ x_textctrl = new wxTextCtrl(m_parent, wxID_ANY, X, wxDefaultPosition, field_size);
+ y_textctrl = new wxTextCtrl(m_parent, wxID_ANY, Y, wxDefaultPosition, field_size);
+
+ temp->Add(new wxStaticText(m_parent, wxID_ANY, "x : "), 0, wxALIGN_CENTER_VERTICAL, 0);
+ temp->Add(x_textctrl);
+ temp->Add(new wxStaticText(m_parent, wxID_ANY, " y : "), 0, wxALIGN_CENTER_VERTICAL, 0);
+ temp->Add(y_textctrl);
+
+ x_textctrl->Bind(wxEVT_TEXT, ([this](wxCommandEvent e) { on_change_field(); }), x_textctrl->GetId());
+ y_textctrl->Bind(wxEVT_TEXT, ([this](wxCommandEvent e) { on_change_field(); }), y_textctrl->GetId());
+
+ // // recast as a wxWindow to fit the calling convention
+ sizer = dynamic_cast<wxSizer*>(temp);
+
+ x_textctrl->SetToolTip(get_tooltip_text(X+", "+Y));
+ y_textctrl->SetToolTip(get_tooltip_text(X+", "+Y));
+}
+
+void PointCtrl::set_value(const Vec2d& value, bool change_event)
+{
+ m_disable_change_event = !change_event;
+
+ double val = value(0);
+ x_textctrl->SetValue(val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 2, wxNumberFormatter::Style_None));
+ val = value(1);
+ y_textctrl->SetValue(val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 2, wxNumberFormatter::Style_None));
+
+ m_disable_change_event = false;
+}
+
+void PointCtrl::set_value(const boost::any& value, bool change_event)
+{
+ Vec2d pt(Vec2d::Zero());
+ const Vec2d *ptf = boost::any_cast<Vec2d>(&value);
+ if (!ptf)
+ {
+ ConfigOptionPoints* pts = boost::any_cast<ConfigOptionPoints*>(value);
+ pt = pts->values.at(0);
+ }
+ else
+ pt = *ptf;
+ set_value(pt, change_event);
+}
+
+boost::any& PointCtrl::get_value()
+{
+ double x, y;
+ x_textctrl->GetValue().ToDouble(&x);
+ y_textctrl->GetValue().ToDouble(&y);
+ return m_value = Vec2d(x, y);
+}
+
+void StaticText::BUILD()
+{
+ auto size = wxSize(wxDefaultSize);
+ if (m_opt.height >= 0) size.SetHeight(m_opt.height);
+ if (m_opt.width >= 0) size.SetWidth(m_opt.width);
+
+ wxString legend(static_cast<const ConfigOptionString*>(m_opt.default_value)->value);
+ auto temp = new wxStaticText(m_parent, wxID_ANY, legend, wxDefaultPosition, size);
+ temp->SetFont(bold_font());
+
+ // // recast as a wxWindow to fit the calling convention
+ window = dynamic_cast<wxWindow*>(temp);
+
+ temp->SetToolTip(get_tooltip_text(legend));
+}
+
+void SliderCtrl::BUILD()
+{
+ auto size = wxSize(wxDefaultSize);
+ if (m_opt.height >= 0) size.SetHeight(m_opt.height);
+ if (m_opt.width >= 0) size.SetWidth(m_opt.width);
+
+ auto temp = new wxBoxSizer(wxHORIZONTAL);
+
+ auto def_val = static_cast<const ConfigOptionInt*>(m_opt.default_value)->value;
+ auto min = m_opt.min == INT_MIN ? 0 : m_opt.min;
+ auto max = m_opt.max == INT_MAX ? 100 : m_opt.max;
+
+ m_slider = new wxSlider(m_parent, wxID_ANY, def_val * m_scale,
+ min * m_scale, max * m_scale,
+ wxDefaultPosition, size);
+ wxSize field_size(40, -1);
+
+ m_textctrl = new wxTextCtrl(m_parent, wxID_ANY, wxString::Format("%d", m_slider->GetValue()/m_scale),
+ wxDefaultPosition, field_size);
+
+ temp->Add(m_slider, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL, 0);
+ temp->Add(m_textctrl, 0, wxALIGN_CENTER_VERTICAL, 0);
+
+ m_slider->Bind(wxEVT_SLIDER, ([this](wxCommandEvent e) {
+ if (!m_disable_change_event){
+ int val = boost::any_cast<int>(get_value());
+ m_textctrl->SetLabel(wxString::Format("%d", val));
+ on_change_field();
+ }
+ }), m_slider->GetId());
+
+ m_textctrl->Bind(wxEVT_TEXT, ([this](wxCommandEvent e) {
+ std::string value = e.GetString().utf8_str().data();
+ if (is_matched(value, "^-?\\d+(\\.\\d*)?$")){
+ m_disable_change_event = true;
+ m_slider->SetValue(stoi(value)*m_scale);
+ m_disable_change_event = false;
+ on_change_field();
+ }
+ }), m_textctrl->GetId());
+
+ m_sizer = dynamic_cast<wxSizer*>(temp);
+}
+
+void SliderCtrl::set_value(const boost::any& value, bool change_event)
+{
+ m_disable_change_event = !change_event;
+
+ m_slider->SetValue(boost::any_cast<int>(value)*m_scale);
+ int val = boost::any_cast<int>(get_value());
+ m_textctrl->SetLabel(wxString::Format("%d", val));
+
+ m_disable_change_event = false;
+}
+
+boost::any& SliderCtrl::get_value()
+{
+// int ret_val;
+// x_textctrl->GetValue().ToDouble(&val);
+ return m_value = int(m_slider->GetValue()/m_scale);
+}
+
+
+} // GUI
+} // Slic3r
+
+
diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp
new file mode 100644
index 000000000..c38658e2b
--- /dev/null
+++ b/src/slic3r/GUI/Field.hpp
@@ -0,0 +1,466 @@
+#ifndef SLIC3R_GUI_FIELD_HPP
+#define SLIC3R_GUI_FIELD_HPP
+
+#include <wx/wxprec.h>
+#ifndef WX_PRECOMP
+ #include <wx/wx.h>
+#endif
+
+#include <memory>
+#include <functional>
+#include <boost/any.hpp>
+
+#include <wx/spinctrl.h>
+#include <wx/clrpicker.h>
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/Config.hpp"
+
+//#include "slic3r_gui.hpp"
+#include "GUI.hpp"
+#include "Utils.hpp"
+
+#ifdef __WXMSW__
+#define wxMSW true
+#else
+#define wxMSW false
+#endif
+
+namespace Slic3r { namespace GUI {
+
+class Field;
+using t_field = std::unique_ptr<Field>;
+using t_kill_focus = std::function<void()>;
+using t_change = std::function<void(t_config_option_key, const boost::any&)>;
+using t_back_to_init = std::function<void(const std::string&)>;
+
+wxString double_to_string(double const value);
+
+class MyButton : public wxButton
+{
+ bool hidden = false; // never show button if it's hidden ones
+public:
+ MyButton() {}
+ MyButton(wxWindow* parent, wxWindowID id, const wxString& label = wxEmptyString,
+ const wxPoint& pos = wxDefaultPosition,
+ const wxSize& size = wxDefaultSize, long style = 0,
+ const wxValidator& validator = wxDefaultValidator,
+ const wxString& name = wxTextCtrlNameStr)
+ {
+ this->Create(parent, id, label, pos, size, style, validator, name);
+ }
+
+ // overridden from wxWindow base class
+ virtual bool
+ AcceptsFocusFromKeyboard() const { return false; }
+
+ virtual bool Show(bool show = true) override {
+ if (!show)
+ hidden = true;
+ return wxButton::Show(!hidden);
+ }
+};
+
+class Field {
+protected:
+ // factory function to defer and enforce creation of derived type.
+ virtual void PostInitialize();
+
+ /// Finish constructing the Field's wxWidget-related properties, including setting its own sizer, etc.
+ virtual void BUILD() = 0;
+
+ /// Call the attached on_kill_focus method.
+ //! It's important to use wxEvent instead of wxFocusEvent,
+ //! in another case we can't unfocused control at all
+ void on_kill_focus(wxEvent& event);
+ /// Call the attached on_change method.
+ void on_change_field();
+ /// Call the attached m_back_to_initial_value method.
+ void on_back_to_initial_value();
+ /// Call the attached m_back_to_sys_value method.
+ void on_back_to_sys_value();
+
+public:
+ /// parent wx item, opportunity to refactor (probably not necessary - data duplication)
+ wxWindow* m_parent {nullptr};
+
+ /// Function object to store callback passed in from owning object.
+ t_kill_focus m_on_kill_focus {nullptr};
+
+ /// Function object to store callback passed in from owning object.
+ t_change m_on_change {nullptr};
+
+ /// Function object to store callback passed in from owning object.
+ t_back_to_init m_back_to_initial_value{ nullptr };
+ t_back_to_init m_back_to_sys_value{ nullptr };
+
+ // This is used to avoid recursive invocation of the field change/update by wxWidgets.
+ bool m_disable_change_event {false};
+ bool m_is_modified_value {false};
+ bool m_is_nonsys_value {true};
+
+ /// Copy of ConfigOption for deduction purposes
+ const ConfigOptionDef m_opt {ConfigOptionDef()};
+ const t_config_option_key m_opt_id;//! {""};
+ int m_opt_idx = 0;
+
+ /// Sets a value for this control.
+ /// subclasses should overload with a specific version
+ /// Postcondition: Method does not fire the on_change event.
+ virtual void set_value(const boost::any& value, bool change_event) = 0;
+
+ /// Gets a boost::any representing this control.
+ /// subclasses should overload with a specific version
+ virtual boost::any& get_value() = 0;
+
+ virtual void enable() = 0;
+ virtual void disable() = 0;
+
+ /// Fires the enable or disable function, based on the input.
+ inline void toggle(bool en) { en ? enable() : disable(); }
+
+ virtual wxString get_tooltip_text(const wxString& default_string);
+
+ // set icon to "UndoToSystemValue" button according to an inheritance of preset
+// void set_nonsys_btn_icon(const wxBitmap& icon);
+
+ Field(const ConfigOptionDef& opt, const t_config_option_key& id) : m_opt(opt), m_opt_id(id) {};
+ Field(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : m_parent(parent), m_opt(opt), m_opt_id(id) {};
+
+ /// If you don't know what you are getting back, check both methods for nullptr.
+ virtual wxSizer* getSizer() { return nullptr; }
+ virtual wxWindow* getWindow() { return nullptr; }
+
+ bool is_matched(const std::string& string, const std::string& pattern);
+ void get_value_by_opt_type(wxString& str);
+
+ /// Factory method for generating new derived classes.
+ template<class T>
+ static t_field Create(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) // interface for creating shared objects
+ {
+ auto p = Slic3r::make_unique<T>(parent, opt, id);
+ p->PostInitialize();
+ return std::move(p); //!p;
+ }
+
+ bool set_undo_bitmap(const wxBitmap *bmp) {
+ if (m_undo_bitmap != bmp) {
+ m_undo_bitmap = bmp;
+ m_Undo_btn->SetBitmap(*bmp);
+ return true;
+ }
+ return false;
+ }
+
+ bool set_undo_to_sys_bitmap(const wxBitmap *bmp) {
+ if (m_undo_to_sys_bitmap != bmp) {
+ m_undo_to_sys_bitmap = bmp;
+ m_Undo_to_sys_btn->SetBitmap(*bmp);
+ return true;
+ }
+ return false;
+ }
+
+ bool set_label_colour(const wxColour *clr) {
+ if (m_Label == nullptr) return false;
+ if (m_label_color != clr) {
+ m_label_color = clr;
+ m_Label->SetForegroundColour(*clr);
+ m_Label->Refresh(true);
+ }
+ return false;
+ }
+
+ bool set_label_colour_force(const wxColour *clr) {
+ if (m_Label == nullptr) return false;
+ m_Label->SetForegroundColour(*clr);
+ m_Label->Refresh(true);
+ return false;
+ }
+
+ bool set_undo_tooltip(const wxString *tip) {
+ if (m_undo_tooltip != tip) {
+ m_undo_tooltip = tip;
+ m_Undo_btn->SetToolTip(*tip);
+ return true;
+ }
+ return false;
+ }
+
+ bool set_undo_to_sys_tooltip(const wxString *tip) {
+ if (m_undo_to_sys_tooltip != tip) {
+ m_undo_to_sys_tooltip = tip;
+ m_Undo_to_sys_btn->SetToolTip(*tip);
+ return true;
+ }
+ return false;
+ }
+
+ void set_side_text_ptr(wxStaticText* side_text) {
+ m_side_text = side_text;
+ }
+
+protected:
+ MyButton* m_Undo_btn = nullptr;
+ // Bitmap and Tooltip text for m_Undo_btn. The wxButton will be updated only if the new wxBitmap pointer differs from the currently rendered one.
+ const wxBitmap* m_undo_bitmap = nullptr;
+ const wxString* m_undo_tooltip = nullptr;
+ MyButton* m_Undo_to_sys_btn = nullptr;
+ // Bitmap and Tooltip text for m_Undo_to_sys_btn. The wxButton will be updated only if the new wxBitmap pointer differs from the currently rendered one.
+ const wxBitmap* m_undo_to_sys_bitmap = nullptr;
+ const wxString* m_undo_to_sys_tooltip = nullptr;
+
+ wxStaticText* m_Label = nullptr;
+ // Color for Label. The wxColour will be updated only if the new wxColour pointer differs from the currently rendered one.
+ const wxColour* m_label_color = nullptr;
+
+ wxStaticText* m_side_text = nullptr;
+
+ // current value
+ boost::any m_value;
+
+ friend class OptionsGroup;
+};
+
+/// Convenience function, accepts a const reference to t_field and checks to see whether
+/// or not both wx pointers are null.
+inline bool is_bad_field(const t_field& obj) { return obj->getSizer() == nullptr && obj->getWindow() == nullptr; }
+
+/// Covenience function to determine whether this field is a valid window field.
+inline bool is_window_field(const t_field& obj) { return !is_bad_field(obj) && obj->getWindow() != nullptr && obj->getSizer() == nullptr; }
+
+/// Covenience function to determine whether this field is a valid sizer field.
+inline bool is_sizer_field(const t_field& obj) { return !is_bad_field(obj) && obj->getSizer() != nullptr; }
+
+class TextCtrl : public Field {
+ using Field::Field;
+#ifdef __WXGTK__
+ bool bChangedValueEvent = true;
+ void change_field_value(wxEvent& event);
+#endif //__WXGTK__
+public:
+ TextCtrl(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
+ TextCtrl(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
+ ~TextCtrl() {}
+
+ void BUILD();
+ wxWindow* window {nullptr};
+
+ virtual void set_value(const std::string& value, bool change_event = false) {
+ m_disable_change_event = !change_event;
+ dynamic_cast<wxTextCtrl*>(window)->SetValue(wxString(value));
+ m_disable_change_event = false;
+ }
+ virtual void set_value(const boost::any& value, bool change_event = false) {
+ m_disable_change_event = !change_event;
+ dynamic_cast<wxTextCtrl*>(window)->SetValue(boost::any_cast<wxString>(value));
+ m_disable_change_event = false;
+ }
+
+ boost::any& get_value() override;
+
+ virtual void enable();
+ virtual void disable();
+ virtual wxWindow* getWindow() { return window; }
+};
+
+class CheckBox : public Field {
+ using Field::Field;
+public:
+ CheckBox(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
+ CheckBox(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
+ ~CheckBox() {}
+
+ wxWindow* window{ nullptr };
+ void BUILD() override;
+
+ void set_value(const bool value, bool change_event = false) {
+ m_disable_change_event = !change_event;
+ dynamic_cast<wxCheckBox*>(window)->SetValue(value);
+ m_disable_change_event = false;
+ }
+ void set_value(const boost::any& value, bool change_event = false) {
+ m_disable_change_event = !change_event;
+ dynamic_cast<wxCheckBox*>(window)->SetValue(boost::any_cast<bool>(value));
+ m_disable_change_event = false;
+ }
+ boost::any& get_value() override;
+
+ void enable() override { dynamic_cast<wxCheckBox*>(window)->Enable(); }
+ void disable() override { dynamic_cast<wxCheckBox*>(window)->Disable(); }
+ wxWindow* getWindow() override { return window; }
+};
+
+class SpinCtrl : public Field {
+ using Field::Field;
+public:
+ SpinCtrl(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id), tmp_value(-9999) {}
+ SpinCtrl(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id), tmp_value(-9999) {}
+ ~SpinCtrl() {}
+
+ int tmp_value;
+
+ wxWindow* window{ nullptr };
+ void BUILD() override;
+
+ void set_value(const std::string& value, bool change_event = false) {
+ m_disable_change_event = !change_event;
+ dynamic_cast<wxSpinCtrl*>(window)->SetValue(value);
+ m_disable_change_event = false;
+ }
+ void set_value(const boost::any& value, bool change_event = false) {
+ m_disable_change_event = !change_event;
+ tmp_value = boost::any_cast<int>(value);
+ dynamic_cast<wxSpinCtrl*>(window)->SetValue(tmp_value);
+ m_disable_change_event = false;
+ }
+ boost::any& get_value() override {
+// return boost::any(tmp_value);
+ return m_value = tmp_value;
+ }
+
+ void enable() override { dynamic_cast<wxSpinCtrl*>(window)->Enable(); }
+ void disable() override { dynamic_cast<wxSpinCtrl*>(window)->Disable(); }
+ wxWindow* getWindow() override { return window; }
+};
+
+class Choice : public Field {
+ using Field::Field;
+public:
+ Choice(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
+ Choice(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
+ ~Choice() {}
+
+ wxWindow* window{ nullptr };
+ void BUILD() override;
+
+ void set_selection();
+ void set_value(const std::string& value, bool change_event = false);
+ void set_value(const boost::any& value, bool change_event = false);
+ void set_values(const std::vector<std::string> &values);
+ boost::any& get_value() override;
+
+ void enable() override { dynamic_cast<wxComboBox*>(window)->Enable(); };
+ void disable() override{ dynamic_cast<wxComboBox*>(window)->Disable(); };
+ wxWindow* getWindow() override { return window; }
+};
+
+class ColourPicker : public Field {
+ using Field::Field;
+public:
+ ColourPicker(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
+ ColourPicker(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
+ ~ColourPicker() {}
+
+ wxWindow* window{ nullptr };
+ void BUILD() override;
+
+ void set_value(const std::string& value, bool change_event = false) {
+ m_disable_change_event = !change_event;
+ dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(value);
+ m_disable_change_event = false;
+ }
+ void set_value(const boost::any& value, bool change_event = false) {
+ m_disable_change_event = !change_event;
+ dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(boost::any_cast<wxString>(value));
+ m_disable_change_event = false;
+ }
+
+ boost::any& get_value() override;
+
+ void enable() override { dynamic_cast<wxColourPickerCtrl*>(window)->Enable(); };
+ void disable() override{ dynamic_cast<wxColourPickerCtrl*>(window)->Disable(); };
+ wxWindow* getWindow() override { return window; }
+};
+
+class PointCtrl : public Field {
+ using Field::Field;
+public:
+ PointCtrl(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
+ PointCtrl(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
+ ~PointCtrl() {}
+
+ wxSizer* sizer{ nullptr };
+ wxTextCtrl* x_textctrl{ nullptr };
+ wxTextCtrl* y_textctrl{ nullptr };
+
+ void BUILD() override;
+
+ void set_value(const Vec2d& value, bool change_event = false);
+ void set_value(const boost::any& value, bool change_event = false);
+ boost::any& get_value() override;
+
+ void enable() override {
+ x_textctrl->Enable();
+ y_textctrl->Enable(); }
+ void disable() override{
+ x_textctrl->Disable();
+ y_textctrl->Disable(); }
+ wxSizer* getSizer() override { return sizer; }
+};
+
+class StaticText : public Field {
+ using Field::Field;
+public:
+ StaticText(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
+ StaticText(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
+ ~StaticText() {}
+
+ wxWindow* window{ nullptr };
+ void BUILD() override;
+
+ void set_value(const std::string& value, bool change_event = false) {
+ m_disable_change_event = !change_event;
+ dynamic_cast<wxStaticText*>(window)->SetLabel(value);
+ m_disable_change_event = false;
+ }
+ void set_value(const boost::any& value, bool change_event = false) {
+ m_disable_change_event = !change_event;
+ dynamic_cast<wxStaticText*>(window)->SetLabel(boost::any_cast<wxString>(value));
+ m_disable_change_event = false;
+ }
+
+ boost::any& get_value()override { return m_value; }
+
+ void enable() override { dynamic_cast<wxStaticText*>(window)->Enable(); };
+ void disable() override{ dynamic_cast<wxStaticText*>(window)->Disable(); };
+ wxWindow* getWindow() override { return window; }
+};
+
+class SliderCtrl : public Field {
+ using Field::Field;
+public:
+ SliderCtrl(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
+ SliderCtrl(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
+ ~SliderCtrl() {}
+
+ wxSizer* m_sizer{ nullptr };
+ wxTextCtrl* m_textctrl{ nullptr };
+ wxSlider* m_slider{ nullptr };
+
+ int m_scale = 10;
+
+ void BUILD() override;
+
+ void set_value(const int value, bool change_event = false);
+ void set_value(const boost::any& value, bool change_event = false);
+ boost::any& get_value() override;
+
+ void enable() override {
+ m_slider->Enable();
+ m_textctrl->Enable();
+ m_textctrl->SetEditable(true);
+ }
+ void disable() override{
+ m_slider->Disable();
+ m_textctrl->Disable();
+ m_textctrl->SetEditable(false);
+ }
+ wxSizer* getSizer() override { return m_sizer; }
+ wxWindow* getWindow() override { return dynamic_cast<wxWindow*>(m_slider); }
+};
+
+} // GUI
+} // Slic3r
+
+#endif /* SLIC3R_GUI_FIELD_HPP */
diff --git a/src/slic3r/GUI/FirmwareDialog.cpp b/src/slic3r/GUI/FirmwareDialog.cpp
new file mode 100644
index 000000000..d5ac64d90
--- /dev/null
+++ b/src/slic3r/GUI/FirmwareDialog.cpp
@@ -0,0 +1,846 @@
+#include <numeric>
+#include <algorithm>
+#include <thread>
+#include <condition_variable>
+#include <stdexcept>
+#include <boost/format.hpp>
+#include <boost/asio.hpp>
+#include <boost/filesystem/path.hpp>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/log/trivial.hpp>
+#include <boost/optional.hpp>
+
+#include "libslic3r/Utils.hpp"
+#include "avrdude/avrdude-slic3r.hpp"
+#include "GUI.hpp"
+#include "MsgDialog.hpp"
+#include "../Utils/HexFile.hpp"
+#include "../Utils/Serial.hpp"
+
+// wx includes need to come after asio because of the WinSock.h problem
+#include "FirmwareDialog.hpp"
+
+#include <wx/app.h>
+#include <wx/event.h>
+#include <wx/sizer.h>
+#include <wx/settings.h>
+#include <wx/timer.h>
+#include <wx/panel.h>
+#include <wx/button.h>
+#include <wx/filepicker.h>
+#include <wx/textctrl.h>
+#include <wx/stattext.h>
+#include <wx/combobox.h>
+#include <wx/gauge.h>
+#include <wx/collpane.h>
+#include <wx/msgdlg.h>
+#include <wx/filefn.h>
+
+
+namespace fs = boost::filesystem;
+namespace asio = boost::asio;
+using boost::system::error_code;
+using boost::optional;
+
+
+namespace Slic3r {
+
+using Utils::HexFile;
+using Utils::SerialPortInfo;
+using Utils::Serial;
+
+
+// USB IDs used to perform device lookup
+enum {
+ USB_VID_PRUSA = 0x2c99,
+ USB_PID_MK2 = 1,
+ USB_PID_MK3 = 2,
+ USB_PID_MMU_BOOT = 3,
+ USB_PID_MMU_APP = 4,
+};
+
+// This enum discriminates the kind of information in EVT_AVRDUDE,
+// it's stored in the ExtraLong field of wxCommandEvent.
+enum AvrdudeEvent
+{
+ AE_MESSAGE,
+ AE_PROGRESS,
+ AE_STATUS,
+ AE_EXIT,
+};
+
+wxDECLARE_EVENT(EVT_AVRDUDE, wxCommandEvent);
+wxDEFINE_EVENT(EVT_AVRDUDE, wxCommandEvent);
+
+wxDECLARE_EVENT(EVT_ASYNC_DIALOG, wxCommandEvent);
+wxDEFINE_EVENT(EVT_ASYNC_DIALOG, wxCommandEvent);
+
+
+// Private
+
+struct FirmwareDialog::priv
+{
+ enum AvrDudeComplete
+ {
+ AC_NONE,
+ AC_SUCCESS,
+ AC_FAILURE,
+ AC_USER_CANCELLED,
+ };
+
+ FirmwareDialog *q; // PIMPL back pointer ("Q-Pointer")
+
+ // GUI elements
+ wxComboBox *port_picker;
+ wxStaticText *port_autodetect;
+ wxFilePickerCtrl *hex_picker;
+ wxStaticText *txt_status;
+ wxGauge *progressbar;
+ wxCollapsiblePane *spoiler;
+ wxTextCtrl *txt_stdout;
+ wxButton *btn_rescan;
+ wxButton *btn_close;
+ wxButton *btn_flash;
+ wxString btn_flash_label_ready;
+ wxString btn_flash_label_flashing;
+ wxString label_status_flashing;
+
+ wxTimer timer_pulse;
+
+ // Async modal dialog during flashing
+ std::mutex mutex;
+ int modal_response;
+ std::condition_variable response_cv;
+
+ // Data
+ std::vector<SerialPortInfo> ports;
+ optional<SerialPortInfo> port;
+ HexFile hex_file;
+
+ // This is a shared pointer holding the background AvrDude task
+ // also serves as a status indication (it is set _iff_ the background task is running, otherwise it is reset).
+ AvrDude::Ptr avrdude;
+ std::string avrdude_config;
+ unsigned progress_tasks_done;
+ unsigned progress_tasks_bar;
+ bool user_cancelled;
+ const bool extra_verbose; // For debugging
+
+ priv(FirmwareDialog *q) :
+ q(q),
+ btn_flash_label_ready(_(L("Flash!"))),
+ btn_flash_label_flashing(_(L("Cancel"))),
+ label_status_flashing(_(L("Flashing in progress. Please do not disconnect the printer!"))),
+ timer_pulse(q),
+ avrdude_config((fs::path(::Slic3r::resources_dir()) / "avrdude" / "avrdude.conf").string()),
+ progress_tasks_done(0),
+ progress_tasks_bar(0),
+ user_cancelled(false),
+ extra_verbose(false)
+ {}
+
+ void find_serial_ports();
+ void fit_no_shrink();
+ void set_txt_status(const wxString &label);
+ void flashing_start(unsigned tasks);
+ void flashing_done(AvrDudeComplete complete);
+ void enable_port_picker(bool enable);
+ void load_hex_file(const wxString &path);
+ void queue_status(wxString message);
+ void queue_error(const wxString &message);
+
+ bool ask_model_id_mismatch(const std::string &printer_model);
+ bool check_model_id();
+ void wait_for_mmu_bootloader(unsigned retries);
+ void mmu_reboot(const SerialPortInfo &port);
+ void lookup_port_mmu();
+ void prepare_common();
+ void prepare_mk2();
+ void prepare_mk3();
+ void prepare_mm_control();
+ void perform_upload();
+
+ void user_cancel();
+ void on_avrdude(const wxCommandEvent &evt);
+ void on_async_dialog(const wxCommandEvent &evt);
+ void ensure_joined();
+};
+
+void FirmwareDialog::priv::find_serial_ports()
+{
+ auto new_ports = Utils::scan_serial_ports_extended();
+ if (new_ports != this->ports) {
+ this->ports = new_ports;
+ port_picker->Clear();
+ for (const auto &port : this->ports)
+ port_picker->Append(wxString::FromUTF8(port.friendly_name.data()));
+ if (ports.size() > 0) {
+ int idx = port_picker->GetValue().IsEmpty() ? 0 : -1;
+ for (int i = 0; i < (int)this->ports.size(); ++ i)
+ if (this->ports[i].is_printer) {
+ idx = i;
+ break;
+ }
+ if (idx != -1)
+ port_picker->SetSelection(idx);
+ }
+ }
+}
+
+void FirmwareDialog::priv::fit_no_shrink()
+{
+ // Ensure content fits into window and window is not shrinked
+ const auto old_size = q->GetSize();
+ q->Layout();
+ q->Fit();
+ const auto new_size = q->GetSize();
+ const auto new_width = std::max(old_size.GetWidth(), new_size.GetWidth());
+ const auto new_height = std::max(old_size.GetHeight(), new_size.GetHeight());
+ q->SetSize(new_width, new_height);
+}
+
+void FirmwareDialog::priv::set_txt_status(const wxString &label)
+{
+ const auto width = txt_status->GetSize().GetWidth();
+ txt_status->SetLabel(label);
+ txt_status->Wrap(width);
+
+ fit_no_shrink();
+}
+
+void FirmwareDialog::priv::flashing_start(unsigned tasks)
+{
+ modal_response = wxID_NONE;
+ txt_stdout->Clear();
+ set_txt_status(label_status_flashing);
+ txt_status->SetForegroundColour(GUI::get_label_clr_modified());
+ port_picker->Disable();
+ btn_rescan->Disable();
+ hex_picker->Disable();
+ btn_close->Disable();
+ btn_flash->SetLabel(btn_flash_label_flashing);
+ progressbar->SetRange(200 * tasks); // See progress callback below
+ progressbar->SetValue(0);
+ progress_tasks_done = 0;
+ progress_tasks_bar = 0;
+ user_cancelled = false;
+ timer_pulse.Start(50);
+}
+
+void FirmwareDialog::priv::flashing_done(AvrDudeComplete complete)
+{
+ auto text_color = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
+ port_picker->Enable();
+ btn_rescan->Enable();
+ hex_picker->Enable();
+ btn_close->Enable();
+ btn_flash->SetLabel(btn_flash_label_ready);
+ txt_status->SetForegroundColour(text_color);
+ timer_pulse.Stop();
+ progressbar->SetValue(progressbar->GetRange());
+
+ switch (complete) {
+ case AC_SUCCESS: set_txt_status(_(L("Flashing succeeded!"))); break;
+ case AC_FAILURE: set_txt_status(_(L("Flashing failed. Please see the avrdude log below."))); break;
+ case AC_USER_CANCELLED: set_txt_status(_(L("Flashing cancelled."))); break;
+ default: break;
+ }
+}
+
+void FirmwareDialog::priv::enable_port_picker(bool enable)
+{
+ port_picker->Show(enable);
+ btn_rescan->Show(enable);
+ port_autodetect->Show(! enable);
+ q->Layout();
+ fit_no_shrink();
+}
+
+void FirmwareDialog::priv::load_hex_file(const wxString &path)
+{
+ hex_file = HexFile(path.wx_str());
+ enable_port_picker(hex_file.device != HexFile::DEV_MM_CONTROL);
+}
+
+void FirmwareDialog::priv::queue_status(wxString message)
+{
+ auto evt = new wxCommandEvent(EVT_AVRDUDE, this->q->GetId());
+ evt->SetExtraLong(AE_STATUS);
+ evt->SetString(std::move(message));
+ wxQueueEvent(this->q, evt);
+}
+
+void FirmwareDialog::priv::queue_error(const wxString &message)
+{
+ auto evt = new wxCommandEvent(EVT_AVRDUDE, this->q->GetId());
+ evt->SetExtraLong(AE_STATUS);
+ evt->SetString(wxString::Format(_(L("Flashing failed: %s")), message));
+
+ wxQueueEvent(this->q, evt); avrdude->cancel();
+}
+
+bool FirmwareDialog::priv::ask_model_id_mismatch(const std::string &printer_model)
+{
+ // model_id in the hex file doesn't match what the printer repoted.
+ // Ask the user if it should be flashed anyway.
+
+ std::unique_lock<std::mutex> lock(mutex);
+
+ auto evt = new wxCommandEvent(EVT_ASYNC_DIALOG, this->q->GetId());
+ evt->SetString(wxString::Format(_(L(
+ "This firmware hex file does not match the printer model.\n"
+ "The hex file is intended for: %s\n"
+ "Printer reported: %s\n\n"
+ "Do you want to continue and flash this hex file anyway?\n"
+ "Please only continue if you are sure this is the right thing to do.")),
+ hex_file.model_id, printer_model
+ ));
+ wxQueueEvent(this->q, evt);
+
+ response_cv.wait(lock, [this]() { return this->modal_response != wxID_NONE; });
+
+ if (modal_response == wxID_YES) {
+ return true;
+ } else {
+ user_cancel();
+ return false;
+ }
+}
+
+bool FirmwareDialog::priv::check_model_id()
+{
+ // XXX: The implementation in Serial doesn't currently work reliably enough to be used.
+ // Therefore, regretably, so far the check cannot be used and we just return true here.
+ // TODO: Rewrite Serial using more platform-native code.
+ return true;
+
+ // if (hex_file.model_id.empty()) {
+ // // No data to check against, assume it's ok
+ // return true;
+ // }
+
+ // asio::io_service io;
+ // Serial serial(io, port->port, 115200);
+ // serial.printer_setup();
+
+ // enum {
+ // TIMEOUT = 2000,
+ // RETREIES = 5,
+ // };
+
+ // if (! serial.printer_ready_wait(RETREIES, TIMEOUT)) {
+ // queue_error(wxString::Format(_(L("Could not connect to the printer at %s")), port->port));
+ // return false;
+ // }
+
+ // std::string line;
+ // error_code ec;
+ // serial.printer_write_line("PRUSA Rev");
+ // while (serial.read_line(TIMEOUT, line, ec)) {
+ // if (ec) {
+ // queue_error(wxString::Format(_(L("Could not connect to the printer at %s")), port->port));
+ // return false;
+ // }
+
+ // if (line == "ok") { continue; }
+
+ // if (line == hex_file.model_id) {
+ // return true;
+ // } else {
+ // return ask_model_id_mismatch(line);
+ // }
+
+ // line.clear();
+ // }
+
+ // return false;
+}
+
+void FirmwareDialog::priv::wait_for_mmu_bootloader(unsigned retries)
+{
+ enum {
+ SLEEP_MS = 500,
+ };
+
+ for (unsigned i = 0; i < retries && !user_cancelled; i++) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(SLEEP_MS));
+
+ auto ports = Utils::scan_serial_ports_extended();
+ ports.erase(std::remove_if(ports.begin(), ports.end(), [=](const SerialPortInfo &port ) {
+ return port.id_vendor != USB_VID_PRUSA || port.id_product != USB_PID_MMU_BOOT;
+ }), ports.end());
+
+ if (ports.size() == 1) {
+ port = ports[0];
+ return;
+ } else if (ports.size() > 1) {
+ BOOST_LOG_TRIVIAL(error) << "Several VID/PID 0x2c99/3 devices found";
+ queue_error(_(L("Multiple Original Prusa i3 MMU 2.0 devices found. Please only connect one at a time for flashing.")));
+ return;
+ }
+ }
+}
+
+void FirmwareDialog::priv::mmu_reboot(const SerialPortInfo &port)
+{
+ asio::io_service io;
+ Serial serial(io, port.port, 1200);
+ std::this_thread::sleep_for(std::chrono::milliseconds(50));
+}
+
+void FirmwareDialog::priv::lookup_port_mmu()
+{
+ static const auto msg_not_found =
+ "The Multi Material Control device was not found.\n"
+ "If the device is connected, please press the Reset button next to the USB connector ...";
+
+ BOOST_LOG_TRIVIAL(info) << "Flashing MMU 2.0, looking for VID/PID 0x2c99/3 or 0x2c99/4 ...";
+
+ auto ports = Utils::scan_serial_ports_extended();
+ ports.erase(std::remove_if(ports.begin(), ports.end(), [=](const SerialPortInfo &port ) {
+ return port.id_vendor != USB_VID_PRUSA ||
+ port.id_product != USB_PID_MMU_BOOT &&
+ port.id_product != USB_PID_MMU_APP;
+ }), ports.end());
+
+ if (ports.size() == 0) {
+ BOOST_LOG_TRIVIAL(info) << "MMU 2.0 device not found, asking the user to press Reset and waiting for the device to show up ...";
+ queue_status(_(L(msg_not_found)));
+ wait_for_mmu_bootloader(30);
+ } else if (ports.size() > 1) {
+ BOOST_LOG_TRIVIAL(error) << "Several VID/PID 0x2c99/3 devices found";
+ queue_error(_(L("Multiple Original Prusa i3 MMU 2.0 devices found. Please only connect one at a time for flashing.")));
+ } else {
+ if (ports[0].id_product == USB_PID_MMU_APP) {
+ // The device needs to be rebooted into the bootloader mode
+ BOOST_LOG_TRIVIAL(info) << boost::format("Found VID/PID 0x2c99/4 at `%1%`, rebooting the device ...") % ports[0].port;
+ mmu_reboot(ports[0]);
+ wait_for_mmu_bootloader(10);
+
+ if (! port) {
+ // The device in bootloader mode was not found, inform the user and wait some more...
+ BOOST_LOG_TRIVIAL(info) << "MMU 2.0 bootloader device not found after reboot, asking the user to press Reset and waiting for the device to show up ...";
+ queue_status(_(L(msg_not_found)));
+ wait_for_mmu_bootloader(30);
+ }
+ } else {
+ port = ports[0];
+ }
+ }
+}
+
+void FirmwareDialog::priv::prepare_common()
+{
+ std::vector<std::string> args {{
+ extra_verbose ? "-vvvvv" : "-v",
+ "-p", "atmega2560",
+ // Using the "Wiring" mode to program Rambo or Einsy, using the STK500v2 protocol (not the STK500).
+ // The Prusa's avrdude is patched to never send semicolons inside the data packets, as the USB to serial chip
+ // is flashed with a buggy firmware.
+ "-c", "wiring",
+ "-P", port->port,
+ "-b", "115200", // TODO: Allow other rates? Ditto elsewhere.
+ "-D",
+ "-U", (boost::format("flash:w:0:%1%:i") % hex_file.path.string()).str(),
+ }};
+
+ BOOST_LOG_TRIVIAL(info) << "Invoking avrdude, arguments: "
+ << std::accumulate(std::next(args.begin()), args.end(), args[0], [](std::string a, const std::string &b) {
+ return a + ' ' + b;
+ });
+
+ avrdude->push_args(std::move(args));
+}
+
+void FirmwareDialog::priv::prepare_mk2()
+{
+ if (! port) { return; }
+
+ if (! check_model_id()) {
+ avrdude->cancel();
+ return;
+ }
+
+ prepare_common();
+}
+
+void FirmwareDialog::priv::prepare_mk3()
+{
+ if (! port) { return; }
+
+ if (! check_model_id()) {
+ avrdude->cancel();
+ return;
+ }
+
+ prepare_common();
+
+ // The hex file also contains another section with l10n data to be flashed into the external flash on MK3 (Einsy)
+ // This is done via another avrdude invocation, here we build arg list for that:
+ std::vector<std::string> args {{
+ extra_verbose ? "-vvvvv" : "-v",
+ "-p", "atmega2560",
+ // Using the "Arduino" mode to program Einsy's external flash with languages, using the STK500 protocol (not the STK500v2).
+ // The Prusa's avrdude is patched again to never send semicolons inside the data packets.
+ "-c", "arduino",
+ "-P", port->port,
+ "-b", "115200",
+ "-D",
+ "-u", // disable safe mode
+ "-U", (boost::format("flash:w:1:%1%:i") % hex_file.path.string()).str(),
+ }};
+
+ BOOST_LOG_TRIVIAL(info) << "Invoking avrdude for external flash flashing, arguments: "
+ << std::accumulate(std::next(args.begin()), args.end(), args[0], [](std::string a, const std::string &b) {
+ return a + ' ' + b;
+ });
+
+ avrdude->push_args(std::move(args));
+}
+
+void FirmwareDialog::priv::prepare_mm_control()
+{
+ port = boost::none;
+ lookup_port_mmu();
+ if (! port) {
+ queue_error(_(L("The device could not have been found")));
+ return;
+ }
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Found VID/PID 0x2c99/3 at `%1%`, flashing ...") % port->port;
+ queue_status(label_status_flashing);
+
+ std::vector<std::string> args {{
+ extra_verbose ? "-vvvvv" : "-v",
+ "-p", "atmega32u4",
+ "-c", "avr109",
+ "-P", port->port,
+ "-b", "57600",
+ "-D",
+ "-U", (boost::format("flash:w:0:%1%:i") % hex_file.path.string()).str(),
+ }};
+
+ BOOST_LOG_TRIVIAL(info) << "Invoking avrdude, arguments: "
+ << std::accumulate(std::next(args.begin()), args.end(), args[0], [](std::string a, const std::string &b) {
+ return a + ' ' + b;
+ });
+
+ avrdude->push_args(std::move(args));
+}
+
+
+void FirmwareDialog::priv::perform_upload()
+{
+ auto filename = hex_picker->GetPath();
+ if (filename.IsEmpty()) { return; }
+
+ load_hex_file(filename); // Might already be loaded, but we want to make sure it's fresh
+
+ int selection = port_picker->GetSelection();
+ if (selection != wxNOT_FOUND) {
+ port = this->ports[selection];
+
+ // Verify whether the combo box list selection equals to the combo box edit value.
+ if (wxString::FromUTF8(port->friendly_name.data()) != port_picker->GetValue()) {
+ return;
+ }
+ }
+
+ const bool extra_verbose = false; // For debugging
+
+ flashing_start(hex_file.device == HexFile::DEV_MK3 ? 2 : 1);
+
+ // Init the avrdude object
+ AvrDude avrdude(avrdude_config);
+
+ // It is ok here to use the q-pointer to the FirmwareDialog
+ // because the dialog ensures it doesn't exit before the background thread is done.
+ auto q = this->q;
+
+ avrdude
+ .on_run([this](AvrDude::Ptr avrdude) {
+ this->avrdude = std::move(avrdude);
+
+ try {
+ switch (this->hex_file.device) {
+ case HexFile::DEV_MK3:
+ this->prepare_mk3();
+ break;
+
+ case HexFile::DEV_MM_CONTROL:
+ this->prepare_mm_control();
+ break;
+
+ default:
+ this->prepare_mk2();
+ break;
+ }
+ } catch (const std::exception &ex) {
+ queue_error(wxString::Format(_(L("Error accessing port at %s: %s")), port->port, ex.what()));
+ }
+ })
+ .on_message(std::move([q, extra_verbose](const char *msg, unsigned /* size */) {
+ if (extra_verbose) {
+ BOOST_LOG_TRIVIAL(debug) << "avrdude: " << msg;
+ }
+
+ auto evt = new wxCommandEvent(EVT_AVRDUDE, q->GetId());
+ auto wxmsg = wxString::FromUTF8(msg);
+ evt->SetExtraLong(AE_MESSAGE);
+ evt->SetString(std::move(wxmsg));
+ wxQueueEvent(q, evt);
+ }))
+ .on_progress(std::move([q](const char * /* task */, unsigned progress) {
+ auto evt = new wxCommandEvent(EVT_AVRDUDE, q->GetId());
+ evt->SetExtraLong(AE_PROGRESS);
+ evt->SetInt(progress);
+ wxQueueEvent(q, evt);
+ }))
+ .on_complete(std::move([this]() {
+ auto evt = new wxCommandEvent(EVT_AVRDUDE, this->q->GetId());
+ evt->SetExtraLong(AE_EXIT);
+ evt->SetInt(this->avrdude->exit_code());
+ wxQueueEvent(this->q, evt);
+ }))
+ .run();
+}
+
+void FirmwareDialog::priv::user_cancel()
+{
+ if (avrdude) {
+ user_cancelled = true;
+ avrdude->cancel();
+ }
+}
+
+void FirmwareDialog::priv::on_avrdude(const wxCommandEvent &evt)
+{
+ AvrDudeComplete complete_kind;
+
+ switch (evt.GetExtraLong()) {
+ case AE_MESSAGE:
+ txt_stdout->AppendText(evt.GetString());
+ break;
+
+ case AE_PROGRESS:
+ // We try to track overall progress here.
+ // Avrdude performs 3 tasks per one memory operation ("-U" arg),
+ // first of which is reading of status data (very short).
+ // We use the timer_pulse during the very first task to indicate intialization
+ // and then display overall progress during the latter tasks.
+
+ if (progress_tasks_done > 0) {
+ progressbar->SetValue(progress_tasks_bar + evt.GetInt());
+ }
+
+ if (evt.GetInt() == 100) {
+ timer_pulse.Stop();
+ if (progress_tasks_done % 3 != 0) {
+ progress_tasks_bar += 100;
+ }
+ progress_tasks_done++;
+ }
+
+ break;
+
+ case AE_EXIT:
+ BOOST_LOG_TRIVIAL(info) << "avrdude exit code: " << evt.GetInt();
+
+ // Figure out the exit state
+ if (user_cancelled) { complete_kind = AC_USER_CANCELLED; }
+ else if (avrdude->cancelled()) { complete_kind = AC_NONE; } // Ie. cancelled programatically
+ else { complete_kind = evt.GetInt() == 0 ? AC_SUCCESS : AC_FAILURE; }
+
+ flashing_done(complete_kind);
+ ensure_joined();
+ break;
+
+ case AE_STATUS:
+ set_txt_status(evt.GetString());
+ break;
+
+ default:
+ break;
+ }
+}
+
+void FirmwareDialog::priv::on_async_dialog(const wxCommandEvent &evt)
+{
+ wxMessageDialog dlg(this->q, evt.GetString(), wxMessageBoxCaptionStr, wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION);
+ {
+ std::lock_guard<std::mutex> lock(mutex);
+ modal_response = dlg.ShowModal();
+ }
+ response_cv.notify_all();
+}
+
+void FirmwareDialog::priv::ensure_joined()
+{
+ // Make sure the background thread is collected and the AvrDude object reset
+ if (avrdude) { avrdude->join(); }
+ avrdude.reset();
+}
+
+
+// Public
+
+FirmwareDialog::FirmwareDialog(wxWindow *parent) :
+ wxDialog(parent, wxID_ANY, _(L("Firmware flasher")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER),
+ p(new priv(this))
+{
+ enum {
+ DIALOG_MARGIN = 15,
+ SPACING = 10,
+ MIN_WIDTH = 600,
+ MIN_HEIGHT = 200,
+ MIN_HEIGHT_EXPANDED = 500,
+ };
+
+ wxFont status_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
+ status_font.MakeBold();
+ wxFont mono_font(wxFontInfo().Family(wxFONTFAMILY_TELETYPE));
+ mono_font.MakeSmaller();
+
+ // Create GUI components and layout
+
+ auto *panel = new wxPanel(this);
+ wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
+ panel->SetSizer(vsizer);
+
+ auto *label_hex_picker = new wxStaticText(panel, wxID_ANY, _(L("Firmware image:")));
+ p->hex_picker = new wxFilePickerCtrl(panel, wxID_ANY, wxEmptyString, wxFileSelectorPromptStr,
+ "Hex files (*.hex)|*.hex|All files|*.*");
+
+ auto *label_port_picker = new wxStaticText(panel, wxID_ANY, _(L("Serial port:")));
+ p->port_picker = new wxComboBox(panel, wxID_ANY);
+ p->port_autodetect = new wxStaticText(panel, wxID_ANY, _(L("Autodetected")));
+ p->btn_rescan = new wxButton(panel, wxID_ANY, _(L("Rescan")));
+ auto *port_sizer = new wxBoxSizer(wxHORIZONTAL);
+ port_sizer->Add(p->port_picker, 1, wxEXPAND | wxRIGHT, SPACING);
+ port_sizer->Add(p->btn_rescan, 0);
+ port_sizer->Add(p->port_autodetect, 1, wxEXPAND);
+ p->enable_port_picker(true);
+
+ auto *label_progress = new wxStaticText(panel, wxID_ANY, _(L("Progress:")));
+ p->progressbar = new wxGauge(panel, wxID_ANY, 1, wxDefaultPosition, wxDefaultSize, wxGA_HORIZONTAL | wxGA_SMOOTH);
+
+ auto *label_status = new wxStaticText(panel, wxID_ANY, _(L("Status:")));
+ p->txt_status = new wxStaticText(panel, wxID_ANY, _(L("Ready")));
+ p->txt_status->SetFont(status_font);
+
+ auto *grid = new wxFlexGridSizer(2, SPACING, SPACING);
+ grid->AddGrowableCol(1);
+
+ grid->Add(label_hex_picker, 0, wxALIGN_CENTER_VERTICAL);
+ grid->Add(p->hex_picker, 0, wxEXPAND);
+
+ grid->Add(label_port_picker, 0, wxALIGN_CENTER_VERTICAL);
+ grid->Add(port_sizer, 0, wxEXPAND);
+
+ grid->Add(label_progress, 0, wxALIGN_CENTER_VERTICAL);
+ grid->Add(p->progressbar, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL);
+
+ grid->Add(label_status, 0, wxALIGN_CENTER_VERTICAL);
+ grid->Add(p->txt_status, 0, wxEXPAND);
+
+ vsizer->Add(grid, 0, wxEXPAND | wxTOP | wxBOTTOM, SPACING);
+
+ p->spoiler = new wxCollapsiblePane(panel, wxID_ANY, _(L("Advanced: avrdude output log")), wxDefaultPosition, wxDefaultSize, wxCP_DEFAULT_STYLE | wxCP_NO_TLW_RESIZE);
+ auto *spoiler_pane = p->spoiler->GetPane();
+ auto *spoiler_sizer = new wxBoxSizer(wxVERTICAL);
+ p->txt_stdout = new wxTextCtrl(spoiler_pane, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
+ p->txt_stdout->SetFont(mono_font);
+ spoiler_sizer->Add(p->txt_stdout, 1, wxEXPAND);
+ spoiler_pane->SetSizer(spoiler_sizer);
+ // The doc says proportion need to be 0 for wxCollapsiblePane.
+ // Experience says it needs to be 1, otherwise things won't get sized properly.
+ vsizer->Add(p->spoiler, 1, wxEXPAND | wxBOTTOM, SPACING);
+
+ p->btn_close = new wxButton(panel, wxID_CLOSE);
+ p->btn_flash = new wxButton(panel, wxID_ANY, p->btn_flash_label_ready);
+ p->btn_flash->Disable();
+ auto *bsizer = new wxBoxSizer(wxHORIZONTAL);
+ bsizer->Add(p->btn_close);
+ bsizer->AddStretchSpacer();
+ bsizer->Add(p->btn_flash);
+ vsizer->Add(bsizer, 0, wxEXPAND);
+
+ auto *topsizer = new wxBoxSizer(wxVERTICAL);
+ topsizer->Add(panel, 1, wxEXPAND | wxALL, DIALOG_MARGIN);
+ SetMinSize(wxSize(MIN_WIDTH, MIN_HEIGHT));
+ SetSizerAndFit(topsizer);
+ const auto size = GetSize();
+ SetSize(std::max(size.GetWidth(), static_cast<int>(MIN_WIDTH)), std::max(size.GetHeight(), static_cast<int>(MIN_HEIGHT)));
+ Layout();
+
+ // Bind events
+
+ p->hex_picker->Bind(wxEVT_FILEPICKER_CHANGED, [this](wxFileDirPickerEvent& evt) {
+ if (wxFileExists(evt.GetPath())) {
+ this->p->load_hex_file(evt.GetPath());
+ this->p->btn_flash->Enable();
+ }
+ });
+
+ p->spoiler->Bind(wxEVT_COLLAPSIBLEPANE_CHANGED, [this](wxCollapsiblePaneEvent &evt) {
+ if (evt.GetCollapsed()) {
+ this->SetMinSize(wxSize(MIN_WIDTH, MIN_HEIGHT));
+ const auto new_height = this->GetSize().GetHeight() - this->p->txt_stdout->GetSize().GetHeight();
+ this->SetSize(this->GetSize().GetWidth(), new_height);
+ } else {
+ this->SetMinSize(wxSize(MIN_WIDTH, MIN_HEIGHT_EXPANDED));
+ }
+
+ this->Layout();
+ this->p->fit_no_shrink();
+ });
+
+ p->btn_close->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { this->Close(); });
+ p->btn_rescan->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { this->p->find_serial_ports(); });
+
+ p->btn_flash->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) {
+ if (this->p->avrdude) {
+ // Flashing is in progress, ask the user if they're really sure about canceling it
+ wxMessageDialog dlg(this,
+ _(L("Are you sure you want to cancel firmware flashing?\nThis could leave your printer in an unusable state!")),
+ _(L("Confirmation")),
+ wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION);
+ if (dlg.ShowModal() == wxID_YES) {
+ this->p->set_txt_status(_(L("Cancelling...")));
+ this->p->user_cancel();
+ }
+ } else {
+ // Start a flashing task
+ this->p->perform_upload();
+ }
+ });
+
+ Bind(wxEVT_TIMER, [this](wxTimerEvent &evt) { this->p->progressbar->Pulse(); });
+
+ Bind(EVT_AVRDUDE, [this](wxCommandEvent &evt) { this->p->on_avrdude(evt); });
+ Bind(EVT_ASYNC_DIALOG, [this](wxCommandEvent &evt) { this->p->on_async_dialog(evt); });
+
+ Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent &evt) {
+ if (this->p->avrdude) {
+ evt.Veto();
+ } else {
+ evt.Skip();
+ }
+ });
+
+ p->find_serial_ports();
+}
+
+FirmwareDialog::~FirmwareDialog()
+{
+ // Needed bacuse of forward defs
+}
+
+void FirmwareDialog::run(wxWindow *parent)
+{
+ FirmwareDialog dialog(parent);
+ dialog.ShowModal();
+}
+
+
+}
diff --git a/src/slic3r/GUI/FirmwareDialog.hpp b/src/slic3r/GUI/FirmwareDialog.hpp
new file mode 100644
index 000000000..ad048bf5d
--- /dev/null
+++ b/src/slic3r/GUI/FirmwareDialog.hpp
@@ -0,0 +1,31 @@
+#ifndef slic3r_FirmwareDialog_hpp_
+#define slic3r_FirmwareDialog_hpp_
+
+#include <memory>
+
+#include <wx/dialog.h>
+
+
+namespace Slic3r {
+
+
+class FirmwareDialog: public wxDialog
+{
+public:
+ FirmwareDialog(wxWindow *parent);
+ FirmwareDialog(FirmwareDialog &&) = delete;
+ FirmwareDialog(const FirmwareDialog &) = delete;
+ FirmwareDialog &operator=(FirmwareDialog &&) = delete;
+ FirmwareDialog &operator=(const FirmwareDialog &) = delete;
+ ~FirmwareDialog();
+
+ static void run(wxWindow *parent);
+private:
+ struct priv;
+ std::unique_ptr<priv> p;
+};
+
+
+}
+
+#endif
diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp
new file mode 100644
index 000000000..cb3250916
--- /dev/null
+++ b/src/slic3r/GUI/GLCanvas3D.cpp
@@ -0,0 +1,5522 @@
+#include "GLCanvas3D.hpp"
+
+#include "../../admesh/stl.h"
+#include "../../libslic3r/libslic3r.h"
+#include "../../slic3r/GUI/3DScene.hpp"
+#include "../../slic3r/GUI/GLShader.hpp"
+#include "../../slic3r/GUI/GUI.hpp"
+#include "../../slic3r/GUI/PresetBundle.hpp"
+#include "../../slic3r/GUI/GLGizmo.hpp"
+#include "../../libslic3r/ClipperUtils.hpp"
+#include "../../libslic3r/PrintConfig.hpp"
+#include "../../libslic3r/GCode/PreviewData.hpp"
+
+#include <GL/glew.h>
+
+#include <wx/glcanvas.h>
+#include <wx/timer.h>
+#include <wx/bitmap.h>
+#include <wx/dcmemory.h>
+#include <wx/image.h>
+#include <wx/settings.h>
+
+// Print now includes tbb, and tbb includes Windows. This breaks compilation of wxWidgets if included before wx.
+#include "../../libslic3r/Print.hpp"
+
+#include <tbb/parallel_for.h>
+#include <tbb/spin_mutex.h>
+
+#include <boost/log/trivial.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+
+#include <iostream>
+#include <float.h>
+#include <algorithm>
+
+static const float TRACKBALLSIZE = 0.8f;
+static const float GIMBALL_LOCK_THETA_MAX = 180.0f;
+static const float GROUND_Z = -0.02f;
+
+// phi / theta angles to orient the camera.
+static const float VIEW_DEFAULT[2] = { 45.0f, 45.0f };
+static const float VIEW_LEFT[2] = { 90.0f, 90.0f };
+static const float VIEW_RIGHT[2] = { -90.0f, 90.0f };
+static const float VIEW_TOP[2] = { 0.0f, 0.0f };
+static const float VIEW_BOTTOM[2] = { 0.0f, 180.0f };
+static const float VIEW_FRONT[2] = { 0.0f, 90.0f };
+static const float VIEW_REAR[2] = { 180.0f, 90.0f };
+
+static const float VARIABLE_LAYER_THICKNESS_BAR_WIDTH = 70.0f;
+static const float VARIABLE_LAYER_THICKNESS_RESET_BUTTON_HEIGHT = 22.0f;
+
+static const float UNIT_MATRIX[] = { 1.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f };
+
+static const float DEFAULT_BG_COLOR[3] = { 10.0f / 255.0f, 98.0f / 255.0f, 144.0f / 255.0f };
+static const float ERROR_BG_COLOR[3] = { 144.0f / 255.0f, 49.0f / 255.0f, 10.0f / 255.0f };
+
+namespace Slic3r {
+namespace GUI {
+
+bool GeometryBuffer::set_from_triangles(const Polygons& triangles, float z, bool generate_tex_coords)
+{
+ m_vertices.clear();
+ m_tex_coords.clear();
+
+ unsigned int v_size = 9 * (unsigned int)triangles.size();
+ unsigned int t_size = 6 * (unsigned int)triangles.size();
+ if (v_size == 0)
+ return false;
+
+ m_vertices = std::vector<float>(v_size, 0.0f);
+ if (generate_tex_coords)
+ m_tex_coords = std::vector<float>(t_size, 0.0f);
+
+ float min_x = unscale<float>(triangles[0].points[0](0));
+ float min_y = unscale<float>(triangles[0].points[0](1));
+ float max_x = min_x;
+ float max_y = min_y;
+
+ unsigned int v_coord = 0;
+ unsigned int t_coord = 0;
+ for (const Polygon& t : triangles)
+ {
+ for (unsigned int v = 0; v < 3; ++v)
+ {
+ const Point& p = t.points[v];
+ float x = unscale<float>(p(0));
+ float y = unscale<float>(p(1));
+
+ m_vertices[v_coord++] = x;
+ m_vertices[v_coord++] = y;
+ m_vertices[v_coord++] = z;
+
+ if (generate_tex_coords)
+ {
+ m_tex_coords[t_coord++] = x;
+ m_tex_coords[t_coord++] = y;
+
+ min_x = std::min(min_x, x);
+ max_x = std::max(max_x, x);
+ min_y = std::min(min_y, y);
+ max_y = std::max(max_y, y);
+ }
+ }
+ }
+
+ if (generate_tex_coords)
+ {
+ float size_x = max_x - min_x;
+ float size_y = max_y - min_y;
+
+ if ((size_x != 0.0f) && (size_y != 0.0f))
+ {
+ float inv_size_x = 1.0f / size_x;
+ float inv_size_y = -1.0f / size_y;
+ for (unsigned int i = 0; i < m_tex_coords.size(); i += 2)
+ {
+ m_tex_coords[i] *= inv_size_x;
+ m_tex_coords[i + 1] *= inv_size_y;
+ }
+ }
+ }
+
+ return true;
+}
+
+bool GeometryBuffer::set_from_lines(const Lines& lines, float z)
+{
+ m_vertices.clear();
+ m_tex_coords.clear();
+
+ unsigned int size = 6 * (unsigned int)lines.size();
+ if (size == 0)
+ return false;
+
+ m_vertices = std::vector<float>(size, 0.0f);
+
+ unsigned int coord = 0;
+ for (const Line& l : lines)
+ {
+ m_vertices[coord++] = unscale<float>(l.a(0));
+ m_vertices[coord++] = unscale<float>(l.a(1));
+ m_vertices[coord++] = z;
+ m_vertices[coord++] = unscale<float>(l.b(0));
+ m_vertices[coord++] = unscale<float>(l.b(1));
+ m_vertices[coord++] = z;
+ }
+
+ return true;
+}
+
+const float* GeometryBuffer::get_vertices() const
+{
+ return m_vertices.data();
+}
+
+const float* GeometryBuffer::get_tex_coords() const
+{
+ return m_tex_coords.data();
+}
+
+unsigned int GeometryBuffer::get_vertices_count() const
+{
+ return (unsigned int)m_vertices.size() / 3;
+}
+
+Size::Size()
+ : m_width(0)
+ , m_height(0)
+{
+}
+
+Size::Size(int width, int height)
+ : m_width(width)
+ , m_height(height)
+{
+}
+
+int Size::get_width() const
+{
+ return m_width;
+}
+
+void Size::set_width(int width)
+{
+ m_width = width;
+}
+
+int Size::get_height() const
+{
+ return m_height;
+}
+
+void Size::set_height(int height)
+{
+ m_height = height;
+}
+
+Rect::Rect()
+ : m_left(0.0f)
+ , m_top(0.0f)
+ , m_right(0.0f)
+ , m_bottom(0.0f)
+{
+}
+
+Rect::Rect(float left, float top, float right, float bottom)
+ : m_left(left)
+ , m_top(top)
+ , m_right(right)
+ , m_bottom(bottom)
+{
+}
+
+float Rect::get_left() const
+{
+ return m_left;
+}
+
+void Rect::set_left(float left)
+{
+ m_left = left;
+}
+
+float Rect::get_top() const
+{
+ return m_top;
+}
+
+void Rect::set_top(float top)
+{
+ m_top = top;
+}
+
+float Rect::get_right() const
+{
+ return m_right;
+}
+
+void Rect::set_right(float right)
+{
+ m_right = right;
+}
+
+float Rect::get_bottom() const
+{
+ return m_bottom;
+}
+
+void Rect::set_bottom(float bottom)
+{
+ m_bottom = bottom;
+}
+
+GLCanvas3D::Camera::Camera()
+ : type(Ortho)
+ , zoom(1.0f)
+ , phi(45.0f)
+// , distance(0.0f)
+ , target(0.0, 0.0, 0.0)
+ , m_theta(45.0f)
+{
+}
+
+std::string GLCanvas3D::Camera::get_type_as_string() const
+{
+ switch (type)
+ {
+ default:
+ case Unknown:
+ return "unknown";
+// case Perspective:
+// return "perspective";
+ case Ortho:
+ return "ortho";
+ };
+}
+
+float GLCanvas3D::Camera::get_theta() const
+{
+ return m_theta;
+}
+
+void GLCanvas3D::Camera::set_theta(float theta)
+{
+ m_theta = clamp(0.0f, GIMBALL_LOCK_THETA_MAX, theta);
+}
+
+GLCanvas3D::Bed::Bed()
+ : m_type(Custom)
+{
+}
+
+bool GLCanvas3D::Bed::is_prusa() const
+{
+ return (m_type == MK2) || (m_type == MK3);
+}
+
+bool GLCanvas3D::Bed::is_custom() const
+{
+ return m_type == Custom;
+}
+
+const Pointfs& GLCanvas3D::Bed::get_shape() const
+{
+ return m_shape;
+}
+
+bool GLCanvas3D::Bed::set_shape(const Pointfs& shape)
+{
+ EType new_type = _detect_type();
+ if (m_shape == shape && m_type == new_type)
+ // No change, no need to update the UI.
+ return false;
+ m_shape = shape;
+ m_type = new_type;
+
+ _calc_bounding_box();
+
+ ExPolygon poly;
+ for (const Vec2d& p : m_shape)
+ {
+ poly.contour.append(Point(scale_(p(0)), scale_(p(1))));
+ }
+
+ _calc_triangles(poly);
+
+ const BoundingBox& bed_bbox = poly.contour.bounding_box();
+ _calc_gridlines(poly, bed_bbox);
+
+ m_polygon = offset_ex(poly.contour, (float)bed_bbox.radius() * 1.7f, jtRound, scale_(0.5))[0].contour;
+ // Let the calee to update the UI.
+ return true;
+}
+
+const BoundingBoxf3& GLCanvas3D::Bed::get_bounding_box() const
+{
+ return m_bounding_box;
+}
+
+bool GLCanvas3D::Bed::contains(const Point& point) const
+{
+ return m_polygon.contains(point);
+}
+
+Point GLCanvas3D::Bed::point_projection(const Point& point) const
+{
+ return m_polygon.point_projection(point);
+}
+
+void GLCanvas3D::Bed::render(float theta) const
+{
+ switch (m_type)
+ {
+ case MK2:
+ {
+ _render_mk2(theta);
+ break;
+ }
+ case MK3:
+ {
+ _render_mk3(theta);
+ break;
+ }
+ default:
+ case Custom:
+ {
+ _render_custom();
+ break;
+ }
+ }
+}
+
+void GLCanvas3D::Bed::_calc_bounding_box()
+{
+ m_bounding_box = BoundingBoxf3();
+ for (const Vec2d& p : m_shape)
+ {
+ m_bounding_box.merge(Vec3d(p(0), p(1), 0.0));
+ }
+}
+
+void GLCanvas3D::Bed::_calc_triangles(const ExPolygon& poly)
+{
+ Polygons triangles;
+ poly.triangulate(&triangles);
+
+ if (!m_triangles.set_from_triangles(triangles, GROUND_Z, m_type != Custom))
+ printf("Unable to create bed triangles\n");
+}
+
+void GLCanvas3D::Bed::_calc_gridlines(const ExPolygon& poly, const BoundingBox& bed_bbox)
+{
+ Polylines axes_lines;
+ for (coord_t x = bed_bbox.min(0); x <= bed_bbox.max(0); x += scale_(10.0))
+ {
+ Polyline line;
+ line.append(Point(x, bed_bbox.min(1)));
+ line.append(Point(x, bed_bbox.max(1)));
+ axes_lines.push_back(line);
+ }
+ for (coord_t y = bed_bbox.min(1); y <= bed_bbox.max(1); y += scale_(10.0))
+ {
+ Polyline line;
+ line.append(Point(bed_bbox.min(0), y));
+ line.append(Point(bed_bbox.max(0), y));
+ axes_lines.push_back(line);
+ }
+
+ // clip with a slightly grown expolygon because our lines lay on the contours and may get erroneously clipped
+ Lines gridlines = to_lines(intersection_pl(axes_lines, offset(poly, SCALED_EPSILON)));
+
+ // append bed contours
+ Lines contour_lines = to_lines(poly);
+ std::copy(contour_lines.begin(), contour_lines.end(), std::back_inserter(gridlines));
+
+ if (!m_gridlines.set_from_lines(gridlines, GROUND_Z))
+ printf("Unable to create bed grid lines\n");
+}
+
+GLCanvas3D::Bed::EType GLCanvas3D::Bed::_detect_type() const
+{
+ EType type = Custom;
+
+ const PresetBundle* bundle = get_preset_bundle();
+ if (bundle != nullptr)
+ {
+ const Preset* curr = &bundle->printers.get_selected_preset();
+ while (curr != nullptr)
+ {
+ if (curr->config.has("bed_shape") && _are_equal(m_shape, dynamic_cast<const ConfigOptionPoints*>(curr->config.option("bed_shape"))->values))
+ {
+ if ((curr->vendor != nullptr) && (curr->vendor->name == "Prusa Research"))
+ {
+ if (boost::contains(curr->name, "MK2"))
+ {
+ type = MK2;
+ break;
+ }
+ else if (boost::contains(curr->name, "MK3"))
+ {
+ type = MK3;
+ break;
+ }
+ }
+ }
+
+ curr = bundle->printers.get_preset_parent(*curr);
+ }
+ }
+
+ return type;
+}
+
+void GLCanvas3D::Bed::_render_mk2(float theta) const
+{
+ std::string filename = resources_dir() + "/icons/bed/mk2_top.png";
+ if ((m_top_texture.get_id() == 0) || (m_top_texture.get_source() != filename))
+ {
+ if (!m_top_texture.load_from_file(filename, true))
+ {
+ _render_custom();
+ return;
+ }
+ }
+
+ filename = resources_dir() + "/icons/bed/mk2_bottom.png";
+ if ((m_bottom_texture.get_id() == 0) || (m_bottom_texture.get_source() != filename))
+ {
+ if (!m_bottom_texture.load_from_file(filename, true))
+ {
+ _render_custom();
+ return;
+ }
+ }
+
+ _render_prusa(theta);
+}
+
+void GLCanvas3D::Bed::_render_mk3(float theta) const
+{
+ std::string filename = resources_dir() + "/icons/bed/mk3_top.png";
+ if ((m_top_texture.get_id() == 0) || (m_top_texture.get_source() != filename))
+ {
+ if (!m_top_texture.load_from_file(filename, true))
+ {
+ _render_custom();
+ return;
+ }
+ }
+
+ filename = resources_dir() + "/icons/bed/mk3_bottom.png";
+ if ((m_bottom_texture.get_id() == 0) || (m_bottom_texture.get_source() != filename))
+ {
+ if (!m_bottom_texture.load_from_file(filename, true))
+ {
+ _render_custom();
+ return;
+ }
+ }
+
+ _render_prusa(theta);
+}
+
+void GLCanvas3D::Bed::_render_prusa(float theta) const
+{
+ unsigned int triangles_vcount = m_triangles.get_vertices_count();
+ if (triangles_vcount > 0)
+ {
+ ::glEnable(GL_DEPTH_TEST);
+ ::glDepthMask(GL_FALSE);
+
+ ::glEnable(GL_BLEND);
+ ::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ ::glEnable(GL_TEXTURE_2D);
+ ::glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
+
+ ::glEnableClientState(GL_VERTEX_ARRAY);
+ ::glEnableClientState(GL_TEXTURE_COORD_ARRAY);
+
+ if (theta > 90.0f)
+ ::glFrontFace(GL_CW);
+
+ ::glBindTexture(GL_TEXTURE_2D, (theta <= 90.0f) ? (GLuint)m_top_texture.get_id() : (GLuint)m_bottom_texture.get_id());
+ ::glVertexPointer(3, GL_FLOAT, 0, (GLvoid*)m_triangles.get_vertices());
+ ::glTexCoordPointer(2, GL_FLOAT, 0, (GLvoid*)m_triangles.get_tex_coords());
+ ::glDrawArrays(GL_TRIANGLES, 0, (GLsizei)triangles_vcount);
+
+ if (theta > 90.0f)
+ ::glFrontFace(GL_CCW);
+
+ ::glBindTexture(GL_TEXTURE_2D, 0);
+ ::glDisableClientState(GL_TEXTURE_COORD_ARRAY);
+ ::glDisableClientState(GL_VERTEX_ARRAY);
+
+ ::glDisable(GL_TEXTURE_2D);
+
+ ::glDisable(GL_BLEND);
+ ::glDepthMask(GL_TRUE);
+ }
+}
+
+void GLCanvas3D::Bed::_render_custom() const
+{
+ m_top_texture.reset();
+ m_bottom_texture.reset();
+
+ unsigned int triangles_vcount = m_triangles.get_vertices_count();
+ if (triangles_vcount > 0)
+ {
+ ::glEnable(GL_LIGHTING);
+ ::glDisable(GL_DEPTH_TEST);
+
+ ::glEnable(GL_BLEND);
+ ::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ ::glEnableClientState(GL_VERTEX_ARRAY);
+
+ ::glColor4f(0.8f, 0.6f, 0.5f, 0.4f);
+ ::glNormal3d(0.0f, 0.0f, 1.0f);
+ ::glVertexPointer(3, GL_FLOAT, 0, (GLvoid*)m_triangles.get_vertices());
+ ::glDrawArrays(GL_TRIANGLES, 0, (GLsizei)triangles_vcount);
+
+ // draw grid
+ unsigned int gridlines_vcount = m_gridlines.get_vertices_count();
+
+ // we need depth test for grid, otherwise it would disappear when looking the object from below
+ ::glEnable(GL_DEPTH_TEST);
+ ::glLineWidth(3.0f);
+ ::glColor4f(0.2f, 0.2f, 0.2f, 0.4f);
+ ::glVertexPointer(3, GL_FLOAT, 0, (GLvoid*)m_gridlines.get_vertices());
+ ::glDrawArrays(GL_LINES, 0, (GLsizei)gridlines_vcount);
+
+ ::glDisableClientState(GL_VERTEX_ARRAY);
+
+ ::glDisable(GL_BLEND);
+ ::glDisable(GL_LIGHTING);
+ }
+}
+
+bool GLCanvas3D::Bed::_are_equal(const Pointfs& bed_1, const Pointfs& bed_2)
+{
+ if (bed_1.size() != bed_2.size())
+ return false;
+
+ for (unsigned int i = 0; i < (unsigned int)bed_1.size(); ++i)
+ {
+ if (bed_1[i] != bed_2[i])
+ return false;
+ }
+
+ return true;
+}
+
+GLCanvas3D::Axes::Axes()
+ : origin(0, 0, 0), length(0.0f)
+{
+}
+
+void GLCanvas3D::Axes::render(bool depth_test) const
+{
+ if (depth_test)
+ ::glEnable(GL_DEPTH_TEST);
+ else
+ ::glDisable(GL_DEPTH_TEST);
+
+ ::glLineWidth(2.0f);
+ ::glBegin(GL_LINES);
+ // draw line for x axis
+ ::glColor3f(1.0f, 0.0f, 0.0f);
+ ::glVertex3f((GLfloat)origin(0), (GLfloat)origin(1), (GLfloat)origin(2));
+ ::glVertex3f((GLfloat)origin(0) + length, (GLfloat)origin(1), (GLfloat)origin(2));
+ // draw line for y axis
+ ::glColor3f(0.0f, 1.0f, 0.0f);
+ ::glVertex3f((GLfloat)origin(0), (GLfloat)origin(1), (GLfloat)origin(2));
+ ::glVertex3f((GLfloat)origin(0), (GLfloat)origin(1) + length, (GLfloat)origin(2));
+ ::glEnd();
+ // draw line for Z axis
+ // (re-enable depth test so that axis is correctly shown when objects are behind it)
+ if (!depth_test)
+ ::glEnable(GL_DEPTH_TEST);
+
+ ::glBegin(GL_LINES);
+ ::glColor3f(0.0f, 0.0f, 1.0f);
+ ::glVertex3f((GLfloat)origin(0), (GLfloat)origin(1), (GLfloat)origin(2));
+ ::glVertex3f((GLfloat)origin(0), (GLfloat)origin(1), (GLfloat)origin(2) + length);
+ ::glEnd();
+}
+
+GLCanvas3D::CuttingPlane::CuttingPlane()
+ : m_z(-1.0f)
+{
+}
+
+bool GLCanvas3D::CuttingPlane::set(float z, const ExPolygons& polygons)
+{
+ m_z = z;
+
+ // grow slices in order to display them better
+ ExPolygons expolygons = offset_ex(polygons, scale_(0.1));
+ Lines lines = to_lines(expolygons);
+ return m_lines.set_from_lines(lines, m_z);
+}
+
+void GLCanvas3D::CuttingPlane::render(const BoundingBoxf3& bb) const
+{
+ _render_plane(bb);
+ _render_contour();
+}
+
+void GLCanvas3D::CuttingPlane::_render_plane(const BoundingBoxf3& bb) const
+{
+ if (m_z >= 0.0f)
+ {
+ ::glDisable(GL_CULL_FACE);
+ ::glEnable(GL_BLEND);
+ ::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ float margin = 20.0f;
+ float min_x = bb.min(0) - margin;
+ float max_x = bb.max(0) + margin;
+ float min_y = bb.min(1) - margin;
+ float max_y = bb.max(1) + margin;
+
+ ::glBegin(GL_QUADS);
+ ::glColor4f(0.8f, 0.8f, 0.8f, 0.5f);
+ ::glVertex3f(min_x, min_y, m_z);
+ ::glVertex3f(max_x, min_y, m_z);
+ ::glVertex3f(max_x, max_y, m_z);
+ ::glVertex3f(min_x, max_y, m_z);
+ ::glEnd();
+
+ ::glEnable(GL_CULL_FACE);
+ ::glDisable(GL_BLEND);
+ }
+}
+
+void GLCanvas3D::CuttingPlane::_render_contour() const
+{
+ ::glEnableClientState(GL_VERTEX_ARRAY);
+
+ if (m_z >= 0.0f)
+ {
+ unsigned int lines_vcount = m_lines.get_vertices_count();
+
+ ::glLineWidth(2.0f);
+ ::glColor3f(0.0f, 0.0f, 0.0f);
+ ::glVertexPointer(3, GL_FLOAT, 0, (GLvoid*)m_lines.get_vertices());
+ ::glDrawArrays(GL_LINES, 0, (GLsizei)lines_vcount);
+ }
+
+ ::glDisableClientState(GL_VERTEX_ARRAY);
+}
+
+GLCanvas3D::Shader::Shader()
+ : m_shader(nullptr)
+{
+}
+
+GLCanvas3D::Shader::~Shader()
+{
+ _reset();
+}
+
+bool GLCanvas3D::Shader::init(const std::string& vertex_shader_filename, const std::string& fragment_shader_filename)
+{
+ if (is_initialized())
+ return true;
+
+ m_shader = new GLShader();
+ if (m_shader != nullptr)
+ {
+ if (!m_shader->load_from_file(fragment_shader_filename.c_str(), vertex_shader_filename.c_str()))
+ {
+ std::cout << "Compilaton of shader failed:" << std::endl;
+ std::cout << m_shader->last_error << std::endl;
+ _reset();
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool GLCanvas3D::Shader::is_initialized() const
+{
+ return (m_shader != nullptr);
+}
+
+bool GLCanvas3D::Shader::start_using() const
+{
+ if (is_initialized())
+ {
+ m_shader->enable();
+ return true;
+ }
+ else
+ return false;
+}
+
+void GLCanvas3D::Shader::stop_using() const
+{
+ if (m_shader != nullptr)
+ m_shader->disable();
+}
+
+void GLCanvas3D::Shader::set_uniform(const std::string& name, float value) const
+{
+ if (m_shader != nullptr)
+ m_shader->set_uniform(name.c_str(), value);
+}
+
+void GLCanvas3D::Shader::set_uniform(const std::string& name, const float* matrix) const
+{
+ if (m_shader != nullptr)
+ m_shader->set_uniform(name.c_str(), matrix);
+}
+
+const GLShader* GLCanvas3D::Shader::get_shader() const
+{
+ return m_shader;
+}
+
+void GLCanvas3D::Shader::_reset()
+{
+ if (m_shader != nullptr)
+ {
+ m_shader->release();
+ delete m_shader;
+ m_shader = nullptr;
+ }
+}
+
+GLCanvas3D::LayersEditing::LayersEditing()
+ : m_use_legacy_opengl(false)
+ , m_enabled(false)
+ , m_z_texture_id(0)
+ , state(Unknown)
+ , band_width(2.0f)
+ , strength(0.005f)
+ , last_object_id(-1)
+ , last_z(0.0f)
+ , last_action(0)
+{
+}
+
+GLCanvas3D::LayersEditing::~LayersEditing()
+{
+ if (m_z_texture_id != 0)
+ {
+ ::glDeleteTextures(1, &m_z_texture_id);
+ m_z_texture_id = 0;
+ }
+}
+
+bool GLCanvas3D::LayersEditing::init(const std::string& vertex_shader_filename, const std::string& fragment_shader_filename)
+{
+ if (!m_shader.init(vertex_shader_filename, fragment_shader_filename))
+ return false;
+
+ ::glGenTextures(1, (GLuint*)&m_z_texture_id);
+ ::glBindTexture(GL_TEXTURE_2D, m_z_texture_id);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ ::glBindTexture(GL_TEXTURE_2D, 0);
+
+ return true;
+}
+
+bool GLCanvas3D::LayersEditing::is_allowed() const
+{
+ return !m_use_legacy_opengl && m_shader.is_initialized();
+}
+
+void GLCanvas3D::LayersEditing::set_use_legacy_opengl(bool use_legacy_opengl)
+{
+ m_use_legacy_opengl = use_legacy_opengl;
+}
+
+bool GLCanvas3D::LayersEditing::is_enabled() const
+{
+ return m_enabled;
+}
+
+void GLCanvas3D::LayersEditing::set_enabled(bool enabled)
+{
+ m_enabled = is_allowed() && enabled;
+}
+
+unsigned int GLCanvas3D::LayersEditing::get_z_texture_id() const
+{
+ return m_z_texture_id;
+}
+
+void GLCanvas3D::LayersEditing::render(const GLCanvas3D& canvas, const PrintObject& print_object, const GLVolume& volume) const
+{
+ if (!m_enabled)
+ return;
+
+ const Rect& bar_rect = get_bar_rect_viewport(canvas);
+ const Rect& reset_rect = get_reset_rect_viewport(canvas);
+
+ ::glDisable(GL_DEPTH_TEST);
+
+ // The viewport and camera are set to complete view and glOrtho(-$x / 2, $x / 2, -$y / 2, $y / 2, -$depth, $depth),
+ // where x, y is the window size divided by $self->_zoom.
+ ::glPushMatrix();
+ ::glLoadIdentity();
+
+ _render_tooltip_texture(canvas, bar_rect, reset_rect);
+ _render_reset_texture(reset_rect);
+ _render_active_object_annotations(canvas, volume, print_object, bar_rect);
+ _render_profile(print_object, bar_rect);
+
+ // Revert the matrices.
+ ::glPopMatrix();
+
+ ::glEnable(GL_DEPTH_TEST);
+}
+
+int GLCanvas3D::LayersEditing::get_shader_program_id() const
+{
+ const GLShader* shader = m_shader.get_shader();
+ return (shader != nullptr) ? shader->shader_program_id : -1;
+}
+
+float GLCanvas3D::LayersEditing::get_cursor_z_relative(const GLCanvas3D& canvas)
+{
+ const Point& mouse_pos = canvas.get_local_mouse_position();
+ const Rect& rect = get_bar_rect_screen(canvas);
+ float x = (float)mouse_pos(0);
+ float y = (float)mouse_pos(1);
+ float t = rect.get_top();
+ float b = rect.get_bottom();
+
+ return ((rect.get_left() <= x) && (x <= rect.get_right()) && (t <= y) && (y <= b)) ?
+ // Inside the bar.
+ (b - y - 1.0f) / (b - t - 1.0f) :
+ // Outside the bar.
+ -1000.0f;
+}
+
+bool GLCanvas3D::LayersEditing::bar_rect_contains(const GLCanvas3D& canvas, float x, float y)
+{
+ const Rect& rect = get_bar_rect_screen(canvas);
+ return (rect.get_left() <= x) && (x <= rect.get_right()) && (rect.get_top() <= y) && (y <= rect.get_bottom());
+}
+
+bool GLCanvas3D::LayersEditing::reset_rect_contains(const GLCanvas3D& canvas, float x, float y)
+{
+ const Rect& rect = get_reset_rect_screen(canvas);
+ return (rect.get_left() <= x) && (x <= rect.get_right()) && (rect.get_top() <= y) && (y <= rect.get_bottom());
+}
+
+Rect GLCanvas3D::LayersEditing::get_bar_rect_screen(const GLCanvas3D& canvas)
+{
+ const Size& cnv_size = canvas.get_canvas_size();
+ float w = (float)cnv_size.get_width();
+ float h = (float)cnv_size.get_height();
+
+ return Rect(w - VARIABLE_LAYER_THICKNESS_BAR_WIDTH, 0.0f, w, h - VARIABLE_LAYER_THICKNESS_RESET_BUTTON_HEIGHT);
+}
+
+Rect GLCanvas3D::LayersEditing::get_reset_rect_screen(const GLCanvas3D& canvas)
+{
+ const Size& cnv_size = canvas.get_canvas_size();
+ float w = (float)cnv_size.get_width();
+ float h = (float)cnv_size.get_height();
+
+ return Rect(w - VARIABLE_LAYER_THICKNESS_BAR_WIDTH, h - VARIABLE_LAYER_THICKNESS_RESET_BUTTON_HEIGHT, w, h);
+}
+
+Rect GLCanvas3D::LayersEditing::get_bar_rect_viewport(const GLCanvas3D& canvas)
+{
+ const Size& cnv_size = canvas.get_canvas_size();
+ float half_w = 0.5f * (float)cnv_size.get_width();
+ float half_h = 0.5f * (float)cnv_size.get_height();
+
+ float zoom = canvas.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+
+ return Rect((half_w - VARIABLE_LAYER_THICKNESS_BAR_WIDTH) * inv_zoom, half_h * inv_zoom, half_w * inv_zoom, (-half_h + VARIABLE_LAYER_THICKNESS_RESET_BUTTON_HEIGHT) * inv_zoom);
+}
+
+Rect GLCanvas3D::LayersEditing::get_reset_rect_viewport(const GLCanvas3D& canvas)
+{
+ const Size& cnv_size = canvas.get_canvas_size();
+ float half_w = 0.5f * (float)cnv_size.get_width();
+ float half_h = 0.5f * (float)cnv_size.get_height();
+
+ float zoom = canvas.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+
+ return Rect((half_w - VARIABLE_LAYER_THICKNESS_BAR_WIDTH) * inv_zoom, (-half_h + VARIABLE_LAYER_THICKNESS_RESET_BUTTON_HEIGHT) * inv_zoom, half_w * inv_zoom, -half_h * inv_zoom);
+}
+
+
+bool GLCanvas3D::LayersEditing::_is_initialized() const
+{
+ return m_shader.is_initialized();
+}
+
+void GLCanvas3D::LayersEditing::_render_tooltip_texture(const GLCanvas3D& canvas, const Rect& bar_rect, const Rect& reset_rect) const
+{
+ if (m_tooltip_texture.get_id() == 0)
+ {
+ std::string filename = resources_dir() + "/icons/variable_layer_height_tooltip.png";
+ if (!m_tooltip_texture.load_from_file(filename, false))
+ return;
+ }
+
+ float zoom = canvas.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+ float gap = 10.0f * inv_zoom;
+
+ float bar_left = bar_rect.get_left();
+ float reset_bottom = reset_rect.get_bottom();
+
+ float l = bar_left - (float)m_tooltip_texture.get_width() * inv_zoom - gap;
+ float r = bar_left - gap;
+ float t = reset_bottom + (float)m_tooltip_texture.get_height() * inv_zoom + gap;
+ float b = reset_bottom + gap;
+
+ GLTexture::render_texture(m_tooltip_texture.get_id(), l, r, b, t);
+}
+
+void GLCanvas3D::LayersEditing::_render_reset_texture(const Rect& reset_rect) const
+{
+ if (m_reset_texture.get_id() == 0)
+ {
+ std::string filename = resources_dir() + "/icons/variable_layer_height_reset.png";
+ if (!m_reset_texture.load_from_file(filename, false))
+ return;
+ }
+
+ GLTexture::render_texture(m_reset_texture.get_id(), reset_rect.get_left(), reset_rect.get_right(), reset_rect.get_bottom(), reset_rect.get_top());
+}
+
+void GLCanvas3D::LayersEditing::_render_active_object_annotations(const GLCanvas3D& canvas, const GLVolume& volume, const PrintObject& print_object, const Rect& bar_rect) const
+{
+ float max_z = print_object.model_object()->bounding_box().max(2);
+
+ m_shader.start_using();
+
+ m_shader.set_uniform("z_to_texture_row", (float)volume.layer_height_texture_z_to_row_id());
+ m_shader.set_uniform("z_texture_row_to_normalized", 1.0f / (float)volume.layer_height_texture_height());
+ m_shader.set_uniform("z_cursor", max_z * get_cursor_z_relative(canvas));
+ m_shader.set_uniform("z_cursor_band_width", band_width);
+ // The shader requires the original model coordinates when rendering to the texture, so we pass it the unit matrix
+ m_shader.set_uniform("volume_world_matrix", UNIT_MATRIX);
+
+ GLsizei w = (GLsizei)volume.layer_height_texture_width();
+ GLsizei h = (GLsizei)volume.layer_height_texture_height();
+ GLsizei half_w = w / 2;
+ GLsizei half_h = h / 2;
+
+ ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ ::glBindTexture(GL_TEXTURE_2D, m_z_texture_id);
+ ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
+ ::glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, half_w, half_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
+ ::glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, volume.layer_height_texture_data_ptr_level0());
+ ::glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, half_w, half_h, GL_RGBA, GL_UNSIGNED_BYTE, volume.layer_height_texture_data_ptr_level1());
+
+ // Render the color bar
+ float l = bar_rect.get_left();
+ float r = bar_rect.get_right();
+ float t = bar_rect.get_top();
+ float b = bar_rect.get_bottom();
+
+ ::glBegin(GL_QUADS);
+ ::glVertex3f(l, b, 0.0f);
+ ::glVertex3f(r, b, 0.0f);
+ ::glVertex3f(r, t, max_z);
+ ::glVertex3f(l, t, max_z);
+ ::glEnd();
+ ::glBindTexture(GL_TEXTURE_2D, 0);
+
+ m_shader.stop_using();
+}
+
+void GLCanvas3D::LayersEditing::_render_profile(const PrintObject& print_object, const Rect& bar_rect) const
+{
+ // FIXME show some kind of legend.
+
+ // Get a maximum layer height value.
+ // FIXME This is a duplicate code of Slicing.cpp.
+ double layer_height_max = DBL_MAX;
+ const PrintConfig& print_config = print_object.print()->config();
+ const std::vector<double>& nozzle_diameters = dynamic_cast<const ConfigOptionFloats*>(print_config.option("nozzle_diameter"))->values;
+ const std::vector<double>& layer_heights_min = dynamic_cast<const ConfigOptionFloats*>(print_config.option("min_layer_height"))->values;
+ const std::vector<double>& layer_heights_max = dynamic_cast<const ConfigOptionFloats*>(print_config.option("max_layer_height"))->values;
+ for (unsigned int i = 0; i < (unsigned int)nozzle_diameters.size(); ++i)
+ {
+ double lh_min = (layer_heights_min[i] == 0.0) ? 0.07 : std::max(0.01, layer_heights_min[i]);
+ double lh_max = (layer_heights_max[i] == 0.0) ? (0.75 * nozzle_diameters[i]) : layer_heights_max[i];
+ layer_height_max = std::min(layer_height_max, std::max(lh_min, lh_max));
+ }
+
+ // Make the vertical bar a bit wider so the layer height curve does not touch the edge of the bar region.
+ layer_height_max *= 1.12;
+
+ double max_z = unscale<double>(print_object.size(2));
+ double layer_height = dynamic_cast<const ConfigOptionFloat*>(print_object.config().option("layer_height"))->value;
+ float l = bar_rect.get_left();
+ float w = bar_rect.get_right() - l;
+ float b = bar_rect.get_bottom();
+ float t = bar_rect.get_top();
+ float h = t - b;
+ float scale_x = w / (float)layer_height_max;
+ float scale_y = h / (float)max_z;
+ float x = l + (float)layer_height * scale_x;
+
+ // Baseline
+ ::glColor3f(0.0f, 0.0f, 0.0f);
+ ::glBegin(GL_LINE_STRIP);
+ ::glVertex2f(x, b);
+ ::glVertex2f(x, t);
+ ::glEnd();
+
+ // Curve
+ const ModelObject* model_object = print_object.model_object();
+ if (model_object->layer_height_profile_valid)
+ {
+ const std::vector<double>& profile = model_object->layer_height_profile;
+
+ ::glColor3f(0.0f, 0.0f, 1.0f);
+ ::glBegin(GL_LINE_STRIP);
+ for (unsigned int i = 0; i < profile.size(); i += 2)
+ {
+ ::glVertex2f(l + (float)profile[i + 1] * scale_x, b + (float)profile[i] * scale_y);
+ }
+ ::glEnd();
+ }
+}
+
+const Point GLCanvas3D::Mouse::Drag::Invalid_2D_Point(INT_MAX, INT_MAX);
+const Vec3d GLCanvas3D::Mouse::Drag::Invalid_3D_Point(DBL_MAX, DBL_MAX, DBL_MAX);
+
+GLCanvas3D::Mouse::Drag::Drag()
+ : start_position_2D(Invalid_2D_Point)
+ , start_position_3D(Invalid_3D_Point)
+ , volume_center_offset(0, 0, 0)
+ , move_with_shift(false)
+ , move_volume_idx(-1)
+ , gizmo_volume_idx(-1)
+{
+}
+
+GLCanvas3D::Mouse::Mouse()
+ : dragging(false)
+ , position(DBL_MAX, DBL_MAX)
+{
+}
+
+void GLCanvas3D::Mouse::set_start_position_2D_as_invalid()
+{
+ drag.start_position_2D = Drag::Invalid_2D_Point;
+}
+
+void GLCanvas3D::Mouse::set_start_position_3D_as_invalid()
+{
+ drag.start_position_3D = Drag::Invalid_3D_Point;
+}
+
+bool GLCanvas3D::Mouse::is_start_position_2D_defined() const
+{
+ return (drag.start_position_2D != Drag::Invalid_2D_Point);
+}
+
+bool GLCanvas3D::Mouse::is_start_position_3D_defined() const
+{
+ return (drag.start_position_3D != Drag::Invalid_3D_Point);
+}
+
+const float GLCanvas3D::Gizmos::OverlayTexturesScale = 0.75f;
+const float GLCanvas3D::Gizmos::OverlayOffsetX = 10.0f * OverlayTexturesScale;
+const float GLCanvas3D::Gizmos::OverlayGapY = 5.0f * OverlayTexturesScale;
+
+GLCanvas3D::Gizmos::Gizmos()
+ : m_enabled(false)
+ , m_current(Undefined)
+{
+}
+
+GLCanvas3D::Gizmos::~Gizmos()
+{
+ _reset();
+}
+
+bool GLCanvas3D::Gizmos::init(GLCanvas3D& parent)
+{
+ GLGizmoBase* gizmo = new GLGizmoMove3D(parent);
+ if (gizmo == nullptr)
+ return false;
+
+ if (!gizmo->init())
+ return false;
+
+#if !ENABLE_MODELINSTANCE_3D_OFFSET
+ // temporary disable z grabber
+ gizmo->disable_grabber(2);
+#endif // !ENABLE_MODELINSTANCE_3D_OFFSET
+
+ m_gizmos.insert(GizmosMap::value_type(Move, gizmo));
+
+ gizmo = new GLGizmoScale3D(parent);
+ if (gizmo == nullptr)
+ return false;
+
+ if (!gizmo->init())
+ return false;
+
+ // temporary disable x grabbers
+ gizmo->disable_grabber(0);
+ gizmo->disable_grabber(1);
+ // temporary disable y grabbers
+ gizmo->disable_grabber(2);
+ gizmo->disable_grabber(3);
+ // temporary disable z grabbers
+ gizmo->disable_grabber(4);
+ gizmo->disable_grabber(5);
+
+ m_gizmos.insert(GizmosMap::value_type(Scale, gizmo));
+
+ gizmo = new GLGizmoRotate3D(parent);
+ if (gizmo == nullptr)
+ {
+ _reset();
+ return false;
+ }
+
+ if (!gizmo->init())
+ {
+ _reset();
+ return false;
+ }
+
+ // temporary disable x and y grabbers
+ gizmo->disable_grabber(0);
+ gizmo->disable_grabber(1);
+
+ m_gizmos.insert(GizmosMap::value_type(Rotate, gizmo));
+
+ gizmo = new GLGizmoFlatten(parent);
+ if (gizmo == nullptr)
+ return false;
+
+ if (!gizmo->init()) {
+ _reset();
+ return false;
+ }
+
+ m_gizmos.insert(GizmosMap::value_type(Flatten, gizmo));
+
+
+ return true;
+}
+
+bool GLCanvas3D::Gizmos::is_enabled() const
+{
+ return m_enabled;
+}
+
+void GLCanvas3D::Gizmos::set_enabled(bool enable)
+{
+ m_enabled = enable;
+}
+
+void GLCanvas3D::Gizmos::update_hover_state(const GLCanvas3D& canvas, const Vec2d& mouse_pos)
+{
+ if (!m_enabled)
+ return;
+
+ float cnv_h = (float)canvas.get_canvas_size().get_height();
+ float height = _get_total_overlay_height();
+ float top_y = 0.5f * (cnv_h - height);
+ for (GizmosMap::const_iterator it = m_gizmos.begin(); it != m_gizmos.end(); ++it)
+ {
+ if (it->second == nullptr)
+ continue;
+
+ float tex_size = (float)it->second->get_textures_size() * OverlayTexturesScale;
+ float half_tex_size = 0.5f * tex_size;
+
+ // we currently use circular icons for gizmo, so we check the radius
+ if (it->second->get_state() != GLGizmoBase::On)
+ {
+ bool inside = (mouse_pos - Vec2d(OverlayOffsetX + half_tex_size, top_y + half_tex_size)).norm() < half_tex_size;
+ it->second->set_state(inside ? GLGizmoBase::Hover : GLGizmoBase::Off);
+ }
+ top_y += (tex_size + OverlayGapY);
+ }
+}
+
+void GLCanvas3D::Gizmos::update_on_off_state(const GLCanvas3D& canvas, const Vec2d& mouse_pos)
+{
+ if (!m_enabled)
+ return;
+
+ float cnv_h = (float)canvas.get_canvas_size().get_height();
+ float height = _get_total_overlay_height();
+ float top_y = 0.5f * (cnv_h - height);
+ for (GizmosMap::const_iterator it = m_gizmos.begin(); it != m_gizmos.end(); ++it)
+ {
+ if (it->second == nullptr)
+ continue;
+
+ float tex_size = (float)it->second->get_textures_size() * OverlayTexturesScale;
+ float half_tex_size = 0.5f * tex_size;
+
+ // we currently use circular icons for gizmo, so we check the radius
+ if ((mouse_pos - Vec2d(OverlayOffsetX + half_tex_size, top_y + half_tex_size)).norm() < half_tex_size)
+ {
+ if ((it->second->get_state() == GLGizmoBase::On))
+ {
+ it->second->set_state(GLGizmoBase::Off);
+ m_current = Undefined;
+ }
+ else
+ {
+ it->second->set_state(GLGizmoBase::On);
+ m_current = it->first;
+ }
+ }
+ else
+ it->second->set_state(GLGizmoBase::Off);
+
+ top_y += (tex_size + OverlayGapY);
+ }
+}
+
+void GLCanvas3D::Gizmos::reset_all_states()
+{
+ if (!m_enabled)
+ return;
+
+ for (GizmosMap::const_iterator it = m_gizmos.begin(); it != m_gizmos.end(); ++it)
+ {
+ if (it->second != nullptr)
+ {
+ it->second->set_state(GLGizmoBase::Off);
+ it->second->set_hover_id(-1);
+ }
+ }
+
+ m_current = Undefined;
+}
+
+void GLCanvas3D::Gizmos::set_hover_id(int id)
+{
+ if (!m_enabled)
+ return;
+
+ for (GizmosMap::const_iterator it = m_gizmos.begin(); it != m_gizmos.end(); ++it)
+ {
+ if ((it->second != nullptr) && (it->second->get_state() == GLGizmoBase::On))
+ it->second->set_hover_id(id);
+ }
+}
+
+bool GLCanvas3D::Gizmos::overlay_contains_mouse(const GLCanvas3D& canvas, const Vec2d& mouse_pos) const
+{
+ if (!m_enabled)
+ return false;
+
+ float cnv_h = (float)canvas.get_canvas_size().get_height();
+ float height = _get_total_overlay_height();
+ float top_y = 0.5f * (cnv_h - height);
+ for (GizmosMap::const_iterator it = m_gizmos.begin(); it != m_gizmos.end(); ++it)
+ {
+ if (it->second == nullptr)
+ continue;
+
+ float tex_size = (float)it->second->get_textures_size() * OverlayTexturesScale;
+ float half_tex_size = 0.5f * tex_size;
+
+ // we currently use circular icons for gizmo, so we check the radius
+ if ((mouse_pos - Vec2d(OverlayOffsetX + half_tex_size, top_y + half_tex_size)).norm() < half_tex_size)
+ return true;
+
+ top_y += (tex_size + OverlayGapY);
+ }
+
+ return false;
+}
+
+bool GLCanvas3D::Gizmos::grabber_contains_mouse() const
+{
+ if (!m_enabled)
+ return false;
+
+ GLGizmoBase* curr = _get_current();
+ return (curr != nullptr) ? (curr->get_hover_id() != -1) : false;
+}
+
+void GLCanvas3D::Gizmos::update(const Linef3& mouse_ray)
+{
+ if (!m_enabled)
+ return;
+
+ GLGizmoBase* curr = _get_current();
+ if (curr != nullptr)
+ curr->update(mouse_ray);
+}
+
+GLCanvas3D::Gizmos::EType GLCanvas3D::Gizmos::get_current_type() const
+{
+ return m_current;
+}
+
+bool GLCanvas3D::Gizmos::is_running() const
+{
+ if (!m_enabled)
+ return false;
+
+ GLGizmoBase* curr = _get_current();
+ return (curr != nullptr) ? (curr->get_state() == GLGizmoBase::On) : false;
+}
+
+bool GLCanvas3D::Gizmos::is_dragging() const
+{
+ GLGizmoBase* curr = _get_current();
+ return (curr != nullptr) ? curr->is_dragging() : false;
+}
+
+void GLCanvas3D::Gizmos::start_dragging(const BoundingBoxf3& box)
+{
+ GLGizmoBase* curr = _get_current();
+ if (curr != nullptr)
+ curr->start_dragging(box);
+}
+
+void GLCanvas3D::Gizmos::stop_dragging()
+{
+ GLGizmoBase* curr = _get_current();
+ if (curr != nullptr)
+ curr->stop_dragging();
+}
+
+Vec3d GLCanvas3D::Gizmos::get_position() const
+{
+ if (!m_enabled)
+ return Vec3d::Zero();
+
+ GizmosMap::const_iterator it = m_gizmos.find(Move);
+ return (it != m_gizmos.end()) ? reinterpret_cast<GLGizmoMove3D*>(it->second)->get_position() : Vec3d::Zero();
+}
+
+void GLCanvas3D::Gizmos::set_position(const Vec3d& position)
+{
+ if (!m_enabled)
+ return;
+
+ GizmosMap::const_iterator it = m_gizmos.find(Move);
+ if (it != m_gizmos.end())
+ reinterpret_cast<GLGizmoMove3D*>(it->second)->set_position(position);
+}
+
+float GLCanvas3D::Gizmos::get_scale() const
+{
+ if (!m_enabled)
+ return 1.0f;
+
+ GizmosMap::const_iterator it = m_gizmos.find(Scale);
+ return (it != m_gizmos.end()) ? reinterpret_cast<GLGizmoScale3D*>(it->second)->get_scale_x() : 1.0f;
+}
+
+void GLCanvas3D::Gizmos::set_scale(float scale)
+{
+ if (!m_enabled)
+ return;
+
+ GizmosMap::const_iterator it = m_gizmos.find(Scale);
+ if (it != m_gizmos.end())
+ reinterpret_cast<GLGizmoScale3D*>(it->second)->set_scale(scale);
+}
+
+float GLCanvas3D::Gizmos::get_angle_z() const
+{
+ if (!m_enabled)
+ return 0.0f;
+
+ GizmosMap::const_iterator it = m_gizmos.find(Rotate);
+ return (it != m_gizmos.end()) ? reinterpret_cast<GLGizmoRotate3D*>(it->second)->get_angle_z() : 0.0f;
+}
+
+void GLCanvas3D::Gizmos::set_angle_z(float angle_z)
+{
+ if (!m_enabled)
+ return;
+
+ GizmosMap::const_iterator it = m_gizmos.find(Rotate);
+ if (it != m_gizmos.end())
+ reinterpret_cast<GLGizmoRotate3D*>(it->second)->set_angle_z(angle_z);
+}
+
+Vec3d GLCanvas3D::Gizmos::get_flattening_normal() const
+{
+ if (!m_enabled)
+ return Vec3d::Zero();
+
+ GizmosMap::const_iterator it = m_gizmos.find(Flatten);
+ return (it != m_gizmos.end()) ? reinterpret_cast<GLGizmoFlatten*>(it->second)->get_flattening_normal() : Vec3d::Zero();
+}
+
+void GLCanvas3D::Gizmos::set_flattening_data(const ModelObject* model_object)
+{
+ if (!m_enabled)
+ return;
+
+ GizmosMap::const_iterator it = m_gizmos.find(Flatten);
+ if (it != m_gizmos.end())
+ reinterpret_cast<GLGizmoFlatten*>(it->second)->set_flattening_data(model_object);
+}
+
+void GLCanvas3D::Gizmos::render_current_gizmo(const BoundingBoxf3& box) const
+{
+ if (!m_enabled)
+ return;
+
+ ::glDisable(GL_DEPTH_TEST);
+
+ if (box.radius() > 0.0)
+ _render_current_gizmo(box);
+}
+
+void GLCanvas3D::Gizmos::render_current_gizmo_for_picking_pass(const BoundingBoxf3& box) const
+{
+ if (!m_enabled)
+ return;
+
+ GLGizmoBase* curr = _get_current();
+ if (curr != nullptr)
+ curr->render_for_picking(box);
+}
+
+void GLCanvas3D::Gizmos::render_overlay(const GLCanvas3D& canvas) const
+{
+ if (!m_enabled)
+ return;
+
+ ::glDisable(GL_DEPTH_TEST);
+
+ ::glPushMatrix();
+ ::glLoadIdentity();
+
+ _render_overlay(canvas);
+
+ ::glPopMatrix();
+}
+
+void GLCanvas3D::Gizmos::_reset()
+{
+ for (GizmosMap::value_type& gizmo : m_gizmos)
+ {
+ delete gizmo.second;
+ gizmo.second = nullptr;
+ }
+
+ m_gizmos.clear();
+}
+
+void GLCanvas3D::Gizmos::_render_overlay(const GLCanvas3D& canvas) const
+{
+ if (m_gizmos.empty())
+ return;
+
+ float cnv_w = (float)canvas.get_canvas_size().get_width();
+ float zoom = canvas.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+
+ float height = _get_total_overlay_height();
+ float top_x = (OverlayOffsetX - 0.5f * cnv_w) * inv_zoom;
+ float top_y = 0.5f * height * inv_zoom;
+ float scaled_gap_y = OverlayGapY * inv_zoom;
+ for (GizmosMap::const_iterator it = m_gizmos.begin(); it != m_gizmos.end(); ++it)
+ {
+ float tex_size = (float)it->second->get_textures_size() * OverlayTexturesScale * inv_zoom;
+ GLTexture::render_texture(it->second->get_texture_id(), top_x, top_x + tex_size, top_y - tex_size, top_y);
+ top_y -= (tex_size + scaled_gap_y);
+ }
+}
+
+void GLCanvas3D::Gizmos::_render_current_gizmo(const BoundingBoxf3& box) const
+{
+ GLGizmoBase* curr = _get_current();
+ if (curr != nullptr)
+ curr->render(box);
+}
+
+float GLCanvas3D::Gizmos::_get_total_overlay_height() const
+{
+ float height = 0.0f;
+
+ for (GizmosMap::const_iterator it = m_gizmos.begin(); it != m_gizmos.end(); ++it)
+ {
+ height += (float)it->second->get_textures_size();
+ if (std::distance(it, m_gizmos.end()) > 1)
+ height += OverlayGapY;
+ }
+
+ return height;
+}
+
+const unsigned char GLCanvas3D::WarningTexture::Background_Color[3] = { 9, 91, 134 };
+const unsigned char GLCanvas3D::WarningTexture::Opacity = 255;
+
+GLCanvas3D::WarningTexture::WarningTexture()
+ : GUI::GLTexture()
+ , m_original_width(0)
+ , m_original_height(0)
+{
+}
+
+bool GLCanvas3D::WarningTexture::generate(const std::string& msg)
+{
+ reset();
+
+ if (msg.empty())
+ return false;
+
+ wxMemoryDC memDC;
+ // select default font
+ wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
+ font.MakeLarger();
+ font.MakeBold();
+ memDC.SetFont(font);
+
+ // calculates texture size
+ wxCoord w, h;
+ memDC.GetTextExtent(msg, &w, &h);
+
+ int pow_of_two_size = next_highest_power_of_2(std::max<unsigned int>(w, h));
+
+ m_original_width = (int)w;
+ m_original_height = (int)h;
+ m_width = pow_of_two_size;
+ m_height = pow_of_two_size;
+
+ // generates bitmap
+ wxBitmap bitmap(m_width, m_height);
+
+#if defined(__APPLE__) || defined(_MSC_VER)
+ bitmap.UseAlpha();
+#endif
+
+ memDC.SelectObject(bitmap);
+ memDC.SetBackground(wxBrush(wxColour(Background_Color[0], Background_Color[1], Background_Color[2])));
+ memDC.Clear();
+
+ memDC.SetTextForeground(*wxWHITE);
+
+ // draw message
+ memDC.DrawText(msg, 0, 0);
+
+ memDC.SelectObject(wxNullBitmap);
+
+ // Convert the bitmap into a linear data ready to be loaded into the GPU.
+ wxImage image = bitmap.ConvertToImage();
+ image.SetMaskColour(Background_Color[0], Background_Color[1], Background_Color[2]);
+
+ // prepare buffer
+ std::vector<unsigned char> data(4 * m_width * m_height, 0);
+ for (int h = 0; h < m_height; ++h)
+ {
+ int hh = h * m_width;
+ unsigned char* px_ptr = data.data() + 4 * hh;
+ for (int w = 0; w < m_width; ++w)
+ {
+ *px_ptr++ = image.GetRed(w, h);
+ *px_ptr++ = image.GetGreen(w, h);
+ *px_ptr++ = image.GetBlue(w, h);
+ *px_ptr++ = image.IsTransparent(w, h) ? 0 : Opacity;
+ }
+ }
+
+ // sends buffer to gpu
+ ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ ::glGenTextures(1, &m_id);
+ ::glBindTexture(GL_TEXTURE_2D, (GLuint)m_id);
+ ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data());
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ ::glBindTexture(GL_TEXTURE_2D, 0);
+
+ return true;
+}
+
+void GLCanvas3D::WarningTexture::render(const GLCanvas3D& canvas) const
+{
+ if ((m_id > 0) && (m_original_width > 0) && (m_original_height > 0) && (m_width > 0) && (m_height > 0))
+ {
+ ::glDisable(GL_DEPTH_TEST);
+ ::glPushMatrix();
+ ::glLoadIdentity();
+
+ const Size& cnv_size = canvas.get_canvas_size();
+ float zoom = canvas.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+ float left = (-0.5f * (float)m_original_width) * inv_zoom;
+ float top = (-0.5f * (float)cnv_size.get_height() + (float)m_original_height + 2.0f) * inv_zoom;
+ float right = left + (float)m_original_width * inv_zoom;
+ float bottom = top - (float)m_original_height * inv_zoom;
+
+ float uv_left = 0.0f;
+ float uv_top = 0.0f;
+ float uv_right = (float)m_original_width / (float)m_width;
+ float uv_bottom = (float)m_original_height / (float)m_height;
+
+ GLTexture::Quad_UVs uvs;
+ uvs.left_top = { uv_left, uv_top };
+ uvs.left_bottom = { uv_left, uv_bottom };
+ uvs.right_bottom = { uv_right, uv_bottom };
+ uvs.right_top = { uv_right, uv_top };
+
+ GLTexture::render_sub_texture(m_id, left, right, bottom, top, uvs);
+
+ ::glPopMatrix();
+ ::glEnable(GL_DEPTH_TEST);
+ }
+}
+
+const unsigned char GLCanvas3D::LegendTexture::Squares_Border_Color[3] = { 64, 64, 64 };
+const unsigned char GLCanvas3D::LegendTexture::Background_Color[3] = { 9, 91, 134 };
+const unsigned char GLCanvas3D::LegendTexture::Opacity = 255;
+
+GLCanvas3D::LegendTexture::LegendTexture()
+ : GUI::GLTexture()
+ , m_original_width(0)
+ , m_original_height(0)
+{
+}
+
+bool GLCanvas3D::LegendTexture::generate(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors)
+{
+ reset();
+
+ // collects items to render
+ auto title = _(preview_data.get_legend_title());
+ const GCodePreviewData::LegendItemsList& items = preview_data.get_legend_items(tool_colors);
+
+ unsigned int items_count = (unsigned int)items.size();
+ if (items_count == 0)
+ // nothing to render, return
+ return false;
+
+ wxMemoryDC memDC;
+ // select default font
+ memDC.SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT));
+
+ // calculates texture size
+ wxCoord w, h;
+ memDC.GetTextExtent(title, &w, &h);
+ int title_width = (int)w;
+ int title_height = (int)h;
+
+ int max_text_width = 0;
+ int max_text_height = 0;
+ for (const GCodePreviewData::LegendItem& item : items)
+ {
+ memDC.GetTextExtent(GUI::from_u8(item.text), &w, &h);
+ max_text_width = std::max(max_text_width, (int)w);
+ max_text_height = std::max(max_text_height, (int)h);
+ }
+
+ m_original_width = std::max(2 * Px_Border + title_width, 2 * (Px_Border + Px_Square_Contour) + Px_Square + Px_Text_Offset + max_text_width);
+ m_original_height = 2 * (Px_Border + Px_Square_Contour) + title_height + Px_Title_Offset + items_count * Px_Square;
+ if (items_count > 1)
+ m_original_height += (items_count - 1) * Px_Square_Contour;
+
+ int pow_of_two_size = next_highest_power_of_2(std::max(m_original_width, m_original_height));
+
+ m_width = pow_of_two_size;
+ m_height = pow_of_two_size;
+
+ // generates bitmap
+ wxBitmap bitmap(m_width, m_height);
+
+#if defined(__APPLE__) || defined(_MSC_VER)
+ bitmap.UseAlpha();
+#endif
+
+ memDC.SelectObject(bitmap);
+ memDC.SetBackground(wxBrush(wxColour(Background_Color[0], Background_Color[1], Background_Color[2])));
+ memDC.Clear();
+
+ memDC.SetTextForeground(*wxWHITE);
+
+ // draw title
+ int title_x = Px_Border;
+ int title_y = Px_Border;
+ memDC.DrawText(title, title_x, title_y);
+
+ // draw icons contours as background
+ int squares_contour_x = Px_Border;
+ int squares_contour_y = Px_Border + title_height + Px_Title_Offset;
+ int squares_contour_width = Px_Square + 2 * Px_Square_Contour;
+ int squares_contour_height = items_count * Px_Square + 2 * Px_Square_Contour;
+ if (items_count > 1)
+ squares_contour_height += (items_count - 1) * Px_Square_Contour;
+
+ wxColour color(Squares_Border_Color[0], Squares_Border_Color[1], Squares_Border_Color[2]);
+ wxPen pen(color);
+ wxBrush brush(color);
+ memDC.SetPen(pen);
+ memDC.SetBrush(brush);
+ memDC.DrawRectangle(wxRect(squares_contour_x, squares_contour_y, squares_contour_width, squares_contour_height));
+
+ // draw items (colored icon + text)
+ int icon_x = squares_contour_x + Px_Square_Contour;
+ int icon_x_inner = icon_x + 1;
+ int icon_y = squares_contour_y + Px_Square_Contour;
+ int icon_y_step = Px_Square + Px_Square_Contour;
+
+ int text_x = icon_x + Px_Square + Px_Text_Offset;
+ int text_y_offset = (Px_Square - max_text_height) / 2;
+
+ int px_inner_square = Px_Square - 2;
+
+ for (const GCodePreviewData::LegendItem& item : items)
+ {
+ // draw darker icon perimeter
+ const std::vector<unsigned char>& item_color_bytes = item.color.as_bytes();
+ wxImage::HSVValue dark_hsv = wxImage::RGBtoHSV(wxImage::RGBValue(item_color_bytes[0], item_color_bytes[1], item_color_bytes[2]));
+ dark_hsv.value *= 0.75;
+ wxImage::RGBValue dark_rgb = wxImage::HSVtoRGB(dark_hsv);
+ color.Set(dark_rgb.red, dark_rgb.green, dark_rgb.blue, item_color_bytes[3]);
+ pen.SetColour(color);
+ brush.SetColour(color);
+ memDC.SetPen(pen);
+ memDC.SetBrush(brush);
+ memDC.DrawRectangle(wxRect(icon_x, icon_y, Px_Square, Px_Square));
+
+ // draw icon interior
+ color.Set(item_color_bytes[0], item_color_bytes[1], item_color_bytes[2], item_color_bytes[3]);
+ pen.SetColour(color);
+ brush.SetColour(color);
+ memDC.SetPen(pen);
+ memDC.SetBrush(brush);
+ memDC.DrawRectangle(wxRect(icon_x_inner, icon_y + 1, px_inner_square, px_inner_square));
+
+ // draw text
+ memDC.DrawText(GUI::from_u8(item.text), text_x, icon_y + text_y_offset);
+
+ // update y
+ icon_y += icon_y_step;
+ }
+
+ memDC.SelectObject(wxNullBitmap);
+
+ // Convert the bitmap into a linear data ready to be loaded into the GPU.
+ wxImage image = bitmap.ConvertToImage();
+ image.SetMaskColour(Background_Color[0], Background_Color[1], Background_Color[2]);
+
+ // prepare buffer
+ std::vector<unsigned char> data(4 * m_width * m_height, 0);
+ for (int h = 0; h < m_height; ++h)
+ {
+ int hh = h * m_width;
+ unsigned char* px_ptr = data.data() + 4 * hh;
+ for (int w = 0; w < m_width; ++w)
+ {
+ *px_ptr++ = image.GetRed(w, h);
+ *px_ptr++ = image.GetGreen(w, h);
+ *px_ptr++ = image.GetBlue(w, h);
+ *px_ptr++ = image.IsTransparent(w, h) ? 0 : Opacity;
+ }
+ }
+
+ // sends buffer to gpu
+ ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ ::glGenTextures(1, &m_id);
+ ::glBindTexture(GL_TEXTURE_2D, (GLuint)m_id);
+ ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data());
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ ::glBindTexture(GL_TEXTURE_2D, 0);
+
+ return true;
+}
+
+void GLCanvas3D::LegendTexture::render(const GLCanvas3D& canvas) const
+{
+ if ((m_id > 0) && (m_original_width > 0) && (m_original_height > 0) && (m_width > 0) && (m_height > 0))
+ {
+ ::glDisable(GL_DEPTH_TEST);
+ ::glPushMatrix();
+ ::glLoadIdentity();
+
+ const Size& cnv_size = canvas.get_canvas_size();
+ float zoom = canvas.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+ float left = (-0.5f * (float)cnv_size.get_width()) * inv_zoom;
+ float top = (0.5f * (float)cnv_size.get_height()) * inv_zoom;
+ float right = left + (float)m_original_width * inv_zoom;
+ float bottom = top - (float)m_original_height * inv_zoom;
+
+ float uv_left = 0.0f;
+ float uv_top = 0.0f;
+ float uv_right = (float)m_original_width / (float)m_width;
+ float uv_bottom = (float)m_original_height / (float)m_height;
+
+ GLTexture::Quad_UVs uvs;
+ uvs.left_top = { uv_left, uv_top };
+ uvs.left_bottom = { uv_left, uv_bottom };
+ uvs.right_bottom = { uv_right, uv_bottom };
+ uvs.right_top = { uv_right, uv_top };
+
+ GLTexture::render_sub_texture(m_id, left, right, bottom, top, uvs);
+
+ ::glPopMatrix();
+ ::glEnable(GL_DEPTH_TEST);
+ }
+}
+
+GLGizmoBase* GLCanvas3D::Gizmos::_get_current() const
+{
+ GizmosMap::const_iterator it = m_gizmos.find(m_current);
+ return (it != m_gizmos.end()) ? it->second : nullptr;
+}
+
+GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas)
+ : m_canvas(canvas)
+ , m_context(nullptr)
+ , m_timer(nullptr)
+ , m_toolbar(*this)
+ , m_config(nullptr)
+ , m_print(nullptr)
+ , m_model(nullptr)
+ , m_dirty(true)
+ , m_initialized(false)
+ , m_use_VBOs(false)
+ , m_force_zoom_to_bed_enabled(false)
+ , m_apply_zoom_to_volumes_filter(false)
+ , m_hover_volume_id(-1)
+ , m_toolbar_action_running(false)
+ , m_warning_texture_enabled(false)
+ , m_legend_texture_enabled(false)
+ , m_picking_enabled(false)
+ , m_moving_enabled(false)
+ , m_shader_enabled(false)
+ , m_dynamic_background_enabled(false)
+ , m_multisample_allowed(false)
+ , m_color_by("volume")
+ , m_select_by("object")
+ , m_drag_by("instance")
+ , m_reload_delayed(false)
+{
+ if (m_canvas != nullptr)
+ {
+ m_context = new wxGLContext(m_canvas);
+ m_timer = new wxTimer(m_canvas);
+ }
+}
+
+GLCanvas3D::~GLCanvas3D()
+{
+ reset_volumes();
+
+ if (m_timer != nullptr)
+ {
+ delete m_timer;
+ m_timer = nullptr;
+ }
+
+ if (m_context != nullptr)
+ {
+ delete m_context;
+ m_context = nullptr;
+ }
+
+ _deregister_callbacks();
+}
+
+bool GLCanvas3D::init(bool useVBOs, bool use_legacy_opengl)
+{
+ if (m_initialized)
+ return true;
+
+ if ((m_canvas == nullptr) || (m_context == nullptr))
+ return false;
+
+ ::glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
+ ::glClearDepth(1.0f);
+
+ ::glDepthFunc(GL_LESS);
+
+ ::glEnable(GL_DEPTH_TEST);
+ ::glEnable(GL_CULL_FACE);
+ ::glEnable(GL_BLEND);
+ ::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ // Set antialiasing / multisampling
+ ::glDisable(GL_LINE_SMOOTH);
+ ::glDisable(GL_POLYGON_SMOOTH);
+
+ // ambient lighting
+ GLfloat ambient[4] = { 0.3f, 0.3f, 0.3f, 1.0f };
+ ::glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient);
+
+ ::glEnable(GL_LIGHT0);
+ ::glEnable(GL_LIGHT1);
+
+ // light from camera
+ GLfloat specular_cam[4] = { 0.3f, 0.3f, 0.3f, 1.0f };
+ ::glLightfv(GL_LIGHT1, GL_SPECULAR, specular_cam);
+ GLfloat diffuse_cam[4] = { 0.2f, 0.2f, 0.2f, 1.0f };
+ ::glLightfv(GL_LIGHT1, GL_DIFFUSE, diffuse_cam);
+
+ // light from above
+ GLfloat specular_top[4] = { 0.2f, 0.2f, 0.2f, 1.0f };
+ ::glLightfv(GL_LIGHT0, GL_SPECULAR, specular_top);
+ GLfloat diffuse_top[4] = { 0.5f, 0.5f, 0.5f, 1.0f };
+ ::glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse_top);
+
+ // Enables Smooth Color Shading; try GL_FLAT for (lack of) fun.
+ ::glShadeModel(GL_SMOOTH);
+
+ // A handy trick -- have surface material mirror the color.
+ ::glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
+ ::glEnable(GL_COLOR_MATERIAL);
+
+ if (m_multisample_allowed)
+ ::glEnable(GL_MULTISAMPLE);
+
+ if (useVBOs && !m_shader.init("gouraud.vs", "gouraud.fs"))
+ return false;
+
+ if (useVBOs && !m_layers_editing.init("variable_layer_height.vs", "variable_layer_height.fs"))
+ return false;
+
+ m_use_VBOs = useVBOs;
+ m_layers_editing.set_use_legacy_opengl(use_legacy_opengl);
+
+ // on linux the gl context is not valid until the canvas is not shown on screen
+ // we defer the geometry finalization of volumes until the first call to render()
+ if (!m_volumes.empty())
+ m_volumes.finalize_geometry(m_use_VBOs);
+
+ if (m_gizmos.is_enabled() && !m_gizmos.init(*this))
+ return false;
+
+ if (!_init_toolbar())
+ return false;
+
+ m_initialized = true;
+
+ return true;
+}
+
+bool GLCanvas3D::set_current()
+{
+ if ((m_canvas != nullptr) && (m_context != nullptr))
+ return m_canvas->SetCurrent(*m_context);
+
+ return false;
+}
+
+void GLCanvas3D::set_as_dirty()
+{
+ m_dirty = true;
+}
+
+unsigned int GLCanvas3D::get_volumes_count() const
+{
+ return (unsigned int)m_volumes.volumes.size();
+}
+
+void GLCanvas3D::reset_volumes()
+{
+ if (!m_volumes.empty())
+ {
+ // ensures this canvas is current
+ if (!set_current())
+ return;
+
+ m_volumes.release_geometry();
+ m_volumes.clear();
+ m_dirty = true;
+ }
+
+ enable_warning_texture(false);
+ _reset_warning_texture();
+}
+
+void GLCanvas3D::deselect_volumes()
+{
+ for (GLVolume* vol : m_volumes.volumes)
+ {
+ if (vol != nullptr)
+ vol->selected = false;
+ }
+}
+
+void GLCanvas3D::select_volume(unsigned int id)
+{
+ if (id < (unsigned int)m_volumes.volumes.size())
+ {
+ GLVolume* vol = m_volumes.volumes[id];
+ if (vol != nullptr)
+ vol->selected = true;
+ }
+}
+
+void GLCanvas3D::update_volumes_selection(const std::vector<int>& selections)
+{
+ if (m_model == nullptr)
+ return;
+
+ if (selections.empty())
+ return;
+
+ for (unsigned int obj_idx = 0; obj_idx < (unsigned int)m_model->objects.size(); ++obj_idx)
+ {
+ if ((selections[obj_idx] == 1) && (obj_idx < (unsigned int)m_objects_volumes_idxs.size()))
+ {
+ const std::vector<int>& volume_idxs = m_objects_volumes_idxs[obj_idx];
+ for (int v : volume_idxs)
+ {
+ select_volume(v);
+ }
+ }
+ }
+}
+
+int GLCanvas3D::check_volumes_outside_state(const DynamicPrintConfig* config) const
+{
+ ModelInstance::EPrintVolumeState state;
+ m_volumes.check_outside_state(config, &state);
+ return (int)state;
+}
+
+bool GLCanvas3D::move_volume_up(unsigned int id)
+{
+ if ((id > 0) && (id < (unsigned int)m_volumes.volumes.size()))
+ {
+ std::swap(m_volumes.volumes[id - 1], m_volumes.volumes[id]);
+ std::swap(m_volumes.volumes[id - 1]->composite_id, m_volumes.volumes[id]->composite_id);
+ std::swap(m_volumes.volumes[id - 1]->select_group_id, m_volumes.volumes[id]->select_group_id);
+ std::swap(m_volumes.volumes[id - 1]->drag_group_id, m_volumes.volumes[id]->drag_group_id);
+ return true;
+ }
+
+ return false;
+}
+
+bool GLCanvas3D::move_volume_down(unsigned int id)
+{
+ if ((id >= 0) && (id + 1 < (unsigned int)m_volumes.volumes.size()))
+ {
+ std::swap(m_volumes.volumes[id + 1], m_volumes.volumes[id]);
+ std::swap(m_volumes.volumes[id + 1]->composite_id, m_volumes.volumes[id]->composite_id);
+ std::swap(m_volumes.volumes[id + 1]->select_group_id, m_volumes.volumes[id]->select_group_id);
+ std::swap(m_volumes.volumes[id + 1]->drag_group_id, m_volumes.volumes[id]->drag_group_id);
+ return true;
+ }
+
+ return false;
+}
+
+void GLCanvas3D::set_objects_selections(const std::vector<int>& selections)
+{
+ m_objects_selections = selections;
+}
+
+void GLCanvas3D::set_config(DynamicPrintConfig* config)
+{
+ m_config = config;
+}
+
+void GLCanvas3D::set_print(Print* print)
+{
+ m_print = print;
+}
+
+void GLCanvas3D::set_model(Model* model)
+{
+ m_model = model;
+}
+
+void GLCanvas3D::set_bed_shape(const Pointfs& shape)
+{
+ bool new_shape = m_bed.set_shape(shape);
+
+ // Set the origin and size for painting of the coordinate system axes.
+ m_axes.origin = Vec3d(0.0, 0.0, (double)GROUND_Z);
+ set_axes_length(0.3f * (float)m_bed.get_bounding_box().max_size());
+
+ if (new_shape)
+ {
+ // forces the selection of the proper camera target
+ if (m_volumes.volumes.empty())
+ zoom_to_bed();
+ else
+ zoom_to_volumes();
+ }
+
+ m_dirty = true;
+}
+
+void GLCanvas3D::set_auto_bed_shape()
+{
+ // draw a default square bed around object center
+ const BoundingBoxf3& bbox = volumes_bounding_box();
+ double max_size = bbox.max_size();
+ const Vec3d center = bbox.center();
+
+ Pointfs bed_shape;
+ bed_shape.reserve(4);
+ bed_shape.emplace_back(center(0) - max_size, center(1) - max_size);
+ bed_shape.emplace_back(center(0) + max_size, center(1) - max_size);
+ bed_shape.emplace_back(center(0) + max_size, center(1) + max_size);
+ bed_shape.emplace_back(center(0) - max_size, center(1) + max_size);
+
+ set_bed_shape(bed_shape);
+
+ // Set the origin for painting of the coordinate system axes.
+ m_axes.origin = Vec3d(center(0), center(1), (double)GROUND_Z);
+}
+
+void GLCanvas3D::set_axes_length(float length)
+{
+ m_axes.length = length;
+}
+
+void GLCanvas3D::set_cutting_plane(float z, const ExPolygons& polygons)
+{
+ m_cutting_plane.set(z, polygons);
+}
+
+void GLCanvas3D::set_color_by(const std::string& value)
+{
+ m_color_by = value;
+}
+
+void GLCanvas3D::set_select_by(const std::string& value)
+{
+ m_select_by = value;
+ m_volumes.set_select_by(value);
+}
+
+void GLCanvas3D::set_drag_by(const std::string& value)
+{
+ m_drag_by = value;
+ m_volumes.set_drag_by(value);
+}
+
+const std::string& GLCanvas3D::get_select_by() const
+{
+ return m_select_by;
+}
+
+const std::string& GLCanvas3D::get_drag_by() const
+{
+ return m_drag_by;
+}
+
+float GLCanvas3D::get_camera_zoom() const
+{
+ return m_camera.zoom;
+}
+
+BoundingBoxf3 GLCanvas3D::volumes_bounding_box() const
+{
+ BoundingBoxf3 bb;
+ for (const GLVolume* volume : m_volumes.volumes)
+ {
+ if (!m_apply_zoom_to_volumes_filter || ((volume != nullptr) && volume->zoom_to_volumes))
+ bb.merge(volume->transformed_bounding_box());
+ }
+ return bb;
+}
+
+bool GLCanvas3D::is_layers_editing_enabled() const
+{
+ return m_layers_editing.is_enabled();
+}
+
+bool GLCanvas3D::is_layers_editing_allowed() const
+{
+ return m_layers_editing.is_allowed();
+}
+
+bool GLCanvas3D::is_shader_enabled() const
+{
+ return m_shader_enabled;
+}
+
+bool GLCanvas3D::is_reload_delayed() const
+{
+ return m_reload_delayed;
+}
+
+void GLCanvas3D::enable_layers_editing(bool enable)
+{
+ m_layers_editing.set_enabled(enable);
+}
+
+void GLCanvas3D::enable_warning_texture(bool enable)
+{
+ m_warning_texture_enabled = enable;
+}
+
+void GLCanvas3D::enable_legend_texture(bool enable)
+{
+ m_legend_texture_enabled = enable;
+}
+
+void GLCanvas3D::enable_picking(bool enable)
+{
+ m_picking_enabled = enable;
+}
+
+void GLCanvas3D::enable_moving(bool enable)
+{
+ m_moving_enabled = enable;
+}
+
+void GLCanvas3D::enable_gizmos(bool enable)
+{
+ m_gizmos.set_enabled(enable);
+}
+
+void GLCanvas3D::enable_toolbar(bool enable)
+{
+ m_toolbar.set_enabled(enable);
+}
+
+void GLCanvas3D::enable_shader(bool enable)
+{
+ m_shader_enabled = enable;
+}
+
+void GLCanvas3D::enable_force_zoom_to_bed(bool enable)
+{
+ m_force_zoom_to_bed_enabled = enable;
+}
+
+void GLCanvas3D::enable_dynamic_background(bool enable)
+{
+ m_dynamic_background_enabled = enable;
+}
+
+void GLCanvas3D::allow_multisample(bool allow)
+{
+ m_multisample_allowed = allow;
+}
+
+void GLCanvas3D::enable_toolbar_item(const std::string& name, bool enable)
+{
+ if (enable)
+ m_toolbar.enable_item(name);
+ else
+ m_toolbar.disable_item(name);
+}
+
+bool GLCanvas3D::is_toolbar_item_pressed(const std::string& name) const
+{
+ return m_toolbar.is_item_pressed(name);
+}
+
+void GLCanvas3D::zoom_to_bed()
+{
+ _zoom_to_bounding_box(m_bed.get_bounding_box());
+}
+
+void GLCanvas3D::zoom_to_volumes()
+{
+ m_apply_zoom_to_volumes_filter = true;
+ _zoom_to_bounding_box(volumes_bounding_box());
+ m_apply_zoom_to_volumes_filter = false;
+}
+
+void GLCanvas3D::select_view(const std::string& direction)
+{
+ const float* dir_vec = nullptr;
+
+ if (direction == "iso")
+ dir_vec = VIEW_DEFAULT;
+ else if (direction == "left")
+ dir_vec = VIEW_LEFT;
+ else if (direction == "right")
+ dir_vec = VIEW_RIGHT;
+ else if (direction == "top")
+ dir_vec = VIEW_TOP;
+ else if (direction == "bottom")
+ dir_vec = VIEW_BOTTOM;
+ else if (direction == "front")
+ dir_vec = VIEW_FRONT;
+ else if (direction == "rear")
+ dir_vec = VIEW_REAR;
+
+ if ((dir_vec != nullptr) && !empty(volumes_bounding_box()))
+ {
+ m_camera.phi = dir_vec[0];
+ m_camera.set_theta(dir_vec[1]);
+
+ m_on_viewport_changed_callback.call();
+
+ if (m_canvas != nullptr)
+ m_canvas->Refresh();
+ }
+}
+
+void GLCanvas3D::set_viewport_from_scene(const GLCanvas3D& other)
+{
+ m_camera.phi = other.m_camera.phi;
+ m_camera.set_theta(other.m_camera.get_theta());
+ m_camera.target = other.m_camera.target;
+ m_camera.zoom = other.m_camera.zoom;
+ m_dirty = true;
+}
+
+void GLCanvas3D::update_volumes_colors_by_extruder()
+{
+ if (m_config != nullptr)
+ m_volumes.update_colors_by_extruder(m_config);
+}
+
+void GLCanvas3D::update_gizmos_data()
+{
+ if (!m_gizmos.is_enabled())
+ return;
+
+ int id = _get_first_selected_object_id();
+ if ((id != -1) && (m_model != nullptr))
+ {
+ ModelObject* model_object = m_model->objects[id];
+ if (model_object != nullptr)
+ {
+ ModelInstance* model_instance = model_object->instances[0];
+ if (model_instance != nullptr)
+ {
+#if ENABLE_MODELINSTANCE_3D_OFFSET
+ m_gizmos.set_position(model_instance->get_offset());
+#else
+ m_gizmos.set_position(Vec3d(model_instance->offset(0), model_instance->offset(1), 0.0));
+#endif // ENABLE_MODELINSTANCE_3D_OFFSET
+ m_gizmos.set_scale(model_instance->scaling_factor);
+ m_gizmos.set_angle_z(model_instance->rotation);
+ m_gizmos.set_flattening_data(model_object);
+ }
+ }
+ }
+ else
+ {
+ m_gizmos.set_position(Vec3d::Zero());
+ m_gizmos.set_scale(1.0f);
+ m_gizmos.set_angle_z(0.0f);
+ m_gizmos.set_flattening_data(nullptr);
+ }
+}
+
+void GLCanvas3D::render()
+{
+ if (m_canvas == nullptr)
+ return;
+
+ if (!_is_shown_on_screen())
+ return;
+
+ // ensures this canvas is current and initialized
+ if (!set_current() || !_3DScene::init(m_canvas))
+ return;
+
+ if (m_force_zoom_to_bed_enabled)
+ _force_zoom_to_bed();
+
+ _camera_tranform();
+
+ GLfloat position_cam[4] = { 1.0f, 0.0f, 1.0f, 0.0f };
+ ::glLightfv(GL_LIGHT1, GL_POSITION, position_cam);
+ GLfloat position_top[4] = { -0.5f, -0.5f, 1.0f, 0.0f };
+ ::glLightfv(GL_LIGHT0, GL_POSITION, position_top);
+
+ float theta = m_camera.get_theta();
+ bool is_custom_bed = m_bed.is_custom();
+
+ // picking pass
+ _picking_pass();
+
+ // draw scene
+ _render_background();
+
+ if (is_custom_bed) // untextured bed needs to be rendered before objects
+ {
+ _render_bed(theta);
+ // disable depth testing so that axes are not covered by ground
+ _render_axes(false);
+ }
+ _render_objects();
+ if (!is_custom_bed) // textured bed needs to be rendered after objects
+ {
+ _render_axes(true);
+ _render_bed(theta);
+ }
+
+ _render_current_gizmo();
+ _render_cutting_plane();
+
+ // draw overlays
+ _render_gizmos_overlay();
+ _render_warning_texture();
+ _render_legend_texture();
+ _render_toolbar();
+ _render_layer_editing_overlay();
+
+ m_canvas->SwapBuffers();
+}
+
+std::vector<double> GLCanvas3D::get_current_print_zs(bool active_only) const
+{
+ return m_volumes.get_current_print_zs(active_only);
+}
+
+void GLCanvas3D::set_toolpaths_range(double low, double high)
+{
+ m_volumes.set_range(low, high);
+}
+
+std::vector<int> GLCanvas3D::load_object(const ModelObject& model_object, int obj_idx, std::vector<int> instance_idxs)
+{
+ if (instance_idxs.empty())
+ {
+ for (unsigned int i = 0; i < model_object.instances.size(); ++i)
+ {
+ instance_idxs.push_back(i);
+ }
+ }
+ return m_volumes.load_object(&model_object, obj_idx, instance_idxs, m_color_by, m_select_by, m_drag_by, m_use_VBOs && m_initialized);
+}
+
+std::vector<int> GLCanvas3D::load_object(const Model& model, int obj_idx)
+{
+ if ((0 <= obj_idx) && (obj_idx < (int)model.objects.size()))
+ {
+ const ModelObject* model_object = model.objects[obj_idx];
+ if (model_object != nullptr)
+ return load_object(*model_object, obj_idx, std::vector<int>());
+ }
+
+ return std::vector<int>();
+}
+
+int GLCanvas3D::get_first_volume_id(int obj_idx) const
+{
+ for (int i = 0; i < (int)m_volumes.volumes.size(); ++i)
+ {
+ if ((m_volumes.volumes[i] != nullptr) && (m_volumes.volumes[i]->object_idx() == obj_idx))
+ return i;
+ }
+
+ return -1;
+}
+
+int GLCanvas3D::get_in_object_volume_id(int scene_vol_idx) const
+{
+ return ((0 <= scene_vol_idx) && (scene_vol_idx < (int)m_volumes.volumes.size())) ? m_volumes.volumes[scene_vol_idx]->volume_idx() : -1;
+}
+
+void GLCanvas3D::reload_scene(bool force)
+{
+ if ((m_canvas == nullptr) || (m_config == nullptr) || (m_model == nullptr))
+ return;
+
+ reset_volumes();
+
+ // ensures this canvas is current
+ if (!set_current())
+ return;
+
+ set_bed_shape(dynamic_cast<const ConfigOptionPoints*>(m_config->option("bed_shape"))->values);
+
+ if (!m_canvas->IsShown() && !force)
+ {
+ m_reload_delayed = true;
+ return;
+ }
+
+ m_reload_delayed = false;
+
+ m_objects_volumes_idxs.clear();
+
+ for (unsigned int obj_idx = 0; obj_idx < (unsigned int)m_model->objects.size(); ++obj_idx)
+ {
+ m_objects_volumes_idxs.push_back(load_object(*m_model, obj_idx));
+ }
+
+ // 1st call to reset if no objects left
+ update_gizmos_data();
+ update_volumes_selection(m_objects_selections);
+ // 2nd call to restore selection, if any
+ if (!m_objects_selections.empty())
+ update_gizmos_data();
+
+ if (m_config->has("nozzle_diameter"))
+ {
+ // Should the wipe tower be visualized ?
+ unsigned int extruders_count = (unsigned int)dynamic_cast<const ConfigOptionFloats*>(m_config->option("nozzle_diameter"))->values.size();
+
+ bool semm = dynamic_cast<const ConfigOptionBool*>(m_config->option("single_extruder_multi_material"))->value;
+ bool wt = dynamic_cast<const ConfigOptionBool*>(m_config->option("wipe_tower"))->value;
+ bool co = dynamic_cast<const ConfigOptionBool*>(m_config->option("complete_objects"))->value;
+
+ if ((extruders_count > 1) && semm && wt && !co)
+ {
+ // Height of a print (Show at least a slab)
+ double height = std::max(m_model->bounding_box().max(2), 10.0);
+
+ float x = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_x"))->value;
+ float y = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_y"))->value;
+ float w = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_width"))->value;
+ float a = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_rotation_angle"))->value;
+
+ float depth = m_print->get_wipe_tower_depth();
+ if (!m_print->is_step_done(psWipeTower))
+ depth = (900.f/w) * (float)(extruders_count - 1) ;
+
+ m_volumes.load_wipe_tower_preview(1000, x, y, w, depth, (float)height, a, m_use_VBOs && m_initialized, !m_print->is_step_done(psWipeTower),
+ m_print->config().nozzle_diameter.values[0] * 1.25f * 4.5f);
+ }
+ }
+
+ update_volumes_colors_by_extruder();
+
+ // checks for geometry outside the print volume to render it accordingly
+ if (!m_volumes.empty())
+ {
+ ModelInstance::EPrintVolumeState state;
+ bool contained = m_volumes.check_outside_state(m_config, &state);
+
+ if (!contained)
+ {
+ enable_warning_texture(true);
+ _generate_warning_texture(L("Detected object outside print volume"));
+ m_on_enable_action_buttons_callback.call(state == ModelInstance::PVS_Fully_Outside);
+ }
+ else
+ {
+ enable_warning_texture(false);
+ m_volumes.reset_outside_state();
+ _reset_warning_texture();
+ m_on_enable_action_buttons_callback.call(!m_model->objects.empty());
+ }
+ }
+ else
+ {
+ enable_warning_texture(false);
+ _reset_warning_texture();
+ m_on_enable_action_buttons_callback.call(false);
+ }
+}
+
+void GLCanvas3D::load_gcode_preview(const GCodePreviewData& preview_data, const std::vector<std::string>& str_tool_colors)
+{
+ if ((m_canvas != nullptr) && (m_print != nullptr))
+ {
+ // ensures that this canvas is current
+ if (!set_current())
+ return;
+
+ if (m_volumes.empty())
+ {
+ std::vector<float> tool_colors = _parse_colors(str_tool_colors);
+
+ m_gcode_preview_volume_index.reset();
+
+ _load_gcode_extrusion_paths(preview_data, tool_colors);
+ _load_gcode_travel_paths(preview_data, tool_colors);
+ _load_gcode_retractions(preview_data);
+ _load_gcode_unretractions(preview_data);
+
+ if (m_volumes.empty())
+ reset_legend_texture();
+ else
+ {
+ _generate_legend_texture(preview_data, tool_colors);
+
+ // removes empty volumes
+ m_volumes.volumes.erase(std::remove_if(m_volumes.volumes.begin(), m_volumes.volumes.end(),
+ [](const GLVolume* volume) { return volume->print_zs.empty(); }), m_volumes.volumes.end());
+
+ _load_shells();
+ }
+ _update_toolpath_volumes_outside_state();
+ }
+
+ _update_gcode_volumes_visibility(preview_data);
+ _show_warning_texture_if_needed();
+ }
+}
+
+void GLCanvas3D::load_preview(const std::vector<std::string>& str_tool_colors)
+{
+ if (m_print == nullptr)
+ return;
+
+ _load_print_toolpaths();
+ _load_wipe_tower_toolpaths(str_tool_colors);
+ for (const PrintObject* object : m_print->objects())
+ {
+ if (object != nullptr)
+ _load_print_object_toolpaths(*object, str_tool_colors);
+ }
+
+ for (GLVolume* volume : m_volumes.volumes)
+ {
+ volume->is_extrusion_path = true;
+ }
+
+ _update_toolpath_volumes_outside_state();
+ _show_warning_texture_if_needed();
+ reset_legend_texture();
+}
+
+void GLCanvas3D::register_on_viewport_changed_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_viewport_changed_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_double_click_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_double_click_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_right_click_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_right_click_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_select_object_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_select_object_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_model_update_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_model_update_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_remove_object_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_remove_object_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_arrange_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_arrange_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_rotate_object_left_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_rotate_object_left_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_rotate_object_right_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_rotate_object_right_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_scale_object_uniformly_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_scale_object_uniformly_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_increase_objects_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_increase_objects_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_decrease_objects_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_decrease_objects_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_instance_moved_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_instance_moved_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_wipe_tower_moved_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_wipe_tower_moved_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_enable_action_buttons_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_enable_action_buttons_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_gizmo_scale_uniformly_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_gizmo_scale_uniformly_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_gizmo_rotate_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_gizmo_rotate_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_gizmo_flatten_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_gizmo_flatten_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_on_update_geometry_info_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_on_update_geometry_info_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_add_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_add_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_delete_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_delete_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_deleteall_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_deleteall_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_arrange_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_arrange_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_more_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_more_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_fewer_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_fewer_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_split_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_split_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_cut_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_cut_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_settings_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_settings_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_layersediting_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_layersediting_callback.register_callback(callback);
+}
+
+void GLCanvas3D::register_action_selectbyparts_callback(void* callback)
+{
+ if (callback != nullptr)
+ m_action_selectbyparts_callback.register_callback(callback);
+}
+
+void GLCanvas3D::bind_event_handlers()
+{
+ if (m_canvas != nullptr)
+ {
+ m_canvas->Bind(wxEVT_SIZE, &GLCanvas3D::on_size, this);
+ m_canvas->Bind(wxEVT_IDLE, &GLCanvas3D::on_idle, this);
+ m_canvas->Bind(wxEVT_CHAR, &GLCanvas3D::on_char, this);
+ m_canvas->Bind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this);
+ m_canvas->Bind(wxEVT_TIMER, &GLCanvas3D::on_timer, this);
+ m_canvas->Bind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
+ m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
+ m_canvas->Bind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key_down, this);
+ }
+}
+
+void GLCanvas3D::unbind_event_handlers()
+{
+ if (m_canvas != nullptr)
+ {
+ m_canvas->Unbind(wxEVT_SIZE, &GLCanvas3D::on_size, this);
+ m_canvas->Unbind(wxEVT_IDLE, &GLCanvas3D::on_idle, this);
+ m_canvas->Unbind(wxEVT_CHAR, &GLCanvas3D::on_char, this);
+ m_canvas->Unbind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this);
+ m_canvas->Unbind(wxEVT_TIMER, &GLCanvas3D::on_timer, this);
+ m_canvas->Unbind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
+ m_canvas->Unbind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
+ m_canvas->Unbind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key_down, this);
+ }
+}
+
+void GLCanvas3D::on_size(wxSizeEvent& evt)
+{
+ m_dirty = true;
+}
+
+void GLCanvas3D::on_idle(wxIdleEvent& evt)
+{
+ if (!m_dirty)
+ return;
+
+ _refresh_if_shown_on_screen();
+}
+
+void GLCanvas3D::on_char(wxKeyEvent& evt)
+{
+ if (evt.HasModifiers())
+ evt.Skip();
+ else
+ {
+ int keyCode = evt.GetKeyCode();
+ switch (keyCode - 48)
+ {
+ // numerical input
+ case 0: { select_view("iso"); break; }
+ case 1: { select_view("top"); break; }
+ case 2: { select_view("bottom"); break; }
+ case 3: { select_view("front"); break; }
+ case 4: { select_view("rear"); break; }
+ case 5: { select_view("left"); break; }
+ case 6: { select_view("right"); break; }
+ default:
+ {
+ // text input
+ switch (keyCode)
+ {
+ // key +
+ case 43: { m_on_increase_objects_callback.call(); break; }
+ // key -
+ case 45: { m_on_decrease_objects_callback.call(); break; }
+ // key A/a
+ case 65:
+ case 97: { m_on_arrange_callback.call(); break; }
+ // key B/b
+ case 66:
+ case 98: { zoom_to_bed(); break; }
+ // key L/l
+ case 76:
+ case 108: { m_on_rotate_object_left_callback.call(); break; }
+ // key R/r
+ case 82:
+ case 114: { m_on_rotate_object_right_callback.call(); break; }
+ // key S/s
+ case 83:
+ case 115: { m_on_scale_object_uniformly_callback.call(); break; }
+ // key Z/z
+ case 90:
+ case 122: { zoom_to_volumes(); break; }
+ default:
+ {
+ evt.Skip();
+ break;
+ }
+ }
+ }
+ }
+ }
+}
+
+void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt)
+{
+ // Ignore the wheel events if the middle button is pressed.
+ if (evt.MiddleIsDown())
+ return;
+
+ // Performs layers editing updates, if enabled
+ if (is_layers_editing_enabled())
+ {
+ int object_idx_selected = _get_first_selected_object_id();
+ if (object_idx_selected != -1)
+ {
+ // A volume is selected. Test, whether hovering over a layer thickness bar.
+ if (m_layers_editing.bar_rect_contains(*this, (float)evt.GetX(), (float)evt.GetY()))
+ {
+ // Adjust the width of the selection.
+ m_layers_editing.band_width = std::max(std::min(m_layers_editing.band_width * (1.0f + 0.1f * (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta()), 10.0f), 1.5f);
+ if (m_canvas != nullptr)
+ m_canvas->Refresh();
+
+ return;
+ }
+ }
+ }
+
+ // Calculate the zoom delta and apply it to the current zoom factor
+ float zoom = (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta();
+ zoom = std::max(std::min(zoom, 4.0f), -4.0f) / 10.0f;
+ zoom = get_camera_zoom() / (1.0f - zoom);
+
+ // Don't allow to zoom too far outside the scene.
+ float zoom_min = _get_zoom_to_bounding_box_factor(_max_bounding_box());
+ if (zoom_min > 0.0f)
+ zoom = std::max(zoom, zoom_min * 0.8f);
+
+ m_camera.zoom = zoom;
+ m_on_viewport_changed_callback.call();
+
+ _refresh_if_shown_on_screen();
+}
+
+void GLCanvas3D::on_timer(wxTimerEvent& evt)
+{
+ if (m_layers_editing.state != LayersEditing::Editing)
+ return;
+
+ _perform_layer_editing_action();
+}
+
+void GLCanvas3D::on_mouse(wxMouseEvent& evt)
+{
+ Point pos(evt.GetX(), evt.GetY());
+
+ int selected_object_idx = _get_first_selected_object_id();
+ int layer_editing_object_idx = is_layers_editing_enabled() ? selected_object_idx : -1;
+ m_layers_editing.last_object_id = layer_editing_object_idx;
+ bool gizmos_overlay_contains_mouse = m_gizmos.overlay_contains_mouse(*this, m_mouse.position);
+ int toolbar_contains_mouse = m_toolbar.contains_mouse(m_mouse.position);
+
+ if (evt.Entering())
+ {
+#if defined(__WXMSW__) || defined(__linux__)
+ // On Windows and Linux needs focus in order to catch key events
+ if (m_canvas != nullptr)
+ m_canvas->SetFocus();
+
+ m_mouse.set_start_position_2D_as_invalid();
+#endif
+ }
+ else if (evt.Leaving())
+ {
+ // to remove hover on objects when the mouse goes out of this canvas
+ m_mouse.position = Vec2d(-1.0, -1.0);
+ m_dirty = true;
+ }
+ else if (evt.LeftDClick() && (m_hover_volume_id != -1) && !gizmos_overlay_contains_mouse && (toolbar_contains_mouse == -1))
+ m_on_double_click_callback.call();
+ else if (evt.LeftDClick() && (toolbar_contains_mouse != -1))
+ {
+ m_toolbar_action_running = true;
+ m_toolbar.do_action((unsigned int)toolbar_contains_mouse);
+ }
+ else if (evt.LeftDown() || evt.RightDown())
+ {
+ // If user pressed left or right button we first check whether this happened
+ // on a volume or not.
+ int volume_idx = m_hover_volume_id;
+ m_layers_editing.state = LayersEditing::Unknown;
+ if ((layer_editing_object_idx != -1) && m_layers_editing.bar_rect_contains(*this, pos(0), pos(1)))
+ {
+ // A volume is selected and the mouse is inside the layer thickness bar.
+ // Start editing the layer height.
+ m_layers_editing.state = LayersEditing::Editing;
+ _perform_layer_editing_action(&evt);
+ }
+ else if ((layer_editing_object_idx != -1) && m_layers_editing.reset_rect_contains(*this, pos(0), pos(1)))
+ {
+ if (evt.LeftDown())
+ {
+ // A volume is selected and the mouse is inside the reset button.
+ // The PrintObject::adjust_layer_height_profile() call adjusts the profile of its associated ModelObject, it does not modify the profile of the PrintObject itself,
+ // therefore it is safe to call it while the background processing is running.
+ const_cast<PrintObject*>(m_print->get_object(layer_editing_object_idx))->reset_layer_height_profile();
+ // Index 2 means no editing, just wait for mouse up event.
+ m_layers_editing.state = LayersEditing::Completed;
+
+ m_dirty = true;
+ }
+ }
+ else if ((selected_object_idx != -1) && gizmos_overlay_contains_mouse)
+ {
+ update_gizmos_data();
+ m_gizmos.update_on_off_state(*this, m_mouse.position);
+ m_dirty = true;
+ }
+ else if ((selected_object_idx != -1) && m_gizmos.grabber_contains_mouse())
+ {
+ update_gizmos_data();
+ m_gizmos.start_dragging(_selected_volumes_bounding_box());
+ m_mouse.drag.gizmo_volume_idx = _get_first_selected_volume_id(selected_object_idx);
+
+ if (m_gizmos.get_current_type() == Gizmos::Flatten) {
+ // Rotate the object so the normal points downward:
+ Vec3d normal = m_gizmos.get_flattening_normal();
+ if (normal(0) != 0.0 || normal(1) != 0.0 || normal(2) != 0.0) {
+ Vec3d axis = normal(2) > 0.999 ? Vec3d::UnitX() : normal.cross(-Vec3d::UnitZ()).normalized();
+ float angle = acos(clamp(-1.0, 1.0, -normal(2)));
+ m_on_gizmo_flatten_callback.call(angle, (float)axis(0), (float)axis(1), (float)axis(2));
+ }
+ }
+
+ m_dirty = true;
+ }
+ else if (toolbar_contains_mouse != -1)
+ {
+ m_toolbar_action_running = true;
+ m_toolbar.do_action((unsigned int)toolbar_contains_mouse);
+ }
+ else
+ {
+ // Select volume in this 3D canvas.
+ // Don't deselect a volume if layer editing is enabled. We want the object to stay selected
+ // during the scene manipulation.
+
+ if (m_picking_enabled && ((volume_idx != -1) || !is_layers_editing_enabled()))
+ {
+ if (volume_idx != -1)
+ {
+ deselect_volumes();
+ select_volume(volume_idx);
+ int group_id = m_volumes.volumes[volume_idx]->select_group_id;
+ if (group_id != -1)
+ {
+ for (GLVolume* vol : m_volumes.volumes)
+ {
+ if ((vol != nullptr) && (vol->select_group_id == group_id))
+ vol->selected = true;
+ }
+ }
+
+ update_gizmos_data();
+ m_dirty = true;
+ }
+ }
+
+ // propagate event through callback
+ if (m_picking_enabled && (volume_idx != -1))
+ _on_select(volume_idx, selected_object_idx);
+
+ if (volume_idx != -1)
+ {
+ if (evt.LeftDown() && m_moving_enabled)
+ {
+ // The mouse_to_3d gets the Z coordinate from the Z buffer at the screen coordinate pos x, y,
+ // an converts the screen space coordinate to unscaled object space.
+ Vec3d pos3d = (volume_idx == -1) ? Vec3d(DBL_MAX, DBL_MAX, DBL_MAX) : _mouse_to_3d(pos);
+
+ // Only accept the initial position, if it is inside the volume bounding box.
+ BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box();
+ volume_bbox.offset(1.0);
+ if (volume_bbox.contains(pos3d))
+ {
+ // The dragging operation is initiated.
+ m_mouse.drag.move_with_shift = evt.ShiftDown();
+ m_mouse.drag.move_volume_idx = volume_idx;
+ m_mouse.drag.start_position_3D = pos3d;
+ // Remember the shift to to the object center.The object center will later be used
+ // to limit the object placement close to the bed.
+ m_mouse.drag.volume_center_offset = volume_bbox.center() - pos3d;
+ }
+ }
+ else if (evt.RightDown())
+ {
+ // forces a frame render to ensure that m_hover_volume_id is updated even when the user right clicks while
+ // the context menu is already shown, ensuring it to disappear if the mouse is outside any volume
+ m_mouse.position = Vec2d((double)pos(0), (double)pos(1));
+ render();
+ if (m_hover_volume_id != -1)
+ {
+ // if right clicking on volume, propagate event through callback (shows context menu)
+ if (m_volumes.volumes[volume_idx]->hover)
+ m_on_right_click_callback.call(pos(0), pos(1));
+ }
+ }
+ }
+ }
+ }
+ else if (evt.Dragging() && evt.LeftIsDown() && !gizmos_overlay_contains_mouse && (m_layers_editing.state == LayersEditing::Unknown) && (m_mouse.drag.move_volume_idx != -1))
+ {
+ m_mouse.dragging = true;
+
+ // Get new position at the same Z of the initial click point.
+ float z0 = 0.0f;
+ float z1 = 1.0f;
+ Vec3d cur_pos = Linef3(_mouse_to_3d(pos, &z0), _mouse_to_3d(pos, &z1)).intersect_plane(m_mouse.drag.start_position_3D(2));
+
+ // Clip the new position, so the object center remains close to the bed.
+ cur_pos += m_mouse.drag.volume_center_offset;
+ Point cur_pos2(scale_(cur_pos(0)), scale_(cur_pos(1)));
+ if (!m_bed.contains(cur_pos2))
+ {
+ Point ip = m_bed.point_projection(cur_pos2);
+ cur_pos(0) = unscale<double>(ip(0));
+ cur_pos(1) = unscale<double>(ip(1));
+ }
+ cur_pos -= m_mouse.drag.volume_center_offset;
+
+ // Calculate the translation vector.
+ Vec3d vector = cur_pos - m_mouse.drag.start_position_3D;
+ // Get the volume being dragged.
+ GLVolume* volume = m_volumes.volumes[m_mouse.drag.move_volume_idx];
+ // Get all volumes belonging to the same group, if any.
+ std::vector<GLVolume*> volumes;
+ int group_id = m_mouse.drag.move_with_shift ? volume->select_group_id : volume->drag_group_id;
+ if (group_id == -1)
+ volumes.push_back(volume);
+ else
+ {
+ for (GLVolume* v : m_volumes.volumes)
+ {
+ if (v != nullptr)
+ {
+ if ((m_mouse.drag.move_with_shift && (v->select_group_id == group_id)) || (!m_mouse.drag.move_with_shift && (v->drag_group_id == group_id)))
+ volumes.push_back(v);
+ }
+ }
+ }
+
+ // Apply new temporary volume origin and ignore Z.
+ for (GLVolume* v : volumes)
+ {
+ v->set_offset(v->get_offset() + Vec3d(vector(0), vector(1), 0.0));
+ }
+
+ update_position_values(volume->get_offset());
+ m_mouse.drag.start_position_3D = cur_pos;
+
+ m_dirty = true;
+ }
+ else if (evt.Dragging() && m_gizmos.is_dragging())
+ {
+ if (!m_canvas->HasCapture())
+ m_canvas->CaptureMouse();
+
+ m_mouse.dragging = true;
+ m_gizmos.update(mouse_ray(pos));
+
+ std::vector<GLVolume*> volumes;
+ if (m_mouse.drag.gizmo_volume_idx != -1)
+ {
+ GLVolume* volume = m_volumes.volumes[m_mouse.drag.gizmo_volume_idx];
+ // Get all volumes belonging to the same group, if any.
+ if (volume->select_group_id == -1)
+ volumes.push_back(volume);
+ else
+ {
+ for (GLVolume* v : m_volumes.volumes)
+ {
+ if ((v != nullptr) && (v->select_group_id == volume->select_group_id))
+ volumes.push_back(v);
+ }
+ }
+ }
+
+ switch (m_gizmos.get_current_type())
+ {
+ case Gizmos::Move:
+ {
+ // Apply new temporary offset
+ GLVolume* volume = m_volumes.volumes[m_mouse.drag.gizmo_volume_idx];
+ Vec3d offset = m_gizmos.get_position() - volume->get_offset();
+ for (GLVolume* v : volumes)
+ {
+ v->set_offset(v->get_offset() + offset);
+ }
+ update_position_values(volume->get_offset());
+ break;
+ }
+ case Gizmos::Scale:
+ {
+ // Apply new temporary scale factor
+ float scale_factor = m_gizmos.get_scale();
+ for (GLVolume* v : volumes)
+ {
+ v->set_scaling_factor((double)scale_factor);
+ }
+ update_scale_values((double)scale_factor);
+ break;
+ }
+ case Gizmos::Rotate:
+ {
+ // Apply new temporary angle_z
+ float angle_z = m_gizmos.get_angle_z();
+ for (GLVolume* v : volumes)
+ {
+ v->set_rotation((double)angle_z);
+ }
+ update_rotation_value((double)angle_z, Z);
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (!volumes.empty())
+ {
+ BoundingBoxf3 bb;
+ for (const GLVolume* volume : volumes)
+ {
+ bb.merge(volume->transformed_bounding_box());
+ }
+ const Vec3d& size = bb.size();
+ m_on_update_geometry_info_callback.call(size(0), size(1), size(2), m_gizmos.get_scale());
+ }
+
+ m_dirty = true;
+ }
+ else if (evt.Dragging() && !gizmos_overlay_contains_mouse)
+ {
+ m_mouse.dragging = true;
+
+ if ((m_layers_editing.state != LayersEditing::Unknown) && (layer_editing_object_idx != -1))
+ {
+ if (m_layers_editing.state == LayersEditing::Editing)
+ _perform_layer_editing_action(&evt);
+ }
+ else if (evt.LeftIsDown())
+ {
+ // if dragging over blank area with left button, rotate
+ if (m_mouse.is_start_position_3D_defined())
+ {
+ const Vec3d& orig = m_mouse.drag.start_position_3D;
+ m_camera.phi += (((float)pos(0) - (float)orig(0)) * TRACKBALLSIZE);
+ m_camera.set_theta(m_camera.get_theta() - ((float)pos(1) - (float)orig(1)) * TRACKBALLSIZE);
+
+ m_on_viewport_changed_callback.call();
+
+ m_dirty = true;
+ }
+ m_mouse.drag.start_position_3D = Vec3d((double)pos(0), (double)pos(1), 0.0);
+ }
+ else if (evt.MiddleIsDown() || evt.RightIsDown())
+ {
+ // If dragging over blank area with right button, pan.
+ if (m_mouse.is_start_position_2D_defined())
+ {
+ // get point in model space at Z = 0
+ float z = 0.0f;
+ const Vec3d& cur_pos = _mouse_to_3d(pos, &z);
+ Vec3d orig = _mouse_to_3d(m_mouse.drag.start_position_2D, &z);
+ m_camera.target += orig - cur_pos;
+
+ m_on_viewport_changed_callback.call();
+
+ m_dirty = true;
+ }
+
+ m_mouse.drag.start_position_2D = pos;
+ }
+ }
+ else if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
+ {
+ if (m_layers_editing.state != LayersEditing::Unknown)
+ {
+ m_layers_editing.state = LayersEditing::Unknown;
+ _stop_timer();
+
+ if (layer_editing_object_idx != -1)
+ m_on_model_update_callback.call();
+ }
+ else if ((m_mouse.drag.move_volume_idx != -1) && m_mouse.dragging)
+ {
+ // get all volumes belonging to the same group, if any
+ std::vector<int> volume_idxs;
+ int vol_id = m_mouse.drag.move_volume_idx;
+ int group_id = m_mouse.drag.move_with_shift ? m_volumes.volumes[vol_id]->select_group_id : m_volumes.volumes[vol_id]->drag_group_id;
+ if (group_id == -1)
+ volume_idxs.push_back(vol_id);
+ else
+ {
+ for (int i = 0; i < (int)m_volumes.volumes.size(); ++i)
+ {
+ if ((m_mouse.drag.move_with_shift && (m_volumes.volumes[i]->select_group_id == group_id)) || (m_volumes.volumes[i]->drag_group_id == group_id))
+ volume_idxs.push_back(i);
+ }
+ }
+
+ _on_move(volume_idxs);
+
+ // force re-selection of the wipe tower, if needed
+ if ((volume_idxs.size() == 1) && m_volumes.volumes[volume_idxs[0]]->is_wipe_tower)
+ select_volume(volume_idxs[0]);
+ }
+ else if (evt.LeftUp() && !m_mouse.dragging && (m_hover_volume_id == -1) && !gizmos_overlay_contains_mouse && !m_gizmos.is_dragging() && !is_layers_editing_enabled())
+ {
+ // deselect and propagate event through callback
+ if (m_picking_enabled && !m_toolbar_action_running)
+ {
+ deselect_volumes();
+ _on_select(-1, -1);
+ update_gizmos_data();
+ }
+ }
+ else if (evt.LeftUp() && m_gizmos.is_dragging())
+ {
+ switch (m_gizmos.get_current_type())
+ {
+ case Gizmos::Move:
+ {
+ // get all volumes belonging to the same group, if any
+ std::vector<int> volume_idxs;
+ int vol_id = m_mouse.drag.gizmo_volume_idx;
+ int group_id = m_volumes.volumes[vol_id]->select_group_id;
+ if (group_id == -1)
+ volume_idxs.push_back(vol_id);
+ else
+ {
+ for (int i = 0; i < (int)m_volumes.volumes.size(); ++i)
+ {
+ if (m_volumes.volumes[i]->select_group_id == group_id)
+ volume_idxs.push_back(i);
+ }
+ }
+
+ _on_move(volume_idxs);
+
+ break;
+ }
+ case Gizmos::Scale:
+ {
+ m_on_gizmo_scale_uniformly_callback.call((double)m_gizmos.get_scale());
+ break;
+ }
+ case Gizmos::Rotate:
+ {
+ m_on_gizmo_rotate_callback.call((double)m_gizmos.get_angle_z());
+ break;
+ }
+ default:
+ break;
+ }
+ m_gizmos.stop_dragging();
+ Slic3r::GUI::update_settings_value();
+ }
+
+ m_mouse.drag.move_volume_idx = -1;
+ m_mouse.drag.gizmo_volume_idx = -1;
+ m_mouse.set_start_position_3D_as_invalid();
+ m_mouse.set_start_position_2D_as_invalid();
+ m_mouse.dragging = false;
+ m_toolbar_action_running = false;
+ m_dirty = true;
+
+ if (m_canvas->HasCapture())
+ m_canvas->ReleaseMouse();
+ }
+ else if (evt.Moving())
+ {
+ m_mouse.position = Vec2d((double)pos(0), (double)pos(1));
+ // Only refresh if picking is enabled, in that case the objects may get highlighted if the mouse cursor hovers over.
+ if (m_picking_enabled)
+ m_dirty = true;
+ }
+ else
+ evt.Skip();
+}
+
+void GLCanvas3D::on_paint(wxPaintEvent& evt)
+{
+ render();
+}
+
+void GLCanvas3D::on_key_down(wxKeyEvent& evt)
+{
+ if (evt.HasModifiers())
+ evt.Skip();
+ else
+ {
+ int key = evt.GetKeyCode();
+ if (key == WXK_DELETE)
+ m_on_remove_object_callback.call();
+ else
+ {
+#ifdef __WXOSX__
+ if (key == WXK_BACK)
+ m_on_remove_object_callback.call();
+#endif
+ evt.Skip();
+ }
+ }
+}
+
+Size GLCanvas3D::get_canvas_size() const
+{
+ int w = 0;
+ int h = 0;
+
+ if (m_canvas != nullptr)
+ m_canvas->GetSize(&w, &h);
+
+ return Size(w, h);
+}
+
+Point GLCanvas3D::get_local_mouse_position() const
+{
+ if (m_canvas == nullptr)
+ return Point();
+
+ wxPoint mouse_pos = m_canvas->ScreenToClient(wxGetMousePosition());
+ return Point(mouse_pos.x, mouse_pos.y);
+}
+
+void GLCanvas3D::reset_legend_texture()
+{
+ if (!set_current())
+ return;
+
+ m_legend_texture.reset();
+}
+
+void GLCanvas3D::set_tooltip(const std::string& tooltip)
+{
+ if (m_canvas != nullptr)
+ m_canvas->SetToolTip(tooltip);
+}
+
+bool GLCanvas3D::_is_shown_on_screen() const
+{
+ return (m_canvas != nullptr) ? m_canvas->IsShownOnScreen() : false;
+}
+
+void GLCanvas3D::_force_zoom_to_bed()
+{
+ zoom_to_bed();
+ m_force_zoom_to_bed_enabled = false;
+}
+
+bool GLCanvas3D::_init_toolbar()
+{
+ if (!m_toolbar.is_enabled())
+ return true;
+
+ if (!m_toolbar.init("toolbar.png", 36, 1, 1))
+ {
+ // unable to init the toolbar texture, disable it
+ m_toolbar.set_enabled(false);
+ return true;
+ }
+
+// m_toolbar.set_layout_type(GLToolbar::Layout::Vertical);
+ m_toolbar.set_layout_type(GLToolbar::Layout::Horizontal);
+ m_toolbar.set_separator_size(5);
+ m_toolbar.set_gap_size(2);
+
+ GLToolbarItem::Data item;
+
+ item.name = "add";
+ item.tooltip = GUI::L_str("Add...");
+ item.sprite_id = 0;
+ item.is_toggable = false;
+ item.action_callback = &m_action_add_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ item.name = "delete";
+ item.tooltip = GUI::L_str("Delete");
+ item.sprite_id = 1;
+ item.is_toggable = false;
+ item.action_callback = &m_action_delete_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ item.name = "deleteall";
+ item.tooltip = GUI::L_str("Delete all");
+ item.sprite_id = 2;
+ item.is_toggable = false;
+ item.action_callback = &m_action_deleteall_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ item.name = "arrange";
+ item.tooltip = GUI::L_str("Arrange");
+ item.sprite_id = 3;
+ item.is_toggable = false;
+ item.action_callback = &m_action_arrange_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ if (!m_toolbar.add_separator())
+ return false;
+
+ item.name = "more";
+ item.tooltip = GUI::L_str("Add instance");
+ item.sprite_id = 4;
+ item.is_toggable = false;
+ item.action_callback = &m_action_more_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ item.name = "fewer";
+ item.tooltip = GUI::L_str("Remove instance");
+ item.sprite_id = 5;
+ item.is_toggable = false;
+ item.action_callback = &m_action_fewer_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ if (!m_toolbar.add_separator())
+ return false;
+
+ item.name = "split";
+ item.tooltip = GUI::L_str("Split");
+ item.sprite_id = 6;
+ item.is_toggable = false;
+ item.action_callback = &m_action_split_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ item.name = "cut";
+ item.tooltip = GUI::L_str("Cut...");
+ item.sprite_id = 7;
+ item.is_toggable = false;
+ item.action_callback = &m_action_cut_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ if (!m_toolbar.add_separator())
+ return false;
+
+ item.name = "settings";
+ item.tooltip = GUI::L_str("Settings...");
+ item.sprite_id = 8;
+ item.is_toggable = false;
+ item.action_callback = &m_action_settings_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ item.name = "layersediting";
+ item.tooltip = GUI::L_str("Layers editing");
+ item.sprite_id = 9;
+ item.is_toggable = true;
+ item.action_callback = &m_action_layersediting_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ if (!m_toolbar.add_separator())
+ return false;
+
+ item.name = "selectbyparts";
+ item.tooltip = GUI::L_str("Select by parts");
+ item.sprite_id = 10;
+ item.is_toggable = true;
+ item.action_callback = &m_action_selectbyparts_callback;
+ if (!m_toolbar.add_item(item))
+ return false;
+
+ enable_toolbar_item("add", true);
+
+ return true;
+}
+
+void GLCanvas3D::_resize(unsigned int w, unsigned int h)
+{
+ if ((m_canvas == nullptr) && (m_context == nullptr))
+ return;
+
+ // ensures that this canvas is current
+ set_current();
+ ::glViewport(0, 0, w, h);
+
+ ::glMatrixMode(GL_PROJECTION);
+ ::glLoadIdentity();
+
+ const BoundingBoxf3& bbox = _max_bounding_box();
+
+ switch (m_camera.type)
+ {
+ case Camera::Ortho:
+ {
+ float w2 = w;
+ float h2 = h;
+ float two_zoom = 2.0f * get_camera_zoom();
+ if (two_zoom != 0.0f)
+ {
+ float inv_two_zoom = 1.0f / two_zoom;
+ w2 *= inv_two_zoom;
+ h2 *= inv_two_zoom;
+ }
+
+ // FIXME: calculate a tighter value for depth will improve z-fighting
+ float depth = 5.0f * (float)bbox.max_size();
+ ::glOrtho(-w2, w2, -h2, h2, -depth, depth);
+
+ break;
+ }
+// case Camera::Perspective:
+// {
+// float bbox_r = (float)bbox.radius();
+// float fov = PI * 45.0f / 180.0f;
+// float fov_tan = tan(0.5f * fov);
+// float cam_distance = 0.5f * bbox_r / fov_tan;
+// m_camera.distance = cam_distance;
+//
+// float nr = cam_distance - bbox_r * 1.1f;
+// float fr = cam_distance + bbox_r * 1.1f;
+// if (nr < 1.0f)
+// nr = 1.0f;
+//
+// if (fr < nr + 1.0f)
+// fr = nr + 1.0f;
+//
+// float h2 = fov_tan * nr;
+// float w2 = h2 * w / h;
+// ::glFrustum(-w2, w2, -h2, h2, nr, fr);
+//
+// break;
+// }
+ default:
+ {
+ throw std::runtime_error("Invalid camera type.");
+ break;
+ }
+ }
+
+ ::glMatrixMode(GL_MODELVIEW);
+
+ m_dirty = false;
+}
+
+BoundingBoxf3 GLCanvas3D::_max_bounding_box() const
+{
+ BoundingBoxf3 bb = m_bed.get_bounding_box();
+ bb.merge(volumes_bounding_box());
+ return bb;
+}
+
+BoundingBoxf3 GLCanvas3D::_selected_volumes_bounding_box() const
+{
+ BoundingBoxf3 bb;
+
+ std::vector<const GLVolume*> selected_volumes;
+ for (const GLVolume* volume : m_volumes.volumes)
+ {
+ if ((volume != nullptr) && !volume->is_wipe_tower && volume->selected)
+ selected_volumes.push_back(volume);
+ }
+
+ bool use_drag_group_id = selected_volumes.size() > 1;
+ if (use_drag_group_id)
+ {
+ int drag_group_id = selected_volumes[0]->drag_group_id;
+ for (const GLVolume* volume : selected_volumes)
+ {
+ if (drag_group_id != volume->drag_group_id)
+ {
+ use_drag_group_id = false;
+ break;
+ }
+ }
+ }
+
+ if (use_drag_group_id)
+ {
+ for (const GLVolume* volume : selected_volumes)
+ {
+ bb.merge(volume->bounding_box);
+ }
+
+ bb = bb.transformed(selected_volumes[0]->world_matrix().cast<double>());
+ }
+ else
+ {
+ for (const GLVolume* volume : selected_volumes)
+ {
+ bb.merge(volume->transformed_bounding_box());
+ }
+ }
+
+ return bb;
+}
+
+void GLCanvas3D::_zoom_to_bounding_box(const BoundingBoxf3& bbox)
+{
+ // Calculate the zoom factor needed to adjust viewport to bounding box.
+ float zoom = _get_zoom_to_bounding_box_factor(bbox);
+ if (zoom > 0.0f)
+ {
+ m_camera.zoom = zoom;
+ // center view around bounding box center
+ m_camera.target = bbox.center();
+
+ m_on_viewport_changed_callback.call();
+
+ _refresh_if_shown_on_screen();
+ }
+}
+
+float GLCanvas3D::_get_zoom_to_bounding_box_factor(const BoundingBoxf3& bbox) const
+{
+ float max_bb_size = bbox.max_size();
+ if (max_bb_size == 0.0f)
+ return -1.0f;
+
+ // project the bbox vertices on a plane perpendicular to the camera forward axis
+ // then calculates the vertices coordinate on this plane along the camera xy axes
+
+ // we need the view matrix, we let opengl calculate it (same as done in render())
+ _camera_tranform();
+
+ // get the view matrix back from opengl
+ GLfloat matrix[16];
+ ::glGetFloatv(GL_MODELVIEW_MATRIX, matrix);
+
+ // camera axes
+ Vec3d right((double)matrix[0], (double)matrix[4], (double)matrix[8]);
+ Vec3d up((double)matrix[1], (double)matrix[5], (double)matrix[9]);
+ Vec3d forward((double)matrix[2], (double)matrix[6], (double)matrix[10]);
+
+ Vec3d bb_min = bbox.min;
+ Vec3d bb_max = bbox.max;
+ Vec3d bb_center = bbox.center();
+
+ // bbox vertices in world space
+ std::vector<Vec3d> vertices;
+ vertices.reserve(8);
+ vertices.push_back(bb_min);
+ vertices.emplace_back(bb_max(0), bb_min(1), bb_min(2));
+ vertices.emplace_back(bb_max(0), bb_max(1), bb_min(2));
+ vertices.emplace_back(bb_min(0), bb_max(1), bb_min(2));
+ vertices.emplace_back(bb_min(0), bb_min(1), bb_max(2));
+ vertices.emplace_back(bb_max(0), bb_min(1), bb_max(2));
+ vertices.push_back(bb_max);
+ vertices.emplace_back(bb_min(0), bb_max(1), bb_max(2));
+
+ double max_x = 0.0;
+ double max_y = 0.0;
+
+ // margin factor to give some empty space around the bbox
+ double margin_factor = 1.25;
+
+ for (const Vec3d v : vertices)
+ {
+ // project vertex on the plane perpendicular to camera forward axis
+ Vec3d pos(v(0) - bb_center(0), v(1) - bb_center(1), v(2) - bb_center(2));
+ Vec3d proj_on_plane = pos - pos.dot(forward) * forward;
+
+ // calculates vertex coordinate along camera xy axes
+ double x_on_plane = proj_on_plane.dot(right);
+ double y_on_plane = proj_on_plane.dot(up);
+
+ max_x = std::max(max_x, margin_factor * std::abs(x_on_plane));
+ max_y = std::max(max_y, margin_factor * std::abs(y_on_plane));
+ }
+
+ if ((max_x == 0.0) || (max_y == 0.0))
+ return -1.0f;
+
+ max_x *= 2.0;
+ max_y *= 2.0;
+
+ const Size& cnv_size = get_canvas_size();
+ return (float)std::min((double)cnv_size.get_width() / max_x, (double)cnv_size.get_height() / max_y);
+}
+
+void GLCanvas3D::_deregister_callbacks()
+{
+ m_on_viewport_changed_callback.deregister_callback();
+ m_on_double_click_callback.deregister_callback();
+ m_on_right_click_callback.deregister_callback();
+ m_on_select_object_callback.deregister_callback();
+ m_on_model_update_callback.deregister_callback();
+ m_on_remove_object_callback.deregister_callback();
+ m_on_arrange_callback.deregister_callback();
+ m_on_rotate_object_left_callback.deregister_callback();
+ m_on_rotate_object_right_callback.deregister_callback();
+ m_on_scale_object_uniformly_callback.deregister_callback();
+ m_on_increase_objects_callback.deregister_callback();
+ m_on_decrease_objects_callback.deregister_callback();
+ m_on_instance_moved_callback.deregister_callback();
+ m_on_wipe_tower_moved_callback.deregister_callback();
+ m_on_enable_action_buttons_callback.deregister_callback();
+ m_on_gizmo_scale_uniformly_callback.deregister_callback();
+ m_on_gizmo_rotate_callback.deregister_callback();
+ m_on_gizmo_flatten_callback.deregister_callback();
+ m_on_update_geometry_info_callback.deregister_callback();
+
+ m_action_add_callback.deregister_callback();
+ m_action_delete_callback.deregister_callback();
+ m_action_deleteall_callback.deregister_callback();
+ m_action_arrange_callback.deregister_callback();
+ m_action_more_callback.deregister_callback();
+ m_action_fewer_callback.deregister_callback();
+ m_action_split_callback.deregister_callback();
+ m_action_cut_callback.deregister_callback();
+ m_action_settings_callback.deregister_callback();
+ m_action_layersediting_callback.deregister_callback();
+ m_action_selectbyparts_callback.deregister_callback();
+}
+
+void GLCanvas3D::_mark_volumes_for_layer_height() const
+{
+ if (m_print == nullptr)
+ return;
+
+ for (GLVolume* vol : m_volumes.volumes)
+ {
+ int object_id = int(vol->select_group_id / 1000000);
+ int shader_id = m_layers_editing.get_shader_program_id();
+
+ if (is_layers_editing_enabled() && (shader_id != -1) && vol->selected &&
+ vol->has_layer_height_texture() && (object_id < (int)m_print->objects().size()))
+ {
+ vol->set_layer_height_texture_data(m_layers_editing.get_z_texture_id(), shader_id,
+ m_print->get_object(object_id), _get_layers_editing_cursor_z_relative(), m_layers_editing.band_width);
+ }
+ else
+ vol->reset_layer_height_texture_data();
+ }
+}
+
+void GLCanvas3D::_refresh_if_shown_on_screen()
+{
+ if (_is_shown_on_screen())
+ {
+ const Size& cnv_size = get_canvas_size();
+ _resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height());
+ if (m_canvas != nullptr)
+ m_canvas->Refresh();
+ }
+}
+
+void GLCanvas3D::_camera_tranform() const
+{
+ ::glMatrixMode(GL_MODELVIEW);
+ ::glLoadIdentity();
+
+ ::glRotatef(-m_camera.get_theta(), 1.0f, 0.0f, 0.0f); // pitch
+ ::glRotatef(m_camera.phi, 0.0f, 0.0f, 1.0f); // yaw
+
+ Vec3d neg_target = - m_camera.target;
+ ::glTranslatef((GLfloat)neg_target(0), (GLfloat)neg_target(1), (GLfloat)neg_target(2));
+}
+
+void GLCanvas3D::_picking_pass() const
+{
+ const Vec2d& pos = m_mouse.position;
+
+ if (m_picking_enabled && !m_mouse.dragging && (pos != Vec2d(DBL_MAX, DBL_MAX)))
+ {
+ // Render the object for picking.
+ // FIXME This cannot possibly work in a multi - sampled context as the color gets mangled by the anti - aliasing.
+ // Better to use software ray - casting on a bounding - box hierarchy.
+
+ if (m_multisample_allowed)
+ ::glDisable(GL_MULTISAMPLE);
+
+ ::glDisable(GL_BLEND);
+ ::glEnable(GL_DEPTH_TEST);
+
+ ::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ _render_volumes(true);
+ m_gizmos.render_current_gizmo_for_picking_pass(_selected_volumes_bounding_box());
+
+ if (m_multisample_allowed)
+ ::glEnable(GL_MULTISAMPLE);
+
+ int volume_id = -1;
+ for (GLVolume* vol : m_volumes.volumes)
+ {
+ vol->hover = false;
+ }
+
+ GLubyte color[4] = { 0, 0, 0, 0 };
+ const Size& cnv_size = get_canvas_size();
+ bool inside = (0 <= pos(0)) && (pos(0) < cnv_size.get_width()) && (0 <= pos(1)) && (pos(1) < cnv_size.get_height());
+ if (inside)
+ {
+ ::glReadPixels(pos(0), cnv_size.get_height() - pos(1) - 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, (void*)color);
+ volume_id = color[0] + color[1] * 256 + color[2] * 256 * 256;
+ }
+
+ if ((0 <= volume_id) && (volume_id < (int)m_volumes.volumes.size()))
+ {
+ m_hover_volume_id = volume_id;
+ m_volumes.volumes[volume_id]->hover = true;
+ int group_id = m_volumes.volumes[volume_id]->select_group_id;
+ if (group_id != -1)
+ {
+ for (GLVolume* vol : m_volumes.volumes)
+ {
+ if (vol->select_group_id == group_id)
+ vol->hover = true;
+ }
+ }
+ m_gizmos.set_hover_id(-1);
+ }
+ else
+ {
+ m_hover_volume_id = -1;
+ m_gizmos.set_hover_id(inside ? (254 - (int)color[2]) : -1);
+ }
+
+ // updates gizmos overlay
+ if (_get_first_selected_object_id() != -1)
+ m_gizmos.update_hover_state(*this, pos);
+ else
+ m_gizmos.reset_all_states();
+
+ m_toolbar.update_hover_state(pos);
+ }
+}
+
+void GLCanvas3D::_render_background() const
+{
+ ::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ ::glPushMatrix();
+ ::glLoadIdentity();
+ ::glMatrixMode(GL_PROJECTION);
+ ::glPushMatrix();
+ ::glLoadIdentity();
+
+ // Draws a bluish bottom to top gradient over the complete screen.
+ ::glDisable(GL_DEPTH_TEST);
+
+ ::glBegin(GL_QUADS);
+ ::glColor3f(0.0f, 0.0f, 0.0f);
+ ::glVertex2f(-1.0f, -1.0f);
+ ::glVertex2f(1.0f, -1.0f);
+
+ if (m_dynamic_background_enabled && _is_any_volume_outside())
+ ::glColor3f(ERROR_BG_COLOR[0], ERROR_BG_COLOR[1], ERROR_BG_COLOR[2]);
+ else
+ ::glColor3f(DEFAULT_BG_COLOR[0], DEFAULT_BG_COLOR[1], DEFAULT_BG_COLOR[2]);
+
+ ::glVertex2f(1.0f, 1.0f);
+ ::glVertex2f(-1.0f, 1.0f);
+ ::glEnd();
+
+ ::glEnable(GL_DEPTH_TEST);
+
+ ::glPopMatrix();
+ ::glMatrixMode(GL_MODELVIEW);
+ ::glPopMatrix();
+}
+
+void GLCanvas3D::_render_bed(float theta) const
+{
+ m_bed.render(theta);
+}
+
+void GLCanvas3D::_render_axes(bool depth_test) const
+{
+ m_axes.render(depth_test);
+}
+
+void GLCanvas3D::_render_objects() const
+{
+ if (m_volumes.empty())
+ return;
+
+ ::glEnable(GL_LIGHTING);
+ ::glEnable(GL_DEPTH_TEST);
+
+ if (!m_shader_enabled)
+ _render_volumes(false);
+ else if (m_use_VBOs)
+ {
+ if (m_picking_enabled)
+ {
+ _mark_volumes_for_layer_height();
+
+ if (m_config != nullptr)
+ {
+ const BoundingBoxf3& bed_bb = m_bed.get_bounding_box();
+ m_volumes.set_print_box((float)bed_bb.min(0), (float)bed_bb.min(1), 0.0f, (float)bed_bb.max(0), (float)bed_bb.max(1), (float)m_config->opt_float("max_print_height"));
+ m_volumes.check_outside_state(m_config, nullptr);
+ }
+ // do not cull backfaces to show broken geometry, if any
+ ::glDisable(GL_CULL_FACE);
+ }
+
+ m_shader.start_using();
+ m_volumes.render_VBOs();
+ m_shader.stop_using();
+
+ if (m_picking_enabled)
+ ::glEnable(GL_CULL_FACE);
+ }
+ else
+ {
+ // do not cull backfaces to show broken geometry, if any
+ if (m_picking_enabled)
+ ::glDisable(GL_CULL_FACE);
+
+ m_volumes.render_legacy();
+
+ if (m_picking_enabled)
+ ::glEnable(GL_CULL_FACE);
+ }
+
+ ::glDisable(GL_LIGHTING);
+}
+
+void GLCanvas3D::_render_cutting_plane() const
+{
+ m_cutting_plane.render(volumes_bounding_box());
+}
+
+void GLCanvas3D::_render_warning_texture() const
+{
+ if (!m_warning_texture_enabled)
+ return;
+
+ m_warning_texture.render(*this);
+}
+
+void GLCanvas3D::_render_legend_texture() const
+{
+ if (!m_legend_texture_enabled)
+ return;
+
+ m_legend_texture.render(*this);
+}
+
+void GLCanvas3D::_render_layer_editing_overlay() const
+{
+ if (m_print == nullptr)
+ return;
+
+ GLVolume* volume = nullptr;
+
+ for (GLVolume* vol : m_volumes.volumes)
+ {
+ if ((vol != nullptr) && vol->selected && vol->has_layer_height_texture())
+ {
+ volume = vol;
+ break;
+ }
+ }
+
+ if (volume == nullptr)
+ return;
+
+ // If the active object was not allocated at the Print, go away.This should only be a momentary case between an object addition / deletion
+ // and an update by Platter::async_apply_config.
+ int object_idx = int(volume->select_group_id / 1000000);
+ if ((int)m_print->objects().size() < object_idx)
+ return;
+
+ const PrintObject* print_object = m_print->get_object(object_idx);
+ if (print_object == nullptr)
+ return;
+
+ m_layers_editing.render(*this, *print_object, *volume);
+}
+
+void GLCanvas3D::_render_volumes(bool fake_colors) const
+{
+ static const GLfloat INV_255 = 1.0f / 255.0f;
+
+ if (!fake_colors)
+ ::glEnable(GL_LIGHTING);
+
+ // do not cull backfaces to show broken geometry, if any
+ ::glDisable(GL_CULL_FACE);
+
+ ::glEnable(GL_BLEND);
+ ::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ ::glEnableClientState(GL_VERTEX_ARRAY);
+ ::glEnableClientState(GL_NORMAL_ARRAY);
+
+ unsigned int volume_id = 0;
+ for (GLVolume* vol : m_volumes.volumes)
+ {
+ if (fake_colors)
+ {
+ // Object picking mode. Render the object with a color encoding the object index.
+ unsigned int r = (volume_id & 0x000000FF) >> 0;
+ unsigned int g = (volume_id & 0x0000FF00) >> 8;
+ unsigned int b = (volume_id & 0x00FF0000) >> 16;
+ ::glColor3f((GLfloat)r * INV_255, (GLfloat)g * INV_255, (GLfloat)b * INV_255);
+ }
+ else
+ {
+ vol->set_render_color();
+ ::glColor4f(vol->render_color[0], vol->render_color[1], vol->render_color[2], vol->render_color[3]);
+ }
+
+ vol->render();
+ ++volume_id;
+ }
+
+ ::glDisableClientState(GL_NORMAL_ARRAY);
+ ::glDisableClientState(GL_VERTEX_ARRAY);
+ ::glDisable(GL_BLEND);
+
+ ::glEnable(GL_CULL_FACE);
+
+ if (!fake_colors)
+ ::glDisable(GL_LIGHTING);
+}
+
+void GLCanvas3D::_render_current_gizmo() const
+{
+ m_gizmos.render_current_gizmo(_selected_volumes_bounding_box());
+}
+
+void GLCanvas3D::_render_gizmos_overlay() const
+{
+ m_gizmos.render_overlay(*this);
+}
+
+void GLCanvas3D::_render_toolbar() const
+{
+ _resize_toolbar();
+ m_toolbar.render();
+}
+
+float GLCanvas3D::_get_layers_editing_cursor_z_relative() const
+{
+ return m_layers_editing.get_cursor_z_relative(*this);
+}
+
+void GLCanvas3D::_perform_layer_editing_action(wxMouseEvent* evt)
+{
+ int object_idx_selected = m_layers_editing.last_object_id;
+ if (object_idx_selected == -1)
+ return;
+
+ if (m_print == nullptr)
+ return;
+
+ const PrintObject* selected_obj = m_print->get_object(object_idx_selected);
+ if (selected_obj == nullptr)
+ return;
+
+ // A volume is selected. Test, whether hovering over a layer thickness bar.
+ if (evt != nullptr)
+ {
+ const Rect& rect = LayersEditing::get_bar_rect_screen(*this);
+ float b = rect.get_bottom();
+ m_layers_editing.last_z = unscale<double>(selected_obj->size(2)) * (b - evt->GetY() - 1.0f) / (b - rect.get_top());
+ m_layers_editing.last_action = evt->ShiftDown() ? (evt->RightIsDown() ? 3 : 2) : (evt->RightIsDown() ? 0 : 1);
+ }
+
+ // Mark the volume as modified, so Print will pick its layer height profile ? Where to mark it ?
+ // Start a timer to refresh the print ? schedule_background_process() ?
+ // The PrintObject::adjust_layer_height_profile() call adjusts the profile of its associated ModelObject, it does not modify the profile of the PrintObject itself,
+ // therefore it is safe to call it while the background processing is running.
+ const_cast<PrintObject*>(selected_obj)->adjust_layer_height_profile(m_layers_editing.last_z, m_layers_editing.strength, m_layers_editing.band_width, m_layers_editing.last_action);
+
+ // searches the id of the first volume of the selected object
+ int volume_idx = 0;
+ for (int i = 0; i < object_idx_selected; ++i)
+ {
+ const PrintObject* obj = m_print->get_object(i);
+ if (obj != nullptr)
+ {
+ for (int j = 0; j < (int)obj->region_volumes.size(); ++j)
+ {
+ volume_idx += (int)obj->region_volumes[j].size();
+ }
+ }
+ }
+
+ m_volumes.volumes[volume_idx]->generate_layer_height_texture(selected_obj, 1);
+ _refresh_if_shown_on_screen();
+
+ // Automatic action on mouse down with the same coordinate.
+ _start_timer();
+}
+
+Vec3d GLCanvas3D::_mouse_to_3d(const Point& mouse_pos, float* z)
+{
+ if (m_canvas == nullptr)
+ return Vec3d(DBL_MAX, DBL_MAX, DBL_MAX);
+
+ _camera_tranform();
+
+ GLint viewport[4];
+ ::glGetIntegerv(GL_VIEWPORT, viewport);
+ GLdouble modelview_matrix[16];
+ ::glGetDoublev(GL_MODELVIEW_MATRIX, modelview_matrix);
+ GLdouble projection_matrix[16];
+ ::glGetDoublev(GL_PROJECTION_MATRIX, projection_matrix);
+
+ GLint y = viewport[3] - (GLint)mouse_pos(1);
+ GLfloat mouse_z;
+ if (z == nullptr)
+ ::glReadPixels((GLint)mouse_pos(0), y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, (void*)&mouse_z);
+ else
+ mouse_z = *z;
+
+ GLdouble out_x, out_y, out_z;
+ ::gluUnProject((GLdouble)mouse_pos(0), (GLdouble)y, (GLdouble)mouse_z, modelview_matrix, projection_matrix, viewport, &out_x, &out_y, &out_z);
+ return Vec3d((double)out_x, (double)out_y, (double)out_z);
+}
+
+Vec3d GLCanvas3D::_mouse_to_bed_3d(const Point& mouse_pos)
+{
+ return mouse_ray(mouse_pos).intersect_plane(0.0);
+}
+
+Linef3 GLCanvas3D::mouse_ray(const Point& mouse_pos)
+{
+ float z0 = 0.0f;
+ float z1 = 1.0f;
+ return Linef3(_mouse_to_3d(mouse_pos, &z0), _mouse_to_3d(mouse_pos, &z1));
+}
+
+void GLCanvas3D::_start_timer()
+{
+ if (m_timer != nullptr)
+ m_timer->Start(100, wxTIMER_CONTINUOUS);
+}
+
+void GLCanvas3D::_stop_timer()
+{
+ if (m_timer != nullptr)
+ m_timer->Stop();
+}
+
+int GLCanvas3D::_get_first_selected_object_id() const
+{
+ if (m_print != nullptr)
+ {
+ int objects_count = (int)m_print->objects().size();
+
+ for (const GLVolume* vol : m_volumes.volumes)
+ {
+ if ((vol != nullptr) && vol->selected)
+ {
+ int object_id = vol->select_group_id / 1000000;
+ // Objects with object_id >= 1000 have a specific meaning, for example the wipe tower proxy.
+ if (object_id < 10000)
+ return (object_id >= objects_count) ? -1 : object_id;
+ }
+ }
+ }
+ return -1;
+}
+
+int GLCanvas3D::_get_first_selected_volume_id(int object_id) const
+{
+ int volume_id = -1;
+
+ for (const GLVolume* vol : m_volumes.volumes)
+ {
+ ++volume_id;
+ if ((vol != nullptr) && vol->selected && (object_id == vol->select_group_id / 1000000))
+ return volume_id;
+ }
+
+ return -1;
+}
+
+void GLCanvas3D::_load_print_toolpaths()
+{
+ // ensures this canvas is current
+ if (!set_current())
+ return;
+
+ if (m_print == nullptr)
+ return;
+
+ if (!m_print->is_step_done(psSkirt) || !m_print->is_step_done(psBrim))
+ return;
+
+ if (!m_print->has_skirt() && (m_print->config().brim_width.value == 0))
+ return;
+
+ const float color[] = { 0.5f, 1.0f, 0.5f, 1.0f }; // greenish
+
+ // number of skirt layers
+ size_t total_layer_count = 0;
+ for (const PrintObject* print_object : m_print->objects())
+ {
+ total_layer_count = std::max(total_layer_count, print_object->total_layer_count());
+ }
+ size_t skirt_height = m_print->has_infinite_skirt() ? total_layer_count : std::min<size_t>(m_print->config().skirt_height.value, total_layer_count);
+ if ((skirt_height == 0) && (m_print->config().brim_width.value > 0))
+ skirt_height = 1;
+
+ // get first skirt_height layers (maybe this should be moved to a PrintObject method?)
+ const PrintObject* object0 = m_print->objects().front();
+ std::vector<float> print_zs;
+ print_zs.reserve(skirt_height * 2);
+ for (size_t i = 0; i < std::min(skirt_height, object0->layers().size()); ++i)
+ {
+ print_zs.push_back(float(object0->layers()[i]->print_z));
+ }
+ //FIXME why there are support layers?
+ for (size_t i = 0; i < std::min(skirt_height, object0->support_layers().size()); ++i)
+ {
+ print_zs.push_back(float(object0->support_layers()[i]->print_z));
+ }
+ sort_remove_duplicates(print_zs);
+ if (print_zs.size() > skirt_height)
+ print_zs.erase(print_zs.begin() + skirt_height, print_zs.end());
+
+ m_volumes.volumes.emplace_back(new GLVolume(color));
+ GLVolume& volume = *m_volumes.volumes.back();
+ for (size_t i = 0; i < skirt_height; ++i) {
+ volume.print_zs.push_back(print_zs[i]);
+ volume.offsets.push_back(volume.indexed_vertex_array.quad_indices.size());
+ volume.offsets.push_back(volume.indexed_vertex_array.triangle_indices.size());
+ if (i == 0)
+ _3DScene::extrusionentity_to_verts(m_print->brim(), print_zs[i], Point(0, 0), volume);
+
+ _3DScene::extrusionentity_to_verts(m_print->skirt(), print_zs[i], Point(0, 0), volume);
+ }
+ volume.bounding_box = volume.indexed_vertex_array.bounding_box();
+ volume.indexed_vertex_array.finalize_geometry(m_use_VBOs && m_initialized);
+}
+
+void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, const std::vector<std::string>& str_tool_colors)
+{
+ std::vector<float> tool_colors = _parse_colors(str_tool_colors);
+
+ struct Ctxt
+ {
+ const Points *shifted_copies;
+ std::vector<const Layer*> layers;
+ bool has_perimeters;
+ bool has_infill;
+ bool has_support;
+ const std::vector<float>* tool_colors;
+
+ // Number of vertices (each vertex is 6x4=24 bytes long)
+ static const size_t alloc_size_max() { return 131072; } // 3.15MB
+ // static const size_t alloc_size_max () { return 65536; } // 1.57MB
+ // static const size_t alloc_size_max () { return 32768; } // 786kB
+ static const size_t alloc_size_reserve() { return alloc_size_max() * 2; }
+
+ static const float* color_perimeters() { static float color[4] = { 1.0f, 1.0f, 0.0f, 1.f }; return color; } // yellow
+ static const float* color_infill() { static float color[4] = { 1.0f, 0.5f, 0.5f, 1.f }; return color; } // redish
+ static const float* color_support() { static float color[4] = { 0.5f, 1.0f, 0.5f, 1.f }; return color; } // greenish
+
+ // For cloring by a tool, return a parsed color.
+ bool color_by_tool() const { return tool_colors != nullptr; }
+ size_t number_tools() const { return this->color_by_tool() ? tool_colors->size() / 4 : 0; }
+ const float* color_tool(size_t tool) const { return tool_colors->data() + tool * 4; }
+ int volume_idx(int extruder, int feature) const
+ {
+ return this->color_by_tool() ? std::min<int>(this->number_tools() - 1, std::max<int>(extruder - 1, 0)) : feature;
+ }
+ } ctxt;
+
+ ctxt.shifted_copies = &print_object.copies();
+
+ // order layers by print_z
+ ctxt.layers.reserve(print_object.layers().size() + print_object.support_layers().size());
+ for (const Layer *layer : print_object.layers())
+ ctxt.layers.push_back(layer);
+ for (const Layer *layer : print_object.support_layers())
+ ctxt.layers.push_back(layer);
+ std::sort(ctxt.layers.begin(), ctxt.layers.end(), [](const Layer *l1, const Layer *l2) { return l1->print_z < l2->print_z; });
+
+ // Maximum size of an allocation block: 32MB / sizeof(float)
+ ctxt.has_perimeters = print_object.is_step_done(posPerimeters);
+ ctxt.has_infill = print_object.is_step_done(posInfill);
+ ctxt.has_support = print_object.is_step_done(posSupportMaterial);
+ ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors;
+
+ BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - start";
+
+ //FIXME Improve the heuristics for a grain size.
+ size_t grain_size = std::max(ctxt.layers.size() / 16, size_t(1));
+ tbb::spin_mutex new_volume_mutex;
+ auto new_volume = [this, &new_volume_mutex](const float *color) -> GLVolume* {
+ auto *volume = new GLVolume(color);
+ new_volume_mutex.lock();
+ m_volumes.volumes.emplace_back(volume);
+ new_volume_mutex.unlock();
+ return volume;
+ };
+ const size_t volumes_cnt_initial = m_volumes.volumes.size();
+ std::vector<GLVolumeCollection> volumes_per_thread(ctxt.layers.size());
+ tbb::parallel_for(
+ tbb::blocked_range<size_t>(0, ctxt.layers.size(), grain_size),
+ [&ctxt, &new_volume](const tbb::blocked_range<size_t>& range) {
+ std::vector<GLVolume*> vols;
+ if (ctxt.color_by_tool()) {
+ for (size_t i = 0; i < ctxt.number_tools(); ++i)
+ vols.emplace_back(new_volume(ctxt.color_tool(i)));
+ }
+ else
+ vols = { new_volume(ctxt.color_perimeters()), new_volume(ctxt.color_infill()), new_volume(ctxt.color_support()) };
+ for (GLVolume *vol : vols)
+ vol->indexed_vertex_array.reserve(ctxt.alloc_size_reserve());
+ for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) {
+ const Layer *layer = ctxt.layers[idx_layer];
+ for (size_t i = 0; i < vols.size(); ++i) {
+ GLVolume &vol = *vols[i];
+ if (vol.print_zs.empty() || vol.print_zs.back() != layer->print_z) {
+ vol.print_zs.push_back(layer->print_z);
+ vol.offsets.push_back(vol.indexed_vertex_array.quad_indices.size());
+ vol.offsets.push_back(vol.indexed_vertex_array.triangle_indices.size());
+ }
+ }
+ for (const Point &copy : *ctxt.shifted_copies) {
+ for (const LayerRegion *layerm : layer->regions()) {
+ if (ctxt.has_perimeters)
+ _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy,
+ *vols[ctxt.volume_idx(layerm->region()->config().perimeter_extruder.value, 0)]);
+ if (ctxt.has_infill) {
+ for (const ExtrusionEntity *ee : layerm->fills.entities) {
+ // fill represents infill extrusions of a single island.
+ const auto *fill = dynamic_cast<const ExtrusionEntityCollection*>(ee);
+ if (!fill->entities.empty())
+ _3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy,
+ *vols[ctxt.volume_idx(
+ is_solid_infill(fill->entities.front()->role()) ?
+ layerm->region()->config().solid_infill_extruder :
+ layerm->region()->config().infill_extruder,
+ 1)]);
+ }
+ }
+ }
+ if (ctxt.has_support) {
+ const SupportLayer *support_layer = dynamic_cast<const SupportLayer*>(layer);
+ if (support_layer) {
+ for (const ExtrusionEntity *extrusion_entity : support_layer->support_fills.entities)
+ _3DScene::extrusionentity_to_verts(extrusion_entity, float(layer->print_z), copy,
+ *vols[ctxt.volume_idx(
+ (extrusion_entity->role() == erSupportMaterial) ?
+ support_layer->object()->config().support_material_extruder :
+ support_layer->object()->config().support_material_interface_extruder,
+ 2)]);
+ }
+ }
+ }
+ for (size_t i = 0; i < vols.size(); ++i) {
+ GLVolume &vol = *vols[i];
+ if (vol.indexed_vertex_array.vertices_and_normals_interleaved.size() / 6 > ctxt.alloc_size_max()) {
+ // Store the vertex arrays and restart their containers,
+ vols[i] = new_volume(vol.color);
+ GLVolume &vol_new = *vols[i];
+ // Assign the large pre-allocated buffers to the new GLVolume.
+ vol_new.indexed_vertex_array = std::move(vol.indexed_vertex_array);
+ // Copy the content back to the old GLVolume.
+ vol.indexed_vertex_array = vol_new.indexed_vertex_array;
+ // Finalize a bounding box of the old GLVolume.
+ vol.bounding_box = vol.indexed_vertex_array.bounding_box();
+ // Clear the buffers, but keep them pre-allocated.
+ vol_new.indexed_vertex_array.clear();
+ // Just make sure that clear did not clear the reserved memory.
+ vol_new.indexed_vertex_array.reserve(ctxt.alloc_size_reserve());
+ }
+ }
+ }
+ for (GLVolume *vol : vols) {
+ vol->bounding_box = vol->indexed_vertex_array.bounding_box();
+ vol->indexed_vertex_array.shrink_to_fit();
+ }
+ });
+
+ BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - finalizing results";
+ // Remove empty volumes from the newly added volumes.
+ m_volumes.volumes.erase(
+ std::remove_if(m_volumes.volumes.begin() + volumes_cnt_initial, m_volumes.volumes.end(),
+ [](const GLVolume *volume) { return volume->empty(); }),
+ m_volumes.volumes.end());
+ for (size_t i = volumes_cnt_initial; i < m_volumes.volumes.size(); ++i)
+ m_volumes.volumes[i]->indexed_vertex_array.finalize_geometry(m_use_VBOs && m_initialized);
+
+ BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - end";
+}
+
+void GLCanvas3D::_load_wipe_tower_toolpaths(const std::vector<std::string>& str_tool_colors)
+{
+ if ((m_print == nullptr) || m_print->wipe_tower_data().tool_changes.empty())
+ return;
+
+ if (!m_print->is_step_done(psWipeTower))
+ return;
+
+ std::vector<float> tool_colors = _parse_colors(str_tool_colors);
+
+ struct Ctxt
+ {
+ const Print *print;
+ const std::vector<float> *tool_colors;
+ WipeTower::xy wipe_tower_pos;
+ float wipe_tower_angle;
+
+ // Number of vertices (each vertex is 6x4=24 bytes long)
+ static const size_t alloc_size_max() { return 131072; } // 3.15MB
+ static const size_t alloc_size_reserve() { return alloc_size_max() * 2; }
+
+ static const float* color_support() { static float color[4] = { 0.5f, 1.0f, 0.5f, 1.f }; return color; } // greenish
+
+ // For cloring by a tool, return a parsed color.
+ bool color_by_tool() const { return tool_colors != nullptr; }
+ size_t number_tools() const { return this->color_by_tool() ? tool_colors->size() / 4 : 0; }
+ const float* color_tool(size_t tool) const { return tool_colors->data() + tool * 4; }
+ int volume_idx(int tool, int feature) const
+ {
+ return this->color_by_tool() ? std::min<int>(this->number_tools() - 1, std::max<int>(tool, 0)) : feature;
+ }
+
+ const std::vector<WipeTower::ToolChangeResult>& tool_change(size_t idx) {
+ const auto &tool_changes = print->wipe_tower_data().tool_changes;
+ return priming.empty() ?
+ ((idx == tool_changes.size()) ? final : tool_changes[idx]) :
+ ((idx == 0) ? priming : (idx == tool_changes.size() + 1) ? final : tool_changes[idx - 1]);
+ }
+ std::vector<WipeTower::ToolChangeResult> priming;
+ std::vector<WipeTower::ToolChangeResult> final;
+ } ctxt;
+
+ ctxt.print = m_print;
+ ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors;
+ if (m_print->wipe_tower_data().priming && m_print->config().single_extruder_multi_material_priming)
+ ctxt.priming.emplace_back(*m_print->wipe_tower_data().priming.get());
+ if (m_print->wipe_tower_data().final_purge)
+ ctxt.final.emplace_back(*m_print->wipe_tower_data().final_purge.get());
+
+ ctxt.wipe_tower_angle = ctxt.print->config().wipe_tower_rotation_angle.value/180.f * PI;
+ ctxt.wipe_tower_pos = WipeTower::xy(ctxt.print->config().wipe_tower_x.value, ctxt.print->config().wipe_tower_y.value);
+
+ BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - start";
+
+ //FIXME Improve the heuristics for a grain size.
+ size_t n_items = m_print->wipe_tower_data().tool_changes.size() + (ctxt.priming.empty() ? 0 : 1);
+ size_t grain_size = std::max(n_items / 128, size_t(1));
+ tbb::spin_mutex new_volume_mutex;
+ auto new_volume = [this, &new_volume_mutex](const float *color) -> GLVolume* {
+ auto *volume = new GLVolume(color);
+ new_volume_mutex.lock();
+ m_volumes.volumes.emplace_back(volume);
+ new_volume_mutex.unlock();
+ return volume;
+ };
+ const size_t volumes_cnt_initial = m_volumes.volumes.size();
+ std::vector<GLVolumeCollection> volumes_per_thread(n_items);
+ tbb::parallel_for(
+ tbb::blocked_range<size_t>(0, n_items, grain_size),
+ [&ctxt, &new_volume](const tbb::blocked_range<size_t>& range) {
+ // Bounding box of this slab of a wipe tower.
+ std::vector<GLVolume*> vols;
+ if (ctxt.color_by_tool()) {
+ for (size_t i = 0; i < ctxt.number_tools(); ++i)
+ vols.emplace_back(new_volume(ctxt.color_tool(i)));
+ }
+ else
+ vols = { new_volume(ctxt.color_support()) };
+ for (GLVolume *volume : vols)
+ volume->indexed_vertex_array.reserve(ctxt.alloc_size_reserve());
+ for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) {
+ const std::vector<WipeTower::ToolChangeResult> &layer = ctxt.tool_change(idx_layer);
+ for (size_t i = 0; i < vols.size(); ++i) {
+ GLVolume &vol = *vols[i];
+ if (vol.print_zs.empty() || vol.print_zs.back() != layer.front().print_z) {
+ vol.print_zs.push_back(layer.front().print_z);
+ vol.offsets.push_back(vol.indexed_vertex_array.quad_indices.size());
+ vol.offsets.push_back(vol.indexed_vertex_array.triangle_indices.size());
+ }
+ }
+ for (const WipeTower::ToolChangeResult &extrusions : layer) {
+ for (size_t i = 1; i < extrusions.extrusions.size();) {
+ const WipeTower::Extrusion &e = extrusions.extrusions[i];
+ if (e.width == 0.) {
+ ++i;
+ continue;
+ }
+ size_t j = i + 1;
+ if (ctxt.color_by_tool())
+ for (; j < extrusions.extrusions.size() && extrusions.extrusions[j].tool == e.tool && extrusions.extrusions[j].width > 0.f; ++j);
+ else
+ for (; j < extrusions.extrusions.size() && extrusions.extrusions[j].width > 0.f; ++j);
+ size_t n_lines = j - i;
+ Lines lines;
+ std::vector<double> widths;
+ std::vector<double> heights;
+ lines.reserve(n_lines);
+ widths.reserve(n_lines);
+ heights.assign(n_lines, extrusions.layer_height);
+ WipeTower::Extrusion e_prev = extrusions.extrusions[i-1];
+
+ if (!extrusions.priming) { // wipe tower extrusions describe the wipe tower at the origin with no rotation
+ e_prev.pos.rotate(ctxt.wipe_tower_angle);
+ e_prev.pos.translate(ctxt.wipe_tower_pos);
+ }
+
+ for (; i < j; ++i) {
+ WipeTower::Extrusion e = extrusions.extrusions[i];
+ assert(e.width > 0.f);
+ if (!extrusions.priming) {
+ e.pos.rotate(ctxt.wipe_tower_angle);
+ e.pos.translate(ctxt.wipe_tower_pos);
+ }
+
+ lines.emplace_back(Point::new_scale(e_prev.pos.x, e_prev.pos.y), Point::new_scale(e.pos.x, e.pos.y));
+ widths.emplace_back(e.width);
+
+ e_prev = e;
+ }
+ _3DScene::thick_lines_to_verts(lines, widths, heights, lines.front().a == lines.back().b, extrusions.print_z,
+ *vols[ctxt.volume_idx(e.tool, 0)]);
+ }
+ }
+ }
+ for (size_t i = 0; i < vols.size(); ++i) {
+ GLVolume &vol = *vols[i];
+ if (vol.indexed_vertex_array.vertices_and_normals_interleaved.size() / 6 > ctxt.alloc_size_max()) {
+ // Store the vertex arrays and restart their containers,
+ vols[i] = new_volume(vol.color);
+ GLVolume &vol_new = *vols[i];
+ // Assign the large pre-allocated buffers to the new GLVolume.
+ vol_new.indexed_vertex_array = std::move(vol.indexed_vertex_array);
+ // Copy the content back to the old GLVolume.
+ vol.indexed_vertex_array = vol_new.indexed_vertex_array;
+ // Finalize a bounding box of the old GLVolume.
+ vol.bounding_box = vol.indexed_vertex_array.bounding_box();
+ // Clear the buffers, but keep them pre-allocated.
+ vol_new.indexed_vertex_array.clear();
+ // Just make sure that clear did not clear the reserved memory.
+ vol_new.indexed_vertex_array.reserve(ctxt.alloc_size_reserve());
+ }
+ }
+ for (GLVolume *vol : vols) {
+ vol->bounding_box = vol->indexed_vertex_array.bounding_box();
+ vol->indexed_vertex_array.shrink_to_fit();
+ }
+ });
+
+ BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - finalizing results";
+ // Remove empty volumes from the newly added volumes.
+ m_volumes.volumes.erase(
+ std::remove_if(m_volumes.volumes.begin() + volumes_cnt_initial, m_volumes.volumes.end(),
+ [](const GLVolume *volume) { return volume->empty(); }),
+ m_volumes.volumes.end());
+ for (size_t i = volumes_cnt_initial; i < m_volumes.volumes.size(); ++i)
+ m_volumes.volumes[i]->indexed_vertex_array.finalize_geometry(m_use_VBOs && m_initialized);
+
+ BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - end";
+}
+
+static inline int hex_digit_to_int(const char c)
+{
+ return
+ (c >= '0' && c <= '9') ? int(c - '0') :
+ (c >= 'A' && c <= 'F') ? int(c - 'A') + 10 :
+ (c >= 'a' && c <= 'f') ? int(c - 'a') + 10 : -1;
+}
+
+void GLCanvas3D::_load_gcode_extrusion_paths(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors)
+{
+ // helper functions to select data in dependence of the extrusion view type
+ struct Helper
+ {
+ static float path_filter(GCodePreviewData::Extrusion::EViewType type, const ExtrusionPath& path)
+ {
+ switch (type)
+ {
+ case GCodePreviewData::Extrusion::FeatureType:
+ return (float)path.role();
+ case GCodePreviewData::Extrusion::Height:
+ return path.height;
+ case GCodePreviewData::Extrusion::Width:
+ return path.width;
+ case GCodePreviewData::Extrusion::Feedrate:
+ return path.feedrate;
+ case GCodePreviewData::Extrusion::VolumetricRate:
+ return path.feedrate * (float)path.mm3_per_mm;
+ case GCodePreviewData::Extrusion::Tool:
+ return (float)path.extruder_id;
+ default:
+ return 0.0f;
+ }
+
+ return 0.0f;
+ }
+
+ static GCodePreviewData::Color path_color(const GCodePreviewData& data, const std::vector<float>& tool_colors, float value)
+ {
+ switch (data.extrusion.view_type)
+ {
+ case GCodePreviewData::Extrusion::FeatureType:
+ return data.get_extrusion_role_color((ExtrusionRole)(int)value);
+ case GCodePreviewData::Extrusion::Height:
+ return data.get_height_color(value);
+ case GCodePreviewData::Extrusion::Width:
+ return data.get_width_color(value);
+ case GCodePreviewData::Extrusion::Feedrate:
+ return data.get_feedrate_color(value);
+ case GCodePreviewData::Extrusion::VolumetricRate:
+ return data.get_volumetric_rate_color(value);
+ case GCodePreviewData::Extrusion::Tool:
+ {
+ GCodePreviewData::Color color;
+ ::memcpy((void*)color.rgba, (const void*)(tool_colors.data() + (unsigned int)value * 4), 4 * sizeof(float));
+ return color;
+ }
+ default:
+ return GCodePreviewData::Color::Dummy;
+ }
+
+ return GCodePreviewData::Color::Dummy;
+ }
+ };
+
+ // Helper structure for filters
+ struct Filter
+ {
+ float value;
+ ExtrusionRole role;
+ GLVolume* volume;
+
+ Filter(float value, ExtrusionRole role)
+ : value(value)
+ , role(role)
+ , volume(nullptr)
+ {
+ }
+
+ bool operator == (const Filter& other) const
+ {
+ if (value != other.value)
+ return false;
+
+ if (role != other.role)
+ return false;
+
+ return true;
+ }
+ };
+
+ typedef std::vector<Filter> FiltersList;
+ size_t initial_volumes_count = m_volumes.volumes.size();
+
+ // detects filters
+ FiltersList filters;
+ for (const GCodePreviewData::Extrusion::Layer& layer : preview_data.extrusion.layers)
+ {
+ for (const ExtrusionPath& path : layer.paths)
+ {
+ ExtrusionRole role = path.role();
+ float path_filter = Helper::path_filter(preview_data.extrusion.view_type, path);
+ if (std::find(filters.begin(), filters.end(), Filter(path_filter, role)) == filters.end())
+ filters.emplace_back(path_filter, role);
+ }
+ }
+
+ // nothing to render, return
+ if (filters.empty())
+ return;
+
+ // creates a new volume for each filter
+ for (Filter& filter : filters)
+ {
+ m_gcode_preview_volume_index.first_volumes.emplace_back(GCodePreviewVolumeIndex::Extrusion, (unsigned int)filter.role, (unsigned int)m_volumes.volumes.size());
+ GLVolume* volume = new GLVolume(Helper::path_color(preview_data, tool_colors, filter.value).rgba);
+ if (volume != nullptr)
+ {
+ filter.volume = volume;
+ volume->is_extrusion_path = true;
+ m_volumes.volumes.emplace_back(volume);
+ }
+ else
+ {
+ // an error occourred - restore to previous state and return
+ m_gcode_preview_volume_index.first_volumes.pop_back();
+ if (initial_volumes_count != m_volumes.volumes.size())
+ {
+ std::vector<GLVolume*>::iterator begin = m_volumes.volumes.begin() + initial_volumes_count;
+ std::vector<GLVolume*>::iterator end = m_volumes.volumes.end();
+ for (std::vector<GLVolume*>::iterator it = begin; it < end; ++it)
+ {
+ GLVolume* volume = *it;
+ delete volume;
+ }
+ m_volumes.volumes.erase(begin, end);
+ return;
+ }
+ }
+ }
+
+ // populates volumes
+ for (const GCodePreviewData::Extrusion::Layer& layer : preview_data.extrusion.layers)
+ {
+ for (const ExtrusionPath& path : layer.paths)
+ {
+ float path_filter = Helper::path_filter(preview_data.extrusion.view_type, path);
+ FiltersList::iterator filter = std::find(filters.begin(), filters.end(), Filter(path_filter, path.role()));
+ if (filter != filters.end())
+ {
+ filter->volume->print_zs.push_back(layer.z);
+ filter->volume->offsets.push_back(filter->volume->indexed_vertex_array.quad_indices.size());
+ filter->volume->offsets.push_back(filter->volume->indexed_vertex_array.triangle_indices.size());
+
+ _3DScene::extrusionentity_to_verts(path, layer.z, *filter->volume);
+ }
+ }
+ }
+
+ // finalize volumes and sends geometry to gpu
+ if (m_volumes.volumes.size() > initial_volumes_count)
+ {
+ for (size_t i = initial_volumes_count; i < m_volumes.volumes.size(); ++i)
+ {
+ GLVolume* volume = m_volumes.volumes[i];
+ volume->bounding_box = volume->indexed_vertex_array.bounding_box();
+ volume->indexed_vertex_array.finalize_geometry(m_use_VBOs && m_initialized);
+ }
+ }
+}
+
+void GLCanvas3D::_load_gcode_travel_paths(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors)
+{
+ size_t initial_volumes_count = m_volumes.volumes.size();
+ m_gcode_preview_volume_index.first_volumes.emplace_back(GCodePreviewVolumeIndex::Travel, 0, (unsigned int)initial_volumes_count);
+
+ bool res = true;
+ switch (preview_data.extrusion.view_type)
+ {
+ case GCodePreviewData::Extrusion::Feedrate:
+ {
+ res = _travel_paths_by_feedrate(preview_data);
+ break;
+ }
+ case GCodePreviewData::Extrusion::Tool:
+ {
+ res = _travel_paths_by_tool(preview_data, tool_colors);
+ break;
+ }
+ default:
+ {
+ res = _travel_paths_by_type(preview_data);
+ break;
+ }
+ }
+
+ if (!res)
+ {
+ // an error occourred - restore to previous state and return
+ if (initial_volumes_count != m_volumes.volumes.size())
+ {
+ std::vector<GLVolume*>::iterator begin = m_volumes.volumes.begin() + initial_volumes_count;
+ std::vector<GLVolume*>::iterator end = m_volumes.volumes.end();
+ for (std::vector<GLVolume*>::iterator it = begin; it < end; ++it)
+ {
+ GLVolume* volume = *it;
+ delete volume;
+ }
+ m_volumes.volumes.erase(begin, end);
+ }
+
+ return;
+ }
+
+ // finalize volumes and sends geometry to gpu
+ if (m_volumes.volumes.size() > initial_volumes_count)
+ {
+ for (size_t i = initial_volumes_count; i < m_volumes.volumes.size(); ++i)
+ {
+ GLVolume* volume = m_volumes.volumes[i];
+ volume->bounding_box = volume->indexed_vertex_array.bounding_box();
+ volume->indexed_vertex_array.finalize_geometry(m_use_VBOs && m_initialized);
+ }
+ }
+}
+
+bool GLCanvas3D::_travel_paths_by_type(const GCodePreviewData& preview_data)
+{
+ // Helper structure for types
+ struct Type
+ {
+ GCodePreviewData::Travel::EType value;
+ GLVolume* volume;
+
+ explicit Type(GCodePreviewData::Travel::EType value)
+ : value(value)
+ , volume(nullptr)
+ {
+ }
+
+ bool operator == (const Type& other) const
+ {
+ return value == other.value;
+ }
+ };
+
+ typedef std::vector<Type> TypesList;
+
+ // colors travels by travel type
+
+ // detects types
+ TypesList types;
+ for (const GCodePreviewData::Travel::Polyline& polyline : preview_data.travel.polylines)
+ {
+ if (std::find(types.begin(), types.end(), Type(polyline.type)) == types.end())
+ types.emplace_back(polyline.type);
+ }
+
+ // nothing to render, return
+ if (types.empty())
+ return true;
+
+ // creates a new volume for each type
+ for (Type& type : types)
+ {
+ GLVolume* volume = new GLVolume(preview_data.travel.type_colors[type.value].rgba);
+ if (volume == nullptr)
+ return false;
+ else
+ {
+ type.volume = volume;
+ m_volumes.volumes.emplace_back(volume);
+ }
+ }
+
+ // populates volumes
+ for (const GCodePreviewData::Travel::Polyline& polyline : preview_data.travel.polylines)
+ {
+ TypesList::iterator type = std::find(types.begin(), types.end(), Type(polyline.type));
+ if (type != types.end())
+ {
+ type->volume->print_zs.push_back(unscale<double>(polyline.polyline.bounding_box().min(2)));
+ type->volume->offsets.push_back(type->volume->indexed_vertex_array.quad_indices.size());
+ type->volume->offsets.push_back(type->volume->indexed_vertex_array.triangle_indices.size());
+
+ _3DScene::polyline3_to_verts(polyline.polyline, preview_data.travel.width, preview_data.travel.height, *type->volume);
+ }
+ }
+
+ return true;
+}
+
+bool GLCanvas3D::_travel_paths_by_feedrate(const GCodePreviewData& preview_data)
+{
+ // Helper structure for feedrate
+ struct Feedrate
+ {
+ float value;
+ GLVolume* volume;
+
+ explicit Feedrate(float value)
+ : value(value)
+ , volume(nullptr)
+ {
+ }
+
+ bool operator == (const Feedrate& other) const
+ {
+ return value == other.value;
+ }
+ };
+
+ typedef std::vector<Feedrate> FeedratesList;
+
+ // colors travels by feedrate
+
+ // detects feedrates
+ FeedratesList feedrates;
+ for (const GCodePreviewData::Travel::Polyline& polyline : preview_data.travel.polylines)
+ {
+ if (std::find(feedrates.begin(), feedrates.end(), Feedrate(polyline.feedrate)) == feedrates.end())
+ feedrates.emplace_back(polyline.feedrate);
+ }
+
+ // nothing to render, return
+ if (feedrates.empty())
+ return true;
+
+ // creates a new volume for each feedrate
+ for (Feedrate& feedrate : feedrates)
+ {
+ GLVolume* volume = new GLVolume(preview_data.get_feedrate_color(feedrate.value).rgba);
+ if (volume == nullptr)
+ return false;
+ else
+ {
+ feedrate.volume = volume;
+ m_volumes.volumes.emplace_back(volume);
+ }
+ }
+
+ // populates volumes
+ for (const GCodePreviewData::Travel::Polyline& polyline : preview_data.travel.polylines)
+ {
+ FeedratesList::iterator feedrate = std::find(feedrates.begin(), feedrates.end(), Feedrate(polyline.feedrate));
+ if (feedrate != feedrates.end())
+ {
+ feedrate->volume->print_zs.push_back(unscale<double>(polyline.polyline.bounding_box().min(2)));
+ feedrate->volume->offsets.push_back(feedrate->volume->indexed_vertex_array.quad_indices.size());
+ feedrate->volume->offsets.push_back(feedrate->volume->indexed_vertex_array.triangle_indices.size());
+
+ _3DScene::polyline3_to_verts(polyline.polyline, preview_data.travel.width, preview_data.travel.height, *feedrate->volume);
+ }
+ }
+
+ return true;
+}
+
+bool GLCanvas3D::_travel_paths_by_tool(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors)
+{
+ // Helper structure for tool
+ struct Tool
+ {
+ unsigned int value;
+ GLVolume* volume;
+
+ explicit Tool(unsigned int value)
+ : value(value)
+ , volume(nullptr)
+ {
+ }
+
+ bool operator == (const Tool& other) const
+ {
+ return value == other.value;
+ }
+ };
+
+ typedef std::vector<Tool> ToolsList;
+
+ // colors travels by tool
+
+ // detects tools
+ ToolsList tools;
+ for (const GCodePreviewData::Travel::Polyline& polyline : preview_data.travel.polylines)
+ {
+ if (std::find(tools.begin(), tools.end(), Tool(polyline.extruder_id)) == tools.end())
+ tools.emplace_back(polyline.extruder_id);
+ }
+
+ // nothing to render, return
+ if (tools.empty())
+ return true;
+
+ // creates a new volume for each tool
+ for (Tool& tool : tools)
+ {
+ GLVolume* volume = new GLVolume(tool_colors.data() + tool.value * 4);
+ if (volume == nullptr)
+ return false;
+ else
+ {
+ tool.volume = volume;
+ m_volumes.volumes.emplace_back(volume);
+ }
+ }
+
+ // populates volumes
+ for (const GCodePreviewData::Travel::Polyline& polyline : preview_data.travel.polylines)
+ {
+ ToolsList::iterator tool = std::find(tools.begin(), tools.end(), Tool(polyline.extruder_id));
+ if (tool != tools.end())
+ {
+ tool->volume->print_zs.push_back(unscale<double>(polyline.polyline.bounding_box().min(2)));
+ tool->volume->offsets.push_back(tool->volume->indexed_vertex_array.quad_indices.size());
+ tool->volume->offsets.push_back(tool->volume->indexed_vertex_array.triangle_indices.size());
+
+ _3DScene::polyline3_to_verts(polyline.polyline, preview_data.travel.width, preview_data.travel.height, *tool->volume);
+ }
+ }
+
+ return true;
+}
+
+void GLCanvas3D::_load_gcode_retractions(const GCodePreviewData& preview_data)
+{
+ m_gcode_preview_volume_index.first_volumes.emplace_back(GCodePreviewVolumeIndex::Retraction, 0, (unsigned int)m_volumes.volumes.size());
+
+ // nothing to render, return
+ if (preview_data.retraction.positions.empty())
+ return;
+
+ GLVolume* volume = new GLVolume(preview_data.retraction.color.rgba);
+ if (volume != nullptr)
+ {
+ m_volumes.volumes.emplace_back(volume);
+
+ GCodePreviewData::Retraction::PositionsList copy(preview_data.retraction.positions);
+ std::sort(copy.begin(), copy.end(), [](const GCodePreviewData::Retraction::Position& p1, const GCodePreviewData::Retraction::Position& p2){ return p1.position(2) < p2.position(2); });
+
+ for (const GCodePreviewData::Retraction::Position& position : copy)
+ {
+ volume->print_zs.push_back(unscale<double>(position.position(2)));
+ volume->offsets.push_back(volume->indexed_vertex_array.quad_indices.size());
+ volume->offsets.push_back(volume->indexed_vertex_array.triangle_indices.size());
+
+ _3DScene::point3_to_verts(position.position, position.width, position.height, *volume);
+ }
+
+ // finalize volumes and sends geometry to gpu
+ volume->bounding_box = volume->indexed_vertex_array.bounding_box();
+ volume->indexed_vertex_array.finalize_geometry(m_use_VBOs && m_initialized);
+ }
+}
+
+void GLCanvas3D::_load_gcode_unretractions(const GCodePreviewData& preview_data)
+{
+ m_gcode_preview_volume_index.first_volumes.emplace_back(GCodePreviewVolumeIndex::Unretraction, 0, (unsigned int)m_volumes.volumes.size());
+
+ // nothing to render, return
+ if (preview_data.unretraction.positions.empty())
+ return;
+
+ GLVolume* volume = new GLVolume(preview_data.unretraction.color.rgba);
+ if (volume != nullptr)
+ {
+ m_volumes.volumes.emplace_back(volume);
+
+ GCodePreviewData::Retraction::PositionsList copy(preview_data.unretraction.positions);
+ std::sort(copy.begin(), copy.end(), [](const GCodePreviewData::Retraction::Position& p1, const GCodePreviewData::Retraction::Position& p2){ return p1.position(2) < p2.position(2); });
+
+ for (const GCodePreviewData::Retraction::Position& position : copy)
+ {
+ volume->print_zs.push_back(unscale<double>(position.position(2)));
+ volume->offsets.push_back(volume->indexed_vertex_array.quad_indices.size());
+ volume->offsets.push_back(volume->indexed_vertex_array.triangle_indices.size());
+
+ _3DScene::point3_to_verts(position.position, position.width, position.height, *volume);
+ }
+
+ // finalize volumes and sends geometry to gpu
+ volume->bounding_box = volume->indexed_vertex_array.bounding_box();
+ volume->indexed_vertex_array.finalize_geometry(m_use_VBOs && m_initialized);
+ }
+}
+
+void GLCanvas3D::_load_shells()
+{
+ size_t initial_volumes_count = m_volumes.volumes.size();
+ m_gcode_preview_volume_index.first_volumes.emplace_back(GCodePreviewVolumeIndex::Shell, 0, (unsigned int)initial_volumes_count);
+
+ if (m_print->objects().empty())
+ // nothing to render, return
+ return;
+
+ // adds objects' volumes
+ unsigned int object_id = 0;
+ for (const PrintObject* obj : m_print->objects())
+ {
+ const ModelObject* model_obj = obj->model_object();
+
+ std::vector<int> instance_ids(model_obj->instances.size());
+ for (int i = 0; i < (int)model_obj->instances.size(); ++i)
+ {
+ instance_ids[i] = i;
+ }
+
+ m_volumes.load_object(model_obj, object_id, instance_ids, "object", "object", "object", m_use_VBOs && m_initialized);
+
+ ++object_id;
+ }
+
+ // adds wipe tower's volume
+ double max_z = m_print->objects()[0]->model_object()->get_model()->bounding_box().max(2);
+ const PrintConfig& config = m_print->config();
+ unsigned int extruders_count = config.nozzle_diameter.size();
+ if ((extruders_count > 1) && config.single_extruder_multi_material && config.wipe_tower && !config.complete_objects) {
+ float depth = m_print->get_wipe_tower_depth();
+ if (!m_print->is_step_done(psWipeTower))
+ depth = (900.f/config.wipe_tower_width) * (float)(extruders_count - 1) ;
+ m_volumes.load_wipe_tower_preview(1000, config.wipe_tower_x, config.wipe_tower_y, config.wipe_tower_width, depth, max_z, config.wipe_tower_rotation_angle,
+ m_use_VBOs && m_initialized, !m_print->is_step_done(psWipeTower), m_print->config().nozzle_diameter.values[0] * 1.25f * 4.5f);
+ }
+}
+
+void GLCanvas3D::_update_gcode_volumes_visibility(const GCodePreviewData& preview_data)
+{
+ unsigned int size = (unsigned int)m_gcode_preview_volume_index.first_volumes.size();
+ for (unsigned int i = 0; i < size; ++i)
+ {
+ std::vector<GLVolume*>::iterator begin = m_volumes.volumes.begin() + m_gcode_preview_volume_index.first_volumes[i].id;
+ std::vector<GLVolume*>::iterator end = (i + 1 < size) ? m_volumes.volumes.begin() + m_gcode_preview_volume_index.first_volumes[i + 1].id : m_volumes.volumes.end();
+
+ for (std::vector<GLVolume*>::iterator it = begin; it != end; ++it)
+ {
+ GLVolume* volume = *it;
+
+ switch (m_gcode_preview_volume_index.first_volumes[i].type)
+ {
+ case GCodePreviewVolumeIndex::Extrusion:
+ {
+ if ((ExtrusionRole)m_gcode_preview_volume_index.first_volumes[i].flag == erCustom)
+ volume->zoom_to_volumes = false;
+
+ volume->is_active = preview_data.extrusion.is_role_flag_set((ExtrusionRole)m_gcode_preview_volume_index.first_volumes[i].flag);
+ break;
+ }
+ case GCodePreviewVolumeIndex::Travel:
+ {
+ volume->is_active = preview_data.travel.is_visible;
+ volume->zoom_to_volumes = false;
+ break;
+ }
+ case GCodePreviewVolumeIndex::Retraction:
+ {
+ volume->is_active = preview_data.retraction.is_visible;
+ volume->zoom_to_volumes = false;
+ break;
+ }
+ case GCodePreviewVolumeIndex::Unretraction:
+ {
+ volume->is_active = preview_data.unretraction.is_visible;
+ volume->zoom_to_volumes = false;
+ break;
+ }
+ case GCodePreviewVolumeIndex::Shell:
+ {
+ volume->is_active = preview_data.shell.is_visible;
+ volume->color[3] = 0.25f;
+ volume->zoom_to_volumes = false;
+ break;
+ }
+ default:
+ {
+ volume->is_active = false;
+ volume->zoom_to_volumes = false;
+ break;
+ }
+ }
+ }
+ }
+}
+
+void GLCanvas3D::_update_toolpath_volumes_outside_state()
+{
+ // tolerance to avoid false detection at bed edges
+ static const double tolerance_x = 0.05;
+ static const double tolerance_y = 0.05;
+
+ BoundingBoxf3 print_volume;
+ if (m_config != nullptr)
+ {
+ const ConfigOptionPoints* opt = dynamic_cast<const ConfigOptionPoints*>(m_config->option("bed_shape"));
+ if (opt != nullptr)
+ {
+ BoundingBox bed_box_2D = get_extents(Polygon::new_scale(opt->values));
+ print_volume = BoundingBoxf3(Vec3d(unscale<double>(bed_box_2D.min(0)) - tolerance_x, unscale<double>(bed_box_2D.min(1)) - tolerance_y, 0.0), Vec3d(unscale<double>(bed_box_2D.max(0)) + tolerance_x, unscale<double>(bed_box_2D.max(1)) + tolerance_y, m_config->opt_float("max_print_height")));
+ // Allow the objects to protrude below the print bed
+ print_volume.min(2) = -1e10;
+ }
+ }
+
+ for (GLVolume* volume : m_volumes.volumes)
+ {
+ volume->is_outside = ((print_volume.radius() > 0.0) && volume->is_extrusion_path) ? !print_volume.contains(volume->bounding_box) : false;
+ }
+}
+
+void GLCanvas3D::_show_warning_texture_if_needed()
+{
+ if (_is_any_volume_outside())
+ {
+ enable_warning_texture(true);
+ _generate_warning_texture(L("Detected toolpath outside print volume"));
+ }
+ else
+ {
+ enable_warning_texture(false);
+ _reset_warning_texture();
+ }
+}
+
+void GLCanvas3D::_on_move(const std::vector<int>& volume_idxs)
+{
+ if (m_model == nullptr)
+ return;
+
+ std::set<std::string> done; // prevent moving instances twice
+ bool object_moved = false;
+ Vec3d wipe_tower_origin = Vec3d::Zero();
+ for (int volume_idx : volume_idxs)
+ {
+ GLVolume* volume = m_volumes.volumes[volume_idx];
+ int obj_idx = volume->object_idx();
+ int instance_idx = volume->instance_idx();
+
+ // prevent moving instances twice
+ char done_id[64];
+ ::sprintf(done_id, "%d_%d", obj_idx, instance_idx);
+ if (done.find(done_id) != done.end())
+ continue;
+
+ done.insert(done_id);
+
+ if (obj_idx < 1000)
+ {
+ // Move a regular object.
+ ModelObject* model_object = m_model->objects[obj_idx];
+ if (model_object != nullptr)
+ {
+#if ENABLE_MODELINSTANCE_3D_OFFSET
+ model_object->instances[instance_idx]->set_offset(volume->get_offset());
+#else
+ const Vec3d& offset = volume->get_offset();
+ model_object->instances[instance_idx]->offset = Vec2d(offset(0), offset(1));
+#endif // ENABLE_MODELINSTANCE_3D_OFFSET
+ model_object->invalidate_bounding_box();
+ update_position_values();
+ object_moved = true;
+ }
+ }
+ else if (obj_idx == 1000)
+ // Move a wipe tower proxy.
+ wipe_tower_origin = volume->get_offset();
+ }
+
+ if (object_moved)
+ m_on_instance_moved_callback.call();
+
+ if (wipe_tower_origin != Vec3d::Zero())
+ m_on_wipe_tower_moved_callback.call(wipe_tower_origin(0), wipe_tower_origin(1));
+}
+
+void GLCanvas3D::_on_select(int volume_idx, int object_idx)
+{
+ int vol_id = -1;
+ int obj_id = -1;
+
+ if ((volume_idx != -1) && (volume_idx < (int)m_volumes.volumes.size()))
+ {
+ if (m_select_by == "volume")
+ {
+ if (m_volumes.volumes[volume_idx]->object_idx() != object_idx)
+ {
+ set_select_by("object");
+ obj_id = m_volumes.volumes[volume_idx]->object_idx();
+ vol_id = -1;
+ }
+ else
+ {
+ obj_id = object_idx;
+ vol_id = m_volumes.volumes[volume_idx]->volume_idx();
+ }
+ }
+ else if (m_select_by == "object")
+ {
+ obj_id = m_volumes.volumes[volume_idx]->object_idx();
+ vol_id = -1;
+ }
+ }
+
+ m_on_select_object_callback.call(obj_id, vol_id);
+ Slic3r::GUI::select_current_volume(obj_id, vol_id);
+}
+
+std::vector<float> GLCanvas3D::_parse_colors(const std::vector<std::string>& colors)
+{
+ static const float INV_255 = 1.0f / 255.0f;
+
+ std::vector<float> output(colors.size() * 4, 1.0f);
+ for (size_t i = 0; i < colors.size(); ++i)
+ {
+ const std::string& color = colors[i];
+ const char* c = color.data() + 1;
+ if ((color.size() == 7) && (color.front() == '#'))
+ {
+ for (size_t j = 0; j < 3; ++j)
+ {
+ int digit1 = hex_digit_to_int(*c++);
+ int digit2 = hex_digit_to_int(*c++);
+ if ((digit1 == -1) || (digit2 == -1))
+ break;
+
+ output[i * 4 + j] = float(digit1 * 16 + digit2) * INV_255;
+ }
+ }
+ }
+ return output;
+}
+
+void GLCanvas3D::_generate_legend_texture(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors)
+{
+ if (!set_current())
+ return;
+
+ m_legend_texture.generate(preview_data, tool_colors);
+}
+
+void GLCanvas3D::_generate_warning_texture(const std::string& msg)
+{
+ if (!set_current())
+ return;
+
+ m_warning_texture.generate(msg);
+}
+
+void GLCanvas3D::_reset_warning_texture()
+{
+ if (!set_current())
+ return;
+
+ m_warning_texture.reset();
+}
+
+bool GLCanvas3D::_is_any_volume_outside() const
+{
+ for (const GLVolume* volume : m_volumes.volumes)
+ {
+ if ((volume != nullptr) && volume->is_outside)
+ return true;
+ }
+
+ return false;
+}
+
+void GLCanvas3D::_resize_toolbar() const
+{
+ Size cnv_size = get_canvas_size();
+ float zoom = get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+
+ switch (m_toolbar.get_layout_type())
+ {
+ default:
+ case GLToolbar::Layout::Horizontal:
+ {
+ // centers the toolbar on the top edge of the 3d scene
+ unsigned int toolbar_width = m_toolbar.get_width();
+ float top = (0.5f * (float)cnv_size.get_height() - 2.0f) * inv_zoom;
+ float left = -0.5f * (float)toolbar_width * inv_zoom;
+ m_toolbar.set_position(top, left);
+ break;
+ }
+ case GLToolbar::Layout::Vertical:
+ {
+ // centers the toolbar on the right edge of the 3d scene
+ unsigned int toolbar_width = m_toolbar.get_width();
+ unsigned int toolbar_height = m_toolbar.get_height();
+ float top = 0.5f * (float)toolbar_height * inv_zoom;
+ float left = (0.5f * (float)cnv_size.get_width() - toolbar_width - 2.0f) * inv_zoom;
+ m_toolbar.set_position(top, left);
+ break;
+ }
+ }
+}
+
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp
new file mode 100644
index 000000000..528f73fc1
--- /dev/null
+++ b/src/slic3r/GUI/GLCanvas3D.hpp
@@ -0,0 +1,765 @@
+#ifndef slic3r_GLCanvas3D_hpp_
+#define slic3r_GLCanvas3D_hpp_
+
+#include "../../slic3r/GUI/3DScene.hpp"
+#include "../../slic3r/GUI/GLToolbar.hpp"
+
+class wxTimer;
+class wxSizeEvent;
+class wxIdleEvent;
+class wxKeyEvent;
+class wxMouseEvent;
+class wxTimerEvent;
+class wxPaintEvent;
+
+namespace Slic3r {
+
+class GLShader;
+class ExPolygon;
+
+namespace GUI {
+
+class GLGizmoBase;
+
+class GeometryBuffer
+{
+ std::vector<float> m_vertices;
+ std::vector<float> m_tex_coords;
+
+public:
+ bool set_from_triangles(const Polygons& triangles, float z, bool generate_tex_coords);
+ bool set_from_lines(const Lines& lines, float z);
+
+ const float* get_vertices() const;
+ const float* get_tex_coords() const;
+
+ unsigned int get_vertices_count() const;
+};
+
+class Size
+{
+ int m_width;
+ int m_height;
+
+public:
+ Size();
+ Size(int width, int height);
+
+ int get_width() const;
+ void set_width(int width);
+
+ int get_height() const;
+ void set_height(int height);
+};
+
+class Rect
+{
+ float m_left;
+ float m_top;
+ float m_right;
+ float m_bottom;
+
+public:
+ Rect();
+ Rect(float left, float top, float right, float bottom);
+
+ float get_left() const;
+ void set_left(float left);
+
+ float get_top() const;
+ void set_top(float top);
+
+ float get_right() const;
+ void set_right(float right);
+
+ float get_bottom() const;
+ void set_bottom(float bottom);
+};
+
+class GLCanvas3D
+{
+ struct GCodePreviewVolumeIndex
+ {
+ enum EType
+ {
+ Extrusion,
+ Travel,
+ Retraction,
+ Unretraction,
+ Shell,
+ Num_Geometry_Types
+ };
+
+ struct FirstVolume
+ {
+ EType type;
+ unsigned int flag;
+ // Index of the first volume in a GLVolumeCollection.
+ unsigned int id;
+
+ FirstVolume(EType type, unsigned int flag, unsigned int id) : type(type), flag(flag), id(id) {}
+ };
+
+ std::vector<FirstVolume> first_volumes;
+
+ void reset() { first_volumes.clear(); }
+ };
+
+ struct Camera
+ {
+ enum EType : unsigned char
+ {
+ Unknown,
+// Perspective,
+ Ortho,
+ Num_types
+ };
+
+ EType type;
+ float zoom;
+ float phi;
+// float distance;
+ Vec3d target;
+
+ private:
+ float m_theta;
+
+ public:
+ Camera();
+
+ std::string get_type_as_string() const;
+
+ float get_theta() const;
+ void set_theta(float theta);
+ };
+
+ class Bed
+ {
+ public:
+ enum EType : unsigned char
+ {
+ MK2,
+ MK3,
+ Custom,
+ Num_Types
+ };
+
+ private:
+ EType m_type;
+ Pointfs m_shape;
+ BoundingBoxf3 m_bounding_box;
+ Polygon m_polygon;
+ GeometryBuffer m_triangles;
+ GeometryBuffer m_gridlines;
+ mutable GLTexture m_top_texture;
+ mutable GLTexture m_bottom_texture;
+
+ public:
+ Bed();
+
+ bool is_prusa() const;
+ bool is_custom() const;
+
+ const Pointfs& get_shape() const;
+ // Return true if the bed shape changed, so the calee will update the UI.
+ bool set_shape(const Pointfs& shape);
+
+ const BoundingBoxf3& get_bounding_box() const;
+ bool contains(const Point& point) const;
+ Point point_projection(const Point& point) const;
+
+ void render(float theta) const;
+
+ private:
+ void _calc_bounding_box();
+ void _calc_triangles(const ExPolygon& poly);
+ void _calc_gridlines(const ExPolygon& poly, const BoundingBox& bed_bbox);
+ EType _detect_type() const;
+ void _render_mk2(float theta) const;
+ void _render_mk3(float theta) const;
+ void _render_prusa(float theta) const;
+ void _render_custom() const;
+ static bool _are_equal(const Pointfs& bed_1, const Pointfs& bed_2);
+ };
+
+ struct Axes
+ {
+ Vec3d origin;
+ float length;
+
+ Axes();
+
+ void render(bool depth_test) const;
+ };
+
+ class CuttingPlane
+ {
+ float m_z;
+ GeometryBuffer m_lines;
+
+ public:
+ CuttingPlane();
+
+ bool set(float z, const ExPolygons& polygons);
+
+ void render(const BoundingBoxf3& bb) const;
+
+ private:
+ void _render_plane(const BoundingBoxf3& bb) const;
+ void _render_contour() const;
+ };
+
+ class Shader
+ {
+ GLShader* m_shader;
+
+ public:
+ Shader();
+ ~Shader();
+
+ bool init(const std::string& vertex_shader_filename, const std::string& fragment_shader_filename);
+
+ bool is_initialized() const;
+
+ bool start_using() const;
+ void stop_using() const;
+
+ void set_uniform(const std::string& name, float value) const;
+ void set_uniform(const std::string& name, const float* matrix) const;
+
+ const GLShader* get_shader() const;
+
+ private:
+ void _reset();
+ };
+
+ class LayersEditing
+ {
+ public:
+ enum EState : unsigned char
+ {
+ Unknown,
+ Editing,
+ Completed,
+ Num_States
+ };
+
+ private:
+ bool m_use_legacy_opengl;
+ bool m_enabled;
+ Shader m_shader;
+ unsigned int m_z_texture_id;
+ mutable GLTexture m_tooltip_texture;
+ mutable GLTexture m_reset_texture;
+
+ public:
+ EState state;
+ float band_width;
+ float strength;
+ int last_object_id;
+ float last_z;
+ unsigned int last_action;
+
+ LayersEditing();
+ ~LayersEditing();
+
+ bool init(const std::string& vertex_shader_filename, const std::string& fragment_shader_filename);
+
+ bool is_allowed() const;
+ void set_use_legacy_opengl(bool use_legacy_opengl);
+
+ bool is_enabled() const;
+ void set_enabled(bool enabled);
+
+ unsigned int get_z_texture_id() const;
+
+ void render(const GLCanvas3D& canvas, const PrintObject& print_object, const GLVolume& volume) const;
+
+ int get_shader_program_id() const;
+
+ static float get_cursor_z_relative(const GLCanvas3D& canvas);
+ static bool bar_rect_contains(const GLCanvas3D& canvas, float x, float y);
+ static bool reset_rect_contains(const GLCanvas3D& canvas, float x, float y);
+ static Rect get_bar_rect_screen(const GLCanvas3D& canvas);
+ static Rect get_reset_rect_screen(const GLCanvas3D& canvas);
+ static Rect get_bar_rect_viewport(const GLCanvas3D& canvas);
+ static Rect get_reset_rect_viewport(const GLCanvas3D& canvas);
+
+ private:
+ bool _is_initialized() const;
+ void _render_tooltip_texture(const GLCanvas3D& canvas, const Rect& bar_rect, const Rect& reset_rect) const;
+ void _render_reset_texture(const Rect& reset_rect) const;
+ void _render_active_object_annotations(const GLCanvas3D& canvas, const GLVolume& volume, const PrintObject& print_object, const Rect& bar_rect) const;
+ void _render_profile(const PrintObject& print_object, const Rect& bar_rect) const;
+ };
+
+ struct Mouse
+ {
+ struct Drag
+ {
+ static const Point Invalid_2D_Point;
+ static const Vec3d Invalid_3D_Point;
+
+ Point start_position_2D;
+ Vec3d start_position_3D;
+ Vec3d volume_center_offset;
+
+ bool move_with_shift;
+ int move_volume_idx;
+ int gizmo_volume_idx;
+
+ public:
+ Drag();
+ };
+
+ bool dragging;
+ Vec2d position;
+ Drag drag;
+
+ Mouse();
+
+ void set_start_position_2D_as_invalid();
+ void set_start_position_3D_as_invalid();
+
+ bool is_start_position_2D_defined() const;
+ bool is_start_position_3D_defined() const;
+ };
+
+ class Gizmos
+ {
+ static const float OverlayTexturesScale;
+ static const float OverlayOffsetX;
+ static const float OverlayGapY;
+
+ public:
+ enum EType : unsigned char
+ {
+ Undefined,
+ Move,
+ Scale,
+ Rotate,
+ Flatten,
+ Num_Types
+ };
+
+ private:
+ bool m_enabled;
+ typedef std::map<EType, GLGizmoBase*> GizmosMap;
+ GizmosMap m_gizmos;
+ EType m_current;
+
+ public:
+ Gizmos();
+ ~Gizmos();
+
+ bool init(GLCanvas3D& parent);
+
+ bool is_enabled() const;
+ void set_enabled(bool enable);
+
+ void update_hover_state(const GLCanvas3D& canvas, const Vec2d& mouse_pos);
+ void update_on_off_state(const GLCanvas3D& canvas, const Vec2d& mouse_pos);
+ void reset_all_states();
+
+ void set_hover_id(int id);
+
+ bool overlay_contains_mouse(const GLCanvas3D& canvas, const Vec2d& mouse_pos) const;
+ bool grabber_contains_mouse() const;
+ void update(const Linef3& mouse_ray);
+
+ EType get_current_type() const;
+
+ bool is_running() const;
+
+ bool is_dragging() const;
+ void start_dragging(const BoundingBoxf3& box);
+ void stop_dragging();
+
+ Vec3d get_position() const;
+ void set_position(const Vec3d& position);
+
+ float get_scale() const;
+ void set_scale(float scale);
+
+ float get_angle_z() const;
+ void set_angle_z(float angle_z);
+
+ void set_flattening_data(const ModelObject* model_object);
+ Vec3d get_flattening_normal() const;
+
+ void render_current_gizmo(const BoundingBoxf3& box) const;
+
+ void render_current_gizmo_for_picking_pass(const BoundingBoxf3& box) const;
+ void render_overlay(const GLCanvas3D& canvas) const;
+
+ private:
+ void _reset();
+
+ void _render_overlay(const GLCanvas3D& canvas) const;
+ void _render_current_gizmo(const BoundingBoxf3& box) const;
+
+ float _get_total_overlay_height() const;
+ GLGizmoBase* _get_current() const;
+ };
+
+ class WarningTexture : public GUI::GLTexture
+ {
+ static const unsigned char Background_Color[3];
+ static const unsigned char Opacity;
+
+ int m_original_width;
+ int m_original_height;
+
+ public:
+ WarningTexture();
+
+ bool generate(const std::string& msg);
+
+ void render(const GLCanvas3D& canvas) const;
+ };
+
+ class LegendTexture : public GUI::GLTexture
+ {
+ static const int Px_Title_Offset = 5;
+ static const int Px_Text_Offset = 5;
+ static const int Px_Square = 20;
+ static const int Px_Square_Contour = 1;
+ static const int Px_Border = Px_Square / 2;
+ static const unsigned char Squares_Border_Color[3];
+ static const unsigned char Background_Color[3];
+ static const unsigned char Opacity;
+
+ int m_original_width;
+ int m_original_height;
+
+ public:
+ LegendTexture();
+
+ bool generate(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors);
+
+ void render(const GLCanvas3D& canvas) const;
+ };
+
+ wxGLCanvas* m_canvas;
+ wxGLContext* m_context;
+ LegendTexture m_legend_texture;
+ WarningTexture m_warning_texture;
+ wxTimer* m_timer;
+ Camera m_camera;
+ Bed m_bed;
+ Axes m_axes;
+ CuttingPlane m_cutting_plane;
+ LayersEditing m_layers_editing;
+ Shader m_shader;
+ Mouse m_mouse;
+ mutable Gizmos m_gizmos;
+ mutable GLToolbar m_toolbar;
+
+ mutable GLVolumeCollection m_volumes;
+ DynamicPrintConfig* m_config;
+ Print* m_print;
+ Model* m_model;
+
+ bool m_dirty;
+ bool m_initialized;
+ bool m_use_VBOs;
+ bool m_force_zoom_to_bed_enabled;
+ bool m_apply_zoom_to_volumes_filter;
+ mutable int m_hover_volume_id;
+ bool m_toolbar_action_running;
+ bool m_warning_texture_enabled;
+ bool m_legend_texture_enabled;
+ bool m_picking_enabled;
+ bool m_moving_enabled;
+ bool m_shader_enabled;
+ bool m_dynamic_background_enabled;
+ bool m_multisample_allowed;
+
+ std::string m_color_by;
+ std::string m_select_by;
+ std::string m_drag_by;
+
+ bool m_reload_delayed;
+ std::vector<std::vector<int>> m_objects_volumes_idxs;
+ std::vector<int> m_objects_selections;
+
+ GCodePreviewVolumeIndex m_gcode_preview_volume_index;
+
+ PerlCallback m_on_viewport_changed_callback;
+ PerlCallback m_on_double_click_callback;
+ PerlCallback m_on_right_click_callback;
+ PerlCallback m_on_select_object_callback;
+ PerlCallback m_on_model_update_callback;
+ PerlCallback m_on_remove_object_callback;
+ PerlCallback m_on_arrange_callback;
+ PerlCallback m_on_rotate_object_left_callback;
+ PerlCallback m_on_rotate_object_right_callback;
+ PerlCallback m_on_scale_object_uniformly_callback;
+ PerlCallback m_on_increase_objects_callback;
+ PerlCallback m_on_decrease_objects_callback;
+ PerlCallback m_on_instance_moved_callback;
+ PerlCallback m_on_wipe_tower_moved_callback;
+ PerlCallback m_on_enable_action_buttons_callback;
+ PerlCallback m_on_gizmo_scale_uniformly_callback;
+ PerlCallback m_on_gizmo_rotate_callback;
+ PerlCallback m_on_gizmo_flatten_callback;
+ PerlCallback m_on_update_geometry_info_callback;
+
+ PerlCallback m_action_add_callback;
+ PerlCallback m_action_delete_callback;
+ PerlCallback m_action_deleteall_callback;
+ PerlCallback m_action_arrange_callback;
+ PerlCallback m_action_more_callback;
+ PerlCallback m_action_fewer_callback;
+ PerlCallback m_action_split_callback;
+ PerlCallback m_action_cut_callback;
+ PerlCallback m_action_settings_callback;
+ PerlCallback m_action_layersediting_callback;
+ PerlCallback m_action_selectbyparts_callback;
+
+public:
+ GLCanvas3D(wxGLCanvas* canvas);
+ ~GLCanvas3D();
+
+ bool init(bool useVBOs, bool use_legacy_opengl);
+
+ bool set_current();
+
+ void set_as_dirty();
+
+ unsigned int get_volumes_count() const;
+ void reset_volumes();
+ void deselect_volumes();
+ void select_volume(unsigned int id);
+ void update_volumes_selection(const std::vector<int>& selections);
+ int check_volumes_outside_state(const DynamicPrintConfig* config) const;
+ bool move_volume_up(unsigned int id);
+ bool move_volume_down(unsigned int id);
+
+ void set_objects_selections(const std::vector<int>& selections);
+
+ void set_config(DynamicPrintConfig* config);
+ void set_print(Print* print);
+ void set_model(Model* model);
+
+ // Set the bed shape to a single closed 2D polygon(array of two element arrays),
+ // triangulate the bed and store the triangles into m_bed.m_triangles,
+ // fills the m_bed.m_grid_lines and sets m_bed.m_origin.
+ // Sets m_bed.m_polygon to limit the object placement.
+ void set_bed_shape(const Pointfs& shape);
+ // Used by ObjectCutDialog and ObjectPartsPanel to generate a rectangular ground plane to support the scene objects.
+ void set_auto_bed_shape();
+
+ void set_axes_length(float length);
+
+ void set_cutting_plane(float z, const ExPolygons& polygons);
+
+ void set_color_by(const std::string& value);
+ void set_select_by(const std::string& value);
+ void set_drag_by(const std::string& value);
+
+ const std::string& get_select_by() const;
+ const std::string& get_drag_by() const;
+
+ float get_camera_zoom() const;
+
+ BoundingBoxf3 volumes_bounding_box() const;
+
+ bool is_layers_editing_enabled() const;
+ bool is_layers_editing_allowed() const;
+ bool is_shader_enabled() const;
+
+ bool is_reload_delayed() const;
+
+ void enable_layers_editing(bool enable);
+ void enable_warning_texture(bool enable);
+ void enable_legend_texture(bool enable);
+ void enable_picking(bool enable);
+ void enable_moving(bool enable);
+ void enable_gizmos(bool enable);
+ void enable_toolbar(bool enable);
+ void enable_shader(bool enable);
+ void enable_force_zoom_to_bed(bool enable);
+ void enable_dynamic_background(bool enable);
+ void allow_multisample(bool allow);
+
+ void enable_toolbar_item(const std::string& name, bool enable);
+ bool is_toolbar_item_pressed(const std::string& name) const;
+
+ void zoom_to_bed();
+ void zoom_to_volumes();
+ void select_view(const std::string& direction);
+ void set_viewport_from_scene(const GLCanvas3D& other);
+
+ void update_volumes_colors_by_extruder();
+ void update_gizmos_data();
+
+ void render();
+
+ std::vector<double> get_current_print_zs(bool active_only) const;
+ void set_toolpaths_range(double low, double high);
+
+ std::vector<int> load_object(const ModelObject& model_object, int obj_idx, std::vector<int> instance_idxs);
+ std::vector<int> load_object(const Model& model, int obj_idx);
+
+ int get_first_volume_id(int obj_idx) const;
+ int get_in_object_volume_id(int scene_vol_idx) const;
+
+ void reload_scene(bool force);
+
+ void load_gcode_preview(const GCodePreviewData& preview_data, const std::vector<std::string>& str_tool_colors);
+ void load_preview(const std::vector<std::string>& str_tool_colors);
+
+ void register_on_viewport_changed_callback(void* callback);
+ void register_on_double_click_callback(void* callback);
+ void register_on_right_click_callback(void* callback);
+ void register_on_select_object_callback(void* callback);
+ void register_on_model_update_callback(void* callback);
+ void register_on_remove_object_callback(void* callback);
+ void register_on_arrange_callback(void* callback);
+ void register_on_rotate_object_left_callback(void* callback);
+ void register_on_rotate_object_right_callback(void* callback);
+ void register_on_scale_object_uniformly_callback(void* callback);
+ void register_on_increase_objects_callback(void* callback);
+ void register_on_decrease_objects_callback(void* callback);
+ void register_on_instance_moved_callback(void* callback);
+ void register_on_wipe_tower_moved_callback(void* callback);
+ void register_on_enable_action_buttons_callback(void* callback);
+ void register_on_gizmo_scale_uniformly_callback(void* callback);
+ void register_on_gizmo_rotate_callback(void* callback);
+ void register_on_gizmo_flatten_callback(void* callback);
+ void register_on_update_geometry_info_callback(void* callback);
+
+ void register_action_add_callback(void* callback);
+ void register_action_delete_callback(void* callback);
+ void register_action_deleteall_callback(void* callback);
+ void register_action_arrange_callback(void* callback);
+ void register_action_more_callback(void* callback);
+ void register_action_fewer_callback(void* callback);
+ void register_action_split_callback(void* callback);
+ void register_action_cut_callback(void* callback);
+ void register_action_settings_callback(void* callback);
+ void register_action_layersediting_callback(void* callback);
+ void register_action_selectbyparts_callback(void* callback);
+
+ void bind_event_handlers();
+ void unbind_event_handlers();
+
+ void on_size(wxSizeEvent& evt);
+ void on_idle(wxIdleEvent& evt);
+ void on_char(wxKeyEvent& evt);
+ void on_mouse_wheel(wxMouseEvent& evt);
+ void on_timer(wxTimerEvent& evt);
+ void on_mouse(wxMouseEvent& evt);
+ void on_paint(wxPaintEvent& evt);
+ void on_key_down(wxKeyEvent& evt);
+
+ Size get_canvas_size() const;
+ Point get_local_mouse_position() const;
+
+ void reset_legend_texture();
+
+ void set_tooltip(const std::string& tooltip);
+
+private:
+ bool _is_shown_on_screen() const;
+ void _force_zoom_to_bed();
+
+ bool _init_toolbar();
+
+ void _resize(unsigned int w, unsigned int h);
+
+ BoundingBoxf3 _max_bounding_box() const;
+ BoundingBoxf3 _selected_volumes_bounding_box() const;
+
+ void _zoom_to_bounding_box(const BoundingBoxf3& bbox);
+ float _get_zoom_to_bounding_box_factor(const BoundingBoxf3& bbox) const;
+
+ void _deregister_callbacks();
+
+ void _mark_volumes_for_layer_height() const;
+ void _refresh_if_shown_on_screen();
+
+ void _camera_tranform() const;
+ void _picking_pass() const;
+ void _render_background() const;
+ void _render_bed(float theta) const;
+ void _render_axes(bool depth_test) const;
+ void _render_objects() const;
+ void _render_cutting_plane() const;
+ void _render_warning_texture() const;
+ void _render_legend_texture() const;
+ void _render_layer_editing_overlay() const;
+ void _render_volumes(bool fake_colors) const;
+ void _render_current_gizmo() const;
+ void _render_gizmos_overlay() const;
+ void _render_toolbar() const;
+
+ float _get_layers_editing_cursor_z_relative() const;
+ void _perform_layer_editing_action(wxMouseEvent* evt = nullptr);
+
+ // Convert the screen space coordinate to an object space coordinate.
+ // If the Z screen space coordinate is not provided, a depth buffer value is substituted.
+ Vec3d _mouse_to_3d(const Point& mouse_pos, float* z = nullptr);
+
+ // Convert the screen space coordinate to world coordinate on the bed.
+ Vec3d _mouse_to_bed_3d(const Point& mouse_pos);
+
+ // Returns the view ray line, in world coordinate, at the given mouse position.
+ Linef3 mouse_ray(const Point& mouse_pos);
+
+ void _start_timer();
+ void _stop_timer();
+
+ int _get_first_selected_object_id() const;
+ int _get_first_selected_volume_id(int object_id) const;
+
+ // Create 3D thick extrusion lines for a skirt and brim.
+ // Adds a new Slic3r::GUI::3DScene::Volume to volumes.
+ void _load_print_toolpaths();
+ // Create 3D thick extrusion lines for object forming extrusions.
+ // Adds a new Slic3r::GUI::3DScene::Volume to $self->volumes,
+ // one for perimeters, one for infill and one for supports.
+ void _load_print_object_toolpaths(const PrintObject& print_object, const std::vector<std::string>& str_tool_colors);
+ // Create 3D thick extrusion lines for wipe tower extrusions
+ void _load_wipe_tower_toolpaths(const std::vector<std::string>& str_tool_colors);
+
+ // generates gcode extrusion paths geometry
+ void _load_gcode_extrusion_paths(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors);
+ // generates gcode travel paths geometry
+ void _load_gcode_travel_paths(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors);
+ bool _travel_paths_by_type(const GCodePreviewData& preview_data);
+ bool _travel_paths_by_feedrate(const GCodePreviewData& preview_data);
+ bool _travel_paths_by_tool(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors);
+ // generates gcode retractions geometry
+ void _load_gcode_retractions(const GCodePreviewData& preview_data);
+ // generates gcode unretractions geometry
+ void _load_gcode_unretractions(const GCodePreviewData& preview_data);
+ // generates objects and wipe tower geometry
+ void _load_shells();
+ // sets gcode geometry visibility according to user selection
+ void _update_gcode_volumes_visibility(const GCodePreviewData& preview_data);
+ void _update_toolpath_volumes_outside_state();
+ void _show_warning_texture_if_needed();
+
+ void _on_move(const std::vector<int>& volume_idxs);
+ void _on_select(int volume_idx, int object_idx);
+
+ // generates the legend texture in dependence of the current shown view type
+ void _generate_legend_texture(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors);
+
+ // generates a warning texture containing the given message
+ void _generate_warning_texture(const std::string& msg);
+ void _reset_warning_texture();
+
+ bool _is_any_volume_outside() const;
+
+ void _resize_toolbar() const;
+
+ static std::vector<float> _parse_colors(const std::vector<std::string>& colors);
+};
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif // slic3r_GLCanvas3D_hpp_
diff --git a/src/slic3r/GUI/GLCanvas3DManager.cpp b/src/slic3r/GUI/GLCanvas3DManager.cpp
new file mode 100644
index 000000000..495f49425
--- /dev/null
+++ b/src/slic3r/GUI/GLCanvas3DManager.cpp
@@ -0,0 +1,819 @@
+#include "GLCanvas3DManager.hpp"
+#include "../../slic3r/GUI/GUI.hpp"
+#include "../../slic3r/GUI/AppConfig.hpp"
+#include "../../slic3r/GUI/GLCanvas3D.hpp"
+
+#include <GL/glew.h>
+
+#include <boost/algorithm/string/split.hpp>
+#include <boost/algorithm/string/classification.hpp>
+
+#include <wx/glcanvas.h>
+#include <wx/timer.h>
+
+#include <vector>
+#include <string>
+#include <iostream>
+
+namespace Slic3r {
+namespace GUI {
+
+GLCanvas3DManager::GLInfo::GLInfo()
+ : version("")
+ , glsl_version("")
+ , vendor("")
+ , renderer("")
+{
+}
+
+void GLCanvas3DManager::GLInfo::detect()
+{
+ const char* data = (const char*)::glGetString(GL_VERSION);
+ if (data != nullptr)
+ version = data;
+
+ data = (const char*)::glGetString(GL_SHADING_LANGUAGE_VERSION);
+ if (data != nullptr)
+ glsl_version = data;
+
+ data = (const char*)::glGetString(GL_VENDOR);
+ if (data != nullptr)
+ vendor = data;
+
+ data = (const char*)::glGetString(GL_RENDERER);
+ if (data != nullptr)
+ renderer = data;
+}
+
+bool GLCanvas3DManager::GLInfo::is_version_greater_or_equal_to(unsigned int major, unsigned int minor) const
+{
+ std::vector<std::string> tokens;
+ boost::split(tokens, version, boost::is_any_of(" "), boost::token_compress_on);
+
+ if (tokens.empty())
+ return false;
+
+ std::vector<std::string> numbers;
+ boost::split(numbers, tokens[0], boost::is_any_of("."), boost::token_compress_on);
+
+ unsigned int gl_major = 0;
+ unsigned int gl_minor = 0;
+
+ if (numbers.size() > 0)
+ gl_major = ::atoi(numbers[0].c_str());
+
+ if (numbers.size() > 1)
+ gl_minor = ::atoi(numbers[1].c_str());
+
+ if (gl_major < major)
+ return false;
+ else if (gl_major > major)
+ return true;
+ else
+ return gl_minor >= minor;
+}
+
+std::string GLCanvas3DManager::GLInfo::to_string(bool format_as_html, bool extensions) const
+{
+ std::stringstream out;
+
+ std::string h2_start = format_as_html ? "<b>" : "";
+ std::string h2_end = format_as_html ? "</b>" : "";
+ std::string b_start = format_as_html ? "<b>" : "";
+ std::string b_end = format_as_html ? "</b>" : "";
+ std::string line_end = format_as_html ? "<br>" : "\n";
+
+ out << h2_start << "OpenGL installation" << h2_end << line_end;
+ out << b_start << "GL version: " << b_end << (version.empty() ? "N/A" : version) << line_end;
+ out << b_start << "Vendor: " << b_end << (vendor.empty() ? "N/A" : vendor) << line_end;
+ out << b_start << "Renderer: " << b_end << (renderer.empty() ? "N/A" : renderer) << line_end;
+ out << b_start << "GLSL version: " << b_end << (glsl_version.empty() ? "N/A" : glsl_version) << line_end;
+
+ if (extensions)
+ {
+ std::vector<std::string> extensions_list;
+ std::string extensions_str = (const char*)::glGetString(GL_EXTENSIONS);
+ boost::split(extensions_list, extensions_str, boost::is_any_of(" "), boost::token_compress_off);
+
+ if (!extensions_list.empty())
+ {
+ out << h2_start << "Installed extensions:" << h2_end << line_end;
+
+ std::sort(extensions_list.begin(), extensions_list.end());
+ for (const std::string& ext : extensions_list)
+ {
+ out << ext << line_end;
+ }
+ }
+ }
+
+ return out.str();
+}
+
+GLCanvas3DManager::GLCanvas3DManager()
+ : m_current(nullptr)
+ , m_gl_initialized(false)
+ , m_use_legacy_opengl(false)
+ , m_use_VBOs(false)
+{
+}
+
+bool GLCanvas3DManager::add(wxGLCanvas* canvas)
+{
+ if (canvas == nullptr)
+ return false;
+
+ if (_get_canvas(canvas) != m_canvases.end())
+ return false;
+
+ GLCanvas3D* canvas3D = new GLCanvas3D(canvas);
+ if (canvas3D == nullptr)
+ return false;
+
+ canvas3D->bind_event_handlers();
+ m_canvases.insert(CanvasesMap::value_type(canvas, canvas3D));
+
+ return true;
+}
+
+bool GLCanvas3DManager::remove(wxGLCanvas* canvas)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it == m_canvases.end())
+ return false;
+
+ it->second->unbind_event_handlers();
+ delete it->second;
+ m_canvases.erase(it);
+
+ return true;
+}
+
+void GLCanvas3DManager::remove_all()
+{
+ for (CanvasesMap::value_type& item : m_canvases)
+ {
+ item.second->unbind_event_handlers();
+ delete item.second;
+ }
+ m_canvases.clear();
+}
+
+unsigned int GLCanvas3DManager::count() const
+{
+ return (unsigned int)m_canvases.size();
+}
+
+void GLCanvas3DManager::init_gl()
+{
+ if (!m_gl_initialized)
+ {
+ glewInit();
+ m_gl_info.detect();
+ const AppConfig* config = GUI::get_app_config();
+ m_use_legacy_opengl = (config == nullptr) || (config->get("use_legacy_opengl") == "1");
+ m_use_VBOs = !m_use_legacy_opengl && m_gl_info.is_version_greater_or_equal_to(2, 0);
+ m_gl_initialized = true;
+ }
+}
+
+std::string GLCanvas3DManager::get_gl_info(bool format_as_html, bool extensions) const
+{
+ return m_gl_info.to_string(format_as_html, extensions);
+}
+
+bool GLCanvas3DManager::use_VBOs() const
+{
+ return m_use_VBOs;
+}
+
+bool GLCanvas3DManager::init(wxGLCanvas* canvas)
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ return (it->second != nullptr) ? _init(*it->second) : false;
+ else
+ return false;
+}
+
+void GLCanvas3DManager::set_as_dirty(wxGLCanvas* canvas)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_as_dirty();
+}
+
+unsigned int GLCanvas3DManager::get_volumes_count(wxGLCanvas* canvas) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->get_volumes_count() : 0;
+}
+
+void GLCanvas3DManager::reset_volumes(wxGLCanvas* canvas)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->reset_volumes();
+}
+
+void GLCanvas3DManager::deselect_volumes(wxGLCanvas* canvas)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->deselect_volumes();
+}
+
+void GLCanvas3DManager::select_volume(wxGLCanvas* canvas, unsigned int id)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->select_volume(id);
+}
+
+void GLCanvas3DManager::update_volumes_selection(wxGLCanvas* canvas, const std::vector<int>& selections)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->update_volumes_selection(selections);
+}
+
+int GLCanvas3DManager::check_volumes_outside_state(wxGLCanvas* canvas, const DynamicPrintConfig* config) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->check_volumes_outside_state(config) : false;
+}
+
+bool GLCanvas3DManager::move_volume_up(wxGLCanvas* canvas, unsigned int id)
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->move_volume_up(id) : false;
+}
+
+bool GLCanvas3DManager::move_volume_down(wxGLCanvas* canvas, unsigned int id)
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->move_volume_down(id) : false;
+}
+
+void GLCanvas3DManager::set_objects_selections(wxGLCanvas* canvas, const std::vector<int>& selections)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_objects_selections(selections);
+}
+
+void GLCanvas3DManager::set_config(wxGLCanvas* canvas, DynamicPrintConfig* config)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_config(config);
+}
+
+void GLCanvas3DManager::set_print(wxGLCanvas* canvas, Print* print)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_print(print);
+}
+
+void GLCanvas3DManager::set_model(wxGLCanvas* canvas, Model* model)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_model(model);
+}
+
+void GLCanvas3DManager::set_bed_shape(wxGLCanvas* canvas, const Pointfs& shape)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_bed_shape(shape);
+}
+
+void GLCanvas3DManager::set_auto_bed_shape(wxGLCanvas* canvas)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_auto_bed_shape();
+}
+
+BoundingBoxf3 GLCanvas3DManager::get_volumes_bounding_box(wxGLCanvas* canvas)
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->volumes_bounding_box() : BoundingBoxf3();
+}
+
+void GLCanvas3DManager::set_axes_length(wxGLCanvas* canvas, float length)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_axes_length(length);
+}
+
+void GLCanvas3DManager::set_cutting_plane(wxGLCanvas* canvas, float z, const ExPolygons& polygons)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_cutting_plane(z, polygons);
+}
+
+void GLCanvas3DManager::set_color_by(wxGLCanvas* canvas, const std::string& value)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_color_by(value);
+}
+
+void GLCanvas3DManager::set_select_by(wxGLCanvas* canvas, const std::string& value)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_select_by(value);
+}
+
+void GLCanvas3DManager::set_drag_by(wxGLCanvas* canvas, const std::string& value)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_drag_by(value);
+}
+
+std::string GLCanvas3DManager::get_select_by(wxGLCanvas* canvas) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->get_select_by() : "";
+}
+
+bool GLCanvas3DManager::is_layers_editing_enabled(wxGLCanvas* canvas) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->is_layers_editing_enabled() : false;
+}
+
+bool GLCanvas3DManager::is_layers_editing_allowed(wxGLCanvas* canvas) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->is_layers_editing_allowed() : false;
+}
+
+bool GLCanvas3DManager::is_shader_enabled(wxGLCanvas* canvas) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->is_shader_enabled() : false;
+}
+
+bool GLCanvas3DManager::is_reload_delayed(wxGLCanvas* canvas) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->is_reload_delayed() : false;
+}
+
+void GLCanvas3DManager::enable_layers_editing(wxGLCanvas* canvas, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_layers_editing(enable);
+}
+
+void GLCanvas3DManager::enable_warning_texture(wxGLCanvas* canvas, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_warning_texture(enable);
+}
+
+void GLCanvas3DManager::enable_legend_texture(wxGLCanvas* canvas, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_legend_texture(enable);
+}
+
+void GLCanvas3DManager::enable_picking(wxGLCanvas* canvas, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_picking(enable);
+}
+
+void GLCanvas3DManager::enable_moving(wxGLCanvas* canvas, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_moving(enable);
+}
+
+void GLCanvas3DManager::enable_gizmos(wxGLCanvas* canvas, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_gizmos(enable);
+}
+
+void GLCanvas3DManager::enable_toolbar(wxGLCanvas* canvas, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_toolbar(enable);
+}
+
+void GLCanvas3DManager::enable_shader(wxGLCanvas* canvas, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_shader(enable);
+}
+
+void GLCanvas3DManager::enable_force_zoom_to_bed(wxGLCanvas* canvas, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_force_zoom_to_bed(enable);
+}
+
+void GLCanvas3DManager::enable_dynamic_background(wxGLCanvas* canvas, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_dynamic_background(enable);
+}
+
+void GLCanvas3DManager::allow_multisample(wxGLCanvas* canvas, bool allow)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->allow_multisample(allow);
+}
+
+void GLCanvas3DManager::enable_toolbar_item(wxGLCanvas* canvas, const std::string& name, bool enable)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->enable_toolbar_item(name, enable);
+}
+
+bool GLCanvas3DManager::is_toolbar_item_pressed(wxGLCanvas* canvas, const std::string& name) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->is_toolbar_item_pressed(name) : false;
+}
+
+void GLCanvas3DManager::zoom_to_bed(wxGLCanvas* canvas)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->zoom_to_bed();
+}
+
+void GLCanvas3DManager::zoom_to_volumes(wxGLCanvas* canvas)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->zoom_to_volumes();
+}
+
+void GLCanvas3DManager::select_view(wxGLCanvas* canvas, const std::string& direction)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->select_view(direction);
+}
+
+void GLCanvas3DManager::set_viewport_from_scene(wxGLCanvas* canvas, wxGLCanvas* other)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ {
+ CanvasesMap::iterator other_it = _get_canvas(other);
+ if (other_it != m_canvases.end())
+ it->second->set_viewport_from_scene(*other_it->second);
+ }
+}
+
+void GLCanvas3DManager::update_volumes_colors_by_extruder(wxGLCanvas* canvas)
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->update_volumes_colors_by_extruder();
+}
+
+void GLCanvas3DManager::update_gizmos_data(wxGLCanvas* canvas)
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->update_gizmos_data();
+}
+
+void GLCanvas3DManager::render(wxGLCanvas* canvas) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->render();
+}
+
+std::vector<double> GLCanvas3DManager::get_current_print_zs(wxGLCanvas* canvas, bool active_only) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->get_current_print_zs(active_only) : std::vector<double>();
+}
+
+void GLCanvas3DManager::set_toolpaths_range(wxGLCanvas* canvas, double low, double high)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->set_toolpaths_range(low, high);
+}
+
+std::vector<int> GLCanvas3DManager::load_object(wxGLCanvas* canvas, const ModelObject* model_object, int obj_idx, std::vector<int> instance_idxs)
+{
+ if (model_object == nullptr)
+ return std::vector<int>();
+
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->load_object(*model_object, obj_idx, instance_idxs) : std::vector<int>();
+}
+
+std::vector<int> GLCanvas3DManager::load_object(wxGLCanvas* canvas, const Model* model, int obj_idx)
+{
+ if (model == nullptr)
+ return std::vector<int>();
+
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->load_object(*model, obj_idx) : std::vector<int>();
+}
+
+int GLCanvas3DManager::get_first_volume_id(wxGLCanvas* canvas, int obj_idx) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->get_first_volume_id(obj_idx) : -1;
+}
+
+int GLCanvas3DManager::get_in_object_volume_id(wxGLCanvas* canvas, int scene_vol_idx) const
+{
+ CanvasesMap::const_iterator it = _get_canvas(canvas);
+ return (it != m_canvases.end()) ? it->second->get_in_object_volume_id(scene_vol_idx) : -1;
+}
+
+void GLCanvas3DManager::reload_scene(wxGLCanvas* canvas, bool force)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->reload_scene(force);
+}
+
+void GLCanvas3DManager::load_gcode_preview(wxGLCanvas* canvas, const GCodePreviewData* preview_data, const std::vector<std::string>& str_tool_colors)
+{
+ if (preview_data == nullptr)
+ return;
+
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->load_gcode_preview(*preview_data, str_tool_colors);
+}
+
+void GLCanvas3DManager::load_preview(wxGLCanvas* canvas, const std::vector<std::string>& str_tool_colors)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->load_preview(str_tool_colors);
+}
+
+void GLCanvas3DManager::reset_legend_texture()
+{
+ for (CanvasesMap::value_type& canvas : m_canvases)
+ {
+ if (canvas.second != nullptr)
+ canvas.second->reset_legend_texture();
+ }
+}
+
+void GLCanvas3DManager::register_on_viewport_changed_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_viewport_changed_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_double_click_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_double_click_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_right_click_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_right_click_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_select_object_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_select_object_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_model_update_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_model_update_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_remove_object_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_remove_object_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_arrange_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_arrange_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_rotate_object_left_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_rotate_object_left_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_rotate_object_right_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_rotate_object_right_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_scale_object_uniformly_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_scale_object_uniformly_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_increase_objects_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_increase_objects_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_decrease_objects_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_decrease_objects_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_instance_moved_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_instance_moved_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_wipe_tower_moved_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_wipe_tower_moved_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_enable_action_buttons_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_enable_action_buttons_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_gizmo_scale_uniformly_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_gizmo_scale_uniformly_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_gizmo_rotate_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_gizmo_rotate_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_gizmo_flatten_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_gizmo_flatten_callback(callback);
+}
+
+void GLCanvas3DManager::register_on_update_geometry_info_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_on_update_geometry_info_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_add_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_add_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_delete_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_delete_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_deleteall_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_deleteall_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_arrange_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_arrange_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_more_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_more_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_fewer_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_fewer_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_split_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_split_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_cut_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_cut_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_settings_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_settings_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_layersediting_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_layersediting_callback(callback);
+}
+
+void GLCanvas3DManager::register_action_selectbyparts_callback(wxGLCanvas* canvas, void* callback)
+{
+ CanvasesMap::iterator it = _get_canvas(canvas);
+ if (it != m_canvases.end())
+ it->second->register_action_selectbyparts_callback(callback);
+}
+
+GLCanvas3DManager::CanvasesMap::iterator GLCanvas3DManager::_get_canvas(wxGLCanvas* canvas)
+{
+ return (canvas == nullptr) ? m_canvases.end() : m_canvases.find(canvas);
+}
+
+GLCanvas3DManager::CanvasesMap::const_iterator GLCanvas3DManager::_get_canvas(wxGLCanvas* canvas) const
+{
+ return (canvas == nullptr) ? m_canvases.end() : m_canvases.find(canvas);
+}
+
+bool GLCanvas3DManager::_init(GLCanvas3D& canvas)
+{
+ if (!m_gl_initialized)
+ init_gl();
+
+ return canvas.init(m_use_VBOs, m_use_legacy_opengl);
+}
+
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/GLCanvas3DManager.hpp b/src/slic3r/GUI/GLCanvas3DManager.hpp
new file mode 100644
index 000000000..4922b6171
--- /dev/null
+++ b/src/slic3r/GUI/GLCanvas3DManager.hpp
@@ -0,0 +1,192 @@
+#ifndef slic3r_GLCanvas3DManager_hpp_
+#define slic3r_GLCanvas3DManager_hpp_
+
+#include "../../libslic3r/BoundingBox.hpp"
+
+#include <map>
+#include <vector>
+
+class wxGLCanvas;
+class wxGLContext;
+
+namespace Slic3r {
+
+class DynamicPrintConfig;
+class Print;
+class Model;
+class ExPolygon;
+typedef std::vector<ExPolygon> ExPolygons;
+class ModelObject;
+class PrintObject;
+class GCodePreviewData;
+
+namespace GUI {
+
+class GLCanvas3D;
+
+class GLCanvas3DManager
+{
+ struct GLInfo
+ {
+ std::string version;
+ std::string glsl_version;
+ std::string vendor;
+ std::string renderer;
+
+ GLInfo();
+
+ void detect();
+ bool is_version_greater_or_equal_to(unsigned int major, unsigned int minor) const;
+
+ std::string to_string(bool format_as_html, bool extensions) const;
+ };
+
+ typedef std::map<wxGLCanvas*, GLCanvas3D*> CanvasesMap;
+
+ CanvasesMap m_canvases;
+ wxGLCanvas* m_current;
+ GLInfo m_gl_info;
+ bool m_gl_initialized;
+ bool m_use_legacy_opengl;
+ bool m_use_VBOs;
+
+public:
+ GLCanvas3DManager();
+
+ bool add(wxGLCanvas* canvas);
+ bool remove(wxGLCanvas* canvas);
+
+ void remove_all();
+
+ unsigned int count() const;
+
+ void init_gl();
+ std::string get_gl_info(bool format_as_html, bool extensions) const;
+
+ bool use_VBOs() const;
+ bool layer_editing_allowed() const;
+
+ bool init(wxGLCanvas* canvas);
+
+ void set_as_dirty(wxGLCanvas* canvas);
+
+ unsigned int get_volumes_count(wxGLCanvas* canvas) const;
+ void reset_volumes(wxGLCanvas* canvas);
+ void deselect_volumes(wxGLCanvas* canvas);
+ void select_volume(wxGLCanvas* canvas, unsigned int id);
+ void update_volumes_selection(wxGLCanvas* canvas, const std::vector<int>& selections);
+ int check_volumes_outside_state(wxGLCanvas* canvas, const DynamicPrintConfig* config) const;
+ bool move_volume_up(wxGLCanvas* canvas, unsigned int id);
+ bool move_volume_down(wxGLCanvas* canvas, unsigned int id);
+
+ void set_objects_selections(wxGLCanvas* canvas, const std::vector<int>& selections);
+
+ void set_config(wxGLCanvas* canvas, DynamicPrintConfig* config);
+ void set_print(wxGLCanvas* canvas, Print* print);
+ void set_model(wxGLCanvas* canvas, Model* model);
+
+ void set_bed_shape(wxGLCanvas* canvas, const Pointfs& shape);
+ void set_auto_bed_shape(wxGLCanvas* canvas);
+
+ BoundingBoxf3 get_volumes_bounding_box(wxGLCanvas* canvas);
+
+ void set_axes_length(wxGLCanvas* canvas, float length);
+
+ void set_cutting_plane(wxGLCanvas* canvas, float z, const ExPolygons& polygons);
+
+ void set_color_by(wxGLCanvas* canvas, const std::string& value);
+ void set_select_by(wxGLCanvas* canvas, const std::string& value);
+ void set_drag_by(wxGLCanvas* canvas, const std::string& value);
+
+ std::string get_select_by(wxGLCanvas* canvas) const;
+
+ bool is_layers_editing_enabled(wxGLCanvas* canvas) const;
+ bool is_layers_editing_allowed(wxGLCanvas* canvas) const;
+ bool is_shader_enabled(wxGLCanvas* canvas) const;
+
+ bool is_reload_delayed(wxGLCanvas* canvas) const;
+
+ void enable_layers_editing(wxGLCanvas* canvas, bool enable);
+ void enable_warning_texture(wxGLCanvas* canvas, bool enable);
+ void enable_legend_texture(wxGLCanvas* canvas, bool enable);
+ void enable_picking(wxGLCanvas* canvas, bool enable);
+ void enable_moving(wxGLCanvas* canvas, bool enable);
+ void enable_gizmos(wxGLCanvas* canvas, bool enable);
+ void enable_toolbar(wxGLCanvas* canvas, bool enable);
+ void enable_shader(wxGLCanvas* canvas, bool enable);
+ void enable_force_zoom_to_bed(wxGLCanvas* canvas, bool enable);
+ void enable_dynamic_background(wxGLCanvas* canvas, bool enable);
+ void allow_multisample(wxGLCanvas* canvas, bool allow);
+
+ void enable_toolbar_item(wxGLCanvas* canvas, const std::string& name, bool enable);
+ bool is_toolbar_item_pressed(wxGLCanvas* canvas, const std::string& name) const;
+
+ void zoom_to_bed(wxGLCanvas* canvas);
+ void zoom_to_volumes(wxGLCanvas* canvas);
+ void select_view(wxGLCanvas* canvas, const std::string& direction);
+ void set_viewport_from_scene(wxGLCanvas* canvas, wxGLCanvas* other);
+
+ void update_volumes_colors_by_extruder(wxGLCanvas* canvas);
+ void update_gizmos_data(wxGLCanvas* canvas);
+
+ void render(wxGLCanvas* canvas) const;
+
+ std::vector<double> get_current_print_zs(wxGLCanvas* canvas, bool active_only) const;
+ void set_toolpaths_range(wxGLCanvas* canvas, double low, double high);
+
+ std::vector<int> load_object(wxGLCanvas* canvas, const ModelObject* model_object, int obj_idx, std::vector<int> instance_idxs);
+ std::vector<int> load_object(wxGLCanvas* canvas, const Model* model, int obj_idx);
+
+ int get_first_volume_id(wxGLCanvas* canvas, int obj_idx) const;
+ int get_in_object_volume_id(wxGLCanvas* canvas, int scene_vol_idx) const;
+
+ void reload_scene(wxGLCanvas* canvas, bool force);
+
+ void load_gcode_preview(wxGLCanvas* canvas, const GCodePreviewData* preview_data, const std::vector<std::string>& str_tool_colors);
+ void load_preview(wxGLCanvas* canvas, const std::vector<std::string>& str_tool_colors);
+
+ void reset_legend_texture();
+
+ void register_on_viewport_changed_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_double_click_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_right_click_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_select_object_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_model_update_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_remove_object_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_arrange_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_rotate_object_left_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_rotate_object_right_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_scale_object_uniformly_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_increase_objects_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_decrease_objects_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_instance_moved_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_wipe_tower_moved_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_enable_action_buttons_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_gizmo_scale_uniformly_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_gizmo_rotate_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_gizmo_flatten_callback(wxGLCanvas* canvas, void* callback);
+ void register_on_update_geometry_info_callback(wxGLCanvas* canvas, void* callback);
+
+ void register_action_add_callback(wxGLCanvas* canvas, void* callback);
+ void register_action_delete_callback(wxGLCanvas* canvas, void* callback);
+ void register_action_deleteall_callback(wxGLCanvas* canvas, void* callback);
+ void register_action_arrange_callback(wxGLCanvas* canvas, void* callback);
+ void register_action_more_callback(wxGLCanvas* canvas, void* callback);
+ void register_action_fewer_callback(wxGLCanvas* canvas, void* callback);
+ void register_action_split_callback(wxGLCanvas* canvas, void* callback);
+ void register_action_cut_callback(wxGLCanvas* canvas, void* callback);
+ void register_action_settings_callback(wxGLCanvas* canvas, void* callback);
+ void register_action_layersediting_callback(wxGLCanvas* canvas, void* callback);
+ void register_action_selectbyparts_callback(wxGLCanvas* canvas, void* callback);
+
+private:
+ CanvasesMap::iterator _get_canvas(wxGLCanvas* canvas);
+ CanvasesMap::const_iterator _get_canvas(wxGLCanvas* canvas) const;
+
+ bool _init(GLCanvas3D& canvas);
+};
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif // slic3r_GLCanvas3DManager_hpp_
diff --git a/src/slic3r/GUI/GLGizmo.cpp b/src/slic3r/GUI/GLGizmo.cpp
new file mode 100644
index 000000000..e23958c1d
--- /dev/null
+++ b/src/slic3r/GUI/GLGizmo.cpp
@@ -0,0 +1,1503 @@
+#include "GLGizmo.hpp"
+
+#include "../../libslic3r/Utils.hpp"
+#include "../../slic3r/GUI/GLCanvas3D.hpp"
+
+#include <Eigen/Dense>
+#include "../../libslic3r/Geometry.hpp"
+
+#include <GL/glew.h>
+
+#include <iostream>
+#include <numeric>
+
+static const float DEFAULT_BASE_COLOR[3] = { 0.625f, 0.625f, 0.625f };
+static const float DEFAULT_DRAG_COLOR[3] = { 1.0f, 1.0f, 1.0f };
+static const float DEFAULT_HIGHLIGHT_COLOR[3] = { 1.0f, 0.38f, 0.0f };
+
+static const float AXES_COLOR[3][3] = { { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } };
+
+namespace Slic3r {
+namespace GUI {
+
+// returns the intersection of the given ray with the plane parallel to plane XY and passing through the given center
+// coordinates are local to the plane
+Vec3d intersection_on_plane_xy(const Linef3& ray, const Vec3d& center)
+{
+ Transform3d m = Transform3d::Identity();
+ m.translate(-center);
+ Vec2d mouse_pos_2d = to_2d(transform(ray, m).intersect_plane(0.0));
+ return Vec3d(mouse_pos_2d(0), mouse_pos_2d(1), 0.0);
+}
+
+// returns the intersection of the given ray with the plane parallel to plane XZ and passing through the given center
+// coordinates are local to the plane
+Vec3d intersection_on_plane_xz(const Linef3& ray, const Vec3d& center)
+{
+ Transform3d m = Transform3d::Identity();
+ m.rotate(Eigen::AngleAxisd(-0.5 * (double)PI, Vec3d::UnitX()));
+ m.translate(-center);
+ Vec2d mouse_pos_2d = to_2d(transform(ray, m).intersect_plane(0.0));
+ return Vec3d(mouse_pos_2d(0), 0.0, mouse_pos_2d(1));
+}
+
+// returns the intersection of the given ray with the plane parallel to plane YZ and passing through the given center
+// coordinates are local to the plane
+Vec3d intersection_on_plane_yz(const Linef3& ray, const Vec3d& center)
+{
+ Transform3d m = Transform3d::Identity();
+ m.rotate(Eigen::AngleAxisd(-0.5f * (double)PI, Vec3d::UnitY()));
+ m.translate(-center);
+ Vec2d mouse_pos_2d = to_2d(transform(ray, m).intersect_plane(0.0));
+
+ return Vec3d(0.0, mouse_pos_2d(1), -mouse_pos_2d(0));
+}
+
+// return an index:
+// 0 for plane XY
+// 1 for plane XZ
+// 2 for plane YZ
+// which indicates which plane is best suited for intersecting the given unit vector
+// giving precedence to the plane with the given index
+unsigned int select_best_plane(const Vec3d& unit_vector, unsigned int preferred_plane)
+{
+ unsigned int ret = preferred_plane;
+
+ // 1st checks if the given vector is not parallel to the given preferred plane
+ double dot_to_normal = 0.0;
+ switch (ret)
+ {
+ case 0: // plane xy
+ {
+ dot_to_normal = std::abs(unit_vector.dot(Vec3d::UnitZ()));
+ break;
+ }
+ case 1: // plane xz
+ {
+ dot_to_normal = std::abs(unit_vector.dot(-Vec3d::UnitY()));
+ break;
+ }
+ case 2: // plane yz
+ {
+ dot_to_normal = std::abs(unit_vector.dot(Vec3d::UnitX()));
+ break;
+ }
+ default:
+ {
+ break;
+ }
+ }
+
+ // if almost parallel, select the plane whose normal direction is closest to the given vector direction,
+ // otherwise return the given preferred plane index
+ if (dot_to_normal < 0.1)
+ {
+ typedef std::map<double, unsigned int> ProjsMap;
+ ProjsMap projs_map;
+ projs_map.insert(ProjsMap::value_type(std::abs(unit_vector.dot(Vec3d::UnitZ())), 0)); // plane xy
+ projs_map.insert(ProjsMap::value_type(std::abs(unit_vector.dot(-Vec3d::UnitY())), 1)); // plane xz
+ projs_map.insert(ProjsMap::value_type(std::abs(unit_vector.dot(Vec3d::UnitX())), 2)); // plane yz
+ ret = projs_map.rbegin()->second;
+ }
+
+ return ret;
+}
+
+const float GLGizmoBase::Grabber::SizeFactor = 0.025f;
+const float GLGizmoBase::Grabber::MinHalfSize = 1.5f;
+const float GLGizmoBase::Grabber::DraggingScaleFactor = 1.25f;
+
+GLGizmoBase::Grabber::Grabber()
+ : center(Vec3d::Zero())
+ , angles(Vec3d::Zero())
+ , dragging(false)
+ , enabled(true)
+{
+ color[0] = 1.0f;
+ color[1] = 1.0f;
+ color[2] = 1.0f;
+}
+
+void GLGizmoBase::Grabber::render(bool hover, const BoundingBoxf3& box) const
+{
+ float render_color[3];
+ if (hover)
+ {
+ render_color[0] = 1.0f - color[0];
+ render_color[1] = 1.0f - color[1];
+ render_color[2] = 1.0f - color[2];
+ }
+ else
+ ::memcpy((void*)render_color, (const void*)color, 3 * sizeof(float));
+
+ render(box, render_color, true);
+}
+
+void GLGizmoBase::Grabber::render(const BoundingBoxf3& box, const float* render_color, bool use_lighting) const
+{
+ float max_size = (float)box.max_size();
+ float half_size = dragging ? max_size * SizeFactor * DraggingScaleFactor : max_size * SizeFactor;
+ half_size = std::max(half_size, MinHalfSize);
+
+ if (use_lighting)
+ ::glEnable(GL_LIGHTING);
+
+ ::glColor3f((GLfloat)render_color[0], (GLfloat)render_color[1], (GLfloat)render_color[2]);
+
+ ::glPushMatrix();
+ ::glTranslatef((GLfloat)center(0), (GLfloat)center(1), (GLfloat)center(2));
+
+ float rad_to_deg = 180.0f / (GLfloat)PI;
+ ::glRotatef((GLfloat)angles(0) * rad_to_deg, 1.0f, 0.0f, 0.0f);
+ ::glRotatef((GLfloat)angles(1) * rad_to_deg, 0.0f, 1.0f, 0.0f);
+ ::glRotatef((GLfloat)angles(2) * rad_to_deg, 0.0f, 0.0f, 1.0f);
+
+ // face min x
+ ::glPushMatrix();
+ ::glTranslatef(-(GLfloat)half_size, 0.0f, 0.0f);
+ ::glRotatef(-90.0f, 0.0f, 1.0f, 0.0f);
+ render_face(half_size);
+ ::glPopMatrix();
+
+ // face max x
+ ::glPushMatrix();
+ ::glTranslatef((GLfloat)half_size, 0.0f, 0.0f);
+ ::glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
+ render_face(half_size);
+ ::glPopMatrix();
+
+ // face min y
+ ::glPushMatrix();
+ ::glTranslatef(0.0f, -(GLfloat)half_size, 0.0f);
+ ::glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
+ render_face(half_size);
+ ::glPopMatrix();
+
+ // face max y
+ ::glPushMatrix();
+ ::glTranslatef(0.0f, (GLfloat)half_size, 0.0f);
+ ::glRotatef(-90.0f, 1.0f, 0.0f, 0.0f);
+ render_face(half_size);
+ ::glPopMatrix();
+
+ // face min z
+ ::glPushMatrix();
+ ::glTranslatef(0.0f, 0.0f, -(GLfloat)half_size);
+ ::glRotatef(180.0f, 1.0f, 0.0f, 0.0f);
+ render_face(half_size);
+ ::glPopMatrix();
+
+ // face max z
+ ::glPushMatrix();
+ ::glTranslatef(0.0f, 0.0f, (GLfloat)half_size);
+ render_face(half_size);
+ ::glPopMatrix();
+
+ ::glPopMatrix();
+
+ if (use_lighting)
+ ::glDisable(GL_LIGHTING);
+}
+
+void GLGizmoBase::Grabber::render_face(float half_size) const
+{
+ ::glBegin(GL_TRIANGLES);
+ ::glNormal3f(0.0f, 0.0f, 1.0f);
+ ::glVertex3f(-(GLfloat)half_size, -(GLfloat)half_size, 0.0f);
+ ::glVertex3f((GLfloat)half_size, -(GLfloat)half_size, 0.0f);
+ ::glVertex3f((GLfloat)half_size, (GLfloat)half_size, 0.0f);
+ ::glVertex3f((GLfloat)half_size, (GLfloat)half_size, 0.0f);
+ ::glVertex3f(-(GLfloat)half_size, (GLfloat)half_size, 0.0f);
+ ::glVertex3f(-(GLfloat)half_size, -(GLfloat)half_size, 0.0f);
+ ::glEnd();
+}
+
+GLGizmoBase::GLGizmoBase(GLCanvas3D& parent)
+ : m_parent(parent)
+ , m_group_id(-1)
+ , m_state(Off)
+ , m_hover_id(-1)
+ , m_dragging(false)
+{
+ ::memcpy((void*)m_base_color, (const void*)DEFAULT_BASE_COLOR, 3 * sizeof(float));
+ ::memcpy((void*)m_drag_color, (const void*)DEFAULT_DRAG_COLOR, 3 * sizeof(float));
+ ::memcpy((void*)m_highlight_color, (const void*)DEFAULT_HIGHLIGHT_COLOR, 3 * sizeof(float));
+}
+
+void GLGizmoBase::set_hover_id(int id)
+{
+ if (m_grabbers.empty() || (id < (int)m_grabbers.size()))
+ {
+ m_hover_id = id;
+ on_set_hover_id();
+ }
+}
+
+void GLGizmoBase::set_highlight_color(const float* color)
+{
+ if (color != nullptr)
+ ::memcpy((void*)m_highlight_color, (const void*)color, 3 * sizeof(float));
+}
+
+void GLGizmoBase::enable_grabber(unsigned int id)
+{
+ if ((0 <= id) && (id < (unsigned int)m_grabbers.size()))
+ m_grabbers[id].enabled = true;
+
+ on_enable_grabber(id);
+}
+
+void GLGizmoBase::disable_grabber(unsigned int id)
+{
+ if ((0 <= id) && (id < (unsigned int)m_grabbers.size()))
+ m_grabbers[id].enabled = false;
+
+ on_disable_grabber(id);
+}
+
+void GLGizmoBase::start_dragging(const BoundingBoxf3& box)
+{
+ m_dragging = true;
+
+ for (int i = 0; i < (int)m_grabbers.size(); ++i)
+ {
+ m_grabbers[i].dragging = (m_hover_id == i);
+ }
+
+ on_start_dragging(box);
+}
+
+void GLGizmoBase::stop_dragging()
+{
+ m_dragging = false;
+ set_tooltip("");
+
+ for (int i = 0; i < (int)m_grabbers.size(); ++i)
+ {
+ m_grabbers[i].dragging = false;
+ }
+
+ on_stop_dragging();
+}
+
+void GLGizmoBase::update(const Linef3& mouse_ray)
+{
+ if (m_hover_id != -1)
+ on_update(mouse_ray);
+}
+
+float GLGizmoBase::picking_color_component(unsigned int id) const
+{
+ int color = 254 - (int)id;
+ if (m_group_id > -1)
+ color -= m_group_id;
+
+ return (float)color / 255.0f;
+}
+
+void GLGizmoBase::render_grabbers(const BoundingBoxf3& box) const
+{
+ for (int i = 0; i < (int)m_grabbers.size(); ++i)
+ {
+ if (m_grabbers[i].enabled)
+ m_grabbers[i].render((m_hover_id == i), box);
+ }
+}
+
+void GLGizmoBase::render_grabbers_for_picking(const BoundingBoxf3& box) const
+{
+ for (unsigned int i = 0; i < (unsigned int)m_grabbers.size(); ++i)
+ {
+ if (m_grabbers[i].enabled)
+ {
+ m_grabbers[i].color[0] = 1.0f;
+ m_grabbers[i].color[1] = 1.0f;
+ m_grabbers[i].color[2] = picking_color_component(i);
+ m_grabbers[i].render_for_picking(box);
+ }
+ }
+}
+
+void GLGizmoBase::set_tooltip(const std::string& tooltip) const
+{
+ m_parent.set_tooltip(tooltip);
+}
+
+std::string GLGizmoBase::format(float value, unsigned int decimals) const
+{
+ char buf[1024];
+ ::sprintf(buf, "%.*f", decimals, value);
+ return buf;
+}
+
+const float GLGizmoRotate::Offset = 5.0f;
+const unsigned int GLGizmoRotate::CircleResolution = 64;
+const unsigned int GLGizmoRotate::AngleResolution = 64;
+const unsigned int GLGizmoRotate::ScaleStepsCount = 72;
+const float GLGizmoRotate::ScaleStepRad = 2.0f * (float)PI / GLGizmoRotate::ScaleStepsCount;
+const unsigned int GLGizmoRotate::ScaleLongEvery = 2;
+const float GLGizmoRotate::ScaleLongTooth = 2.0f;
+const float GLGizmoRotate::ScaleShortTooth = 1.0f;
+const unsigned int GLGizmoRotate::SnapRegionsCount = 8;
+const float GLGizmoRotate::GrabberOffset = 5.0f;
+
+GLGizmoRotate::GLGizmoRotate(GLCanvas3D& parent, GLGizmoRotate::Axis axis)
+ : GLGizmoBase(parent)
+ , m_axis(axis)
+ , m_angle(0.0)
+ , m_center(0.0, 0.0, 0.0)
+ , m_radius(0.0f)
+{
+}
+
+void GLGizmoRotate::set_angle(double angle)
+{
+ if (std::abs(angle - 2.0 * (double)PI) < EPSILON)
+ angle = 0.0;
+
+ m_angle = angle;
+}
+
+bool GLGizmoRotate::on_init()
+{
+ m_grabbers.push_back(Grabber());
+ return true;
+}
+
+void GLGizmoRotate::on_start_dragging(const BoundingBoxf3& box)
+{
+ m_center = box.center();
+ m_radius = Offset + box.radius();
+}
+
+void GLGizmoRotate::on_update(const Linef3& mouse_ray)
+{
+ Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(mouse_ray));
+
+ Vec2d orig_dir = Vec2d::UnitX();
+ Vec2d new_dir = mouse_pos.normalized();
+
+ double theta = ::acos(clamp(-1.0, 1.0, new_dir.dot(orig_dir)));
+ if (cross2(orig_dir, new_dir) < 0.0)
+ theta = 2.0 * (double)PI - theta;
+
+ double len = mouse_pos.norm();
+
+ // snap to snap region
+ double in_radius = (double)m_radius / 3.0;
+ double out_radius = 2.0 * (double)in_radius;
+ if ((in_radius <= len) && (len <= out_radius))
+ {
+ double step = 2.0 * (double)PI / (double)SnapRegionsCount;
+ theta = step * (double)std::round(theta / step);
+ }
+ else
+ {
+ // snap to scale
+ in_radius = (double)m_radius;
+ out_radius = in_radius + (double)ScaleLongTooth;
+ if ((in_radius <= len) && (len <= out_radius))
+ {
+ double step = 2.0 * (double)PI / (double)ScaleStepsCount;
+ theta = step * (double)std::round(theta / step);
+ }
+ }
+
+ if (theta == 2.0 * (double)PI)
+ theta = 0.0;
+
+ m_angle = theta;
+}
+
+void GLGizmoRotate::on_render(const BoundingBoxf3& box) const
+{
+ if (!m_grabbers[0].enabled)
+ return;
+
+ if (m_dragging)
+ set_tooltip(format(m_angle * 180.0f / (float)PI, 4));
+ else
+ {
+ m_center = box.center();
+ m_radius = Offset + box.radius();
+ }
+
+ ::glEnable(GL_DEPTH_TEST);
+
+ ::glPushMatrix();
+ transform_to_local();
+
+ ::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f);
+ ::glColor3fv((m_hover_id != -1) ? m_drag_color : m_highlight_color);
+
+ render_circle();
+
+ if (m_hover_id != -1)
+ {
+ render_scale();
+ render_snap_radii();
+ render_reference_radius();
+ }
+
+ ::glColor3fv(m_highlight_color);
+
+ if (m_hover_id != -1)
+ render_angle();
+
+ render_grabber(box);
+
+ ::glPopMatrix();
+}
+
+void GLGizmoRotate::on_render_for_picking(const BoundingBoxf3& box) const
+{
+ ::glDisable(GL_DEPTH_TEST);
+
+ ::glPushMatrix();
+
+ transform_to_local();
+ render_grabbers_for_picking(box);
+
+ ::glPopMatrix();
+}
+
+void GLGizmoRotate::render_circle() const
+{
+ ::glBegin(GL_LINE_LOOP);
+ for (unsigned int i = 0; i < ScaleStepsCount; ++i)
+ {
+ float angle = (float)i * ScaleStepRad;
+ float x = ::cos(angle) * m_radius;
+ float y = ::sin(angle) * m_radius;
+ float z = 0.0f;
+ ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z);
+ }
+ ::glEnd();
+}
+
+void GLGizmoRotate::render_scale() const
+{
+ float out_radius_long = m_radius + ScaleLongTooth;
+ float out_radius_short = m_radius + ScaleShortTooth;
+
+ ::glBegin(GL_LINES);
+ for (unsigned int i = 0; i < ScaleStepsCount; ++i)
+ {
+ float angle = (float)i * ScaleStepRad;
+ float cosa = ::cos(angle);
+ float sina = ::sin(angle);
+ float in_x = cosa * m_radius;
+ float in_y = sina * m_radius;
+ float in_z = 0.0f;
+ float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short;
+ float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short;
+ float out_z = 0.0f;
+ ::glVertex3f((GLfloat)in_x, (GLfloat)in_y, (GLfloat)in_z);
+ ::glVertex3f((GLfloat)out_x, (GLfloat)out_y, (GLfloat)out_z);
+ }
+ ::glEnd();
+}
+
+void GLGizmoRotate::render_snap_radii() const
+{
+ float step = 2.0f * (float)PI / (float)SnapRegionsCount;
+
+ float in_radius = m_radius / 3.0f;
+ float out_radius = 2.0f * in_radius;
+
+ ::glBegin(GL_LINES);
+ for (unsigned int i = 0; i < SnapRegionsCount; ++i)
+ {
+ float angle = (float)i * step;
+ float cosa = ::cos(angle);
+ float sina = ::sin(angle);
+ float in_x = cosa * in_radius;
+ float in_y = sina * in_radius;
+ float in_z = 0.0f;
+ float out_x = cosa * out_radius;
+ float out_y = sina * out_radius;
+ float out_z = 0.0f;
+ ::glVertex3f((GLfloat)in_x, (GLfloat)in_y, (GLfloat)in_z);
+ ::glVertex3f((GLfloat)out_x, (GLfloat)out_y, (GLfloat)out_z);
+ }
+ ::glEnd();
+}
+
+void GLGizmoRotate::render_reference_radius() const
+{
+ ::glBegin(GL_LINES);
+ ::glVertex3f(0.0f, 0.0f, 0.0f);
+ ::glVertex3f((GLfloat)(m_radius + GrabberOffset), 0.0f, 0.0f);
+ ::glEnd();
+}
+
+void GLGizmoRotate::render_angle() const
+{
+ float step_angle = (float)m_angle / AngleResolution;
+ float ex_radius = m_radius + GrabberOffset;
+
+ ::glBegin(GL_LINE_STRIP);
+ for (unsigned int i = 0; i <= AngleResolution; ++i)
+ {
+ float angle = (float)i * step_angle;
+ float x = ::cos(angle) * ex_radius;
+ float y = ::sin(angle) * ex_radius;
+ float z = 0.0f;
+ ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z);
+ }
+ ::glEnd();
+}
+
+void GLGizmoRotate::render_grabber(const BoundingBoxf3& box) const
+{
+ double grabber_radius = (double)(m_radius + GrabberOffset);
+ m_grabbers[0].center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0);
+ m_grabbers[0].angles(2) = m_angle;
+
+ ::glColor3fv((m_hover_id != -1) ? m_drag_color : m_highlight_color);
+
+ ::glBegin(GL_LINES);
+ ::glVertex3f(0.0f, 0.0f, 0.0f);
+ ::glVertex3f((GLfloat)m_grabbers[0].center(0), (GLfloat)m_grabbers[0].center(1), (GLfloat)m_grabbers[0].center(2));
+ ::glEnd();
+
+ ::memcpy((void*)m_grabbers[0].color, (const void*)m_highlight_color, 3 * sizeof(float));
+ render_grabbers(box);
+}
+
+void GLGizmoRotate::transform_to_local() const
+{
+ ::glTranslatef((GLfloat)m_center(0), (GLfloat)m_center(1), (GLfloat)m_center(2));
+
+ switch (m_axis)
+ {
+ case X:
+ {
+ ::glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
+ ::glRotatef(90.0f, 0.0f, 0.0f, 1.0f);
+ break;
+ }
+ case Y:
+ {
+ ::glRotatef(-90.0f, 1.0f, 0.0f, 0.0f);
+ ::glRotatef(180.0f, 0.0f, 0.0f, 1.0f);
+ break;
+ }
+ default:
+ case Z:
+ {
+ // no rotation
+ break;
+ }
+ }
+}
+
+Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray) const
+{
+ double half_pi = 0.5 * (double)PI;
+
+ Transform3d m = Transform3d::Identity();
+
+ switch (m_axis)
+ {
+ case X:
+ {
+ m.rotate(Eigen::AngleAxisd(-half_pi, Vec3d::UnitZ()));
+ m.rotate(Eigen::AngleAxisd(-half_pi, Vec3d::UnitY()));
+ break;
+ }
+ case Y:
+ {
+ m.rotate(Eigen::AngleAxisd(-(double)PI, Vec3d::UnitZ()));
+ m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitX()));
+ break;
+ }
+ default:
+ case Z:
+ {
+ // no rotation applied
+ break;
+ }
+ }
+
+ m.translate(-m_center);
+
+ return transform(mouse_ray, m).intersect_plane(0.0);
+}
+
+GLGizmoRotate3D::GLGizmoRotate3D(GLCanvas3D& parent)
+ : GLGizmoBase(parent)
+{
+ m_gizmos.emplace_back(parent, GLGizmoRotate::X);
+ m_gizmos.emplace_back(parent, GLGizmoRotate::Y);
+ m_gizmos.emplace_back(parent, GLGizmoRotate::Z);
+
+ for (unsigned int i = 0; i < 3; ++i)
+ {
+ m_gizmos[i].set_group_id(i);
+ }
+}
+
+bool GLGizmoRotate3D::on_init()
+{
+ for (GLGizmoRotate& g : m_gizmos)
+ {
+ if (!g.init())
+ return false;
+ }
+
+ for (unsigned int i = 0; i < 3; ++i)
+ {
+ m_gizmos[i].set_highlight_color(AXES_COLOR[i]);
+ }
+
+ std::string path = resources_dir() + "/icons/overlay/";
+
+ std::string filename = path + "rotate_off.png";
+ if (!m_textures[Off].load_from_file(filename, false))
+ return false;
+
+ filename = path + "rotate_hover.png";
+ if (!m_textures[Hover].load_from_file(filename, false))
+ return false;
+
+ filename = path + "rotate_on.png";
+ if (!m_textures[On].load_from_file(filename, false))
+ return false;
+
+ return true;
+}
+
+void GLGizmoRotate3D::on_start_dragging(const BoundingBoxf3& box)
+{
+ if ((0 <= m_hover_id) && (m_hover_id < 3))
+ m_gizmos[m_hover_id].start_dragging(box);
+}
+
+void GLGizmoRotate3D::on_stop_dragging()
+{
+ if ((0 <= m_hover_id) && (m_hover_id < 3))
+ m_gizmos[m_hover_id].stop_dragging();
+}
+
+void GLGizmoRotate3D::on_render(const BoundingBoxf3& box) const
+{
+ if ((m_hover_id == -1) || (m_hover_id == 0))
+ m_gizmos[X].render(box);
+
+ if ((m_hover_id == -1) || (m_hover_id == 1))
+ m_gizmos[Y].render(box);
+
+ if ((m_hover_id == -1) || (m_hover_id == 2))
+ m_gizmos[Z].render(box);
+}
+
+const float GLGizmoScale3D::Offset = 5.0f;
+const Vec3d GLGizmoScale3D::OffsetVec = (double)GLGizmoScale3D::Offset * Vec3d::Ones();
+
+GLGizmoScale3D::GLGizmoScale3D(GLCanvas3D& parent)
+ : GLGizmoBase(parent)
+ , m_scale(Vec3d::Ones())
+ , m_starting_scale(Vec3d::Ones())
+ , m_show_starting_box(false)
+{
+}
+
+bool GLGizmoScale3D::on_init()
+{
+ std::string path = resources_dir() + "/icons/overlay/";
+
+ std::string filename = path + "scale_off.png";
+ if (!m_textures[Off].load_from_file(filename, false))
+ return false;
+
+ filename = path + "scale_hover.png";
+ if (!m_textures[Hover].load_from_file(filename, false))
+ return false;
+
+ filename = path + "scale_on.png";
+ if (!m_textures[On].load_from_file(filename, false))
+ return false;
+
+ for (int i = 0; i < 10; ++i)
+ {
+ m_grabbers.push_back(Grabber());
+ }
+
+ double half_pi = 0.5 * (double)PI;
+
+ // x axis
+ m_grabbers[0].angles(1) = half_pi;
+ m_grabbers[1].angles(1) = half_pi;
+
+ // y axis
+ m_grabbers[2].angles(0) = half_pi;
+ m_grabbers[3].angles(0) = half_pi;
+
+ return true;
+}
+
+void GLGizmoScale3D::on_start_dragging(const BoundingBoxf3& box)
+{
+ if (m_hover_id != -1)
+ {
+ m_starting_drag_position = m_grabbers[m_hover_id].center;
+ m_show_starting_box = true;
+ m_starting_box = BoundingBoxf3(box.min - OffsetVec, box.max + OffsetVec);
+ }
+}
+
+void GLGizmoScale3D::on_update(const Linef3& mouse_ray)
+{
+ if ((m_hover_id == 0) || (m_hover_id == 1))
+ do_scale_x(mouse_ray);
+ else if ((m_hover_id == 2) || (m_hover_id == 3))
+ do_scale_y(mouse_ray);
+ else if ((m_hover_id == 4) || (m_hover_id == 5))
+ do_scale_z(mouse_ray);
+ else if (m_hover_id >= 6)
+ do_scale_uniform(mouse_ray);
+}
+
+void GLGizmoScale3D::on_render(const BoundingBoxf3& box) const
+{
+ if (m_grabbers[0].dragging || m_grabbers[1].dragging)
+ set_tooltip("X: " + format(100.0f * m_scale(0), 4) + "%");
+ else if (m_grabbers[2].dragging || m_grabbers[3].dragging)
+ set_tooltip("Y: " + format(100.0f * m_scale(1), 4) + "%");
+ else if (m_grabbers[4].dragging || m_grabbers[5].dragging)
+ set_tooltip("Z: " + format(100.0f * m_scale(2), 4) + "%");
+ else if (m_grabbers[6].dragging || m_grabbers[7].dragging || m_grabbers[8].dragging || m_grabbers[9].dragging)
+ {
+ std::string tooltip = "X: " + format(100.0f * m_scale(0), 4) + "%\n";
+ tooltip += "Y: " + format(100.0f * m_scale(1), 4) + "%\n";
+ tooltip += "Z: " + format(100.0f * m_scale(2), 4) + "%";
+ set_tooltip(tooltip);
+ }
+
+ ::glEnable(GL_DEPTH_TEST);
+
+ m_box = BoundingBoxf3(box.min - OffsetVec, box.max + OffsetVec);
+ const Vec3d& center = m_box.center();
+
+ // x axis
+ m_grabbers[0].center = Vec3d(m_box.min(0), center(1), center(2));
+ m_grabbers[1].center = Vec3d(m_box.max(0), center(1), center(2));
+ ::memcpy((void*)m_grabbers[0].color, (const void*)&AXES_COLOR[0], 3 * sizeof(float));
+ ::memcpy((void*)m_grabbers[1].color, (const void*)&AXES_COLOR[0], 3 * sizeof(float));
+
+ // y axis
+ m_grabbers[2].center = Vec3d(center(0), m_box.min(1), center(2));
+ m_grabbers[3].center = Vec3d(center(0), m_box.max(1), center(2));
+ ::memcpy((void*)m_grabbers[2].color, (const void*)&AXES_COLOR[1], 3 * sizeof(float));
+ ::memcpy((void*)m_grabbers[3].color, (const void*)&AXES_COLOR[1], 3 * sizeof(float));
+
+ // z axis
+ m_grabbers[4].center = Vec3d(center(0), center(1), m_box.min(2));
+ m_grabbers[5].center = Vec3d(center(0), center(1), m_box.max(2));
+ ::memcpy((void*)m_grabbers[4].color, (const void*)&AXES_COLOR[2], 3 * sizeof(float));
+ ::memcpy((void*)m_grabbers[5].color, (const void*)&AXES_COLOR[2], 3 * sizeof(float));
+
+ // uniform
+ m_grabbers[6].center = Vec3d(m_box.min(0), m_box.min(1), m_box.min(2));
+ m_grabbers[7].center = Vec3d(m_box.max(0), m_box.min(1), m_box.min(2));
+ m_grabbers[8].center = Vec3d(m_box.max(0), m_box.max(1), m_box.min(2));
+ m_grabbers[9].center = Vec3d(m_box.min(0), m_box.max(1), m_box.min(2));
+ for (int i = 6; i < 10; ++i)
+ {
+ ::memcpy((void*)m_grabbers[i].color, (const void*)m_highlight_color, 3 * sizeof(float));
+ }
+
+ ::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f);
+
+ if (m_hover_id == -1)
+ {
+ // draw box
+ ::glColor3fv(m_base_color);
+ render_box(m_box);
+ // draw connections
+ if (m_grabbers[0].enabled && m_grabbers[1].enabled)
+ {
+ ::glColor3fv(m_grabbers[0].color);
+ render_grabbers_connection(0, 1);
+ }
+ if (m_grabbers[2].enabled && m_grabbers[3].enabled)
+ {
+ ::glColor3fv(m_grabbers[2].color);
+ render_grabbers_connection(2, 3);
+ }
+ if (m_grabbers[4].enabled && m_grabbers[5].enabled)
+ {
+ ::glColor3fv(m_grabbers[4].color);
+ render_grabbers_connection(4, 5);
+ }
+ // draw grabbers
+ render_grabbers(m_box);
+ }
+ else if ((m_hover_id == 0) || (m_hover_id == 1))
+ {
+ // draw starting box
+ if (m_show_starting_box)
+ {
+ ::glColor3fv(m_base_color);
+ render_box(m_starting_box);
+ }
+ // draw current box
+ ::glColor3fv(m_drag_color);
+ render_box(m_box);
+ // draw connection
+ ::glColor3fv(m_grabbers[0].color);
+ render_grabbers_connection(0, 1);
+ // draw grabbers
+ m_grabbers[0].render(true, m_box);
+ m_grabbers[1].render(true, m_box);
+ }
+ else if ((m_hover_id == 2) || (m_hover_id == 3))
+ {
+ // draw starting box
+ if (m_show_starting_box)
+ {
+ ::glColor3fv(m_base_color);
+ render_box(m_starting_box);
+ }
+ // draw current box
+ ::glColor3fv(m_drag_color);
+ render_box(m_box);
+ // draw connection
+ ::glColor3fv(m_grabbers[2].color);
+ render_grabbers_connection(2, 3);
+ // draw grabbers
+ m_grabbers[2].render(true, m_box);
+ m_grabbers[3].render(true, m_box);
+ }
+ else if ((m_hover_id == 4) || (m_hover_id == 5))
+ {
+ // draw starting box
+ if (m_show_starting_box)
+ {
+ ::glColor3fv(m_base_color);
+ render_box(m_starting_box);
+ }
+ // draw current box
+ ::glColor3fv(m_drag_color);
+ render_box(m_box);
+ // draw connection
+ ::glColor3fv(m_grabbers[4].color);
+ render_grabbers_connection(4, 5);
+ // draw grabbers
+ m_grabbers[4].render(true, m_box);
+ m_grabbers[5].render(true, m_box);
+ }
+ else if (m_hover_id >= 6)
+ {
+ // draw starting box
+ if (m_show_starting_box)
+ {
+ ::glColor3fv(m_base_color);
+ render_box(m_starting_box);
+ }
+ // draw current box
+ ::glColor3fv(m_drag_color);
+ render_box(m_box);
+ // draw grabbers
+ for (int i = 6; i < 10; ++i)
+ {
+ m_grabbers[i].render(true, m_box);
+ }
+ }
+}
+
+void GLGizmoScale3D::on_render_for_picking(const BoundingBoxf3& box) const
+{
+ ::glDisable(GL_DEPTH_TEST);
+
+ render_grabbers_for_picking(box);
+}
+
+void GLGizmoScale3D::render_box(const BoundingBoxf3& box) const
+{
+ // bottom face
+ ::glBegin(GL_LINE_LOOP);
+ ::glVertex3f((GLfloat)box.min(0), (GLfloat)box.min(1), (GLfloat)box.min(2));
+ ::glVertex3f((GLfloat)box.min(0), (GLfloat)box.max(1), (GLfloat)box.min(2));
+ ::glVertex3f((GLfloat)box.max(0), (GLfloat)box.max(1), (GLfloat)box.min(2));
+ ::glVertex3f((GLfloat)box.max(0), (GLfloat)box.min(1), (GLfloat)box.min(2));
+ ::glEnd();
+
+ // top face
+ ::glBegin(GL_LINE_LOOP);
+ ::glVertex3f((GLfloat)box.min(0), (GLfloat)box.min(1), (GLfloat)box.max(2));
+ ::glVertex3f((GLfloat)box.min(0), (GLfloat)box.max(1), (GLfloat)box.max(2));
+ ::glVertex3f((GLfloat)box.max(0), (GLfloat)box.max(1), (GLfloat)box.max(2));
+ ::glVertex3f((GLfloat)box.max(0), (GLfloat)box.min(1), (GLfloat)box.max(2));
+ ::glEnd();
+
+ // vertical edges
+ ::glBegin(GL_LINES);
+ ::glVertex3f((GLfloat)box.min(0), (GLfloat)box.min(1), (GLfloat)box.min(2)); ::glVertex3f((GLfloat)box.min(0), (GLfloat)box.min(1), (GLfloat)box.max(2));
+ ::glVertex3f((GLfloat)box.min(0), (GLfloat)box.max(1), (GLfloat)box.min(2)); ::glVertex3f((GLfloat)box.min(0), (GLfloat)box.max(1), (GLfloat)box.max(2));
+ ::glVertex3f((GLfloat)box.max(0), (GLfloat)box.max(1), (GLfloat)box.min(2)); ::glVertex3f((GLfloat)box.max(0), (GLfloat)box.max(1), (GLfloat)box.max(2));
+ ::glVertex3f((GLfloat)box.max(0), (GLfloat)box.min(1), (GLfloat)box.min(2)); ::glVertex3f((GLfloat)box.max(0), (GLfloat)box.min(1), (GLfloat)box.max(2));
+ ::glEnd();
+}
+
+void GLGizmoScale3D::render_grabbers_connection(unsigned int id_1, unsigned int id_2) const
+{
+ unsigned int grabbers_count = (unsigned int)m_grabbers.size();
+ if ((id_1 < grabbers_count) && (id_2 < grabbers_count))
+ {
+ ::glBegin(GL_LINES);
+ ::glVertex3f((GLfloat)m_grabbers[id_1].center(0), (GLfloat)m_grabbers[id_1].center(1), (GLfloat)m_grabbers[id_1].center(2));
+ ::glVertex3f((GLfloat)m_grabbers[id_2].center(0), (GLfloat)m_grabbers[id_2].center(1), (GLfloat)m_grabbers[id_2].center(2));
+ ::glEnd();
+ }
+}
+
+void GLGizmoScale3D::do_scale_x(const Linef3& mouse_ray)
+{
+ double ratio = calc_ratio(1, mouse_ray, m_starting_box.center());
+
+ if (ratio > 0.0)
+ m_scale(0) = m_starting_scale(0) * ratio;
+}
+
+void GLGizmoScale3D::do_scale_y(const Linef3& mouse_ray)
+{
+ double ratio = calc_ratio(2, mouse_ray, m_starting_box.center());
+
+ if (ratio > 0.0)
+ m_scale(0) = m_starting_scale(1) * ratio; // << this is temporary
+// m_scale(1) = m_starting_scale(1) * ratio;
+}
+
+void GLGizmoScale3D::do_scale_z(const Linef3& mouse_ray)
+{
+ double ratio = calc_ratio(1, mouse_ray, m_starting_box.center());
+
+ if (ratio > 0.0)
+ m_scale(0) = m_starting_scale(2) * ratio; // << this is temporary
+// m_scale(2) = m_starting_scale(2) * ratio;
+}
+
+void GLGizmoScale3D::do_scale_uniform(const Linef3& mouse_ray)
+{
+ Vec3d center = m_starting_box.center();
+ center(2) = m_box.min(2);
+ double ratio = calc_ratio(0, mouse_ray, center);
+
+ if (ratio > 0.0)
+ m_scale = m_starting_scale * ratio;
+}
+
+double GLGizmoScale3D::calc_ratio(unsigned int preferred_plane_id, const Linef3& mouse_ray, const Vec3d& center) const
+{
+ double ratio = 0.0;
+
+ Vec3d starting_vec = m_starting_drag_position - center;
+ double len_starting_vec = starting_vec.norm();
+ if (len_starting_vec == 0.0)
+ return ratio;
+
+ Vec3d starting_vec_dir = starting_vec.normalized();
+ Vec3d mouse_dir = mouse_ray.unit_vector();
+
+ unsigned int plane_id = select_best_plane(mouse_dir, preferred_plane_id);
+ // ratio is given by the projection of the calculated intersection on the starting vector divided by the starting vector length
+ switch (plane_id)
+ {
+ case 0:
+ {
+ ratio = starting_vec_dir.dot(intersection_on_plane_xy(mouse_ray, center)) / len_starting_vec;
+ break;
+ }
+ case 1:
+ {
+ ratio = starting_vec_dir.dot(intersection_on_plane_xz(mouse_ray, center)) / len_starting_vec;
+ break;
+ }
+ case 2:
+ {
+ ratio = starting_vec_dir.dot(intersection_on_plane_yz(mouse_ray, center)) / len_starting_vec;
+ break;
+ }
+ }
+
+ return ratio;
+}
+
+const double GLGizmoMove3D::Offset = 10.0;
+
+GLGizmoMove3D::GLGizmoMove3D(GLCanvas3D& parent)
+ : GLGizmoBase(parent)
+ , m_position(Vec3d::Zero())
+ , m_starting_drag_position(Vec3d::Zero())
+ , m_starting_box_center(Vec3d::Zero())
+ , m_starting_box_bottom_center(Vec3d::Zero())
+{
+}
+
+bool GLGizmoMove3D::on_init()
+{
+ std::string path = resources_dir() + "/icons/overlay/";
+
+ std::string filename = path + "move_off.png";
+ if (!m_textures[Off].load_from_file(filename, false))
+ return false;
+
+ filename = path + "move_hover.png";
+ if (!m_textures[Hover].load_from_file(filename, false))
+ return false;
+
+ filename = path + "move_on.png";
+ if (!m_textures[On].load_from_file(filename, false))
+ return false;
+
+ for (int i = 0; i < 3; ++i)
+ {
+ m_grabbers.push_back(Grabber());
+ }
+
+ return true;
+}
+
+void GLGizmoMove3D::on_start_dragging(const BoundingBoxf3& box)
+{
+ if (m_hover_id != -1)
+ {
+ m_starting_drag_position = m_grabbers[m_hover_id].center;
+ m_starting_box_center = box.center();
+ m_starting_box_bottom_center = box.center();
+ m_starting_box_bottom_center(2) = box.min(2);
+ }
+}
+
+void GLGizmoMove3D::on_update(const Linef3& mouse_ray)
+{
+ if (m_hover_id == 0)
+ m_position(0) = 2.0 * m_starting_box_center(0) + calc_projection(X, 1, mouse_ray) - m_starting_drag_position(0);
+ else if (m_hover_id == 1)
+ m_position(1) = 2.0 * m_starting_box_center(1) + calc_projection(Y, 2, mouse_ray) - m_starting_drag_position(1);
+ else if (m_hover_id == 2)
+ m_position(2) = 2.0 * m_starting_box_bottom_center(2) + calc_projection(Z, 1, mouse_ray) - m_starting_drag_position(2);
+}
+
+void GLGizmoMove3D::on_render(const BoundingBoxf3& box) const
+{
+ if (m_grabbers[0].dragging)
+ set_tooltip("X: " + format(m_position(0), 2));
+ else if (m_grabbers[1].dragging)
+ set_tooltip("Y: " + format(m_position(1), 2));
+ else if (m_grabbers[2].dragging)
+ set_tooltip("Z: " + format(m_position(2), 2));
+
+ ::glEnable(GL_DEPTH_TEST);
+
+ const Vec3d& center = box.center();
+
+ // x axis
+ m_grabbers[0].center = Vec3d(box.max(0) + Offset, center(1), center(2));
+ ::memcpy((void*)m_grabbers[0].color, (const void*)&AXES_COLOR[0], 3 * sizeof(float));
+
+ // y axis
+ m_grabbers[1].center = Vec3d(center(0), box.max(1) + Offset, center(2));
+ ::memcpy((void*)m_grabbers[1].color, (const void*)&AXES_COLOR[1], 3 * sizeof(float));
+
+ // z axis
+ m_grabbers[2].center = Vec3d(center(0), center(1), box.max(2) + Offset);
+ ::memcpy((void*)m_grabbers[2].color, (const void*)&AXES_COLOR[2], 3 * sizeof(float));
+
+ ::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f);
+
+ if (m_hover_id == -1)
+ {
+ // draw axes
+ for (unsigned int i = 0; i < 3; ++i)
+ {
+ if (m_grabbers[i].enabled)
+ {
+ ::glColor3fv(AXES_COLOR[i]);
+ ::glBegin(GL_LINES);
+ ::glVertex3f(center(0), center(1), center(2));
+ ::glVertex3f((GLfloat)m_grabbers[i].center(0), (GLfloat)m_grabbers[i].center(1), (GLfloat)m_grabbers[i].center(2));
+ ::glEnd();
+ }
+ }
+
+ // draw grabbers
+ render_grabbers(box);
+ }
+ else
+ {
+ // draw axis
+ ::glColor3fv(AXES_COLOR[m_hover_id]);
+ ::glBegin(GL_LINES);
+ ::glVertex3f(center(0), center(1), center(2));
+ ::glVertex3f((GLfloat)m_grabbers[m_hover_id].center(0), (GLfloat)m_grabbers[m_hover_id].center(1), (GLfloat)m_grabbers[m_hover_id].center(2));
+ ::glEnd();
+
+ // draw grabber
+ m_grabbers[m_hover_id].render(true, box);
+ }
+}
+
+void GLGizmoMove3D::on_render_for_picking(const BoundingBoxf3& box) const
+{
+ ::glDisable(GL_DEPTH_TEST);
+
+ render_grabbers_for_picking(box);
+}
+
+double GLGizmoMove3D::calc_projection(Axis axis, unsigned int preferred_plane_id, const Linef3& mouse_ray) const
+{
+ double projection = 0.0;
+
+ Vec3d starting_vec = (axis == Z) ? m_starting_drag_position - m_starting_box_bottom_center : m_starting_drag_position - m_starting_box_center;
+ double len_starting_vec = starting_vec.norm();
+ if (len_starting_vec == 0.0)
+ return projection;
+
+ Vec3d starting_vec_dir = starting_vec.normalized();
+ Vec3d mouse_dir = mouse_ray.unit_vector();
+
+ unsigned int plane_id = select_best_plane(mouse_dir, preferred_plane_id);
+
+ switch (plane_id)
+ {
+ case 0:
+ {
+ projection = starting_vec_dir.dot(intersection_on_plane_xy(mouse_ray, (axis == Z) ? m_starting_box_bottom_center : m_starting_box_center));
+ break;
+ }
+ case 1:
+ {
+ projection = starting_vec_dir.dot(intersection_on_plane_xz(mouse_ray, (axis == Z) ? m_starting_box_bottom_center : m_starting_box_center));
+ break;
+ }
+ case 2:
+ {
+ projection = starting_vec_dir.dot(intersection_on_plane_yz(mouse_ray, (axis == Z) ? m_starting_box_bottom_center : m_starting_box_center));
+ break;
+ }
+ }
+
+ return projection;
+}
+
+GLGizmoFlatten::GLGizmoFlatten(GLCanvas3D& parent)
+ : GLGizmoBase(parent)
+ , m_normal(Vec3d::Zero())
+ , m_starting_center(Vec3d::Zero())
+{
+}
+
+bool GLGizmoFlatten::on_init()
+{
+ std::string path = resources_dir() + "/icons/overlay/";
+
+ std::string filename = path + "layflat_off.png";
+ if (!m_textures[Off].load_from_file(filename, false))
+ return false;
+
+ filename = path + "layflat_hover.png";
+ if (!m_textures[Hover].load_from_file(filename, false))
+ return false;
+
+ filename = path + "layflat_on.png";
+ if (!m_textures[On].load_from_file(filename, false))
+ return false;
+
+ return true;
+}
+
+void GLGizmoFlatten::on_start_dragging(const BoundingBoxf3& box)
+{
+ if (m_hover_id != -1)
+ {
+ m_normal = m_planes[m_hover_id].normal;
+ m_starting_center = box.center();
+ }
+}
+
+void GLGizmoFlatten::on_render(const BoundingBoxf3& box) const
+{
+ // the dragged_offset is a vector measuring where was the object moved
+ // with the gizmo being on. This is reset in set_flattening_data and
+ // does not work correctly when there are multiple copies.
+ Vec3d dragged_offset(Vec3d::Zero());
+ if (m_dragging)
+ dragged_offset = box.center() - m_starting_center;
+
+ ::glEnable(GL_BLEND);
+ ::glEnable(GL_DEPTH_TEST);
+
+ for (int i=0; i<(int)m_planes.size(); ++i) {
+ if (i == m_hover_id)
+ ::glColor4f(0.9f, 0.9f, 0.9f, 0.75f);
+ else
+ ::glColor4f(0.9f, 0.9f, 0.9f, 0.5f);
+
+#if ENABLE_MODELINSTANCE_3D_OFFSET
+ for (Vec3d offset : m_instances_positions) {
+ offset += dragged_offset;
+#else
+ for (Vec2d offset : m_instances_positions) {
+ offset += to_2d(dragged_offset);
+#endif // ENABLE_MODELINSTANCE_3D_OFFSET
+ ::glPushMatrix();
+#if ENABLE_MODELINSTANCE_3D_OFFSET
+ ::glTranslated(offset(0), offset(1), offset(2));
+#else
+ ::glTranslatef((GLfloat)offset(0), (GLfloat)offset(1), 0.0f);
+#endif // ENABLE_MODELINSTANCE_3D_OFFSET
+ ::glBegin(GL_POLYGON);
+ for (const Vec3d& vertex : m_planes[i].vertices)
+ ::glVertex3f((GLfloat)vertex(0), (GLfloat)vertex(1), (GLfloat)vertex(2));
+ ::glEnd();
+ ::glPopMatrix();
+ }
+ }
+
+ ::glDisable(GL_BLEND);
+}
+
+void GLGizmoFlatten::on_render_for_picking(const BoundingBoxf3& box) const
+{
+ ::glEnable(GL_DEPTH_TEST);
+
+ for (unsigned int i = 0; i < m_planes.size(); ++i)
+ {
+ ::glColor3f(1.0f, 1.0f, picking_color_component(i));
+#if ENABLE_MODELINSTANCE_3D_OFFSET
+ for (const Vec3d& offset : m_instances_positions) {
+#else
+ for (const Vec2d& offset : m_instances_positions) {
+#endif // ENABLE_MODELINSTANCE_3D_OFFSET
+ ::glPushMatrix();
+#if ENABLE_MODELINSTANCE_3D_OFFSET
+ ::glTranslated(offset(0), offset(1), offset(2));
+#else
+ ::glTranslatef((GLfloat)offset(0), (GLfloat)offset(1), 0.0f);
+#endif // ENABLE_MODELINSTANCE_3D_OFFSET
+ ::glBegin(GL_POLYGON);
+ for (const Vec3d& vertex : m_planes[i].vertices)
+ ::glVertex3f((GLfloat)vertex(0), (GLfloat)vertex(1), (GLfloat)vertex(2));
+ ::glEnd();
+ ::glPopMatrix();
+ }
+ }
+}
+
+void GLGizmoFlatten::set_flattening_data(const ModelObject* model_object)
+{
+ m_model_object = model_object;
+
+ // ...and save the updated positions of the object instances:
+ if (m_model_object && !m_model_object->instances.empty()) {
+ m_instances_positions.clear();
+ for (const auto* instance : m_model_object->instances)
+#if ENABLE_MODELINSTANCE_3D_OFFSET
+ m_instances_positions.emplace_back(instance->get_offset());
+#else
+ m_instances_positions.emplace_back(instance->offset);
+#endif // ENABLE_MODELINSTANCE_3D_OFFSET
+ }
+
+ if (is_plane_update_necessary())
+ update_planes();
+}
+
+void GLGizmoFlatten::update_planes()
+{
+ TriangleMesh ch;
+ for (const ModelVolume* vol : m_model_object->volumes)
+ ch.merge(vol->get_convex_hull());
+ ch = ch.convex_hull_3d();
+ ch.scale(m_model_object->instances.front()->scaling_factor);
+ ch.rotate_z(m_model_object->instances.front()->rotation);
+
+ m_planes.clear();
+
+ // Now we'll go through all the facets and append Points of facets sharing the same normal:
+ const int num_of_facets = ch.stl.stats.number_of_facets;
+ std::vector<int> facet_queue(num_of_facets, 0);
+ std::vector<bool> facet_visited(num_of_facets, false);
+ int facet_queue_cnt = 0;
+ const stl_normal* normal_ptr = nullptr;
+ while (1) {
+ // Find next unvisited triangle:
+ int facet_idx = 0;
+ for (; facet_idx < num_of_facets; ++ facet_idx)
+ if (!facet_visited[facet_idx]) {
+ facet_queue[facet_queue_cnt ++] = facet_idx;
+ facet_visited[facet_idx] = true;
+ normal_ptr = &ch.stl.facet_start[facet_idx].normal;
+ m_planes.emplace_back();
+ break;
+ }
+ if (facet_idx == num_of_facets)
+ break; // Everything was visited already
+
+ while (facet_queue_cnt > 0) {
+ int facet_idx = facet_queue[-- facet_queue_cnt];
+ const stl_normal& this_normal = ch.stl.facet_start[facet_idx].normal;
+ if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) {
+ stl_vertex* first_vertex = ch.stl.facet_start[facet_idx].vertex;
+ for (int j=0; j<3; ++j)
+ m_planes.back().vertices.emplace_back(first_vertex[j](0), first_vertex[j](1), first_vertex[j](2));
+
+ facet_visited[facet_idx] = true;
+ for (int j = 0; j < 3; ++ j) {
+ int neighbor_idx = ch.stl.neighbors_start[facet_idx].neighbor[j];
+ if (! facet_visited[neighbor_idx])
+ facet_queue[facet_queue_cnt ++] = neighbor_idx;
+ }
+ }
+ }
+ m_planes.back().normal = Vec3d((double)(*normal_ptr)(0), (double)(*normal_ptr)(1), (double)(*normal_ptr)(2));
+
+ // if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected anyway):
+ if (m_planes.back().vertices.size() == 3 &&
+ (m_planes.back().vertices[0] - m_planes.back().vertices[1]).norm() < 1.f
+ || (m_planes.back().vertices[0] - m_planes.back().vertices[2]).norm() < 1.f)
+ m_planes.pop_back();
+ }
+
+ // Now we'll go through all the polygons, transform the points into xy plane to process them:
+ for (unsigned int polygon_id=0; polygon_id < m_planes.size(); ++polygon_id) {
+ Pointf3s& polygon = m_planes[polygon_id].vertices;
+ const Vec3d& normal = m_planes[polygon_id].normal;
+
+ // We are going to rotate about z and y to flatten the plane
+ float angle_z = 0.f;
+ float angle_y = 0.f;
+ if (std::abs(normal(1)) > 0.001)
+ angle_z = -atan2(normal(1), normal(0)); // angle to rotate so that normal ends up in xz-plane
+ if (std::abs(normal(0)*cos(angle_z) - normal(1)*sin(angle_z)) > 0.001)
+ angle_y = -atan2(normal(0)*cos(angle_z) - normal(1)*sin(angle_z), normal(2)); // angle to rotate to make normal point upwards
+ else {
+ // In case it already was in z-direction, we must ensure it is not the wrong way:
+ angle_y = normal(2) > 0.f ? 0.f : -PI;
+ }
+
+ // Rotate all points to the xy plane:
+ Transform3d m = Transform3d::Identity();
+ m.rotate(Eigen::AngleAxisd((double)angle_y, Vec3d::UnitY()));
+ m.rotate(Eigen::AngleAxisd((double)angle_z, Vec3d::UnitZ()));
+ polygon = transform(polygon, m);
+
+ polygon = Slic3r::Geometry::convex_hull(polygon); // To remove the inner points
+
+ // We will calculate area of the polygon and discard ones that are too small
+ // The limit is more forgiving in case the normal is in the direction of the coordinate axes
+ const float minimal_area = (std::abs(normal(0)) > 0.999f || std::abs(normal(1)) > 0.999f || std::abs(normal(2)) > 0.999f) ? 1.f : 20.f;
+ float& area = m_planes[polygon_id].area;
+ area = 0.f;
+ for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula
+ area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1);
+ area = std::abs(area / 2.f);
+ if (area < minimal_area) {
+ m_planes.erase(m_planes.begin()+(polygon_id--));
+ continue;
+ }
+
+ // We will shrink the polygon a little bit so it does not touch the object edges:
+ Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0));
+ centroid /= (double)polygon.size();
+ for (auto& vertex : polygon)
+ vertex = 0.9f*vertex + 0.1f*centroid;
+
+ // Polygon is now simple and convex, we'll round the corners to make them look nicer.
+ // The algorithm takes a vertex, calculates middles of respective sides and moves the vertex
+ // towards their average (controlled by 'aggressivity'). This is repeated k times.
+ // In next iterations, the neighbours are not always taken at the middle (to increase the
+ // rounding effect at the corners, where we need it most).
+ const unsigned int k = 10; // number of iterations
+ const float aggressivity = 0.2f; // agressivity
+ const unsigned int N = polygon.size();
+ std::vector<std::pair<unsigned int, unsigned int>> neighbours;
+ if (k != 0) {
+ Pointf3s points_out(2*k*N); // vector long enough to store the future vertices
+ for (unsigned int j=0; j<N; ++j) {
+ points_out[j*2*k] = polygon[j];
+ neighbours.push_back(std::make_pair((int)(j*2*k-k) < 0 ? (N-1)*2*k+k : j*2*k-k, j*2*k+k));
+ }
+
+ for (unsigned int i=0; i<k; ++i) {
+ // Calculate middle of each edge so that neighbours points to something useful:
+ for (unsigned int j=0; j<N; ++j)
+ if (i==0)
+ points_out[j*2*k+k] = 0.5f * (points_out[j*2*k] + points_out[j==N-1 ? 0 : (j+1)*2*k]);
+ else {
+ float r = 0.2+0.3/(k-1)*i; // the neighbours are not always taken in the middle
+ points_out[neighbours[j].first] = r*points_out[j*2*k] + (1-r) * points_out[neighbours[j].first-1];
+ points_out[neighbours[j].second] = r*points_out[j*2*k] + (1-r) * points_out[neighbours[j].second+1];
+ }
+ // Now we have a triangle and valid neighbours, we can do an iteration:
+ for (unsigned int j=0; j<N; ++j)
+ points_out[2*k*j] = (1-aggressivity) * points_out[2*k*j] +
+ aggressivity*0.5f*(points_out[neighbours[j].first] + points_out[neighbours[j].second]);
+
+ for (auto& n : neighbours) {
+ ++n.first;
+ --n.second;
+ }
+ }
+ polygon = points_out; // replace the coarse polygon with the smooth one that we just created
+ }
+
+ // Transform back to 3D;
+ for (auto& b : polygon) {
+ b(2) += 0.1f; // raise a bit above the object surface to avoid flickering
+ }
+
+ m = m.inverse();
+ polygon = transform(polygon, m);
+ }
+
+ // We'll sort the planes by area and only keep the 254 largest ones (because of the picking pass limitations):
+ std::sort(m_planes.rbegin(), m_planes.rend(), [](const PlaneData& a, const PlaneData& b) { return a.area < b.area; });
+ m_planes.resize(std::min((int)m_planes.size(), 254));
+
+ // Planes are finished - let's save what we calculated it from:
+ m_source_data.bounding_boxes.clear();
+ for (const auto& vol : m_model_object->volumes)
+ m_source_data.bounding_boxes.push_back(vol->get_convex_hull().bounding_box());
+ m_source_data.scaling_factor = m_model_object->instances.front()->scaling_factor;
+ m_source_data.rotation = m_model_object->instances.front()->rotation;
+ const float* first_vertex = m_model_object->volumes.front()->get_convex_hull().first_vertex();
+ m_source_data.mesh_first_point = Vec3d((double)first_vertex[0], (double)first_vertex[1], (double)first_vertex[2]);
+}
+
+// Check if the bounding boxes of each volume's convex hull is the same as before
+// and that scaling and rotation has not changed. In that case we don't have to recalculate it.
+bool GLGizmoFlatten::is_plane_update_necessary() const
+{
+ if (m_state != On || !m_model_object || m_model_object->instances.empty())
+ return false;
+
+ if (m_model_object->volumes.size() != m_source_data.bounding_boxes.size()
+ || m_model_object->instances.front()->scaling_factor != m_source_data.scaling_factor
+ || m_model_object->instances.front()->rotation != m_source_data.rotation)
+ return true;
+
+ // now compare the bounding boxes:
+ for (unsigned int i=0; i<m_model_object->volumes.size(); ++i)
+ if (m_model_object->volumes[i]->get_convex_hull().bounding_box() != m_source_data.bounding_boxes[i])
+ return true;
+
+ const float* first_vertex = m_model_object->volumes.front()->get_convex_hull().first_vertex();
+ Vec3d first_point((double)first_vertex[0], (double)first_vertex[1], (double)first_vertex[2]);
+ if (first_point != m_source_data.mesh_first_point)
+ return true;
+
+ return false;
+}
+
+Vec3d GLGizmoFlatten::get_flattening_normal() const {
+ Vec3d normal = m_model_object->instances.front()->world_matrix(true).matrix().block(0, 0, 3, 3).inverse() * m_normal;
+ m_normal = Vec3d::Zero();
+ return normal.normalized();
+}
+
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/GLGizmo.hpp b/src/slic3r/GUI/GLGizmo.hpp
new file mode 100644
index 000000000..2430b5af5
--- /dev/null
+++ b/src/slic3r/GUI/GLGizmo.hpp
@@ -0,0 +1,366 @@
+#ifndef slic3r_GLGizmo_hpp_
+#define slic3r_GLGizmo_hpp_
+
+#include "../../slic3r/GUI/GLTexture.hpp"
+#include "../../libslic3r/Point.hpp"
+#include "../../libslic3r/BoundingBox.hpp"
+
+#include <vector>
+
+namespace Slic3r {
+
+class BoundingBoxf3;
+class Linef3;
+class ModelObject;
+
+namespace GUI {
+
+class GLCanvas3D;
+
+class GLGizmoBase
+{
+protected:
+ struct Grabber
+ {
+ static const float SizeFactor;
+ static const float MinHalfSize;
+ static const float DraggingScaleFactor;
+
+ Vec3d center;
+ Vec3d angles;
+ float color[3];
+ bool enabled;
+ bool dragging;
+
+ Grabber();
+
+ void render(bool hover, const BoundingBoxf3& box) const;
+ void render_for_picking(const BoundingBoxf3& box) const { render(box, color, false); }
+
+ private:
+ void render(const BoundingBoxf3& box, const float* render_color, bool use_lighting) const;
+ void render_face(float half_size) const;
+ };
+
+public:
+ enum EState
+ {
+ Off,
+ Hover,
+ On,
+ Num_States
+ };
+
+protected:
+ GLCanvas3D& m_parent;
+
+ int m_group_id;
+ EState m_state;
+ // textures are assumed to be square and all with the same size in pixels, no internal check is done
+ GLTexture m_textures[Num_States];
+ int m_hover_id;
+ bool m_dragging;
+ float m_base_color[3];
+ float m_drag_color[3];
+ float m_highlight_color[3];
+ mutable std::vector<Grabber> m_grabbers;
+
+public:
+ explicit GLGizmoBase(GLCanvas3D& parent);
+ virtual ~GLGizmoBase() {}
+
+ bool init() { return on_init(); }
+
+ int get_group_id() const { return m_group_id; }
+ void set_group_id(int id) { m_group_id = id; }
+
+ EState get_state() const { return m_state; }
+ void set_state(EState state) { m_state = state; on_set_state(); }
+
+ unsigned int get_texture_id() const { return m_textures[m_state].get_id(); }
+ int get_textures_size() const { return m_textures[Off].get_width(); }
+
+ int get_hover_id() const { return m_hover_id; }
+ void set_hover_id(int id);
+
+ void set_highlight_color(const float* color);
+
+ void enable_grabber(unsigned int id);
+ void disable_grabber(unsigned int id);
+
+ void start_dragging(const BoundingBoxf3& box);
+ void stop_dragging();
+ bool is_dragging() const { return m_dragging; }
+
+ void update(const Linef3& mouse_ray);
+
+ void render(const BoundingBoxf3& box) const { on_render(box); }
+ void render_for_picking(const BoundingBoxf3& box) const { on_render_for_picking(box); }
+
+protected:
+ virtual bool on_init() = 0;
+ virtual void on_set_state() {}
+ virtual void on_set_hover_id() {}
+ virtual void on_enable_grabber(unsigned int id) {}
+ virtual void on_disable_grabber(unsigned int id) {}
+ virtual void on_start_dragging(const BoundingBoxf3& box) {}
+ virtual void on_stop_dragging() {}
+ virtual void on_update(const Linef3& mouse_ray) = 0;
+ virtual void on_render(const BoundingBoxf3& box) const = 0;
+ virtual void on_render_for_picking(const BoundingBoxf3& box) const = 0;
+
+ float picking_color_component(unsigned int id) const;
+ void render_grabbers(const BoundingBoxf3& box) const;
+ void render_grabbers_for_picking(const BoundingBoxf3& box) const;
+
+ void set_tooltip(const std::string& tooltip) const;
+ std::string format(float value, unsigned int decimals) const;
+};
+
+class GLGizmoRotate : public GLGizmoBase
+{
+ static const float Offset;
+ static const unsigned int CircleResolution;
+ static const unsigned int AngleResolution;
+ static const unsigned int ScaleStepsCount;
+ static const float ScaleStepRad;
+ static const unsigned int ScaleLongEvery;
+ static const float ScaleLongTooth;
+ static const float ScaleShortTooth;
+ static const unsigned int SnapRegionsCount;
+ static const float GrabberOffset;
+
+public:
+ enum Axis : unsigned char
+ {
+ X,
+ Y,
+ Z
+ };
+
+private:
+ Axis m_axis;
+ double m_angle;
+
+ mutable Vec3d m_center;
+ mutable float m_radius;
+
+public:
+ GLGizmoRotate(GLCanvas3D& parent, Axis axis);
+
+ double get_angle() const { return m_angle; }
+ void set_angle(double angle);
+
+protected:
+ virtual bool on_init();
+ virtual void on_start_dragging(const BoundingBoxf3& box);
+ virtual void on_update(const Linef3& mouse_ray);
+ virtual void on_render(const BoundingBoxf3& box) const;
+ virtual void on_render_for_picking(const BoundingBoxf3& box) const;
+
+private:
+ void render_circle() const;
+ void render_scale() const;
+ void render_snap_radii() const;
+ void render_reference_radius() const;
+ void render_angle() const;
+ void render_grabber(const BoundingBoxf3& box) const;
+
+ void transform_to_local() const;
+ // returns the intersection of the mouse ray with the plane perpendicular to the gizmo axis, in local coordinate
+ Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray) const;
+};
+
+class GLGizmoRotate3D : public GLGizmoBase
+{
+ std::vector<GLGizmoRotate> m_gizmos;
+
+public:
+ explicit GLGizmoRotate3D(GLCanvas3D& parent);
+
+ double get_angle_x() const { return m_gizmos[X].get_angle(); }
+ void set_angle_x(double angle) { m_gizmos[X].set_angle(angle); }
+
+ double get_angle_y() const { return m_gizmos[Y].get_angle(); }
+ void set_angle_y(double angle) { m_gizmos[Y].set_angle(angle); }
+
+ double get_angle_z() const { return m_gizmos[Z].get_angle(); }
+ void set_angle_z(double angle) { m_gizmos[Z].set_angle(angle); }
+
+protected:
+ virtual bool on_init();
+ virtual void on_set_state()
+ {
+ for (GLGizmoRotate& g : m_gizmos)
+ {
+ g.set_state(m_state);
+ }
+ }
+ virtual void on_set_hover_id()
+ {
+ for (unsigned int i = 0; i < 3; ++i)
+ {
+ m_gizmos[i].set_hover_id((m_hover_id == i) ? 0 : -1);
+ }
+ }
+ virtual void on_enable_grabber(unsigned int id)
+ {
+ if ((0 <= id) && (id < 3))
+ m_gizmos[id].enable_grabber(0);
+ }
+ virtual void on_disable_grabber(unsigned int id)
+ {
+ if ((0 <= id) && (id < 3))
+ m_gizmos[id].disable_grabber(0);
+ }
+ virtual void on_start_dragging(const BoundingBoxf3& box);
+ virtual void on_stop_dragging();
+ virtual void on_update(const Linef3& mouse_ray)
+ {
+ for (GLGizmoRotate& g : m_gizmos)
+ {
+ g.update(mouse_ray);
+ }
+ }
+ virtual void on_render(const BoundingBoxf3& box) const;
+ virtual void on_render_for_picking(const BoundingBoxf3& box) const
+ {
+ for (const GLGizmoRotate& g : m_gizmos)
+ {
+ g.render_for_picking(box);
+ }
+ }
+};
+
+class GLGizmoScale3D : public GLGizmoBase
+{
+ static const float Offset;
+ static const Vec3d OffsetVec;
+
+ mutable BoundingBoxf3 m_box;
+
+ Vec3d m_scale;
+
+ Vec3d m_starting_scale;
+ Vec3d m_starting_drag_position;
+ bool m_show_starting_box;
+ BoundingBoxf3 m_starting_box;
+
+public:
+ explicit GLGizmoScale3D(GLCanvas3D& parent);
+
+ double get_scale_x() const { return m_scale(0); }
+ void set_scale_x(double scale) { m_starting_scale(0) = scale; }
+
+ double get_scale_y() const { return m_scale(1); }
+ void set_scale_y(double scale) { m_starting_scale(1) = scale; }
+
+ double get_scale_z() const { return m_scale(2); }
+ void set_scale_z(double scale) { m_starting_scale(2) = scale; }
+
+ void set_scale(double scale) { m_starting_scale = scale * Vec3d::Ones(); }
+
+protected:
+ virtual bool on_init();
+ virtual void on_start_dragging(const BoundingBoxf3& box);
+ virtual void on_stop_dragging() { m_show_starting_box = false; }
+ virtual void on_update(const Linef3& mouse_ray);
+ virtual void on_render(const BoundingBoxf3& box) const;
+ virtual void on_render_for_picking(const BoundingBoxf3& box) const;
+
+private:
+ void render_box(const BoundingBoxf3& box) const;
+ void render_grabbers_connection(unsigned int id_1, unsigned int id_2) const;
+
+ void do_scale_x(const Linef3& mouse_ray);
+ void do_scale_y(const Linef3& mouse_ray);
+ void do_scale_z(const Linef3& mouse_ray);
+ void do_scale_uniform(const Linef3& mouse_ray);
+
+ double calc_ratio(unsigned int preferred_plane_id, const Linef3& mouse_ray, const Vec3d& center) const;
+};
+
+class GLGizmoMove3D : public GLGizmoBase
+{
+ static const double Offset;
+
+ Vec3d m_position;
+ Vec3d m_starting_drag_position;
+ Vec3d m_starting_box_center;
+ Vec3d m_starting_box_bottom_center;
+
+public:
+ explicit GLGizmoMove3D(GLCanvas3D& parent);
+
+ const Vec3d& get_position() const { return m_position; }
+ void set_position(const Vec3d& position) { m_position = position; }
+
+protected:
+ virtual bool on_init();
+ virtual void on_start_dragging(const BoundingBoxf3& box);
+ virtual void on_update(const Linef3& mouse_ray);
+ virtual void on_render(const BoundingBoxf3& box) const;
+ virtual void on_render_for_picking(const BoundingBoxf3& box) const;
+
+private:
+ double calc_projection(Axis axis, unsigned int preferred_plane_id, const Linef3& mouse_ray) const;
+};
+
+class GLGizmoFlatten : public GLGizmoBase
+{
+// This gizmo does not use grabbers. The m_hover_id relates to polygon managed by the class itself.
+
+private:
+ mutable Vec3d m_normal;
+
+ struct PlaneData {
+ std::vector<Vec3d> vertices;
+ Vec3d normal;
+ float area;
+ };
+ struct SourceDataSummary {
+ std::vector<BoundingBoxf3> bounding_boxes; // bounding boxes of convex hulls of individual volumes
+ float scaling_factor;
+ float rotation;
+ Vec3d mesh_first_point;
+ };
+
+ // This holds information to decide whether recalculation is necessary:
+ SourceDataSummary m_source_data;
+
+ std::vector<PlaneData> m_planes;
+#if ENABLE_MODELINSTANCE_3D_OFFSET
+ Pointf3s m_instances_positions;
+#else
+ std::vector<Vec2d> m_instances_positions;
+#endif // ENABLE_MODELINSTANCE_3D_OFFSET
+ Vec3d m_starting_center;
+ const ModelObject* m_model_object = nullptr;
+
+ void update_planes();
+ bool is_plane_update_necessary() const;
+
+public:
+ explicit GLGizmoFlatten(GLCanvas3D& parent);
+
+ void set_flattening_data(const ModelObject* model_object);
+ Vec3d get_flattening_normal() const;
+
+protected:
+ virtual bool on_init();
+ virtual void on_start_dragging(const BoundingBoxf3& box);
+ virtual void on_update(const Linef3& mouse_ray) {}
+ virtual void on_render(const BoundingBoxf3& box) const;
+ virtual void on_render_for_picking(const BoundingBoxf3& box) const;
+ virtual void on_set_state()
+ {
+ if (m_state == On && is_plane_update_necessary())
+ update_planes();
+ }
+};
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif // slic3r_GLGizmo_hpp_
+
diff --git a/src/slic3r/GUI/GLShader.cpp b/src/slic3r/GUI/GLShader.cpp
new file mode 100644
index 000000000..e2995f7c3
--- /dev/null
+++ b/src/slic3r/GUI/GLShader.cpp
@@ -0,0 +1,256 @@
+#include <GL/glew.h>
+
+#include "GLShader.hpp"
+
+#include "../../libslic3r/Utils.hpp"
+#include <boost/nowide/fstream.hpp>
+
+#include <string>
+#include <utility>
+#include <assert.h>
+
+namespace Slic3r {
+
+GLShader::~GLShader()
+{
+ assert(fragment_program_id == 0);
+ assert(vertex_program_id == 0);
+ assert(shader_program_id == 0);
+}
+
+// A safe wrapper around glGetString to report a "N/A" string in case glGetString returns nullptr.
+inline std::string gl_get_string_safe(GLenum param)
+{
+ const char *value = (const char*)glGetString(param);
+ return std::string(value ? value : "N/A");
+}
+
+bool GLShader::load_from_text(const char *fragment_shader, const char *vertex_shader)
+{
+ std::string gl_version = gl_get_string_safe(GL_VERSION);
+ int major = atoi(gl_version.c_str());
+ //int minor = atoi(gl_version.c_str() + gl_version.find('.') + 1);
+ if (major < 2) {
+ // Cannot create a shader object on OpenGL 1.x.
+ // Form an error message.
+ std::string gl_vendor = gl_get_string_safe(GL_VENDOR);
+ std::string gl_renderer = gl_get_string_safe(GL_RENDERER);
+ std::string glsl_version = gl_get_string_safe(GL_SHADING_LANGUAGE_VERSION);
+ last_error = "Your computer does not support OpenGL shaders.\n";
+#ifdef _WIN32
+ if (gl_vendor == "Microsoft Corporation" && gl_renderer == "GDI Generic") {
+ last_error = "Windows is using a software OpenGL renderer.\n"
+ "You are either connected over remote desktop,\n"
+ "or a hardware acceleration is not available.\n";
+ }
+#endif
+ last_error += "GL version: " + gl_version + "\n";
+ last_error += "vendor: " + gl_vendor + "\n";
+ last_error += "renderer: " + gl_renderer + "\n";
+ last_error += "GLSL version: " + glsl_version + "\n";
+ return false;
+ }
+
+ if (fragment_shader != nullptr) {
+ this->fragment_program_id = glCreateShader(GL_FRAGMENT_SHADER);
+ if (this->fragment_program_id == 0) {
+ last_error = "glCreateShader(GL_FRAGMENT_SHADER) failed.";
+ return false;
+ }
+ GLint len = (GLint)strlen(fragment_shader);
+ glShaderSource(this->fragment_program_id, 1, &fragment_shader, &len);
+ glCompileShader(this->fragment_program_id);
+ GLint params;
+ glGetShaderiv(this->fragment_program_id, GL_COMPILE_STATUS, &params);
+ if (params == GL_FALSE) {
+ // Compilation failed. Get the log.
+ glGetShaderiv(this->fragment_program_id, GL_INFO_LOG_LENGTH, &params);
+ std::vector<char> msg(params);
+ glGetShaderInfoLog(this->fragment_program_id, params, &params, msg.data());
+ this->last_error = std::string("Fragment shader compilation failed:\n") + msg.data();
+ this->release();
+ return false;
+ }
+ }
+
+ if (vertex_shader != nullptr) {
+ this->vertex_program_id = glCreateShader(GL_VERTEX_SHADER);
+ if (this->vertex_program_id == 0) {
+ last_error = "glCreateShader(GL_VERTEX_SHADER) failed.";
+ this->release();
+ return false;
+ }
+ GLint len = (GLint)strlen(vertex_shader);
+ glShaderSource(this->vertex_program_id, 1, &vertex_shader, &len);
+ glCompileShader(this->vertex_program_id);
+ GLint params;
+ glGetShaderiv(this->vertex_program_id, GL_COMPILE_STATUS, &params);
+ if (params == GL_FALSE) {
+ // Compilation failed. Get the log.
+ glGetShaderiv(this->vertex_program_id, GL_INFO_LOG_LENGTH, &params);
+ std::vector<char> msg(params);
+ glGetShaderInfoLog(this->vertex_program_id, params, &params, msg.data());
+ this->last_error = std::string("Vertex shader compilation failed:\n") + msg.data();
+ this->release();
+ return false;
+ }
+ }
+
+ // Link shaders
+ this->shader_program_id = glCreateProgram();
+ if (this->shader_program_id == 0) {
+ last_error = "glCreateProgram() failed.";
+ this->release();
+ return false;
+ }
+
+ if (this->fragment_program_id)
+ glAttachShader(this->shader_program_id, this->fragment_program_id);
+ if (this->vertex_program_id)
+ glAttachShader(this->shader_program_id, this->vertex_program_id);
+ glLinkProgram(this->shader_program_id);
+
+ GLint params;
+ glGetProgramiv(this->shader_program_id, GL_LINK_STATUS, &params);
+ if (params == GL_FALSE) {
+ // Linking failed. Get the log.
+ glGetProgramiv(this->vertex_program_id, GL_INFO_LOG_LENGTH, &params);
+ std::vector<char> msg(params);
+ glGetProgramInfoLog(this->vertex_program_id, params, &params, msg.data());
+ this->last_error = std::string("Shader linking failed:\n") + msg.data();
+ this->release();
+ return false;
+ }
+
+ last_error.clear();
+ return true;
+}
+
+bool GLShader::load_from_file(const char* fragment_shader_filename, const char* vertex_shader_filename)
+{
+ const std::string& path = resources_dir() + "/shaders/";
+
+ boost::nowide::ifstream vs(path + std::string(vertex_shader_filename), boost::nowide::ifstream::binary);
+ if (!vs.good())
+ return false;
+
+ vs.seekg(0, vs.end);
+ int file_length = vs.tellg();
+ vs.seekg(0, vs.beg);
+ std::string vertex_shader(file_length, '\0');
+ vs.read(const_cast<char*>(vertex_shader.data()), file_length);
+ if (!vs.good())
+ return false;
+
+ vs.close();
+
+ boost::nowide::ifstream fs(path + std::string(fragment_shader_filename), boost::nowide::ifstream::binary);
+ if (!fs.good())
+ return false;
+
+ fs.seekg(0, fs.end);
+ file_length = fs.tellg();
+ fs.seekg(0, fs.beg);
+ std::string fragment_shader(file_length, '\0');
+ fs.read(const_cast<char*>(fragment_shader.data()), file_length);
+ if (!fs.good())
+ return false;
+
+ fs.close();
+
+ return load_from_text(fragment_shader.c_str(), vertex_shader.c_str());
+}
+
+void GLShader::release()
+{
+ if (this->shader_program_id) {
+ if (this->vertex_program_id)
+ glDetachShader(this->shader_program_id, this->vertex_program_id);
+ if (this->fragment_program_id)
+ glDetachShader(this->shader_program_id, this->fragment_program_id);
+ glDeleteProgram(this->shader_program_id);
+ this->shader_program_id = 0;
+ }
+
+ if (this->vertex_program_id) {
+ glDeleteShader(this->vertex_program_id);
+ this->vertex_program_id = 0;
+ }
+ if (this->fragment_program_id) {
+ glDeleteShader(this->fragment_program_id);
+ this->fragment_program_id = 0;
+ }
+}
+
+void GLShader::enable() const
+{
+ glUseProgram(this->shader_program_id);
+}
+
+void GLShader::disable() const
+{
+ glUseProgram(0);
+}
+
+// Return shader vertex attribute ID
+int GLShader::get_attrib_location(const char *name) const
+{
+ return this->shader_program_id ? glGetAttribLocation(this->shader_program_id, name) : -1;
+}
+
+// Return shader uniform variable ID
+int GLShader::get_uniform_location(const char *name) const
+{
+ return this->shader_program_id ? glGetUniformLocation(this->shader_program_id, name) : -1;
+}
+
+bool GLShader::set_uniform(const char *name, float value) const
+{
+ int id = this->get_uniform_location(name);
+ if (id >= 0) {
+ glUniform1fARB(id, value);
+ return true;
+ }
+ return false;
+}
+
+bool GLShader::set_uniform(const char* name, const float* matrix) const
+{
+ int id = get_uniform_location(name);
+ if (id >= 0)
+ {
+ ::glUniformMatrix4fv(id, 1, GL_FALSE, (const GLfloat*)matrix);
+ return true;
+ }
+ return false;
+}
+
+/*
+# Set shader vector
+sub SetVector
+{
+ my($self,$var,@values) = @_;
+
+ my $id = $self->Map($var);
+ return 'Unable to map $var' if (!defined($id));
+
+ my $count = scalar(@values);
+ eval('glUniform'.$count.'fARB($id,@values)');
+
+ return '';
+}
+
+# Set shader 4x4 matrix
+sub SetMatrix
+{
+ my($self,$var,$oga) = @_;
+
+ my $id = $self->Map($var);
+ return 'Unable to map $var' if (!defined($id));
+
+ glUniformMatrix4fvARB_c($id,1,0,$oga->ptr());
+ return '';
+}
+*/
+
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/GLShader.hpp b/src/slic3r/GUI/GLShader.hpp
new file mode 100644
index 000000000..803b2f154
--- /dev/null
+++ b/src/slic3r/GUI/GLShader.hpp
@@ -0,0 +1,41 @@
+#ifndef slic3r_GLShader_hpp_
+#define slic3r_GLShader_hpp_
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/Point.hpp"
+
+namespace Slic3r {
+
+class GLShader
+{
+public:
+ GLShader() :
+ fragment_program_id(0),
+ vertex_program_id(0),
+ shader_program_id(0)
+ {}
+ ~GLShader();
+
+ bool load_from_text(const char *fragment_shader, const char *vertex_shader);
+ bool load_from_file(const char* fragment_shader_filename, const char* vertex_shader_filename);
+
+ void release();
+
+ int get_attrib_location(const char *name) const;
+ int get_uniform_location(const char *name) const;
+
+ bool set_uniform(const char *name, float value) const;
+ bool set_uniform(const char* name, const float* matrix) const;
+
+ void enable() const;
+ void disable() const;
+
+ unsigned int fragment_program_id;
+ unsigned int vertex_program_id;
+ unsigned int shader_program_id;
+ std::string last_error;
+};
+
+}
+
+#endif /* slic3r_GLShader_hpp_ */
diff --git a/src/slic3r/GUI/GLTexture.cpp b/src/slic3r/GUI/GLTexture.cpp
new file mode 100644
index 000000000..235e3d93b
--- /dev/null
+++ b/src/slic3r/GUI/GLTexture.cpp
@@ -0,0 +1,199 @@
+#include "GLTexture.hpp"
+
+#include <GL/glew.h>
+
+#include <wx/image.h>
+
+#include <boost/filesystem.hpp>
+
+#include <vector>
+#include <algorithm>
+
+namespace Slic3r {
+namespace GUI {
+
+GLTexture::Quad_UVs GLTexture::FullTextureUVs = { { 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f } };
+
+GLTexture::GLTexture()
+ : m_id(0)
+ , m_width(0)
+ , m_height(0)
+ , m_source("")
+{
+}
+
+GLTexture::~GLTexture()
+{
+ reset();
+}
+
+bool GLTexture::load_from_file(const std::string& filename, bool generate_mipmaps)
+{
+ reset();
+
+ if (!boost::filesystem::exists(filename))
+ return false;
+
+ // Load a PNG with an alpha channel.
+ wxImage image;
+ if (!image.LoadFile(filename, wxBITMAP_TYPE_PNG))
+ {
+ reset();
+ return false;
+ }
+
+ m_width = image.GetWidth();
+ m_height = image.GetHeight();
+ int n_pixels = m_width * m_height;
+
+ if (n_pixels <= 0)
+ {
+ reset();
+ return false;
+ }
+
+ // Get RGB & alpha raw data from wxImage, pack them into an array.
+ unsigned char* img_rgb = image.GetData();
+ if (img_rgb == nullptr)
+ {
+ reset();
+ return false;
+ }
+
+ unsigned char* img_alpha = image.GetAlpha();
+
+ std::vector<unsigned char> data(n_pixels * 4, 0);
+ for (int i = 0; i < n_pixels; ++i)
+ {
+ int data_id = i * 4;
+ int img_id = i * 3;
+ data[data_id + 0] = img_rgb[img_id + 0];
+ data[data_id + 1] = img_rgb[img_id + 1];
+ data[data_id + 2] = img_rgb[img_id + 2];
+ data[data_id + 3] = (img_alpha != nullptr) ? img_alpha[i] : 255;
+ }
+
+ // sends data to gpu
+ ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ ::glGenTextures(1, &m_id);
+ ::glBindTexture(GL_TEXTURE_2D, m_id);
+ ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data());
+ if (generate_mipmaps)
+ {
+ // we manually generate mipmaps because glGenerateMipmap() function is not reliable on all graphics cards
+ unsigned int levels_count = _generate_mipmaps(image);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1 + levels_count);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+ }
+ else
+ {
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ }
+ ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ ::glBindTexture(GL_TEXTURE_2D, 0);
+
+ m_source = filename;
+ return true;
+}
+
+void GLTexture::reset()
+{
+ if (m_id != 0)
+ ::glDeleteTextures(1, &m_id);
+
+ m_id = 0;
+ m_width = 0;
+ m_height = 0;
+ m_source = "";
+}
+
+unsigned int GLTexture::get_id() const
+{
+ return m_id;
+}
+
+int GLTexture::get_width() const
+{
+ return m_width;
+}
+
+int GLTexture::get_height() const
+{
+ return m_height;
+}
+
+const std::string& GLTexture::get_source() const
+{
+ return m_source;
+}
+
+void GLTexture::render_texture(unsigned int tex_id, float left, float right, float bottom, float top)
+{
+ render_sub_texture(tex_id, left, right, bottom, top, FullTextureUVs);
+}
+
+void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right, float bottom, float top, const GLTexture::Quad_UVs& uvs)
+{
+ ::glEnable(GL_BLEND);
+ ::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ ::glEnable(GL_TEXTURE_2D);
+ ::glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
+
+ ::glBindTexture(GL_TEXTURE_2D, (GLuint)tex_id);
+
+ ::glBegin(GL_QUADS);
+ ::glTexCoord2f(uvs.left_bottom.u, uvs.left_bottom.v); ::glVertex2f(left, bottom);
+ ::glTexCoord2f(uvs.right_bottom.u, uvs.right_bottom.v); ::glVertex2f(right, bottom);
+ ::glTexCoord2f(uvs.right_top.u, uvs.right_top.v); ::glVertex2f(right, top);
+ ::glTexCoord2f(uvs.left_top.u, uvs.left_top.v); ::glVertex2f(left, top);
+ ::glEnd();
+
+ ::glBindTexture(GL_TEXTURE_2D, 0);
+
+ ::glDisable(GL_TEXTURE_2D);
+ ::glDisable(GL_BLEND);
+}
+
+unsigned int GLTexture::_generate_mipmaps(wxImage& image)
+{
+ int w = image.GetWidth();
+ int h = image.GetHeight();
+ GLint level = 0;
+ std::vector<unsigned char> data(w * h * 4, 0);
+
+ while ((w > 1) || (h > 1))
+ {
+ ++level;
+
+ w = std::max(w / 2, 1);
+ h = std::max(h / 2, 1);
+
+ int n_pixels = w * h;
+
+ image = image.ResampleBicubic(w, h);
+
+ unsigned char* img_rgb = image.GetData();
+ unsigned char* img_alpha = image.GetAlpha();
+
+ data.resize(n_pixels * 4);
+ for (int i = 0; i < n_pixels; ++i)
+ {
+ int data_id = i * 4;
+ int img_id = i * 3;
+ data[data_id + 0] = img_rgb[img_id + 0];
+ data[data_id + 1] = img_rgb[img_id + 1];
+ data[data_id + 2] = img_rgb[img_id + 2];
+ data[data_id + 3] = (img_alpha != nullptr) ? img_alpha[i] : 255;
+ }
+
+ ::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)w, (GLsizei)h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data());
+ }
+
+ return (unsigned int)level;
+}
+
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/GLTexture.hpp b/src/slic3r/GUI/GLTexture.hpp
new file mode 100644
index 000000000..e027bd152
--- /dev/null
+++ b/src/slic3r/GUI/GLTexture.hpp
@@ -0,0 +1,60 @@
+#ifndef slic3r_GLTexture_hpp_
+#define slic3r_GLTexture_hpp_
+
+#include <string>
+
+class wxImage;
+
+namespace Slic3r {
+namespace GUI {
+
+ class GLTexture
+ {
+ public:
+ struct UV
+ {
+ float u;
+ float v;
+ };
+
+ struct Quad_UVs
+ {
+ UV left_bottom;
+ UV right_bottom;
+ UV right_top;
+ UV left_top;
+ };
+
+ static Quad_UVs FullTextureUVs;
+
+ protected:
+ unsigned int m_id;
+ int m_width;
+ int m_height;
+ std::string m_source;
+
+ public:
+ GLTexture();
+ virtual ~GLTexture();
+
+ bool load_from_file(const std::string& filename, bool generate_mipmaps);
+ void reset();
+
+ unsigned int get_id() const;
+ int get_width() const;
+ int get_height() const;
+
+ const std::string& get_source() const;
+
+ static void render_texture(unsigned int tex_id, float left, float right, float bottom, float top);
+ static void render_sub_texture(unsigned int tex_id, float left, float right, float bottom, float top, const Quad_UVs& uvs);
+
+ protected:
+ unsigned int _generate_mipmaps(wxImage& image);
+ };
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif // slic3r_GLTexture_hpp_
+
diff --git a/src/slic3r/GUI/GLToolbar.cpp b/src/slic3r/GUI/GLToolbar.cpp
new file mode 100644
index 000000000..388868b12
--- /dev/null
+++ b/src/slic3r/GUI/GLToolbar.cpp
@@ -0,0 +1,722 @@
+#include "../../libslic3r/Point.hpp"
+#include "GLToolbar.hpp"
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../slic3r/GUI/GLCanvas3D.hpp"
+
+#include <GL/glew.h>
+
+#include <wx/bitmap.h>
+#include <wx/dcmemory.h>
+#include <wx/settings.h>
+
+namespace Slic3r {
+namespace GUI {
+
+GLToolbarItem::Data::Data()
+ : name("")
+ , tooltip("")
+ , sprite_id(-1)
+ , is_toggable(false)
+ , action_callback(nullptr)
+{
+}
+
+GLToolbarItem::GLToolbarItem(GLToolbarItem::EType type, const GLToolbarItem::Data& data)
+ : m_type(type)
+ , m_state(Disabled)
+ , m_data(data)
+{
+}
+
+GLToolbarItem::EState GLToolbarItem::get_state() const
+{
+ return m_state;
+}
+
+void GLToolbarItem::set_state(GLToolbarItem::EState state)
+{
+ m_state = state;
+}
+
+const std::string& GLToolbarItem::get_name() const
+{
+ return m_data.name;
+}
+
+const std::string& GLToolbarItem::get_tooltip() const
+{
+ return m_data.tooltip;
+}
+
+void GLToolbarItem::do_action()
+{
+ if (m_data.action_callback != nullptr)
+ m_data.action_callback->call();
+}
+
+bool GLToolbarItem::is_enabled() const
+{
+ return m_state != Disabled;
+}
+
+bool GLToolbarItem::is_hovered() const
+{
+ return (m_state == Hover) || (m_state == HoverPressed);
+}
+
+bool GLToolbarItem::is_pressed() const
+{
+ return (m_state == Pressed) || (m_state == HoverPressed);
+}
+
+bool GLToolbarItem::is_toggable() const
+{
+ return m_data.is_toggable;
+}
+
+bool GLToolbarItem::is_separator() const
+{
+ return m_type == Separator;
+}
+
+void GLToolbarItem::render(unsigned int tex_id, float left, float right, float bottom, float top, unsigned int texture_size, unsigned int border_size, unsigned int icon_size, unsigned int gap_size) const
+{
+ GLTexture::render_sub_texture(tex_id, left, right, bottom, top, get_uvs(texture_size, border_size, icon_size, gap_size));
+}
+
+GLTexture::Quad_UVs GLToolbarItem::get_uvs(unsigned int texture_size, unsigned int border_size, unsigned int icon_size, unsigned int gap_size) const
+{
+ GLTexture::Quad_UVs uvs;
+
+ float inv_texture_size = (texture_size != 0) ? 1.0f / (float)texture_size : 0.0f;
+
+ float scaled_icon_size = (float)icon_size * inv_texture_size;
+ float scaled_border_size = (float)border_size * inv_texture_size;
+ float scaled_gap_size = (float)gap_size * inv_texture_size;
+ float stride = scaled_icon_size + scaled_gap_size;
+
+ float left = scaled_border_size + (float)m_state * stride;
+ float right = left + scaled_icon_size;
+ float top = scaled_border_size + (float)m_data.sprite_id * stride;
+ float bottom = top + scaled_icon_size;
+
+ uvs.left_top = { left, top };
+ uvs.left_bottom = { left, bottom };
+ uvs.right_bottom = { right, bottom };
+ uvs.right_top = { right, top };
+
+ return uvs;
+}
+
+GLToolbar::ItemsIconsTexture::ItemsIconsTexture()
+ : items_icon_size(0)
+ , items_icon_border_size(0)
+ , items_icon_gap_size(0)
+{
+}
+
+GLToolbar::Layout::Layout()
+ : type(Horizontal)
+ , top(0.0f)
+ , left(0.0f)
+ , separator_size(0.0f)
+ , gap_size(0.0f)
+{
+}
+
+GLToolbar::GLToolbar(GLCanvas3D& parent)
+ : m_parent(parent)
+ , m_enabled(false)
+{
+}
+
+bool GLToolbar::init(const std::string& icons_texture_filename, unsigned int items_icon_size, unsigned int items_icon_border_size, unsigned int items_icon_gap_size)
+{
+ std::string path = resources_dir() + "/icons/";
+ bool res = !icons_texture_filename.empty() && m_icons_texture.texture.load_from_file(path + icons_texture_filename, false);
+ if (res)
+ {
+ m_icons_texture.items_icon_size = items_icon_size;
+ m_icons_texture.items_icon_border_size = items_icon_border_size;
+ m_icons_texture.items_icon_gap_size = items_icon_gap_size;
+ }
+
+ return res;
+}
+
+GLToolbar::Layout::Type GLToolbar::get_layout_type() const
+{
+ return m_layout.type;
+}
+
+void GLToolbar::set_layout_type(GLToolbar::Layout::Type type)
+{
+ m_layout.type = type;
+}
+
+void GLToolbar::set_position(float top, float left)
+{
+ m_layout.top = top;
+ m_layout.left = left;
+}
+
+void GLToolbar::set_separator_size(float size)
+{
+ m_layout.separator_size = size;
+}
+
+void GLToolbar::set_gap_size(float size)
+{
+ m_layout.gap_size = size;
+}
+
+bool GLToolbar::is_enabled() const
+{
+ return m_enabled;
+}
+
+void GLToolbar::set_enabled(bool enable)
+{
+ m_enabled = true;
+}
+
+bool GLToolbar::add_item(const GLToolbarItem::Data& data)
+{
+ GLToolbarItem* item = new GLToolbarItem(GLToolbarItem::Action, data);
+ if (item == nullptr)
+ return false;
+
+ m_items.push_back(item);
+ return true;
+}
+
+bool GLToolbar::add_separator()
+{
+ GLToolbarItem::Data data;
+ GLToolbarItem* item = new GLToolbarItem(GLToolbarItem::Separator, data);
+ if (item == nullptr)
+ return false;
+
+ m_items.push_back(item);
+ return true;
+}
+
+float GLToolbar::get_width() const
+{
+ switch (m_layout.type)
+ {
+ default:
+ case Layout::Horizontal:
+ {
+ return get_width_horizontal();
+ }
+ case Layout::Vertical:
+ {
+ return get_width_vertical();
+ }
+ }
+}
+
+float GLToolbar::get_height() const
+{
+ switch (m_layout.type)
+ {
+ default:
+ case Layout::Horizontal:
+ {
+ return get_height_horizontal();
+ }
+ case Layout::Vertical:
+ {
+ return get_height_vertical();
+ }
+ }
+}
+
+void GLToolbar::enable_item(const std::string& name)
+{
+ for (GLToolbarItem* item : m_items)
+ {
+ if ((item->get_name() == name) && (item->get_state() == GLToolbarItem::Disabled))
+ {
+ item->set_state(GLToolbarItem::Normal);
+ return;
+ }
+ }
+}
+
+void GLToolbar::disable_item(const std::string& name)
+{
+ for (GLToolbarItem* item : m_items)
+ {
+ if (item->get_name() == name)
+ {
+ item->set_state(GLToolbarItem::Disabled);
+ return;
+ }
+ }
+}
+
+bool GLToolbar::is_item_pressed(const std::string& name) const
+{
+ for (GLToolbarItem* item : m_items)
+ {
+ if (item->get_name() == name)
+ return item->is_pressed();
+ }
+
+ return false;
+}
+
+void GLToolbar::update_hover_state(const Vec2d& mouse_pos)
+{
+ if (!m_enabled)
+ return;
+
+ switch (m_layout.type)
+ {
+ default:
+ case Layout::Horizontal:
+ {
+ update_hover_state_horizontal(mouse_pos);
+ break;
+ }
+ case Layout::Vertical:
+ {
+ update_hover_state_vertical(mouse_pos);
+ break;
+ }
+ }
+}
+
+int GLToolbar::contains_mouse(const Vec2d& mouse_pos) const
+{
+ if (!m_enabled)
+ return -1;
+
+ switch (m_layout.type)
+ {
+ default:
+ case Layout::Horizontal:
+ {
+ return contains_mouse_horizontal(mouse_pos);
+ }
+ case Layout::Vertical:
+ {
+ return contains_mouse_vertical(mouse_pos);
+ }
+ }
+}
+
+void GLToolbar::do_action(unsigned int item_id)
+{
+ if (item_id < (unsigned int)m_items.size())
+ {
+ GLToolbarItem* item = m_items[item_id];
+ if ((item != nullptr) && !item->is_separator() && item->is_hovered())
+ {
+ if (item->is_toggable())
+ {
+ GLToolbarItem::EState state = item->get_state();
+ if (state == GLToolbarItem::Hover)
+ item->set_state(GLToolbarItem::HoverPressed);
+ else if (state == GLToolbarItem::HoverPressed)
+ item->set_state(GLToolbarItem::Hover);
+
+ m_parent.render();
+ item->do_action();
+ }
+ else
+ {
+ item->set_state(GLToolbarItem::HoverPressed);
+ m_parent.render();
+ item->do_action();
+ if (item->get_state() != GLToolbarItem::Disabled)
+ {
+ // the item may get disabled during the action, if not, set it back to hover state
+ item->set_state(GLToolbarItem::Hover);
+ m_parent.render();
+ }
+ }
+ }
+ }
+}
+
+void GLToolbar::render() const
+{
+ if (!m_enabled || m_items.empty())
+ return;
+
+ ::glDisable(GL_DEPTH_TEST);
+
+ ::glPushMatrix();
+ ::glLoadIdentity();
+
+ switch (m_layout.type)
+ {
+ default:
+ case Layout::Horizontal:
+ {
+ render_horizontal();
+ break;
+ }
+ case Layout::Vertical:
+ {
+ render_vertical();
+ break;
+ }
+ }
+
+ ::glPopMatrix();
+}
+
+float GLToolbar::get_width_horizontal() const
+{
+ return get_main_size();
+}
+
+float GLToolbar::get_width_vertical() const
+{
+ return m_icons_texture.items_icon_size;
+}
+
+float GLToolbar::get_height_horizontal() const
+{
+ return m_icons_texture.items_icon_size;
+}
+
+float GLToolbar::get_height_vertical() const
+{
+ return get_main_size();
+}
+
+float GLToolbar::get_main_size() const
+{
+ float size = 0.0f;
+ for (unsigned int i = 0; i < (unsigned int)m_items.size(); ++i)
+ {
+ if (m_items[i]->is_separator())
+ size += m_layout.separator_size;
+ else
+ size += (float)m_icons_texture.items_icon_size;
+ }
+
+ if (m_items.size() > 1)
+ size += ((float)m_items.size() - 1.0f) * m_layout.gap_size;
+
+ return size;
+}
+
+void GLToolbar::update_hover_state_horizontal(const Vec2d& mouse_pos)
+{
+ float zoom = m_parent.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+
+ Size cnv_size = m_parent.get_canvas_size();
+ Vec2d scaled_mouse_pos((mouse_pos(0) - 0.5 * (double)cnv_size.get_width()) * inv_zoom, (0.5 * (double)cnv_size.get_height() - mouse_pos(1)) * inv_zoom);
+
+ float scaled_icons_size = (float)m_icons_texture.items_icon_size * inv_zoom;
+ float scaled_separator_size = m_layout.separator_size * inv_zoom;
+ float scaled_gap_size = m_layout.gap_size * inv_zoom;
+
+ float separator_stride = scaled_separator_size + scaled_gap_size;
+ float icon_stride = scaled_icons_size + scaled_gap_size;
+
+ float left = m_layout.left;
+ float top = m_layout.top;
+
+ std::string tooltip = "";
+
+ for (GLToolbarItem* item : m_items)
+ {
+ if (item->is_separator())
+ left += separator_stride;
+ else
+ {
+ float right = left + scaled_icons_size;
+ float bottom = top - scaled_icons_size;
+
+ GLToolbarItem::EState state = item->get_state();
+ bool inside = (left <= (float)scaled_mouse_pos(0)) && ((float)scaled_mouse_pos(0) <= right) && (bottom <= (float)scaled_mouse_pos(1)) && ((float)scaled_mouse_pos(1) <= top);
+
+ switch (state)
+ {
+ case GLToolbarItem::Normal:
+ {
+ if (inside)
+ item->set_state(GLToolbarItem::Hover);
+
+ break;
+ }
+ case GLToolbarItem::Hover:
+ {
+ if (inside)
+ tooltip = item->get_tooltip();
+ else
+ item->set_state(GLToolbarItem::Normal);
+
+ break;
+ }
+ case GLToolbarItem::Pressed:
+ {
+ if (inside)
+ item->set_state(GLToolbarItem::HoverPressed);
+
+ break;
+ }
+ case GLToolbarItem::HoverPressed:
+ {
+ if (inside)
+ tooltip = item->get_tooltip();
+ else
+ item->set_state(GLToolbarItem::Pressed);
+
+ break;
+ }
+ default:
+ case GLToolbarItem::Disabled:
+ {
+ break;
+ }
+ }
+
+ left += icon_stride;
+ }
+ }
+
+ m_parent.set_tooltip(tooltip);
+}
+
+void GLToolbar::update_hover_state_vertical(const Vec2d& mouse_pos)
+{
+ float zoom = m_parent.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+
+ Size cnv_size = m_parent.get_canvas_size();
+ Vec2d scaled_mouse_pos((mouse_pos(0) - 0.5 * (double)cnv_size.get_width()) * inv_zoom, (0.5 * (double)cnv_size.get_height() - mouse_pos(1)) * inv_zoom);
+
+ float scaled_icons_size = (float)m_icons_texture.items_icon_size * inv_zoom;
+ float scaled_separator_size = m_layout.separator_size * inv_zoom;
+ float scaled_gap_size = m_layout.gap_size * inv_zoom;
+
+ float separator_stride = scaled_separator_size + scaled_gap_size;
+ float icon_stride = scaled_icons_size + scaled_gap_size;
+
+ float left = m_layout.left;
+ float top = m_layout.top;
+
+ std::string tooltip = "";
+
+ for (GLToolbarItem* item : m_items)
+ {
+ if (item->is_separator())
+ top -= separator_stride;
+ else
+ {
+ float right = left + scaled_icons_size;
+ float bottom = top - scaled_icons_size;
+
+ GLToolbarItem::EState state = item->get_state();
+ bool inside = (left <= (float)scaled_mouse_pos(0)) && ((float)scaled_mouse_pos(0) <= right) && (bottom <= (float)scaled_mouse_pos(1)) && ((float)scaled_mouse_pos(1) <= top);
+
+ switch (state)
+ {
+ case GLToolbarItem::Normal:
+ {
+ if (inside)
+ item->set_state(GLToolbarItem::Hover);
+
+ break;
+ }
+ case GLToolbarItem::Hover:
+ {
+ if (inside)
+ tooltip = item->get_tooltip();
+ else
+ item->set_state(GLToolbarItem::Normal);
+
+ break;
+ }
+ case GLToolbarItem::Pressed:
+ {
+ if (inside)
+ item->set_state(GLToolbarItem::HoverPressed);
+
+ break;
+ }
+ case GLToolbarItem::HoverPressed:
+ {
+ if (inside)
+ tooltip = item->get_tooltip();
+ else
+ item->set_state(GLToolbarItem::Pressed);
+
+ break;
+ }
+ default:
+ case GLToolbarItem::Disabled:
+ {
+ break;
+ }
+ }
+
+ top -= icon_stride;
+ }
+ }
+
+ m_parent.set_tooltip(tooltip);
+}
+
+int GLToolbar::contains_mouse_horizontal(const Vec2d& mouse_pos) const
+{
+ float zoom = m_parent.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+
+ Size cnv_size = m_parent.get_canvas_size();
+ Vec2d scaled_mouse_pos((mouse_pos(0) - 0.5 * (double)cnv_size.get_width()) * inv_zoom, (0.5 * (double)cnv_size.get_height() - mouse_pos(1)) * inv_zoom);
+
+ float scaled_icons_size = (float)m_icons_texture.items_icon_size * inv_zoom;
+ float scaled_separator_size = m_layout.separator_size * inv_zoom;
+ float scaled_gap_size = m_layout.gap_size * inv_zoom;
+
+ float separator_stride = scaled_separator_size + scaled_gap_size;
+ float icon_stride = scaled_icons_size + scaled_gap_size;
+
+ float left = m_layout.left;
+ float top = m_layout.top;
+
+ int id = -1;
+
+ for (GLToolbarItem* item : m_items)
+ {
+ ++id;
+
+ if (item->is_separator())
+ left += separator_stride;
+ else
+ {
+ float right = left + scaled_icons_size;
+ float bottom = top - scaled_icons_size;
+
+ if ((left <= (float)scaled_mouse_pos(0)) && ((float)scaled_mouse_pos(0) <= right) && (bottom <= (float)scaled_mouse_pos(1)) && ((float)scaled_mouse_pos(1) <= top))
+ return id;
+
+ left += icon_stride;
+ }
+ }
+
+ return -1;
+}
+
+int GLToolbar::contains_mouse_vertical(const Vec2d& mouse_pos) const
+{
+ float zoom = m_parent.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+
+ Size cnv_size = m_parent.get_canvas_size();
+ Vec2d scaled_mouse_pos((mouse_pos(0) - 0.5 * (double)cnv_size.get_width()) * inv_zoom, (0.5 * (double)cnv_size.get_height() - mouse_pos(1)) * inv_zoom);
+
+ float scaled_icons_size = (float)m_icons_texture.items_icon_size * inv_zoom;
+ float scaled_separator_size = m_layout.separator_size * inv_zoom;
+ float scaled_gap_size = m_layout.gap_size * inv_zoom;
+
+ float separator_stride = scaled_separator_size + scaled_gap_size;
+ float icon_stride = scaled_icons_size + scaled_gap_size;
+
+ float left = m_layout.left;
+ float top = m_layout.top;
+
+ int id = -1;
+
+ for (GLToolbarItem* item : m_items)
+ {
+ ++id;
+
+ if (item->is_separator())
+ top -= separator_stride;
+ else
+ {
+ float right = left + scaled_icons_size;
+ float bottom = top - scaled_icons_size;
+
+ if ((left <= (float)scaled_mouse_pos(0)) && ((float)scaled_mouse_pos(0) <= right) && (bottom <= (float)scaled_mouse_pos(1)) && ((float)scaled_mouse_pos(1) <= top))
+ return id;
+
+ top -= icon_stride;
+ }
+ }
+
+ return -1;
+}
+
+void GLToolbar::render_horizontal() const
+{
+ unsigned int tex_id = m_icons_texture.texture.get_id();
+ int tex_size = m_icons_texture.texture.get_width();
+
+ if ((tex_id == 0) || (tex_size <= 0))
+ return;
+
+ float zoom = m_parent.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+
+ float scaled_icons_size = (float)m_icons_texture.items_icon_size * inv_zoom;
+ float scaled_separator_size = m_layout.separator_size * inv_zoom;
+ float scaled_gap_size = m_layout.gap_size * inv_zoom;
+
+ float separator_stride = scaled_separator_size + scaled_gap_size;
+ float icon_stride = scaled_icons_size + scaled_gap_size;
+
+ float left = m_layout.left;
+ float top = m_layout.top;
+
+ // renders icons
+ for (const GLToolbarItem* item : m_items)
+ {
+ if (item->is_separator())
+ left += separator_stride;
+ else
+ {
+ item->render(tex_id, left, left + scaled_icons_size, top - scaled_icons_size, top, (unsigned int)tex_size, m_icons_texture.items_icon_border_size, m_icons_texture.items_icon_size, m_icons_texture.items_icon_gap_size);
+ left += icon_stride;
+ }
+ }
+}
+
+void GLToolbar::render_vertical() const
+{
+ unsigned int tex_id = m_icons_texture.texture.get_id();
+ int tex_size = m_icons_texture.texture.get_width();
+
+ if ((tex_id == 0) || (tex_size <= 0))
+ return;
+
+ float zoom = m_parent.get_camera_zoom();
+ float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f;
+
+ float scaled_icons_size = (float)m_icons_texture.items_icon_size * inv_zoom;
+ float scaled_separator_size = m_layout.separator_size * inv_zoom;
+ float scaled_gap_size = m_layout.gap_size * inv_zoom;
+
+ float separator_stride = scaled_separator_size + scaled_gap_size;
+ float icon_stride = scaled_icons_size + scaled_gap_size;
+
+ float left = m_layout.left;
+ float top = m_layout.top;
+
+ // renders icons
+ for (const GLToolbarItem* item : m_items)
+ {
+ if (item->is_separator())
+ top -= separator_stride;
+ else
+ {
+ item->render(tex_id, left, left + scaled_icons_size, top - scaled_icons_size, top, (unsigned int)tex_size, m_icons_texture.items_icon_border_size, m_icons_texture.items_icon_size, m_icons_texture.items_icon_gap_size);
+ top -= icon_stride;
+ }
+ }
+}
+
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/GLToolbar.hpp b/src/slic3r/GUI/GLToolbar.hpp
new file mode 100644
index 000000000..65d6748ff
--- /dev/null
+++ b/src/slic3r/GUI/GLToolbar.hpp
@@ -0,0 +1,175 @@
+#ifndef slic3r_GLToolbar_hpp_
+#define slic3r_GLToolbar_hpp_
+
+#include "../../slic3r/GUI/GLTexture.hpp"
+#include "../../callback.hpp"
+
+#include <string>
+#include <vector>
+
+namespace Slic3r {
+namespace GUI {
+
+class GLCanvas3D;
+
+class GLToolbarItem
+{
+public:
+ enum EType : unsigned char
+ {
+ Action,
+ Separator,
+ Num_Types
+ };
+
+ enum EState : unsigned char
+ {
+ Normal,
+ Pressed,
+ Disabled,
+ Hover,
+ HoverPressed,
+ Num_States
+ };
+
+ struct Data
+ {
+ std::string name;
+ std::string tooltip;
+ unsigned int sprite_id;
+ bool is_toggable;
+ PerlCallback* action_callback;
+
+ Data();
+ };
+
+private:
+ EType m_type;
+ EState m_state;
+ Data m_data;
+
+public:
+ GLToolbarItem(EType type, const Data& data);
+
+ EState get_state() const;
+ void set_state(EState state);
+
+ const std::string& get_name() const;
+ const std::string& get_tooltip() const;
+
+ void do_action();
+
+ bool is_enabled() const;
+ bool is_hovered() const;
+ bool is_pressed() const;
+
+ bool is_toggable() const;
+ bool is_separator() const;
+
+ void render(unsigned int tex_id, float left, float right, float bottom, float top, unsigned int texture_size, unsigned int border_size, unsigned int icon_size, unsigned int gap_size) const;
+
+private:
+ GLTexture::Quad_UVs get_uvs(unsigned int texture_size, unsigned int border_size, unsigned int icon_size, unsigned int gap_size) const;
+};
+
+class GLToolbar
+{
+public:
+ // items icon textures are assumed to be square and all with the same size in pixels, no internal check is done
+ // icons are layed-out into the texture starting from the top-left corner in the same order as enum GLToolbarItem::EState
+ // from left to right
+ struct ItemsIconsTexture
+ {
+ GLTexture texture;
+ // size of the square icons, in pixels
+ unsigned int items_icon_size;
+ // distance from the border, in pixels
+ unsigned int items_icon_border_size;
+ // distance between two adjacent icons (to avoid filtering artifacts), in pixels
+ unsigned int items_icon_gap_size;
+
+ ItemsIconsTexture();
+ };
+
+ struct Layout
+ {
+ enum Type : unsigned char
+ {
+ Horizontal,
+ Vertical,
+ Num_Types
+ };
+
+ Type type;
+ float top;
+ float left;
+ float separator_size;
+ float gap_size;
+
+ Layout();
+ };
+
+private:
+ typedef std::vector<GLToolbarItem*> ItemsList;
+
+ GLCanvas3D& m_parent;
+ bool m_enabled;
+ ItemsIconsTexture m_icons_texture;
+ Layout m_layout;
+
+ ItemsList m_items;
+
+public:
+ explicit GLToolbar(GLCanvas3D& parent);
+
+ bool init(const std::string& icons_texture_filename, unsigned int items_icon_size, unsigned int items_icon_border_size, unsigned int items_icon_gap_size);
+
+ Layout::Type get_layout_type() const;
+ void set_layout_type(Layout::Type type);
+
+ void set_position(float top, float left);
+ void set_separator_size(float size);
+ void set_gap_size(float size);
+
+ bool is_enabled() const;
+ void set_enabled(bool enable);
+
+ bool add_item(const GLToolbarItem::Data& data);
+ bool add_separator();
+
+ float get_width() const;
+ float get_height() const;
+
+ void enable_item(const std::string& name);
+ void disable_item(const std::string& name);
+
+ bool is_item_pressed(const std::string& name) const;
+
+ void update_hover_state(const Vec2d& mouse_pos);
+
+ // returns the id of the item under the given mouse position or -1 if none
+ int contains_mouse(const Vec2d& mouse_pos) const;
+
+ void do_action(unsigned int item_id);
+
+ void render() const;
+
+private:
+ float get_width_horizontal() const;
+ float get_width_vertical() const;
+ float get_height_horizontal() const;
+ float get_height_vertical() const;
+ float get_main_size() const;
+ void update_hover_state_horizontal(const Vec2d& mouse_pos);
+ void update_hover_state_vertical(const Vec2d& mouse_pos);
+ int contains_mouse_horizontal(const Vec2d& mouse_pos) const;
+ int contains_mouse_vertical(const Vec2d& mouse_pos) const;
+
+ void render_horizontal() const;
+ void render_vertical() const;
+};
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif // slic3r_GLToolbar_hpp_
diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp
new file mode 100644
index 000000000..24d459921
--- /dev/null
+++ b/src/slic3r/GUI/GUI.cpp
@@ -0,0 +1,1402 @@
+#include "GUI.hpp"
+#include "WipeTowerDialog.hpp"
+
+#include <assert.h>
+#include <cmath>
+
+#include <boost/lexical_cast.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/format.hpp>
+#include <boost/lexical_cast.hpp>
+
+#if __APPLE__
+#import <IOKit/pwr_mgt/IOPMLib.h>
+#elif _WIN32
+#include <Windows.h>
+// Undefine min/max macros incompatible with the standard library
+// For example, std::numeric_limits<std::streamsize>::max()
+// produces some weird errors
+#ifdef min
+#undef min
+#endif
+#ifdef max
+#undef max
+#endif
+#include "boost/nowide/convert.hpp"
+#endif
+
+#include <wx/app.h>
+#include <wx/button.h>
+#include <wx/dir.h>
+#include <wx/filename.h>
+#include <wx/frame.h>
+#include <wx/menu.h>
+#include <wx/notebook.h>
+#include <wx/panel.h>
+#include <wx/sizer.h>
+#include <wx/combo.h>
+#include <wx/window.h>
+#include <wx/msgdlg.h>
+#include <wx/settings.h>
+#include <wx/display.h>
+#include <wx/collpane.h>
+#include <wx/wupdlock.h>
+
+#include "wxExtensions.hpp"
+
+#include "Tab.hpp"
+#include "TabIface.hpp"
+#include "GUI_Preview.hpp"
+#include "GUI_PreviewIface.hpp"
+#include "AboutDialog.hpp"
+#include "AppConfig.hpp"
+#include "ConfigSnapshotDialog.hpp"
+#include "ProgressStatusBar.hpp"
+#include "Utils.hpp"
+#include "MsgDialog.hpp"
+#include "ConfigWizard.hpp"
+#include "Preferences.hpp"
+#include "PresetBundle.hpp"
+#include "UpdateDialogs.hpp"
+#include "FirmwareDialog.hpp"
+#include "GUI_ObjectParts.hpp"
+
+#include "../Utils/PresetUpdater.hpp"
+#include "../Config/Snapshot.hpp"
+
+#include "3DScene.hpp"
+#include "libslic3r/I18N.hpp"
+#include "Model.hpp"
+#include "LambdaObjectDialog.hpp"
+
+#include "../../libslic3r/Print.hpp"
+
+namespace Slic3r { namespace GUI {
+
+#if __APPLE__
+IOPMAssertionID assertionID;
+#endif
+
+void disable_screensaver()
+{
+ #if __APPLE__
+ CFStringRef reasonForActivity = CFSTR("Slic3r");
+ IOReturn success = IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep,
+ kIOPMAssertionLevelOn, reasonForActivity, &assertionID);
+ // ignore result: success == kIOReturnSuccess
+ #elif _WIN32
+ SetThreadExecutionState(ES_DISPLAY_REQUIRED | ES_CONTINUOUS);
+ #endif
+}
+
+void enable_screensaver()
+{
+ #if __APPLE__
+ IOReturn success = IOPMAssertionRelease(assertionID);
+ #elif _WIN32
+ SetThreadExecutionState(ES_CONTINUOUS);
+ #endif
+}
+
+bool debugged()
+{
+ #ifdef _WIN32
+ return IsDebuggerPresent();
+ #else
+ return false;
+ #endif /* _WIN32 */
+}
+
+void break_to_debugger()
+{
+ #ifdef _WIN32
+ if (IsDebuggerPresent())
+ DebugBreak();
+ #endif /* _WIN32 */
+}
+
+// Passing the wxWidgets GUI classes instantiated by the Perl part to C++.
+wxApp *g_wxApp = nullptr;
+wxFrame *g_wxMainFrame = nullptr;
+ProgressStatusBar *g_progress_status_bar = nullptr;
+wxNotebook *g_wxTabPanel = nullptr;
+wxPanel *g_wxPlater = nullptr;
+AppConfig *g_AppConfig = nullptr;
+PresetBundle *g_PresetBundle= nullptr;
+PresetUpdater *g_PresetUpdater = nullptr;
+wxColour g_color_label_modified;
+wxColour g_color_label_sys;
+wxColour g_color_label_default;
+
+std::vector<Tab *> g_tabs_list;
+
+wxLocale* g_wxLocale;
+
+wxFont g_small_font;
+wxFont g_bold_font;
+
+std::vector <std::shared_ptr<ConfigOptionsGroup>> m_optgroups;
+double m_brim_width = 0.0;
+size_t m_label_width = 100;
+wxButton* g_wiping_dialog_button = nullptr;
+
+//showed/hided controls according to the view mode
+wxWindow *g_right_panel = nullptr;
+wxBoxSizer *g_frequently_changed_parameters_sizer = nullptr;
+wxBoxSizer *g_info_sizer = nullptr;
+wxBoxSizer *g_object_list_sizer = nullptr;
+std::vector<wxButton*> g_buttons;
+wxStaticBitmap *g_manifold_warning_icon = nullptr;
+bool g_show_print_info = false;
+bool g_show_manifold_warning_icon = false;
+
+PreviewIface* g_preview = nullptr;
+
+static void init_label_colours()
+{
+ auto luma = get_colour_approx_luma(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+ if (luma >= 128) {
+ g_color_label_modified = wxColour(252, 77, 1);
+ g_color_label_sys = wxColour(26, 132, 57);
+ } else {
+ g_color_label_modified = wxColour(253, 111, 40);
+ g_color_label_sys = wxColour(115, 220, 103);
+ }
+ g_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
+}
+
+void update_label_colours_from_appconfig()
+{
+ if (g_AppConfig->has("label_clr_sys")){
+ auto str = g_AppConfig->get("label_clr_sys");
+ if (str != "")
+ g_color_label_sys = wxColour(str);
+ }
+
+ if (g_AppConfig->has("label_clr_modified")){
+ auto str = g_AppConfig->get("label_clr_modified");
+ if (str != "")
+ g_color_label_modified = wxColour(str);
+ }
+}
+
+static void init_fonts()
+{
+ g_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
+ g_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold();
+#ifdef __WXMAC__
+ g_small_font.SetPointSize(11);
+ g_bold_font.SetPointSize(13);
+#endif /*__WXMAC__*/
+}
+
+static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); }
+
+void set_wxapp(wxApp *app)
+{
+ g_wxApp = app;
+ // Let the libslic3r know the callback, which will translate messages on demand.
+ Slic3r::I18N::set_translate_callback(libslic3r_translate_callback);
+ init_label_colours();
+ init_fonts();
+}
+
+void set_main_frame(wxFrame *main_frame)
+{
+ g_wxMainFrame = main_frame;
+}
+
+wxFrame* get_main_frame() { return g_wxMainFrame; }
+
+void set_progress_status_bar(ProgressStatusBar *prsb)
+{
+ g_progress_status_bar = prsb;
+}
+
+ProgressStatusBar* get_progress_status_bar() { return g_progress_status_bar; }
+
+void set_tab_panel(wxNotebook *tab_panel)
+{
+ g_wxTabPanel = tab_panel;
+}
+
+void set_plater(wxPanel *plater)
+{
+ g_wxPlater = plater;
+}
+
+void set_app_config(AppConfig *app_config)
+{
+ g_AppConfig = app_config;
+}
+
+void set_preset_bundle(PresetBundle *preset_bundle)
+{
+ g_PresetBundle = preset_bundle;
+}
+
+void set_preset_updater(PresetUpdater *updater)
+{
+ g_PresetUpdater = updater;
+}
+
+enum ActionButtons
+{
+ abExportGCode,
+ abReslice,
+ abPrint,
+ abSendGCode,
+};
+
+void set_objects_from_perl( wxWindow* parent,
+ wxBoxSizer *frequently_changed_parameters_sizer,
+ wxBoxSizer *info_sizer,
+ wxButton *btn_export_gcode,
+ wxButton *btn_reslice,
+ wxButton *btn_print,
+ wxButton *btn_send_gcode,
+ wxStaticBitmap *manifold_warning_icon)
+{
+ g_right_panel = parent->GetParent();
+ g_frequently_changed_parameters_sizer = frequently_changed_parameters_sizer;
+ g_info_sizer = info_sizer;
+
+ g_buttons.push_back(btn_export_gcode);
+ g_buttons.push_back(btn_reslice);
+ g_buttons.push_back(btn_print);
+ g_buttons.push_back(btn_send_gcode);
+
+ // Update font style for buttons
+ for (auto btn : g_buttons)
+ btn->SetFont(bold_font());
+
+ g_manifold_warning_icon = manifold_warning_icon;
+}
+
+void set_show_print_info(bool show)
+{
+ g_show_print_info = show;
+}
+
+void set_show_manifold_warning_icon(bool show)
+{
+ g_show_manifold_warning_icon = show;
+ if (!g_manifold_warning_icon)
+ return;
+
+ // update manifold_warning_icon showing
+ if (show && !g_info_sizer->IsShown(static_cast<size_t>(0)))
+ g_show_manifold_warning_icon = false;
+
+ g_manifold_warning_icon->Show(g_show_manifold_warning_icon);
+ g_manifold_warning_icon->GetParent()->Layout();
+}
+
+void set_objects_list_sizer(wxBoxSizer *objects_list_sizer){
+ g_object_list_sizer = objects_list_sizer;
+}
+
+std::vector<Tab *>& get_tabs_list()
+{
+ return g_tabs_list;
+}
+
+bool checked_tab(Tab* tab)
+{
+ bool ret = true;
+ if (find(g_tabs_list.begin(), g_tabs_list.end(), tab) == g_tabs_list.end())
+ ret = false;
+ return ret;
+}
+
+void delete_tab_from_list(Tab* tab)
+{
+ std::vector<Tab *>::iterator itr = find(g_tabs_list.begin(), g_tabs_list.end(), tab);
+ if (itr != g_tabs_list.end())
+ g_tabs_list.erase(itr);
+}
+
+bool select_language(wxArrayString & names,
+ wxArrayLong & identifiers)
+{
+ wxCHECK_MSG(names.Count() == identifiers.Count(), false,
+ _(L("Array of language names and identifiers should have the same size.")));
+ int init_selection = 0;
+ long current_language = g_wxLocale ? g_wxLocale->GetLanguage() : wxLANGUAGE_UNKNOWN;
+ for (auto lang : identifiers){
+ if (lang == current_language)
+ break;
+ else
+ ++init_selection;
+ }
+ if (init_selection == identifiers.size())
+ init_selection = 0;
+ long index = wxGetSingleChoiceIndex(_(L("Select the language")), _(L("Language")),
+ names, init_selection);
+ if (index != -1)
+ {
+ g_wxLocale = new wxLocale;
+ g_wxLocale->Init(identifiers[index]);
+ g_wxLocale->AddCatalogLookupPathPrefix(wxPathOnly(localization_dir()));
+ g_wxLocale->AddCatalog(g_wxApp->GetAppName());
+ wxSetlocale(LC_NUMERIC, "C");
+ Preset::update_suffix_modified();
+ return true;
+ }
+ return false;
+}
+
+bool load_language()
+{
+ wxString language = wxEmptyString;
+ if (g_AppConfig->has("translation_language"))
+ language = g_AppConfig->get("translation_language");
+
+ if (language.IsEmpty())
+ return false;
+ wxArrayString names;
+ wxArrayLong identifiers;
+ get_installed_languages(names, identifiers);
+ for (size_t i = 0; i < identifiers.Count(); i++)
+ {
+ if (wxLocale::GetLanguageCanonicalName(identifiers[i]) == language)
+ {
+ g_wxLocale = new wxLocale;
+ g_wxLocale->Init(identifiers[i]);
+ g_wxLocale->AddCatalogLookupPathPrefix(wxPathOnly(localization_dir()));
+ g_wxLocale->AddCatalog(g_wxApp->GetAppName());
+ wxSetlocale(LC_NUMERIC, "C");
+ Preset::update_suffix_modified();
+ return true;
+ }
+ }
+ return false;
+}
+
+void save_language()
+{
+ wxString language = wxEmptyString;
+ if (g_wxLocale)
+ language = g_wxLocale->GetCanonicalName();
+
+ g_AppConfig->set("translation_language", language.ToStdString());
+ g_AppConfig->save();
+}
+
+void get_installed_languages(wxArrayString & names,
+ wxArrayLong & identifiers)
+{
+ names.Clear();
+ identifiers.Clear();
+
+ wxDir dir(wxPathOnly(localization_dir()));
+ wxString filename;
+ const wxLanguageInfo * langinfo;
+ wxString name = wxLocale::GetLanguageName(wxLANGUAGE_DEFAULT);
+ if (!name.IsEmpty())
+ {
+ names.Add(_(L("Default")));
+ identifiers.Add(wxLANGUAGE_DEFAULT);
+ }
+ for (bool cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_DIRS);
+ cont; cont = dir.GetNext(&filename))
+ {
+ langinfo = wxLocale::FindLanguageInfo(filename);
+ if (langinfo != NULL)
+ {
+ auto full_file_name = dir.GetName() + wxFileName::GetPathSeparator() +
+ filename + wxFileName::GetPathSeparator() +
+ g_wxApp->GetAppName() + wxT(".mo");
+ if (wxFileExists(full_file_name))
+ {
+ names.Add(langinfo->Description);
+ identifiers.Add(langinfo->Language);
+ }
+ }
+ }
+}
+
+enum ConfigMenuIDs {
+ ConfigMenuWizard,
+ ConfigMenuSnapshots,
+ ConfigMenuTakeSnapshot,
+ ConfigMenuUpdate,
+ ConfigMenuPreferences,
+ ConfigMenuModeSimple,
+ ConfigMenuModeExpert,
+ ConfigMenuLanguage,
+ ConfigMenuFlashFirmware,
+ ConfigMenuCnt,
+};
+
+ConfigMenuIDs get_view_mode()
+{
+ if (!g_AppConfig->has("view_mode"))
+ return ConfigMenuModeSimple;
+
+ const auto mode = g_AppConfig->get("view_mode");
+ return mode == "expert" ? ConfigMenuModeExpert : ConfigMenuModeSimple;
+}
+
+static wxString dots("…", wxConvUTF8);
+
+void add_config_menu(wxMenuBar *menu, int event_preferences_changed, int event_language_change)
+{
+ auto local_menu = new wxMenu();
+ wxWindowID config_id_base = wxWindow::NewControlId((int)ConfigMenuCnt);
+
+ const auto config_wizard_name = _(ConfigWizard::name().wx_str());
+ const auto config_wizard_tooltip = wxString::Format(_(L("Run %s")), config_wizard_name);
+ // Cmd+, is standard on OS X - what about other operating systems?
+ local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip);
+ local_menu->Append(config_id_base + ConfigMenuSnapshots, _(L("Configuration Snapshots"))+dots, _(L("Inspect / activate configuration snapshots")));
+ local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _(L("Take Configuration Snapshot")), _(L("Capture a configuration snapshot")));
+// local_menu->Append(config_id_base + ConfigMenuUpdate, _(L("Check for updates")), _(L("Check for configuration updates")));
+ local_menu->AppendSeparator();
+ local_menu->Append(config_id_base + ConfigMenuPreferences, _(L("Preferences"))+dots+"\tCtrl+,", _(L("Application preferences")));
+ local_menu->AppendSeparator();
+ auto mode_menu = new wxMenu();
+ mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _(L("&Simple")), _(L("Simple View Mode")));
+ mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _(L("&Expert")), _(L("Expert View Mode")));
+ mode_menu->Check(config_id_base + get_view_mode(), true);
+ local_menu->AppendSubMenu(mode_menu, _(L("&Mode")), _(L("Slic3r View Mode")));
+ local_menu->AppendSeparator();
+ local_menu->Append(config_id_base + ConfigMenuLanguage, _(L("Change Application Language")));
+ local_menu->AppendSeparator();
+ local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _(L("Flash printer firmware")), _(L("Upload a firmware image into an Arduino based printer")));
+ // TODO: for when we're able to flash dictionaries
+ // local_menu->Append(config_id_base + FirmwareMenuDict, _(L("Flash language file")), _(L("Upload a language dictionary file into a Prusa printer")));
+
+ 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(ConfigWizard::RR_USER);
+ break;
+ case ConfigMenuTakeSnapshot:
+ // Take a configuration snapshot.
+ if (check_unsaved_changes()) {
+ wxTextEntryDialog dlg(nullptr, _(L("Taking configuration snapshot")), _(L("Snapshot name")));
+ if (dlg.ShowModal() == wxID_OK)
+ 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()) {
+ 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()) {
+ 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.
+ load_current_presets();
+ }
+ }
+ break;
+ case ConfigMenuPreferences:
+ {
+ PreferencesDialog dlg(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) {
+ _3DScene::remove_all_canvases();// remove all canvas before recreate GUI
+ wxCommandEvent event(event_language_change);
+ g_wxApp->ProcessEvent(event);
+ }
+ }
+ break;
+ }
+ case ConfigMenuFlashFirmware:
+ FirmwareDialog::run(g_wxMainFrame);
+ break;
+ default:
+ break;
+ }
+ });
+ mode_menu->Bind(wxEVT_MENU, [config_id_base](wxEvent& event) {
+ std::string mode = event.GetId() - config_id_base == ConfigMenuModeExpert ?
+ "expert" : "simple";
+ g_AppConfig->set("view_mode", mode);
+ g_AppConfig->save();
+ update_mode();
+ });
+ menu->Append(local_menu, _(L("&Configuration")));
+}
+
+void add_menus(wxMenuBar *menu, int event_preferences_changed, int event_language_change)
+{
+ add_config_menu(menu, event_preferences_changed, event_language_change);
+}
+
+void open_model(wxWindow *parent, wxArrayString& input_files){
+ t_file_wild_card vec_FILE_WILDCARDS = get_file_wild_card();
+ std::vector<std::string> file_types = { "known", "stl", "obj", "amf", "3mf", "prusa" };
+ wxString MODEL_WILDCARD;
+ for (auto file_type : file_types)
+ MODEL_WILDCARD += vec_FILE_WILDCARDS.at(file_type) + "|";
+
+ auto dlg_title = _(L("Choose one or more files (STL/OBJ/AMF/3MF/PRUSA):"));
+ auto dialog = new wxFileDialog(parent /*? parent : GetTopWindow(g_wxMainFrame)*/, dlg_title,
+ g_AppConfig->get_last_dir(), "",
+ MODEL_WILDCARD, wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST);
+ if (dialog->ShowModal() != wxID_OK) {
+ dialog->Destroy();
+ return ;
+ }
+
+ dialog->GetPaths(input_files);
+ dialog->Destroy();
+}
+
+// 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()
+{
+ 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;
+}
+
+bool config_wizard_startup(bool app_config_exists)
+{
+ if (! app_config_exists || g_PresetBundle->printers.size() <= 1) {
+ config_wizard(ConfigWizard::RR_DATA_EMPTY);
+ return true;
+ } else if (g_AppConfig->legacy_datadir()) {
+ // Looks like user has legacy pre-vendorbundle data directory,
+ // explain what this is and run the wizard
+
+ MsgDataLegacy dlg;
+ dlg.ShowModal();
+
+ config_wizard(ConfigWizard::RR_DATA_LEGACY);
+ return true;
+ }
+ return false;
+}
+
+void config_wizard(int reason)
+{
+ // Exit wizard if there are unsaved changes and the user cancels the action.
+ if (! check_unsaved_changes())
+ return;
+
+ try {
+ ConfigWizard wizard(nullptr, static_cast<ConfigWizard::RunReason>(reason));
+ wizard.run(g_PresetBundle, g_PresetUpdater);
+ }
+ catch (const std::exception &e) {
+ show_error(nullptr, e.what());
+ }
+
+ // Load the currently selected preset into the GUI, update the preset selection box.
+ load_current_presets();
+}
+
+void open_preferences_dialog(int event_preferences)
+{
+ auto dlg = new PreferencesDialog(g_wxMainFrame, event_preferences);
+ dlg->ShowModal();
+}
+
+void create_preset_tabs(int event_value_change, int event_presets_changed)
+{
+ update_label_colours_from_appconfig();
+ add_created_tab(new TabPrint (g_wxTabPanel), event_value_change, event_presets_changed);
+ add_created_tab(new TabFilament (g_wxTabPanel), event_value_change, event_presets_changed);
+ add_created_tab(new TabSLAMaterial (g_wxTabPanel), event_value_change, event_presets_changed);
+ add_created_tab(new TabPrinter (g_wxTabPanel), event_value_change, event_presets_changed);
+}
+
+std::vector<PresetTab> preset_tabs = {
+ { "print", nullptr, ptFFF },
+ { "filament", nullptr, ptFFF },
+ { "sla_material", nullptr, ptSLA }
+};
+const std::vector<PresetTab>& get_preset_tabs() {
+ return preset_tabs;
+}
+
+Tab* get_tab(const std::string& name)
+{
+ std::vector<PresetTab>::iterator it = std::find_if(preset_tabs.begin(), preset_tabs.end(),
+ [name](PresetTab& tab){ return name == tab.name; });
+ return it != preset_tabs.end() ? it->panel : nullptr;
+}
+
+TabIface* get_preset_tab_iface(char *name)
+{
+ Tab* tab = get_tab(name);
+ if (tab) return new TabIface(tab);
+
+ for (size_t i = 0; i < g_wxTabPanel->GetPageCount(); ++ i) {
+ Tab *tab = dynamic_cast<Tab*>(g_wxTabPanel->GetPage(i));
+ if (! tab)
+ continue;
+ if (tab->name() == name) {
+ return new TabIface(tab);
+ }
+ }
+ return new TabIface(nullptr);
+}
+
+PreviewIface* create_preview_iface(wxNotebook* parent, DynamicPrintConfig* config, Print* print, GCodePreviewData* gcode_preview_data)
+{
+ if (g_preview == nullptr)
+ {
+ Preview* panel = new Preview(parent, config, print, gcode_preview_data);
+ g_preview = new PreviewIface(panel);
+ }
+
+ return g_preview;
+}
+
+// opt_index = 0, by the reason of zero-index in ConfigOptionVector by default (in case only one element)
+void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt_key, const boost::any& value, int opt_index /*= 0*/)
+{
+ try{
+ switch (config.def()->get(opt_key)->type){
+ case coFloatOrPercent:{
+ std::string str = boost::any_cast<std::string>(value);
+ bool percent = false;
+ if (str.back() == '%'){
+ str.pop_back();
+ percent = true;
+ }
+ double val = stod(str);
+ config.set_key_value(opt_key, new ConfigOptionFloatOrPercent(val, percent));
+ break;}
+ case coPercent:
+ config.set_key_value(opt_key, new ConfigOptionPercent(boost::any_cast<double>(value)));
+ break;
+ case coFloat:{
+ double& val = config.opt_float(opt_key);
+ val = boost::any_cast<double>(value);
+ break;
+ }
+ case coPercents:{
+ ConfigOptionPercents* vec_new = new ConfigOptionPercents{ boost::any_cast<double>(value) };
+ config.option<ConfigOptionPercents>(opt_key)->set_at(vec_new, opt_index, opt_index);
+ break;
+ }
+ case coFloats:{
+ ConfigOptionFloats* vec_new = new ConfigOptionFloats{ boost::any_cast<double>(value) };
+ config.option<ConfigOptionFloats>(opt_key)->set_at(vec_new, opt_index, opt_index);
+ break;
+ }
+ case coString:
+ config.set_key_value(opt_key, new ConfigOptionString(boost::any_cast<std::string>(value)));
+ break;
+ case coStrings:{
+ if (opt_key.compare("compatible_printers") == 0) {
+ config.option<ConfigOptionStrings>(opt_key)->values =
+ boost::any_cast<std::vector<std::string>>(value);
+ }
+ else if (config.def()->get(opt_key)->gui_flags.compare("serialized") == 0){
+ std::string str = boost::any_cast<std::string>(value);
+ if (str.back() == ';') str.pop_back();
+ // Split a string to multiple strings by a semi - colon.This is the old way of storing multi - string values.
+ // Currently used for the post_process config value only.
+ std::vector<std::string> values;
+ boost::split(values, str, boost::is_any_of(";"));
+ if (values.size() == 1 && values[0] == "")
+ break;
+ config.option<ConfigOptionStrings>(opt_key)->values = values;
+ }
+ else{
+ ConfigOptionStrings* vec_new = new ConfigOptionStrings{ boost::any_cast<std::string>(value) };
+ config.option<ConfigOptionStrings>(opt_key)->set_at(vec_new, opt_index, 0);
+ }
+ }
+ break;
+ case coBool:
+ config.set_key_value(opt_key, new ConfigOptionBool(boost::any_cast<bool>(value)));
+ break;
+ case coBools:{
+ ConfigOptionBools* vec_new = new ConfigOptionBools{ (bool)boost::any_cast<unsigned char>(value) };
+ config.option<ConfigOptionBools>(opt_key)->set_at(vec_new, opt_index, 0);
+ break;}
+ case coInt:
+ config.set_key_value(opt_key, new ConfigOptionInt(boost::any_cast<int>(value)));
+ break;
+ case coInts:{
+ ConfigOptionInts* vec_new = new ConfigOptionInts{ boost::any_cast<int>(value) };
+ config.option<ConfigOptionInts>(opt_key)->set_at(vec_new, opt_index, 0);
+ }
+ break;
+ case coEnum:{
+ if (opt_key.compare("external_fill_pattern") == 0 ||
+ opt_key.compare("fill_pattern") == 0)
+ config.set_key_value(opt_key, new ConfigOptionEnum<InfillPattern>(boost::any_cast<InfillPattern>(value)));
+ else if (opt_key.compare("gcode_flavor") == 0)
+ config.set_key_value(opt_key, new ConfigOptionEnum<GCodeFlavor>(boost::any_cast<GCodeFlavor>(value)));
+ else if (opt_key.compare("support_material_pattern") == 0)
+ config.set_key_value(opt_key, new ConfigOptionEnum<SupportMaterialPattern>(boost::any_cast<SupportMaterialPattern>(value)));
+ else if (opt_key.compare("seam_position") == 0)
+ config.set_key_value(opt_key, new ConfigOptionEnum<SeamPosition>(boost::any_cast<SeamPosition>(value)));
+ else if (opt_key.compare("host_type") == 0)
+ config.set_key_value(opt_key, new ConfigOptionEnum<PrintHostType>(boost::any_cast<PrintHostType>(value)));
+ }
+ break;
+ case coPoints:{
+ if (opt_key.compare("bed_shape") == 0){
+ config.option<ConfigOptionPoints>(opt_key)->values = boost::any_cast<std::vector<Vec2d>>(value);
+ break;
+ }
+ ConfigOptionPoints* vec_new = new ConfigOptionPoints{ boost::any_cast<Vec2d>(value) };
+ config.option<ConfigOptionPoints>(opt_key)->set_at(vec_new, opt_index, 0);
+ }
+ break;
+ case coNone:
+ break;
+ default:
+ break;
+ }
+ }
+ catch (const std::exception &e)
+ {
+ int i = 0;//no reason, just experiment
+ }
+}
+
+void add_created_tab(Tab* panel, int event_value_change, int event_presets_changed)
+{
+ panel->create_preset_tab(g_PresetBundle);
+
+ // Load the currently selected preset into the GUI, update the preset selection box.
+ panel->load_current_preset();
+
+ panel->set_event_value_change(wxEventType(event_value_change));
+ panel->set_event_presets_changed(wxEventType(event_presets_changed));
+
+ const wxString& tab_name = panel->GetName();
+ bool add_panel = true;
+
+ auto it = std::find_if( preset_tabs.begin(), preset_tabs.end(),
+ [tab_name](PresetTab& tab){return tab.name == tab_name; });
+ if (it != preset_tabs.end()) {
+ it->panel = panel;
+ add_panel = it->technology == g_PresetBundle->printers.get_edited_preset().printer_technology();
+ }
+
+ if (add_panel)
+ g_wxTabPanel->AddPage(panel, panel->title());
+}
+
+void load_current_presets()
+{
+ for (Tab *tab : g_tabs_list) {
+ tab->load_current_preset();
+ }
+}
+
+void show_error(wxWindow* parent, const wxString& message) {
+ ErrorDialog msg(parent, message);
+ msg.ShowModal();
+}
+
+void show_error_id(int id, const std::string& message) {
+ auto *parent = id != 0 ? wxWindow::FindWindowById(id) : nullptr;
+ show_error(parent, wxString::FromUTF8(message.data()));
+}
+
+void show_info(wxWindow* parent, const wxString& message, const wxString& title){
+ wxMessageDialog msg_wingow(parent, message, title.empty() ? _(L("Notice")) : title, wxOK | wxICON_INFORMATION);
+ msg_wingow.ShowModal();
+}
+
+void warning_catcher(wxWindow* parent, const wxString& message){
+ if (message == "GLUquadricObjPtr | " + _(L("Attempt to free unreferenced scalar")) )
+ return;
+ wxMessageDialog msg(parent, message, _(L("Warning")), wxOK | wxICON_WARNING);
+ msg.ShowModal();
+}
+
+// Assign a Lambda to the print object to emit a wxWidgets Command with the provided ID
+// to deliver a progress status message.
+void set_print_callback_event(Print *print, int id)
+{
+ print->set_status_callback([id](int percent, const std::string &message){
+ wxCommandEvent event(id);
+ event.SetInt(percent);
+ event.SetString(message);
+ wxQueueEvent(g_wxMainFrame, event.Clone());
+ });
+}
+
+wxApp* get_app(){
+ return g_wxApp;
+}
+
+PresetBundle* get_preset_bundle()
+{
+ return g_PresetBundle;
+}
+
+const wxColour& get_label_clr_modified() {
+ return g_color_label_modified;
+}
+
+const wxColour& get_label_clr_sys() {
+ return g_color_label_sys;
+}
+
+void set_label_clr_modified(const wxColour& clr) {
+ g_color_label_modified = clr;
+ auto clr_str = wxString::Format(wxT("#%02X%02X%02X"), clr.Red(), clr.Green(), clr.Blue());
+ std::string str = clr_str.ToStdString();
+ g_AppConfig->set("label_clr_modified", str);
+ g_AppConfig->save();
+}
+
+void set_label_clr_sys(const wxColour& clr) {
+ g_color_label_sys = clr;
+ auto clr_str = wxString::Format(wxT("#%02X%02X%02X"), clr.Red(), clr.Green(), clr.Blue());
+ std::string str = clr_str.ToStdString();
+ g_AppConfig->set("label_clr_sys", str);
+ g_AppConfig->save();
+}
+
+const wxFont& small_font(){
+ return g_small_font;
+}
+
+const wxFont& bold_font(){
+ return g_bold_font;
+}
+
+const wxColour& get_label_clr_default() {
+ return g_color_label_default;
+}
+
+unsigned get_colour_approx_luma(const wxColour &colour)
+{
+ double r = colour.Red();
+ double g = colour.Green();
+ double b = colour.Blue();
+
+ return std::round(std::sqrt(
+ r * r * .241 +
+ g * g * .691 +
+ b * b * .068
+ ));
+}
+
+wxWindow* get_right_panel(){
+ return g_right_panel;
+}
+
+wxNotebook * get_tab_panel() {
+ return g_wxTabPanel;
+}
+
+const size_t& label_width(){
+ return m_label_width;
+}
+
+void create_combochecklist(wxComboCtrl* comboCtrl, std::string text, std::string items, bool initial_value)
+{
+ if (comboCtrl == nullptr)
+ return;
+
+ wxCheckListBoxComboPopup* popup = new wxCheckListBoxComboPopup;
+ if (popup != nullptr)
+ {
+ // FIXME If the following line is removed, the combo box popup list will not react to mouse clicks.
+ // On the other side, with this line the combo box popup cannot be closed by clicking on the combo button on Windows 10.
+ comboCtrl->UseAltPopupWindow();
+
+ comboCtrl->EnablePopupAnimation(false);
+ comboCtrl->SetPopupControl(popup);
+ popup->SetStringValue(from_u8(text));
+ popup->Bind(wxEVT_CHECKLISTBOX, [popup](wxCommandEvent& evt) { popup->OnCheckListBox(evt); });
+ popup->Bind(wxEVT_LISTBOX, [popup](wxCommandEvent& evt) { popup->OnListBoxSelection(evt); });
+ popup->Bind(wxEVT_KEY_DOWN, [popup](wxKeyEvent& evt) { popup->OnKeyEvent(evt); });
+ popup->Bind(wxEVT_KEY_UP, [popup](wxKeyEvent& evt) { popup->OnKeyEvent(evt); });
+
+ std::vector<std::string> items_str;
+ boost::split(items_str, items, boost::is_any_of("|"), boost::token_compress_off);
+
+ for (const std::string& item : items_str)
+ {
+ popup->Append(from_u8(item));
+ }
+
+ for (unsigned int i = 0; i < popup->GetCount(); ++i)
+ {
+ popup->Check(i, initial_value);
+ }
+ }
+}
+
+int combochecklist_get_flags(wxComboCtrl* comboCtrl)
+{
+ int flags = 0;
+
+ wxCheckListBoxComboPopup* popup = wxDynamicCast(comboCtrl->GetPopupControl(), wxCheckListBoxComboPopup);
+ if (popup != nullptr)
+ {
+ for (unsigned int i = 0; i < popup->GetCount(); ++i)
+ {
+ if (popup->IsChecked(i))
+ flags |= 1 << i;
+ }
+ }
+
+ return flags;
+}
+
+AppConfig* get_app_config()
+{
+ return g_AppConfig;
+}
+
+wxString L_str(const std::string &str)
+{
+ //! Explicitly specify that the source string is already in UTF-8 encoding
+ return wxGetTranslation(wxString(str.c_str(), wxConvUTF8));
+}
+
+wxString from_u8(const std::string &str)
+{
+ return wxString::FromUTF8(str.c_str());
+}
+
+void set_model_events_from_perl(Model &model,
+ int event_object_selection_changed,
+ int event_object_settings_changed,
+ int event_remove_object,
+ int event_update_scene)
+{
+ set_event_object_selection_changed(event_object_selection_changed);
+ set_event_object_settings_changed(event_object_settings_changed);
+ set_event_remove_object(event_remove_object);
+ set_event_update_scene(event_update_scene);
+ set_objects_from_model(model);
+ init_mesh_icons();
+
+// wxWindowUpdateLocker noUpdates(parent);
+
+// add_objects_list(parent, sizer);
+
+// add_collapsible_panes(parent, sizer);
+}
+
+void add_frequently_changed_parameters(wxWindow* parent, wxBoxSizer* sizer, wxFlexGridSizer* preset_sizer)
+{
+ DynamicPrintConfig* config = &g_PresetBundle->prints.get_edited_preset().config;
+ std::shared_ptr<ConfigOptionsGroup> optgroup = std::make_shared<ConfigOptionsGroup>(parent, "", config);
+ const wxArrayInt& ar = preset_sizer->GetColWidths();
+ m_label_width = ar.IsEmpty() ? 100 : ar.front()-4;
+ optgroup->label_width = m_label_width;
+
+ //Frequently changed parameters
+ optgroup->m_on_change = [config](t_config_option_key opt_key, boost::any value){
+ TabPrint* tab_print = nullptr;
+ for (size_t i = 0; i < g_wxTabPanel->GetPageCount(); ++i) {
+ Tab *tab = dynamic_cast<Tab*>(g_wxTabPanel->GetPage(i));
+ if (!tab)
+ continue;
+ if (tab->name() == "print"){
+ tab_print = static_cast<TabPrint*>(tab);
+ break;
+ }
+ }
+ if (tab_print == nullptr)
+ return;
+
+ if (opt_key == "fill_density"){
+ value = m_optgroups[ogFrequentlyChangingParameters]->get_config_value(*config, opt_key);
+ tab_print->set_value(opt_key, value);
+ tab_print->update();
+ }
+ else{
+ DynamicPrintConfig new_conf = *config;
+ if (opt_key == "brim"){
+ double new_val;
+ double brim_width = config->opt_float("brim_width");
+ if (boost::any_cast<bool>(value) == true)
+ {
+ new_val = m_brim_width == 0.0 ? 10 :
+ m_brim_width < 0.0 ? m_brim_width * (-1) :
+ m_brim_width;
+ }
+ else{
+ m_brim_width = brim_width * (-1);
+ new_val = 0;
+ }
+ new_conf.set_key_value("brim_width", new ConfigOptionFloat(new_val));
+ }
+ else{ //(opt_key == "support")
+ const wxString& selection = boost::any_cast<wxString>(value);
+
+ auto support_material = selection == _("None") ? false : true;
+ new_conf.set_key_value("support_material", new ConfigOptionBool(support_material));
+
+ if (selection == _("Everywhere"))
+ new_conf.set_key_value("support_material_buildplate_only", new ConfigOptionBool(false));
+ else if (selection == _("Support on build plate only"))
+ new_conf.set_key_value("support_material_buildplate_only", new ConfigOptionBool(true));
+ }
+ tab_print->load_config(new_conf);
+ }
+
+ tab_print->update_dirty();
+ };
+
+ Option option = optgroup->get_option("fill_density");
+ option.opt.sidetext = "";
+ option.opt.full_width = true;
+ optgroup->append_single_option_line(option);
+
+ ConfigOptionDef def;
+
+ def.label = L("Support");
+ def.type = coStrings;
+ def.gui_type = "select_open";
+ def.tooltip = L("Select what kind of support do you need");
+ def.enum_labels.push_back(L("None"));
+ def.enum_labels.push_back(L("Support on build plate only"));
+ def.enum_labels.push_back(L("Everywhere"));
+ std::string selection = !config->opt_bool("support_material") ?
+ "None" :
+ config->opt_bool("support_material_buildplate_only") ?
+ "Support on build plate only" :
+ "Everywhere";
+ def.default_value = new ConfigOptionStrings { selection };
+ option = Option(def, "support");
+ option.opt.full_width = true;
+ optgroup->append_single_option_line(option);
+
+ m_brim_width = config->opt_float("brim_width");
+ def.label = L("Brim");
+ def.type = coBool;
+ def.tooltip = L("This flag enables the brim that will be printed around each object on the first layer.");
+ def.gui_type = "";
+ def.default_value = new ConfigOptionBool{ m_brim_width > 0.0 ? true : false };
+ option = Option(def, "brim");
+ optgroup->append_single_option_line(option);
+
+
+ Line line = { "", "" };
+ line.widget = [config](wxWindow* parent){
+ g_wiping_dialog_button = new wxButton(parent, wxID_ANY, _(L("Purging volumes")) + dots, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT);
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(g_wiping_dialog_button);
+ g_wiping_dialog_button->Bind(wxEVT_BUTTON, ([parent](wxCommandEvent& e)
+ {
+ auto &config = g_PresetBundle->project_config;
+ const std::vector<double> &init_matrix = (config.option<ConfigOptionFloats>("wiping_volumes_matrix"))->values;
+ const std::vector<double> &init_extruders = (config.option<ConfigOptionFloats>("wiping_volumes_extruders"))->values;
+
+ WipingDialog dlg(parent,cast<float>(init_matrix),cast<float>(init_extruders));
+
+ if (dlg.ShowModal() == wxID_OK) {
+ std::vector<float> matrix = dlg.get_matrix();
+ std::vector<float> extruders = dlg.get_extruders();
+ (config.option<ConfigOptionFloats>("wiping_volumes_matrix"))->values = std::vector<double>(matrix.begin(),matrix.end());
+ (config.option<ConfigOptionFloats>("wiping_volumes_extruders"))->values = std::vector<double>(extruders.begin(),extruders.end());
+ g_on_request_update_callback.call();
+ }
+ }));
+ return sizer;
+ };
+ optgroup->append_line(line);
+
+ sizer->Add(optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT, 2);
+
+ m_optgroups.push_back(optgroup);// ogFrequentlyChangingParameters
+
+ // Object List
+ add_objects_list(parent, sizer);
+
+ // Frequently Object Settings
+ add_object_settings(parent, sizer);
+}
+
+void show_frequently_changed_parameters(bool show)
+{
+ g_frequently_changed_parameters_sizer->Show(show);
+ if (!show) return;
+
+ for (size_t i = 0; i < g_wxTabPanel->GetPageCount(); ++i) {
+ Tab *tab = dynamic_cast<Tab*>(g_wxTabPanel->GetPage(i));
+ if (!tab)
+ continue;
+ tab->update_wiping_button_visibility();
+ break;
+ }
+}
+
+void show_buttons(bool show)
+{
+ g_buttons[abReslice]->Show(show);
+ for (size_t i = 0; i < g_wxTabPanel->GetPageCount(); ++i) {
+ TabPrinter *tab = dynamic_cast<TabPrinter*>(g_wxTabPanel->GetPage(i));
+ if (!tab)
+ continue;
+ if (g_PresetBundle->printers.get_selected_preset().printer_technology() == ptFFF) {
+ g_buttons[abPrint]->Show(show && !tab->m_config->opt_string("serial_port").empty());
+ g_buttons[abSendGCode]->Show(show && !tab->m_config->opt_string("print_host").empty());
+ }
+ break;
+ }
+}
+
+void show_info_sizer(const bool show)
+{
+ g_info_sizer->Show(static_cast<size_t>(0), show);
+ g_info_sizer->Show(1, show && g_show_print_info);
+ g_manifold_warning_icon->Show(show && g_show_manifold_warning_icon);
+}
+
+void show_object_name(bool show)
+{
+ wxGridSizer* grid_sizer = get_optgroup(ogFrequentlyObjectSettings)->get_grid_sizer();
+ grid_sizer->Show(static_cast<size_t>(0), show);
+ grid_sizer->Show(static_cast<size_t>(1), show);
+}
+
+void update_mode()
+{
+ wxWindowUpdateLocker noUpdates(g_right_panel->GetParent());
+
+ ConfigMenuIDs mode = get_view_mode();
+
+ g_object_list_sizer->Show(mode == ConfigMenuModeExpert);
+ show_info_sizer(mode == ConfigMenuModeExpert);
+ show_buttons(mode == ConfigMenuModeExpert);
+ show_object_name(mode == ConfigMenuModeSimple);
+ show_manipulation_sizer(mode == ConfigMenuModeSimple);
+
+ // TODO There is a not the best place of it!
+ // *** Update showing of the collpane_settings
+// show_collpane_settings(mode == ConfigMenuModeExpert);
+ // *************************
+ g_right_panel->Layout();
+ g_right_panel->GetParent()->Layout();
+}
+
+bool is_expert_mode(){
+ return get_view_mode() == ConfigMenuModeExpert;
+}
+
+ConfigOptionsGroup* get_optgroup(size_t i)
+{
+ return m_optgroups[i].get();
+}
+
+std::vector <std::shared_ptr<ConfigOptionsGroup>>& get_optgroups() {
+ return m_optgroups;
+}
+
+wxButton* get_wiping_dialog_button()
+{
+ return g_wiping_dialog_button;
+}
+
+wxWindow* export_option_creator(wxWindow* parent)
+{
+ wxPanel* panel = new wxPanel(parent, -1);
+ wxSizer* sizer = new wxBoxSizer(wxHORIZONTAL);
+ wxCheckBox* cbox = new wxCheckBox(panel, wxID_HIGHEST + 1, L("Export print config"));
+ cbox->SetValue(true);
+ sizer->AddSpacer(5);
+ sizer->Add(cbox, 0, wxEXPAND | wxALL | wxALIGN_CENTER_VERTICAL, 5);
+ panel->SetSizer(sizer);
+ sizer->SetSizeHints(panel);
+ return panel;
+}
+
+void add_export_option(wxFileDialog* dlg, const std::string& format)
+{
+ if ((dlg != nullptr) && (format == "AMF") || (format == "3MF"))
+ {
+ if (dlg->SupportsExtraControl())
+ dlg->SetExtraControlCreator(export_option_creator);
+ }
+}
+
+int get_export_option(wxFileDialog* dlg)
+{
+ if (dlg != nullptr)
+ {
+ wxWindow* wnd = dlg->GetExtraControl();
+ if (wnd != nullptr)
+ {
+ wxPanel* panel = dynamic_cast<wxPanel*>(wnd);
+ if (panel != nullptr)
+ {
+ wxWindow* child = panel->FindWindow(wxID_HIGHEST + 1);
+ if (child != nullptr)
+ {
+ wxCheckBox* cbox = dynamic_cast<wxCheckBox*>(child);
+ if (cbox != nullptr)
+ return cbox->IsChecked() ? 1 : 0;
+ }
+ }
+ }
+ }
+
+ return 0;
+
+}
+
+bool get_current_screen_size(wxWindow *window, unsigned &width, unsigned &height)
+{
+ const auto idx = wxDisplay::GetFromWindow(window);
+ if (idx == wxNOT_FOUND) {
+ return false;
+ }
+
+ wxDisplay display(idx);
+ const auto disp_size = display.GetClientArea();
+ width = disp_size.GetWidth();
+ height = disp_size.GetHeight();
+
+ return true;
+}
+
+void save_window_size(wxTopLevelWindow *window, const std::string &name)
+{
+ const wxSize size = window->GetSize();
+ const wxPoint pos = window->GetPosition();
+ const auto maximized = window->IsMaximized() ? "1" : "0";
+
+ g_AppConfig->set((boost::format("window_%1%_size") % name).str(), (boost::format("%1%;%2%") % size.GetWidth() % size.GetHeight()).str());
+ g_AppConfig->set((boost::format("window_%1%_maximized") % name).str(), maximized);
+}
+
+void restore_window_size(wxTopLevelWindow *window, const std::string &name)
+{
+ // XXX: This still doesn't behave nicely in some situations (mostly on Linux).
+ // The problem is that it's hard to obtain window position with respect to screen geometry reliably
+ // from wxWidgets. Sometimes wxWidgets claim a window is located on a different screen than on which
+ // it's actually visible. I suspect this has something to do with window initialization (maybe we
+ // restore window geometry too early), but haven't yet found a workaround.
+
+ const auto display_idx = wxDisplay::GetFromWindow(window);
+ if (display_idx == wxNOT_FOUND) { return; }
+
+ const auto display = wxDisplay(display_idx).GetClientArea();
+ std::vector<std::string> pair;
+
+ try {
+ const auto key_size = (boost::format("window_%1%_size") % name).str();
+ if (g_AppConfig->has(key_size)) {
+ if (unescape_strings_cstyle(g_AppConfig->get(key_size), pair) && pair.size() == 2) {
+ auto width = boost::lexical_cast<int>(pair[0]);
+ auto height = boost::lexical_cast<int>(pair[1]);
+
+ window->SetSize(width, height);
+ }
+ }
+ } catch(const boost::bad_lexical_cast &) {}
+
+ // Maximizing should be the last thing to do.
+ // This ensure the size and position are sane when the user un-maximizes the window.
+ const auto key_maximized = (boost::format("window_%1%_maximized") % name).str();
+ if (g_AppConfig->get(key_maximized) == "1") {
+ window->Maximize(true);
+ }
+}
+
+void enable_action_buttons(bool enable)
+{
+ if (g_buttons.empty())
+ return;
+
+ // Update background colour for buttons
+ const wxColour bgrd_color = enable ? wxColour(224, 224, 224/*255, 96, 0*/) : wxColour(204, 204, 204);
+
+ for (auto btn : g_buttons) {
+ btn->Enable(enable);
+ btn->SetBackgroundColour(bgrd_color);
+ }
+}
+
+void about()
+{
+ AboutDialog dlg;
+ dlg.ShowModal();
+ dlg.Destroy();
+}
+
+void desktop_open_datadir_folder()
+{
+ // Execute command to open a file explorer, platform dependent.
+ // FIXME: The const_casts aren't needed in wxWidgets 3.1, remove them when we upgrade.
+
+ const auto path = data_dir();
+#ifdef _WIN32
+ const auto widepath = wxString::FromUTF8(path.data());
+ const wchar_t *argv[] = { L"explorer", widepath.GetData(), nullptr };
+ ::wxExecute(const_cast<wchar_t**>(argv), wxEXEC_ASYNC, nullptr);
+#elif __APPLE__
+ const char *argv[] = { "open", path.data(), nullptr };
+ ::wxExecute(const_cast<char**>(argv), wxEXEC_ASYNC, nullptr);
+#else
+ const char *argv[] = { "xdg-open", path.data(), nullptr };
+
+ // Check if we're running in an AppImage container, if so, we need to remove AppImage's env vars,
+ // because they may mess up the environment expected by the file manager.
+ // Mostly this is about LD_LIBRARY_PATH, but we remove a few more too for good measure.
+ if (wxGetEnv("APPIMAGE", nullptr)) {
+ // We're running from AppImage
+ wxEnvVariableHashMap env_vars;
+ wxGetEnvMap(&env_vars);
+
+ env_vars.erase("APPIMAGE");
+ env_vars.erase("APPDIR");
+ env_vars.erase("LD_LIBRARY_PATH");
+ env_vars.erase("LD_PRELOAD");
+ env_vars.erase("UNION_PRELOAD");
+
+ wxExecuteEnv exec_env;
+ exec_env.env = std::move(env_vars);
+
+ wxString owd;
+ if (wxGetEnv("OWD", &owd)) {
+ // This is the original work directory from which the AppImage image was run,
+ // set it as CWD for the child process:
+ exec_env.cwd = std::move(owd);
+ }
+
+ ::wxExecute(const_cast<char**>(argv), wxEXEC_ASYNC, nullptr, &exec_env);
+ } else {
+ // Looks like we're NOT running from AppImage, we'll make no changes to the environment.
+ ::wxExecute(const_cast<char**>(argv), wxEXEC_ASYNC, nullptr, nullptr);
+ }
+#endif
+}
+
+} }
diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp
new file mode 100644
index 000000000..8dfaf42c6
--- /dev/null
+++ b/src/slic3r/GUI/GUI.hpp
@@ -0,0 +1,260 @@
+#ifndef slic3r_GUI_hpp_
+#define slic3r_GUI_hpp_
+
+#include <string>
+#include <vector>
+#include "PrintConfig.hpp"
+#include "../../callback.hpp"
+#include "GUI_ObjectParts.hpp"
+
+#include <wx/intl.h>
+#include <wx/string.h>
+
+class wxApp;
+class wxWindow;
+class wxFrame;
+class wxMenuBar;
+class wxNotebook;
+class wxPanel;
+class wxComboCtrl;
+class wxString;
+class wxArrayString;
+class wxArrayLong;
+class wxColour;
+class wxBoxSizer;
+class wxFlexGridSizer;
+class wxButton;
+class wxFileDialog;
+class wxStaticBitmap;
+class wxFont;
+class wxTopLevelWindow;
+
+namespace Slic3r {
+
+class PresetBundle;
+class PresetCollection;
+class Print;
+class ProgressStatusBar;
+class AppConfig;
+class PresetUpdater;
+class DynamicPrintConfig;
+class TabIface;
+class PreviewIface;
+class Print;
+class GCodePreviewData;
+
+#define _(s) Slic3r::GUI::I18N::translate((s))
+
+namespace GUI { namespace I18N {
+ inline wxString translate(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)); }
+ inline wxString translate(const wchar_t *s) { return wxGetTranslation(s); }
+ inline wxString translate(const std::string &s) { return wxGetTranslation(wxString(s.c_str(), wxConvUTF8)); }
+ inline wxString translate(const std::wstring &s) { return wxGetTranslation(s.c_str()); }
+} }
+
+// !!! If you needed to translate some wxString,
+// !!! please use _(L(string))
+// !!! _() - is a standard wxWidgets macro to translate
+// !!! L() is used only for marking localizable string
+// !!! It will be used in "xgettext" to create a Locating Message Catalog.
+#define L(s) s
+
+//! macro used to localization, return wxScopedCharBuffer
+//! With wxConvUTF8 explicitly specify that the source string is already in UTF-8 encoding
+#define _CHB(s) wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str()
+
+// Minimal buffer length for translated string (char buf[MIN_BUF_LENGTH_FOR_L])
+#define MIN_BUF_LENGTH_FOR_L 512
+
+namespace GUI {
+
+class Tab;
+class ConfigOptionsGroup;
+// Map from an file_type name to full file wildcard name.
+typedef std::map<std::string, std::string> t_file_wild_card;
+inline t_file_wild_card& get_file_wild_card() {
+ static t_file_wild_card FILE_WILDCARDS;
+ if (FILE_WILDCARDS.empty()){
+ FILE_WILDCARDS["known"] = "Known files (*.stl, *.obj, *.amf, *.xml, *.prusa)|*.stl;*.STL;*.obj;*.OBJ;*.amf;*.AMF;*.xml;*.XML;*.prusa;*.PRUSA";
+ FILE_WILDCARDS["stl"] = "STL files (*.stl)|*.stl;*.STL";
+ FILE_WILDCARDS["obj"] = "OBJ files (*.obj)|*.obj;*.OBJ";
+ FILE_WILDCARDS["amf"] = "AMF files (*.amf)|*.zip.amf;*.amf;*.AMF;*.xml;*.XML";
+ FILE_WILDCARDS["3mf"] = "3MF files (*.3mf)|*.3mf;*.3MF;";
+ FILE_WILDCARDS["prusa"] = "Prusa Control files (*.prusa)|*.prusa;*.PRUSA";
+ FILE_WILDCARDS["ini"] = "INI files *.ini|*.ini;*.INI";
+ FILE_WILDCARDS["gcode"] = "G-code files (*.gcode, *.gco, *.g, *.ngc)|*.gcode;*.GCODE;*.gco;*.GCO;*.g;*.G;*.ngc;*.NGC";
+ FILE_WILDCARDS["svg"] = "SVG files *.svg|*.svg;*.SVG";
+ }
+ return FILE_WILDCARDS;
+}
+
+struct PresetTab {
+ std::string name;
+ Tab* panel;
+ PrinterTechnology technology;
+};
+
+
+void disable_screensaver();
+void enable_screensaver();
+bool debugged();
+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);
+void set_progress_status_bar(ProgressStatusBar *prsb);
+void set_tab_panel(wxNotebook *tab_panel);
+void set_plater(wxPanel *plater);
+void set_app_config(AppConfig *app_config);
+void set_preset_bundle(PresetBundle *preset_bundle);
+void set_preset_updater(PresetUpdater *updater);
+void set_objects_from_perl( wxWindow* parent,
+ wxBoxSizer *frequently_changed_parameters_sizer,
+ wxBoxSizer *info_sizer,
+ wxButton *btn_export_gcode,
+ wxButton *btn_reslice,
+ wxButton *btn_print,
+ wxButton *btn_send_gcode,
+ wxStaticBitmap *manifold_warning_icon);
+void set_show_print_info(bool show);
+void set_show_manifold_warning_icon(bool show);
+void set_objects_list_sizer(wxBoxSizer *objects_list_sizer);
+
+AppConfig* get_app_config();
+wxApp* get_app();
+PresetBundle* get_preset_bundle();
+wxFrame* get_main_frame();
+ProgressStatusBar* get_progress_status_bar();
+wxNotebook * get_tab_panel();
+wxNotebook* get_tab_panel();
+
+const wxColour& get_label_clr_modified();
+const wxColour& get_label_clr_sys();
+const wxColour& get_label_clr_default();
+unsigned get_colour_approx_luma(const wxColour &colour);
+void set_label_clr_modified(const wxColour& clr);
+void set_label_clr_sys(const wxColour& clr);
+
+const wxFont& small_font();
+const wxFont& bold_font();
+
+void open_model(wxWindow *parent, wxArrayString& input_files);
+
+wxWindow* get_right_panel();
+const size_t& label_width();
+
+Tab* get_tab(const std::string& name);
+const std::vector<PresetTab>& get_preset_tabs();
+
+extern void add_menus(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();
+
+// Checks if configuration wizard needs to run, calls config_wizard if so.
+// Returns whether the Wizard ran.
+extern bool config_wizard_startup(bool app_config_exists);
+
+// Opens the configuration wizard, returns true if wizard is finished & accepted.
+// The run_reason argument is actually ConfigWizard::RunReason, but int is used here because of Perl.
+extern void config_wizard(int run_reason);
+
+// Create "Preferences" dialog after selecting menu "Preferences" in Perl part
+extern void open_preferences_dialog(int event_preferences);
+
+// Create a new preset tab (print, filament and printer),
+void create_preset_tabs(int event_value_change, int event_presets_changed);
+TabIface* get_preset_tab_iface(char *name);
+
+PreviewIface* create_preview_iface(wxNotebook* notebook, DynamicPrintConfig* config, Print* print, GCodePreviewData* gcode_preview_data);
+
+// add it at the end of the tab panel.
+void add_created_tab(Tab* panel, int event_value_change, int event_presets_changed);
+// Change option value in config
+void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt_key, const boost::any& value, int opt_index = 0);
+
+// Update UI / Tabs to reflect changes in the currently loaded presets
+void load_current_presets();
+
+void show_error(wxWindow* parent, const wxString& message);
+void show_error_id(int id, const std::string& message); // For Perl
+void show_info(wxWindow* parent, const wxString& message, const wxString& title);
+void warning_catcher(wxWindow* parent, const wxString& message);
+
+// Assign a Lambda to the print object to emit a wxWidgets Command with the provided ID
+// to deliver a progress status message.
+void set_print_callback_event(Print *print, int id);
+
+// load language saved at application config
+bool load_language();
+// save language at application config
+void save_language();
+// get list of installed languages
+void get_installed_languages(wxArrayString & names, wxArrayLong & identifiers);
+// select language from the list of installed languages
+bool select_language(wxArrayString & names, wxArrayLong & identifiers);
+// update right panel of the Plater according to view mode
+void update_mode();
+
+void show_info_sizer(const bool show);
+
+std::vector<Tab *>& get_tabs_list();
+bool checked_tab(Tab* tab);
+void delete_tab_from_list(Tab* tab);
+
+// Creates a wxCheckListBoxComboPopup inside the given wxComboCtrl, filled with the given text and items.
+// Items are all initialized to the given value.
+// Items must be separated by '|', for example "Item1|Item2|Item3", and so on.
+void create_combochecklist(wxComboCtrl* comboCtrl, std::string text, std::string items, bool initial_value);
+
+// Returns the current state of the items listed in the wxCheckListBoxComboPopup contained in the given wxComboCtrl,
+// encoded inside an int.
+int combochecklist_get_flags(wxComboCtrl* comboCtrl);
+
+// Return translated std::string as a wxString
+wxString L_str(const std::string &str);
+// Return wxString from std::string in UTF8
+wxString from_u8(const std::string &str);
+
+void set_model_events_from_perl(Model &model,
+ int event_object_selection_changed,
+ int event_object_settings_changed,
+ int event_remove_object,
+ int event_update_scene);
+void add_frequently_changed_parameters(wxWindow* parent, wxBoxSizer* sizer, wxFlexGridSizer* preset_sizer);
+// Update view mode according to selected menu
+void update_mode();
+bool is_expert_mode();
+
+// Callback to trigger a configuration update timer on the Plater.
+static PerlCallback g_on_request_update_callback;
+
+ConfigOptionsGroup* get_optgroup(size_t i);
+std::vector <std::shared_ptr<ConfigOptionsGroup>>& get_optgroups();
+wxButton* get_wiping_dialog_button();
+
+void add_export_option(wxFileDialog* dlg, const std::string& format);
+int get_export_option(wxFileDialog* dlg);
+
+// Returns the dimensions of the screen on which the main frame is displayed
+bool get_current_screen_size(wxWindow *window, unsigned &width, unsigned &height);
+
+// Save window size and maximized status into AppConfig
+void save_window_size(wxTopLevelWindow *window, const std::string &name);
+// Restore the above
+void restore_window_size(wxTopLevelWindow *window, const std::string &name);
+
+// Update buttons view according to enable/disable
+void enable_action_buttons(bool enable);
+
+// Display an About dialog
+extern void about();
+// Ask the destop to open the datadir using the default file explorer.
+extern void desktop_open_datadir_folder();
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif
diff --git a/src/slic3r/GUI/GUI_ObjectParts.cpp b/src/slic3r/GUI/GUI_ObjectParts.cpp
new file mode 100644
index 000000000..ae34359ce
--- /dev/null
+++ b/src/slic3r/GUI/GUI_ObjectParts.cpp
@@ -0,0 +1,2041 @@
+#include "GUI.hpp"
+#include "OptionsGroup.hpp"
+#include "PresetBundle.hpp"
+#include "GUI_ObjectParts.hpp"
+#include "Model.hpp"
+#include "wxExtensions.hpp"
+#include "LambdaObjectDialog.hpp"
+#include "../../libslic3r/Utils.hpp"
+
+#include <wx/msgdlg.h>
+#include <boost/filesystem.hpp>
+#include <boost/algorithm/string.hpp>
+#include "Geometry.hpp"
+#include "slic3r/Utils/FixModelByWin10.hpp"
+
+#include <wx/glcanvas.h>
+#include "3DScene.hpp"
+
+namespace Slic3r
+{
+namespace GUI
+{
+wxSizer *m_sizer_object_buttons = nullptr;
+wxSizer *m_sizer_part_buttons = nullptr;
+wxSizer *m_sizer_object_movers = nullptr;
+wxDataViewCtrl *m_objects_ctrl = nullptr;
+PrusaObjectDataViewModel *m_objects_model = nullptr;
+wxCollapsiblePane *m_collpane_settings = nullptr;
+PrusaDoubleSlider *m_slider = nullptr;
+wxGLCanvas *m_preview_canvas = nullptr;
+
+wxBitmap m_icon_modifiermesh;
+wxBitmap m_icon_solidmesh;
+wxBitmap m_icon_manifold_warning;
+wxBitmap m_bmp_cog;
+wxBitmap m_bmp_split;
+
+wxSlider* m_mover_x = nullptr;
+wxSlider* m_mover_y = nullptr;
+wxSlider* m_mover_z = nullptr;
+wxButton* m_btn_move_up = nullptr;
+wxButton* m_btn_move_down = nullptr;
+Vec3d m_move_options;
+Vec3d m_last_coords;
+int m_selected_object_id = -1;
+
+bool g_prevent_list_events = false; // We use this flag to avoid circular event handling Select()
+ // happens to fire a wxEVT_LIST_ITEM_SELECTED on OSX, whose event handler
+ // calls this method again and again and again
+bool g_is_percent_scale = false; // It indicates if scale unit is percentage
+bool g_is_uniform_scale = false; // It indicates if scale is uniform
+ModelObjectPtrs* m_objects;
+std::shared_ptr<DynamicPrintConfig*> m_config;
+std::shared_ptr<DynamicPrintConfig> m_default_config;
+wxBoxSizer* m_option_sizer = nullptr;
+
+// option groups for settings
+std::vector <std::shared_ptr<ConfigOptionsGroup>> m_og_settings;
+
+int m_event_object_selection_changed = 0;
+int m_event_object_settings_changed = 0;
+int m_event_remove_object = 0;
+int m_event_update_scene = 0;
+
+bool m_parts_changed = false;
+bool m_part_settings_changed = false;
+
+#ifdef __WXOSX__
+ wxString g_selected_extruder = "";
+#endif //__WXOSX__
+
+inline t_category_icon& get_category_icon() {
+ static t_category_icon CATEGORY_ICON;
+ if (CATEGORY_ICON.empty()){
+ CATEGORY_ICON[L("Layers and Perimeters")] = wxBitmap(from_u8(Slic3r::var("layers.png")), wxBITMAP_TYPE_PNG);
+ CATEGORY_ICON[L("Infill")] = wxBitmap(from_u8(Slic3r::var("infill.png")), wxBITMAP_TYPE_PNG);
+ CATEGORY_ICON[L("Support material")] = wxBitmap(from_u8(Slic3r::var("building.png")), wxBITMAP_TYPE_PNG);
+ CATEGORY_ICON[L("Speed")] = wxBitmap(from_u8(Slic3r::var("time.png")), wxBITMAP_TYPE_PNG);
+ CATEGORY_ICON[L("Extruders")] = wxBitmap(from_u8(Slic3r::var("funnel.png")), wxBITMAP_TYPE_PNG);
+ CATEGORY_ICON[L("Extrusion Width")] = wxBitmap(from_u8(Slic3r::var("funnel.png")), wxBITMAP_TYPE_PNG);
+// CATEGORY_ICON[L("Skirt and brim")] = wxBitmap(from_u8(Slic3r::var("box.png")), wxBITMAP_TYPE_PNG);
+// CATEGORY_ICON[L("Speed > Acceleration")] = wxBitmap(from_u8(Slic3r::var("time.png")), wxBITMAP_TYPE_PNG);
+ CATEGORY_ICON[L("Advanced")] = wxBitmap(from_u8(Slic3r::var("wand.png")), wxBITMAP_TYPE_PNG);
+ }
+ return CATEGORY_ICON;
+}
+
+std::vector<std::string> get_options(const bool is_part)
+{
+ PrintRegionConfig reg_config;
+ auto options = reg_config.keys();
+ if (!is_part) {
+ PrintObjectConfig obj_config;
+ std::vector<std::string> obj_options = obj_config.keys();
+ options.insert(options.end(), obj_options.begin(), obj_options.end());
+ }
+ return options;
+}
+
+// category -> vector ( option ; label )
+typedef std::map< std::string, std::vector< std::pair<std::string, std::string> > > settings_menu_hierarchy;
+void get_options_menu(settings_menu_hierarchy& settings_menu, bool is_part)
+{
+ auto options = get_options(is_part);
+
+ auto extruders_cnt = get_preset_bundle()->printers.get_selected_preset().printer_technology() == ptSLA ? 1 :
+ get_preset_bundle()->printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->values.size();
+
+ DynamicPrintConfig config;
+ for (auto& option : options)
+ {
+ auto const opt = config.def()->get(option);
+ auto category = opt->category;
+ if (category.empty() ||
+ (category == "Extruders" && extruders_cnt == 1)) continue;
+
+ std::pair<std::string, std::string> option_label(option, opt->label);
+ std::vector< std::pair<std::string, std::string> > new_category;
+ auto& cat_opt_label = settings_menu.find(category) == settings_menu.end() ? new_category : settings_menu.at(category);
+ cat_opt_label.push_back(option_label);
+ if (cat_opt_label.size() == 1)
+ settings_menu[category] = cat_opt_label;
+ }
+}
+
+void set_event_object_selection_changed(const int& event){
+ m_event_object_selection_changed = event;
+}
+void set_event_object_settings_changed(const int& event){
+ m_event_object_settings_changed = event;
+}
+void set_event_remove_object(const int& event){
+ m_event_remove_object = event;
+}
+void set_event_update_scene(const int& event){
+ m_event_update_scene = event;
+}
+
+void set_objects_from_model(Model &model) {
+ m_objects = &(model.objects);
+}
+
+void init_mesh_icons(){
+ m_icon_modifiermesh = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("lambda.png")), wxBITMAP_TYPE_PNG);//(Slic3r::var("plugin.png")), wxBITMAP_TYPE_PNG);
+ m_icon_solidmesh = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("object.png")), wxBITMAP_TYPE_PNG);//(Slic3r::var("package.png")), wxBITMAP_TYPE_PNG);
+
+ // init icon for manifold warning
+ m_icon_manifold_warning = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("exclamation_mark_.png")), wxBITMAP_TYPE_PNG);//(Slic3r::var("error.png")), wxBITMAP_TYPE_PNG);
+
+ // init bitmap for "Split to sub-objects" context menu
+ m_bmp_split = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("split.png")), wxBITMAP_TYPE_PNG);
+
+ // init bitmap for "Add Settings" context menu
+ m_bmp_cog = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("cog.png")), wxBITMAP_TYPE_PNG);
+}
+
+bool is_parts_changed(){return m_parts_changed;}
+bool is_part_settings_changed(){ return m_part_settings_changed; }
+
+static wxString dots("…", wxConvUTF8);
+
+void set_tooltip_for_item(const wxPoint& pt)
+{
+ wxDataViewItem item;
+ wxDataViewColumn* col;
+ m_objects_ctrl->HitTest(pt, item, col);
+ if (!item) return;
+
+ if (col->GetTitle() == " ")
+ m_objects_ctrl->GetMainWindow()->SetToolTip(_(L("Right button click the icon to change the object settings")));
+ else if (col->GetTitle() == _("Name") &&
+ m_objects_model->GetIcon(item).GetRefData() == m_icon_manifold_warning.GetRefData()) {
+ int obj_idx = m_objects_model->GetIdByItem(item);
+ auto& stats = (*m_objects)[obj_idx]->volumes[0]->mesh.stl.stats;
+ int errors = stats.degenerate_facets + stats.edges_fixed + stats.facets_removed +
+ stats.facets_added + stats.facets_reversed + stats.backwards_edges;
+
+ wxString tooltip = wxString::Format(_(L("Auto-repaired (%d errors):\n")), errors);
+
+ std::map<std::string, int> error_msg;
+ error_msg[L("degenerate facets")] = stats.degenerate_facets;
+ error_msg[L("edges fixed")] = stats.edges_fixed;
+ error_msg[L("facets removed")] = stats.facets_removed;
+ error_msg[L("facets added")] = stats.facets_added;
+ error_msg[L("facets reversed")] = stats.facets_reversed;
+ error_msg[L("backwards edges")] = stats.backwards_edges;
+
+ for (auto error : error_msg)
+ {
+ if (error.second > 0)
+ tooltip += wxString::Format(_("\t%d %s\n"), error.second, error.first);
+ }
+// OR
+// tooltip += wxString::Format(_(L("%d degenerate facets, %d edges fixed, %d facets removed, "
+// "%d facets added, %d facets reversed, %d backwards edges")),
+// stats.degenerate_facets, stats.edges_fixed, stats.facets_removed,
+// stats.facets_added, stats.facets_reversed, stats.backwards_edges);
+
+ if (is_windows10())
+ tooltip += _(L("Right button click the icon to fix STL through Netfabb"));
+
+ m_objects_ctrl->GetMainWindow()->SetToolTip(tooltip);
+ }
+ else
+ m_objects_ctrl->GetMainWindow()->SetToolTip(""); // hide tooltip
+}
+
+wxPoint get_mouse_position_in_control() {
+ const wxPoint& pt = wxGetMousePosition();
+ wxWindow* win = m_objects_ctrl->GetMainWindow();
+ return wxPoint(pt.x - win->GetScreenPosition().x,
+ pt.y - win->GetScreenPosition().y);
+}
+
+bool is_mouse_position_in_control(wxPoint& pt) {
+ pt = get_mouse_position_in_control();
+ const wxSize& cz = m_objects_ctrl->GetSize();
+ if (pt.x > 0 && pt.x < cz.x &&
+ pt.y > 0 && pt.y < cz.y)
+ return true;
+ return false;
+}
+
+wxDataViewColumn* object_ctrl_create_extruder_column(int extruders_count)
+{
+ wxArrayString choices;
+ choices.Add("default");
+ for (int i = 1; i <= extruders_count; ++i)
+ choices.Add(wxString::Format("%d", i));
+ wxDataViewChoiceRenderer *c =
+ new wxDataViewChoiceRenderer(choices, wxDATAVIEW_CELL_EDITABLE, wxALIGN_CENTER_HORIZONTAL);
+ wxDataViewColumn* column = new wxDataViewColumn(_(L("Extruder")), c, 2, 60, wxALIGN_CENTER_HORIZONTAL, wxDATAVIEW_COL_RESIZABLE);
+ return column;
+}
+
+void create_objects_ctrl(wxWindow* win, wxBoxSizer*& objects_sz)
+{
+ m_objects_ctrl = new wxDataViewCtrl(win, wxID_ANY, wxDefaultPosition, wxDefaultSize);
+ m_objects_ctrl->SetMinSize(wxSize(-1, 150)); // TODO - Set correct height according to the opened/closed objects
+
+ objects_sz = new wxBoxSizer(wxVERTICAL);
+ objects_sz->Add(m_objects_ctrl, 1, wxGROW | wxLEFT, 20);
+
+ m_objects_model = new PrusaObjectDataViewModel;
+ m_objects_ctrl->AssociateModel(m_objects_model);
+#if wxUSE_DRAG_AND_DROP && wxUSE_UNICODE
+ m_objects_ctrl->EnableDragSource(wxDF_UNICODETEXT);
+ m_objects_ctrl->EnableDropTarget(wxDF_UNICODETEXT);
+#endif // wxUSE_DRAG_AND_DROP && wxUSE_UNICODE
+
+ // column 0(Icon+Text) of the view control:
+ // And Icon can be consisting of several bitmaps
+ m_objects_ctrl->AppendColumn(new wxDataViewColumn(_(L("Name")), new PrusaBitmapTextRenderer(),
+ 0, 200, wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE));
+
+ // column 1 of the view control:
+ m_objects_ctrl->AppendTextColumn(_(L("Copy")), 1, wxDATAVIEW_CELL_INERT, 45,
+ wxALIGN_CENTER_HORIZONTAL, wxDATAVIEW_COL_RESIZABLE);
+
+ // column 2 of the view control:
+ m_objects_ctrl->AppendColumn(object_ctrl_create_extruder_column(4));
+
+ // column 3 of the view control:
+ m_objects_ctrl->AppendBitmapColumn(" ", 3, wxDATAVIEW_CELL_INERT, 25,
+ wxALIGN_CENTER_HORIZONTAL, wxDATAVIEW_COL_RESIZABLE);
+}
+
+// ****** from GUI.cpp
+wxBoxSizer* create_objects_list(wxWindow *win)
+{
+ wxBoxSizer* objects_sz;
+ // create control
+ create_objects_ctrl(win, objects_sz);
+
+ // describe control behavior
+ m_objects_ctrl->Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, [](wxEvent& event) {
+ object_ctrl_selection_changed();
+#ifndef __WXMSW__
+ set_tooltip_for_item(get_mouse_position_in_control());
+#endif //__WXMSW__
+ });
+
+ m_objects_ctrl->Bind(wxEVT_DATAVIEW_ITEM_CONTEXT_MENU, [](wxDataViewEvent& event) {
+ object_ctrl_context_menu();
+// event.Skip();
+ });
+
+ m_objects_ctrl->Bind(wxEVT_CHAR, [](wxKeyEvent& event) { object_ctrl_key_event(event); }); // doesn't work on OSX
+
+#ifdef __WXMSW__
+ // Extruder value changed
+ m_objects_ctrl->Bind(wxEVT_CHOICE, [](wxCommandEvent& event) { update_extruder_in_config(event.GetString()); });
+
+ m_objects_ctrl->GetMainWindow()->Bind(wxEVT_MOTION, [](wxMouseEvent& event) {
+ set_tooltip_for_item(event.GetPosition());
+ event.Skip();
+ });
+#else
+ // equivalent to wxEVT_CHOICE on __WXMSW__
+ m_objects_ctrl->Bind(wxEVT_DATAVIEW_ITEM_VALUE_CHANGED, [](wxDataViewEvent& event) { object_ctrl_item_value_change(event); });
+#endif //__WXMSW__
+
+ m_objects_ctrl->Bind(wxEVT_DATAVIEW_ITEM_BEGIN_DRAG, [](wxDataViewEvent& e) {on_begin_drag(e);});
+ m_objects_ctrl->Bind(wxEVT_DATAVIEW_ITEM_DROP_POSSIBLE, [](wxDataViewEvent& e) {on_drop_possible(e); });
+ m_objects_ctrl->Bind(wxEVT_DATAVIEW_ITEM_DROP, [](wxDataViewEvent& e) {on_drop(e);});
+ return objects_sz;
+}
+
+wxBoxSizer* create_edit_object_buttons(wxWindow* win)
+{
+ auto sizer = new wxBoxSizer(wxVERTICAL);
+
+ auto btn_load_part = new wxButton(win, wxID_ANY, /*Load */"part" + dots, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER/*wxBU_LEFT*/);
+ auto btn_load_modifier = new wxButton(win, wxID_ANY, /*Load */"modifier" + dots, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER/*wxBU_LEFT*/);
+ auto btn_load_lambda_modifier = new wxButton(win, wxID_ANY, /*Load */"generic" + dots, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER/*wxBU_LEFT*/);
+ auto btn_delete = new wxButton(win, wxID_ANY, "Delete"/*" part"*/, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER/*wxBU_LEFT*/);
+ auto btn_split = new wxButton(win, wxID_ANY, "Split"/*" part"*/, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER/*wxBU_LEFT*/);
+ m_btn_move_up = new wxButton(win, wxID_ANY, "", wxDefaultPosition, wxDefaultSize/*wxSize(30, -1)*/, wxBU_LEFT);
+ m_btn_move_down = new wxButton(win, wxID_ANY, "", wxDefaultPosition, wxDefaultSize/*wxSize(30, -1)*/, wxBU_LEFT);
+
+ //*** button's functions
+ btn_load_part->Bind(wxEVT_BUTTON, [win](wxEvent&) {
+// on_btn_load(win);
+ });
+
+ btn_load_modifier->Bind(wxEVT_BUTTON, [win](wxEvent&) {
+// on_btn_load(win, true);
+ });
+
+ btn_load_lambda_modifier->Bind(wxEVT_BUTTON, [win](wxEvent&) {
+// on_btn_load(win, true, true);
+ });
+
+ btn_delete ->Bind(wxEVT_BUTTON, [](wxEvent&) { on_btn_del(); });
+ btn_split ->Bind(wxEVT_BUTTON, [](wxEvent&) { on_btn_split(true); });
+ m_btn_move_up ->Bind(wxEVT_BUTTON, [](wxEvent&) { on_btn_move_up(); });
+ m_btn_move_down ->Bind(wxEVT_BUTTON, [](wxEvent&) { on_btn_move_down(); });
+ //***
+
+ m_btn_move_up->SetMinSize(wxSize(20, -1));
+ m_btn_move_down->SetMinSize(wxSize(20, -1));
+ btn_load_part->SetBitmap(wxBitmap(from_u8(Slic3r::var("brick_add.png")), wxBITMAP_TYPE_PNG));
+ btn_load_modifier->SetBitmap(wxBitmap(from_u8(Slic3r::var("brick_add.png")), wxBITMAP_TYPE_PNG));
+ btn_load_lambda_modifier->SetBitmap(wxBitmap(from_u8(Slic3r::var("brick_add.png")), wxBITMAP_TYPE_PNG));
+ btn_delete->SetBitmap(wxBitmap(from_u8(Slic3r::var("brick_delete.png")), wxBITMAP_TYPE_PNG));
+ btn_split->SetBitmap(wxBitmap(from_u8(Slic3r::var("shape_ungroup.png")), wxBITMAP_TYPE_PNG));
+ m_btn_move_up->SetBitmap(wxBitmap(from_u8(Slic3r::var("bullet_arrow_up.png")), wxBITMAP_TYPE_PNG));
+ m_btn_move_down->SetBitmap(wxBitmap(from_u8(Slic3r::var("bullet_arrow_down.png")), wxBITMAP_TYPE_PNG));
+
+ m_sizer_object_buttons = new wxGridSizer(1, 3, 0, 0);
+ m_sizer_object_buttons->Add(btn_load_part, 0, wxEXPAND);
+ m_sizer_object_buttons->Add(btn_load_modifier, 0, wxEXPAND);
+ m_sizer_object_buttons->Add(btn_load_lambda_modifier, 0, wxEXPAND);
+ m_sizer_object_buttons->Show(false);
+
+ m_sizer_part_buttons = new wxGridSizer(1, 3, 0, 0);
+ m_sizer_part_buttons->Add(btn_delete, 0, wxEXPAND);
+ m_sizer_part_buttons->Add(btn_split, 0, wxEXPAND);
+ {
+ auto up_down_sizer = new wxGridSizer(1, 2, 0, 0);
+ up_down_sizer->Add(m_btn_move_up, 1, wxEXPAND);
+ up_down_sizer->Add(m_btn_move_down, 1, wxEXPAND);
+ m_sizer_part_buttons->Add(up_down_sizer, 0, wxEXPAND);
+ }
+ m_sizer_part_buttons->Show(false);
+
+ btn_load_part->SetFont(Slic3r::GUI::small_font());
+ btn_load_modifier->SetFont(Slic3r::GUI::small_font());
+ btn_load_lambda_modifier->SetFont(Slic3r::GUI::small_font());
+ btn_delete->SetFont(Slic3r::GUI::small_font());
+ btn_split->SetFont(Slic3r::GUI::small_font());
+ m_btn_move_up->SetFont(Slic3r::GUI::small_font());
+ m_btn_move_down->SetFont(Slic3r::GUI::small_font());
+
+ sizer->Add(m_sizer_object_buttons, 0, wxEXPAND | wxLEFT, 20);
+ sizer->Add(m_sizer_part_buttons, 0, wxEXPAND | wxLEFT, 20);
+ return sizer;
+}
+
+void update_after_moving()
+{
+ auto item = m_objects_ctrl->GetSelection();
+ if (!item || m_selected_object_id<0)
+ return;
+
+ auto volume_id = m_objects_model->GetVolumeIdByItem(item);
+ if (volume_id < 0)
+ return;
+
+ auto d = m_move_options - m_last_coords;
+ auto volume = (*m_objects)[m_selected_object_id]->volumes[volume_id];
+ volume->mesh.translate(d(0), d(1), d(2));
+ m_last_coords = m_move_options;
+
+ m_parts_changed = true;
+ parts_changed(m_selected_object_id);
+}
+
+wxSizer* object_movers(wxWindow *win)
+{
+// DynamicPrintConfig* config = &get_preset_bundle()->/*full_config();//*/printers.get_edited_preset().config; // TODO get config from Model_volume
+ std::shared_ptr<ConfigOptionsGroup> optgroup = std::make_shared<ConfigOptionsGroup>(win, "Move"/*, config*/);
+ optgroup->label_width = 20;
+ optgroup->m_on_change = [](t_config_option_key opt_key, boost::any value){
+ int val = boost::any_cast<int>(value);
+ bool update = false;
+ if (opt_key == "x" && m_move_options(0) != val){
+ update = true;
+ m_move_options(0) = val;
+ }
+ else if (opt_key == "y" && m_move_options(1) != val){
+ update = true;
+ m_move_options(1) = val;
+ }
+ else if (opt_key == "z" && m_move_options(2) != val){
+ update = true;
+ m_move_options(2) = val;
+ }
+ if (update) update_after_moving();
+ };
+
+ ConfigOptionDef def;
+ def.label = L("X");
+ def.type = coInt;
+ def.gui_type = "slider";
+ def.default_value = new ConfigOptionInt(0);
+
+ Option option = Option(def, "x");
+ option.opt.full_width = true;
+ optgroup->append_single_option_line(option);
+ m_mover_x = dynamic_cast<wxSlider*>(optgroup->get_field("x")->getWindow());
+
+ def.label = L("Y");
+ option = Option(def, "y");
+ optgroup->append_single_option_line(option);
+ m_mover_y = dynamic_cast<wxSlider*>(optgroup->get_field("y")->getWindow());
+
+ def.label = L("Z");
+ option = Option(def, "z");
+ optgroup->append_single_option_line(option);
+ m_mover_z = dynamic_cast<wxSlider*>(optgroup->get_field("z")->getWindow());
+
+ get_optgroups().push_back(optgroup); // ogObjectMovers
+
+ m_sizer_object_movers = optgroup->sizer;
+ m_sizer_object_movers->Show(false);
+
+ m_move_options = Vec3d(0, 0, 0);
+ m_last_coords = Vec3d(0, 0, 0);
+
+ return optgroup->sizer;
+}
+
+wxBoxSizer* content_settings(wxWindow *win)
+{
+ DynamicPrintConfig* config = &get_preset_bundle()->/*full_config();//*/printers.get_edited_preset().config; // TODO get config from Model_volume
+ std::shared_ptr<ConfigOptionsGroup> optgroup = std::make_shared<ConfigOptionsGroup>(win, "Extruders", config);
+ optgroup->label_width = label_width();
+
+ Option option = optgroup->get_option("extruder");
+ option.opt.default_value = new ConfigOptionInt(1);
+ optgroup->append_single_option_line(option);
+
+ get_optgroups().push_back(optgroup); // ogObjectSettings
+
+ auto sizer = new wxBoxSizer(wxVERTICAL);
+ sizer->Add(create_edit_object_buttons(win), 0, wxEXPAND, 0); // *** Edit Object Buttons***
+
+ sizer->Add(optgroup->sizer, 1, wxEXPAND | wxLEFT, 20);
+
+ auto add_btn = new wxButton(win, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER);
+ if (wxMSW) add_btn->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+ add_btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("add.png")), wxBITMAP_TYPE_PNG));
+ sizer->Add(add_btn, 0, wxALIGN_LEFT | wxLEFT, 20);
+
+ sizer->Add(object_movers(win), 0, wxEXPAND | wxLEFT, 20);
+
+ return sizer;
+}
+
+void add_objects_list(wxWindow* parent, wxBoxSizer* sizer)
+{
+ const auto ol_sizer = create_objects_list(parent);
+ sizer->Add(ol_sizer, 1, wxEXPAND | wxTOP, 20);
+ set_objects_list_sizer(ol_sizer);
+}
+
+Line add_og_to_object_settings(const std::string& option_name, const std::string& sidetext, int def_value = 0)
+{
+ Line line = { _(option_name), "" };
+ if (option_name == "Scale") {
+ line.near_label_widget = [](wxWindow* parent) {
+ auto btn = new PrusaLockButton(parent, wxID_ANY);
+ btn->Bind(wxEVT_BUTTON, [btn](wxCommandEvent &event){
+ event.Skip();
+ wxTheApp->CallAfter([btn]() { set_uniform_scaling(btn->IsLocked()); });
+ });
+ return btn;
+ };
+ }
+
+ ConfigOptionDef def;
+ def.type = coInt;
+ def.default_value = new ConfigOptionInt(def_value);
+ def.width = 55;
+
+ if (option_name == "Rotation")
+ def.min = -360;
+
+ const std::string lower_name = boost::algorithm::to_lower_copy(option_name);
+
+ std::vector<std::string> axes{ "x", "y", "z" };
+ for (auto axis : axes) {
+ if (axis == "z" && option_name != "Scale")
+ def.sidetext = sidetext;
+ Option option = Option(def, lower_name + "_" + axis);
+ option.opt.full_width = true;
+ line.append_option(option);
+ }
+
+ if (option_name == "Scale")
+ {
+ def.width = 45;
+ def.type = coStrings;
+ def.gui_type = "select_open";
+ def.enum_labels.push_back(L("%"));
+ def.enum_labels.push_back(L("mm"));
+ def.default_value = new ConfigOptionStrings{ "mm" };
+
+ const Option option = Option(def, lower_name + "_unit");
+ line.append_option(option);
+ }
+
+ return line;
+}
+
+void add_object_settings(wxWindow* parent, wxBoxSizer* sizer)
+{
+ auto optgroup = std::make_shared<ConfigOptionsGroup>(parent, _(L("Object Settings")));
+ optgroup->label_width = 100;
+ optgroup->set_grid_vgap(5);
+
+ optgroup->m_on_change = [](t_config_option_key opt_key, boost::any value){
+ if (opt_key == "scale_unit"){
+ const wxString& selection = boost::any_cast<wxString>(value);
+ std::vector<std::string> axes{ "x", "y", "z" };
+ for (auto axis : axes) {
+ std::string key = "scale_" + axis;
+ get_optgroup(ogFrequentlyObjectSettings)->set_side_text(key, selection);
+ }
+
+ g_is_percent_scale = selection == _("%");
+ update_scale_values();
+ }
+ };
+
+ ConfigOptionDef def;
+
+ // Objects(sub-objects) name
+ def.label = L("Name");
+// def.type = coString;
+ def.gui_type = "legend";
+ def.tooltip = L("Object name");
+ def.full_width = true;
+ def.default_value = new ConfigOptionString{ " " };
+ optgroup->append_single_option_line(Option(def, "object_name"));
+
+
+ // Legend for object modification
+ auto line = Line{ "", "" };
+ def.label = "";
+ def.type = coString;
+ def.width = 55;
+
+ std::vector<std::string> axes{ "x", "y", "z" };
+ for (const auto axis : axes) {
+ const auto label = boost::algorithm::to_upper_copy(axis);
+ def.default_value = new ConfigOptionString{ " "+label };
+ Option option = Option(def, axis + "_axis_legend");
+ line.append_option(option);
+ }
+ optgroup->append_line(line);
+
+
+ // Settings table
+ optgroup->append_line(add_og_to_object_settings(L("Position"), L("mm")));
+ optgroup->append_line(add_og_to_object_settings(L("Rotation"), "°"));
+ optgroup->append_line(add_og_to_object_settings(L("Scale"), "mm"));
+
+
+ def.label = L("Place on bed");
+ def.type = coBool;
+ def.tooltip = L("Automatic placing of models on printing bed in Y axis");
+ def.gui_type = "";
+ def.sidetext = "";
+ def.default_value = new ConfigOptionBool{ false };
+ optgroup->append_single_option_line(Option(def, "place_on_bed"));
+
+ m_option_sizer = new wxBoxSizer(wxVERTICAL);
+ optgroup->sizer->Add(m_option_sizer, 1, wxEXPAND | wxLEFT, 5);
+
+ sizer->Add(optgroup->sizer, 0, wxEXPAND | wxLEFT | wxTOP, 20);
+
+ optgroup->disable();
+
+ get_optgroups().push_back(optgroup); // ogFrequentlyObjectSettings
+}
+
+
+// add Collapsible Pane to sizer
+wxCollapsiblePane* add_collapsible_pane(wxWindow* parent, wxBoxSizer* sizer_parent, const wxString& name, std::function<wxSizer *(wxWindow *)> content_function)
+{
+#ifdef __WXMSW__
+ auto *collpane = new PrusaCollapsiblePaneMSW(parent, wxID_ANY, name);
+#else
+ auto *collpane = new PrusaCollapsiblePane/*wxCollapsiblePane*/(parent, wxID_ANY, name);
+#endif // __WXMSW__
+ // add the pane with a zero proportion value to the sizer which contains it
+ sizer_parent->Add(collpane, 0, wxGROW | wxALL, 0);
+
+ wxWindow *win = collpane->GetPane();
+
+ wxSizer *sizer = content_function(win);
+
+ wxSizer *sizer_pane = new wxBoxSizer(wxVERTICAL);
+ sizer_pane->Add(sizer, 1, wxGROW | wxEXPAND | wxBOTTOM, 2);
+ win->SetSizer(sizer_pane);
+ // sizer_pane->SetSizeHints(win);
+ return collpane;
+}
+
+void add_collapsible_panes(wxWindow* parent, wxBoxSizer* sizer)
+{
+ // *** Objects List ***
+ auto collpane = add_collapsible_pane(parent, sizer, "Objects List:", create_objects_list);
+ collpane->Bind(wxEVT_COLLAPSIBLEPANE_CHANGED, ([collpane](wxCommandEvent& e){
+ // wxWindowUpdateLocker noUpdates(g_right_panel);
+ if (collpane->IsCollapsed()) {
+ m_sizer_object_buttons->Show(false);
+ m_sizer_part_buttons->Show(false);
+ m_sizer_object_movers->Show(false);
+ if (!m_objects_ctrl->HasSelection())
+ m_collpane_settings->Show(false);
+ }
+ }));
+
+ // *** Object/Part Settings ***
+ m_collpane_settings = add_collapsible_pane(parent, sizer, "Object Settings", content_settings);
+}
+
+void show_collpane_settings(bool expert_mode)
+{
+ m_collpane_settings->Show(expert_mode && !m_objects_model->IsEmpty());
+}
+
+void add_object_to_list(const std::string &name, ModelObject* model_object)
+{
+ wxString item_name = name;
+ auto item = m_objects_model->Add(item_name, model_object->instances.size());
+ m_objects_ctrl->Select(item);
+
+ // Add error icon if detected auto-repaire
+ auto stats = model_object->volumes[0]->mesh.stl.stats;
+ int errors = stats.degenerate_facets + stats.edges_fixed + stats.facets_removed +
+ stats.facets_added + stats.facets_reversed + stats.backwards_edges;
+ if (errors > 0) {
+ const PrusaDataViewBitmapText data(item_name, m_icon_manifold_warning);
+ wxVariant variant;
+ variant << data;
+ m_objects_model->SetValue(variant, item, 0);
+ }
+
+ if (model_object->volumes.size() > 1) {
+ for (auto id = 0; id < model_object->volumes.size(); id++)
+ m_objects_model->AddChild(item,
+ model_object->volumes[id]->name,
+ m_icon_solidmesh,
+ model_object->volumes[id]->config.option<ConfigOptionInt>("extruder")->value,
+ false);
+ m_objects_ctrl->Expand(item);
+ }
+
+#ifndef __WXOSX__
+ object_ctrl_selection_changed();
+#endif //__WXMSW__
+}
+
+void delete_object_from_list()
+{
+ auto item = m_objects_ctrl->GetSelection();
+ if (!item || m_objects_model->GetParent(item) != wxDataViewItem(0))
+ return;
+// m_objects_ctrl->Select(m_objects_model->Delete(item));
+ m_objects_model->Delete(item);
+
+ part_selection_changed();
+
+// if (m_objects_model->IsEmpty())
+// m_collpane_settings->Show(false);
+}
+
+void delete_all_objects_from_list()
+{
+ m_objects_model->DeleteAll();
+
+ part_selection_changed();
+// m_collpane_settings->Show(false);
+}
+
+void set_object_count(int idx, int count)
+{
+ m_objects_model->SetValue(wxString::Format("%d", count), idx, 1);
+ m_objects_ctrl->Refresh();
+}
+
+void unselect_objects()
+{
+ if (!m_objects_ctrl->GetSelection())
+ return;
+
+ g_prevent_list_events = true;
+ m_objects_ctrl->UnselectAll();
+ part_selection_changed();
+ g_prevent_list_events = false;
+}
+
+void select_current_object(int idx)
+{
+ g_prevent_list_events = true;
+ m_objects_ctrl->UnselectAll();
+ if (idx>=0)
+ m_objects_ctrl->Select(m_objects_model->GetItemById(idx));
+ part_selection_changed();
+ g_prevent_list_events = false;
+}
+
+void select_current_volume(int idx, int vol_idx)
+{
+ if (vol_idx < 0) {
+ select_current_object(idx);
+ return;
+ }
+ g_prevent_list_events = true;
+ m_objects_ctrl->UnselectAll();
+ if (idx >= 0)
+ m_objects_ctrl->Select(m_objects_model->GetItemByVolumeId(idx, vol_idx));
+ part_selection_changed();
+ g_prevent_list_events = false;
+}
+
+void remove()
+{
+ auto item = m_objects_ctrl->GetSelection();
+ if (!item)
+ return;
+
+ if (m_objects_model->GetParent(item) == wxDataViewItem(0)) {
+ if (m_event_remove_object > 0) {
+ wxCommandEvent event(m_event_remove_object);
+ get_main_frame()->ProcessWindowEvent(event);
+ }
+// delete_object_from_list();
+ }
+ else
+ on_btn_del();
+}
+
+void object_ctrl_selection_changed()
+{
+ if (g_prevent_list_events) return;
+
+ part_selection_changed();
+
+ if (m_event_object_selection_changed > 0) {
+ wxCommandEvent event(m_event_object_selection_changed);
+ event.SetId(m_selected_object_id); // set $obj_idx
+ const wxDataViewItem item = m_objects_ctrl->GetSelection();
+ if (!item || m_objects_model->GetParent(item) == wxDataViewItem(0))
+ event.SetInt(-1); // set $vol_idx
+ else {
+ const int vol_idx = m_objects_model->GetVolumeIdByItem(item);
+ if (vol_idx == -2) // is settings item
+ event.SetInt(m_objects_model->GetVolumeIdByItem(m_objects_model->GetParent(item))); // set $vol_idx
+ else
+ event.SetInt(vol_idx);
+ }
+ get_main_frame()->ProcessWindowEvent(event);
+ }
+
+#ifdef __WXOSX__
+ update_extruder_in_config(g_selected_extruder);
+#endif //__WXOSX__
+}
+
+void object_ctrl_context_menu()
+{
+ wxDataViewItem item;
+ wxDataViewColumn* col;
+// printf("object_ctrl_context_menu\n");
+ const wxPoint pt = get_mouse_position_in_control();
+// printf("mouse_position_in_control: x = %d, y = %d\n", pt.x, pt.y);
+ m_objects_ctrl->HitTest(pt, item, col);
+ if (!item)
+#ifdef __WXOSX__ // #ys_FIXME temporary workaround for OSX
+ // after Yosemite OS X version, HitTest return undefined item
+ item = m_objects_ctrl->GetSelection();
+ if (item)
+ show_context_menu();
+ else
+ printf("undefined item\n");
+ return;
+#else
+ return;
+#endif // __WXOSX__
+// printf("item exists\n");
+ const wxString title = col->GetTitle();
+// printf("title = *%s*\n", title.data().AsChar());
+
+ if (title == " ")
+ show_context_menu();
+// #ys_FIXME
+// else if (title == _("Name") && pt.x >15 &&
+// m_objects_model->GetIcon(item).GetRefData() == m_icon_manifold_warning.GetRefData())
+// {
+// if (is_windows10())
+// fix_through_netfabb();
+// }
+#ifndef __WXMSW__
+ m_objects_ctrl->GetMainWindow()->SetToolTip(""); // hide tooltip
+#endif //__WXMSW__
+}
+
+void object_ctrl_key_event(wxKeyEvent& event)
+{
+ if (event.GetKeyCode() == WXK_TAB)
+ m_objects_ctrl->Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward);
+ else if (event.GetKeyCode() == WXK_DELETE
+#ifdef __WXOSX__
+ || event.GetKeyCode() == WXK_BACK
+#endif //__WXOSX__
+ ){
+ printf("WXK_BACK\n");
+ remove();
+ }
+ else
+ event.Skip();
+}
+
+void object_ctrl_item_value_change(wxDataViewEvent& event)
+{
+ if (event.GetColumn() == 2)
+ {
+ wxVariant variant;
+ m_objects_model->GetValue(variant, event.GetItem(), 2);
+#ifdef __WXOSX__
+ g_selected_extruder = variant.GetString();
+#else // --> for Linux
+ update_extruder_in_config(variant.GetString());
+#endif //__WXOSX__
+ }
+}
+
+void show_manipulation_og(const bool show)
+{
+ wxGridSizer* grid_sizer = get_optgroup(ogFrequentlyObjectSettings)->get_grid_sizer();
+ if (show == grid_sizer->IsShown(2))
+ return;
+ for (size_t id = 2; id < 12; id++)
+ grid_sizer->Show(id, show);
+}
+
+//update_optgroup
+void update_settings_list()
+{
+#ifdef __WXGTK__
+ auto parent = get_optgroup(ogFrequentlyObjectSettings)->get_parent();
+#else
+ auto parent = get_optgroup(ogFrequentlyObjectSettings)->parent();
+#endif /* __WXGTK__ */
+
+// There is a bug related to Ubuntu overlay scrollbars, see https://github.com/prusa3d/Slic3r/issues/898 and https://github.com/prusa3d/Slic3r/issues/952.
+// The issue apparently manifests when Show()ing a window with overlay scrollbars while the UI is frozen. For this reason,
+// we will Thaw the UI prematurely on Linux. This means destroing the no_updates object prematurely.
+#ifdef __linux__
+ std::unique_ptr<wxWindowUpdateLocker> no_updates(new wxWindowUpdateLocker(parent));
+#else
+ wxWindowUpdateLocker noUpdates(parent);
+#endif
+
+ m_option_sizer->Clear(true);
+
+ bool show_manipulations = true;
+ const auto item = m_objects_ctrl->GetSelection();
+ if (m_config && m_objects_model->IsSettingsItem(item))
+ {
+ auto extra_column = [](wxWindow* parent, const Line& line)
+ {
+ auto opt_key = (line.get_options())[0].opt_id; //we assume that we have one option per line
+
+ auto btn = new wxBitmapButton(parent, wxID_ANY, wxBitmap(from_u8(var("colorchange_delete_on.png")), wxBITMAP_TYPE_PNG),
+ wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
+#ifdef __WXMSW__
+ btn->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+#endif // __WXMSW__
+ btn->Bind(wxEVT_BUTTON, [opt_key](wxEvent &event){
+ (*m_config)->erase(opt_key);
+ wxTheApp->CallAfter([]() { update_settings_list(); });
+ });
+ return btn;
+ };
+
+ std::map<std::string, std::vector<std::string>> cat_options;
+ auto opt_keys = (*m_config)->keys();
+ m_og_settings.resize(0);
+ std::vector<std::string> categories;
+ if (!(opt_keys.size() == 1 && opt_keys[0] == "extruder"))// return;
+ {
+ auto extruders_cnt = get_preset_bundle()->printers.get_selected_preset().printer_technology() == ptSLA ? 1 :
+ get_preset_bundle()->printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->values.size();
+
+ for (auto& opt_key : opt_keys) {
+ auto category = (*m_config)->def()->get(opt_key)->category;
+ if (category.empty() ||
+ (category == "Extruders" && extruders_cnt == 1)) continue;
+
+ std::vector< std::string > new_category;
+
+ auto& cat_opt = cat_options.find(category) == cat_options.end() ? new_category : cat_options.at(category);
+ cat_opt.push_back(opt_key);
+ if (cat_opt.size() == 1)
+ cat_options[category] = cat_opt;
+ }
+
+ for (auto& cat : cat_options) {
+ if (cat.second.size() == 1 && cat.second[0] == "extruder")
+ continue;
+
+ auto optgroup = std::make_shared<ConfigOptionsGroup>(parent, cat.first, *m_config, false, ogDEFAULT, extra_column);
+ optgroup->label_width = 150;
+ optgroup->sidetext_width = 70;
+
+ for (auto& opt : cat.second)
+ {
+ if (opt == "extruder")
+ continue;
+ Option option = optgroup->get_option(opt);
+ option.opt.width = 70;
+ optgroup->append_single_option_line(option);
+ }
+ optgroup->reload_config();
+ m_option_sizer->Add(optgroup->sizer, 0, wxEXPAND | wxALL, 0);
+ m_og_settings.push_back(optgroup);
+
+ categories.push_back(cat.first);
+ }
+ }
+
+ if (m_og_settings.empty()) {
+ m_objects_ctrl->Select(m_objects_model->Delete(item));
+ part_selection_changed();
+ }
+ else {
+ if (!categories.empty())
+ m_objects_model->UpdateSettingsDigest(item, categories);
+ show_manipulations = false;
+ }
+ }
+
+ show_manipulation_og(show_manipulations);
+ show_info_sizer(show_manipulations && item && m_objects_model->GetParent(item) == wxDataViewItem(0));
+
+#ifdef __linux__
+ no_updates.reset(nullptr);
+#endif
+
+ parent->Layout();
+ get_right_panel()->GetParent()->Layout();
+}
+
+void get_settings_choice(wxMenu *menu, int id, bool is_part)
+{
+ const auto category_name = menu->GetLabel(id);
+
+ wxArrayString names;
+ wxArrayInt selections;
+
+ settings_menu_hierarchy settings_menu;
+ get_options_menu(settings_menu, is_part);
+ std::vector< std::pair<std::string, std::string> > *settings_list = nullptr;
+
+ auto opt_keys = (*m_config)->keys();
+
+ for (auto& cat : settings_menu)
+ {
+ if (_(cat.first) == category_name) {
+ int sel = 0;
+ for (auto& pair : cat.second) {
+ names.Add(_(pair.second));
+ if (find(opt_keys.begin(), opt_keys.end(), pair.first) != opt_keys.end())
+ selections.Add(sel);
+ sel++;
+ }
+ settings_list = &cat.second;
+ break;
+ }
+ }
+
+ if (!settings_list)
+ return;
+
+ if (wxGetMultipleChoices(selections, _(L("Select showing settings")), category_name, names) ==0 )
+ return;
+
+ std::vector <std::string> selected_options;
+ for (auto sel : selections)
+ selected_options.push_back((*settings_list)[sel].first);
+
+ for (auto& setting:(*settings_list) )
+ {
+ auto& opt_key = setting.first;
+ if (find(opt_keys.begin(), opt_keys.end(), opt_key) != opt_keys.end() &&
+ find(selected_options.begin(), selected_options.end(), opt_key) == selected_options.end())
+ (*m_config)->erase(opt_key);
+
+ if(find(opt_keys.begin(), opt_keys.end(), opt_key) == opt_keys.end() &&
+ find(selected_options.begin(), selected_options.end(), opt_key) != selected_options.end())
+ (*m_config)->set_key_value(opt_key, m_default_config.get()->option(opt_key)->clone());
+ }
+
+
+ // Add settings item for object
+ const auto item = m_objects_ctrl->GetSelection();
+ if (item) {
+ const auto settings_item = m_objects_model->HasSettings(item);
+ m_objects_ctrl->Select(settings_item ? settings_item :
+ m_objects_model->AddSettingsChild(item));
+#ifndef __WXOSX__
+ part_selection_changed();
+#endif //no __WXOSX__
+ }
+ else
+ update_settings_list();
+}
+
+void menu_item_add_generic(wxMenuItem* &menu, int id) {
+ auto sub_menu = new wxMenu;
+
+ std::vector<std::string> menu_items = { L("Box"), L("Cylinder"), L("Sphere"), L("Slab") };
+ for (auto& item : menu_items)
+ sub_menu->Append(new wxMenuItem(sub_menu, ++id, _(item)));
+
+#ifndef __WXMSW__
+ sub_menu->Bind(wxEVT_MENU, [sub_menu](wxEvent &event) {
+ load_lambda(sub_menu->GetLabel(event.GetId()).ToStdString());
+ });
+#endif //no __WXMSW__
+
+ menu->SetSubMenu(sub_menu);
+}
+
+wxMenuItem* menu_item_split(wxMenu* menu, int id) {
+ auto menu_item = new wxMenuItem(menu, id, _(L("Split to parts")));
+ menu_item->SetBitmap(m_bmp_split);
+ return menu_item;
+}
+
+wxMenuItem* menu_item_settings(wxMenu* menu, int id, const bool is_part) {
+ auto menu_item = new wxMenuItem(menu, id, _(L("Add settings")));
+ menu_item->SetBitmap(m_bmp_cog);
+
+ auto sub_menu = create_add_settings_popupmenu(is_part);
+ menu_item->SetSubMenu(sub_menu);
+ return menu_item;
+}
+
+wxMenu *create_add_part_popupmenu()
+{
+ wxMenu *menu = new wxMenu;
+ std::vector<std::string> menu_items = { L("Add part"), L("Add modifier"), L("Add generic") };
+
+ wxWindowID config_id_base = wxWindow::NewControlId(menu_items.size()+4+2);
+
+ int i = 0;
+ for (auto& item : menu_items) {
+ auto menu_item = new wxMenuItem(menu, config_id_base + i, _(item));
+ menu_item->SetBitmap(i == 0 ? m_icon_solidmesh : m_icon_modifiermesh);
+ if (item == "Add generic")
+ menu_item_add_generic(menu_item, config_id_base + i);
+ menu->Append(menu_item);
+ i++;
+ }
+
+ menu->AppendSeparator();
+ auto menu_item = menu_item_split(menu, config_id_base + i + 4);
+ menu->Append(menu_item);
+ menu_item->Enable(is_splittable_object(false));
+
+ menu->AppendSeparator();
+ // Append settings popupmenu
+ menu->Append(menu_item_settings(menu, config_id_base + i + 5, false));
+
+ menu->Bind(wxEVT_MENU, [config_id_base, menu](wxEvent &event){
+ switch (event.GetId() - config_id_base) {
+ case 0:
+ on_btn_load();
+ break;
+ case 1:
+ on_btn_load(true);
+ break;
+ case 2:
+// on_btn_load(true, true);
+ break;
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+#ifdef __WXMSW__
+ load_lambda(menu->GetLabel(event.GetId()).ToStdString());
+#endif // __WXMSW__
+ break;
+ case 7: //3:
+ on_btn_split(false);
+ break;
+ default:
+#ifdef __WXMSW__
+ get_settings_choice(menu, event.GetId(), false);
+#endif // __WXMSW__
+ break;
+ }
+ });
+
+ return menu;
+}
+
+wxMenu *create_part_settings_popupmenu()
+{
+ wxMenu *menu = new wxMenu;
+ wxWindowID config_id_base = wxWindow::NewControlId(2);
+
+ auto menu_item = menu_item_split(menu, config_id_base);
+ menu->Append(menu_item);
+ menu_item->Enable(is_splittable_object(true));
+
+ menu->AppendSeparator();
+ // Append settings popupmenu
+ menu->Append(menu_item_settings(menu, config_id_base + 1, true));
+
+ menu->Bind(wxEVT_MENU, [config_id_base, menu](wxEvent &event){
+ switch (event.GetId() - config_id_base) {
+ case 0:
+ on_btn_split(true);
+ break;
+ default:{
+ get_settings_choice(menu, event.GetId(), true);
+ break; }
+ }
+ });
+
+ return menu;
+}
+
+wxMenu *create_add_settings_popupmenu(bool is_part)
+{
+ wxMenu *menu = new wxMenu;
+
+ auto categories = get_category_icon();
+
+ settings_menu_hierarchy settings_menu;
+ get_options_menu(settings_menu, is_part);
+
+ for (auto cat : settings_menu)
+ {
+ auto menu_item = new wxMenuItem(menu, wxID_ANY, _(cat.first));
+ menu_item->SetBitmap(categories.find(cat.first) == categories.end() ?
+ wxNullBitmap : categories.at(cat.first));
+ menu->Append(menu_item);
+ }
+#ifndef __WXMSW__
+ menu->Bind(wxEVT_MENU, [menu,is_part](wxEvent &event) {
+ get_settings_choice(menu, event.GetId(), is_part);
+ });
+#endif //no __WXMSW__
+ return menu;
+}
+
+void show_context_menu()
+{
+ const auto item = m_objects_ctrl->GetSelection();
+ if (item)
+ {
+ if (m_objects_model->IsSettingsItem(item))
+ return;
+ const auto menu = m_objects_model->GetParent(item) == wxDataViewItem(0) ?
+ create_add_part_popupmenu() :
+ create_part_settings_popupmenu();
+ get_tab_panel()->GetPage(0)->PopupMenu(menu);
+ }
+}
+
+// ******
+
+void load_part( ModelObject* model_object,
+ wxArrayString& part_names, const bool is_modifier)
+{
+ wxWindow* parent = get_tab_panel()->GetPage(0);
+
+ wxArrayString input_files;
+ open_model(parent, input_files);
+ for (int i = 0; i < input_files.size(); ++i) {
+ std::string input_file = input_files.Item(i).ToStdString();
+
+ Model model;
+ try {
+ model = Model::read_from_file(input_file);
+ }
+ catch (std::exception &e) {
+ auto msg = _(L("Error! ")) + input_file + " : " + e.what() + ".";
+ show_error(parent, msg);
+ exit(1);
+ }
+
+ for ( auto object : model.objects) {
+ for (auto volume : object->volumes) {
+ auto new_volume = model_object->add_volume(*volume);
+ new_volume->set_type(is_modifier ? ModelVolume::PARAMETER_MODIFIER : ModelVolume::MODEL_PART);
+ boost::filesystem::path(input_file).filename().string();
+ new_volume->name = boost::filesystem::path(input_file).filename().string();
+
+ part_names.Add(new_volume->name);
+
+ // apply the same translation we applied to the object
+ new_volume->mesh.translate( model_object->origin_translation(0),
+ model_object->origin_translation(1),
+ model_object->origin_translation(2) );
+ // set a default extruder value, since user can't add it manually
+ new_volume->config.set_key_value("extruder", new ConfigOptionInt(0));
+
+ m_parts_changed = true;
+ }
+ }
+ }
+}
+
+void load_lambda( ModelObject* model_object,
+ wxArrayString& part_names, const bool is_modifier)
+{
+ auto dlg = new LambdaObjectDialog(m_objects_ctrl->GetMainWindow());
+ if (dlg->ShowModal() == wxID_CANCEL) {
+ return;
+ }
+
+ std::string name = "lambda-";
+ TriangleMesh mesh;
+
+ auto params = dlg->ObjectParameters();
+ switch (params.type)
+ {
+ case LambdaTypeBox:{
+ mesh = make_cube(params.dim[0], params.dim[1], params.dim[2]);
+ name += "Box";
+ break;}
+ case LambdaTypeCylinder:{
+ mesh = make_cylinder(params.cyl_r, params.cyl_h);
+ name += "Cylinder";
+ break;}
+ case LambdaTypeSphere:{
+ mesh = make_sphere(params.sph_rho);
+ name += "Sphere";
+ break;}
+ case LambdaTypeSlab:{
+ const auto& size = model_object->bounding_box().size();
+ mesh = make_cube(size(0)*1.5, size(1)*1.5, params.slab_h);
+ // box sets the base coordinate at 0, 0, move to center of plate and move it up to initial_z
+ mesh.translate(-size(0)*1.5 / 2.0, -size(1)*1.5 / 2.0, params.slab_z);
+ name += "Slab";
+ break; }
+ default:
+ break;
+ }
+ mesh.repair();
+
+ auto new_volume = model_object->add_volume(mesh);
+ new_volume->set_type(is_modifier ? ModelVolume::PARAMETER_MODIFIER : ModelVolume::MODEL_PART);
+
+ new_volume->name = name;
+ // set a default extruder value, since user can't add it manually
+ new_volume->config.set_key_value("extruder", new ConfigOptionInt(0));
+
+ part_names.Add(name);
+
+ m_parts_changed = true;
+}
+
+void load_lambda(const std::string& type_name)
+{
+ if (m_selected_object_id < 0) return;
+
+ auto dlg = new LambdaObjectDialog(m_objects_ctrl->GetMainWindow(), type_name);
+ if (dlg->ShowModal() == wxID_CANCEL)
+ return;
+
+ const std::string name = "lambda-"+type_name;
+ TriangleMesh mesh;
+
+ const auto params = dlg->ObjectParameters();
+ if (type_name == _("Box"))
+ mesh = make_cube(params.dim[0], params.dim[1], params.dim[2]);
+ else if (type_name == _("Cylinder"))
+ mesh = make_cylinder(params.cyl_r, params.cyl_h);
+ else if (type_name == _("Sphere"))
+ mesh = make_sphere(params.sph_rho);
+ else if (type_name == _("Slab")){
+ const auto& size = (*m_objects)[m_selected_object_id]->bounding_box().size();
+ mesh = make_cube(size(0)*1.5, size(1)*1.5, params.slab_h);
+ // box sets the base coordinate at 0, 0, move to center of plate and move it up to initial_z
+ mesh.translate(-size(0)*1.5 / 2.0, -size(1)*1.5 / 2.0, params.slab_z);
+ }
+ mesh.repair();
+
+ auto new_volume = (*m_objects)[m_selected_object_id]->add_volume(mesh);
+ new_volume->set_type(ModelVolume::PARAMETER_MODIFIER);
+
+ new_volume->name = name;
+ // set a default extruder value, since user can't add it manually
+ new_volume->config.set_key_value("extruder", new ConfigOptionInt(0));
+
+ m_parts_changed = true;
+ parts_changed(m_selected_object_id);
+
+ m_objects_ctrl->Select(m_objects_model->AddChild(m_objects_ctrl->GetSelection(),
+ name, m_icon_modifiermesh));
+#ifndef __WXOSX__ //#ifdef __WXMSW__ // #ys_FIXME
+ object_ctrl_selection_changed();
+#endif //no __WXOSX__ //__WXMSW__
+}
+
+void on_btn_load(bool is_modifier /*= false*/, bool is_lambda/* = false*/)
+{
+ auto item = m_objects_ctrl->GetSelection();
+ if (!item)
+ return;
+ int obj_idx = -1;
+ if (m_objects_model->GetParent(item) == wxDataViewItem(0))
+ obj_idx = m_objects_model->GetIdByItem(item);
+ else
+ return;
+
+ if (obj_idx < 0) return;
+ wxArrayString part_names;
+ if (is_lambda)
+ load_lambda((*m_objects)[obj_idx], part_names, is_modifier);
+ else
+ load_part((*m_objects)[obj_idx], part_names, is_modifier);
+
+ parts_changed(obj_idx);
+
+ for (int i = 0; i < part_names.size(); ++i)
+ m_objects_ctrl->Select( m_objects_model->AddChild(item, part_names.Item(i),
+ is_modifier ? m_icon_modifiermesh : m_icon_solidmesh));
+#ifndef __WXOSX__ //#ifdef __WXMSW__ // #ys_FIXME
+ object_ctrl_selection_changed();
+#endif //no __WXOSX__//__WXMSW__
+}
+
+void remove_settings_from_config()
+{
+ auto opt_keys = (*m_config)->keys();
+ if (opt_keys.size() == 1 && opt_keys[0] == "extruder")
+ return;
+ int extruder = -1;
+ if ((*m_config)->has("extruder"))
+ extruder = (*m_config)->option<ConfigOptionInt>("extruder")->value;
+
+ (*m_config)->clear();
+
+ if (extruder >=0 )
+ (*m_config)->set_key_value("extruder", new ConfigOptionInt(extruder));
+}
+
+bool remove_subobject_from_object(const int volume_id)
+{
+ const auto volume = (*m_objects)[m_selected_object_id]->volumes[volume_id];
+
+ // if user is deleting the last solid part, throw error
+ int solid_cnt = 0;
+ for (auto vol : (*m_objects)[m_selected_object_id]->volumes)
+ if (vol->is_model_part())
+ ++solid_cnt;
+ if (volume->is_model_part() && solid_cnt == 1) {
+ Slic3r::GUI::show_error(nullptr, _(L("You can't delete the last solid part from this object.")));
+ return false;
+ }
+
+ (*m_objects)[m_selected_object_id]->delete_volume(volume_id);
+ m_parts_changed = true;
+
+ parts_changed(m_selected_object_id);
+ return true;
+}
+
+void on_btn_del()
+{
+ auto item = m_objects_ctrl->GetSelection();
+ if (!item) return;
+
+ const auto volume_id = m_objects_model->GetVolumeIdByItem(item);
+ if (volume_id ==-1)
+ return;
+
+ if (volume_id ==-2)
+ remove_settings_from_config();
+ else if (!remove_subobject_from_object(volume_id))
+ return;
+
+ m_objects_ctrl->Select(m_objects_model->Delete(item));
+ part_selection_changed();
+}
+
+bool get_volume_by_item(const bool split_part, const wxDataViewItem& item, ModelVolume*& volume)
+{
+ if (!item || m_selected_object_id < 0)
+ return false;
+ const auto volume_id = m_objects_model->GetVolumeIdByItem(item);
+ if (volume_id < 0) {
+ if (split_part) return false;
+ volume = (*m_objects)[m_selected_object_id]->volumes[0];
+ }
+ else
+ volume = (*m_objects)[m_selected_object_id]->volumes[volume_id];
+ if (volume)
+ return true;
+ return false;
+}
+
+bool is_splittable_object(const bool split_part)
+{
+ const wxDataViewItem item = m_objects_ctrl->GetSelection();
+ if (!item) return false;
+
+ wxDataViewItemArray children;
+ if (!split_part && m_objects_model->GetChildren(item, children) > 0)
+ return false;
+
+ ModelVolume* volume;
+ if (!get_volume_by_item(split_part, item, volume) || !volume)
+ return false;
+
+ TriangleMeshPtrs meshptrs = volume->mesh.split();
+ if (meshptrs.size() <= 1) {
+ delete meshptrs.front();
+ return false;
+ }
+
+ return true;
+}
+
+void on_btn_split(const bool split_part)
+{
+ const auto item = m_objects_ctrl->GetSelection();
+ if (!item || m_selected_object_id<0)
+ return;
+ ModelVolume* volume;
+ if (!get_volume_by_item(split_part, item, volume)) return;
+ DynamicPrintConfig& config = get_preset_bundle()->printers.get_edited_preset().config;
+ const auto nozzle_dmrs_cnt = config.option<ConfigOptionFloats>("nozzle_diameter")->values.size();
+ if (volume->split(nozzle_dmrs_cnt) == 1) {
+ wxMessageBox(_(L("The selected object couldn't be split because it contains only one part.")));
+ return;
+ }
+
+ auto model_object = (*m_objects)[m_selected_object_id];
+
+ if (split_part) {
+ auto parent = m_objects_model->GetParent(item);
+ m_objects_model->DeleteChildren(parent);
+
+ for (auto id = 0; id < model_object->volumes.size(); id++)
+ m_objects_model->AddChild(parent, model_object->volumes[id]->name,
+ model_object->volumes[id]->is_modifier() ? m_icon_modifiermesh : m_icon_solidmesh,
+ model_object->volumes[id]->config.has("extruder") ?
+ model_object->volumes[id]->config.option<ConfigOptionInt>("extruder")->value : 0,
+ false);
+
+ m_objects_ctrl->Expand(parent);
+ }
+ else {
+ for (auto id = 0; id < model_object->volumes.size(); id++)
+ m_objects_model->AddChild(item, model_object->volumes[id]->name,
+ m_icon_solidmesh,
+ model_object->volumes[id]->config.has("extruder") ?
+ model_object->volumes[id]->config.option<ConfigOptionInt>("extruder")->value : 0,
+ false);
+ m_objects_ctrl->Expand(item);
+ }
+
+ m_parts_changed = true;
+ parts_changed(m_selected_object_id);
+}
+
+void on_btn_move_up(){
+ auto item = m_objects_ctrl->GetSelection();
+ if (!item)
+ return;
+ auto volume_id = m_objects_model->GetVolumeIdByItem(item);
+ if (volume_id < 0)
+ return;
+ auto& volumes = (*m_objects)[m_selected_object_id]->volumes;
+ if (0 < volume_id && volume_id < volumes.size()) {
+ std::swap(volumes[volume_id - 1], volumes[volume_id]);
+ m_parts_changed = true;
+ m_objects_ctrl->Select(m_objects_model->MoveChildUp(item));
+ part_selection_changed();
+// #ifdef __WXMSW__
+// object_ctrl_selection_changed();
+// #endif //__WXMSW__
+ }
+}
+
+void on_btn_move_down(){
+ auto item = m_objects_ctrl->GetSelection();
+ if (!item)
+ return;
+ auto volume_id = m_objects_model->GetVolumeIdByItem(item);
+ if (volume_id < 0)
+ return;
+ auto& volumes = (*m_objects)[m_selected_object_id]->volumes;
+ if (0 <= volume_id && volume_id+1 < volumes.size()) {
+ std::swap(volumes[volume_id + 1], volumes[volume_id]);
+ m_parts_changed = true;
+ m_objects_ctrl->Select(m_objects_model->MoveChildDown(item));
+ part_selection_changed();
+// #ifdef __WXMSW__
+// object_ctrl_selection_changed();
+// #endif //__WXMSW__
+ }
+}
+
+void parts_changed(int obj_idx)
+{
+ if (m_event_object_settings_changed <= 0) return;
+
+ wxCommandEvent e(m_event_object_settings_changed);
+ auto event_str = wxString::Format("%d %d %d", obj_idx,
+ is_parts_changed() ? 1 : 0,
+ is_part_settings_changed() ? 1 : 0);
+ e.SetString(event_str);
+ get_main_frame()->ProcessWindowEvent(e);
+}
+
+void update_settings_value()
+{
+ auto og = get_optgroup(ogFrequentlyObjectSettings);
+ if (m_selected_object_id < 0 || m_objects->size() <= m_selected_object_id) {
+ og->set_value("position_x", 0);
+ og->set_value("position_y", 0);
+ og->set_value("position_z", 0);
+ og->set_value("scale_x", 0);
+ og->set_value("scale_y", 0);
+ og->set_value("scale_z", 0);
+ og->set_value("rotation_x", 0);
+ og->set_value("rotation_y", 0);
+ og->set_value("rotation_z", 0);
+ og->disable();
+ return;
+ }
+ g_is_percent_scale = boost::any_cast<wxString>(og->get_value("scale_unit")) == _("%");
+ update_position_values();
+ update_scale_values();
+ update_rotation_values();
+ og->enable();
+}
+
+void part_selection_changed()
+{
+ auto item = m_objects_ctrl->GetSelection();
+ int obj_idx = -1;
+ auto og = get_optgroup(ogFrequentlyObjectSettings);
+ m_config = nullptr;
+ wxString object_name = wxEmptyString;
+ if (item)
+ {
+ const bool is_settings_item = m_objects_model->IsSettingsItem(item);
+ bool is_part = false;
+ wxString og_name = wxEmptyString;
+ if (m_objects_model->GetParent(item) == wxDataViewItem(0)) {
+ obj_idx = m_objects_model->GetIdByItem(item);
+ og_name = _(L("Object manipulation"));
+ m_config = std::make_shared<DynamicPrintConfig*>(&(*m_objects)[obj_idx]->config);
+ }
+ else {
+ auto parent = m_objects_model->GetParent(item);
+ // Take ID of the parent object to "inform" perl-side which object have to be selected on the scene
+ obj_idx = m_objects_model->GetIdByItem(parent);
+ if (is_settings_item) {
+ if (m_objects_model->GetParent(parent) == wxDataViewItem(0)) {
+ og_name = _(L("Object Settings to modify"));
+ m_config = std::make_shared<DynamicPrintConfig*>(&(*m_objects)[obj_idx]->config);
+ }
+ else {
+ og_name = _(L("Part Settings to modify"));
+ is_part = true;
+ auto main_parent = m_objects_model->GetParent(parent);
+ obj_idx = m_objects_model->GetIdByItem(main_parent);
+ const auto volume_id = m_objects_model->GetVolumeIdByItem(parent);
+ m_config = std::make_shared<DynamicPrintConfig*>(&(*m_objects)[obj_idx]->volumes[volume_id]->config);
+ }
+ }
+ else {
+ og_name = _(L("Part manipulation"));
+ is_part = true;
+ const auto volume_id = m_objects_model->GetVolumeIdByItem(item);
+ m_config = std::make_shared<DynamicPrintConfig*>(&(*m_objects)[obj_idx]->volumes[volume_id]->config);
+ }
+ }
+
+ og->set_name(" " + og_name + " ");
+ object_name = m_objects_model->GetName(item);
+ m_default_config = std::make_shared<DynamicPrintConfig>(*DynamicPrintConfig::new_from_defaults_keys(get_options(is_part)));
+ }
+ og->set_value("object_name", object_name);
+
+ update_settings_list();
+
+ m_selected_object_id = obj_idx;
+
+ update_settings_value();
+
+/* wxWindowUpdateLocker noUpdates(get_right_panel());
+
+ m_move_options = Point3(0, 0, 0);
+ m_last_coords = Point3(0, 0, 0);
+ // reset move sliders
+ std::vector<std::string> opt_keys = {"x", "y", "z"};
+ auto og = get_optgroup(ogObjectMovers);
+ for (auto opt_key: opt_keys)
+ og->set_value(opt_key, int(0));
+
+// if (!item || m_selected_object_id < 0){
+ if (m_selected_object_id < 0){
+ m_sizer_object_buttons->Show(false);
+ m_sizer_part_buttons->Show(false);
+ m_sizer_object_movers->Show(false);
+ m_collpane_settings->Show(false);
+ return;
+ }
+
+ m_collpane_settings->Show(true);
+
+ auto volume_id = m_objects_model->GetVolumeIdByItem(item);
+ if (volume_id < 0){
+ m_sizer_object_buttons->Show(true);
+ m_sizer_part_buttons->Show(false);
+ m_sizer_object_movers->Show(false);
+ m_collpane_settings->SetLabelText(_(L("Object Settings")) + ":");
+
+// elsif($itemData->{type} eq 'object') {
+// # select nothing in 3D preview
+//
+// # attach object config to settings panel
+// $self->{optgroup_movers}->disable;
+// $self->{staticbox}->SetLabel('Object Settings');
+// @opt_keys = (map @{$_->get_keys}, Slic3r::Config::PrintObject->new, Slic3r::Config::PrintRegion->new);
+// $config = $self->{model_object}->config;
+// }
+
+ return;
+ }
+
+ m_collpane_settings->SetLabelText(_(L("Part Settings")) + ":");
+
+ m_sizer_object_buttons->Show(false);
+ m_sizer_part_buttons->Show(true);
+ m_sizer_object_movers->Show(true);
+
+ auto bb_size = m_objects[m_selected_object_id]->bounding_box().size();
+ int scale = 10; //??
+
+ m_mover_x->SetMin(-bb_size.x * 4 * scale);
+ m_mover_x->SetMax(bb_size.x * 4 * scale);
+
+ m_mover_y->SetMin(-bb_size.y * 4 * scale);
+ m_mover_y->SetMax(bb_size.y * 4 * scale);
+
+ m_mover_z->SetMin(-bb_size.z * 4 * scale);
+ m_mover_z->SetMax(bb_size.z * 4 * scale);
+
+
+
+// my ($config, @opt_keys);
+ m_btn_move_up->Enable(volume_id > 0);
+ m_btn_move_down->Enable(volume_id + 1 < m_objects[m_selected_object_id]->volumes.size());
+
+ // attach volume config to settings panel
+ auto volume = m_objects[m_selected_object_id]->volumes[volume_id];
+
+ if (volume->modifier)
+ og->enable();
+ else
+ og->disable();
+
+// auto config = volume->config;
+
+ // get default values
+// @opt_keys = @{Slic3r::Config::PrintRegion->new->get_keys};
+// }
+/*
+ # get default values
+ my $default_config = Slic3r::Config::new_from_defaults_keys(\@opt_keys);
+
+ # append default extruder
+ push @opt_keys, 'extruder';
+ $default_config->set('extruder', 0);
+ $config->set_ifndef('extruder', 0);
+ $self->{settings_panel}->set_default_config($default_config);
+ $self->{settings_panel}->set_config($config);
+ $self->{settings_panel}->set_opt_keys(\@opt_keys);
+ $self->{settings_panel}->set_fixed_options([qw(extruder)]);
+ $self->{settings_panel}->enable;
+ }
+ */
+}
+
+void set_extruder_column_hidden(bool hide)
+{
+ m_objects_ctrl->GetColumn(2)->SetHidden(hide);
+}
+
+void update_extruder_in_config(const wxString& selection)
+{
+ if (!m_config || selection.empty())
+ return;
+
+ int extruder = selection.size() > 1 ? 0 : atoi(selection.c_str());
+ (*m_config)->set_key_value("extruder", new ConfigOptionInt(extruder));
+
+ if (m_event_update_scene > 0) {
+ wxCommandEvent e(m_event_update_scene);
+ get_main_frame()->ProcessWindowEvent(e);
+ }
+}
+
+void update_scale_values()
+{
+ auto og = get_optgroup(ogFrequentlyObjectSettings);
+ auto instance = (*m_objects)[m_selected_object_id]->instances.front();
+ auto size = (*m_objects)[m_selected_object_id]->instance_bounding_box(0).size();
+
+ if (g_is_percent_scale) {
+ auto scale = instance->scaling_factor * 100.0;
+ og->set_value("scale_x", int(scale));
+ og->set_value("scale_y", int(scale));
+ og->set_value("scale_z", int(scale));
+ }
+ else {
+ og->set_value("scale_x", int(instance->scaling_factor * size(0) + 0.5));
+ og->set_value("scale_y", int(instance->scaling_factor * size(1) + 0.5));
+ og->set_value("scale_z", int(instance->scaling_factor * size(2) + 0.5));
+ }
+}
+
+void update_position_values()
+{
+ auto og = get_optgroup(ogFrequentlyObjectSettings);
+ auto instance = (*m_objects)[m_selected_object_id]->instances.front();
+
+#if ENABLE_MODELINSTANCE_3D_OFFSET
+ og->set_value("position_x", int(instance->get_offset(X)));
+ og->set_value("position_y", int(instance->get_offset(Y)));
+ og->set_value("position_z", int(instance->get_offset(Z)));
+#else
+ og->set_value("position_x", int(instance->offset(0)));
+ og->set_value("position_y", int(instance->offset(1)));
+ og->set_value("position_z", 0);
+#endif // ENABLE_MODELINSTANCE_3D_OFFSET
+}
+
+void update_position_values(const Vec3d& position)
+{
+ auto og = get_optgroup(ogFrequentlyObjectSettings);
+
+ og->set_value("position_x", int(position(0)));
+ og->set_value("position_y", int(position(1)));
+ og->set_value("position_z", int(position(2)));
+}
+
+void update_scale_values(double scaling_factor)
+{
+ auto og = get_optgroup(ogFrequentlyObjectSettings);
+
+ // this is temporary
+ // to be able to update the values as size
+ // we need to store somewhere the original size
+ // or have it passed as parameter
+ if (!g_is_percent_scale)
+ og->set_value("scale_unit", _("%"));
+
+ auto scale = scaling_factor * 100.0;
+ og->set_value("scale_x", int(scale));
+ og->set_value("scale_y", int(scale));
+ og->set_value("scale_z", int(scale));
+}
+
+void update_rotation_values()
+{
+ auto og = get_optgroup(ogFrequentlyObjectSettings);
+ auto instance = (*m_objects)[m_selected_object_id]->instances.front();
+ og->set_value("rotation_x", 0);
+ og->set_value("rotation_y", 0);
+ og->set_value("rotation_z", int(Geometry::rad2deg(instance->rotation)));
+}
+
+void update_rotation_value(double angle, Axis axis)
+{
+ auto og = get_optgroup(ogFrequentlyObjectSettings);
+
+ std::string axis_str;
+ switch (axis)
+ {
+ case X:
+ {
+ axis_str = "rotation_x";
+ break;
+ }
+ case Y:
+ {
+ axis_str = "rotation_y";
+ break;
+ }
+ case Z:
+ {
+ axis_str = "rotation_z";
+ break;
+ }
+ }
+
+ og->set_value(axis_str, int(Geometry::rad2deg(angle)));
+}
+
+void set_uniform_scaling(const bool uniform_scale)
+{
+ g_is_uniform_scale = uniform_scale;
+}
+
+void on_begin_drag(wxDataViewEvent &event)
+{
+ wxDataViewItem item(event.GetItem());
+
+ // only allow drags for item, not containers
+ if (m_objects_model->GetParent(item) == wxDataViewItem(0) || m_objects_model->IsSettingsItem(item)) {
+ event.Veto();
+ return;
+ }
+
+ /* Under MSW or OSX, DnD moves an item to the place of another selected item
+ * But under GTK, DnD moves an item between another two items.
+ * And as a result - call EVT_CHANGE_SELECTION to unselect all items.
+ * To prevent such behavior use g_prevent_list_events
+ **/
+ g_prevent_list_events = true;//it's needed for GTK
+
+ wxTextDataObject *obj = new wxTextDataObject;
+ obj->SetText(wxString::Format("%d", m_objects_model->GetVolumeIdByItem(item)));
+ event.SetDataObject(obj);
+ event.SetDragFlags(/*wxDrag_AllowMove*/wxDrag_DefaultMove); // allows both copy and move;
+}
+
+void on_drop_possible(wxDataViewEvent &event)
+{
+ wxDataViewItem item(event.GetItem());
+
+ // only allow drags for item or background, not containers
+ if (item.IsOk() && m_objects_model->GetParent(item) == wxDataViewItem(0) ||
+ event.GetDataFormat() != wxDF_UNICODETEXT || m_objects_model->IsSettingsItem(item))
+ event.Veto();
+}
+
+void on_drop(wxDataViewEvent &event)
+{
+ wxDataViewItem item(event.GetItem());
+
+ // only allow drops for item, not containers
+ if (item.IsOk() && m_objects_model->GetParent(item) == wxDataViewItem(0) ||
+ event.GetDataFormat() != wxDF_UNICODETEXT || m_objects_model->IsSettingsItem(item)) {
+ event.Veto();
+ return;
+ }
+
+ wxTextDataObject obj;
+ obj.SetData(wxDF_UNICODETEXT, event.GetDataSize(), event.GetDataBuffer());
+
+ int from_volume_id = std::stoi(obj.GetText().ToStdString());
+ int to_volume_id = m_objects_model->GetVolumeIdByItem(item);
+
+#ifdef __WXGTK__
+ /* Under GTK, DnD moves an item between another two items.
+ * And event.GetItem() return item, which is under "insertion line"
+ * So, if we move item down we should to decrease the to_volume_id value
+ **/
+ if (to_volume_id > from_volume_id) to_volume_id--;
+#endif // __WXGTK__
+
+ m_objects_ctrl->Select(m_objects_model->ReorganizeChildren(from_volume_id, to_volume_id,
+ m_objects_model->GetParent(item)));
+
+ auto& volumes = (*m_objects)[m_selected_object_id]->volumes;
+ auto delta = to_volume_id < from_volume_id ? -1 : 1;
+ int cnt = 0;
+ for (int id = from_volume_id; cnt < abs(from_volume_id - to_volume_id); id+=delta, cnt++)
+ std::swap(volumes[id], volumes[id +delta]);
+
+ m_parts_changed = true;
+ parts_changed(m_selected_object_id);
+
+ g_prevent_list_events = false;
+}
+
+void update_objects_list_extruder_column(int extruders_count)
+{
+ if (get_preset_bundle()->printers.get_selected_preset().printer_technology() == ptSLA)
+ extruders_count = 1;
+
+ // delete old 3rd column
+ m_objects_ctrl->DeleteColumn(m_objects_ctrl->GetColumn(2));
+ // insert new created 3rd column
+ m_objects_ctrl->InsertColumn(2, object_ctrl_create_extruder_column(extruders_count));
+ // set show/hide for this column
+ set_extruder_column_hidden(extruders_count <= 1);
+}
+
+void create_double_slider(wxWindow* parent, wxBoxSizer* sizer, wxGLCanvas* canvas)
+{
+ m_slider = new PrusaDoubleSlider(parent, wxID_ANY, 0, 0, 0, 100);
+ sizer->Add(m_slider, 0, wxEXPAND, 0);
+
+ m_preview_canvas = canvas;
+ m_preview_canvas->Bind(wxEVT_KEY_DOWN, update_double_slider_from_canvas);
+
+ m_slider->Bind(wxEVT_SCROLL_CHANGED, [parent](wxEvent& event) {
+ _3DScene::set_toolpaths_range(m_preview_canvas, m_slider->GetLowerValueD() - 1e-6, m_slider->GetHigherValueD() + 1e-6);
+ if (parent->IsShown())
+ m_preview_canvas->Refresh();
+ });
+}
+
+void fill_slider_values(std::vector<std::pair<int, double>> &values,
+ const std::vector<double> &layers_z)
+{
+ std::vector<double> layers_all_z = _3DScene::get_current_print_zs(m_preview_canvas, false);
+ if (layers_all_z.size() == layers_z.size())
+ for (int i = 0; i < layers_z.size(); i++)
+ values.push_back(std::pair<int, double>(i+1, layers_z[i]));
+ else if (layers_all_z.size() > layers_z.size()) {
+ int cur_id = 0;
+ for (int i = 0; i < layers_z.size(); i++)
+ for (int j = cur_id; j < layers_all_z.size(); j++)
+ if (layers_z[i] - 1e-6 < layers_all_z[j] && layers_all_z[j] < layers_z[i] + 1e-6) {
+ values.push_back(std::pair<int, double>(j+1, layers_z[i]));
+ cur_id = j;
+ break;
+ }
+ }
+}
+
+void set_double_slider_thumbs( const bool force_sliders_full_range,
+ const std::vector<double> &layers_z,
+ const double z_low, const double z_high)
+{
+ // Force slider full range only when slider is created.
+ // Support selected diapason on the all next steps
+ if (/*force_sliders_full_range*/z_high == 0.0) {
+ m_slider->SetLowerValue(0);
+ m_slider->SetHigherValue(layers_z.size() - 1);
+ return;
+ }
+
+ for (int i = layers_z.size() - 1; i >= 0; i--)
+ if (z_low >= layers_z[i]) {
+ m_slider->SetLowerValue(i);
+ break;
+ }
+ for (int i = layers_z.size() - 1; i >= 0 ; i--)
+ if (z_high >= layers_z[i]) {
+ m_slider->SetHigherValue(i);
+ break;
+ }
+}
+
+void update_double_slider(bool force_sliders_full_range)
+{
+ std::vector<std::pair<int, double>> values;
+ std::vector<double> layers_z = _3DScene::get_current_print_zs(m_preview_canvas, true);
+ fill_slider_values(values, layers_z);
+
+ const double z_low = m_slider->GetLowerValueD();
+ const double z_high = m_slider->GetHigherValueD();
+ m_slider->SetMaxValue(layers_z.size() - 1);
+ m_slider->SetSliderValues(values);
+
+ set_double_slider_thumbs(force_sliders_full_range, layers_z, z_low, z_high);
+}
+
+void reset_double_slider()
+{
+ m_slider->SetHigherValue(0);
+ m_slider->SetLowerValue(0);
+}
+
+void update_double_slider_from_canvas(wxKeyEvent& event)
+{
+ if (event.HasModifiers()) {
+ event.Skip();
+ return;
+ }
+
+ const auto key = event.GetKeyCode();
+
+ if (key == 'U' || key == 'D') {
+ const int new_pos = key == 'U' ? m_slider->GetHigherValue() + 1 : m_slider->GetHigherValue() - 1;
+ m_slider->SetHigherValue(new_pos);
+ if (event.ShiftDown()) m_slider->SetLowerValue(m_slider->GetHigherValue());
+ }
+ else if (key == 'S')
+ m_slider->ChangeOneLayerLock();
+ else
+ event.Skip();
+}
+
+void show_manipulation_sizer(const bool is_simple_mode)
+{
+ auto item = m_objects_ctrl->GetSelection();
+ if (!item || !is_simple_mode)
+ return;
+
+ if (m_objects_model->IsSettingsItem(item)) {
+ m_objects_ctrl->Select(m_objects_model->GetParent(item));
+ part_selection_changed();
+ }
+}
+
+} //namespace GUI
+} //namespace Slic3r \ No newline at end of file
diff --git a/src/slic3r/GUI/GUI_ObjectParts.hpp b/src/slic3r/GUI/GUI_ObjectParts.hpp
new file mode 100644
index 000000000..e66b4d1db
--- /dev/null
+++ b/src/slic3r/GUI/GUI_ObjectParts.hpp
@@ -0,0 +1,147 @@
+#ifndef slic3r_GUI_ObjectParts_hpp_
+#define slic3r_GUI_ObjectParts_hpp_
+
+class wxWindow;
+class wxSizer;
+class wxBoxSizer;
+class wxString;
+class wxArrayString;
+class wxMenu;
+class wxDataViewEvent;
+class wxKeyEvent;
+class wxGLCanvas;
+class wxBitmap;
+
+namespace Slic3r {
+class ModelObject;
+class Model;
+
+namespace GUI {
+//class wxGLCanvas;
+
+enum ogGroup{
+ ogFrequentlyChangingParameters,
+ ogFrequentlyObjectSettings,
+ ogCurrentSettings
+// ogObjectSettings,
+// ogObjectMovers,
+// ogPartSettings
+};
+
+enum LambdaTypeIDs{
+ LambdaTypeBox,
+ LambdaTypeCylinder,
+ LambdaTypeSphere,
+ LambdaTypeSlab
+};
+
+struct OBJECT_PARAMETERS
+{
+ LambdaTypeIDs type = LambdaTypeBox;
+ double dim[3];// = { 1.0, 1.0, 1.0 };
+ int cyl_r = 1;
+ int cyl_h = 1;
+ double sph_rho = 1.0;
+ double slab_h = 1.0;
+ double slab_z = 0.0;
+};
+
+typedef std::map<std::string, wxBitmap> t_category_icon;
+inline t_category_icon& get_category_icon();
+
+void add_collapsible_panes(wxWindow* parent, wxBoxSizer* sizer);
+void add_objects_list(wxWindow* parent, wxBoxSizer* sizer);
+void add_object_settings(wxWindow* parent, wxBoxSizer* sizer);
+void show_collpane_settings(bool expert_mode);
+
+wxMenu *create_add_settings_popupmenu(bool is_part);
+wxMenu *create_add_part_popupmenu();
+wxMenu *create_part_settings_popupmenu();
+
+// Add object to the list
+//void add_object(const std::string &name);
+void add_object_to_list(const std::string &name, ModelObject* model_object);
+// Delete object from the list
+void delete_object_from_list();
+// Delete all objects from the list
+void delete_all_objects_from_list();
+// Set count of object on c++ side
+void set_object_count(int idx, int count);
+// Unselect all objects in the list on c++ side
+void unselect_objects();
+// Select current object in the list on c++ side
+void select_current_object(int idx);
+// Select current volume in the list on c++ side
+void select_current_volume(int idx, int vol_idx);
+// Remove objects/sub-object from the list
+void remove();
+
+void object_ctrl_selection_changed();
+void object_ctrl_context_menu();
+void object_ctrl_key_event(wxKeyEvent& event);
+void object_ctrl_item_value_change(wxDataViewEvent& event);
+void show_context_menu();
+bool is_splittable_object(const bool split_part);
+
+void init_mesh_icons();
+void set_event_object_selection_changed(const int& event);
+void set_event_object_settings_changed(const int& event);
+void set_event_remove_object(const int& event);
+void set_event_update_scene(const int& event);
+void set_objects_from_model(Model &model);
+
+bool is_parts_changed();
+bool is_part_settings_changed();
+
+void load_part( ModelObject* model_object,
+ wxArrayString& part_names, const bool is_modifier);
+
+void load_lambda( ModelObject* model_object,
+ wxArrayString& part_names, const bool is_modifier);
+void load_lambda( const std::string& type_name);
+
+void on_btn_load(bool is_modifier = false, bool is_lambda = false);
+void on_btn_del();
+void on_btn_split(const bool split_part);
+void on_btn_move_up();
+void on_btn_move_down();
+
+void parts_changed(int obj_idx);
+void part_selection_changed();
+
+void update_settings_value();
+// show/hide "Extruder" column for Objects List
+void set_extruder_column_hidden(bool hide);
+// update extruder in current config
+void update_extruder_in_config(const wxString& selection);
+// update position values displacements or "gizmos"
+void update_position_values();
+void update_position_values(const Vec3d& position);
+// update scale values after scale unit changing or "gizmos"
+void update_scale_values();
+void update_scale_values(double scaling_factor);
+// update rotation values object selection changing
+void update_rotation_values();
+// update rotation value after "gizmos"
+void update_rotation_value(double angle, Axis axis);
+void set_uniform_scaling(const bool uniform_scale);
+
+void on_begin_drag(wxDataViewEvent &event);
+void on_drop_possible(wxDataViewEvent &event);
+void on_drop(wxDataViewEvent &event);
+
+// update extruder column for objects_ctrl according to extruders count
+void update_objects_list_extruder_column(int extruders_count);
+
+// Create/Update/Reset double slider on 3dPreview
+void create_double_slider(wxWindow* parent, wxBoxSizer* sizer, wxGLCanvas* canvas);
+void update_double_slider(bool force_sliders_full_range);
+void reset_double_slider();
+// update DoubleSlider after keyDown in canvas
+void update_double_slider_from_canvas(wxKeyEvent& event);
+
+void show_manipulation_sizer(const bool is_simple_mode);
+
+} //namespace GUI
+} //namespace Slic3r
+#endif //slic3r_GUI_ObjectParts_hpp_ \ No newline at end of file
diff --git a/src/slic3r/GUI/LambdaObjectDialog.cpp b/src/slic3r/GUI/LambdaObjectDialog.cpp
new file mode 100644
index 000000000..7d741be7f
--- /dev/null
+++ b/src/slic3r/GUI/LambdaObjectDialog.cpp
@@ -0,0 +1,199 @@
+#include "LambdaObjectDialog.hpp"
+
+#include <wx/window.h>
+#include <wx/button.h>
+#include "OptionsGroup.hpp"
+
+namespace Slic3r
+{
+namespace GUI
+{
+static wxString dots("…", wxConvUTF8);
+
+LambdaObjectDialog::LambdaObjectDialog(wxWindow* parent,
+ const wxString type_name):
+ m_type_name(type_name)
+{
+ Create(parent, wxID_ANY, _(L("Lambda Object")),
+ parent->GetScreenPosition(), wxDefaultSize,
+ wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
+
+ // instead of double dim[3] = { 1.0, 1.0, 1.0 };
+ object_parameters.dim[0] = 1.0;
+ object_parameters.dim[1] = 1.0;
+ object_parameters.dim[2] = 1.0;
+
+ sizer = new wxBoxSizer(wxVERTICAL);
+
+ // modificator options
+ if (m_type_name == wxEmptyString) {
+ m_modificator_options_book = new wxChoicebook( this, wxID_ANY, wxDefaultPosition,
+ wxDefaultSize, wxCHB_TOP);
+ sizer->Add(m_modificator_options_book, 1, wxEXPAND | wxALL, 10);
+ }
+ else {
+ m_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize);
+ sizer->Add(m_panel, 1, wxEXPAND | wxALL, 10);
+ }
+
+ ConfigOptionDef def;
+ def.width = 70;
+ auto optgroup = init_modificator_options_page(_(L("Box")));
+ if (optgroup){
+ optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value){
+ int opt_id = opt_key == "l" ? 0 :
+ opt_key == "w" ? 1 :
+ opt_key == "h" ? 2 : -1;
+ if (opt_id < 0) return;
+ object_parameters.dim[opt_id] = boost::any_cast<double>(value);
+ };
+
+ def.type = coFloat;
+ def.default_value = new ConfigOptionFloat{ 1.0 };
+ def.label = L("L");
+ Option option(def, "l");
+ optgroup->append_single_option_line(option);
+
+ def.label = L("W");
+ option = Option(def, "w");
+ optgroup->append_single_option_line(option);
+
+ def.label = L("H");
+ option = Option(def, "h");
+ optgroup->append_single_option_line(option);
+ }
+
+ optgroup = init_modificator_options_page(_(L("Cylinder")));
+ if (optgroup){
+ optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value){
+ int val = boost::any_cast<int>(value);
+ if (opt_key == "cyl_r")
+ object_parameters.cyl_r = val;
+ else if (opt_key == "cyl_h")
+ object_parameters.cyl_h = val;
+ else return;
+ };
+
+ def.type = coInt;
+ def.default_value = new ConfigOptionInt{ 1 };
+ def.label = L("Radius");
+ auto option = Option(def, "cyl_r");
+ optgroup->append_single_option_line(option);
+
+ def.label = L("Height");
+ option = Option(def, "cyl_h");
+ optgroup->append_single_option_line(option);
+ }
+
+ optgroup = init_modificator_options_page(_(L("Sphere")));
+ if (optgroup){
+ optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value){
+ if (opt_key == "sph_rho")
+ object_parameters.sph_rho = boost::any_cast<double>(value);
+ else return;
+ };
+
+ def.type = coFloat;
+ def.default_value = new ConfigOptionFloat{ 1.0 };
+ def.label = L("Rho");
+ auto option = Option(def, "sph_rho");
+ optgroup->append_single_option_line(option);
+ }
+
+ optgroup = init_modificator_options_page(_(L("Slab")));
+ if (optgroup){
+ optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value){
+ double val = boost::any_cast<double>(value);
+ if (opt_key == "slab_z")
+ object_parameters.slab_z = val;
+ else if (opt_key == "slab_h")
+ object_parameters.slab_h = val;
+ else return;
+ };
+
+ def.type = coFloat;
+ def.default_value = new ConfigOptionFloat{ 1.0 };
+ def.label = L("H");
+ auto option = Option(def, "slab_h");
+ optgroup->append_single_option_line(option);
+
+ def.label = L("Initial Z");
+ option = Option(def, "slab_z");
+ optgroup->append_single_option_line(option);
+ }
+
+ Bind(wxEVT_CHOICEBOOK_PAGE_CHANGED, ([this](wxCommandEvent e)
+ {
+ auto page_idx = m_modificator_options_book->GetSelection();
+ if (page_idx < 0) return;
+ switch (page_idx)
+ {
+ case 0:
+ object_parameters.type = LambdaTypeBox;
+ break;
+ case 1:
+ object_parameters.type = LambdaTypeCylinder;
+ break;
+ case 2:
+ object_parameters.type = LambdaTypeSphere;
+ break;
+ case 3:
+ object_parameters.type = LambdaTypeSlab;
+ break;
+ default:
+ break;
+ }
+ }));
+
+ const auto button_sizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL);
+
+ wxButton* btn_OK = static_cast<wxButton*>(FindWindowById(wxID_OK, this));
+ btn_OK->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
+ // validate user input
+ if (!CanClose())return;
+ EndModal(wxID_OK);
+ Destroy();
+ });
+
+ wxButton* btn_CANCEL = static_cast<wxButton*>(FindWindowById(wxID_CANCEL, this));
+ btn_CANCEL->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
+ // validate user input
+ if (!CanClose())return;
+ EndModal(wxID_CANCEL);
+ Destroy();
+ });
+
+ sizer->Add(button_sizer, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10);
+
+ SetSizer(sizer);
+ sizer->Fit(this);
+ sizer->SetSizeHints(this);
+}
+
+// Called from the constructor.
+// Create a panel for a rectangular / circular / custom bed shape.
+ConfigOptionsGroupShp LambdaObjectDialog::init_modificator_options_page(const wxString& title){
+ if (!m_type_name.IsEmpty() && m_type_name != title)
+ return nullptr;
+
+ auto panel = m_type_name.IsEmpty() ? new wxPanel(m_modificator_options_book) : m_panel;
+
+ ConfigOptionsGroupShp optgroup;
+ optgroup = std::make_shared<ConfigOptionsGroup>(panel, _(L("Add")) + " " +title + " " +dots);
+ optgroup->label_width = 100;
+
+ m_optgroups.push_back(optgroup);
+
+ if (m_type_name.IsEmpty()) {
+ panel->SetSizerAndFit(optgroup->sizer);
+ m_modificator_options_book->AddPage(panel, title);
+ }
+ else
+ panel->SetSizer(optgroup->sizer);
+
+ return optgroup;
+}
+
+
+} //namespace GUI
+} //namespace Slic3r
diff --git a/src/slic3r/GUI/LambdaObjectDialog.hpp b/src/slic3r/GUI/LambdaObjectDialog.hpp
new file mode 100644
index 000000000..8f3e8cd80
--- /dev/null
+++ b/src/slic3r/GUI/LambdaObjectDialog.hpp
@@ -0,0 +1,40 @@
+#ifndef slic3r_LambdaObjectDialog_hpp_
+#define slic3r_LambdaObjectDialog_hpp_
+
+#include "GUI.hpp"
+
+#include <wx/dialog.h>
+#include <wx/sizer.h>
+#include <wx/choicebk.h>
+
+class wxPanel;
+
+namespace Slic3r
+{
+namespace GUI
+{
+using ConfigOptionsGroupShp = std::shared_ptr<ConfigOptionsGroup>;
+class LambdaObjectDialog : public wxDialog
+{
+ wxChoicebook* m_modificator_options_book = nullptr;
+ std::vector <ConfigOptionsGroupShp> m_optgroups;
+ wxString m_type_name;
+ wxPanel* m_panel = nullptr;
+public:
+ LambdaObjectDialog(wxWindow* parent,
+ const wxString type_name = wxEmptyString);
+ ~LambdaObjectDialog(){}
+
+ bool CanClose() { return true; } // ???
+ OBJECT_PARAMETERS& ObjectParameters(){ return object_parameters; }
+
+ ConfigOptionsGroupShp init_modificator_options_page(const wxString& title);
+
+ // Note whether the window was already closed, so a pending update is not executed.
+ bool m_already_closed = false;
+ OBJECT_PARAMETERS object_parameters;
+ wxBoxSizer* sizer = nullptr;
+};
+} //namespace GUI
+} //namespace Slic3r
+#endif //slic3r_LambdaObjectDialog_hpp_
diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp
new file mode 100644
index 000000000..58679ed9e
--- /dev/null
+++ b/src/slic3r/GUI/MsgDialog.cpp
@@ -0,0 +1,88 @@
+#include "MsgDialog.hpp"
+
+#include <wx/settings.h>
+#include <wx/sizer.h>
+#include <wx/stattext.h>
+#include <wx/button.h>
+#include <wx/statbmp.h>
+#include <wx/scrolwin.h>
+
+#include "libslic3r/libslic3r.h"
+#include "libslic3r/Utils.hpp"
+#include "GUI.hpp"
+#include "ConfigWizard.hpp"
+
+namespace Slic3r {
+namespace GUI {
+
+
+MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &headline, wxWindowID button_id) :
+ MsgDialog(parent, title, headline, wxBitmap(from_u8(Slic3r::var("Slic3r_192px.png")), wxBITMAP_TYPE_PNG), button_id)
+{}
+
+MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &headline, wxBitmap bitmap, wxWindowID button_id) :
+ wxDialog(parent, wxID_ANY, title),
+ boldfont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)),
+ content_sizer(new wxBoxSizer(wxVERTICAL)),
+ btn_sizer(new wxBoxSizer(wxHORIZONTAL))
+{
+ boldfont.SetWeight(wxFONTWEIGHT_BOLD);
+
+ auto *topsizer = new wxBoxSizer(wxHORIZONTAL);
+ auto *rightsizer = new wxBoxSizer(wxVERTICAL);
+
+ auto *headtext = new wxStaticText(this, wxID_ANY, headline);
+ headtext->SetFont(boldfont);
+ headtext->Wrap(CONTENT_WIDTH);
+ rightsizer->Add(headtext);
+ rightsizer->AddSpacer(VERT_SPACING);
+
+ rightsizer->Add(content_sizer, 1, wxEXPAND);
+
+ if (button_id != wxID_NONE) {
+ auto *button = new wxButton(this, button_id);
+ button->SetFocus();
+ btn_sizer->Add(button);
+ }
+
+ rightsizer->Add(btn_sizer, 0, wxALIGN_CENTRE_HORIZONTAL);
+
+ auto *logo = new wxStaticBitmap(this, wxID_ANY, std::move(bitmap));
+
+ topsizer->Add(logo, 0, wxALL, BORDER);
+ topsizer->Add(rightsizer, 1, wxALL | wxEXPAND, BORDER);
+
+ SetSizerAndFit(topsizer);
+}
+
+MsgDialog::~MsgDialog() {}
+
+
+// ErrorDialog
+
+ErrorDialog::ErrorDialog(wxWindow *parent, const wxString &msg) :
+ MsgDialog(parent, _(L("Slic3r error")), _(L("Slic3r has encountered an error")), wxBitmap(from_u8(Slic3r::var("Slic3r_192px_grayscale.png")), wxBITMAP_TYPE_PNG))
+{
+ auto *panel = new wxScrolledWindow(this);
+ auto *p_sizer = new wxBoxSizer(wxVERTICAL);
+ panel->SetSizer(p_sizer);
+
+ auto *text = new wxStaticText(panel, wxID_ANY, msg);
+ text->Wrap(CONTENT_WIDTH);
+ p_sizer->Add(text, 1, wxEXPAND);
+
+ panel->SetMinSize(wxSize(CONTENT_WIDTH, 0));
+ panel->SetScrollRate(0, 5);
+
+ content_sizer->Add(panel, 1, wxEXPAND);
+
+ SetMaxSize(wxSize(-1, CONTENT_MAX_HEIGHT));
+ Fit();
+}
+
+ErrorDialog::~ErrorDialog() {}
+
+
+
+}
+}
diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp
new file mode 100644
index 000000000..ca349eb5c
--- /dev/null
+++ b/src/slic3r/GUI/MsgDialog.hpp
@@ -0,0 +1,67 @@
+#ifndef slic3r_MsgDialog_hpp_
+#define slic3r_MsgDialog_hpp_
+
+#include <string>
+#include <unordered_map>
+
+#include <wx/dialog.h>
+#include <wx/font.h>
+#include <wx/bitmap.h>
+
+#include "slic3r/Utils/Semver.hpp"
+
+class wxBoxSizer;
+class wxCheckBox;
+
+namespace Slic3r {
+
+namespace GUI {
+
+
+// A message / query dialog with a bitmap on the left and any content on the right
+// with buttons underneath.
+struct MsgDialog : wxDialog
+{
+ MsgDialog(MsgDialog &&) = delete;
+ MsgDialog(const MsgDialog &) = delete;
+ MsgDialog &operator=(MsgDialog &&) = delete;
+ MsgDialog &operator=(const MsgDialog &) = delete;
+ virtual ~MsgDialog();
+
+ // TODO: refactor with CreateStdDialogButtonSizer usage
+
+protected:
+ enum {
+ CONTENT_WIDTH = 500,
+ CONTENT_MAX_HEIGHT = 600,
+ BORDER = 30,
+ VERT_SPACING = 15,
+ HORIZ_SPACING = 5,
+ };
+
+ // button_id is an id of a button that can be added by default, use wxID_NONE to disable
+ MsgDialog(wxWindow *parent, const wxString &title, const wxString &headline, wxWindowID button_id = wxID_OK);
+ MsgDialog(wxWindow *parent, const wxString &title, const wxString &headline, wxBitmap bitmap, wxWindowID button_id = wxID_OK);
+
+ wxFont boldfont;
+ wxBoxSizer *content_sizer;
+ wxBoxSizer *btn_sizer;
+};
+
+
+// Generic error dialog, used for displaying exceptions
+struct ErrorDialog : MsgDialog
+{
+ ErrorDialog(wxWindow *parent, const wxString &msg);
+ ErrorDialog(ErrorDialog &&) = delete;
+ ErrorDialog(const ErrorDialog &) = delete;
+ ErrorDialog &operator=(ErrorDialog &&) = delete;
+ ErrorDialog &operator=(const ErrorDialog &) = delete;
+ virtual ~ErrorDialog();
+};
+
+
+}
+}
+
+#endif
diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp
new file mode 100644
index 000000000..ea22b2cb5
--- /dev/null
+++ b/src/slic3r/GUI/OptionsGroup.cpp
@@ -0,0 +1,545 @@
+#include "OptionsGroup.hpp"
+#include "ConfigExceptions.hpp"
+
+#include <utility>
+#include <wx/numformatter.h>
+#include <boost/algorithm/string/split.hpp>
+#include <boost/algorithm/string/classification.hpp>
+#include "Utils.hpp"
+
+namespace Slic3r { namespace GUI {
+
+const t_field& OptionsGroup::build_field(const Option& opt, wxStaticText* label/* = nullptr*/) {
+ return build_field(opt.opt_id, opt.opt, label);
+}
+const t_field& OptionsGroup::build_field(const t_config_option_key& id, wxStaticText* label/* = nullptr*/) {
+ const ConfigOptionDef& opt = m_options.at(id).opt;
+ return build_field(id, opt, label);
+}
+
+const t_field& OptionsGroup::build_field(const t_config_option_key& id, const ConfigOptionDef& opt, wxStaticText* label/* = nullptr*/) {
+ // Check the gui_type field first, fall through
+ // is the normal type.
+ if (opt.gui_type.compare("select") == 0) {
+ } else if (opt.gui_type.compare("select_open") == 0) {
+ m_fields.emplace(id, STDMOVE(Choice::Create<Choice>(parent(), opt, id)));
+ } else if (opt.gui_type.compare("color") == 0) {
+ m_fields.emplace(id, STDMOVE(ColourPicker::Create<ColourPicker>(parent(), opt, id)));
+ } else if (opt.gui_type.compare("f_enum_open") == 0 ||
+ opt.gui_type.compare("i_enum_open") == 0 ||
+ opt.gui_type.compare("i_enum_closed") == 0) {
+ m_fields.emplace(id, STDMOVE(Choice::Create<Choice>(parent(), opt, id)));
+ } else if (opt.gui_type.compare("slider") == 0) {
+ m_fields.emplace(id, STDMOVE(SliderCtrl::Create<SliderCtrl>(parent(), opt, id)));
+ } else if (opt.gui_type.compare("i_spin") == 0) { // Spinctrl
+ } else if (opt.gui_type.compare("legend") == 0) { // StaticText
+ m_fields.emplace(id, STDMOVE(StaticText::Create<StaticText>(parent(), opt, id)));
+ } else {
+ switch (opt.type) {
+ case coFloatOrPercent:
+ case coFloat:
+ case coFloats:
+ case coPercent:
+ case coPercents:
+ case coString:
+ case coStrings:
+ m_fields.emplace(id, STDMOVE(TextCtrl::Create<TextCtrl>(parent(), opt, id)));
+ break;
+ case coBool:
+ case coBools:
+ m_fields.emplace(id, STDMOVE(CheckBox::Create<CheckBox>(parent(), opt, id)));
+ break;
+ case coInt:
+ case coInts:
+ m_fields.emplace(id, STDMOVE(SpinCtrl::Create<SpinCtrl>(parent(), opt, id)));
+ break;
+ case coEnum:
+ m_fields.emplace(id, STDMOVE(Choice::Create<Choice>(parent(), opt, id)));
+ break;
+ case coPoints:
+ m_fields.emplace(id, STDMOVE(PointCtrl::Create<PointCtrl>(parent(), opt, id)));
+ break;
+ case coNone: break;
+ default:
+ throw /*//!ConfigGUITypeError("")*/std::logic_error("This control doesn't exist till now"); break;
+ }
+ }
+ // Grab a reference to fields for convenience
+ const t_field& field = m_fields[id];
+ field->m_on_change = [this](std::string opt_id, boost::any value){
+ //! This function will be called from Field.
+ //! Call OptionGroup._on_change(...)
+ if (!m_disabled)
+ this->on_change_OG(opt_id, value);
+ };
+ field->m_on_kill_focus = [this](){
+ //! This function will be called from Field.
+ if (!m_disabled)
+ this->on_kill_focus();
+ };
+ field->m_parent = parent();
+
+ //! Label to change background color, when option is modified
+ field->m_Label = label;
+ field->m_back_to_initial_value = [this](std::string opt_id){
+ if (!m_disabled)
+ this->back_to_initial_value(opt_id);
+ };
+ field->m_back_to_sys_value = [this](std::string opt_id){
+ if (!this->m_disabled)
+ this->back_to_sys_value(opt_id);
+ };
+
+ // assign function objects for callbacks, etc.
+ return field;
+}
+
+void OptionsGroup::add_undo_buttuns_to_sizer(wxSizer* sizer, const t_field& field)
+{
+ if (!m_show_modified_btns) {
+ field->m_Undo_btn->Hide();
+ field->m_Undo_to_sys_btn->Hide();
+ return;
+ }
+
+ sizer->Add(field->m_Undo_to_sys_btn, 0, wxALIGN_CENTER_VERTICAL);
+ sizer->Add(field->m_Undo_btn, 0, wxALIGN_CENTER_VERTICAL);
+}
+
+void OptionsGroup::append_line(const Line& line, wxStaticText** colored_Label/* = nullptr*/) {
+//! if (line.sizer != nullptr || (line.widget != nullptr && line.full_width > 0)){
+ if ( (line.sizer != nullptr || line.widget != nullptr) && line.full_width){
+ if (line.sizer != nullptr) {
+ sizer->Add(line.sizer, 0, wxEXPAND | wxALL, wxOSX ? 0 : 15);
+ return;
+ }
+ if (line.widget != nullptr) {
+ sizer->Add(line.widget(m_parent), 0, wxEXPAND | wxALL, wxOSX ? 0 : 15);
+ return;
+ }
+ }
+
+ auto option_set = line.get_options();
+ for (auto opt : option_set)
+ m_options.emplace(opt.opt_id, opt);
+
+ // if we have a single option with no label, no sidetext just add it directly to sizer
+ if (option_set.size() == 1 && label_width == 0 && option_set.front().opt.full_width &&
+ option_set.front().opt.sidetext.size() == 0 && option_set.front().side_widget == nullptr &&
+ line.get_extra_widgets().size() == 0) {
+ wxSizer* tmp_sizer;
+#ifdef __WXGTK__
+ tmp_sizer = new wxBoxSizer(wxVERTICAL);
+ m_panel->SetSizer(tmp_sizer);
+ m_panel->Layout();
+#else
+ tmp_sizer = sizer;
+#endif /* __WXGTK__ */
+
+ const auto& option = option_set.front();
+ const auto& field = build_field(option);
+
+ auto btn_sizer = new wxBoxSizer(wxHORIZONTAL);
+ add_undo_buttuns_to_sizer(btn_sizer, field);
+ tmp_sizer->Add(btn_sizer, 0, wxEXPAND | wxALL, 0);
+ if (is_window_field(field))
+ tmp_sizer->Add(field->getWindow(), 0, wxEXPAND | wxALL, wxOSX ? 0 : 5);
+ if (is_sizer_field(field))
+ tmp_sizer->Add(field->getSizer(), 0, wxEXPAND | wxALL, wxOSX ? 0 : 5);
+ return;
+ }
+
+ auto grid_sizer = m_grid_sizer;
+#ifdef __WXGTK__
+ m_panel->SetSizer(m_grid_sizer);
+ m_panel->Layout();
+#endif /* __WXGTK__ */
+
+ // if we have an extra column, build it
+ if (extra_column) {
+ if (extra_column) {
+ grid_sizer->Add(extra_column(parent(), line), 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 3);
+ }
+ else {
+ // if the callback provides no sizer for the extra cell, put a spacer
+ grid_sizer->AddSpacer(1);
+ }
+ }
+
+
+ // Build a label if we have it
+ wxStaticText* label=nullptr;
+ if (label_width != 0) {
+ long label_style = staticbox ? 0 : wxALIGN_RIGHT;
+#ifdef __WXGTK__
+ // workaround for correct text align of the StaticBox on Linux
+ // flags wxALIGN_RIGHT and wxALIGN_CENTRE don't work when Ellipsize flags are _not_ given.
+ // Text is properly aligned only when Ellipsize is checked.
+ label_style |= staticbox ? 0 : wxST_ELLIPSIZE_END;
+#endif /* __WXGTK__ */
+ label = new wxStaticText(parent(), wxID_ANY, line.label + (line.label.IsEmpty() ? "" : ":"),
+ wxDefaultPosition, wxSize(label_width, -1), label_style);
+ label->SetFont(label_font);
+ label->Wrap(label_width); // avoid a Linux/GTK bug
+ if (!line.near_label_widget)
+ grid_sizer->Add(label, 0, (staticbox ? 0 : wxALIGN_RIGHT | wxRIGHT) |
+ (m_flag == ogSIDE_OPTIONS_VERTICAL ? wxTOP : wxALIGN_CENTER_VERTICAL), 5);
+ else {
+ // If we're here, we have some widget near the label
+ // so we need a horizontal sizer to arrange these things
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ grid_sizer->Add(sizer, 0, wxEXPAND | (staticbox ? wxALL : wxBOTTOM | wxTOP | wxLEFT), staticbox ? 0 : 1);
+ sizer->Add(line.near_label_widget(parent()), 0, wxRIGHT, 7);
+ sizer->Add(label, 0, (staticbox ? 0 : wxALIGN_RIGHT | wxRIGHT) |
+ (m_flag == ogSIDE_OPTIONS_VERTICAL ? wxTOP : wxALIGN_CENTER_VERTICAL), 5);
+ }
+ if (line.label_tooltip.compare("") != 0)
+ label->SetToolTip(line.label_tooltip);
+ }
+
+ // If there's a widget, build it and add the result to the sizer.
+ if (line.widget != nullptr) {
+ auto wgt = line.widget(parent());
+ // If widget doesn't have label, don't use border
+ grid_sizer->Add(wgt, 0, wxEXPAND | wxBOTTOM | wxTOP, (wxOSX || line.label.IsEmpty()) ? 0 : 5);
+ if (colored_Label != nullptr) *colored_Label = label;
+ return;
+ }
+
+ // If we're here, we have more than one option or a single option with sidetext
+ // so we need a horizontal sizer to arrange these things
+ auto sizer = new wxBoxSizer(m_flag == ogSIDE_OPTIONS_VERTICAL ? wxVERTICAL : wxHORIZONTAL);
+ grid_sizer->Add(sizer, 0, wxEXPAND | (staticbox ? wxALL : wxBOTTOM | wxTOP | wxLEFT), staticbox ? 0 : 1);
+ // If we have a single option with no sidetext just add it directly to the grid sizer
+ if (option_set.size() == 1 && option_set.front().opt.sidetext.size() == 0 &&
+ option_set.front().side_widget == nullptr && line.get_extra_widgets().size() == 0) {
+ const auto& option = option_set.front();
+ const auto& field = build_field(option, label);
+
+ add_undo_buttuns_to_sizer(sizer, field);
+ if (is_window_field(field))
+ sizer->Add(field->getWindow(), option.opt.full_width ? 1 : 0, (option.opt.full_width ? wxEXPAND : 0) |
+ wxBOTTOM | wxTOP | wxALIGN_CENTER_VERTICAL, (wxOSX||!staticbox) ? 0 : 2);
+ if (is_sizer_field(field))
+ sizer->Add(field->getSizer(), 1, (option.opt.full_width ? wxEXPAND : 0) | wxALIGN_CENTER_VERTICAL, 0);
+ return;
+ }
+
+ for (auto opt : option_set) {
+ ConfigOptionDef option = opt.opt;
+ wxSizer* sizer_tmp;
+ if (m_flag == ogSIDE_OPTIONS_VERTICAL){
+ auto sz = new wxFlexGridSizer(1, 3, 2, 2);
+ sz->RemoveGrowableCol(2);
+ sizer_tmp = sz;
+ }
+ else
+ sizer_tmp = sizer;
+ // add label if any
+ if (option.label != "") {
+ wxString str_label = _(option.label);
+//! To correct translation by context have to use wxGETTEXT_IN_CONTEXT macro from wxWidget 3.1.1
+// wxString str_label = (option.label == "Top" || option.label == "Bottom") ?
+// wxGETTEXT_IN_CONTEXT("Layers", wxString(option.label.c_str()):
+// L_str(option.label);
+ label = new wxStaticText(parent(), wxID_ANY, str_label + ":", wxDefaultPosition, wxDefaultSize);
+ label->SetFont(label_font);
+ sizer_tmp->Add(label, 0, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, 0);
+ }
+
+ // add field
+ const Option& opt_ref = opt;
+ auto& field = build_field(opt_ref, label);
+ add_undo_buttuns_to_sizer(sizer_tmp, field);
+ is_sizer_field(field) ?
+ sizer_tmp->Add(field->getSizer(), 0, wxALIGN_CENTER_VERTICAL, 0) :
+ sizer_tmp->Add(field->getWindow(), 0, wxALIGN_CENTER_VERTICAL, 0);
+
+ // add sidetext if any
+ if (option.sidetext != "") {
+ auto sidetext = new wxStaticText( parent(), wxID_ANY, _(option.sidetext), wxDefaultPosition,
+ wxSize(sidetext_width, -1)/*wxDefaultSize*/, wxALIGN_LEFT);
+ sidetext->SetFont(sidetext_font);
+ sizer_tmp->Add(sidetext, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, m_flag == ogSIDE_OPTIONS_VERTICAL ? 0 : 4);
+ field->set_side_text_ptr(sidetext);
+ }
+
+ // add side widget if any
+ if (opt.side_widget != nullptr) {
+ sizer_tmp->Add(opt.side_widget(parent())/*!.target<wxWindow>()*/, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, 1); //! requires verification
+ }
+
+ if (opt.opt_id != option_set.back().opt_id && m_flag != ogSIDE_OPTIONS_VERTICAL) //! istead of (opt != option_set.back())
+ {
+ sizer_tmp->AddSpacer(6);
+ }
+
+ if (m_flag == ogSIDE_OPTIONS_VERTICAL)
+ sizer->Add(sizer_tmp, 0, wxALIGN_RIGHT|wxALL, 0);
+ }
+ // add extra sizers if any
+ for (auto extra_widget : line.get_extra_widgets()) {
+ sizer->Add(extra_widget(parent())/*!.target<wxWindow>()*/, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, 4); //! requires verification
+ }
+}
+
+Line OptionsGroup::create_single_option_line(const Option& option) const {
+ Line retval{ _(option.opt.label), _(option.opt.tooltip) };
+ Option tmp(option);
+ tmp.opt.label = std::string("");
+ retval.append_option(tmp);
+ return retval;
+}
+
+void OptionsGroup::on_change_OG(const t_config_option_key& opt_id, const boost::any& value) {
+ if (m_on_change != nullptr)
+ m_on_change(opt_id, value);
+}
+
+Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index /*= -1*/)
+{
+ if (!m_config->has(opt_key)) {
+ std::cerr << "No " << opt_key << " in ConfigOptionsGroup config.\n";
+ }
+
+ std::string opt_id = opt_index == -1 ? opt_key : opt_key + "#" + std::to_string(opt_index);
+ std::pair<std::string, int> pair(opt_key, opt_index);
+ m_opt_map.emplace(opt_id, pair);
+
+ return Option(*m_config->def()->get(opt_key), opt_id);
+}
+
+void ConfigOptionsGroup::on_change_OG(const t_config_option_key& opt_id, const boost::any& value)
+{
+ if (!m_opt_map.empty())
+ {
+ auto it = m_opt_map.find(opt_id);
+ if (it == m_opt_map.end())
+ {
+ OptionsGroup::on_change_OG(opt_id, value);
+ return;
+ }
+
+ auto itOption = it->second;
+ std::string opt_key = itOption.first;
+ int opt_index = itOption.second;
+
+ auto option = m_options.at(opt_id).opt;
+
+ // get value
+//! auto field_value = get_value(opt_id);
+ if (option.gui_flags.compare("serialized")==0) {
+ if (opt_index != -1){
+ // die "Can't set serialized option indexed value" ;
+ }
+ change_opt_value(*m_config, opt_key, value);
+ }
+ else {
+ if (opt_index == -1) {
+ // change_opt_value(*m_config, opt_key, field_value);
+ //!? why field_value?? in this case changed value will be lose! No?
+ change_opt_value(*m_config, opt_key, value);
+ }
+ else {
+ change_opt_value(*m_config, opt_key, value, opt_index);
+// auto value = m_config->get($opt_key);
+// $value->[$opt_index] = $field_value;
+// $self->config->set($opt_key, $value);
+ }
+ }
+ }
+
+ OptionsGroup::on_change_OG(opt_id, value); //!? Why doing this
+}
+
+void ConfigOptionsGroup::back_to_initial_value(const std::string& opt_key)
+{
+ if (m_get_initial_config == nullptr)
+ return;
+ back_to_config_value(m_get_initial_config(), opt_key);
+}
+
+void ConfigOptionsGroup::back_to_sys_value(const std::string& opt_key)
+{
+ if (m_get_sys_config == nullptr)
+ return;
+ if (!have_sys_config())
+ return;
+ back_to_config_value(m_get_sys_config(), opt_key);
+}
+
+void ConfigOptionsGroup::back_to_config_value(const DynamicPrintConfig& config, const std::string& opt_key)
+{
+ boost::any value;
+ if (opt_key == "extruders_count"){
+ auto *nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(config.option("nozzle_diameter"));
+ value = int(nozzle_diameter->values.size());
+ }
+ else if (m_opt_map.find(opt_key) != m_opt_map.end())
+ {
+ auto opt_id = m_opt_map.find(opt_key)->first;
+ std::string opt_short_key = m_opt_map.at(opt_id).first;
+ int opt_index = m_opt_map.at(opt_id).second;
+ value = get_config_value(config, opt_short_key, opt_index);
+ }
+ else{
+ value = get_config_value(config, opt_key);
+ change_opt_value(*m_config, opt_key, value);
+ return;
+ }
+
+ set_value(opt_key, value);
+ on_change_OG(opt_key, get_value(opt_key));
+}
+
+void ConfigOptionsGroup::reload_config(){
+ for (t_opt_map::iterator it = m_opt_map.begin(); it != m_opt_map.end(); ++it) {
+ auto opt_id = it->first;
+ std::string opt_key = m_opt_map.at(opt_id).first;
+ int opt_index = m_opt_map.at(opt_id).second;
+ auto option = m_options.at(opt_id).opt;
+ set_value(opt_id, config_value(opt_key, opt_index, option.gui_flags.compare("serialized") == 0 ));
+ }
+
+}
+
+boost::any ConfigOptionsGroup::config_value(const std::string& opt_key, int opt_index, bool deserialize){
+
+ if (deserialize) {
+ // Want to edit a vector value(currently only multi - strings) in a single edit box.
+ // Aggregate the strings the old way.
+ // Currently used for the post_process config value only.
+ if (opt_index != -1)
+ throw std::out_of_range("Can't deserialize option indexed value");
+// return join(';', m_config->get(opt_key)});
+ return get_config_value(*m_config, opt_key);
+ }
+ else {
+// return opt_index == -1 ? m_config->get(opt_key) : m_config->get_at(opt_key, opt_index);
+ return get_config_value(*m_config, opt_key, opt_index);
+ }
+}
+
+boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config, const std::string& opt_key, int opt_index /*= -1*/)
+{
+ size_t idx = opt_index == -1 ? 0 : opt_index;
+
+ boost::any ret;
+ wxString text_value = wxString("");
+ const ConfigOptionDef* opt = config.def()->get(opt_key);
+ switch (opt->type){
+ case coFloatOrPercent:{
+ const auto &value = *config.option<ConfigOptionFloatOrPercent>(opt_key);
+ if (value.percent)
+ {
+ text_value = wxString::Format(_T("%i"), int(value.value));
+ text_value += "%";
+ }
+ else
+ text_value = double_to_string(value.value);
+ ret = text_value;
+ break;
+ }
+ case coPercent:{
+ double val = config.option<ConfigOptionPercent>(opt_key)->value;
+ text_value = wxString::Format(_T("%i"), int(val));
+ ret = text_value;// += "%";
+ }
+ break;
+ case coPercents:
+ case coFloats:
+ case coFloat:{
+ double val = opt->type == coFloats ?
+ config.opt_float(opt_key, idx) :
+ opt->type == coFloat ? config.opt_float(opt_key) :
+ config.option<ConfigOptionPercents>(opt_key)->get_at(idx);
+ ret = double_to_string(val);
+ }
+ break;
+ case coString:
+ ret = static_cast<wxString>(config.opt_string(opt_key));
+ break;
+ case coStrings:
+ if (opt_key.compare("compatible_printers") == 0){
+ ret = config.option<ConfigOptionStrings>(opt_key)->values;
+ break;
+ }
+ if (config.option<ConfigOptionStrings>(opt_key)->values.empty())
+ ret = text_value;
+ else if (opt->gui_flags.compare("serialized") == 0){
+ std::vector<std::string> values = config.option<ConfigOptionStrings>(opt_key)->values;
+ if (!values.empty() && values[0].compare("") != 0)
+ for (auto el : values)
+ text_value += el + ";";
+ ret = text_value;
+ }
+ else
+ ret = static_cast<wxString>(config.opt_string(opt_key, static_cast<unsigned int>(idx)));
+ break;
+ case coBool:
+ ret = config.opt_bool(opt_key);
+ break;
+ case coBools:
+ ret = config.opt_bool(opt_key, idx);
+ break;
+ case coInt:
+ ret = config.opt_int(opt_key);
+ break;
+ case coInts:
+ ret = config.opt_int(opt_key, idx);
+ break;
+ case coEnum:{
+ if (opt_key.compare("external_fill_pattern") == 0 ||
+ opt_key.compare("fill_pattern") == 0 ){
+ ret = static_cast<int>(config.option<ConfigOptionEnum<InfillPattern>>(opt_key)->value);
+ }
+ else if (opt_key.compare("gcode_flavor") == 0 ){
+ ret = static_cast<int>(config.option<ConfigOptionEnum<GCodeFlavor>>(opt_key)->value);
+ }
+ else if (opt_key.compare("support_material_pattern") == 0){
+ ret = static_cast<int>(config.option<ConfigOptionEnum<SupportMaterialPattern>>(opt_key)->value);
+ }
+ else if (opt_key.compare("seam_position") == 0){
+ ret = static_cast<int>(config.option<ConfigOptionEnum<SeamPosition>>(opt_key)->value);
+ }
+ else if (opt_key.compare("host_type") == 0){
+ ret = static_cast<int>(config.option<ConfigOptionEnum<PrintHostType>>(opt_key)->value);
+ }
+ }
+ break;
+ case coPoints:
+ if (opt_key.compare("bed_shape") == 0)
+ ret = config.option<ConfigOptionPoints>(opt_key)->values;
+ else
+ ret = config.option<ConfigOptionPoints>(opt_key)->get_at(idx);
+ break;
+ case coNone:
+ default:
+ break;
+ }
+ return ret;
+}
+
+Field* ConfigOptionsGroup::get_fieldc(const t_config_option_key& opt_key, int opt_index){
+ Field* field = get_field(opt_key);
+ if (field != nullptr)
+ return field;
+ std::string opt_id = "";
+ for (t_opt_map::iterator it = m_opt_map.begin(); it != m_opt_map.end(); ++it) {
+ if (opt_key == m_opt_map.at(it->first).first && opt_index == m_opt_map.at(it->first).second){
+ opt_id = it->first;
+ break;
+ }
+ }
+ return opt_id.empty() ? nullptr : get_field(opt_id);
+}
+
+void ogStaticText::SetText(const wxString& value, bool wrap/* = true*/)
+{
+ SetLabel(value);
+ if (wrap) Wrap(400);
+ GetParent()->Layout();
+}
+
+} // GUI
+} // Slic3r
diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp
new file mode 100644
index 000000000..4941e5453
--- /dev/null
+++ b/src/slic3r/GUI/OptionsGroup.hpp
@@ -0,0 +1,271 @@
+#ifndef slic3r_OptionsGroup_hpp_
+#define slic3r_OptionsGroup_hpp_
+
+#include <wx/wx.h>
+#include <wx/stattext.h>
+#include <wx/settings.h>
+//#include <wx/window.h>
+
+#include <map>
+#include <functional>
+
+#include "libslic3r/Config.hpp"
+#include "libslic3r/PrintConfig.hpp"
+#include "libslic3r/libslic3r.h"
+
+#include "Field.hpp"
+
+// Translate the ifdef
+#ifdef __WXOSX__
+ #define wxOSX true
+#else
+ #define wxOSX false
+#endif
+
+#define BORDER(a, b) ((wxOSX ? a : b))
+
+namespace Slic3r { namespace GUI {
+
+enum ogDrawFlag{
+ ogDEFAULT,
+ ogSIDE_OPTIONS_VERTICAL
+};
+
+/// Widget type describes a function object that returns a wxWindow (our widget) and accepts a wxWidget (parent window).
+using widget_t = std::function<wxSizer*(wxWindow*)>;//!std::function<wxWindow*(wxWindow*)>;
+
+//auto default_label_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); //GetSystemColour
+//auto modified_label_clr = *new wxColour(254, 189, 101);
+
+/// Wraps a ConfigOptionDef and adds function object for creating a side_widget.
+struct Option {
+ ConfigOptionDef opt { ConfigOptionDef() };
+ t_config_option_key opt_id;//! {""};
+ widget_t side_widget {nullptr};
+ bool readonly {false};
+
+ Option(const ConfigOptionDef& _opt, t_config_option_key id) :
+ opt(_opt), opt_id(id) {}
+};
+using t_option = std::unique_ptr<Option>; //!
+
+/// Represents option lines
+class Line {
+public:
+ wxString label {wxString("")};
+ wxString label_tooltip {wxString("")};
+ size_t full_width {0};
+ wxSizer* sizer {nullptr};
+ widget_t widget {nullptr};
+ std::function<wxWindow*(wxWindow*)> near_label_widget{ nullptr };
+
+ void append_option(const Option& option) {
+ m_options.push_back(option);
+ }
+ void append_widget(const widget_t widget) {
+ m_extra_widgets.push_back(widget);
+ }
+ Line(wxString label, wxString tooltip) :
+ label(label), label_tooltip(tooltip) {}
+
+ const std::vector<widget_t>& get_extra_widgets() const {return m_extra_widgets;}
+ const std::vector<Option>& get_options() const { return m_options; }
+
+private:
+ std::vector<Option> m_options;//! {std::vector<Option>()};
+ std::vector<widget_t> m_extra_widgets;//! {std::vector<widget_t>()};
+};
+
+using column_t = std::function<wxWindow*(wxWindow* parent, const Line&)>;//std::function<wxSizer*(const Line&)>;
+
+using t_optionfield_map = std::map<t_config_option_key, t_field>;
+using t_opt_map = std::map< std::string, std::pair<std::string, int> >;
+
+class OptionsGroup {
+ wxStaticBox* stb;
+public:
+ const bool staticbox {true};
+ const wxString title {wxString("")};
+ size_t label_width {200};
+ wxSizer* sizer {nullptr};
+ column_t extra_column {nullptr};
+ t_change m_on_change {nullptr};
+ std::function<DynamicPrintConfig()> m_get_initial_config{ nullptr };
+ std::function<DynamicPrintConfig()> m_get_sys_config{ nullptr };
+ std::function<bool()> have_sys_config{ nullptr };
+
+ wxFont sidetext_font {wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT) };
+ wxFont label_font {wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT) };
+ int sidetext_width{ -1 };
+
+ /// Returns a copy of the pointer of the parent wxWindow.
+ /// Accessor function is because users are not allowed to change the parent
+ /// but defining it as const means a lot of const_casts to deal with wx functions.
+ inline wxWindow* parent() const {
+#ifdef __WXGTK__
+ return m_panel;
+#else
+ return m_parent;
+#endif /* __WXGTK__ */
+ }
+#ifdef __WXGTK__
+ wxWindow* get_parent() const {
+ return m_parent;
+ }
+#endif /* __WXGTK__ */
+
+ void append_line(const Line& line, wxStaticText** colored_Label = nullptr);
+ Line create_single_option_line(const Option& option) const;
+ void append_single_option_line(const Option& option) { append_line(create_single_option_line(option)); }
+
+ // return a non-owning pointer reference
+ inline Field* get_field(const t_config_option_key& id) const{
+ if (m_fields.find(id) == m_fields.end()) return nullptr;
+ return m_fields.at(id).get();
+ }
+ bool set_value(const t_config_option_key& id, const boost::any& value, bool change_event = false) {
+ if (m_fields.find(id) == m_fields.end()) return false;
+ m_fields.at(id)->set_value(value, change_event);
+ return true;
+ }
+ boost::any get_value(const t_config_option_key& id) {
+ boost::any out;
+ if (m_fields.find(id) == m_fields.end()) ;
+ else
+ out = m_fields.at(id)->get_value();
+ return out;
+ }
+
+ bool set_side_text(const t_config_option_key& opt_key, const wxString& side_text) {
+ if (m_fields.find(opt_key) == m_fields.end()) return false;
+ auto st = m_fields.at(opt_key)->m_side_text;
+ if (!st) return false;
+ st->SetLabel(side_text);
+ return true;
+ }
+
+ void set_name(const wxString& new_name) {
+ stb->SetLabel(new_name);
+ }
+
+ inline void enable() { for (auto& field : m_fields) field.second->enable(); }
+ inline void disable() { for (auto& field : m_fields) field.second->disable(); }
+ void set_flag(ogDrawFlag flag) { m_flag = flag; }
+ void set_grid_vgap(int gap) { m_grid_sizer->SetVGap(gap); }
+
+ void set_show_modified_btns_val(bool show) {
+ m_show_modified_btns = show;
+ }
+
+ OptionsGroup( wxWindow* _parent, const wxString& title, bool is_tab_opt = false,
+ ogDrawFlag flag = ogDEFAULT, column_t extra_clmn = nullptr) :
+ m_parent(_parent), title(title), m_show_modified_btns(is_tab_opt),
+ staticbox(title!=""), m_flag(flag), extra_column(extra_clmn){
+ if (staticbox) {
+ stb = new wxStaticBox(_parent, wxID_ANY, title);
+ stb->SetFont(bold_font());
+ }
+ sizer = (staticbox ? new wxStaticBoxSizer(stb, wxVERTICAL) : new wxBoxSizer(wxVERTICAL));
+ auto num_columns = 1U;
+ if (label_width != 0) num_columns++;
+ if (extra_column != nullptr) num_columns++;
+ m_grid_sizer = new wxFlexGridSizer(0, num_columns, 1,0);
+ static_cast<wxFlexGridSizer*>(m_grid_sizer)->SetFlexibleDirection(wxBOTH/*wxHORIZONTAL*/);
+ static_cast<wxFlexGridSizer*>(m_grid_sizer)->AddGrowableCol(label_width != 0);
+#ifdef __WXGTK__
+ m_panel = new wxPanel( _parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
+ sizer->Fit(m_panel);
+ sizer->Add(m_panel, 0, wxEXPAND | wxALL, wxOSX||!staticbox ? 0: 5);
+#else
+ sizer->Add(m_grid_sizer, 0, wxEXPAND | wxALL, wxOSX||!staticbox ? 0: 5);
+#endif /* __WXGTK__ */
+ }
+
+ wxGridSizer* get_grid_sizer(){ return m_grid_sizer; }
+
+protected:
+ std::map<t_config_option_key, Option> m_options;
+ wxWindow* m_parent {nullptr};
+
+ /// Field list, contains unique_ptrs of the derived type.
+ /// using types that need to know what it is beyond the public interface
+ /// need to cast based on the related ConfigOptionDef.
+ t_optionfield_map m_fields;
+ bool m_disabled {false};
+ wxGridSizer* m_grid_sizer {nullptr};
+ // "true" if option is created in preset tabs
+ bool m_show_modified_btns{ false };
+
+ ogDrawFlag m_flag{ ogDEFAULT };
+
+ // This panel is needed for correct showing of the ToolTips for Button, StaticText and CheckBox
+ // Tooltips on GTK doesn't work inside wxStaticBoxSizer unless you insert a panel
+ // inside it before you insert the other controls.
+#ifdef __WXGTK__
+ wxPanel* m_panel {nullptr};
+#endif /* __WXGTK__ */
+
+ /// Generate a wxSizer or wxWindow from a configuration option
+ /// Precondition: opt resolves to a known ConfigOption
+ /// Postcondition: fields contains a wx gui object.
+ const t_field& build_field(const t_config_option_key& id, const ConfigOptionDef& opt, wxStaticText* label = nullptr);
+ const t_field& build_field(const t_config_option_key& id, wxStaticText* label = nullptr);
+ const t_field& build_field(const Option& opt, wxStaticText* label = nullptr);
+ void add_undo_buttuns_to_sizer(wxSizer* sizer, const t_field& field);
+
+ virtual void on_kill_focus (){};
+ virtual void on_change_OG(const t_config_option_key& opt_id, const boost::any& value);
+ virtual void back_to_initial_value(const std::string& opt_key){}
+ virtual void back_to_sys_value(const std::string& opt_key){}
+};
+
+class ConfigOptionsGroup: public OptionsGroup {
+public:
+ ConfigOptionsGroup( wxWindow* parent, const wxString& title, DynamicPrintConfig* _config = nullptr,
+ bool is_tab_opt = false, ogDrawFlag flag = ogDEFAULT, column_t extra_clmn = nullptr) :
+ OptionsGroup(parent, title, is_tab_opt, flag, extra_clmn), m_config(_config) {}
+
+ /// reference to libslic3r config, non-owning pointer (?).
+ DynamicPrintConfig* m_config {nullptr};
+ bool m_full_labels {0};
+ t_opt_map m_opt_map;
+
+ Option get_option(const std::string& opt_key, int opt_index = -1);
+ Line create_single_option_line(const std::string& title, int idx = -1) /*const*/{
+ Option option = get_option(title, idx);
+ return OptionsGroup::create_single_option_line(option);
+ }
+ void append_single_option_line(const Option& option) {
+ OptionsGroup::append_single_option_line(option);
+ }
+ void append_single_option_line(const std::string title, int idx = -1)
+ {
+ Option option = get_option(title, idx);
+ append_single_option_line(option);
+ }
+
+ void on_change_OG(const t_config_option_key& opt_id, const boost::any& value) override;
+ void back_to_initial_value(const std::string& opt_key) override;
+ void back_to_sys_value(const std::string& opt_key) override;
+ void back_to_config_value(const DynamicPrintConfig& config, const std::string& opt_key);
+ void on_kill_focus() override{ reload_config();}
+ void reload_config();
+ boost::any config_value(const std::string& opt_key, int opt_index, bool deserialize);
+ // return option value from config
+ boost::any get_config_value(const DynamicPrintConfig& config, const std::string& opt_key, int opt_index = -1);
+ Field* get_fieldc(const t_config_option_key& opt_key, int opt_index);
+};
+
+// Static text shown among the options.
+class ogStaticText :public wxStaticText{
+public:
+ ogStaticText() {}
+ ogStaticText(wxWindow* parent, const char *text) : wxStaticText(parent, wxID_ANY, text, wxDefaultPosition, wxDefaultSize){}
+ ~ogStaticText(){}
+
+ void SetText(const wxString& value, bool wrap = true);
+};
+
+}}
+
+#endif /* slic3r_OptionsGroup_hpp_ */
diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp
new file mode 100644
index 000000000..89a8ead92
--- /dev/null
+++ b/src/slic3r/GUI/Preferences.cpp
@@ -0,0 +1,134 @@
+#include "Preferences.hpp"
+#include "AppConfig.hpp"
+#include "OptionsGroup.hpp"
+
+namespace Slic3r {
+namespace GUI {
+
+PreferencesDialog::PreferencesDialog(wxWindow* parent, int event_preferences) :
+ wxDialog(parent, wxID_ANY, _(L("Preferences")), wxDefaultPosition, wxDefaultSize),
+ m_event_preferences(event_preferences) {
+ build();
+ }
+
+void PreferencesDialog::build()
+{
+ auto app_config = get_app_config();
+ m_optgroup = std::make_shared<ConfigOptionsGroup>(this, _(L("General")));
+ 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<bool>(value) ? "1" : "0";
+ };
+
+ // TODO
+// $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
+// opt_id = > 'version_check',
+// type = > 'bool',
+// label = > 'Check for updates',
+// tooltip = > 'If this is enabled, Slic3r will check for updates daily and display a reminder if a newer version is available.',
+// default = > $app_config->get("version_check") // 1,
+// readonly = > !wxTheApp->have_version_check,
+// ));
+
+ ConfigOptionDef def;
+ def.label = L("Remember output directory");
+ def.type = coBool;
+ def.tooltip = L("If this is enabled, Slic3r will prompt the last output directory "
+ "instead of the one containing the input files.");
+ def.default_value = new ConfigOptionBool{ app_config->has("remember_output_path") ? app_config->get("remember_output_path")[0] == '1' : true }; // 1;
+ Option option(def, "remember_output_path");
+ m_optgroup->append_single_option_line(option);
+
+ def.label = L("Auto-center parts");
+ def.type = coBool;
+ def.tooltip = L("If this is enabled, Slic3r will auto-center objects "
+ "around the print bed center.");
+ def.default_value = new ConfigOptionBool{ app_config->get("autocenter")[0] == '1' }; // 1;
+ option = Option (def,"autocenter");
+ m_optgroup->append_single_option_line(option);
+
+ def.label = L("Background processing");
+ def.type = coBool;
+ def.tooltip = L("If this is enabled, Slic3r will pre-process objects as soon "
+ "as they\'re loaded in order to save time when exporting G-code.");
+ def.default_value = new ConfigOptionBool{ app_config->get("background_processing")[0] == '1' }; // 1;
+ 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("Suppress \" - default - \" presets");
+ def.type = coBool;
+ def.tooltip = L("Suppress \" - default - \" presets in the Print / Filament / Printer "
+ "selections once there are any other valid presets available.");
+ def.default_value = new ConfigOptionBool{ app_config->get("no_defaults")[0] == '1' }; // 1;
+ option = Option (def,"no_defaults");
+ m_optgroup->append_single_option_line(option);
+
+ def.label = L("Show incompatible print and filament presets");
+ def.type = coBool;
+ def.tooltip = L("When checked, the print and filament presets are shown in the preset editor "
+ "even if they are marked as incompatible with the active printer");
+ def.default_value = new ConfigOptionBool{ app_config->get("show_incompatible_presets")[0] == '1' }; // 1;
+ option = Option (def,"show_incompatible_presets");
+ m_optgroup->append_single_option_line(option);
+
+ def.label = L("Use legacy OpenGL 1.1 rendering");
+ def.type = coBool;
+ def.tooltip = L("If you have rendering issues caused by a buggy OpenGL 2.0 driver, "
+ "you may try to check this checkbox. This will disable the layer height "
+ "editing and anti aliasing, so it is likely better to upgrade your graphics driver.");
+ def.default_value = new ConfigOptionBool{ app_config->get("use_legacy_opengl")[0] == '1' }; // 1;
+ option = Option (def,"use_legacy_opengl");
+ m_optgroup->append_single_option_line(option);
+
+ auto sizer = new wxBoxSizer(wxVERTICAL);
+ sizer->Add(m_optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
+
+ auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL);
+ wxButton* btn = static_cast<wxButton*>(FindWindowById(wxID_OK, this));
+ btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { accept(); });
+ sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10);
+
+ SetSizer(sizer);
+ sizer->SetSizeHints(this);
+}
+
+void PreferencesDialog::accept()
+{
+ if (m_values.find("no_defaults") != m_values.end()||
+ m_values.find("use_legacy_opengl")!= m_values.end()) {
+ warning_catcher(this, _(L("You need to restart Slic3r to make the changes effective.")));
+ }
+
+ auto app_config = get_app_config();
+ for (std::map<std::string, std::string>::iterator it = m_values.begin(); it != m_values.end(); ++it) {
+ app_config->set(it->first, it->second);
+ }
+
+ EndModal(wxID_OK);
+ Close(); // needed on Linux
+
+ // Nothify the UI to update itself from the ini file.
+ if (m_event_preferences > 0) {
+ wxCommandEvent event(m_event_preferences);
+ get_app()->ProcessEvent(event);
+ }
+}
+
+} // GUI
+} // Slic3r \ No newline at end of file
diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp
new file mode 100644
index 000000000..d01d78b70
--- /dev/null
+++ b/src/slic3r/GUI/Preferences.hpp
@@ -0,0 +1,31 @@
+#ifndef slic3r_Preferences_hpp_
+#define slic3r_Preferences_hpp_
+
+#include "GUI.hpp"
+
+#include <wx/dialog.h>
+#include <map>
+
+namespace Slic3r {
+namespace GUI {
+
+class ConfigOptionsGroup;
+
+class PreferencesDialog : public wxDialog
+{
+ std::map<std::string, std::string> m_values;
+ std::shared_ptr<ConfigOptionsGroup> m_optgroup;
+ int m_event_preferences;
+public:
+ PreferencesDialog(wxWindow* parent, int event_preferences);
+ ~PreferencesDialog(){ }
+
+ void build();
+ void accept();
+};
+
+} // GUI
+} // Slic3r
+
+
+#endif /* slic3r_Preferences_hpp_ */
diff --git a/src/slic3r/GUI/Preset.cpp b/src/slic3r/GUI/Preset.cpp
new file mode 100644
index 000000000..9911caa5b
--- /dev/null
+++ b/src/slic3r/GUI/Preset.cpp
@@ -0,0 +1,1019 @@
+//#undef NDEBUG
+#include <cassert>
+
+#include "Preset.hpp"
+#include "AppConfig.hpp"
+#include "BitmapCache.hpp"
+
+#include <fstream>
+#include <stdexcept>
+#include <boost/format.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+
+#include <boost/nowide/cenv.hpp>
+#include <boost/nowide/cstdio.hpp>
+#include <boost/nowide/fstream.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/locale.hpp>
+#include <boost/log/trivial.hpp>
+
+#include <wx/image.h>
+#include <wx/choice.h>
+#include <wx/bmpcbox.h>
+#include <wx/wupdlock.h>
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/Utils.hpp"
+#include "../../libslic3r/PlaceholderParser.hpp"
+
+using boost::property_tree::ptree;
+
+namespace Slic3r {
+
+ConfigFileType guess_config_file_type(const ptree &tree)
+{
+ size_t app_config = 0;
+ size_t bundle = 0;
+ size_t config = 0;
+ for (const ptree::value_type &v : tree) {
+ if (v.second.empty()) {
+ if (v.first == "background_processing" ||
+ v.first == "last_output_path" ||
+ v.first == "no_controller" ||
+ v.first == "no_defaults")
+ ++ app_config;
+ else if (v.first == "nozzle_diameter" ||
+ v.first == "filament_diameter")
+ ++ config;
+ } else if (boost::algorithm::starts_with(v.first, "print:") ||
+ boost::algorithm::starts_with(v.first, "filament:") ||
+ boost::algorithm::starts_with(v.first, "printer:") ||
+ v.first == "settings")
+ ++ bundle;
+ else if (v.first == "presets") {
+ ++ app_config;
+ ++ bundle;
+ } else if (v.first == "recent") {
+ for (auto &kvp : v.second)
+ if (kvp.first == "config_directory" || kvp.first == "skein_directory")
+ ++ app_config;
+ }
+ }
+ return (app_config > bundle && app_config > config) ? CONFIG_FILE_TYPE_APP_CONFIG :
+ (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();
+
+ 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
+ {
+ 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 &section : 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<std::string>("name", model.id);
+ auto technology_field = section.second.get<std::string>("technology", "FFF");
+ if (! ConfigOptionEnum<PrinterTechnology>::from_string(technology_field, model.technology)) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: Invalid printer technology field: `%2%`") % id % technology_field;
+ model.technology = ptFFF;
+ }
+ section.second.get<std::string>("variants", "");
+ const auto variants_field = section.second.get<std::string>("variants", "");
+ std::vector<std::string> variants;
+ if (Slic3r::unescape_strings_cstyle(variants_field, variants)) {
+ for (const std::string &variant_name : variants) {
+ if (model.variant(variant_name) == nullptr)
+ model.variants.emplace_back(VendorProfile::PrinterVariant(variant_name));
+ }
+ } else {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: Malformed variants field: `%2%`") % id % variants_field;
+ }
+ 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()
+{
+ return g_suffix_modified;
+}
+
+void Preset::update_suffix_modified()
+{
+ g_suffix_modified = (" (" + _(L("modified")) + ")").ToUTF8().data();
+}
+// Remove an optional "(modified)" suffix from a name.
+// This converts a UI name to a unique preset identifier.
+std::string Preset::remove_suffix_modified(const std::string &name)
+{
+ return boost::algorithm::ends_with(name, g_suffix_modified) ?
+ name.substr(0, name.size() - g_suffix_modified.size()) :
+ name;
+}
+
+void Preset::set_num_extruders(DynamicPrintConfig &config, unsigned int num_extruders)
+{
+ const auto &defaults = FullPrintConfig::defaults();
+ for (const std::string &key : Preset::nozzle_options()) {
+ auto *opt = config.option(key, false);
+ assert(opt != nullptr);
+ assert(opt->is_vector());
+ if (opt != nullptr && opt->is_vector() && key != "default_filament_profile")
+ static_cast<ConfigOptionVectorBase*>(opt)->resize(num_extruders, defaults.option(key));
+ }
+}
+
+// Update new extruder fields at the printer profile.
+void Preset::normalize(DynamicPrintConfig &config)
+{
+ auto *nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(config.option("nozzle_diameter"));
+ if (nozzle_diameter != nullptr)
+ // Loaded the FFF Printer settings. Verify, that all extruder dependent values have enough values.
+ set_num_extruders(config, (unsigned int)nozzle_diameter->values.size());
+ if (config.option("filament_diameter") != nullptr) {
+ // This config contains single or multiple filament presets.
+ // Ensure that the filament preset vector options contain the correct number of values.
+ size_t n = (nozzle_diameter == nullptr) ? 1 : nozzle_diameter->values.size();
+ const auto &defaults = FullPrintConfig::defaults();
+ for (const std::string &key : Preset::filament_options()) {
+ if (key == "compatible_printers")
+ continue;
+ auto *opt = config.option(key, false);
+ /*assert(opt != nullptr);
+ assert(opt->is_vector());*/
+ if (opt != nullptr && opt->is_vector())
+ static_cast<ConfigOptionVectorBase*>(opt)->resize(n, defaults.option(key));
+ }
+ // The following keys are mandatory for the UI, but they are not part of FullPrintConfig, therefore they are handled separately.
+ for (const std::string &key : { "filament_settings_id" }) {
+ auto *opt = config.option(key, false);
+ assert(opt != nullptr);
+ assert(opt->type() == coStrings);
+ if (opt != nullptr && opt->type() == coStrings)
+ static_cast<ConfigOptionStrings*>(opt)->values.resize(n, std::string());
+ }
+ }
+}
+
+DynamicPrintConfig& Preset::load(const std::vector<std::string> &keys, const StaticPrintConfig &defaults)
+{
+ // Set the configuration from the defaults.
+ this->config.apply_only(defaults, keys.empty() ? defaults.keys() : keys);
+ if (! this->is_default) {
+ // Load the preset file, apply preset values on top of defaults.
+ try {
+ this->config.load_from_ini(this->file);
+ Preset::normalize(this->config);
+ } catch (const std::ifstream::failure &err) {
+ throw std::runtime_error(std::string("The selected preset cannot be loaded: ") + this->file + "\n\tReason: " + err.what());
+ } catch (const std::runtime_error &err) {
+ throw std::runtime_error(std::string("Failed loading the preset file: ") + this->file + "\n\tReason: " + err.what());
+ }
+ }
+ this->loaded = true;
+ return this->config;
+}
+
+void Preset::save()
+{
+ this->config.save(this->file);
+}
+
+// Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty.
+std::string Preset::label() const
+{
+ return this->name + (this->is_dirty ? g_suffix_modified : "");
+}
+
+bool Preset::is_compatible_with_printer(const Preset &active_printer, const DynamicPrintConfig *extra_config) const
+{
+ auto &condition = this->compatible_printers_condition();
+ auto *compatible_printers = dynamic_cast<const ConfigOptionStrings*>(this->config.option("compatible_printers"));
+ bool has_compatible_printers = compatible_printers != nullptr && ! compatible_printers->values.empty();
+ if (! has_compatible_printers && ! condition.empty()) {
+ try {
+ return PlaceholderParser::evaluate_boolean_expression(condition, active_printer.config, extra_config);
+ } catch (const std::runtime_error &err) {
+ //FIXME in case of an error, return "compatible with everything".
+ printf("Preset::is_compatible_with_printer - parsing error of compatible_printers_condition %s:\n%s\n", active_printer.name.c_str(), err.what());
+ return true;
+ }
+ }
+ return this->is_default || active_printer.name.empty() || ! has_compatible_printers ||
+ std::find(compatible_printers->values.begin(), compatible_printers->values.end(), active_printer.name) !=
+ compatible_printers->values.end();
+}
+
+bool Preset::is_compatible_with_printer(const Preset &active_printer) const
+{
+ DynamicPrintConfig config;
+ config.set_key_value("printer_preset", new ConfigOptionString(active_printer.name));
+ const ConfigOption *opt = active_printer.config.option("nozzle_diameter");
+ if (opt)
+ config.set_key_value("num_extruders", new ConfigOptionInt((int)static_cast<const ConfigOptionFloats*>(opt)->values.size()));
+ return this->is_compatible_with_printer(active_printer, &config);
+}
+
+bool Preset::update_compatible_with_printer(const Preset &active_printer, const DynamicPrintConfig *extra_config)
+{
+ 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);
+}
+
+const std::vector<std::string>& Preset::print_options()
+{
+ static std::vector<std::string> s_opts {
+ "layer_height", "first_layer_height", "perimeters", "spiral_vase", "top_solid_layers", "bottom_solid_layers",
+ "extra_perimeters", "ensure_vertical_shell_thickness", "avoid_crossing_perimeters", "thin_walls", "overhangs",
+ "seam_position", "external_perimeters_first", "fill_density", "fill_pattern", "external_fill_pattern",
+ "infill_every_layers", "infill_only_where_needed", "solid_infill_every_layers", "fill_angle", "bridge_angle",
+ "solid_infill_below_area", "only_retract_when_crossing_perimeters", "infill_first", "max_print_speed",
+ "max_volumetric_speed", "max_volumetric_extrusion_rate_slope_positive", "max_volumetric_extrusion_rate_slope_negative",
+ "perimeter_speed", "small_perimeter_speed", "external_perimeter_speed", "infill_speed", "solid_infill_speed",
+ "top_solid_infill_speed", "support_material_speed", "support_material_xy_spacing", "support_material_interface_speed",
+ "bridge_speed", "gap_fill_speed", "travel_speed", "first_layer_speed", "perimeter_acceleration", "infill_acceleration",
+ "bridge_acceleration", "first_layer_acceleration", "default_acceleration", "skirts", "skirt_distance", "skirt_height",
+ "min_skirt_length", "brim_width", "support_material", "support_material_auto", "support_material_threshold", "support_material_enforce_layers",
+ "raft_layers", "support_material_pattern", "support_material_with_sheath", "support_material_spacing",
+ "support_material_synchronize_layers", "support_material_angle", "support_material_interface_layers",
+ "support_material_interface_spacing", "support_material_interface_contact_loops", "support_material_contact_distance",
+ "support_material_buildplate_only", "dont_support_bridges", "notes", "complete_objects", "extruder_clearance_radius",
+ "extruder_clearance_height", "gcode_comments", "output_filename_format", "post_process", "perimeter_extruder",
+ "infill_extruder", "solid_infill_extruder", "support_material_extruder", "support_material_interface_extruder",
+ "ooze_prevention", "standby_temperature_delta", "interface_shells", "extrusion_width", "first_layer_extrusion_width",
+ "perimeter_extrusion_width", "external_perimeter_extrusion_width", "infill_extrusion_width", "solid_infill_extrusion_width",
+ "top_infill_extrusion_width", "support_material_extrusion_width", "infill_overlap", "bridge_flow_ratio", "clip_multipart_objects",
+ "elefant_foot_compensation", "xy_size_compensation", "threads", "resolution", "wipe_tower", "wipe_tower_x", "wipe_tower_y",
+ "wipe_tower_width", "wipe_tower_rotation_angle", "wipe_tower_bridging", "single_extruder_multi_material_priming",
+ "compatible_printers", "compatible_printers_condition","inherits"
+ };
+ return s_opts;
+}
+
+const std::vector<std::string>& Preset::filament_options()
+{
+ static std::vector<std::string> s_opts {
+ "filament_colour", "filament_diameter", "filament_type", "filament_soluble", "filament_notes", "filament_max_volumetric_speed",
+ "extrusion_multiplier", "filament_density", "filament_cost", "filament_loading_speed", "filament_loading_speed_start", "filament_load_time",
+ "filament_unloading_speed", "filament_unloading_speed_start", "filament_unload_time", "filament_toolchange_delay", "filament_cooling_moves",
+ "filament_cooling_initial_speed", "filament_cooling_final_speed", "filament_ramming_parameters", "filament_minimal_purge_on_wipe_tower",
+ "temperature", "first_layer_temperature", "bed_temperature", "first_layer_bed_temperature", "fan_always_on", "cooling", "min_fan_speed",
+ "max_fan_speed", "bridge_fan_speed", "disable_fan_first_layers", "fan_below_layer_time", "slowdown_below_layer_time", "min_print_speed",
+ "start_filament_gcode", "end_filament_gcode","compatible_printers", "compatible_printers_condition", "inherits"
+ };
+ return s_opts;
+}
+
+const std::vector<std::string>& Preset::printer_options()
+{
+ static std::vector<std::string> s_opts;
+ if (s_opts.empty()) {
+ s_opts = {
+ "printer_technology",
+ "bed_shape", "z_offset", "gcode_flavor", "use_relative_e_distances", "serial_port", "serial_speed",
+ "use_firmware_retraction", "use_volumetric_e", "variable_layer_height",
+ "host_type", "print_host", "printhost_apikey", "printhost_cafile",
+ "single_extruder_multi_material", "start_gcode", "end_gcode", "before_layer_gcode", "layer_gcode", "toolchange_gcode",
+ "between_objects_gcode", "printer_vendor", "printer_model", "printer_variant", "printer_notes", "cooling_tube_retraction",
+ "cooling_tube_length", "parking_pos_retraction", "extra_loading_move", "max_print_height", "default_print_profile", "inherits",
+ "remaining_times", "silent_mode", "machine_max_acceleration_extruding", "machine_max_acceleration_retracting",
+ "machine_max_acceleration_x", "machine_max_acceleration_y", "machine_max_acceleration_z", "machine_max_acceleration_e",
+ "machine_max_feedrate_x", "machine_max_feedrate_y", "machine_max_feedrate_z", "machine_max_feedrate_e",
+ "machine_min_extruding_rate", "machine_min_travel_rate",
+ "machine_max_jerk_x", "machine_max_jerk_y", "machine_max_jerk_z", "machine_max_jerk_e"
+ };
+ s_opts.insert(s_opts.end(), Preset::nozzle_options().begin(), Preset::nozzle_options().end());
+ }
+ return s_opts;
+}
+
+// The following nozzle options of a printer profile will be adjusted to match the size
+// of the nozzle_diameter vector.
+const std::vector<std::string>& Preset::nozzle_options()
+{
+ // ConfigOptionFloats, ConfigOptionPercents, ConfigOptionBools, ConfigOptionStrings
+ static std::vector<std::string> s_opts {
+ "nozzle_diameter", "min_layer_height", "max_layer_height", "extruder_offset",
+ "retract_length", "retract_lift", "retract_lift_above", "retract_lift_below", "retract_speed", "deretract_speed",
+ "retract_before_wipe", "retract_restart_extra", "retract_before_travel", "wipe",
+ "retract_layer_change", "retract_length_toolchange", "retract_restart_extra_toolchange", "extruder_colour",
+ "default_filament_profile"
+ };
+ return s_opts;
+}
+
+const std::vector<std::string>& Preset::sla_printer_options()
+{
+ static std::vector<std::string> s_opts;
+ if (s_opts.empty()) {
+ s_opts = {
+ "printer_technology",
+ "bed_shape", "max_print_height",
+ "display_width", "display_height", "display_pixels_x", "display_pixels_y",
+ "printer_correction",
+ "printer_notes",
+ "inherits"
+ };
+ }
+ return s_opts;
+}
+
+const std::vector<std::string>& Preset::sla_material_options()
+{
+ static std::vector<std::string> s_opts;
+ if (s_opts.empty()) {
+ s_opts = {
+ "layer_height", "initial_layer_height",
+ "exposure_time", "initial_exposure_time",
+ "material_correction_printing", "material_correction_curing",
+ "material_notes",
+ "compatible_printers",
+ "compatible_printers_condition", "inherits"
+ };
+ }
+ return s_opts;
+}
+
+PresetCollection::PresetCollection(Preset::Type type, const std::vector<std::string> &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &default_name) :
+ m_type(type),
+ m_edited_preset(type, "", false),
+ m_idx_selected(0),
+ m_bitmap_main_frame(new wxBitmap),
+ m_bitmap_cache(new GUI::BitmapCache)
+{
+ // Insert just the default preset.
+ this->add_default_preset(keys, defaults, default_name);
+ m_edited_preset.config.apply(m_presets.front().config);
+}
+
+PresetCollection::~PresetCollection()
+{
+ delete m_bitmap_main_frame;
+ m_bitmap_main_frame = nullptr;
+ delete m_bitmap_cache;
+ m_bitmap_cache = nullptr;
+}
+
+void PresetCollection::reset(bool delete_files)
+{
+ if (m_presets.size() > m_num_default_presets) {
+ if (delete_files) {
+ // Erase the preset files.
+ for (Preset &preset : m_presets)
+ if (! preset.is_default && ! preset.is_external && ! preset.is_system)
+ boost::nowide::remove(preset.file.c_str());
+ }
+ // Don't use m_presets.resize() here as it requires a default constructor for Preset.
+ m_presets.erase(m_presets.begin() + m_num_default_presets, m_presets.end());
+ this->select_preset(0);
+ }
+}
+
+void PresetCollection::add_default_preset(const std::vector<std::string> &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name)
+{
+ // Insert just the default preset.
+ m_presets.emplace_back(Preset(this->type(), preset_name, true));
+ m_presets.back().load(keys, defaults);
+ ++ m_num_default_presets;
+}
+
+// Load all presets found in dir_path.
+// Throws an exception on error.
+void PresetCollection::load_presets(const std::string &dir_path, const std::string &subdir)
+{
+ boost::filesystem::path dir = boost::filesystem::canonical(boost::filesystem::path(dir_path) / subdir).make_preferred();
+ m_dir_path = dir.string();
+ t_config_option_keys keys = this->default_preset().config.keys();
+ std::string errors_cummulative;
+ 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();
+ // Remove the .ini suffix.
+ name.erase(name.size() - 4);
+ if (this->find_preset(name, false)) {
+ // This happens when there's is a preset (most likely legacy one) with the same name as a system preset
+ // that's already been loaded from a bundle.
+ BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << name;
+ continue;
+ }
+ try {
+ Preset preset(m_type, name, false);
+ preset.file = dir_entry.path().string();
+ //FIXME One should initialize with SLAFullPrintConfig for the SLA profiles!
+ preset.load(keys, static_cast<const HostConfig&>(FullPrintConfig::defaults()));
+ m_presets.emplace_back(preset);
+ } catch (const std::runtime_error &err) {
+ errors_cummulative += err.what();
+ errors_cummulative += "\n";
+ }
+ }
+ std::sort(m_presets.begin() + m_num_default_presets, m_presets.end());
+ this->select_preset(first_visible_idx());
+ if (! errors_cummulative.empty())
+ throw std::runtime_error(errors_cummulative);
+}
+
+// Load a preset from an already parsed config file, insert it into the sorted sequence of presets
+// and select it, losing previous modifications.
+Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, const DynamicPrintConfig &config, bool select)
+{
+ DynamicPrintConfig cfg(this->default_preset().config);
+ cfg.apply_only(config, cfg.keys(), true);
+ return this->load_preset(path, name, std::move(cfg), select);
+}
+
+static bool profile_print_params_same(const DynamicPrintConfig &cfg1, const DynamicPrintConfig &cfg2)
+{
+ t_config_option_keys diff = cfg1.diff(cfg2);
+ // Following keys are used by the UI, not by the slicing core, therefore they are not important
+ // when comparing profiles for equality. Ignore them.
+ for (const char *key : { "compatible_printers", "compatible_printers_condition", "inherits",
+ "print_settings_id", "filament_settings_id", "printer_settings_id",
+ "printer_model", "printer_variant", "default_print_profile", "default_filament_profile" })
+ diff.erase(std::remove(diff.begin(), diff.end(), key), diff.end());
+ // Preset with the same name as stored inside the config exists.
+ return diff.empty();
+}
+
+// Load a preset from an already parsed config file, insert it into the sorted sequence of presets
+// and select it, losing previous modifications.
+// In case
+Preset& PresetCollection::load_external_preset(
+ // Path to the profile source file (a G-code, an AMF or 3MF file, a config file)
+ const std::string &path,
+ // Name of the profile, derived from the source file name.
+ const std::string &name,
+ // Original name of the profile, extracted from the loaded config. Empty, if the name has not been stored.
+ const std::string &original_name,
+ // Config to initialize the preset from.
+ const DynamicPrintConfig &config,
+ // Select the preset after loading?
+ bool select)
+{
+ // Load the preset over a default preset, so that the missing fields are filled in from the default preset.
+ DynamicPrintConfig cfg(this->default_preset().config);
+ cfg.apply_only(config, cfg.keys(), true);
+ // Is there a preset already loaded with the name stored inside the config?
+ std::deque<Preset>::iterator it = this->find_preset_internal(original_name);
+ if (it != m_presets.end() && it->name == original_name && profile_print_params_same(it->config, cfg)) {
+ // The preset exists and it matches the values stored inside config.
+ if (select)
+ this->select_preset(it - m_presets.begin());
+ return *it;
+ }
+ // Update the "inherits" field.
+ std::string &inherits = Preset::inherits(cfg);
+ if (it != m_presets.end() && inherits.empty()) {
+ // There is a profile with the same name already loaded. Should we update the "inherits" field?
+ if (it->vendor == nullptr)
+ inherits = it->inherits();
+ else
+ inherits = it->name;
+ }
+ // The external preset does not match an internal preset, load the external preset.
+ std::string new_name;
+ for (size_t idx = 0;; ++ idx) {
+ std::string suffix;
+ if (original_name.empty()) {
+ if (idx > 0)
+ suffix = " (" + std::to_string(idx) + ")";
+ } else {
+ if (idx == 0)
+ suffix = " (" + original_name + ")";
+ else
+ suffix = " (" + original_name + "-" + std::to_string(idx) + ")";
+ }
+ new_name = name + suffix;
+ it = this->find_preset_internal(new_name);
+ if (it == m_presets.end() || it->name != new_name)
+ // Unique profile name. Insert a new profile.
+ break;
+ if (profile_print_params_same(it->config, cfg)) {
+ // The preset exists and it matches the values stored inside config.
+ if (select)
+ this->select_preset(it - m_presets.begin());
+ return *it;
+ }
+ // Form another profile name.
+ }
+ // Insert a new profile.
+ Preset &preset = this->load_preset(path, new_name, std::move(cfg), select);
+ preset.is_external = true;
+ if (&this->get_selected_preset() == &preset)
+ this->get_edited_preset().is_external = true;
+
+ return preset;
+}
+
+Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select)
+{
+ auto it = this->find_preset_internal(name);
+ if (it == m_presets.end() || it->name != name) {
+ // The preset was not found. Create a new preset.
+ it = m_presets.emplace(it, Preset(m_type, name, false));
+ }
+ Preset &preset = *it;
+ preset.file = path;
+ preset.config = std::move(config);
+ preset.loaded = true;
+ preset.is_dirty = false;
+ if (select)
+ this->select_preset_by_name(name, true);
+ return preset;
+}
+
+void PresetCollection::save_current_preset(const std::string &new_name)
+{
+ // 1) Find the preset with a new_name or create a new one,
+ // initialize it with the edited config.
+ auto it = this->find_preset_internal(new_name);
+ if (it != m_presets.end() && it->name == new_name) {
+ // Preset with the same name found.
+ Preset &preset = *it;
+ if (preset.is_default || preset.is_external || preset.is_system)
+ // Cannot overwrite the default preset.
+ return;
+ // Overwriting an existing preset.
+ preset.config = std::move(m_edited_preset.config);
+ } else {
+ // Creating a new preset.
+ Preset &preset = *m_presets.insert(it, m_edited_preset);
+ std::string &inherits = preset.inherits();
+ std::string old_name = preset.name;
+ preset.name = new_name;
+ preset.file = this->path_from_name(new_name);
+ preset.vendor = nullptr;
+ if (preset.is_system) {
+ // Inheriting from a system preset.
+ inherits = /* preset.vendor->name + "/" + */ old_name;
+ } else if (inherits.empty()) {
+ // Inheriting from a user preset. Link the new preset to the old preset.
+ // inherits = old_name;
+ } else {
+ // Inherited from a user preset. Just maintain the "inherited" flag,
+ // meaning it will inherit from either the system preset, or the inherited user preset.
+ }
+ preset.is_default = false;
+ preset.is_system = false;
+ preset.is_external = false;
+ }
+ // 2) Activate the saved preset.
+ this->select_preset_by_name(new_name, true);
+ // 2) Store the active preset to disk.
+ this->get_selected_preset().save();
+}
+
+void PresetCollection::delete_current_preset()
+{
+ const Preset &selected = this->get_selected_preset();
+ if (selected.is_default)
+ return;
+ if (! selected.is_external && ! selected.is_system) {
+ // Erase the preset file.
+ boost::nowide::remove(selected.file.c_str());
+ }
+ // Remove the preset from the list.
+ m_presets.erase(m_presets.begin() + m_idx_selected);
+ // Find the next visible preset.
+ size_t new_selected_idx = m_idx_selected;
+ if (new_selected_idx < m_presets.size())
+ for (; new_selected_idx < m_presets.size() && ! m_presets[new_selected_idx].is_visible; ++ new_selected_idx) ;
+ if (new_selected_idx == m_presets.size())
+ for (--new_selected_idx; new_selected_idx > 0 && !m_presets[new_selected_idx].is_visible; --new_selected_idx);
+ this->select_preset(new_selected_idx);
+}
+
+bool PresetCollection::load_bitmap_default(const std::string &file_name)
+{
+ return m_bitmap_main_frame->LoadFile(wxString::FromUTF8(Slic3r::var(file_name).c_str()), wxBITMAP_TYPE_PNG);
+}
+
+const Preset* PresetCollection::get_selected_preset_parent() const
+{
+ const std::string &inherits = this->get_edited_preset().inherits();
+ if (inherits.empty())
+ return this->get_selected_preset().is_system ? &this->get_selected_preset() : nullptr;
+ const Preset* preset = this->find_preset(inherits, false);
+ return (preset == nullptr || preset->is_default || preset->is_external) ? nullptr : preset;
+}
+
+const Preset* PresetCollection::get_preset_parent(const Preset& child) const
+{
+ const std::string &inherits = child.inherits();
+ if (inherits.empty())
+// return this->get_selected_preset().is_system ? &this->get_selected_preset() : nullptr;
+ return nullptr;
+ const Preset* preset = this->find_preset(inherits, false);
+ return (preset == nullptr/* || preset->is_default */|| preset->is_external) ? nullptr : preset;
+}
+
+const std::string& PresetCollection::get_suffix_modified() {
+ return g_suffix_modified;
+}
+
+// Return a preset by its name. If the preset is active, a temporary copy is returned.
+// If a preset is not found by its name, null is returned.
+Preset* PresetCollection::find_preset(const std::string &name, bool first_visible_if_not_found)
+{
+ Preset key(m_type, name, false);
+ auto it = this->find_preset_internal(name);
+ // Ensure that a temporary copy is returned if the preset found is currently selected.
+ return (it != m_presets.end() && it->name == key.name) ? &this->preset(it - m_presets.begin()) :
+ first_visible_if_not_found ? &this->first_visible() : nullptr;
+}
+
+// Return index of the first visible preset. Certainly at least the '- default -' preset shall be visible.
+size_t PresetCollection::first_visible_idx() const
+{
+ size_t idx = m_default_suppressed ? m_num_default_presets : 0;
+ for (; idx < this->m_presets.size(); ++ idx)
+ if (m_presets[idx].is_visible)
+ break;
+ if (idx == m_presets.size())
+ idx = 0;
+ return idx;
+}
+
+void PresetCollection::set_default_suppressed(bool default_suppressed)
+{
+ if (m_default_suppressed != default_suppressed) {
+ m_default_suppressed = default_suppressed;
+ m_presets.front().is_visible = ! default_suppressed || (m_presets.size() > m_num_default_presets && m_idx_selected > 0);
+ }
+}
+
+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));
+ const ConfigOption *opt = active_printer.config.option("nozzle_diameter");
+ if (opt)
+ config.set_key_value("num_extruders", new ConfigOptionInt((int)static_cast<const ConfigOptionFloats*>(opt)->values.size()));
+ for (size_t idx_preset = m_num_default_presets; idx_preset < m_presets.size(); ++ idx_preset) {
+ bool selected = idx_preset == m_idx_selected;
+ 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 && unselect_if_incompatible)
+ m_idx_selected = (size_t)-1;
+ if (selected)
+ preset_selected.is_compatible = preset_edited.is_compatible;
+ }
+ return m_idx_selected;
+}
+
+// Save the preset under a new name. If the name is different from the old one,
+// a new preset is stored into the list of presets.
+// All presets are marked as not modified and the new preset is activated.
+//void PresetCollection::save_current_preset(const std::string &new_name);
+
+// Delete the current preset, activate the first visible preset.
+//void PresetCollection::delete_current_preset();
+
+// Update the wxChoice UI component from this list of presets.
+// Hide the
+void PresetCollection::update_platter_ui(wxBitmapComboBox *ui)
+{
+ if (ui == nullptr)
+ return;
+ // Otherwise fill in the list from scratch.
+ ui->Freeze();
+ ui->Clear();
+ size_t selected_preset_item = 0;
+
+ const Preset &selected_preset = this->get_selected_preset();
+ // Show wide icons if the currently selected preset is not compatible with the current printer,
+ // and draw a red flag in front of the selected preset.
+ bool wide_icons = !selected_preset.is_compatible && m_bitmap_incompatible != nullptr;
+
+ std::map<wxString, wxBitmap*> nonsys_presets;
+ wxString selected = "";
+ if (!this->m_presets.front().is_visible)
+ ui->Append("------- " +_(L("System presets")) + " -------", wxNullBitmap);
+ for (size_t i = this->m_presets.front().is_visible ? 0 : m_num_default_presets; i < this->m_presets.size(); ++i) {
+ const Preset &preset = this->m_presets[i];
+ if (! preset.is_visible || (! preset.is_compatible && i != m_idx_selected))
+ continue;
+ std::string bitmap_key = "";
+ // If the filament preset is not compatible and there is a "red flag" icon loaded, show it left
+ // to the filament color image.
+ if (wide_icons)
+ bitmap_key += preset.is_compatible ? ",cmpt" : ",ncmpt";
+ bitmap_key += (preset.is_system || preset.is_default) ? ",syst" : ",nsyst";
+ wxBitmap *bmp = m_bitmap_cache->find(bitmap_key);
+ if (bmp == nullptr) {
+ // Create the bitmap with color bars.
+ std::vector<wxBitmap> bmps;
+ if (wide_icons)
+ // Paint a red flag for incompatible presets.
+ bmps.emplace_back(preset.is_compatible ? m_bitmap_cache->mkclear(16, 16) : *m_bitmap_incompatible);
+ // Paint the color bars.
+ bmps.emplace_back(m_bitmap_cache->mkclear(4, 16));
+ bmps.emplace_back(*m_bitmap_main_frame);
+ // Paint a lock at the system presets.
+ bmps.emplace_back(m_bitmap_cache->mkclear(6, 16));
+ bmps.emplace_back((preset.is_system || preset.is_default) ? *m_bitmap_lock : m_bitmap_cache->mkclear(16, 16));
+ bmp = m_bitmap_cache->insert(bitmap_key, bmps);
+ }
+
+ if (preset.is_default || preset.is_system){
+ ui->Append(wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()),
+ (bmp == 0) ? (m_bitmap_main_frame ? *m_bitmap_main_frame : wxNullBitmap) : *bmp);
+ if (i == m_idx_selected)
+ selected_preset_item = ui->GetCount() - 1;
+ }
+ else
+ {
+ nonsys_presets.emplace(wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), bmp/*preset.is_compatible*/);
+ if (i == m_idx_selected)
+ selected = wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str());
+ }
+ if (i + 1 == m_num_default_presets)
+ ui->Append("------- " + _(L("System presets")) + " -------", wxNullBitmap);
+ }
+ if (!nonsys_presets.empty())
+ {
+ ui->Append("------- " + _(L("User presets")) + " -------", wxNullBitmap);
+ for (std::map<wxString, wxBitmap*>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) {
+ ui->Append(it->first, *it->second);
+ if (it->first == selected)
+ selected_preset_item = ui->GetCount() - 1;
+ }
+ }
+
+ ui->SetSelection(selected_preset_item);
+ ui->SetToolTip(ui->GetString(selected_preset_item));
+ ui->Thaw();
+}
+
+size_t PresetCollection::update_tab_ui(wxBitmapComboBox *ui, bool show_incompatible)
+{
+ if (ui == nullptr)
+ return 0;
+ ui->Freeze();
+ ui->Clear();
+ size_t selected_preset_item = 0;
+
+ std::map<wxString, wxBitmap*> nonsys_presets;
+ wxString selected = "";
+ if (!this->m_presets.front().is_visible)
+ ui->Append("------- " + _(L("System presets")) + " -------", wxNullBitmap);
+ for (size_t i = this->m_presets.front().is_visible ? 0 : m_num_default_presets; i < this->m_presets.size(); ++i) {
+ const Preset &preset = this->m_presets[i];
+ if (! preset.is_visible || (! show_incompatible && ! preset.is_compatible && i != m_idx_selected))
+ continue;
+ std::string bitmap_key = "tab";
+ bitmap_key += preset.is_compatible ? ",cmpt" : ",ncmpt";
+ bitmap_key += (preset.is_system || preset.is_default) ? ",syst" : ",nsyst";
+ wxBitmap *bmp = m_bitmap_cache->find(bitmap_key);
+ if (bmp == nullptr) {
+ // Create the bitmap with color bars.
+ std::vector<wxBitmap> bmps;
+ const wxBitmap* tmp_bmp = preset.is_compatible ? m_bitmap_compatible : m_bitmap_incompatible;
+ bmps.emplace_back((tmp_bmp == 0) ? (m_bitmap_main_frame ? *m_bitmap_main_frame : wxNullBitmap) : *tmp_bmp);
+ // Paint a lock at the system presets.
+ bmps.emplace_back((preset.is_system || preset.is_default) ? *m_bitmap_lock : m_bitmap_cache->mkclear(16, 16));
+ bmp = m_bitmap_cache->insert(bitmap_key, bmps);
+ }
+
+ if (preset.is_default || preset.is_system){
+ ui->Append(wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()),
+ (bmp == 0) ? (m_bitmap_main_frame ? *m_bitmap_main_frame : wxNullBitmap) : *bmp);
+ if (i == m_idx_selected)
+ selected_preset_item = ui->GetCount() - 1;
+ }
+ else
+ {
+ nonsys_presets.emplace(wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), bmp/*preset.is_compatible*/);
+ if (i == m_idx_selected)
+ selected = wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str());
+ }
+ if (i + 1 == m_num_default_presets)
+ ui->Append("------- " + _(L("System presets")) + " -------", wxNullBitmap);
+ }
+ if (!nonsys_presets.empty())
+ {
+ ui->Append("------- " + _(L("User presets")) + " -------", wxNullBitmap);
+ for (std::map<wxString, wxBitmap*>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) {
+ ui->Append(it->first, *it->second);
+ if (it->first == selected)
+ selected_preset_item = ui->GetCount() - 1;
+ }
+ }
+ ui->SetSelection(selected_preset_item);
+ ui->SetToolTip(ui->GetString(selected_preset_item));
+ ui->Thaw();
+ return selected_preset_item;
+}
+
+// Update a dirty floag of the current preset, update the labels of the UI component accordingly.
+// Return true if the dirty flag changed.
+bool PresetCollection::update_dirty_ui(wxBitmapComboBox *ui)
+{
+ wxWindowUpdateLocker noUpdates(ui);
+ // 1) Update the dirty flag of the current preset.
+ bool was_dirty = this->get_selected_preset().is_dirty;
+ bool is_dirty = current_is_dirty();
+ this->get_selected_preset().is_dirty = is_dirty;
+ this->get_edited_preset().is_dirty = is_dirty;
+ // 2) Update the labels.
+ for (unsigned int ui_id = 0; ui_id < ui->GetCount(); ++ ui_id) {
+ std::string old_label = ui->GetString(ui_id).utf8_str().data();
+ std::string preset_name = Preset::remove_suffix_modified(old_label);
+ const Preset *preset = this->find_preset(preset_name, false);
+ assert(preset != nullptr);
+ if (preset != nullptr) {
+ std::string new_label = preset->is_dirty ? preset->name + g_suffix_modified : preset->name;
+ if (old_label != new_label)
+ ui->SetString(ui_id, wxString::FromUTF8(new_label.c_str()));
+ }
+ }
+#ifdef __APPLE__
+ // wxWidgets on OSX do not upload the text of the combo box line automatically.
+ // Force it to update by re-selecting.
+ ui->SetSelection(ui->GetSelection());
+#endif /* __APPLE __ */
+ return was_dirty != is_dirty;
+}
+
+std::vector<std::string> PresetCollection::dirty_options(const Preset *edited, const Preset *reference, const bool deep_compare /*= false*/)
+{
+ std::vector<std::string> changed;
+ if (edited != nullptr && reference != nullptr) {
+ changed = deep_compare ?
+ reference->config.deep_diff(edited->config) :
+ reference->config.diff(edited->config);
+ // The "compatible_printers" option key is handled differently from the others:
+ // It is not mandatory. If the key is missing, it means it is compatible with any printer.
+ // If the key exists and it is empty, it means it is compatible with no printer.
+ std::initializer_list<const char*> optional_keys { "compatible_printers" };
+ for (auto &opt_key : optional_keys) {
+ if (reference->config.has(opt_key) != edited->config.has(opt_key))
+ changed.emplace_back(opt_key);
+ }
+ }
+ return changed;
+}
+
+// Select a new preset. This resets all the edits done to the currently selected preset.
+// If the preset with index idx does not exist, a first visible preset is selected.
+Preset& PresetCollection::select_preset(size_t idx)
+{
+ for (Preset &preset : m_presets)
+ preset.is_dirty = false;
+ if (idx >= m_presets.size())
+ idx = first_visible_idx();
+ m_idx_selected = idx;
+ m_edited_preset = m_presets[idx];
+ m_presets.front().is_visible = ! m_default_suppressed || m_idx_selected == 0;
+ return m_presets[idx];
+}
+
+bool PresetCollection::select_preset_by_name(const std::string &name_w_suffix, bool force)
+{
+ std::string name = Preset::remove_suffix_modified(name_w_suffix);
+ // 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 && it->is_visible)
+ // Preset found by its name and it is visible.
+ idx = it - m_presets.begin();
+ else {
+ // Find the first visible preset.
+ for (size_t i = m_default_suppressed ? m_num_default_presets : 0; i < m_presets.size(); ++ i)
+ if (m_presets[i].is_visible) {
+ idx = i;
+ break;
+ }
+ // If the first visible preset was not found, return the 0th element, which is the default preset.
+ }
+
+ // 2) Select the new preset.
+ if (m_idx_selected != idx || force) {
+ this->select_preset(idx);
+ return true;
+ }
+
+ 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;
+}
+
+// Merge one vendor's presets with the other vendor's presets, report duplicates.
+std::vector<std::string> PresetCollection::merge_presets(PresetCollection &&other, const std::set<VendorProfile> &new_vendors)
+{
+ std::vector<std::string> 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() + m_num_default_presets, 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()) {
+ case Preset::TYPE_PRINT: return "print";
+ case Preset::TYPE_FILAMENT: return "filament";
+ case Preset::TYPE_PRINTER: return "printer";
+ default: return "invalid";
+ }
+}
+
+// Generate a file path from a profile name. Add the ".ini" suffix if it is missing.
+std::string PresetCollection::path_from_name(const std::string &new_name) const
+{
+ std::string file_name = boost::iends_with(new_name, ".ini") ? new_name : (new_name + ".ini");
+ return (boost::filesystem::path(m_dir_path) / file_name).make_preferred().string();
+}
+
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/Preset.hpp b/src/slic3r/GUI/Preset.hpp
new file mode 100644
index 000000000..821d7dc54
--- /dev/null
+++ b/src/slic3r/GUI/Preset.hpp
@@ -0,0 +1,444 @@
+#ifndef slic3r_Preset_hpp_
+#define slic3r_Preset_hpp_
+
+#include <deque>
+
+#include <boost/filesystem/path.hpp>
+#include <boost/property_tree/ptree_fwd.hpp>
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/PrintConfig.hpp"
+#include "slic3r/Utils/Semver.hpp"
+
+class wxBitmap;
+class wxChoice;
+class wxBitmapComboBox;
+class wxItemContainer;
+
+namespace Slic3r {
+
+class AppConfig;
+class PresetBundle;
+
+namespace GUI {
+ class BitmapCache;
+}
+
+enum ConfigFileType
+{
+ CONFIG_FILE_TYPE_UNKNOWN,
+ CONFIG_FILE_TYPE_APP_CONFIG,
+ CONFIG_FILE_TYPE_CONFIG,
+ CONFIG_FILE_TYPE_CONFIG_BUNDLE,
+};
+
+extern ConfigFileType guess_config_file_type(const boost::property_tree::ptree &tree);
+
+class VendorProfile
+{
+public:
+ std::string name;
+ std::string id;
+ Semver config_version;
+ std::string config_update_url;
+
+ struct PrinterVariant {
+ PrinterVariant() {}
+ PrinterVariant(const std::string &name) : name(name) {}
+ std::string name;
+ };
+
+ struct PrinterModel {
+ PrinterModel() {}
+ std::string id;
+ std::string name;
+ PrinterTechnology technology;
+ std::vector<PrinterVariant> variants;
+ PrinterVariant* variant(const std::string &name) {
+ for (auto &v : this->variants)
+ if (v.name == name)
+ return &v;
+ return nullptr;
+ }
+ const PrinterVariant* variant(const std::string &name) const { return const_cast<PrinterModel*>(this)->variant(name); }
+ };
+ std::vector<PrinterModel> models;
+
+ 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);
+
+ size_t num_variants() const { size_t n = 0; for (auto &model : models) n += model.variants.size(); return n; }
+
+ bool operator< (const VendorProfile &rhs) const { return this->id < rhs.id; }
+ bool operator==(const VendorProfile &rhs) const { return this->id == rhs.id; }
+};
+
+class Preset
+{
+public:
+ enum Type
+ {
+ TYPE_INVALID,
+ TYPE_PRINT,
+ TYPE_FILAMENT,
+ TYPE_SLA_MATERIAL,
+ TYPE_PRINTER,
+ };
+
+ Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
+
+ Type type = TYPE_INVALID;
+
+ // The preset represents a "default" set of properties,
+ // pulled from the default values of the PrintConfig (see PrintConfigDef for their definitions).
+ bool is_default;
+ // External preset points to a configuration, which has been loaded but not imported
+ // into the Slic3r default configuration location.
+ bool is_external = false;
+ // System preset is read-only.
+ bool is_system = false;
+ // 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?
+ bool is_dirty = false;
+ // Is this preset compatible with the currently active printer?
+ bool is_compatible = true;
+
+ // Name of the preset, usually derived form the file name.
+ std::string name;
+ // File name of the preset. This could be a Print / Filament / Printer preset,
+ // or a Configuration file bundling the Print + Filament + Printer presets (in that case is_external and possibly is_system will be true),
+ // or it could be a G-code (again, is_external will be true).
+ std::string file;
+ // If this is a system profile, then there should be a vendor data available to display at the UI.
+ const VendorProfile *vendor = nullptr;
+
+ // Has this profile been loaded?
+ bool loaded = false;
+
+ // Configuration data, loaded from a file, or set from the defaults.
+ DynamicPrintConfig config;
+
+ // Load this profile for the following keys only.
+ DynamicPrintConfig& load(const std::vector<std::string> &keys, const StaticPrintConfig &defaults);
+
+ void save();
+
+ // Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty.
+ std::string label() const;
+
+ // Set the is_dirty flag if the provided config is different from the active one.
+ void set_dirty(const DynamicPrintConfig &config) { this->is_dirty = ! this->config.diff(config).empty(); }
+ void set_dirty(bool dirty = true) { this->is_dirty = dirty; }
+ void reset_dirty() { this->is_dirty = false; }
+
+ bool is_compatible_with_printer(const Preset &active_printer, const DynamicPrintConfig *extra_config) const;
+ bool is_compatible_with_printer(const Preset &active_printer) const;
+
+ // Returns the name of the preset, from which this preset inherits.
+ static std::string& inherits(DynamicPrintConfig &cfg) { return cfg.option<ConfigOptionString>("inherits", true)->value; }
+ std::string& inherits() { return Preset::inherits(this->config); }
+ const std::string& inherits() const { return Preset::inherits(const_cast<Preset*>(this)->config); }
+
+ // Returns the "compatible_printers_condition".
+ static std::string& compatible_printers_condition(DynamicPrintConfig &cfg) { return cfg.option<ConfigOptionString>("compatible_printers_condition", true)->value; }
+ std::string& compatible_printers_condition() { return Preset::compatible_printers_condition(this->config); }
+ const std::string& compatible_printers_condition() const { return Preset::compatible_printers_condition(const_cast<Preset*>(this)->config); }
+
+ static PrinterTechnology& printer_technology(DynamicPrintConfig &cfg) { return cfg.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology", true)->value; }
+ PrinterTechnology& printer_technology() { return Preset::printer_technology(this->config); }
+ const PrinterTechnology& printer_technology() const { return Preset::printer_technology(const_cast<Preset*>(this)->config); }
+
+ // 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); }
+
+ // Sort lexicographically by a preset name. The preset name shall be unique across a single PresetCollection.
+ bool operator<(const Preset &other) const { return this->name < other.name; }
+
+ static const std::vector<std::string>& print_options();
+ static const std::vector<std::string>& filament_options();
+ // Printer options contain the nozzle options.
+ static const std::vector<std::string>& printer_options();
+ // Nozzle options of the printer options.
+ static const std::vector<std::string>& nozzle_options();
+
+ static const std::vector<std::string>& sla_printer_options();
+ static const std::vector<std::string>& sla_material_options();
+
+ static void update_suffix_modified();
+
+protected:
+ friend class PresetCollection;
+ friend class PresetBundle;
+ static void normalize(DynamicPrintConfig &config);
+ // Resize the extruder specific vectors ()
+ static void set_num_extruders(DynamicPrintConfig &config, unsigned int n);
+ static const std::string& suffix_modified();
+ static std::string remove_suffix_modified(const std::string &name);
+};
+
+// Collections of presets of the same type (one of the Print, Filament or Printer type).
+class PresetCollection
+{
+public:
+ // Initialize the PresetCollection with the "- default -" preset.
+ PresetCollection(Preset::Type type, const std::vector<std::string> &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &default_name = "- default -");
+ ~PresetCollection();
+
+ typedef std::deque<Preset>::iterator Iterator;
+ typedef std::deque<Preset>::const_iterator ConstIterator;
+ Iterator begin() { return m_presets.begin() + m_num_default_presets; }
+ ConstIterator begin() const { return m_presets.begin() + m_num_default_presets; }
+ 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; }
+ std::string name() const;
+ const std::deque<Preset>& operator()() const { return m_presets; }
+
+ // Add default preset at the start of the collection, increment the m_default_preset counter.
+ void add_default_preset(const std::vector<std::string> &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name);
+
+ // Load ini files of the particular type from the provided directory path.
+ void load_presets(const std::string &dir_path, const std::string &subdir);
+
+ // Load a preset from an already parsed config file, insert it into the sorted sequence of presets
+ // and select it, losing previous modifications.
+ Preset& load_preset(const std::string &path, const std::string &name, const DynamicPrintConfig &config, bool select = true);
+ Preset& load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select = true);
+
+ Preset& load_external_preset(
+ // Path to the profile source file (a G-code, an AMF or 3MF file, a config file)
+ const std::string &path,
+ // Name of the profile, derived from the source file name.
+ const std::string &name,
+ // Original name of the profile, extracted from the loaded config. Empty, if the name has not been stored.
+ const std::string &original_name,
+ // Config to initialize the preset from.
+ const DynamicPrintConfig &config,
+ // Select the preset after loading?
+ bool select = true);
+
+ // Save the preset under a new name. If the name is different from the old one,
+ // a new preset is stored into the list of presets.
+ // All presets are marked as not modified and the new preset is activated.
+ void save_current_preset(const std::string &new_name);
+
+ // Delete the current preset, activate the first visible preset.
+ void delete_current_preset();
+
+ // Load default bitmap to be placed at the wxBitmapComboBox of a MainFrame.
+ bool load_bitmap_default(const std::string &file_name);
+
+ // Compatible & incompatible marks, to be placed at the wxBitmapComboBox items.
+ void set_bitmap_compatible (const wxBitmap *bmp) { m_bitmap_compatible = bmp; }
+ void set_bitmap_incompatible(const wxBitmap *bmp) { m_bitmap_incompatible = bmp; }
+ void set_bitmap_lock (const wxBitmap *bmp) { m_bitmap_lock = bmp; }
+ void set_bitmap_lock_open (const wxBitmap *bmp) { m_bitmap_lock_open = bmp; }
+
+ // Enable / disable the "- default -" preset.
+ void set_default_suppressed(bool default_suppressed);
+ bool is_default_suppressed() const { return m_default_suppressed; }
+
+ // Select a preset. If an invalid index is provided, the first visible preset is selected.
+ Preset& select_preset(size_t idx);
+ // Return the selected preset, without the user modifications applied.
+ Preset& get_selected_preset() { return m_presets[m_idx_selected]; }
+ const Preset& get_selected_preset() const { return m_presets[m_idx_selected]; }
+ int get_selected_idx() const { return m_idx_selected; }
+ // Returns the name of the selected preset, or an empty string if no preset is selected.
+ std::string get_selected_preset_name() const { return (m_idx_selected == -1) ? std::string() : this->get_selected_preset().name; }
+ // For the current edited preset, return the parent preset if there is one.
+ // If there is no parent preset, nullptr is returned.
+ // The parent preset may be a system preset or a user preset, which will be
+ // reflected by the UI.
+ const Preset* get_selected_preset_parent() const;
+ // get parent preset for some child preset
+ const Preset* get_preset_parent(const Preset& child) const;
+ // Return the selected preset including the user modifications.
+ Preset& get_edited_preset() { return m_edited_preset; }
+ const Preset& get_edited_preset() const { return m_edited_preset; }
+
+ // used to update preset_choice from Tab
+ const std::deque<Preset>& get_presets() { return m_presets; }
+ int get_idx_selected() { return m_idx_selected; }
+ static const std::string& get_suffix_modified();
+
+ // Return a preset possibly with modifications.
+ Preset& default_preset() { return m_presets.front(); }
+ const Preset& default_preset() const { return m_presets.front(); }
+ // Return a preset by an index. If the preset is active, a temporary copy is returned.
+ Preset& preset(size_t idx) { return (int(idx) == m_idx_selected) ? m_edited_preset : m_presets[idx]; }
+ const Preset& preset(size_t idx) const { return const_cast<PresetCollection*>(this)->preset(idx); }
+ void discard_current_changes() { m_presets[m_idx_selected].reset_dirty(); m_edited_preset = m_presets[m_idx_selected]; }
+
+ // Return a preset by its name. If the preset is active, a temporary copy is returned.
+ // If a preset is not found by its name, null is returned.
+ Preset* find_preset(const std::string &name, bool first_visible_if_not_found = false);
+ const Preset* find_preset(const std::string &name, bool first_visible_if_not_found = false) const
+ { return const_cast<PresetCollection*>(this)->find_preset(name, first_visible_if_not_found); }
+
+ size_t first_visible_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<typename PreferedCondition>
+ size_t first_compatible_idx(PreferedCondition prefered_condition) const
+ {
+ size_t i = m_default_suppressed ? m_num_default_presets : 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<typename PreferedCondition>
+ 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 m_presets.size(); }
+ bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; }
+
+ // For Print / Filament presets, disable those, which are not compatible with the printer.
+ template<typename PreferedCondition>
+ 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;}); }
+
+ // Compare the content of get_selected_preset() with get_edited_preset() configs, return true if they differ.
+ bool current_is_dirty() const { return ! this->current_dirty_options().empty(); }
+ // Compare the content of get_selected_preset() with get_edited_preset() configs, return the list of keys where they differ.
+ std::vector<std::string> current_dirty_options(const bool deep_compare = false) const
+ { return dirty_options(&this->get_edited_preset(), &this->get_selected_preset(), deep_compare); }
+ // Compare the content of get_selected_preset() with get_edited_preset() configs, return the list of keys where they differ.
+ std::vector<std::string> current_different_from_parent_options(const bool deep_compare = false) const
+ { return dirty_options(&this->get_edited_preset(), this->get_selected_preset_parent(), deep_compare); }
+
+ // Update the choice UI from the list of presets.
+ // If show_incompatible, all presets are shown, otherwise only the compatible presets are shown.
+ // If an incompatible preset is selected, it is shown as well.
+ size_t update_tab_ui(wxBitmapComboBox *ui, bool show_incompatible);
+ // Update the choice UI from the list of presets.
+ // Only the compatible presets are shown.
+ // If an incompatible preset is selected, it is shown as well.
+ void update_platter_ui(wxBitmapComboBox *ui);
+
+ // Update a dirty floag of the current preset, update the labels of the UI component accordingly.
+ // Return true if the dirty flag changed.
+ bool update_dirty_ui(wxBitmapComboBox *ui);
+
+ // Select a profile by its name. Return true if the selection changed.
+ // Without force, the selection is only updated if the index changes.
+ // With force, the changes are reverted if the new index is the same as the old index.
+ bool select_preset_by_name(const std::string &name, bool force);
+
+ // 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);
+
+ // Merge one vendor's presets with the other vendor's presets, report duplicates.
+ std::vector<std::string> merge_presets(PresetCollection &&other, const std::set<VendorProfile> &new_vendors);
+
+private:
+ PresetCollection();
+ PresetCollection(const PresetCollection &other);
+ PresetCollection& operator=(const PresetCollection &other);
+
+ // Find a preset position in the sorted list of presets.
+ // The "-- default -- " preset is always the first, so it needs
+ // to be handled differently.
+ // If a preset does not exist, an iterator is returned indicating where to insert a preset with the same name.
+ std::deque<Preset>::iterator find_preset_internal(const std::string &name)
+ {
+ Preset key(m_type, name);
+ auto it = std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key);
+ if (it == m_presets.end() || it->name != name) {
+ // Preset has not been not found in the sorted list of non-default presets. Try the defaults.
+ for (size_t i = 0; i < m_num_default_presets; ++ i)
+ if (m_presets[i].name == name) {
+ it = m_presets.begin() + i;
+ break;
+ }
+ }
+ return it;
+ }
+ std::deque<Preset>::const_iterator find_preset_internal(const std::string &name) const
+ { return const_cast<PresetCollection*>(this)->find_preset_internal(name); }
+
+ size_t update_compatible_with_printer_internal(const Preset &active_printer, bool unselect_if_incompatible);
+
+ static std::vector<std::string> dirty_options(const Preset *edited, const Preset *reference, const bool is_printer_type = false);
+
+ // Type of this PresetCollection: TYPE_PRINT, TYPE_FILAMENT or TYPE_PRINTER.
+ Preset::Type m_type;
+ // List of presets, starting with the "- default -" preset.
+ // Use deque to force the container to allocate an object per each entry,
+ // so that the addresses of the presets don't change during resizing of the container.
+ std::deque<Preset> m_presets;
+ // Initially this preset contains a copy of the selected preset. Later on, this copy may be modified by the user.
+ Preset m_edited_preset;
+ // Selected preset.
+ int m_idx_selected;
+ // Is the "- default -" preset suppressed?
+ bool m_default_suppressed = true;
+ size_t m_num_default_presets = 0;
+ // Compatible & incompatible marks, to be placed at the wxBitmapComboBox items of a Platter.
+ // These bitmaps are not owned by PresetCollection, but by a PresetBundle.
+ const wxBitmap *m_bitmap_compatible = nullptr;
+ const wxBitmap *m_bitmap_incompatible = nullptr;
+ const wxBitmap *m_bitmap_lock = nullptr;
+ const wxBitmap *m_bitmap_lock_open = nullptr;
+ // Marks placed at the wxBitmapComboBox of a MainFrame.
+ // These bitmaps are owned by PresetCollection.
+ wxBitmap *m_bitmap_main_frame;
+ // Path to the directory to store the config files into.
+ std::string m_dir_path;
+
+ // Caching color bitmaps for the filament combo box.
+ GUI::BitmapCache *m_bitmap_cache = nullptr;
+
+ // to access select_preset_by_name_strict()
+ friend class PresetBundle;
+};
+
+} // namespace Slic3r
+
+#endif /* slic3r_Preset_hpp_ */
diff --git a/src/slic3r/GUI/PresetBundle.cpp b/src/slic3r/GUI/PresetBundle.cpp
new file mode 100644
index 000000000..cd3924dd0
--- /dev/null
+++ b/src/slic3r/GUI/PresetBundle.cpp
@@ -0,0 +1,1401 @@
+//#undef NDEBUG
+#include <cassert>
+
+#include "PresetBundle.hpp"
+#include "BitmapCache.hpp"
+
+#include <algorithm>
+#include <fstream>
+#include <boost/filesystem.hpp>
+#include <boost/algorithm/clamp.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+
+#include <boost/nowide/cenv.hpp>
+#include <boost/nowide/cstdio.hpp>
+#include <boost/nowide/fstream.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/locale.hpp>
+#include <boost/log/trivial.hpp>
+
+#include <wx/dcmemory.h>
+#include <wx/image.h>
+#include <wx/choice.h>
+#include <wx/bmpcbox.h>
+#include <wx/wupdlock.h>
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/PlaceholderParser.hpp"
+#include "../../libslic3r/Utils.hpp"
+
+// Store the print/filament/printer presets into a "presets" subdirectory of the Slic3rPE config dir.
+// This breaks compatibility with the upstream Slic3r if the --datadir is used to switch between the two versions.
+// #define SLIC3R_PROFILE_USE_PRESETS_SUBDIR
+
+namespace Slic3r {
+
+static std::vector<std::string> s_project_options {
+ "wiping_volumes_extruders",
+ "wiping_volumes_matrix"
+};
+
+PresetBundle::PresetBundle() :
+ prints(Preset::TYPE_PRINT, Preset::print_options(), static_cast<const HostConfig&>(FullPrintConfig::defaults())),
+ filaments(Preset::TYPE_FILAMENT, Preset::filament_options(), static_cast<const HostConfig&>(FullPrintConfig::defaults())),
+ sla_materials(Preset::TYPE_SLA_MATERIAL, Preset::sla_material_options(), static_cast<const SLAMaterialConfig&>(SLAFullPrintConfig::defaults())),
+ printers(Preset::TYPE_PRINTER, Preset::printer_options(), static_cast<const HostConfig&>(FullPrintConfig::defaults()), "- default FFF -"),
+ m_bitmapCompatible(new wxBitmap),
+ m_bitmapIncompatible(new wxBitmap),
+ m_bitmapLock(new wxBitmap),
+ m_bitmapLockOpen(new wxBitmap),
+ m_bitmapCache(new GUI::BitmapCache)
+{
+ if (wxImage::FindHandler(wxBITMAP_TYPE_PNG) == nullptr)
+ wxImage::AddHandler(new wxPNGHandler);
+
+ // The following keys are handled by the UI, they do not have a counterpart in any StaticPrintConfig derived classes,
+ // therefore they need to be handled differently. As they have no counterpart in StaticPrintConfig, they are not being
+ // initialized based on PrintConfigDef(), but to empty values (zeros, empty vectors, empty strings).
+ //
+ // "compatible_printers", "compatible_printers_condition", "inherits",
+ // "print_settings_id", "filament_settings_id", "printer_settings_id",
+ // "printer_vendor", "printer_model", "printer_variant", "default_print_profile", "default_filament_profile"
+
+ // Create the ID config keys, as they are not part of the Static print config classes.
+ this->prints.default_preset().config.optptr("print_settings_id", true);
+ this->prints.default_preset().compatible_printers_condition();
+ this->prints.default_preset().inherits();
+
+ this->filaments.default_preset().config.option<ConfigOptionStrings>("filament_settings_id", true)->values = { "" };
+ this->filaments.default_preset().compatible_printers_condition();
+ this->filaments.default_preset().inherits();
+
+ this->sla_materials.default_preset().config.optptr("sla_material_settings_id", true);
+ this->sla_materials.default_preset().compatible_printers_condition();
+ this->sla_materials.default_preset().inherits();
+
+ this->printers.add_default_preset(Preset::sla_printer_options(), static_cast<const SLAMaterialConfig&>(SLAFullPrintConfig::defaults()), "- default SLA -");
+ this->printers.preset(1).printer_technology() = ptSLA;
+ for (size_t i = 0; i < 2; ++ i) {
+ Preset &preset = this->printers.preset(i);
+ preset.config.optptr("printer_settings_id", true);
+ preset.config.optptr("printer_vendor", true);
+ preset.config.optptr("printer_model", true);
+ preset.config.optptr("printer_variant", true);
+ preset.config.optptr("default_print_profile", true);
+ preset.config.option<ConfigOptionStrings>("default_filament_profile", true)->values = { "" };
+ preset.inherits();
+ }
+
+ // Load the default preset bitmaps.
+ this->prints .load_bitmap_default("cog.png");
+ this->filaments .load_bitmap_default("spool.png");
+ this->sla_materials.load_bitmap_default("package_green.png");
+ this->printers .load_bitmap_default("printer_empty.png");
+ this->load_compatible_bitmaps();
+
+ // Re-activate the default presets, so their "edited" preset copies will be updated with the additional configuration values above.
+ this->prints .select_preset(0);
+ this->filaments .select_preset(0);
+ this->sla_materials.select_preset(0);
+ this->printers .select_preset(0);
+
+ this->project_config.apply_only(FullPrintConfig::defaults(), s_project_options);
+}
+
+PresetBundle::~PresetBundle()
+{
+ assert(m_bitmapCompatible != nullptr);
+ assert(m_bitmapIncompatible != nullptr);
+ assert(m_bitmapLock != nullptr);
+ assert(m_bitmapLockOpen != nullptr);
+ delete m_bitmapCompatible;
+ m_bitmapCompatible = nullptr;
+ delete m_bitmapIncompatible;
+ m_bitmapIncompatible = nullptr;
+ delete m_bitmapLock;
+ m_bitmapLock = nullptr;
+ delete m_bitmapLockOpen;
+ m_bitmapLockOpen = nullptr;
+ delete m_bitmapCache;
+ m_bitmapCache = nullptr;
+}
+
+void PresetBundle::reset(bool delete_files)
+{
+ // Clear the existing presets, delete their respective files.
+ this->vendors.clear();
+ this->prints .reset(delete_files);
+ this->filaments .reset(delete_files);
+ this->sla_materials.reset(delete_files);
+ this->printers .reset(delete_files);
+ this->filament_presets.clear();
+ this->filament_presets.emplace_back(this->filaments.get_selected_preset_name());
+ this->obsolete_presets.prints.clear();
+ this->obsolete_presets.filaments.clear();
+ this->obsolete_presets.sla_materials.clear();
+ this->obsolete_presets.printers.clear();
+}
+
+void PresetBundle::setup_directories()
+{
+ boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir());
+ std::initializer_list<boost::filesystem::path> 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",
+ data_dir / "presets" / "print",
+ data_dir / "presets" / "filament",
+ data_dir / "presets" / "sla_material",
+ data_dir / "presets" / "printer"
+#else
+ // Store the print/filament/printer presets at the same location as the upstream Slic3r.
+ data_dir / "print",
+ data_dir / "filament",
+ data_dir / "sla_material",
+ data_dir / "printer"
+#endif
+ };
+ for (const boost::filesystem::path &path : paths) {
+ boost::filesystem::path subdir = path;
+ subdir.make_preferred();
+ if (! boost::filesystem::is_directory(subdir) &&
+ ! boost::filesystem::create_directory(subdir))
+ throw std::runtime_error(std::string("Slic3r was unable to create its data directory at ") + subdir.string());
+ }
+}
+
+void PresetBundle::load_presets(const AppConfig &config)
+{
+ // First load the vendor specific system presets.
+ std::string errors_cummulative = this->load_system_presets();
+
+ const std::string dir_user_presets = data_dir()
+#ifdef SLIC3R_PROFILE_USE_PRESETS_SUBDIR
+ // Store the print/filament/printer presets into a "presets" directory.
+ + "/presets"
+#else
+ // Store the print/filament/printer presets at the same location as the upstream Slic3r.
+#endif
+ ;
+ try {
+ this->prints.load_presets(dir_user_presets, "print");
+ } catch (const std::runtime_error &err) {
+ errors_cummulative += err.what();
+ }
+ try {
+ this->filaments.load_presets(dir_user_presets, "filament");
+ } catch (const std::runtime_error &err) {
+ errors_cummulative += err.what();
+ }
+ try {
+ this->sla_materials.load_presets(dir_user_presets, "sla_material");
+ } catch (const std::runtime_error &err) {
+ errors_cummulative += err.what();
+ }
+ try {
+ this->printers.load_presets(dir_user_presets, "printer");
+ } catch (const std::runtime_error &err) {
+ errors_cummulative += err.what();
+ }
+ this->update_multi_material_filament_presets();
+ this->update_compatible_with_printer(false);
+ if (! errors_cummulative.empty())
+ throw std::runtime_error(errors_cummulative);
+
+ this->load_selections(config);
+}
+
+// Load system presets into this PresetBundle.
+// For each vendor, there will be a single PresetBundle loaded.
+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();
+ // Remove the .ini suffix.
+ name.erase(name.size() - 4);
+ try {
+ // Load the config bundle, flatten it.
+ 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<std::string> 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";
+ }
+ }
+ if (first) {
+ // No config bundle loaded, reset.
+ this->reset(false);
+ }
+ return errors_cummulative;
+}
+
+// Merge one vendor's presets with the other vendor's presets, report duplicates.
+std::vector<std::string> PresetBundle::merge_presets(PresetBundle &&other)
+{
+ this->vendors.insert(other.vendors.begin(), other.vendors.end());
+ std::vector<std::string> duplicate_prints = this->prints .merge_presets(std::move(other.prints), this->vendors);
+ std::vector<std::string> duplicate_filaments = this->filaments .merge_presets(std::move(other.filaments), this->vendors);
+ std::vector<std::string> duplicate_sla_materials = this->sla_materials.merge_presets(std::move(other.sla_materials), this->vendors);
+ std::vector<std::string> duplicate_printers = this->printers .merge_presets(std::move(other.printers), this->vendors);
+ append(this->obsolete_presets.prints, std::move(other.obsolete_presets.prints));
+ append(this->obsolete_presets.filaments, std::move(other.obsolete_presets.filaments));
+ append(this->obsolete_presets.sla_materials, std::move(other.obsolete_presets.sla_materials));
+ append(this->obsolete_presets.printers, std::move(other.obsolete_presets.printers));
+ append(duplicate_prints, std::move(duplicate_filaments));
+ append(duplicate_prints, std::move(duplicate_sla_materials));
+ 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;
+ if (boost::iends_with(out, ".ini"))
+ out.erase(out.end() - 4, out.end());
+ return out;
+}
+
+// Set the "enabled" flag for printer vendors, printer models and printer variants
+// based on the user configuration.
+// If the "vendor" section is missing, enable all models and variants of the particular vendor.
+void PresetBundle::load_installed_printers(const AppConfig &config)
+{
+ for (auto &preset : printers) {
+ preset.set_visible_from_appconfig(config);
+ }
+}
+
+// Load selections (current print, current filaments, current printer) from config.ini
+// This is done on application start up or after updates are applied.
+void PresetBundle::load_selections(const AppConfig &config)
+{
+ // 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::string initial_filament_profile_name = remove_ini_suffix(config.get("presets", "filament"));
+ std::string initial_sla_material_profile_name = remove_ini_suffix(config.get("presets", "sla_material"));
+ std::string initial_printer_profile_name = remove_ini_suffix(config.get("presets", "printer"));
+
+ // 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_name);
+ sla_materials.select_preset_by_name_strict(initial_sla_material_profile_name);
+ printers.select_preset_by_name(initial_printer_profile_name, true);
+
+ if (printers.get_selected_preset().printer_technology() == ptFFF){
+ // Load the names of the other filament profiles selected for a multi-material printer.
+ auto *nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(printers.get_selected_preset().config.option("nozzle_diameter"));
+ size_t num_extruders = nozzle_diameter->values.size();
+ this->filament_presets = { initial_filament_profile_name };
+ 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->filament_presets.emplace_back(remove_ini_suffix(config.get("presets", name)));
+ }
+ // Do not define the missing filaments, so that the update_compatible_with_printer() will use the preferred filaments.
+ this->filament_presets.resize(num_extruders, "");
+ }
+
+ // 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
+ // exist.
+ this->update_compatible_with_printer(true);
+ this->update_multi_material_filament_presets();
+}
+
+// Export selections (current print, current filaments, current printer) into config.ini
+void PresetBundle::export_selections(AppConfig &config)
+{
+ assert(filament_presets.size() >= 1);
+ assert(filament_presets.size() > 1 || filaments.get_selected_preset_name() == filament_presets.front());
+ config.clear_section("presets");
+ config.set("presets", "print", prints.get_selected_preset_name());
+ config.set("presets", "filament", filament_presets.front());
+ for (int i = 1; i < filament_presets.size(); ++i) {
+ char name[64];
+ sprintf(name, "filament_%d", i);
+ config.set("presets", name, filament_presets[i]);
+ }
+ config.set("presets", "sla_material", sla_materials.get_selected_preset_name());
+ config.set("presets", "printer", printers.get_selected_preset_name());
+}
+
+void PresetBundle::export_selections(PlaceholderParser &pp)
+{
+ assert(filament_presets.size() >= 1);
+ assert(filament_presets.size() > 1 || filaments.get_selected_preset_name() == filament_presets.front());
+ switch (printers.get_edited_preset().printer_technology()) {
+ case ptFFF:
+ pp.set("print_preset", prints.get_selected_preset().name);
+ pp.set("filament_preset", filament_presets);
+ break;
+ case ptSLA:
+ pp.set("sla_material_preset", sla_materials.get_selected_preset().name);
+ break;
+ }
+ pp.set("printer_preset", printers.get_selected_preset().name);
+}
+
+bool PresetBundle::load_compatible_bitmaps()
+{
+ const std::string path_bitmap_compatible = "flag-green-icon.png";
+ const std::string path_bitmap_incompatible = "flag-red-icon.png";
+ const std::string path_bitmap_lock = "sys_lock.png";//"lock.png";
+ const std::string path_bitmap_lock_open = "sys_unlock.png";//"lock_open.png";
+ bool loaded_compatible = m_bitmapCompatible ->LoadFile(
+ wxString::FromUTF8(Slic3r::var(path_bitmap_compatible).c_str()), wxBITMAP_TYPE_PNG);
+ bool loaded_incompatible = m_bitmapIncompatible->LoadFile(
+ wxString::FromUTF8(Slic3r::var(path_bitmap_incompatible).c_str()), wxBITMAP_TYPE_PNG);
+ bool loaded_lock = m_bitmapLock->LoadFile(
+ wxString::FromUTF8(Slic3r::var(path_bitmap_lock).c_str()), wxBITMAP_TYPE_PNG);
+ bool loaded_lock_open = m_bitmapLockOpen->LoadFile(
+ wxString::FromUTF8(Slic3r::var(path_bitmap_lock_open).c_str()), wxBITMAP_TYPE_PNG);
+ if (loaded_compatible) {
+ prints .set_bitmap_compatible(m_bitmapCompatible);
+ filaments .set_bitmap_compatible(m_bitmapCompatible);
+ sla_materials.set_bitmap_compatible(m_bitmapCompatible);
+// printers .set_bitmap_compatible(m_bitmapCompatible);
+ }
+ if (loaded_incompatible) {
+ prints .set_bitmap_incompatible(m_bitmapIncompatible);
+ filaments .set_bitmap_incompatible(m_bitmapIncompatible);
+ sla_materials.set_bitmap_incompatible(m_bitmapIncompatible);
+// printers .set_bitmap_incompatible(m_bitmapIncompatible);
+ }
+ if (loaded_lock) {
+ prints .set_bitmap_lock(m_bitmapLock);
+ filaments .set_bitmap_lock(m_bitmapLock);
+ sla_materials.set_bitmap_lock(m_bitmapLock);
+ printers .set_bitmap_lock(m_bitmapLock);
+ }
+ if (loaded_lock_open) {
+ prints .set_bitmap_lock_open(m_bitmapLock);
+ filaments .set_bitmap_lock_open(m_bitmapLock);
+ sla_materials.set_bitmap_lock_open(m_bitmapLock);
+ printers .set_bitmap_lock_open(m_bitmapLock);
+ }
+ return loaded_compatible && loaded_incompatible && loaded_lock && loaded_lock_open;
+}
+
+DynamicPrintConfig PresetBundle::full_config() const
+{
+ return (this->printers.get_edited_preset().printer_technology() == ptFFF) ?
+ this->full_fff_config() :
+ this->full_sla_config();
+}
+
+DynamicPrintConfig PresetBundle::full_fff_config() const
+{
+ DynamicPrintConfig out;
+ out.apply(FullPrintConfig::defaults());
+ out.apply(this->prints.get_edited_preset().config);
+ // Add the default filament preset to have the "filament_preset_id" defined.
+ out.apply(this->filaments.default_preset().config);
+ out.apply(this->printers.get_edited_preset().config);
+ out.apply(this->project_config);
+
+ auto *nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(out.option("nozzle_diameter"));
+ size_t num_extruders = nozzle_diameter->values.size();
+ // Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector.
+ std::vector<std::string> compatible_printers_condition;
+ std::vector<std::string> inherits;
+ compatible_printers_condition.emplace_back(this->prints.get_edited_preset().compatible_printers_condition());
+ inherits .emplace_back(this->prints.get_edited_preset().inherits());
+
+ if (num_extruders <= 1) {
+ out.apply(this->filaments.get_edited_preset().config);
+ compatible_printers_condition.emplace_back(this->filaments.get_edited_preset().compatible_printers_condition());
+ inherits .emplace_back(this->filaments.get_edited_preset().inherits());
+ } else {
+ // Retrieve filament presets and build a single config object for them.
+ // First collect the filament configurations based on the user selection of this->filament_presets.
+ // Here this->filaments.find_preset() and this->filaments.first_visible() return the edited copy of the preset if active.
+ std::vector<const DynamicPrintConfig*> filament_configs;
+ for (const std::string &filament_preset_name : this->filament_presets)
+ filament_configs.emplace_back(&this->filaments.find_preset(filament_preset_name, true)->config);
+ while (filament_configs.size() < num_extruders)
+ filament_configs.emplace_back(&this->filaments.first_visible().config);
+ for (const DynamicPrintConfig *cfg : filament_configs) {
+ compatible_printers_condition.emplace_back(Preset::compatible_printers_condition(*const_cast<DynamicPrintConfig*>(cfg)));
+ inherits .emplace_back(Preset::inherits(*const_cast<DynamicPrintConfig*>(cfg)));
+ }
+ // Option values to set a ConfigOptionVector from.
+ std::vector<const ConfigOption*> filament_opts(num_extruders, nullptr);
+ // loop through options and apply them to the resulting config.
+ for (const t_config_option_key &key : this->filaments.default_preset().config.keys()) {
+ if (key == "compatible_printers")
+ continue;
+ // Get a destination option.
+ ConfigOption *opt_dst = out.option(key, false);
+ if (opt_dst->is_scalar()) {
+ // Get an option, do not create if it does not exist.
+ const ConfigOption *opt_src = filament_configs.front()->option(key);
+ if (opt_src != nullptr)
+ opt_dst->set(opt_src);
+ } else {
+ // Setting a vector value from all filament_configs.
+ for (size_t i = 0; i < filament_opts.size(); ++ i)
+ filament_opts[i] = filament_configs[i]->option(key);
+ static_cast<ConfigOptionVectorBase*>(opt_dst)->set(filament_opts);
+ }
+ }
+ }
+
+ // Don't store the "compatible_printers_condition" for the printer profile, there is none.
+ inherits.emplace_back(this->printers.get_edited_preset().inherits());
+
+ // These two value types clash between the print and filament profiles. They should be renamed.
+ out.erase("compatible_printers");
+ out.erase("compatible_printers_condition");
+ out.erase("inherits");
+
+ static const char *keys[] = { "perimeter", "infill", "solid_infill", "support_material", "support_material_interface" };
+ for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++ i) {
+ std::string key = std::string(keys[i]) + "_extruder";
+ auto *opt = dynamic_cast<ConfigOptionInt*>(out.option(key, false));
+ assert(opt != nullptr);
+ opt->value = boost::algorithm::clamp<int>(opt->value, 0, int(num_extruders));
+ }
+
+ out.option<ConfigOptionString >("print_settings_id", true)->value = this->prints.get_selected_preset().name;
+ out.option<ConfigOptionStrings>("filament_settings_id", true)->values = this->filament_presets;
+ out.option<ConfigOptionString >("printer_settings_id", true)->value = this->printers.get_selected_preset().name;
+
+ // Serialize the collected "compatible_printers_condition" and "inherits" fields.
+ // There will be 1 + num_exturders fields for "inherits" and 2 + num_extruders for "compatible_printers_condition" stored.
+ // The vector will not be stored if all fields are empty strings.
+ auto add_if_some_non_empty = [&out](std::vector<std::string> &&values, const std::string &key) {
+ bool nonempty = false;
+ for (const std::string &v : values)
+ if (! v.empty()) {
+ nonempty = true;
+ break;
+ }
+ if (nonempty)
+ out.set_key_value(key, new ConfigOptionStrings(std::move(values)));
+ };
+ add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_printers_condition_cummulative");
+ add_if_some_non_empty(std::move(inherits), "inherits_cummulative");
+ return out;
+}
+
+DynamicPrintConfig PresetBundle::full_sla_config() const
+{
+ DynamicPrintConfig out;
+ out.apply(SLAFullPrintConfig::defaults());
+ out.apply(this->sla_materials.get_edited_preset().config);
+ out.apply(this->printers.get_edited_preset().config);
+ // There are no project configuration values as of now, the project_config is reserved for FFF printers.
+// out.apply(this->project_config);
+
+ // Collect the "compatible_printers_condition" and "inherits" values over all presets (sla_materials, printers) into a single vector.
+ std::vector<std::string> compatible_printers_condition;
+ std::vector<std::string> inherits;
+ compatible_printers_condition.emplace_back(this->/*prints*/sla_materials.get_edited_preset().compatible_printers_condition());
+ inherits .emplace_back(this->/*prints*/sla_materials.get_edited_preset().inherits());
+ inherits .emplace_back(this->printers.get_edited_preset().inherits());
+
+ // These two value types clash between the print and filament profiles. They should be renamed.
+ out.erase("compatible_printers");
+ out.erase("compatible_printers_condition");
+ out.erase("inherits");
+
+ out.option<ConfigOptionString >("sla_material_settings_id", true)->value = this->sla_materials.get_selected_preset().name;
+ out.option<ConfigOptionString >("printer_settings_id", true)->value = this->printers.get_selected_preset().name;
+
+ // Serialize the collected "compatible_printers_condition" and "inherits" fields.
+ // There will be 1 + num_exturders fields for "inherits" and 2 + num_extruders for "compatible_printers_condition" stored.
+ // The vector will not be stored if all fields are empty strings.
+ auto add_if_some_non_empty = [&out](std::vector<std::string> &&values, const std::string &key) {
+ bool nonempty = false;
+ for (const std::string &v : values)
+ if (! v.empty()) {
+ nonempty = true;
+ break;
+ }
+ if (nonempty)
+ out.set_key_value(key, new ConfigOptionStrings(std::move(values)));
+ };
+ add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_printers_condition_cummulative");
+ add_if_some_non_empty(std::move(inherits), "inherits_cummulative");
+ return out;
+}
+
+// Load an external config file containing the print, filament and printer presets.
+// Instead of a config file, a G-code may be loaded containing the full set of parameters.
+// In the future the configuration will likely be read from an AMF file as well.
+// If the file is loaded successfully, its print / filament / printer profiles will be activated.
+void PresetBundle::load_config_file(const std::string &path)
+{
+ if (boost::iends_with(path, ".gcode") || boost::iends_with(path, ".g")) {
+ DynamicPrintConfig config;
+ config.apply(FullPrintConfig::defaults());
+ config.load_from_gcode_file(path);
+ Preset::normalize(config);
+ load_config_file_config(path, true, std::move(config));
+ return;
+ }
+
+ // 1) Try to load the config file into a boost property tree.
+ boost::property_tree::ptree tree;
+ try {
+ boost::nowide::ifstream ifs(path);
+ boost::property_tree::read_ini(ifs, tree);
+ } catch (const std::ifstream::failure &err) {
+ throw std::runtime_error(std::string("The config file cannot be loaded: ") + path + "\n\tReason: " + err.what());
+ } catch (const std::runtime_error &err) {
+ throw std::runtime_error(std::string("Failed loading the preset file: ") + path + "\n\tReason: " + err.what());
+ }
+
+ // 2) Continue based on the type of the configuration file.
+ ConfigFileType config_file_type = guess_config_file_type(tree);
+ switch (config_file_type) {
+ case CONFIG_FILE_TYPE_UNKNOWN:
+ throw std::runtime_error(std::string("Unknown configuration file type: ") + path);
+ case CONFIG_FILE_TYPE_APP_CONFIG:
+ throw std::runtime_error(std::string("Invalid configuration file: ") + path + ". This is an application config file.");
+ case CONFIG_FILE_TYPE_CONFIG:
+ {
+ // Initialize a config from full defaults.
+ DynamicPrintConfig config;
+ config.apply(FullPrintConfig::defaults());
+ config.load(tree);
+ Preset::normalize(config);
+ load_config_file_config(path, true, std::move(config));
+ break;
+ }
+ case CONFIG_FILE_TYPE_CONFIG_BUNDLE:
+ load_config_file_config_bundle(path, tree);
+ break;
+ }
+}
+
+void PresetBundle::load_config_string(const char* str, const char* source_filename)
+{
+ if (str != nullptr)
+ {
+ DynamicPrintConfig config;
+ config.apply(FullPrintConfig::defaults());
+ config.load_from_gcode_string(str);
+ Preset::normalize(config);
+ load_config_file_config((source_filename == nullptr) ? "" : source_filename, true, std::move(config));
+ }
+}
+
+// Load a config file from a boost property_tree. This is a private method called from load_config_file.
+void PresetBundle::load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config)
+{
+ PrinterTechnology printer_technology = Preset::printer_technology(config);
+
+ // The "compatible_printers" field should not have been exported into a config.ini or a G-code anyway,
+ // but some of the alpha versions of Slic3r did.
+ {
+ ConfigOption *opt_compatible = config.optptr("compatible_printers");
+ if (opt_compatible != nullptr) {
+ assert(opt_compatible->type() == coStrings);
+ if (opt_compatible->type() == coStrings)
+ static_cast<ConfigOptionStrings*>(opt_compatible)->values.clear();
+ }
+ }
+
+ size_t num_extruders = (printer_technology == ptFFF) ?
+ std::min(config.option<ConfigOptionFloats>("nozzle_diameter" )->values.size(),
+ config.option<ConfigOptionFloats>("filament_diameter")->values.size()) :
+ 0;
+ // Make a copy of the "compatible_printers_condition_cummulative" and "inherits_cummulative" vectors, which
+ // accumulate values over all presets (print, filaments, printers).
+ // These values will be distributed into their particular presets when loading.
+ std::vector<std::string> compatible_printers_condition_values = std::move(config.option<ConfigOptionStrings>("compatible_printers_condition_cummulative", true)->values);
+ std::vector<std::string> inherits_values = std::move(config.option<ConfigOptionStrings>("inherits_cummulative", true)->values);
+ std::string &compatible_printers_condition = Preset::compatible_printers_condition(config);
+ std::string &inherits = Preset::inherits(config);
+ compatible_printers_condition_values.resize(num_extruders + 2, std::string());
+ inherits_values.resize(num_extruders + 2, std::string());
+ // The "default_filament_profile" will be later extracted into the printer profile.
+ if (printer_technology == ptFFF)
+ config.option<ConfigOptionStrings>("default_filament_profile", true)->values.resize(num_extruders, std::string());
+
+ // 1) Create a name from the file name.
+ // Keep the suffix (.ini, .gcode, .amf, .3mf etc) to differentiate it from the normal profiles.
+ std::string name = is_external ? boost::filesystem::path(name_or_path).filename().string() : name_or_path;
+
+ // 2) If the loading succeeded, split and load the config into print / filament / printer settings.
+ // First load the print and printer presets.
+ for (size_t i_group = 0; i_group < 2; ++ i_group) {
+ PresetCollection &presets = (i_group == 0) ? ((printer_technology == ptFFF) ? this->prints : this->sla_materials) : this->printers;
+ // Split the "compatible_printers_condition" and "inherits" values one by one from a single vector to the print & printer profiles.
+ size_t idx = (i_group == 0) ? 0 : num_extruders + 1;
+ inherits = inherits_values[idx];
+ compatible_printers_condition = compatible_printers_condition_values[idx];
+ if (is_external)
+ presets.load_external_preset(name_or_path, name,
+ config.opt_string((i_group == 0) ? ((printer_technology == ptFFF) ? "print_settings_id" : "sla_material_id") : "printer_settings_id", true),
+ config);
+ else
+ presets.load_preset(presets.path_from_name(name), name, config).save();
+ }
+
+ if (Preset::printer_technology(config) == ptFFF) {
+ // 3) Now load the filaments. If there are multiple filament presets, split them and load them.
+ auto old_filament_profile_names = config.option<ConfigOptionStrings>("filament_settings_id", true);
+ old_filament_profile_names->values.resize(num_extruders, std::string());
+
+ if (num_extruders <= 1) {
+ // Split the "compatible_printers_condition" and "inherits" from the cummulative vectors to separate filament presets.
+ inherits = inherits_values[1];
+ compatible_printers_condition = compatible_printers_condition_values[1];
+ if (is_external)
+ this->filaments.load_external_preset(name_or_path, name, old_filament_profile_names->values.front(), config);
+ else
+ this->filaments.load_preset(this->filaments.path_from_name(name), name, config).save();
+ this->filament_presets.clear();
+ this->filament_presets.emplace_back(name);
+ } else {
+ // Split the filament presets, load each of them separately.
+ std::vector<DynamicPrintConfig> configs(num_extruders, this->filaments.default_preset().config);
+ // loop through options and scatter them into configs.
+ for (const t_config_option_key &key : this->filaments.default_preset().config.keys()) {
+ const ConfigOption *other_opt = config.option(key);
+ if (other_opt == nullptr)
+ continue;
+ if (other_opt->is_scalar()) {
+ for (size_t i = 0; i < configs.size(); ++ i)
+ configs[i].option(key, false)->set(other_opt);
+ } else if (key != "compatible_printers") {
+ for (size_t i = 0; i < configs.size(); ++ i)
+ static_cast<ConfigOptionVectorBase*>(configs[i].option(key, false))->set_at(other_opt, 0, i);
+ }
+ }
+ // Load the configs into this->filaments and make them active.
+ this->filament_presets.clear();
+ for (size_t i = 0; i < configs.size(); ++ i) {
+ DynamicPrintConfig &cfg = configs[i];
+ // Split the "compatible_printers_condition" and "inherits" from the cummulative vectors to separate filament presets.
+ cfg.opt_string("compatible_printers_condition", true) = compatible_printers_condition_values[i + 1];
+ cfg.opt_string("inherits", true) = inherits_values[i + 1];
+ // Load all filament presets, but only select the first one in the preset dialog.
+ Preset *loaded = nullptr;
+ if (is_external)
+ loaded = &this->filaments.load_external_preset(name_or_path, name,
+ (i < old_filament_profile_names->values.size()) ? old_filament_profile_names->values[i] : "",
+ std::move(cfg), i == 0);
+ else {
+ // Used by the config wizard when creating a custom setup.
+ // Therefore this block should only be called for a single extruder.
+ char suffix[64];
+ if (i == 0)
+ suffix[0] = 0;
+ else
+ sprintf(suffix, "%d", i);
+ std::string new_name = name + suffix;
+ loaded = &this->filaments.load_preset(this->filaments.path_from_name(new_name),
+ new_name, std::move(cfg), i == 0);
+ loaded->save();
+ }
+ this->filament_presets.emplace_back(loaded->name);
+ }
+ }
+
+ // 4) Load the project config values (the per extruder wipe matrix etc).
+ this->project_config.apply_only(config, s_project_options);
+ }
+
+ this->update_compatible_with_printer(false);
+}
+
+// Load the active configuration of a config bundle from a boost property_tree. This is a private method called from load_config_file.
+void PresetBundle::load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree)
+{
+ // 1) Load the config bundle into a temp data.
+ PresetBundle tmp_bundle;
+ // Load the config bundle, don't save the loaded presets to user profile directory.
+ tmp_bundle.load_configbundle(path, 0);
+ std::string bundle_name = std::string(" - ") + boost::filesystem::path(path).filename().string();
+
+ // 2) Extract active configs from the config bundle, copy them and activate them in this bundle.
+ auto load_one = [this, &path, &bundle_name](PresetCollection &collection_dst, PresetCollection &collection_src, const std::string &preset_name_src, bool activate) -> std::string {
+ Preset *preset_src = collection_src.find_preset(preset_name_src, false);
+ Preset *preset_dst = collection_dst.find_preset(preset_name_src, false);
+ assert(preset_src != nullptr);
+ std::string preset_name_dst;
+ if (preset_dst != nullptr && preset_dst->is_default) {
+ // No need to copy a default preset, it always exists in collection_dst.
+ if (activate)
+ collection_dst.select_preset(0);
+ return preset_name_src;
+ } else if (preset_dst != nullptr && preset_src->config == preset_dst->config) {
+ // Don't save as the config exists in the current bundle and its content is the same.
+ return preset_name_src;
+ } else {
+ // Generate a new unique name.
+ preset_name_dst = preset_name_src + bundle_name;
+ Preset *preset_dup = nullptr;
+ for (size_t i = 1; (preset_dup = collection_dst.find_preset(preset_name_dst, false)) != nullptr; ++ i) {
+ if (preset_src->config == preset_dup->config)
+ // The preset has been already copied into collection_dst.
+ return preset_name_dst;
+ // Try to generate another name.
+ char buf[64];
+ sprintf(buf, " (%d)", i);
+ preset_name_dst = preset_name_src + buf + bundle_name;
+ }
+ }
+ assert(! preset_name_dst.empty());
+ // Save preset_src->config into collection_dst under preset_name_dst.
+ // The "compatible_printers" field should not have been exported into a config.ini or a G-code anyway,
+ // but some of the alpha versions of Slic3r did.
+ ConfigOption *opt_compatible = preset_src->config.optptr("compatible_printers");
+ if (opt_compatible != nullptr) {
+ assert(opt_compatible->type() == coStrings);
+ if (opt_compatible->type() == coStrings)
+ static_cast<ConfigOptionStrings*>(opt_compatible)->values.clear();
+ }
+ collection_dst.load_preset(path, preset_name_dst, std::move(preset_src->config), activate).is_external = true;
+ return preset_name_dst;
+ };
+ load_one(this->prints, tmp_bundle.prints, tmp_bundle.prints .get_selected_preset().name, true);
+ load_one(this->filaments, tmp_bundle.filaments, tmp_bundle.filaments .get_selected_preset().name, true);
+ load_one(this->sla_materials, tmp_bundle.sla_materials, tmp_bundle.sla_materials.get_selected_preset().name, true);
+ load_one(this->printers, tmp_bundle.printers, tmp_bundle.printers .get_selected_preset().name, true);
+ this->update_multi_material_filament_presets();
+ for (size_t i = 1; i < std::min(tmp_bundle.filament_presets.size(), this->filament_presets.size()); ++ i)
+ this->filament_presets[i] = load_one(this->filaments, tmp_bundle.filaments, tmp_bundle.filament_presets[i], false);
+
+ this->update_compatible_with_printer(false);
+}
+
+// Process the Config Bundle loaded as a Boost property tree.
+// For each print, filament and printer preset (group defined by group_name), apply the inherited presets.
+// The presets starting with '*' are considered non-terminal and they are
+// removed through the flattening process by this function.
+// This function will never fail, but it will produce error messages through boost::log.
+static void flatten_configbundle_hierarchy(boost::property_tree::ptree &tree, const std::string &group_name)
+{
+ namespace pt = boost::property_tree;
+
+ typedef std::pair<pt::ptree::key_type, pt::ptree> ptree_child_type;
+
+ // 1) For the group given by group_name, initialize the presets.
+ struct Prst {
+ Prst(const std::string &name, pt::ptree *node) : name(name), node(node) {}
+ // Name of this preset. If the name starts with '*', it is an intermediate preset,
+ // which will not make it into the result.
+ const std::string name;
+ // Link to the source boost property tree node, owned by tree.
+ pt::ptree *node;
+ // Link to the presets, from which this preset inherits.
+ std::vector<Prst*> inherits;
+ // Link to the presets, for which this preset is a direct parent.
+ std::vector<Prst*> parent_of;
+ // When running the Kahn's Topological sorting algorithm, this counter is decreased from inherits.size() to zero.
+ // A cycle is indicated, if the number does not drop to zero after the Kahn's algorithm finishes.
+ size_t num_incoming_edges_left = 0;
+ // Sorting by the name, to be used when inserted into std::set.
+ bool operator==(const Prst &rhs) const { return this->name == rhs.name; }
+ bool operator< (const Prst &rhs) const { return this->name < rhs.name; }
+ };
+ // Find the presets, store them into a std::map, addressed by their names.
+ std::set<Prst> presets;
+ std::string group_name_preset = group_name + ":";
+ for (auto &section : tree)
+ if (boost::starts_with(section.first, group_name_preset) && section.first.size() > group_name_preset.size())
+ presets.emplace(section.first.substr(group_name_preset.size()), &section.second);
+ // Fill in the "inherits" and "parent_of" members, report invalid inheritance fields.
+ for (const Prst &prst : presets) {
+ // Parse the list of comma separated values, possibly enclosed in quotes.
+ std::vector<std::string> inherits_names;
+ if (Slic3r::unescape_strings_cstyle(prst.node->get<std::string>("inherits", ""), inherits_names)) {
+ // Resolve the inheritance by name.
+ std::vector<Prst*> &inherits_nodes = const_cast<Prst&>(prst).inherits;
+ for (const std::string &node_name : inherits_names) {
+ auto it = presets.find(Prst(node_name, nullptr));
+ if (it == presets.end())
+ BOOST_LOG_TRIVIAL(error) << "flatten_configbundle_hierarchy: The preset " << prst.name << " inherits an unknown preset \"" << node_name << "\"";
+ else {
+ inherits_nodes.emplace_back(const_cast<Prst*>(&(*it)));
+ inherits_nodes.back()->parent_of.emplace_back(const_cast<Prst*>(&prst));
+ }
+ }
+ } else {
+ BOOST_LOG_TRIVIAL(error) << "flatten_configbundle_hierarchy: The preset " << prst.name << " has an invalid \"inherits\" field";
+ }
+ // Remove the "inherits" key, it has no meaning outside the config bundle.
+ const_cast<pt::ptree*>(prst.node)->erase("inherits");
+ }
+
+ // 2) Create a linear ordering for the directed acyclic graph of preset inheritance.
+ // https://en.wikipedia.org/wiki/Topological_sorting
+ // Kahn's algorithm.
+ std::vector<Prst*> sorted;
+ {
+ // Initialize S with the set of all nodes with no incoming edge.
+ std::deque<Prst*> S;
+ for (const Prst &prst : presets)
+ if (prst.inherits.empty())
+ S.emplace_back(const_cast<Prst*>(&prst));
+ else
+ const_cast<Prst*>(&prst)->num_incoming_edges_left = prst.inherits.size();
+ while (! S.empty()) {
+ Prst *n = S.front();
+ S.pop_front();
+ sorted.emplace_back(n);
+ for (Prst *m : n->parent_of) {
+ assert(m->num_incoming_edges_left > 0);
+ if (-- m->num_incoming_edges_left == 0) {
+ // We have visited all parents of m.
+ S.emplace_back(m);
+ }
+ }
+ }
+ if (sorted.size() < presets.size()) {
+ for (const Prst &prst : presets)
+ if (prst.num_incoming_edges_left)
+ BOOST_LOG_TRIVIAL(error) << "flatten_configbundle_hierarchy: The preset " << prst.name << " has cyclic dependencies";
+ }
+ }
+
+ // Apply the dependencies in their topological ordering.
+ for (Prst *prst : sorted) {
+ // Merge the preset nodes in their order of application.
+ // Iterate in a reverse order, so the last change will be placed first in merged.
+ for (auto it_inherits = prst->inherits.rbegin(); it_inherits != prst->inherits.rend(); ++ it_inherits)
+ for (auto it = (*it_inherits)->node->begin(); it != (*it_inherits)->node->end(); ++ it)
+ if (prst->node->find(it->first) == prst->node->not_found())
+ prst->node->add_child(it->first, it->second);
+ }
+
+ // Remove the "internal" presets from the ptree. These presets are marked with '*'.
+ group_name_preset += '*';
+ for (auto it_section = tree.begin(); it_section != tree.end(); ) {
+ if (boost::starts_with(it_section->first, group_name_preset) && it_section->first.size() > group_name_preset.size())
+ // Remove the "internal" preset from the ptree.
+ it_section = tree.erase(it_section);
+ else
+ // Keep the preset.
+ ++ it_section;
+ }
+}
+
+static void flatten_configbundle_hierarchy(boost::property_tree::ptree &tree)
+{
+ flatten_configbundle_hierarchy(tree, "print");
+ flatten_configbundle_hierarchy(tree, "filament");
+ flatten_configbundle_hierarchy(tree, "sla_material");
+ 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.
+size_t PresetBundle::load_configbundle(const std::string &path, unsigned int flags)
+{
+ if (flags & (LOAD_CFGBNDLE_RESET_USER_PROFILE | LOAD_CFGBNDLE_SYSTEM))
+ // Reset this bundle, delete user profile files if LOAD_CFGBNDLE_SAVE.
+ this->reset(flags & LOAD_CFGBNDLE_SAVE);
+
+ // 1) Read the complete config file into a boost::property_tree.
+ namespace pt = boost::property_tree;
+ pt::ptree tree;
+ boost::nowide::ifstream ifs(path);
+ pt::read_ini(ifs, tree);
+
+ const VendorProfile *vendor_profile = nullptr;
+ if (flags & (LOAD_CFGBNDLE_SYSTEM | LOAD_CFGBUNDLE_VENDOR_ONLY)) {
+ auto vp = VendorProfile::from_ini(tree, path);
+ if (vp.num_variants() == 0)
+ 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.
+ // Parse the obsolete preset names, to be deleted when upgrading from the old configuration structure.
+ std::vector<std::string> loaded_prints;
+ std::vector<std::string> loaded_filaments;
+ std::vector<std::string> loaded_sla_materials;
+ std::vector<std::string> loaded_printers;
+ std::string active_print;
+ std::vector<std::string> active_filaments;
+ std::string active_sla_material;
+ std::string active_printer;
+ size_t presets_loaded = 0;
+ for (const auto &section : tree) {
+ PresetCollection *presets = nullptr;
+ std::vector<std::string> *loaded = nullptr;
+ std::string preset_name;
+ if (boost::starts_with(section.first, "print:")) {
+ presets = &this->prints;
+ loaded = &loaded_prints;
+ preset_name = section.first.substr(6);
+ } else if (boost::starts_with(section.first, "filament:")) {
+ presets = &this->filaments;
+ loaded = &loaded_filaments;
+ preset_name = section.first.substr(9);
+ } else if (boost::starts_with(section.first, "sla_material:")) {
+ presets = &this->sla_materials;
+ loaded = &loaded_sla_materials;
+ preset_name = section.first.substr(9);
+ } else if (boost::starts_with(section.first, "printer:")) {
+ presets = &this->printers;
+ loaded = &loaded_printers;
+ preset_name = section.first.substr(8);
+ } else if (section.first == "presets") {
+ // Load the names of the active presets.
+ for (auto &kvp : section.second) {
+ if (kvp.first == "print") {
+ active_print = kvp.second.data();
+ } else if (boost::starts_with(kvp.first, "filament")) {
+ int idx = 0;
+ if (kvp.first == "filament" || sscanf(kvp.first.c_str(), "filament_%d", &idx) == 1) {
+ if (int(active_filaments.size()) <= idx)
+ active_filaments.resize(idx + 1, std::string());
+ active_filaments[idx] = kvp.second.data();
+ }
+ } else if (kvp.first == "sla_material") {
+ active_sla_material = kvp.second.data();
+ } else if (kvp.first == "printer") {
+ active_printer = kvp.second.data();
+ }
+ }
+ } else if (section.first == "obsolete_presets") {
+ // Parse the names of obsolete presets. These presets will be deleted from user's
+ // profile directory on installation of this vendor preset.
+ for (auto &kvp : section.second) {
+ std::vector<std::string> *dst = nullptr;
+ if (kvp.first == "print")
+ dst = &this->obsolete_presets.prints;
+ else if (kvp.first == "filament")
+ dst = &this->obsolete_presets.filaments;
+ else if (kvp.first == "sla_material")
+ dst = &this->obsolete_presets.sla_materials;
+ else if (kvp.first == "printer")
+ dst = &this->obsolete_presets.printers;
+ if (dst)
+ unescape_strings_cstyle(kvp.second.data(), *dst);
+ }
+ } else if (section.first == "settings") {
+ // Load the settings.
+ for (auto &kvp : section.second) {
+ if (kvp.first == "autocenter") {
+ }
+ }
+ } else
+ // Ignore an unknown section.
+ continue;
+ if (presets != nullptr) {
+ // Load the print, filament or printer preset.
+ const DynamicPrintConfig &default_config = presets->default_preset().config;
+ DynamicPrintConfig config(default_config);
+ for (auto &kvp : section.second)
+ config.set_deserialize(kvp.first, kvp.second.data());
+ Preset::normalize(config);
+ // Report configuration fields, which are misplaced into a wrong group.
+ std::string incorrect_keys;
+ size_t n_incorrect_keys = 0;
+ for (const std::string &key : config.keys())
+ if (! default_config.has(key)) {
+ if (incorrect_keys.empty())
+ incorrect_keys = key;
+ else {
+ incorrect_keys += ", ";
+ incorrect_keys += key;
+ }
+ config.erase(key);
+ ++ n_incorrect_keys;
+ }
+ if (! incorrect_keys.empty())
+ BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" <<
+ section.first << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed";
+ if ((flags & LOAD_CFGBNDLE_SYSTEM) && presets == &printers) {
+ // Filter out printer presets, which are not mentioned in the vendor profile.
+ // These presets are considered not installed.
+ auto printer_model = config.opt_string("printer_model");
+ if (printer_model.empty()) {
+ BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" <<
+ section.first << "\" defines no printer model, it will be ignored.";
+ continue;
+ }
+ auto printer_variant = config.opt_string("printer_variant");
+ if (printer_variant.empty()) {
+ BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" <<
+ section.first << "\" defines no printer variant, it will be ignored.";
+ continue;
+ }
+ 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.";
+ continue;
+ }
+ auto it_variant = it_model->variant(printer_variant);
+ if (it_variant == nullptr) {
+ BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" <<
+ section.first << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored.";
+ continue;
+ }
+ const Preset *preset_existing = presets->find_preset(section.first, false);
+ if (preset_existing != nullptr) {
+ BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" <<
+ section.first << "\" has already been loaded from another Confing Bundle.";
+ continue;
+ }
+ }
+ // Decide a full path to this .ini file.
+ auto file_name = boost::algorithm::iends_with(preset_name, ".ini") ? preset_name : preset_name + ".ini";
+ auto file_path = (boost::filesystem::path(data_dir())
+#ifdef SLIC3R_PROFILE_USE_PRESETS_SUBDIR
+ // Store the print/filament/printer presets into a "presets" directory.
+ / "presets"
+#else
+ // Store the print/filament/printer presets at the same location as the upstream Slic3r.
+#endif
+ / presets->name() / file_name).make_preferred();
+ // Load the preset into the list of presets, save it to disk.
+ Preset &loaded = presets->load_preset(file_path.string(), preset_name, std::move(config), false);
+ if (flags & LOAD_CFGBNDLE_SAVE)
+ loaded.save();
+ if (flags & LOAD_CFGBNDLE_SYSTEM) {
+ loaded.is_system = true;
+ loaded.vendor = vendor_profile;
+ }
+ ++ presets_loaded;
+ }
+ }
+
+ // 3) Activate the presets.
+ if ((flags & LOAD_CFGBNDLE_SYSTEM) == 0) {
+ if (! active_print.empty())
+ prints.select_preset_by_name(active_print, true);
+ if (! active_sla_material.empty())
+ sla_materials.select_preset_by_name(active_sla_material, true);
+ if (! active_printer.empty())
+ printers.select_preset_by_name(active_printer, true);
+ // Activate the first filament preset.
+ if (! active_filaments.empty() && ! active_filaments.front().empty())
+ filaments.select_preset_by_name(active_filaments.front(), true);
+ this->update_multi_material_filament_presets();
+ for (size_t i = 0; i < std::min(this->filament_presets.size(), active_filaments.size()); ++ i)
+ this->filament_presets[i] = filaments.find_preset(active_filaments[i], true)->name;
+ this->update_compatible_with_printer(false);
+ }
+
+ return presets_loaded;
+}
+
+void PresetBundle::update_multi_material_filament_presets()
+{
+ if (printers.get_edited_preset().printer_technology() != ptFFF)
+ return;
+
+ // Verify and select the filament presets.
+ auto *nozzle_diameter = static_cast<const ConfigOptionFloats*>(printers.get_edited_preset().config.option("nozzle_diameter"));
+ size_t num_extruders = nozzle_diameter->values.size();
+ // Verify validity of the current filament presets.
+ for (size_t i = 0; i < std::min(this->filament_presets.size(), num_extruders); ++ i)
+ this->filament_presets[i] = this->filaments.find_preset(this->filament_presets[i], true)->name;
+ // Append the rest of filament presets.
+ this->filament_presets.resize(num_extruders, this->filament_presets.empty() ? this->filaments.first_visible().name : this->filament_presets.back());
+
+ // Now verify if wiping_volumes_matrix has proper size (it is used to deduce number of extruders in wipe tower generator):
+ std::vector<double> old_matrix = this->project_config.option<ConfigOptionFloats>("wiping_volumes_matrix")->values;
+ size_t old_number_of_extruders = int(sqrt(old_matrix.size())+EPSILON);
+ if (num_extruders != old_number_of_extruders) {
+ // First verify if purging volumes presets for each extruder matches number of extruders
+ std::vector<double>& extruders = this->project_config.option<ConfigOptionFloats>("wiping_volumes_extruders")->values;
+ while (extruders.size() < 2*num_extruders) {
+ extruders.push_back(extruders.size()>1 ? extruders[0] : 50.); // copy the values from the first extruder
+ extruders.push_back(extruders.size()>1 ? extruders[1] : 50.);
+ }
+ while (extruders.size() > 2*num_extruders) {
+ extruders.pop_back();
+ extruders.pop_back();
+ }
+
+ std::vector<double> new_matrix;
+ for (unsigned int i=0;i<num_extruders;++i)
+ for (unsigned int j=0;j<num_extruders;++j) {
+ // append the value for this pair from the old matrix (if it's there):
+ if (i<old_number_of_extruders && j<old_number_of_extruders)
+ new_matrix.push_back(old_matrix[i*old_number_of_extruders + j]);
+ else
+ new_matrix.push_back( i==j ? 0. : extruders[2*i]+extruders[2*j+1]); // so it matches new extruder volumes
+ }
+ this->project_config.option<ConfigOptionFloats>("wiping_volumes_matrix")->values = new_matrix;
+ }
+}
+
+void PresetBundle::update_compatible_with_printer(bool select_other_if_incompatible)
+{
+ const Preset &printer_preset = this->printers.get_edited_preset();
+
+ switch (printers.get_edited_preset().printer_technology()) {
+ case ptFFF:
+ {
+ const std::string &prefered_print_profile = printer_preset.config.opt_string("default_print_profile");
+ const std::vector<std::string> &prefered_filament_profiles = printer_preset.config.option<ConfigOptionStrings>("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.
+ 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;
+ }
+ }
+ }
+ }
+ }
+ case ptSLA:
+ {
+ const std::string &prefered_print_profile = printer_preset.config.opt_string("default_print_profile");
+ const std::vector<std::string> &prefered_filament_profiles = printer_preset.config.option<ConfigOptionStrings>("default_filament_profile")->values;
+ prefered_print_profile.empty() ?
+ this->sla_materials.update_compatible_with_printer(printer_preset, select_other_if_incompatible) :
+ this->sla_materials.update_compatible_with_printer(printer_preset, select_other_if_incompatible,
+ [&prefered_print_profile](const std::string& profile_name){ return profile_name == prefered_print_profile; });
+ }
+ }
+}
+
+void PresetBundle::export_configbundle(const std::string &path) //, const DynamicPrintConfig &settings
+{
+ boost::nowide::ofstream c;
+ c.open(path, std::ios::out | std::ios::trunc);
+
+ // Put a comment at the first line including the time stamp and Slic3r version.
+ c << "# " << Slic3r::header_slic3r_generated() << std::endl;
+
+ // Export the print, filament and printer profiles.
+ for (size_t i_group = 0; i_group < 3; ++ i_group) {
+ const PresetCollection &presets = (i_group == 0) ? this->prints : (i_group == 1) ? this->filaments : this->printers;
+ for (const Preset &preset : presets()) {
+ if (preset.is_default || preset.is_external)
+ // Only export the common presets, not external files or the default preset.
+ continue;
+ c << std::endl << "[" << presets.name() << ":" << preset.name << "]" << std::endl;
+ for (const std::string &opt_key : preset.config.keys())
+ c << opt_key << " = " << preset.config.serialize(opt_key) << std::endl;
+ }
+ }
+
+ // Export the names of the active presets.
+ c << std::endl << "[presets]" << std::endl;
+ c << "print = " << this->prints.get_selected_preset().name << std::endl;
+ c << "sla_material = " << this->sla_materials.get_selected_preset().name << std::endl;
+ c << "printer = " << this->printers.get_selected_preset().name << std::endl;
+ for (size_t i = 0; i < this->filament_presets.size(); ++ i) {
+ char suffix[64];
+ if (i > 0)
+ sprintf(suffix, "_%d", i);
+ else
+ suffix[0] = 0;
+ c << "filament" << suffix << " = " << this->filament_presets[i] << std::endl;
+ }
+
+#if 0
+ // Export the following setting values from the provided setting repository.
+ static const char *settings_keys[] = { "autocenter" };
+ c << "[settings]" << std::endl;
+ for (size_t i = 0; i < sizeof(settings_keys) / sizeof(settings_keys[0]); ++ i)
+ c << settings_keys[i] << " = " << settings.serialize(settings_keys[i]) << std::endl;
+#endif
+
+ c.close();
+}
+
+// Set the filament preset name. As the name could come from the UI selection box,
+// an optional "(modified)" suffix will be removed from the filament name.
+void PresetBundle::set_filament_preset(size_t idx, const std::string &name)
+{
+ if (name.find_first_of("-------") == 0)
+ return;
+
+ if (idx >= filament_presets.size())
+ filament_presets.resize(idx + 1, filaments.default_preset().name);
+ filament_presets[idx] = Preset::remove_suffix_modified(name);
+}
+
+static inline int hex_digit_to_int(const char c)
+{
+ return
+ (c >= '0' && c <= '9') ? int(c - '0') :
+ (c >= 'A' && c <= 'F') ? int(c - 'A') + 10 :
+ (c >= 'a' && c <= 'f') ? int(c - 'a') + 10 : -1;
+}
+
+bool PresetBundle::parse_color(const std::string &scolor, unsigned char *rgb_out)
+{
+ rgb_out[0] = rgb_out[1] = rgb_out[2] = 0;
+ if (scolor.size() != 7 || scolor.front() != '#')
+ return false;
+ const char *c = scolor.data() + 1;
+ for (size_t i = 0; i < 3; ++ i) {
+ int digit1 = hex_digit_to_int(*c ++);
+ int digit2 = hex_digit_to_int(*c ++);
+ if (digit1 == -1 || digit2 == -1)
+ return false;
+ rgb_out[i] = (unsigned char)(digit1 * 16 + digit2);
+ }
+ return true;
+}
+
+void PresetBundle::update_platter_filament_ui(unsigned int idx_extruder, wxBitmapComboBox *ui)
+{
+ if (ui == nullptr || this->printers.get_edited_preset().printer_technology() == ptSLA)
+ return;
+
+ unsigned char rgb[3];
+ std::string extruder_color = this->printers.get_edited_preset().config.opt_string("extruder_colour", idx_extruder);
+ if (! parse_color(extruder_color, rgb))
+ // Extruder color is not defined.
+ extruder_color.clear();
+
+ // Fill in the list from scratch.
+ ui->Freeze();
+ ui->Clear();
+ size_t selected_preset_item = 0;
+ const Preset *selected_preset = this->filaments.find_preset(this->filament_presets[idx_extruder]);
+ // Show wide icons if the currently selected preset is not compatible with the current printer,
+ // and draw a red flag in front of the selected preset.
+ bool wide_icons = selected_preset != nullptr && ! selected_preset->is_compatible && m_bitmapIncompatible != nullptr;
+ assert(selected_preset != nullptr);
+ std::map<wxString, wxBitmap*> nonsys_presets;
+ wxString selected_str = "";
+ if (!this->filaments().front().is_visible)
+ ui->Append("------- " + _(L("System presets")) + " -------", wxNullBitmap);
+ for (int i = this->filaments().front().is_visible ? 0 : 1; i < int(this->filaments().size()); ++i) {
+ const Preset &preset = this->filaments.preset(i);
+ bool selected = this->filament_presets[idx_extruder] == preset.name;
+ if (! preset.is_visible || (! preset.is_compatible && ! selected))
+ continue;
+ // Assign an extruder color to the selected item if the extruder color is defined.
+ std::string filament_rgb = preset.config.opt_string("filament_colour", 0);
+ std::string extruder_rgb = (selected && !extruder_color.empty()) ? extruder_color : filament_rgb;
+ bool single_bar = filament_rgb == extruder_rgb;
+ std::string bitmap_key = single_bar ? filament_rgb : filament_rgb + extruder_rgb;
+ // If the filament preset is not compatible and there is a "red flag" icon loaded, show it left
+ // to the filament color image.
+ if (wide_icons)
+ bitmap_key += preset.is_compatible ? ",cmpt" : ",ncmpt";
+ bitmap_key += (preset.is_system || preset.is_default) ? ",syst" : ",nsyst";
+ if (preset.is_dirty)
+ bitmap_key += ",drty";
+ wxBitmap *bitmap = m_bitmapCache->find(bitmap_key);
+ if (bitmap == nullptr) {
+ // Create the bitmap with color bars.
+ std::vector<wxBitmap> bmps;
+ if (wide_icons)
+ // Paint a red flag for incompatible presets.
+ bmps.emplace_back(preset.is_compatible ? m_bitmapCache->mkclear(16, 16) : *m_bitmapIncompatible);
+ // Paint the color bars.
+ parse_color(filament_rgb, rgb);
+ bmps.emplace_back(m_bitmapCache->mksolid(single_bar ? 24 : 16, 16, rgb));
+ if (! single_bar) {
+ parse_color(extruder_rgb, rgb);
+ bmps.emplace_back(m_bitmapCache->mksolid(8, 16, rgb));
+ }
+ // Paint a lock at the system presets.
+ bmps.emplace_back(m_bitmapCache->mkclear(2, 16));
+ bmps.emplace_back((preset.is_system || preset.is_default) ? *m_bitmapLock : m_bitmapCache->mkclear(16, 16));
+// (preset.is_dirty ? *m_bitmapLockOpen : *m_bitmapLock) : m_bitmapCache->mkclear(16, 16));
+ bitmap = m_bitmapCache->insert(bitmap_key, bmps);
+ }
+
+ if (preset.is_default || preset.is_system){
+ ui->Append(wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()),
+ (bitmap == 0) ? wxNullBitmap : *bitmap);
+ if (selected)
+ selected_preset_item = ui->GetCount() - 1;
+ }
+ else
+ {
+ nonsys_presets.emplace(wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()),
+ (bitmap == 0) ? &wxNullBitmap : bitmap);
+ if (selected)
+ selected_str = wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str());
+ }
+ if (preset.is_default)
+ ui->Append("------- " + _(L("System presets")) + " -------", wxNullBitmap);
+ }
+
+ if (!nonsys_presets.empty())
+ {
+ ui->Append("------- " + _(L("User presets")) + " -------", wxNullBitmap);
+ for (std::map<wxString, wxBitmap*>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) {
+ ui->Append(it->first, *it->second);
+ if (it->first == selected_str)
+ selected_preset_item = ui->GetCount() - 1;
+ }
+ }
+ ui->SetSelection(selected_preset_item);
+ ui->SetToolTip(ui->GetString(selected_preset_item));
+ ui->Thaw();
+}
+
+void PresetBundle::set_default_suppressed(bool default_suppressed)
+{
+ prints.set_default_suppressed(default_suppressed);
+ filaments.set_default_suppressed(default_suppressed);
+ sla_materials.set_default_suppressed(default_suppressed);
+ printers.set_default_suppressed(default_suppressed);
+}
+
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/PresetBundle.hpp b/src/slic3r/GUI/PresetBundle.hpp
new file mode 100644
index 000000000..68ec534da
--- /dev/null
+++ b/src/slic3r/GUI/PresetBundle.hpp
@@ -0,0 +1,168 @@
+#ifndef slic3r_PresetBundle_hpp_
+#define slic3r_PresetBundle_hpp_
+
+#include "AppConfig.hpp"
+#include "Preset.hpp"
+
+#include <set>
+#include <boost/filesystem/path.hpp>
+
+namespace Slic3r {
+
+namespace GUI {
+ class BitmapCache;
+};
+
+class PlaceholderParser;
+
+// Bundle of Print + Filament + Printer presets.
+class PresetBundle
+{
+public:
+ PresetBundle();
+ ~PresetBundle();
+
+ // Remove all the presets but the "-- default --".
+ // Optionally remove all the files referenced by the presets from the user profile directory.
+ void reset(bool delete_files);
+
+ void setup_directories();
+
+ // Load ini files of all types (print, filament, printer) from Slic3r::data_dir() / presets.
+ // Load selections (current print, current filaments, current printer) from config.ini
+ // This is done just once on application start up.
+ void load_presets(const AppConfig &config);
+
+ // Export selections (current print, current filaments, current printer) into config.ini
+ void export_selections(AppConfig &config);
+ // Export selections (current print, current filaments, current printer) into a placeholder parser.
+ void export_selections(PlaceholderParser &pp);
+
+ PresetCollection prints;
+ PresetCollection filaments;
+ PresetCollection sla_materials;
+ PresetCollection printers;
+ // Filament preset names for a multi-extruder or multi-material print.
+ // extruders.size() should be the same as printers.get_edited_preset().config.nozzle_diameter.size()
+ std::vector<std::string> filament_presets;
+
+ // The project configuration values are kept separated from the print/filament/printer preset,
+ // they are being serialized / deserialized from / to the .amf, .3mf, .config, .gcode,
+ // and they are being used by slicing core.
+ DynamicPrintConfig project_config;
+
+ // There will be an entry for each system profile loaded,
+ // and the system profiles will point to the VendorProfile instances owned by PresetBundle::vendors.
+ std::set<VendorProfile> vendors;
+
+ struct ObsoletePresets {
+ std::vector<std::string> prints;
+ std::vector<std::string> filaments;
+ std::vector<std::string> sla_materials;
+ std::vector<std::string> printers;
+ };
+ ObsoletePresets obsolete_presets;
+
+ bool has_defauls_only() const
+ { return prints.has_defaults_only() && filaments.has_defaults_only() && printers.has_defaults_only(); }
+
+ DynamicPrintConfig full_config() const;
+
+ // Load user configuration and store it into the user profiles.
+ // This method is called by the configuration wizard.
+ void load_config(const std::string &name, DynamicPrintConfig config)
+ { this->load_config_file_config(name, false, std::move(config)); }
+
+ // Load an external config file containing the print, filament and printer presets.
+ // Instead of a config file, a G-code may be loaded containing the full set of parameters.
+ // In the future the configuration will likely be read from an AMF file as well.
+ // If the file is loaded successfully, its print / filament / printer profiles will be activated.
+ void load_config_file(const std::string &path);
+
+ // Load an external config source containing the print, filament and printer presets.
+ // The given string must contain the full set of parameters (same as those exported to gcode).
+ // If the string is parsed successfully, its print / filament / printer profiles will be activated.
+ void load_config_string(const char* str, const char* source_filename = nullptr);
+
+ // Load a config bundle file, into presets and store the loaded presets into separate files
+ // of the local configuration directory.
+ // Load settings into the provided settings instance.
+ // Activate the presets stored in the config bundle.
+ // Returns the number of presets loaded successfully.
+ enum {
+ // Save the profiles, which have been loaded.
+ LOAD_CFGBNDLE_SAVE = 1,
+ // Delete all old config profiles before loading.
+ 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);
+
+ // 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);
+
+ // Update a filament selection combo box on the platter for an idx_extruder.
+ void update_platter_filament_ui(unsigned int idx_extruder, wxBitmapComboBox *ui);
+
+ // Enable / disable the "- default -" preset.
+ void set_default_suppressed(bool default_suppressed);
+
+ // Set the filament preset name. As the name could come from the UI selection box,
+ // an optional "(modified)" suffix will be removed from the filament name.
+ void set_filament_preset(size_t idx, const std::string &name);
+
+ // Read out the number of extruders from an active printer preset,
+ // update size and content of filament_presets.
+ void update_multi_material_filament_presets();
+
+ // Update the is_compatible flag of all print and filament presets depending on whether they are marked
+ // as compatible with the currently selected printer.
+ // Also updates the is_visible flag of each preset.
+ // If select_other_if_incompatible is true, then the print or filament preset is switched to some compatible
+ // 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);
+
+private:
+ std::string load_system_presets();
+ // Merge one vendor's presets with the other vendor's presets, report duplicates.
+ std::vector<std::string> merge_presets(PresetBundle &&other);
+
+ // Set the "enabled" flag for printer vendors, printer models and printer variants
+ // based on the user configuration.
+ // If the "vendor" section is missing, enable all models and variants of the particular vendor.
+ void load_installed_printers(const AppConfig &config);
+
+ // Load selections (current print, current filaments, current printer) from config.ini
+ // This is done just once on application start up.
+ void load_selections(const AppConfig &config);
+
+ // Load print, filament & printer presets from a config. If it is an external config, then the name is extracted from the external path.
+ // and the external config is just referenced, not stored into user profile directory.
+ // If it is not an external config, then the config will be stored into the user profile directory.
+ void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config);
+ void load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree);
+ bool load_compatible_bitmaps();
+
+ DynamicPrintConfig full_fff_config() const;
+ DynamicPrintConfig full_sla_config() const;
+
+ // Indicator, that the preset is compatible with the selected printer.
+ wxBitmap *m_bitmapCompatible;
+ // Indicator, that the preset is NOT compatible with the selected printer.
+ wxBitmap *m_bitmapIncompatible;
+ // Indicator, that the preset is system and not modified.
+ wxBitmap *m_bitmapLock;
+ // Indicator, that the preset is system and user modified.
+ wxBitmap *m_bitmapLockOpen;
+ // Caching color bitmaps for the filament combo box.
+ GUI::BitmapCache *m_bitmapCache;
+};
+
+} // namespace Slic3r
+
+#endif /* slic3r_PresetBundle_hpp_ */
diff --git a/src/slic3r/GUI/PresetHints.cpp b/src/slic3r/GUI/PresetHints.cpp
new file mode 100644
index 000000000..d4c929c1c
--- /dev/null
+++ b/src/slic3r/GUI/PresetHints.cpp
@@ -0,0 +1,278 @@
+//#undef NDEBUG
+#include <cassert>
+
+#include "PresetBundle.hpp"
+#include "PresetHints.hpp"
+#include "Flow.hpp"
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <wx/intl.h>
+
+#include "../../libslic3r/libslic3r.h"
+#include "GUI.hpp"
+
+namespace Slic3r {
+
+#define MIN_BUF_LENGTH 4096
+std::string PresetHints::cooling_description(const Preset &preset)
+{
+ std::string out;
+ char buf[MIN_BUF_LENGTH/*4096*/];
+ if (preset.config.opt_bool("cooling", 0)) {
+ int slowdown_below_layer_time = preset.config.opt_int("slowdown_below_layer_time", 0);
+ int min_fan_speed = preset.config.opt_int("min_fan_speed", 0);
+ int max_fan_speed = preset.config.opt_int("max_fan_speed", 0);
+ int min_print_speed = int(preset.config.opt_float("min_print_speed", 0) + 0.5);
+ int fan_below_layer_time = preset.config.opt_int("fan_below_layer_time", 0);
+ sprintf(buf, _CHB(L("If estimated layer time is below ~%ds, fan will run at %d%% and print speed will be reduced so that no less than %ds are spent on that layer (however, speed will never be reduced below %dmm/s).")),
+ slowdown_below_layer_time, max_fan_speed, slowdown_below_layer_time, min_print_speed);
+ out += buf;
+ if (fan_below_layer_time > slowdown_below_layer_time) {
+ sprintf(buf, _CHB(L("\nIf estimated layer time is greater, but still below ~%ds, fan will run at a proportionally decreasing speed between %d%% and %d%%.")),
+ fan_below_layer_time, max_fan_speed, min_fan_speed);
+ out += buf;
+ }
+ out += _CHB(L("\nDuring the other layers, fan "));
+ } else {
+ out = _CHB(L("Fan "));
+ }
+ if (preset.config.opt_bool("fan_always_on", 0)) {
+ int disable_fan_first_layers = preset.config.opt_int("disable_fan_first_layers", 0);
+ int min_fan_speed = preset.config.opt_int("min_fan_speed", 0);
+ sprintf(buf, _CHB(L("will always run at %d%% ")), min_fan_speed);
+ out += buf;
+ if (disable_fan_first_layers > 1) {
+ sprintf(buf, _CHB(L("except for the first %d layers")), disable_fan_first_layers);
+ out += buf;
+ }
+ else if (disable_fan_first_layers == 1)
+ out += _CHB(L("except for the first layer"));
+ } else
+ out += _CHB(L("will be turned off."));
+
+ return out;
+}
+
+static const ConfigOptionFloatOrPercent& first_positive(const ConfigOptionFloatOrPercent *v1, const ConfigOptionFloatOrPercent &v2, const ConfigOptionFloatOrPercent &v3)
+{
+ return (v1 != nullptr && v1->value > 0) ? *v1 : ((v2.value > 0) ? v2 : v3);
+}
+
+std::string PresetHints::maximum_volumetric_flow_description(const PresetBundle &preset_bundle)
+{
+ // Find out, to which nozzle index is the current filament profile assigned.
+ int idx_extruder = 0;
+ int num_extruders = (int)preset_bundle.filament_presets.size();
+ for (; idx_extruder < num_extruders; ++ idx_extruder)
+ if (preset_bundle.filament_presets[idx_extruder] == preset_bundle.filaments.get_selected_preset().name)
+ break;
+ if (idx_extruder == num_extruders)
+ // The current filament preset is not active for any extruder.
+ idx_extruder = -1;
+
+ const DynamicPrintConfig &print_config = preset_bundle.prints .get_edited_preset().config;
+ const DynamicPrintConfig &filament_config = preset_bundle.filaments.get_edited_preset().config;
+ const DynamicPrintConfig &printer_config = preset_bundle.printers .get_edited_preset().config;
+
+ // Current printer values.
+ float nozzle_diameter = (float)printer_config.opt_float("nozzle_diameter", idx_extruder);
+
+ // Print config values
+ double layer_height = print_config.opt_float("layer_height");
+ double first_layer_height = print_config.get_abs_value("first_layer_height", layer_height);
+ double support_material_speed = print_config.opt_float("support_material_speed");
+ double support_material_interface_speed = print_config.get_abs_value("support_material_interface_speed", support_material_speed);
+ double bridge_speed = print_config.opt_float("bridge_speed");
+ double bridge_flow_ratio = print_config.opt_float("bridge_flow_ratio");
+ double perimeter_speed = print_config.opt_float("perimeter_speed");
+ double external_perimeter_speed = print_config.get_abs_value("external_perimeter_speed", perimeter_speed);
+ double gap_fill_speed = print_config.opt_float("gap_fill_speed");
+ double infill_speed = print_config.opt_float("infill_speed");
+ double small_perimeter_speed = print_config.get_abs_value("small_perimeter_speed", perimeter_speed);
+ double solid_infill_speed = print_config.get_abs_value("solid_infill_speed", infill_speed);
+ double top_solid_infill_speed = print_config.get_abs_value("top_solid_infill_speed", solid_infill_speed);
+ // Maximum print speed when auto-speed is enabled by setting any of the above speed values to zero.
+ double max_print_speed = print_config.opt_float("max_print_speed");
+ // Maximum volumetric speed allowed for the print profile.
+ double max_volumetric_speed = print_config.opt_float("max_volumetric_speed");
+
+ const auto &extrusion_width = *print_config.option<ConfigOptionFloatOrPercent>("extrusion_width");
+ const auto &external_perimeter_extrusion_width = *print_config.option<ConfigOptionFloatOrPercent>("external_perimeter_extrusion_width");
+ const auto &first_layer_extrusion_width = *print_config.option<ConfigOptionFloatOrPercent>("first_layer_extrusion_width");
+ const auto &infill_extrusion_width = *print_config.option<ConfigOptionFloatOrPercent>("infill_extrusion_width");
+ const auto &perimeter_extrusion_width = *print_config.option<ConfigOptionFloatOrPercent>("perimeter_extrusion_width");
+ const auto &solid_infill_extrusion_width = *print_config.option<ConfigOptionFloatOrPercent>("solid_infill_extrusion_width");
+ const auto &support_material_extrusion_width = *print_config.option<ConfigOptionFloatOrPercent>("support_material_extrusion_width");
+ const auto &top_infill_extrusion_width = *print_config.option<ConfigOptionFloatOrPercent>("top_infill_extrusion_width");
+ const auto &first_layer_speed = *print_config.option<ConfigOptionFloatOrPercent>("first_layer_speed");
+
+ // Index of an extruder assigned to a feature. If set to 0, an active extruder will be used for a multi-material print.
+ // If different from idx_extruder, it will not be taken into account for this hint.
+ auto feature_extruder_active = [idx_extruder, num_extruders](int i) {
+ return i <= 0 || i > num_extruders || idx_extruder == -1 || idx_extruder == i - 1;
+ };
+ bool perimeter_extruder_active = feature_extruder_active(print_config.opt_int("perimeter_extruder"));
+ bool infill_extruder_active = feature_extruder_active(print_config.opt_int("infill_extruder"));
+ bool solid_infill_extruder_active = feature_extruder_active(print_config.opt_int("solid_infill_extruder"));
+ bool support_material_extruder_active = feature_extruder_active(print_config.opt_int("support_material_extruder"));
+ bool support_material_interface_extruder_active = feature_extruder_active(print_config.opt_int("support_material_interface_extruder"));
+
+ // Current filament values
+ double filament_diameter = filament_config.opt_float("filament_diameter", 0);
+ double filament_crossection = M_PI * 0.25 * filament_diameter * filament_diameter;
+ double extrusion_multiplier = filament_config.opt_float("extrusion_multiplier", 0);
+ // The following value will be annotated by this hint, so it does not take part in the calculation.
+// double filament_max_volumetric_speed = filament_config.opt_float("filament_max_volumetric_speed", 0);
+
+ std::string out;
+ for (size_t idx_type = (first_layer_extrusion_width.value == 0) ? 1 : 0; idx_type < 3; ++ idx_type) {
+ // First test the maximum volumetric extrusion speed for non-bridging extrusions.
+ bool first_layer = idx_type == 0;
+ bool bridging = idx_type == 2;
+ const ConfigOptionFloatOrPercent *first_layer_extrusion_width_ptr = (first_layer && first_layer_extrusion_width.value > 0) ?
+ &first_layer_extrusion_width : nullptr;
+ const float lh = float(first_layer ? first_layer_height : layer_height);
+ const float bfr = bridging ? bridge_flow_ratio : 0.f;
+ double max_flow = 0.;
+ std::string max_flow_extrusion_type;
+ auto limit_by_first_layer_speed = [&first_layer_speed, first_layer](double speed_normal, double speed_max) {
+ if (first_layer && first_layer_speed.value > 0)
+ // Apply the first layer limit.
+ speed_normal = first_layer_speed.get_abs_value(speed_normal);
+ return (speed_normal > 0.) ? speed_normal : speed_max;
+ };
+ if (perimeter_extruder_active) {
+ double external_perimeter_rate = Flow::new_from_config_width(frExternalPerimeter,
+ first_positive(first_layer_extrusion_width_ptr, external_perimeter_extrusion_width, extrusion_width),
+ nozzle_diameter, lh, bfr).mm3_per_mm() *
+ (bridging ? bridge_speed :
+ limit_by_first_layer_speed(std::max(external_perimeter_speed, small_perimeter_speed), max_print_speed));
+ if (max_flow < external_perimeter_rate) {
+ max_flow = external_perimeter_rate;
+ max_flow_extrusion_type = _CHB(L("external perimeters"));
+ }
+ double perimeter_rate = Flow::new_from_config_width(frPerimeter,
+ first_positive(first_layer_extrusion_width_ptr, perimeter_extrusion_width, extrusion_width),
+ nozzle_diameter, lh, bfr).mm3_per_mm() *
+ (bridging ? bridge_speed :
+ limit_by_first_layer_speed(std::max(perimeter_speed, small_perimeter_speed), max_print_speed));
+ if (max_flow < perimeter_rate) {
+ max_flow = perimeter_rate;
+ max_flow_extrusion_type = _CHB(L("perimeters"));
+ }
+ }
+ if (! bridging && infill_extruder_active) {
+ double infill_rate = Flow::new_from_config_width(frInfill,
+ first_positive(first_layer_extrusion_width_ptr, infill_extrusion_width, extrusion_width),
+ nozzle_diameter, lh, bfr).mm3_per_mm() * limit_by_first_layer_speed(infill_speed, max_print_speed);
+ if (max_flow < infill_rate) {
+ max_flow = infill_rate;
+ max_flow_extrusion_type = _CHB(L("infill"));
+ }
+ }
+ if (solid_infill_extruder_active) {
+ double solid_infill_rate = Flow::new_from_config_width(frInfill,
+ first_positive(first_layer_extrusion_width_ptr, solid_infill_extrusion_width, extrusion_width),
+ nozzle_diameter, lh, 0).mm3_per_mm() *
+ (bridging ? bridge_speed : limit_by_first_layer_speed(solid_infill_speed, max_print_speed));
+ if (max_flow < solid_infill_rate) {
+ max_flow = solid_infill_rate;
+ max_flow_extrusion_type = _CHB(L("solid infill"));
+ }
+ if (! bridging) {
+ double top_solid_infill_rate = Flow::new_from_config_width(frInfill,
+ first_positive(first_layer_extrusion_width_ptr, top_infill_extrusion_width, extrusion_width),
+ nozzle_diameter, lh, bfr).mm3_per_mm() * limit_by_first_layer_speed(top_solid_infill_speed, max_print_speed);
+ if (max_flow < top_solid_infill_rate) {
+ max_flow = top_solid_infill_rate;
+ max_flow_extrusion_type = _CHB(L("top solid infill"));
+ }
+ }
+ }
+ if (support_material_extruder_active) {
+ double support_material_rate = Flow::new_from_config_width(frSupportMaterial,
+ first_positive(first_layer_extrusion_width_ptr, support_material_extrusion_width, extrusion_width),
+ nozzle_diameter, lh, bfr).mm3_per_mm() *
+ (bridging ? bridge_speed : limit_by_first_layer_speed(support_material_speed, max_print_speed));
+ if (max_flow < support_material_rate) {
+ max_flow = support_material_rate;
+ max_flow_extrusion_type = _CHB(L("support"));
+ }
+ }
+ if (support_material_interface_extruder_active) {
+ double support_material_interface_rate = Flow::new_from_config_width(frSupportMaterialInterface,
+ first_positive(first_layer_extrusion_width_ptr, support_material_extrusion_width, extrusion_width),
+ nozzle_diameter, lh, bfr).mm3_per_mm() *
+ (bridging ? bridge_speed : limit_by_first_layer_speed(support_material_interface_speed, max_print_speed));
+ if (max_flow < support_material_interface_rate) {
+ max_flow = support_material_interface_rate;
+ max_flow_extrusion_type = _CHB(L("support interface"));
+ }
+ }
+ //FIXME handle gap_fill_speed
+ if (! out.empty())
+ out += "\n";
+ out += (first_layer ? _CHB(L("First layer volumetric")) : (bridging ? _CHB(L("Bridging volumetric")) : _CHB(L("Volumetric"))));
+ out += _CHB(L(" flow rate is maximized "));
+ bool limited_by_max_volumetric_speed = max_volumetric_speed > 0 && max_volumetric_speed < max_flow;
+ out += (limited_by_max_volumetric_speed ?
+ _CHB(L("by the print profile maximum")) :
+ (_CHB(L("when printing ")) + max_flow_extrusion_type))
+ + _CHB(L(" with a volumetric rate "));
+ if (limited_by_max_volumetric_speed)
+ max_flow = max_volumetric_speed;
+ char buf[MIN_BUF_LENGTH/*2048*/];
+ sprintf(buf, _CHB(L("%3.2f mm³/s")), max_flow);
+ out += buf;
+ sprintf(buf, _CHB(L(" at filament speed %3.2f mm/s.")), max_flow / filament_crossection);
+ out += buf;
+ }
+
+ return out;
+}
+
+std::string PresetHints::recommended_thin_wall_thickness(const PresetBundle &preset_bundle)
+{
+ const DynamicPrintConfig &print_config = preset_bundle.prints .get_edited_preset().config;
+ const DynamicPrintConfig &printer_config = preset_bundle.printers .get_edited_preset().config;
+
+ float layer_height = float(print_config.opt_float("layer_height"));
+ int num_perimeters = print_config.opt_int("perimeters");
+ bool thin_walls = print_config.opt_bool("thin_walls");
+ float nozzle_diameter = float(printer_config.opt_float("nozzle_diameter", 0));
+
+ std::string out;
+ if (layer_height <= 0.f){
+ out += _CHB(L("Recommended object thin wall thickness: Not available due to invalid layer height."));
+ return out;
+ }
+
+ Flow external_perimeter_flow = Flow::new_from_config_width(
+ frExternalPerimeter,
+ *print_config.opt<ConfigOptionFloatOrPercent>("external_perimeter_extrusion_width"),
+ nozzle_diameter, layer_height, false);
+ Flow perimeter_flow = Flow::new_from_config_width(
+ frPerimeter,
+ *print_config.opt<ConfigOptionFloatOrPercent>("perimeter_extrusion_width"),
+ nozzle_diameter, layer_height, false);
+
+
+ if (num_perimeters > 0) {
+ int num_lines = std::min(num_perimeters * 2, 10);
+ char buf[MIN_BUF_LENGTH/*256*/];
+ sprintf(buf, _CHB(L("Recommended object thin wall thickness for layer height %.2f and ")), layer_height);
+ out += buf;
+ // Start with the width of two closely spaced
+ double width = external_perimeter_flow.width + external_perimeter_flow.spacing();
+ for (int i = 2; i <= num_lines; thin_walls ? ++ i : i += 2) {
+ if (i > 2)
+ out += ", ";
+ sprintf(buf, _CHB(L("%d lines: %.2lf mm")), i, width);
+ out += buf;
+ width += perimeter_flow.spacing() * (thin_walls ? 1.f : 2.f);
+ }
+ }
+ return out;
+}
+
+}; // namespace Slic3r
diff --git a/src/slic3r/GUI/PresetHints.hpp b/src/slic3r/GUI/PresetHints.hpp
new file mode 100644
index 000000000..39bf0b100
--- /dev/null
+++ b/src/slic3r/GUI/PresetHints.hpp
@@ -0,0 +1,30 @@
+#ifndef slic3r_PresetHints_hpp_
+#define slic3r_PresetHints_hpp_
+
+#include <string>
+
+#include "PresetBundle.hpp"
+
+namespace Slic3r {
+
+// GUI utility functions to produce hint messages from the current profile.
+class PresetHints
+{
+public:
+ // Produce a textual description of the cooling logic of a currently active filament.
+ static std::string cooling_description(const Preset &preset);
+
+ // Produce a textual description of the maximum flow achived for the current configuration
+ // (the current printer, filament and print settigns).
+ // This description will be useful for getting a gut feeling for the maximum volumetric
+ // print speed achievable with the extruder.
+ static std::string maximum_volumetric_flow_description(const PresetBundle &preset_bundle);
+
+ // Produce a textual description of a recommended thin wall thickness
+ // from the provided number of perimeters and the external / internal perimeter width.
+ static std::string recommended_thin_wall_thickness(const PresetBundle &preset_bundle);
+};
+
+} // namespace Slic3r
+
+#endif /* slic3r_PresetHints_hpp_ */
diff --git a/src/slic3r/GUI/ProgressIndicator.hpp b/src/slic3r/GUI/ProgressIndicator.hpp
new file mode 100644
index 000000000..0cf8b4a17
--- /dev/null
+++ b/src/slic3r/GUI/ProgressIndicator.hpp
@@ -0,0 +1,70 @@
+#ifndef IPROGRESSINDICATOR_HPP
+#define IPROGRESSINDICATOR_HPP
+
+#include <string>
+#include <functional>
+
+namespace Slic3r {
+
+/**
+ * @brief Generic progress indication interface.
+ */
+class ProgressIndicator {
+public:
+ using CancelFn = std::function<void(void)>; // Cancel function signature.
+
+private:
+ float state_ = .0f, max_ = 1.f, step_;
+ CancelFn cancelfunc_ = [](){};
+
+public:
+
+ inline virtual ~ProgressIndicator() {}
+
+ /// Get the maximum of the progress range.
+ float max() const { return max_; }
+
+ /// Get the current progress state
+ float state() const { return state_; }
+
+ /// Set the maximum of the progress range
+ virtual void max(float maxval) { max_ = maxval; }
+
+ /// Set the current state of the progress.
+ virtual void state(float val) { state_ = val; }
+
+ /**
+ * @brief Number of states int the progress. Can be used instead of giving a
+ * maximum value.
+ */
+ virtual void states(unsigned statenum) {
+ step_ = max_ / statenum;
+ }
+
+ /// Message shown on the next status update.
+ virtual void message(const std::string&) = 0;
+
+ /// Title of the operation.
+ virtual void title(const std::string&) = 0;
+
+ /// Formatted message for the next status update. Works just like sprintf.
+ virtual void message_fmt(const std::string& fmt, ...);
+
+ /// Set up a cancel callback for the operation if feasible.
+ virtual void on_cancel(CancelFn func = CancelFn()) { cancelfunc_ = func; }
+
+ /**
+ * Explicitly shut down the progress indicator and call the associated
+ * callback.
+ */
+ virtual void cancel() { cancelfunc_(); }
+
+ /// Convenience function to call message and status update in one function.
+ void update(float st, const std::string& msg) {
+ message(msg); state(st);
+ }
+};
+
+}
+
+#endif // IPROGRESSINDICATOR_HPP
diff --git a/src/slic3r/GUI/ProgressStatusBar.cpp b/src/slic3r/GUI/ProgressStatusBar.cpp
new file mode 100644
index 000000000..363e34cb2
--- /dev/null
+++ b/src/slic3r/GUI/ProgressStatusBar.cpp
@@ -0,0 +1,152 @@
+#include "ProgressStatusBar.hpp"
+
+#include <wx/timer.h>
+#include <wx/gauge.h>
+#include <wx/button.h>
+#include <wx/statusbr.h>
+#include <wx/frame.h>
+#include "GUI.hpp"
+
+#include <iostream>
+
+namespace Slic3r {
+
+ProgressStatusBar::ProgressStatusBar(wxWindow *parent, int id):
+ self(new wxStatusBar(parent ? parent : GUI::get_main_frame(),
+ id == -1? wxID_ANY : id)),
+ timer_(new wxTimer(self)),
+ prog_ (new wxGauge(self,
+ wxGA_HORIZONTAL,
+ 100,
+ wxDefaultPosition,
+ wxDefaultSize)),
+ cancelbutton_(new wxButton(self,
+ -1,
+ "Cancel",
+ wxDefaultPosition,
+ wxDefaultSize))
+{
+ prog_->Hide();
+ cancelbutton_->Hide();
+
+ self->SetFieldsCount(3);
+ int w[] = {-1, 150, 155};
+ self->SetStatusWidths(3, w);
+
+ self->Bind(wxEVT_TIMER, [this](const wxTimerEvent&) {
+ if (prog_->IsShown()) timer_->Stop();
+ if(is_busy()) prog_->Pulse();
+ });
+
+ self->Bind(wxEVT_SIZE, [this](wxSizeEvent& event){
+ wxRect rect;
+ self->GetFieldRect(1, rect);
+ auto offset = 0;
+ cancelbutton_->Move(rect.GetX() + offset, rect.GetY() + offset);
+ cancelbutton_->SetSize(rect.GetWidth() - offset, rect.GetHeight());
+
+ self->GetFieldRect(2, rect);
+ prog_->Move(rect.GetX() + offset, rect.GetY() + offset);
+ prog_->SetSize(rect.GetWidth() - offset, rect.GetHeight());
+
+ event.Skip();
+ });
+
+ cancelbutton_->Bind(wxEVT_BUTTON, [this](const wxCommandEvent&) {
+ if(cancel_cb_) cancel_cb_();
+ m_perl_cancel_callback.call();
+ cancelbutton_->Hide();
+ });
+}
+
+ProgressStatusBar::~ProgressStatusBar() {
+ if(timer_->IsRunning()) timer_->Stop();
+}
+
+int ProgressStatusBar::get_progress() const
+{
+ return prog_->GetValue();
+}
+
+void ProgressStatusBar::set_progress(int val)
+{
+ if(!prog_->IsShown()) show_progress(true);
+
+ if(val == prog_->GetRange()) {
+ prog_->SetValue(0);
+ show_progress(false);
+ } else {
+ prog_->SetValue(val);
+ }
+}
+
+int ProgressStatusBar::get_range() const
+{
+ return prog_->GetRange();
+}
+
+void ProgressStatusBar::set_range(int val)
+{
+ if(val != prog_->GetRange()) {
+ prog_->SetRange(val);
+ }
+}
+
+void ProgressStatusBar::show_progress(bool show)
+{
+ prog_->Show(show);
+ prog_->Pulse();
+}
+
+void ProgressStatusBar::start_busy(int rate)
+{
+ busy_ = true;
+ show_progress(true);
+ if (!timer_->IsRunning()) {
+ timer_->Start(rate);
+ }
+}
+
+void ProgressStatusBar::stop_busy()
+{
+ timer_->Stop();
+ show_progress(false);
+ prog_->SetValue(0);
+ busy_ = false;
+}
+
+void ProgressStatusBar::set_cancel_callback(ProgressStatusBar::CancelFn ccb) {
+ cancel_cb_ = ccb;
+ if(ccb) cancelbutton_->Show();
+ else cancelbutton_->Hide();
+}
+
+void ProgressStatusBar::run(int rate)
+{
+ if(!timer_->IsRunning()) {
+ timer_->Start(rate);
+ }
+}
+
+void ProgressStatusBar::embed(wxFrame *frame)
+{
+ wxFrame* mf = frame? frame : GUI::get_main_frame();
+ mf->SetStatusBar(self);
+}
+
+void ProgressStatusBar::set_status_text(const wxString& txt)
+{
+ self->SetStatusText(wxString::FromUTF8(txt.c_str()));
+}
+
+void ProgressStatusBar::show_cancel_button()
+{
+ cancelbutton_->Show();
+}
+
+void ProgressStatusBar::hide_cancel_button()
+{
+ cancelbutton_->Hide();
+}
+
+}
diff --git a/src/slic3r/GUI/ProgressStatusBar.hpp b/src/slic3r/GUI/ProgressStatusBar.hpp
new file mode 100644
index 000000000..7c2171a5e
--- /dev/null
+++ b/src/slic3r/GUI/ProgressStatusBar.hpp
@@ -0,0 +1,68 @@
+#ifndef PROGRESSSTATUSBAR_HPP
+#define PROGRESSSTATUSBAR_HPP
+
+#include <memory>
+#include <functional>
+
+#include "callback.hpp"
+
+class wxTimer;
+class wxGauge;
+class wxButton;
+class wxTimerEvent;
+class wxStatusBar;
+class wxWindow;
+class wxFrame;
+class wxString;
+
+namespace Slic3r {
+
+/**
+ * @brief The ProgressStatusBar class is the widgets occupying the lower area
+ * of the Slicer main window. It consists of a message area to the left and a
+ * progress indication area to the right with an optional cancel button.
+ */
+class ProgressStatusBar {
+ wxStatusBar *self; // we cheat! It should be the base class but: perl!
+ wxTimer *timer_;
+ wxGauge *prog_;
+ wxButton *cancelbutton_;
+public:
+
+ /// Cancel callback function type
+ using CancelFn = std::function<void()>;
+
+ ProgressStatusBar(wxWindow *parent = nullptr, int id = -1);
+ ~ProgressStatusBar();
+
+ int get_progress() const;
+ void set_progress(int);
+ int get_range() const;
+ void set_range(int = 100);
+ void show_progress(bool);
+ void start_busy(int = 100);
+ void stop_busy();
+ inline bool is_busy() const { return busy_; }
+ void set_cancel_callback(CancelFn = CancelFn());
+ inline void remove_cancel_callback() { set_cancel_callback(); }
+ void run(int rate);
+ void embed(wxFrame *frame = nullptr);
+ void set_status_text(const wxString& txt);
+
+ // Temporary methods to satisfy Perl side
+ void show_cancel_button();
+ void hide_cancel_button();
+
+ PerlCallback m_perl_cancel_callback;
+private:
+ bool busy_ = false;
+ CancelFn cancel_cb_;
+};
+
+namespace GUI {
+ using Slic3r::ProgressStatusBar;
+}
+
+}
+
+#endif // PROGRESSSTATUSBAR_HPP
diff --git a/src/slic3r/GUI/RammingChart.cpp b/src/slic3r/GUI/RammingChart.cpp
new file mode 100644
index 000000000..8954ff93b
--- /dev/null
+++ b/src/slic3r/GUI/RammingChart.cpp
@@ -0,0 +1,279 @@
+#include <algorithm>
+#include <wx/dcbuffer.h>
+
+#include "RammingChart.hpp"
+#include "GUI.hpp"
+
+
+wxDEFINE_EVENT(EVT_WIPE_TOWER_CHART_CHANGED, wxCommandEvent);
+
+
+void Chart::draw() {
+ wxAutoBufferedPaintDC dc(this); // unbuffered DC caused flickering on win
+
+ dc.SetBrush(GetBackgroundColour());
+ dc.SetPen(GetBackgroundColour());
+ dc.DrawRectangle(GetClientRect()); // otherwise the background would end up black on windows
+
+ dc.SetPen(*wxBLACK_PEN);
+ dc.SetBrush(*wxWHITE_BRUSH);
+ dc.DrawRectangle(m_rect);
+
+ if (visible_area.m_width < 0.499) {
+ dc.DrawText(_(L("NO RAMMING AT ALL")),wxPoint(m_rect.GetLeft()+m_rect.GetWidth()/2-50,m_rect.GetBottom()-m_rect.GetHeight()/2));
+ return;
+ }
+
+
+ if (!m_line_to_draw.empty()) {
+ for (unsigned int i=0;i<m_line_to_draw.size()-2;++i) {
+ int color = 510*((m_rect.GetBottom()-(m_line_to_draw)[i])/double(m_rect.GetHeight()));
+ dc.SetPen( wxPen( wxColor(std::min(255,color),255-std::max(color-255,0),0), 1 ) );
+ dc.DrawLine(m_rect.GetLeft()+1+i,(m_line_to_draw)[i],m_rect.GetLeft()+1+i,m_rect.GetBottom());
+ }
+ dc.SetPen( wxPen( wxColor(0,0,0), 1 ) );
+ for (unsigned int i=0;i<m_line_to_draw.size()-2;++i) {
+ if (splines)
+ dc.DrawLine(m_rect.GetLeft()+i,(m_line_to_draw)[i],m_rect.GetLeft()+i+1,(m_line_to_draw)[i+1]);
+ else {
+ dc.DrawLine(m_rect.GetLeft()+i,(m_line_to_draw)[i],m_rect.GetLeft()+i+1,(m_line_to_draw)[i]);
+ dc.DrawLine(m_rect.GetLeft()+i+1,(m_line_to_draw)[i],m_rect.GetLeft()+i+1,(m_line_to_draw)[i+1]);
+ }
+ }
+ }
+
+ // draw draggable buttons
+ dc.SetBrush(*wxBLUE_BRUSH);
+ dc.SetPen( wxPen( wxColor(0,0,0), 1 ) );
+ for (auto& button : m_buttons)
+ //dc.DrawRectangle(math_to_screen(button.get_pos())-wxPoint(side/2.,side/2.), wxSize(side,side));
+ dc.DrawCircle(math_to_screen(button.get_pos()),side/2.);
+ //dc.DrawRectangle(math_to_screen(button.get_pos()-wxPoint2DDouble(0.125,0))-wxPoint(0,5),wxSize(50,10));
+
+ // draw x-axis:
+ float last_mark = -10000;
+ for (float math_x=int(visible_area.m_x*10)/10 ; math_x < (visible_area.m_x+visible_area.m_width) ; math_x+=0.1) {
+ int x = math_to_screen(wxPoint2DDouble(math_x,visible_area.m_y)).x;
+ int y = m_rect.GetBottom();
+ if (x-last_mark < 50) continue;
+ dc.DrawLine(x,y+3,x,y-3);
+ dc.DrawText(wxString().Format(wxT("%.1f"), math_x),wxPoint(x-10,y+7));
+ last_mark = x;
+ }
+
+ // draw y-axis:
+ last_mark=10000;
+ for (int math_y=visible_area.m_y ; math_y < (visible_area.m_y+visible_area.m_height) ; math_y+=1) {
+ int y = math_to_screen(wxPoint2DDouble(visible_area.m_x,math_y)).y;
+ int x = m_rect.GetLeft();
+ if (last_mark-y < 50) continue;
+ dc.DrawLine(x-3,y,x+3,y);
+ dc.DrawText(wxString()<<math_y,wxPoint(x-25,y-2/*7*/));
+ last_mark = y;
+ }
+
+ // axis labels:
+ wxString label = _(L("Time")) + " ("+_(L("s"))+")";
+ int text_width = 0;
+ int text_height = 0;
+ dc.GetTextExtent(label,&text_width,&text_height);
+ dc.DrawText(label,wxPoint(0.5*(m_rect.GetRight()+m_rect.GetLeft())-text_width/2.f, m_rect.GetBottom()+25));
+ label = _(L("Volumetric speed")) + " (" + _(L("mm")) + wxString("³/", wxConvUTF8) + _(L("s")) + ")";
+ dc.GetTextExtent(label,&text_width,&text_height);
+ dc.DrawRotatedText(label,wxPoint(0,0.5*(m_rect.GetBottom()+m_rect.GetTop())+text_width/2.f),90);
+}
+
+void Chart::mouse_right_button_clicked(wxMouseEvent& event) {
+ if (!manual_points_manipulation)
+ return;
+ wxPoint point = event.GetPosition();
+ int button_index = which_button_is_clicked(point);
+ if (button_index != -1 && m_buttons.size()>2) {
+ m_buttons.erase(m_buttons.begin()+button_index);
+ recalculate_line();
+ }
+}
+
+
+
+void Chart::mouse_clicked(wxMouseEvent& event) {
+ wxPoint point = event.GetPosition();
+ int button_index = which_button_is_clicked(point);
+ if ( button_index != -1) {
+ m_dragged = &m_buttons[button_index];
+ m_previous_mouse = point;
+ }
+}
+
+
+
+void Chart::mouse_moved(wxMouseEvent& event) {
+ if (!event.Dragging() || !m_dragged) return;
+ wxPoint pos = event.GetPosition();
+ wxRect rect = m_rect;
+ rect.Deflate(side/2.);
+ if (!(rect.Contains(pos))) { // the mouse left chart area
+ mouse_left_window(event);
+ return;
+ }
+ int delta_x = pos.x - m_previous_mouse.x;
+ int delta_y = pos.y - m_previous_mouse.y;
+ m_dragged->move(fixed_x?0:double(delta_x)/m_rect.GetWidth() * visible_area.m_width,-double(delta_y)/m_rect.GetHeight() * visible_area.m_height);
+ m_previous_mouse = pos;
+ recalculate_line();
+}
+
+
+
+void Chart::mouse_double_clicked(wxMouseEvent& event) {
+ if (!manual_points_manipulation)
+ return;
+ wxPoint point = event.GetPosition();
+ if (!m_rect.Contains(point)) // the click is outside the chart
+ return;
+ m_buttons.push_back(screen_to_math(point));
+ std::sort(m_buttons.begin(),m_buttons.end());
+ recalculate_line();
+ return;
+}
+
+
+
+
+void Chart::recalculate_line() {
+ std::vector<wxPoint> points;
+ for (auto& but : m_buttons) {
+ points.push_back(wxPoint(math_to_screen(but.get_pos())));
+ if (points.size()>1 && points.back().x==points[points.size()-2].x) points.pop_back();
+ if (points.size()>1 && points.back().x > m_rect.GetRight()) {
+ points.pop_back();
+ break;
+ }
+ }
+ std::sort(points.begin(),points.end(),[](wxPoint& a,wxPoint& b) { return a.x < b.x; });
+
+ m_line_to_draw.clear();
+ m_total_volume = 0.f;
+
+
+ // Cubic spline interpolation: see https://en.wikiversity.org/wiki/Cubic_Spline_Interpolation#Methods
+ const bool boundary_first_derivative = true; // true - first derivative is 0 at the leftmost and rightmost point
+ // false - second ---- || -------
+ const int N = points.size()-1; // last point can be accessed as N, we have N+1 total points
+ std::vector<float> diag(N+1);
+ std::vector<float> mu(N+1);
+ std::vector<float> lambda(N+1);
+ std::vector<float> h(N+1);
+ std::vector<float> rhs(N+1);
+
+ // let's fill in inner equations
+ for (int i=1;i<=N;++i) h[i] = points[i].x-points[i-1].x;
+ std::fill(diag.begin(),diag.end(),2.f);
+ for (int i=1;i<=N-1;++i) {
+ mu[i] = h[i]/(h[i]+h[i+1]);
+ lambda[i] = 1.f - mu[i];
+ rhs[i] = 6 * ( float(points[i+1].y-points[i].y )/(h[i+1]*(points[i+1].x-points[i-1].x)) -
+ float(points[i].y -points[i-1].y)/(h[i] *(points[i+1].x-points[i-1].x)) );
+ }
+
+ // now fill in the first and last equations, according to boundary conditions:
+ if (boundary_first_derivative) {
+ const float endpoints_derivative = 0;
+ lambda[0] = 1;
+ mu[N] = 1;
+ rhs[0] = (6.f/h[1]) * (float(points[0].y-points[1].y)/(points[0].x-points[1].x) - endpoints_derivative);
+ rhs[N] = (6.f/h[N]) * (endpoints_derivative - float(points[N-1].y-points[N].y)/(points[N-1].x-points[N].x));
+ }
+ else {
+ lambda[0] = 0;
+ mu[N] = 0;
+ rhs[0] = 0;
+ rhs[N] = 0;
+ }
+
+ // the trilinear system is ready to be solved:
+ for (int i=1;i<=N;++i) {
+ float multiple = mu[i]/diag[i-1]; // let's subtract proper multiple of above equation
+ diag[i]-= multiple * lambda[i-1];
+ rhs[i] -= multiple * rhs[i-1];
+ }
+ // now the back substitution (vector mu contains invalid values from now on):
+ rhs[N] = rhs[N]/diag[N];
+ for (int i=N-1;i>=0;--i)
+ rhs[i] = (rhs[i]-lambda[i]*rhs[i+1])/diag[i];
+
+
+
+
+ unsigned int i=1;
+ float y=0.f;
+ for (int x=m_rect.GetLeft(); x<=m_rect.GetRight() ; ++x) {
+ if (splines) {
+ if (i<points.size()-1 && points[i].x < x ) {
+ ++i;
+ }
+ if (points[0].x > x)
+ y = points[0].y;
+ else
+ if (points[N].x < x)
+ y = points[N].y;
+ else
+ y = (rhs[i-1]*pow(points[i].x-x,3)+rhs[i]*pow(x-points[i-1].x,3)) / (6*h[i]) +
+ (points[i-1].y-rhs[i-1]*h[i]*h[i]/6.f) * (points[i].x-x)/h[i] +
+ (points[i].y -rhs[i] *h[i]*h[i]/6.f) * (x-points[i-1].x)/h[i];
+ m_line_to_draw.push_back(y);
+ }
+ else {
+ float x_math = screen_to_math(wxPoint(x,0)).m_x;
+ if (i+2<=points.size() && m_buttons[i+1].get_pos().m_x-0.125 < x_math)
+ ++i;
+ m_line_to_draw.push_back(math_to_screen(wxPoint2DDouble(x_math,m_buttons[i].get_pos().m_y)).y);
+ }
+
+
+ m_line_to_draw.back() = std::max(m_line_to_draw.back(), m_rect.GetTop()-1);
+ m_line_to_draw.back() = std::min(m_line_to_draw.back(), m_rect.GetBottom()-1);
+ m_total_volume += (m_rect.GetBottom() - m_line_to_draw.back()) * (visible_area.m_width / m_rect.GetWidth()) * (visible_area.m_height / m_rect.GetHeight());
+ }
+
+ wxPostEvent(this->GetParent(), wxCommandEvent(EVT_WIPE_TOWER_CHART_CHANGED));
+ Refresh();
+}
+
+
+
+std::vector<float> Chart::get_ramming_speed(float sampling) const {
+ std::vector<float> speeds_out;
+
+ const int number_of_samples = std::round( visible_area.m_width / sampling);
+ if (number_of_samples>0) {
+ const int dx = (m_line_to_draw.size()-1) / number_of_samples;
+ for (int j=0;j<number_of_samples;++j) {
+ float left = screen_to_math(wxPoint(0,m_line_to_draw[j*dx])).m_y;
+ float right = screen_to_math(wxPoint(0,m_line_to_draw[(j+1)*dx])).m_y;
+ speeds_out.push_back((left+right)/2.f);
+ }
+ }
+ return speeds_out;
+}
+
+
+std::vector<std::pair<float,float>> Chart::get_buttons() const {
+ std::vector<std::pair<float, float>> buttons_out;
+ for (const auto& button : m_buttons)
+ buttons_out.push_back(std::make_pair(float(button.get_pos().m_x),float(button.get_pos().m_y)));
+ return buttons_out;
+}
+
+
+
+
+BEGIN_EVENT_TABLE(Chart, wxWindow)
+EVT_MOTION(Chart::mouse_moved)
+EVT_LEFT_DOWN(Chart::mouse_clicked)
+EVT_LEFT_UP(Chart::mouse_released)
+EVT_LEFT_DCLICK(Chart::mouse_double_clicked)
+EVT_RIGHT_DOWN(Chart::mouse_right_button_clicked)
+EVT_LEAVE_WINDOW(Chart::mouse_left_window)
+EVT_PAINT(Chart::paint_event)
+END_EVENT_TABLE()
diff --git a/src/slic3r/GUI/RammingChart.hpp b/src/slic3r/GUI/RammingChart.hpp
new file mode 100644
index 000000000..7d3b9a962
--- /dev/null
+++ b/src/slic3r/GUI/RammingChart.hpp
@@ -0,0 +1,115 @@
+#ifndef RAMMING_CHART_H_
+#define RAMMING_CHART_H_
+
+#include <vector>
+#include <wx/wxprec.h>
+#ifndef WX_PRECOMP
+ #include <wx/wx.h>
+#endif
+
+wxDECLARE_EVENT(EVT_WIPE_TOWER_CHART_CHANGED, wxCommandEvent);
+
+
+class Chart : public wxWindow {
+
+public:
+ Chart(wxWindow* parent, wxRect rect,const std::vector<std::pair<float,float>>& initial_buttons,int ramming_speed_size, float sampling) :
+ wxWindow(parent,wxID_ANY,rect.GetTopLeft(),rect.GetSize())
+ {
+ SetBackgroundStyle(wxBG_STYLE_PAINT);
+ m_rect = wxRect(wxPoint(50,0),rect.GetSize()-wxSize(50,50));
+ visible_area = wxRect2DDouble(0.0, 0.0, sampling*ramming_speed_size, 20.);
+ m_buttons.clear();
+ if (initial_buttons.size()>0)
+ for (const auto& pair : initial_buttons)
+ m_buttons.push_back(wxPoint2DDouble(pair.first,pair.second));
+ recalculate_line();
+ }
+ void set_xy_range(float x,float y) {
+ x = int(x/0.5) * 0.5;
+ if (x>=0) visible_area.SetRight(x);
+ if (y>=0) visible_area.SetBottom(y);
+ recalculate_line();
+ }
+ float get_volume() const { return m_total_volume; }
+ float get_time() const { return visible_area.m_width; }
+
+ std::vector<float> get_ramming_speed(float sampling) const; //returns sampled ramming speed
+ std::vector<std::pair<float,float>> get_buttons() const; // returns buttons position
+
+ void draw();
+
+ void mouse_clicked(wxMouseEvent& event);
+ void mouse_right_button_clicked(wxMouseEvent& event);
+ void mouse_moved(wxMouseEvent& event);
+ void mouse_double_clicked(wxMouseEvent& event);
+ void mouse_left_window(wxMouseEvent&) { m_dragged = nullptr; }
+ void mouse_released(wxMouseEvent&) { m_dragged = nullptr; }
+ void paint_event(wxPaintEvent&) { draw(); }
+ DECLARE_EVENT_TABLE()
+
+
+
+private:
+ static const bool fixed_x = true;
+ static const bool splines = true;
+ static const bool manual_points_manipulation = false;
+ static const int side = 10; // side of draggable button
+
+ class ButtonToDrag {
+ public:
+ bool operator<(const ButtonToDrag& a) const { return m_pos.m_x < a.m_pos.m_x; }
+ ButtonToDrag(wxPoint2DDouble pos) : m_pos{pos} {};
+ wxPoint2DDouble get_pos() const { return m_pos; }
+ void move(double x,double y) { m_pos.m_x+=x; m_pos.m_y+=y; }
+ private:
+ wxPoint2DDouble m_pos; // position in math coordinates
+ };
+
+
+
+ wxPoint math_to_screen(const wxPoint2DDouble& math) const {
+ wxPoint screen;
+ screen.x = (math.m_x-visible_area.m_x) * (m_rect.GetWidth() / visible_area.m_width );
+ screen.y = (math.m_y-visible_area.m_y) * (m_rect.GetHeight() / visible_area.m_height );
+ screen.y *= -1;
+ screen += m_rect.GetLeftBottom();
+ return screen;
+ }
+ wxPoint2DDouble screen_to_math(const wxPoint& screen) const {
+ wxPoint2DDouble math = screen;
+ math -= m_rect.GetLeftBottom();
+ math.m_y *= -1;
+ math.m_x *= visible_area.m_width / m_rect.GetWidth(); // scales to [0;1]x[0,1]
+ math.m_y *= visible_area.m_height / m_rect.GetHeight();
+ return (math+visible_area.GetLeftTop());
+ }
+
+ int which_button_is_clicked(const wxPoint& point) const {
+ if (!m_rect.Contains(point))
+ return -1;
+ for (unsigned int i=0;i<m_buttons.size();++i) {
+ wxRect rect(math_to_screen(m_buttons[i].get_pos())-wxPoint(side/2.,side/2.),wxSize(side,side)); // bounding rectangle of this button
+ if ( rect.Contains(point) )
+ return i;
+ }
+ return (-1);
+ }
+
+
+ void recalculate_line();
+ void recalculate_volume();
+
+
+ wxRect m_rect; // rectangle on screen the chart is mapped into (screen coordinates)
+ wxPoint m_previous_mouse;
+ std::vector<ButtonToDrag> m_buttons;
+ std::vector<int> m_line_to_draw;
+ wxRect2DDouble visible_area;
+ ButtonToDrag* m_dragged = nullptr;
+ float m_total_volume = 0.f;
+
+};
+
+
+#endif // RAMMING_CHART_H_ \ No newline at end of file
diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp
new file mode 100644
index 000000000..e0db63803
--- /dev/null
+++ b/src/slic3r/GUI/Tab.cpp
@@ -0,0 +1,3033 @@
+#include "../../libslic3r/GCodeSender.hpp"
+#include "Tab.hpp"
+#include "PresetBundle.hpp"
+#include "PresetHints.hpp"
+#include "../../libslic3r/Utils.hpp"
+
+#include "slic3r/Utils/Http.hpp"
+#include "slic3r/Utils/PrintHost.hpp"
+#include "slic3r/Utils/Serial.hpp"
+#include "BonjourDialog.hpp"
+#include "WipeTowerDialog.hpp"
+#include "ButtonsDescription.hpp"
+
+#include <wx/app.h>
+#include <wx/button.h>
+#include <wx/scrolwin.h>
+#include <wx/sizer.h>
+
+#include <wx/bmpcbox.h>
+#include <wx/bmpbuttn.h>
+#include <wx/treectrl.h>
+#include <wx/imaglist.h>
+#include <wx/settings.h>
+#include <wx/filedlg.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include "wxExtensions.hpp"
+#include <wx/wupdlock.h>
+
+#include <chrono>
+
+namespace Slic3r {
+namespace GUI {
+
+static wxString dots("…", wxConvUTF8);
+
+// sub new
+void Tab::create_preset_tab(PresetBundle *preset_bundle)
+{
+ m_preset_bundle = preset_bundle;
+
+ // Vertical sizer to hold the choice menu and the rest of the page.
+#ifdef __WXOSX__
+ auto *main_sizer = new wxBoxSizer(wxVERTICAL);
+ main_sizer->SetSizeHints(this);
+ this->SetSizer(main_sizer);
+
+ // Create additional panel to Fit() it from OnActivate()
+ // It's needed for tooltip showing on OSX
+ m_tmp_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL);
+ auto panel = m_tmp_panel;
+ auto sizer = new wxBoxSizer(wxVERTICAL);
+ m_tmp_panel->SetSizer(sizer);
+ m_tmp_panel->Layout();
+
+ main_sizer->Add(m_tmp_panel, 1, wxEXPAND | wxALL, 0);
+#else
+ Tab *panel = this;
+ auto *sizer = new wxBoxSizer(wxVERTICAL);
+ sizer->SetSizeHints(panel);
+ panel->SetSizer(sizer);
+#endif //__WXOSX__
+
+ // preset chooser
+ m_presets_choice = new wxBitmapComboBox(panel, wxID_ANY, "", wxDefaultPosition, wxSize(270, -1), 0, 0,wxCB_READONLY);
+
+ auto color = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
+
+ //buttons
+ wxBitmap bmpMenu;
+ bmpMenu = wxBitmap(from_u8(Slic3r::var("disk.png")), wxBITMAP_TYPE_PNG);
+ m_btn_save_preset = new wxBitmapButton(panel, wxID_ANY, bmpMenu, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
+ if (wxMSW) m_btn_save_preset->SetBackgroundColour(color);
+ bmpMenu = wxBitmap(from_u8(Slic3r::var("delete.png")), wxBITMAP_TYPE_PNG);
+ m_btn_delete_preset = new wxBitmapButton(panel, wxID_ANY, bmpMenu, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
+ if (wxMSW) m_btn_delete_preset->SetBackgroundColour(color);
+
+ m_show_incompatible_presets = false;
+ m_bmp_show_incompatible_presets.LoadFile(from_u8(Slic3r::var("flag-red-icon.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_hide_incompatible_presets.LoadFile(from_u8(Slic3r::var("flag-green-icon.png")), wxBITMAP_TYPE_PNG);
+ m_btn_hide_incompatible_presets = new wxBitmapButton(panel, wxID_ANY, m_bmp_hide_incompatible_presets, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
+ if (wxMSW) m_btn_hide_incompatible_presets->SetBackgroundColour(color);
+
+ m_btn_save_preset->SetToolTip(_(L("Save current ")) + m_title);
+ m_btn_delete_preset->SetToolTip(_(L("Delete this preset")));
+ m_btn_delete_preset->Disable();
+
+ m_undo_btn = new wxButton(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER);
+ m_undo_to_sys_btn = new wxButton(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER);
+ m_question_btn = new wxButton(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER);
+ if (wxMSW) {
+ m_undo_btn->SetBackgroundColour(color);
+ m_undo_to_sys_btn->SetBackgroundColour(color);
+ m_question_btn->SetBackgroundColour(color);
+ }
+
+ m_question_btn->SetToolTip(_(L("Hover the cursor over buttons to find more information \n"
+ "or click this button.")));
+
+ // Determine the theme color of OS (dark or light)
+ auto luma = get_colour_approx_luma(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+ // Bitmaps to be shown on the "Revert to system" aka "Lock to system" button next to each input field.
+ m_bmp_value_lock .LoadFile(from_u8(var("sys_lock.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_value_unlock .LoadFile(from_u8(var(luma >= 128 ? "sys_unlock.png" : "sys_unlock_grey.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_non_system = &m_bmp_white_bullet;
+ // Bitmaps to be shown on the "Undo user changes" button next to each input field.
+ m_bmp_value_revert .LoadFile(from_u8(var(luma >= 128 ? "action_undo.png" : "action_undo_grey.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_white_bullet .LoadFile(from_u8(var("bullet_white.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_question .LoadFile(from_u8(var("question_mark_01.png")), wxBITMAP_TYPE_PNG);
+
+ fill_icon_descriptions();
+ set_tooltips_text();
+
+ m_undo_btn->SetBitmap(m_bmp_white_bullet);
+ m_undo_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent){ on_roll_back_value(); }));
+ m_undo_to_sys_btn->SetBitmap(m_bmp_white_bullet);
+ m_undo_to_sys_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent){ on_roll_back_value(true); }));
+ m_question_btn->SetBitmap(m_bmp_question);
+ m_question_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent)
+ {
+ auto dlg = new ButtonsDescription(this, &m_icon_descriptions);
+ if (dlg->ShowModal() == wxID_OK){
+ // Colors for ui "decoration"
+ for (Tab *tab : get_tabs_list()){
+ tab->m_sys_label_clr = get_label_clr_sys();
+ tab->m_modified_label_clr = get_label_clr_modified();
+ tab->update_labels_colour();
+ }
+ }
+ }));
+
+ // Colors for ui "decoration"
+ m_sys_label_clr = get_label_clr_sys();
+ m_modified_label_clr = get_label_clr_modified();
+ m_default_text_clr = get_label_clr_default();
+
+ m_hsizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(m_hsizer, 0, wxBOTTOM, 3);
+ m_hsizer->Add(m_presets_choice, 1, wxLEFT | wxRIGHT | wxTOP | wxALIGN_CENTER_VERTICAL, 3);
+ m_hsizer->AddSpacer(4);
+ m_hsizer->Add(m_btn_save_preset, 0, wxALIGN_CENTER_VERTICAL);
+ m_hsizer->AddSpacer(4);
+ m_hsizer->Add(m_btn_delete_preset, 0, wxALIGN_CENTER_VERTICAL);
+ m_hsizer->AddSpacer(16);
+ m_hsizer->Add(m_btn_hide_incompatible_presets, 0, wxALIGN_CENTER_VERTICAL);
+ m_hsizer->AddSpacer(64);
+ m_hsizer->Add(m_undo_to_sys_btn, 0, wxALIGN_CENTER_VERTICAL);
+ m_hsizer->Add(m_undo_btn, 0, wxALIGN_CENTER_VERTICAL);
+ m_hsizer->AddSpacer(32);
+ m_hsizer->Add(m_question_btn, 0, wxALIGN_CENTER_VERTICAL);
+// m_hsizer->Add(m_cc_presets_choice, 1, wxLEFT | wxRIGHT | wxTOP | wxALIGN_CENTER_VERTICAL, 3);
+
+ //Horizontal sizer to hold the tree and the selected page.
+ m_hsizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(m_hsizer, 1, wxEXPAND, 0);
+
+ //left vertical sizer
+ m_left_sizer = new wxBoxSizer(wxVERTICAL);
+ m_hsizer->Add(m_left_sizer, 0, wxEXPAND | wxLEFT | wxTOP | wxBOTTOM, 3);
+
+ // tree
+ m_treectrl = new wxTreeCtrl(panel, wxID_ANY, wxDefaultPosition, wxSize(185, -1),
+ wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_SUNKEN | wxWANTS_CHARS);
+ m_left_sizer->Add(m_treectrl, 1, wxEXPAND);
+ m_icons = new wxImageList(16, 16, true, 1);
+ // Index of the last icon inserted into $self->{icons}.
+ m_icon_count = -1;
+ m_treectrl->AssignImageList(m_icons);
+ m_treectrl->AddRoot("root");
+ m_treectrl->SetIndent(0);
+ m_disable_tree_sel_changed_event = 0;
+
+ m_treectrl->Bind(wxEVT_TREE_SEL_CHANGED, &Tab::OnTreeSelChange, this);
+ m_treectrl->Bind(wxEVT_KEY_DOWN, &Tab::OnKeyDown, this);
+
+ m_presets_choice->Bind(wxEVT_COMBOBOX, ([this](wxCommandEvent e){
+ //! Because of The MSW and GTK version of wxBitmapComboBox derived from wxComboBox,
+ //! but the OSX version derived from wxOwnerDrawnCombo, instead of:
+ //! select_preset(m_presets_choice->GetStringSelection().ToStdString());
+ //! we doing next:
+ int selected_item = m_presets_choice->GetSelection();
+ if (m_selected_preset_item == selected_item && !m_presets->current_is_dirty())
+ return;
+ if (selected_item >= 0){
+ std::string selected_string = m_presets_choice->GetString(selected_item).ToUTF8().data();
+ if (selected_string.find("-------") == 0
+ /*selected_string == "------- System presets -------" ||
+ selected_string == "------- User presets -------"*/){
+ m_presets_choice->SetSelection(m_selected_preset_item);
+ return;
+ }
+ m_selected_preset_item = selected_item;
+ select_preset(selected_string);
+ }
+ }));
+
+ m_btn_save_preset->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e){ save_preset(); }));
+ m_btn_delete_preset->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e){ delete_preset(); }));
+ m_btn_hide_incompatible_presets->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e){
+ toggle_show_hide_incompatible();
+ }));
+
+ // Initialize the DynamicPrintConfig by default keys/values.
+ build();
+ rebuild_page_tree();
+ update();
+}
+
+void Tab::load_initial_data()
+{
+ m_config = &m_presets->get_edited_preset().config;
+ m_bmp_non_system = m_presets->get_selected_preset_parent() ? &m_bmp_value_unlock : &m_bmp_white_bullet;
+ m_ttg_non_system = m_presets->get_selected_preset_parent() ? &m_ttg_value_unlock : &m_ttg_white_bullet_ns;
+ m_tt_non_system = m_presets->get_selected_preset_parent() ? &m_tt_value_unlock : &m_ttg_white_bullet_ns;
+}
+
+Slic3r::GUI::PageShp Tab::add_options_page(const wxString& title, const std::string& icon, bool is_extruder_pages /*= false*/)
+{
+ // Index of icon in an icon list $self->{icons}.
+ auto icon_idx = 0;
+ if (!icon.empty()) {
+ icon_idx = (m_icon_index.find(icon) == m_icon_index.end()) ? -1 : m_icon_index.at(icon);
+ if (icon_idx == -1) {
+ // Add a new icon to the icon list.
+ const auto img_icon = new wxIcon(from_u8(Slic3r::var(icon)), wxBITMAP_TYPE_PNG);
+ m_icons->Add(*img_icon);
+ icon_idx = ++m_icon_count;
+ m_icon_index[icon] = icon_idx;
+ }
+ }
+ // Initialize the page.
+#ifdef __WXOSX__
+ auto panel = m_tmp_panel;
+#else
+ auto panel = this;
+#endif
+ PageShp page(new Page(panel, title, icon_idx));
+ page->SetScrollbars(1, 1, 1, 1);
+ page->Hide();
+ m_hsizer->Add(page.get(), 1, wxEXPAND | wxLEFT, 5);
+
+ if (!is_extruder_pages)
+ m_pages.push_back(page);
+
+ page->set_config(m_config);
+ return page;
+}
+
+void Tab::OnActivate()
+{
+#ifdef __WXOSX__
+ wxWindowUpdateLocker noUpdates(this);
+
+ auto size = GetSizer()->GetSize();
+ m_tmp_panel->GetSizer()->SetMinSize(size.x + m_size_move, size.y);
+ Fit();
+ m_size_move *= -1;
+#endif // __WXOSX__
+}
+
+void Tab::update_labels_colour()
+{
+ Freeze();
+ //update options "decoration"
+ for (const auto opt : m_options_list)
+ {
+ const wxColour *color = &m_sys_label_clr;
+
+ // value isn't equal to system value
+ if ((opt.second & osSystemValue) == 0){
+ // value is equal to last saved
+ if ((opt.second & osInitValue) != 0)
+ color = &m_default_text_clr;
+ // value is modified
+ else
+ color = &m_modified_label_clr;
+ }
+ if (opt.first == "bed_shape" || opt.first == "compatible_printers") {
+ if (m_colored_Label != nullptr) {
+ m_colored_Label->SetForegroundColour(*color);
+ m_colored_Label->Refresh(true);
+ }
+ continue;
+ }
+
+ Field* field = get_field(opt.first);
+ if (field == nullptr) continue;
+ field->set_label_colour_force(color);
+ }
+ Thaw();
+
+ auto cur_item = m_treectrl->GetFirstVisibleItem();
+ while (cur_item){
+ auto title = m_treectrl->GetItemText(cur_item);
+ for (auto page : m_pages)
+ {
+ if (page->title() != title)
+ continue;
+
+ const wxColor *clr = !page->m_is_nonsys_values ? &m_sys_label_clr :
+ page->m_is_modified_values ? &m_modified_label_clr :
+ &m_default_text_clr;
+
+ m_treectrl->SetItemTextColour(cur_item, *clr);
+ break;
+ }
+ cur_item = m_treectrl->GetNextVisible(cur_item);
+ }
+}
+
+// Update UI according to changes
+void Tab::update_changed_ui()
+{
+ if (m_postpone_update_ui)
+ return;
+
+ const bool deep_compare = (m_name == "printer" || m_name == "sla_material");
+ auto dirty_options = m_presets->current_dirty_options(deep_compare);
+ auto nonsys_options = m_presets->current_different_from_parent_options(deep_compare);
+ if (name() == "printer"){
+ TabPrinter* tab = static_cast<TabPrinter*>(this);
+ if (tab->m_initial_extruders_count != tab->m_extruders_count)
+ dirty_options.emplace_back("extruders_count");
+ if (tab->m_sys_extruders_count != tab->m_extruders_count)
+ nonsys_options.emplace_back("extruders_count");
+ }
+
+ for (auto& it : m_options_list)
+ it.second = m_opt_status_value;
+
+ for (auto opt_key : dirty_options) m_options_list[opt_key] &= ~osInitValue;
+ for (auto opt_key : nonsys_options) m_options_list[opt_key] &= ~osSystemValue;
+
+ Freeze();
+ //update options "decoration"
+ for (const auto opt : m_options_list)
+ {
+ bool is_nonsys_value = false;
+ bool is_modified_value = true;
+ const wxBitmap *sys_icon = &m_bmp_value_lock;
+ const wxBitmap *icon = &m_bmp_value_revert;
+
+ const wxColour *color = &m_sys_label_clr;
+
+ const wxString *sys_tt = &m_tt_value_lock;
+ const wxString *tt = &m_tt_value_revert;
+
+ // value isn't equal to system value
+ if ((opt.second & osSystemValue) == 0){
+ is_nonsys_value = true;
+ sys_icon = m_bmp_non_system;
+ sys_tt = m_tt_non_system;
+ // value is equal to last saved
+ if ((opt.second & osInitValue) != 0)
+ color = &m_default_text_clr;
+ // value is modified
+ else
+ color = &m_modified_label_clr;
+ }
+ if ((opt.second & osInitValue) != 0)
+ {
+ is_modified_value = false;
+ icon = &m_bmp_white_bullet;
+ tt = &m_tt_white_bullet;
+ }
+ if (opt.first == "bed_shape" || opt.first == "compatible_printers") {
+ if (m_colored_Label != nullptr) {
+ m_colored_Label->SetForegroundColour(*color);
+ m_colored_Label->Refresh(true);
+ }
+ continue;
+ }
+
+ Field* field = get_field(opt.first);
+ if (field == nullptr) continue;
+ field->m_is_nonsys_value = is_nonsys_value;
+ field->m_is_modified_value = is_modified_value;
+ field->set_undo_bitmap(icon);
+ field->set_undo_to_sys_bitmap(sys_icon);
+ field->set_undo_tooltip(tt);
+ field->set_undo_to_sys_tooltip(sys_tt);
+ field->set_label_colour(color);
+ }
+ Thaw();
+
+ wxTheApp->CallAfter([this]() {
+ update_changed_tree_ui();
+ });
+}
+
+void Tab::init_options_list()
+{
+ if (!m_options_list.empty())
+ m_options_list.clear();
+
+ for (const auto opt_key : m_config->keys())
+ m_options_list.emplace(opt_key, m_opt_status_value);
+}
+
+template<class T>
+void add_correct_opts_to_options_list(const std::string &opt_key, std::map<std::string, int>& map, Tab *tab, const int& value)
+{
+ T *opt_cur = static_cast<T*>(tab->m_config->option(opt_key));
+ for (int i = 0; i < opt_cur->values.size(); i++)
+ map.emplace(opt_key + "#" + std::to_string(i), value);
+}
+
+void TabPrinter::init_options_list()
+{
+ if (!m_options_list.empty())
+ m_options_list.clear();
+
+ for (const auto opt_key : m_config->keys())
+ {
+ if (opt_key == "bed_shape"){
+ m_options_list.emplace(opt_key, m_opt_status_value);
+ continue;
+ }
+ switch (m_config->option(opt_key)->type())
+ {
+ case coInts: add_correct_opts_to_options_list<ConfigOptionInts >(opt_key, m_options_list, this, m_opt_status_value); break;
+ case coBools: add_correct_opts_to_options_list<ConfigOptionBools >(opt_key, m_options_list, this, m_opt_status_value); break;
+ case coFloats: add_correct_opts_to_options_list<ConfigOptionFloats >(opt_key, m_options_list, this, m_opt_status_value); break;
+ case coStrings: add_correct_opts_to_options_list<ConfigOptionStrings >(opt_key, m_options_list, this, m_opt_status_value); break;
+ case coPercents:add_correct_opts_to_options_list<ConfigOptionPercents >(opt_key, m_options_list, this, m_opt_status_value); break;
+ case coPoints: add_correct_opts_to_options_list<ConfigOptionPoints >(opt_key, m_options_list, this, m_opt_status_value); break;
+ default: m_options_list.emplace(opt_key, m_opt_status_value); break;
+ }
+ }
+ m_options_list.emplace("extruders_count", m_opt_status_value);
+}
+
+void TabSLAMaterial::init_options_list()
+{
+ if (!m_options_list.empty())
+ m_options_list.clear();
+
+ for (const auto opt_key : m_config->keys())
+ {
+ if (opt_key == "compatible_printers"){
+ m_options_list.emplace(opt_key, m_opt_status_value);
+ continue;
+ }
+ switch (m_config->option(opt_key)->type())
+ {
+ case coInts: add_correct_opts_to_options_list<ConfigOptionInts >(opt_key, m_options_list, this, m_opt_status_value); break;
+ case coBools: add_correct_opts_to_options_list<ConfigOptionBools >(opt_key, m_options_list, this, m_opt_status_value); break;
+ case coFloats: add_correct_opts_to_options_list<ConfigOptionFloats >(opt_key, m_options_list, this, m_opt_status_value); break;
+ case coStrings: add_correct_opts_to_options_list<ConfigOptionStrings >(opt_key, m_options_list, this, m_opt_status_value); break;
+ case coPercents:add_correct_opts_to_options_list<ConfigOptionPercents >(opt_key, m_options_list, this, m_opt_status_value); break;
+ case coPoints: add_correct_opts_to_options_list<ConfigOptionPoints >(opt_key, m_options_list, this, m_opt_status_value); break;
+ default: m_options_list.emplace(opt_key, m_opt_status_value); break;
+ }
+ }
+}
+
+void Tab::get_sys_and_mod_flags(const std::string& opt_key, bool& sys_page, bool& modified_page)
+{
+ auto opt = m_options_list.find(opt_key);
+ if (sys_page) sys_page = (opt->second & osSystemValue) != 0;
+ if (!modified_page) modified_page = (opt->second & osInitValue) == 0;
+}
+
+void Tab::update_changed_tree_ui()
+{
+ auto cur_item = m_treectrl->GetFirstVisibleItem();
+ auto selection = m_treectrl->GetItemText(m_treectrl->GetSelection());
+ while (cur_item){
+ auto title = m_treectrl->GetItemText(cur_item);
+ for (auto page : m_pages)
+ {
+ if (page->title() != title)
+ continue;
+ bool sys_page = true;
+ bool modified_page = false;
+ if (title == _("General")){
+ std::initializer_list<const char*> optional_keys{ "extruders_count", "bed_shape" };
+ for (auto &opt_key : optional_keys) {
+ get_sys_and_mod_flags(opt_key, sys_page, modified_page);
+ }
+ }
+ if (title == _("Dependencies")){
+ if (name() != "printer")
+ get_sys_and_mod_flags("compatible_printers", sys_page, modified_page);
+ else {
+ sys_page = m_presets->get_selected_preset_parent() ? true:false;
+ modified_page = false;
+ }
+ }
+ for (auto group : page->m_optgroups)
+ {
+ if (!sys_page && modified_page)
+ break;
+ for (t_opt_map::iterator it = group->m_opt_map.begin(); it != group->m_opt_map.end(); ++it) {
+ const std::string& opt_key = it->first;
+ get_sys_and_mod_flags(opt_key, sys_page, modified_page);
+ }
+ }
+
+ const wxColor *clr = sys_page ? &m_sys_label_clr :
+ modified_page ? &m_modified_label_clr :
+ &m_default_text_clr;
+
+ if (page->set_item_colour(clr))
+ m_treectrl->SetItemTextColour(cur_item, *clr);
+
+ page->m_is_nonsys_values = !sys_page;
+ page->m_is_modified_values = modified_page;
+
+ if (selection == title){
+ m_is_nonsys_values = page->m_is_nonsys_values;
+ m_is_modified_values = page->m_is_modified_values;
+ }
+ break;
+ }
+ auto next_item = m_treectrl->GetNextVisible(cur_item);
+ cur_item = next_item;
+ }
+ update_undo_buttons();
+}
+
+void Tab::update_undo_buttons()
+{
+ m_undo_btn->SetBitmap(m_is_modified_values ? m_bmp_value_revert : m_bmp_white_bullet);
+ m_undo_to_sys_btn->SetBitmap(m_is_nonsys_values ? *m_bmp_non_system : m_bmp_value_lock);
+
+ m_undo_btn->SetToolTip(m_is_modified_values ? m_ttg_value_revert : m_ttg_white_bullet);
+ m_undo_to_sys_btn->SetToolTip(m_is_nonsys_values ? *m_ttg_non_system : m_ttg_value_lock);
+}
+
+void Tab::on_roll_back_value(const bool to_sys /*= true*/)
+{
+ int os;
+ if (to_sys) {
+ if (!m_is_nonsys_values) return;
+ os = osSystemValue;
+ }
+ else {
+ if (!m_is_modified_values) return;
+ os = osInitValue;
+ }
+
+ m_postpone_update_ui = true;
+
+ auto selection = m_treectrl->GetItemText(m_treectrl->GetSelection());
+ for (auto page : m_pages)
+ if (page->title() == selection) {
+ for (auto group : page->m_optgroups){
+ if (group->title == _("Capabilities")){
+ if ((m_options_list["extruders_count"] & os) == 0)
+ to_sys ? group->back_to_sys_value("extruders_count") : group->back_to_initial_value("extruders_count");
+ }
+ if (group->title == _("Size and coordinates")){
+ if ((m_options_list["bed_shape"] & os) == 0){
+ to_sys ? group->back_to_sys_value("bed_shape") : group->back_to_initial_value("bed_shape");
+ load_key_value("bed_shape", true/*some value*/, true);
+ }
+
+ }
+ if (group->title == _("Profile dependencies") && name() != "printer"){
+ if ((m_options_list["compatible_printers"] & os) == 0){
+ to_sys ? group->back_to_sys_value("compatible_printers") : group->back_to_initial_value("compatible_printers");
+ load_key_value("compatible_printers", true/*some value*/, true);
+
+ bool is_empty = m_config->option<ConfigOptionStrings>("compatible_printers")->values.empty();
+ m_compatible_printers_checkbox->SetValue(is_empty);
+ is_empty ? m_compatible_printers_btn->Disable() : m_compatible_printers_btn->Enable();
+ }
+ }
+ for (t_opt_map::iterator it = group->m_opt_map.begin(); it != group->m_opt_map.end(); ++it) {
+ const std::string& opt_key = it->first;
+ if ((m_options_list[opt_key] & os) == 0)
+ to_sys ? group->back_to_sys_value(opt_key) : group->back_to_initial_value(opt_key);
+ }
+ }
+ break;
+ }
+
+ m_postpone_update_ui = false;
+ update_changed_ui();
+}
+
+// Update the combo box label of the selected preset based on its "dirty" state,
+// comparing the selected preset config with $self->{config}.
+void Tab::update_dirty(){
+ m_presets->update_dirty_ui(m_presets_choice);
+ on_presets_changed();
+ update_changed_ui();
+// update_dirty_presets(m_cc_presets_choice);
+}
+
+void Tab::update_tab_ui()
+{
+ m_selected_preset_item = m_presets->update_tab_ui(m_presets_choice, m_show_incompatible_presets);
+// update_tab_presets(m_cc_presets_choice, m_show_incompatible_presets);
+// update_presetsctrl(m_presetctrl, m_show_incompatible_presets);
+}
+
+// Load a provied DynamicConfig into the tab, modifying the active preset.
+// This could be used for example by setting a Wipe Tower position by interactive manipulation in the 3D view.
+void Tab::load_config(const DynamicPrintConfig& config)
+{
+ bool modified = 0;
+ for(auto opt_key : m_config->diff(config)) {
+ m_config->set_key_value(opt_key, config.option(opt_key)->clone());
+ modified = 1;
+ }
+ if (modified) {
+ update_dirty();
+ //# Initialize UI components with the config values.
+ reload_config();
+ update();
+ }
+}
+
+// Reload current $self->{config} (aka $self->{presets}->edited_preset->config) into the UI fields.
+void Tab::reload_config(){
+ Freeze();
+ for (auto page : m_pages)
+ page->reload_config();
+ Thaw();
+}
+
+Field* Tab::get_field(const t_config_option_key& opt_key, int opt_index/* = -1*/) const
+{
+ Field* field = nullptr;
+ for (auto page : m_pages){
+ field = page->get_field(opt_key, opt_index);
+ if (field != nullptr)
+ return field;
+ }
+ return field;
+}
+
+// Set a key/value pair on this page. Return true if the value has been modified.
+// Currently used for distributing extruders_count over preset pages of Slic3r::GUI::Tab::Printer
+// after a preset is loaded.
+bool Tab::set_value(const t_config_option_key& opt_key, const boost::any& value){
+ bool changed = false;
+ for(auto page: m_pages) {
+ if (page->set_value(opt_key, value))
+ changed = true;
+ }
+ return changed;
+}
+
+// To be called by custom widgets, load a value into a config,
+// update the preset selection boxes (the dirty flags)
+// If value is saved before calling this function, put saved_value = true,
+// and value can be some random value because in this case it will not been used
+void Tab::load_key_value(const std::string& opt_key, const boost::any& value, bool saved_value /*= false*/)
+{
+ if (!saved_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) {
+ // 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();
+ update();
+}
+
+extern wxFrame *g_wxMainFrame;
+
+void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
+{
+ if (m_event_value_change > 0) {
+ wxCommandEvent event(m_event_value_change);
+ std::string str_out = opt_key + " " + m_name;
+ event.SetString(str_out);
+ if (opt_key == "extruders_count")
+ {
+ int val = boost::any_cast<size_t>(value);
+ event.SetInt(val);
+ }
+
+ if (opt_key == "printer_technology")
+ {
+ int val = boost::any_cast<PrinterTechnology>(value);
+ event.SetInt(val);
+ g_wxMainFrame->ProcessWindowEvent(event);
+ return;
+ }
+
+ g_wxMainFrame->ProcessWindowEvent(event);
+ }
+ if (opt_key == "fill_density")
+ {
+ boost::any val = get_optgroup(ogFrequentlyChangingParameters)->get_config_value(*m_config, opt_key);
+ get_optgroup(ogFrequentlyChangingParameters)->set_value(opt_key, val);
+ }
+ if (opt_key == "support_material" || opt_key == "support_material_buildplate_only")
+ {
+ wxString new_selection = !m_config->opt_bool("support_material") ?
+ _("None") :
+ m_config->opt_bool("support_material_buildplate_only") ?
+ _("Support on build plate only") :
+ _("Everywhere");
+ get_optgroup(ogFrequentlyChangingParameters)->set_value("support", new_selection);
+ }
+ if (opt_key == "brim_width")
+ {
+ bool val = m_config->opt_float("brim_width") > 0.0 ? true : false;
+ get_optgroup(ogFrequentlyChangingParameters)->set_value("brim", val);
+ }
+
+ if (opt_key == "wipe_tower" || opt_key == "single_extruder_multi_material" || opt_key == "extruders_count" )
+ update_wiping_button_visibility();
+
+ update();
+}
+
+// Show/hide the 'purging volumes' button
+void Tab::update_wiping_button_visibility() {
+ if (get_preset_bundle()->printers.get_selected_preset().printer_technology() == ptSLA)
+ return; // ys_FIXME
+ bool wipe_tower_enabled = dynamic_cast<ConfigOptionBool*>( (m_preset_bundle->prints.get_edited_preset().config ).option("wipe_tower"))->value;
+ bool multiple_extruders = dynamic_cast<ConfigOptionFloats*>((m_preset_bundle->printers.get_edited_preset().config).option("nozzle_diameter"))->values.size() > 1;
+ bool single_extruder_mm = dynamic_cast<ConfigOptionBool*>( (m_preset_bundle->printers.get_edited_preset().config).option("single_extruder_multi_material"))->value;
+
+ get_wiping_dialog_button()->Show(wipe_tower_enabled && multiple_extruders && single_extruder_mm);
+
+ (get_wiping_dialog_button()->GetParent())->Layout();
+}
+
+
+// Call a callback to update the selection of presets on the platter:
+// To update the content of the selection boxes,
+// to update the filament colors of the selection boxes,
+// to update the "dirty" flags of the selection boxes,
+// to uddate number of "filament" selection boxes when the number of extruders change.
+void Tab::on_presets_changed()
+{
+ if (m_event_presets_changed > 0) {
+ wxCommandEvent event(m_event_presets_changed);
+ event.SetString(m_name);
+ g_wxMainFrame->ProcessWindowEvent(event);
+ }
+ update_preset_description_line();
+}
+
+void Tab::update_preset_description_line()
+{
+ const Preset* parent = m_presets->get_selected_preset_parent();
+ const Preset& preset = m_presets->get_edited_preset();
+
+ wxString description_line = preset.is_default ?
+ _(L("It's a default preset.")) : preset.is_system ?
+ _(L("It's a system preset.")) :
+ _(L("Current preset is inherited from ")) + (parent == nullptr ?
+ "default preset." :
+ ":\n\t" + parent->name);
+
+ if (preset.is_default || preset.is_system)
+ description_line += "\n\t" + _(L("It can't be deleted or modified. ")) +
+ "\n\t" + _(L("Any modifications should be saved as a new preset inherited from this one. ")) +
+ "\n\t" + _(L("To do that please specify a new name for the preset."));
+
+ if (parent && parent->vendor)
+ {
+ description_line += "\n\n" + _(L("Additional information:")) + "\n";
+ description_line += "\t" + _(L("vendor")) + ": " + (name()=="printer" ? "\n\t\t" : "") + parent->vendor->name +
+ ", ver: " + parent->vendor->config_version.to_string();
+ if (name() == "printer"){
+ const std::string &printer_model = preset.config.opt_string("printer_model");
+ const std::string &default_print_profile = preset.config.opt_string("default_print_profile");
+ const std::vector<std::string> &default_filament_profiles = preset.config.option<ConfigOptionStrings>("default_filament_profile")->values;
+ if (!printer_model.empty())
+ description_line += "\n\n\t" + _(L("printer model")) + ": \n\t\t" + printer_model;
+ if (!default_print_profile.empty())
+ description_line += "\n\n\t" + _(L("default print profile")) + ": \n\t\t" + default_print_profile;
+ if (!default_filament_profiles.empty())
+ {
+ description_line += "\n\n\t" + _(L("default filament profile")) + ": \n\t\t";
+ for (auto& profile : default_filament_profiles){
+ if (&profile != &*default_filament_profiles.begin())
+ description_line += ", ";
+ description_line += profile;
+ }
+ }
+ }
+ }
+
+ m_parent_preset_description_line->SetText(description_line, false);
+}
+
+void Tab::update_frequently_changed_parameters()
+{
+ boost::any value = get_optgroup(ogFrequentlyChangingParameters)->get_config_value(*m_config, "fill_density");
+ get_optgroup(ogFrequentlyChangingParameters)->set_value("fill_density", value);
+
+ wxString new_selection = !m_config->opt_bool("support_material") ?
+ _("None") :
+ m_config->opt_bool("support_material_buildplate_only") ?
+ _("Support on build plate only") :
+ _("Everywhere");
+ get_optgroup(ogFrequentlyChangingParameters)->set_value("support", new_selection);
+
+ bool val = m_config->opt_float("brim_width") > 0.0 ? true : false;
+ get_optgroup(ogFrequentlyChangingParameters)->set_value("brim", val);
+
+ update_wiping_button_visibility();
+}
+
+void Tab::reload_compatible_printers_widget()
+{
+ bool has_any = !m_config->option<ConfigOptionStrings>("compatible_printers")->values.empty();
+ has_any ? m_compatible_printers_btn->Enable() : m_compatible_printers_btn->Disable();
+ m_compatible_printers_checkbox->SetValue(!has_any);
+ get_field("compatible_printers_condition")->toggle(!has_any);
+}
+
+void TabPrint::build()
+{
+ m_presets = &m_preset_bundle->prints;
+ load_initial_data();
+
+ auto page = add_options_page(_(L("Layers and perimeters")), "layers.png");
+ auto optgroup = page->new_optgroup(_(L("Layer height")));
+ optgroup->append_single_option_line("layer_height");
+ optgroup->append_single_option_line("first_layer_height");
+
+ optgroup = page->new_optgroup(_(L("Vertical shells")));
+ optgroup->append_single_option_line("perimeters");
+ optgroup->append_single_option_line("spiral_vase");
+
+ Line line { "", "" };
+ line.full_width = 1;
+ line.widget = [this](wxWindow* parent) {
+ return description_line_widget(parent, &m_recommended_thin_wall_thickness_description_line);
+ };
+ optgroup->append_line(line);
+
+ optgroup = page->new_optgroup(_(L("Horizontal shells")));
+ line = { _(L("Solid layers")), "" };
+ line.append_option(optgroup->get_option("top_solid_layers"));
+ line.append_option(optgroup->get_option("bottom_solid_layers"));
+ optgroup->append_line(line);
+
+ optgroup = page->new_optgroup(_(L("Quality (slower slicing)")));
+ optgroup->append_single_option_line("extra_perimeters");
+ optgroup->append_single_option_line("ensure_vertical_shell_thickness");
+ optgroup->append_single_option_line("avoid_crossing_perimeters");
+ optgroup->append_single_option_line("thin_walls");
+ optgroup->append_single_option_line("overhangs");
+
+ optgroup = page->new_optgroup(_(L("Advanced")));
+ optgroup->append_single_option_line("seam_position");
+ optgroup->append_single_option_line("external_perimeters_first");
+
+ page = add_options_page(_(L("Infill")), "infill.png");
+ optgroup = page->new_optgroup(_(L("Infill")));
+ optgroup->append_single_option_line("fill_density");
+ optgroup->append_single_option_line("fill_pattern");
+ optgroup->append_single_option_line("external_fill_pattern");
+
+ optgroup = page->new_optgroup(_(L("Reducing printing time")));
+ optgroup->append_single_option_line("infill_every_layers");
+ optgroup->append_single_option_line("infill_only_where_needed");
+
+ optgroup = page->new_optgroup(_(L("Advanced")));
+ optgroup->append_single_option_line("solid_infill_every_layers");
+ optgroup->append_single_option_line("fill_angle");
+ optgroup->append_single_option_line("solid_infill_below_area");
+ optgroup->append_single_option_line("bridge_angle");
+ optgroup->append_single_option_line("only_retract_when_crossing_perimeters");
+ optgroup->append_single_option_line("infill_first");
+
+ page = add_options_page(_(L("Skirt and brim")), "box.png");
+ optgroup = page->new_optgroup(_(L("Skirt")));
+ optgroup->append_single_option_line("skirts");
+ optgroup->append_single_option_line("skirt_distance");
+ optgroup->append_single_option_line("skirt_height");
+ optgroup->append_single_option_line("min_skirt_length");
+
+ optgroup = page->new_optgroup(_(L("Brim")));
+ optgroup->append_single_option_line("brim_width");
+
+ page = add_options_page(_(L("Support material")), "building.png");
+ optgroup = page->new_optgroup(_(L("Support material")));
+ optgroup->append_single_option_line("support_material");
+ optgroup->append_single_option_line("support_material_auto");
+ optgroup->append_single_option_line("support_material_threshold");
+ optgroup->append_single_option_line("support_material_enforce_layers");
+
+ optgroup = page->new_optgroup(_(L("Raft")));
+ optgroup->append_single_option_line("raft_layers");
+// # optgroup->append_single_option_line(get_option_("raft_contact_distance");
+
+ optgroup = page->new_optgroup(_(L("Options for support material and raft")));
+ optgroup->append_single_option_line("support_material_contact_distance");
+ optgroup->append_single_option_line("support_material_pattern");
+ optgroup->append_single_option_line("support_material_with_sheath");
+ optgroup->append_single_option_line("support_material_spacing");
+ optgroup->append_single_option_line("support_material_angle");
+ optgroup->append_single_option_line("support_material_interface_layers");
+ optgroup->append_single_option_line("support_material_interface_spacing");
+ optgroup->append_single_option_line("support_material_interface_contact_loops");
+ optgroup->append_single_option_line("support_material_buildplate_only");
+ optgroup->append_single_option_line("support_material_xy_spacing");
+ optgroup->append_single_option_line("dont_support_bridges");
+ optgroup->append_single_option_line("support_material_synchronize_layers");
+
+ page = add_options_page(_(L("Speed")), "time.png");
+ optgroup = page->new_optgroup(_(L("Speed for print moves")));
+ optgroup->append_single_option_line("perimeter_speed");
+ optgroup->append_single_option_line("small_perimeter_speed");
+ optgroup->append_single_option_line("external_perimeter_speed");
+ optgroup->append_single_option_line("infill_speed");
+ optgroup->append_single_option_line("solid_infill_speed");
+ optgroup->append_single_option_line("top_solid_infill_speed");
+ optgroup->append_single_option_line("support_material_speed");
+ optgroup->append_single_option_line("support_material_interface_speed");
+ optgroup->append_single_option_line("bridge_speed");
+ optgroup->append_single_option_line("gap_fill_speed");
+
+ optgroup = page->new_optgroup(_(L("Speed for non-print moves")));
+ optgroup->append_single_option_line("travel_speed");
+
+ optgroup = page->new_optgroup(_(L("Modifiers")));
+ optgroup->append_single_option_line("first_layer_speed");
+
+ optgroup = page->new_optgroup(_(L("Acceleration control (advanced)")));
+ optgroup->append_single_option_line("perimeter_acceleration");
+ optgroup->append_single_option_line("infill_acceleration");
+ optgroup->append_single_option_line("bridge_acceleration");
+ optgroup->append_single_option_line("first_layer_acceleration");
+ optgroup->append_single_option_line("default_acceleration");
+
+ optgroup = page->new_optgroup(_(L("Autospeed (advanced)")));
+ optgroup->append_single_option_line("max_print_speed");
+ optgroup->append_single_option_line("max_volumetric_speed");
+ optgroup->append_single_option_line("max_volumetric_extrusion_rate_slope_positive");
+ optgroup->append_single_option_line("max_volumetric_extrusion_rate_slope_negative");
+
+ page = add_options_page(_(L("Multiple Extruders")), "funnel.png");
+ optgroup = page->new_optgroup(_(L("Extruders")));
+ optgroup->append_single_option_line("perimeter_extruder");
+ optgroup->append_single_option_line("infill_extruder");
+ optgroup->append_single_option_line("solid_infill_extruder");
+ optgroup->append_single_option_line("support_material_extruder");
+ optgroup->append_single_option_line("support_material_interface_extruder");
+
+ optgroup = page->new_optgroup(_(L("Ooze prevention")));
+ optgroup->append_single_option_line("ooze_prevention");
+ optgroup->append_single_option_line("standby_temperature_delta");
+
+ optgroup = page->new_optgroup(_(L("Wipe tower")));
+ optgroup->append_single_option_line("wipe_tower");
+ optgroup->append_single_option_line("wipe_tower_x");
+ optgroup->append_single_option_line("wipe_tower_y");
+ optgroup->append_single_option_line("wipe_tower_width");
+ optgroup->append_single_option_line("wipe_tower_rotation_angle");
+ optgroup->append_single_option_line("wipe_tower_bridging");
+ optgroup->append_single_option_line("single_extruder_multi_material_priming");
+
+ optgroup = page->new_optgroup(_(L("Advanced")));
+ optgroup->append_single_option_line("interface_shells");
+
+ page = add_options_page(_(L("Advanced")), "wrench.png");
+ optgroup = page->new_optgroup(_(L("Extrusion width")));
+ optgroup->append_single_option_line("extrusion_width");
+ optgroup->append_single_option_line("first_layer_extrusion_width");
+ optgroup->append_single_option_line("perimeter_extrusion_width");
+ optgroup->append_single_option_line("external_perimeter_extrusion_width");
+ optgroup->append_single_option_line("infill_extrusion_width");
+ optgroup->append_single_option_line("solid_infill_extrusion_width");
+ optgroup->append_single_option_line("top_infill_extrusion_width");
+ optgroup->append_single_option_line("support_material_extrusion_width");
+
+ optgroup = page->new_optgroup(_(L("Overlap")));
+ optgroup->append_single_option_line("infill_overlap");
+
+ optgroup = page->new_optgroup(_(L("Flow")));
+ optgroup->append_single_option_line("bridge_flow_ratio");
+
+ optgroup = page->new_optgroup(_(L("Other")));
+ optgroup->append_single_option_line("clip_multipart_objects");
+ optgroup->append_single_option_line("elefant_foot_compensation");
+ optgroup->append_single_option_line("xy_size_compensation");
+// # optgroup->append_single_option_line("threads");
+ optgroup->append_single_option_line("resolution");
+
+ page = add_options_page(_(L("Output options")), "page_white_go.png");
+ optgroup = page->new_optgroup(_(L("Sequential printing")));
+ optgroup->append_single_option_line("complete_objects");
+ line = { _(L("Extruder clearance (mm)")), "" };
+ Option option = optgroup->get_option("extruder_clearance_radius");
+ option.opt.width = 60;
+ line.append_option(option);
+ option = optgroup->get_option("extruder_clearance_height");
+ option.opt.width = 60;
+ line.append_option(option);
+ optgroup->append_line(line);
+
+ optgroup = page->new_optgroup(_(L("Output file")));
+ optgroup->append_single_option_line("gcode_comments");
+ option = optgroup->get_option("output_filename_format");
+ option.opt.full_width = true;
+ optgroup->append_single_option_line(option);
+
+ optgroup = page->new_optgroup(_(L("Post-processing scripts")), 0);
+ option = optgroup->get_option("post_process");
+ option.opt.full_width = true;
+ option.opt.height = 50;
+ optgroup->append_single_option_line(option);
+
+ page = add_options_page(_(L("Notes")), "note.png");
+ optgroup = page->new_optgroup(_(L("Notes")), 0);
+ option = optgroup->get_option("notes");
+ option.opt.full_width = true;
+ option.opt.height = 250;
+ optgroup->append_single_option_line(option);
+
+ page = add_options_page(_(L("Dependencies")), "wrench.png");
+ optgroup = page->new_optgroup(_(L("Profile dependencies")));
+ line = { _(L("Compatible printers")), "" };
+ line.widget = [this](wxWindow* parent){
+ return compatible_printers_widget(parent, &m_compatible_printers_checkbox, &m_compatible_printers_btn);
+ };
+ optgroup->append_line(line, &m_colored_Label);
+
+ option = optgroup->get_option("compatible_printers_condition");
+ option.opt.full_width = true;
+ optgroup->append_single_option_line(option);
+
+ line = Line{ "", "" };
+ line.full_width = 1;
+ line.widget = [this](wxWindow* parent) {
+ return description_line_widget(parent, &m_parent_preset_description_line);
+ };
+ optgroup->append_line(line);
+}
+
+// Reload current config (aka presets->edited_preset->config) into the UI fields.
+void TabPrint::reload_config(){
+ reload_compatible_printers_widget();
+ Tab::reload_config();
+}
+
+void TabPrint::update()
+{
+ if (get_preset_bundle()->printers.get_selected_preset().printer_technology() == ptSLA)
+ return; // ys_FIXME
+
+ Freeze();
+
+ double fill_density = m_config->option<ConfigOptionPercent>("fill_density")->value;
+
+ if (m_config->opt_bool("spiral_vase") &&
+ !(m_config->opt_int("perimeters") == 1 && m_config->opt_int("top_solid_layers") == 0 &&
+ fill_density == 0)) {
+ wxString msg_text = _(L("The Spiral Vase mode requires:\n"
+ "- one perimeter\n"
+ "- no top solid layers\n"
+ "- 0% fill density\n"
+ "- no support material\n"
+ "- no ensure_vertical_shell_thickness\n"
+ "\nShall I adjust those settings in order to enable Spiral Vase?"));
+ auto dialog = new wxMessageDialog(parent(), msg_text, _(L("Spiral Vase")), wxICON_WARNING | wxYES | wxNO);
+ DynamicPrintConfig new_conf = *m_config;
+ if (dialog->ShowModal() == wxID_YES) {
+ new_conf.set_key_value("perimeters", new ConfigOptionInt(1));
+ new_conf.set_key_value("top_solid_layers", new ConfigOptionInt(0));
+ new_conf.set_key_value("fill_density", new ConfigOptionPercent(0));
+ new_conf.set_key_value("support_material", new ConfigOptionBool(false));
+ new_conf.set_key_value("support_material_enforce_layers", new ConfigOptionInt(0));
+ new_conf.set_key_value("ensure_vertical_shell_thickness", new ConfigOptionBool(false));
+ fill_density = 0;
+ }
+ else {
+ new_conf.set_key_value("spiral_vase", new ConfigOptionBool(false));
+ }
+ load_config(new_conf);
+ on_value_change("fill_density", fill_density);
+ }
+
+ if (m_config->opt_bool("wipe_tower") && m_config->opt_bool("support_material") &&
+ m_config->opt_float("support_material_contact_distance") > 0. &&
+ (m_config->opt_int("support_material_extruder") != 0 || m_config->opt_int("support_material_interface_extruder") != 0)) {
+ wxString msg_text = _(L("The Wipe Tower currently supports the non-soluble supports only\n"
+ "if they are printed with the current extruder without triggering a tool change.\n"
+ "(both support_material_extruder and support_material_interface_extruder need to be set to 0).\n"
+ "\nShall I adjust those settings in order to enable the Wipe Tower?"));
+ auto dialog = new wxMessageDialog(parent(), msg_text, _(L("Wipe Tower")), wxICON_WARNING | wxYES | wxNO);
+ DynamicPrintConfig new_conf = *m_config;
+ if (dialog->ShowModal() == wxID_YES) {
+ new_conf.set_key_value("support_material_extruder", new ConfigOptionInt(0));
+ new_conf.set_key_value("support_material_interface_extruder", new ConfigOptionInt(0));
+ }
+ else
+ new_conf.set_key_value("wipe_tower", new ConfigOptionBool(false));
+ load_config(new_conf);
+ }
+
+ if (m_config->opt_bool("wipe_tower") && m_config->opt_bool("support_material") &&
+ m_config->opt_float("support_material_contact_distance") == 0 &&
+ !m_config->opt_bool("support_material_synchronize_layers")) {
+ wxString msg_text = _(L("For the Wipe Tower to work with the soluble supports, the support layers\n"
+ "need to be synchronized with the object layers.\n"
+ "\nShall I synchronize support layers in order to enable the Wipe Tower?"));
+ auto dialog = new wxMessageDialog(parent(), msg_text, _(L("Wipe Tower")), wxICON_WARNING | wxYES | wxNO);
+ DynamicPrintConfig new_conf = *m_config;
+ if (dialog->ShowModal() == wxID_YES) {
+ new_conf.set_key_value("support_material_synchronize_layers", new ConfigOptionBool(true));
+ }
+ else
+ new_conf.set_key_value("wipe_tower", new ConfigOptionBool(false));
+ load_config(new_conf);
+ }
+
+ if (m_config->opt_bool("support_material")) {
+ // Ask only once.
+ if (!m_support_material_overhangs_queried) {
+ m_support_material_overhangs_queried = true;
+ if (!m_config->opt_bool("overhangs")/* != 1*/) {
+ wxString msg_text = _(L("Supports work better, if the following feature is enabled:\n"
+ "- Detect bridging perimeters\n"
+ "\nShall I adjust those settings for supports?"));
+ auto dialog = new wxMessageDialog(parent(), msg_text, _(L("Support Generator")), wxICON_WARNING | wxYES | wxNO | wxCANCEL);
+ DynamicPrintConfig new_conf = *m_config;
+ auto answer = dialog->ShowModal();
+ if (answer == wxID_YES) {
+ // Enable "detect bridging perimeters".
+ new_conf.set_key_value("overhangs", new ConfigOptionBool(true));
+ } else if (answer == wxID_NO) {
+ // Do nothing, leave supports on and "detect bridging perimeters" off.
+ } else if (answer == wxID_CANCEL) {
+ // Disable supports.
+ new_conf.set_key_value("support_material", new ConfigOptionBool(false));
+ m_support_material_overhangs_queried = false;
+ }
+ load_config(new_conf);
+ }
+ }
+ }
+ else {
+ m_support_material_overhangs_queried = false;
+ }
+
+ if (m_config->option<ConfigOptionPercent>("fill_density")->value == 100) {
+ auto fill_pattern = m_config->option<ConfigOptionEnum<InfillPattern>>("fill_pattern")->value;
+ std::string str_fill_pattern = "";
+ t_config_enum_values map_names = m_config->option<ConfigOptionEnum<InfillPattern>>("fill_pattern")->get_enum_values();
+ for (auto it : map_names) {
+ if (fill_pattern == it.second) {
+ str_fill_pattern = it.first;
+ break;
+ }
+ }
+ if (!str_fill_pattern.empty()){
+ auto external_fill_pattern = m_config->def()->get("external_fill_pattern")->enum_values;
+ bool correct_100p_fill = false;
+ for (auto fill : external_fill_pattern)
+ {
+ if (str_fill_pattern.compare(fill) == 0)
+ correct_100p_fill = true;
+ }
+ // get fill_pattern name from enum_labels for using this one at dialog_msg
+ str_fill_pattern = m_config->def()->get("fill_pattern")->enum_labels[fill_pattern];
+ if (!correct_100p_fill){
+ wxString msg_text = _(L("The ")) + str_fill_pattern + _(L(" infill pattern is not supposed to work at 100% density.\n"
+ "\nShall I switch to rectilinear fill pattern?"));
+ auto dialog = new wxMessageDialog(parent(), msg_text, _(L("Infill")), wxICON_WARNING | wxYES | wxNO);
+ DynamicPrintConfig new_conf = *m_config;
+ if (dialog->ShowModal() == wxID_YES) {
+ new_conf.set_key_value("fill_pattern", new ConfigOptionEnum<InfillPattern>(ipRectilinear));
+ fill_density = 100;
+ }
+ else
+ fill_density = m_presets->get_selected_preset().config.option<ConfigOptionPercent>("fill_density")->value;
+ new_conf.set_key_value("fill_density", new ConfigOptionPercent(fill_density));
+ load_config(new_conf);
+ on_value_change("fill_density", fill_density);
+ }
+ }
+ }
+
+ bool have_perimeters = m_config->opt_int("perimeters") > 0;
+ for (auto el : {"extra_perimeters", "ensure_vertical_shell_thickness", "thin_walls", "overhangs",
+ "seam_position", "external_perimeters_first", "external_perimeter_extrusion_width",
+ "perimeter_speed", "small_perimeter_speed", "external_perimeter_speed" })
+ get_field(el)->toggle(have_perimeters);
+
+ bool have_infill = m_config->option<ConfigOptionPercent>("fill_density")->value > 0;
+ // infill_extruder uses the same logic as in Print::extruders()
+ for (auto el : {"fill_pattern", "infill_every_layers", "infill_only_where_needed",
+ "solid_infill_every_layers", "solid_infill_below_area", "infill_extruder" })
+ get_field(el)->toggle(have_infill);
+
+ bool have_solid_infill = m_config->opt_int("top_solid_layers") > 0 || m_config->opt_int("bottom_solid_layers") > 0;
+ // solid_infill_extruder uses the same logic as in Print::extruders()
+ for (auto el : {"external_fill_pattern", "infill_first", "solid_infill_extruder",
+ "solid_infill_extrusion_width", "solid_infill_speed" })
+ get_field(el)->toggle(have_solid_infill);
+
+ for (auto el : {"fill_angle", "bridge_angle", "infill_extrusion_width",
+ "infill_speed", "bridge_speed" })
+ get_field(el)->toggle(have_infill || have_solid_infill);
+
+ get_field("gap_fill_speed")->toggle(have_perimeters && have_infill);
+
+ bool have_top_solid_infill = m_config->opt_int("top_solid_layers") > 0;
+ for (auto el : { "top_infill_extrusion_width", "top_solid_infill_speed" })
+ get_field(el)->toggle(have_top_solid_infill);
+
+ bool have_default_acceleration = m_config->opt_float("default_acceleration") > 0;
+ for (auto el : {"perimeter_acceleration", "infill_acceleration",
+ "bridge_acceleration", "first_layer_acceleration" })
+ get_field(el)->toggle(have_default_acceleration);
+
+ bool have_skirt = m_config->opt_int("skirts") > 0 || m_config->opt_float("min_skirt_length") > 0;
+ for (auto el : { "skirt_distance", "skirt_height" })
+ get_field(el)->toggle(have_skirt);
+
+ bool have_brim = m_config->opt_float("brim_width") > 0;
+ // perimeter_extruder uses the same logic as in Print::extruders()
+ get_field("perimeter_extruder")->toggle(have_perimeters || have_brim);
+
+ bool have_raft = m_config->opt_int("raft_layers") > 0;
+ bool have_support_material = m_config->opt_bool("support_material") || have_raft;
+ bool have_support_material_auto = have_support_material && m_config->opt_bool("support_material_auto");
+ bool have_support_interface = m_config->opt_int("support_material_interface_layers") > 0;
+ bool have_support_soluble = have_support_material && m_config->opt_float("support_material_contact_distance") == 0;
+ for (auto el : {"support_material_pattern", "support_material_with_sheath",
+ "support_material_spacing", "support_material_angle", "support_material_interface_layers",
+ "dont_support_bridges", "support_material_extrusion_width", "support_material_contact_distance",
+ "support_material_xy_spacing" })
+ get_field(el)->toggle(have_support_material);
+ get_field("support_material_threshold")->toggle(have_support_material_auto);
+
+ for (auto el : {"support_material_interface_spacing", "support_material_interface_extruder",
+ "support_material_interface_speed", "support_material_interface_contact_loops" })
+ get_field(el)->toggle(have_support_material && have_support_interface);
+ get_field("support_material_synchronize_layers")->toggle(have_support_soluble);
+
+ get_field("perimeter_extrusion_width")->toggle(have_perimeters || have_skirt || have_brim);
+ get_field("support_material_extruder")->toggle(have_support_material || have_skirt);
+ get_field("support_material_speed")->toggle(have_support_material || have_brim || have_skirt);
+
+ bool have_sequential_printing = m_config->opt_bool("complete_objects");
+ for (auto el : { "extruder_clearance_radius", "extruder_clearance_height" })
+ get_field(el)->toggle(have_sequential_printing);
+
+ bool have_ooze_prevention = m_config->opt_bool("ooze_prevention");
+ get_field("standby_temperature_delta")->toggle(have_ooze_prevention);
+
+ bool have_wipe_tower = m_config->opt_bool("wipe_tower");
+ for (auto el : { "wipe_tower_x", "wipe_tower_y", "wipe_tower_width", "wipe_tower_rotation_angle", "wipe_tower_bridging"})
+ get_field(el)->toggle(have_wipe_tower);
+
+ m_recommended_thin_wall_thickness_description_line->SetText(
+ from_u8(PresetHints::recommended_thin_wall_thickness(*m_preset_bundle)));
+
+ Thaw();
+}
+
+void TabPrint::OnActivate()
+{
+ m_recommended_thin_wall_thickness_description_line->SetText(
+ from_u8(PresetHints::recommended_thin_wall_thickness(*m_preset_bundle)));
+ Tab::OnActivate();
+}
+
+void TabFilament::build()
+{
+ m_presets = &m_preset_bundle->filaments;
+ load_initial_data();
+
+ auto page = add_options_page(_(L("Filament")), "spool.png");
+ auto optgroup = page->new_optgroup(_(L("Filament")));
+ optgroup->append_single_option_line("filament_colour");
+ optgroup->append_single_option_line("filament_diameter");
+ optgroup->append_single_option_line("extrusion_multiplier");
+ optgroup->append_single_option_line("filament_density");
+ optgroup->append_single_option_line("filament_cost");
+
+ optgroup = page->new_optgroup(_(L("Temperature ")) + wxString("°C", wxConvUTF8));
+ Line line = { _(L("Extruder")), "" };
+ line.append_option(optgroup->get_option("first_layer_temperature"));
+ line.append_option(optgroup->get_option("temperature"));
+ optgroup->append_line(line);
+
+ line = { _(L("Bed")), "" };
+ line.append_option(optgroup->get_option("first_layer_bed_temperature"));
+ line.append_option(optgroup->get_option("bed_temperature"));
+ optgroup->append_line(line);
+
+ page = add_options_page(_(L("Cooling")), "hourglass.png");
+ optgroup = page->new_optgroup(_(L("Enable")));
+ optgroup->append_single_option_line("fan_always_on");
+ optgroup->append_single_option_line("cooling");
+
+ line = { "", "" };
+ line.full_width = 1;
+ line.widget = [this](wxWindow* parent) {
+ return description_line_widget(parent, &m_cooling_description_line);
+ };
+ optgroup->append_line(line);
+
+ optgroup = page->new_optgroup(_(L("Fan settings")));
+ line = { _(L("Fan speed")), "" };
+ line.append_option(optgroup->get_option("min_fan_speed"));
+ line.append_option(optgroup->get_option("max_fan_speed"));
+ optgroup->append_line(line);
+
+ optgroup->append_single_option_line("bridge_fan_speed");
+ optgroup->append_single_option_line("disable_fan_first_layers");
+
+ optgroup = page->new_optgroup(_(L("Cooling thresholds")), 250);
+ optgroup->append_single_option_line("fan_below_layer_time");
+ optgroup->append_single_option_line("slowdown_below_layer_time");
+ optgroup->append_single_option_line("min_print_speed");
+
+ page = add_options_page(_(L("Advanced")), "wrench.png");
+ optgroup = page->new_optgroup(_(L("Filament properties")));
+ optgroup->append_single_option_line("filament_type");
+ optgroup->append_single_option_line("filament_soluble");
+
+ optgroup = page->new_optgroup(_(L("Print speed override")));
+ optgroup->append_single_option_line("filament_max_volumetric_speed");
+
+ line = { "", "" };
+ line.full_width = 1;
+ line.widget = [this](wxWindow* parent) {
+ return description_line_widget(parent, &m_volumetric_speed_description_line);
+ };
+ optgroup->append_line(line);
+
+ optgroup = page->new_optgroup(_(L("Toolchange parameters with single extruder MM printers")));
+ optgroup->append_single_option_line("filament_loading_speed_start");
+ optgroup->append_single_option_line("filament_loading_speed");
+ optgroup->append_single_option_line("filament_unloading_speed_start");
+ optgroup->append_single_option_line("filament_unloading_speed");
+ optgroup->append_single_option_line("filament_load_time");
+ optgroup->append_single_option_line("filament_unload_time");
+ optgroup->append_single_option_line("filament_toolchange_delay");
+ optgroup->append_single_option_line("filament_cooling_moves");
+ optgroup->append_single_option_line("filament_cooling_initial_speed");
+ optgroup->append_single_option_line("filament_cooling_final_speed");
+ optgroup->append_single_option_line("filament_minimal_purge_on_wipe_tower");
+
+ line = { _(L("Ramming")), "" };
+ line.widget = [this](wxWindow* parent){
+ auto ramming_dialog_btn = new wxButton(parent, wxID_ANY, _(L("Ramming settings"))+dots, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT);
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(ramming_dialog_btn);
+
+ ramming_dialog_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent& e)
+ {
+ RammingDialog dlg(this,(m_config->option<ConfigOptionStrings>("filament_ramming_parameters"))->get_at(0));
+ if (dlg.ShowModal() == wxID_OK)
+ (m_config->option<ConfigOptionStrings>("filament_ramming_parameters"))->get_at(0) = dlg.get_parameters();
+ }));
+ return sizer;
+ };
+ optgroup->append_line(line);
+
+
+ page = add_options_page(_(L("Custom G-code")), "cog.png");
+ optgroup = page->new_optgroup(_(L("Start G-code")), 0);
+ Option option = optgroup->get_option("start_filament_gcode");
+ option.opt.full_width = true;
+ option.opt.height = 150;
+ optgroup->append_single_option_line(option);
+
+ optgroup = page->new_optgroup(_(L("End G-code")), 0);
+ option = optgroup->get_option("end_filament_gcode");
+ option.opt.full_width = true;
+ option.opt.height = 150;
+ optgroup->append_single_option_line(option);
+
+ page = add_options_page(_(L("Notes")), "note.png");
+ optgroup = page->new_optgroup(_(L("Notes")), 0);
+ optgroup->label_width = 0;
+ option = optgroup->get_option("filament_notes");
+ option.opt.full_width = true;
+ option.opt.height = 250;
+ optgroup->append_single_option_line(option);
+
+ page = add_options_page(_(L("Dependencies")), "wrench.png");
+ optgroup = page->new_optgroup(_(L("Profile dependencies")));
+ line = { _(L("Compatible printers")), "" };
+ line.widget = [this](wxWindow* parent){
+ return compatible_printers_widget(parent, &m_compatible_printers_checkbox, &m_compatible_printers_btn);
+ };
+ optgroup->append_line(line, &m_colored_Label);
+
+ option = optgroup->get_option("compatible_printers_condition");
+ option.opt.full_width = true;
+ optgroup->append_single_option_line(option);
+
+ line = Line{ "", "" };
+ line.full_width = 1;
+ line.widget = [this](wxWindow* parent) {
+ return description_line_widget(parent, &m_parent_preset_description_line);
+ };
+ optgroup->append_line(line);
+}
+
+// Reload current config (aka presets->edited_preset->config) into the UI fields.
+void TabFilament::reload_config(){
+ reload_compatible_printers_widget();
+ Tab::reload_config();
+}
+
+void TabFilament::update()
+{
+ if (get_preset_bundle()->printers.get_selected_preset().printer_technology() == ptSLA)
+ return; // ys_FIXME
+
+ Freeze();
+ wxString text = from_u8(PresetHints::cooling_description(m_presets->get_edited_preset()));
+ m_cooling_description_line->SetText(text);
+ text = from_u8(PresetHints::maximum_volumetric_flow_description(*m_preset_bundle));
+ m_volumetric_speed_description_line->SetText(text);
+
+ bool cooling = m_config->opt_bool("cooling", 0);
+ bool fan_always_on = cooling || m_config->opt_bool("fan_always_on", 0);
+
+ for (auto el : { "max_fan_speed", "fan_below_layer_time", "slowdown_below_layer_time", "min_print_speed" })
+ get_field(el)->toggle(cooling);
+
+ for (auto el : { "min_fan_speed", "disable_fan_first_layers" })
+ get_field(el)->toggle(fan_always_on);
+ Thaw();
+}
+
+void TabFilament::OnActivate()
+{
+ m_volumetric_speed_description_line->SetText(from_u8(PresetHints::maximum_volumetric_flow_description(*m_preset_bundle)));
+ Tab::OnActivate();
+}
+
+wxSizer* Tab::description_line_widget(wxWindow* parent, ogStaticText* *StaticText)
+{
+ *StaticText = new ogStaticText(parent, "");
+
+ auto font = (new wxSystemSettings)->GetFont(wxSYS_DEFAULT_GUI_FONT);
+ (*StaticText)->SetFont(font);
+
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(*StaticText, 1, wxEXPAND|wxALL, 0);
+ return sizer;
+}
+
+bool Tab::current_preset_is_dirty()
+{
+ return m_presets->current_is_dirty();
+}
+
+void TabPrinter::build()
+{
+ m_presets = &m_preset_bundle->printers;
+ load_initial_data();
+
+ m_printer_technology = m_presets->get_selected_preset().printer_technology();
+
+ m_presets->get_selected_preset().printer_technology() == ptSLA ? build_sla() : build_fff();
+
+// on_value_change("printer_technology", m_printer_technology); // to update show/hide preset ComboBoxes
+}
+
+void TabPrinter::build_fff()
+{
+ if (!m_pages.empty())
+ m_pages.resize(0);
+ // to avoid redundant memory allocation / deallocation during extruders count changing
+ m_pages.reserve(30);
+
+ auto *nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(m_config->option("nozzle_diameter"));
+ m_initial_extruders_count = m_extruders_count = nozzle_diameter->values.size();
+ const Preset* parent_preset = m_presets->get_selected_preset_parent();
+ m_sys_extruders_count = parent_preset == nullptr ? 0 :
+ static_cast<const ConfigOptionFloats*>(parent_preset->config.option("nozzle_diameter"))->values.size();
+
+ auto page = add_options_page(_(L("General")), "printer_empty.png");
+ auto optgroup = page->new_optgroup(_(L("Size and coordinates")));
+
+ Line line{ _(L("Bed shape")), "" };
+ line.widget = [this](wxWindow* parent){
+ auto btn = new wxButton(parent, wxID_ANY, _(L(" Set "))+dots, wxDefaultPosition, wxDefaultSize, wxBU_LEFT | wxBU_EXACTFIT);
+ btn->SetFont(Slic3r::GUI::small_font());
+ btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("printer_empty.png")), wxBITMAP_TYPE_PNG));
+
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(btn);
+
+ btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e)
+ {
+ auto dlg = new BedShapeDialog(this);
+ dlg->build_dialog(m_config->option<ConfigOptionPoints>("bed_shape"));
+ if (dlg->ShowModal() == wxID_OK){
+ load_key_value("bed_shape", dlg->GetValue());
+ update_changed_ui();
+ }
+ }));
+
+ return sizer;
+ };
+ optgroup->append_line(line, &m_colored_Label);
+ optgroup->append_single_option_line("max_print_height");
+ optgroup->append_single_option_line("z_offset");
+
+ optgroup = page->new_optgroup(_(L("Capabilities")));
+ ConfigOptionDef def;
+ def.type = coInt,
+ def.default_value = new ConfigOptionInt(1);
+ def.label = L("Extruders");
+ def.tooltip = L("Number of extruders of the printer.");
+ def.min = 1;
+ Option option(def, "extruders_count");
+ optgroup->append_single_option_line(option);
+ optgroup->append_single_option_line("single_extruder_multi_material");
+
+ optgroup->m_on_change = [this, optgroup](t_config_option_key opt_key, boost::any value){
+ size_t extruders_count = boost::any_cast<int>(optgroup->get_value("extruders_count"));
+ wxTheApp->CallAfter([this, opt_key, value, extruders_count](){
+ if (opt_key.compare("extruders_count")==0 || opt_key.compare("single_extruder_multi_material")==0) {
+ extruders_count_changed(extruders_count);
+ update_dirty();
+ if (opt_key.compare("single_extruder_multi_material")==0) // the single_extruder_multimaterial was added to force pages
+ on_value_change(opt_key, value); // rebuild - let's make sure the on_value_change is not skipped
+ }
+ else {
+ update_dirty();
+ on_value_change(opt_key, value);
+ }
+ });
+ };
+
+
+#if 0
+ if (!m_no_controller)
+ {
+ optgroup = page->new_optgroup(_(L("USB/Serial connection")));
+ line = {_(L("Serial port")), ""};
+ Option serial_port = optgroup->get_option("serial_port");
+ serial_port.side_widget = ([this](wxWindow* parent){
+ auto btn = new wxBitmapButton(parent, wxID_ANY, wxBitmap(from_u8(Slic3r::var("arrow_rotate_clockwise.png")), wxBITMAP_TYPE_PNG),
+ wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
+ btn->SetToolTip(_(L("Rescan serial ports")));
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(btn);
+
+ btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent e) {update_serial_ports(); });
+ return sizer;
+ });
+ auto serial_test = [this](wxWindow* parent){
+ auto btn = m_serial_test_btn = new wxButton(parent, wxID_ANY,
+ _(L("Test")), wxDefaultPosition, wxDefaultSize, wxBU_LEFT | wxBU_EXACTFIT);
+ btn->SetFont(Slic3r::GUI::small_font());
+ btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("wrench.png")), wxBITMAP_TYPE_PNG));
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(btn);
+
+ btn->Bind(wxEVT_BUTTON, [this, parent](wxCommandEvent e){
+ auto sender = Slic3r::make_unique<GCodeSender>();
+ auto res = sender->connect(
+ m_config->opt_string("serial_port"),
+ m_config->opt_int("serial_speed")
+ );
+ if (res && sender->wait_connected()) {
+ show_info(parent, _(L("Connection to printer works correctly.")), _(L("Success!")));
+ }
+ else {
+ show_error(parent, _(L("Connection failed.")));
+ }
+ });
+ return sizer;
+ };
+
+ line.append_option(serial_port);
+ line.append_option(optgroup->get_option("serial_speed"));
+ line.append_widget(serial_test);
+ optgroup->append_line(line);
+ }
+#endif
+
+ optgroup = page->new_optgroup(_(L("Printer Host upload")));
+
+ optgroup->append_single_option_line("host_type");
+
+ auto printhost_browse = [this, optgroup] (wxWindow* parent) {
+ auto btn = m_printhost_browse_btn = new wxButton(parent, wxID_ANY, _(L(" Browse "))+dots, wxDefaultPosition, wxDefaultSize, wxBU_LEFT);
+ btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("zoom.png")), wxBITMAP_TYPE_PNG));
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(btn);
+
+ btn->Bind(wxEVT_BUTTON, [this, parent, optgroup](wxCommandEvent e) {
+ BonjourDialog dialog(parent);
+ if (dialog.show_and_lookup()) {
+ optgroup->set_value("print_host", std::move(dialog.get_selected()), true);
+ }
+ });
+
+ return sizer;
+ };
+
+ auto print_host_test = [this](wxWindow* parent) {
+ auto btn = m_print_host_test_btn = new wxButton(parent, wxID_ANY, _(L("Test")),
+ wxDefaultPosition, wxDefaultSize, wxBU_LEFT | wxBU_EXACTFIT);
+ btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("wrench.png")), wxBITMAP_TYPE_PNG));
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(btn);
+
+ btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent e) {
+ std::unique_ptr<PrintHost> host(PrintHost::get_print_host(m_config));
+ if (! host) {
+ const auto text = wxString::Format("%s",
+ _(L("Could not get a valid Printer Host reference")));
+ show_error(this, text);
+ return;
+ }
+ wxString msg;
+ if (host->test(msg)) {
+ show_info(this, host->get_test_ok_msg(), _(L("Success!")));
+ } else {
+ show_error(this, host->get_test_failed_msg(msg));
+ }
+ });
+
+ return sizer;
+ };
+
+ Line host_line = optgroup->create_single_option_line("print_host");
+ host_line.append_widget(printhost_browse);
+ host_line.append_widget(print_host_test);
+ optgroup->append_line(host_line);
+ optgroup->append_single_option_line("printhost_apikey");
+
+ if (Http::ca_file_supported()) {
+
+ Line cafile_line = optgroup->create_single_option_line("printhost_cafile");
+
+ auto printhost_cafile_browse = [this, optgroup] (wxWindow* parent) {
+ auto btn = new wxButton(parent, wxID_ANY, _(L(" Browse "))+dots, wxDefaultPosition, wxDefaultSize, wxBU_LEFT);
+ btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("zoom.png")), wxBITMAP_TYPE_PNG));
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(btn);
+
+ btn->Bind(wxEVT_BUTTON, [this, optgroup] (wxCommandEvent e){
+ static const auto filemasks = _(L("Certificate files (*.crt, *.pem)|*.crt;*.pem|All files|*.*"));
+ wxFileDialog openFileDialog(this, _(L("Open CA certificate file")), "", "", filemasks, wxFD_OPEN | wxFD_FILE_MUST_EXIST);
+ if (openFileDialog.ShowModal() != wxID_CANCEL) {
+ optgroup->set_value("printhost_cafile", std::move(openFileDialog.GetPath()), true);
+ }
+ });
+
+ return sizer;
+ };
+
+ cafile_line.append_widget(printhost_cafile_browse);
+ optgroup->append_line(cafile_line);
+
+ auto printhost_cafile_hint = [this, optgroup] (wxWindow* parent) {
+ auto txt = new wxStaticText(parent, wxID_ANY,
+ _(L("HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate.")));
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(txt);
+ return sizer;
+ };
+
+ Line cafile_hint { "", "" };
+ cafile_hint.full_width = 1;
+ cafile_hint.widget = std::move(printhost_cafile_hint);
+ optgroup->append_line(cafile_hint);
+
+ }
+
+ optgroup = page->new_optgroup(_(L("Firmware")));
+ optgroup->append_single_option_line("gcode_flavor");
+ optgroup->append_single_option_line("silent_mode");
+ optgroup->append_single_option_line("remaining_times");
+
+ optgroup->m_on_change = [this, optgroup](t_config_option_key opt_key, boost::any value){
+ wxTheApp->CallAfter([this, opt_key, value](){
+ if (opt_key.compare("silent_mode") == 0) {
+ bool val = boost::any_cast<bool>(value);
+ if (m_use_silent_mode != val) {
+ m_rebuild_kinematics_page = true;
+ m_use_silent_mode = val;
+ }
+ }
+ build_extruder_pages();
+ update_dirty();
+ on_value_change(opt_key, value);
+ });
+ };
+
+ optgroup = page->new_optgroup(_(L("Advanced")));
+ optgroup->append_single_option_line("use_relative_e_distances");
+ optgroup->append_single_option_line("use_firmware_retraction");
+ optgroup->append_single_option_line("use_volumetric_e");
+ optgroup->append_single_option_line("variable_layer_height");
+
+ page = add_options_page(_(L("Custom G-code")), "cog.png");
+ optgroup = page->new_optgroup(_(L("Start G-code")), 0);
+ option = optgroup->get_option("start_gcode");
+ option.opt.full_width = true;
+ option.opt.height = 150;
+ optgroup->append_single_option_line(option);
+
+ optgroup = page->new_optgroup(_(L("End G-code")), 0);
+ option = optgroup->get_option("end_gcode");
+ option.opt.full_width = true;
+ option.opt.height = 150;
+ optgroup->append_single_option_line(option);
+
+ optgroup = page->new_optgroup(_(L("Before layer change G-code")), 0);
+ option = optgroup->get_option("before_layer_gcode");
+ option.opt.full_width = true;
+ option.opt.height = 150;
+ optgroup->append_single_option_line(option);
+
+ optgroup = page->new_optgroup(_(L("After layer change G-code")), 0);
+ option = optgroup->get_option("layer_gcode");
+ option.opt.full_width = true;
+ option.opt.height = 150;
+ optgroup->append_single_option_line(option);
+
+ optgroup = page->new_optgroup(_(L("Tool change G-code")), 0);
+ option = optgroup->get_option("toolchange_gcode");
+ option.opt.full_width = true;
+ option.opt.height = 150;
+ optgroup->append_single_option_line(option);
+
+ optgroup = page->new_optgroup(_(L("Between objects G-code (for sequential printing)")), 0);
+ option = optgroup->get_option("between_objects_gcode");
+ option.opt.full_width = true;
+ option.opt.height = 150;
+ optgroup->append_single_option_line(option);
+
+ page = add_options_page(_(L("Notes")), "note.png");
+ optgroup = page->new_optgroup(_(L("Notes")), 0);
+ option = optgroup->get_option("printer_notes");
+ option.opt.full_width = true;
+ option.opt.height = 250;
+ optgroup->append_single_option_line(option);
+
+ page = add_options_page(_(L("Dependencies")), "wrench.png");
+ optgroup = page->new_optgroup(_(L("Profile dependencies")));
+ line = Line{ "", "" };
+ line.full_width = 1;
+ line.widget = [this](wxWindow* parent) {
+ return description_line_widget(parent, &m_parent_preset_description_line);
+ };
+ optgroup->append_line(line);
+
+ build_extruder_pages();
+
+#if 0
+ if (!m_no_controller)
+ update_serial_ports();
+#endif
+}
+
+void TabPrinter::build_sla()
+{
+ if (!m_pages.empty())
+ m_pages.resize(0);
+ auto page = add_options_page(_(L("General")), "printer_empty.png");
+ auto optgroup = page->new_optgroup(_(L("Size and coordinates")));
+
+ Line line{ _(L("Bed shape")), "" };
+ line.widget = [this](wxWindow* parent){
+ auto btn = new wxButton(parent, wxID_ANY, _(L(" Set ")) + dots, wxDefaultPosition, wxDefaultSize, wxBU_LEFT | wxBU_EXACTFIT);
+ // btn->SetFont(Slic3r::GUI::small_font);
+ btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("printer_empty.png")), wxBITMAP_TYPE_PNG));
+
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(btn);
+
+ btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e)
+ {
+ auto dlg = new BedShapeDialog(this);
+ dlg->build_dialog(m_config->option<ConfigOptionPoints>("bed_shape"));
+ if (dlg->ShowModal() == wxID_OK){
+ load_key_value("bed_shape", dlg->GetValue());
+ update_changed_ui();
+ }
+ }));
+
+ return sizer;
+ };
+ optgroup->append_line(line, &m_colored_Label);
+ optgroup->append_single_option_line("max_print_height");
+
+ optgroup = page->new_optgroup(_(L("Display")));
+ optgroup->append_single_option_line("display_width");
+ optgroup->append_single_option_line("display_height");
+
+ auto option = optgroup->get_option("display_pixels_x");
+ line = { _(option.opt.full_label), "" };
+ line.append_option(option);
+ line.append_option(optgroup->get_option("display_pixels_y"));
+ optgroup->append_line(line);
+
+ optgroup = page->new_optgroup(_(L("Corrections")));
+ line = Line{ m_config->def()->get("printer_correction")->full_label, "" };
+ std::vector<std::string> axes{ "X", "Y", "Z" };
+ int id = 0;
+ for (auto& axis : axes) {
+ auto opt = optgroup->get_option("printer_correction", id);
+ opt.opt.label = axis;
+ line.append_option(opt);
+ ++id;
+ }
+ optgroup->append_line(line);
+
+ page = add_options_page(_(L("Notes")), "note.png");
+ optgroup = page->new_optgroup(_(L("Notes")), 0);
+ option = optgroup->get_option("printer_notes");
+ option.opt.full_width = true;
+ option.opt.height = 250;
+ optgroup->append_single_option_line(option);
+
+ page = add_options_page(_(L("Dependencies")), "wrench.png");
+ optgroup = page->new_optgroup(_(L("Profile dependencies")));
+ line = Line{ "", "" };
+ line.full_width = 1;
+ line.widget = [this](wxWindow* parent) {
+ return description_line_widget(parent, &m_parent_preset_description_line);
+ };
+ optgroup->append_line(line);
+}
+
+void TabPrinter::update_serial_ports(){
+ Field *field = get_field("serial_port");
+ Choice *choice = static_cast<Choice *>(field);
+ choice->set_values(Utils::scan_serial_ports());
+}
+
+void TabPrinter::extruders_count_changed(size_t extruders_count){
+ m_extruders_count = extruders_count;
+ m_preset_bundle->printers.get_edited_preset().set_num_extruders(extruders_count);
+ m_preset_bundle->update_multi_material_filament_presets();
+ build_extruder_pages();
+ reload_config();
+ on_value_change("extruders_count", extruders_count);
+ update_objects_list_extruder_column(extruders_count);
+}
+
+void TabPrinter::append_option_line(ConfigOptionsGroupShp optgroup, const std::string opt_key)
+{
+ auto option = optgroup->get_option(opt_key, 0);
+ auto line = Line{ option.opt.full_label, "" };
+ line.append_option(option);
+ if (m_use_silent_mode)
+ line.append_option(optgroup->get_option(opt_key, 1));
+ optgroup->append_line(line);
+}
+
+PageShp TabPrinter::build_kinematics_page()
+{
+ auto page = add_options_page(_(L("Machine limits")), "cog.png", true);
+
+ if (m_use_silent_mode) {
+ // Legend for OptionsGroups
+ auto optgroup = page->new_optgroup(_(L("")));
+ optgroup->set_show_modified_btns_val(false);
+ optgroup->label_width = 230;
+ auto line = Line{ "", "" };
+
+ ConfigOptionDef def;
+ def.type = coString;
+ def.width = 150;
+ def.gui_type = "legend";
+ def.tooltip = L("Values in this column are for Full Power mode");
+ def.default_value = new ConfigOptionString{ L("Full Power") };
+
+ auto option = Option(def, "full_power_legend");
+ line.append_option(option);
+
+ def.tooltip = L("Values in this column are for Silent mode");
+ def.default_value = new ConfigOptionString{ L("Silent") };
+ option = Option(def, "silent_legend");
+ line.append_option(option);
+
+ optgroup->append_line(line);
+ }
+
+ std::vector<std::string> axes{ "x", "y", "z", "e" };
+ auto optgroup = page->new_optgroup(_(L("Maximum feedrates")));
+ for (const std::string &axis : axes) {
+ append_option_line(optgroup, "machine_max_feedrate_" + axis);
+ }
+
+ optgroup = page->new_optgroup(_(L("Maximum accelerations")));
+ for (const std::string &axis : axes) {
+ append_option_line(optgroup, "machine_max_acceleration_" + axis);
+ }
+ append_option_line(optgroup, "machine_max_acceleration_extruding");
+ append_option_line(optgroup, "machine_max_acceleration_retracting");
+
+ optgroup = page->new_optgroup(_(L("Jerk limits")));
+ for (const std::string &axis : axes) {
+ append_option_line(optgroup, "machine_max_jerk_" + axis);
+ }
+
+ optgroup = page->new_optgroup(_(L("Minimum feedrates")));
+ append_option_line(optgroup, "machine_min_extruding_rate");
+ append_option_line(optgroup, "machine_min_travel_rate");
+
+ return page;
+}
+
+
+void TabPrinter::build_extruder_pages()
+{
+ size_t n_before_extruders = 2; // Count of pages before Extruder pages
+ bool is_marlin_flavor = m_config->option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor")->value == gcfMarlin;
+
+ // Add/delete Kinematics page according to is_marlin_flavor
+ size_t existed_page = 0;
+ for (int i = n_before_extruders; i < m_pages.size(); ++i) // first make sure it's not there already
+ if (m_pages[i]->title().find(_(L("Machine limits"))) != std::string::npos) {
+ if (!is_marlin_flavor || m_rebuild_kinematics_page)
+ m_pages.erase(m_pages.begin() + i);
+ else
+ existed_page = i;
+ break;
+ }
+
+ if (existed_page < n_before_extruders && is_marlin_flavor){
+ auto page = build_kinematics_page();
+ m_pages.insert(m_pages.begin() + n_before_extruders, page);
+ }
+
+ if (is_marlin_flavor)
+ n_before_extruders++;
+ size_t n_after_single_extruder_MM = 2; // Count of pages after single_extruder_multi_material page
+
+ if (m_extruders_count_old == m_extruders_count ||
+ (m_has_single_extruder_MM_page && m_extruders_count == 1))
+ {
+ // if we have a single extruder MM setup, add a page with configuration options:
+ for (int i = 0; i < m_pages.size(); ++i) // first make sure it's not there already
+ if (m_pages[i]->title().find(_(L("Single extruder MM setup"))) != std::string::npos) {
+ m_pages.erase(m_pages.begin() + i);
+ break;
+ }
+ m_has_single_extruder_MM_page = false;
+ }
+ if (m_extruders_count > 1 && m_config->opt_bool("single_extruder_multi_material") && !m_has_single_extruder_MM_page) {
+ // create a page, but pretend it's an extruder page, so we can add it to m_pages ourselves
+ auto page = add_options_page(_(L("Single extruder MM setup")), "printer_empty.png", true);
+ auto optgroup = page->new_optgroup(_(L("Single extruder multimaterial parameters")));
+ optgroup->append_single_option_line("cooling_tube_retraction");
+ optgroup->append_single_option_line("cooling_tube_length");
+ optgroup->append_single_option_line("parking_pos_retraction");
+ optgroup->append_single_option_line("extra_loading_move");
+ m_pages.insert(m_pages.end() - n_after_single_extruder_MM, page);
+ m_has_single_extruder_MM_page = true;
+ }
+
+
+ for (auto extruder_idx = m_extruders_count_old; extruder_idx < m_extruders_count; ++extruder_idx){
+ //# build page
+ char buf[MIN_BUF_LENGTH_FOR_L];
+ sprintf(buf, _CHB(L("Extruder %d")), extruder_idx + 1);
+ auto page = add_options_page(from_u8(buf), "funnel.png", true);
+ m_pages.insert(m_pages.begin() + n_before_extruders + extruder_idx, page);
+
+ auto optgroup = page->new_optgroup(_(L("Size")));
+ optgroup->append_single_option_line("nozzle_diameter", extruder_idx);
+
+ optgroup = page->new_optgroup(_(L("Layer height limits")));
+ optgroup->append_single_option_line("min_layer_height", extruder_idx);
+ optgroup->append_single_option_line("max_layer_height", extruder_idx);
+
+
+ optgroup = page->new_optgroup(_(L("Position (for multi-extruder printers)")));
+ optgroup->append_single_option_line("extruder_offset", extruder_idx);
+
+ optgroup = page->new_optgroup(_(L("Retraction")));
+ optgroup->append_single_option_line("retract_length", extruder_idx);
+ optgroup->append_single_option_line("retract_lift", extruder_idx);
+ Line line = { _(L("Only lift Z")), "" };
+ line.append_option(optgroup->get_option("retract_lift_above", extruder_idx));
+ line.append_option(optgroup->get_option("retract_lift_below", extruder_idx));
+ optgroup->append_line(line);
+
+ optgroup->append_single_option_line("retract_speed", extruder_idx);
+ optgroup->append_single_option_line("deretract_speed", extruder_idx);
+ optgroup->append_single_option_line("retract_restart_extra", extruder_idx);
+ optgroup->append_single_option_line("retract_before_travel", extruder_idx);
+ optgroup->append_single_option_line("retract_layer_change", extruder_idx);
+ optgroup->append_single_option_line("wipe", extruder_idx);
+ optgroup->append_single_option_line("retract_before_wipe", extruder_idx);
+
+ optgroup = page->new_optgroup(_(L("Retraction when tool is disabled (advanced settings for multi-extruder setups)")));
+ optgroup->append_single_option_line("retract_length_toolchange", extruder_idx);
+ optgroup->append_single_option_line("retract_restart_extra_toolchange", extruder_idx);
+
+ optgroup = page->new_optgroup(_(L("Preview")));
+ optgroup->append_single_option_line("extruder_colour", extruder_idx);
+ }
+
+ // # remove extra pages
+ if (m_extruders_count < m_extruders_count_old)
+ m_pages.erase( m_pages.begin() + n_before_extruders + m_extruders_count,
+ m_pages.begin() + n_before_extruders + m_extruders_count_old);
+
+ m_extruders_count_old = m_extruders_count;
+ rebuild_page_tree();
+}
+
+// this gets executed after preset is loaded and before GUI fields are updated
+void TabPrinter::on_preset_loaded()
+{
+ // update the extruders count field
+ auto *nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(m_config->option("nozzle_diameter"));
+ int extruders_count = nozzle_diameter->values.size();
+ set_value("extruders_count", extruders_count);
+ // update the GUI field according to the number of nozzle diameters supplied
+ extruders_count_changed(extruders_count);
+}
+
+void TabPrinter::update_pages()
+{
+ // update m_pages ONLY if printer technology is changed
+ if (m_presets->get_edited_preset().printer_technology() == m_printer_technology)
+ return;
+
+ // hide all old pages
+ for (auto& el : m_pages)
+ el.get()->Hide();
+
+ // set m_pages to m_pages_(technology before changing)
+ m_printer_technology == ptFFF ? m_pages.swap(m_pages_fff) : m_pages.swap(m_pages_sla);
+
+ // build Tab according to the technology, if it's not exist jet OR
+ // set m_pages_(technology after changing) to m_pages
+ if (m_presets->get_edited_preset().printer_technology() == ptFFF)
+ m_pages_fff.empty() ? build_fff() : m_pages.swap(m_pages_fff);
+ else
+ m_pages_sla.empty() ? build_sla() : m_pages.swap(m_pages_sla);
+
+ rebuild_page_tree(true);
+
+ on_value_change("printer_technology", m_presets->get_edited_preset().printer_technology()); // to update show/hide preset ComboBoxes
+}
+
+void TabPrinter::update()
+{
+ m_presets->get_edited_preset().printer_technology() == ptFFF ? update_fff() : update_sla();
+}
+
+void TabPrinter::update_fff()
+{
+ Freeze();
+
+ bool en;
+ auto serial_speed = get_field("serial_speed");
+ if (serial_speed != nullptr) {
+ en = !m_config->opt_string("serial_port").empty();
+ get_field("serial_speed")->toggle(en);
+ if (m_config->opt_int("serial_speed") != 0 && en)
+ m_serial_test_btn->Enable();
+ else
+ m_serial_test_btn->Disable();
+ }
+
+ {
+ std::unique_ptr<PrintHost> host(PrintHost::get_print_host(m_config));
+ m_print_host_test_btn->Enable(!m_config->opt_string("print_host").empty() && host->can_test());
+ m_printhost_browse_btn->Enable(host->has_auto_discovery());
+ }
+
+ bool have_multiple_extruders = m_extruders_count > 1;
+ get_field("toolchange_gcode")->toggle(have_multiple_extruders);
+ get_field("single_extruder_multi_material")->toggle(have_multiple_extruders);
+
+ bool is_marlin_flavor = m_config->option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor")->value == gcfMarlin;
+
+ {
+ Field *sm = get_field("silent_mode");
+ if (! is_marlin_flavor)
+ // Disable silent mode for non-marlin firmwares.
+ get_field("silent_mode")->toggle(false);
+ if (is_marlin_flavor)
+ sm->enable();
+ else
+ sm->disable();
+ }
+
+ if (m_use_silent_mode != m_config->opt_bool("silent_mode")) {
+ m_rebuild_kinematics_page = true;
+ m_use_silent_mode = m_config->opt_bool("silent_mode");
+ }
+
+ for (size_t i = 0; i < m_extruders_count; ++i) {
+ bool have_retract_length = m_config->opt_float("retract_length", i) > 0;
+
+ // when using firmware retraction, firmware decides retraction length
+ bool use_firmware_retraction = m_config->opt_bool("use_firmware_retraction");
+ get_field("retract_length", i)->toggle(!use_firmware_retraction);
+
+ // user can customize travel length if we have retraction length or we"re using
+ // firmware retraction
+ get_field("retract_before_travel", i)->toggle(have_retract_length || use_firmware_retraction);
+
+ // user can customize other retraction options if retraction is enabled
+ bool retraction = (have_retract_length || use_firmware_retraction);
+ std::vector<std::string> vec = { "retract_lift", "retract_layer_change" };
+ for (auto el : vec)
+ get_field(el, i)->toggle(retraction);
+
+ // retract lift above / below only applies if using retract lift
+ vec.resize(0);
+ vec = { "retract_lift_above", "retract_lift_below" };
+ for (auto el : vec)
+ get_field(el, i)->toggle(retraction && m_config->opt_float("retract_lift", i) > 0);
+
+ // some options only apply when not using firmware retraction
+ vec.resize(0);
+ vec = { "retract_speed", "deretract_speed", "retract_before_wipe", "retract_restart_extra", "wipe" };
+ for (auto el : vec)
+ get_field(el, i)->toggle(retraction && !use_firmware_retraction);
+
+ bool wipe = m_config->opt_bool("wipe", i);
+ get_field("retract_before_wipe", i)->toggle(wipe);
+
+ if (use_firmware_retraction && wipe) {
+ auto dialog = new wxMessageDialog(parent(),
+ _(L("The Wipe option is not available when using the Firmware Retraction mode.\n"
+ "\nShall I disable it in order to enable Firmware Retraction?")),
+ _(L("Firmware Retraction")), wxICON_WARNING | wxYES | wxNO);
+
+ DynamicPrintConfig new_conf = *m_config;
+ if (dialog->ShowModal() == wxID_YES) {
+ auto wipe = static_cast<ConfigOptionBools*>(m_config->option("wipe")->clone());
+ for (int w = 0; w < wipe->values.size(); w++)
+ wipe->values[w] = false;
+ new_conf.set_key_value("wipe", wipe);
+ }
+ else {
+ new_conf.set_key_value("use_firmware_retraction", new ConfigOptionBool(false));
+ }
+ load_config(new_conf);
+ }
+
+ get_field("retract_length_toolchange", i)->toggle(have_multiple_extruders);
+
+ bool toolchange_retraction = m_config->opt_float("retract_length_toolchange", i) > 0;
+ get_field("retract_restart_extra_toolchange", i)->toggle
+ (have_multiple_extruders && toolchange_retraction);
+ }
+
+ Thaw();
+}
+
+void TabPrinter::update_sla(){ ; }
+
+// Initialize the UI from the current preset
+void Tab::load_current_preset()
+{
+ auto preset = m_presets->get_edited_preset();
+
+ (preset.is_default || preset.is_system) ? m_btn_delete_preset->Disable() : m_btn_delete_preset->Enable(true);
+
+ update();
+ // For the printer profile, generate the extruder pages.
+ if (preset.printer_technology() == ptFFF)
+ on_preset_loaded();
+ // Reload preset pages with the new configuration values.
+ reload_config();
+
+ m_bmp_non_system = m_presets->get_selected_preset_parent() ? &m_bmp_value_unlock : &m_bmp_white_bullet;
+ m_ttg_non_system = m_presets->get_selected_preset_parent() ? &m_ttg_value_unlock : &m_ttg_white_bullet_ns;
+ m_tt_non_system = m_presets->get_selected_preset_parent() ? &m_tt_value_unlock : &m_ttg_white_bullet_ns;
+
+ m_undo_to_sys_btn->Enable(!preset.is_default);
+
+ // use CallAfter because some field triggers schedule on_change calls using CallAfter,
+ // and we don't want them to be called after this update_dirty() as they would mark the
+ // preset dirty again
+ // (not sure this is true anymore now that update_dirty is idempotent)
+ wxTheApp->CallAfter([this]{
+ // checking out if this Tab exists till this moment
+ if (!checked_tab(this))
+ return;
+ update_tab_ui();
+
+ // update show/hide tabs
+ if (m_name == "printer"){
+ PrinterTechnology& printer_technology = m_presets->get_edited_preset().printer_technology();
+ if (printer_technology != static_cast<TabPrinter*>(this)->m_printer_technology)
+ {
+ for (auto& tab : get_preset_tabs()){
+ if (tab.technology != printer_technology)
+ {
+ int page_id = get_tab_panel()->FindPage(tab.panel);
+ get_tab_panel()->GetPage(page_id)->Show(false);
+ get_tab_panel()->RemovePage(page_id);
+ }
+ else
+ get_tab_panel()->InsertPage(get_tab_panel()->FindPage(this), tab.panel, tab.panel->title());
+ }
+
+ static_cast<TabPrinter*>(this)->m_printer_technology = printer_technology;
+ }
+ }
+
+ on_presets_changed();
+
+ if (name() == "print")
+ update_frequently_changed_parameters();
+ if (m_name == "printer"){
+ static_cast<TabPrinter*>(this)->m_initial_extruders_count = static_cast<TabPrinter*>(this)->m_extruders_count;
+ const Preset* parent_preset = m_presets->get_selected_preset_parent();
+ static_cast<TabPrinter*>(this)->m_sys_extruders_count = parent_preset == nullptr ? 0 :
+ static_cast<const ConfigOptionFloats*>(parent_preset->config.option("nozzle_diameter"))->values.size();
+ }
+ m_opt_status_value = (m_presets->get_selected_preset_parent() ? osSystemValue : 0) | osInitValue;
+ init_options_list();
+ update_changed_ui();
+ });
+}
+
+//Regerenerate content of the page tree.
+void Tab::rebuild_page_tree(bool tree_sel_change_event /*= false*/)
+{
+ Freeze();
+ // get label of the currently selected item
+ auto selected = m_treectrl->GetItemText(m_treectrl->GetSelection());
+ auto rootItem = m_treectrl->GetRootItem();
+
+ auto have_selection = 0;
+ m_treectrl->DeleteChildren(rootItem);
+ for (auto p : m_pages)
+ {
+ auto itemId = m_treectrl->AppendItem(rootItem, p->title(), p->iconID());
+ m_treectrl->SetItemTextColour(itemId, p->get_item_colour());
+ if (p->title() == selected) {
+ if (!(p->title() == _(L("Machine limits")) || p->title() == _(L("Single extruder MM setup")))) // These Pages have to be updated inside OnTreeSelChange
+ m_disable_tree_sel_changed_event = !tree_sel_change_event;
+ m_treectrl->SelectItem(itemId);
+ m_disable_tree_sel_changed_event = false;
+ have_selection = 1;
+ }
+ }
+
+ if (!have_selection) {
+ // this is triggered on first load, so we don't disable the sel change event
+ m_treectrl->SelectItem(m_treectrl->GetFirstVisibleItem());//! (treectrl->GetFirstChild(rootItem));
+ }
+ Thaw();
+}
+
+// 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 /*= ""*/)
+{
+ // If no name is provided, select the "-- default --" preset.
+ if (preset_name.empty())
+ preset_name = m_presets->default_preset().name;
+ auto current_dirty = m_presets->current_is_dirty();
+ auto printer_tab = m_presets->name() == "printer";
+ auto canceled = false;
+ m_reload_dependent_tabs = {};
+ if (current_dirty && !may_discard_current_dirty_preset()) {
+ canceled = true;
+ } else if (printer_tab) {
+ // Before switching the printer to a new one, verify, whether the currently active print and filament
+ // are compatible with the new printer.
+ // If they are not compatible and the current print or filament are dirty, let user decide
+ // whether to discard the changes or keep the current printer selection.
+ //
+ // With the introduction of the SLA printer types, we need to support switching between
+ // the FFF and SLA printers.
+ const Preset &new_printer_preset = *m_presets->find_preset(preset_name, true);
+ PrinterTechnology old_printer_technology = m_presets->get_edited_preset().printer_technology();
+ PrinterTechnology new_printer_technology = new_printer_preset.printer_technology();
+ struct PresetUpdate {
+ std::string name;
+ PresetCollection *presets;
+ PrinterTechnology technology;
+ bool old_preset_dirty;
+ bool new_preset_compatible;
+ };
+ std::vector<PresetUpdate> updates = {
+ { "print", &m_preset_bundle->prints, ptFFF },
+ { "filament", &m_preset_bundle->filaments, ptFFF },
+ { "sla_material", &m_preset_bundle->sla_materials, ptSLA }
+ };
+ for (PresetUpdate &pu : updates) {
+ pu.old_preset_dirty = (old_printer_technology == pu.technology) && pu.presets->current_is_dirty();
+ pu.new_preset_compatible = (new_printer_technology == pu.technology) && pu.presets->get_edited_preset().is_compatible_with_printer(new_printer_preset);
+ if (! canceled)
+ canceled = pu.old_preset_dirty && ! pu.new_preset_compatible && ! may_discard_current_dirty_preset(pu.presets, preset_name);
+ }
+ if (! canceled) {
+ for (PresetUpdate &pu : updates) {
+ // The preset will be switched to a different, compatible preset, or the '-- default --'.
+ if (pu.technology == new_printer_technology)
+ m_reload_dependent_tabs.emplace_back(pu.name);
+ if (pu.old_preset_dirty)
+ pu.presets->discard_current_changes();
+ }
+ }
+ }
+ if (canceled) {
+ update_tab_ui();
+ // Trigger the on_presets_changed event so that we also restore the previous value in the plater selector,
+ // if this action was initiated from the platter.
+ on_presets_changed();
+ } else {
+ if (current_dirty)
+ m_presets->discard_current_changes() ;
+ m_presets->select_preset_by_name(preset_name, false);
+ // Mark the print & filament enabled if they are compatible with the currently selected preset.
+ // The following method should not discard changes of current print or filament presets on change of a printer profile,
+ // if they are compatible with the current printer.
+ if (current_dirty || printer_tab)
+ m_preset_bundle->update_compatible_with_printer(true);
+ // Initialize the UI from the current preset.
+ if (printer_tab)
+ static_cast<TabPrinter*>(this)->update_pages();
+ load_current_preset();
+ }
+}
+
+// If the current preset is dirty, the user is asked whether the changes may be discarded.
+// if the current preset was not dirty, or the user agreed to discard the changes, 1 is returned.
+bool Tab::may_discard_current_dirty_preset(PresetCollection* presets /*= nullptr*/, const std::string& new_printer_name /*= ""*/)
+{
+ if (presets == nullptr) presets = m_presets;
+ // Display a dialog showing the dirty options in a human readable form.
+ auto old_preset = presets->get_edited_preset();
+ auto type_name = presets->name();
+ auto tab = " ";
+ auto name = old_preset.is_default ?
+ _(L("Default ")) + type_name + _(L(" preset")) :
+ (type_name + _(L(" preset\n")) + tab + old_preset.name);
+ // Collect descriptions of the dirty options.
+ std::vector<std::string> option_names;
+ for(auto opt_key: presets->current_dirty_options()) {
+ auto opt = m_config->def()->options.at(opt_key);
+ std::string name = "";
+ if (!opt.category.empty())
+ name += opt.category + " > ";
+ name += !opt.full_label.empty() ?
+ opt.full_label :
+ opt.label;
+ option_names.push_back(name);
+ }
+ // Show a confirmation dialog with the list of dirty options.
+ std::string changes = "";
+ for (auto changed_name : option_names)
+ changes += tab + changed_name + "\n";
+ auto message = (!new_printer_name.empty()) ?
+ name + _(L("\n\nis not compatible with printer\n")) +tab + new_printer_name+ _(L("\n\nand it has the following unsaved changes:")) :
+ name + _(L("\n\nhas the following unsaved changes:"));
+ auto confirm = new wxMessageDialog(parent(),
+ message + "\n" +changes +_(L("\n\nDiscard changes and continue anyway?")),
+ _(L("Unsaved Changes")), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION);
+ return confirm->ShowModal() == wxID_YES;
+}
+
+void Tab::OnTreeSelChange(wxTreeEvent& event)
+{
+ if (m_disable_tree_sel_changed_event) return;
+
+// There is a bug related to Ubuntu overlay scrollbars, see https://github.com/prusa3d/Slic3r/issues/898 and https://github.com/prusa3d/Slic3r/issues/952.
+// The issue apparently manifests when Show()ing a window with overlay scrollbars while the UI is frozen. For this reason,
+// we will Thaw the UI prematurely on Linux. This means destroing the no_updates object prematurely.
+#ifdef __linux__
+ std::unique_ptr<wxWindowUpdateLocker> no_updates(new wxWindowUpdateLocker(this));
+#else
+ wxWindowUpdateLocker noUpdates(this);
+#endif
+
+ Page* page = nullptr;
+ auto selection = m_treectrl->GetItemText(m_treectrl->GetSelection());
+ for (auto p : m_pages)
+ if (p->title() == selection)
+ {
+ page = p.get();
+ m_is_nonsys_values = page->m_is_nonsys_values;
+ m_is_modified_values = page->m_is_modified_values;
+ break;
+ }
+ if (page == nullptr) return;
+
+ for (auto& el : m_pages)
+ el.get()->Hide();
+
+#ifdef __linux__
+ no_updates.reset(nullptr);
+#endif
+
+ page->Show();
+ m_hsizer->Layout();
+ Refresh();
+
+ update_undo_buttons();
+}
+
+void Tab::OnKeyDown(wxKeyEvent& event)
+{
+ if (event.GetKeyCode() == WXK_TAB)
+ m_treectrl->Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward);
+ else
+ event.Skip();
+}
+
+// Save the current preset into file.
+// This removes the "dirty" flag of the preset, possibly creates a new preset under a new name,
+// and activates the new preset.
+// Wizard calls save_preset with a name "My Settings", otherwise no name is provided and this method
+// opens a Slic3r::GUI::SavePresetWindow dialog.
+void Tab::save_preset(std::string name /*= ""*/)
+{
+ // since buttons(and choices too) don't get focus on Mac, we set focus manually
+ // to the treectrl so that the EVT_* events are fired for the input field having
+ // focus currently.is there anything better than this ?
+//! m_treectrl->OnSetFocus();
+
+ if (name.empty()) {
+ auto preset = m_presets->get_selected_preset();
+ auto default_name = preset.is_default ? "Untitled" : preset.name;
+ bool have_extention = boost::iends_with(default_name, ".ini");
+ if (have_extention)
+ {
+ size_t len = default_name.length()-4;
+ default_name.resize(len);
+ }
+ //[map $_->name, grep !$_->default && !$_->external, @{$self->{presets}}],
+ std::vector<std::string> values;
+ for (size_t i = 0; i < m_presets->size(); ++i) {
+ const Preset &preset = m_presets->preset(i);
+ if (preset.is_default || preset.is_system || preset.is_external)
+ continue;
+ values.push_back(preset.name);
+ }
+
+ auto dlg = new SavePresetWindow(parent());
+ dlg->build(title(), default_name, values);
+ if (dlg->ShowModal() != wxID_OK)
+ return;
+ name = dlg->get_name();
+ if (name == ""){
+ show_error(this, _(L("The supplied name is empty. It can't be saved.")));
+ return;
+ }
+ const Preset *existing = m_presets->find_preset(name, false);
+ if (existing && (existing->is_default || existing->is_system)) {
+ show_error(this, _(L("Cannot overwrite a system profile.")));
+ return;
+ }
+ if (existing && (existing->is_external)) {
+ show_error(this, _(L("Cannot overwrite an external profile.")));
+ return;
+ }
+ }
+
+ // Save the preset into Slic3r::data_dir / presets / section_name / preset_name.ini
+ m_presets->save_current_preset(name);
+ // Mark the print & filament enabled if they are compatible with the currently selected preset.
+ m_preset_bundle->update_compatible_with_printer(false);
+ // Add the new item into the UI component, remove dirty flags and activate the saved item.
+ update_tab_ui();
+ // Update the selection boxes at the platter.
+ on_presets_changed();
+ // If current profile is saved, "delete preset" button have to be enabled
+ m_btn_delete_preset->Enable(true);
+
+ if (m_name == "printer")
+ static_cast<TabPrinter*>(this)->m_initial_extruders_count = static_cast<TabPrinter*>(this)->m_extruders_count;
+ update_changed_ui();
+}
+
+// Called for a currently selected preset.
+void Tab::delete_preset()
+{
+ auto current_preset = m_presets->get_selected_preset();
+ // Don't let the user delete the ' - default - ' configuration.
+ wxString action = current_preset.is_external ? _(L("remove")) : _(L("delete"));
+ wxString msg = _(L("Are you sure you want to ")) + action + _(L(" the selected preset?"));
+ action = current_preset.is_external ? _(L("Remove")) : _(L("Delete"));
+ wxString title = action + _(L(" Preset"));
+ if (current_preset.is_default ||
+ wxID_YES != wxMessageDialog(parent(), msg, title, wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION).ShowModal())
+ return;
+ // Delete the file and select some other reasonable preset.
+ // The 'external' presets will only be removed from the preset list, their files will not be deleted.
+ try{ m_presets->delete_current_preset(); }
+ catch (const std::exception &e)
+ {
+ return;
+ }
+ // Load the newly selected preset into the UI, update selection combo boxes with their dirty flags.
+ load_current_preset();
+}
+
+void Tab::toggle_show_hide_incompatible()
+{
+ m_show_incompatible_presets = !m_show_incompatible_presets;
+ update_show_hide_incompatible_button();
+ update_tab_ui();
+}
+
+void Tab::update_show_hide_incompatible_button()
+{
+ m_btn_hide_incompatible_presets->SetBitmap(m_show_incompatible_presets ?
+ m_bmp_show_incompatible_presets : m_bmp_hide_incompatible_presets);
+ m_btn_hide_incompatible_presets->SetToolTip(m_show_incompatible_presets ?
+ "Both compatible an incompatible presets are shown. Click to hide presets not compatible with the current printer." :
+ "Only compatible presets are shown. Click to show both the presets compatible and not compatible with the current printer.");
+}
+
+void Tab::update_ui_from_settings()
+{
+ // Show the 'show / hide presets' button only for the print and filament tabs, and only if enabled
+ // in application preferences.
+ m_show_btn_incompatible_presets = get_app_config()->get("show_incompatible_presets")[0] == '1' ? true : false;
+ bool show = m_show_btn_incompatible_presets && m_presets->name().compare("printer") != 0;
+ show ? m_btn_hide_incompatible_presets->Show() : m_btn_hide_incompatible_presets->Hide();
+ // If the 'show / hide presets' button is hidden, hide the incompatible presets.
+ if (show) {
+ update_show_hide_incompatible_button();
+ }
+ else {
+ if (m_show_incompatible_presets) {
+ m_show_incompatible_presets = false;
+ update_tab_ui();
+ }
+ }
+}
+
+// Return a callback to create a Tab widget to mark the preferences as compatible / incompatible to the current printer.
+wxSizer* Tab::compatible_printers_widget(wxWindow* parent, wxCheckBox** checkbox, wxButton** btn)
+{
+ *checkbox = new wxCheckBox(parent, wxID_ANY, _(L("All")));
+ *btn = new wxButton(parent, wxID_ANY, _(L(" Set "))+dots, wxDefaultPosition, wxDefaultSize, wxBU_LEFT | wxBU_EXACTFIT);
+
+ (*btn)->SetBitmap(wxBitmap(from_u8(Slic3r::var("printer_empty.png")), wxBITMAP_TYPE_PNG));
+
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add((*checkbox), 0, wxALIGN_CENTER_VERTICAL);
+ sizer->Add((*btn), 0, wxALIGN_CENTER_VERTICAL);
+
+ (*checkbox)->Bind(wxEVT_CHECKBOX, ([=](wxCommandEvent e)
+ {
+ (*btn)->Enable(!(*checkbox)->GetValue());
+ // All printers have been made compatible with this preset.
+ if ((*checkbox)->GetValue())
+ load_key_value("compatible_printers", std::vector<std::string> {});
+ get_field("compatible_printers_condition")->toggle((*checkbox)->GetValue());
+ update_changed_ui();
+ }) );
+
+ (*btn)->Bind(wxEVT_BUTTON, ([this, parent, checkbox, btn](wxCommandEvent e)
+ {
+ // # Collect names of non-default non-external printer profiles.
+ PresetCollection *printers = &m_preset_bundle->printers;
+ wxArrayString presets;
+ for (size_t idx = 0; idx < printers->size(); ++idx)
+ {
+ Preset& preset = printers->preset(idx);
+ if (!preset.is_default && !preset.is_external && !preset.is_system)
+ presets.Add(preset.name);
+ }
+
+ 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<const ConfigOptionStrings*>(m_config->option("compatible_printers"));
+ if (compatible_printers != nullptr || !compatible_printers->values.empty())
+ for (auto preset_name : compatible_printers->values)
+ for (size_t idx = 0; idx < presets.GetCount(); ++idx)
+ if (presets[idx].compare(preset_name) == 0)
+ {
+ selections.Add(idx);
+ break;
+ }
+ dlg.SetSelections(selections);
+ std::vector<std::string> value;
+ // Show the dialog.
+ if (dlg.ShowModal() == wxID_OK) {
+ selections.Clear();
+ selections = dlg.GetSelections();
+ for (auto idx : selections)
+ value.push_back(presets[idx].ToStdString());
+ if (value.empty()) {
+ (*checkbox)->SetValue(1);
+ (*btn)->Disable();
+ }
+ // All printers have been made compatible with this preset.
+ load_key_value("compatible_printers", value);
+ update_changed_ui();
+ }
+ }));
+ return sizer;
+}
+
+void Tab::update_presetsctrl(wxDataViewTreeCtrl* ui, bool show_incompatible)
+{
+ if (ui == nullptr)
+ return;
+ ui->Freeze();
+ ui->DeleteAllItems();
+ auto presets = m_presets->get_presets();
+ auto idx_selected = m_presets->get_idx_selected();
+ auto suffix_modified = m_presets->get_suffix_modified();
+ int icon_compatible = 0;
+ int icon_incompatible = 1;
+ int cnt_items = 0;
+
+ auto root_sys = ui->AppendContainer(wxDataViewItem(0), _(L("System presets")));
+ auto root_def = ui->AppendContainer(wxDataViewItem(0), _(L("Default presets")));
+
+ auto show_def = get_app_config()->get("no_defaults")[0] != '1';
+
+ for (size_t i = presets.front().is_visible ? 0 : 1; i < presets.size(); ++i) {
+ const Preset &preset = presets[i];
+ if (!preset.is_visible || (!show_incompatible && !preset.is_compatible && i != idx_selected))
+ continue;
+
+ auto preset_name = wxString::FromUTF8((preset.name + (preset.is_dirty ? suffix_modified : "")).c_str());
+
+ wxDataViewItem item;
+ if (preset.is_system)
+ item = ui->AppendItem(root_sys, preset_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ else if (show_def && preset.is_default)
+ item = ui->AppendItem(root_def, preset_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ else
+ {
+ auto parent = m_presets->get_preset_parent(preset);
+ if (parent == nullptr)
+ item = ui->AppendItem(root_def, preset_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ else
+ {
+ auto parent_name = parent->name;
+
+ wxDataViewTreeStoreContainerNode *node = ui->GetStore()->FindContainerNode(root_sys);
+ if (node)
+ {
+ wxDataViewTreeStoreNodeList::iterator iter;
+ for (iter = node->GetChildren().begin(); iter != node->GetChildren().end(); iter++)
+ {
+ wxDataViewTreeStoreNode* child = *iter;
+ auto child_item = child->GetItem();
+ auto item_text = ui->GetItemText(child_item);
+ if (item_text == parent_name)
+ {
+ auto added_child = ui->AppendItem(child->GetItem(), preset_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ if (!added_child){
+ ui->DeleteItem(child->GetItem());
+ auto new_parent = ui->AppendContainer(root_sys, parent_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ ui->AppendItem(new_parent, preset_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ cnt_items++;
+ if (i == idx_selected){
+ ui->Select(item);
+ m_cc_presets_choice->SetText(preset_name);
+ }
+ }
+ if (ui->GetStore()->GetChildCount(root_def) == 0)
+ ui->DeleteItem(root_def);
+
+ ui->Thaw();
+}
+
+void Tab::update_tab_presets(wxComboCtrl* ui, bool show_incompatible)
+{
+ if (ui == nullptr)
+ return;
+ ui->Freeze();
+ ui->Clear();
+ auto presets = m_presets->get_presets();
+ auto idx_selected = m_presets->get_idx_selected();
+ auto suffix_modified = m_presets->get_suffix_modified();
+ int icon_compatible = 0;
+ int icon_incompatible = 1;
+ int cnt_items = 0;
+
+ wxDataViewTreeCtrlComboPopup* popup = wxDynamicCast(m_cc_presets_choice->GetPopupControl(), wxDataViewTreeCtrlComboPopup);
+ if (popup != nullptr)
+ {
+ popup->DeleteAllItems();
+
+ auto root_sys = popup->AppendContainer(wxDataViewItem(0), _(L("System presets")));
+ auto root_def = popup->AppendContainer(wxDataViewItem(0), _(L("Default presets")));
+
+ auto show_def = get_app_config()->get("no_defaults")[0] != '1';
+
+ for (size_t i = presets.front().is_visible ? 0 : 1; i < presets.size(); ++i) {
+ const Preset &preset = presets[i];
+ if (!preset.is_visible || (!show_incompatible && !preset.is_compatible && i != idx_selected))
+ continue;
+
+ auto preset_name = wxString::FromUTF8((preset.name + (preset.is_dirty ? suffix_modified : "")).c_str());
+
+ wxDataViewItem item;
+ if (preset.is_system)
+ item = popup->AppendItem(root_sys, preset_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ else if (show_def && preset.is_default)
+ item = popup->AppendItem(root_def, preset_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ else
+ {
+ auto parent = m_presets->get_preset_parent(preset);
+ if (parent == nullptr)
+ item = popup->AppendItem(root_def, preset_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ else
+ {
+ auto parent_name = parent->name;
+
+ wxDataViewTreeStoreContainerNode *node = popup->GetStore()->FindContainerNode(root_sys);
+ if (node)
+ {
+ wxDataViewTreeStoreNodeList::iterator iter;
+ for (iter = node->GetChildren().begin(); iter != node->GetChildren().end(); iter++)
+ {
+ wxDataViewTreeStoreNode* child = *iter;
+ auto child_item = child->GetItem();
+ auto item_text = popup->GetItemText(child_item);
+ if (item_text == parent_name)
+ {
+ auto added_child = popup->AppendItem(child->GetItem(), preset_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ if (!added_child){
+ popup->DeleteItem(child->GetItem());
+ auto new_parent = popup->AppendContainer(root_sys, parent_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ popup->AppendItem(new_parent, preset_name,
+ preset.is_compatible ? icon_compatible : icon_incompatible);
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ cnt_items++;
+ if (i == idx_selected){
+ popup->Select(item);
+ m_cc_presets_choice->SetText(preset_name);
+ }
+ }
+ if (popup->GetStore()->GetChildCount(root_def) == 0)
+ popup->DeleteItem(root_def);
+ }
+ ui->Thaw();
+}
+
+void Tab::fill_icon_descriptions()
+{
+ m_icon_descriptions.push_back(t_icon_description(&m_bmp_value_lock, L("LOCKED LOCK;"
+ "indicates that the settings are the same as the system values for the current option group")));
+
+ m_icon_descriptions.push_back(t_icon_description(&m_bmp_value_unlock, L("UNLOCKED LOCK;"
+ "indicates that some settings were changed and are not equal to the system values for "
+ "the current option group.\n"
+ "Click the UNLOCKED LOCK icon to reset all settings for current option group to "
+ "the system values.")));
+
+ m_icon_descriptions.push_back(t_icon_description(&m_bmp_white_bullet, L("WHITE BULLET;"
+ "for the left button: \tindicates a non-system preset,\n"
+ "for the right button: \tindicates that the settings hasn't been modified.")));
+
+ m_icon_descriptions.push_back(t_icon_description(&m_bmp_value_revert, L("BACK ARROW;"
+ "indicates that the settings were changed and are not equal to the last saved preset for "
+ "the current option group.\n"
+ "Click the BACK ARROW icon to reset all settings for the current option group to "
+ "the last saved preset.")));
+}
+
+void Tab::set_tooltips_text()
+{
+// m_undo_to_sys_btn->SetToolTip(_(L( "LOCKED LOCK icon indicates that the settings are the same as the system values "
+// "for the current option group.\n"
+// "UNLOCKED LOCK icon indicates that some settings were changed and are not equal "
+// "to the system values for the current option group.\n"
+// "WHITE BULLET icon indicates a non system preset.\n\n"
+// "Click the UNLOCKED LOCK icon to reset all settings for current option group to "
+// "the system values.")));
+//
+// m_undo_btn->SetToolTip(_(L( "WHITE BULLET icon indicates that the settings are the same as in the last saved"
+// "preset for the current option group.\n"
+// "BACK ARROW icon indicates that the settings were changed and are not equal to "
+// "the last saved preset for the current option group.\n\n"
+// "Click the BACK ARROW icon to reset all settings for the current option group to "
+// "the last saved preset.")));
+
+ // --- Tooltip text for reset buttons (for whole options group)
+ // Text to be shown on the "Revert to system" aka "Lock to system" button next to each input field.
+ m_ttg_value_lock = _(L("LOCKED LOCK icon indicates that the settings are the same as the system values "
+ "for the current option group"));
+ m_ttg_value_unlock = _(L("UNLOCKED LOCK icon indicates that some settings were changed and are not equal "
+ "to the system values for the current option group.\n"
+ "Click to reset all settings for current option group to the system values."));
+ m_ttg_white_bullet_ns = _(L("WHITE BULLET icon indicates a non system preset."));
+ m_ttg_non_system = &m_ttg_white_bullet_ns;
+ // Text to be shown on the "Undo user changes" button next to each input field.
+ m_ttg_white_bullet = _(L("WHITE BULLET icon indicates that the settings are the same as in the last saved "
+ "preset for the current option group."));
+ m_ttg_value_revert = _(L("BACK ARROW icon indicates that the settings were changed and are not equal to "
+ "the last saved preset for the current option group.\n"
+ "Click to reset all settings for the current option group to the last saved preset."));
+
+ // --- Tooltip text for reset buttons (for each option in group)
+ // Text to be shown on the "Revert to system" aka "Lock to system" button next to each input field.
+ m_tt_value_lock = _(L("LOCKED LOCK icon indicates that the value is the same as the system value."));
+ m_tt_value_unlock = _(L("UNLOCKED LOCK icon indicates that the value was changed and is not equal "
+ "to the system value.\n"
+ "Click to reset current value to the system value."));
+ // m_tt_white_bullet_ns= _(L("WHITE BULLET icon indicates a non system preset."));
+ m_tt_non_system = &m_ttg_white_bullet_ns;
+ // Text to be shown on the "Undo user changes" button next to each input field.
+ m_tt_white_bullet = _(L("WHITE BULLET icon indicates that the value is the same as in the last saved preset."));
+ m_tt_value_revert = _(L("BACK ARROW icon indicates that the value was changed and is not equal to the last saved preset.\n"
+ "Click to reset current value to the last saved preset."));
+}
+
+void Page::reload_config()
+{
+ for (auto group : m_optgroups)
+ group->reload_config();
+}
+
+Field* Page::get_field(const t_config_option_key& opt_key, int opt_index /*= -1*/) const
+{
+ Field* field = nullptr;
+ for (auto opt : m_optgroups){
+ field = opt->get_fieldc(opt_key, opt_index);
+ if (field != nullptr)
+ return field;
+ }
+ return field;
+}
+
+bool Page::set_value(const t_config_option_key& opt_key, const boost::any& value){
+ bool changed = false;
+ for(auto optgroup: m_optgroups) {
+ if (optgroup->set_value(opt_key, value))
+ changed = 1 ;
+ }
+ return changed;
+}
+
+// package Slic3r::GUI::Tab::Page;
+ConfigOptionsGroupShp Page::new_optgroup(const wxString& title, int noncommon_label_width /*= -1*/)
+{
+ //! config_ have to be "right"
+ ConfigOptionsGroupShp optgroup = std::make_shared<ConfigOptionsGroup>(this, title, m_config, true);
+ if (noncommon_label_width >= 0)
+ optgroup->label_width = noncommon_label_width;
+
+#ifdef __WXOSX__
+ auto tab = GetParent()->GetParent();
+#else
+ auto tab = GetParent();
+#endif
+ optgroup->m_on_change = [this, tab](t_config_option_key opt_key, boost::any value){
+ //! This function will be called from OptionGroup.
+ //! Using of CallAfter is redundant.
+ //! And in some cases it causes update() function to be recalled again
+//! wxTheApp->CallAfter([this, opt_key, value]() {
+ static_cast<Tab*>(tab)->update_dirty();
+ static_cast<Tab*>(tab)->on_value_change(opt_key, value);
+//! });
+ };
+
+ optgroup->m_get_initial_config = [this, tab](){
+ DynamicPrintConfig config = static_cast<Tab*>(tab)->m_presets->get_selected_preset().config;
+ return config;
+ };
+
+ optgroup->m_get_sys_config = [this, tab](){
+ DynamicPrintConfig config = static_cast<Tab*>(tab)->m_presets->get_selected_preset_parent()->config;
+ return config;
+ };
+
+ optgroup->have_sys_config = [this, tab](){
+ return static_cast<Tab*>(tab)->m_presets->get_selected_preset_parent() != nullptr;
+ };
+
+ vsizer()->Add(optgroup->sizer, 0, wxEXPAND | wxALL, 10);
+ m_optgroups.push_back(optgroup);
+
+ return optgroup;
+}
+
+void SavePresetWindow::build(const wxString& title, const std::string& default_name, std::vector<std::string> &values)
+{
+ auto text = new wxStaticText(this, wxID_ANY, _(L("Save ")) + title + _(L(" as:")),
+ wxDefaultPosition, wxDefaultSize);
+ m_combo = new wxComboBox(this, wxID_ANY, from_u8(default_name),
+ wxDefaultPosition, wxDefaultSize, 0, 0, wxTE_PROCESS_ENTER);
+ for (auto value : values)
+ m_combo->Append(from_u8(value));
+ auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL);
+
+ auto sizer = new wxBoxSizer(wxVERTICAL);
+ sizer->Add(text, 0, wxEXPAND | wxALL, 10);
+ sizer->Add(m_combo, 0, wxEXPAND | wxLEFT | wxRIGHT, 10);
+ sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxALL, 10);
+
+ wxButton* btn = static_cast<wxButton*>(FindWindowById(wxID_OK, this));
+ btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { accept(); });
+ m_combo->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { accept(); });
+
+ SetSizer(sizer);
+ sizer->SetSizeHints(this);
+}
+
+void SavePresetWindow::accept()
+{
+ m_chosen_name = normalize_utf8_nfc(m_combo->GetValue().ToUTF8());
+ if (!m_chosen_name.empty()) {
+ const char* unusable_symbols = "<>:/\\|?*\"";
+ bool is_unusable_symbol = false;
+ bool is_unusable_postfix = false;
+ const std::string unusable_postfix = PresetCollection::get_suffix_modified();//"(modified)";
+ for (size_t i = 0; i < std::strlen(unusable_symbols); i++){
+ if (m_chosen_name.find_first_of(unusable_symbols[i]) != std::string::npos){
+ is_unusable_symbol = true;
+ break;
+ }
+ }
+ if (m_chosen_name.find(unusable_postfix) != std::string::npos)
+ is_unusable_postfix = true;
+
+ if (is_unusable_symbol) {
+ show_error(this,_(L("The supplied name is not valid;")) + "\n" +
+ _(L("the following characters are not allowed:")) + " <>:/\\|?*\"");
+ }
+ else if (is_unusable_postfix){
+ show_error(this,_(L("The supplied name is not valid;")) + "\n" +
+ _(L("the following postfix are not allowed:")) + "\n\t" + //unusable_postfix);
+ wxString::FromUTF8(unusable_postfix.c_str()));
+ }
+ else if (m_chosen_name.compare("- default -") == 0) {
+ show_error(this, _(L("The supplied name is not available.")));
+ }
+ else {
+ EndModal(wxID_OK);
+ }
+ }
+}
+
+void TabSLAMaterial::build()
+{
+ m_presets = &m_preset_bundle->sla_materials;
+ load_initial_data();
+
+ auto page = add_options_page(_(L("Material")), "package_green.png");
+
+ auto optgroup = page->new_optgroup(_(L("Layers")));
+ optgroup->append_single_option_line("layer_height");
+ optgroup->append_single_option_line("initial_layer_height");
+
+ optgroup = page->new_optgroup(_(L("Exposure")));
+ optgroup->append_single_option_line("exposure_time");
+ optgroup->append_single_option_line("initial_exposure_time");
+
+ optgroup = page->new_optgroup(_(L("Corrections")));
+ optgroup->label_width = 190;
+ std::vector<std::string> corrections = { "material_correction_printing", "material_correction_curing" };
+ std::vector<std::string> axes{ "X", "Y", "Z" };
+ for (auto& opt_key : corrections){
+ auto line = Line{ m_config->def()->get(opt_key)->full_label, "" };
+ int id = 0;
+ for (auto& axis : axes) {
+ auto opt = optgroup->get_option(opt_key, id);
+ opt.opt.label = axis;
+ opt.opt.width = 60;
+ line.append_option(opt);
+ ++id;
+ }
+ optgroup->append_line(line);
+ }
+
+ page = add_options_page(_(L("Notes")), "note.png");
+ optgroup = page->new_optgroup(_(L("Notes")), 0);
+ optgroup->label_width = 0;
+ Option option = optgroup->get_option("material_notes");
+ option.opt.full_width = true;
+ option.opt.height = 250;
+ optgroup->append_single_option_line(option);
+
+ page = add_options_page(_(L("Dependencies")), "wrench.png");
+ optgroup = page->new_optgroup(_(L("Profile dependencies")));
+ auto line = Line { _(L("Compatible printers")), "" };
+ line.widget = [this](wxWindow* parent){
+ return compatible_printers_widget(parent, &m_compatible_printers_checkbox, &m_compatible_printers_btn);
+ };
+ optgroup->append_line(line, &m_colored_Label);
+
+ option = optgroup->get_option("compatible_printers_condition");
+ option.opt.full_width = true;
+ optgroup->append_single_option_line(option);
+
+ line = Line{ "", "" };
+ line.full_width = 1;
+ line.widget = [this](wxWindow* parent) {
+ return description_line_widget(parent, &m_parent_preset_description_line);
+ };
+ optgroup->append_line(line);
+}
+
+void TabSLAMaterial::update()
+{
+ if (get_preset_bundle()->printers.get_selected_preset().printer_technology() == ptFFF)
+ return; // ys_FIXME
+}
+
+} // GUI
+} // Slic3r
diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp
new file mode 100644
index 000000000..e4e37d4eb
--- /dev/null
+++ b/src/slic3r/GUI/Tab.hpp
@@ -0,0 +1,385 @@
+#ifndef slic3r_Tab_hpp_
+#define slic3r_Tab_hpp_
+
+// The "Expert" tab at the right of the main tabbed window.
+//
+// This file implements following packages:
+// Slic3r::GUI::Tab;
+// Slic3r::GUI::Tab::Print;
+// Slic3r::GUI::Tab::Filament;
+// Slic3r::GUI::Tab::Printer;
+// Slic3r::GUI::Tab::Page
+// - Option page: For example, the Slic3r::GUI::Tab::Print has option pages "Layers and perimeters", "Infill", "Skirt and brim" ...
+// Slic3r::GUI::SavePresetWindow
+// - Dialog to select a new preset name to store the configuration.
+// Slic3r::GUI::Tab::Preset;
+// - Single preset item: name, file is default or external.
+
+#include <wx/panel.h>
+#include <wx/notebook.h>
+#include <wx/scrolwin.h>
+#include <wx/sizer.h>
+#include <wx/bmpcbox.h>
+#include <wx/bmpbuttn.h>
+#include <wx/treectrl.h>
+#include <wx/imaglist.h>
+#include <wx/statbox.h>
+#include <wx/dataview.h>
+
+#include <map>
+#include <vector>
+#include <memory>
+
+#include "BedShapeDialog.hpp"
+
+//!enum { ID_TAB_TREE = wxID_HIGHEST + 1 };
+
+namespace Slic3r {
+namespace GUI {
+
+typedef std::pair<wxBitmap*, std::string> t_icon_description;
+typedef std::vector<std::pair<wxBitmap*, std::string>> t_icon_descriptions;
+
+// Single Tab page containing a{ vsizer } of{ optgroups }
+// package Slic3r::GUI::Tab::Page;
+using ConfigOptionsGroupShp = std::shared_ptr<ConfigOptionsGroup>;
+class Page : public wxScrolledWindow
+{
+ wxWindow* m_parent;
+ wxString m_title;
+ size_t m_iconID;
+ wxBoxSizer* m_vsizer;
+public:
+ Page(wxWindow* parent, const wxString title, const int iconID) :
+ m_parent(parent),
+ m_title(title),
+ m_iconID(iconID)
+ {
+ Create(m_parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
+ m_vsizer = new wxBoxSizer(wxVERTICAL);
+ m_item_color = &get_label_clr_default();
+ SetSizer(m_vsizer);
+ }
+ ~Page(){}
+
+ bool m_is_modified_values{ false };
+ bool m_is_nonsys_values{ true };
+
+public:
+ std::vector <ConfigOptionsGroupShp> m_optgroups;
+ DynamicPrintConfig* m_config;
+
+ wxBoxSizer* vsizer() const { return m_vsizer; }
+ wxWindow* parent() const { return m_parent; }
+ wxString title() const { return m_title; }
+ size_t iconID() const { return m_iconID; }
+ void set_config(DynamicPrintConfig* config_in) { m_config = config_in; }
+ void reload_config();
+ Field* get_field(const t_config_option_key& opt_key, int opt_index = -1) const;
+ bool set_value(const t_config_option_key& opt_key, const boost::any& value);
+ ConfigOptionsGroupShp new_optgroup(const wxString& title, int noncommon_label_width = -1);
+
+ bool set_item_colour(const wxColour *clr) {
+ if (m_item_color != clr) {
+ m_item_color = clr;
+ return true;
+ }
+ return false;
+ }
+
+ const wxColour get_item_colour() {
+ return *m_item_color;
+ }
+
+protected:
+ // Color of TreeCtrlItem. The wxColour will be updated only if the new wxColour pointer differs from the currently rendered one.
+ const wxColour* m_item_color;
+};
+
+// Slic3r::GUI::Tab;
+
+using PageShp = std::shared_ptr<Page>;
+class Tab: public wxPanel
+{
+ wxNotebook* m_parent;
+#ifdef __WXOSX__
+ wxPanel* m_tmp_panel;
+ int m_size_move = -1;
+#endif // __WXOSX__
+protected:
+ std::string m_name;
+ const wxString m_title;
+ wxBitmapComboBox* m_presets_choice;
+ wxBitmapButton* m_btn_save_preset;
+ wxBitmapButton* m_btn_delete_preset;
+ wxBitmapButton* m_btn_hide_incompatible_presets;
+ wxBoxSizer* m_hsizer;
+ wxBoxSizer* m_left_sizer;
+ wxTreeCtrl* m_treectrl;
+ wxImageList* m_icons;
+ wxCheckBox* m_compatible_printers_checkbox;
+ wxButton* m_compatible_printers_btn;
+ wxButton* m_undo_btn;
+ wxButton* m_undo_to_sys_btn;
+ wxButton* m_question_btn;
+ wxComboCtrl* m_cc_presets_choice;
+ wxDataViewTreeCtrl* m_presetctrl;
+ wxImageList* m_preset_icons;
+
+ // Cached bitmaps.
+ // A "flag" icon to be displayned next to the preset name in the Tab's combo box.
+ wxBitmap m_bmp_show_incompatible_presets;
+ wxBitmap m_bmp_hide_incompatible_presets;
+ // Bitmaps to be shown on the "Revert to system" aka "Lock to system" button next to each input field.
+ wxBitmap m_bmp_value_lock;
+ wxBitmap m_bmp_value_unlock;
+ wxBitmap m_bmp_white_bullet;
+ // The following bitmap points to either m_bmp_value_unlock or m_bmp_white_bullet, depending on whether the current preset has a parent preset.
+ wxBitmap *m_bmp_non_system;
+ // Bitmaps to be shown on the "Undo user changes" button next to each input field.
+ wxBitmap m_bmp_value_revert;
+// wxBitmap m_bmp_value_unmodified;
+ wxBitmap m_bmp_question;
+
+ // Colors for ui "decoration"
+ wxColour m_sys_label_clr;
+ wxColour m_modified_label_clr;
+ wxColour m_default_text_clr;
+
+ // Tooltip text for reset buttons (for whole options group)
+ wxString m_ttg_value_lock;
+ wxString m_ttg_value_unlock;
+ wxString m_ttg_white_bullet_ns;
+ // The following text points to either m_ttg_value_unlock or m_ttg_white_bullet_ns, depending on whether the current preset has a parent preset.
+ wxString *m_ttg_non_system;
+ // Tooltip text to be shown on the "Undo user changes" button next to each input field.
+ wxString m_ttg_white_bullet;
+ wxString m_ttg_value_revert;
+
+ // Tooltip text for reset buttons (for each option in group)
+ wxString m_tt_value_lock;
+ wxString m_tt_value_unlock;
+ // The following text points to either m_tt_value_unlock or m_ttg_white_bullet_ns, depending on whether the current preset has a parent preset.
+ wxString *m_tt_non_system;
+ // Tooltip text to be shown on the "Undo user changes" button next to each input field.
+ wxString m_tt_white_bullet;
+ wxString m_tt_value_revert;
+
+ int m_icon_count;
+ std::map<std::string, size_t> m_icon_index; // Map from an icon file name to its index
+ std::vector<PageShp> m_pages;
+ bool m_disable_tree_sel_changed_event;
+ bool m_show_incompatible_presets;
+
+ std::vector<std::string> m_reload_dependent_tabs = {};
+ enum OptStatus { osSystemValue = 1, osInitValue = 2 };
+ std::map<std::string, int> m_options_list;
+ int m_opt_status_value = 0;
+
+ t_icon_descriptions m_icon_descriptions = {};
+
+ // The two following two event IDs are generated at Plater.pm by calling Wx::NewEventType.
+ wxEventType m_event_value_change = 0;
+ wxEventType m_event_presets_changed = 0;
+
+ bool m_is_modified_values{ false };
+ bool m_is_nonsys_values{ true };
+ bool m_postpone_update_ui {false};
+
+ size_t m_selected_preset_item{ 0 };
+
+public:
+ PresetBundle* m_preset_bundle;
+ bool m_show_btn_incompatible_presets = false;
+ PresetCollection* m_presets;
+ DynamicPrintConfig* m_config;
+ ogStaticText* m_parent_preset_description_line;
+ wxStaticText* m_colored_Label = nullptr;
+
+public:
+ Tab() {}
+ Tab(wxNotebook* parent, const wxString& title, const char* name) :
+ m_parent(parent), m_title(title), m_name(name) {
+ Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL, name);
+ get_tabs_list().push_back(this);
+ }
+ ~Tab(){
+ delete_tab_from_list(this);
+ }
+
+ wxWindow* parent() const { return m_parent; }
+ wxString title() const { return m_title; }
+ std::string name() const { return m_name; }
+
+ // Set the events to the callbacks posted to the main frame window (currently implemented in Perl).
+ void set_event_value_change(wxEventType evt) { m_event_value_change = evt; }
+ void set_event_presets_changed(wxEventType evt) { m_event_presets_changed = evt; }
+
+ void create_preset_tab(PresetBundle *preset_bundle);
+ void load_current_preset();
+ void rebuild_page_tree(bool tree_sel_change_event = false);
+ void select_preset(std::string preset_name = "");
+ bool may_discard_current_dirty_preset(PresetCollection* presets = nullptr, const std::string& new_printer_name = "");
+ wxSizer* compatible_printers_widget(wxWindow* parent, wxCheckBox** checkbox, wxButton** btn);
+
+ void update_presetsctrl(wxDataViewTreeCtrl* ui, bool show_incompatible);
+ void load_key_value(const std::string& opt_key, const boost::any& value, bool saved_value = false);
+ void reload_compatible_printers_widget();
+
+ void OnTreeSelChange(wxTreeEvent& event);
+ void OnKeyDown(wxKeyEvent& event);
+
+ void save_preset(std::string name = "");
+ void delete_preset();
+ void toggle_show_hide_incompatible();
+ void update_show_hide_incompatible_button();
+ void update_ui_from_settings();
+ void update_labels_colour();
+ void update_changed_ui();
+ void get_sys_and_mod_flags(const std::string& opt_key, bool& sys_page, bool& modified_page);
+ void update_changed_tree_ui();
+ void update_undo_buttons();
+
+ void on_roll_back_value(const bool to_sys = false);
+
+ PageShp add_options_page(const wxString& title, const std::string& icon, bool is_extruder_pages = false);
+
+ virtual void OnActivate();
+ virtual void on_preset_loaded(){}
+ virtual void build() = 0;
+ virtual void update() = 0;
+ virtual void init_options_list();
+ void load_initial_data();
+ void update_dirty();
+ void update_tab_ui();
+ void load_config(const DynamicPrintConfig& config);
+ virtual void reload_config();
+ Field* get_field(const t_config_option_key& opt_key, int opt_index = -1) const;
+ bool set_value(const t_config_option_key& opt_key, const boost::any& value);
+ wxSizer* description_line_widget(wxWindow* parent, ogStaticText** StaticText);
+ bool current_preset_is_dirty();
+
+ DynamicPrintConfig* get_config() { return m_config; }
+ PresetCollection* get_presets() { return m_presets; }
+ std::vector<std::string> get_dependent_tabs() { return m_reload_dependent_tabs; }
+ size_t get_selected_preset_item() { return m_selected_preset_item; }
+
+ void on_value_change(const std::string& opt_key, const boost::any& value);
+
+ void update_wiping_button_visibility();
+protected:
+ void on_presets_changed();
+ void update_preset_description_line();
+ void update_frequently_changed_parameters();
+ void update_tab_presets(wxComboCtrl* ui, bool show_incompatible);
+ void fill_icon_descriptions();
+ void set_tooltips_text();
+};
+
+//Slic3r::GUI::Tab::Print;
+class TabPrint : public Tab
+{
+public:
+ TabPrint() {}
+ TabPrint(wxNotebook* parent) :
+ Tab(parent, _(L("Print Settings")), "print") {}
+ ~TabPrint(){}
+
+ ogStaticText* m_recommended_thin_wall_thickness_description_line;
+ bool m_support_material_overhangs_queried = false;
+
+ void build() override;
+ void reload_config() override;
+ void update() override;
+ void OnActivate() override;
+};
+
+//Slic3r::GUI::Tab::Filament;
+class TabFilament : public Tab
+{
+ ogStaticText* m_volumetric_speed_description_line;
+ ogStaticText* m_cooling_description_line;
+public:
+ TabFilament() {}
+ TabFilament(wxNotebook* parent) :
+ Tab(parent, _(L("Filament Settings")), "filament") {}
+ ~TabFilament(){}
+
+ void build() override;
+ void reload_config() override;
+ void update() override;
+ void OnActivate() override;
+};
+
+//Slic3r::GUI::Tab::Printer;
+class TabPrinter : public Tab
+{
+ bool m_has_single_extruder_MM_page = false;
+ bool m_use_silent_mode = false;
+ void append_option_line(ConfigOptionsGroupShp optgroup, const std::string opt_key);
+ bool m_rebuild_kinematics_page = false;
+
+ std::vector<PageShp> m_pages_fff;
+ std::vector<PageShp> m_pages_sla;
+public:
+ wxButton* m_serial_test_btn;
+ wxButton* m_print_host_test_btn;
+ wxButton* m_printhost_browse_btn;
+
+ size_t m_extruders_count;
+ size_t m_extruders_count_old = 0;
+ size_t m_initial_extruders_count;
+ size_t m_sys_extruders_count;
+
+ PrinterTechnology m_printer_technology = ptFFF;
+
+ TabPrinter() {}
+ TabPrinter(wxNotebook* parent) : Tab(parent, _(L("Printer Settings")), "printer") {}
+ ~TabPrinter(){}
+
+ void build() override;
+ void build_fff();
+ void build_sla();
+ void update() override;
+ void update_fff();
+ void update_sla();
+ void update_pages(); // update m_pages according to printer technology
+ void update_serial_ports();
+ void extruders_count_changed(size_t extruders_count);
+ PageShp build_kinematics_page();
+ void build_extruder_pages();
+ void on_preset_loaded() override;
+ void init_options_list() override;
+};
+
+class TabSLAMaterial : public Tab
+{
+public:
+ TabSLAMaterial() {}
+ TabSLAMaterial(wxNotebook* parent) :
+ Tab(parent, _(L("SLA Material Settings")), "sla_material") {}
+ ~TabSLAMaterial(){}
+
+ void build() override;
+ void update() override;
+ void init_options_list() override;
+};
+
+class SavePresetWindow :public wxDialog
+{
+public:
+ SavePresetWindow(wxWindow* parent) :wxDialog(parent, wxID_ANY, _(L("Save preset"))){}
+ ~SavePresetWindow(){}
+
+ std::string m_chosen_name;
+ wxComboBox* m_combo;
+
+ void build(const wxString& title, const std::string& default_name, std::vector<std::string> &values);
+ void accept();
+ std::string get_name() { return m_chosen_name; }
+};
+
+} // GUI
+} // Slic3r
+
+#endif /* slic3r_Tab_hpp_ */
diff --git a/src/slic3r/GUI/TabIface.cpp b/src/slic3r/GUI/TabIface.cpp
new file mode 100644
index 000000000..29833322b
--- /dev/null
+++ b/src/slic3r/GUI/TabIface.cpp
@@ -0,0 +1,20 @@
+#include "TabIface.hpp"
+#include "Tab.hpp"
+
+namespace Slic3r {
+
+void TabIface::load_current_preset() { m_tab->load_current_preset(); }
+void TabIface::update_tab_ui() { m_tab->update_tab_ui(); }
+void TabIface::update_ui_from_settings() { m_tab->update_ui_from_settings();}
+void TabIface::select_preset(char* name) { m_tab->select_preset(name);}
+void TabIface::load_config(DynamicPrintConfig* config) { m_tab->load_config(*config);}
+void TabIface::load_key_value(char* opt_key, char* value){ m_tab->load_key_value(opt_key, static_cast<std::string>(value)); }
+bool TabIface::current_preset_is_dirty() { return m_tab->current_preset_is_dirty();}
+void TabIface::OnActivate() { return m_tab->OnActivate();}
+size_t TabIface::get_selected_preset_item() { return m_tab->get_selected_preset_item(); }
+std::string TabIface::title() { return m_tab->title().ToUTF8().data(); }
+DynamicPrintConfig* TabIface::get_config() { return m_tab->get_config(); }
+PresetCollection* TabIface::get_presets() { return m_tab!=nullptr ? m_tab->get_presets() : nullptr; }
+std::vector<std::string> TabIface::get_dependent_tabs() { return m_tab->get_dependent_tabs(); }
+
+}; // namespace Slic3r
diff --git a/src/slic3r/GUI/TabIface.hpp b/src/slic3r/GUI/TabIface.hpp
new file mode 100644
index 000000000..2f7f4e8e7
--- /dev/null
+++ b/src/slic3r/GUI/TabIface.hpp
@@ -0,0 +1,41 @@
+#ifndef slic3r_TabIface_hpp_
+#define slic3r_TabIface_hpp_
+
+#include <vector>
+#include <string>
+
+namespace Slic3r {
+ class DynamicPrintConfig;
+ class PresetCollection;
+
+namespace GUI {
+ class Tab;
+}
+
+class TabIface {
+public:
+ TabIface() : m_tab(nullptr) {}
+ TabIface(GUI::Tab *tab) : m_tab(tab) {}
+// TabIface(const TabIface &rhs) : m_tab(rhs.m_tab) {}
+
+ void load_current_preset();
+ void update_tab_ui();
+ void update_ui_from_settings();
+ void select_preset(char* name);
+ std::string title();
+ void load_config(DynamicPrintConfig* config);
+ void load_key_value(char* opt_key, char* value);
+ bool current_preset_is_dirty();
+ void OnActivate();
+ DynamicPrintConfig* get_config();
+ PresetCollection* get_presets();
+ std::vector<std::string> get_dependent_tabs();
+ size_t get_selected_preset_item();
+
+protected:
+ GUI::Tab *m_tab;
+}; // namespace GUI
+
+}; // namespace Slic3r
+
+#endif /* slic3r_TabIface_hpp_ */
diff --git a/src/slic3r/GUI/UpdateDialogs.cpp b/src/slic3r/GUI/UpdateDialogs.cpp
new file mode 100644
index 000000000..70d9c851c
--- /dev/null
+++ b/src/slic3r/GUI/UpdateDialogs.cpp
@@ -0,0 +1,196 @@
+#include "UpdateDialogs.hpp"
+
+#include <wx/settings.h>
+#include <wx/sizer.h>
+#include <wx/event.h>
+#include <wx/stattext.h>
+#include <wx/button.h>
+#include <wx/hyperlink.h>
+#include <wx/statbmp.h>
+#include <wx/checkbox.h>
+
+#include "libslic3r/libslic3r.h"
+#include "libslic3r/Utils.hpp"
+#include "GUI.hpp"
+#include "ConfigWizard.hpp"
+
+namespace Slic3r {
+namespace GUI {
+
+
+static const std::string CONFIG_UPDATE_WIKI_URL("https://github.com/prusa3d/Slic3r/wiki/Slic3r-PE-1.40-configuration-update");
+
+
+// MsgUpdateSlic3r
+
+MsgUpdateSlic3r::MsgUpdateSlic3r(const Semver &ver_current, const Semver &ver_online) :
+ MsgDialog(nullptr, _(L("Update available")), _(L("New version of Slic3r PE is available"))),
+ ver_current(ver_current),
+ ver_online(ver_online)
+{
+ 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("To download, follow the link below.")));
+ const auto link_width = link->GetSize().GetWidth();
+ text->Wrap(CONTENT_WIDTH > link_width ? CONTENT_WIDTH : link_width);
+ content_sizer->Add(text);
+ content_sizer->AddSpacer(VERT_SPACING);
+
+ auto *versions = new wxFlexGridSizer(2, 0, VERT_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()));
+ content_sizer->Add(versions);
+ content_sizer->AddSpacer(VERT_SPACING);
+
+ content_sizer->Add(link);
+ content_sizer->AddSpacer(2*VERT_SPACING);
+
+ cbox = new wxCheckBox(this, wxID_ANY, _(L("Don't notify about new releases any more")));
+ content_sizer->Add(cbox);
+ content_sizer->AddSpacer(VERT_SPACING);
+
+ Fit();
+}
+
+MsgUpdateSlic3r::~MsgUpdateSlic3r() {}
+
+bool MsgUpdateSlic3r::disable_version_check() const
+{
+ return cbox->GetValue();
+}
+
+
+// MsgUpdateConfig
+
+MsgUpdateConfig::MsgUpdateConfig(const std::unordered_map<std::string, std::string> &updates) :
+ MsgDialog(nullptr, _(L("Configuration update")), _(L("Configuration update is available")), wxID_NONE)
+{
+ auto *text = new wxStaticText(this, wxID_ANY, _(L(
+ "Would you like to install it?\n\n"
+ "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:"
+ )));
+ text->Wrap(CONTENT_WIDTH);
+ content_sizer->Add(text);
+ content_sizer->AddSpacer(VERT_SPACING);
+
+ auto *versions = new wxFlexGridSizer(2, 0, VERT_SPACING);
+ for (const auto &update : updates) {
+ auto *text_vendor = new wxStaticText(this, wxID_ANY, update.first);
+ text_vendor->SetFont(boldfont);
+ versions->Add(text_vendor);
+ versions->Add(new wxStaticText(this, wxID_ANY, update.second));
+ }
+
+ content_sizer->Add(versions);
+ content_sizer->AddSpacer(2*VERT_SPACING);
+
+ auto *btn_cancel = new wxButton(this, wxID_CANCEL);
+ btn_sizer->Add(btn_cancel);
+ btn_sizer->AddSpacer(HORIZ_SPACING);
+ auto *btn_ok = new wxButton(this, wxID_OK);
+ btn_sizer->Add(btn_ok);
+ btn_ok->SetFocus();
+
+ Fit();
+}
+
+MsgUpdateConfig::~MsgUpdateConfig() {}
+
+
+// MsgDataIncompatible
+
+MsgDataIncompatible::MsgDataIncompatible(const std::unordered_map<std::string, wxString> &incompats) :
+ MsgDialog(nullptr, _(L("Slic3r incompatibility")), _(L("Slic3r configuration is incompatible")), wxBitmap(from_u8(Slic3r::var("Slic3r_192px_grayscale.png")), wxBITMAP_TYPE_PNG), wxID_NONE)
+{
+ auto *text = new wxStaticText(this, wxID_ANY, _(L(
+ "This version of Slic3r PE is not compatible with currently installed configuration bundles.\n"
+ "This probably happened as a result of running an older Slic3r PE after using a newer one.\n\n"
+
+ "You may either exit Slic3r and try again with a newer version, or you may re-run the initial configuration. "
+ "Doing so will create a backup snapshot of the existing configuration before installing files compatible with this Slic3r.\n"
+ )));
+ text->Wrap(CONTENT_WIDTH);
+ content_sizer->Add(text);
+
+ auto *text2 = new wxStaticText(this, wxID_ANY, wxString::Format(_(L("This Slic3r PE version: %s")), SLIC3R_VERSION));
+ text2->Wrap(CONTENT_WIDTH);
+ content_sizer->Add(text2);
+ content_sizer->AddSpacer(VERT_SPACING);
+
+ auto *text3 = new wxStaticText(this, wxID_ANY, _(L("Incompatible bundles:")));
+ text3->Wrap(CONTENT_WIDTH);
+ content_sizer->Add(text3);
+ content_sizer->AddSpacer(VERT_SPACING);
+
+ auto *versions = new wxFlexGridSizer(2, 0, VERT_SPACING);
+ for (const auto &incompat : incompats) {
+ auto *text_vendor = new wxStaticText(this, wxID_ANY, incompat.first);
+ text_vendor->SetFont(boldfont);
+ versions->Add(text_vendor);
+ versions->Add(new wxStaticText(this, wxID_ANY, incompat.second));
+ }
+
+ content_sizer->Add(versions);
+ content_sizer->AddSpacer(2*VERT_SPACING);
+
+ auto *btn_exit = new wxButton(this, wxID_EXIT, _(L("Exit Slic3r")));
+ btn_sizer->Add(btn_exit);
+ btn_sizer->AddSpacer(HORIZ_SPACING);
+ auto *btn_reconf = new wxButton(this, wxID_REPLACE, _(L("Re-configure")));
+ btn_sizer->Add(btn_reconf);
+ btn_exit->SetFocus();
+
+ auto exiter = [this](const wxCommandEvent& evt) { this->EndModal(evt.GetId()); };
+ btn_exit->Bind(wxEVT_BUTTON, exiter);
+ btn_reconf->Bind(wxEVT_BUTTON, exiter);
+
+ Fit();
+}
+
+MsgDataIncompatible::~MsgDataIncompatible() {}
+
+
+// MsgDataLegacy
+
+MsgDataLegacy::MsgDataLegacy() :
+ MsgDialog(nullptr, _(L("Configuration update")), _(L("Configuration update")))
+{
+ auto *text = new wxStaticText(this, wxID_ANY, 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"
+
+ "Please proceed with the %s that follows to set up the new presets "
+ "and to choose whether to enable automatic preset updates."
+ )),
+ ConfigWizard::name()
+ ));
+ text->Wrap(CONTENT_WIDTH);
+ content_sizer->Add(text);
+ content_sizer->AddSpacer(VERT_SPACING);
+
+ auto *text2 = new wxStaticText(this, wxID_ANY, _(L("For more information please visit our wiki page:")));
+ static const wxString url("https://github.com/prusa3d/Slic3r/wiki/Slic3r-PE-1.40-configuration-update");
+ // The wiki page name is intentionally not localized:
+ auto *link = new wxHyperlinkCtrl(this, wxID_ANY, "Slic3r PE 1.40 configuration update", CONFIG_UPDATE_WIKI_URL);
+ content_sizer->Add(text2);
+ content_sizer->Add(link);
+ content_sizer->AddSpacer(VERT_SPACING);
+
+ Fit();
+}
+
+MsgDataLegacy::~MsgDataLegacy() {}
+
+
+}
+}
diff --git a/src/slic3r/GUI/UpdateDialogs.hpp b/src/slic3r/GUI/UpdateDialogs.hpp
new file mode 100644
index 000000000..62548b98b
--- /dev/null
+++ b/src/slic3r/GUI/UpdateDialogs.hpp
@@ -0,0 +1,81 @@
+#ifndef slic3r_UpdateDialogs_hpp_
+#define slic3r_UpdateDialogs_hpp_
+
+#include <string>
+#include <unordered_map>
+
+#include "slic3r/Utils/Semver.hpp"
+#include "MsgDialog.hpp"
+
+class wxBoxSizer;
+class wxCheckBox;
+
+namespace Slic3r {
+
+namespace GUI {
+
+
+// A confirmation dialog listing configuration updates
+class MsgUpdateSlic3r : public MsgDialog
+{
+public:
+ MsgUpdateSlic3r(const Semver &ver_current, const Semver &ver_online);
+ MsgUpdateSlic3r(MsgUpdateSlic3r &&) = delete;
+ MsgUpdateSlic3r(const MsgUpdateSlic3r &) = delete;
+ MsgUpdateSlic3r &operator=(MsgUpdateSlic3r &&) = delete;
+ MsgUpdateSlic3r &operator=(const MsgUpdateSlic3r &) = delete;
+ virtual ~MsgUpdateSlic3r();
+
+ // Tells whether the user checked the "don't bother me again" checkbox
+ bool disable_version_check() const;
+
+private:
+ const Semver &ver_current;
+ const Semver &ver_online;
+ wxCheckBox *cbox;
+};
+
+
+// Confirmation dialog informing about configuration update. Lists updated bundles & their versions.
+class MsgUpdateConfig : public MsgDialog
+{
+public:
+ // updates is a map of "vendor name" -> "version (comment)"
+ MsgUpdateConfig(const std::unordered_map<std::string, std::string> &updates);
+ MsgUpdateConfig(MsgUpdateConfig &&) = delete;
+ MsgUpdateConfig(const MsgUpdateConfig &) = delete;
+ MsgUpdateConfig &operator=(MsgUpdateConfig &&) = delete;
+ MsgUpdateConfig &operator=(const MsgUpdateConfig &) = delete;
+ ~MsgUpdateConfig();
+};
+
+// Informs about currently installed bundles not being compatible with the running Slic3r. Asks about action.
+class MsgDataIncompatible : public MsgDialog
+{
+public:
+ // incompats is a map of "vendor name" -> "version restrictions"
+ MsgDataIncompatible(const std::unordered_map<std::string, wxString> &incompats);
+ MsgDataIncompatible(MsgDataIncompatible &&) = delete;
+ MsgDataIncompatible(const MsgDataIncompatible &) = delete;
+ MsgDataIncompatible &operator=(MsgDataIncompatible &&) = delete;
+ MsgDataIncompatible &operator=(const MsgDataIncompatible &) = delete;
+ ~MsgDataIncompatible();
+};
+
+// Informs about a legacy data directory - an update from Slic3r PE < 1.40
+class MsgDataLegacy : public MsgDialog
+{
+public:
+ MsgDataLegacy();
+ MsgDataLegacy(MsgDataLegacy &&) = delete;
+ MsgDataLegacy(const MsgDataLegacy &) = delete;
+ MsgDataLegacy &operator=(MsgDataLegacy &&) = delete;
+ MsgDataLegacy &operator=(const MsgDataLegacy &) = delete;
+ ~MsgDataLegacy();
+};
+
+
+}
+}
+
+#endif
diff --git a/src/slic3r/GUI/Widget.hpp b/src/slic3r/GUI/Widget.hpp
new file mode 100644
index 000000000..bcf772469
--- /dev/null
+++ b/src/slic3r/GUI/Widget.hpp
@@ -0,0 +1,16 @@
+#ifndef WIDGET_HPP
+#define WIDGET_HPP
+#include <wx/wxprec.h>
+#ifndef WX_PRECOM
+#include <wx/wx.h>
+#endif
+
+class Widget {
+protected:
+ wxSizer* _sizer;
+public:
+ Widget(): _sizer(nullptr) { }
+ bool valid() const { return _sizer != nullptr; }
+ wxSizer* sizer() const { return _sizer; }
+};
+#endif
diff --git a/src/slic3r/GUI/WipeTowerDialog.cpp b/src/slic3r/GUI/WipeTowerDialog.cpp
new file mode 100644
index 000000000..eef4017c1
--- /dev/null
+++ b/src/slic3r/GUI/WipeTowerDialog.cpp
@@ -0,0 +1,338 @@
+#include <algorithm>
+#include <sstream>
+#include "WipeTowerDialog.hpp"
+#include "GUI.hpp"
+
+#include <wx/sizer.h>
+
+RammingDialog::RammingDialog(wxWindow* parent,const std::string& parameters)
+: wxDialog(parent, wxID_ANY, _(L("Ramming customization")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE/* | wxRESIZE_BORDER*/)
+{
+ m_panel_ramming = new RammingPanel(this,parameters);
+
+ // Not found another way of getting the background colours of RammingDialog, RammingPanel and Chart correct than setting
+ // them all explicitely. Reading the parent colour yielded colour that didn't really match it, no wxSYS_COLOUR_... matched
+ // colour used for the dialog. Same issue (and "solution") here : https://forums.wxwidgets.org/viewtopic.php?f=1&t=39608
+ // Whoever can fix this, feel free to do so.
+ this-> SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_FRAMEBK));
+ m_panel_ramming->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_FRAMEBK));
+ m_panel_ramming->Show(true);
+ this->Show();
+
+ auto main_sizer = new wxBoxSizer(wxVERTICAL);
+ main_sizer->Add(m_panel_ramming, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5);
+ main_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxBOTTOM, 10);
+ SetSizer(main_sizer);
+ main_sizer->SetSizeHints(this);
+
+ this->Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) { EndModal(wxCANCEL); });
+
+ this->Bind(wxEVT_BUTTON,[this](wxCommandEvent&) {
+ m_output_data = m_panel_ramming->get_parameters();
+ EndModal(wxID_OK);
+ },wxID_OK);
+ this->Show();
+ wxMessageDialog(this,_(L("Ramming denotes the rapid extrusion just before a tool change in a single-extruder MM printer. Its purpose is to "
+ "properly shape the end of the unloaded filament so it does not prevent insertion of the new filament and can itself "
+ "be reinserted later. This phase is important and different materials can require different extrusion speeds to get "
+ "the good shape. For this reason, the extrusion rates during ramming are adjustable.\n\nThis is an expert-level "
+ "setting, incorrect adjustment will likely lead to jams, extruder wheel grinding into filament etc.")),_(L("Warning")),wxOK|wxICON_EXCLAMATION).ShowModal();
+}
+
+
+
+
+
+RammingPanel::RammingPanel(wxWindow* parent, const std::string& parameters)
+: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize/*,wxPoint(50,50), wxSize(800,350),wxBORDER_RAISED*/)
+{
+ auto sizer_chart = new wxBoxSizer(wxVERTICAL);
+ auto sizer_param = new wxBoxSizer(wxVERTICAL);
+
+ std::stringstream stream{ parameters };
+ stream >> m_ramming_line_width_multiplicator >> m_ramming_step_multiplicator;
+ int ramming_speed_size = 0;
+ float dummy = 0.f;
+ while (stream >> dummy)
+ ++ramming_speed_size;
+ stream.clear();
+ stream.get();
+
+ std::vector<std::pair<float, float>> buttons;
+ float x = 0.f;
+ float y = 0.f;
+ while (stream >> x >> y)
+ buttons.push_back(std::make_pair(x, y));
+
+ m_chart = new Chart(this, wxRect(10, 10, 480, 360), buttons, ramming_speed_size, 0.25f);
+ m_chart->SetBackgroundColour(parent->GetBackgroundColour()); // see comment in RammingDialog constructor
+ sizer_chart->Add(m_chart, 0, wxALL, 5);
+
+ m_widget_time = new wxSpinCtrlDouble(this,wxID_ANY,wxEmptyString,wxDefaultPosition,wxSize(75, -1),wxSP_ARROW_KEYS,0.,5.0,3.,0.5);
+ m_widget_volume = new wxSpinCtrl(this,wxID_ANY,wxEmptyString,wxDefaultPosition,wxSize(75, -1),wxSP_ARROW_KEYS,0,10000,0);
+ m_widget_ramming_line_width_multiplicator = new wxSpinCtrl(this,wxID_ANY,wxEmptyString,wxDefaultPosition,wxSize(75, -1),wxSP_ARROW_KEYS,10,200,100);
+ m_widget_ramming_step_multiplicator = new wxSpinCtrl(this,wxID_ANY,wxEmptyString,wxDefaultPosition,wxSize(75, -1),wxSP_ARROW_KEYS,10,200,100);
+
+ auto gsizer_param = new wxFlexGridSizer(2, 5, 15);
+ gsizer_param->Add(new wxStaticText(this, wxID_ANY, wxString(_(L("Total ramming time")) + " (" + _(L("s")) + "):")), 0, wxALIGN_CENTER_VERTICAL);
+ gsizer_param->Add(m_widget_time);
+ gsizer_param->Add(new wxStaticText(this, wxID_ANY, wxString(_(L("Total rammed volume")) + " (" + _(L("mm")) + wxString("³):", wxConvUTF8))), 0, wxALIGN_CENTER_VERTICAL);
+ gsizer_param->Add(m_widget_volume);
+ gsizer_param->AddSpacer(20);
+ gsizer_param->AddSpacer(20);
+ gsizer_param->Add(new wxStaticText(this, wxID_ANY, wxString(_(L("Ramming line width")) + " (%):")), 0, wxALIGN_CENTER_VERTICAL);
+ gsizer_param->Add(m_widget_ramming_line_width_multiplicator);
+ gsizer_param->Add(new wxStaticText(this, wxID_ANY, wxString(_(L("Ramming line spacing")) + " (%):")), 0, wxALIGN_CENTER_VERTICAL);
+ gsizer_param->Add(m_widget_ramming_step_multiplicator);
+
+ sizer_param->Add(gsizer_param, 0, wxTOP, 100);
+
+ m_widget_time->SetValue(m_chart->get_time());
+ m_widget_time->SetDigits(2);
+ m_widget_volume->SetValue(m_chart->get_volume());
+ m_widget_volume->Disable();
+ m_widget_ramming_line_width_multiplicator->SetValue(m_ramming_line_width_multiplicator);
+ m_widget_ramming_step_multiplicator->SetValue(m_ramming_step_multiplicator);
+
+ m_widget_ramming_step_multiplicator->Bind(wxEVT_TEXT,[this](wxCommandEvent&) { line_parameters_changed(); });
+ m_widget_ramming_line_width_multiplicator->Bind(wxEVT_TEXT,[this](wxCommandEvent&) { line_parameters_changed(); });
+
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(sizer_chart, 0, wxALL, 5);
+ sizer->Add(sizer_param, 0, wxALL, 10);
+
+ sizer->SetSizeHints(this);
+ SetSizer(sizer);
+
+ m_widget_time->Bind(wxEVT_TEXT,[this](wxCommandEvent&) {m_chart->set_xy_range(m_widget_time->GetValue(),-1);});
+ m_widget_time->Bind(wxEVT_CHAR,[](wxKeyEvent&){}); // do nothing - prevents the user to change the value
+ m_widget_volume->Bind(wxEVT_CHAR,[](wxKeyEvent&){}); // do nothing - prevents the user to change the value
+ Bind(EVT_WIPE_TOWER_CHART_CHANGED,[this](wxCommandEvent&) {m_widget_volume->SetValue(m_chart->get_volume()); m_widget_time->SetValue(m_chart->get_time());} );
+ Refresh(this);
+}
+
+void RammingPanel::line_parameters_changed() {
+ m_ramming_line_width_multiplicator = m_widget_ramming_line_width_multiplicator->GetValue();
+ m_ramming_step_multiplicator = m_widget_ramming_step_multiplicator->GetValue();
+}
+
+std::string RammingPanel::get_parameters()
+{
+ std::vector<float> speeds = m_chart->get_ramming_speed(0.25f);
+ std::vector<std::pair<float,float>> buttons = m_chart->get_buttons();
+ std::stringstream stream;
+ stream << m_ramming_line_width_multiplicator << " " << m_ramming_step_multiplicator;
+ for (const float& speed_value : speeds)
+ stream << " " << speed_value;
+ stream << "|";
+ for (const auto& button : buttons)
+ stream << " " << button.first << " " << button.second;
+ return stream.str();
+}
+
+
+#define ITEM_WIDTH 60
+// Parent dialog for purging volume adjustments - it fathers WipingPanel widget (that contains all controls) and a button to toggle simple/advanced mode:
+WipingDialog::WipingDialog(wxWindow* parent,const std::vector<float>& matrix, const std::vector<float>& extruders)
+: wxDialog(parent, wxID_ANY, _(L("Wipe tower - Purging volume adjustment")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE/* | wxRESIZE_BORDER*/)
+{
+ auto widget_button = new wxButton(this,wxID_ANY,"-",wxPoint(0,0),wxDefaultSize);
+ m_panel_wiping = new WipingPanel(this,matrix,extruders, widget_button);
+
+ auto main_sizer = new wxBoxSizer(wxVERTICAL);
+
+ // set min sizer width according to extruders count
+ const auto sizer_width = (int)((sqrt(matrix.size()) + 2.8)*ITEM_WIDTH);
+ main_sizer->SetMinSize(wxSize(sizer_width, -1));
+
+ main_sizer->Add(m_panel_wiping, 0, wxEXPAND | wxALL, 5);
+ main_sizer->Add(widget_button, 0, wxALIGN_CENTER_HORIZONTAL | wxCENTER | wxBOTTOM, 5);
+ main_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10);
+ SetSizer(main_sizer);
+ main_sizer->SetSizeHints(this);
+
+ this->Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) { EndModal(wxCANCEL); });
+
+ this->Bind(wxEVT_BUTTON,[this](wxCommandEvent&) { // if OK button is clicked..
+ m_output_matrix = m_panel_wiping->read_matrix_values(); // ..query wiping panel and save returned values
+ m_output_extruders = m_panel_wiping->read_extruders_values(); // so they can be recovered later by calling get_...()
+ EndModal(wxID_OK);
+ },wxID_OK);
+
+ this->Show();
+}
+
+// This function allows to "play" with sizers parameters (like align or border)
+void WipingPanel::format_sizer(wxSizer* sizer, wxPanel* page, wxGridSizer* grid_sizer, const wxString& info, const wxString& table_title, int table_lshift/*=0*/)
+{
+ sizer->Add(new wxStaticText(page, wxID_ANY, info,wxDefaultPosition,wxSize(0,50)), 0, wxEXPAND | wxLEFT, 15);
+ auto table_sizer = new wxBoxSizer(wxVERTICAL);
+ sizer->Add(table_sizer, 0, wxALIGN_CENTER | wxCENTER, table_lshift);
+ table_sizer->Add(new wxStaticText(page, wxID_ANY, table_title), 0, wxALIGN_CENTER | wxTOP, 50);
+ table_sizer->Add(grid_sizer, 0, wxALIGN_CENTER | wxTOP, 10);
+}
+
+// This panel contains all control widgets for both simple and advanced mode (these reside in separate sizers)
+WipingPanel::WipingPanel(wxWindow* parent, const std::vector<float>& matrix, const std::vector<float>& extruders, wxButton* widget_button)
+: wxPanel(parent,wxID_ANY, wxDefaultPosition, wxDefaultSize/*,wxBORDER_RAISED*/)
+{
+ m_widget_button = widget_button; // pointer to the button in parent dialog
+ m_widget_button->Bind(wxEVT_BUTTON,[this](wxCommandEvent&){ toggle_advanced(true); });
+
+ m_number_of_extruders = (int)(sqrt(matrix.size())+0.001);
+
+ // Create two switched panels with their own sizers
+ m_sizer_simple = new wxBoxSizer(wxVERTICAL);
+ m_sizer_advanced = new wxBoxSizer(wxVERTICAL);
+ m_page_simple = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
+ m_page_advanced = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
+ m_page_simple->SetSizer(m_sizer_simple);
+ m_page_advanced->SetSizer(m_sizer_advanced);
+
+ auto gridsizer_simple = new wxGridSizer(3, 5, 10);
+ m_gridsizer_advanced = new wxGridSizer(m_number_of_extruders+1, 5, 1);
+
+ // First create controls for advanced mode and assign them to m_page_advanced:
+ for (unsigned int i = 0; i < m_number_of_extruders; ++i) {
+ edit_boxes.push_back(std::vector<wxTextCtrl*>(0));
+
+ for (unsigned int j = 0; j < m_number_of_extruders; ++j) {
+ edit_boxes.back().push_back(new wxTextCtrl(m_page_advanced, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(ITEM_WIDTH, -1)));
+ if (i == j)
+ edit_boxes[i][j]->Disable();
+ else
+ edit_boxes[i][j]->SetValue(wxString("") << int(matrix[m_number_of_extruders*j + i]));
+ }
+ }
+ m_gridsizer_advanced->Add(new wxStaticText(m_page_advanced, wxID_ANY, wxString("")));
+ for (unsigned int i = 0; i < m_number_of_extruders; ++i)
+ m_gridsizer_advanced->Add(new wxStaticText(m_page_advanced, wxID_ANY, wxString("") << i + 1), 0, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
+ for (unsigned int i = 0; i < m_number_of_extruders; ++i) {
+ m_gridsizer_advanced->Add(new wxStaticText(m_page_advanced, wxID_ANY, wxString("") << i + 1), 0, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
+ for (unsigned int j = 0; j < m_number_of_extruders; ++j)
+ m_gridsizer_advanced->Add(edit_boxes[j][i], 0);
+ }
+
+ // collect and format sizer
+ format_sizer(m_sizer_advanced, m_page_advanced, m_gridsizer_advanced,
+ _(L("Here you can adjust required purging volume (mm³) for any given pair of tools.")),
+ _(L("Extruder changed to")));
+
+ // Hide preview page before new page creating
+ // It allows to do that from a beginning of the main panel
+ m_page_advanced->Hide();
+
+ // Now the same for simple mode:
+ gridsizer_simple->Add(new wxStaticText(m_page_simple, wxID_ANY, wxString("")), 0, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
+ gridsizer_simple->Add(new wxStaticText(m_page_simple, wxID_ANY, wxString(_(L("unloaded")))), 0, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
+ gridsizer_simple->Add(new wxStaticText(m_page_simple,wxID_ANY,wxString(_(L("loaded")))), 0, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
+
+ for (unsigned int i=0;i<m_number_of_extruders;++i) {
+ m_old.push_back(new wxSpinCtrl(m_page_simple,wxID_ANY,wxEmptyString,wxDefaultPosition, wxSize(80, -1),wxSP_ARROW_KEYS|wxALIGN_RIGHT,0,300,extruders[2*i]));
+ m_new.push_back(new wxSpinCtrl(m_page_simple,wxID_ANY,wxEmptyString,wxDefaultPosition, wxSize(80, -1),wxSP_ARROW_KEYS|wxALIGN_RIGHT,0,300,extruders[2*i+1]));
+ gridsizer_simple->Add(new wxStaticText(m_page_simple, wxID_ANY, wxString(_(L("Tool #"))) << i + 1 << ": "), 0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+ gridsizer_simple->Add(m_old.back(),0);
+ gridsizer_simple->Add(m_new.back(),0);
+ }
+
+ // collect and format sizer
+ format_sizer(m_sizer_simple, m_page_simple, gridsizer_simple,
+ _(L("Total purging volume is calculated by summing two values below, depending on which tools are loaded/unloaded.")),
+ _(L("Volume to purge (mm³) when the filament is being")), 50);
+
+ m_sizer = new wxBoxSizer(wxVERTICAL);
+ m_sizer->Add(m_page_simple, 0, wxEXPAND | wxALL, 25);
+ m_sizer->Add(m_page_advanced, 0, wxEXPAND | wxALL, 25);
+
+ m_sizer->SetSizeHints(this);
+ SetSizer(m_sizer);
+
+ toggle_advanced(); // to show/hide what is appropriate
+
+ m_page_advanced->Bind(wxEVT_PAINT,[this](wxPaintEvent&) {
+ wxPaintDC dc(m_page_advanced);
+ int y_pos = 0.5 * (edit_boxes[0][0]->GetPosition().y + edit_boxes[0][edit_boxes.size()-1]->GetPosition().y + edit_boxes[0][edit_boxes.size()-1]->GetSize().y);
+ wxString label = _(L("From"));
+ int text_width = 0;
+ int text_height = 0;
+ dc.GetTextExtent(label,&text_width,&text_height);
+ int xpos = m_gridsizer_advanced->GetPosition().x;
+ dc.DrawRotatedText(label,xpos-text_height,y_pos + text_width/2.f,90);
+ });
+}
+
+
+
+
+// Reads values from the (advanced) wiping matrix:
+std::vector<float> WipingPanel::read_matrix_values() {
+ if (!m_advanced)
+ fill_in_matrix();
+ std::vector<float> output;
+ for (unsigned int i=0;i<m_number_of_extruders;++i) {
+ for (unsigned int j=0;j<m_number_of_extruders;++j) {
+ double val = 0.;
+ edit_boxes[j][i]->GetValue().ToDouble(&val);
+ output.push_back((float)val);
+ }
+ }
+ return output;
+}
+
+// Reads values from simple mode to save them for next time:
+std::vector<float> WipingPanel::read_extruders_values() {
+ std::vector<float> output;
+ for (unsigned int i=0;i<m_number_of_extruders;++i) {
+ output.push_back(m_old[i]->GetValue());
+ output.push_back(m_new[i]->GetValue());
+ }
+ return output;
+}
+
+// This updates the "advanced" matrix based on values from "simple" mode
+void WipingPanel::fill_in_matrix() {
+ for (unsigned i=0;i<m_number_of_extruders;++i) {
+ for (unsigned j=0;j<m_number_of_extruders;++j) {
+ if (i==j) continue;
+ edit_boxes[j][i]->SetValue(wxString("")<< (m_old[i]->GetValue() + m_new[j]->GetValue()));
+ }
+ }
+}
+
+
+
+// Function to check if simple and advanced settings are matching
+bool WipingPanel::advanced_matches_simple() {
+ for (unsigned i=0;i<m_number_of_extruders;++i) {
+ for (unsigned j=0;j<m_number_of_extruders;++j) {
+ if (i==j) continue;
+ if (edit_boxes[j][i]->GetValue() != (wxString("")<< (m_old[i]->GetValue() + m_new[j]->GetValue())))
+ return false;
+ }
+ }
+ return true;
+}
+
+
+// Switches the dialog from simple to advanced mode and vice versa
+void WipingPanel::toggle_advanced(bool user_action) {
+ if (m_advanced && !advanced_matches_simple() && user_action) {
+ if (wxMessageDialog(this,wxString(_(L("Switching to simple settings will discard changes done in the advanced mode!\n\nDo you want to proceed?"))),
+ wxString(_(L("Warning"))),wxYES_NO|wxICON_EXCLAMATION).ShowModal() != wxID_YES)
+ return;
+ }
+ if (user_action)
+ m_advanced = !m_advanced; // user demands a change -> toggle
+ else
+ m_advanced = !advanced_matches_simple(); // if called from constructor, show what is appropriate
+
+ (m_advanced ? m_page_advanced : m_page_simple)->Show();
+ (!m_advanced ? m_page_advanced : m_page_simple)->Hide();
+
+ m_widget_button->SetLabel(m_advanced ? _(L("Show simplified settings")) : _(L("Show advanced settings")));
+ if (m_advanced)
+ if (user_action) fill_in_matrix(); // otherwise keep values loaded from config
+
+ m_sizer->Layout();
+ Refresh();
+}
diff --git a/src/slic3r/GUI/WipeTowerDialog.hpp b/src/slic3r/GUI/WipeTowerDialog.hpp
new file mode 100644
index 000000000..d858062da
--- /dev/null
+++ b/src/slic3r/GUI/WipeTowerDialog.hpp
@@ -0,0 +1,90 @@
+#ifndef _WIPE_TOWER_DIALOG_H_
+#define _WIPE_TOWER_DIALOG_H_
+
+#include <wx/spinctrl.h>
+#include <wx/stattext.h>
+#include <wx/textctrl.h>
+#include <wx/checkbox.h>
+#include <wx/msgdlg.h>
+
+#include "RammingChart.hpp"
+
+
+class RammingPanel : public wxPanel {
+public:
+ RammingPanel(wxWindow* parent);
+ RammingPanel(wxWindow* parent,const std::string& data);
+ std::string get_parameters();
+
+private:
+ Chart* m_chart = nullptr;
+ wxSpinCtrl* m_widget_volume = nullptr;
+ wxSpinCtrl* m_widget_ramming_line_width_multiplicator = nullptr;
+ wxSpinCtrl* m_widget_ramming_step_multiplicator = nullptr;
+ wxSpinCtrlDouble* m_widget_time = nullptr;
+ int m_ramming_step_multiplicator;
+ int m_ramming_line_width_multiplicator;
+
+ void line_parameters_changed();
+};
+
+
+class RammingDialog : public wxDialog {
+public:
+ RammingDialog(wxWindow* parent,const std::string& parameters);
+ std::string get_parameters() { return m_output_data; }
+private:
+ RammingPanel* m_panel_ramming = nullptr;
+ std::string m_output_data;
+};
+
+
+
+
+
+
+
+class WipingPanel : public wxPanel {
+public:
+ WipingPanel(wxWindow* parent, const std::vector<float>& matrix, const std::vector<float>& extruders, wxButton* widget_button);
+ std::vector<float> read_matrix_values();
+ std::vector<float> read_extruders_values();
+ void toggle_advanced(bool user_action = false);
+ void format_sizer(wxSizer* sizer, wxPanel* page, wxGridSizer* grid_sizer, const wxString& info, const wxString& table_title, int table_lshift=0);
+
+private:
+ void fill_in_matrix();
+ bool advanced_matches_simple();
+
+ std::vector<wxSpinCtrl*> m_old;
+ std::vector<wxSpinCtrl*> m_new;
+ std::vector<std::vector<wxTextCtrl*>> edit_boxes;
+ unsigned int m_number_of_extruders = 0;
+ bool m_advanced = false;
+ wxPanel* m_page_simple = nullptr;
+ wxPanel* m_page_advanced = nullptr;
+ wxBoxSizer* m_sizer = nullptr;
+ wxBoxSizer* m_sizer_simple = nullptr;
+ wxBoxSizer* m_sizer_advanced = nullptr;
+ wxGridSizer* m_gridsizer_advanced = nullptr;
+ wxButton* m_widget_button = nullptr;
+};
+
+
+
+
+
+class WipingDialog : public wxDialog {
+public:
+ WipingDialog(wxWindow* parent,const std::vector<float>& matrix, const std::vector<float>& extruders);
+ std::vector<float> get_matrix() const { return m_output_matrix; }
+ std::vector<float> get_extruders() const { return m_output_extruders; }
+
+
+private:
+ WipingPanel* m_panel_wiping = nullptr;
+ std::vector<float> m_output_matrix;
+ std::vector<float> m_output_extruders;
+};
+
+#endif // _WIPE_TOWER_DIALOG_H_ \ No newline at end of file
diff --git a/src/slic3r/GUI/callback.hpp b/src/slic3r/GUI/callback.hpp
new file mode 100644
index 000000000..ac92721a5
--- /dev/null
+++ b/src/slic3r/GUI/callback.hpp
@@ -0,0 +1,30 @@
+// I AM A PHONY PLACEHOLDER FOR THE PERL CALLBACK.
+// GET RID OF ME!
+
+#ifndef slic3r_GUI_PerlCallback_phony_hpp_
+#define slic3r_GUI_PerlCallback_phony_hpp_
+
+#include <vector>
+
+namespace Slic3r {
+
+class PerlCallback {
+public:
+ PerlCallback(void *) {}
+ PerlCallback() {}
+ void register_callback(void *) {}
+ void deregister_callback() {}
+ void call() const {}
+ void call(int) const {}
+ void call(int, int) const {}
+ void call(const std::vector<int>&) const {}
+ void call(double) const {}
+ void call(double, double) const {}
+ void call(double, double, double) const {}
+ void call(double, double, double, double) const {}
+ void call(bool b) const {}
+};
+
+} // namespace Slic3r
+
+#endif /* slic3r_GUI_PerlCallback_phony_hpp_ */
diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp
new file mode 100644
index 000000000..13730a497
--- /dev/null
+++ b/src/slic3r/GUI/wxExtensions.cpp
@@ -0,0 +1,1662 @@
+#include "wxExtensions.hpp"
+
+#include "GUI.hpp"
+#include "../../libslic3r/Utils.hpp"
+#include "BitmapCache.hpp"
+
+#include <wx/sizer.h>
+#include <wx/statline.h>
+#include <wx/dcclient.h>
+#include <wx/numformatter.h>
+
+const unsigned int wxCheckListBoxComboPopup::DefaultWidth = 200;
+const unsigned int wxCheckListBoxComboPopup::DefaultHeight = 200;
+const unsigned int wxCheckListBoxComboPopup::DefaultItemHeight = 18;
+
+bool wxCheckListBoxComboPopup::Create(wxWindow* parent)
+{
+ return wxCheckListBox::Create(parent, wxID_HIGHEST + 1, wxPoint(0, 0));
+}
+
+wxWindow* wxCheckListBoxComboPopup::GetControl()
+{
+ return this;
+}
+
+void wxCheckListBoxComboPopup::SetStringValue(const wxString& value)
+{
+ m_text = value;
+}
+
+wxString wxCheckListBoxComboPopup::GetStringValue() const
+{
+ return m_text;
+}
+
+wxSize wxCheckListBoxComboPopup::GetAdjustedSize(int minWidth, int prefHeight, int maxHeight)
+{
+ // matches owner wxComboCtrl's width
+ // and sets height dinamically in dependence of contained items count
+
+ wxComboCtrl* cmb = GetComboCtrl();
+ if (cmb != nullptr)
+ {
+ wxSize size = GetComboCtrl()->GetSize();
+
+ unsigned int count = GetCount();
+ if (count > 0)
+ size.SetHeight(count * DefaultItemHeight);
+ else
+ size.SetHeight(DefaultHeight);
+
+ return size;
+ }
+ else
+ return wxSize(DefaultWidth, DefaultHeight);
+}
+
+void wxCheckListBoxComboPopup::OnKeyEvent(wxKeyEvent& evt)
+{
+ // filters out all the keys which are not working properly
+ switch (evt.GetKeyCode())
+ {
+ case WXK_LEFT:
+ case WXK_UP:
+ case WXK_RIGHT:
+ case WXK_DOWN:
+ case WXK_PAGEUP:
+ case WXK_PAGEDOWN:
+ case WXK_END:
+ case WXK_HOME:
+ case WXK_NUMPAD_LEFT:
+ case WXK_NUMPAD_UP:
+ case WXK_NUMPAD_RIGHT:
+ case WXK_NUMPAD_DOWN:
+ case WXK_NUMPAD_PAGEUP:
+ case WXK_NUMPAD_PAGEDOWN:
+ case WXK_NUMPAD_END:
+ case WXK_NUMPAD_HOME:
+ {
+ break;
+ }
+ default:
+ {
+ evt.Skip();
+ break;
+ }
+ }
+}
+
+void wxCheckListBoxComboPopup::OnCheckListBox(wxCommandEvent& evt)
+{
+ // forwards the checklistbox event to the owner wxComboCtrl
+
+ if (m_check_box_events_status == OnCheckListBoxFunction::FreeToProceed )
+ {
+ wxComboCtrl* cmb = GetComboCtrl();
+ if (cmb != nullptr) {
+ wxCommandEvent event(wxEVT_CHECKLISTBOX, cmb->GetId());
+ event.SetEventObject(cmb);
+ cmb->ProcessWindowEvent(event);
+ }
+ }
+
+ evt.Skip();
+
+ #ifndef _WIN32 // events are sent differently on OSX+Linux vs Win (more description in header file)
+ if ( m_check_box_events_status == OnCheckListBoxFunction::RefuseToProceed )
+ // this happens if the event was resent by OnListBoxSelection - next call to OnListBoxSelection is due to user clicking the text, so the function should
+ // explicitly change the state on the checkbox
+ m_check_box_events_status = OnCheckListBoxFunction::WasRefusedLastTime;
+ else
+ // if the user clicked the checkbox square, this event was sent before OnListBoxSelection was called, so we don't want it to resend it
+ m_check_box_events_status = OnCheckListBoxFunction::RefuseToProceed;
+ #endif
+}
+
+void wxCheckListBoxComboPopup::OnListBoxSelection(wxCommandEvent& evt)
+{
+ // transforms list box item selection event into checklistbox item toggle event
+
+ int selId = GetSelection();
+ if (selId != wxNOT_FOUND)
+ {
+ #ifndef _WIN32
+ if (m_check_box_events_status == OnCheckListBoxFunction::RefuseToProceed)
+ #endif
+ Check((unsigned int)selId, !IsChecked((unsigned int)selId));
+
+ m_check_box_events_status = OnCheckListBoxFunction::FreeToProceed; // so the checkbox reacts to square-click the next time
+
+ SetSelection(wxNOT_FOUND);
+ wxCommandEvent event(wxEVT_CHECKLISTBOX, GetId());
+ event.SetInt(selId);
+ event.SetEventObject(this);
+ ProcessEvent(event);
+ }
+}
+
+
+// *** wxDataViewTreeCtrlComboPopup ***
+
+const unsigned int wxDataViewTreeCtrlComboPopup::DefaultWidth = 270;
+const unsigned int wxDataViewTreeCtrlComboPopup::DefaultHeight = 200;
+const unsigned int wxDataViewTreeCtrlComboPopup::DefaultItemHeight = 22;
+
+bool wxDataViewTreeCtrlComboPopup::Create(wxWindow* parent)
+{
+ return wxDataViewTreeCtrl::Create(parent, wxID_ANY/*HIGHEST + 1*/, wxPoint(0, 0), wxDefaultSize/*wxSize(270, -1)*/, wxDV_NO_HEADER);
+}
+/*
+wxSize wxDataViewTreeCtrlComboPopup::GetAdjustedSize(int minWidth, int prefHeight, int maxHeight)
+{
+ // matches owner wxComboCtrl's width
+ // and sets height dinamically in dependence of contained items count
+ wxComboCtrl* cmb = GetComboCtrl();
+ if (cmb != nullptr)
+ {
+ wxSize size = GetComboCtrl()->GetSize();
+ if (m_cnt_open_items > 0)
+ size.SetHeight(m_cnt_open_items * DefaultItemHeight);
+ else
+ size.SetHeight(DefaultHeight);
+
+ return size;
+ }
+ else
+ return wxSize(DefaultWidth, DefaultHeight);
+}
+*/
+void wxDataViewTreeCtrlComboPopup::OnKeyEvent(wxKeyEvent& evt)
+{
+ // filters out all the keys which are not working properly
+ if (evt.GetKeyCode() == WXK_UP)
+ {
+ return;
+ }
+ else if (evt.GetKeyCode() == WXK_DOWN)
+ {
+ return;
+ }
+ else
+ {
+ evt.Skip();
+ return;
+ }
+}
+
+void wxDataViewTreeCtrlComboPopup::OnDataViewTreeCtrlSelection(wxCommandEvent& evt)
+{
+ wxComboCtrl* cmb = GetComboCtrl();
+ auto selected = GetItemText(GetSelection());
+ cmb->SetText(selected);
+}
+
+// ----------------------------------------------------------------------------
+// *** PrusaCollapsiblePane ***
+// ----------------------------------------------------------------------------
+void PrusaCollapsiblePane::OnStateChange(const wxSize& sz)
+{
+#ifdef __WXOSX__
+ wxCollapsiblePane::OnStateChange(sz);
+#else
+ SetSize(sz);
+
+ if (this->HasFlag(wxCP_NO_TLW_RESIZE))
+ {
+ // the user asked to explicitly handle the resizing itself...
+ return;
+ }
+
+ auto top = GetParent(); //right_panel
+ if (!top)
+ return;
+
+ wxSizer *sizer = top->GetSizer();
+ if (!sizer)
+ return;
+
+ const wxSize newBestSize = sizer->ComputeFittingClientSize(top);
+ top->SetMinClientSize(newBestSize);
+
+ wxWindowUpdateLocker noUpdates_p(top->GetParent());
+ // we shouldn't attempt to resize a maximized window, whatever happens
+ // if (!top->IsMaximized())
+ // top->SetClientSize(newBestSize);
+ top->GetParent()->Layout();
+ top->Refresh();
+#endif //__WXOSX__
+}
+
+// ----------------------------------------------------------------------------
+// *** PrusaCollapsiblePaneMSW *** used only #ifdef __WXMSW__
+// ----------------------------------------------------------------------------
+#ifdef __WXMSW__
+bool PrusaCollapsiblePaneMSW::Create(wxWindow *parent, wxWindowID id, const wxString& label,
+ const wxPoint& pos, const wxSize& size, long style, const wxValidator& val, const wxString& name)
+{
+ if (!wxControl::Create(parent, id, pos, size, style, val, name))
+ return false;
+ m_pStaticLine = NULL;
+ m_strLabel = label;
+
+ // sizer containing the expand button and possibly a static line
+ m_sz = new wxBoxSizer(wxHORIZONTAL);
+
+ m_bmp_close.LoadFile(Slic3r::GUI::from_u8(Slic3r::var("disclosure_triangle_close.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_open.LoadFile(Slic3r::GUI::from_u8(Slic3r::var("disclosure_triangle_open.png")), wxBITMAP_TYPE_PNG);
+
+ m_pDisclosureTriangleButton = new wxButton(this, wxID_ANY, m_strLabel, wxPoint(0, 0),
+ wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER);
+ UpdateBtnBmp();
+ m_pDisclosureTriangleButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event)
+ {
+ if (event.GetEventObject() != m_pDisclosureTriangleButton)
+ {
+ event.Skip();
+ return;
+ }
+
+ Collapse(!IsCollapsed());
+
+ // this change was generated by the user - send the event
+ wxCollapsiblePaneEvent ev(this, GetId(), IsCollapsed());
+ GetEventHandler()->ProcessEvent(ev);
+ });
+
+ m_sz->Add(m_pDisclosureTriangleButton, 0, wxLEFT | wxTOP | wxBOTTOM, GetBorder());
+
+ // do not set sz as our sizers since we handle the pane window without using sizers
+ m_pPane = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
+ wxTAB_TRAVERSAL | wxNO_BORDER, wxT("wxCollapsiblePanePane"));
+
+ wxColour& clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
+ m_pDisclosureTriangleButton->SetBackgroundColour(clr);
+ this->SetBackgroundColour(clr);
+ m_pPane->SetBackgroundColour(clr);
+
+ // start as collapsed:
+ m_pPane->Hide();
+
+ return true;
+}
+
+void PrusaCollapsiblePaneMSW::UpdateBtnBmp()
+{
+ if (IsCollapsed())
+ m_pDisclosureTriangleButton->SetBitmap(m_bmp_close);
+ else{
+ m_pDisclosureTriangleButton->SetBitmap(m_bmp_open);
+ // To updating button bitmap it's needed to lost focus on this button, so
+ // we set focus to mainframe
+ //GetParent()->GetParent()->GetParent()->SetFocus();
+ //or to pane
+ GetPane()->SetFocus();
+ }
+ Layout();
+}
+
+void PrusaCollapsiblePaneMSW::SetLabel(const wxString &label)
+{
+ m_strLabel = label;
+ m_pDisclosureTriangleButton->SetLabel(m_strLabel);
+ Layout();
+}
+
+bool PrusaCollapsiblePaneMSW::Layout()
+{
+ if (!m_pDisclosureTriangleButton || !m_pPane || !m_sz)
+ return false; // we need to complete the creation first!
+
+ wxSize oursz(GetSize());
+
+ // move & resize the button and the static line
+ m_sz->SetDimension(0, 0, oursz.GetWidth(), m_sz->GetMinSize().GetHeight());
+ m_sz->Layout();
+
+ if (IsExpanded())
+ {
+ // move & resize the container window
+ int yoffset = m_sz->GetSize().GetHeight() + GetBorder();
+ m_pPane->SetSize(0, yoffset,
+ oursz.x, oursz.y - yoffset);
+
+ // this is very important to make the pane window layout show correctly
+ m_pPane->Layout();
+ }
+
+ return true;
+}
+
+void PrusaCollapsiblePaneMSW::Collapse(bool collapse)
+{
+ // optimization
+ if (IsCollapsed() == collapse)
+ return;
+
+ InvalidateBestSize();
+
+ // update our state
+ m_pPane->Show(!collapse);
+
+ // update button bitmap
+ UpdateBtnBmp();
+
+ OnStateChange(GetBestSize());
+}
+#endif //__WXMSW__
+
+// *****************************************************************************
+// ----------------------------------------------------------------------------
+// PrusaObjectDataViewModelNode
+// ----------------------------------------------------------------------------
+
+void PrusaObjectDataViewModelNode::set_object_action_icon() {
+ m_action_icon = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("add_object.png")), wxBITMAP_TYPE_PNG);
+}
+void PrusaObjectDataViewModelNode::set_part_action_icon() {
+ m_action_icon = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("cog.png")), wxBITMAP_TYPE_PNG);
+}
+
+Slic3r::GUI::BitmapCache *m_bitmap_cache = nullptr;
+bool PrusaObjectDataViewModelNode::update_settings_digest(const std::vector<std::string>& categories)
+{
+ if (m_type != "settings" || m_opt_categories == categories)
+ return false;
+
+ m_opt_categories = categories;
+ m_name = wxEmptyString;
+ m_icon = m_empty_icon;
+
+ auto categories_icon = Slic3r::GUI::get_category_icon();
+
+ for (auto& cat : m_opt_categories)
+ m_name += cat + "; ";
+
+ wxBitmap *bmp = m_bitmap_cache->find(m_name.ToStdString());
+ if (bmp == nullptr) {
+ std::vector<wxBitmap> bmps;
+ for (auto& cat : m_opt_categories)
+ bmps.emplace_back(categories_icon.find(cat) == categories_icon.end() ?
+ wxNullBitmap : categories_icon.at(cat));
+ bmp = m_bitmap_cache->insert(m_name.ToStdString(), bmps);
+ }
+
+ m_bmp = *bmp;
+
+ return true;
+}
+
+// *****************************************************************************
+// ----------------------------------------------------------------------------
+// PrusaObjectDataViewModel
+// ----------------------------------------------------------------------------
+
+PrusaObjectDataViewModel::PrusaObjectDataViewModel()
+{
+ m_bitmap_cache = new Slic3r::GUI::BitmapCache;
+}
+
+PrusaObjectDataViewModel::~PrusaObjectDataViewModel()
+{
+ for (auto object : m_objects)
+ delete object;
+ delete m_bitmap_cache;
+ m_bitmap_cache = nullptr;
+}
+
+wxDataViewItem PrusaObjectDataViewModel::Add(const wxString &name)
+{
+ auto root = new PrusaObjectDataViewModelNode(name);
+ m_objects.push_back(root);
+ // notify control
+ wxDataViewItem child((void*)root);
+ wxDataViewItem parent((void*)NULL);
+ ItemAdded(parent, child);
+ return child;
+}
+
+wxDataViewItem PrusaObjectDataViewModel::Add(const wxString &name, const int instances_count/*, int scale*/)
+{
+ auto root = new PrusaObjectDataViewModelNode(name, instances_count);
+ m_objects.push_back(root);
+ // notify control
+ wxDataViewItem child((void*)root);
+ wxDataViewItem parent((void*)NULL);
+ ItemAdded(parent, child);
+ return child;
+}
+
+wxDataViewItem PrusaObjectDataViewModel::AddChild( const wxDataViewItem &parent_item,
+ const wxString &name,
+ const wxBitmap& icon,
+ const int extruder/* = 0*/,
+ const bool create_frst_child/* = true*/)
+{
+ PrusaObjectDataViewModelNode *root = (PrusaObjectDataViewModelNode*)parent_item.GetID();
+ if (!root) return wxDataViewItem(0);
+
+ const wxString extruder_str = extruder == 0 ? "default" : wxString::Format("%d", extruder);
+
+ if (create_frst_child && (root->GetChildren().Count() == 0 ||
+ (root->GetChildren().Count() == 1 && root->GetNthChild(0)->m_type == "settings")))
+ {
+ const auto icon_solid_mesh = wxIcon(Slic3r::GUI::from_u8(Slic3r::var("object.png")), wxBITMAP_TYPE_PNG);
+ const auto node = new PrusaObjectDataViewModelNode(root, root->m_name, icon_solid_mesh, extruder_str, 0);
+ root->Append(node);
+ // notify control
+ const wxDataViewItem child((void*)node);
+ ItemAdded(parent_item, child);
+ }
+
+ const auto volume_id = root->GetChildCount() > 0 && root->GetNthChild(0)->m_type == "settings" ?
+ root->GetChildCount() - 1 : root->GetChildCount();
+
+ const auto node = new PrusaObjectDataViewModelNode(root, name, icon, extruder_str, volume_id);
+ root->Append(node);
+ // notify control
+ const wxDataViewItem child((void*)node);
+ ItemAdded(parent_item, child);
+ return child;
+}
+
+wxDataViewItem PrusaObjectDataViewModel::AddSettingsChild(const wxDataViewItem &parent_item)
+{
+ PrusaObjectDataViewModelNode *root = (PrusaObjectDataViewModelNode*)parent_item.GetID();
+ if (!root) return wxDataViewItem(0);
+
+ const auto node = new PrusaObjectDataViewModelNode(root);
+ root->Insert(node, 0);
+ // notify control
+ const wxDataViewItem child((void*)node);
+ ItemAdded(parent_item, child);
+ return child;
+}
+
+wxDataViewItem PrusaObjectDataViewModel::Delete(const wxDataViewItem &item)
+{
+ auto ret_item = wxDataViewItem(0);
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ if (!node) // happens if item.IsOk()==false
+ return ret_item;
+
+ auto node_parent = node->GetParent();
+ wxDataViewItem parent(node_parent);
+
+ // first remove the node from the parent's array of children;
+ // NOTE: MyObjectTreeModelNodePtrArray is only an array of _pointers_
+ // thus removing the node from it doesn't result in freeing it
+ if (node_parent){
+ auto id = node_parent->GetChildren().Index(node);
+ auto v_id = node->GetVolumeId();
+ node_parent->GetChildren().Remove(node);
+ if (id > 0){
+ if(id == node_parent->GetChildCount()) id--;
+ ret_item = wxDataViewItem(node_parent->GetChildren().Item(id));
+ }
+
+ //update volume_id value for remaining child-nodes
+ auto children = node_parent->GetChildren();
+ for (size_t i = 0; i < node_parent->GetChildCount() && v_id>=0; i++)
+ {
+ auto volume_id = children[i]->GetVolumeId();
+ if (volume_id > v_id)
+ children[i]->SetVolumeId(volume_id-1);
+ }
+ }
+ else
+ {
+ auto it = find(m_objects.begin(), m_objects.end(), node);
+ auto id = it - m_objects.begin();
+ if (it != m_objects.end())
+ m_objects.erase(it);
+ if (id > 0){
+ if(id == m_objects.size()) id--;
+ ret_item = wxDataViewItem(m_objects[id]);
+ }
+ }
+ // free the node
+ delete node;
+
+ // set m_containet to FALSE if parent has no child
+ if (node_parent) {
+#ifndef __WXGTK__
+ if (node_parent->GetChildCount() == 0)
+ node_parent->m_container = false;
+#endif //__WXGTK__
+ ret_item = parent;
+ }
+
+ // notify control
+ ItemDeleted(parent, item);
+ return ret_item;
+}
+
+void PrusaObjectDataViewModel::DeleteAll()
+{
+ while (!m_objects.empty())
+ {
+ auto object = m_objects.back();
+// object->RemoveAllChildren();
+ Delete(wxDataViewItem(object));
+ }
+}
+
+void PrusaObjectDataViewModel::DeleteChildren(wxDataViewItem& parent)
+{
+ PrusaObjectDataViewModelNode *root = (PrusaObjectDataViewModelNode*)parent.GetID();
+ if (!root) // happens if item.IsOk()==false
+ return;
+
+ // first remove the node from the parent's array of children;
+ // NOTE: MyObjectTreeModelNodePtrArray is only an array of _pointers_
+ // thus removing the node from it doesn't result in freeing it
+ auto& children = root->GetChildren();
+ for (int id = root->GetChildCount() - 1; id >= 0; --id)
+ {
+ auto node = children[id];
+ auto item = wxDataViewItem(node);
+ children.RemoveAt(id);
+
+ // free the node
+ delete node;
+
+ // notify control
+ ItemDeleted(parent, item);
+ }
+
+ // set m_containet to FALSE if parent has no child
+#ifndef __WXGTK__
+ root->m_container = false;
+#endif //__WXGTK__
+}
+
+wxDataViewItem PrusaObjectDataViewModel::GetItemById(int obj_idx)
+{
+ if (obj_idx >= m_objects.size())
+ {
+ printf("Error! Out of objects range.\n");
+ return wxDataViewItem(0);
+ }
+ return wxDataViewItem(m_objects[obj_idx]);
+}
+
+
+wxDataViewItem PrusaObjectDataViewModel::GetItemByVolumeId(int obj_idx, int volume_idx)
+{
+ if (obj_idx >= m_objects.size()) {
+ printf("Error! Out of objects range.\n");
+ return wxDataViewItem(0);
+ }
+
+ auto parent = m_objects[obj_idx];
+ if (parent->GetChildCount() == 0) {
+ printf("Error! Object has no one volume.\n");
+ return wxDataViewItem(0);
+ }
+
+ for (size_t i = 0; i < parent->GetChildCount(); i++)
+ if (parent->GetNthChild(i)->m_volume_id == volume_idx)
+ return wxDataViewItem(parent->GetNthChild(i));
+
+ return wxDataViewItem(0);
+}
+
+int PrusaObjectDataViewModel::GetIdByItem(wxDataViewItem& item)
+{
+ wxASSERT(item.IsOk());
+
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ auto it = find(m_objects.begin(), m_objects.end(), node);
+ if (it == m_objects.end())
+ return -1;
+
+ return it - m_objects.begin();
+}
+
+int PrusaObjectDataViewModel::GetVolumeIdByItem(const wxDataViewItem& item)
+{
+ wxASSERT(item.IsOk());
+
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ if (!node) // happens if item.IsOk()==false
+ return -1;
+ return node->GetVolumeId();
+}
+
+wxString PrusaObjectDataViewModel::GetName(const wxDataViewItem &item) const
+{
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ if (!node) // happens if item.IsOk()==false
+ return wxEmptyString;
+
+ return node->m_name;
+}
+
+wxString PrusaObjectDataViewModel::GetCopy(const wxDataViewItem &item) const
+{
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ if (!node) // happens if item.IsOk()==false
+ return wxEmptyString;
+
+ return node->m_copy;
+}
+
+wxIcon& PrusaObjectDataViewModel::GetIcon(const wxDataViewItem &item) const
+{
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ return node->m_icon;
+}
+
+wxBitmap& PrusaObjectDataViewModel::GetBitmap(const wxDataViewItem &item) const
+{
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ return node->m_bmp;
+}
+
+void PrusaObjectDataViewModel::GetValue(wxVariant &variant, const wxDataViewItem &item, unsigned int col) const
+{
+ wxASSERT(item.IsOk());
+
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ switch (col)
+ {
+ case 0:{
+ const PrusaDataViewBitmapText data(node->m_name, node->m_bmp);
+ variant << data;
+ break;}
+ case 1:
+ variant = node->m_copy;
+ break;
+ case 2:
+ variant = node->m_extruder;
+ break;
+ case 3:
+ variant << node->m_action_icon;
+ break;
+ default:
+ ;
+ }
+}
+
+bool PrusaObjectDataViewModel::SetValue(const wxVariant &variant, const wxDataViewItem &item, unsigned int col)
+{
+ wxASSERT(item.IsOk());
+
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ return node->SetValue(variant, col);
+}
+
+bool PrusaObjectDataViewModel::SetValue(const wxVariant &variant, const int item_idx, unsigned int col)
+{
+ if (item_idx < 0 || item_idx >= m_objects.size())
+ return false;
+
+ return m_objects[item_idx]->SetValue(variant, col);
+}
+
+wxDataViewItem PrusaObjectDataViewModel::MoveChildUp(const wxDataViewItem &item)
+{
+ auto ret_item = wxDataViewItem(0);
+ wxASSERT(item.IsOk());
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ if (!node) // happens if item.IsOk()==false
+ return ret_item;
+
+ auto node_parent = node->GetParent();
+ if (!node_parent) // If isn't part, but object
+ return ret_item;
+
+ auto volume_id = node->GetVolumeId();
+ if (0 < volume_id && volume_id < node_parent->GetChildCount()){
+ node_parent->SwapChildrens(volume_id - 1, volume_id);
+ ret_item = wxDataViewItem(node_parent->GetNthChild(volume_id - 1));
+ ItemChanged(item);
+ ItemChanged(ret_item);
+ }
+ else
+ ret_item = wxDataViewItem(node_parent->GetNthChild(0));
+ return ret_item;
+}
+
+wxDataViewItem PrusaObjectDataViewModel::MoveChildDown(const wxDataViewItem &item)
+{
+ auto ret_item = wxDataViewItem(0);
+ wxASSERT(item.IsOk());
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ if (!node) // happens if item.IsOk()==false
+ return ret_item;
+
+ auto node_parent = node->GetParent();
+ if (!node_parent) // If isn't part, but object
+ return ret_item;
+
+ auto volume_id = node->GetVolumeId();
+ if (0 <= volume_id && volume_id+1 < node_parent->GetChildCount()){
+ node_parent->SwapChildrens(volume_id + 1, volume_id);
+ ret_item = wxDataViewItem(node_parent->GetNthChild(volume_id + 1));
+ ItemChanged(item);
+ ItemChanged(ret_item);
+ }
+ else
+ ret_item = wxDataViewItem(node_parent->GetNthChild(node_parent->GetChildCount()-1));
+ return ret_item;
+}
+
+wxDataViewItem PrusaObjectDataViewModel::ReorganizeChildren(int current_volume_id, int new_volume_id, const wxDataViewItem &parent)
+{
+ auto ret_item = wxDataViewItem(0);
+ if (current_volume_id == new_volume_id)
+ return ret_item;
+ wxASSERT(parent.IsOk());
+ PrusaObjectDataViewModelNode *node_parent = (PrusaObjectDataViewModelNode*)parent.GetID();
+ if (!node_parent) // happens if item.IsOk()==false
+ return ret_item;
+
+ const size_t shift = node_parent->GetChildren().Item(0)->m_type == "settings" ? 1 : 0;
+
+ PrusaObjectDataViewModelNode *deleted_node = node_parent->GetNthChild(current_volume_id+shift);
+ node_parent->GetChildren().Remove(deleted_node);
+ ItemDeleted(parent, wxDataViewItem(deleted_node));
+ node_parent->Insert(deleted_node, new_volume_id+shift);
+ ItemAdded(parent, wxDataViewItem(deleted_node));
+ const auto settings_item = HasSettings(wxDataViewItem(deleted_node));
+ if (settings_item)
+ ItemAdded(wxDataViewItem(deleted_node), settings_item);
+
+ //update volume_id value for child-nodes
+ auto children = node_parent->GetChildren();
+ int id_frst = current_volume_id < new_volume_id ? current_volume_id : new_volume_id;
+ int id_last = current_volume_id > new_volume_id ? current_volume_id : new_volume_id;
+ for (int id = id_frst; id <= id_last; ++id)
+ children[id+shift]->SetVolumeId(id);
+
+ return wxDataViewItem(node_parent->GetNthChild(new_volume_id+shift));
+}
+
+bool PrusaObjectDataViewModel::IsEnabled(const wxDataViewItem &item, unsigned int col) const
+{
+ wxASSERT(item.IsOk());
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+
+ // disable extruder selection for the "Settings" item
+ return !(col == 2 && node->m_extruder.IsEmpty());
+}
+
+wxDataViewItem PrusaObjectDataViewModel::GetParent(const wxDataViewItem &item) const
+{
+ // the invisible root node has no parent
+ if (!item.IsOk())
+ return wxDataViewItem(0);
+
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+
+ // objects nodes has no parent too
+ if (find(m_objects.begin(), m_objects.end(),node) != m_objects.end())
+ return wxDataViewItem(0);
+
+ return wxDataViewItem((void*)node->GetParent());
+}
+
+bool PrusaObjectDataViewModel::IsContainer(const wxDataViewItem &item) const
+{
+ // the invisible root node can have children
+ if (!item.IsOk())
+ return true;
+
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ return node->IsContainer();
+}
+
+unsigned int PrusaObjectDataViewModel::GetChildren(const wxDataViewItem &parent, wxDataViewItemArray &array) const
+{
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)parent.GetID();
+ if (!node)
+ {
+ for (auto object : m_objects)
+ array.Add(wxDataViewItem((void*)object));
+ return m_objects.size();
+ }
+
+ if (node->GetChildCount() == 0)
+ {
+ return 0;
+ }
+
+ unsigned int count = node->GetChildren().GetCount();
+ for (unsigned int pos = 0; pos < count; pos++)
+ {
+ PrusaObjectDataViewModelNode *child = node->GetChildren().Item(pos);
+ array.Add(wxDataViewItem((void*)child));
+ }
+
+ return count;
+}
+
+wxDataViewItem PrusaObjectDataViewModel::HasSettings(const wxDataViewItem &item) const
+{
+ if (!item.IsOk())
+ return wxDataViewItem(0);
+
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ if (node->GetChildCount() == 0)
+ return wxDataViewItem(0);
+
+ auto& children = node->GetChildren();
+ if (children[0]->m_type == "settings")
+ return wxDataViewItem((void*)children[0]);;
+
+ return wxDataViewItem(0);
+}
+
+bool PrusaObjectDataViewModel::IsSettingsItem(const wxDataViewItem &item) const
+{
+ if (!item.IsOk())
+ return false;
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ return node->m_type == "settings";
+}
+
+
+
+void PrusaObjectDataViewModel::UpdateSettingsDigest(const wxDataViewItem &item,
+ const std::vector<std::string>& categories)
+{
+ if (!item.IsOk()) return;
+ PrusaObjectDataViewModelNode *node = (PrusaObjectDataViewModelNode*)item.GetID();
+ if (!node->update_settings_digest(categories))
+ return;
+ ItemChanged(item);
+}
+
+IMPLEMENT_VARIANT_OBJECT(PrusaDataViewBitmapText)
+// ---------------------------------------------------------
+// PrusaIconTextRenderer
+// ---------------------------------------------------------
+
+bool PrusaBitmapTextRenderer::SetValue(const wxVariant &value)
+{
+ m_value << value;
+ return true;
+}
+
+bool PrusaBitmapTextRenderer::GetValue(wxVariant& WXUNUSED(value)) const
+{
+ return false;
+}
+
+bool PrusaBitmapTextRenderer::Render(wxRect rect, wxDC *dc, int state)
+{
+ int xoffset = 0;
+
+ const wxBitmap& icon = m_value.GetBitmap();
+ if (icon.IsOk())
+ {
+ dc->DrawBitmap(icon, rect.x, rect.y + (rect.height - icon.GetHeight()) / 2);
+ xoffset = icon.GetWidth() + 4;
+ }
+
+ RenderText(m_value.GetText(), xoffset, rect, dc, state);
+
+ return true;
+}
+
+wxSize PrusaBitmapTextRenderer::GetSize() const
+{
+ if (!m_value.GetText().empty())
+ {
+ wxSize size = GetTextExtent(m_value.GetText());
+
+ if (m_value.GetBitmap().IsOk())
+ size.x += m_value.GetBitmap().GetWidth() + 4;
+ return size;
+ }
+ return wxSize(80, 20);
+}
+
+
+// ----------------------------------------------------------------------------
+// PrusaDoubleSlider
+// ----------------------------------------------------------------------------
+
+PrusaDoubleSlider::PrusaDoubleSlider(wxWindow *parent,
+ wxWindowID id,
+ int lowerValue,
+ int higherValue,
+ int minValue,
+ int maxValue,
+ const wxPoint& pos,
+ const wxSize& size,
+ long style,
+ const wxValidator& val,
+ const wxString& name) :
+ wxControl(parent, id, pos, size, wxWANTS_CHARS | wxBORDER_NONE),
+ m_lower_value(lowerValue), m_higher_value (higherValue),
+ m_min_value(minValue), m_max_value(maxValue),
+ m_style(style == wxSL_HORIZONTAL || style == wxSL_VERTICAL ? style: wxSL_HORIZONTAL)
+{
+#ifndef __WXOSX__ // SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX
+ SetDoubleBuffered(true);
+#endif //__WXOSX__
+
+ m_bmp_thumb_higher = wxBitmap(style == wxSL_HORIZONTAL ? Slic3r::GUI::from_u8(Slic3r::var("right_half_circle.png")) :
+ Slic3r::GUI::from_u8(Slic3r::var("up_half_circle.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_thumb_lower = wxBitmap(style == wxSL_HORIZONTAL ? Slic3r::GUI::from_u8(Slic3r::var("left_half_circle.png")) :
+ Slic3r::GUI::from_u8(Slic3r::var("down_half_circle.png")), wxBITMAP_TYPE_PNG);
+ m_thumb_size = m_bmp_thumb_lower.GetSize();
+
+ m_bmp_add_tick_on = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("colorchange_add_on.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_add_tick_off = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("colorchange_add_off.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_del_tick_on = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("colorchange_delete_on.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_del_tick_off = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("colorchange_delete_off.png")), wxBITMAP_TYPE_PNG);
+ m_tick_icon_dim = m_bmp_add_tick_on.GetSize().x;
+
+ m_bmp_one_layer_lock_on = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("one_layer_lock_on.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_one_layer_lock_off = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("one_layer_lock_off.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_one_layer_unlock_on = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("one_layer_unlock_on.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_one_layer_unlock_off = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("one_layer_unlock_off.png")), wxBITMAP_TYPE_PNG);
+ m_lock_icon_dim = m_bmp_one_layer_lock_on.GetSize().x;
+
+ m_selection = ssUndef;
+
+ // slider events
+ Bind(wxEVT_PAINT, &PrusaDoubleSlider::OnPaint, this);
+ Bind(wxEVT_LEFT_DOWN, &PrusaDoubleSlider::OnLeftDown, this);
+ Bind(wxEVT_MOTION, &PrusaDoubleSlider::OnMotion, this);
+ Bind(wxEVT_LEFT_UP, &PrusaDoubleSlider::OnLeftUp, this);
+ Bind(wxEVT_MOUSEWHEEL, &PrusaDoubleSlider::OnWheel, this);
+ Bind(wxEVT_ENTER_WINDOW,&PrusaDoubleSlider::OnEnterWin, this);
+ Bind(wxEVT_LEAVE_WINDOW,&PrusaDoubleSlider::OnLeaveWin, this);
+ Bind(wxEVT_KEY_DOWN, &PrusaDoubleSlider::OnKeyDown, this);
+ Bind(wxEVT_KEY_UP, &PrusaDoubleSlider::OnKeyUp, this);
+ Bind(wxEVT_RIGHT_DOWN, &PrusaDoubleSlider::OnRightDown,this);
+ Bind(wxEVT_RIGHT_UP, &PrusaDoubleSlider::OnRightUp, this);
+
+ // control's view variables
+ SLIDER_MARGIN = 4 + (style == wxSL_HORIZONTAL ? m_bmp_thumb_higher.GetWidth() : m_bmp_thumb_higher.GetHeight());
+
+ DARK_ORANGE_PEN = wxPen(wxColour(253, 84, 2));
+ ORANGE_PEN = wxPen(wxColour(253, 126, 66));
+ LIGHT_ORANGE_PEN = wxPen(wxColour(254, 177, 139));
+
+ DARK_GREY_PEN = wxPen(wxColour(128, 128, 128));
+ GREY_PEN = wxPen(wxColour(164, 164, 164));
+ LIGHT_GREY_PEN = wxPen(wxColour(204, 204, 204));
+
+ line_pens = { &DARK_GREY_PEN, &GREY_PEN, &LIGHT_GREY_PEN };
+ segm_pens = { &DARK_ORANGE_PEN, &ORANGE_PEN, &LIGHT_ORANGE_PEN };
+}
+
+int PrusaDoubleSlider::GetActiveValue() const
+{
+ return m_selection == ssLower ?
+ m_lower_value : m_selection == ssHigher ?
+ m_higher_value : -1;
+}
+
+wxSize PrusaDoubleSlider::DoGetBestSize() const
+{
+ const wxSize size = wxControl::DoGetBestSize();
+ if (size.x > 1 && size.y > 1)
+ return size;
+ const int new_size = is_horizontal() ? 80 : 120;
+ return wxSize(new_size, new_size);
+}
+
+void PrusaDoubleSlider::SetLowerValue(const int lower_val)
+{
+ m_selection = ssLower;
+ m_lower_value = lower_val;
+ correct_lower_value();
+ Refresh();
+ Update();
+
+ wxCommandEvent e(wxEVT_SCROLL_CHANGED);
+ e.SetEventObject(this);
+ ProcessWindowEvent(e);
+}
+
+void PrusaDoubleSlider::SetHigherValue(const int higher_val)
+{
+ m_selection = ssHigher;
+ m_higher_value = higher_val;
+ correct_higher_value();
+ Refresh();
+ Update();
+
+ wxCommandEvent e(wxEVT_SCROLL_CHANGED);
+ e.SetEventObject(this);
+ ProcessWindowEvent(e);
+}
+
+void PrusaDoubleSlider::SetMaxValue(const int max_value)
+{
+ m_max_value = max_value;
+ Refresh();
+ Update();
+}
+
+void PrusaDoubleSlider::draw_scroll_line(wxDC& dc, const int lower_pos, const int higher_pos)
+{
+ int width;
+ int height;
+ get_size(&width, &height);
+
+ wxCoord line_beg_x = is_horizontal() ? SLIDER_MARGIN : width*0.5 - 1;
+ wxCoord line_beg_y = is_horizontal() ? height*0.5 - 1 : SLIDER_MARGIN;
+ wxCoord line_end_x = is_horizontal() ? width - SLIDER_MARGIN + 1 : width*0.5 - 1;
+ wxCoord line_end_y = is_horizontal() ? height*0.5 - 1 : height - SLIDER_MARGIN + 1;
+
+ wxCoord segm_beg_x = is_horizontal() ? lower_pos : width*0.5 - 1;
+ wxCoord segm_beg_y = is_horizontal() ? height*0.5 - 1 : lower_pos-1;
+ wxCoord segm_end_x = is_horizontal() ? higher_pos : width*0.5 - 1;
+ wxCoord segm_end_y = is_horizontal() ? height*0.5 - 1 : higher_pos-1;
+
+ for (int id = 0; id < line_pens.size(); id++)
+ {
+ dc.SetPen(*line_pens[id]);
+ dc.DrawLine(line_beg_x, line_beg_y, line_end_x, line_end_y);
+ dc.SetPen(*segm_pens[id]);
+ dc.DrawLine(segm_beg_x, segm_beg_y, segm_end_x, segm_end_y);
+ if (is_horizontal())
+ line_beg_y = line_end_y = segm_beg_y = segm_end_y += 1;
+ else
+ line_beg_x = line_end_x = segm_beg_x = segm_end_x += 1;
+ }
+}
+
+double PrusaDoubleSlider::get_scroll_step()
+{
+ const wxSize sz = get_size();
+ const int& slider_len = m_style == wxSL_HORIZONTAL ? sz.x : sz.y;
+ return double(slider_len - SLIDER_MARGIN * 2) / (m_max_value - m_min_value);
+}
+
+// get position on the slider line from entered value
+wxCoord PrusaDoubleSlider::get_position_from_value(const int value)
+{
+ const double step = get_scroll_step();
+ const int val = is_horizontal() ? value : m_max_value - value;
+ return wxCoord(SLIDER_MARGIN + int(val*step + 0.5));
+}
+
+wxSize PrusaDoubleSlider::get_size()
+{
+ int w, h;
+ get_size(&w, &h);
+ return wxSize(w, h);
+}
+
+void PrusaDoubleSlider::get_size(int *w, int *h)
+{
+ GetSize(w, h);
+ is_horizontal() ? *w -= m_lock_icon_dim : *h -= m_lock_icon_dim;
+}
+
+double PrusaDoubleSlider::get_double_value(const SelectedSlider& selection) const
+{
+ if (m_values.empty())
+ return 0.0;
+ return m_values[selection == ssLower ? m_lower_value : m_higher_value].second;
+}
+
+void PrusaDoubleSlider::get_lower_and_higher_position(int& lower_pos, int& higher_pos)
+{
+ const double step = get_scroll_step();
+ if (is_horizontal()) {
+ lower_pos = SLIDER_MARGIN + int(m_lower_value*step + 0.5);
+ higher_pos = SLIDER_MARGIN + int(m_higher_value*step + 0.5);
+ }
+ else {
+ lower_pos = SLIDER_MARGIN + int((m_max_value - m_lower_value)*step + 0.5);
+ higher_pos = SLIDER_MARGIN + int((m_max_value - m_higher_value)*step + 0.5);
+ }
+}
+
+void PrusaDoubleSlider::draw_focus_rect()
+{
+ if (!m_is_focused)
+ return;
+ const wxSize sz = GetSize();
+ wxPaintDC dc(this);
+ const wxPen pen = wxPen(wxColour(128, 128, 10), 1, wxPENSTYLE_DOT);
+ dc.SetPen(pen);
+ dc.SetBrush(wxBrush(wxColour(0, 0, 0), wxBRUSHSTYLE_TRANSPARENT));
+ dc.DrawRectangle(1, 1, sz.x - 2, sz.y - 2);
+}
+
+void PrusaDoubleSlider::render()
+{
+ SetBackgroundColour(GetParent()->GetBackgroundColour());
+ draw_focus_rect();
+
+ wxPaintDC dc(this);
+ wxFont font = dc.GetFont();
+ const wxFont smaller_font = font.Smaller();
+ dc.SetFont(smaller_font);
+
+ const wxCoord lower_pos = get_position_from_value(m_lower_value);
+ const wxCoord higher_pos = get_position_from_value(m_higher_value);
+
+ // draw line
+ draw_scroll_line(dc, lower_pos, higher_pos);
+
+// //lower slider:
+// draw_thumb(dc, lower_pos, ssLower);
+// //higher slider:
+// draw_thumb(dc, higher_pos, ssHigher);
+
+ // draw both sliders
+ draw_thumbs(dc, lower_pos, higher_pos);
+
+ //draw color print ticks
+ draw_ticks(dc);
+
+ //draw color print ticks
+ draw_one_layer_icon(dc);
+}
+
+void PrusaDoubleSlider::draw_action_icon(wxDC& dc, const wxPoint pt_beg, const wxPoint pt_end)
+{
+ const int tick = m_selection == ssLower ? m_lower_value : m_higher_value;
+ wxBitmap* icon = m_is_action_icon_focesed ? &m_bmp_add_tick_off : &m_bmp_add_tick_on;
+ if (m_ticks.find(tick) != m_ticks.end())
+ icon = m_is_action_icon_focesed ? &m_bmp_del_tick_off : &m_bmp_del_tick_on;
+
+ wxCoord x_draw, y_draw;
+ is_horizontal() ? x_draw = pt_beg.x - 0.5*m_tick_icon_dim : y_draw = pt_beg.y - 0.5*m_tick_icon_dim;
+ if (m_selection == ssLower)
+ is_horizontal() ? y_draw = pt_end.y + 3 : x_draw = pt_beg.x - m_tick_icon_dim-2;
+ else
+ is_horizontal() ? y_draw = pt_beg.y - m_tick_icon_dim-2 : x_draw = pt_end.x + 3;
+
+ dc.DrawBitmap(*icon, x_draw, y_draw);
+
+ //update rect of the tick action icon
+ m_rect_tick_action = wxRect(x_draw, y_draw, m_tick_icon_dim, m_tick_icon_dim);
+}
+
+void PrusaDoubleSlider::draw_info_line_with_icon(wxDC& dc, const wxPoint& pos, const SelectedSlider selection)
+{
+ if (m_selection == selection) {
+ //draw info line
+ dc.SetPen(DARK_ORANGE_PEN);
+ const wxPoint pt_beg = is_horizontal() ? wxPoint(pos.x, pos.y - m_thumb_size.y) : wxPoint(pos.x - m_thumb_size.x, pos.y - 1);
+ const wxPoint pt_end = is_horizontal() ? wxPoint(pos.x, pos.y + m_thumb_size.y) : wxPoint(pos.x + m_thumb_size.x, pos.y - 1);
+ dc.DrawLine(pt_beg, pt_end);
+
+ //draw action icon
+ draw_action_icon(dc, pt_beg, pt_end);
+ }
+}
+
+wxString PrusaDoubleSlider::get_label(const SelectedSlider& selection) const
+{
+ const int value = selection == ssLower ? m_lower_value : m_higher_value;
+
+ if (m_label_koef == 1.0 && m_values.empty())
+ return wxString::Format("%d", value);
+
+ const wxString str = m_values.empty() ?
+ wxNumberFormatter::ToString(m_label_koef*value, 2, wxNumberFormatter::Style_None) :
+ wxNumberFormatter::ToString(m_values[value].second, 2, wxNumberFormatter::Style_None);
+ return wxString::Format("%s\n(%d)", str, m_values.empty() ? value : m_values[value].first);
+}
+
+void PrusaDoubleSlider::draw_thumb_text(wxDC& dc, const wxPoint& pos, const SelectedSlider& selection) const
+{
+ if ((m_is_one_layer || m_higher_value==m_lower_value) && selection != m_selection || !selection)
+ return;
+ wxCoord text_width, text_height;
+ const wxString label = get_label(selection);
+ dc.GetMultiLineTextExtent(label, &text_width, &text_height);
+ wxPoint text_pos;
+ if (selection ==ssLower)
+ text_pos = is_horizontal() ? wxPoint(pos.x + 1, pos.y + m_thumb_size.x) :
+ wxPoint(pos.x + m_thumb_size.x+1, pos.y - 0.5*text_height - 1);
+ else
+ text_pos = is_horizontal() ? wxPoint(pos.x - text_width - 1, pos.y - m_thumb_size.x - text_height) :
+ wxPoint(pos.x - text_width - 1 - m_thumb_size.x, pos.y - 0.5*text_height + 1);
+ dc.DrawText(label, text_pos);
+}
+
+void PrusaDoubleSlider::draw_thumb_item(wxDC& dc, const wxPoint& pos, const SelectedSlider& selection)
+{
+ wxCoord x_draw, y_draw;
+ if (selection == ssLower) {
+ if (is_horizontal()) {
+ x_draw = pos.x - m_thumb_size.x;
+ y_draw = pos.y - int(0.5*m_thumb_size.y);
+ }
+ else {
+ x_draw = pos.x - int(0.5*m_thumb_size.x);
+ y_draw = pos.y;
+ }
+ }
+ else{
+ if (is_horizontal()) {
+ x_draw = pos.x;
+ y_draw = pos.y - int(0.5*m_thumb_size.y);
+ }
+ else {
+ x_draw = pos.x - int(0.5*m_thumb_size.x);
+ y_draw = pos.y - m_thumb_size.y;
+ }
+ }
+ dc.DrawBitmap(selection == ssLower ? m_bmp_thumb_lower : m_bmp_thumb_higher, x_draw, y_draw);
+
+ // Update thumb rect
+ update_thumb_rect(x_draw, y_draw, selection);
+}
+
+void PrusaDoubleSlider::draw_thumb(wxDC& dc, const wxCoord& pos_coord, const SelectedSlider& selection)
+{
+ //calculate thumb position on slider line
+ int width, height;
+ get_size(&width, &height);
+ const wxPoint pos = is_horizontal() ? wxPoint(pos_coord, height*0.5) : wxPoint(0.5*width, pos_coord);
+
+ // Draw thumb
+ draw_thumb_item(dc, pos, selection);
+
+ // Draw info_line
+ draw_info_line_with_icon(dc, pos, selection);
+
+ // Draw thumb text
+ draw_thumb_text(dc, pos, selection);
+}
+
+void PrusaDoubleSlider::draw_thumbs(wxDC& dc, const wxCoord& lower_pos, const wxCoord& higher_pos)
+{
+ //calculate thumb position on slider line
+ int width, height;
+ get_size(&width, &height);
+ const wxPoint pos_l = is_horizontal() ? wxPoint(lower_pos, height*0.5) : wxPoint(0.5*width, lower_pos);
+ const wxPoint pos_h = is_horizontal() ? wxPoint(higher_pos, height*0.5) : wxPoint(0.5*width, higher_pos);
+
+ // Draw lower thumb
+ draw_thumb_item(dc, pos_l, ssLower);
+ // Draw lower info_line
+ draw_info_line_with_icon(dc, pos_l, ssLower);
+
+ // Draw higher thumb
+ draw_thumb_item(dc, pos_h, ssHigher);
+ // Draw higher info_line
+ draw_info_line_with_icon(dc, pos_h, ssHigher);
+ // Draw higher thumb text
+ draw_thumb_text(dc, pos_h, ssHigher);
+
+ // Draw lower thumb text
+ draw_thumb_text(dc, pos_l, ssLower);
+}
+
+void PrusaDoubleSlider::draw_ticks(wxDC& dc)
+{
+ dc.SetPen(DARK_GREY_PEN);
+ int height, width;
+ get_size(&width, &height);
+ const wxCoord mid = is_horizontal() ? 0.5*height : 0.5*width;
+ for (auto tick : m_ticks)
+ {
+ const wxCoord pos = get_position_from_value(tick);
+
+ is_horizontal() ? dc.DrawLine(pos, mid-14, pos, mid-9) :
+ dc.DrawLine(mid - 14, pos - 1, mid - 9, pos - 1);
+ is_horizontal() ? dc.DrawLine(pos, mid+14, pos, mid+9) :
+ dc.DrawLine(mid + 14, pos - 1, mid + 9, pos - 1);
+ }
+}
+
+void PrusaDoubleSlider::draw_one_layer_icon(wxDC& dc)
+{
+ wxBitmap* icon = m_is_one_layer ?
+ m_is_one_layer_icon_focesed ? &m_bmp_one_layer_lock_off : &m_bmp_one_layer_lock_on :
+ m_is_one_layer_icon_focesed ? &m_bmp_one_layer_unlock_off : &m_bmp_one_layer_unlock_on;
+
+ int width, height;
+ get_size(&width, &height);
+
+ wxCoord x_draw, y_draw;
+ is_horizontal() ? x_draw = width-2 : x_draw = 0.5*width - 0.5*m_lock_icon_dim;
+ is_horizontal() ? y_draw = 0.5*height - 0.5*m_lock_icon_dim : y_draw = height-2;
+
+ dc.DrawBitmap(*icon, x_draw, y_draw);
+
+ //update rect of the lock/unlock icon
+ m_rect_one_layer_icon = wxRect(x_draw, y_draw, m_lock_icon_dim, m_lock_icon_dim);
+}
+
+void PrusaDoubleSlider::update_thumb_rect(const wxCoord& begin_x, const wxCoord& begin_y, const SelectedSlider& selection)
+{
+ const wxRect& rect = wxRect(begin_x, begin_y, m_thumb_size.x, m_thumb_size.y);
+ if (selection == ssLower)
+ m_rect_lower_thumb = rect;
+ else
+ m_rect_higher_thumb = rect;
+}
+
+int PrusaDoubleSlider::get_value_from_position(const wxCoord x, const wxCoord y)
+{
+ const int height = get_size().y;
+ const double step = get_scroll_step();
+
+ if (is_horizontal())
+ return int(double(x - SLIDER_MARGIN) / step + 0.5);
+ else
+ return int(m_min_value + double(height - SLIDER_MARGIN - y) / step + 0.5);
+}
+
+void PrusaDoubleSlider::detect_selected_slider(const wxPoint& pt, const bool is_mouse_wheel /*= false*/)
+{
+ if (is_mouse_wheel)
+ {
+ if (is_horizontal()) {
+ m_selection = pt.x <= m_rect_lower_thumb.GetRight() ? ssLower :
+ pt.x >= m_rect_higher_thumb.GetLeft() ? ssHigher : ssUndef;
+ }
+ else {
+ m_selection = pt.y >= m_rect_lower_thumb.GetTop() ? ssLower :
+ pt.y <= m_rect_higher_thumb.GetBottom() ? ssHigher : ssUndef;
+ }
+ return;
+ }
+
+ m_selection = is_point_in_rect(pt, m_rect_lower_thumb) ? ssLower :
+ is_point_in_rect(pt, m_rect_higher_thumb) ? ssHigher : ssUndef;
+}
+
+bool PrusaDoubleSlider::is_point_in_rect(const wxPoint& pt, const wxRect& rect)
+{
+ if (rect.GetLeft() <= pt.x && pt.x <= rect.GetRight() &&
+ rect.GetTop() <= pt.y && pt.y <= rect.GetBottom())
+ return true;
+ return false;
+}
+
+void PrusaDoubleSlider::ChangeOneLayerLock()
+{
+ m_is_one_layer = !m_is_one_layer;
+ m_selection == ssLower ? correct_lower_value() : correct_higher_value();
+ if (!m_selection) m_selection = ssHigher;
+
+ Refresh();
+ Update();
+
+ wxCommandEvent e(wxEVT_SCROLL_CHANGED);
+ e.SetEventObject(this);
+ ProcessWindowEvent(e);
+}
+
+void PrusaDoubleSlider::OnLeftDown(wxMouseEvent& event)
+{
+ this->CaptureMouse();
+ wxClientDC dc(this);
+ wxPoint pos = event.GetLogicalPosition(dc);
+ if (is_point_in_rect(pos, m_rect_tick_action)) {
+ action_tick(taOnIcon);
+ return;
+ }
+
+ m_is_left_down = true;
+ if (is_point_in_rect(pos, m_rect_one_layer_icon)){
+ m_is_one_layer = !m_is_one_layer;
+ m_selection == ssLower ? correct_lower_value() : correct_higher_value();
+ if (!m_selection) m_selection = ssHigher;
+ }
+ else
+ detect_selected_slider(pos);
+
+ Refresh();
+ Update();
+ event.Skip();
+}
+
+void PrusaDoubleSlider::correct_lower_value()
+{
+ if (m_lower_value < m_min_value)
+ m_lower_value = m_min_value;
+ else if (m_lower_value > m_max_value)
+ m_lower_value = m_max_value;
+
+ if (m_lower_value >= m_higher_value && m_lower_value <= m_max_value || m_is_one_layer)
+ m_higher_value = m_lower_value;
+}
+
+void PrusaDoubleSlider::correct_higher_value()
+{
+ if (m_higher_value > m_max_value)
+ m_higher_value = m_max_value;
+ else if (m_higher_value < m_min_value)
+ m_higher_value = m_min_value;
+
+ if (m_higher_value <= m_lower_value && m_higher_value >= m_min_value || m_is_one_layer)
+ m_lower_value = m_higher_value;
+}
+
+void PrusaDoubleSlider::OnMotion(wxMouseEvent& event)
+{
+ const wxClientDC dc(this);
+ const wxPoint pos = event.GetLogicalPosition(dc);
+ m_is_one_layer_icon_focesed = is_point_in_rect(pos, m_rect_one_layer_icon);
+ if (!m_is_left_down && !m_is_one_layer){
+ m_is_action_icon_focesed = is_point_in_rect(pos, m_rect_tick_action);
+ }
+ else if (m_is_left_down || m_is_right_down){
+ if (m_selection == ssLower) {
+ m_lower_value = get_value_from_position(pos.x, pos.y);
+ correct_lower_value();
+ }
+ else if (m_selection == ssHigher) {
+ m_higher_value = get_value_from_position(pos.x, pos.y);
+ correct_higher_value();
+ }
+ }
+ Refresh();
+ Update();
+ event.Skip();
+
+ wxCommandEvent e(wxEVT_SCROLL_CHANGED);
+ e.SetEventObject(this);
+ ProcessWindowEvent(e);
+}
+
+void PrusaDoubleSlider::OnLeftUp(wxMouseEvent& event)
+{
+ this->ReleaseMouse();
+ m_is_left_down = false;
+ Refresh();
+ Update();
+ event.Skip();
+
+ wxCommandEvent e(wxEVT_SCROLL_CHANGED);
+ e.SetEventObject(this);
+ ProcessWindowEvent(e);
+}
+
+void PrusaDoubleSlider::enter_window(wxMouseEvent& event, const bool enter)
+{
+ m_is_focused = enter;
+ Refresh();
+ Update();
+ event.Skip();
+}
+
+// "condition" have to be true for:
+// - value increase (if wxSL_VERTICAL)
+// - value decrease (if wxSL_HORIZONTAL)
+void PrusaDoubleSlider::move_current_thumb(const bool condition)
+{
+ m_is_one_layer = wxGetKeyState(WXK_CONTROL);
+ int delta = condition ? -1 : 1;
+ if (is_horizontal())
+ delta *= -1;
+
+ if (m_selection == ssLower) {
+ m_lower_value -= delta;
+ correct_lower_value();
+ }
+ else if (m_selection == ssHigher) {
+ m_higher_value -= delta;
+ correct_higher_value();
+ }
+ Refresh();
+ Update();
+
+ wxCommandEvent e(wxEVT_SCROLL_CHANGED);
+ e.SetEventObject(this);
+ ProcessWindowEvent(e);
+}
+
+void PrusaDoubleSlider::action_tick(const TicksAction action)
+{
+ if (m_selection == ssUndef)
+ return;
+
+ const int tick = m_selection == ssLower ? m_lower_value : m_higher_value;
+
+ if (action == taOnIcon && !m_ticks.insert(tick).second)
+ m_ticks.erase(tick);
+ else {
+ const auto it = m_ticks.find(tick);
+ if (it == m_ticks.end() && action == taAdd)
+ m_ticks.insert(tick);
+ else if (it != m_ticks.end() && action == taDel)
+ m_ticks.erase(tick);
+ else
+ return;
+ }
+
+ Refresh();
+ Update();
+}
+
+void PrusaDoubleSlider::OnWheel(wxMouseEvent& event)
+{
+ wxClientDC dc(this);
+ wxPoint pos = event.GetLogicalPosition(dc);
+ detect_selected_slider(pos, true);
+
+ if (m_selection == ssUndef)
+ return;
+
+ move_current_thumb(event.GetWheelRotation() > 0);
+}
+
+void PrusaDoubleSlider::OnKeyDown(wxKeyEvent &event)
+{
+ const int key = event.GetKeyCode();
+ if (key == '+' || key == WXK_NUMPAD_ADD)
+ action_tick(taAdd);
+ else if (key == '-' || key == 390 || key == WXK_DELETE || key == WXK_BACK)
+ action_tick(taDel);
+ else if (is_horizontal())
+ {
+ if (key == WXK_LEFT || key == WXK_RIGHT)
+ move_current_thumb(key == WXK_LEFT);
+ else if (key == WXK_UP || key == WXK_DOWN){
+ m_selection = key == WXK_UP ? ssHigher : ssLower;
+ Refresh();
+ }
+ }
+ else {
+ if (key == WXK_LEFT || key == WXK_RIGHT) {
+ m_selection = key == WXK_LEFT ? ssHigher : ssLower;
+ Refresh();
+ }
+ else if (key == WXK_UP || key == WXK_DOWN)
+ move_current_thumb(key == WXK_UP);
+ }
+}
+
+void PrusaDoubleSlider::OnKeyUp(wxKeyEvent &event)
+{
+ if (event.GetKeyCode() == WXK_CONTROL)
+ m_is_one_layer = false;
+ Refresh();
+ Update();
+ event.Skip();
+}
+
+void PrusaDoubleSlider::OnRightDown(wxMouseEvent& event)
+{
+ this->CaptureMouse();
+ const wxClientDC dc(this);
+ detect_selected_slider(event.GetLogicalPosition(dc));
+ if (!m_selection)
+ return;
+
+ if (m_selection == ssLower)
+ m_higher_value = m_lower_value;
+ else
+ m_lower_value = m_higher_value;
+
+ m_is_right_down = m_is_one_layer = true;
+
+ Refresh();
+ Update();
+ event.Skip();
+}
+
+void PrusaDoubleSlider::OnRightUp(wxMouseEvent& event)
+{
+ this->ReleaseMouse();
+ m_is_right_down = m_is_one_layer = false;
+
+ Refresh();
+ Update();
+ event.Skip();
+}
+
+
+// ----------------------------------------------------------------------------
+// PrusaLockButton
+// ----------------------------------------------------------------------------
+
+PrusaLockButton::PrusaLockButton( wxWindow *parent,
+ wxWindowID id,
+ const wxPoint& pos /*= wxDefaultPosition*/,
+ const wxSize& size /*= wxDefaultSize*/):
+ wxButton(parent, id, wxEmptyString, pos, size, wxBU_EXACTFIT | wxNO_BORDER)
+{
+ m_bmp_lock_on = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("one_layer_lock_on.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_lock_off = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("one_layer_lock_off.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_unlock_on = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("one_layer_unlock_on.png")), wxBITMAP_TYPE_PNG);
+ m_bmp_unlock_off = wxBitmap(Slic3r::GUI::from_u8(Slic3r::var("one_layer_unlock_off.png")), wxBITMAP_TYPE_PNG);
+ m_lock_icon_dim = m_bmp_lock_on.GetSize().x;
+
+#ifdef __WXMSW__
+ SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+#endif // __WXMSW__
+ SetBitmap(m_bmp_unlock_on);
+
+ //button events
+ Bind(wxEVT_BUTTON, &PrusaLockButton::OnButton, this);
+ Bind(wxEVT_ENTER_WINDOW, &PrusaLockButton::OnEnterBtn, this);
+ Bind(wxEVT_LEAVE_WINDOW, &PrusaLockButton::OnLeaveBtn, this);
+}
+
+void PrusaLockButton::OnButton(wxCommandEvent& event)
+{
+ m_is_pushed = !m_is_pushed;
+ enter_button(true);
+
+ event.Skip();
+}
+
+void PrusaLockButton::enter_button(const bool enter)
+{
+ wxBitmap* icon = m_is_pushed ?
+ enter ? &m_bmp_lock_off : &m_bmp_lock_on :
+ enter ? &m_bmp_unlock_off : &m_bmp_unlock_on;
+ SetBitmap(*icon);
+
+ Refresh();
+ Update();
+}
+
+// ************************************** EXPERIMENTS ***************************************
+
+// *****************************************************************************
+
+
+
diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp
new file mode 100644
index 000000000..51c02035c
--- /dev/null
+++ b/src/slic3r/GUI/wxExtensions.hpp
@@ -0,0 +1,773 @@
+#ifndef slic3r_GUI_wxExtensions_hpp_
+#define slic3r_GUI_wxExtensions_hpp_
+
+#include <wx/checklst.h>
+#include <wx/combo.h>
+#include <wx/dataview.h>
+#include <wx/dc.h>
+#include <wx/collpane.h>
+#include <wx/wupdlock.h>
+#include <wx/button.h>
+#include <wx/slider.h>
+
+#include <vector>
+#include <set>
+
+class wxCheckListBoxComboPopup : public wxCheckListBox, public wxComboPopup
+{
+ static const unsigned int DefaultWidth;
+ static const unsigned int DefaultHeight;
+ static const unsigned int DefaultItemHeight;
+
+ wxString m_text;
+
+ // Events sent on mouseclick are quite complex. Function OnListBoxSelection is supposed to pass the event to the checkbox, which works fine on
+ // Win. On OSX and Linux the events are generated differently - clicking on the checkbox square generates the event twice (and the square
+ // therefore seems not to respond).
+ // This enum is meant to save current state of affairs, i.e., if the event forwarding is ok to do or not. It is only used on Linux
+ // and OSX by some #ifdefs. It also stores information whether OnListBoxSelection is supposed to change the checkbox status,
+ // or if it changed status on its own already (which happens when the square is clicked). More comments in OnCheckListBox(...)
+ // There indeed is a better solution, maybe making a custom event used for the event passing to distinguish the original and passed message
+ // and blocking one of them on OSX and Linux. Feel free to refactor, but carefully test on all platforms.
+ enum class OnCheckListBoxFunction{
+ FreeToProceed,
+ RefuseToProceed,
+ WasRefusedLastTime
+ } m_check_box_events_status = OnCheckListBoxFunction::FreeToProceed;
+
+
+public:
+ virtual bool Create(wxWindow* parent);
+ virtual wxWindow* GetControl();
+ virtual void SetStringValue(const wxString& value);
+ virtual wxString GetStringValue() const;
+ virtual wxSize GetAdjustedSize(int minWidth, int prefHeight, int maxHeight);
+
+ virtual void OnKeyEvent(wxKeyEvent& evt);
+
+ void OnCheckListBox(wxCommandEvent& evt);
+ void OnListBoxSelection(wxCommandEvent& evt);
+};
+
+
+// *** wxDataViewTreeCtrlComboBox ***
+
+class wxDataViewTreeCtrlComboPopup: public wxDataViewTreeCtrl, public wxComboPopup
+{
+ static const unsigned int DefaultWidth;
+ static const unsigned int DefaultHeight;
+ static const unsigned int DefaultItemHeight;
+
+ wxString m_text;
+ int m_cnt_open_items{0};
+
+public:
+ virtual bool Create(wxWindow* parent);
+ virtual wxWindow* GetControl() { return this; }
+ virtual void SetStringValue(const wxString& value) { m_text = value; }
+ virtual wxString GetStringValue() const { return m_text; }
+// virtual wxSize GetAdjustedSize(int minWidth, int prefHeight, int maxHeight);
+
+ virtual void OnKeyEvent(wxKeyEvent& evt);
+ void OnDataViewTreeCtrlSelection(wxCommandEvent& evt);
+ void SetItemsCnt(int cnt) { m_cnt_open_items = cnt; }
+};
+
+
+
+// *** PrusaCollapsiblePane ***
+// ----------------------------------------------------------------------------
+class PrusaCollapsiblePane : public wxCollapsiblePane
+{
+public:
+ PrusaCollapsiblePane() {}
+ PrusaCollapsiblePane(wxWindow *parent,
+ wxWindowID winid,
+ const wxString& label,
+ const wxPoint& pos = wxDefaultPosition,
+ const wxSize& size = wxDefaultSize,
+ long style = wxCP_DEFAULT_STYLE,
+ const wxValidator& val = wxDefaultValidator,
+ const wxString& name = wxCollapsiblePaneNameStr)
+ {
+ Create(parent, winid, label, pos, size, style, val, name);
+ }
+ ~PrusaCollapsiblePane() {}
+
+ void OnStateChange(const wxSize& sz); //override/hide of OnStateChange from wxCollapsiblePane
+ virtual bool Show(bool show = true) override {
+ wxCollapsiblePane::Show(show);
+ OnStateChange(GetBestSize());
+ return true;
+ }
+};
+
+
+// *** PrusaCollapsiblePaneMSW *** used only #ifdef __WXMSW__
+// ----------------------------------------------------------------------------
+#ifdef __WXMSW__
+class PrusaCollapsiblePaneMSW : public PrusaCollapsiblePane//wxCollapsiblePane
+{
+ wxButton* m_pDisclosureTriangleButton = nullptr;
+ wxBitmap m_bmp_close;
+ wxBitmap m_bmp_open;
+public:
+ PrusaCollapsiblePaneMSW() {}
+ PrusaCollapsiblePaneMSW( wxWindow *parent,
+ wxWindowID winid,
+ const wxString& label,
+ const wxPoint& pos = wxDefaultPosition,
+ const wxSize& size = wxDefaultSize,
+ long style = wxCP_DEFAULT_STYLE,
+ const wxValidator& val = wxDefaultValidator,
+ const wxString& name = wxCollapsiblePaneNameStr)
+ {
+ Create(parent, winid, label, pos, size, style, val, name);
+ }
+
+ ~PrusaCollapsiblePaneMSW() {}
+
+ bool Create(wxWindow *parent,
+ wxWindowID id,
+ const wxString& label,
+ const wxPoint& pos,
+ const wxSize& size,
+ long style,
+ const wxValidator& val,
+ const wxString& name);
+
+ void UpdateBtnBmp();
+ void SetLabel(const wxString &label) override;
+ bool Layout() override;
+ void Collapse(bool collapse) override;
+};
+#endif //__WXMSW__
+
+// *****************************************************************************
+
+// ----------------------------------------------------------------------------
+// PrusaDataViewBitmapText: helper class used by PrusaBitmapTextRenderer
+// ----------------------------------------------------------------------------
+
+class PrusaDataViewBitmapText : public wxObject
+{
+public:
+ PrusaDataViewBitmapText(const wxString &text = wxEmptyString,
+ const wxBitmap& bmp = wxNullBitmap) :
+ m_text(text), m_bmp(bmp)
+ { }
+
+ PrusaDataViewBitmapText(const PrusaDataViewBitmapText &other)
+ : wxObject(),
+ m_text(other.m_text),
+ m_bmp(other.m_bmp)
+ { }
+
+ void SetText(const wxString &text) { m_text = text; }
+ wxString GetText() const { return m_text; }
+ void SetBitmap(const wxIcon &icon) { m_bmp = icon; }
+ const wxBitmap &GetBitmap() const { return m_bmp; }
+
+ bool IsSameAs(const PrusaDataViewBitmapText& other) const {
+ return m_text == other.m_text && m_bmp.IsSameAs(other.m_bmp);
+ }
+
+ bool operator==(const PrusaDataViewBitmapText& other) const {
+ return IsSameAs(other);
+ }
+
+ bool operator!=(const PrusaDataViewBitmapText& other) const {
+ return !IsSameAs(other);
+ }
+
+private:
+ wxString m_text;
+ wxBitmap m_bmp;
+};
+DECLARE_VARIANT_OBJECT(PrusaDataViewBitmapText)
+
+
+// ----------------------------------------------------------------------------
+// PrusaObjectDataViewModelNode: a node inside PrusaObjectDataViewModel
+// ----------------------------------------------------------------------------
+
+class PrusaObjectDataViewModelNode;
+WX_DEFINE_ARRAY_PTR(PrusaObjectDataViewModelNode*, MyObjectTreeModelNodePtrArray);
+
+class PrusaObjectDataViewModelNode
+{
+ PrusaObjectDataViewModelNode* m_parent;
+ MyObjectTreeModelNodePtrArray m_children;
+ wxIcon m_empty_icon;
+ wxBitmap m_empty_bmp;
+ std::vector< std::string > m_opt_categories;
+public:
+ PrusaObjectDataViewModelNode(const wxString &name, const int instances_count=1) {
+ m_parent = NULL;
+ m_name = name;
+ m_copy = wxString::Format("%d", instances_count);
+ m_type = "object";
+ m_volume_id = -1;
+#ifdef __WXGTK__
+ // it's necessary on GTK because of control have to know if this item will be container
+ // in another case you couldn't to add subitem for this item
+ // it will be produce "segmentation fault"
+ m_container = true;
+#endif //__WXGTK__
+ set_object_action_icon();
+ }
+
+ PrusaObjectDataViewModelNode( PrusaObjectDataViewModelNode* parent,
+ const wxString& sub_obj_name,
+ const wxBitmap& bmp,
+ const wxString& extruder,
+ const int volume_id=-1) {
+ m_parent = parent;
+ m_name = sub_obj_name;
+ m_copy = wxEmptyString;
+ m_bmp = bmp;
+ m_type = "volume";
+ m_volume_id = volume_id;
+ m_extruder = extruder;
+#ifdef __WXGTK__
+ // it's necessary on GTK because of control have to know if this item will be container
+ // in another case you couldn't to add subitem for this item
+ // it will be produce "segmentation fault"
+ m_container = true;
+#endif //__WXGTK__
+ set_part_action_icon();
+ }
+
+ PrusaObjectDataViewModelNode( PrusaObjectDataViewModelNode* parent) :
+ m_parent(parent),
+ m_name("Settings to modified"),
+ m_copy(wxEmptyString),
+ m_type("settings"),
+ m_extruder(wxEmptyString) {}
+
+ ~PrusaObjectDataViewModelNode()
+ {
+ // free all our children nodes
+ size_t count = m_children.GetCount();
+ for (size_t i = 0; i < count; i++)
+ {
+ PrusaObjectDataViewModelNode *child = m_children[i];
+ delete child;
+ }
+ }
+
+ wxString m_name;
+ wxIcon& m_icon = m_empty_icon;
+ wxBitmap& m_bmp = m_empty_bmp;
+ wxString m_copy;
+ std::string m_type;
+ int m_volume_id = -2;
+ bool m_container = false;
+ wxString m_extruder = "default";
+ wxBitmap m_action_icon;
+
+ bool IsContainer() const
+ {
+ return m_container;
+ }
+
+ PrusaObjectDataViewModelNode* GetParent()
+ {
+ return m_parent;
+ }
+ MyObjectTreeModelNodePtrArray& GetChildren()
+ {
+ return m_children;
+ }
+ PrusaObjectDataViewModelNode* GetNthChild(unsigned int n)
+ {
+ return m_children.Item(n);
+ }
+ void Insert(PrusaObjectDataViewModelNode* child, unsigned int n)
+ {
+ if (!m_container)
+ m_container = true;
+ m_children.Insert(child, n);
+ }
+ void Append(PrusaObjectDataViewModelNode* child)
+ {
+ if (!m_container)
+ m_container = true;
+ m_children.Add(child);
+ }
+ void RemoveAllChildren()
+ {
+ if (GetChildCount() == 0)
+ return;
+ for (size_t id = GetChildCount() - 1; id >= 0; --id)
+ {
+ if (m_children.Item(id)->GetChildCount() > 0)
+ m_children[id]->RemoveAllChildren();
+ auto node = m_children[id];
+ m_children.RemoveAt(id);
+ delete node;
+ }
+ }
+
+ size_t GetChildCount() const
+ {
+ return m_children.GetCount();
+ }
+
+ bool SetValue(const wxVariant &variant, unsigned int col)
+ {
+ switch (col)
+ {
+ case 0:{
+ PrusaDataViewBitmapText data;
+ data << variant;
+ m_bmp = data.GetBitmap();
+ m_name = data.GetText();
+ return true;}
+ case 1:
+ m_copy = variant.GetString();
+ return true;
+ case 2:
+ m_extruder = variant.GetString();
+ return true;
+ case 3:
+ m_action_icon << variant;
+ return true;
+ default:
+ printf("MyObjectTreeModel::SetValue: wrong column");
+ }
+ return false;
+ }
+ void SetIcon(const wxIcon &icon)
+ {
+ m_icon = icon;
+ }
+
+ void SetBitmap(const wxBitmap &icon)
+ {
+ m_bmp = icon;
+ }
+
+ void SetType(const std::string& type){
+ m_type = type;
+ }
+ const std::string& GetType(){
+ return m_type;
+ }
+
+ void SetVolumeId(const int& volume_id){
+ m_volume_id = volume_id;
+ }
+ const int& GetVolumeId(){
+ return m_volume_id;
+ }
+
+ // use this function only for childrens
+ void AssignAllVal(PrusaObjectDataViewModelNode& from_node)
+ {
+ // ! Don't overwrite other values because of equality of this values for all children --
+ m_name = from_node.m_name;
+ m_icon = from_node.m_icon;
+ m_volume_id = from_node.m_volume_id;
+ m_extruder = from_node.m_extruder;
+ }
+
+ bool SwapChildrens(int frst_id, int scnd_id) {
+ if (GetChildCount() < 2 ||
+ frst_id < 0 || frst_id >= GetChildCount() ||
+ scnd_id < 0 || scnd_id >= GetChildCount())
+ return false;
+
+ PrusaObjectDataViewModelNode new_scnd = *GetNthChild(frst_id);
+ PrusaObjectDataViewModelNode new_frst = *GetNthChild(scnd_id);
+
+ new_scnd.m_volume_id = m_children.Item(scnd_id)->m_volume_id;
+ new_frst.m_volume_id = m_children.Item(frst_id)->m_volume_id;
+
+ m_children.Item(frst_id)->AssignAllVal(new_frst);
+ m_children.Item(scnd_id)->AssignAllVal(new_scnd);
+ return true;
+ }
+
+ // Set action icons for node
+ void set_object_action_icon();
+ void set_part_action_icon();
+ bool update_settings_digest(const std::vector<std::string>& categories);
+};
+
+// ----------------------------------------------------------------------------
+// PrusaObjectDataViewModel
+// ----------------------------------------------------------------------------
+
+class PrusaObjectDataViewModel :public wxDataViewModel
+{
+ std::vector<PrusaObjectDataViewModelNode*> m_objects;
+public:
+ PrusaObjectDataViewModel();
+ ~PrusaObjectDataViewModel();
+
+ wxDataViewItem Add(const wxString &name);
+ wxDataViewItem Add(const wxString &name, const int instances_count);
+ wxDataViewItem AddChild(const wxDataViewItem &parent_item,
+ const wxString &name,
+ const wxBitmap& icon,
+ const int extruder = 0,
+ const bool create_frst_child = true);
+ wxDataViewItem AddSettingsChild(const wxDataViewItem &parent_item);
+ wxDataViewItem Delete(const wxDataViewItem &item);
+ void DeleteAll();
+ void DeleteChildren(wxDataViewItem& parent);
+ wxDataViewItem GetItemById(int obj_idx);
+ wxDataViewItem GetItemByVolumeId(int obj_idx, int volume_idx);
+ int GetIdByItem(wxDataViewItem& item);
+ int GetVolumeIdByItem(const wxDataViewItem& item);
+ bool IsEmpty() { return m_objects.empty(); }
+
+ // helper method for wxLog
+
+ wxString GetName(const wxDataViewItem &item) const;
+ wxString GetCopy(const wxDataViewItem &item) const;
+ wxIcon& GetIcon(const wxDataViewItem &item) const;
+ wxBitmap& GetBitmap(const wxDataViewItem &item) const;
+
+ // helper methods to change the model
+
+ virtual unsigned int GetColumnCount() const override { return 3;}
+ virtual wxString GetColumnType(unsigned int col) const override{ return wxT("string"); }
+
+ virtual void GetValue(wxVariant &variant,
+ const wxDataViewItem &item, unsigned int col) const override;
+ virtual bool SetValue(const wxVariant &variant,
+ const wxDataViewItem &item, unsigned int col) override;
+ bool SetValue(const wxVariant &variant, const int item_idx, unsigned int col);
+
+ wxDataViewItem MoveChildUp(const wxDataViewItem &item);
+ wxDataViewItem MoveChildDown(const wxDataViewItem &item);
+ // For parent move child from cur_volume_id place to new_volume_id
+ // Remaining items will moved up/down accordingly
+ wxDataViewItem ReorganizeChildren(int cur_volume_id,
+ int new_volume_id,
+ const wxDataViewItem &parent);
+
+ virtual bool IsEnabled(const wxDataViewItem &item, unsigned int col) const override;
+
+ virtual wxDataViewItem GetParent(const wxDataViewItem &item) const override;
+ virtual bool IsContainer(const wxDataViewItem &item) const override;
+ virtual unsigned int GetChildren(const wxDataViewItem &parent,
+ wxDataViewItemArray &array) const override;
+
+ // Is the container just a header or an item with all columns
+ // In our case it is an item with all columns
+ virtual bool HasContainerColumns(const wxDataViewItem& WXUNUSED(item)) const override { return true; }
+
+ wxDataViewItem HasSettings(const wxDataViewItem &item) const;
+ bool IsSettingsItem(const wxDataViewItem &item) const;
+ void UpdateSettingsDigest(const wxDataViewItem &item, const std::vector<std::string>& categories);
+};
+
+// ----------------------------------------------------------------------------
+// PrusaBitmapTextRenderer
+// ----------------------------------------------------------------------------
+
+class PrusaBitmapTextRenderer : public wxDataViewCustomRenderer
+{
+public:
+ PrusaBitmapTextRenderer( wxDataViewCellMode mode = wxDATAVIEW_CELL_INERT,
+ int align = wxDVR_DEFAULT_ALIGNMENT):
+ wxDataViewCustomRenderer(wxT("wxObject"), mode, align) {}
+
+ bool SetValue(const wxVariant &value);
+ bool GetValue(wxVariant &value) const;
+
+ virtual bool Render(wxRect cell, wxDC *dc, int state);
+ virtual wxSize GetSize() const;
+
+ virtual bool HasEditorCtrl() const { return false; }
+
+private:
+// wxDataViewIconText m_value;
+ PrusaDataViewBitmapText m_value;
+};
+
+
+// ----------------------------------------------------------------------------
+// MyCustomRenderer
+// ----------------------------------------------------------------------------
+
+class MyCustomRenderer : public wxDataViewCustomRenderer
+{
+public:
+ // This renderer can be either activatable or editable, for demonstration
+ // purposes. In real programs, you should select whether the user should be
+ // able to activate or edit the cell and it doesn't make sense to switch
+ // between the two -- but this is just an example, so it doesn't stop us.
+ explicit MyCustomRenderer(wxDataViewCellMode mode)
+ : wxDataViewCustomRenderer("string", mode, wxALIGN_CENTER)
+ { }
+
+ virtual bool Render(wxRect rect, wxDC *dc, int state) override/*wxOVERRIDE*/
+ {
+ dc->SetBrush(*wxLIGHT_GREY_BRUSH);
+ dc->SetPen(*wxTRANSPARENT_PEN);
+
+ rect.Deflate(2);
+ dc->DrawRoundedRectangle(rect, 5);
+
+ RenderText(m_value,
+ 0, // no offset
+ wxRect(dc->GetTextExtent(m_value)).CentreIn(rect),
+ dc,
+ state);
+ return true;
+ }
+
+ virtual bool ActivateCell(const wxRect& WXUNUSED(cell),
+ wxDataViewModel *WXUNUSED(model),
+ const wxDataViewItem &WXUNUSED(item),
+ unsigned int WXUNUSED(col),
+ const wxMouseEvent *mouseEvent) override/*wxOVERRIDE*/
+ {
+ wxString position;
+ if (mouseEvent)
+ position = wxString::Format("via mouse at %d, %d", mouseEvent->m_x, mouseEvent->m_y);
+ else
+ position = "from keyboard";
+// wxLogMessage("MyCustomRenderer ActivateCell() %s", position);
+ return false;
+ }
+
+ virtual wxSize GetSize() const override/*wxOVERRIDE*/
+ {
+ return wxSize(60, 20);
+ }
+
+ virtual bool SetValue(const wxVariant &value) override/*wxOVERRIDE*/
+ {
+ m_value = value.GetString();
+ return true;
+ }
+
+ virtual bool GetValue(wxVariant &WXUNUSED(value)) const override/*wxOVERRIDE*/{ return true; }
+
+ virtual bool HasEditorCtrl() const override/*wxOVERRIDE*/{ return true; }
+
+ virtual wxWindow*
+ CreateEditorCtrl(wxWindow* parent,
+ wxRect labelRect,
+ const wxVariant& value) override/*wxOVERRIDE*/
+ {
+ wxTextCtrl* text = new wxTextCtrl(parent, wxID_ANY, value,
+ labelRect.GetPosition(),
+ labelRect.GetSize(),
+ wxTE_PROCESS_ENTER);
+ text->SetInsertionPointEnd();
+
+ return text;
+ }
+
+ virtual bool
+ GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value) override/*wxOVERRIDE*/
+ {
+ wxTextCtrl* text = wxDynamicCast(ctrl, wxTextCtrl);
+ if (!text)
+ return false;
+
+ value = text->GetValue();
+
+ return true;
+ }
+
+private:
+ wxString m_value;
+};
+
+
+// ----------------------------------------------------------------------------
+// PrusaDoubleSlider
+// ----------------------------------------------------------------------------
+
+enum SelectedSlider {
+ ssUndef,
+ ssLower,
+ ssHigher
+};
+enum TicksAction{
+ taOnIcon,
+ taAdd,
+ taDel
+};
+class PrusaDoubleSlider : public wxControl
+{
+public:
+ PrusaDoubleSlider(
+ wxWindow *parent,
+ wxWindowID id,
+ int lowerValue,
+ int higherValue,
+ int minValue,
+ int maxValue,
+ const wxPoint& pos = wxDefaultPosition,
+ const wxSize& size = wxDefaultSize,
+ long style = wxSL_VERTICAL,
+ const wxValidator& val = wxDefaultValidator,
+ const wxString& name = wxEmptyString);
+ ~PrusaDoubleSlider(){}
+
+ int GetLowerValue() const {
+ return m_lower_value;
+ }
+ int GetHigherValue() const {
+ return m_higher_value;
+ }
+ int GetActiveValue() const;
+ double GetLowerValueD() const { return get_double_value(ssLower); }
+ double GetHigherValueD() const { return get_double_value(ssHigher); }
+ wxSize DoGetBestSize() const override;
+ void SetLowerValue(const int lower_val);
+ void SetHigherValue(const int higher_val);
+ void SetMaxValue(const int max_value);
+ void SetKoefForLabels(const double koef) {
+ m_label_koef = koef;
+ }
+ void SetSliderValues(const std::vector<std::pair<int, double>>& values) {
+ m_values = values;
+ }
+ void ChangeOneLayerLock();
+
+ void OnPaint(wxPaintEvent& ){ render();}
+ void OnLeftDown(wxMouseEvent& event);
+ void OnMotion(wxMouseEvent& event);
+ void OnLeftUp(wxMouseEvent& event);
+ void OnEnterWin(wxMouseEvent& event){ enter_window(event, true); }
+ void OnLeaveWin(wxMouseEvent& event){ enter_window(event, false); }
+ void OnWheel(wxMouseEvent& event);
+ void OnKeyDown(wxKeyEvent &event);
+ void OnKeyUp(wxKeyEvent &event);
+ void OnRightDown(wxMouseEvent& event);
+ void OnRightUp(wxMouseEvent& event);
+
+protected:
+
+ void render();
+ void draw_focus_rect();
+ void draw_action_icon(wxDC& dc, const wxPoint pt_beg, const wxPoint pt_end);
+ void draw_scroll_line(wxDC& dc, const int lower_pos, const int higher_pos);
+ void draw_thumb(wxDC& dc, const wxCoord& pos_coord, const SelectedSlider& selection);
+ void draw_thumbs(wxDC& dc, const wxCoord& lower_pos, const wxCoord& higher_pos);
+ void draw_ticks(wxDC& dc);
+ void draw_one_layer_icon(wxDC& dc);
+ void draw_thumb_item(wxDC& dc, const wxPoint& pos, const SelectedSlider& selection);
+ void draw_info_line_with_icon(wxDC& dc, const wxPoint& pos, SelectedSlider selection);
+ void draw_thumb_text(wxDC& dc, const wxPoint& pos, const SelectedSlider& selection) const;
+
+ void update_thumb_rect(const wxCoord& begin_x, const wxCoord& begin_y, const SelectedSlider& selection);
+ void detect_selected_slider(const wxPoint& pt, const bool is_mouse_wheel = false);
+ void correct_lower_value();
+ void correct_higher_value();
+ void move_current_thumb(const bool condition);
+ void action_tick(const TicksAction action);
+ void enter_window(wxMouseEvent& event, const bool enter);
+
+ bool is_point_in_rect(const wxPoint& pt, const wxRect& rect);
+ bool is_horizontal() const { return m_style == wxSL_HORIZONTAL; }
+
+ double get_scroll_step();
+ wxString get_label(const SelectedSlider& selection) const;
+ void get_lower_and_higher_position(int& lower_pos, int& higher_pos);
+ int get_value_from_position(const wxCoord x, const wxCoord y);
+ wxCoord get_position_from_value(const int value);
+ wxSize get_size();
+ void get_size(int *w, int *h);
+ double get_double_value(const SelectedSlider& selection) const;
+
+private:
+ int m_min_value;
+ int m_max_value;
+ int m_lower_value;
+ int m_higher_value;
+ wxBitmap m_bmp_thumb_higher;
+ wxBitmap m_bmp_thumb_lower;
+ wxBitmap m_bmp_add_tick_on;
+ wxBitmap m_bmp_add_tick_off;
+ wxBitmap m_bmp_del_tick_on;
+ wxBitmap m_bmp_del_tick_off;
+ wxBitmap m_bmp_one_layer_lock_on;
+ wxBitmap m_bmp_one_layer_lock_off;
+ wxBitmap m_bmp_one_layer_unlock_on;
+ wxBitmap m_bmp_one_layer_unlock_off;
+ SelectedSlider m_selection;
+ bool m_is_left_down = false;
+ bool m_is_right_down = false;
+ bool m_is_one_layer = false;
+ bool m_is_focused = false;
+ bool m_is_action_icon_focesed = false;
+ bool m_is_one_layer_icon_focesed = false;
+
+ wxRect m_rect_lower_thumb;
+ wxRect m_rect_higher_thumb;
+ wxRect m_rect_tick_action;
+ wxRect m_rect_one_layer_icon;
+ wxSize m_thumb_size;
+ int m_tick_icon_dim;
+ int m_lock_icon_dim;
+ long m_style;
+ float m_label_koef = 1.0;
+
+// control's view variables
+ wxCoord SLIDER_MARGIN; // margin around slider
+
+ wxPen DARK_ORANGE_PEN;
+ wxPen ORANGE_PEN;
+ wxPen LIGHT_ORANGE_PEN;
+
+ wxPen DARK_GREY_PEN;
+ wxPen GREY_PEN;
+ wxPen LIGHT_GREY_PEN;
+
+ std::vector<wxPen*> line_pens;
+ std::vector<wxPen*> segm_pens;
+ std::set<int> m_ticks;
+ std::vector<std::pair<int,double>> m_values;
+};
+
+
+// ----------------------------------------------------------------------------
+// PrusaLockButton
+// ----------------------------------------------------------------------------
+
+class PrusaLockButton : public wxButton
+{
+public:
+ PrusaLockButton(
+ wxWindow *parent,
+ wxWindowID id,
+ const wxPoint& pos = wxDefaultPosition,
+ const wxSize& size = wxDefaultSize);
+ ~PrusaLockButton(){}
+
+ void OnButton(wxCommandEvent& event);
+ void OnEnterBtn(wxMouseEvent& event){ enter_button(true); event.Skip(); }
+ void OnLeaveBtn(wxMouseEvent& event){ enter_button(false); event.Skip(); }
+
+ bool IsLocked() const { return m_is_pushed; }
+
+protected:
+ void enter_button(const bool enter);
+
+private:
+ bool m_is_pushed = false;
+
+ wxBitmap m_bmp_lock_on;
+ wxBitmap m_bmp_lock_off;
+ wxBitmap m_bmp_unlock_on;
+ wxBitmap m_bmp_unlock_off;
+
+ int m_lock_icon_dim;
+};
+
+
+// ******************************* EXPERIMENTS **********************************************
+// ******************************************************************************************
+
+
+#endif // slic3r_GUI_wxExtensions_hpp_
diff --git a/src/slic3r/GUI/wxinit.h b/src/slic3r/GUI/wxinit.h
new file mode 100644
index 000000000..b55681b92
--- /dev/null
+++ b/src/slic3r/GUI/wxinit.h
@@ -0,0 +1,25 @@
+#ifndef slic3r_wxinit_hpp_
+#define slic3r_wxinit_hpp_
+
+#include <wx/wx.h>
+#include <wx/intl.h>
+#include <wx/html/htmlwin.h>
+
+// Perl redefines a _ macro, so we undef this one
+#undef _
+
+// We do want to use translation however, so define it as __ so we can do a find/replace
+// later when we no longer need to undef _
+#define __(s) wxGetTranslation((s))
+
+// legacy macros
+// https://wiki.wxwidgets.org/EventTypes_and_Event-Table_Macros
+#ifndef wxEVT_BUTTON
+#define wxEVT_BUTTON wxEVT_COMMAND_BUTTON_CLICKED
+#endif
+
+#ifndef wxEVT_HTML_LINK_CLICKED
+#define wxEVT_HTML_LINK_CLICKED wxEVT_COMMAND_HTML_LINK_CLICKED
+#endif
+
+#endif
diff --git a/src/slic3r/Utils/ASCIIFolding.cpp b/src/slic3r/Utils/ASCIIFolding.cpp
new file mode 100644
index 000000000..c61fe2902
--- /dev/null
+++ b/src/slic3r/Utils/ASCIIFolding.cpp
@@ -0,0 +1,1954 @@
+#include "ASCIIFolding.hpp"
+
+#include <stdio.h>
+#include <string.h>
+#include <locale>
+#include <boost/locale/encoding_utf.hpp>
+
+// Based on http://svn.apache.org/repos/asf/lucene/java/tags/lucene_solr_4_5_1/lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/ASCIIFoldingFilter.java
+template<typename OUTPUT_ITERATOR>
+static void fold_to_ascii(wchar_t c, OUTPUT_ITERATOR out)
+{
+ if (c < 0x080) {
+ *out = c;
+ } else {
+ switch (c) {
+ case L'\u00C0': // [LATIN CAPITAL LETTER A WITH GRAVE]
+ case L'\u00C1': // [LATIN CAPITAL LETTER A WITH ACUTE]
+ case L'\u00C2': // [LATIN CAPITAL LETTER A WITH CIRCUMFLEX]
+ case L'\u00C3': // [LATIN CAPITAL LETTER A WITH TILDE]
+ case L'\u00C4': // [LATIN CAPITAL LETTER A WITH DIAERESIS]
+ case L'\u00C5': // [LATIN CAPITAL LETTER A WITH RING ABOVE]
+ case L'\u0100': // [LATIN CAPITAL LETTER A WITH MACRON]
+ case L'\u0102': // [LATIN CAPITAL LETTER A WITH BREVE]
+ case L'\u0104': // [LATIN CAPITAL LETTER A WITH OGONEK]
+ case L'\u018F': // [LATIN CAPITAL LETTER SCHWA]
+ case L'\u01CD': // [LATIN CAPITAL LETTER A WITH CARON]
+ case L'\u01DE': // [LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON]
+ case L'\u01E0': // [LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON]
+ case L'\u01FA': // [LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE]
+ case L'\u0200': // [LATIN CAPITAL LETTER A WITH DOUBLE GRAVE]
+ case L'\u0202': // [LATIN CAPITAL LETTER A WITH INVERTED BREVE]
+ case L'\u0226': // [LATIN CAPITAL LETTER A WITH DOT ABOVE]
+ case L'\u023A': // [LATIN CAPITAL LETTER A WITH STROKE]
+ case L'\u1D00': // [LATIN LETTER SMALL CAPITAL A]
+ case L'\u1E00': // [LATIN CAPITAL LETTER A WITH RING BELOW]
+ case L'\u1EA0': // [LATIN CAPITAL LETTER A WITH DOT BELOW]
+ case L'\u1EA2': // [LATIN CAPITAL LETTER A WITH HOOK ABOVE]
+ case L'\u1EA4': // [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE]
+ case L'\u1EA6': // [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE]
+ case L'\u1EA8': // [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE]
+ case L'\u1EAA': // [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE]
+ case L'\u1EAC': // [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW]
+ case L'\u1EAE': // [LATIN CAPITAL LETTER A WITH BREVE AND ACUTE]
+ case L'\u1EB0': // [LATIN CAPITAL LETTER A WITH BREVE AND GRAVE]
+ case L'\u1EB2': // [LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE]
+ case L'\u1EB4': // [LATIN CAPITAL LETTER A WITH BREVE AND TILDE]
+ case L'\u1EB6': // [LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW]
+ case L'\u24B6': // [CIRCLED LATIN CAPITAL LETTER A]
+ case L'\uFF21': // [FULLWIDTH LATIN CAPITAL LETTER A]
+ *out = 'A';
+ break;
+ case L'\u00E0': // [LATIN SMALL LETTER A WITH GRAVE]
+ case L'\u00E1': // [LATIN SMALL LETTER A WITH ACUTE]
+ case L'\u00E2': // [LATIN SMALL LETTER A WITH CIRCUMFLEX]
+ case L'\u00E3': // [LATIN SMALL LETTER A WITH TILDE]
+ case L'\u00E4': // [LATIN SMALL LETTER A WITH DIAERESIS]
+ case L'\u00E5': // [LATIN SMALL LETTER A WITH RING ABOVE]
+ case L'\u0101': // [LATIN SMALL LETTER A WITH MACRON]
+ case L'\u0103': // [LATIN SMALL LETTER A WITH BREVE]
+ case L'\u0105': // [LATIN SMALL LETTER A WITH OGONEK]
+ case L'\u01CE': // [LATIN SMALL LETTER A WITH CARON]
+ case L'\u01DF': // [LATIN SMALL LETTER A WITH DIAERESIS AND MACRON]
+ case L'\u01E1': // [LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON]
+ case L'\u01FB': // [LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE]
+ case L'\u0201': // [LATIN SMALL LETTER A WITH DOUBLE GRAVE]
+ case L'\u0203': // [LATIN SMALL LETTER A WITH INVERTED BREVE]
+ case L'\u0227': // [LATIN SMALL LETTER A WITH DOT ABOVE]
+ case L'\u0250': // [LATIN SMALL LETTER TURNED A]
+ case L'\u0259': // [LATIN SMALL LETTER SCHWA]
+ case L'\u025A': // [LATIN SMALL LETTER SCHWA WITH HOOK]
+ case L'\u1D8F': // [LATIN SMALL LETTER A WITH RETROFLEX HOOK]
+ case L'\u1D95': // [LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK]
+ case L'\u1E01': // [LATIN SMALL LETTER A WITH RING BELOW]
+ case L'\u1E9A': // [LATIN SMALL LETTER A WITH RIGHT HALF RING]
+ case L'\u1EA1': // [LATIN SMALL LETTER A WITH DOT BELOW]
+ case L'\u1EA3': // [LATIN SMALL LETTER A WITH HOOK ABOVE]
+ case L'\u1EA5': // [LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE]
+ case L'\u1EA7': // [LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE]
+ case L'\u1EA9': // [LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE]
+ case L'\u1EAB': // [LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE]
+ case L'\u1EAD': // [LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW]
+ case L'\u1EAF': // [LATIN SMALL LETTER A WITH BREVE AND ACUTE]
+ case L'\u1EB1': // [LATIN SMALL LETTER A WITH BREVE AND GRAVE]
+ case L'\u1EB3': // [LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE]
+ case L'\u1EB5': // [LATIN SMALL LETTER A WITH BREVE AND TILDE]
+ case L'\u1EB7': // [LATIN SMALL LETTER A WITH BREVE AND DOT BELOW]
+ case L'\u2090': // [LATIN SUBSCRIPT SMALL LETTER A]
+ case L'\u2094': // [LATIN SUBSCRIPT SMALL LETTER SCHWA]
+ case L'\u24D0': // [CIRCLED LATIN SMALL LETTER A]
+ case L'\u2C65': // [LATIN SMALL LETTER A WITH STROKE]
+ case L'\u2C6F': // [LATIN CAPITAL LETTER TURNED A]
+ case L'\uFF41': // [FULLWIDTH LATIN SMALL LETTER A]
+ *out = 'a';
+ break;
+ case L'\uA732': // [LATIN CAPITAL LETTER AA]
+ *out = 'A';
+ *out = 'A';
+ break;
+ case L'\u00C6': // [LATIN CAPITAL LETTER AE]
+ case L'\u01E2': // [LATIN CAPITAL LETTER AE WITH MACRON]
+ case L'\u01FC': // [LATIN CAPITAL LETTER AE WITH ACUTE]
+ case L'\u1D01': // [LATIN LETTER SMALL CAPITAL AE]
+ *out = 'A';
+ *out = 'E';
+ break;
+ case L'\uA734': // [LATIN CAPITAL LETTER AO]
+ *out = 'A';
+ *out = 'O';
+ break;
+ case L'\uA736': // [LATIN CAPITAL LETTER AU]
+ *out = 'A';
+ *out = 'U';
+ break;
+ case L'\uA738': // [LATIN CAPITAL LETTER AV]
+ case L'\uA73A': // [LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR]
+ *out = 'A';
+ *out = 'V';
+ break;
+ case L'\uA73C': // [LATIN CAPITAL LETTER AY]
+ *out = 'A';
+ *out = 'Y';
+ break;
+ case L'\u249C': // [PARENTHESIZED LATIN SMALL LETTER A]
+ *out = '(';
+ *out = 'a';
+ *out = ')';
+ break;
+ case L'\uA733': // [LATIN SMALL LETTER AA]
+ *out = 'a';
+ *out = 'a';
+ break;
+ case L'\u00E6': // [LATIN SMALL LETTER AE]
+ case L'\u01E3': // [LATIN SMALL LETTER AE WITH MACRON]
+ case L'\u01FD': // [LATIN SMALL LETTER AE WITH ACUTE]
+ case L'\u1D02': // [LATIN SMALL LETTER TURNED AE]
+ *out = 'a';
+ *out = 'e';
+ break;
+ case L'\uA735': // [LATIN SMALL LETTER AO]
+ *out = 'a';
+ *out = 'o';
+ break;
+ case L'\uA737': // [LATIN SMALL LETTER AU]
+ *out = 'a';
+ *out = 'u';
+ break;
+ case L'\uA739': // [LATIN SMALL LETTER AV]
+ case L'\uA73B': // [LATIN SMALL LETTER AV WITH HORIZONTAL BAR]
+ *out = 'a';
+ *out = 'v';
+ break;
+ case L'\uA73D': // [LATIN SMALL LETTER AY]
+ *out = 'a';
+ *out = 'y';
+ break;
+ case L'\u0181': // [LATIN CAPITAL LETTER B WITH HOOK]
+ case L'\u0182': // [LATIN CAPITAL LETTER B WITH TOPBAR]
+ case L'\u0243': // [LATIN CAPITAL LETTER B WITH STROKE]
+ case L'\u0299': // [LATIN LETTER SMALL CAPITAL B]
+ case L'\u1D03': // [LATIN LETTER SMALL CAPITAL BARRED B]
+ case L'\u1E02': // [LATIN CAPITAL LETTER B WITH DOT ABOVE]
+ case L'\u1E04': // [LATIN CAPITAL LETTER B WITH DOT BELOW]
+ case L'\u1E06': // [LATIN CAPITAL LETTER B WITH LINE BELOW]
+ case L'\u24B7': // [CIRCLED LATIN CAPITAL LETTER B]
+ case L'\uFF22': // [FULLWIDTH LATIN CAPITAL LETTER B]
+ *out = 'B';
+ break;
+ case L'\u0180': // [LATIN SMALL LETTER B WITH STROKE]
+ case L'\u0183': // [LATIN SMALL LETTER B WITH TOPBAR]
+ case L'\u0253': // [LATIN SMALL LETTER B WITH HOOK]
+ case L'\u1D6C': // [LATIN SMALL LETTER B WITH MIDDLE TILDE]
+ case L'\u1D80': // [LATIN SMALL LETTER B WITH PALATAL HOOK]
+ case L'\u1E03': // [LATIN SMALL LETTER B WITH DOT ABOVE]
+ case L'\u1E05': // [LATIN SMALL LETTER B WITH DOT BELOW]
+ case L'\u1E07': // [LATIN SMALL LETTER B WITH LINE BELOW]
+ case L'\u24D1': // [CIRCLED LATIN SMALL LETTER B]
+ case L'\uFF42': // [FULLWIDTH LATIN SMALL LETTER B]
+ *out = 'b';
+ break;
+ case L'\u249D': // [PARENTHESIZED LATIN SMALL LETTER B]
+ *out = '(';
+ *out = 'b';
+ *out = ')';
+ break;
+ case L'\u00C7': // [LATIN CAPITAL LETTER C WITH CEDILLA]
+ case L'\u0106': // [LATIN CAPITAL LETTER C WITH ACUTE]
+ case L'\u0108': // [LATIN CAPITAL LETTER C WITH CIRCUMFLEX]
+ case L'\u010A': // [LATIN CAPITAL LETTER C WITH DOT ABOVE]
+ case L'\u010C': // [LATIN CAPITAL LETTER C WITH CARON]
+ case L'\u0187': // [LATIN CAPITAL LETTER C WITH HOOK]
+ case L'\u023B': // [LATIN CAPITAL LETTER C WITH STROKE]
+ case L'\u0297': // [LATIN LETTER STRETCHED C]
+ case L'\u1D04': // [LATIN LETTER SMALL CAPITAL C]
+ case L'\u1E08': // [LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE]
+ case L'\u24B8': // [CIRCLED LATIN CAPITAL LETTER C]
+ case L'\uFF23': // [FULLWIDTH LATIN CAPITAL LETTER C]
+ *out = 'C';
+ break;
+ case L'\u00E7': // [LATIN SMALL LETTER C WITH CEDILLA]
+ case L'\u0107': // [LATIN SMALL LETTER C WITH ACUTE]
+ case L'\u0109': // [LATIN SMALL LETTER C WITH CIRCUMFLEX]
+ case L'\u010B': // [LATIN SMALL LETTER C WITH DOT ABOVE]
+ case L'\u010D': // [LATIN SMALL LETTER C WITH CARON]
+ case L'\u0188': // [LATIN SMALL LETTER C WITH HOOK]
+ case L'\u023C': // [LATIN SMALL LETTER C WITH STROKE]
+ case L'\u0255': // [LATIN SMALL LETTER C WITH CURL]
+ case L'\u1E09': // [LATIN SMALL LETTER C WITH CEDILLA AND ACUTE]
+ case L'\u2184': // [LATIN SMALL LETTER REVERSED C]
+ case L'\u24D2': // [CIRCLED LATIN SMALL LETTER C]
+ case L'\uA73E': // [LATIN CAPITAL LETTER REVERSED C WITH DOT]
+ case L'\uA73F': // [LATIN SMALL LETTER REVERSED C WITH DOT]
+ case L'\uFF43': // [FULLWIDTH LATIN SMALL LETTER C]
+ *out = 'c';
+ break;
+ case L'\u249E': // [PARENTHESIZED LATIN SMALL LETTER C]
+ *out = '(';
+ *out = 'c';
+ *out = ')';
+ break;
+ case L'\u00D0': // [LATIN CAPITAL LETTER ETH]
+ case L'\u010E': // [LATIN CAPITAL LETTER D WITH CARON]
+ case L'\u0110': // [LATIN CAPITAL LETTER D WITH STROKE]
+ case L'\u0189': // [LATIN CAPITAL LETTER AFRICAN D]
+ case L'\u018A': // [LATIN CAPITAL LETTER D WITH HOOK]
+ case L'\u018B': // [LATIN CAPITAL LETTER D WITH TOPBAR]
+ case L'\u1D05': // [LATIN LETTER SMALL CAPITAL D]
+ case L'\u1D06': // [LATIN LETTER SMALL CAPITAL ETH]
+ case L'\u1E0A': // [LATIN CAPITAL LETTER D WITH DOT ABOVE]
+ case L'\u1E0C': // [LATIN CAPITAL LETTER D WITH DOT BELOW]
+ case L'\u1E0E': // [LATIN CAPITAL LETTER D WITH LINE BELOW]
+ case L'\u1E10': // [LATIN CAPITAL LETTER D WITH CEDILLA]
+ case L'\u1E12': // [LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW]
+ case L'\u24B9': // [CIRCLED LATIN CAPITAL LETTER D]
+ case L'\uA779': // [LATIN CAPITAL LETTER INSULAR D]
+ case L'\uFF24': // [FULLWIDTH LATIN CAPITAL LETTER D]
+ *out = 'D';
+ break;
+ case L'\u00F0': // [LATIN SMALL LETTER ETH]
+ case L'\u010F': // [LATIN SMALL LETTER D WITH CARON]
+ case L'\u0111': // [LATIN SMALL LETTER D WITH STROKE]
+ case L'\u018C': // [LATIN SMALL LETTER D WITH TOPBAR]
+ case L'\u0221': // [LATIN SMALL LETTER D WITH CURL]
+ case L'\u0256': // [LATIN SMALL LETTER D WITH TAIL]
+ case L'\u0257': // [LATIN SMALL LETTER D WITH HOOK]
+ case L'\u1D6D': // [LATIN SMALL LETTER D WITH MIDDLE TILDE]
+ case L'\u1D81': // [LATIN SMALL LETTER D WITH PALATAL HOOK]
+ case L'\u1D91': // [LATIN SMALL LETTER D WITH HOOK AND TAIL]
+ case L'\u1E0B': // [LATIN SMALL LETTER D WITH DOT ABOVE]
+ case L'\u1E0D': // [LATIN SMALL LETTER D WITH DOT BELOW]
+ case L'\u1E0F': // [LATIN SMALL LETTER D WITH LINE BELOW]
+ case L'\u1E11': // [LATIN SMALL LETTER D WITH CEDILLA]
+ case L'\u1E13': // [LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW]
+ case L'\u24D3': // [CIRCLED LATIN SMALL LETTER D]
+ case L'\uA77A': // [LATIN SMALL LETTER INSULAR D]
+ case L'\uFF44': // [FULLWIDTH LATIN SMALL LETTER D]
+ *out = 'd';
+ break;
+ case L'\u01C4': // [LATIN CAPITAL LETTER DZ WITH CARON]
+ case L'\u01F1': // [LATIN CAPITAL LETTER DZ]
+ *out = 'D';
+ *out = 'Z';
+ break;
+ case L'\u01C5': // [LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON]
+ case L'\u01F2': // [LATIN CAPITAL LETTER D WITH SMALL LETTER Z]
+ *out = 'D';
+ *out = 'z';
+ break;
+ case L'\u249F': // [PARENTHESIZED LATIN SMALL LETTER D]
+ *out = '(';
+ *out = 'd';
+ *out = ')';
+ break;
+ case L'\u0238': // [LATIN SMALL LETTER DB DIGRAPH]
+ *out = 'd';
+ *out = 'b';
+ break;
+ case L'\u01C6': // [LATIN SMALL LETTER DZ WITH CARON]
+ case L'\u01F3': // [LATIN SMALL LETTER DZ]
+ case L'\u02A3': // [LATIN SMALL LETTER DZ DIGRAPH]
+ case L'\u02A5': // [LATIN SMALL LETTER DZ DIGRAPH WITH CURL]
+ *out = 'd';
+ *out = 'z';
+ break;
+ case L'\u00C8': // [LATIN CAPITAL LETTER E WITH GRAVE]
+ case L'\u00C9': // [LATIN CAPITAL LETTER E WITH ACUTE]
+ case L'\u00CA': // [LATIN CAPITAL LETTER E WITH CIRCUMFLEX]
+ case L'\u00CB': // [LATIN CAPITAL LETTER E WITH DIAERESIS]
+ case L'\u0112': // [LATIN CAPITAL LETTER E WITH MACRON]
+ case L'\u0114': // [LATIN CAPITAL LETTER E WITH BREVE]
+ case L'\u0116': // [LATIN CAPITAL LETTER E WITH DOT ABOVE]
+ case L'\u0118': // [LATIN CAPITAL LETTER E WITH OGONEK]
+ case L'\u011A': // [LATIN CAPITAL LETTER E WITH CARON]
+ case L'\u018E': // [LATIN CAPITAL LETTER REVERSED E]
+ case L'\u0190': // [LATIN CAPITAL LETTER OPEN E]
+ case L'\u0204': // [LATIN CAPITAL LETTER E WITH DOUBLE GRAVE]
+ case L'\u0206': // [LATIN CAPITAL LETTER E WITH INVERTED BREVE]
+ case L'\u0228': // [LATIN CAPITAL LETTER E WITH CEDILLA]
+ case L'\u0246': // [LATIN CAPITAL LETTER E WITH STROKE]
+ case L'\u1D07': // [LATIN LETTER SMALL CAPITAL E]
+ case L'\u1E14': // [LATIN CAPITAL LETTER E WITH MACRON AND GRAVE]
+ case L'\u1E16': // [LATIN CAPITAL LETTER E WITH MACRON AND ACUTE]
+ case L'\u1E18': // [LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW]
+ case L'\u1E1A': // [LATIN CAPITAL LETTER E WITH TILDE BELOW]
+ case L'\u1E1C': // [LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE]
+ case L'\u1EB8': // [LATIN CAPITAL LETTER E WITH DOT BELOW]
+ case L'\u1EBA': // [LATIN CAPITAL LETTER E WITH HOOK ABOVE]
+ case L'\u1EBC': // [LATIN CAPITAL LETTER E WITH TILDE]
+ case L'\u1EBE': // [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE]
+ case L'\u1EC0': // [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE]
+ case L'\u1EC2': // [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE]
+ case L'\u1EC4': // [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE]
+ case L'\u1EC6': // [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW]
+ case L'\u24BA': // [CIRCLED LATIN CAPITAL LETTER E]
+ case L'\u2C7B': // [LATIN LETTER SMALL CAPITAL TURNED E]
+ case L'\uFF25': // [FULLWIDTH LATIN CAPITAL LETTER E]
+ *out = 'E';
+ break;
+ case L'\u00E8': // [LATIN SMALL LETTER E WITH GRAVE]
+ case L'\u00E9': // [LATIN SMALL LETTER E WITH ACUTE]
+ case L'\u00EA': // [LATIN SMALL LETTER E WITH CIRCUMFLEX]
+ case L'\u00EB': // [LATIN SMALL LETTER E WITH DIAERESIS]
+ case L'\u0113': // [LATIN SMALL LETTER E WITH MACRON]
+ case L'\u0115': // [LATIN SMALL LETTER E WITH BREVE]
+ case L'\u0117': // [LATIN SMALL LETTER E WITH DOT ABOVE]
+ case L'\u0119': // [LATIN SMALL LETTER E WITH OGONEK]
+ case L'\u011B': // [LATIN SMALL LETTER E WITH CARON]
+ case L'\u01DD': // [LATIN SMALL LETTER TURNED E]
+ case L'\u0205': // [LATIN SMALL LETTER E WITH DOUBLE GRAVE]
+ case L'\u0207': // [LATIN SMALL LETTER E WITH INVERTED BREVE]
+ case L'\u0229': // [LATIN SMALL LETTER E WITH CEDILLA]
+ case L'\u0247': // [LATIN SMALL LETTER E WITH STROKE]
+ case L'\u0258': // [LATIN SMALL LETTER REVERSED E]
+ case L'\u025B': // [LATIN SMALL LETTER OPEN E]
+ case L'\u025C': // [LATIN SMALL LETTER REVERSED OPEN E]
+ case L'\u025D': // [LATIN SMALL LETTER REVERSED OPEN E WITH HOOK]
+ case L'\u025E': // [LATIN SMALL LETTER CLOSED REVERSED OPEN E]
+ case L'\u029A': // [LATIN SMALL LETTER CLOSED OPEN E]
+ case L'\u1D08': // [LATIN SMALL LETTER TURNED OPEN E]
+ case L'\u1D92': // [LATIN SMALL LETTER E WITH RETROFLEX HOOK]
+ case L'\u1D93': // [LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK]
+ case L'\u1D94': // [LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK]
+ case L'\u1E15': // [LATIN SMALL LETTER E WITH MACRON AND GRAVE]
+ case L'\u1E17': // [LATIN SMALL LETTER E WITH MACRON AND ACUTE]
+ case L'\u1E19': // [LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW]
+ case L'\u1E1B': // [LATIN SMALL LETTER E WITH TILDE BELOW]
+ case L'\u1E1D': // [LATIN SMALL LETTER E WITH CEDILLA AND BREVE]
+ case L'\u1EB9': // [LATIN SMALL LETTER E WITH DOT BELOW]
+ case L'\u1EBB': // [LATIN SMALL LETTER E WITH HOOK ABOVE]
+ case L'\u1EBD': // [LATIN SMALL LETTER E WITH TILDE]
+ case L'\u1EBF': // [LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE]
+ case L'\u1EC1': // [LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE]
+ case L'\u1EC3': // [LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE]
+ case L'\u1EC5': // [LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE]
+ case L'\u1EC7': // [LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW]
+ case L'\u2091': // [LATIN SUBSCRIPT SMALL LETTER E]
+ case L'\u24D4': // [CIRCLED LATIN SMALL LETTER E]
+ case L'\u2C78': // [LATIN SMALL LETTER E WITH NOTCH]
+ case L'\uFF45': // [FULLWIDTH LATIN SMALL LETTER E]
+ *out = 'e';
+ break;
+ case L'\u24A0': // [PARENTHESIZED LATIN SMALL LETTER E]
+ *out = '(';
+ *out = 'e';
+ *out = ')';
+ break;
+ case L'\u0191': // [LATIN CAPITAL LETTER F WITH HOOK]
+ case L'\u1E1E': // [LATIN CAPITAL LETTER F WITH DOT ABOVE]
+ case L'\u24BB': // [CIRCLED LATIN CAPITAL LETTER F]
+ case L'\uA730': // [LATIN LETTER SMALL CAPITAL F]
+ case L'\uA77B': // [LATIN CAPITAL LETTER INSULAR F]
+ case L'\uA7FB': // [LATIN EPIGRAPHIC LETTER REVERSED F]
+ case L'\uFF26': // [FULLWIDTH LATIN CAPITAL LETTER F]
+ *out = 'F';
+ break;
+ case L'\u0192': // [LATIN SMALL LETTER F WITH HOOK]
+ case L'\u1D6E': // [LATIN SMALL LETTER F WITH MIDDLE TILDE]
+ case L'\u1D82': // [LATIN SMALL LETTER F WITH PALATAL HOOK]
+ case L'\u1E1F': // [LATIN SMALL LETTER F WITH DOT ABOVE]
+ case L'\u1E9B': // [LATIN SMALL LETTER LONG S WITH DOT ABOVE]
+ case L'\u24D5': // [CIRCLED LATIN SMALL LETTER F]
+ case L'\uA77C': // [LATIN SMALL LETTER INSULAR F]
+ case L'\uFF46': // [FULLWIDTH LATIN SMALL LETTER F]
+ *out = 'f';
+ break;
+ case L'\u24A1': // [PARENTHESIZED LATIN SMALL LETTER F]
+ *out = '(';
+ *out = 'f';
+ *out = ')';
+ break;
+ case L'\uFB00': // [LATIN SMALL LIGATURE FF]
+ *out = 'f';
+ *out = 'f';
+ break;
+ case L'\uFB03': // [LATIN SMALL LIGATURE FFI]
+ *out = 'f';
+ *out = 'f';
+ *out = 'i';
+ break;
+ case L'\uFB04': // [LATIN SMALL LIGATURE FFL]
+ *out = 'f';
+ *out = 'f';
+ *out = 'l';
+ break;
+ case L'\uFB01': // [LATIN SMALL LIGATURE FI]
+ *out = 'f';
+ *out = 'i';
+ break;
+ case L'\uFB02': // [LATIN SMALL LIGATURE FL]
+ *out = 'f';
+ *out = 'l';
+ break;
+ case L'\u011C': // [LATIN CAPITAL LETTER G WITH CIRCUMFLEX]
+ case L'\u011E': // [LATIN CAPITAL LETTER G WITH BREVE]
+ case L'\u0120': // [LATIN CAPITAL LETTER G WITH DOT ABOVE]
+ case L'\u0122': // [LATIN CAPITAL LETTER G WITH CEDILLA]
+ case L'\u0193': // [LATIN CAPITAL LETTER G WITH HOOK]
+ case L'\u01E4': // [LATIN CAPITAL LETTER G WITH STROKE]
+ case L'\u01E5': // [LATIN SMALL LETTER G WITH STROKE]
+ case L'\u01E6': // [LATIN CAPITAL LETTER G WITH CARON]
+ case L'\u01E7': // [LATIN SMALL LETTER G WITH CARON]
+ case L'\u01F4': // [LATIN CAPITAL LETTER G WITH ACUTE]
+ case L'\u0262': // [LATIN LETTER SMALL CAPITAL G]
+ case L'\u029B': // [LATIN LETTER SMALL CAPITAL G WITH HOOK]
+ case L'\u1E20': // [LATIN CAPITAL LETTER G WITH MACRON]
+ case L'\u24BC': // [CIRCLED LATIN CAPITAL LETTER G]
+ case L'\uA77D': // [LATIN CAPITAL LETTER INSULAR G]
+ case L'\uA77E': // [LATIN CAPITAL LETTER TURNED INSULAR G]
+ case L'\uFF27': // [FULLWIDTH LATIN CAPITAL LETTER G]
+ *out = 'G';
+ break;
+ case L'\u011D': // [LATIN SMALL LETTER G WITH CIRCUMFLEX]
+ case L'\u011F': // [LATIN SMALL LETTER G WITH BREVE]
+ case L'\u0121': // [LATIN SMALL LETTER G WITH DOT ABOVE]
+ case L'\u0123': // [LATIN SMALL LETTER G WITH CEDILLA]
+ case L'\u01F5': // [LATIN SMALL LETTER G WITH ACUTE]
+ case L'\u0260': // [LATIN SMALL LETTER G WITH HOOK]
+ case L'\u0261': // [LATIN SMALL LETTER SCRIPT G]
+ case L'\u1D77': // [LATIN SMALL LETTER TURNED G]
+ case L'\u1D79': // [LATIN SMALL LETTER INSULAR G]
+ case L'\u1D83': // [LATIN SMALL LETTER G WITH PALATAL HOOK]
+ case L'\u1E21': // [LATIN SMALL LETTER G WITH MACRON]
+ case L'\u24D6': // [CIRCLED LATIN SMALL LETTER G]
+ case L'\uA77F': // [LATIN SMALL LETTER TURNED INSULAR G]
+ case L'\uFF47': // [FULLWIDTH LATIN SMALL LETTER G]
+ *out = 'g';
+ break;
+ case L'\u24A2': // [PARENTHESIZED LATIN SMALL LETTER G]
+ *out = '(';
+ *out = 'g';
+ *out = ')';
+ break;
+ case L'\u0124': // [LATIN CAPITAL LETTER H WITH CIRCUMFLEX]
+ case L'\u0126': // [LATIN CAPITAL LETTER H WITH STROKE]
+ case L'\u021E': // [LATIN CAPITAL LETTER H WITH CARON]
+ case L'\u029C': // [LATIN LETTER SMALL CAPITAL H]
+ case L'\u1E22': // [LATIN CAPITAL LETTER H WITH DOT ABOVE]
+ case L'\u1E24': // [LATIN CAPITAL LETTER H WITH DOT BELOW]
+ case L'\u1E26': // [LATIN CAPITAL LETTER H WITH DIAERESIS]
+ case L'\u1E28': // [LATIN CAPITAL LETTER H WITH CEDILLA]
+ case L'\u1E2A': // [LATIN CAPITAL LETTER H WITH BREVE BELOW]
+ case L'\u24BD': // [CIRCLED LATIN CAPITAL LETTER H]
+ case L'\u2C67': // [LATIN CAPITAL LETTER H WITH DESCENDER]
+ case L'\u2C75': // [LATIN CAPITAL LETTER HALF H]
+ case L'\uFF28': // [FULLWIDTH LATIN CAPITAL LETTER H]
+ *out = 'H';
+ break;
+ case L'\u0125': // [LATIN SMALL LETTER H WITH CIRCUMFLEX]
+ case L'\u0127': // [LATIN SMALL LETTER H WITH STROKE]
+ case L'\u021F': // [LATIN SMALL LETTER H WITH CARON]
+ case L'\u0265': // [LATIN SMALL LETTER TURNED H]
+ case L'\u0266': // [LATIN SMALL LETTER H WITH HOOK]
+ case L'\u02AE': // [LATIN SMALL LETTER TURNED H WITH FISHHOOK]
+ case L'\u02AF': // [LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL]
+ case L'\u1E23': // [LATIN SMALL LETTER H WITH DOT ABOVE]
+ case L'\u1E25': // [LATIN SMALL LETTER H WITH DOT BELOW]
+ case L'\u1E27': // [LATIN SMALL LETTER H WITH DIAERESIS]
+ case L'\u1E29': // [LATIN SMALL LETTER H WITH CEDILLA]
+ case L'\u1E2B': // [LATIN SMALL LETTER H WITH BREVE BELOW]
+ case L'\u1E96': // [LATIN SMALL LETTER H WITH LINE BELOW]
+ case L'\u24D7': // [CIRCLED LATIN SMALL LETTER H]
+ case L'\u2C68': // [LATIN SMALL LETTER H WITH DESCENDER]
+ case L'\u2C76': // [LATIN SMALL LETTER HALF H]
+ case L'\uFF48': // [FULLWIDTH LATIN SMALL LETTER H]
+ *out = 'h';
+ break;
+ case L'\u01F6': // [LATIN CAPITAL LETTER HWAIR]
+ *out = 'H';
+ *out = 'V';
+ break;
+ case L'\u24A3': // [PARENTHESIZED LATIN SMALL LETTER H]
+ *out = '(';
+ *out = 'h';
+ *out = ')';
+ break;
+ case L'\u0195': // [LATIN SMALL LETTER HV]
+ *out = 'h';
+ *out = 'v';
+ break;
+ case L'\u00CC': // [LATIN CAPITAL LETTER I WITH GRAVE]
+ case L'\u00CD': // [LATIN CAPITAL LETTER I WITH ACUTE]
+ case L'\u00CE': // [LATIN CAPITAL LETTER I WITH CIRCUMFLEX]
+ case L'\u00CF': // [LATIN CAPITAL LETTER I WITH DIAERESIS]
+ case L'\u0128': // [LATIN CAPITAL LETTER I WITH TILDE]
+ case L'\u012A': // [LATIN CAPITAL LETTER I WITH MACRON]
+ case L'\u012C': // [LATIN CAPITAL LETTER I WITH BREVE]
+ case L'\u012E': // [LATIN CAPITAL LETTER I WITH OGONEK]
+ case L'\u0130': // [LATIN CAPITAL LETTER I WITH DOT ABOVE]
+ case L'\u0196': // [LATIN CAPITAL LETTER IOTA]
+ case L'\u0197': // [LATIN CAPITAL LETTER I WITH STROKE]
+ case L'\u01CF': // [LATIN CAPITAL LETTER I WITH CARON]
+ case L'\u0208': // [LATIN CAPITAL LETTER I WITH DOUBLE GRAVE]
+ case L'\u020A': // [LATIN CAPITAL LETTER I WITH INVERTED BREVE]
+ case L'\u026A': // [LATIN LETTER SMALL CAPITAL I]
+ case L'\u1D7B': // [LATIN SMALL CAPITAL LETTER I WITH STROKE]
+ case L'\u1E2C': // [LATIN CAPITAL LETTER I WITH TILDE BELOW]
+ case L'\u1E2E': // [LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE]
+ case L'\u1EC8': // [LATIN CAPITAL LETTER I WITH HOOK ABOVE]
+ case L'\u1ECA': // [LATIN CAPITAL LETTER I WITH DOT BELOW]
+ case L'\u24BE': // [CIRCLED LATIN CAPITAL LETTER I]
+ case L'\uA7FE': // [LATIN EPIGRAPHIC LETTER I LONGA]
+ case L'\uFF29': // [FULLWIDTH LATIN CAPITAL LETTER I]
+ *out = 'I';
+ break;
+ case L'\u00EC': // [LATIN SMALL LETTER I WITH GRAVE]
+ case L'\u00ED': // [LATIN SMALL LETTER I WITH ACUTE]
+ case L'\u00EE': // [LATIN SMALL LETTER I WITH CIRCUMFLEX]
+ case L'\u00EF': // [LATIN SMALL LETTER I WITH DIAERESIS]
+ case L'\u0129': // [LATIN SMALL LETTER I WITH TILDE]
+ case L'\u012B': // [LATIN SMALL LETTER I WITH MACRON]
+ case L'\u012D': // [LATIN SMALL LETTER I WITH BREVE]
+ case L'\u012F': // [LATIN SMALL LETTER I WITH OGONEK]
+ case L'\u0131': // [LATIN SMALL LETTER DOTLESS I]
+ case L'\u01D0': // [LATIN SMALL LETTER I WITH CARON]
+ case L'\u0209': // [LATIN SMALL LETTER I WITH DOUBLE GRAVE]
+ case L'\u020B': // [LATIN SMALL LETTER I WITH INVERTED BREVE]
+ case L'\u0268': // [LATIN SMALL LETTER I WITH STROKE]
+ case L'\u1D09': // [LATIN SMALL LETTER TURNED I]
+ case L'\u1D62': // [LATIN SUBSCRIPT SMALL LETTER I]
+ case L'\u1D7C': // [LATIN SMALL LETTER IOTA WITH STROKE]
+ case L'\u1D96': // [LATIN SMALL LETTER I WITH RETROFLEX HOOK]
+ case L'\u1E2D': // [LATIN SMALL LETTER I WITH TILDE BELOW]
+ case L'\u1E2F': // [LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE]
+ case L'\u1EC9': // [LATIN SMALL LETTER I WITH HOOK ABOVE]
+ case L'\u1ECB': // [LATIN SMALL LETTER I WITH DOT BELOW]
+ case L'\u2071': // [SUPERSCRIPT LATIN SMALL LETTER I]
+ case L'\u24D8': // [CIRCLED LATIN SMALL LETTER I]
+ case L'\uFF49': // [FULLWIDTH LATIN SMALL LETTER I]
+ *out = 'i';
+ break;
+ case L'\u0132': // [LATIN CAPITAL LIGATURE IJ]
+ *out = 'I';
+ *out = 'J';
+ break;
+ case L'\u24A4': // [PARENTHESIZED LATIN SMALL LETTER I]
+ *out = '(';
+ *out = 'i';
+ *out = ')';
+ break;
+ case L'\u0133': // [LATIN SMALL LIGATURE IJ]
+ *out = 'i';
+ *out = 'j';
+ break;
+ case L'\u0134': // [LATIN CAPITAL LETTER J WITH CIRCUMFLEX]
+ case L'\u0248': // [LATIN CAPITAL LETTER J WITH STROKE]
+ case L'\u1D0A': // [LATIN LETTER SMALL CAPITAL J]
+ case L'\u24BF': // [CIRCLED LATIN CAPITAL LETTER J]
+ case L'\uFF2A': // [FULLWIDTH LATIN CAPITAL LETTER J]
+ *out = 'J';
+ break;
+ case L'\u0135': // [LATIN SMALL LETTER J WITH CIRCUMFLEX]
+ case L'\u01F0': // [LATIN SMALL LETTER J WITH CARON]
+ case L'\u0237': // [LATIN SMALL LETTER DOTLESS J]
+ case L'\u0249': // [LATIN SMALL LETTER J WITH STROKE]
+ case L'\u025F': // [LATIN SMALL LETTER DOTLESS J WITH STROKE]
+ case L'\u0284': // [LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK]
+ case L'\u029D': // [LATIN SMALL LETTER J WITH CROSSED-TAIL]
+ case L'\u24D9': // [CIRCLED LATIN SMALL LETTER J]
+ case L'\u2C7C': // [LATIN SUBSCRIPT SMALL LETTER J]
+ case L'\uFF4A': // [FULLWIDTH LATIN SMALL LETTER J]
+ *out = 'j';
+ break;
+ case L'\u24A5': // [PARENTHESIZED LATIN SMALL LETTER J]
+ *out = '(';
+ *out = 'j';
+ *out = ')';
+ break;
+ case L'\u0136': // [LATIN CAPITAL LETTER K WITH CEDILLA]
+ case L'\u0198': // [LATIN CAPITAL LETTER K WITH HOOK]
+ case L'\u01E8': // [LATIN CAPITAL LETTER K WITH CARON]
+ case L'\u1D0B': // [LATIN LETTER SMALL CAPITAL K]
+ case L'\u1E30': // [LATIN CAPITAL LETTER K WITH ACUTE]
+ case L'\u1E32': // [LATIN CAPITAL LETTER K WITH DOT BELOW]
+ case L'\u1E34': // [LATIN CAPITAL LETTER K WITH LINE BELOW]
+ case L'\u24C0': // [CIRCLED LATIN CAPITAL LETTER K]
+ case L'\u2C69': // [LATIN CAPITAL LETTER K WITH DESCENDER]
+ case L'\uA740': // [LATIN CAPITAL LETTER K WITH STROKE]
+ case L'\uA742': // [LATIN CAPITAL LETTER K WITH DIAGONAL STROKE]
+ case L'\uA744': // [LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE]
+ case L'\uFF2B': // [FULLWIDTH LATIN CAPITAL LETTER K]
+ *out = 'K';
+ break;
+ case L'\u0137': // [LATIN SMALL LETTER K WITH CEDILLA]
+ case L'\u0199': // [LATIN SMALL LETTER K WITH HOOK]
+ case L'\u01E9': // [LATIN SMALL LETTER K WITH CARON]
+ case L'\u029E': // [LATIN SMALL LETTER TURNED K]
+ case L'\u1D84': // [LATIN SMALL LETTER K WITH PALATAL HOOK]
+ case L'\u1E31': // [LATIN SMALL LETTER K WITH ACUTE]
+ case L'\u1E33': // [LATIN SMALL LETTER K WITH DOT BELOW]
+ case L'\u1E35': // [LATIN SMALL LETTER K WITH LINE BELOW]
+ case L'\u24DA': // [CIRCLED LATIN SMALL LETTER K]
+ case L'\u2C6A': // [LATIN SMALL LETTER K WITH DESCENDER]
+ case L'\uA741': // [LATIN SMALL LETTER K WITH STROKE]
+ case L'\uA743': // [LATIN SMALL LETTER K WITH DIAGONAL STROKE]
+ case L'\uA745': // [LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE]
+ case L'\uFF4B': // [FULLWIDTH LATIN SMALL LETTER K]
+ *out = 'k';
+ break;
+ case L'\u24A6': // [PARENTHESIZED LATIN SMALL LETTER K]
+ *out = '(';
+ *out = 'k';
+ *out = ')';
+ break;
+ case L'\u0139': // [LATIN CAPITAL LETTER L WITH ACUTE]
+ case L'\u013B': // [LATIN CAPITAL LETTER L WITH CEDILLA]
+ case L'\u013D': // [LATIN CAPITAL LETTER L WITH CARON]
+ case L'\u013F': // [LATIN CAPITAL LETTER L WITH MIDDLE DOT]
+ case L'\u0141': // [LATIN CAPITAL LETTER L WITH STROKE]
+ case L'\u023D': // [LATIN CAPITAL LETTER L WITH BAR]
+ case L'\u029F': // [LATIN LETTER SMALL CAPITAL L]
+ case L'\u1D0C': // [LATIN LETTER SMALL CAPITAL L WITH STROKE]
+ case L'\u1E36': // [LATIN CAPITAL LETTER L WITH DOT BELOW]
+ case L'\u1E38': // [LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON]
+ case L'\u1E3A': // [LATIN CAPITAL LETTER L WITH LINE BELOW]
+ case L'\u1E3C': // [LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW]
+ case L'\u24C1': // [CIRCLED LATIN CAPITAL LETTER L]
+ case L'\u2C60': // [LATIN CAPITAL LETTER L WITH DOUBLE BAR]
+ case L'\u2C62': // [LATIN CAPITAL LETTER L WITH MIDDLE TILDE]
+ case L'\uA746': // [LATIN CAPITAL LETTER BROKEN L]
+ case L'\uA748': // [LATIN CAPITAL LETTER L WITH HIGH STROKE]
+ case L'\uA780': // [LATIN CAPITAL LETTER TURNED L]
+ case L'\uFF2C': // [FULLWIDTH LATIN CAPITAL LETTER L]
+ *out = 'L';
+ break;
+ case L'\u013A': // [LATIN SMALL LETTER L WITH ACUTE]
+ case L'\u013C': // [LATIN SMALL LETTER L WITH CEDILLA]
+ case L'\u013E': // [LATIN SMALL LETTER L WITH CARON]
+ case L'\u0140': // [LATIN SMALL LETTER L WITH MIDDLE DOT]
+ case L'\u0142': // [LATIN SMALL LETTER L WITH STROKE]
+ case L'\u019A': // [LATIN SMALL LETTER L WITH BAR]
+ case L'\u0234': // [LATIN SMALL LETTER L WITH CURL]
+ case L'\u026B': // [LATIN SMALL LETTER L WITH MIDDLE TILDE]
+ case L'\u026C': // [LATIN SMALL LETTER L WITH BELT]
+ case L'\u026D': // [LATIN SMALL LETTER L WITH RETROFLEX HOOK]
+ case L'\u1D85': // [LATIN SMALL LETTER L WITH PALATAL HOOK]
+ case L'\u1E37': // [LATIN SMALL LETTER L WITH DOT BELOW]
+ case L'\u1E39': // [LATIN SMALL LETTER L WITH DOT BELOW AND MACRON]
+ case L'\u1E3B': // [LATIN SMALL LETTER L WITH LINE BELOW]
+ case L'\u1E3D': // [LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW]
+ case L'\u24DB': // [CIRCLED LATIN SMALL LETTER L]
+ case L'\u2C61': // [LATIN SMALL LETTER L WITH DOUBLE BAR]
+ case L'\uA747': // [LATIN SMALL LETTER BROKEN L]
+ case L'\uA749': // [LATIN SMALL LETTER L WITH HIGH STROKE]
+ case L'\uA781': // [LATIN SMALL LETTER TURNED L]
+ case L'\uFF4C': // [FULLWIDTH LATIN SMALL LETTER L]
+ *out = 'l';
+ break;
+ case L'\u01C7': // [LATIN CAPITAL LETTER LJ]
+ *out = 'L';
+ *out = 'J';
+ break;
+ case L'\u1EFA': // [LATIN CAPITAL LETTER MIDDLE-WELSH LL]
+ *out = 'L';
+ *out = 'L';
+ break;
+ case L'\u01C8': // [LATIN CAPITAL LETTER L WITH SMALL LETTER J]
+ *out = 'L';
+ *out = 'j';
+ break;
+ case L'\u24A7': // [PARENTHESIZED LATIN SMALL LETTER L]
+ *out = '(';
+ *out = 'l';
+ *out = ')';
+ break;
+ case L'\u01C9': // [LATIN SMALL LETTER LJ]
+ *out = 'l';
+ *out = 'j';
+ break;
+ case L'\u1EFB': // [LATIN SMALL LETTER MIDDLE-WELSH LL]
+ *out = 'l';
+ *out = 'l';
+ break;
+ case L'\u02AA': // [LATIN SMALL LETTER LS DIGRAPH]
+ *out = 'l';
+ *out = 's';
+ break;
+ case L'\u02AB': // [LATIN SMALL LETTER LZ DIGRAPH]
+ *out = 'l';
+ *out = 'z';
+ break;
+ case L'\u019C': // [LATIN CAPITAL LETTER TURNED M]
+ case L'\u1D0D': // [LATIN LETTER SMALL CAPITAL M]
+ case L'\u1E3E': // [LATIN CAPITAL LETTER M WITH ACUTE]
+ case L'\u1E40': // [LATIN CAPITAL LETTER M WITH DOT ABOVE]
+ case L'\u1E42': // [LATIN CAPITAL LETTER M WITH DOT BELOW]
+ case L'\u24C2': // [CIRCLED LATIN CAPITAL LETTER M]
+ case L'\u2C6E': // [LATIN CAPITAL LETTER M WITH HOOK]
+ case L'\uA7FD': // [LATIN EPIGRAPHIC LETTER INVERTED M]
+ case L'\uA7FF': // [LATIN EPIGRAPHIC LETTER ARCHAIC M]
+ case L'\uFF2D': // [FULLWIDTH LATIN CAPITAL LETTER M]
+ *out = 'M';
+ break;
+ case L'\u026F': // [LATIN SMALL LETTER TURNED M]
+ case L'\u0270': // [LATIN SMALL LETTER TURNED M WITH LONG LEG]
+ case L'\u0271': // [LATIN SMALL LETTER M WITH HOOK]
+ case L'\u1D6F': // [LATIN SMALL LETTER M WITH MIDDLE TILDE]
+ case L'\u1D86': // [LATIN SMALL LETTER M WITH PALATAL HOOK]
+ case L'\u1E3F': // [LATIN SMALL LETTER M WITH ACUTE]
+ case L'\u1E41': // [LATIN SMALL LETTER M WITH DOT ABOVE]
+ case L'\u1E43': // [LATIN SMALL LETTER M WITH DOT BELOW]
+ case L'\u24DC': // [CIRCLED LATIN SMALL LETTER M]
+ case L'\uFF4D': // [FULLWIDTH LATIN SMALL LETTER M]
+ *out = 'm';
+ break;
+ case L'\u24A8': // [PARENTHESIZED LATIN SMALL LETTER M]
+ *out = '(';
+ *out = 'm';
+ *out = ')';
+ break;
+ case L'\u00D1': // [LATIN CAPITAL LETTER N WITH TILDE]
+ case L'\u0143': // [LATIN CAPITAL LETTER N WITH ACUTE]
+ case L'\u0145': // [LATIN CAPITAL LETTER N WITH CEDILLA]
+ case L'\u0147': // [LATIN CAPITAL LETTER N WITH CARON]
+ case L'\u014A': // [LATIN CAPITAL LETTER ENG]
+ case L'\u019D': // [LATIN CAPITAL LETTER N WITH LEFT HOOK]
+ case L'\u01F8': // [LATIN CAPITAL LETTER N WITH GRAVE]
+ case L'\u0220': // [LATIN CAPITAL LETTER N WITH LONG RIGHT LEG]
+ case L'\u0274': // [LATIN LETTER SMALL CAPITAL N]
+ case L'\u1D0E': // [LATIN LETTER SMALL CAPITAL REVERSED N]
+ case L'\u1E44': // [LATIN CAPITAL LETTER N WITH DOT ABOVE]
+ case L'\u1E46': // [LATIN CAPITAL LETTER N WITH DOT BELOW]
+ case L'\u1E48': // [LATIN CAPITAL LETTER N WITH LINE BELOW]
+ case L'\u1E4A': // [LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW]
+ case L'\u24C3': // [CIRCLED LATIN CAPITAL LETTER N]
+ case L'\uFF2E': // [FULLWIDTH LATIN CAPITAL LETTER N]
+ *out = 'N';
+ break;
+ case L'\u00F1': // [LATIN SMALL LETTER N WITH TILDE]
+ case L'\u0144': // [LATIN SMALL LETTER N WITH ACUTE]
+ case L'\u0146': // [LATIN SMALL LETTER N WITH CEDILLA]
+ case L'\u0148': // [LATIN SMALL LETTER N WITH CARON]
+ case L'\u0149': // [LATIN SMALL LETTER N PRECEDED BY APOSTROPHE]
+ case L'\u014B': // [LATIN SMALL LETTER ENG]
+ case L'\u019E': // [LATIN SMALL LETTER N WITH LONG RIGHT LEG]
+ case L'\u01F9': // [LATIN SMALL LETTER N WITH GRAVE]
+ case L'\u0235': // [LATIN SMALL LETTER N WITH CURL]
+ case L'\u0272': // [LATIN SMALL LETTER N WITH LEFT HOOK]
+ case L'\u0273': // [LATIN SMALL LETTER N WITH RETROFLEX HOOK]
+ case L'\u1D70': // [LATIN SMALL LETTER N WITH MIDDLE TILDE]
+ case L'\u1D87': // [LATIN SMALL LETTER N WITH PALATAL HOOK]
+ case L'\u1E45': // [LATIN SMALL LETTER N WITH DOT ABOVE]
+ case L'\u1E47': // [LATIN SMALL LETTER N WITH DOT BELOW]
+ case L'\u1E49': // [LATIN SMALL LETTER N WITH LINE BELOW]
+ case L'\u1E4B': // [LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW]
+ case L'\u207F': // [SUPERSCRIPT LATIN SMALL LETTER N]
+ case L'\u24DD': // [CIRCLED LATIN SMALL LETTER N]
+ case L'\uFF4E': // [FULLWIDTH LATIN SMALL LETTER N]
+ *out = 'n';
+ break;
+ case L'\u01CA': // [LATIN CAPITAL LETTER NJ]
+ *out = 'N';
+ *out = 'J';
+ break;
+ case L'\u01CB': // [LATIN CAPITAL LETTER N WITH SMALL LETTER J]
+ *out = 'N';
+ *out = 'j';
+ break;
+ case L'\u24A9': // [PARENTHESIZED LATIN SMALL LETTER N]
+ *out = '(';
+ *out = 'n';
+ *out = ')';
+ break;
+ case L'\u01CC': // [LATIN SMALL LETTER NJ]
+ *out = 'n';
+ *out = 'j';
+ break;
+ case L'\u00D2': // [LATIN CAPITAL LETTER O WITH GRAVE]
+ case L'\u00D3': // [LATIN CAPITAL LETTER O WITH ACUTE]
+ case L'\u00D4': // [LATIN CAPITAL LETTER O WITH CIRCUMFLEX]
+ case L'\u00D5': // [LATIN CAPITAL LETTER O WITH TILDE]
+ case L'\u00D6': // [LATIN CAPITAL LETTER O WITH DIAERESIS]
+ case L'\u00D8': // [LATIN CAPITAL LETTER O WITH STROKE]
+ case L'\u014C': // [LATIN CAPITAL LETTER O WITH MACRON]
+ case L'\u014E': // [LATIN CAPITAL LETTER O WITH BREVE]
+ case L'\u0150': // [LATIN CAPITAL LETTER O WITH DOUBLE ACUTE]
+ case L'\u0186': // [LATIN CAPITAL LETTER OPEN O]
+ case L'\u019F': // [LATIN CAPITAL LETTER O WITH MIDDLE TILDE]
+ case L'\u01A0': // [LATIN CAPITAL LETTER O WITH HORN]
+ case L'\u01D1': // [LATIN CAPITAL LETTER O WITH CARON]
+ case L'\u01EA': // [LATIN CAPITAL LETTER O WITH OGONEK]
+ case L'\u01EC': // [LATIN CAPITAL LETTER O WITH OGONEK AND MACRON]
+ case L'\u01FE': // [LATIN CAPITAL LETTER O WITH STROKE AND ACUTE]
+ case L'\u020C': // [LATIN CAPITAL LETTER O WITH DOUBLE GRAVE]
+ case L'\u020E': // [LATIN CAPITAL LETTER O WITH INVERTED BREVE]
+ case L'\u022A': // [LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON]
+ case L'\u022C': // [LATIN CAPITAL LETTER O WITH TILDE AND MACRON]
+ case L'\u022E': // [LATIN CAPITAL LETTER O WITH DOT ABOVE]
+ case L'\u0230': // [LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON]
+ case L'\u1D0F': // [LATIN LETTER SMALL CAPITAL O]
+ case L'\u1D10': // [LATIN LETTER SMALL CAPITAL OPEN O]
+ case L'\u1E4C': // [LATIN CAPITAL LETTER O WITH TILDE AND ACUTE]
+ case L'\u1E4E': // [LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS]
+ case L'\u1E50': // [LATIN CAPITAL LETTER O WITH MACRON AND GRAVE]
+ case L'\u1E52': // [LATIN CAPITAL LETTER O WITH MACRON AND ACUTE]
+ case L'\u1ECC': // [LATIN CAPITAL LETTER O WITH DOT BELOW]
+ case L'\u1ECE': // [LATIN CAPITAL LETTER O WITH HOOK ABOVE]
+ case L'\u1ED0': // [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE]
+ case L'\u1ED2': // [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE]
+ case L'\u1ED4': // [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE]
+ case L'\u1ED6': // [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE]
+ case L'\u1ED8': // [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW]
+ case L'\u1EDA': // [LATIN CAPITAL LETTER O WITH HORN AND ACUTE]
+ case L'\u1EDC': // [LATIN CAPITAL LETTER O WITH HORN AND GRAVE]
+ case L'\u1EDE': // [LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE]
+ case L'\u1EE0': // [LATIN CAPITAL LETTER O WITH HORN AND TILDE]
+ case L'\u1EE2': // [LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW]
+ case L'\u24C4': // [CIRCLED LATIN CAPITAL LETTER O]
+ case L'\uA74A': // [LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY]
+ case L'\uA74C': // [LATIN CAPITAL LETTER O WITH LOOP]
+ case L'\uFF2F': // [FULLWIDTH LATIN CAPITAL LETTER O]
+ *out = 'O';
+ break;
+ case L'\u00F2': // [LATIN SMALL LETTER O WITH GRAVE]
+ case L'\u00F3': // [LATIN SMALL LETTER O WITH ACUTE]
+ case L'\u00F4': // [LATIN SMALL LETTER O WITH CIRCUMFLEX]
+ case L'\u00F5': // [LATIN SMALL LETTER O WITH TILDE]
+ case L'\u00F6': // [LATIN SMALL LETTER O WITH DIAERESIS]
+ case L'\u00F8': // [LATIN SMALL LETTER O WITH STROKE]
+ case L'\u014D': // [LATIN SMALL LETTER O WITH MACRON]
+ case L'\u014F': // [LATIN SMALL LETTER O WITH BREVE]
+ case L'\u0151': // [LATIN SMALL LETTER O WITH DOUBLE ACUTE]
+ case L'\u01A1': // [LATIN SMALL LETTER O WITH HORN]
+ case L'\u01D2': // [LATIN SMALL LETTER O WITH CARON]
+ case L'\u01EB': // [LATIN SMALL LETTER O WITH OGONEK]
+ case L'\u01ED': // [LATIN SMALL LETTER O WITH OGONEK AND MACRON]
+ case L'\u01FF': // [LATIN SMALL LETTER O WITH STROKE AND ACUTE]
+ case L'\u020D': // [LATIN SMALL LETTER O WITH DOUBLE GRAVE]
+ case L'\u020F': // [LATIN SMALL LETTER O WITH INVERTED BREVE]
+ case L'\u022B': // [LATIN SMALL LETTER O WITH DIAERESIS AND MACRON]
+ case L'\u022D': // [LATIN SMALL LETTER O WITH TILDE AND MACRON]
+ case L'\u022F': // [LATIN SMALL LETTER O WITH DOT ABOVE]
+ case L'\u0231': // [LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON]
+ case L'\u0254': // [LATIN SMALL LETTER OPEN O]
+ case L'\u0275': // [LATIN SMALL LETTER BARRED O]
+ case L'\u1D16': // [LATIN SMALL LETTER TOP HALF O]
+ case L'\u1D17': // [LATIN SMALL LETTER BOTTOM HALF O]
+ case L'\u1D97': // [LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK]
+ case L'\u1E4D': // [LATIN SMALL LETTER O WITH TILDE AND ACUTE]
+ case L'\u1E4F': // [LATIN SMALL LETTER O WITH TILDE AND DIAERESIS]
+ case L'\u1E51': // [LATIN SMALL LETTER O WITH MACRON AND GRAVE]
+ case L'\u1E53': // [LATIN SMALL LETTER O WITH MACRON AND ACUTE]
+ case L'\u1ECD': // [LATIN SMALL LETTER O WITH DOT BELOW]
+ case L'\u1ECF': // [LATIN SMALL LETTER O WITH HOOK ABOVE]
+ case L'\u1ED1': // [LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE]
+ case L'\u1ED3': // [LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE]
+ case L'\u1ED5': // [LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE]
+ case L'\u1ED7': // [LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE]
+ case L'\u1ED9': // [LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW]
+ case L'\u1EDB': // [LATIN SMALL LETTER O WITH HORN AND ACUTE]
+ case L'\u1EDD': // [LATIN SMALL LETTER O WITH HORN AND GRAVE]
+ case L'\u1EDF': // [LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE]
+ case L'\u1EE1': // [LATIN SMALL LETTER O WITH HORN AND TILDE]
+ case L'\u1EE3': // [LATIN SMALL LETTER O WITH HORN AND DOT BELOW]
+ case L'\u2092': // [LATIN SUBSCRIPT SMALL LETTER O]
+ case L'\u24DE': // [CIRCLED LATIN SMALL LETTER O]
+ case L'\u2C7A': // [LATIN SMALL LETTER O WITH LOW RING INSIDE]
+ case L'\uA74B': // [LATIN SMALL LETTER O WITH LONG STROKE OVERLAY]
+ case L'\uA74D': // [LATIN SMALL LETTER O WITH LOOP]
+ case L'\uFF4F': // [FULLWIDTH LATIN SMALL LETTER O]
+ *out = 'o';
+ break;
+ case L'\u0152': // [LATIN CAPITAL LIGATURE OE]
+ case L'\u0276': // [LATIN LETTER SMALL CAPITAL OE]
+ *out = 'O';
+ *out = 'E';
+ break;
+ case L'\uA74E': // [LATIN CAPITAL LETTER OO]
+ *out = 'O';
+ *out = 'O';
+ break;
+ case L'\u0222': // [LATIN CAPITAL LETTER OU]
+ case L'\u1D15': // [LATIN LETTER SMALL CAPITAL OU]
+ *out = 'O';
+ *out = 'U';
+ break;
+ case L'\u24AA': // [PARENTHESIZED LATIN SMALL LETTER O]
+ *out = '(';
+ *out = 'o';
+ *out = ')';
+ break;
+ case L'\u0153': // [LATIN SMALL LIGATURE OE]
+ case L'\u1D14': // [LATIN SMALL LETTER TURNED OE]
+ *out = 'o';
+ *out = 'e';
+ break;
+ case L'\uA74F': // [LATIN SMALL LETTER OO]
+ *out = 'o';
+ *out = 'o';
+ break;
+ case L'\u0223': // [LATIN SMALL LETTER OU]
+ *out = 'o';
+ *out = 'u';
+ break;
+ case L'\u01A4': // [LATIN CAPITAL LETTER P WITH HOOK]
+ case L'\u1D18': // [LATIN LETTER SMALL CAPITAL P]
+ case L'\u1E54': // [LATIN CAPITAL LETTER P WITH ACUTE]
+ case L'\u1E56': // [LATIN CAPITAL LETTER P WITH DOT ABOVE]
+ case L'\u24C5': // [CIRCLED LATIN CAPITAL LETTER P]
+ case L'\u2C63': // [LATIN CAPITAL LETTER P WITH STROKE]
+ case L'\uA750': // [LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER]
+ case L'\uA752': // [LATIN CAPITAL LETTER P WITH FLOURISH]
+ case L'\uA754': // [LATIN CAPITAL LETTER P WITH SQUIRREL TAIL]
+ case L'\uFF30': // [FULLWIDTH LATIN CAPITAL LETTER P]
+ *out = 'P';
+ break;
+ case L'\u01A5': // [LATIN SMALL LETTER P WITH HOOK]
+ case L'\u1D71': // [LATIN SMALL LETTER P WITH MIDDLE TILDE]
+ case L'\u1D7D': // [LATIN SMALL LETTER P WITH STROKE]
+ case L'\u1D88': // [LATIN SMALL LETTER P WITH PALATAL HOOK]
+ case L'\u1E55': // [LATIN SMALL LETTER P WITH ACUTE]
+ case L'\u1E57': // [LATIN SMALL LETTER P WITH DOT ABOVE]
+ case L'\u24DF': // [CIRCLED LATIN SMALL LETTER P]
+ case L'\uA751': // [LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER]
+ case L'\uA753': // [LATIN SMALL LETTER P WITH FLOURISH]
+ case L'\uA755': // [LATIN SMALL LETTER P WITH SQUIRREL TAIL]
+ case L'\uA7FC': // [LATIN EPIGRAPHIC LETTER REVERSED P]
+ case L'\uFF50': // [FULLWIDTH LATIN SMALL LETTER P]
+ *out = 'p';
+ break;
+ case L'\u24AB': // [PARENTHESIZED LATIN SMALL LETTER P]
+ *out = '(';
+ *out = 'p';
+ *out = ')';
+ break;
+ case L'\u024A': // [LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL]
+ case L'\u24C6': // [CIRCLED LATIN CAPITAL LETTER Q]
+ case L'\uA756': // [LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER]
+ case L'\uA758': // [LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE]
+ case L'\uFF31': // [FULLWIDTH LATIN CAPITAL LETTER Q]
+ *out = 'Q';
+ break;
+ case L'\u0138': // [LATIN SMALL LETTER KRA]
+ case L'\u024B': // [LATIN SMALL LETTER Q WITH HOOK TAIL]
+ case L'\u02A0': // [LATIN SMALL LETTER Q WITH HOOK]
+ case L'\u24E0': // [CIRCLED LATIN SMALL LETTER Q]
+ case L'\uA757': // [LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER]
+ case L'\uA759': // [LATIN SMALL LETTER Q WITH DIAGONAL STROKE]
+ case L'\uFF51': // [FULLWIDTH LATIN SMALL LETTER Q]
+ *out = 'q';
+ break;
+ case L'\u24AC': // [PARENTHESIZED LATIN SMALL LETTER Q]
+ *out = '(';
+ *out = 'q';
+ *out = ')';
+ break;
+ case L'\u0239': // [LATIN SMALL LETTER QP DIGRAPH]
+ *out = 'q';
+ *out = 'p';
+ break;
+ case L'\u0154': // [LATIN CAPITAL LETTER R WITH ACUTE]
+ case L'\u0156': // [LATIN CAPITAL LETTER R WITH CEDILLA]
+ case L'\u0158': // [LATIN CAPITAL LETTER R WITH CARON]
+ case L'\u0210': // [LATIN CAPITAL LETTER R WITH DOUBLE GRAVE]
+ case L'\u0212': // [LATIN CAPITAL LETTER R WITH INVERTED BREVE]
+ case L'\u024C': // [LATIN CAPITAL LETTER R WITH STROKE]
+ case L'\u0280': // [LATIN LETTER SMALL CAPITAL R]
+ case L'\u0281': // [LATIN LETTER SMALL CAPITAL INVERTED R]
+ case L'\u1D19': // [LATIN LETTER SMALL CAPITAL REVERSED R]
+ case L'\u1D1A': // [LATIN LETTER SMALL CAPITAL TURNED R]
+ case L'\u1E58': // [LATIN CAPITAL LETTER R WITH DOT ABOVE]
+ case L'\u1E5A': // [LATIN CAPITAL LETTER R WITH DOT BELOW]
+ case L'\u1E5C': // [LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON]
+ case L'\u1E5E': // [LATIN CAPITAL LETTER R WITH LINE BELOW]
+ case L'\u24C7': // [CIRCLED LATIN CAPITAL LETTER R]
+ case L'\u2C64': // [LATIN CAPITAL LETTER R WITH TAIL]
+ case L'\uA75A': // [LATIN CAPITAL LETTER R ROTUNDA]
+ case L'\uA782': // [LATIN CAPITAL LETTER INSULAR R]
+ case L'\uFF32': // [FULLWIDTH LATIN CAPITAL LETTER R]
+ *out = 'R';
+ break;
+ case L'\u0155': // [LATIN SMALL LETTER R WITH ACUTE]
+ case L'\u0157': // [LATIN SMALL LETTER R WITH CEDILLA]
+ case L'\u0159': // [LATIN SMALL LETTER R WITH CARON]
+ case L'\u0211': // [LATIN SMALL LETTER R WITH DOUBLE GRAVE]
+ case L'\u0213': // [LATIN SMALL LETTER R WITH INVERTED BREVE]
+ case L'\u024D': // [LATIN SMALL LETTER R WITH STROKE]
+ case L'\u027C': // [LATIN SMALL LETTER R WITH LONG LEG]
+ case L'\u027D': // [LATIN SMALL LETTER R WITH TAIL]
+ case L'\u027E': // [LATIN SMALL LETTER R WITH FISHHOOK]
+ case L'\u027F': // [LATIN SMALL LETTER REVERSED R WITH FISHHOOK]
+ case L'\u1D63': // [LATIN SUBSCRIPT SMALL LETTER R]
+ case L'\u1D72': // [LATIN SMALL LETTER R WITH MIDDLE TILDE]
+ case L'\u1D73': // [LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE]
+ case L'\u1D89': // [LATIN SMALL LETTER R WITH PALATAL HOOK]
+ case L'\u1E59': // [LATIN SMALL LETTER R WITH DOT ABOVE]
+ case L'\u1E5B': // [LATIN SMALL LETTER R WITH DOT BELOW]
+ case L'\u1E5D': // [LATIN SMALL LETTER R WITH DOT BELOW AND MACRON]
+ case L'\u1E5F': // [LATIN SMALL LETTER R WITH LINE BELOW]
+ case L'\u24E1': // [CIRCLED LATIN SMALL LETTER R]
+ case L'\uA75B': // [LATIN SMALL LETTER R ROTUNDA]
+ case L'\uA783': // [LATIN SMALL LETTER INSULAR R]
+ case L'\uFF52': // [FULLWIDTH LATIN SMALL LETTER R]
+ *out = 'r';
+ break;
+ case L'\u24AD': // [PARENTHESIZED LATIN SMALL LETTER R]
+ *out = '(';
+ *out = 'r';
+ *out = ')';
+ break;
+ case L'\u015A': // [LATIN CAPITAL LETTER S WITH ACUTE]
+ case L'\u015C': // [LATIN CAPITAL LETTER S WITH CIRCUMFLEX]
+ case L'\u015E': // [LATIN CAPITAL LETTER S WITH CEDILLA]
+ case L'\u0160': // [LATIN CAPITAL LETTER S WITH CARON]
+ case L'\u0218': // [LATIN CAPITAL LETTER S WITH COMMA BELOW]
+ case L'\u1E60': // [LATIN CAPITAL LETTER S WITH DOT ABOVE]
+ case L'\u1E62': // [LATIN CAPITAL LETTER S WITH DOT BELOW]
+ case L'\u1E64': // [LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE]
+ case L'\u1E66': // [LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE]
+ case L'\u1E68': // [LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE]
+ case L'\u24C8': // [CIRCLED LATIN CAPITAL LETTER S]
+ case L'\uA731': // [LATIN LETTER SMALL CAPITAL S]
+ case L'\uA785': // [LATIN SMALL LETTER INSULAR S]
+ case L'\uFF33': // [FULLWIDTH LATIN CAPITAL LETTER S]
+ *out = 'S';
+ break;
+ case L'\u015B': // [LATIN SMALL LETTER S WITH ACUTE]
+ case L'\u015D': // [LATIN SMALL LETTER S WITH CIRCUMFLEX]
+ case L'\u015F': // [LATIN SMALL LETTER S WITH CEDILLA]
+ case L'\u0161': // [LATIN SMALL LETTER S WITH CARON]
+ case L'\u017F': // [LATIN SMALL LETTER LONG S]
+ case L'\u0219': // [LATIN SMALL LETTER S WITH COMMA BELOW]
+ case L'\u023F': // [LATIN SMALL LETTER S WITH SWASH TAIL]
+ case L'\u0282': // [LATIN SMALL LETTER S WITH HOOK]
+ case L'\u1D74': // [LATIN SMALL LETTER S WITH MIDDLE TILDE]
+ case L'\u1D8A': // [LATIN SMALL LETTER S WITH PALATAL HOOK]
+ case L'\u1E61': // [LATIN SMALL LETTER S WITH DOT ABOVE]
+ case L'\u1E63': // [LATIN SMALL LETTER S WITH DOT BELOW]
+ case L'\u1E65': // [LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE]
+ case L'\u1E67': // [LATIN SMALL LETTER S WITH CARON AND DOT ABOVE]
+ case L'\u1E69': // [LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE]
+ case L'\u1E9C': // [LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE]
+ case L'\u1E9D': // [LATIN SMALL LETTER LONG S WITH HIGH STROKE]
+ case L'\u24E2': // [CIRCLED LATIN SMALL LETTER S]
+ case L'\uA784': // [LATIN CAPITAL LETTER INSULAR S]
+ case L'\uFF53': // [FULLWIDTH LATIN SMALL LETTER S]
+ *out = 's';
+ break;
+ case L'\u1E9E': // [LATIN CAPITAL LETTER SHARP S]
+ *out = 'S';
+ *out = 'S';
+ break;
+ case L'\u24AE': // [PARENTHESIZED LATIN SMALL LETTER S]
+ *out = '(';
+ *out = 's';
+ *out = ')';
+ break;
+ case L'\u00DF': // [LATIN SMALL LETTER SHARP S]
+ *out = 's';
+ *out = 's';
+ break;
+ case L'\uFB06': // [LATIN SMALL LIGATURE ST]
+ *out = 's';
+ *out = 't';
+ break;
+ case L'\u0162': // [LATIN CAPITAL LETTER T WITH CEDILLA]
+ case L'\u0164': // [LATIN CAPITAL LETTER T WITH CARON]
+ case L'\u0166': // [LATIN CAPITAL LETTER T WITH STROKE]
+ case L'\u01AC': // [LATIN CAPITAL LETTER T WITH HOOK]
+ case L'\u01AE': // [LATIN CAPITAL LETTER T WITH RETROFLEX HOOK]
+ case L'\u021A': // [LATIN CAPITAL LETTER T WITH COMMA BELOW]
+ case L'\u023E': // [LATIN CAPITAL LETTER T WITH DIAGONAL STROKE]
+ case L'\u1D1B': // [LATIN LETTER SMALL CAPITAL T]
+ case L'\u1E6A': // [LATIN CAPITAL LETTER T WITH DOT ABOVE]
+ case L'\u1E6C': // [LATIN CAPITAL LETTER T WITH DOT BELOW]
+ case L'\u1E6E': // [LATIN CAPITAL LETTER T WITH LINE BELOW]
+ case L'\u1E70': // [LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW]
+ case L'\u24C9': // [CIRCLED LATIN CAPITAL LETTER T]
+ case L'\uA786': // [LATIN CAPITAL LETTER INSULAR T]
+ case L'\uFF34': // [FULLWIDTH LATIN CAPITAL LETTER T]
+ *out = 'T';
+ break;
+ case L'\u0163': // [LATIN SMALL LETTER T WITH CEDILLA]
+ case L'\u0165': // [LATIN SMALL LETTER T WITH CARON]
+ case L'\u0167': // [LATIN SMALL LETTER T WITH STROKE]
+ case L'\u01AB': // [LATIN SMALL LETTER T WITH PALATAL HOOK]
+ case L'\u01AD': // [LATIN SMALL LETTER T WITH HOOK]
+ case L'\u021B': // [LATIN SMALL LETTER T WITH COMMA BELOW]
+ case L'\u0236': // [LATIN SMALL LETTER T WITH CURL]
+ case L'\u0287': // [LATIN SMALL LETTER TURNED T]
+ case L'\u0288': // [LATIN SMALL LETTER T WITH RETROFLEX HOOK]
+ case L'\u1D75': // [LATIN SMALL LETTER T WITH MIDDLE TILDE]
+ case L'\u1E6B': // [LATIN SMALL LETTER T WITH DOT ABOVE]
+ case L'\u1E6D': // [LATIN SMALL LETTER T WITH DOT BELOW]
+ case L'\u1E6F': // [LATIN SMALL LETTER T WITH LINE BELOW]
+ case L'\u1E71': // [LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW]
+ case L'\u1E97': // [LATIN SMALL LETTER T WITH DIAERESIS]
+ case L'\u24E3': // [CIRCLED LATIN SMALL LETTER T]
+ case L'\u2C66': // [LATIN SMALL LETTER T WITH DIAGONAL STROKE]
+ case L'\uFF54': // [FULLWIDTH LATIN SMALL LETTER T]
+ *out = 't';
+ break;
+ case L'\u00DE': // [LATIN CAPITAL LETTER THORN]
+ case L'\uA766': // [LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER]
+ *out = 'T';
+ *out = 'H';
+ break;
+ case L'\uA728': // [LATIN CAPITAL LETTER TZ]
+ *out = 'T';
+ *out = 'Z';
+ break;
+ case L'\u24AF': // [PARENTHESIZED LATIN SMALL LETTER T]
+ *out = '(';
+ *out = 't';
+ *out = ')';
+ break;
+ case L'\u02A8': // [LATIN SMALL LETTER TC DIGRAPH WITH CURL]
+ *out = 't';
+ *out = 'c';
+ break;
+ case L'\u00FE': // [LATIN SMALL LETTER THORN]
+ case L'\u1D7A': // [LATIN SMALL LETTER TH WITH STRIKETHROUGH]
+ case L'\uA767': // [LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER]
+ *out = 't';
+ *out = 'h';
+ break;
+ case L'\u02A6': // [LATIN SMALL LETTER TS DIGRAPH]
+ *out = 't';
+ *out = 's';
+ break;
+ case L'\uA729': // [LATIN SMALL LETTER TZ]
+ *out = 't';
+ *out = 'z';
+ break;
+ case L'\u00D9': // [LATIN CAPITAL LETTER U WITH GRAVE]
+ case L'\u00DA': // [LATIN CAPITAL LETTER U WITH ACUTE]
+ case L'\u00DB': // [LATIN CAPITAL LETTER U WITH CIRCUMFLEX]
+ case L'\u00DC': // [LATIN CAPITAL LETTER U WITH DIAERESIS]
+ case L'\u0168': // [LATIN CAPITAL LETTER U WITH TILDE]
+ case L'\u016A': // [LATIN CAPITAL LETTER U WITH MACRON]
+ case L'\u016C': // [LATIN CAPITAL LETTER U WITH BREVE]
+ case L'\u016E': // [LATIN CAPITAL LETTER U WITH RING ABOVE]
+ case L'\u0170': // [LATIN CAPITAL LETTER U WITH DOUBLE ACUTE]
+ case L'\u0172': // [LATIN CAPITAL LETTER U WITH OGONEK]
+ case L'\u01AF': // [LATIN CAPITAL LETTER U WITH HORN]
+ case L'\u01D3': // [LATIN CAPITAL LETTER U WITH CARON]
+ case L'\u01D5': // [LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON]
+ case L'\u01D7': // [LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE]
+ case L'\u01D9': // [LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON]
+ case L'\u01DB': // [LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE]
+ case L'\u0214': // [LATIN CAPITAL LETTER U WITH DOUBLE GRAVE]
+ case L'\u0216': // [LATIN CAPITAL LETTER U WITH INVERTED BREVE]
+ case L'\u0244': // [LATIN CAPITAL LETTER U BAR]
+ case L'\u1D1C': // [LATIN LETTER SMALL CAPITAL U]
+ case L'\u1D7E': // [LATIN SMALL CAPITAL LETTER U WITH STROKE]
+ case L'\u1E72': // [LATIN CAPITAL LETTER U WITH DIAERESIS BELOW]
+ case L'\u1E74': // [LATIN CAPITAL LETTER U WITH TILDE BELOW]
+ case L'\u1E76': // [LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW]
+ case L'\u1E78': // [LATIN CAPITAL LETTER U WITH TILDE AND ACUTE]
+ case L'\u1E7A': // [LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS]
+ case L'\u1EE4': // [LATIN CAPITAL LETTER U WITH DOT BELOW]
+ case L'\u1EE6': // [LATIN CAPITAL LETTER U WITH HOOK ABOVE]
+ case L'\u1EE8': // [LATIN CAPITAL LETTER U WITH HORN AND ACUTE]
+ case L'\u1EEA': // [LATIN CAPITAL LETTER U WITH HORN AND GRAVE]
+ case L'\u1EEC': // [LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE]
+ case L'\u1EEE': // [LATIN CAPITAL LETTER U WITH HORN AND TILDE]
+ case L'\u1EF0': // [LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW]
+ case L'\u24CA': // [CIRCLED LATIN CAPITAL LETTER U]
+ case L'\uFF35': // [FULLWIDTH LATIN CAPITAL LETTER U]
+ *out = 'U';
+ break;
+ case L'\u00F9': // [LATIN SMALL LETTER U WITH GRAVE]
+ case L'\u00FA': // [LATIN SMALL LETTER U WITH ACUTE]
+ case L'\u00FB': // [LATIN SMALL LETTER U WITH CIRCUMFLEX]
+ case L'\u00FC': // [LATIN SMALL LETTER U WITH DIAERESIS]
+ case L'\u0169': // [LATIN SMALL LETTER U WITH TILDE]
+ case L'\u016B': // [LATIN SMALL LETTER U WITH MACRON]
+ case L'\u016D': // [LATIN SMALL LETTER U WITH BREVE]
+ case L'\u016F': // [LATIN SMALL LETTER U WITH RING ABOVE]
+ case L'\u0171': // [LATIN SMALL LETTER U WITH DOUBLE ACUTE]
+ case L'\u0173': // [LATIN SMALL LETTER U WITH OGONEK]
+ case L'\u01B0': // [LATIN SMALL LETTER U WITH HORN]
+ case L'\u01D4': // [LATIN SMALL LETTER U WITH CARON]
+ case L'\u01D6': // [LATIN SMALL LETTER U WITH DIAERESIS AND MACRON]
+ case L'\u01D8': // [LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE]
+ case L'\u01DA': // [LATIN SMALL LETTER U WITH DIAERESIS AND CARON]
+ case L'\u01DC': // [LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE]
+ case L'\u0215': // [LATIN SMALL LETTER U WITH DOUBLE GRAVE]
+ case L'\u0217': // [LATIN SMALL LETTER U WITH INVERTED BREVE]
+ case L'\u0289': // [LATIN SMALL LETTER U BAR]
+ case L'\u1D64': // [LATIN SUBSCRIPT SMALL LETTER U]
+ case L'\u1D99': // [LATIN SMALL LETTER U WITH RETROFLEX HOOK]
+ case L'\u1E73': // [LATIN SMALL LETTER U WITH DIAERESIS BELOW]
+ case L'\u1E75': // [LATIN SMALL LETTER U WITH TILDE BELOW]
+ case L'\u1E77': // [LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW]
+ case L'\u1E79': // [LATIN SMALL LETTER U WITH TILDE AND ACUTE]
+ case L'\u1E7B': // [LATIN SMALL LETTER U WITH MACRON AND DIAERESIS]
+ case L'\u1EE5': // [LATIN SMALL LETTER U WITH DOT BELOW]
+ case L'\u1EE7': // [LATIN SMALL LETTER U WITH HOOK ABOVE]
+ case L'\u1EE9': // [LATIN SMALL LETTER U WITH HORN AND ACUTE]
+ case L'\u1EEB': // [LATIN SMALL LETTER U WITH HORN AND GRAVE]
+ case L'\u1EED': // [LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE]
+ case L'\u1EEF': // [LATIN SMALL LETTER U WITH HORN AND TILDE]
+ case L'\u1EF1': // [LATIN SMALL LETTER U WITH HORN AND DOT BELOW]
+ case L'\u24E4': // [CIRCLED LATIN SMALL LETTER U]
+ case L'\uFF55': // [FULLWIDTH LATIN SMALL LETTER U]
+ *out = 'u';
+ break;
+ case L'\u24B0': // [PARENTHESIZED LATIN SMALL LETTER U]
+ *out = '(';
+ *out = 'u';
+ *out = ')';
+ break;
+ case L'\u1D6B': // [LATIN SMALL LETTER UE]
+ *out = 'u';
+ *out = 'e';
+ break;
+ case L'\u01B2': // [LATIN CAPITAL LETTER V WITH HOOK]
+ case L'\u0245': // [LATIN CAPITAL LETTER TURNED V]
+ case L'\u1D20': // [LATIN LETTER SMALL CAPITAL V]
+ case L'\u1E7C': // [LATIN CAPITAL LETTER V WITH TILDE]
+ case L'\u1E7E': // [LATIN CAPITAL LETTER V WITH DOT BELOW]
+ case L'\u1EFC': // [LATIN CAPITAL LETTER MIDDLE-WELSH V]
+ case L'\u24CB': // [CIRCLED LATIN CAPITAL LETTER V]
+ case L'\uA75E': // [LATIN CAPITAL LETTER V WITH DIAGONAL STROKE]
+ case L'\uA768': // [LATIN CAPITAL LETTER VEND]
+ case L'\uFF36': // [FULLWIDTH LATIN CAPITAL LETTER V]
+ *out = 'V';
+ break;
+ case L'\u028B': // [LATIN SMALL LETTER V WITH HOOK]
+ case L'\u028C': // [LATIN SMALL LETTER TURNED V]
+ case L'\u1D65': // [LATIN SUBSCRIPT SMALL LETTER V]
+ case L'\u1D8C': // [LATIN SMALL LETTER V WITH PALATAL HOOK]
+ case L'\u1E7D': // [LATIN SMALL LETTER V WITH TILDE]
+ case L'\u1E7F': // [LATIN SMALL LETTER V WITH DOT BELOW]
+ case L'\u24E5': // [CIRCLED LATIN SMALL LETTER V]
+ case L'\u2C71': // [LATIN SMALL LETTER V WITH RIGHT HOOK]
+ case L'\u2C74': // [LATIN SMALL LETTER V WITH CURL]
+ case L'\uA75F': // [LATIN SMALL LETTER V WITH DIAGONAL STROKE]
+ case L'\uFF56': // [FULLWIDTH LATIN SMALL LETTER V]
+ *out = 'v';
+ break;
+ case L'\uA760': // [LATIN CAPITAL LETTER VY]
+ *out = 'V';
+ *out = 'Y';
+ break;
+ case L'\u24B1': // [PARENTHESIZED LATIN SMALL LETTER V]
+ *out = '(';
+ *out = 'v';
+ *out = ')';
+ break;
+ case L'\uA761': // [LATIN SMALL LETTER VY]
+ *out = 'v';
+ *out = 'y';
+ break;
+ case L'\u0174': // [LATIN CAPITAL LETTER W WITH CIRCUMFLEX]
+ case L'\u01F7': // [LATIN CAPITAL LETTER WYNN]
+ case L'\u1D21': // [LATIN LETTER SMALL CAPITAL W]
+ case L'\u1E80': // [LATIN CAPITAL LETTER W WITH GRAVE]
+ case L'\u1E82': // [LATIN CAPITAL LETTER W WITH ACUTE]
+ case L'\u1E84': // [LATIN CAPITAL LETTER W WITH DIAERESIS]
+ case L'\u1E86': // [LATIN CAPITAL LETTER W WITH DOT ABOVE]
+ case L'\u1E88': // [LATIN CAPITAL LETTER W WITH DOT BELOW]
+ case L'\u24CC': // [CIRCLED LATIN CAPITAL LETTER W]
+ case L'\u2C72': // [LATIN CAPITAL LETTER W WITH HOOK]
+ case L'\uFF37': // [FULLWIDTH LATIN CAPITAL LETTER W]
+ *out = 'W';
+ break;
+ case L'\u0175': // [LATIN SMALL LETTER W WITH CIRCUMFLEX]
+ case L'\u01BF': // [LATIN LETTER WYNN]
+ case L'\u028D': // [LATIN SMALL LETTER TURNED W]
+ case L'\u1E81': // [LATIN SMALL LETTER W WITH GRAVE]
+ case L'\u1E83': // [LATIN SMALL LETTER W WITH ACUTE]
+ case L'\u1E85': // [LATIN SMALL LETTER W WITH DIAERESIS]
+ case L'\u1E87': // [LATIN SMALL LETTER W WITH DOT ABOVE]
+ case L'\u1E89': // [LATIN SMALL LETTER W WITH DOT BELOW]
+ case L'\u1E98': // [LATIN SMALL LETTER W WITH RING ABOVE]
+ case L'\u24E6': // [CIRCLED LATIN SMALL LETTER W]
+ case L'\u2C73': // [LATIN SMALL LETTER W WITH HOOK]
+ case L'\uFF57': // [FULLWIDTH LATIN SMALL LETTER W]
+ *out = 'w';
+ break;
+ case L'\u24B2': // [PARENTHESIZED LATIN SMALL LETTER W]
+ *out = '(';
+ *out = 'w';
+ *out = ')';
+ break;
+ case L'\u1E8A': // [LATIN CAPITAL LETTER X WITH DOT ABOVE]
+ case L'\u1E8C': // [LATIN CAPITAL LETTER X WITH DIAERESIS]
+ case L'\u24CD': // [CIRCLED LATIN CAPITAL LETTER X]
+ case L'\uFF38': // [FULLWIDTH LATIN CAPITAL LETTER X]
+ *out = 'X';
+ break;
+ case L'\u1D8D': // [LATIN SMALL LETTER X WITH PALATAL HOOK]
+ case L'\u1E8B': // [LATIN SMALL LETTER X WITH DOT ABOVE]
+ case L'\u1E8D': // [LATIN SMALL LETTER X WITH DIAERESIS]
+ case L'\u2093': // [LATIN SUBSCRIPT SMALL LETTER X]
+ case L'\u24E7': // [CIRCLED LATIN SMALL LETTER X]
+ case L'\uFF58': // [FULLWIDTH LATIN SMALL LETTER X]
+ *out = 'x';
+ break;
+ case L'\u24B3': // [PARENTHESIZED LATIN SMALL LETTER X]
+ *out = '(';
+ *out = 'x';
+ *out = ')';
+ break;
+ case L'\u00DD': // [LATIN CAPITAL LETTER Y WITH ACUTE]
+ case L'\u0176': // [LATIN CAPITAL LETTER Y WITH CIRCUMFLEX]
+ case L'\u0178': // [LATIN CAPITAL LETTER Y WITH DIAERESIS]
+ case L'\u01B3': // [LATIN CAPITAL LETTER Y WITH HOOK]
+ case L'\u0232': // [LATIN CAPITAL LETTER Y WITH MACRON]
+ case L'\u024E': // [LATIN CAPITAL LETTER Y WITH STROKE]
+ case L'\u028F': // [LATIN LETTER SMALL CAPITAL Y]
+ case L'\u1E8E': // [LATIN CAPITAL LETTER Y WITH DOT ABOVE]
+ case L'\u1EF2': // [LATIN CAPITAL LETTER Y WITH GRAVE]
+ case L'\u1EF4': // [LATIN CAPITAL LETTER Y WITH DOT BELOW]
+ case L'\u1EF6': // [LATIN CAPITAL LETTER Y WITH HOOK ABOVE]
+ case L'\u1EF8': // [LATIN CAPITAL LETTER Y WITH TILDE]
+ case L'\u1EFE': // [LATIN CAPITAL LETTER Y WITH LOOP]
+ case L'\u24CE': // [CIRCLED LATIN CAPITAL LETTER Y]
+ case L'\uFF39': // [FULLWIDTH LATIN CAPITAL LETTER Y]
+ *out = 'Y';
+ break;
+ case L'\u00FD': // [LATIN SMALL LETTER Y WITH ACUTE]
+ case L'\u00FF': // [LATIN SMALL LETTER Y WITH DIAERESIS]
+ case L'\u0177': // [LATIN SMALL LETTER Y WITH CIRCUMFLEX]
+ case L'\u01B4': // [LATIN SMALL LETTER Y WITH HOOK]
+ case L'\u0233': // [LATIN SMALL LETTER Y WITH MACRON]
+ case L'\u024F': // [LATIN SMALL LETTER Y WITH STROKE]
+ case L'\u028E': // [LATIN SMALL LETTER TURNED Y]
+ case L'\u1E8F': // [LATIN SMALL LETTER Y WITH DOT ABOVE]
+ case L'\u1E99': // [LATIN SMALL LETTER Y WITH RING ABOVE]
+ case L'\u1EF3': // [LATIN SMALL LETTER Y WITH GRAVE]
+ case L'\u1EF5': // [LATIN SMALL LETTER Y WITH DOT BELOW]
+ case L'\u1EF7': // [LATIN SMALL LETTER Y WITH HOOK ABOVE]
+ case L'\u1EF9': // [LATIN SMALL LETTER Y WITH TILDE]
+ case L'\u1EFF': // [LATIN SMALL LETTER Y WITH LOOP]
+ case L'\u24E8': // [CIRCLED LATIN SMALL LETTER Y]
+ case L'\uFF59': // [FULLWIDTH LATIN SMALL LETTER Y]
+ *out = 'y';
+ break;
+ case L'\u24B4': // [PARENTHESIZED LATIN SMALL LETTER Y]
+ *out = '(';
+ *out = 'y';
+ *out = ')';
+ break;
+ case L'\u0179': // [LATIN CAPITAL LETTER Z WITH ACUTE]
+ case L'\u017B': // [LATIN CAPITAL LETTER Z WITH DOT ABOVE]
+ case L'\u017D': // [LATIN CAPITAL LETTER Z WITH CARON]
+ case L'\u01B5': // [LATIN CAPITAL LETTER Z WITH STROKE]
+ case L'\u021C': // [LATIN CAPITAL LETTER YOGH]
+ case L'\u0224': // [LATIN CAPITAL LETTER Z WITH HOOK]
+ case L'\u1D22': // [LATIN LETTER SMALL CAPITAL Z]
+ case L'\u1E90': // [LATIN CAPITAL LETTER Z WITH CIRCUMFLEX]
+ case L'\u1E92': // [LATIN CAPITAL LETTER Z WITH DOT BELOW]
+ case L'\u1E94': // [LATIN CAPITAL LETTER Z WITH LINE BELOW]
+ case L'\u24CF': // [CIRCLED LATIN CAPITAL LETTER Z]
+ case L'\u2C6B': // [LATIN CAPITAL LETTER Z WITH DESCENDER]
+ case L'\uA762': // [LATIN CAPITAL LETTER VISIGOTHIC Z]
+ case L'\uFF3A': // [FULLWIDTH LATIN CAPITAL LETTER Z]
+ *out = 'Z';
+ break;
+ case L'\u017A': // [LATIN SMALL LETTER Z WITH ACUTE]
+ case L'\u017C': // [LATIN SMALL LETTER Z WITH DOT ABOVE]
+ case L'\u017E': // [LATIN SMALL LETTER Z WITH CARON]
+ case L'\u01B6': // [LATIN SMALL LETTER Z WITH STROKE]
+ case L'\u021D': // [LATIN SMALL LETTER YOGH]
+ case L'\u0225': // [LATIN SMALL LETTER Z WITH HOOK]
+ case L'\u0240': // [LATIN SMALL LETTER Z WITH SWASH TAIL]
+ case L'\u0290': // [LATIN SMALL LETTER Z WITH RETROFLEX HOOK]
+ case L'\u0291': // [LATIN SMALL LETTER Z WITH CURL]
+ case L'\u1D76': // [LATIN SMALL LETTER Z WITH MIDDLE TILDE]
+ case L'\u1D8E': // [LATIN SMALL LETTER Z WITH PALATAL HOOK]
+ case L'\u1E91': // [LATIN SMALL LETTER Z WITH CIRCUMFLEX]
+ case L'\u1E93': // [LATIN SMALL LETTER Z WITH DOT BELOW]
+ case L'\u1E95': // [LATIN SMALL LETTER Z WITH LINE BELOW]
+ case L'\u24E9': // [CIRCLED LATIN SMALL LETTER Z]
+ case L'\u2C6C': // [LATIN SMALL LETTER Z WITH DESCENDER]
+ case L'\uA763': // [LATIN SMALL LETTER VISIGOTHIC Z]
+ case L'\uFF5A': // [FULLWIDTH LATIN SMALL LETTER Z]
+ *out = 'z';
+ break;
+ case L'\u24B5': // [PARENTHESIZED LATIN SMALL LETTER Z]
+ *out = '(';
+ *out = 'z';
+ *out = ')';
+ break;
+ case L'\u2070': // [SUPERSCRIPT ZERO]
+ case L'\u2080': // [SUBSCRIPT ZERO]
+ case L'\u24EA': // [CIRCLED DIGIT ZERO]
+ case L'\u24FF': // [NEGATIVE CIRCLED DIGIT ZERO]
+ case L'\uFF10': // [FULLWIDTH DIGIT ZERO]
+ *out = '0';
+ break;
+ case L'\u00B9': // [SUPERSCRIPT ONE]
+ case L'\u2081': // [SUBSCRIPT ONE]
+ case L'\u2460': // [CIRCLED DIGIT ONE]
+ case L'\u24F5': // [DOUBLE CIRCLED DIGIT ONE]
+ case L'\u2776': // [DINGBAT NEGATIVE CIRCLED DIGIT ONE]
+ case L'\u2780': // [DINGBAT CIRCLED SANS-SERIF DIGIT ONE]
+ case L'\u278A': // [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE]
+ case L'\uFF11': // [FULLWIDTH DIGIT ONE]
+ *out = '1';
+ break;
+ case L'\u2488': // [DIGIT ONE FULL STOP]
+ *out = '1';
+ *out = '.';
+ break;
+ case L'\u2474': // [PARENTHESIZED DIGIT ONE]
+ *out = '(';
+ *out = '1';
+ *out = ')';
+ break;
+ case L'\u00B2': // [SUPERSCRIPT TWO]
+ case L'\u2082': // [SUBSCRIPT TWO]
+ case L'\u2461': // [CIRCLED DIGIT TWO]
+ case L'\u24F6': // [DOUBLE CIRCLED DIGIT TWO]
+ case L'\u2777': // [DINGBAT NEGATIVE CIRCLED DIGIT TWO]
+ case L'\u2781': // [DINGBAT CIRCLED SANS-SERIF DIGIT TWO]
+ case L'\u278B': // [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO]
+ case L'\uFF12': // [FULLWIDTH DIGIT TWO]
+ *out = '2';
+ break;
+ case L'\u2489': // [DIGIT TWO FULL STOP]
+ *out = '2';
+ *out = '.';
+ break;
+ case L'\u2475': // [PARENTHESIZED DIGIT TWO]
+ *out = '(';
+ *out = '2';
+ *out = ')';
+ break;
+ case L'\u00B3': // [SUPERSCRIPT THREE]
+ case L'\u2083': // [SUBSCRIPT THREE]
+ case L'\u2462': // [CIRCLED DIGIT THREE]
+ case L'\u24F7': // [DOUBLE CIRCLED DIGIT THREE]
+ case L'\u2778': // [DINGBAT NEGATIVE CIRCLED DIGIT THREE]
+ case L'\u2782': // [DINGBAT CIRCLED SANS-SERIF DIGIT THREE]
+ case L'\u278C': // [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE]
+ case L'\uFF13': // [FULLWIDTH DIGIT THREE]
+ *out = '3';
+ break;
+ case L'\u248A': // [DIGIT THREE FULL STOP]
+ *out = '3';
+ *out = '.';
+ break;
+ case L'\u2476': // [PARENTHESIZED DIGIT THREE]
+ *out = '(';
+ *out = '3';
+ *out = ')';
+ break;
+ case L'\u2074': // [SUPERSCRIPT FOUR]
+ case L'\u2084': // [SUBSCRIPT FOUR]
+ case L'\u2463': // [CIRCLED DIGIT FOUR]
+ case L'\u24F8': // [DOUBLE CIRCLED DIGIT FOUR]
+ case L'\u2779': // [DINGBAT NEGATIVE CIRCLED DIGIT FOUR]
+ case L'\u2783': // [DINGBAT CIRCLED SANS-SERIF DIGIT FOUR]
+ case L'\u278D': // [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR]
+ case L'\uFF14': // [FULLWIDTH DIGIT FOUR]
+ *out = '4';
+ break;
+ case L'\u248B': // [DIGIT FOUR FULL STOP]
+ *out = '4';
+ *out = '.';
+ break;
+ case L'\u2477': // [PARENTHESIZED DIGIT FOUR]
+ *out = '(';
+ *out = '4';
+ *out = ')';
+ break;
+ case L'\u2075': // [SUPERSCRIPT FIVE]
+ case L'\u2085': // [SUBSCRIPT FIVE]
+ case L'\u2464': // [CIRCLED DIGIT FIVE]
+ case L'\u24F9': // [DOUBLE CIRCLED DIGIT FIVE]
+ case L'\u277A': // [DINGBAT NEGATIVE CIRCLED DIGIT FIVE]
+ case L'\u2784': // [DINGBAT CIRCLED SANS-SERIF DIGIT FIVE]
+ case L'\u278E': // [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE]
+ case L'\uFF15': // [FULLWIDTH DIGIT FIVE]
+ *out = '5';
+ break;
+ case L'\u248C': // [DIGIT FIVE FULL STOP]
+ *out = '5';
+ *out = '.';
+ break;
+ case L'\u2478': // [PARENTHESIZED DIGIT FIVE]
+ *out = '(';
+ *out = '5';
+ *out = ')';
+ break;
+ case L'\u2076': // [SUPERSCRIPT SIX]
+ case L'\u2086': // [SUBSCRIPT SIX]
+ case L'\u2465': // [CIRCLED DIGIT SIX]
+ case L'\u24FA': // [DOUBLE CIRCLED DIGIT SIX]
+ case L'\u277B': // [DINGBAT NEGATIVE CIRCLED DIGIT SIX]
+ case L'\u2785': // [DINGBAT CIRCLED SANS-SERIF DIGIT SIX]
+ case L'\u278F': // [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX]
+ case L'\uFF16': // [FULLWIDTH DIGIT SIX]
+ *out = '6';
+ break;
+ case L'\u248D': // [DIGIT SIX FULL STOP]
+ *out = '6';
+ *out = '.';
+ break;
+ case L'\u2479': // [PARENTHESIZED DIGIT SIX]
+ *out = '(';
+ *out = '6';
+ *out = ')';
+ break;
+ case L'\u2077': // [SUPERSCRIPT SEVEN]
+ case L'\u2087': // [SUBSCRIPT SEVEN]
+ case L'\u2466': // [CIRCLED DIGIT SEVEN]
+ case L'\u24FB': // [DOUBLE CIRCLED DIGIT SEVEN]
+ case L'\u277C': // [DINGBAT NEGATIVE CIRCLED DIGIT SEVEN]
+ case L'\u2786': // [DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN]
+ case L'\u2790': // [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN]
+ case L'\uFF17': // [FULLWIDTH DIGIT SEVEN]
+ *out = '7';
+ break;
+ case L'\u248E': // [DIGIT SEVEN FULL STOP]
+ *out = '7';
+ *out = '.';
+ break;
+ case L'\u247A': // [PARENTHESIZED DIGIT SEVEN]
+ *out = '(';
+ *out = '7';
+ *out = ')';
+ break;
+ case L'\u2078': // [SUPERSCRIPT EIGHT]
+ case L'\u2088': // [SUBSCRIPT EIGHT]
+ case L'\u2467': // [CIRCLED DIGIT EIGHT]
+ case L'\u24FC': // [DOUBLE CIRCLED DIGIT EIGHT]
+ case L'\u277D': // [DINGBAT NEGATIVE CIRCLED DIGIT EIGHT]
+ case L'\u2787': // [DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT]
+ case L'\u2791': // [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT]
+ case L'\uFF18': // [FULLWIDTH DIGIT EIGHT]
+ *out = '8';
+ break;
+ case L'\u248F': // [DIGIT EIGHT FULL STOP]
+ *out = '8';
+ *out = '.';
+ break;
+ case L'\u247B': // [PARENTHESIZED DIGIT EIGHT]
+ *out = '(';
+ *out = '8';
+ *out = ')';
+ break;
+ case L'\u2079': // [SUPERSCRIPT NINE]
+ case L'\u2089': // [SUBSCRIPT NINE]
+ case L'\u2468': // [CIRCLED DIGIT NINE]
+ case L'\u24FD': // [DOUBLE CIRCLED DIGIT NINE]
+ case L'\u277E': // [DINGBAT NEGATIVE CIRCLED DIGIT NINE]
+ case L'\u2788': // [DINGBAT CIRCLED SANS-SERIF DIGIT NINE]
+ case L'\u2792': // [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE]
+ case L'\uFF19': // [FULLWIDTH DIGIT NINE]
+ *out = '9';
+ break;
+ case L'\u2490': // [DIGIT NINE FULL STOP]
+ *out = '9';
+ *out = '.';
+ break;
+ case L'\u247C': // [PARENTHESIZED DIGIT NINE]
+ *out = '(';
+ *out = '9';
+ *out = ')';
+ break;
+ case L'\u2469': // [CIRCLED NUMBER TEN]
+ case L'\u24FE': // [DOUBLE CIRCLED NUMBER TEN]
+ case L'\u277F': // [DINGBAT NEGATIVE CIRCLED NUMBER TEN]
+ case L'\u2789': // [DINGBAT CIRCLED SANS-SERIF NUMBER TEN]
+ case L'\u2793': // [DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN]
+ *out = '1';
+ *out = '0';
+ break;
+ case L'\u2491': // [NUMBER TEN FULL STOP]
+ *out = '1';
+ *out = '0';
+ *out = '.';
+ break;
+ case L'\u247D': // [PARENTHESIZED NUMBER TEN]
+ *out = '(';
+ *out = '1';
+ *out = '0';
+ *out = ')';
+ break;
+ case L'\u246A': // [CIRCLED NUMBER ELEVEN]
+ case L'\u24EB': // [NEGATIVE CIRCLED NUMBER ELEVEN]
+ *out = '1';
+ *out = '1';
+ break;
+ case L'\u2492': // [NUMBER ELEVEN FULL STOP]
+ *out = '1';
+ *out = '1';
+ *out = '.';
+ break;
+ case L'\u247E': // [PARENTHESIZED NUMBER ELEVEN]
+ *out = '(';
+ *out = '1';
+ *out = '1';
+ *out = ')';
+ break;
+ case L'\u246B': // [CIRCLED NUMBER TWELVE]
+ case L'\u24EC': // [NEGATIVE CIRCLED NUMBER TWELVE]
+ *out = '1';
+ *out = '2';
+ break;
+ case L'\u2493': // [NUMBER TWELVE FULL STOP]
+ *out = '1';
+ *out = '2';
+ *out = '.';
+ break;
+ case L'\u247F': // [PARENTHESIZED NUMBER TWELVE]
+ *out = '(';
+ *out = '1';
+ *out = '2';
+ *out = ')';
+ break;
+ case L'\u246C': // [CIRCLED NUMBER THIRTEEN]
+ case L'\u24ED': // [NEGATIVE CIRCLED NUMBER THIRTEEN]
+ *out = '1';
+ *out = '3';
+ break;
+ case L'\u2494': // [NUMBER THIRTEEN FULL STOP]
+ *out = '1';
+ *out = '3';
+ *out = '.';
+ break;
+ case L'\u2480': // [PARENTHESIZED NUMBER THIRTEEN]
+ *out = '(';
+ *out = '1';
+ *out = '3';
+ *out = ')';
+ break;
+ case L'\u246D': // [CIRCLED NUMBER FOURTEEN]
+ case L'\u24EE': // [NEGATIVE CIRCLED NUMBER FOURTEEN]
+ *out = '1';
+ *out = '4';
+ break;
+ case L'\u2495': // [NUMBER FOURTEEN FULL STOP]
+ *out = '1';
+ *out = '4';
+ *out = '.';
+ break;
+ case L'\u2481': // [PARENTHESIZED NUMBER FOURTEEN]
+ *out = '(';
+ *out = '1';
+ *out = '4';
+ *out = ')';
+ break;
+ case L'\u246E': // [CIRCLED NUMBER FIFTEEN]
+ case L'\u24EF': // [NEGATIVE CIRCLED NUMBER FIFTEEN]
+ *out = '1';
+ *out = '5';
+ break;
+ case L'\u2496': // [NUMBER FIFTEEN FULL STOP]
+ *out = '1';
+ *out = '5';
+ *out = '.';
+ break;
+ case L'\u2482': // [PARENTHESIZED NUMBER FIFTEEN]
+ *out = '(';
+ *out = '1';
+ *out = '5';
+ *out = ')';
+ break;
+ case L'\u246F': // [CIRCLED NUMBER SIXTEEN]
+ case L'\u24F0': // [NEGATIVE CIRCLED NUMBER SIXTEEN]
+ *out = '1';
+ *out = '6';
+ break;
+ case L'\u2497': // [NUMBER SIXTEEN FULL STOP]
+ *out = '1';
+ *out = '6';
+ *out = '.';
+ break;
+ case L'\u2483': // [PARENTHESIZED NUMBER SIXTEEN]
+ *out = '(';
+ *out = '1';
+ *out = '6';
+ *out = ')';
+ break;
+ case L'\u2470': // [CIRCLED NUMBER SEVENTEEN]
+ case L'\u24F1': // [NEGATIVE CIRCLED NUMBER SEVENTEEN]
+ *out = '1';
+ *out = '7';
+ break;
+ case L'\u2498': // [NUMBER SEVENTEEN FULL STOP]
+ *out = '1';
+ *out = '7';
+ *out = '.';
+ break;
+ case L'\u2484': // [PARENTHESIZED NUMBER SEVENTEEN]
+ *out = '(';
+ *out = '1';
+ *out = '7';
+ *out = ')';
+ break;
+ case L'\u2471': // [CIRCLED NUMBER EIGHTEEN]
+ case L'\u24F2': // [NEGATIVE CIRCLED NUMBER EIGHTEEN]
+ *out = '1';
+ *out = '8';
+ break;
+ case L'\u2499': // [NUMBER EIGHTEEN FULL STOP]
+ *out = '1';
+ *out = '8';
+ *out = '.';
+ break;
+ case L'\u2485': // [PARENTHESIZED NUMBER EIGHTEEN]
+ *out = '(';
+ *out = '1';
+ *out = '8';
+ *out = ')';
+ break;
+ case L'\u2472': // [CIRCLED NUMBER NINETEEN]
+ case L'\u24F3': // [NEGATIVE CIRCLED NUMBER NINETEEN]
+ *out = '1';
+ *out = '9';
+ break;
+ case L'\u249A': // [NUMBER NINETEEN FULL STOP]
+ *out = '1';
+ *out = '9';
+ *out = '.';
+ break;
+ case L'\u2486': // [PARENTHESIZED NUMBER NINETEEN]
+ *out = '(';
+ *out = '1';
+ *out = '9';
+ *out = ')';
+ break;
+ case L'\u2473': // [CIRCLED NUMBER TWENTY]
+ case L'\u24F4': // [NEGATIVE CIRCLED NUMBER TWENTY]
+ *out = '2';
+ *out = '0';
+ break;
+ case L'\u249B': // [NUMBER TWENTY FULL STOP]
+ *out = '2';
+ *out = '0';
+ *out = '.';
+ break;
+ case L'\u2487': // [PARENTHESIZED NUMBER TWENTY]
+ *out = '(';
+ *out = '2';
+ *out = '0';
+ *out = ')';
+ break;
+ case L'\u00AB': // [LEFT-POINTING DOUBLE ANGLE QUOTATION MARK]
+ case L'\u00BB': // [RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK]
+ case L'\u201C': // [LEFT DOUBLE QUOTATION MARK]
+ case L'\u201D': // [RIGHT DOUBLE QUOTATION MARK]
+ case L'\u201E': // [DOUBLE LOW-9 QUOTATION MARK]
+ case L'\u2033': // [DOUBLE PRIME]
+ case L'\u2036': // [REVERSED DOUBLE PRIME]
+ case L'\u275D': // [HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT]
+ case L'\u275E': // [HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT]
+ case L'\u276E': // [HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT]
+ case L'\u276F': // [HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT]
+ case L'\uFF02': // [FULLWIDTH QUOTATION MARK]
+ *out = '"';
+ break;
+ case L'\u2018': // [LEFT SINGLE QUOTATION MARK]
+ case L'\u2019': // [RIGHT SINGLE QUOTATION MARK]
+ case L'\u201A': // [SINGLE LOW-9 QUOTATION MARK]
+ case L'\u201B': // [SINGLE HIGH-REVERSED-9 QUOTATION MARK]
+ case L'\u2032': // [PRIME]
+ case L'\u2035': // [REVERSED PRIME]
+ case L'\u2039': // [SINGLE LEFT-POINTING ANGLE QUOTATION MARK]
+ case L'\u203A': // [SINGLE RIGHT-POINTING ANGLE QUOTATION MARK]
+ case L'\u275B': // [HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT]
+ case L'\u275C': // [HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT]
+ case L'\uFF07': // [FULLWIDTH APOSTROPHE]
+ *out = '\'';
+ break;
+ case L'\u2010': // [HYPHEN]
+ case L'\u2011': // [NON-BREAKING HYPHEN]
+ case L'\u2012': // [FIGURE DASH]
+ case L'\u2013': // [EN DASH]
+ case L'\u2014': // [EM DASH]
+ case L'\u207B': // [SUPERSCRIPT MINUS]
+ case L'\u208B': // [SUBSCRIPT MINUS]
+ case L'\uFF0D': // [FULLWIDTH HYPHEN-MINUS]
+ *out = '-';
+ break;
+ case L'\u2045': // [LEFT SQUARE BRACKET WITH QUILL]
+ case L'\u2772': // [LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT]
+ case L'\uFF3B': // [FULLWIDTH LEFT SQUARE BRACKET]
+ *out = '[';
+ break;
+ case L'\u2046': // [RIGHT SQUARE BRACKET WITH QUILL]
+ case L'\u2773': // [LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT]
+ case L'\uFF3D': // [FULLWIDTH RIGHT SQUARE BRACKET]
+ *out = ']';
+ break;
+ case L'\u207D': // [SUPERSCRIPT LEFT PARENTHESIS]
+ case L'\u208D': // [SUBSCRIPT LEFT PARENTHESIS]
+ case L'\u2768': // [MEDIUM LEFT PARENTHESIS ORNAMENT]
+ case L'\u276A': // [MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT]
+ case L'\uFF08': // [FULLWIDTH LEFT PARENTHESIS]
+ *out = '(';
+ break;
+ case L'\u2E28': // [LEFT DOUBLE PARENTHESIS]
+ *out = '(';
+ *out = '(';
+ break;
+ case L'\u207E': // [SUPERSCRIPT RIGHT PARENTHESIS]
+ case L'\u208E': // [SUBSCRIPT RIGHT PARENTHESIS]
+ case L'\u2769': // [MEDIUM RIGHT PARENTHESIS ORNAMENT]
+ case L'\u276B': // [MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT]
+ case L'\uFF09': // [FULLWIDTH RIGHT PARENTHESIS]
+ *out = ')';
+ break;
+ case L'\u2E29': // [RIGHT DOUBLE PARENTHESIS]
+ *out = ')';
+ *out = ')';
+ break;
+ case L'\u276C': // [MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT]
+ case L'\u2770': // [HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT]
+ case L'\uFF1C': // [FULLWIDTH LESS-THAN SIGN]
+ *out = '<';
+ break;
+ case L'\u276D': // [MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT]
+ case L'\u2771': // [HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT]
+ case L'\uFF1E': // [FULLWIDTH GREATER-THAN SIGN]
+ *out = '>';
+ break;
+ case L'\u2774': // [MEDIUM LEFT CURLY BRACKET ORNAMENT]
+ case L'\uFF5B': // [FULLWIDTH LEFT CURLY BRACKET]
+ *out = '{';
+ break;
+ case L'\u2775': // [MEDIUM RIGHT CURLY BRACKET ORNAMENT]
+ case L'\uFF5D': // [FULLWIDTH RIGHT CURLY BRACKET]
+ *out = '}';
+ break;
+ case L'\u207A': // [SUPERSCRIPT PLUS SIGN]
+ case L'\u208A': // [SUBSCRIPT PLUS SIGN]
+ case L'\uFF0B': // [FULLWIDTH PLUS SIGN]
+ *out = '+';
+ break;
+ case L'\u207C': // [SUPERSCRIPT EQUALS SIGN]
+ case L'\u208C': // [SUBSCRIPT EQUALS SIGN]
+ case L'\uFF1D': // [FULLWIDTH EQUALS SIGN]
+ *out = '=';
+ break;
+ case L'\uFF01': // [FULLWIDTH EXCLAMATION MARK]
+ *out = '!';
+ break;
+ case L'\u203C': // [DOUBLE EXCLAMATION MARK]
+ *out = '!';
+ *out = '!';
+ break;
+ case L'\u2049': // [EXCLAMATION QUESTION MARK]
+ *out = '!';
+ *out = '?';
+ break;
+ case L'\uFF03': // [FULLWIDTH NUMBER SIGN]
+ *out = '#';
+ break;
+ case L'\uFF04': // [FULLWIDTH DOLLAR SIGN]
+ *out = '$';
+ break;
+ case L'\u2052': // [COMMERCIAL MINUS SIGN]
+ case L'\uFF05': // [FULLWIDTH PERCENT SIGN]
+ *out = '%';
+ break;
+ case L'\uFF06': // [FULLWIDTH AMPERSAND]
+ *out = '&';
+ break;
+ case L'\u204E': // [LOW ASTERISK]
+ case L'\uFF0A': // [FULLWIDTH ASTERISK]
+ *out = '*';
+ break;
+ case L'\uFF0C': // [FULLWIDTH COMMA]
+ *out = ',';
+ break;
+ case L'\uFF0E': // [FULLWIDTH FULL STOP]
+ *out = '.';
+ break;
+ case L'\u2044': // [FRACTION SLASH]
+ case L'\uFF0F': // [FULLWIDTH SOLIDUS]
+ *out = '/';
+ break;
+ case L'\uFF1A': // [FULLWIDTH COLON]
+ *out = ':';
+ break;
+ case L'\u204F': // [REVERSED SEMICOLON]
+ case L'\uFF1B': // [FULLWIDTH SEMICOLON]
+ *out = ';';
+ break;
+ case L'\uFF1F': // [FULLWIDTH QUESTION MARK]
+ *out = '?';
+ break;
+ case L'\u2047': // [DOUBLE QUESTION MARK]
+ *out = '?';
+ *out = '?';
+ break;
+ case L'\u2048': // [QUESTION EXCLAMATION MARK]
+ *out = '?';
+ *out = '!';
+ break;
+ case L'\uFF20': // [FULLWIDTH COMMERCIAL AT]
+ *out = '@';
+ break;
+ case L'\uFF3C': // [FULLWIDTH REVERSE SOLIDUS]
+ *out = '\\';
+ break;
+ case L'\u2038': // [CARET]
+ case L'\uFF3E': // [FULLWIDTH CIRCUMFLEX ACCENT]
+ *out = '^';
+ break;
+ case L'\uFF3F': // [FULLWIDTH LOW LINE]
+ *out = '_';
+ break;
+ case L'\u2053': // [SWUNG DASH]
+ case L'\uFF5E': // [FULLWIDTH TILDE]
+ *out = '~';
+ break;
+ default:
+ *out = c;
+ break;
+ }
+ }
+}
+
+namespace Slic3r {
+
+std::string fold_utf8_to_ascii(const std::string &src)
+{
+ std::wstring wstr = boost::locale::conv::utf_to_utf<wchar_t>(src.c_str(), src.c_str() + src.size());
+ std::wstring dst;
+ dst.reserve(wstr.size());
+ auto out = std::back_insert_iterator<std::wstring>(dst);
+ for (wchar_t c : wstr)
+ fold_to_ascii(c, out);
+ return boost::locale::conv::utf_to_utf<char>(dst.c_str(), dst.c_str() + dst.size());
+}
+
+std::string fold_utf8_to_ascii(const char *src)
+{
+ std::wstring wstr = boost::locale::conv::utf_to_utf<wchar_t>(src, src + strlen(src));
+ std::wstring dst;
+ dst.reserve(wstr.size());
+ auto out = std::back_insert_iterator<std::wstring>(dst);
+ for (wchar_t c : wstr)
+ fold_to_ascii(c, out);
+ return boost::locale::conv::utf_to_utf<char>(dst.c_str(), dst.c_str() + dst.size());
+}
+
+}; // namespace Slic3r
diff --git a/src/slic3r/Utils/ASCIIFolding.hpp b/src/slic3r/Utils/ASCIIFolding.hpp
new file mode 100644
index 000000000..55f56482d
--- /dev/null
+++ b/src/slic3r/Utils/ASCIIFolding.hpp
@@ -0,0 +1,15 @@
+#ifndef slic3r_ASCIIFolding_hpp_
+#define slic3r_ASCIIFolding_hpp_
+
+#include <string>
+
+namespace Slic3r {
+
+// If possible, remove accents from accented latin characters.
+// This function is useful for generating file names to be processed by legacy firmwares.
+extern std::string fold_utf8_to_ascii(const char *src);
+extern std::string fold_utf8_to_ascii(const std::string &src);
+
+}; // namespace Slic3r
+
+#endif /* slic3r_ASCIIFolding_hpp_ */
diff --git a/src/slic3r/Utils/Bonjour.cpp b/src/slic3r/Utils/Bonjour.cpp
new file mode 100644
index 000000000..09d9b5873
--- /dev/null
+++ b/src/slic3r/Utils/Bonjour.cpp
@@ -0,0 +1,781 @@
+#include "Bonjour.hpp"
+
+#include <cstdint>
+#include <algorithm>
+#include <array>
+#include <vector>
+#include <string>
+#include <random>
+#include <thread>
+#include <boost/optional.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/endian/conversion.hpp>
+#include <boost/asio.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/format.hpp>
+
+using boost::optional;
+using boost::system::error_code;
+namespace endian = boost::endian;
+namespace asio = boost::asio;
+using boost::asio::ip::udp;
+
+
+namespace Slic3r {
+
+
+// Minimal implementation of a MDNS/DNS-SD client
+// This implementation is extremely simple, only the bits that are useful
+// for basic MDNS discovery of OctoPi devices are present.
+// However, the bits that are present are implemented with security in mind.
+// Only fully correct DNS replies are allowed through.
+// While decoding the decoder will bail the moment it encounters anything fishy.
+// At least that's the idea. To help prove this is actually the case,
+// the implementations has been tested with AFL.
+
+
+struct DnsName: public std::string
+{
+ enum
+ {
+ MAX_RECURSION = 10, // Keep this low
+ };
+
+ static optional<DnsName> decode(const std::vector<char> &buffer, size_t &offset, unsigned depth = 0)
+ {
+ // Check offset sanity:
+ if (offset + 1 >= buffer.size()) {
+ return boost::none;
+ }
+
+ // Check for recursion depth to prevent parsing names that are nested too deeply or end up cyclic:
+ if (depth >= MAX_RECURSION) {
+ return boost::none;
+ }
+
+ DnsName res;
+ const size_t bsize = buffer.size();
+
+ while (true) {
+ const char* ptr = buffer.data() + offset;
+ unsigned len = static_cast<unsigned char>(*ptr);
+ if (len & 0xc0) {
+ // This is a recursive label
+ unsigned len_2 = static_cast<unsigned char>(ptr[1]);
+ size_t pointer = (len & 0x3f) << 8 | len_2;
+ const auto nested = decode(buffer, pointer, depth + 1);
+ if (!nested) {
+ return boost::none;
+ } else {
+ if (res.size() > 0) {
+ res.push_back('.');
+ }
+ res.append(*nested);
+ offset += 2;
+ return std::move(res);
+ }
+ } else if (len == 0) {
+ // This is a name terminator
+ offset++;
+ break;
+ } else {
+ // This is a regular label
+ len &= 0x3f;
+ if (len + offset + 1 >= bsize) {
+ return boost::none;
+ }
+
+ res.reserve(len);
+ if (res.size() > 0) {
+ res.push_back('.');
+ }
+
+ ptr++;
+ for (const auto end = ptr + len; ptr < end; ptr++) {
+ char c = *ptr;
+ if (c >= 0x20 && c <= 0x7f) {
+ res.push_back(c);
+ } else {
+ return boost::none;
+ }
+ }
+
+ offset += len + 1;
+ }
+ }
+
+ if (res.size() > 0) {
+ return std::move(res);
+ } else {
+ return boost::none;
+ }
+ }
+};
+
+struct DnsHeader
+{
+ uint16_t id;
+ uint16_t flags;
+ uint16_t qdcount;
+ uint16_t ancount;
+ uint16_t nscount;
+ uint16_t arcount;
+
+ enum
+ {
+ SIZE = 12,
+ };
+
+ static DnsHeader decode(const std::vector<char> &buffer) {
+ DnsHeader res;
+ const uint16_t *data_16 = reinterpret_cast<const uint16_t*>(buffer.data());
+ res.id = endian::big_to_native(data_16[0]);
+ res.flags = endian::big_to_native(data_16[1]);
+ res.qdcount = endian::big_to_native(data_16[2]);
+ res.ancount = endian::big_to_native(data_16[3]);
+ res.nscount = endian::big_to_native(data_16[4]);
+ res.arcount = endian::big_to_native(data_16[5]);
+ return res;
+ }
+
+ uint32_t rrcount() const {
+ return ancount + nscount + arcount;
+ }
+};
+
+struct DnsQuestion
+{
+ enum
+ {
+ MIN_SIZE = 5,
+ };
+
+ DnsName name;
+ uint16_t type;
+ uint16_t qclass;
+
+ DnsQuestion() :
+ type(0),
+ qclass(0)
+ {}
+
+ static optional<DnsQuestion> decode(const std::vector<char> &buffer, size_t &offset)
+ {
+ auto qname = DnsName::decode(buffer, offset);
+ if (!qname) {
+ return boost::none;
+ }
+
+ DnsQuestion res;
+ res.name = std::move(*qname);
+ const uint16_t *data_16 = reinterpret_cast<const uint16_t*>(buffer.data() + offset);
+ res.type = endian::big_to_native(data_16[0]);
+ res.qclass = endian::big_to_native(data_16[1]);
+
+ offset += 4;
+ return std::move(res);
+ }
+};
+
+struct DnsResource
+{
+ DnsName name;
+ uint16_t type;
+ uint16_t rclass;
+ uint32_t ttl;
+ std::vector<char> data;
+
+ DnsResource() :
+ type(0),
+ rclass(0),
+ ttl(0)
+ {}
+
+ static optional<DnsResource> decode(const std::vector<char> &buffer, size_t &offset, size_t &dataoffset)
+ {
+ const size_t bsize = buffer.size();
+ if (offset + 1 >= bsize) {
+ return boost::none;
+ }
+
+ auto rname = DnsName::decode(buffer, offset);
+ if (!rname) {
+ return boost::none;
+ }
+
+ if (offset + 10 >= bsize) {
+ return boost::none;
+ }
+
+ DnsResource res;
+ res.name = std::move(*rname);
+ const uint16_t *data_16 = reinterpret_cast<const uint16_t*>(buffer.data() + offset);
+ res.type = endian::big_to_native(data_16[0]);
+ res.rclass = endian::big_to_native(data_16[1]);
+ res.ttl = endian::big_to_native(*reinterpret_cast<const uint32_t*>(data_16 + 2));
+ uint16_t rdlength = endian::big_to_native(data_16[4]);
+
+ offset += 10;
+ if (offset + rdlength > bsize) {
+ return boost::none;
+ }
+
+ dataoffset = offset;
+ res.data = std::move(std::vector<char>(buffer.begin() + offset, buffer.begin() + offset + rdlength));
+ offset += rdlength;
+
+ return std::move(res);
+ }
+};
+
+struct DnsRR_A
+{
+ enum { TAG = 0x1 };
+
+ asio::ip::address_v4 ip;
+
+ static void decode(optional<DnsRR_A> &result, const DnsResource &rr)
+ {
+ if (rr.data.size() == 4) {
+ DnsRR_A res;
+ const uint32_t ip = endian::big_to_native(*reinterpret_cast<const uint32_t*>(rr.data.data()));
+ res.ip = asio::ip::address_v4(ip);
+ result = std::move(res);
+ }
+ }
+};
+
+struct DnsRR_AAAA
+{
+ enum { TAG = 0x1c };
+
+ asio::ip::address_v6 ip;
+
+ static void decode(optional<DnsRR_AAAA> &result, const DnsResource &rr)
+ {
+ if (rr.data.size() == 16) {
+ DnsRR_AAAA res;
+ std::array<unsigned char, 16> ip;
+ std::copy_n(rr.data.begin(), 16, ip.begin());
+ res.ip = asio::ip::address_v6(ip);
+ result = std::move(res);
+ }
+ }
+};
+
+struct DnsRR_SRV
+{
+ enum
+ {
+ TAG = 0x21,
+ MIN_SIZE = 8,
+ };
+
+ uint16_t priority;
+ uint16_t weight;
+ uint16_t port;
+ DnsName hostname;
+
+ static optional<DnsRR_SRV> decode(const std::vector<char> &buffer, const DnsResource &rr, size_t dataoffset)
+ {
+ if (rr.data.size() < MIN_SIZE) {
+ return boost::none;
+ }
+
+ DnsRR_SRV res;
+
+ const uint16_t *data_16 = reinterpret_cast<const uint16_t*>(rr.data.data());
+ res.priority = endian::big_to_native(data_16[0]);
+ res.weight = endian::big_to_native(data_16[1]);
+ res.port = endian::big_to_native(data_16[2]);
+
+ size_t offset = dataoffset + 6;
+ auto hostname = DnsName::decode(buffer, offset);
+
+ if (hostname) {
+ res.hostname = std::move(*hostname);
+ return std::move(res);
+ } else {
+ return boost::none;
+ }
+ }
+};
+
+struct DnsRR_TXT
+{
+ enum
+ {
+ TAG = 0x10,
+ };
+
+ std::vector<std::string> values;
+
+ static optional<DnsRR_TXT> decode(const DnsResource &rr)
+ {
+ const size_t size = rr.data.size();
+ if (size < 2) {
+ return boost::none;
+ }
+
+ DnsRR_TXT res;
+
+ for (auto it = rr.data.begin(); it != rr.data.end(); ) {
+ unsigned val_size = static_cast<unsigned char>(*it);
+ if (val_size == 0 || it + val_size >= rr.data.end()) {
+ return boost::none;
+ }
+ ++it;
+
+ std::string value(val_size, ' ');
+ std::copy(it, it + val_size, value.begin());
+ res.values.push_back(std::move(value));
+
+ it += val_size;
+ }
+
+ return std::move(res);
+ }
+};
+
+struct DnsSDPair
+{
+ optional<DnsRR_SRV> srv;
+ optional<DnsRR_TXT> txt;
+};
+
+struct DnsSDMap : public std::map<std::string, DnsSDPair>
+{
+ void insert_srv(std::string &&name, DnsRR_SRV &&srv)
+ {
+ auto hit = this->find(name);
+ if (hit != this->end()) {
+ hit->second.srv = std::move(srv);
+ } else {
+ DnsSDPair pair;
+ pair.srv = std::move(srv);
+ this->insert(std::make_pair(std::move(name), std::move(pair)));
+ }
+ }
+
+ void insert_txt(std::string &&name, DnsRR_TXT &&txt)
+ {
+ auto hit = this->find(name);
+ if (hit != this->end()) {
+ hit->second.txt = std::move(txt);
+ } else {
+ DnsSDPair pair;
+ pair.txt = std::move(txt);
+ this->insert(std::make_pair(std::move(name), std::move(pair)));
+ }
+ }
+};
+
+struct DnsMessage
+{
+ enum
+ {
+ MAX_SIZE = 4096,
+ MAX_ANS = 30,
+ };
+
+ DnsHeader header;
+ optional<DnsQuestion> question;
+
+ optional<DnsRR_A> rr_a;
+ optional<DnsRR_AAAA> rr_aaaa;
+ std::vector<DnsRR_SRV> rr_srv;
+
+ DnsSDMap sdmap;
+
+ static optional<DnsMessage> decode(const std::vector<char> &buffer, optional<uint16_t> id_wanted = boost::none)
+ {
+ const auto size = buffer.size();
+ if (size < DnsHeader::SIZE + DnsQuestion::MIN_SIZE || size > MAX_SIZE) {
+ return boost::none;
+ }
+
+ DnsMessage res;
+ res.header = DnsHeader::decode(buffer);
+
+ if (id_wanted && *id_wanted != res.header.id) {
+ return boost::none;
+ }
+
+ if (res.header.qdcount > 1 || res.header.ancount > MAX_ANS) {
+ return boost::none;
+ }
+
+ size_t offset = DnsHeader::SIZE;
+ if (res.header.qdcount == 1) {
+ res.question = DnsQuestion::decode(buffer, offset);
+ }
+
+ for (unsigned i = 0; i < res.header.rrcount(); i++) {
+ size_t dataoffset = 0;
+ auto rr = DnsResource::decode(buffer, offset, dataoffset);
+ if (!rr) {
+ return boost::none;
+ } else {
+ res.parse_rr(buffer, std::move(*rr), dataoffset);
+ }
+ }
+
+ return std::move(res);
+ }
+private:
+ void parse_rr(const std::vector<char> &buffer, DnsResource &&rr, size_t dataoffset)
+ {
+ switch (rr.type) {
+ case DnsRR_A::TAG: DnsRR_A::decode(this->rr_a, rr); break;
+ case DnsRR_AAAA::TAG: DnsRR_AAAA::decode(this->rr_aaaa, rr); break;
+ case DnsRR_SRV::TAG: {
+ auto srv = DnsRR_SRV::decode(buffer, rr, dataoffset);
+ if (srv) { this->sdmap.insert_srv(std::move(rr.name), std::move(*srv)); }
+ break;
+ }
+ case DnsRR_TXT::TAG: {
+ auto txt = DnsRR_TXT::decode(rr);
+ if (txt) { this->sdmap.insert_txt(std::move(rr.name), std::move(*txt)); }
+ break;
+ }
+ }
+ }
+};
+
+std::ostream& operator<<(std::ostream &os, const DnsMessage &msg)
+{
+ os << "DnsMessage(ID: " << msg.header.id << ", "
+ << "Q: " << (msg.question ? msg.question->name.c_str() : "none") << ", "
+ << "A: " << (msg.rr_a ? msg.rr_a->ip.to_string() : "none") << ", "
+ << "AAAA: " << (msg.rr_aaaa ? msg.rr_aaaa->ip.to_string() : "none") << ", "
+ << "services: [";
+
+ enum { SRV_PRINT_MAX = 3 };
+ unsigned i = 0;
+ for (const auto &sdpair : msg.sdmap) {
+ os << sdpair.first << ", ";
+
+ if (++i >= SRV_PRINT_MAX) {
+ os << "...";
+ break;
+ }
+ }
+
+ os << "])";
+
+ return os;
+}
+
+
+struct BonjourRequest
+{
+ static const asio::ip::address_v4 MCAST_IP4;
+ static const uint16_t MCAST_PORT;
+
+ uint16_t id;
+ std::vector<char> data;
+
+ static optional<BonjourRequest> make(const std::string &service, const std::string &protocol);
+
+private:
+ BonjourRequest(uint16_t id, std::vector<char> &&data) :
+ id(id),
+ data(std::move(data))
+ {}
+};
+
+const asio::ip::address_v4 BonjourRequest::MCAST_IP4{0xe00000fb};
+const uint16_t BonjourRequest::MCAST_PORT = 5353;
+
+optional<BonjourRequest> BonjourRequest::make(const std::string &service, const std::string &protocol)
+{
+ if (service.size() > 15 || protocol.size() > 15) {
+ return boost::none;
+ }
+
+ std::random_device dev;
+ std::uniform_int_distribution<uint16_t> dist;
+ uint16_t id = dist(dev);
+ uint16_t id_big = endian::native_to_big(id);
+ const char *id_char = reinterpret_cast<char*>(&id_big);
+
+ std::vector<char> data;
+ data.reserve(service.size() + 18);
+
+ // Add the transaction ID
+ data.push_back(id_char[0]);
+ data.push_back(id_char[1]);
+
+ // Add metadata
+ static const unsigned char rq_meta[] = {
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ };
+ std::copy(rq_meta, rq_meta + sizeof(rq_meta), std::back_inserter(data));
+
+ // Add PTR query name
+ data.push_back(service.size() + 1);
+ data.push_back('_');
+ data.insert(data.end(), service.begin(), service.end());
+ data.push_back(protocol.size() + 1);
+ data.push_back('_');
+ data.insert(data.end(), protocol.begin(), protocol.end());
+
+ // Add the rest of PTR record
+ static const unsigned char ptr_tail[] = {
+ 0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x00, 0x00, 0x0c, 0x00, 0xff,
+ };
+ std::copy(ptr_tail, ptr_tail + sizeof(ptr_tail), std::back_inserter(data));
+
+ return BonjourRequest(id, std::move(data));
+}
+
+
+// API - private part
+
+struct Bonjour::priv
+{
+ const std::string service;
+ const std::string protocol;
+ const std::string service_dn;
+ unsigned timeout;
+ unsigned retries;
+ uint16_t rq_id;
+
+ std::vector<char> buffer;
+ std::thread io_thread;
+ Bonjour::ReplyFn replyfn;
+ Bonjour::CompleteFn completefn;
+
+ priv(std::string service, std::string protocol);
+
+ std::string strip_service_dn(const std::string &service_name) const;
+ void udp_receive(udp::endpoint from, size_t bytes);
+ void lookup_perform();
+};
+
+Bonjour::priv::priv(std::string service, std::string protocol) :
+ service(std::move(service)),
+ protocol(std::move(protocol)),
+ service_dn((boost::format("_%1%._%2%.local") % this->service % this->protocol).str()),
+ timeout(10),
+ retries(1),
+ rq_id(0)
+{
+ buffer.resize(DnsMessage::MAX_SIZE);
+}
+
+std::string Bonjour::priv::strip_service_dn(const std::string &service_name) const
+{
+ if (service_name.size() <= service_dn.size()) {
+ return service_name;
+ }
+
+ auto needle = service_name.rfind(service_dn);
+ if (needle == service_name.size() - service_dn.size()) {
+ return service_name.substr(0, needle - 1);
+ } else {
+ return service_name;
+ }
+}
+
+void Bonjour::priv::udp_receive(udp::endpoint from, size_t bytes)
+{
+ if (bytes == 0 || !replyfn) {
+ return;
+ }
+
+ buffer.resize(bytes);
+ const auto dns_msg = DnsMessage::decode(buffer, rq_id);
+ if (dns_msg) {
+ asio::ip::address ip = from.address();
+ if (dns_msg->rr_a) { ip = dns_msg->rr_a->ip; }
+ else if (dns_msg->rr_aaaa) { ip = dns_msg->rr_aaaa->ip; }
+
+ for (const auto &sdpair : dns_msg->sdmap) {
+ if (! sdpair.second.srv) {
+ continue;
+ }
+
+ const auto &srv = *sdpair.second.srv;
+ auto service_name = strip_service_dn(sdpair.first);
+
+ std::string path;
+ std::string version;
+
+ if (sdpair.second.txt) {
+ static const std::string tag_path = "path=";
+ static const std::string tag_version = "version=";
+
+ for (const auto &value : sdpair.second.txt->values) {
+ if (value.size() > tag_path.size() && value.compare(0, tag_path.size(), tag_path) == 0) {
+ path = std::move(value.substr(tag_path.size()));
+ } else if (value.size() > tag_version.size() && value.compare(0, tag_version.size(), tag_version) == 0) {
+ version = std::move(value.substr(tag_version.size()));
+ }
+ }
+ }
+
+ BonjourReply reply(ip, srv.port, std::move(service_name), srv.hostname, std::move(path), std::move(version));
+ replyfn(std::move(reply));
+ }
+ }
+}
+
+void Bonjour::priv::lookup_perform()
+{
+ const auto brq = BonjourRequest::make(service, protocol);
+ if (!brq) {
+ return;
+ }
+
+ auto self = this;
+ rq_id = brq->id;
+
+ try {
+ boost::asio::io_service io_service;
+ udp::socket socket(io_service);
+ socket.open(udp::v4());
+ socket.set_option(udp::socket::reuse_address(true));
+ udp::endpoint mcast(BonjourRequest::MCAST_IP4, BonjourRequest::MCAST_PORT);
+ socket.send_to(asio::buffer(brq->data), mcast);
+
+ bool expired = false;
+ bool retry = false;
+ asio::deadline_timer timer(io_service);
+ retries--;
+ std::function<void(const error_code &)> timer_handler = [&](const error_code &error) {
+ if (retries == 0 || error) {
+ expired = true;
+ if (self->completefn) {
+ self->completefn();
+ }
+ } else {
+ retry = true;
+ retries--;
+ timer.expires_from_now(boost::posix_time::seconds(timeout));
+ timer.async_wait(timer_handler);
+ }
+ };
+
+ timer.expires_from_now(boost::posix_time::seconds(timeout));
+ timer.async_wait(timer_handler);
+
+ udp::endpoint recv_from;
+ const auto recv_handler = [&](const error_code &error, size_t bytes) {
+ if (!error) { self->udp_receive(recv_from, bytes); }
+ };
+ socket.async_receive_from(asio::buffer(buffer, buffer.size()), recv_from, recv_handler);
+
+ while (io_service.run_one()) {
+ if (expired) {
+ socket.cancel();
+ } else if (retry) {
+ retry = false;
+ socket.send_to(asio::buffer(brq->data), mcast);
+ } else {
+ buffer.resize(DnsMessage::MAX_SIZE);
+ socket.async_receive_from(asio::buffer(buffer, buffer.size()), recv_from, recv_handler);
+ }
+ }
+ } catch (std::exception& e) {
+ }
+}
+
+
+// API - public part
+
+BonjourReply::BonjourReply(boost::asio::ip::address ip, uint16_t port, std::string service_name, std::string hostname, std::string path, std::string version) :
+ ip(std::move(ip)),
+ port(port),
+ service_name(std::move(service_name)),
+ hostname(std::move(hostname)),
+ path(path.empty() ? std::move(std::string("/")) : std::move(path)),
+ version(version.empty() ? std::move(std::string("Unknown")) : std::move(version))
+{
+ std::string proto;
+ std::string port_suffix;
+ if (port == 443) { proto = "https://"; }
+ if (port != 443 && port != 80) { port_suffix = std::to_string(port).insert(0, 1, ':'); }
+ if (this->path[0] != '/') { this->path.insert(0, 1, '/'); }
+ full_address = proto + ip.to_string() + port_suffix;
+ if (this->path != "/") { full_address += path; }
+}
+
+bool BonjourReply::operator==(const BonjourReply &other) const
+{
+ return this->full_address == other.full_address
+ && this->service_name == other.service_name;
+}
+
+bool BonjourReply::operator<(const BonjourReply &other) const
+{
+ if (this->ip != other.ip) {
+ // So that the common case doesn't involve string comparison
+ return this->ip < other.ip;
+ } else {
+ auto cmp = this->full_address.compare(other.full_address);
+ return cmp != 0 ? cmp < 0 : this->service_name < other.service_name;
+ }
+}
+
+std::ostream& operator<<(std::ostream &os, const BonjourReply &reply)
+{
+ os << "BonjourReply(" << reply.ip.to_string() << ", " << reply.service_name << ", "
+ << reply.hostname << ", " << reply.path << ", " << reply.version << ")";
+ return os;
+}
+
+
+Bonjour::Bonjour(std::string service, std::string protocol) :
+ p(new priv(std::move(service), std::move(protocol)))
+{}
+
+Bonjour::Bonjour(Bonjour &&other) : p(std::move(other.p)) {}
+
+Bonjour::~Bonjour()
+{
+ if (p && p->io_thread.joinable()) {
+ p->io_thread.detach();
+ }
+}
+
+Bonjour& Bonjour::set_timeout(unsigned timeout)
+{
+ if (p) { p->timeout = timeout; }
+ return *this;
+}
+
+Bonjour& Bonjour::set_retries(unsigned retries)
+{
+ if (p && retries > 0) { p->retries = retries; }
+ return *this;
+}
+
+Bonjour& Bonjour::on_reply(ReplyFn fn)
+{
+ if (p) { p->replyfn = std::move(fn); }
+ return *this;
+}
+
+Bonjour& Bonjour::on_complete(CompleteFn fn)
+{
+ if (p) { p->completefn = std::move(fn); }
+ return *this;
+}
+
+Bonjour::Ptr Bonjour::lookup()
+{
+ auto self = std::make_shared<Bonjour>(std::move(*this));
+
+ if (self->p) {
+ auto io_thread = std::thread([self]() {
+ self->p->lookup_perform();
+ });
+ self->p->io_thread = std::move(io_thread);
+ }
+
+ return self;
+}
+
+
+}
diff --git a/src/slic3r/Utils/Bonjour.hpp b/src/slic3r/Utils/Bonjour.hpp
new file mode 100644
index 000000000..63f34638c
--- /dev/null
+++ b/src/slic3r/Utils/Bonjour.hpp
@@ -0,0 +1,64 @@
+#ifndef slic3r_Bonjour_hpp_
+#define slic3r_Bonjour_hpp_
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <functional>
+#include <boost/asio/ip/address.hpp>
+
+
+namespace Slic3r {
+
+
+struct BonjourReply
+{
+ boost::asio::ip::address ip;
+ uint16_t port;
+ std::string service_name;
+ std::string hostname;
+ std::string full_address;
+ std::string path;
+ std::string version;
+
+ BonjourReply() = delete;
+ BonjourReply(boost::asio::ip::address ip, uint16_t port, std::string service_name, std::string hostname, std::string path, std::string version);
+
+ bool operator==(const BonjourReply &other) const;
+ bool operator<(const BonjourReply &other) const;
+};
+
+std::ostream& operator<<(std::ostream &, const BonjourReply &);
+
+
+/// Bonjour lookup performer
+class Bonjour : public std::enable_shared_from_this<Bonjour> {
+private:
+ struct priv;
+public:
+ typedef std::shared_ptr<Bonjour> Ptr;
+ typedef std::function<void(BonjourReply &&)> ReplyFn;
+ typedef std::function<void()> CompleteFn;
+
+ Bonjour(std::string service, std::string protocol = "tcp");
+ Bonjour(Bonjour &&other);
+ ~Bonjour();
+
+ Bonjour& set_timeout(unsigned timeout);
+ Bonjour& set_retries(unsigned retries);
+ // ^ Note: By default there is 1 retry (meaning 1 broadcast is sent).
+ // Timeout is per one retry, ie. total time spent listening = retries * timeout.
+ // If retries > 1, then care needs to be taken as more than one reply from the same service may be received.
+
+ Bonjour& on_reply(ReplyFn fn);
+ Bonjour& on_complete(CompleteFn fn);
+
+ Ptr lookup();
+private:
+ std::unique_ptr<priv> p;
+};
+
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/Duet.cpp b/src/slic3r/Utils/Duet.cpp
new file mode 100644
index 000000000..f25327161
--- /dev/null
+++ b/src/slic3r/Utils/Duet.cpp
@@ -0,0 +1,279 @@
+#include "Duet.hpp"
+#include "PrintHostSendDialog.hpp"
+
+#include <algorithm>
+#include <ctime>
+#include <boost/filesystem/path.hpp>
+#include <boost/format.hpp>
+#include <boost/log/trivial.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/json_parser.hpp>
+
+#include <wx/frame.h>
+#include <wx/event.h>
+#include <wx/progdlg.h>
+#include <wx/sizer.h>
+#include <wx/stattext.h>
+#include <wx/textctrl.h>
+#include <wx/checkbox.h>
+
+#include "libslic3r/PrintConfig.hpp"
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/MsgDialog.hpp"
+#include "Http.hpp"
+
+namespace fs = boost::filesystem;
+namespace pt = boost::property_tree;
+
+namespace Slic3r {
+
+Duet::Duet(DynamicPrintConfig *config) :
+ host(config->opt_string("print_host")),
+ password(config->opt_string("printhost_apikey"))
+{}
+
+Duet::~Duet() {}
+
+bool Duet::test(wxString &msg) const
+{
+ bool connected = connect(msg);
+ if (connected) {
+ disconnect();
+ }
+
+ return connected;
+}
+
+wxString Duet::get_test_ok_msg () const
+{
+ return wxString::Format("%s", _(L("Connection to Duet works correctly.")));
+}
+
+wxString Duet::get_test_failed_msg (wxString &msg) const
+{
+ return wxString::Format("%s: %s", _(L("Could not connect to Duet")), msg);
+}
+
+bool Duet::send_gcode(const std::string &filename) const
+{
+ enum { PROGRESS_RANGE = 1000 };
+
+ const auto errortitle = _(L("Error while uploading to the Duet"));
+ fs::path filepath(filename);
+
+ PrintHostSendDialog send_dialog(filepath.filename(), true);
+ if (send_dialog.ShowModal() != wxID_OK) { return false; }
+
+ const bool print = send_dialog.print();
+ const auto upload_filepath = send_dialog.filename();
+ const auto upload_filename = upload_filepath.filename();
+ const auto upload_parent_path = upload_filepath.parent_path();
+
+ wxProgressDialog progress_dialog(
+ _(L("Duet upload")),
+ _(L("Sending G-code file to Duet...")),
+ PROGRESS_RANGE, nullptr, wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT);
+ progress_dialog.Pulse();
+
+ wxString connect_msg;
+ if (!connect(connect_msg)) {
+ auto errormsg = wxString::Format("%s: %s", errortitle, connect_msg);
+ GUI::show_error(&progress_dialog, std::move(errormsg));
+ return false;
+ }
+
+ bool res = true;
+
+ auto upload_cmd = get_upload_url(upload_filepath.string());
+ BOOST_LOG_TRIVIAL(info) << boost::format("Duet: Uploading file %1%, filename: %2%, path: %3%, print: %4%, command: %5%")
+ % filepath.string()
+ % upload_filename.string()
+ % upload_parent_path.string()
+ % print
+ % upload_cmd;
+
+ auto http = Http::post(std::move(upload_cmd));
+ http.set_post_body(filename)
+ .on_complete([&](std::string body, unsigned status) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Duet: File uploaded: HTTP %1%: %2%") % status % body;
+ progress_dialog.Update(PROGRESS_RANGE);
+
+ int err_code = get_err_code_from_body(body);
+ if (err_code != 0) {
+ auto msg = format_error(body, L("Unknown error occured"), 0);
+ GUI::show_error(&progress_dialog, std::move(msg));
+ res = false;
+ } else if (print) {
+ wxString errormsg;
+ res = start_print(errormsg, upload_filepath.string());
+ if (!res) {
+ GUI::show_error(&progress_dialog, std::move(errormsg));
+ }
+ }
+ })
+ .on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error uploading file: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ auto errormsg = wxString::Format("%s: %s", errortitle, format_error(body, error, status));
+ GUI::show_error(&progress_dialog, std::move(errormsg));
+ res = false;
+ })
+ .on_progress([&](Http::Progress progress, bool &cancel) {
+ if (cancel) {
+ // Upload was canceled
+ res = false;
+ } else if (progress.ultotal > 0) {
+ int value = PROGRESS_RANGE * progress.ulnow / progress.ultotal;
+ cancel = !progress_dialog.Update(std::min(value, PROGRESS_RANGE - 1)); // Cap the value to prevent premature dialog closing
+ } else {
+ cancel = !progress_dialog.Pulse();
+ }
+ })
+ .perform_sync();
+
+ disconnect();
+
+ return res;
+}
+
+bool Duet::has_auto_discovery() const
+{
+ return false;
+}
+
+bool Duet::can_test() const
+{
+ return true;
+}
+
+bool Duet::connect(wxString &msg) const
+{
+ bool res = false;
+ auto url = get_connect_url();
+
+ auto http = Http::get(std::move(url));
+ http.on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error connecting: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ msg = format_error(body, error, status);
+ })
+ .on_complete([&](std::string body, unsigned) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Duet: Got: %1%") % body;
+
+ int err_code = get_err_code_from_body(body);
+ switch (err_code) {
+ case 0:
+ res = true;
+ break;
+ case 1:
+ msg = format_error(body, L("Wrong password"), 0);
+ break;
+ case 2:
+ msg = format_error(body, L("Could not get resources to create a new connection"), 0);
+ break;
+ default:
+ msg = format_error(body, L("Unknown error occured"), 0);
+ break;
+ }
+
+ })
+ .perform_sync();
+
+ return res;
+}
+
+void Duet::disconnect() const
+{
+ auto url = (boost::format("%1%rr_disconnect")
+ % get_base_url()).str();
+
+ auto http = Http::get(std::move(url));
+ http.on_error([&](std::string body, std::string error, unsigned status) {
+ // we don't care about it, if disconnect is not working Duet will disconnect automatically after some time
+ BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error disconnecting: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ })
+ .perform_sync();
+}
+
+std::string Duet::get_upload_url(const std::string &filename) const
+{
+ return (boost::format("%1%rr_upload?name=0:/gcodes/%2%&%3%")
+ % get_base_url()
+ % Http::url_encode(filename)
+ % timestamp_str()).str();
+}
+
+std::string Duet::get_connect_url() const
+{
+ return (boost::format("%1%rr_connect?password=%2%&%3%")
+ % get_base_url()
+ % (password.empty() ? "reprap" : password)
+ % timestamp_str()).str();
+}
+
+std::string Duet::get_base_url() const
+{
+ if (host.find("http://") == 0 || host.find("https://") == 0) {
+ if (host.back() == '/') {
+ return host;
+ } else {
+ return (boost::format("%1%/") % host).str();
+ }
+ } else {
+ return (boost::format("http://%1%/") % host).str();
+ }
+}
+
+std::string Duet::timestamp_str() const
+{
+ enum { BUFFER_SIZE = 32 };
+
+ auto t = std::time(nullptr);
+ auto tm = *std::localtime(&t);
+
+ char buffer[BUFFER_SIZE];
+ std::strftime(buffer, BUFFER_SIZE, "time=%Y-%m-%dT%H:%M:%S", &tm);
+
+ return std::string(buffer);
+}
+
+wxString Duet::format_error(const std::string &body, const std::string &error, unsigned status)
+{
+ if (status != 0) {
+ auto wxbody = wxString::FromUTF8(body.data());
+ return wxString::Format("HTTP %u: %s", status, wxbody);
+ } else {
+ return wxString::FromUTF8(error.data());
+ }
+}
+
+bool Duet::start_print(wxString &msg, const std::string &filename) const
+{
+ bool res = false;
+
+ auto url = (boost::format("%1%rr_gcode?gcode=M32%%20\"%2%\"")
+ % get_base_url()
+ % Http::url_encode(filename)).str();
+
+ auto http = Http::get(std::move(url));
+ http.on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Duet: Error starting print: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ msg = format_error(body, error, status);
+ })
+ .on_complete([&](std::string body, unsigned) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Duet: Got: %1%") % body;
+ res = true;
+ })
+ .perform_sync();
+
+ return res;
+}
+
+int Duet::get_err_code_from_body(const std::string &body) const
+{
+ pt::ptree root;
+ std::istringstream iss (body); // wrap returned json to istringstream
+ pt::read_json(iss, root);
+
+ return root.get<int>("err", 0);
+}
+
+}
diff --git a/src/slic3r/Utils/Duet.hpp b/src/slic3r/Utils/Duet.hpp
new file mode 100644
index 000000000..bc210d7a4
--- /dev/null
+++ b/src/slic3r/Utils/Duet.hpp
@@ -0,0 +1,47 @@
+#ifndef slic3r_Duet_hpp_
+#define slic3r_Duet_hpp_
+
+#include <string>
+#include <wx/string.h>
+
+#include "PrintHost.hpp"
+
+
+namespace Slic3r {
+
+
+class DynamicPrintConfig;
+class Http;
+
+class Duet : public PrintHost
+{
+public:
+ Duet(DynamicPrintConfig *config);
+ virtual ~Duet();
+
+ bool test(wxString &curl_msg) const;
+ wxString get_test_ok_msg () const;
+ wxString get_test_failed_msg (wxString &msg) const;
+ // Send gcode file to duet, filename is expected to be in UTF-8
+ bool send_gcode(const std::string &filename) const;
+ bool has_auto_discovery() const;
+ bool can_test() const;
+private:
+ std::string host;
+ std::string password;
+
+ std::string get_upload_url(const std::string &filename) const;
+ std::string get_connect_url() const;
+ std::string get_base_url() const;
+ std::string timestamp_str() const;
+ bool connect(wxString &msg) const;
+ void disconnect() const;
+ bool start_print(wxString &msg, const std::string &filename) const;
+ int get_err_code_from_body(const std::string &body) const;
+ static wxString format_error(const std::string &body, const std::string &error, unsigned status);
+};
+
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/FixModelByWin10.cpp b/src/slic3r/Utils/FixModelByWin10.cpp
new file mode 100644
index 000000000..556035a5b
--- /dev/null
+++ b/src/slic3r/Utils/FixModelByWin10.cpp
@@ -0,0 +1,402 @@
+#ifdef HAS_WIN10SDK
+
+#ifndef NOMINMAX
+# define NOMINMAX
+#endif
+
+#include "FixModelByWin10.hpp"
+
+#include <atomic>
+#include <chrono>
+#include <cstdint>
+#include <condition_variable>
+#include <exception>
+#include <string>
+#include <thread>
+
+#include <boost/filesystem.hpp>
+#include <boost/nowide/convert.hpp>
+#include <boost/nowide/cstdio.hpp>
+
+#include <roapi.h>
+// for ComPtr
+#include <wrl/client.h>
+// from C:/Program Files (x86)/Windows Kits/10/Include/10.0.17134.0/
+#include <winrt/robuffer.h>
+#include <winrt/windows.storage.provider.h>
+#include <winrt/windows.graphics.printing3d.h>
+
+#include "libslic3r/Model.hpp"
+#include "libslic3r/Print.hpp"
+#include "libslic3r/Format/3mf.hpp"
+#include "../GUI/GUI.hpp"
+#include "../GUI/PresetBundle.hpp"
+
+#include <wx/msgdlg.h>
+#include <wx/progdlg.h>
+
+extern "C"{
+ // from rapi.h
+ typedef HRESULT (__stdcall* FunctionRoInitialize)(int);
+ typedef HRESULT (__stdcall* FunctionRoUninitialize)();
+ typedef HRESULT (__stdcall* FunctionRoActivateInstance)(HSTRING activatableClassId, IInspectable **instance);
+ typedef HRESULT (__stdcall* FunctionRoGetActivationFactory)(HSTRING activatableClassId, REFIID iid, void **factory);
+ // from winstring.h
+ typedef HRESULT (__stdcall* FunctionWindowsCreateString)(LPCWSTR sourceString, UINT32 length, HSTRING *string);
+ typedef HRESULT (__stdcall* FunctionWindowsDelteString)(HSTRING string);
+}
+
+namespace Slic3r {
+
+HMODULE s_hRuntimeObjectLibrary = nullptr;
+FunctionRoInitialize s_RoInitialize = nullptr;
+FunctionRoUninitialize s_RoUninitialize = nullptr;
+FunctionRoActivateInstance s_RoActivateInstance = nullptr;
+FunctionRoGetActivationFactory s_RoGetActivationFactory = nullptr;
+FunctionWindowsCreateString s_WindowsCreateString = nullptr;
+FunctionWindowsDelteString s_WindowsDeleteString = nullptr;
+
+bool winrt_load_runtime_object_library()
+{
+ if (s_hRuntimeObjectLibrary == nullptr)
+ s_hRuntimeObjectLibrary = LoadLibrary(L"ComBase.dll");
+ if (s_hRuntimeObjectLibrary != nullptr) {
+ s_RoInitialize = (FunctionRoInitialize) GetProcAddress(s_hRuntimeObjectLibrary, "RoInitialize");
+ s_RoUninitialize = (FunctionRoUninitialize) GetProcAddress(s_hRuntimeObjectLibrary, "RoUninitialize");
+ s_RoActivateInstance = (FunctionRoActivateInstance) GetProcAddress(s_hRuntimeObjectLibrary, "RoActivateInstance");
+ s_RoGetActivationFactory = (FunctionRoGetActivationFactory) GetProcAddress(s_hRuntimeObjectLibrary, "RoGetActivationFactory");
+ s_WindowsCreateString = (FunctionWindowsCreateString) GetProcAddress(s_hRuntimeObjectLibrary, "WindowsCreateString");
+ s_WindowsDeleteString = (FunctionWindowsDelteString) GetProcAddress(s_hRuntimeObjectLibrary, "WindowsDeleteString");
+ }
+ return s_RoInitialize && s_RoUninitialize && s_RoActivateInstance && s_WindowsCreateString && s_WindowsDeleteString;
+}
+
+static HRESULT winrt_activate_instance(const std::wstring &class_name, IInspectable **pinst)
+{
+ HSTRING hClassName;
+ HRESULT hr = (*s_WindowsCreateString)(class_name.c_str(), class_name.size(), &hClassName);
+ if (S_OK != hr)
+ return hr;
+ hr = (*s_RoActivateInstance)(hClassName, pinst);
+ (*s_WindowsDeleteString)(hClassName);
+ return hr;
+}
+
+template<typename TYPE>
+static HRESULT winrt_activate_instance(const std::wstring &class_name, TYPE **pinst)
+{
+ IInspectable *pinspectable = nullptr;
+ HRESULT hr = winrt_activate_instance(class_name, &pinspectable);
+ if (S_OK != hr)
+ return hr;
+ hr = pinspectable->QueryInterface(__uuidof(TYPE), (void**)pinst);
+ pinspectable->Release();
+ return hr;
+}
+
+static HRESULT winrt_get_activation_factory(const std::wstring &class_name, REFIID iid, void **pinst)
+{
+ HSTRING hClassName;
+ HRESULT hr = (*s_WindowsCreateString)(class_name.c_str(), class_name.size(), &hClassName);
+ if (S_OK != hr)
+ return hr;
+ hr = (*s_RoGetActivationFactory)(hClassName, iid, pinst);
+ (*s_WindowsDeleteString)(hClassName);
+ return hr;
+}
+
+template<typename TYPE>
+static HRESULT winrt_get_activation_factory(const std::wstring &class_name, TYPE **pinst)
+{
+ return winrt_get_activation_factory(class_name, __uuidof(TYPE), reinterpret_cast<void**>(pinst));
+}
+
+// To be called often to test whether to cancel the operation.
+typedef std::function<void ()> ThrowOnCancelFn;
+
+template<typename T>
+static AsyncStatus winrt_async_await(const Microsoft::WRL::ComPtr<T> &asyncAction, ThrowOnCancelFn throw_on_cancel, int blocking_tick_ms = 100)
+{
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IAsyncInfo> asyncInfo;
+ asyncAction.As(&asyncInfo);
+ AsyncStatus status;
+ // Ugly blocking loop until the RepairAsync call finishes.
+//FIXME replace with a callback.
+// https://social.msdn.microsoft.com/Forums/en-US/a5038fb4-b7b7-4504-969d-c102faa389fb/trying-to-block-an-async-operation-and-wait-for-a-particular-time?forum=vclanguage
+ for (;;) {
+ asyncInfo->get_Status(&status);
+ if (status != AsyncStatus::Started)
+ return status;
+ throw_on_cancel();
+ ::Sleep(blocking_tick_ms);
+ }
+}
+
+static HRESULT winrt_open_file_stream(
+ const std::wstring &path,
+ ABI::Windows::Storage::FileAccessMode mode,
+ ABI::Windows::Storage::Streams::IRandomAccessStream **fileStream,
+ ThrowOnCancelFn throw_on_cancel)
+{
+ // Get the file factory.
+ Microsoft::WRL::ComPtr<ABI::Windows::Storage::IStorageFileStatics> fileFactory;
+ HRESULT hr = winrt_get_activation_factory(L"Windows.Storage.StorageFile", fileFactory.GetAddressOf());
+ if (FAILED(hr)) return hr;
+
+ // Open the file asynchronously.
+ HSTRING hstr_path;
+ hr = (*s_WindowsCreateString)(path.c_str(), path.size(), &hstr_path);
+ if (FAILED(hr)) return hr;
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IAsyncOperation<ABI::Windows::Storage::StorageFile*>> fileOpenAsync;
+ hr = fileFactory->GetFileFromPathAsync(hstr_path, fileOpenAsync.GetAddressOf());
+ if (FAILED(hr)) return hr;
+ (*s_WindowsDeleteString)(hstr_path);
+
+ // Wait until the file gets open, get the actual file.
+ AsyncStatus status = winrt_async_await(fileOpenAsync, throw_on_cancel);
+ Microsoft::WRL::ComPtr<ABI::Windows::Storage::IStorageFile> storageFile;
+ if (status == AsyncStatus::Completed) {
+ hr = fileOpenAsync->GetResults(storageFile.GetAddressOf());
+ } else {
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IAsyncInfo> asyncInfo;
+ hr = fileOpenAsync.As(&asyncInfo);
+ if (FAILED(hr)) return hr;
+ HRESULT err;
+ hr = asyncInfo->get_ErrorCode(&err);
+ return FAILED(hr) ? hr : err;
+ }
+
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IAsyncOperation<ABI::Windows::Storage::Streams::IRandomAccessStream*>> fileStreamAsync;
+ hr = storageFile->OpenAsync(mode, fileStreamAsync.GetAddressOf());
+ if (FAILED(hr)) return hr;
+
+ status = winrt_async_await(fileStreamAsync, throw_on_cancel);
+ if (status == AsyncStatus::Completed) {
+ hr = fileStreamAsync->GetResults(fileStream);
+ } else {
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IAsyncInfo> asyncInfo;
+ hr = fileStreamAsync.As(&asyncInfo);
+ if (FAILED(hr)) return hr;
+ HRESULT err;
+ hr = asyncInfo->get_ErrorCode(&err);
+ if (!FAILED(hr))
+ hr = err;
+ }
+ return hr;
+}
+
+bool is_windows10()
+{
+ HKEY hKey;
+ LONG lRes = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hKey);
+ if (lRes == ERROR_SUCCESS) {
+ WCHAR szBuffer[512];
+ DWORD dwBufferSize = sizeof(szBuffer);
+ lRes = RegQueryValueExW(hKey, L"ProductName", 0, nullptr, (LPBYTE)szBuffer, &dwBufferSize);
+ if (lRes == ERROR_SUCCESS)
+ return wcsncmp(szBuffer, L"Windows 10", 10) == 0;
+ RegCloseKey(hKey);
+ }
+ return false;
+}
+
+// Progress function, to be called regularly to update the progress.
+typedef std::function<void (const char * /* message */, unsigned /* progress */)> ProgressFn;
+
+void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path_dst, ProgressFn on_progress, ThrowOnCancelFn throw_on_cancel)
+{
+ if (! is_windows10())
+ throw std::runtime_error("fix_model_by_win10_sdk called on non Windows 10 system");
+
+ if (! winrt_load_runtime_object_library())
+ throw std::runtime_error("Failed to initialize the WinRT library.");
+
+ HRESULT hr = (*s_RoInitialize)(RO_INIT_MULTITHREADED);
+ {
+ on_progress(L("Exporting the source model"), 20);
+
+ Microsoft::WRL::ComPtr<ABI::Windows::Storage::Streams::IRandomAccessStream> fileStream;
+ hr = winrt_open_file_stream(boost::nowide::widen(path_src), ABI::Windows::Storage::FileAccessMode::FileAccessMode_Read, fileStream.GetAddressOf(), throw_on_cancel);
+
+ Microsoft::WRL::ComPtr<ABI::Windows::Graphics::Printing3D::IPrinting3D3MFPackage> printing3d3mfpackage;
+ hr = winrt_activate_instance(L"Windows.Graphics.Printing3D.Printing3D3MFPackage", printing3d3mfpackage.GetAddressOf());
+
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IAsyncOperation<ABI::Windows::Graphics::Printing3D::Printing3DModel*>> modelAsync;
+ hr = printing3d3mfpackage->LoadModelFromPackageAsync(fileStream.Get(), modelAsync.GetAddressOf());
+
+ AsyncStatus status = winrt_async_await(modelAsync, throw_on_cancel);
+ Microsoft::WRL::ComPtr<ABI::Windows::Graphics::Printing3D::IPrinting3DModel> model;
+ if (status == AsyncStatus::Completed)
+ hr = modelAsync->GetResults(model.GetAddressOf());
+ else
+ throw std::runtime_error(L("Failed loading the input model."));
+
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::Collections::IVector<ABI::Windows::Graphics::Printing3D::Printing3DMesh*>> meshes;
+ hr = model->get_Meshes(meshes.GetAddressOf());
+ unsigned num_meshes = 0;
+ hr = meshes->get_Size(&num_meshes);
+
+ on_progress(L("Repairing the model by the Netfabb service"), 40);
+
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IAsyncAction> repairAsync;
+ hr = model->RepairAsync(repairAsync.GetAddressOf());
+ status = winrt_async_await(repairAsync, throw_on_cancel);
+ if (status != AsyncStatus::Completed)
+ throw std::runtime_error(L("Mesh repair failed."));
+ repairAsync->GetResults();
+
+ on_progress(L("Loading the repaired model"), 60);
+
+ // Verify the number of meshes returned after the repair action.
+ meshes.Reset();
+ hr = model->get_Meshes(meshes.GetAddressOf());
+ hr = meshes->get_Size(&num_meshes);
+
+ // Save model to this class' Printing3D3MFPackage.
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IAsyncAction> saveToPackageAsync;
+ hr = printing3d3mfpackage->SaveModelToPackageAsync(model.Get(), saveToPackageAsync.GetAddressOf());
+ status = winrt_async_await(saveToPackageAsync, throw_on_cancel);
+ if (status != AsyncStatus::Completed)
+ throw std::runtime_error(L("Saving mesh into the 3MF container failed."));
+ hr = saveToPackageAsync->GetResults();
+
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IAsyncOperation<ABI::Windows::Storage::Streams::IRandomAccessStream*>> generatorStreamAsync;
+ hr = printing3d3mfpackage->SaveAsync(generatorStreamAsync.GetAddressOf());
+ status = winrt_async_await(generatorStreamAsync, throw_on_cancel);
+ if (status != AsyncStatus::Completed)
+ throw std::runtime_error(L("Saving mesh into the 3MF container failed."));
+ Microsoft::WRL::ComPtr<ABI::Windows::Storage::Streams::IRandomAccessStream> generatorStream;
+ hr = generatorStreamAsync->GetResults(generatorStream.GetAddressOf());
+
+ // Go to the beginning of the stream.
+ generatorStream->Seek(0);
+ Microsoft::WRL::ComPtr<ABI::Windows::Storage::Streams::IInputStream> inputStream;
+ hr = generatorStream.As(&inputStream);
+
+ // Get the buffer factory.
+ Microsoft::WRL::ComPtr<ABI::Windows::Storage::Streams::IBufferFactory> bufferFactory;
+ hr = winrt_get_activation_factory(L"Windows.Storage.Streams.Buffer", bufferFactory.GetAddressOf());
+
+ // Open the destination file.
+ FILE *fout = boost::nowide::fopen(path_dst.c_str(), "wb");
+
+ Microsoft::WRL::ComPtr<ABI::Windows::Storage::Streams::IBuffer> buffer;
+ byte *buffer_ptr;
+ bufferFactory->Create(65536 * 2048, buffer.GetAddressOf());
+ {
+ Microsoft::WRL::ComPtr<Windows::Storage::Streams::IBufferByteAccess> bufferByteAccess;
+ buffer.As(&bufferByteAccess);
+ hr = bufferByteAccess->Buffer(&buffer_ptr);
+ }
+ uint32_t length;
+ hr = buffer->get_Length(&length);
+
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IAsyncOperationWithProgress<ABI::Windows::Storage::Streams::IBuffer*, UINT32>> asyncRead;
+ for (;;) {
+ hr = inputStream->ReadAsync(buffer.Get(), 65536 * 2048, ABI::Windows::Storage::Streams::InputStreamOptions_ReadAhead, asyncRead.GetAddressOf());
+ status = winrt_async_await(asyncRead, throw_on_cancel);
+ if (status != AsyncStatus::Completed)
+ throw std::runtime_error(L("Saving mesh into the 3MF container failed."));
+ hr = buffer->get_Length(&length);
+ if (length == 0)
+ break;
+ fwrite(buffer_ptr, length, 1, fout);
+ }
+ fclose(fout);
+ // Here all the COM objects will be released through the ComPtr destructors.
+ }
+ (*s_RoUninitialize)();
+}
+
+class RepairCanceledException : public std::exception {
+public:
+ const char* what() const throw() { return "Model repair has been canceled"; }
+};
+
+void fix_model_by_win10_sdk_gui(const ModelObject &model_object, const Print &print, Model &result)
+{
+ std::mutex mutex;
+ std::condition_variable condition;
+ std::unique_lock<std::mutex> lock(mutex);
+ struct Progress {
+ std::string message;
+ int percent = 0;
+ bool updated = false;
+ } progress;
+ std::atomic<bool> canceled = false;
+ std::atomic<bool> finished = false;
+
+ // Open a progress dialog.
+ wxProgressDialog progress_dialog(
+ _(L("Model fixing")),
+ _(L("Exporting model...")),
+ 100, nullptr, wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT);
+ // Executing the calculation in a background thread, so that the COM context could be created with its own threading model.
+ // (It seems like wxWidgets initialize the COM contex as single threaded and we need a multi-threaded context).
+ bool success = false;
+ auto on_progress = [&mutex, &condition, &progress](const char *msg, unsigned prcnt) {
+ std::lock_guard<std::mutex> lk(mutex);
+ progress.message = msg;
+ progress.percent = prcnt;
+ progress.updated = true;
+ condition.notify_all();
+ };
+ auto worker_thread = boost::thread([&model_object, &print, &result, on_progress, &success, &canceled, &finished]() {
+ try {
+ on_progress(L("Exporting the source model"), 0);
+ boost::filesystem::path path_src = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
+ path_src += ".3mf";
+ Model model;
+ model.add_object(model_object);
+ if (! Slic3r::store_3mf(path_src.string().c_str(), &model, const_cast<Print*>(&print), false)) {
+ boost::filesystem::remove(path_src);
+ throw std::runtime_error(L("Export of a temporary 3mf file failed"));
+ }
+ model.clear_objects();
+ model.clear_materials();
+ boost::filesystem::path path_dst = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
+ path_dst += ".3mf";
+ fix_model_by_win10_sdk(path_src.string().c_str(), path_dst.string(), on_progress,
+ [&canceled]() { if (canceled) throw RepairCanceledException(); });
+ boost::filesystem::remove(path_src);
+ PresetBundle bundle;
+ on_progress(L("Loading the repaired model"), 80);
+ bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), &bundle, &result);
+ boost::filesystem::remove(path_dst);
+ if (! loaded)
+ throw std::runtime_error(L("Import of the repaired 3mf file failed"));
+ success = true;
+ finished = true;
+ on_progress(L("Model repair finished"), 100);
+ } catch (RepairCanceledException &ex) {
+ canceled = true;
+ finished = true;
+ on_progress(L("Model repair canceled"), 100);
+ } catch (std::exception &ex) {
+ success = false;
+ finished = true;
+ on_progress(ex.what(), 100);
+ }
+ });
+ while (! finished) {
+ condition.wait_for(lock, std::chrono::milliseconds(500), [&progress]{ return progress.updated; });
+ if (! progress_dialog.Update(progress.percent, _(progress.message)))
+ canceled = true;
+ progress.updated = false;
+ }
+
+ if (canceled) {
+ // Nothing to show.
+ } else if (success) {
+ wxMessageDialog dlg(nullptr, _(L("Model repaired successfully")), _(L("Model Repair by the Netfabb service")), wxICON_INFORMATION | wxOK_DEFAULT);
+ dlg.ShowModal();
+ } else {
+ wxMessageDialog dlg(nullptr, _(L("Model repair failed: \n")) + _(progress.message), _(L("Model Repair by the Netfabb service")), wxICON_ERROR | wxOK_DEFAULT);
+ dlg.ShowModal();
+ }
+ worker_thread.join();
+}
+
+} // namespace Slic3r
+
+#endif /* HAS_WIN10SDK */
diff --git a/src/slic3r/Utils/FixModelByWin10.hpp b/src/slic3r/Utils/FixModelByWin10.hpp
new file mode 100644
index 000000000..c148a6970
--- /dev/null
+++ b/src/slic3r/Utils/FixModelByWin10.hpp
@@ -0,0 +1,26 @@
+#ifndef slic3r_GUI_Utils_FixModelByWin10_hpp_
+#define slic3r_GUI_Utils_FixModelByWin10_hpp_
+
+#include <string>
+
+namespace Slic3r {
+
+class Model;
+class ModelObject;
+class Print;
+
+#ifdef HAS_WIN10SDK
+
+extern bool is_windows10();
+extern void fix_model_by_win10_sdk_gui(const ModelObject &model_object, const Print &print, Model &result);
+
+#else /* HAS_WIN10SDK */
+
+inline bool is_windows10() { return false; }
+inline void fix_model_by_win10_sdk_gui(const ModelObject &, const Print &, Model &) {}
+
+#endif /* HAS_WIN10SDK */
+
+} // namespace Slic3r
+
+#endif /* slic3r_GUI_Utils_FixModelByWin10_hpp_ */
diff --git a/src/slic3r/Utils/HexFile.cpp b/src/slic3r/Utils/HexFile.cpp
new file mode 100644
index 000000000..282c647bd
--- /dev/null
+++ b/src/slic3r/Utils/HexFile.cpp
@@ -0,0 +1,106 @@
+#include "HexFile.hpp"
+
+#include <sstream>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+
+namespace fs = boost::filesystem;
+namespace pt = boost::property_tree;
+
+
+namespace Slic3r {
+namespace Utils {
+
+
+static HexFile::DeviceKind parse_device_kind(const std::string &str)
+{
+ if (str == "mk2") { return HexFile::DEV_MK2; }
+ else if (str == "mk3") { return HexFile::DEV_MK3; }
+ else if (str == "mm-control") { return HexFile::DEV_MM_CONTROL; }
+ else { return HexFile::DEV_GENERIC; }
+}
+
+static size_t hex_num_sections(fs::ifstream &file)
+{
+ file.seekg(0);
+ if (! file.good()) {
+ return 0;
+ }
+
+ static const char *hex_terminator = ":00000001FF\r";
+ size_t res = 0;
+ std::string line;
+ while (getline(file, line, '\n').good()) {
+ // Account for LF vs CRLF
+ if (!line.empty() && line.back() != '\r') {
+ line.push_back('\r');
+ }
+
+ if (line == hex_terminator) {
+ res++;
+ }
+ }
+
+ return res;
+}
+
+HexFile::HexFile(fs::path path) :
+ path(std::move(path))
+{
+ fs::ifstream file(this->path);
+ if (! file.good()) {
+ return;
+ }
+
+ std::string line;
+ std::stringstream header_ini;
+ while (std::getline(file, line, '\n').good()) {
+ if (line.empty()) {
+ continue;
+ }
+
+ // Account for LF vs CRLF
+ if (!line.empty() && line.back() == '\r') {
+ line.pop_back();
+ }
+
+ if (line.front() == ';') {
+ line.front() = ' ';
+ header_ini << line << std::endl;
+ } else if (line.front() == ':') {
+ break;
+ }
+ }
+
+ pt::ptree ptree;
+ try {
+ pt::read_ini(header_ini, ptree);
+ } catch (std::exception &e) {
+ return;
+ }
+
+ bool has_device_meta = false;
+ const auto device = ptree.find("device");
+ if (device != ptree.not_found()) {
+ this->device = parse_device_kind(device->second.data());
+ has_device_meta = true;
+ }
+
+ const auto model_id = ptree.find("model_id");
+ if (model_id != ptree.not_found()) {
+ this->model_id = model_id->second.data();
+ }
+
+ if (! has_device_meta) {
+ // No device metadata, look at the number of 'sections'
+ if (hex_num_sections(file) == 2) {
+ // Looks like a pre-metadata l10n firmware for the MK3, assume that's the case
+ this->device = DEV_MK3;
+ }
+ }
+}
+
+
+}
+}
diff --git a/src/slic3r/Utils/HexFile.hpp b/src/slic3r/Utils/HexFile.hpp
new file mode 100644
index 000000000..1201d23a4
--- /dev/null
+++ b/src/slic3r/Utils/HexFile.hpp
@@ -0,0 +1,33 @@
+#ifndef slic3r_Hex_hpp_
+#define slic3r_Hex_hpp_
+
+#include <string>
+#include <boost/filesystem/path.hpp>
+
+
+namespace Slic3r {
+namespace Utils {
+
+
+struct HexFile
+{
+ enum DeviceKind {
+ DEV_GENERIC,
+ DEV_MK2,
+ DEV_MK3,
+ DEV_MM_CONTROL,
+ };
+
+ boost::filesystem::path path;
+ DeviceKind device = DEV_GENERIC;
+ std::string model_id;
+
+ HexFile() {}
+ HexFile(boost::filesystem::path path);
+};
+
+
+}
+}
+
+#endif
diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp
new file mode 100644
index 000000000..9b67ceea8
--- /dev/null
+++ b/src/slic3r/Utils/Http.cpp
@@ -0,0 +1,451 @@
+#include "Http.hpp"
+
+#include <cstdlib>
+#include <functional>
+#include <thread>
+#include <deque>
+#include <sstream>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/format.hpp>
+
+#include <curl/curl.h>
+
+#include "../../libslic3r/libslic3r.h"
+
+namespace fs = boost::filesystem;
+
+
+namespace Slic3r {
+
+
+// Private
+
+class CurlGlobalInit
+{
+ static const CurlGlobalInit instance;
+
+ CurlGlobalInit() { ::curl_global_init(CURL_GLOBAL_DEFAULT); }
+ ~CurlGlobalInit() { ::curl_global_cleanup(); }
+};
+
+struct Http::priv
+{
+ enum {
+ DEFAULT_SIZE_LIMIT = 5 * 1024 * 1024,
+ };
+
+ ::CURL *curl;
+ ::curl_httppost *form;
+ ::curl_httppost *form_end;
+ ::curl_slist *headerlist;
+ // Used for reading the body
+ std::string buffer;
+ // Used for storing file streams added as multipart form parts
+ // Using a deque here because unlike vector it doesn't ivalidate pointers on insertion
+ std::deque<fs::ifstream> form_files;
+ std::string postfields;
+ size_t limit;
+ bool cancel;
+
+ std::thread io_thread;
+ Http::CompleteFn completefn;
+ Http::ErrorFn errorfn;
+ Http::ProgressFn progressfn;
+
+ priv(const std::string &url);
+ ~priv();
+
+ static bool ca_file_supported(::CURL *curl);
+ static size_t writecb(void *data, size_t size, size_t nmemb, void *userp);
+ static int xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
+ static int xfercb_legacy(void *userp, double dltotal, double dlnow, double ultotal, double ulnow);
+ static size_t form_file_read_cb(char *buffer, size_t size, size_t nitems, void *userp);
+
+ void form_add_file(const char *name, const fs::path &path, const char* filename);
+ void set_post_body(const fs::path &path);
+
+ std::string curl_error(CURLcode curlcode);
+ std::string body_size_error();
+ void http_perform();
+};
+
+Http::priv::priv(const std::string &url) :
+ curl(::curl_easy_init()),
+ form(nullptr),
+ form_end(nullptr),
+ headerlist(nullptr),
+ limit(0),
+ cancel(false)
+{
+ if (curl == nullptr) {
+ throw std::runtime_error(std::string("Could not construct Curl object"));
+ }
+
+ ::curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); // curl makes a copy internally
+ ::curl_easy_setopt(curl, CURLOPT_USERAGENT, SLIC3R_FORK_NAME "/" SLIC3R_VERSION);
+}
+
+Http::priv::~priv()
+{
+ ::curl_easy_cleanup(curl);
+ ::curl_formfree(form);
+ ::curl_slist_free_all(headerlist);
+}
+
+bool Http::priv::ca_file_supported(::CURL *curl)
+{
+#ifdef _WIN32
+ bool res = false;
+#else
+ bool res = true;
+#endif
+
+ if (curl == nullptr) { return res; }
+
+#if LIBCURL_VERSION_MAJOR >= 7 && LIBCURL_VERSION_MINOR >= 48
+ ::curl_tlssessioninfo *tls;
+ if (::curl_easy_getinfo(curl, CURLINFO_TLS_SSL_PTR, &tls) == CURLE_OK) {
+ if (tls->backend == CURLSSLBACKEND_SCHANNEL || tls->backend == CURLSSLBACKEND_DARWINSSL) {
+ // With Windows and OS X native SSL support, cert files cannot be set
+ res = false;
+ }
+ }
+#endif
+
+ return res;
+}
+
+size_t Http::priv::writecb(void *data, size_t size, size_t nmemb, void *userp)
+{
+ auto self = static_cast<priv*>(userp);
+ const char *cdata = static_cast<char*>(data);
+ const size_t realsize = size * nmemb;
+
+ const size_t limit = self->limit > 0 ? self->limit : DEFAULT_SIZE_LIMIT;
+ if (self->buffer.size() + realsize > limit) {
+ // This makes curl_easy_perform return CURLE_WRITE_ERROR
+ return 0;
+ }
+
+ self->buffer.append(cdata, realsize);
+
+ return realsize;
+}
+
+int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
+{
+ auto self = static_cast<priv*>(userp);
+ bool cb_cancel = false;
+
+ if (self->progressfn) {
+ Progress progress(dltotal, dlnow, ultotal, ulnow);
+ self->progressfn(progress, cb_cancel);
+ }
+
+ return self->cancel || cb_cancel;
+}
+
+int Http::priv::xfercb_legacy(void *userp, double dltotal, double dlnow, double ultotal, double ulnow)
+{
+ return xfercb(userp, dltotal, dlnow, ultotal, ulnow);
+}
+
+size_t Http::priv::form_file_read_cb(char *buffer, size_t size, size_t nitems, void *userp)
+{
+ auto stream = reinterpret_cast<fs::ifstream*>(userp);
+
+ try {
+ stream->read(buffer, size * nitems);
+ } catch (...) {
+ return CURL_READFUNC_ABORT;
+ }
+
+ return stream->gcount();
+}
+
+void Http::priv::form_add_file(const char *name, const fs::path &path, const char* filename)
+{
+ // We can't use CURLFORM_FILECONTENT, because curl doesn't support Unicode filenames on Windows
+ // and so we use CURLFORM_STREAM with boost ifstream to read the file.
+
+ if (filename == nullptr) {
+ filename = path.string().c_str();
+ }
+
+ form_files.emplace_back(path, std::ios::in | std::ios::binary);
+ auto &stream = form_files.back();
+ stream.seekg(0, std::ios::end);
+ size_t size = stream.tellg();
+ stream.seekg(0);
+
+ if (filename != nullptr) {
+ ::curl_formadd(&form, &form_end,
+ CURLFORM_COPYNAME, name,
+ CURLFORM_FILENAME, filename,
+ CURLFORM_CONTENTTYPE, "application/octet-stream",
+ CURLFORM_STREAM, static_cast<void*>(&stream),
+ CURLFORM_CONTENTSLENGTH, static_cast<long>(size),
+ CURLFORM_END
+ );
+ }
+}
+
+void Http::priv::set_post_body(const fs::path &path)
+{
+ std::ifstream file(path.string());
+ std::string file_content { std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>() };
+ postfields = file_content;
+}
+
+std::string Http::priv::curl_error(CURLcode curlcode)
+{
+ return (boost::format("%1% (%2%)")
+ % ::curl_easy_strerror(curlcode)
+ % curlcode
+ ).str();
+}
+
+std::string Http::priv::body_size_error()
+{
+ return (boost::format("HTTP body data size exceeded limit (%1% bytes)") % limit).str();
+}
+
+void Http::priv::http_perform()
+{
+ ::curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+ ::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writecb);
+ ::curl_easy_setopt(curl, CURLOPT_WRITEDATA, static_cast<void*>(this));
+ ::curl_easy_setopt(curl, CURLOPT_READFUNCTION, form_file_read_cb);
+
+ ::curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
+#if LIBCURL_VERSION_MAJOR >= 7 && LIBCURL_VERSION_MINOR >= 32
+ ::curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xfercb);
+ ::curl_easy_setopt(curl, CURLOPT_XFERINFODATA, static_cast<void*>(this));
+ (void)xfercb_legacy; // prevent unused function warning
+#else
+ ::curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, xfercb);
+ ::curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, static_cast<void*>(this));
+#endif
+
+#ifndef NDEBUG
+ ::curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
+#endif
+
+ if (headerlist != nullptr) {
+ ::curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerlist);
+ }
+
+ if (form != nullptr) {
+ ::curl_easy_setopt(curl, CURLOPT_HTTPPOST, form);
+ }
+
+ if (!postfields.empty()) {
+ ::curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields.c_str());
+ ::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, postfields.size());
+ }
+
+ CURLcode res = ::curl_easy_perform(curl);
+
+ if (res != CURLE_OK) {
+ if (res == CURLE_ABORTED_BY_CALLBACK) {
+ if (cancel) {
+ // The abort comes from the request being cancelled programatically
+ Progress dummyprogress(0, 0, 0, 0);
+ bool cancel = true;
+ if (progressfn) { progressfn(dummyprogress, cancel); }
+ } else {
+ // The abort comes from the CURLOPT_READFUNCTION callback, which means reading file failed
+ if (errorfn) { errorfn(std::move(buffer), "Error reading file for file upload", 0); }
+ }
+ }
+ else if (res == CURLE_WRITE_ERROR) {
+ if (errorfn) { errorfn(std::move(buffer), body_size_error(), 0); }
+ } else {
+ if (errorfn) { errorfn(std::move(buffer), curl_error(res), 0); }
+ };
+ } else {
+ long http_status = 0;
+ ::curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status);
+
+ if (http_status >= 400) {
+ if (errorfn) { errorfn(std::move(buffer), std::string(), http_status); }
+ } else {
+ if (completefn) { completefn(std::move(buffer), http_status); }
+ }
+ }
+}
+
+Http::Http(const std::string &url) : p(new priv(url)) {}
+
+
+// Public
+
+Http::Http(Http &&other) : p(std::move(other.p)) {}
+
+Http::~Http()
+{
+ if (p && p->io_thread.joinable()) {
+ p->io_thread.detach();
+ }
+}
+
+
+Http& Http::size_limit(size_t sizeLimit)
+{
+ if (p) { p->limit = sizeLimit; }
+ return *this;
+}
+
+Http& Http::header(std::string name, const std::string &value)
+{
+ if (!p) { return * this; }
+
+ if (name.size() > 0) {
+ name.append(": ").append(value);
+ } else {
+ name.push_back(':');
+ }
+ p->headerlist = curl_slist_append(p->headerlist, name.c_str());
+ return *this;
+}
+
+Http& Http::remove_header(std::string name)
+{
+ if (p) {
+ name.push_back(':');
+ p->headerlist = curl_slist_append(p->headerlist, name.c_str());
+ }
+
+ return *this;
+}
+
+Http& Http::ca_file(const std::string &name)
+{
+ if (p && priv::ca_file_supported(p->curl)) {
+ ::curl_easy_setopt(p->curl, CURLOPT_CAINFO, name.c_str());
+ }
+
+ return *this;
+}
+
+Http& Http::form_add(const std::string &name, const std::string &contents)
+{
+ if (p) {
+ ::curl_formadd(&p->form, &p->form_end,
+ CURLFORM_COPYNAME, name.c_str(),
+ CURLFORM_COPYCONTENTS, contents.c_str(),
+ CURLFORM_END
+ );
+ }
+
+ return *this;
+}
+
+Http& Http::form_add_file(const std::string &name, const fs::path &path)
+{
+ if (p) { p->form_add_file(name.c_str(), path.c_str(), nullptr); }
+ return *this;
+}
+
+Http& Http::form_add_file(const std::string &name, const fs::path &path, const std::string &filename)
+{
+ if (p) { p->form_add_file(name.c_str(), path.c_str(), filename.c_str()); }
+ return *this;
+}
+
+Http& Http::set_post_body(const fs::path &path)
+{
+ if (p) { p->set_post_body(path);}
+ return *this;
+}
+
+Http& Http::on_complete(CompleteFn fn)
+{
+ if (p) { p->completefn = std::move(fn); }
+ return *this;
+}
+
+Http& Http::on_error(ErrorFn fn)
+{
+ if (p) { p->errorfn = std::move(fn); }
+ return *this;
+}
+
+Http& Http::on_progress(ProgressFn fn)
+{
+ if (p) { p->progressfn = std::move(fn); }
+ return *this;
+}
+
+Http::Ptr Http::perform()
+{
+ auto self = std::make_shared<Http>(std::move(*this));
+
+ if (self->p) {
+ auto io_thread = std::thread([self](){
+ self->p->http_perform();
+ });
+ self->p->io_thread = std::move(io_thread);
+ }
+
+ return self;
+}
+
+void Http::perform_sync()
+{
+ if (p) { p->http_perform(); }
+}
+
+void Http::cancel()
+{
+ if (p) { p->cancel = true; }
+}
+
+Http Http::get(std::string url)
+{
+ return std::move(Http{std::move(url)});
+}
+
+Http Http::post(std::string url)
+{
+ Http http{std::move(url)};
+ curl_easy_setopt(http.p->curl, CURLOPT_POST, 1L);
+ return http;
+}
+
+bool Http::ca_file_supported()
+{
+ ::CURL *curl = ::curl_easy_init();
+ bool res = priv::ca_file_supported(curl);
+ if (curl != nullptr) { ::curl_easy_cleanup(curl); }
+ return res;
+}
+
+std::string Http::url_encode(const std::string &str)
+{
+ ::CURL *curl = ::curl_easy_init();
+ if (curl == nullptr) {
+ return str;
+ }
+ char *ce = ::curl_easy_escape(curl, str.c_str(), str.length());
+ std::string encoded = std::string(ce);
+
+ ::curl_free(ce);
+ ::curl_easy_cleanup(curl);
+
+ return encoded;
+}
+
+std::ostream& operator<<(std::ostream &os, const Http::Progress &progress)
+{
+ os << "Http::Progress("
+ << "dltotal = " << progress.dltotal
+ << ", dlnow = " << progress.dlnow
+ << ", ultotal = " << progress.ultotal
+ << ", ulnow = " << progress.ulnow
+ << ")";
+ return os;
+}
+
+
+}
diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp
new file mode 100644
index 000000000..44580b7ea
--- /dev/null
+++ b/src/slic3r/Utils/Http.hpp
@@ -0,0 +1,115 @@
+#ifndef slic3r_Http_hpp_
+#define slic3r_Http_hpp_
+
+#include <memory>
+#include <string>
+#include <functional>
+#include <boost/filesystem/path.hpp>
+
+
+namespace Slic3r {
+
+
+/// Represetns a Http request
+class Http : public std::enable_shared_from_this<Http> {
+private:
+ struct priv;
+public:
+ struct Progress
+ {
+ size_t dltotal; // Total bytes to download
+ size_t dlnow; // Bytes downloaded so far
+ size_t ultotal; // Total bytes to upload
+ size_t ulnow; // Bytes uploaded so far
+
+ Progress(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) :
+ dltotal(dltotal), dlnow(dlnow), ultotal(ultotal), ulnow(ulnow)
+ {}
+ };
+
+ typedef std::shared_ptr<Http> Ptr;
+ typedef std::function<void(std::string /* body */, unsigned /* http_status */)> CompleteFn;
+
+ // A HTTP request may fail at various stages of completeness (URL parsing, DNS lookup, TCP connection, ...).
+ // If the HTTP request could not be made or failed before completion, the `error` arg contains a description
+ // of the error and `http_status` is zero.
+ // If the HTTP request was completed but the response HTTP code is >= 400, `error` is empty and `http_status` contains the response code.
+ // In either case there may or may not be a body.
+ typedef std::function<void(std::string /* body */, std::string /* error */, unsigned /* http_status */)> ErrorFn;
+
+ // See the Progress struct above.
+ // Writing true to the `cancel` reference cancels the request in progress.
+ typedef std::function<void(Progress, bool& /* cancel */)> ProgressFn;
+
+ Http(Http &&other);
+
+ // Note: strings are expected to be UTF-8-encoded
+
+ // These are the primary constructors that create a HTTP object
+ // for a GET and a POST request respectively.
+ static Http get(std::string url);
+ static Http post(std::string url);
+ ~Http();
+
+ Http(const Http &) = delete;
+ Http& operator=(const Http &) = delete;
+ Http& operator=(Http &&) = delete;
+
+ // Sets a maximum size of the data that can be received.
+ // A value of zero sets the default limit, which is is 5MB.
+ Http& size_limit(size_t sizeLimit);
+ // Sets a HTTP header field.
+ Http& header(std::string name, const std::string &value);
+ // Removes a header field.
+ Http& remove_header(std::string name);
+ // Sets a CA certificate file for usage with HTTPS. This is only supported on some backends,
+ // specifically, this is supported with OpenSSL and NOT supported with Windows and OS X native certificate store.
+ // See also ca_file_supported().
+ Http& ca_file(const std::string &filename);
+ // Add a HTTP multipart form field
+ Http& form_add(const std::string &name, const std::string &contents);
+ // Add a HTTP multipart form file data contents, `name` is the name of the part
+ Http& form_add_file(const std::string &name, const boost::filesystem::path &path);
+ // Same as above except also override the file's filename with a custom one
+ Http& form_add_file(const std::string &name, const boost::filesystem::path &path, const std::string &filename);
+
+ // Set the file contents as a POST request body.
+ // The data is used verbatim, it is not additionally encoded in any way.
+ // This can be used for hosts which do not support multipart requests.
+ Http& set_post_body(const boost::filesystem::path &path);
+
+ // Callback called on HTTP request complete
+ Http& on_complete(CompleteFn fn);
+ // Callback called on an error occuring at any stage of the requests: Url parsing, DNS lookup,
+ // TCP connection, HTTP transfer, and finally also when the response indicates an error (status >= 400).
+ // Therefore, a response body may or may not be present.
+ Http& on_error(ErrorFn fn);
+ // Callback called on data download/upload prorgess (called fairly frequently).
+ // See the `Progress` structure for description of the data passed.
+ // Writing a true-ish value into the cancel reference parameter cancels the request.
+ Http& on_progress(ProgressFn fn);
+
+ // Starts performing the request in a background thread
+ Ptr perform();
+ // Starts performing the request on the current thread
+ void perform_sync();
+ // Cancels a request in progress
+ void cancel();
+
+ // Tells whether current backend supports seting up a CA file using ca_file()
+ static bool ca_file_supported();
+
+ // converts the given string to an url_encoded_string
+ static std::string url_encode(const std::string &str);
+private:
+ Http(const std::string &url);
+
+ std::unique_ptr<priv> p;
+};
+
+std::ostream& operator<<(std::ostream &, const Http::Progress &);
+
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp
new file mode 100644
index 000000000..db86d7697
--- /dev/null
+++ b/src/slic3r/Utils/OctoPrint.cpp
@@ -0,0 +1,173 @@
+#include "OctoPrint.hpp"
+#include "PrintHostSendDialog.hpp"
+
+#include <algorithm>
+#include <boost/format.hpp>
+#include <boost/log/trivial.hpp>
+
+#include "libslic3r/PrintConfig.hpp"
+#include "Http.hpp"
+
+namespace fs = boost::filesystem;
+
+
+namespace Slic3r {
+
+OctoPrint::OctoPrint(DynamicPrintConfig *config) :
+ host(config->opt_string("print_host")),
+ apikey(config->opt_string("printhost_apikey")),
+ cafile(config->opt_string("printhost_cafile"))
+{}
+
+OctoPrint::~OctoPrint() {}
+
+bool OctoPrint::test(wxString &msg) const
+{
+ // Since the request is performed synchronously here,
+ // it is ok to refer to `msg` from within the closure
+
+ bool res = true;
+ auto url = make_url("api/version");
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Octoprint: Get version at: %1%") % url;
+
+ auto http = Http::get(std::move(url));
+ set_auth(http);
+ http.on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Octoprint: Error getting version: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ res = false;
+ msg = format_error(body, error, status);
+ })
+ .on_complete([&](std::string body, unsigned) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Octoprint: Got version: %1%") % body;
+ })
+ .perform_sync();
+
+ return res;
+}
+
+wxString OctoPrint::get_test_ok_msg () const
+{
+ return wxString::Format("%s", _(L("Connection to OctoPrint works correctly.")));
+}
+
+wxString OctoPrint::get_test_failed_msg (wxString &msg) const
+{
+ return wxString::Format("%s: %s\n\n%s",
+ _(L("Could not connect to OctoPrint")), msg, _(L("Note: OctoPrint version at least 1.1.0 is required.")));
+}
+
+bool OctoPrint::send_gcode(const std::string &filename) const
+{
+ enum { PROGRESS_RANGE = 1000 };
+
+ const auto errortitle = _(L("Error while uploading to the OctoPrint server"));
+ fs::path filepath(filename);
+
+ PrintHostSendDialog send_dialog(filepath.filename(), true);
+ if (send_dialog.ShowModal() != wxID_OK) { return false; }
+
+ const bool print = send_dialog.print();
+ const auto upload_filepath = send_dialog.filename();
+ const auto upload_filename = upload_filepath.filename();
+ const auto upload_parent_path = upload_filepath.parent_path();
+
+ wxProgressDialog progress_dialog(
+ _(L("OctoPrint upload")),
+ _(L("Sending G-code file to the OctoPrint server...")),
+ PROGRESS_RANGE, nullptr, wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT);
+ progress_dialog.Pulse();
+
+ wxString test_msg;
+ if (!test(test_msg)) {
+ auto errormsg = wxString::Format("%s: %s", errortitle, test_msg);
+ GUI::show_error(&progress_dialog, std::move(errormsg));
+ return false;
+ }
+
+ bool res = true;
+
+ auto url = make_url("api/files/local");
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Octoprint: Uploading file %1% at %2%, filename: %3%, path: %4%, print: %5%")
+ % filepath.string()
+ % url
+ % upload_filename.string()
+ % upload_parent_path.string()
+ % print;
+
+ auto http = Http::post(std::move(url));
+ set_auth(http);
+ http.form_add("print", print ? "true" : "false")
+ .form_add("path", upload_parent_path.string())
+ .form_add_file("file", filename, upload_filename.string())
+ .on_complete([&](std::string body, unsigned status) {
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Octoprint: File uploaded: HTTP %1%: %2%") % status % body;
+ progress_dialog.Update(PROGRESS_RANGE);
+ })
+ .on_error([&](std::string body, std::string error, unsigned status) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("Octoprint: Error uploading file: %1%, HTTP %2%, body: `%3%`") % error % status % body;
+ auto errormsg = wxString::Format("%s: %s", errortitle, format_error(body, error, status));
+ GUI::show_error(&progress_dialog, std::move(errormsg));
+ res = false;
+ })
+ .on_progress([&](Http::Progress progress, bool &cancel) {
+ if (cancel) {
+ // Upload was canceled
+ res = false;
+ } else if (progress.ultotal > 0) {
+ int value = PROGRESS_RANGE * progress.ulnow / progress.ultotal;
+ cancel = !progress_dialog.Update(std::min(value, PROGRESS_RANGE - 1)); // Cap the value to prevent premature dialog closing
+ } else {
+ cancel = !progress_dialog.Pulse();
+ }
+ })
+ .perform_sync();
+
+ return res;
+}
+
+bool OctoPrint::has_auto_discovery() const
+{
+ return true;
+}
+
+bool OctoPrint::can_test() const
+{
+ return true;
+}
+
+void OctoPrint::set_auth(Http &http) const
+{
+ http.header("X-Api-Key", apikey);
+
+ if (! cafile.empty()) {
+ http.ca_file(cafile);
+ }
+}
+
+std::string OctoPrint::make_url(const std::string &path) const
+{
+ if (host.find("http://") == 0 || host.find("https://") == 0) {
+ if (host.back() == '/') {
+ return (boost::format("%1%%2%") % host % path).str();
+ } else {
+ return (boost::format("%1%/%2%") % host % path).str();
+ }
+ } else {
+ return (boost::format("http://%1%/%2%") % host % path).str();
+ }
+}
+
+wxString OctoPrint::format_error(const std::string &body, const std::string &error, unsigned status)
+{
+ if (status != 0) {
+ auto wxbody = wxString::FromUTF8(body.data());
+ return wxString::Format("HTTP %u: %s", status, wxbody);
+ } else {
+ return wxString::FromUTF8(error.data());
+ }
+}
+
+
+}
diff --git a/src/slic3r/Utils/OctoPrint.hpp b/src/slic3r/Utils/OctoPrint.hpp
new file mode 100644
index 000000000..f6c4d58c8
--- /dev/null
+++ b/src/slic3r/Utils/OctoPrint.hpp
@@ -0,0 +1,42 @@
+#ifndef slic3r_OctoPrint_hpp_
+#define slic3r_OctoPrint_hpp_
+
+#include <string>
+#include <wx/string.h>
+
+#include "PrintHost.hpp"
+
+
+namespace Slic3r {
+
+
+class DynamicPrintConfig;
+class Http;
+
+class OctoPrint : public PrintHost
+{
+public:
+ OctoPrint(DynamicPrintConfig *config);
+ virtual ~OctoPrint();
+
+ bool test(wxString &curl_msg) const;
+ wxString get_test_ok_msg () const;
+ wxString get_test_failed_msg (wxString &msg) const;
+ // Send gcode file to octoprint, filename is expected to be in UTF-8
+ bool send_gcode(const std::string &filename) const;
+ bool has_auto_discovery() const;
+ bool can_test() const;
+private:
+ std::string host;
+ std::string apikey;
+ std::string cafile;
+
+ void set_auth(Http &http) const;
+ std::string make_url(const std::string &path) const;
+ static wxString format_error(const std::string &body, const std::string &error, unsigned status);
+};
+
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp
new file mode 100644
index 000000000..2e423dc5e
--- /dev/null
+++ b/src/slic3r/Utils/PresetUpdater.cpp
@@ -0,0 +1,633 @@
+#include "PresetUpdater.hpp"
+
+#include <algorithm>
+#include <thread>
+#include <unordered_map>
+#include <ostream>
+#include <stdexcept>
+#include <boost/format.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/log/trivial.hpp>
+
+#include <wx/app.h>
+#include <wx/event.h>
+#include <wx/msgdlg.h>
+
+#include "libslic3r/libslic3r.h"
+#include "libslic3r/Utils.hpp"
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/PresetBundle.hpp"
+#include "slic3r/GUI/UpdateDialogs.hpp"
+#include "slic3r/GUI/ConfigWizard.hpp"
+#include "slic3r/Utils/Http.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;
+
+
+namespace Slic3r {
+
+
+enum {
+ SLIC3R_VERSION_BODY_MAX = 256,
+};
+
+static const char *INDEX_FILENAME = "index.idx";
+static const char *TMP_EXTENSION = ".download";
+
+
+struct Update
+{
+ fs::path source;
+ fs::path target;
+ Version version;
+
+ Update(fs::path &&source, fs::path &&target, const Version &version) :
+ source(std::move(source)),
+ target(std::move(target)),
+ version(version)
+ {}
+
+ std::string name() const { return source.stem().string(); }
+
+ friend std::ostream& operator<<(std::ostream& os , const Update &self) {
+ os << "Update(" << self.source.string() << " -> " << self.target.string() << ')';
+ return os;
+ }
+};
+
+struct Incompat
+{
+ fs::path bundle;
+ Version version;
+
+ Incompat(fs::path &&bundle, const Version &version) :
+ bundle(std::move(bundle)),
+ version(version)
+ {}
+
+ std::string name() const { return bundle.stem().string(); }
+
+ friend std::ostream& operator<<(std::ostream& os , const Incompat &self) {
+ os << "Incompat(" << self.bundle.string() << ')';
+ return os;
+ }
+};
+
+struct Updates
+{
+ std::vector<Incompat> incompats;
+ std::vector<Update> updates;
+};
+
+
+struct PresetUpdater::priv
+{
+ int version_online_event;
+ std::vector<Index> index_db;
+
+ 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;
+ fs::path vendor_path;
+
+ bool cancel;
+ std::thread thread;
+
+ 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;
+ void prune_tmps() const;
+ void sync_version() const;
+ void sync_config(const std::set<VendorProfile> vendors) const;
+
+ void check_install_indices() const;
+ Updates get_config_updates() const;
+ void perform_updates(Updates &&updates, bool snapshot = true) const;
+
+ static void copy_file(const fs::path &from, const fs::path &to);
+};
+
+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"),
+ cancel(false)
+{
+ set_download_prefs(GUI::get_app_config());
+ check_install_indices();
+ index_db = std::move(Index::load_db());
+}
+
+// Pull relevant preferences from AppConfig
+void PresetUpdater::priv::set_download_prefs(AppConfig *app_config)
+{
+ enabled_version_check = app_config->get("version_check") == "1";
+ version_check_url = app_config->version_check_url();
+ enabled_config_update = app_config->get("preset_update") == "1" && !app_config->legacy_datadir();
+}
+
+// Downloads a file (http get operation). Cancels if the Updater is being destroyed.
+bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &target_path) const
+{
+ bool res = false;
+ fs::path tmp_path = target_path;
+ tmp_path += (boost::format(".%1%%2%") % get_current_pid() % TMP_EXTENSION).str();
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Get: `%1%`\n\t-> `%2%`\n\tvia tmp path `%3%`")
+ % url
+ % target_path.string()
+ % tmp_path.string();
+
+ Http::get(url)
+ .on_progress([this](Http::Progress, bool &cancel) {
+ if (cancel) { cancel = true; }
+ })
+ .on_error([&](std::string body, std::string error, unsigned http_status) {
+ (void)body;
+ BOOST_LOG_TRIVIAL(error) << boost::format("Error getting: `%1%`: HTTP %2%, %3%")
+ % url
+ % http_status
+ % error;
+ })
+ .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;
+ })
+ .perform_sync();
+
+ return res;
+}
+
+// Remove leftover paritally downloaded files, if any.
+void PresetUpdater::priv::prune_tmps() const
+{
+ for (fs::directory_iterator it(cache_path); it != fs::directory_iterator(); ++it) {
+ if (it->path().extension() == TMP_EXTENSION) {
+ BOOST_LOG_TRIVIAL(debug) << "Cache prune: " << it->path().string();
+ fs::remove(it->path());
+ }
+ }
+}
+
+// Get Slic3rPE version available online, save in AppConfig.
+void PresetUpdater::priv::sync_version() const
+{
+ if (! enabled_version_check) { return; }
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Downloading Slic3rPE online version from: `%1%`") % version_check_url;
+
+ Http::get(version_check_url)
+ .size_limit(SLIC3R_VERSION_BODY_MAX)
+ .on_progress([this](Http::Progress, bool &cancel) {
+ cancel = this->cancel;
+ })
+ .on_error([&](std::string body, std::string error, unsigned http_status) {
+ (void)body;
+ BOOST_LOG_TRIVIAL(error) << boost::format("Error getting: `%1%`: HTTP %2%, %3%")
+ % version_check_url
+ % http_status
+ % error;
+ })
+ .on_complete([&](std::string body, unsigned /* http_status */) {
+ boost::trim(body);
+ BOOST_LOG_TRIVIAL(info) << boost::format("Got Slic3rPE online version: `%1%`. Sending to GUI thread...") % body;
+ wxCommandEvent* evt = new wxCommandEvent(version_online_event);
+ evt->SetString(body);
+ GUI::get_app()->QueueEvent(evt);
+ })
+ .perform_sync();
+}
+
+// Download vendor indices. Also download new bundles if an index indicates there's a new one available.
+// Both are saved in cache.
+void PresetUpdater::priv::sync_config(const std::set<VendorProfile> vendors) const
+{
+ BOOST_LOG_TRIVIAL(info) << "Syncing configuration cache";
+
+ if (!enabled_config_update) { return; }
+
+ // Donwload vendor preset bundles
+ for (const auto &index : index_db) {
+ if (cancel) { return; }
+
+ const auto vendor_it = vendors.find(VendorProfile(index.vendor()));
+ if (vendor_it == vendors.end()) {
+ BOOST_LOG_TRIVIAL(warning) << "No such vendor: " << index.vendor();
+ continue;
+ }
+
+ const VendorProfile &vendor = *vendor_it;
+ if (vendor.config_update_url.empty()) {
+ BOOST_LOG_TRIVIAL(info) << "Vendor has no config_update_url: " << vendor.name;
+ continue;
+ }
+
+ // Download a fresh index
+ BOOST_LOG_TRIVIAL(info) << "Downloading index for vendor: " << vendor.name;
+ 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; }
+
+ // 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()) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("No recommended version for vendor: %1%, invalid index?") % vendor.name;
+ continue;
+ }
+ const auto recommended = recommended_it->config_version;
+
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Got index for vendor: %1%: current version: %2%, recommended version: %3%")
+ % vendor.name
+ % vendor.config_version.to_string()
+ % recommended.to_string();
+
+ if (vendor.config_version >= recommended) { continue; }
+
+ // Download a fresh bundle
+ BOOST_LOG_TRIVIAL(info) << "Downloading new bundle for vendor: " << vendor.name;
+ 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; }
+ }
+}
+
+// Install indicies from resources. Only installs those that are either missing or older than in resources.
+void PresetUpdater::priv::check_install_indices() const
+{
+ BOOST_LOG_TRIVIAL(info) << "Checking if indices need to be installed from resources...";
+
+ 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();
+
+ if (! fs::exists(path_in_cache)) {
+ BOOST_LOG_TRIVIAL(info) << "Install index from resources: " << path.filename();
+ copy_file(path, path_in_cache);
+ } else {
+ Index idx_rsrc, idx_cache;
+ idx_rsrc.load(path);
+ idx_cache.load(path_in_cache);
+
+ if (idx_cache.version() < idx_rsrc.version()) {
+ BOOST_LOG_TRIVIAL(info) << "Update index from resources: " << path.filename();
+ copy_file(path, path_in_cache);
+ }
+ }
+ }
+ }
+}
+
+// Generates a list of bundle updates that are to be performed
+Updates PresetUpdater::priv::get_config_updates() const
+{
+ Updates updates;
+
+ BOOST_LOG_TRIVIAL(info) << "Checking for cached configuration updates...";
+
+ for (const auto idx : index_db) {
+ auto bundle_path = vendor_path / (idx.vendor() + ".ini");
+
+ if (! fs::exists(bundle_path)) {
+ BOOST_LOG_TRIVIAL(info) << "Bundle not present for index, skipping: " << idx.vendor();
+ 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()) {
+ auto message = (boost::format("Preset bundle `%1%` version not found in index: %2%") % idx.vendor() % vp.config_version.to_string()).str();
+ BOOST_LOG_TRIVIAL(error) << message;
+ throw std::runtime_error(message);
+ }
+
+ // Getting a recommended version from the latest index, wich may have been downloaded
+ // from the internet, or installed / updated from the installation resources.
+ const auto recommended = idx.recommended();
+ if (recommended == idx.end()) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("No recommended version for vendor: %1%, invalid index?") % idx.vendor();
+ }
+
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Vendor: %1%, version installed: %2%, version cached: %3%")
+ % vp.name
+ % ver_current->config_version.to_string()
+ % recommended->config_version.to_string();
+
+ if (! ver_current->is_current_slic3r_supported()) {
+ BOOST_LOG_TRIVIAL(warning) << "Current Slic3r incompatible with installed bundle: " << bundle_path.string();
+ updates.incompats.emplace_back(std::move(bundle_path), *ver_current);
+ } else if (recommended->config_version > ver_current->config_version) {
+ // Config bundle update situation
+
+ // Check if the update is already present in a snapshot
+ const auto recommended_snap = SnapshotDB::singleton().snapshot_with_vendor_preset(vp.name, recommended->config_version);
+ if (recommended_snap != SnapshotDB::singleton().end()) {
+ BOOST_LOG_TRIVIAL(info) << boost::format("Bundle update %1% %2% already found in snapshot %3%, skipping...")
+ % vp.name
+ % recommended->config_version.to_string()
+ % recommended_snap->id;
+ continue;
+ }
+
+ auto path_src = cache_path / (idx.vendor() + ".ini");
+ auto path_in_rsrc = rsrc_path / (idx.vendor() + ".ini");
+ if (! fs::exists(path_src)) {
+ if (! fs::exists(path_in_rsrc)) {
+ BOOST_LOG_TRIVIAL(warning) << boost::format("Index for vendor %1% indicates update, but bundle found in neither cache nor resources")
+ % idx.vendor();
+ continue;
+ } else {
+ path_src = std::move(path_in_rsrc);
+ path_in_rsrc.clear();
+ }
+ }
+
+ auto new_vp = VendorProfile::from_ini(path_src, false);
+ bool found = false;
+ if (new_vp.config_version == recommended->config_version) {
+ updates.updates.emplace_back(std::move(path_src), std::move(bundle_path), *recommended);
+ found = true;
+ } else if (! path_in_rsrc.empty() && fs::exists(path_in_rsrc)) {
+ new_vp = VendorProfile::from_ini(path_in_rsrc, false);
+ if (new_vp.config_version == recommended->config_version) {
+ updates.updates.emplace_back(std::move(path_in_rsrc), std::move(bundle_path), *recommended);
+ found = true;
+ }
+ }
+ if (! found)
+ BOOST_LOG_TRIVIAL(warning) << boost::format("Index for vendor %1% indicates update (%2%) but the new bundle was found neither in cache nor resources")
+ % idx.vendor()
+ % recommended->config_version.to_string();
+ }
+ }
+
+ return updates;
+}
+
+void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) const
+{
+ if (updates.incompats.size() > 0) {
+ if (snapshot) {
+ BOOST_LOG_TRIVIAL(info) << "Taking a snapshot...";
+ SnapshotDB::singleton().take_snapshot(*GUI::get_app_config(), Snapshot::SNAPSHOT_DOWNGRADE);
+ }
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Deleting %1% incompatible bundles") % updates.incompats.size();
+
+ for (const auto &incompat : updates.incompats) {
+ BOOST_LOG_TRIVIAL(info) << '\t' << incompat;
+ fs::remove(incompat.bundle);
+ }
+ }
+ else if (updates.updates.size() > 0) {
+ if (snapshot) {
+ BOOST_LOG_TRIVIAL(info) << "Taking a snapshot...";
+ SnapshotDB::singleton().take_snapshot(*GUI::get_app_config(), Snapshot::SNAPSHOT_UPGRADE);
+ }
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Performing %1% updates") % updates.updates.size();
+
+ for (const auto &update : updates.updates) {
+ BOOST_LOG_TRIVIAL(info) << '\t' << update;
+
+ copy_file(update.source, update.target);
+
+ PresetBundle bundle;
+ bundle.load_configbundle(update.target.string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM);
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Deleting %1% conflicting presets")
+ % (bundle.prints.size() + bundle.filaments.size() + bundle.printers.size());
+
+ auto preset_remover = [](const Preset &preset) {
+ BOOST_LOG_TRIVIAL(info) << '\t' << preset.file;
+ 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); }
+
+ // Also apply the `obsolete_presets` property, removing obsolete ini files
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Deleting %1% obsolete presets")
+ % (bundle.obsolete_presets.prints.size() + bundle.obsolete_presets.filaments.size() + bundle.obsolete_presets.printers.size());
+
+ auto obsolete_remover = [](const char *subdir, const std::string &preset) {
+ auto path = fs::path(Slic3r::data_dir()) / subdir / preset;
+ path += ".ini";
+ BOOST_LOG_TRIVIAL(info) << '\t' << path.string();
+ fs::remove(path);
+ };
+
+ for (const auto &name : bundle.obsolete_presets.prints) { obsolete_remover("print", name); }
+ for (const auto &name : bundle.obsolete_presets.filaments) { obsolete_remover("filament", name); }
+ for (const auto &name : bundle.obsolete_presets.filaments) { obsolete_remover("sla_material", name); }
+ for (const auto &name : bundle.obsolete_presets.printers) { obsolete_remover("printer", name); }
+ }
+ }
+}
+
+void PresetUpdater::priv::copy_file(const fs::path &source, const fs::path &target)
+{
+ static const auto perms = fs::owner_read | fs::owner_write | fs::group_read | fs::others_read; // aka 644
+
+ // Make sure the file has correct permission both before and after we copy over it
+ if (fs::exists(target)) {
+ fs::permissions(target, perms);
+ }
+ fs::copy_file(source, target, fs::copy_option::overwrite_if_exists);
+ fs::permissions(target, perms);
+}
+
+
+PresetUpdater::PresetUpdater(int version_online_event) :
+ p(new priv(version_online_event))
+{}
+
+
+// Public
+
+PresetUpdater::~PresetUpdater()
+{
+ if (p && p->thread.joinable()) {
+ // This will stop transfers being done by the thread, if any.
+ // Cancelling takes some time, but should complete soon enough.
+ p->cancel = true;
+ p->thread.join();
+ }
+}
+
+void PresetUpdater::sync(PresetBundle *preset_bundle)
+{
+ 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
+ // Unfortunatelly as of C++11, it needs to be copied again
+ // into the closure (but perhaps the compiler can elide this).
+ std::set<VendorProfile> vendors = preset_bundle->vendors;
+
+ p->thread = std::move(std::thread([this, vendors]() {
+ this->p->prune_tmps();
+ this->p->sync_version();
+ this->p->sync_config(std::move(vendors));
+ }));
+}
+
+void PresetUpdater::slic3r_update_notify()
+{
+ if (! p->enabled_version_check) { return; }
+
+ if (p->had_config_update) {
+ BOOST_LOG_TRIVIAL(info) << "New Slic3r version available, but there was a configuration update, notification won't be displayed";
+ return;
+ }
+
+ auto* app_config = GUI::get_app_config();
+ const auto ver_slic3r = Semver::parse(SLIC3R_VERSION);
+ const auto ver_online_str = app_config->get("version_online");
+ const auto ver_online = Semver::parse(ver_online_str);
+ const auto ver_online_seen = Semver::parse(app_config->get("version_online_seen"));
+ if (! ver_slic3r) {
+ throw std::runtime_error("Could not parse Slic3r version string: " SLIC3R_VERSION);
+ }
+
+ if (ver_online) {
+ // Only display the notification if the version available online is newer AND if we haven't seen it before
+ if (*ver_online > *ver_slic3r && (! ver_online_seen || *ver_online_seen < *ver_online)) {
+ GUI::MsgUpdateSlic3r notification(*ver_slic3r, *ver_online);
+ notification.ShowModal();
+ if (notification.disable_version_check()) {
+ app_config->set("version_check", "0");
+ p->enabled_version_check = false;
+ }
+ }
+ app_config->set("version_online_seen", ver_online_str);
+ }
+}
+
+bool PresetUpdater::config_update() const
+{
+ if (! p->enabled_config_update) { return true; }
+
+ auto updates = p->get_config_updates();
+ if (updates.incompats.size() > 0) {
+ BOOST_LOG_TRIVIAL(info) << boost::format("%1% bundles incompatible. Asking for action...") % updates.incompats.size();
+
+ std::unordered_map<std::string, wxString> incompats_map;
+ for (const auto &incompat : updates.incompats) {
+ auto vendor = incompat.name();
+
+ const auto min_slic3r = incompat.version.min_slic3r_version;
+ const auto max_slic3r = incompat.version.max_slic3r_version;
+ wxString restrictions;
+ if (min_slic3r != Semver::zero() && max_slic3r != Semver::inf()) {
+ restrictions = wxString::Format(_(L("requires min. %s and max. %s")),
+ min_slic3r.to_string(),
+ max_slic3r.to_string()
+ );
+ } else if (min_slic3r != Semver::zero()) {
+ restrictions = wxString::Format(_(L("requires min. %s")), min_slic3r.to_string());
+ } else {
+ restrictions = wxString::Format(_(L("requires max. %s")), max_slic3r.to_string());
+ }
+
+ incompats_map.emplace(std::make_pair(std::move(vendor), std::move(restrictions)));
+ }
+
+ p->had_config_update = true; // This needs to be done before a dialog is shown because of OnIdle() + CallAfter() in Perl
+
+ GUI::MsgDataIncompatible dlg(std::move(incompats_map));
+ const auto res = dlg.ShowModal();
+ if (res == wxID_REPLACE) {
+ BOOST_LOG_TRIVIAL(info) << "User wants to re-configure...";
+ p->perform_updates(std::move(updates));
+ GUI::ConfigWizard wizard(nullptr, GUI::ConfigWizard::RR_DATA_INCOMPAT);
+ if (! wizard.run(GUI::get_preset_bundle(), this)) {
+ return false;
+ }
+ GUI::load_current_presets();
+ } else {
+ BOOST_LOG_TRIVIAL(info) << "User wants to exit Slic3r, bye...";
+ return false;
+ }
+ }
+ else if (updates.updates.size() > 0) {
+ BOOST_LOG_TRIVIAL(info) << boost::format("Update of %1% bundles available. Asking for confirmation ...") % updates.updates.size();
+
+ std::unordered_map<std::string, std::string> updates_map;
+ for (const auto &update : updates.updates) {
+ auto vendor = update.name();
+ auto ver_str = update.version.config_version.to_string();
+ if (! update.version.comment.empty()) {
+ ver_str += std::string(" (") + update.version.comment + ")";
+ }
+ updates_map.emplace(std::make_pair(std::move(vendor), std::move(ver_str)));
+ }
+
+ p->had_config_update = true; // Ditto, see above
+
+ GUI::MsgUpdateConfig dlg(std::move(updates_map));
+
+ const auto res = dlg.ShowModal();
+ if (res == wxID_OK) {
+ BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update";
+ p->perform_updates(std::move(updates));
+
+ // Reload global configuration
+ auto *app_config = GUI::get_app_config();
+ GUI::get_preset_bundle()->load_presets(*app_config);
+ GUI::load_current_presets();
+ } else {
+ BOOST_LOG_TRIVIAL(info) << "User refused the update";
+ }
+ } else {
+ BOOST_LOG_TRIVIAL(info) << "No configuration updates available.";
+ }
+
+ return true;
+}
+
+void PresetUpdater::install_bundles_rsrc(std::vector<std::string> bundles, bool snapshot) const
+{
+ Updates updates;
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Installing %1% bundles from resources ...") % bundles.size();
+
+ for (const auto &bundle : bundles) {
+ auto path_in_rsrc = p->rsrc_path / bundle;
+ auto path_in_vendors = p->vendor_path / bundle;
+ updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version());
+ }
+
+ p->perform_updates(std::move(updates), snapshot);
+}
+
+
+}
diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp
new file mode 100644
index 000000000..6a53cca81
--- /dev/null
+++ b/src/slic3r/Utils/PresetUpdater.hpp
@@ -0,0 +1,42 @@
+#ifndef slic3r_PresetUpdate_hpp_
+#define slic3r_PresetUpdate_hpp_
+
+#include <memory>
+#include <vector>
+
+namespace Slic3r {
+
+
+class AppConfig;
+class PresetBundle;
+
+class PresetUpdater
+{
+public:
+ PresetUpdater(int version_online_event);
+ PresetUpdater(PresetUpdater &&) = delete;
+ PresetUpdater(const PresetUpdater &) = delete;
+ PresetUpdater &operator=(PresetUpdater &&) = delete;
+ PresetUpdater &operator=(const PresetUpdater &) = delete;
+ ~PresetUpdater();
+
+ // If either version check or config updating is enabled, get the appropriate data in the background and cache it.
+ void sync(PresetBundle *preset_bundle);
+
+ // If version check is enabled, check if chaced online slic3r version is newer, notify if so.
+ void slic3r_update_notify();
+
+ // If updating is enabled, check if updates are available in cache, if so, ask about installation.
+ // A false return value implies Slic3r should exit due to incompatibility of configuration.
+ bool config_update() const;
+
+ // "Update" a list of bundles from resources (behaves like an online update).
+ void install_bundles_rsrc(std::vector<std::string> bundles, bool snapshot = true) const;
+private:
+ struct priv;
+ std::unique_ptr<priv> p;
+};
+
+
+}
+#endif
diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp
new file mode 100644
index 000000000..dd72bae40
--- /dev/null
+++ b/src/slic3r/Utils/PrintHost.cpp
@@ -0,0 +1,23 @@
+#include "OctoPrint.hpp"
+#include "Duet.hpp"
+
+#include "libslic3r/PrintConfig.hpp"
+
+namespace Slic3r {
+
+
+PrintHost::~PrintHost() {}
+
+PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config)
+{
+ PrintHostType kind = config->option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
+ if (kind == htOctoPrint) {
+ return new OctoPrint(config);
+ } else if (kind == htDuet) {
+ return new Duet(config);
+ }
+ return nullptr;
+}
+
+
+}
diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp
new file mode 100644
index 000000000..bc828ea46
--- /dev/null
+++ b/src/slic3r/Utils/PrintHost.hpp
@@ -0,0 +1,35 @@
+#ifndef slic3r_PrintHost_hpp_
+#define slic3r_PrintHost_hpp_
+
+#include <memory>
+#include <string>
+#include <wx/string.h>
+
+
+namespace Slic3r {
+
+
+class DynamicPrintConfig;
+
+class PrintHost
+{
+public:
+ virtual ~PrintHost();
+
+ virtual bool test(wxString &curl_msg) const = 0;
+ virtual wxString get_test_ok_msg () const = 0;
+ virtual wxString get_test_failed_msg (wxString &msg) const = 0;
+ // Send gcode file to print host, filename is expected to be in UTF-8
+ virtual bool send_gcode(const std::string &filename) const = 0;
+ virtual bool has_auto_discovery() const = 0;
+ virtual bool can_test() const = 0;
+
+ static PrintHost* get_print_host(DynamicPrintConfig *config);
+};
+
+
+
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/PrintHostSendDialog.cpp b/src/slic3r/Utils/PrintHostSendDialog.cpp
new file mode 100644
index 000000000..c5d441f87
--- /dev/null
+++ b/src/slic3r/Utils/PrintHostSendDialog.cpp
@@ -0,0 +1,52 @@
+#include "PrintHostSendDialog.hpp"
+
+#include <wx/frame.h>
+#include <wx/event.h>
+#include <wx/progdlg.h>
+#include <wx/sizer.h>
+#include <wx/stattext.h>
+#include <wx/textctrl.h>
+#include <wx/checkbox.h>
+
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/MsgDialog.hpp"
+
+
+namespace fs = boost::filesystem;
+
+namespace Slic3r {
+
+PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, bool can_start_print) :
+ MsgDialog(nullptr, _(L("Send G-Code to printer host")), _(L("Upload to Printer Host with the following filename:")), wxID_NONE),
+ txt_filename(new wxTextCtrl(this, wxID_ANY, path.filename().wstring())),
+ box_print(new wxCheckBox(this, wxID_ANY, _(L("Start printing after upload")))),
+ can_start_print(can_start_print)
+{
+ auto *label_dir_hint = new wxStaticText(this, wxID_ANY, _(L("Use forward slashes ( / ) as a directory separator if needed.")));
+ label_dir_hint->Wrap(CONTENT_WIDTH);
+
+ content_sizer->Add(txt_filename, 0, wxEXPAND);
+ content_sizer->Add(label_dir_hint);
+ content_sizer->AddSpacer(VERT_SPACING);
+ content_sizer->Add(box_print, 0, wxBOTTOM, 2*VERT_SPACING);
+
+ btn_sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL));
+
+ txt_filename->SetFocus();
+ wxString stem(path.stem().wstring());
+ txt_filename->SetSelection(0, stem.Length());
+
+ box_print->Enable(can_start_print);
+
+ Fit();
+}
+
+fs::path PrintHostSendDialog::filename() const
+{
+ return fs::path(txt_filename->GetValue().wx_str());
+}
+
+bool PrintHostSendDialog::print() const
+{
+ return box_print->GetValue(); }
+}
diff --git a/src/slic3r/Utils/PrintHostSendDialog.hpp b/src/slic3r/Utils/PrintHostSendDialog.hpp
new file mode 100644
index 000000000..dc4a8d6f7
--- /dev/null
+++ b/src/slic3r/Utils/PrintHostSendDialog.hpp
@@ -0,0 +1,38 @@
+#ifndef slic3r_PrintHostSendDialog_hpp_
+#define slic3r_PrintHostSendDialog_hpp_
+
+#include <string>
+
+#include <boost/filesystem/path.hpp>
+
+#include <wx/string.h>
+#include <wx/frame.h>
+#include <wx/event.h>
+#include <wx/progdlg.h>
+#include <wx/sizer.h>
+#include <wx/stattext.h>
+#include <wx/textctrl.h>
+#include <wx/checkbox.h>
+
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/MsgDialog.hpp"
+
+
+namespace Slic3r {
+
+class PrintHostSendDialog : public GUI::MsgDialog
+{
+private:
+ wxTextCtrl *txt_filename;
+ wxCheckBox *box_print;
+ bool can_start_print;
+
+public:
+ PrintHostSendDialog(const boost::filesystem::path &path, bool can_start_print);
+ boost::filesystem::path filename() const;
+ bool print() const;
+};
+
+}
+
+#endif
diff --git a/src/slic3r/Utils/Semver.hpp b/src/slic3r/Utils/Semver.hpp
new file mode 100644
index 000000000..736f9b891
--- /dev/null
+++ b/src/slic3r/Utils/Semver.hpp
@@ -0,0 +1,151 @@
+#ifndef slic3r_Semver_hpp_
+#define slic3r_Semver_hpp_
+
+#include <string>
+#include <cstring>
+#include <ostream>
+#include <stdexcept>
+#include <boost/optional.hpp>
+#include <boost/format.hpp>
+
+#include "semver/semver.h"
+
+namespace Slic3r {
+
+
+class Semver
+{
+public:
+ struct Major { const int i; Major(int i) : i(i) {} };
+ 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<const std::string&> metadata = boost::none,
+ boost::optional<const std::string&> prerelease = boost::none)
+ : ver(semver_zero())
+ {
+ ver.major = major;
+ ver.minor = minor;
+ ver.patch = patch;
+ set_metadata(metadata);
+ set_prerelease(prerelease);
+ }
+
+ Semver(const std::string &str) : ver(semver_zero())
+ {
+ auto parsed = parse(str);
+ if (! parsed) {
+ throw std::runtime_error(std::string("Could not parse version string: ") + str);
+ }
+ ver = parsed->ver;
+ parsed->ver = semver_zero();
+ }
+
+ static boost::optional<Semver> parse(const std::string &str)
+ {
+ semver_t ver = semver_zero();
+ if (::semver_parse(str.c_str(), &ver) == 0) {
+ return Semver(ver);
+ } else {
+ return boost::none;
+ }
+ }
+
+ static const Semver zero() { return Semver(semver_zero()); }
+
+ static const Semver inf()
+ {
+ static semver_t ver = { std::numeric_limits<int>::max(), std::numeric_limits<int>::max(), std::numeric_limits<int>::max(), nullptr, nullptr };
+ return Semver(ver);
+ }
+
+ static const Semver invalid()
+ {
+ static semver_t ver = { -1, 0, 0, nullptr, nullptr };
+ return Semver(ver);
+ }
+
+ 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 = semver_zero();
+ return *this;
+ }
+
+ Semver &operator=(const Semver &other)
+ {
+ ::semver_free(&ver);
+ ver = ::semver_copy(&other.ver);
+ return *this;
+ }
+
+ ~Semver() { ::semver_free(&ver); }
+
+ // const accessors
+ 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; }
+
+ // Setters
+ void set_maj(int maj) { ver.major = maj; }
+ void set_min(int min) { ver.minor = min; }
+ void set_patch(int patch) { ver.patch = patch; }
+ void set_metadata(boost::optional<const std::string&> meta) { ver.metadata = meta ? strdup(*meta) : nullptr; }
+ void set_prerelease(boost::optional<const std::string&> pre) { ver.prerelease = pre ? strdup(*pre) : nullptr; }
+
+ // 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; }
+ bool operator==(const Semver &b) const { return ::semver_compare(ver, b.ver) == 0; }
+ bool operator!=(const Semver &b) const { return ::semver_compare(ver, b.ver) != 0; }
+ bool operator>=(const Semver &b) const { return ::semver_compare(ver, b.ver) >= 0; }
+ bool operator>(const Semver &b) const { return ::semver_compare(ver, b.ver) == 1; }
+ // We're using '&' instead of the '~' operator here as '~' is unary-only:
+ // Satisfies patch if Major and minor are equal.
+ bool operator&(const Semver &b) const { return ::semver_satisfies_patch(ver, b.ver); }
+ bool operator^(const Semver &b) const { return ::semver_satisfies_caret(ver, b.ver); }
+ bool in_range(const Semver &low, const Semver &high) const { return low <= *this && *this <= high; }
+
+ // Conversion
+ std::string to_string() const {
+ auto res = (boost::format("%1%.%2%.%3%") % ver.major % ver.minor % ver.patch).str();
+ if (ver.prerelease != nullptr) { res += '-'; res += ver.prerelease; }
+ if (ver.metadata != nullptr) { res += '+'; res += ver.metadata; }
+ return res;
+ }
+
+ // Arithmetics
+ Semver& operator+=(const Major &b) { ver.major += b.i; return *this; }
+ Semver& operator+=(const Minor &b) { ver.minor += b.i; return *this; }
+ Semver& operator+=(const Patch &b) { ver.patch += b.i; return *this; }
+ Semver& operator-=(const Major &b) { ver.major -= b.i; return *this; }
+ Semver& operator-=(const Minor &b) { ver.minor -= b.i; return *this; }
+ Semver& operator-=(const Patch &b) { ver.patch -= b.i; return *this; }
+ Semver operator+(const Major &b) const { Semver res(*this); return res += b; }
+ Semver operator+(const Minor &b) const { Semver res(*this); return res += b; }
+ Semver operator+(const Patch &b) const { Semver res(*this); return res += b; }
+ Semver operator-(const Major &b) const { Semver res(*this); return res -= b; }
+ Semver operator-(const Minor &b) const { Semver res(*this); return res -= b; }
+ Semver operator-(const Patch &b) const { Semver res(*this); return res -= b; }
+
+private:
+ semver_t ver;
+
+ Semver(semver_t ver) : ver(ver) {}
+
+ static semver_t semver_zero() { return { 0, 0, 0, nullptr, nullptr }; }
+ static char * strdup(const std::string &str) { return ::semver_strdup(const_cast<char*>(str.c_str())); }
+};
+
+
+}
+#endif
diff --git a/src/slic3r/Utils/Serial.cpp b/src/slic3r/Utils/Serial.cpp
new file mode 100644
index 000000000..601719b50
--- /dev/null
+++ b/src/slic3r/Utils/Serial.cpp
@@ -0,0 +1,495 @@
+#include "Serial.hpp"
+
+#include <algorithm>
+#include <string>
+#include <vector>
+#include <chrono>
+#include <thread>
+#include <fstream>
+#include <stdexcept>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/format.hpp>
+#include <boost/optional.hpp>
+
+#if _WIN32
+ #include <Windows.h>
+ #include <Setupapi.h>
+ #include <initguid.h>
+ #include <devguid.h>
+ #include <regex>
+ // Undefine min/max macros incompatible with the standard library
+ // For example, std::numeric_limits<std::streamsize>::max()
+ // produces some weird errors
+ #ifdef min
+ #undef min
+ #endif
+ #ifdef max
+ #undef max
+ #endif
+ #include "boost/nowide/convert.hpp"
+ #pragma comment(lib, "user32.lib")
+#elif __APPLE__
+ #include <CoreFoundation/CoreFoundation.h>
+ #include <CoreFoundation/CFString.h>
+ #include <IOKit/IOKitLib.h>
+ #include <IOKit/serial/IOSerialKeys.h>
+ #include <IOKit/serial/ioss.h>
+ #include <sys/syslimits.h>
+#endif
+
+#ifndef _WIN32
+ #include <sys/ioctl.h>
+ #include <sys/time.h>
+ #include <sys/unistd.h>
+ #include <sys/select.h>
+#endif
+
+#if defined(__APPLE__) || defined(__OpenBSD__)
+ #include <termios.h>
+#elif defined __linux__
+ #include <fcntl.h>
+ #include <asm-generic/ioctls.h>
+#endif
+
+using boost::optional;
+
+
+namespace Slic3r {
+namespace Utils {
+
+static bool looks_like_printer(const std::string &friendly_name)
+{
+ return friendly_name.find("Original Prusa") != std::string::npos;
+}
+
+#if _WIN32
+void parse_hardware_id(const std::string &hardware_id, SerialPortInfo &spi)
+{
+ unsigned vid, pid;
+ std::regex pattern("USB\\\\.*VID_([[:xdigit:]]+)&PID_([[:xdigit:]]+).*");
+ std::smatch matches;
+ if (std::regex_match(hardware_id, matches, pattern)) {
+ try {
+ vid = std::stoul(matches[1].str(), 0, 16);
+ pid = std::stoul(matches[2].str(), 0, 16);
+ spi.id_vendor = vid;
+ spi.id_product = pid;
+ }
+ catch (...) {}
+ }
+}
+#endif
+
+#ifdef __linux__
+optional<std::string> sysfs_tty_prop(const std::string &tty_dev, const std::string &name)
+{
+ const auto prop_path = (boost::format("/sys/class/tty/%1%/device/../%2%") % tty_dev % name).str();
+ std::ifstream file(prop_path);
+ std::string res;
+
+ std::getline(file, res);
+ if (file.good()) { return res; }
+ else { return boost::none; }
+}
+
+optional<unsigned long> sysfs_tty_prop_hex(const std::string &tty_dev, const std::string &name)
+{
+ auto prop = sysfs_tty_prop(tty_dev, name);
+ if (!prop) { return boost::none; }
+
+ try { return std::stoul(*prop, 0, 16); }
+ catch (...) { return boost::none; }
+}
+#endif
+
+std::vector<SerialPortInfo> scan_serial_ports_extended()
+{
+ std::vector<SerialPortInfo> output;
+
+#ifdef _WIN32
+ SP_DEVINFO_DATA devInfoData = { 0 };
+ devInfoData.cbSize = sizeof(devInfoData);
+ // Get the tree containing the info for the ports.
+ HDEVINFO hDeviceInfo = SetupDiGetClassDevs(&GUID_DEVCLASS_PORTS, 0, nullptr, DIGCF_PRESENT);
+ if (hDeviceInfo != INVALID_HANDLE_VALUE) {
+ // Iterate over all the devices in the tree.
+ for (int nDevice = 0; SetupDiEnumDeviceInfo(hDeviceInfo, nDevice, &devInfoData); ++ nDevice) {
+ SerialPortInfo port_info;
+ // Get the registry key which stores the ports settings.
+ HKEY hDeviceKey = SetupDiOpenDevRegKey(hDeviceInfo, &devInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE);
+ if (hDeviceKey) {
+ // Read in the name of the port.
+ wchar_t pszPortName[4096];
+ DWORD dwSize = sizeof(pszPortName);
+ DWORD dwType = 0;
+ if (RegQueryValueEx(hDeviceKey, L"PortName", NULL, &dwType, (LPBYTE)pszPortName, &dwSize) == ERROR_SUCCESS)
+ port_info.port = boost::nowide::narrow(pszPortName);
+ RegCloseKey(hDeviceKey);
+ if (port_info.port.empty())
+ continue;
+ }
+
+ // Find the size required to hold the device info.
+ DWORD regDataType;
+ DWORD reqSize = 0;
+ SetupDiGetDeviceRegistryProperty(hDeviceInfo, &devInfoData, SPDRP_HARDWAREID, nullptr, nullptr, 0, &reqSize);
+ std::vector<wchar_t> hardware_id(reqSize > 1 ? reqSize : 1);
+ // Now store it in a buffer.
+ if (! SetupDiGetDeviceRegistryProperty(hDeviceInfo, &devInfoData, SPDRP_HARDWAREID, &regDataType, (BYTE*)hardware_id.data(), reqSize, nullptr))
+ continue;
+ parse_hardware_id(boost::nowide::narrow(hardware_id.data()), port_info);
+
+ // Find the size required to hold the friendly name.
+ reqSize = 0;
+ SetupDiGetDeviceRegistryProperty(hDeviceInfo, &devInfoData, SPDRP_FRIENDLYNAME, nullptr, nullptr, 0, &reqSize);
+ std::vector<wchar_t> friendly_name;
+ friendly_name.reserve(reqSize > 1 ? reqSize : 1);
+ // Now store it in a buffer.
+ if (! SetupDiGetDeviceRegistryProperty(hDeviceInfo, &devInfoData, SPDRP_FRIENDLYNAME, nullptr, (BYTE*)friendly_name.data(), reqSize, nullptr)) {
+ port_info.friendly_name = port_info.port;
+ } else {
+ port_info.friendly_name = boost::nowide::narrow(friendly_name.data());
+ port_info.is_printer = looks_like_printer(port_info.friendly_name);
+ }
+ output.emplace_back(std::move(port_info));
+ }
+ }
+#elif __APPLE__
+ // inspired by https://sigrok.org/wiki/Libserialport
+ CFMutableDictionaryRef classes = IOServiceMatching(kIOSerialBSDServiceValue);
+ if (classes != 0) {
+ io_iterator_t iter;
+ if (IOServiceGetMatchingServices(kIOMasterPortDefault, classes, &iter) == KERN_SUCCESS) {
+ io_object_t port;
+ while ((port = IOIteratorNext(iter)) != 0) {
+ CFTypeRef cf_property = IORegistryEntryCreateCFProperty(port, CFSTR(kIOCalloutDeviceKey), kCFAllocatorDefault, 0);
+ if (cf_property) {
+ char path[PATH_MAX];
+ Boolean result = CFStringGetCString((CFStringRef)cf_property, path, sizeof(path), kCFStringEncodingUTF8);
+ CFRelease(cf_property);
+ if (result) {
+ SerialPortInfo port_info;
+ port_info.port = path;
+
+ // Attempt to read out the device friendly name
+ if ((cf_property = IORegistryEntrySearchCFProperty(port, kIOServicePlane,
+ CFSTR("USB Interface Name"), kCFAllocatorDefault,
+ kIORegistryIterateRecursively | kIORegistryIterateParents)) ||
+ (cf_property = IORegistryEntrySearchCFProperty(port, kIOServicePlane,
+ CFSTR("USB Product Name"), kCFAllocatorDefault,
+ kIORegistryIterateRecursively | kIORegistryIterateParents)) ||
+ (cf_property = IORegistryEntrySearchCFProperty(port, kIOServicePlane,
+ CFSTR("Product Name"), kCFAllocatorDefault,
+ kIORegistryIterateRecursively | kIORegistryIterateParents)) ||
+ (cf_property = IORegistryEntryCreateCFProperty(port,
+ CFSTR(kIOTTYDeviceKey), kCFAllocatorDefault, 0))) {
+ // Description limited to 127 char, anything longer would not be user friendly anyway.
+ char description[128];
+ if (CFStringGetCString((CFStringRef)cf_property, description, sizeof(description), kCFStringEncodingUTF8)) {
+ port_info.friendly_name = std::string(description) + " (" + port_info.port + ")";
+ port_info.is_printer = looks_like_printer(port_info.friendly_name);
+ }
+ CFRelease(cf_property);
+ }
+ if (port_info.friendly_name.empty())
+ port_info.friendly_name = port_info.port;
+
+ // Attempt to read out the VID & PID
+ int vid, pid;
+ auto cf_vendor = IORegistryEntrySearchCFProperty(port, kIOServicePlane, CFSTR("idVendor"),
+ kCFAllocatorDefault, kIORegistryIterateRecursively | kIORegistryIterateParents);
+ auto cf_product = IORegistryEntrySearchCFProperty(port, kIOServicePlane, CFSTR("idProduct"),
+ kCFAllocatorDefault, kIORegistryIterateRecursively | kIORegistryIterateParents);
+ if (cf_vendor && cf_product) {
+ if (CFNumberGetValue((CFNumberRef)cf_vendor, kCFNumberIntType, &vid) &&
+ CFNumberGetValue((CFNumberRef)cf_product, kCFNumberIntType, &pid)) {
+ port_info.id_vendor = vid;
+ port_info.id_product = pid;
+ }
+ }
+ if (cf_vendor) { CFRelease(cf_vendor); }
+ if (cf_product) { CFRelease(cf_product); }
+
+ output.emplace_back(std::move(port_info));
+ }
+ }
+ IOObjectRelease(port);
+ }
+ }
+ }
+#else
+ // UNIX / Linux
+ std::initializer_list<const char*> prefixes { "ttyUSB" , "ttyACM", "tty.", "cu.", "rfcomm" };
+ for (auto &dir_entry : boost::filesystem::directory_iterator(boost::filesystem::path("/dev"))) {
+ std::string name = dir_entry.path().filename().string();
+ for (const char *prefix : prefixes) {
+ if (boost::starts_with(name, prefix)) {
+ const auto path = dir_entry.path().string();
+ SerialPortInfo spi;
+ spi.port = path;
+#ifdef __linux__
+ auto friendly_name = sysfs_tty_prop(name, "product");
+ if (friendly_name) {
+ spi.is_printer = looks_like_printer(*friendly_name);
+ spi.friendly_name = (boost::format("%1% (%2%)") % *friendly_name % path).str();
+ } else {
+ spi.friendly_name = path;
+ }
+ auto vid = sysfs_tty_prop_hex(name, "idVendor");
+ auto pid = sysfs_tty_prop_hex(name, "idProduct");
+ if (vid && pid) {
+ spi.id_vendor = *vid;
+ spi.id_product = *pid;
+ }
+#else
+ spi.friendly_name = path;
+#endif
+ output.emplace_back(std::move(spi));
+ break;
+ }
+ }
+ }
+#endif
+
+ output.erase(std::remove_if(output.begin(), output.end(),
+ [](const SerialPortInfo &info) {
+ return boost::starts_with(info.port, "Bluetooth") || boost::starts_with(info.port, "FireFly");
+ }),
+ output.end());
+ return output;
+}
+
+std::vector<std::string> scan_serial_ports()
+{
+ std::vector<SerialPortInfo> ports = scan_serial_ports_extended();
+ std::vector<std::string> output;
+ output.reserve(ports.size());
+ for (const SerialPortInfo &spi : ports)
+ output.emplace_back(std::move(spi.port));
+ return output;
+}
+
+
+
+// Class Serial
+
+namespace asio = boost::asio;
+using boost::system::error_code;
+
+Serial::Serial(asio::io_service& io_service) :
+ asio::serial_port(io_service)
+{}
+
+Serial::Serial(asio::io_service& io_service, const std::string &name, unsigned baud_rate) :
+ asio::serial_port(io_service, name)
+{
+ set_baud_rate(baud_rate);
+}
+
+Serial::~Serial() {}
+
+void Serial::set_baud_rate(unsigned baud_rate)
+{
+ try {
+ // This does not support speeds > 115200
+ set_option(boost::asio::serial_port_base::baud_rate(baud_rate));
+ } catch (boost::system::system_error &) {
+ auto handle = native_handle();
+
+ auto handle_errno = [](int retval) {
+ if (retval != 0) {
+ throw std::runtime_error(
+ (boost::format("Could not set baud rate: %1%") % strerror(errno)).str()
+ );
+ }
+ };
+
+#if __APPLE__
+ termios ios;
+ handle_errno(::tcgetattr(handle, &ios));
+ handle_errno(::cfsetspeed(&ios, baud_rate));
+ speed_t newSpeed = baud_rate;
+ handle_errno(::ioctl(handle, IOSSIOSPEED, &newSpeed));
+ handle_errno(::tcsetattr(handle, TCSANOW, &ios));
+#elif __linux
+
+ /* The following definitions are kindly borrowed from:
+ /usr/include/asm-generic/termbits.h
+ Unfortunately we cannot just include that one because
+ it would redefine the "struct termios" already defined
+ the <termios.h> already included by Boost.ASIO. */
+#define K_NCCS 19
+ struct termios2 {
+ tcflag_t c_iflag;
+ tcflag_t c_oflag;
+ tcflag_t c_cflag;
+ tcflag_t c_lflag;
+ cc_t c_line;
+ cc_t c_cc[K_NCCS];
+ speed_t c_ispeed;
+ speed_t c_ospeed;
+ };
+#define BOTHER CBAUDEX
+
+ termios2 ios;
+ handle_errno(::ioctl(handle, TCGETS2, &ios));
+ ios.c_ispeed = ios.c_ospeed = baud_rate;
+ ios.c_cflag &= ~CBAUD;
+ ios.c_cflag |= BOTHER | CLOCAL | CREAD;
+ ios.c_cc[VMIN] = 1; // Minimum of characters to read, prevents eof errors when 0 bytes are read
+ ios.c_cc[VTIME] = 1;
+ handle_errno(::ioctl(handle, TCSETS2, &ios));
+
+#elif __OpenBSD__
+ struct termios ios;
+ handle_errno(::tcgetattr(handle, &ios));
+ handle_errno(::cfsetspeed(&ios, baud_rate));
+ handle_errno(::tcsetattr(handle, TCSAFLUSH, &ios));
+#else
+ throw std::runtime_error("Custom baud rates are not currently supported on this OS");
+#endif
+ }
+}
+
+void Serial::set_DTR(bool on)
+{
+ auto handle = native_handle();
+#if defined(_WIN32) && !defined(__SYMBIAN32__)
+ if (! EscapeCommFunction(handle, on ? SETDTR : CLRDTR)) {
+ throw std::runtime_error("Could not set serial port DTR");
+ }
+#else
+ int status;
+ if (::ioctl(handle, TIOCMGET, &status) == 0) {
+ on ? status |= TIOCM_DTR : status &= ~TIOCM_DTR;
+ if (::ioctl(handle, TIOCMSET, &status) == 0) {
+ return;
+ }
+ }
+
+ throw std::runtime_error(
+ (boost::format("Could not set serial port DTR: %1%") % strerror(errno)).str()
+ );
+#endif
+}
+
+void Serial::reset_line_num()
+{
+ // See https://github.com/MarlinFirmware/Marlin/wiki/M110
+ write_string("M110 N0\n");
+ m_line_num = 0;
+}
+
+bool Serial::read_line(unsigned timeout, std::string &line, error_code &ec)
+{
+ auto &io_service = get_io_service();
+ asio::deadline_timer timer(io_service);
+ char c = 0;
+ bool fail = false;
+
+ while (true) {
+ io_service.reset();
+
+ asio::async_read(*this, boost::asio::buffer(&c, 1), [&](const error_code &read_ec, size_t size) {
+ if (ec || size == 0) {
+ fail = true;
+ ec = read_ec; // FIXME: only if operation not aborted
+ }
+ timer.cancel(); // FIXME: ditto
+ });
+
+ if (timeout > 0) {
+ timer.expires_from_now(boost::posix_time::milliseconds(timeout));
+ timer.async_wait([&](const error_code &ec) {
+ // Ignore timer aborts
+ if (!ec) {
+ fail = true;
+ this->cancel();
+ }
+ });
+ }
+
+ io_service.run();
+
+ if (fail) {
+ return false;
+ } else if (c != '\n') {
+ line += c;
+ } else {
+ return true;
+ }
+ }
+}
+
+void Serial::printer_setup()
+{
+ printer_reset();
+ write_string("\r\r\r\r\r\r\r\r\r\r"); // Gets rid of line noise, if any
+}
+
+size_t Serial::write_string(const std::string &str)
+{
+ // TODO: might be wise to timeout here as well
+ return asio::write(*this, asio::buffer(str));
+}
+
+bool Serial::printer_ready_wait(unsigned retries, unsigned timeout)
+{
+ std::string line;
+ error_code ec;
+
+ for (; retries > 0; retries--) {
+ reset_line_num();
+
+ while (read_line(timeout, line, ec)) {
+ if (line == "ok") {
+ return true;
+ }
+ line.clear();
+ }
+
+ line.clear();
+ }
+
+ return false;
+}
+
+size_t Serial::printer_write_line(const std::string &line, unsigned line_num)
+{
+ const auto formatted_line = Utils::Serial::printer_format_line(line, line_num);
+ return write_string(formatted_line);
+}
+
+size_t Serial::printer_write_line(const std::string &line)
+{
+ m_line_num++;
+ return printer_write_line(line, m_line_num);
+}
+
+void Serial::printer_reset()
+{
+ this->set_DTR(false);
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
+ this->set_DTR(true);
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
+ this->set_DTR(false);
+ std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+}
+
+std::string Serial::printer_format_line(const std::string &line, unsigned line_num)
+{
+ const auto line_num_str = std::to_string(line_num);
+
+ unsigned checksum = 'N';
+ for (auto c : line_num_str) { checksum ^= c; }
+ checksum ^= ' ';
+ for (auto c : line) { checksum ^= c; }
+
+ return (boost::format("N%1% %2%*%3%\n") % line_num_str % line % checksum).str();
+}
+
+
+} // namespace Utils
+} // namespace Slic3r
diff --git a/src/slic3r/Utils/Serial.hpp b/src/slic3r/Utils/Serial.hpp
new file mode 100644
index 000000000..e4a28de09
--- /dev/null
+++ b/src/slic3r/Utils/Serial.hpp
@@ -0,0 +1,82 @@
+#ifndef slic3r_GUI_Utils_Serial_hpp_
+#define slic3r_GUI_Utils_Serial_hpp_
+
+#include <vector>
+#include <string>
+#include <boost/system/error_code.hpp>
+#include <boost/asio.hpp>
+
+
+namespace Slic3r {
+namespace Utils {
+
+struct SerialPortInfo {
+ std::string port;
+ unsigned id_vendor = -1;
+ unsigned id_product = -1;
+ std::string friendly_name;
+ bool is_printer = false;
+
+ bool id_match(unsigned id_vendor, unsigned id_product) const { return id_vendor == this->id_vendor && id_product == this->id_product; }
+};
+
+inline bool operator==(const SerialPortInfo &sp1, const SerialPortInfo &sp2)
+{
+ return
+ sp1.port == sp2.port &&
+ sp1.id_vendor == sp2.id_vendor &&
+ sp1.id_product == sp2.id_product &&
+ sp1.is_printer == sp2.is_printer;
+}
+
+extern std::vector<std::string> scan_serial_ports();
+extern std::vector<SerialPortInfo> scan_serial_ports_extended();
+
+
+class Serial : public boost::asio::serial_port
+{
+public:
+ Serial(boost::asio::io_service &io_service);
+ Serial(boost::asio::io_service &io_service, const std::string &name, unsigned baud_rate);
+ Serial(const Serial &) = delete;
+ Serial &operator=(const Serial &) = delete;
+ ~Serial();
+
+ void set_baud_rate(unsigned baud_rate);
+ void set_DTR(bool on);
+
+ // Resets the line number both internally as well as with the firmware using M110
+ void reset_line_num();
+
+ // Reads a line or times out, the timeout is in milliseconds
+ bool read_line(unsigned timeout, std::string &line, boost::system::error_code &ec);
+
+ // Perform an initial setup for communicating with a printer
+ void printer_setup();
+
+ // Write data from a string
+ size_t write_string(const std::string &str);
+
+ // Attempts to reset the line numer and waits until the printer says "ok"
+ bool printer_ready_wait(unsigned retries, unsigned timeout);
+
+ // Write Marlin-formatted line, with a line number and a checksum
+ size_t printer_write_line(const std::string &line, unsigned line_num);
+
+ // Same as above, but with internally-managed line number
+ size_t printer_write_line(const std::string &line);
+
+ // Toggles DTR to reset the printer
+ void printer_reset();
+
+ // Formats a line Marlin-style, ie. with a sequential number and a checksum
+ static std::string printer_format_line(const std::string &line, unsigned line_num);
+private:
+ unsigned m_line_num = 0;
+};
+
+
+} // Utils
+} // Slic3r
+
+#endif /* slic3r_GUI_Utils_Serial_hpp_ */
diff --git a/src/slic3r/Utils/Time.cpp b/src/slic3r/Utils/Time.cpp
new file mode 100644
index 000000000..f38c4b407
--- /dev/null
+++ b/src/slic3r/Utils/Time.cpp
@@ -0,0 +1,80 @@
+#include "Time.hpp"
+
+#ifdef WIN32
+ #define WIN32_LEAN_AND_MEAN
+ #include <windows.h>
+ #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, 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
+ tms.tm_mon = M - 1; // 0-11
+ tms.tm_mday = d; // 1-31
+ tms.tm_hour = h; // 0-23
+ tms.tm_min = m; // 0-59
+ tms.tm_sec = s; // 0-61 (0-60 in C++11)
+ return mktime(&tms);
+}
+
+std::string format_time_ISO8601Z(time_t time)
+{
+ struct tm tms;
+#ifdef WIN32
+ gmtime_s(&tms, &time);
+#else
+ gmtime_r(&time, &tms);
+#endif
+ char buf[128];
+ 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
+ SYSTEMTIME st;
+ ::GetSystemTime(&st);
+ std::tm tm;
+ tm.tm_sec = st.wSecond;
+ tm.tm_min = st.wMinute;
+ tm.tm_hour = st.wHour;
+ tm.tm_mday = st.wDay;
+ tm.tm_mon = st.wMonth - 1;
+ tm.tm_year = st.wYear - 1900;
+ tm.tm_isdst = -1;
+ return mktime(&tm);
+#else
+ const time_t current_local = time(nullptr);
+ return mktime(gmtime(&current_local));
+#endif
+}
+
+}; // namespace Utils
+}; // namespace Slic3r
diff --git a/src/slic3r/Utils/Time.hpp b/src/slic3r/Utils/Time.hpp
new file mode 100644
index 000000000..7b670bd3e
--- /dev/null
+++ b/src/slic3r/Utils/Time.hpp
@@ -0,0 +1,25 @@
+#ifndef slic3r_Utils_Time_hpp_
+#define slic3r_Utils_Time_hpp_
+
+#include <string>
+#include <time.h>
+
+namespace Slic3r {
+namespace Utils {
+
+// Utilities to convert an UTC time_t to/from an ISO8601 time format,
+// useful for putting timestamps into file and directory names.
+// Returns (time_t)-1 on error.
+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.
+extern time_t get_current_time_utc();
+
+}; // namespace Utils
+}; // namespace Slic3r
+
+#endif /* slic3r_Utils_Time_hpp_ */