mirror of
				https://github.com/SoftFever/OrcaSlicer.git
				synced 2025-10-30 20:21:12 -06:00 
			
		
		
		
	Fixed conflicts after merge with master
This commit is contained in:
		
						commit
						e5c45405d4
					
				
					 80 changed files with 4288 additions and 2581 deletions
				
			
		|  | @ -144,12 +144,19 @@ set(SLIC3R_GUI_SOURCES | |||
|     GUI/UpdateDialogs.hpp | ||||
|     GUI/FirmwareDialog.cpp | ||||
|     GUI/FirmwareDialog.hpp | ||||
|     GUI/ProgressIndicator.hpp | ||||
|     GUI/ProgressStatusBar.hpp | ||||
|     GUI/ProgressStatusBar.cpp | ||||
|     GUI/PrintHostDialogs.cpp | ||||
|     GUI/PrintHostDialogs.hpp | ||||
|     GUI/Job.hpp | ||||
|     GUI/Jobs/Job.hpp | ||||
|     GUI/Jobs/Job.cpp | ||||
|     GUI/Jobs/ArrangeJob.hpp | ||||
|     GUI/Jobs/ArrangeJob.cpp | ||||
|     GUI/Jobs/RotoptimizeJob.hpp | ||||
|     GUI/Jobs/RotoptimizeJob.cpp | ||||
|     GUI/Jobs/SLAImportJob.hpp | ||||
|     GUI/Jobs/SLAImportJob.cpp | ||||
|     GUI/Jobs/ProgressIndicator.hpp | ||||
|     GUI/ProgressStatusBar.hpp | ||||
|     GUI/ProgressStatusBar.cpp | ||||
|     GUI/Mouse3DController.cpp | ||||
|     GUI/Mouse3DController.hpp | ||||
|     GUI/DoubleSlider.cpp | ||||
|  | @ -179,6 +186,8 @@ set(SLIC3R_GUI_SOURCES | |||
|     Utils/HexFile.cpp | ||||
|     Utils/HexFile.hpp | ||||
|     Utils/Thread.hpp | ||||
|     Utils/SLAImport.hpp | ||||
|     Utils/SLAImport.cpp | ||||
| ) | ||||
| 
 | ||||
| if (APPLE) | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ | |||
| #include "libslic3r/Utils.hpp" | ||||
| #include "libslic3r/GCode/PostProcessor.hpp" | ||||
| #include "libslic3r/GCode/PreviewData.hpp" | ||||
| #include "libslic3r/Format/SL1.hpp" | ||||
| #include "libslic3r/libslic3r.h" | ||||
| 
 | ||||
| #include <cassert> | ||||
|  | @ -153,7 +154,7 @@ void BackgroundSlicingProcess::process_sla() | |||
|             const std::string export_path = m_sla_print->print_statistics().finalize_output_path(m_export_path); | ||||
| 
 | ||||
|             Zipper zipper(export_path); | ||||
|             m_sla_print->export_raster(zipper); | ||||
|             m_sla_archive.export_print(zipper, *m_sla_print); | ||||
| 
 | ||||
|             if (m_thumbnail_cb != nullptr) | ||||
|             { | ||||
|  | @ -491,9 +492,9 @@ void BackgroundSlicingProcess::prepare_upload() | |||
|         m_upload_job.upload_data.upload_path = m_fff_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string()); | ||||
|     } else { | ||||
|         m_upload_job.upload_data.upload_path = m_sla_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string()); | ||||
| 
 | ||||
|          | ||||
|         Zipper zipper{source_path.string()}; | ||||
|         m_sla_print->export_raster(zipper, m_upload_job.upload_data.upload_path.string()); | ||||
|         m_sla_archive.export_print(zipper, *m_sla_print, m_upload_job.upload_data.upload_path.string()); | ||||
|         if (m_thumbnail_cb != nullptr) | ||||
|         { | ||||
|             ThumbnailsList thumbnails; | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ | |||
| #include <wx/event.h> | ||||
| 
 | ||||
| #include "libslic3r/Print.hpp" | ||||
| #include "libslic3r/Format/SL1.hpp" | ||||
| #include "slic3r/Utils/PrintHost.hpp" | ||||
| 
 | ||||
| 
 | ||||
|  | @ -19,6 +20,7 @@ class DynamicPrintConfig; | |||
| class GCodePreviewData; | ||||
| class Model; | ||||
| class SLAPrint; | ||||
| class SL1Archive; | ||||
| 
 | ||||
| class SlicingStatusEvent : public wxEvent | ||||
| { | ||||
|  | @ -47,7 +49,7 @@ public: | |||
| 	~BackgroundSlicingProcess(); | ||||
| 
 | ||||
| 	void set_fff_print(Print *print) { m_fff_print = print; } | ||||
| 	void set_sla_print(SLAPrint *print) { m_sla_print = print; } | ||||
|     void set_sla_print(SLAPrint *print) { m_sla_print = print; m_sla_print->set_printer(&m_sla_archive); } | ||||
| 	void set_gcode_preview_data(GCodePreviewData *gpd) { m_gcode_preview_data = gpd; } | ||||
|     void set_thumbnail_cb(ThumbnailsGeneratorCallback cb) { m_thumbnail_cb = cb; } | ||||
| #if ENABLE_GCODE_VIEWER | ||||
|  | @ -157,7 +159,8 @@ private: | |||
| 	// Data structure, to which the G-code export writes its annotations.
 | ||||
| 	GCodePreviewData 		   *m_gcode_preview_data = nullptr; | ||||
|     // Callback function, used to write thumbnails into gcode.
 | ||||
|     ThumbnailsGeneratorCallback m_thumbnail_cb = nullptr; | ||||
| 	ThumbnailsGeneratorCallback m_thumbnail_cb = nullptr; | ||||
| 	SL1Archive                  m_sla_archive; | ||||
| #if ENABLE_GCODE_VIEWER | ||||
| 	GCodeProcessor::Result* m_gcode_result = nullptr; | ||||
| #endif // ENABLE_GCODE_VIEWER
 | ||||
|  |  | |||
|  | @ -624,12 +624,6 @@ void GCodeViewer::render_shells() const | |||
| } | ||||
| 
 | ||||
| void GCodeViewer::render_overlay() const | ||||
| { | ||||
|     render_legend(); | ||||
|     render_toolbar(); | ||||
| } | ||||
| 
 | ||||
| void GCodeViewer::render_legend() const | ||||
| { | ||||
|     static const ImVec4 ORANGE(1.0f, 0.49f, 0.22f, 1.0f); | ||||
|     static const float ICON_BORDER_SIZE = 25.0f; | ||||
|  | @ -803,10 +797,6 @@ void GCodeViewer::render_legend() const | |||
|     ImGui::PopStyleVar(); | ||||
| } | ||||
| 
 | ||||
| void GCodeViewer::render_toolbar() const | ||||
| { | ||||
| } | ||||
| 
 | ||||
| } // namespace GUI
 | ||||
| } // namespace Slic3r
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -160,7 +160,7 @@ private: | |||
|     std::vector<unsigned char> m_extruder_ids; | ||||
|     Extrusions m_extrusions; | ||||
|     Shells m_shells; | ||||
|     mutable EViewType m_view_type{ EViewType::FeatureType }; | ||||
|     EViewType m_view_type{ EViewType::FeatureType }; | ||||
|     bool m_legend_enabled{ true }; | ||||
| 
 | ||||
| public: | ||||
|  | @ -208,8 +208,6 @@ private: | |||
|     void render_toolpaths() const; | ||||
|     void render_shells() const; | ||||
|     void render_overlay() const; | ||||
|     void render_legend() const; | ||||
|     void render_toolbar() const; | ||||
| }; | ||||
| 
 | ||||
| } // namespace GUI
 | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ | |||
| #include <wx/filefn.h> | ||||
| #include <wx/sysopt.h> | ||||
| #include <wx/msgdlg.h> | ||||
| #include <wx/richmsgdlg.h> | ||||
| #include <wx/log.h> | ||||
| #include <wx/intl.h> | ||||
| 
 | ||||
|  | @ -321,20 +322,41 @@ bool GUI_App::on_init_inner() | |||
|         set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); | ||||
| 
 | ||||
|     app_config = new AppConfig(); | ||||
|     preset_bundle = new PresetBundle(); | ||||
| 
 | ||||
|     // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory
 | ||||
|     // supplied as argument to --datadir; in that case we should still run the wizard
 | ||||
|     preset_bundle->setup_directories(); | ||||
| 
 | ||||
|     // load settings
 | ||||
|     app_conf_exists = app_config->exists(); | ||||
|     if (app_conf_exists) { | ||||
|         app_config->load(); | ||||
|     } | ||||
|      | ||||
|     std::string msg = Http::tls_global_init(); | ||||
|     wxRichMessageDialog | ||||
|         dlg(nullptr, | ||||
|             wxString::Format(_(L("%s\nDo you want to continue?")), _(msg)), | ||||
|             "PrusaSlicer", wxICON_QUESTION | wxYES_NO); | ||||
|      | ||||
|     bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes"; | ||||
|     std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); | ||||
|     ssl_accept = ssl_accept && ssl_cert_store == Http::tls_system_cert_store(); | ||||
|      | ||||
|     dlg.ShowCheckBox(_(L("Remember my choice"))); | ||||
|     if (!msg.empty() && !ssl_accept) { | ||||
|         if (dlg.ShowModal() != wxID_YES) return false; | ||||
| 
 | ||||
|         app_config->set("tls_cert_store_accepted", | ||||
|                         dlg.IsCheckBoxChecked() ? "yes" : "no"); | ||||
|         app_config->set("tls_accepted_cert_store_location", | ||||
|                         dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); | ||||
|     } | ||||
|      | ||||
|     app_config->set("version", SLIC3R_VERSION); | ||||
|     app_config->save(); | ||||
|      | ||||
|     preset_bundle = new PresetBundle(); | ||||
|      | ||||
|     // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory
 | ||||
|     // supplied as argument to --datadir; in that case we should still run the wizard
 | ||||
|     preset_bundle->setup_directories(); | ||||
| 
 | ||||
| #ifdef __WXMSW__ | ||||
|     associate_3mf_files(); | ||||
|  |  | |||
|  | @ -2071,37 +2071,40 @@ void ObjectList::load_shape_object(const std::string& type_name) | |||
|     // Create mesh
 | ||||
|     BoundingBoxf3 bb; | ||||
|     TriangleMesh mesh = create_mesh(type_name, bb); | ||||
|     load_mesh_object(mesh, _(L("Shape")) + "-" + _(type_name)); | ||||
| } | ||||
| 
 | ||||
| void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name) | ||||
| {    | ||||
|     // Add mesh to model as a new object
 | ||||
|     Model& model = wxGetApp().plater()->model(); | ||||
|     const wxString name = _(L("Shape")) + "-" + _(type_name); | ||||
| 
 | ||||
| #ifdef _DEBUG | ||||
|     check_model_ids_validity(model); | ||||
| #endif /* _DEBUG */ | ||||
| 
 | ||||
|      | ||||
|     std::vector<size_t> object_idxs; | ||||
|     ModelObject* new_object = model.add_object(); | ||||
|     new_object->name = into_u8(name); | ||||
|     new_object->add_instance(); // each object should have at list one instance
 | ||||
| 
 | ||||
|      | ||||
|     ModelVolume* new_volume = new_object->add_volume(mesh); | ||||
|     new_volume->name = into_u8(name); | ||||
|     // set a default extruder value, since user can't add it manually
 | ||||
|     new_volume->config.set_key_value("extruder", new ConfigOptionInt(0)); | ||||
|     new_object->invalidate_bounding_box(); | ||||
| 
 | ||||
|      | ||||
|     new_object->center_around_origin(); | ||||
|     new_object->ensure_on_bed(); | ||||
| 
 | ||||
|      | ||||
|     const BoundingBoxf bed_shape = wxGetApp().plater()->bed_shape_bb(); | ||||
|     new_object->instances[0]->set_offset(Slic3r::to_3d(bed_shape.center().cast<double>(), -new_object->origin_translation(2))); | ||||
| 
 | ||||
|      | ||||
|     object_idxs.push_back(model.objects.size() - 1); | ||||
| #ifdef _DEBUG | ||||
|     check_model_ids_validity(model); | ||||
| #endif /* _DEBUG */ | ||||
| 
 | ||||
|      | ||||
|     paste_objects_into_list(object_idxs); | ||||
| 
 | ||||
| #ifdef _DEBUG | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ class ConfigOptionsGroup; | |||
| class DynamicPrintConfig; | ||||
| class ModelObject; | ||||
| class ModelVolume; | ||||
| class TriangleMesh; | ||||
| enum class ModelVolumeType : int; | ||||
| 
 | ||||
| // FIXME: broken build on mac os because of this is missing:
 | ||||
|  | @ -265,6 +266,7 @@ public: | |||
|     void                load_part(ModelObject* model_object, std::vector<std::pair<wxString, bool>> &volumes_info, ModelVolumeType type); | ||||
| 	void                load_generic_subobject(const std::string& type_name, const ModelVolumeType type); | ||||
|     void                load_shape_object(const std::string &type_name); | ||||
|     void                load_mesh_object(const TriangleMesh &mesh, const wxString &name);   | ||||
|     void                del_object(const int obj_idx); | ||||
|     void                del_subobject_item(wxDataViewItem& item); | ||||
|     void                del_settings_from_config(const wxDataViewItem& parent_item); | ||||
|  |  | |||
|  | @ -1,155 +0,0 @@ | |||
| #ifndef JOB_HPP | ||||
| #define JOB_HPP | ||||
| 
 | ||||
| #include <atomic> | ||||
| 
 | ||||
| #include <slic3r/Utils/Thread.hpp> | ||||
| #include <slic3r/GUI/I18N.hpp> | ||||
| #include <slic3r/GUI/ProgressIndicator.hpp> | ||||
| 
 | ||||
| #include <wx/event.h> | ||||
| 
 | ||||
| #include <boost/thread.hpp> | ||||
| 
 | ||||
| namespace Slic3r { namespace GUI { | ||||
| 
 | ||||
| // A class to handle UI jobs like arranging and optimizing rotation.
 | ||||
| // These are not instant jobs, the user has to be informed about their
 | ||||
| // state in the status progress indicator. On the other hand they are
 | ||||
| // separated from the background slicing process. Ideally, these jobs should
 | ||||
| // run when the background process is not running.
 | ||||
| //
 | ||||
| // TODO: A mechanism would be useful for blocking the plater interactions:
 | ||||
| // objects would be frozen for the user. In case of arrange, an animation
 | ||||
| // could be shown, or with the optimize orientations, partial results
 | ||||
| // could be displayed.
 | ||||
| class Job : public wxEvtHandler | ||||
| { | ||||
|     int               m_range = 100; | ||||
|     boost::thread     m_thread; | ||||
|     std::atomic<bool> m_running{false}, m_canceled{false}; | ||||
|     bool              m_finalized = false; | ||||
|     std::shared_ptr<ProgressIndicator> m_progress; | ||||
|      | ||||
|     void run() | ||||
|     { | ||||
|         m_running.store(true); | ||||
|         process(); | ||||
|         m_running.store(false); | ||||
|          | ||||
|         // ensure to call the last status to finalize the job
 | ||||
|         update_status(status_range(), ""); | ||||
|     } | ||||
|      | ||||
| protected: | ||||
|     // status range for a particular job
 | ||||
|     virtual int status_range() const { return 100; } | ||||
|      | ||||
|     // status update, to be used from the work thread (process() method)
 | ||||
|     void update_status(int st, const wxString &msg = "") | ||||
|     { | ||||
|         auto evt = new wxThreadEvent(); | ||||
|         evt->SetInt(st); | ||||
|         evt->SetString(msg); | ||||
|         wxQueueEvent(this, evt); | ||||
|     } | ||||
|      | ||||
|     bool        was_canceled() const { return m_canceled.load(); } | ||||
|      | ||||
|     // Launched just before start(), a job can use it to prepare internals
 | ||||
|     virtual void prepare() {} | ||||
|      | ||||
|     // Launched when the job is finished. It refreshes the 3Dscene by def.
 | ||||
|     virtual void finalize() { m_finalized = true; } | ||||
|      | ||||
|      | ||||
| public: | ||||
|     Job(std::shared_ptr<ProgressIndicator> pri) : m_progress(pri) | ||||
|     { | ||||
|         Bind(wxEVT_THREAD, [this](const wxThreadEvent &evt) { | ||||
|             auto msg = evt.GetString(); | ||||
|             if (!msg.empty()) | ||||
|                 m_progress->set_status_text(msg.ToUTF8().data()); | ||||
|              | ||||
|             if (m_finalized) return; | ||||
|              | ||||
|             m_progress->set_progress(evt.GetInt()); | ||||
|             if (evt.GetInt() == status_range()) { | ||||
|                 // set back the original range and cancel callback
 | ||||
|                 m_progress->set_range(m_range); | ||||
|                 m_progress->set_cancel_callback(); | ||||
|                 wxEndBusyCursor(); | ||||
|                  | ||||
|                 finalize(); | ||||
|                  | ||||
|                 // dont do finalization again for the same process
 | ||||
|                 m_finalized = true; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     bool is_finalized() const { return m_finalized; } | ||||
|      | ||||
|     Job(const Job &) = delete; | ||||
|     Job(Job &&)      = delete; | ||||
|     Job &operator=(const Job &) = delete; | ||||
|     Job &operator=(Job &&) = delete; | ||||
|      | ||||
|     virtual void process() = 0; | ||||
|      | ||||
|     void start() | ||||
|     { // Start the job. No effect if the job is already running
 | ||||
|         if (!m_running.load()) { | ||||
|             prepare(); | ||||
|              | ||||
|             // Save the current status indicatior range and push the new one
 | ||||
|             m_range = m_progress->get_range(); | ||||
|             m_progress->set_range(status_range()); | ||||
|              | ||||
|             // init cancellation flag and set the cancel callback
 | ||||
|             m_canceled.store(false); | ||||
|             m_progress->set_cancel_callback( | ||||
|                 [this]() { m_canceled.store(true); }); | ||||
|              | ||||
|             m_finalized = false; | ||||
|              | ||||
|             // Changing cursor to busy
 | ||||
|             wxBeginBusyCursor(); | ||||
|              | ||||
|             try { // Execute the job
 | ||||
|                 m_thread = create_thread([this] { this->run(); }); | ||||
|             } catch (std::exception &) { | ||||
|                 update_status(status_range(), | ||||
|                               _(L("ERROR: not enough resources to " | ||||
|                                   "execute a new job."))); | ||||
|             } | ||||
|              | ||||
|             // The state changes will be undone when the process hits the
 | ||||
|             // last status value, in the status update handler (see ctor)
 | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // To wait for the running job and join the threads. False is
 | ||||
|     // returned if the timeout has been reached and the job is still
 | ||||
|     // running. Call cancel() before this fn if you want to explicitly
 | ||||
|     // end the job.
 | ||||
|     bool join(int timeout_ms = 0) | ||||
|     { | ||||
|         if (!m_thread.joinable()) return true; | ||||
|          | ||||
|         if (timeout_ms <= 0) | ||||
|             m_thread.join(); | ||||
|         else if (!m_thread.try_join_for(boost::chrono::milliseconds(timeout_ms))) | ||||
|             return false; | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     bool is_running() const { return m_running.load(); } | ||||
|     void cancel() { m_canceled.store(true); } | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| #endif // JOB_HPP
 | ||||
							
								
								
									
										223
									
								
								src/slic3r/GUI/Jobs/ArrangeJob.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/slic3r/GUI/Jobs/ArrangeJob.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,223 @@ | |||
| #include "ArrangeJob.hpp" | ||||
| 
 | ||||
| #include "libslic3r/MTUtils.hpp" | ||||
| 
 | ||||
| #include "slic3r/GUI/Plater.hpp" | ||||
| #include "slic3r/GUI/GLCanvas3D.hpp" | ||||
| #include "slic3r/GUI/GUI.hpp" | ||||
| 
 | ||||
| namespace Slic3r { namespace GUI { | ||||
| 
 | ||||
| // Cache the wti info
 | ||||
| class WipeTower: public GLCanvas3D::WipeTowerInfo { | ||||
|     using ArrangePolygon = arrangement::ArrangePolygon; | ||||
| public: | ||||
|     explicit WipeTower(const GLCanvas3D::WipeTowerInfo &wti) | ||||
|         : GLCanvas3D::WipeTowerInfo(wti) | ||||
|     {} | ||||
|      | ||||
|     explicit WipeTower(GLCanvas3D::WipeTowerInfo &&wti) | ||||
|         : GLCanvas3D::WipeTowerInfo(std::move(wti)) | ||||
|     {} | ||||
| 
 | ||||
|     void apply_arrange_result(const Vec2d& tr, double rotation) | ||||
|     { | ||||
|         m_pos = unscaled(tr); m_rotation = rotation; | ||||
|         apply_wipe_tower(); | ||||
|     } | ||||
|      | ||||
|     ArrangePolygon get_arrange_polygon() const | ||||
|     { | ||||
|         Polygon ap({ | ||||
|             {coord_t(0), coord_t(0)}, | ||||
|             {scaled(m_bb_size(X)), coord_t(0)}, | ||||
|             {scaled(m_bb_size)}, | ||||
|             {coord_t(0), scaled(m_bb_size(Y))}, | ||||
|             {coord_t(0), coord_t(0)}, | ||||
|             }); | ||||
|          | ||||
|         ArrangePolygon ret; | ||||
|         ret.poly.contour = std::move(ap); | ||||
|         ret.translation  = scaled(m_pos); | ||||
|         ret.rotation     = m_rotation; | ||||
|         ret.priority++; | ||||
|         return ret; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| static WipeTower get_wipe_tower(Plater &plater) | ||||
| { | ||||
|     return WipeTower{plater.canvas3D()->get_wipe_tower_info()}; | ||||
| } | ||||
| 
 | ||||
| void ArrangeJob::clear_input() | ||||
| { | ||||
|     const Model &model = m_plater->model(); | ||||
|      | ||||
|     size_t count = 0, cunprint = 0; // To know how much space to reserve
 | ||||
|     for (auto obj : model.objects) | ||||
|         for (auto mi : obj->instances) | ||||
|             mi->printable ? count++ : cunprint++; | ||||
|      | ||||
|     m_selected.clear(); | ||||
|     m_unselected.clear(); | ||||
|     m_unprintable.clear(); | ||||
|     m_selected.reserve(count + 1 /* for optional wti */); | ||||
|     m_unselected.reserve(count + 1 /* for optional wti */); | ||||
|     m_unprintable.reserve(cunprint /* for optional wti */); | ||||
| } | ||||
| 
 | ||||
| double ArrangeJob::bed_stride() const { | ||||
|     double bedwidth = m_plater->bed_shape_bb().size().x(); | ||||
|     return scaled<double>((1. + LOGICAL_BED_GAP) * bedwidth); | ||||
| } | ||||
| 
 | ||||
| void ArrangeJob::prepare_all() { | ||||
|     clear_input(); | ||||
|      | ||||
|     for (ModelObject *obj: m_plater->model().objects) | ||||
|         for (ModelInstance *mi : obj->instances) { | ||||
|             ArrangePolygons & cont = mi->printable ? m_selected : m_unprintable; | ||||
|             cont.emplace_back(get_arrange_poly(mi)); | ||||
|         } | ||||
| 
 | ||||
|     if (auto wti = get_wipe_tower(*m_plater)) | ||||
|         m_selected.emplace_back(wti.get_arrange_polygon()); | ||||
| } | ||||
| 
 | ||||
| void ArrangeJob::prepare_selected() { | ||||
|     clear_input(); | ||||
|      | ||||
|     Model &model = m_plater->model(); | ||||
|     double stride = bed_stride(); | ||||
|      | ||||
|     std::vector<const Selection::InstanceIdxsList *> | ||||
|             obj_sel(model.objects.size(), nullptr); | ||||
|      | ||||
|     for (auto &s : m_plater->get_selection().get_content()) | ||||
|         if (s.first < int(obj_sel.size())) | ||||
|             obj_sel[size_t(s.first)] = &s.second; | ||||
|      | ||||
|     // Go through the objects and check if inside the selection
 | ||||
|     for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { | ||||
|         const Selection::InstanceIdxsList * instlist = obj_sel[oidx]; | ||||
|         ModelObject *mo = model.objects[oidx]; | ||||
|          | ||||
|         std::vector<bool> inst_sel(mo->instances.size(), false); | ||||
|          | ||||
|         if (instlist) | ||||
|             for (auto inst_id : *instlist) | ||||
|                 inst_sel[size_t(inst_id)] = true; | ||||
|          | ||||
|         for (size_t i = 0; i < inst_sel.size(); ++i) { | ||||
|             ArrangePolygon &&ap = get_arrange_poly(mo->instances[i]); | ||||
|              | ||||
|             ArrangePolygons &cont = mo->instances[i]->printable ? | ||||
|                         (inst_sel[i] ? m_selected : | ||||
|                                        m_unselected) : | ||||
|                         m_unprintable; | ||||
|              | ||||
|             cont.emplace_back(std::move(ap)); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     if (auto wti = get_wipe_tower(*m_plater)) { | ||||
|         ArrangePolygon &&ap = get_arrange_poly(&wti); | ||||
|          | ||||
|         m_plater->get_selection().is_wipe_tower() ? | ||||
|                     m_selected.emplace_back(std::move(ap)) : | ||||
|                     m_unselected.emplace_back(std::move(ap)); | ||||
|     } | ||||
|      | ||||
|     // If the selection was empty arrange everything
 | ||||
|     if (m_selected.empty()) m_selected.swap(m_unselected); | ||||
|      | ||||
|     // The strides have to be removed from the fixed items. For the
 | ||||
|     // arrangeable (selected) items bed_idx is ignored and the
 | ||||
|     // translation is irrelevant.
 | ||||
|     for (auto &p : m_unselected) p.translation(X) -= p.bed_idx * stride; | ||||
| } | ||||
| 
 | ||||
| void ArrangeJob::prepare() | ||||
| { | ||||
|     wxGetKeyState(WXK_SHIFT) ? prepare_selected() : prepare_all(); | ||||
| } | ||||
| 
 | ||||
| void ArrangeJob::process() | ||||
| { | ||||
|     static const auto arrangestr = _(L("Arranging")); | ||||
|      | ||||
|     double dist = min_object_distance(*m_plater->config()); | ||||
|      | ||||
|     arrangement::ArrangeParams params; | ||||
|     params.min_obj_distance = scaled(dist); | ||||
|      | ||||
|     auto count = unsigned(m_selected.size() + m_unprintable.size()); | ||||
|     Points bedpts = get_bed_shape(*m_plater->config()); | ||||
|      | ||||
|     params.stopcondition = [this]() { return was_canceled(); }; | ||||
|      | ||||
|     try { | ||||
|         params.progressind = [this, count](unsigned st) { | ||||
|             st += m_unprintable.size(); | ||||
|             if (st > 0) update_status(int(count - st), arrangestr); | ||||
|         }; | ||||
|          | ||||
|         arrangement::arrange(m_selected, m_unselected, bedpts, params); | ||||
|          | ||||
|         params.progressind = [this, count](unsigned st) { | ||||
|             if (st > 0) update_status(int(count - st), arrangestr); | ||||
|         }; | ||||
|          | ||||
|         arrangement::arrange(m_unprintable, {}, bedpts, params); | ||||
|     } catch (std::exception & /*e*/) { | ||||
|         GUI::show_error(m_plater, | ||||
|                         _(L("Could not arrange model objects! " | ||||
|                             "Some geometries may be invalid."))); | ||||
|     } | ||||
| 
 | ||||
|     // finalize just here.
 | ||||
|     update_status(int(count), | ||||
|                   was_canceled() ? _(L("Arranging canceled.")) | ||||
|                                    : _(L("Arranging done."))); | ||||
| } | ||||
| 
 | ||||
| void ArrangeJob::finalize() { | ||||
|     // Ignore the arrange result if aborted.
 | ||||
|     if (was_canceled()) return; | ||||
|      | ||||
|     // Unprintable items go to the last virtual bed
 | ||||
|     int beds = 0; | ||||
|      | ||||
|     // Apply the arrange result to all selected objects
 | ||||
|     for (ArrangePolygon &ap : m_selected) { | ||||
|         beds = std::max(ap.bed_idx, beds); | ||||
|         ap.apply(); | ||||
|     } | ||||
|      | ||||
|     // Get the virtual beds from the unselected items
 | ||||
|     for (ArrangePolygon &ap : m_unselected) | ||||
|         beds = std::max(ap.bed_idx, beds); | ||||
|      | ||||
|     // Move the unprintable items to the last virtual bed.
 | ||||
|     for (ArrangePolygon &ap : m_unprintable) { | ||||
|         ap.bed_idx += beds + 1; | ||||
|         ap.apply(); | ||||
|     } | ||||
|      | ||||
|     m_plater->update(); | ||||
|      | ||||
|     Job::finalize(); | ||||
| } | ||||
| 
 | ||||
| arrangement::ArrangePolygon get_wipe_tower_arrangepoly(Plater &plater) | ||||
| { | ||||
|     return WipeTower{plater.canvas3D()->get_wipe_tower_info()}.get_arrange_polygon(); | ||||
| } | ||||
| 
 | ||||
| void apply_wipe_tower_arrangepoly(Plater &plater, const arrangement::ArrangePolygon &ap) | ||||
| { | ||||
|     WipeTower{plater.canvas3D()->get_wipe_tower_info()}.apply_arrange_result(ap.translation.cast<double>(), ap.rotation); | ||||
| } | ||||
| 
 | ||||
| }} // namespace Slic3r::GUI
 | ||||
							
								
								
									
										77
									
								
								src/slic3r/GUI/Jobs/ArrangeJob.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/slic3r/GUI/Jobs/ArrangeJob.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,77 @@ | |||
| #ifndef ARRANGEJOB_HPP | ||||
| #define ARRANGEJOB_HPP | ||||
| 
 | ||||
| #include "Job.hpp" | ||||
| #include "libslic3r/Arrange.hpp" | ||||
| 
 | ||||
| namespace Slic3r { namespace GUI { | ||||
| 
 | ||||
| class Plater; | ||||
| 
 | ||||
| class ArrangeJob : public Job | ||||
| { | ||||
|     Plater *m_plater; | ||||
|      | ||||
|     using ArrangePolygon = arrangement::ArrangePolygon; | ||||
|     using ArrangePolygons = arrangement::ArrangePolygons; | ||||
|      | ||||
|     // The gap between logical beds in the x axis expressed in ratio of
 | ||||
|     // the current bed width.
 | ||||
|     static const constexpr double LOGICAL_BED_GAP = 1. / 5.; | ||||
|      | ||||
|     ArrangePolygons m_selected, m_unselected, m_unprintable; | ||||
|      | ||||
|     // clear m_selected and m_unselected, reserve space for next usage
 | ||||
|     void clear_input(); | ||||
|      | ||||
|     // Stride between logical beds
 | ||||
|     double bed_stride() const; | ||||
|      | ||||
|     // Set up arrange polygon for a ModelInstance and Wipe tower
 | ||||
|     template<class T> ArrangePolygon get_arrange_poly(T *obj) const | ||||
|     { | ||||
|         ArrangePolygon ap = obj->get_arrange_polygon(); | ||||
|         ap.priority       = 0; | ||||
|         ap.bed_idx        = ap.translation.x() / bed_stride(); | ||||
|         ap.setter         = [obj, this](const ArrangePolygon &p) { | ||||
|             if (p.is_arranged()) { | ||||
|                 Vec2d t = p.translation.cast<double>(); | ||||
|                 t.x() += p.bed_idx * bed_stride(); | ||||
|                 obj->apply_arrange_result(t, p.rotation); | ||||
|             } | ||||
|         }; | ||||
|         return ap; | ||||
|     } | ||||
|      | ||||
|     // Prepare all objects on the bed regardless of the selection
 | ||||
|     void prepare_all(); | ||||
|      | ||||
|     // Prepare the selected and unselected items separately. If nothing is
 | ||||
|     // selected, behaves as if everything would be selected.
 | ||||
|     void prepare_selected(); | ||||
|      | ||||
| protected: | ||||
|      | ||||
|     void prepare() override; | ||||
|      | ||||
| public: | ||||
|     ArrangeJob(std::shared_ptr<ProgressIndicator> pri, Plater *plater) | ||||
|         : Job{std::move(pri)}, m_plater{plater} | ||||
|     {} | ||||
|      | ||||
|     int status_range() const override | ||||
|     { | ||||
|         return int(m_selected.size() + m_unprintable.size()); | ||||
|     } | ||||
|      | ||||
|     void process() override; | ||||
|      | ||||
|     void finalize() override; | ||||
| }; | ||||
| 
 | ||||
| arrangement::ArrangePolygon get_wipe_tower_arrangepoly(Plater &); | ||||
| void apply_wipe_tower_arrangepoly(Plater &plater, const arrangement::ArrangePolygon &ap); | ||||
| 
 | ||||
| }} // namespace Slic3r::GUI
 | ||||
| 
 | ||||
| #endif // ARRANGEJOB_HPP
 | ||||
							
								
								
									
										121
									
								
								src/slic3r/GUI/Jobs/Job.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/slic3r/GUI/Jobs/Job.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,121 @@ | |||
| #include <algorithm> | ||||
| 
 | ||||
| #include "Job.hpp" | ||||
| #include <boost/log/trivial.hpp> | ||||
| 
 | ||||
| namespace Slic3r { | ||||
| 
 | ||||
| void GUI::Job::run() | ||||
| { | ||||
|     m_running.store(true); | ||||
|     process(); | ||||
|     m_running.store(false); | ||||
|      | ||||
|     // ensure to call the last status to finalize the job
 | ||||
|     update_status(status_range(), ""); | ||||
| } | ||||
| 
 | ||||
| void GUI::Job::update_status(int st, const wxString &msg) | ||||
| { | ||||
|     auto evt = new wxThreadEvent(); | ||||
|     evt->SetInt(st); | ||||
|     evt->SetString(msg); | ||||
|     wxQueueEvent(this, evt); | ||||
| } | ||||
| 
 | ||||
| GUI::Job::Job(std::shared_ptr<ProgressIndicator> pri) | ||||
|     : m_progress(std::move(pri)) | ||||
| { | ||||
|     Bind(wxEVT_THREAD, [this](const wxThreadEvent &evt) { | ||||
|         auto msg = evt.GetString(); | ||||
|         if (!msg.empty()) | ||||
|             m_progress->set_status_text(msg.ToUTF8().data()); | ||||
|          | ||||
|         if (m_finalized) return; | ||||
|          | ||||
|         m_progress->set_progress(evt.GetInt()); | ||||
|         if (evt.GetInt() == status_range()) { | ||||
|             // set back the original range and cancel callback
 | ||||
|             m_progress->set_range(m_range); | ||||
|             m_progress->set_cancel_callback(); | ||||
|             wxEndBusyCursor(); | ||||
|              | ||||
|             finalize(); | ||||
|              | ||||
|             // dont do finalization again for the same process
 | ||||
|             m_finalized = true; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| void GUI::Job::start() | ||||
| { // Start the job. No effect if the job is already running
 | ||||
|     if (!m_running.load()) { | ||||
|         prepare(); | ||||
|          | ||||
|         // Save the current status indicatior range and push the new one
 | ||||
|         m_range = m_progress->get_range(); | ||||
|         m_progress->set_range(status_range()); | ||||
|          | ||||
|         // init cancellation flag and set the cancel callback
 | ||||
|         m_canceled.store(false); | ||||
|         m_progress->set_cancel_callback( | ||||
|                     [this]() { m_canceled.store(true); }); | ||||
|          | ||||
|         m_finalized = false; | ||||
|          | ||||
|         // Changing cursor to busy
 | ||||
|         wxBeginBusyCursor(); | ||||
|          | ||||
|         try { // Execute the job
 | ||||
|             m_thread = create_thread([this] { this->run(); }); | ||||
|         } catch (std::exception &) { | ||||
|             update_status(status_range(), | ||||
|                           _(L("ERROR: not enough resources to " | ||||
|                               "execute a new job."))); | ||||
|         } | ||||
|          | ||||
|         // The state changes will be undone when the process hits the
 | ||||
|         // last status value, in the status update handler (see ctor)
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| bool GUI::Job::join(int timeout_ms) | ||||
| { | ||||
|     if (!m_thread.joinable()) return true; | ||||
|      | ||||
|     if (timeout_ms <= 0) | ||||
|         m_thread.join(); | ||||
|     else if (!m_thread.try_join_for(boost::chrono::milliseconds(timeout_ms))) | ||||
|         return false; | ||||
|      | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| void GUI::ExclusiveJobGroup::start(size_t jid) { | ||||
|     assert(jid < m_jobs.size()); | ||||
|     stop_all(); | ||||
|     m_jobs[jid]->start(); | ||||
| } | ||||
| 
 | ||||
| void GUI::ExclusiveJobGroup::join_all(int wait_ms) | ||||
| { | ||||
|     std::vector<bool> aborted(m_jobs.size(), false); | ||||
|      | ||||
|     for (size_t jid = 0; jid < m_jobs.size(); ++jid) | ||||
|         aborted[jid] = m_jobs[jid]->join(wait_ms); | ||||
|      | ||||
|     if (!std::all_of(aborted.begin(), aborted.end(), [](bool t) { return t; })) | ||||
|         BOOST_LOG_TRIVIAL(error) << "Could not abort a job!"; | ||||
| } | ||||
| 
 | ||||
| bool GUI::ExclusiveJobGroup::is_any_running() const | ||||
| { | ||||
|     return std::any_of(m_jobs.begin(), m_jobs.end(), | ||||
|                        [](const std::unique_ptr<GUI::Job> &j) { | ||||
|         return j->is_running(); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										110
									
								
								src/slic3r/GUI/Jobs/Job.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/slic3r/GUI/Jobs/Job.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,110 @@ | |||
| #ifndef JOB_HPP | ||||
| #define JOB_HPP | ||||
| 
 | ||||
| #include <atomic> | ||||
| 
 | ||||
| #include <slic3r/Utils/Thread.hpp> | ||||
| #include <slic3r/GUI/I18N.hpp> | ||||
| 
 | ||||
| #include "ProgressIndicator.hpp" | ||||
| 
 | ||||
| #include <wx/event.h> | ||||
| 
 | ||||
| #include <boost/thread.hpp> | ||||
| 
 | ||||
| namespace Slic3r { namespace GUI { | ||||
| 
 | ||||
| // A class to handle UI jobs like arranging and optimizing rotation.
 | ||||
| // These are not instant jobs, the user has to be informed about their
 | ||||
| // state in the status progress indicator. On the other hand they are
 | ||||
| // separated from the background slicing process. Ideally, these jobs should
 | ||||
| // run when the background process is not running.
 | ||||
| //
 | ||||
| // TODO: A mechanism would be useful for blocking the plater interactions:
 | ||||
| // objects would be frozen for the user. In case of arrange, an animation
 | ||||
| // could be shown, or with the optimize orientations, partial results
 | ||||
| // could be displayed.
 | ||||
| class Job : public wxEvtHandler | ||||
| { | ||||
|     int               m_range = 100; | ||||
|     boost::thread     m_thread; | ||||
|     std::atomic<bool> m_running{false}, m_canceled{false}; | ||||
|     bool              m_finalized = false; | ||||
|     std::shared_ptr<ProgressIndicator> m_progress; | ||||
|      | ||||
|     void run(); | ||||
|      | ||||
| protected: | ||||
|     // status range for a particular job
 | ||||
|     virtual int status_range() const { return 100; } | ||||
|      | ||||
|     // status update, to be used from the work thread (process() method)
 | ||||
|     void update_status(int st, const wxString &msg = ""); | ||||
| 
 | ||||
|     bool was_canceled() const { return m_canceled.load(); } | ||||
| 
 | ||||
|     // Launched just before start(), a job can use it to prepare internals
 | ||||
|     virtual void prepare() {} | ||||
|      | ||||
|     // Launched when the job is finished. It refreshes the 3Dscene by def.
 | ||||
|     virtual void finalize() { m_finalized = true; } | ||||
|     | ||||
| public: | ||||
|     Job(std::shared_ptr<ProgressIndicator> pri); | ||||
|      | ||||
|     bool is_finalized() const { return m_finalized; } | ||||
|      | ||||
|     Job(const Job &) = delete; | ||||
|     Job(Job &&)      = delete; | ||||
|     Job &operator=(const Job &) = delete; | ||||
|     Job &operator=(Job &&) = delete; | ||||
|      | ||||
|     virtual void process() = 0; | ||||
|      | ||||
|     void start(); | ||||
|      | ||||
|     // To wait for the running job and join the threads. False is
 | ||||
|     // returned if the timeout has been reached and the job is still
 | ||||
|     // running. Call cancel() before this fn if you want to explicitly
 | ||||
|     // end the job.
 | ||||
|     bool join(int timeout_ms = 0); | ||||
|      | ||||
|     bool is_running() const { return m_running.load(); } | ||||
|     void cancel() { m_canceled.store(true); } | ||||
| }; | ||||
| 
 | ||||
| // Jobs defined inside the group class will be managed so that only one can
 | ||||
| // run at a time. Also, the background process will be stopped if a job is
 | ||||
| // started.
 | ||||
| class ExclusiveJobGroup | ||||
| { | ||||
|     static const int ABORT_WAIT_MAX_MS = 10000; | ||||
|      | ||||
|     std::vector<std::unique_ptr<GUI::Job>> m_jobs; | ||||
|      | ||||
| protected: | ||||
|     virtual void before_start() {} | ||||
|      | ||||
| public: | ||||
|     virtual ~ExclusiveJobGroup() = default; | ||||
|      | ||||
|     size_t add_job(std::unique_ptr<GUI::Job> &&job) | ||||
|     { | ||||
|         m_jobs.emplace_back(std::move(job)); | ||||
|         return m_jobs.size() - 1; | ||||
|     } | ||||
|      | ||||
|     void start(size_t jid); | ||||
|      | ||||
|     void cancel_all() { for (auto& j : m_jobs) j->cancel(); } | ||||
|      | ||||
|     void join_all(int wait_ms = 0); | ||||
|      | ||||
|     void stop_all() { cancel_all(); join_all(ABORT_WAIT_MAX_MS); } | ||||
|      | ||||
|     bool is_any_running() const; | ||||
| }; | ||||
| 
 | ||||
| }} // namespace Slic3r::GUI
 | ||||
| 
 | ||||
| #endif // JOB_HPP
 | ||||
							
								
								
									
										68
									
								
								src/slic3r/GUI/Jobs/RotoptimizeJob.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/slic3r/GUI/Jobs/RotoptimizeJob.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,68 @@ | |||
| #include "RotoptimizeJob.hpp" | ||||
| 
 | ||||
| #include "libslic3r/MTUtils.hpp" | ||||
| #include "libslic3r/SLA/Rotfinder.hpp" | ||||
| #include "libslic3r/MinAreaBoundingBox.hpp" | ||||
| 
 | ||||
| #include "slic3r/GUI/Plater.hpp" | ||||
| 
 | ||||
| namespace Slic3r { namespace GUI { | ||||
| 
 | ||||
| void RotoptimizeJob::process() | ||||
| { | ||||
|     int obj_idx = m_plater->get_selected_object_idx(); | ||||
|     if (obj_idx < 0) { return; } | ||||
|      | ||||
|     ModelObject *o = m_plater->model().objects[size_t(obj_idx)]; | ||||
| 
 | ||||
|     auto r = sla::find_best_rotation( | ||||
|         *o, | ||||
|         .005f, | ||||
|         [this](unsigned s) { | ||||
|             if (s < 100) | ||||
|                 update_status(int(s), | ||||
|                               _(L("Searching for optimal orientation"))); | ||||
|         }, | ||||
|         [this]() { return was_canceled(); }); | ||||
| 
 | ||||
| 
 | ||||
|     double mindist = 6.0; // FIXME
 | ||||
| 
 | ||||
|     if (!was_canceled()) { | ||||
|         for(ModelInstance * oi : o->instances) { | ||||
|             oi->set_rotation({r[X], r[Y], r[Z]}); | ||||
| 
 | ||||
|             auto    trmatrix = oi->get_transformation().get_matrix(); | ||||
|             Polygon trchull  = o->convex_hull_2d(trmatrix); | ||||
| 
 | ||||
|             MinAreaBoundigBox rotbb(trchull, MinAreaBoundigBox::pcConvex); | ||||
|             double            phi = rotbb.angle_to_X(); | ||||
| 
 | ||||
|             // The box should be landscape
 | ||||
|             if(rotbb.width() < rotbb.height()) phi += PI / 2; | ||||
| 
 | ||||
|             Vec3d rt = oi->get_rotation(); rt(Z) += phi; | ||||
| 
 | ||||
|             oi->set_rotation(rt); | ||||
|         } | ||||
| 
 | ||||
|         m_plater->find_new_position(o->instances, scaled(mindist)); | ||||
| 
 | ||||
|         // Correct the z offset of the object which was corrupted be
 | ||||
|         // the rotation
 | ||||
|         o->ensure_on_bed(); | ||||
|     } | ||||
| 
 | ||||
|     update_status(100, was_canceled() ? _(L("Orientation search canceled.")) : | ||||
|                                         _(L("Orientation found."))); | ||||
| } | ||||
| 
 | ||||
| void RotoptimizeJob::finalize() | ||||
| { | ||||
|     if (!was_canceled()) | ||||
|         m_plater->update(); | ||||
|      | ||||
|     Job::finalize(); | ||||
| } | ||||
| 
 | ||||
| }} | ||||
							
								
								
									
										24
									
								
								src/slic3r/GUI/Jobs/RotoptimizeJob.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/slic3r/GUI/Jobs/RotoptimizeJob.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| #ifndef ROTOPTIMIZEJOB_HPP | ||||
| #define ROTOPTIMIZEJOB_HPP | ||||
| 
 | ||||
| #include "Job.hpp" | ||||
| 
 | ||||
| namespace Slic3r { namespace GUI { | ||||
| 
 | ||||
| class Plater; | ||||
| 
 | ||||
| class RotoptimizeJob : public Job | ||||
| { | ||||
|     Plater *m_plater; | ||||
| public: | ||||
|     RotoptimizeJob(std::shared_ptr<ProgressIndicator> pri, Plater *plater) | ||||
|         : Job{std::move(pri)}, m_plater{plater} | ||||
|     {} | ||||
|      | ||||
|     void process() override; | ||||
|     void finalize() override; | ||||
| }; | ||||
| 
 | ||||
| }} // namespace Slic3r::GUI
 | ||||
| 
 | ||||
| #endif // ROTOPTIMIZEJOB_HPP
 | ||||
							
								
								
									
										226
									
								
								src/slic3r/GUI/Jobs/SLAImportJob.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								src/slic3r/GUI/Jobs/SLAImportJob.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,226 @@ | |||
| #include "SLAImportJob.hpp" | ||||
| 
 | ||||
| #include "slic3r/GUI/GUI.hpp" | ||||
| #include "slic3r/GUI/GUI_App.hpp" | ||||
| #include "slic3r/GUI/AppConfig.hpp" | ||||
| #include "slic3r/GUI/Plater.hpp" | ||||
| #include "slic3r/GUI/PresetBundle.hpp" | ||||
| #include "slic3r/GUI/GUI_ObjectList.hpp" | ||||
| #include "slic3r/Utils/SLAImport.hpp" | ||||
| 
 | ||||
| #include <wx/dialog.h> | ||||
| #include <wx/stattext.h> | ||||
| #include <wx/combobox.h> | ||||
| #include <wx/filename.h> | ||||
| #include <wx/filepicker.h> | ||||
| 
 | ||||
| namespace Slic3r { namespace GUI { | ||||
| 
 | ||||
| enum class Sel { modelAndProfile, profileOnly, modelOnly}; | ||||
|      | ||||
| class ImportDlg: public wxDialog { | ||||
|     wxFilePickerCtrl *m_filepicker; | ||||
|     wxComboBox *m_import_dropdown, *m_quality_dropdown; | ||||
|      | ||||
| public: | ||||
|     ImportDlg(Plater *plater) | ||||
|         : wxDialog{plater, wxID_ANY, "Import SLA archive"} | ||||
|     { | ||||
|         auto szvert = new wxBoxSizer{wxVERTICAL}; | ||||
|         auto szfilepck = new wxBoxSizer{wxHORIZONTAL}; | ||||
|          | ||||
|         m_filepicker = new wxFilePickerCtrl(this, wxID_ANY, | ||||
|                                             from_u8(wxGetApp().app_config->get_last_dir()), _(L("Choose SLA archive:")), | ||||
|                                             "SL1 archive files (*.sl1, *.zip)|*.sl1;*.SL1;*.zip;*.ZIP", | ||||
|                                             wxDefaultPosition, wxDefaultSize, wxFLP_DEFAULT_STYLE | wxFD_OPEN | wxFD_FILE_MUST_EXIST); | ||||
|          | ||||
|         szfilepck->Add(new wxStaticText(this, wxID_ANY, _(L("Import file: "))), 0, wxALIGN_CENTER); | ||||
|         szfilepck->Add(m_filepicker, 1); | ||||
|         szvert->Add(szfilepck, 0, wxALL | wxEXPAND, 5); | ||||
|          | ||||
|         auto szchoices = new wxBoxSizer{wxHORIZONTAL}; | ||||
|          | ||||
|         static const std::vector<wxString> inp_choices = { | ||||
|             _(L("Import model and profile")), | ||||
|             _(L("Import profile only")), | ||||
|             _(L("Import model only")) | ||||
|         }; | ||||
|          | ||||
|         m_import_dropdown = new wxComboBox( | ||||
|             this, wxID_ANY, inp_choices[0], wxDefaultPosition, wxDefaultSize, | ||||
|             inp_choices.size(), inp_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); | ||||
|          | ||||
|         szchoices->Add(m_import_dropdown); | ||||
|         szchoices->Add(new wxStaticText(this, wxID_ANY, _(L("Quality: "))), 0, wxALIGN_CENTER | wxALL, 5); | ||||
|          | ||||
|         static const std::vector<wxString> qual_choices = { | ||||
|             _(L("Accurate")), | ||||
|             _(L("Balanced")), | ||||
|             _(L("Quick")) | ||||
|         }; | ||||
|          | ||||
|         m_quality_dropdown = new wxComboBox( | ||||
|             this, wxID_ANY, qual_choices[0], wxDefaultPosition, wxDefaultSize, | ||||
|             qual_choices.size(), qual_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); | ||||
|         szchoices->Add(m_quality_dropdown); | ||||
|          | ||||
|         m_import_dropdown->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) { | ||||
|             if (get_selection() == Sel::profileOnly) | ||||
|                 m_quality_dropdown->Disable(); | ||||
|             else m_quality_dropdown->Enable(); | ||||
|         }); | ||||
|          | ||||
|         szvert->Add(szchoices, 0, wxALL, 5); | ||||
|         szvert->AddStretchSpacer(1); | ||||
|         auto szbtn = new wxBoxSizer(wxHORIZONTAL); | ||||
|         szbtn->Add(new wxButton{this, wxID_CANCEL}); | ||||
|         szbtn->Add(new wxButton{this, wxID_OK}); | ||||
|         szvert->Add(szbtn, 0, wxALIGN_RIGHT | wxALL, 5); | ||||
|          | ||||
|         SetSizerAndFit(szvert); | ||||
|     } | ||||
|      | ||||
|     Sel get_selection() const | ||||
|     { | ||||
|         int sel = m_import_dropdown->GetSelection(); | ||||
|         return Sel(std::min(int(Sel::modelOnly), std::max(0, sel))); | ||||
|     } | ||||
|      | ||||
|     Vec2i get_marchsq_windowsize() const | ||||
|     { | ||||
|         enum { Accurate, Balanced, Fast}; | ||||
|          | ||||
|         switch(m_quality_dropdown->GetSelection()) | ||||
|         { | ||||
|         case Fast: return {8, 8}; | ||||
|         case Balanced: return {4, 4}; | ||||
|         default: | ||||
|         case Accurate: | ||||
|             return {2, 2}; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     wxString get_path() const | ||||
|     { | ||||
|         return m_filepicker->GetPath(); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| class SLAImportJob::priv { | ||||
| public: | ||||
|     Plater *plater; | ||||
|      | ||||
|     Sel sel = Sel::modelAndProfile; | ||||
|      | ||||
|     TriangleMesh       mesh; | ||||
|     DynamicPrintConfig profile; | ||||
|     wxString           path; | ||||
|     Vec2i              win = {2, 2}; | ||||
|     std::string        err; | ||||
|      | ||||
|     priv(Plater *plt): plater{plt} {} | ||||
| }; | ||||
| 
 | ||||
| SLAImportJob::SLAImportJob(std::shared_ptr<ProgressIndicator> pri, Plater *plater) | ||||
|     : Job{std::move(pri)}, p{std::make_unique<priv>(plater)} | ||||
| {} | ||||
| 
 | ||||
| SLAImportJob::~SLAImportJob() = default; | ||||
| 
 | ||||
| void SLAImportJob::process() | ||||
| { | ||||
|     auto progr = [this](int s) { | ||||
|         if (s < 100) update_status(int(s), _(L("Importing SLA archive"))); | ||||
|         return !was_canceled(); | ||||
|     }; | ||||
|      | ||||
|     if (p->path.empty()) return; | ||||
|      | ||||
|     std::string path = p->path.ToUTF8().data(); | ||||
|     try { | ||||
|         switch (p->sel) { | ||||
|         case Sel::modelAndProfile: | ||||
|             import_sla_archive(path, p->win, p->mesh, p->profile, progr); | ||||
|             break; | ||||
|         case Sel::modelOnly: | ||||
|             import_sla_archive(path, p->win, p->mesh, progr); | ||||
|             break; | ||||
|         case Sel::profileOnly: | ||||
|             import_sla_archive(path, p->profile); | ||||
|             break; | ||||
|         } | ||||
|          | ||||
|     } catch (std::exception &ex) { | ||||
|         p->err = ex.what(); | ||||
|     } | ||||
|      | ||||
|     update_status(100, was_canceled() ? _(L("Importing canceled.")) : | ||||
|                                         _(L("Importing done."))); | ||||
| } | ||||
| 
 | ||||
| void SLAImportJob::reset() | ||||
| { | ||||
|     p->sel     = Sel::modelAndProfile; | ||||
|     p->mesh    = {}; | ||||
|     p->profile = {}; | ||||
|     p->win     = {2, 2}; | ||||
|     p->path.Clear(); | ||||
| } | ||||
| 
 | ||||
| void SLAImportJob::prepare() | ||||
| { | ||||
|     reset(); | ||||
|      | ||||
|     ImportDlg dlg{p->plater}; | ||||
|      | ||||
|     if (dlg.ShowModal() == wxID_OK) { | ||||
|         auto path = dlg.get_path(); | ||||
|         auto nm = wxFileName(path); | ||||
|         p->path = !nm.Exists(wxFILE_EXISTS_REGULAR) ? "" : path.ToUTF8(); | ||||
|         p->sel  = dlg.get_selection(); | ||||
|         p->win  = dlg.get_marchsq_windowsize(); | ||||
|     } else { | ||||
|         p->path = ""; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void SLAImportJob::finalize() | ||||
| { | ||||
|     // Ignore the arrange result if aborted.
 | ||||
|     if (was_canceled()) return; | ||||
|      | ||||
|     if (!p->err.empty()) { | ||||
|         show_error(p->plater, p->err); | ||||
|         p->err = ""; | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     std::string name = wxFileName(p->path).GetName().ToUTF8().data(); | ||||
|      | ||||
|     if (!p->profile.empty()) { | ||||
|         const ModelObjectPtrs& objects = p->plater->model().objects; | ||||
|         for (auto object : objects) | ||||
|             if (object->volumes.size() > 1) | ||||
|             { | ||||
|                 Slic3r::GUI::show_info(nullptr, | ||||
|                                        _(L("You cannot load SLA project with a multi-part object on the bed")) + "\n\n" + | ||||
|                                        _(L("Please check your object list before preset changing.")), | ||||
|                                        _(L("Attention!")) ); | ||||
|                 return; | ||||
|             } | ||||
|          | ||||
|         DynamicPrintConfig config = {}; | ||||
|         config.apply(SLAFullPrintConfig::defaults()); | ||||
|         config += std::move(p->profile); | ||||
|          | ||||
|         wxGetApp().preset_bundle->load_config_model(name, std::move(config)); | ||||
|         wxGetApp().load_current_presets(); | ||||
|     } | ||||
|      | ||||
|     if (!p->mesh.empty()) | ||||
|         p->plater->sidebar().obj_list()->load_mesh_object(p->mesh, name); | ||||
|      | ||||
|     reset(); | ||||
| } | ||||
| 
 | ||||
| }} | ||||
							
								
								
									
										31
									
								
								src/slic3r/GUI/Jobs/SLAImportJob.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/slic3r/GUI/Jobs/SLAImportJob.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| #ifndef SLAIMPORTJOB_HPP | ||||
| #define SLAIMPORTJOB_HPP | ||||
| 
 | ||||
| #include "Job.hpp" | ||||
| 
 | ||||
| namespace Slic3r { namespace GUI { | ||||
| 
 | ||||
| class Plater; | ||||
| 
 | ||||
| class SLAImportJob : public Job {     | ||||
|     class priv; | ||||
|      | ||||
|     std::unique_ptr<priv> p; | ||||
|      | ||||
| public: | ||||
|     SLAImportJob(std::shared_ptr<ProgressIndicator> pri, Plater *plater); | ||||
|     ~SLAImportJob(); | ||||
| 
 | ||||
|     void process() override; | ||||
|      | ||||
|     void reset(); | ||||
|      | ||||
| protected: | ||||
|     void prepare() override; | ||||
|      | ||||
|     void finalize() override; | ||||
| }; | ||||
| 
 | ||||
| }}     // namespace Slic3r::GUI
 | ||||
| 
 | ||||
| #endif // SLAIMPORTJOB_HPP
 | ||||
|  | @ -589,6 +589,11 @@ void MainFrame::init_menubar() | |||
|         append_menu_item(import_menu, wxID_ANY, _(L("Import STL/OBJ/AM&F/3MF")) + dots + "\tCtrl+I", _(L("Load a model")), | ||||
|             [this](wxCommandEvent&) { if (m_plater) m_plater->add_model(); }, "import_plater", nullptr, | ||||
|             [this](){return m_plater != nullptr; }, this); | ||||
|          | ||||
|         append_menu_item(import_menu, wxID_ANY, _(L("Import SL1 archive")) + dots, _(L("Load an SL1 output archive")), | ||||
|             [this](wxCommandEvent&) { if (m_plater) m_plater->import_sl1_archive(); }, "import_plater", nullptr, | ||||
|             [this](){return m_plater != nullptr; }, this);     | ||||
|      | ||||
|         import_menu->AppendSeparator(); | ||||
|         append_menu_item(import_menu, wxID_ANY, _(L("Import &Config")) + dots + "\tCtrl+L", _(L("Load exported configuration file")), | ||||
|             [this](wxCommandEvent&) { load_config_file(); }, "import_config", nullptr, | ||||
|  |  | |||
|  | @ -36,7 +36,6 @@ | |||
| #include "libslic3r/GCode/ThumbnailData.hpp" | ||||
| #include "libslic3r/Model.hpp" | ||||
| #include "libslic3r/SLA/Hollowing.hpp" | ||||
| #include "libslic3r/SLA/Rotfinder.hpp" | ||||
| #include "libslic3r/SLA/SupportPoint.hpp" | ||||
| #include "libslic3r/Polygon.hpp" | ||||
| #include "libslic3r/Print.hpp" | ||||
|  | @ -44,13 +43,6 @@ | |||
| #include "libslic3r/SLAPrint.hpp" | ||||
| #include "libslic3r/Utils.hpp" | ||||
| 
 | ||||
| //#include "libslic3r/ClipperUtils.hpp"
 | ||||
| 
 | ||||
| // #include "libnest2d/optimizers/nlopt/genetic.hpp"
 | ||||
| // #include "libnest2d/backends/clipper/geometries.hpp"
 | ||||
| // #include "libnest2d/utils/rotcalipers.hpp"
 | ||||
| #include "libslic3r/MinAreaBoundingBox.hpp" | ||||
| 
 | ||||
| #include "GUI.hpp" | ||||
| #include "GUI_App.hpp" | ||||
| #include "GUI_ObjectList.hpp" | ||||
|  | @ -69,7 +61,9 @@ | |||
| #include "Camera.hpp" | ||||
| #include "Mouse3DController.hpp" | ||||
| #include "Tab.hpp" | ||||
| #include "Job.hpp" | ||||
| #include "Jobs/ArrangeJob.hpp" | ||||
| #include "Jobs/RotoptimizeJob.hpp" | ||||
| #include "Jobs/SLAImportJob.hpp" | ||||
| #include "PresetBundle.hpp" | ||||
| #include "BackgroundSlicingProcess.hpp" | ||||
| #include "ProgressStatusBar.hpp" | ||||
|  | @ -1488,311 +1482,44 @@ struct Plater::priv | |||
|     BackgroundSlicingProcess    background_process; | ||||
|     bool suppressed_backround_processing_update { false }; | ||||
| 
 | ||||
|     // Cache the wti info
 | ||||
|     class WipeTower: public GLCanvas3D::WipeTowerInfo { | ||||
|         using ArrangePolygon = arrangement::ArrangePolygon; | ||||
|         friend priv; | ||||
|     public: | ||||
| 
 | ||||
|         void apply_arrange_result(const Vec2d& tr, double rotation) | ||||
|         { | ||||
|             m_pos = unscaled(tr); m_rotation = rotation; | ||||
|             apply_wipe_tower(); | ||||
|         } | ||||
| 
 | ||||
|         ArrangePolygon get_arrange_polygon() const | ||||
|         { | ||||
|             Polygon p({ | ||||
|                 {coord_t(0), coord_t(0)}, | ||||
|                 {scaled(m_bb_size(X)), coord_t(0)}, | ||||
|                 {scaled(m_bb_size)}, | ||||
|                 {coord_t(0), scaled(m_bb_size(Y))}, | ||||
|                 {coord_t(0), coord_t(0)}, | ||||
|                 }); | ||||
| 
 | ||||
|             ArrangePolygon ret; | ||||
|             ret.poly.contour = std::move(p); | ||||
|             ret.translation  = scaled(m_pos); | ||||
|             ret.rotation     = m_rotation; | ||||
|             ret.priority++; | ||||
|             return ret; | ||||
|         } | ||||
|     } wipetower; | ||||
| 
 | ||||
|     WipeTower& updated_wipe_tower() { | ||||
|         auto wti = view3D->get_canvas3d()->get_wipe_tower_info(); | ||||
|         wipetower.m_pos = wti.pos(); | ||||
|         wipetower.m_rotation = wti.rotation(); | ||||
|         wipetower.m_bb_size  = wti.bb_size(); | ||||
|         return wipetower; | ||||
|     } | ||||
| 
 | ||||
|     // A class to handle UI jobs like arranging and optimizing rotation.
 | ||||
|     // These are not instant jobs, the user has to be informed about their
 | ||||
|     // state in the status progress indicator. On the other hand they are
 | ||||
|     // separated from the background slicing process. Ideally, these jobs should
 | ||||
|     // run when the background process is not running.
 | ||||
|     //
 | ||||
|     // TODO: A mechanism would be useful for blocking the plater interactions:
 | ||||
|     // objects would be frozen for the user. In case of arrange, an animation
 | ||||
|     // could be shown, or with the optimize orientations, partial results
 | ||||
|     // could be displayed.
 | ||||
|     class PlaterJob: public Job | ||||
|     { | ||||
|         priv *m_plater; | ||||
|     protected: | ||||
| 
 | ||||
|         priv &      plater() { return *m_plater; } | ||||
|         const priv &plater() const { return *m_plater; } | ||||
| 
 | ||||
|         // Launched when the job is finished. It refreshes the 3Dscene by def.
 | ||||
|         void finalize() override | ||||
|         { | ||||
|             // Do a full refresh of scene tree, including regenerating
 | ||||
|             // all the GLVolumes. FIXME The update function shall just
 | ||||
|             // reload the modified matrices.
 | ||||
|             if (!Job::was_canceled()) | ||||
|                 plater().update(unsigned(UpdateParams::FORCE_FULL_SCREEN_REFRESH)); | ||||
|              | ||||
|             Job::finalize(); | ||||
|         } | ||||
| 
 | ||||
|     public: | ||||
|         PlaterJob(priv *_plater) | ||||
|             : Job(_plater->statusbar()), m_plater(_plater) | ||||
|         {} | ||||
|     }; | ||||
| 
 | ||||
|     enum class Jobs : size_t { | ||||
|         Arrange, | ||||
|         Rotoptimize | ||||
|     }; | ||||
| 
 | ||||
|     class ArrangeJob : public PlaterJob | ||||
|     { | ||||
|         using ArrangePolygon = arrangement::ArrangePolygon; | ||||
|         using ArrangePolygons = arrangement::ArrangePolygons; | ||||
| 
 | ||||
|         // The gap between logical beds in the x axis expressed in ratio of
 | ||||
|         // the current bed width.
 | ||||
|         static const constexpr double LOGICAL_BED_GAP = 1. / 5.; | ||||
| 
 | ||||
|         ArrangePolygons m_selected, m_unselected, m_unprintable; | ||||
| 
 | ||||
|         // clear m_selected and m_unselected, reserve space for next usage
 | ||||
|         void clear_input() { | ||||
|             const Model &model = plater().model; | ||||
| 
 | ||||
|             size_t count = 0, cunprint = 0; // To know how much space to reserve
 | ||||
|             for (auto obj : model.objects) | ||||
|                 for (auto mi : obj->instances) | ||||
|                     mi->printable ? count++ : cunprint++; | ||||
|              | ||||
|             m_selected.clear(); | ||||
|             m_unselected.clear(); | ||||
|             m_unprintable.clear(); | ||||
|             m_selected.reserve(count + 1 /* for optional wti */); | ||||
|             m_unselected.reserve(count + 1 /* for optional wti */); | ||||
|             m_unprintable.reserve(cunprint /* for optional wti */); | ||||
|         } | ||||
| 
 | ||||
|         // Stride between logical beds
 | ||||
|         double bed_stride() const { | ||||
|             double bedwidth = plater().bed_shape_bb().size().x(); | ||||
|             return scaled<double>((1. + LOGICAL_BED_GAP) * bedwidth); | ||||
|         } | ||||
| 
 | ||||
|         // Set up arrange polygon for a ModelInstance and Wipe tower
 | ||||
|         template<class T> ArrangePolygon get_arrange_poly(T *obj) const { | ||||
|             ArrangePolygon ap = obj->get_arrange_polygon(); | ||||
|             ap.priority       = 0; | ||||
|             ap.bed_idx        = ap.translation.x() / bed_stride(); | ||||
|             ap.setter         = [obj, this](const ArrangePolygon &p) { | ||||
|                 if (p.is_arranged()) { | ||||
|                     Vec2d t = p.translation.cast<double>(); | ||||
|                     t.x() += p.bed_idx * bed_stride(); | ||||
|                     obj->apply_arrange_result(t, p.rotation); | ||||
|                 } | ||||
|             }; | ||||
|             return ap; | ||||
|         } | ||||
| 
 | ||||
|         // Prepare all objects on the bed regardless of the selection
 | ||||
|         void prepare_all() { | ||||
|             clear_input(); | ||||
| 
 | ||||
|             for (ModelObject *obj: plater().model.objects) | ||||
|                 for (ModelInstance *mi : obj->instances) { | ||||
|                     ArrangePolygons & cont = mi->printable ? m_selected : m_unprintable; | ||||
|                     cont.emplace_back(get_arrange_poly(mi)); | ||||
|                 } | ||||
| 
 | ||||
|             auto& wti = plater().updated_wipe_tower(); | ||||
|             if (wti) m_selected.emplace_back(get_arrange_poly(&wti)); | ||||
|         } | ||||
| 
 | ||||
|         // Prepare the selected and unselected items separately. If nothing is
 | ||||
|         // selected, behaves as if everything would be selected.
 | ||||
|         void prepare_selected() { | ||||
|             clear_input(); | ||||
| 
 | ||||
|             Model &model = plater().model; | ||||
|             coord_t stride = bed_stride(); | ||||
| 
 | ||||
|             std::vector<const Selection::InstanceIdxsList *> | ||||
|                 obj_sel(model.objects.size(), nullptr); | ||||
| 
 | ||||
|             for (auto &s : plater().get_selection().get_content()) | ||||
|                 if (s.first < int(obj_sel.size())) | ||||
|                     obj_sel[size_t(s.first)] = &s.second; | ||||
| 
 | ||||
|             // Go through the objects and check if inside the selection
 | ||||
|             for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { | ||||
|                 const Selection::InstanceIdxsList * instlist = obj_sel[oidx]; | ||||
|                 ModelObject *mo = model.objects[oidx]; | ||||
| 
 | ||||
|                 std::vector<bool> inst_sel(mo->instances.size(), false); | ||||
| 
 | ||||
|                 if (instlist) | ||||
|                     for (auto inst_id : *instlist) | ||||
|                         inst_sel[size_t(inst_id)] = true; | ||||
| 
 | ||||
|                 for (size_t i = 0; i < inst_sel.size(); ++i) { | ||||
|                     ArrangePolygon &&ap = get_arrange_poly(mo->instances[i]); | ||||
| 
 | ||||
|                     ArrangePolygons &cont = mo->instances[i]->printable ? | ||||
|                                                 (inst_sel[i] ? m_selected : | ||||
|                                                                m_unselected) : | ||||
|                                                 m_unprintable; | ||||
|                      | ||||
|                     cont.emplace_back(std::move(ap)); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             auto& wti = plater().updated_wipe_tower(); | ||||
|             if (wti) { | ||||
|                 ArrangePolygon &&ap = get_arrange_poly(&wti); | ||||
| 
 | ||||
|                 plater().get_selection().is_wipe_tower() ? | ||||
|                     m_selected.emplace_back(std::move(ap)) : | ||||
|                     m_unselected.emplace_back(std::move(ap)); | ||||
|             } | ||||
| 
 | ||||
|             // If the selection was empty arrange everything
 | ||||
|             if (m_selected.empty()) m_selected.swap(m_unselected); | ||||
| 
 | ||||
|             // The strides have to be removed from the fixed items. For the
 | ||||
|             // arrangeable (selected) items bed_idx is ignored and the
 | ||||
|             // translation is irrelevant.
 | ||||
|             for (auto &p : m_unselected) p.translation(X) -= p.bed_idx * stride; | ||||
|         } | ||||
| 
 | ||||
|     protected: | ||||
| 
 | ||||
|         void prepare() override | ||||
|         { | ||||
|             wxGetKeyState(WXK_SHIFT) ? prepare_selected() : prepare_all(); | ||||
|         } | ||||
| 
 | ||||
|     public: | ||||
|         using PlaterJob::PlaterJob; | ||||
| 
 | ||||
|         int status_range() const override | ||||
|         { | ||||
|             return int(m_selected.size() + m_unprintable.size()); | ||||
|         } | ||||
| 
 | ||||
|         void process() override; | ||||
| 
 | ||||
|         void finalize() override { | ||||
|             // Ignore the arrange result if aborted.
 | ||||
|             if (was_canceled()) return; | ||||
|              | ||||
|             // Unprintable items go to the last virtual bed
 | ||||
|             int beds = 0; | ||||
|              | ||||
|             // Apply the arrange result to all selected objects
 | ||||
|             for (ArrangePolygon &ap : m_selected) { | ||||
|                 beds = std::max(ap.bed_idx, beds); | ||||
|                 ap.apply(); | ||||
|             } | ||||
|              | ||||
|             // Get the virtual beds from the unselected items
 | ||||
|             for (ArrangePolygon &ap : m_unselected) | ||||
|                 beds = std::max(ap.bed_idx, beds); | ||||
|              | ||||
|             // Move the unprintable items to the last virtual bed.
 | ||||
|             for (ArrangePolygon &ap : m_unprintable) { | ||||
|                 ap.bed_idx += beds + 1; | ||||
|                 ap.apply(); | ||||
|             } | ||||
| 
 | ||||
|             plater().update(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     class RotoptimizeJob : public PlaterJob | ||||
|     { | ||||
|     public: | ||||
|         using PlaterJob::PlaterJob; | ||||
|         void process() override; | ||||
|     }; | ||||
| 
 | ||||
| 
 | ||||
|     // Jobs defined inside the group class will be managed so that only one can
 | ||||
|     // run at a time. Also, the background process will be stopped if a job is
 | ||||
|     // started.
 | ||||
|     class ExclusiveJobGroup { | ||||
| 
 | ||||
|         static const int ABORT_WAIT_MAX_MS = 10000; | ||||
| 
 | ||||
|         priv * m_plater; | ||||
| 
 | ||||
|         ArrangeJob arrange_job{m_plater}; | ||||
|         RotoptimizeJob rotoptimize_job{m_plater}; | ||||
| 
 | ||||
|         // To create a new job, just define a new subclass of Job, implement
 | ||||
|         // the process and the optional prepare() and finalize() methods
 | ||||
|         // Register the instance of the class in the m_jobs container
 | ||||
|         // if it cannot run concurrently with other jobs in this group
 | ||||
| 
 | ||||
|         std::vector<std::reference_wrapper<Job>> m_jobs{arrange_job, | ||||
|                                                         rotoptimize_job}; | ||||
| 
 | ||||
|     // started. It is up the the plater to ensure that the background slicing
 | ||||
|     // can't be restarted while a ui job is still running.
 | ||||
|     class Jobs: public ExclusiveJobGroup | ||||
|     { | ||||
|         priv *m; | ||||
|         size_t m_arrange_id, m_rotoptimize_id, m_sla_import_id; | ||||
|          | ||||
|         void before_start() override { m->background_process.stop(); } | ||||
|          | ||||
|     public: | ||||
|         ExclusiveJobGroup(priv *_plater) : m_plater(_plater) {} | ||||
| 
 | ||||
|         void start(Jobs jid) { | ||||
|             m_plater->background_process.stop(); | ||||
|             stop_all(); | ||||
|             m_jobs[size_t(jid)].get().start(); | ||||
|         } | ||||
| 
 | ||||
|         void cancel_all() { for (Job& j : m_jobs) j.cancel(); } | ||||
| 
 | ||||
|         void join_all(int wait_ms = 0) | ||||
|         Jobs(priv *_m) : m(_m) | ||||
|         { | ||||
|             std::vector<bool> aborted(m_jobs.size(), false); | ||||
| 
 | ||||
|             for (size_t jid = 0; jid < m_jobs.size(); ++jid) | ||||
|                 aborted[jid] = m_jobs[jid].get().join(wait_ms); | ||||
| 
 | ||||
|             if (!all_of(aborted)) | ||||
|                 BOOST_LOG_TRIVIAL(error) << "Could not abort a job!"; | ||||
|             m_arrange_id = add_job(std::make_unique<ArrangeJob>(m->statusbar(), m->q)); | ||||
|             m_rotoptimize_id = add_job(std::make_unique<RotoptimizeJob>(m->statusbar(), m->q)); | ||||
|             m_sla_import_id = add_job(std::make_unique<SLAImportJob>(m->statusbar(), m->q)); | ||||
|         } | ||||
| 
 | ||||
|         void stop_all() { cancel_all(); join_all(ABORT_WAIT_MAX_MS); } | ||||
| 
 | ||||
|         const Job& get(Jobs jobid) const { return m_jobs[size_t(jobid)]; } | ||||
| 
 | ||||
|         bool is_any_running() const | ||||
|          | ||||
|         void arrange() | ||||
|         { | ||||
|             return std::any_of(m_jobs.begin(), | ||||
|                                m_jobs.end(), | ||||
|                                [](const Job &j) { return j.is_running(); }); | ||||
|             m->take_snapshot(_(L("Arrange"))); | ||||
|             start(m_arrange_id); | ||||
|         } | ||||
| 
 | ||||
|     } m_ui_jobs{this}; | ||||
|          | ||||
|         void optimize_rotation() | ||||
|         { | ||||
|             m->take_snapshot(_(L("Optimize Rotation"))); | ||||
|             start(m_rotoptimize_id); | ||||
|         } | ||||
|          | ||||
|         void import_sla_arch() | ||||
|         { | ||||
|             m->take_snapshot(_(L("Import SLA archive"))); | ||||
|             start(m_sla_import_id); | ||||
|         } | ||||
|          | ||||
|     } m_ui_jobs; | ||||
| 
 | ||||
|     bool                        delayed_scene_refresh; | ||||
|     std::string                 delayed_error_message; | ||||
|  | @ -1811,10 +1538,10 @@ struct Plater::priv | |||
|     priv(Plater *q, MainFrame *main_frame); | ||||
|     ~priv(); | ||||
| 
 | ||||
| 	enum class UpdateParams { | ||||
|     	FORCE_FULL_SCREEN_REFRESH 			= 1, | ||||
|     	FORCE_BACKGROUND_PROCESSING_UPDATE 	= 2, | ||||
|     	POSTPONE_VALIDATION_ERROR_MESSAGE	= 4, | ||||
|     enum class UpdateParams { | ||||
|         FORCE_FULL_SCREEN_REFRESH          = 1, | ||||
|         FORCE_BACKGROUND_PROCESSING_UPDATE = 2, | ||||
|         POSTPONE_VALIDATION_ERROR_MESSAGE  = 4, | ||||
|     }; | ||||
|     void update(unsigned int flags = 0); | ||||
|     void select_view(const std::string& direction); | ||||
|  | @ -1850,9 +1577,7 @@ struct Plater::priv | |||
|     std::string get_config(const std::string &key) const; | ||||
|     BoundingBoxf bed_shape_bb() const; | ||||
|     BoundingBox scaled_bed_shape_bb() const; | ||||
|     arrangement::BedShapeHint get_bed_shape_hint() const; | ||||
| 
 | ||||
|     void find_new_position(const ModelInstancePtrs  &instances, coord_t min_d); | ||||
|     std::vector<size_t> load_files(const std::vector<fs::path>& input_files, bool load_model, bool load_config); | ||||
|     std::vector<size_t> load_model_objects(const ModelObjectPtrs &model_objects); | ||||
|     wxString get_export_file(GUI::FileType file_type); | ||||
|  | @ -1870,8 +1595,6 @@ struct Plater::priv | |||
|     void delete_object_from_model(size_t obj_idx); | ||||
|     void reset(); | ||||
|     void mirror(Axis axis); | ||||
|     void arrange(); | ||||
|     void sla_optimize_rotation(); | ||||
|     void split_object(); | ||||
|     void split_volume(); | ||||
|     void scale_selection_to_fit_print_volume(); | ||||
|  | @ -2038,6 +1761,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) | |||
|         "support_material", "support_material_extruder", "support_material_interface_extruder", "support_material_contact_distance", "raft_layers" | ||||
|         })) | ||||
|     , sidebar(new Sidebar(q)) | ||||
|     , m_ui_jobs(this) | ||||
|     , delayed_scene_refresh(false) | ||||
|     , view_toolbar(GLToolbar::Radio, "View") | ||||
|     , m_project_filename(wxEmptyString) | ||||
|  | @ -2124,14 +1848,15 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) | |||
|     sidebar->Bind(EVT_SCHEDULE_BACKGROUND_PROCESS, [this](SimpleEvent&) { this->schedule_background_process(); }); | ||||
| 
 | ||||
|     wxGLCanvas* view3D_canvas = view3D->get_wxglcanvas(); | ||||
|      | ||||
|     // 3DScene events:
 | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, [this](SimpleEvent&) { this->schedule_background_process(); }); | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_OBJECT_SELECT, &priv::on_object_select, this); | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_RIGHT_CLICK, &priv::on_right_click, this); | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_REMOVE_OBJECT, [q](SimpleEvent&) { q->remove_selected(); }); | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent&) { arrange(); }); | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent&) { this->q->arrange(); }); | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); }); | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [this](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); }); | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); }); | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int> &evt) | ||||
|         { if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); }); | ||||
|     view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); }); | ||||
|  | @ -2156,7 +1881,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) | |||
|     view3D_canvas->Bind(EVT_GLTOOLBAR_ADD, &priv::on_action_add, this); | ||||
|     view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE, [q](SimpleEvent&) { q->remove_selected(); }); | ||||
|     view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [q](SimpleEvent&) { q->reset_with_confirm(); }); | ||||
|     view3D_canvas->Bind(EVT_GLTOOLBAR_ARRANGE, [this](SimpleEvent&) { arrange(); }); | ||||
|     view3D_canvas->Bind(EVT_GLTOOLBAR_ARRANGE, [this](SimpleEvent&) { this->q->arrange(); }); | ||||
|     view3D_canvas->Bind(EVT_GLTOOLBAR_COPY, [q](SimpleEvent&) { q->copy_selection_to_clipboard(); }); | ||||
|     view3D_canvas->Bind(EVT_GLTOOLBAR_PASTE, [q](SimpleEvent&) { q->paste_from_clipboard(); }); | ||||
|     view3D_canvas->Bind(EVT_GLTOOLBAR_MORE, [q](SimpleEvent&) { q->increase_instances(); }); | ||||
|  | @ -2824,40 +2549,12 @@ void Plater::priv::mirror(Axis axis) | |||
|     view3D->mirror_selection(axis); | ||||
| } | ||||
| 
 | ||||
| void Plater::priv::arrange() | ||||
| { | ||||
|     this->take_snapshot(_L("Arrange")); | ||||
|     m_ui_jobs.start(Jobs::Arrange); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| // This method will find an optimal orientation for the currently selected item
 | ||||
| // Very similar in nature to the arrange method above...
 | ||||
| void Plater::priv::sla_optimize_rotation() { | ||||
|     this->take_snapshot(_L("Optimize Rotation")); | ||||
|     m_ui_jobs.start(Jobs::Rotoptimize); | ||||
| } | ||||
| 
 | ||||
| arrangement::BedShapeHint Plater::priv::get_bed_shape_hint() const { | ||||
| 
 | ||||
|     const auto *bed_shape_opt = config->opt<ConfigOptionPoints>("bed_shape"); | ||||
|     assert(bed_shape_opt); | ||||
| 
 | ||||
|     if (!bed_shape_opt) return {}; | ||||
| 
 | ||||
|     auto &bedpoints = bed_shape_opt->values; | ||||
|     Polyline bedpoly; bedpoly.points.reserve(bedpoints.size()); | ||||
|     for (auto &v : bedpoints) bedpoly.append(scaled(v)); | ||||
| 
 | ||||
|     return arrangement::BedShapeHint(bedpoly); | ||||
| } | ||||
| 
 | ||||
| void Plater::priv::find_new_position(const ModelInstancePtrs &instances, | ||||
| void Plater::find_new_position(const ModelInstancePtrs &instances, | ||||
|                                      coord_t min_d) | ||||
| { | ||||
|     arrangement::ArrangePolygons movable, fixed; | ||||
| 
 | ||||
|     for (const ModelObject *mo : model.objects) | ||||
|      | ||||
|     for (const ModelObject *mo : p->model.objects) | ||||
|         for (const ModelInstance *inst : mo->instances) { | ||||
|             auto it = std::find(instances.begin(), instances.end(), inst); | ||||
|             auto arrpoly = inst->get_arrange_polygon(); | ||||
|  | @ -2867,11 +2564,12 @@ void Plater::priv::find_new_position(const ModelInstancePtrs &instances, | |||
|             else | ||||
|                 movable.emplace_back(std::move(arrpoly)); | ||||
|         } | ||||
| 
 | ||||
|     if (updated_wipe_tower()) | ||||
|         fixed.emplace_back(wipetower.get_arrange_polygon()); | ||||
| 
 | ||||
|     arrangement::arrange(movable, fixed, min_d, get_bed_shape_hint()); | ||||
|      | ||||
|     if (p->view3D->get_canvas3d()->get_wipe_tower_info()) | ||||
|         fixed.emplace_back(get_wipe_tower_arrangepoly(*this)); | ||||
|      | ||||
|     arrangement::arrange(movable, fixed, get_bed_shape(*config()), | ||||
|                          arrangement::ArrangeParams{min_d}); | ||||
| 
 | ||||
|     for (size_t i = 0; i < instances.size(); ++i) | ||||
|         if (movable[i].bed_idx == 0) | ||||
|  | @ -2879,95 +2577,6 @@ void Plater::priv::find_new_position(const ModelInstancePtrs &instances, | |||
|                                                movable[i].rotation); | ||||
| } | ||||
| 
 | ||||
| void Plater::priv::ArrangeJob::process() { | ||||
|     static const auto arrangestr = _L("Arranging"); | ||||
| 
 | ||||
|     // FIXME: I don't know how to obtain the minimum distance, it depends
 | ||||
|     // on printer technology. I guess the following should work but it crashes.
 | ||||
|     double dist = 6; // PrintConfig::min_object_distance(config);
 | ||||
|     if (plater().printer_technology == ptFFF) { | ||||
|         dist = PrintConfig::min_object_distance(plater().config); | ||||
|     } | ||||
| 
 | ||||
|     coord_t min_d = scaled(dist); | ||||
|     auto count = unsigned(m_selected.size() + m_unprintable.size()); | ||||
|     arrangement::BedShapeHint bedshape = plater().get_bed_shape_hint(); | ||||
|      | ||||
|     auto stopfn = [this]() { return was_canceled(); }; | ||||
|      | ||||
|     try { | ||||
|         arrangement::arrange(m_selected, m_unselected, min_d, bedshape, | ||||
|             [this, count](unsigned st) { | ||||
|                 st += m_unprintable.size(); | ||||
|                 if (st > 0) update_status(int(count - st), arrangestr); | ||||
|             }, stopfn); | ||||
|         arrangement::arrange(m_unprintable, {}, min_d, bedshape, | ||||
|             [this, count](unsigned st) { | ||||
|                 if (st > 0) update_status(int(count - st), arrangestr); | ||||
|             }, stopfn); | ||||
|     } catch (std::exception & /*e*/) { | ||||
|         GUI::show_error(plater().q, | ||||
|                         _L("Could not arrange model objects! " | ||||
|                            "Some geometries may be invalid.")); | ||||
|     } | ||||
| 
 | ||||
|     // finalize just here.
 | ||||
|     update_status(int(count), | ||||
|                   was_canceled() ? _L("Arranging canceled.") | ||||
|                                  : _L("Arranging done.")); | ||||
| } | ||||
| 
 | ||||
| void Plater::priv::RotoptimizeJob::process() | ||||
| { | ||||
|     int obj_idx = plater().get_selected_object_idx(); | ||||
|     if (obj_idx < 0) { return; } | ||||
| 
 | ||||
|     ModelObject *o = plater().model.objects[size_t(obj_idx)]; | ||||
| 
 | ||||
|     auto r = sla::find_best_rotation( | ||||
|         *o, | ||||
|         .005f, | ||||
|         [this](unsigned s) { | ||||
|             if (s < 100) | ||||
|                 update_status(int(s), | ||||
|                               _L("Searching for optimal orientation")); | ||||
|         }, | ||||
|         [this]() { return was_canceled(); }); | ||||
| 
 | ||||
| 
 | ||||
|     double mindist = 6.0; // FIXME
 | ||||
| 
 | ||||
|     if (!was_canceled()) { | ||||
|         for(ModelInstance * oi : o->instances) { | ||||
|             oi->set_rotation({r[X], r[Y], r[Z]}); | ||||
| 
 | ||||
|             auto    trmatrix = oi->get_transformation().get_matrix(); | ||||
|             Polygon trchull  = o->convex_hull_2d(trmatrix); | ||||
| 
 | ||||
|             MinAreaBoundigBox rotbb(trchull, MinAreaBoundigBox::pcConvex); | ||||
|             double            r = rotbb.angle_to_X(); | ||||
| 
 | ||||
|             // The box should be landscape
 | ||||
|             if(rotbb.width() < rotbb.height()) r += PI / 2; | ||||
| 
 | ||||
|             Vec3d rt = oi->get_rotation(); rt(Z) += r; | ||||
| 
 | ||||
|             oi->set_rotation(rt); | ||||
|         } | ||||
| 
 | ||||
|         plater().find_new_position(o->instances, scaled(mindist)); | ||||
| 
 | ||||
|         // Correct the z offset of the object which was corrupted be
 | ||||
|         // the rotation
 | ||||
|         o->ensure_on_bed(); | ||||
|     } | ||||
| 
 | ||||
|     update_status(100, | ||||
|                   was_canceled() ? _L("Orientation search canceled.") | ||||
|                                  : _L("Orientation found.")); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| void Plater::priv::split_object() | ||||
| { | ||||
|     int obj_idx = get_selected_object_idx(); | ||||
|  | @ -3608,7 +3217,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) | |||
|     } | ||||
| 
 | ||||
|     // update plater with new config
 | ||||
|     wxGetApp().plater()->on_config_change(wxGetApp().preset_bundle->full_config()); | ||||
|     q->on_config_change(wxGetApp().preset_bundle->full_config()); | ||||
|     /* Settings list can be changed after printer preset changing, so
 | ||||
|      * update all settings items for all item had it. | ||||
|      * Furthermore, Layers editing is implemented only for FFF printers | ||||
|  | @ -4055,8 +3664,12 @@ bool Plater::priv::complit_init_sla_object_menu() | |||
|     sla_object_menu.AppendSeparator(); | ||||
| 
 | ||||
|     // Add the automatic rotation sub-menu
 | ||||
|     append_menu_item(&sla_object_menu, wxID_ANY, _L("Optimize orientation"), _L("Optimize the rotation of the object for better print results."), | ||||
|         [this](wxCommandEvent&) { sla_optimize_rotation(); }); | ||||
|     append_menu_item( | ||||
|         &sla_object_menu, wxID_ANY, _(L("Optimize orientation")), | ||||
|         _(L("Optimize the rotation of the object for better print results.")), | ||||
|         [this](wxCommandEvent &) { | ||||
|             m_ui_jobs.optimize_rotation(); | ||||
|         }); | ||||
| 
 | ||||
|     return true; | ||||
| } | ||||
|  | @ -4660,6 +4273,11 @@ void Plater::add_model() | |||
|     load_files(paths, true, false); | ||||
| } | ||||
| 
 | ||||
| void Plater::import_sl1_archive() | ||||
| { | ||||
|     p->m_ui_jobs.import_sla_arch(); | ||||
| } | ||||
| 
 | ||||
| void Plater::extract_config_from_project() | ||||
| { | ||||
|     wxString input_file; | ||||
|  | @ -4752,7 +4370,7 @@ void Plater::increase_instances(size_t num) | |||
|     sidebar().obj_list()->increase_object_instances(obj_idx, was_one_instance ? num + 1 : num); | ||||
| 
 | ||||
|     if (p->get_config("autocenter") == "1") | ||||
|         p->arrange(); | ||||
|         arrange(); | ||||
| 
 | ||||
|     p->update(); | ||||
| 
 | ||||
|  | @ -5486,6 +5104,11 @@ bool Plater::is_export_gcode_scheduled() const | |||
|     return p->background_process.is_export_scheduled(); | ||||
| } | ||||
| 
 | ||||
| const Selection &Plater::get_selection() const | ||||
| { | ||||
|     return p->get_selection(); | ||||
| } | ||||
| 
 | ||||
| int Plater::get_selected_object_idx() | ||||
| { | ||||
|     return p->get_selected_object_idx(); | ||||
|  | @ -5511,6 +5134,11 @@ BoundingBoxf Plater::bed_shape_bb() const | |||
|     return p->bed_shape_bb(); | ||||
| } | ||||
| 
 | ||||
| void Plater::arrange() | ||||
| { | ||||
|     p->m_ui_jobs.arrange(); | ||||
| } | ||||
| 
 | ||||
| void Plater::set_current_canvas_as_dirty() | ||||
| { | ||||
|     p->set_current_canvas_as_dirty(); | ||||
|  | @ -5533,6 +5161,8 @@ PrinterTechnology Plater::printer_technology() const | |||
|     return p->printer_technology; | ||||
| } | ||||
| 
 | ||||
| const DynamicPrintConfig * Plater::config() const { return p->config; } | ||||
| 
 | ||||
| void Plater::set_printer_technology(PrinterTechnology printer_technology) | ||||
| { | ||||
|     p->printer_technology = printer_technology; | ||||
|  |  | |||
|  | @ -9,8 +9,10 @@ | |||
| #include <wx/bmpcbox.h> | ||||
| 
 | ||||
| #include "Preset.hpp" | ||||
| #include "Selection.hpp" | ||||
| 
 | ||||
| #include "libslic3r/BoundingBox.hpp" | ||||
| #include "Jobs/Job.hpp" | ||||
| #include "wxExtensions.hpp" | ||||
| 
 | ||||
| class wxButton; | ||||
|  | @ -157,6 +159,7 @@ public: | |||
|     void load_project(); | ||||
|     void load_project(const wxString& filename); | ||||
|     void add_model(); | ||||
|     void import_sl1_archive(); | ||||
|     void extract_config_from_project(); | ||||
| 
 | ||||
|     std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, bool load_model = true, bool load_config = true); | ||||
|  | @ -252,12 +255,16 @@ public: | |||
|     void set_project_filename(const wxString& filename); | ||||
| 
 | ||||
|     bool is_export_gcode_scheduled() const; | ||||
| 
 | ||||
|      | ||||
|     const Selection& get_selection() const; | ||||
|     int get_selected_object_idx(); | ||||
|     bool is_single_full_object_selection() const; | ||||
|     GLCanvas3D* canvas3D(); | ||||
|     GLCanvas3D* get_current_canvas3D(); | ||||
|     BoundingBoxf bed_shape_bb() const; | ||||
|      | ||||
|     void arrange(); | ||||
|     void find_new_position(const ModelInstancePtrs  &instances, coord_t min_d); | ||||
| 
 | ||||
|     void set_current_canvas_as_dirty(); | ||||
| #if ENABLE_NON_STATIC_CANVAS_MANAGER | ||||
|  | @ -266,6 +273,7 @@ public: | |||
| #endif // ENABLE_NON_STATIC_CANVAS_MANAGER
 | ||||
| 
 | ||||
|     PrinterTechnology   printer_technology() const; | ||||
|     const DynamicPrintConfig * config() const; | ||||
|     void                set_printer_technology(PrinterTechnology printer_technology); | ||||
| 
 | ||||
|     void copy_selection_to_clipboard(); | ||||
|  | @ -371,6 +379,7 @@ private: | |||
|     bool m_was_scheduled; | ||||
| }; | ||||
| 
 | ||||
| }} | ||||
| } // namespace GUI
 | ||||
| } // namespace Slic3r
 | ||||
| 
 | ||||
| #endif | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
| #include <functional> | ||||
| #include <string> | ||||
| 
 | ||||
| #include "ProgressIndicator.hpp" | ||||
| #include "Jobs/ProgressIndicator.hpp" | ||||
| 
 | ||||
| class wxTimer; | ||||
| class wxGauge; | ||||
|  |  | |||
|  | @ -18,6 +18,8 @@ | |||
| #include <openssl/x509.h> | ||||
| #endif | ||||
| 
 | ||||
| #define L(s) s | ||||
| 
 | ||||
| #include "libslic3r/libslic3r.h" | ||||
| #include "libslic3r/Utils.hpp" | ||||
| 
 | ||||
|  | @ -32,7 +34,8 @@ namespace Slic3r { | |||
| struct CurlGlobalInit | ||||
| { | ||||
|     static std::unique_ptr<CurlGlobalInit> instance; | ||||
| 
 | ||||
|     std::string message; | ||||
|      | ||||
| 	CurlGlobalInit() | ||||
|     { | ||||
| #ifdef OPENSSL_CERT_OVERRIDE // defined if SLIC3R_STATIC=ON
 | ||||
|  | @ -57,21 +60,39 @@ struct CurlGlobalInit | |||
|             ssl_cafile = X509_get_default_cert_file(); | ||||
|          | ||||
|         int replace = true; | ||||
|          | ||||
|         if (!ssl_cafile || !fs::exists(fs::path(ssl_cafile))) | ||||
|             for (const char * bundle : CA_BUNDLES) { | ||||
|                 if (fs::exists(fs::path(bundle))) { | ||||
|                     ::setenv(SSL_CA_FILE, bundle, replace); | ||||
|         if (!ssl_cafile || !fs::exists(fs::path(ssl_cafile))) { | ||||
|             const char * bundle = nullptr; | ||||
|             for (const char * b : CA_BUNDLES) { | ||||
|                 if (fs::exists(fs::path(b))) { | ||||
|                     ::setenv(SSL_CA_FILE, bundle = b, replace); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         BOOST_LOG_TRIVIAL(info) | ||||
|             << "Detected OpenSSL root CA store: " << ::getenv(SSL_CA_FILE); | ||||
|             if (!bundle) | ||||
|                 message = L("Could not detect system SSL certificate store. " | ||||
|                             "PrusaSlicer will be unable to establish secure " | ||||
|                             "network connections."); | ||||
|             else | ||||
|                 message = string_printf( | ||||
|                     L("PrusaSlicer detected system SSL certificate store in: %s"), | ||||
|                     bundle); | ||||
| 
 | ||||
| #endif | ||||
|             message += string_printf( | ||||
|                 L("\nTo specify the system certificate store manually, please " | ||||
|                   "set the %s environment variable to the correct CA bundle " | ||||
|                   "and restart the application."), | ||||
|                 SSL_CA_FILE); | ||||
|         } | ||||
| 
 | ||||
| #endif // OPENSSL_CERT_OVERRIDE
 | ||||
|          | ||||
|         ::curl_global_init(CURL_GLOBAL_DEFAULT); | ||||
|         if (CURLcode ec = ::curl_global_init(CURL_GLOBAL_DEFAULT)) { | ||||
|             message = L("CURL init has failed. PrusaSlicer will be unable to establish " | ||||
|                         "network connections. See logs for additional details."); | ||||
|              | ||||
|             BOOST_LOG_TRIVIAL(error) << ::curl_easy_strerror(ec); | ||||
|         } | ||||
|     } | ||||
|      | ||||
| 	~CurlGlobalInit() { ::curl_global_cleanup(); } | ||||
|  | @ -132,8 +153,7 @@ Http::priv::priv(const std::string &url) | |||
| 	, limit(0) | ||||
| 	, cancel(false) | ||||
| { | ||||
|     if (!CurlGlobalInit::instance) | ||||
|         CurlGlobalInit::instance = std::make_unique<CurlGlobalInit>(); | ||||
|     Http::tls_global_init(); | ||||
|      | ||||
| 	if (curl == nullptr) { | ||||
| 		throw std::runtime_error(std::string("Could not construct Curl object")); | ||||
|  | @ -494,7 +514,26 @@ 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; | ||||
|     return res; | ||||
| } | ||||
| 
 | ||||
| std::string Http::tls_global_init() | ||||
| { | ||||
|     if (!CurlGlobalInit::instance) | ||||
|         CurlGlobalInit::instance = std::make_unique<CurlGlobalInit>(); | ||||
|      | ||||
|     return CurlGlobalInit::instance->message; | ||||
| } | ||||
| 
 | ||||
| std::string Http::tls_system_cert_store() | ||||
| { | ||||
|     std::string ret; | ||||
| 
 | ||||
| #ifdef OPENSSL_CERT_OVERRIDE | ||||
|     ret = ::getenv(X509_get_default_cert_file_env()); | ||||
| #endif | ||||
|      | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| std::string Http::url_encode(const std::string &str) | ||||
|  |  | |||
|  | @ -100,6 +100,10 @@ public: | |||
| 
 | ||||
| 	// Tells whether current backend supports seting up a CA file using ca_file()
 | ||||
| 	static bool ca_file_supported(); | ||||
|      | ||||
|     // Return empty string on success or error message on fail.
 | ||||
|     static std::string tls_global_init(); | ||||
|     static std::string tls_system_cert_store(); | ||||
| 
 | ||||
| 	// converts the given string to an url_encoded_string
 | ||||
| 	static std::string url_encode(const std::string &str); | ||||
|  |  | |||
							
								
								
									
										314
									
								
								src/slic3r/Utils/SLAImport.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								src/slic3r/Utils/SLAImport.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,314 @@ | |||
| #include "SLAImport.hpp" | ||||
| 
 | ||||
| #include <sstream> | ||||
| 
 | ||||
| #include "libslic3r/SlicesToTriangleMesh.hpp" | ||||
| #include "libslic3r/MarchingSquares.hpp" | ||||
| #include "libslic3r/ClipperUtils.hpp" | ||||
| #include "libslic3r/MTUtils.hpp" | ||||
| #include "libslic3r/PrintConfig.hpp" | ||||
| #include "libslic3r/SLA/RasterBase.hpp" | ||||
| #include "libslic3r/miniz_extension.hpp" | ||||
| 
 | ||||
| #include <boost/property_tree/ini_parser.hpp> | ||||
| #include <boost/filesystem/path.hpp> | ||||
| #include <boost/algorithm/string.hpp> | ||||
| 
 | ||||
| #include <wx/image.h> | ||||
| #include <wx/mstream.h> | ||||
| 
 | ||||
| namespace marchsq { | ||||
| 
 | ||||
| // Specialize this struct to register a raster type for the Marching squares alg
 | ||||
| template<> struct _RasterTraits<wxImage> { | ||||
|     using Rst = wxImage; | ||||
|      | ||||
|     // The type of pixel cell in the raster
 | ||||
|     using ValueType = uint8_t; | ||||
|      | ||||
|     // Value at a given position
 | ||||
|     static uint8_t get(const Rst &rst, size_t row, size_t col) | ||||
|     { | ||||
|         return rst.GetRed(col, row); | ||||
|     } | ||||
| 
 | ||||
|     // Number of rows and cols of the raster
 | ||||
|     static size_t rows(const Rst &rst) { return rst.GetHeight(); } | ||||
|     static size_t cols(const Rst &rst) { return rst.GetWidth(); } | ||||
| }; | ||||
| 
 | ||||
| } // namespace marchsq
 | ||||
| 
 | ||||
| namespace Slic3r { | ||||
| 
 | ||||
| namespace { | ||||
| 
 | ||||
| struct ArchiveData { | ||||
|     boost::property_tree::ptree profile, config; | ||||
|     std::vector<sla::EncodedRaster> images; | ||||
| }; | ||||
| 
 | ||||
| static const constexpr char *CONFIG_FNAME  = "config.ini"; | ||||
| static const constexpr char *PROFILE_FNAME = "prusaslicer.ini"; | ||||
| 
 | ||||
| boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry, | ||||
|                                      MZ_Archive &                    zip) | ||||
| { | ||||
|     std::string buf(size_t(entry.m_uncomp_size), '\0'); | ||||
|      | ||||
|     if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, | ||||
|                                            buf.data(), buf.size(), 0)) | ||||
|         throw std::runtime_error(zip.get_errorstr()); | ||||
|      | ||||
|     boost::property_tree::ptree tree; | ||||
|     std::stringstream ss(buf); | ||||
|     boost::property_tree::read_ini(ss, tree); | ||||
|     return tree; | ||||
| } | ||||
| 
 | ||||
| sla::EncodedRaster read_png(const mz_zip_archive_file_stat &entry, | ||||
|                             MZ_Archive &                    zip, | ||||
|                             const std::string &             name) | ||||
| { | ||||
|     std::vector<uint8_t> buf(entry.m_uncomp_size); | ||||
| 
 | ||||
|     if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, | ||||
|                                            buf.data(), buf.size(), 0)) | ||||
|         throw std::runtime_error(zip.get_errorstr()); | ||||
| 
 | ||||
|     return sla::EncodedRaster(std::move(buf), | ||||
|                               name.empty() ? entry.m_filename : name); | ||||
| } | ||||
| 
 | ||||
| ArchiveData extract_sla_archive(const std::string &zipfname, | ||||
|                                  const std::string &exclude) | ||||
| { | ||||
|     ArchiveData arch; | ||||
|      | ||||
|     // Little RAII
 | ||||
|     struct Arch: public MZ_Archive { | ||||
|         Arch(const std::string &fname) { | ||||
|             if (!open_zip_reader(&arch, fname)) | ||||
|                 throw std::runtime_error(get_errorstr()); | ||||
|         } | ||||
|          | ||||
|         ~Arch() { close_zip_reader(&arch); } | ||||
|     } zip (zipfname); | ||||
| 
 | ||||
|     mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch); | ||||
|      | ||||
|     for (mz_uint i = 0; i < num_entries; ++i) | ||||
|     { | ||||
|         mz_zip_archive_file_stat entry; | ||||
|          | ||||
|         if (mz_zip_reader_file_stat(&zip.arch, i, &entry)) | ||||
|         { | ||||
|             std::string name = entry.m_filename; | ||||
|             boost::algorithm::to_lower(name); | ||||
|              | ||||
|             if (boost::algorithm::contains(name, exclude)) continue; | ||||
|              | ||||
|             if (name == CONFIG_FNAME) arch.config = read_ini(entry, zip); | ||||
|             if (name == PROFILE_FNAME) arch.profile = read_ini(entry, zip); | ||||
|              | ||||
|             if (boost::filesystem::path(name).extension().string() == ".png") { | ||||
|                 auto it = std::lower_bound( | ||||
|                     arch.images.begin(), arch.images.end(), sla::EncodedRaster({}, name), | ||||
|                     [](const sla::EncodedRaster &r1, const sla::EncodedRaster &r2) { | ||||
|                         return std::less<std::string>()(r1.extension(), r2.extension()); | ||||
|                     }); | ||||
|                  | ||||
|                 arch.images.insert(it, read_png(entry, zip, name)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return arch; | ||||
| } | ||||
| 
 | ||||
| ExPolygons rings_to_expolygons(const std::vector<marchsq::Ring> &rings, | ||||
|                                double px_w, double px_h) | ||||
| { | ||||
|     ExPolygons polys; polys.reserve(rings.size()); | ||||
|      | ||||
|     for (const marchsq::Ring &ring : rings) { | ||||
|         Polygon poly; Points &pts = poly.points; | ||||
|         pts.reserve(ring.size()); | ||||
|          | ||||
|         for (const marchsq::Coord &crd : ring) | ||||
|             pts.emplace_back(scaled(crd.c * px_w), scaled(crd.r * px_h)); | ||||
|          | ||||
|         polys.emplace_back(poly); | ||||
|     } | ||||
|      | ||||
|     // reverse the raster transformations
 | ||||
|     return union_ex(polys); | ||||
| } | ||||
| 
 | ||||
| template<class Fn> void foreach_vertex(ExPolygon &poly, Fn &&fn) | ||||
| { | ||||
|     for (auto &p : poly.contour.points) fn(p); | ||||
|     for (auto &h : poly.holes) | ||||
|         for (auto &p : h.points) fn(p); | ||||
| } | ||||
| 
 | ||||
| void invert_raster_trafo(ExPolygons &                  expolys, | ||||
|                          const sla::RasterBase::Trafo &trafo, | ||||
|                          coord_t                       width, | ||||
|                          coord_t                       height) | ||||
| { | ||||
|     for (auto &expoly : expolys) { | ||||
|         if (trafo.mirror_y) | ||||
|             foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); }); | ||||
|          | ||||
|         if (trafo.mirror_x) | ||||
|             foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); }); | ||||
|          | ||||
|         expoly.translate(-trafo.center_x, -trafo.center_y); | ||||
|          | ||||
|         if (trafo.flipXY) | ||||
|             foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); }); | ||||
|          | ||||
|         if ((trafo.mirror_x + trafo.mirror_y + trafo.flipXY) % 2) { | ||||
|             expoly.contour.reverse(); | ||||
|             for (auto &h : expoly.holes) h.reverse(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| struct RasterParams { | ||||
|     sla::RasterBase::Trafo trafo; // Raster transformations
 | ||||
|     coord_t        width, height; // scaled raster dimensions (not resolution)
 | ||||
|     double         px_h, px_w;    // pixel dimesions
 | ||||
|     marchsq::Coord win;           // marching squares window size
 | ||||
| }; | ||||
| 
 | ||||
| RasterParams get_raster_params(const DynamicPrintConfig &cfg) | ||||
| { | ||||
|     auto *opt_disp_cols = cfg.option<ConfigOptionInt>("display_pixels_x"); | ||||
|     auto *opt_disp_rows = cfg.option<ConfigOptionInt>("display_pixels_y"); | ||||
|     auto *opt_disp_w    = cfg.option<ConfigOptionFloat>("display_width"); | ||||
|     auto *opt_disp_h    = cfg.option<ConfigOptionFloat>("display_height"); | ||||
|     auto *opt_mirror_x  = cfg.option<ConfigOptionBool>("display_mirror_x"); | ||||
|     auto *opt_mirror_y  = cfg.option<ConfigOptionBool>("display_mirror_y"); | ||||
|     auto *opt_orient    = cfg.option<ConfigOptionEnum<SLADisplayOrientation>>("display_orientation"); | ||||
|      | ||||
|     if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h || | ||||
|         !opt_mirror_x || !opt_mirror_y || !opt_orient) | ||||
|         throw std::runtime_error("Invalid SL1 file"); | ||||
|      | ||||
|     RasterParams rstp; | ||||
|      | ||||
|     rstp.px_w = opt_disp_w->value / (opt_disp_cols->value - 1); | ||||
|     rstp.px_h = opt_disp_h->value / (opt_disp_rows->value - 1); | ||||
|      | ||||
|     sla::RasterBase::Trafo trafo{opt_orient->value == sladoLandscape ? | ||||
|                                      sla::RasterBase::roLandscape : | ||||
|                                      sla::RasterBase::roPortrait, | ||||
|                                  {opt_mirror_x->value, opt_mirror_y->value}}; | ||||
|      | ||||
|     rstp.height = scaled(opt_disp_h->value); | ||||
|     rstp.width  = scaled(opt_disp_w->value); | ||||
|      | ||||
|     return rstp; | ||||
| } | ||||
| 
 | ||||
| struct SliceParams { double layerh = 0., initial_layerh = 0.; }; | ||||
| 
 | ||||
| SliceParams get_slice_params(const DynamicPrintConfig &cfg) | ||||
| { | ||||
|     auto *opt_layerh = cfg.option<ConfigOptionFloat>("layer_height"); | ||||
|     auto *opt_init_layerh = cfg.option<ConfigOptionFloat>("initial_layer_height"); | ||||
|      | ||||
|     if (!opt_layerh || !opt_init_layerh) | ||||
|         throw std::runtime_error("Invalid SL1 file"); | ||||
|      | ||||
|     return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()}; | ||||
| } | ||||
| 
 | ||||
| std::vector<ExPolygons> extract_slices_from_sla_archive( | ||||
|     ArchiveData &            arch, | ||||
|     const RasterParams &     rstp, | ||||
|     std::function<bool(int)> progr) | ||||
| { | ||||
|     auto jobdir = arch.config.get<std::string>("jobDir"); | ||||
|     for (auto &c : jobdir) c = std::tolower(c); | ||||
|      | ||||
|     std::vector<ExPolygons> slices(arch.images.size()); | ||||
| 
 | ||||
|     struct Status | ||||
|     { | ||||
|         double          incr, val, prev; | ||||
|         bool            stop = false; | ||||
|         tbb::spin_mutex mutex; | ||||
|     } st {100. / slices.size(), 0., 0.}; | ||||
|      | ||||
|     tbb::parallel_for(size_t(0), arch.images.size(), | ||||
|                      [&arch, &slices, &st, &rstp, progr](size_t i) { | ||||
|         // Status indication guarded with the spinlock
 | ||||
|         { | ||||
|             std::lock_guard<tbb::spin_mutex> lck(st.mutex); | ||||
|             if (st.stop) return; | ||||
|      | ||||
|             st.val += st.incr; | ||||
|             double curr = std::round(st.val); | ||||
|             if (curr > st.prev) { | ||||
|                 st.prev = curr; | ||||
|                 st.stop = !progr(int(curr)); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         auto &buf = arch.images[i]; | ||||
|         wxMemoryInputStream   stream{buf.data(), buf.size()}; | ||||
|         wxImage               img{stream}; | ||||
|      | ||||
|         auto rings = marchsq::execute(img, 128, rstp.win); | ||||
|         ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, rstp.px_h); | ||||
| 
 | ||||
|         // Invert the raster transformations indicated in
 | ||||
|         // the profile metadata
 | ||||
|         invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height); | ||||
|      | ||||
|         slices[i] = std::move(expolys); | ||||
|     }); | ||||
|      | ||||
|     if (st.stop) slices = {}; | ||||
| 
 | ||||
|     return slices; | ||||
| } | ||||
| 
 | ||||
| } // namespace
 | ||||
| 
 | ||||
| void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out) | ||||
| { | ||||
|     ArchiveData arch = extract_sla_archive(zipfname, "png"); | ||||
|     out.load(arch.profile); | ||||
| } | ||||
| 
 | ||||
| void import_sla_archive( | ||||
|     const std::string &      zipfname, | ||||
|     Vec2i                    windowsize, | ||||
|     TriangleMesh &           out, | ||||
|     DynamicPrintConfig &     profile, | ||||
|     std::function<bool(int)> progr) | ||||
| { | ||||
|     // Ensure minimum window size for marching squares
 | ||||
|     windowsize.x() = std::max(2, windowsize.x()); | ||||
|     windowsize.y() = std::max(2, windowsize.y()); | ||||
| 
 | ||||
|     ArchiveData arch = extract_sla_archive(zipfname, "thumbnail"); | ||||
|     profile.load(arch.profile); | ||||
| 
 | ||||
|     RasterParams rstp = get_raster_params(profile); | ||||
|     rstp.win          = {windowsize.y(), windowsize.x()}; | ||||
|      | ||||
|     SliceParams slicp = get_slice_params(profile); | ||||
|      | ||||
|     std::vector<ExPolygons> slices = | ||||
|         extract_slices_from_sla_archive(arch, rstp, progr); | ||||
|     | ||||
|     if (!slices.empty()) | ||||
|         out = slices_to_triangle_mesh(slices, 0, slicp.layerh, slicp.initial_layerh); | ||||
| } | ||||
| 
 | ||||
| } // namespace Slic3r
 | ||||
							
								
								
									
										36
									
								
								src/slic3r/Utils/SLAImport.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/slic3r/Utils/SLAImport.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| #ifndef SLAIMPORT_HPP | ||||
| #define SLAIMPORT_HPP | ||||
| 
 | ||||
| #include <functional> | ||||
| 
 | ||||
| #include <libslic3r/Point.hpp> | ||||
| #include <libslic3r/TriangleMesh.hpp> | ||||
| #include <libslic3r/PrintConfig.hpp> | ||||
| 
 | ||||
| namespace Slic3r { | ||||
| 
 | ||||
| class TriangleMesh; | ||||
| class DynamicPrintConfig; | ||||
| 
 | ||||
| void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out); | ||||
| 
 | ||||
| void import_sla_archive( | ||||
|     const std::string &      zipfname, | ||||
|     Vec2i                    windowsize, | ||||
|     TriangleMesh &           out, | ||||
|     DynamicPrintConfig &     profile, | ||||
|     std::function<bool(int)> progr = [](int) { return true; }); | ||||
| 
 | ||||
| inline void import_sla_archive( | ||||
|     const std::string &      zipfname, | ||||
|     Vec2i                    windowsize, | ||||
|     TriangleMesh &           out, | ||||
|     std::function<bool(int)> progr = [](int) { return true; }) | ||||
| { | ||||
|     DynamicPrintConfig profile; | ||||
|     import_sla_archive(zipfname, windowsize, out, profile, progr); | ||||
| } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| #endif // SLAIMPORT_HPP
 | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 enricoturri1966
						enricoturri1966