diff options
Diffstat (limited to 'src/slic3r')
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 §ion : 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, ¤t_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, ¤t_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 ©, 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 ©, 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 ©, 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 ©, 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 ©, 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 © 2016-2018 Prusa Research. <br />" + "Copyright © 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 §ion : 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 §ion, 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 §ion, 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 §ion, 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 §ion, 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 §ion, 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 §ion) + { 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 © : *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, ¶ms); + if (params == GL_FALSE) { + // Compilation failed. Get the log. + glGetShaderiv(this->fragment_program_id, GL_INFO_LOG_LENGTH, ¶ms); + std::vector<char> msg(params); + glGetShaderInfoLog(this->fragment_program_id, params, ¶ms, 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, ¶ms); + if (params == GL_FALSE) { + // Compilation failed. Get the log. + glGetShaderiv(this->vertex_program_id, GL_INFO_LOG_LENGTH, ¶ms); + std::vector<char> msg(params); + glGetShaderInfoLog(this->vertex_program_id, params, ¶ms, 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, ¶ms); + if (params == GL_FALSE) { + // Linking failed. Get the log. + glGetProgramiv(this->vertex_program_id, GL_INFO_LOG_LENGTH, ¶ms); + std::vector<char> msg(params); + glGetProgramInfoLog(this->vertex_program_id, params, ¶ms, 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 §ion : tree) { + if (boost::starts_with(section.first, printer_model_key)) { + VendorProfile::PrinterModel model; + model.id = section.first.substr(printer_model_key.size()); + model.name = section.second.get<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 §ion : 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()), §ion.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 §ion : 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, ®DataType, (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(¤t_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_ */ |