mirror of
https://git.mirrors.martin98.com/https://github.com/prusa3d/PrusaSlicer.git
synced 2025-07-31 21:52:02 +08:00
6970 lines
295 KiB
C++
6970 lines
295 KiB
C++
///|/ Copyright (c) Prusa Research 2018 - 2023 Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Oleksandra Iushchenko @YuSanka, Enrico Turri @enricoturri1966, Tomáš Mészáros @tamasmeszaros, David Kocík @kocikdav, Lukáš Hejl @hejllukas, Pavel Mikuš @Godrak, Filip Sykala @Jony01, Vojtěch Král @vojtechkral
|
|
///|/ Copyright (c) 2022 Michael Kirsch
|
|
///|/ Copyright (c) 2021 Boleslaw Ciesielski
|
|
///|/ Copyright (c) 2019 John Drake @foxox
|
|
///|/
|
|
///|/ ported from lib/Slic3r/GUI/Plater.pm:
|
|
///|/ Copyright (c) Prusa Research 2016 - 2019 Vojtěch Bubník @bubnikv, Vojtěch Král @vojtechkral, Enrico Turri @enricoturri1966, Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Tomáš Mészáros @tamasmeszaros
|
|
///|/ Copyright (c) 2018 Martin Loidl @LoidlM
|
|
///|/ Copyright (c) 2017 Matthias Gazzari @qtux
|
|
///|/ Copyright (c) Slic3r 2012 - 2016 Alessandro Ranellucci @alranel
|
|
///|/ Copyright (c) 2017 Joseph Lenox @lordofhyphens
|
|
///|/ Copyright (c) 2015 Daren Schwenke
|
|
///|/ Copyright (c) 2014 Mark Hindess
|
|
///|/ Copyright (c) 2012 Mike Sheldrake @mesheldrake
|
|
///|/ Copyright (c) 2012 Henrik Brix Andersen @henrikbrixandersen
|
|
///|/ Copyright (c) 2012 Sam Wong
|
|
///|/
|
|
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
|
///|/
|
|
#include "Plater.hpp"
|
|
#include "slic3r/GUI/Jobs/UIThreadWorker.hpp"
|
|
|
|
#include <cstddef>
|
|
#include <algorithm>
|
|
#include <numeric>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <regex>
|
|
#include <future>
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/nowide/cstdio.hpp>
|
|
#include <boost/optional.hpp>
|
|
#include <boost/filesystem/fstream.hpp>
|
|
#include <boost/filesystem/path.hpp>
|
|
#include <boost/filesystem/operations.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
#include <boost/nowide/convert.hpp>
|
|
#include <boost/property_tree/ptree.hpp>
|
|
#include <boost/property_tree/json_parser.hpp>
|
|
|
|
#include <wx/sizer.h>
|
|
#include <wx/stattext.h>
|
|
#include <wx/button.h>
|
|
#include <wx/bmpcbox.h>
|
|
#include <wx/statbox.h>
|
|
#include <wx/statbmp.h>
|
|
#include <wx/filedlg.h>
|
|
#include <wx/dnd.h>
|
|
#include <wx/progdlg.h>
|
|
#include <wx/wupdlock.h>
|
|
#include <wx/numdlg.h>
|
|
#include <wx/debug.h>
|
|
#include <wx/busyinfo.h>
|
|
#include <wx/stdpaths.h>
|
|
#if wxUSE_SECRETSTORE
|
|
#include <wx/secretstore.h>
|
|
#endif
|
|
|
|
#include <LibBGCode/convert/convert.hpp>
|
|
|
|
#include "libslic3r/libslic3r.h"
|
|
#include "libslic3r/Format/STL.hpp"
|
|
#include "libslic3r/Format/AMF.hpp"
|
|
#include "libslic3r/Format/3mf.hpp"
|
|
#include "libslic3r/Format/OBJ.hpp"
|
|
#include "libslic3r/GCode/ThumbnailData.hpp"
|
|
#include "libslic3r/Model.hpp"
|
|
#include "libslic3r/SLA/SupportPoint.hpp"
|
|
#include "libslic3r/SLA/ReprojectPointsOnMesh.hpp"
|
|
#include "libslic3r/Print.hpp"
|
|
#include "libslic3r/PrintConfig.hpp"
|
|
#include "libslic3r/SLAPrint.hpp"
|
|
#include "libslic3r/Utils.hpp"
|
|
#include "libslic3r/PresetBundle.hpp"
|
|
#include "libslic3r/miniz_extension.hpp"
|
|
|
|
// For stl export
|
|
#include "libslic3r/CSGMesh/ModelToCSGMesh.hpp"
|
|
#include "libslic3r/CSGMesh/PerformCSGMeshBooleans.hpp"
|
|
|
|
#include "GUI.hpp"
|
|
#include "GUI_App.hpp"
|
|
#include "GUI_ObjectList.hpp"
|
|
#include "GUI_ObjectManipulation.hpp"
|
|
#include "GUI_Utils.hpp"
|
|
#include "GUI_Factories.hpp"
|
|
#include "wxExtensions.hpp"
|
|
#include "MainFrame.hpp"
|
|
#include "format.hpp"
|
|
#include "3DScene.hpp"
|
|
#include "GLCanvas3D.hpp"
|
|
#include "Selection.hpp"
|
|
#include "GLToolbar.hpp"
|
|
#include "GUI_Preview.hpp"
|
|
#include "3DBed.hpp"
|
|
#include "Camera.hpp"
|
|
#include "Mouse3DController.hpp"
|
|
#include "Tab.hpp"
|
|
#include "Jobs/ArrangeJob2.hpp"
|
|
|
|
#include "Jobs/RotoptimizeJob.hpp"
|
|
#include "Jobs/SLAImportJob.hpp"
|
|
#include "Jobs/SLAImportDialog.hpp"
|
|
#include "Jobs/NotificationProgressIndicator.hpp"
|
|
#include "Jobs/PlaterWorker.hpp"
|
|
#include "Jobs/BoostThreadWorker.hpp"
|
|
#include "BackgroundSlicingProcess.hpp"
|
|
#include "PrintHostDialogs.hpp"
|
|
#include "../Utils/ASCIIFolding.hpp"
|
|
#include "../Utils/PrintHost.hpp"
|
|
#include "../Utils/UndoRedo.hpp"
|
|
#include "../Utils/PresetUpdater.hpp"
|
|
#include "../Utils/Process.hpp"
|
|
#include "RemovableDriveManager.hpp"
|
|
#include "InstanceCheck.hpp"
|
|
#include "NotificationManager.hpp"
|
|
#include "PresetComboBoxes.hpp"
|
|
#include "MsgDialog.hpp"
|
|
#include "ProjectDirtyStateManager.hpp"
|
|
#include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification
|
|
#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file
|
|
#include "Gizmos/GLGizmoCut.hpp"
|
|
#include "FileArchiveDialog.hpp"
|
|
#include "UserAccount.hpp"
|
|
#include "DesktopIntegrationDialog.hpp"
|
|
#include "WebViewDialog.hpp"
|
|
|
|
#ifdef __APPLE__
|
|
#include "Gizmos/GLGizmosManager.hpp"
|
|
#endif // __APPLE__
|
|
|
|
#include <wx/glcanvas.h> // Needs to be last because reasons :-/
|
|
#include "WipeTowerDialog.hpp"
|
|
|
|
#include "libslic3r/CustomGCode.hpp"
|
|
#include "libslic3r/Platform.hpp"
|
|
|
|
#include "Widgets/CheckBox.hpp"
|
|
|
|
using boost::optional;
|
|
namespace fs = boost::filesystem;
|
|
using Slic3r::_3DScene;
|
|
using Slic3r::Preset;
|
|
using Slic3r::PrintHostJob;
|
|
using Slic3r::GUI::format_wxstr;
|
|
|
|
static const std::pair<unsigned int, unsigned int> THUMBNAIL_SIZE_3MF = { 256, 256 };
|
|
|
|
namespace Slic3r {
|
|
namespace GUI {
|
|
|
|
// BackgroundSlicingProcess updates UI with slicing progress: Status bar / progress bar has to be updated, possibly scene has to be refreshed,
|
|
// see PrintBase::SlicingStatus for the content of the message.
|
|
wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent);
|
|
// FDM slicing finished, but G-code was not exported yet. Initial G-code preview shall be displayed by the UI.
|
|
wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent);
|
|
// BackgroundSlicingProcess finished either with success or error.
|
|
wxDEFINE_EVENT(EVT_PROCESS_COMPLETED, SlicingProcessCompletedEvent);
|
|
wxDEFINE_EVENT(EVT_EXPORT_BEGAN, wxCommandEvent);
|
|
|
|
// Plater::DropTarget
|
|
|
|
class PlaterDropTarget : public wxFileDropTarget
|
|
{
|
|
public:
|
|
PlaterDropTarget(MainFrame& mainframe, Plater& plater) : m_mainframe(mainframe), m_plater(plater) {
|
|
this->SetDefaultAction(wxDragCopy);
|
|
}
|
|
|
|
virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames);
|
|
|
|
private:
|
|
MainFrame& m_mainframe;
|
|
Plater& m_plater;
|
|
};
|
|
|
|
namespace {
|
|
bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position)
|
|
{
|
|
std::string svg_file_str = into_u8(svg_file);
|
|
GLCanvas3D* canvas = plater.canvas3D();
|
|
if (canvas == nullptr)
|
|
return false;
|
|
auto base_svg = canvas->get_gizmos_manager().get_gizmo(GLGizmosManager::Svg);
|
|
if (base_svg == nullptr)
|
|
return false;
|
|
GLGizmoSVG* svg = dynamic_cast<GLGizmoSVG *>(base_svg);
|
|
if (svg == nullptr)
|
|
return false;
|
|
|
|
// Refresh hover state to find surface point under mouse
|
|
wxMouseEvent evt(wxEVT_MOTION);
|
|
evt.SetPosition(wxPoint(mouse_drop_position.x(), mouse_drop_position.y()));
|
|
canvas->on_mouse(evt); // call render where is call GLCanvas3D::_picking_pass()
|
|
|
|
return svg->create_volume(svg_file_str, mouse_drop_position, ModelVolumeType::MODEL_PART);
|
|
}
|
|
}
|
|
|
|
bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames)
|
|
{
|
|
#ifdef WIN32
|
|
// hides the system icon
|
|
this->MSWUpdateDragImageOnLeave();
|
|
#endif // WIN32
|
|
|
|
m_mainframe.Raise();
|
|
m_mainframe.select_tab(size_t(0));
|
|
if (wxGetApp().is_editor())
|
|
m_plater.select_view_3D("3D");
|
|
|
|
// When only one .svg file is dropped on scene
|
|
if (filenames.size() == 1) {
|
|
const wxString &filename = filenames.Last();
|
|
const wxString file_extension = filename.substr(filename.length() - 4);
|
|
if (file_extension.CmpNoCase(".svg") == 0) {
|
|
const wxPoint offset = m_plater.GetPosition();
|
|
Vec2d mouse_position(x - offset.x, y - offset.y);
|
|
// Scale for retina displays
|
|
const GLCanvas3D *canvas = m_plater.canvas3D();
|
|
canvas->apply_retina_scale(mouse_position);
|
|
return emboss_svg(m_plater, filename, mouse_position);
|
|
}
|
|
}
|
|
bool res = m_plater.load_files(filenames);
|
|
m_mainframe.update_title();
|
|
return res;
|
|
}
|
|
|
|
// State to manage showing after export notifications and device ejecting
|
|
enum ExportingStatus{
|
|
NOT_EXPORTING,
|
|
EXPORTING_TO_REMOVABLE,
|
|
EXPORTING_TO_LOCAL
|
|
};
|
|
|
|
// Plater / private
|
|
struct Plater::priv
|
|
{
|
|
// PIMPL back pointer ("Q-Pointer")
|
|
Plater *q;
|
|
MainFrame *main_frame;
|
|
|
|
MenuFactory menus;
|
|
|
|
// Data
|
|
Slic3r::DynamicPrintConfig *config; // FIXME: leak?
|
|
Slic3r::Print fff_print;
|
|
Slic3r::SLAPrint sla_print;
|
|
Slic3r::Model model;
|
|
PrinterTechnology printer_technology = ptFFF;
|
|
Slic3r::GCodeProcessorResult gcode_result;
|
|
|
|
// GUI elements
|
|
wxSizer* panel_sizer{ nullptr };
|
|
wxPanel* current_panel{ nullptr };
|
|
std::vector<wxPanel*> panels;
|
|
Sidebar *sidebar;
|
|
Bed3D bed;
|
|
Camera camera;
|
|
#if ENABLE_ENVIRONMENT_MAP
|
|
GLTexture environment_texture;
|
|
#endif // ENABLE_ENVIRONMENT_MAP
|
|
Mouse3DController mouse3d_controller;
|
|
View3D* view3D;
|
|
GLToolbar view_toolbar;
|
|
GLToolbar collapse_toolbar;
|
|
Preview *preview;
|
|
std::unique_ptr<NotificationManager> notification_manager;
|
|
std::unique_ptr<UserAccount> user_account;
|
|
|
|
ProjectDirtyStateManager dirty_state;
|
|
|
|
BackgroundSlicingProcess background_process;
|
|
bool suppressed_backround_processing_update { false };
|
|
|
|
// 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.
|
|
//
|
|
// UIThreadWorker can be used as a replacement for BoostThreadWorker if
|
|
// no additional worker threads are desired (useful for debugging or profiling)
|
|
PlaterWorker<BoostThreadWorker> m_worker;
|
|
SLAImportDialog * m_sla_import_dlg;
|
|
|
|
bool delayed_scene_refresh;
|
|
std::string delayed_error_message;
|
|
|
|
wxTimer background_process_timer;
|
|
|
|
std::string label_btn_export;
|
|
std::string label_btn_send;
|
|
|
|
bool show_render_statistic_dialog{ false };
|
|
|
|
static const std::regex pattern_bundle;
|
|
static const std::regex pattern_3mf;
|
|
static const std::regex pattern_zip_amf;
|
|
static const std::regex pattern_any_amf;
|
|
static const std::regex pattern_prusa;
|
|
static const std::regex pattern_zip;
|
|
|
|
priv(Plater *q, MainFrame *main_frame);
|
|
~priv();
|
|
|
|
bool is_project_dirty() const { return dirty_state.is_dirty(); }
|
|
bool is_presets_dirty() const { return dirty_state.is_presets_dirty(); }
|
|
void update_project_dirty_from_presets() { dirty_state.update_from_presets(); }
|
|
int save_project_if_dirty(const wxString& reason) {
|
|
int res = wxID_NO;
|
|
if (dirty_state.is_dirty()) {
|
|
MainFrame* mainframe = wxGetApp().mainframe;
|
|
if (mainframe->can_save_as()) {
|
|
wxString suggested_project_name;
|
|
wxString project_name = suggested_project_name = get_project_filename(".3mf");
|
|
if (suggested_project_name.IsEmpty()) {
|
|
fs::path output_file = get_export_file_path(FT_3MF);
|
|
suggested_project_name = output_file.empty() ? _L("Untitled") : from_u8(output_file.stem().string());
|
|
}
|
|
|
|
std::string act_key = "default_action_on_dirty_project";
|
|
std::string act = wxGetApp().app_config->get(act_key);
|
|
if (act.empty()) {
|
|
RichMessageDialog dialog(mainframe, reason + "\n" + format_wxstr(_L("Do you want to save the changes to \"%1%\"?"), suggested_project_name), wxString(SLIC3R_APP_NAME), wxYES_NO | wxCANCEL);
|
|
dialog.SetYesNoLabels(_L("Save"), _L("Discard"));
|
|
dialog.ShowCheckBox(_L("Remember my choice"));
|
|
res = dialog.ShowModal();
|
|
if (res != wxID_CANCEL)
|
|
if (dialog.IsCheckBoxChecked()) {
|
|
wxString preferences_item = _L("Ask for unsaved changes in project");
|
|
wxString msg =
|
|
_L("PrusaSlicer will remember your choice.") + "\n\n" +
|
|
_L("You will not be asked about it again, when: \n"
|
|
"- Closing PrusaSlicer,\n"
|
|
"- Loading or creating a new project") + "\n\n" +
|
|
format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item);
|
|
|
|
MessageDialog msg_dlg(mainframe, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION);
|
|
if (msg_dlg.ShowModal() == wxID_CANCEL)
|
|
return wxID_CANCEL;
|
|
|
|
get_app_config()->set(act_key, res == wxID_YES ? "1" : "0");
|
|
}
|
|
}
|
|
else
|
|
res = (act == "1") ? wxID_YES : wxID_NO;
|
|
|
|
if (res == wxID_YES)
|
|
if (!mainframe->save_project_as(project_name)) {
|
|
// Return Cancel only, when we don't remember a choice for closing the application.
|
|
// Elsewhere it can causes an impossibility to close the application at all.
|
|
res = act.empty() ? wxID_CANCEL : wxID_NO;
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
void reset_project_dirty_after_save() { m_undo_redo_stack_main.mark_current_as_saved(); dirty_state.reset_after_save(); }
|
|
void reset_project_dirty_initial_presets() { dirty_state.reset_initial_presets(); }
|
|
|
|
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
|
|
void render_project_state_debug_window() const { dirty_state.render_debug_window(); }
|
|
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
|
|
|
|
void update(unsigned int flags = 0);
|
|
void select_view(const std::string& direction);
|
|
void select_view_3D(const std::string& name);
|
|
void select_next_view_3D();
|
|
|
|
bool is_preview_shown() const { return current_panel == preview; }
|
|
bool is_preview_loaded() const { return preview->is_loaded(); }
|
|
bool is_view3D_shown() const { return current_panel == view3D; }
|
|
|
|
bool are_view3D_labels_shown() const { return (current_panel == view3D) && view3D->get_canvas3d()->are_labels_shown(); }
|
|
void show_view3D_labels(bool show) { if (current_panel == view3D) view3D->get_canvas3d()->show_labels(show); }
|
|
|
|
bool is_legend_shown() const { return (current_panel == preview) && preview->get_canvas3d()->is_legend_shown(); }
|
|
void show_legend(bool show) { if (current_panel == preview) preview->get_canvas3d()->show_legend(show); }
|
|
|
|
bool is_sidebar_collapsed() const { return sidebar->is_collapsed; }
|
|
void collapse_sidebar(bool collapse);
|
|
|
|
bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); }
|
|
|
|
void set_current_canvas_as_dirty();
|
|
GLCanvas3D* get_current_canvas3D();
|
|
void unbind_canvas_event_handlers();
|
|
void reset_canvas_volumes();
|
|
|
|
bool init_view_toolbar();
|
|
bool init_collapse_toolbar();
|
|
|
|
void set_preview_layers_slider_values_range(int bottom, int top);
|
|
|
|
void update_preview_moves_slider();
|
|
void enable_preview_moves_slider(bool enable);
|
|
|
|
void reset_gcode_toolpaths();
|
|
|
|
void reset_all_gizmos();
|
|
void apply_free_camera_correction(bool apply = true);
|
|
void update_ui_from_settings();
|
|
void update_main_toolbar_tooltips();
|
|
bool get_config_bool(const std::string &key) const;
|
|
|
|
std::vector<size_t> load_files(const std::vector<fs::path>& input_files, bool load_model, bool load_config, bool used_inches = false);
|
|
std::vector<size_t> load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool call_selection_changed = true);
|
|
|
|
fs::path get_export_file_path(GUI::FileType file_type);
|
|
wxString get_export_file(GUI::FileType file_type);
|
|
|
|
const Selection& get_selection() const;
|
|
Selection& get_selection();
|
|
int get_selected_object_idx() const;
|
|
int get_selected_instance_idx() const;
|
|
int get_selected_volume_idx() const;
|
|
void selection_changed();
|
|
void object_list_changed();
|
|
|
|
void select_all();
|
|
void deselect_all();
|
|
void remove(size_t obj_idx);
|
|
bool delete_object_from_model(size_t obj_idx);
|
|
void delete_all_objects_from_model();
|
|
void reset();
|
|
void mirror(Axis axis);
|
|
void split_object();
|
|
void split_volume();
|
|
void scale_selection_to_fit_print_volume();
|
|
|
|
// Return the active Undo/Redo stack. It may be either the main stack or the Gimzo stack.
|
|
Slic3r::UndoRedo::Stack& undo_redo_stack() { assert(m_undo_redo_stack_active != nullptr); return *m_undo_redo_stack_active; }
|
|
Slic3r::UndoRedo::Stack& undo_redo_stack_main() { return m_undo_redo_stack_main; }
|
|
void enter_gizmos_stack();
|
|
void leave_gizmos_stack();
|
|
|
|
void take_snapshot(const std::string& snapshot_name, UndoRedo::SnapshotType snapshot_type = UndoRedo::SnapshotType::Action);
|
|
void take_snapshot(const wxString& snapshot_name, UndoRedo::SnapshotType snapshot_type = UndoRedo::SnapshotType::Action)
|
|
{ this->take_snapshot(std::string(snapshot_name.ToUTF8().data()), snapshot_type); }
|
|
int get_active_snapshot_index();
|
|
|
|
void undo();
|
|
void redo();
|
|
void undo_redo_to(size_t time_to_load);
|
|
|
|
void suppress_snapshots() { m_prevent_snapshots++; }
|
|
void allow_snapshots() { m_prevent_snapshots--; }
|
|
|
|
void process_validation_warning(const std::vector<std::string>& warning) const;
|
|
|
|
bool background_processing_enabled() const { return this->get_config_bool("background_processing"); }
|
|
void update_print_volume_state();
|
|
void schedule_background_process();
|
|
// Update background processing thread from the current config and Model.
|
|
enum UpdateBackgroundProcessReturnState {
|
|
// update_background_process() reports, that the Print / SLAPrint was updated in a way,
|
|
// that the background process was invalidated and it needs to be re-run.
|
|
UPDATE_BACKGROUND_PROCESS_RESTART = 1,
|
|
// update_background_process() reports, that the Print / SLAPrint was updated in a way,
|
|
// that a scene needs to be refreshed (you should call _3DScene::reload_scene(canvas3Dwidget, false))
|
|
UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE = 2,
|
|
// update_background_process() reports, that the Print / SLAPrint is invalid, and the error message
|
|
// was sent to the status line.
|
|
UPDATE_BACKGROUND_PROCESS_INVALID = 4,
|
|
// Restart even if the background processing is disabled.
|
|
UPDATE_BACKGROUND_PROCESS_FORCE_RESTART = 8,
|
|
// Restart for G-code (or SLA zip) export or upload.
|
|
UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT = 16,
|
|
};
|
|
// returns bit mask of UpdateBackgroundProcessReturnState
|
|
unsigned int update_background_process(bool force_validation = false, bool postpone_error_messages = false);
|
|
// Restart background processing thread based on a bitmask of UpdateBackgroundProcessReturnState.
|
|
bool restart_background_process(unsigned int state);
|
|
// returns bit mask of UpdateBackgroundProcessReturnState
|
|
unsigned int update_restart_background_process(bool force_scene_update, bool force_preview_update);
|
|
void show_delayed_error_message() {
|
|
if (!this->delayed_error_message.empty()) {
|
|
std::string msg = std::move(this->delayed_error_message);
|
|
this->delayed_error_message.clear();
|
|
GUI::show_error(this->q, msg);
|
|
}
|
|
}
|
|
void export_gcode(fs::path output_path, bool output_path_on_removable_media, PrintHostJob upload_job);
|
|
void reload_from_disk();
|
|
bool replace_volume_with_stl(int object_idx, int volume_idx, const fs::path& new_path, const wxString& snapshot = "");
|
|
void replace_with_stl();
|
|
void reload_all_from_disk();
|
|
void set_current_panel(wxPanel* panel);
|
|
|
|
void on_slicing_update(SlicingStatusEvent&);
|
|
void on_slicing_completed(wxCommandEvent&);
|
|
void on_process_completed(SlicingProcessCompletedEvent&);
|
|
void on_export_began(wxCommandEvent&);
|
|
void on_layer_editing_toggled(bool enable);
|
|
void on_slicing_began();
|
|
|
|
void clear_warnings();
|
|
void add_warning(const Slic3r::PrintStateBase::Warning &warning, size_t oid);
|
|
// Update notification manager with the current state of warnings produced by the background process (slicing).
|
|
void actualize_slicing_warnings(const PrintBase &print);
|
|
void actualize_object_warnings(const PrintBase& print);
|
|
// Displays dialog window with list of warnings.
|
|
// Returns true if user clicks OK.
|
|
// Returns true if current_warnings vector is empty without showning the dialog
|
|
bool warnings_dialog();
|
|
|
|
void on_action_add(SimpleEvent&);
|
|
void on_action_split_objects(SimpleEvent&);
|
|
void on_action_split_volumes(SimpleEvent&);
|
|
void on_action_layersediting(SimpleEvent&);
|
|
|
|
void on_object_select(SimpleEvent&);
|
|
void on_right_click(RBtnEvent&);
|
|
void on_wipetower_moved(Vec3dEvent&);
|
|
void on_wipetower_rotated(Vec3dEvent&);
|
|
void on_update_geometry(Vec3dsEvent<2>&);
|
|
void on_3dcanvas_mouse_dragging_started(SimpleEvent&);
|
|
void on_3dcanvas_mouse_dragging_finished(SimpleEvent&);
|
|
|
|
void show_action_buttons(const bool is_ready_to_slice) const;
|
|
|
|
// Set the bed shape to a single closed 2D polygon(array of two element arrays),
|
|
// triangulate the bed and store the triangles into m_bed.m_triangles,
|
|
// fills the m_bed.m_grid_lines and sets m_bed.m_origin.
|
|
// Sets m_bed.m_polygon to limit the object placement.
|
|
void set_bed_shape(const Pointfs& shape, const double max_print_height, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom = false);
|
|
|
|
bool can_delete() const;
|
|
bool can_delete_all() const;
|
|
bool can_increase_instances() const;
|
|
bool can_decrease_instances(int obj_idx = -1) const;
|
|
bool can_split_to_objects() const;
|
|
bool can_split_to_volumes() const;
|
|
bool can_arrange() const;
|
|
bool can_layers_editing() const;
|
|
bool can_fix_through_winsdk() const;
|
|
bool can_simplify() const;
|
|
bool can_set_instance_to_object() const;
|
|
bool can_mirror() const;
|
|
bool can_reload_from_disk() const;
|
|
bool can_replace_with_stl() const;
|
|
bool can_split(bool to_objects) const;
|
|
bool can_scale_to_print_volume() const;
|
|
|
|
void generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, Camera::EType camera_type);
|
|
ThumbnailsList generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type);
|
|
|
|
void bring_instance_forward() const;
|
|
|
|
// returns the path to project file with the given extension (none if extension == wxEmptyString)
|
|
// extension should contain the leading dot, i.e.: ".3mf"
|
|
wxString get_project_filename(const wxString& extension = wxEmptyString) const;
|
|
void set_project_filename(const wxString& filename);
|
|
// Call after plater and Canvas#D is initialized
|
|
void init_notification_manager();
|
|
|
|
// Caching last value of show_action_buttons parameter for show_action_buttons(), so that a callback which does not know this state will not override it.
|
|
mutable bool ready_to_slice = { false };
|
|
// Flag indicating that the G-code export targets a removable device, therefore the show_action_buttons() needs to be called at any case when the background processing finishes.
|
|
ExportingStatus exporting_status { NOT_EXPORTING };
|
|
std::string last_output_path;
|
|
std::string last_output_dir_path;
|
|
bool inside_snapshot_capture() { return m_prevent_snapshots != 0; }
|
|
bool process_completed_with_error { false };
|
|
|
|
private:
|
|
bool layers_height_allowed() const;
|
|
|
|
void update_fff_scene();
|
|
void update_sla_scene();
|
|
void undo_redo_to(std::vector<UndoRedo::Snapshot>::const_iterator it_snapshot);
|
|
void update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bool temp_snapshot_was_taken = false);
|
|
|
|
// path to project file stored with no extension
|
|
wxString m_project_filename;
|
|
Slic3r::UndoRedo::Stack m_undo_redo_stack_main;
|
|
Slic3r::UndoRedo::Stack m_undo_redo_stack_gizmos;
|
|
Slic3r::UndoRedo::Stack *m_undo_redo_stack_active = &m_undo_redo_stack_main;
|
|
int m_prevent_snapshots = 0; /* Used for avoid of excess "snapshoting".
|
|
* Like for "delete selected" or "set numbers of copies"
|
|
* we should call tack_snapshot just ones
|
|
* instead of calls for each action separately
|
|
* */
|
|
std::string m_last_fff_printer_profile_name;
|
|
std::string m_last_sla_printer_profile_name;
|
|
|
|
// vector of all warnings generated by last slicing
|
|
std::vector<std::pair<Slic3r::PrintStateBase::Warning, size_t>> current_warnings;
|
|
bool show_warning_dialog { false };
|
|
|
|
};
|
|
|
|
const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|zip[.]amf|3mf|prusa)", std::regex::icase);
|
|
const std::regex Plater::priv::pattern_3mf(".*3mf", std::regex::icase);
|
|
const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::icase);
|
|
const std::regex Plater::priv::pattern_any_amf(".*[.](amf|amf[.]xml|zip[.]amf)", std::regex::icase);
|
|
const std::regex Plater::priv::pattern_prusa(".*prusa", std::regex::icase);
|
|
const std::regex Plater::priv::pattern_zip(".*zip", std::regex::icase);
|
|
|
|
Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
|
: q(q)
|
|
, main_frame(main_frame)
|
|
, config(Slic3r::DynamicPrintConfig::new_from_defaults_keys({
|
|
"bed_shape", "bed_custom_texture", "bed_custom_model", "complete_objects", "duplicate_distance", "extruder_clearance_radius", "skirts", "skirt_distance",
|
|
"brim_width", "brim_separation", "brim_type", "variable_layer_height", "nozzle_diameter", "single_extruder_multi_material",
|
|
"wipe_tower", "wipe_tower_x", "wipe_tower_y", "wipe_tower_width", "wipe_tower_rotation_angle", "wipe_tower_brim_width", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extruder",
|
|
"extruder_colour", "filament_colour", "material_colour", "max_print_height", "printer_model", "printer_notes", "printer_technology",
|
|
// These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor.
|
|
"layer_height", "first_layer_height", "min_layer_height", "max_layer_height",
|
|
"brim_width", "perimeters", "perimeter_extruder", "fill_density", "infill_extruder", "top_solid_layers",
|
|
"support_material", "support_material_extruder", "support_material_interface_extruder",
|
|
"support_material_contact_distance", "support_material_bottom_contact_distance", "raft_layers"
|
|
}))
|
|
, sidebar(new Sidebar(q))
|
|
, notification_manager(std::make_unique<NotificationManager>(q))
|
|
, user_account(std::make_unique<UserAccount>(q, wxGetApp().app_config, wxGetApp().get_instance_hash_string()))
|
|
, m_worker{q, std::make_unique<NotificationProgressIndicator>(notification_manager.get()), "ui_worker"}
|
|
, m_sla_import_dlg{new SLAImportDialog{q}}
|
|
, delayed_scene_refresh(false)
|
|
, view_toolbar(GLToolbar::Radio, "View")
|
|
, collapse_toolbar(GLToolbar::Normal, "Collapse")
|
|
, m_project_filename(wxEmptyString)
|
|
{
|
|
background_process.set_fff_print(&fff_print);
|
|
background_process.set_sla_print(&sla_print);
|
|
background_process.set_gcode_result(&gcode_result);
|
|
background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, Camera::EType::Ortho); });
|
|
background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED);
|
|
background_process.set_finished_event(EVT_PROCESS_COMPLETED);
|
|
background_process.set_export_began_event(EVT_EXPORT_BEGAN);
|
|
// Default printer technology for default config.
|
|
background_process.select_technology(this->printer_technology);
|
|
// Register progress callback from the Print class to the Plater.
|
|
|
|
auto statuscb = [this](const Slic3r::PrintBase::SlicingStatus &status) {
|
|
wxQueueEvent(this->q, new Slic3r::SlicingStatusEvent(EVT_SLICING_UPDATE, 0, status));
|
|
};
|
|
fff_print.set_status_callback(statuscb);
|
|
sla_print.set_status_callback(statuscb);
|
|
this->q->Bind(EVT_SLICING_UPDATE, &priv::on_slicing_update, this);
|
|
|
|
view3D = new View3D(q, bed, &model, config, &background_process);
|
|
preview = new Preview(q, bed, &model, config, &background_process, &gcode_result, [this]() { schedule_background_process(); });
|
|
|
|
#ifdef __APPLE__
|
|
// set default view_toolbar icons size equal to GLGizmosManager::Default_Icons_Size
|
|
view_toolbar.set_icons_size(GLGizmosManager::Default_Icons_Size);
|
|
#endif // __APPLE__
|
|
|
|
panels.push_back(view3D);
|
|
panels.push_back(preview);
|
|
|
|
this->background_process_timer.SetOwner(this->q, 0);
|
|
this->q->Bind(wxEVT_TIMER, [this](wxTimerEvent &evt)
|
|
{
|
|
if (!this->suppressed_backround_processing_update)
|
|
this->update_restart_background_process(false, false);
|
|
});
|
|
|
|
update();
|
|
|
|
auto *hsizer = new wxBoxSizer(wxHORIZONTAL);
|
|
panel_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0);
|
|
panel_sizer->Add(preview, 1, wxEXPAND | wxALL, 0);
|
|
hsizer->Add(panel_sizer, 1, wxEXPAND | wxALL, 0);
|
|
hsizer->Add(sidebar, 0, wxEXPAND | wxLEFT | wxRIGHT, 0);
|
|
q->SetSizer(hsizer);
|
|
|
|
menus.init(q);
|
|
|
|
// Events:
|
|
|
|
if (wxGetApp().is_editor()) {
|
|
// Preset change event
|
|
this->q->Bind(EVT_OBJ_LIST_OBJECT_SELECT, [this](wxEvent&) { priv::selection_changed(); });
|
|
this->q->Bind(EVT_SCHEDULE_BACKGROUND_PROCESS, [this](SimpleEvent&) { this->schedule_background_process(); });
|
|
}
|
|
|
|
wxGLCanvas* view3D_canvas = view3D->get_wxglcanvas();
|
|
|
|
if (wxGetApp().is_editor()) {
|
|
// 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&) { this->q->arrange(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
|
|
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(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_FORCE_UPDATE, [this](SimpleEvent&) { update(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_WIPETOWER_MOVED, &priv::on_wipetower_moved, this);
|
|
view3D_canvas->Bind(EVT_GLCANVAS_WIPETOWER_ROTATED, &priv::on_wipetower_rotated, this);
|
|
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_ROTATED, [this](SimpleEvent&) { update(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_RESET_SKEW, [this](SimpleEvent&) { update(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_SCALED, [this](SimpleEvent&) { update(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MIRRORED, [this](SimpleEvent&) { update(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event<bool>& evt) { this->sidebar->enable_buttons(evt.data); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_GEOMETRY, &priv::on_update_geometry, this);
|
|
view3D_canvas->Bind(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED, &priv::on_3dcanvas_mouse_dragging_started, this);
|
|
view3D_canvas->Bind(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, &priv::on_3dcanvas_mouse_dragging_finished, this);
|
|
view3D_canvas->Bind(EVT_GLCANVAS_TAB, [this](SimpleEvent&) { select_next_view_3D(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_RESETGIZMOS, [this](SimpleEvent&) { reset_all_gizmos(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_UNDO, [this](SimpleEvent&) { this->undo(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_REDO, [this](SimpleEvent&) { this->redo(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, [this](SimpleEvent&) { this->view3D->get_canvas3d()->reset_layer_height_profile(); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, [this](Event<float>& evt) { this->view3D->get_canvas3d()->adaptive_layer_height_profile(evt.data); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, [this](HeightProfileSmoothEvent& evt) { this->view3D->get_canvas3d()->smooth_layer_height_profile(evt.data); });
|
|
view3D_canvas->Bind(EVT_GLCANVAS_RELOAD_FROM_DISK, [this](SimpleEvent&) { this->reload_all_from_disk(); });
|
|
|
|
// 3DScene/Toolbar:
|
|
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, [this](SimpleEvent&) { delete_all_objects_from_model(); });
|
|
// view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [q](SimpleEvent&) { q->reset_with_confirm(); });
|
|
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(); });
|
|
view3D_canvas->Bind(EVT_GLTOOLBAR_FEWER, [q](SimpleEvent&) { q->decrease_instances(); });
|
|
view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_OBJECTS, &priv::on_action_split_objects, this);
|
|
view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_VOLUMES, &priv::on_action_split_volumes, this);
|
|
view3D_canvas->Bind(EVT_GLTOOLBAR_LAYERSEDITING, &priv::on_action_layersediting, this);
|
|
}
|
|
view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); });
|
|
|
|
// Preview events:
|
|
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
|
|
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); });
|
|
if (wxGetApp().is_editor()) {
|
|
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_TAB, [this](SimpleEvent&) { select_next_view_3D(); });
|
|
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); });
|
|
}
|
|
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_JUMP_TO, [this](wxKeyEvent& evt) { preview->jump_layers_slider(evt); });
|
|
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_MOVE_SLIDERS, [this](wxKeyEvent& evt) {
|
|
preview->move_layers_slider(evt);
|
|
preview->move_moves_slider(evt);
|
|
});
|
|
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_EDIT_COLOR_CHANGE, [this](wxKeyEvent& evt) { preview->edit_layers_slider(evt); });
|
|
if (wxGetApp().is_gcode_viewer())
|
|
preview->Bind(EVT_GLCANVAS_RELOAD_FROM_DISK, [this](SimpleEvent&) { this->q->reload_gcode_from_disk(); });
|
|
|
|
if (wxGetApp().is_editor()) {
|
|
q->Bind(EVT_SLICING_COMPLETED, &priv::on_slicing_completed, this);
|
|
q->Bind(EVT_PROCESS_COMPLETED, &priv::on_process_completed, this);
|
|
q->Bind(EVT_EXPORT_BEGAN, &priv::on_export_began, this);
|
|
q->Bind(EVT_GLVIEWTOOLBAR_3D, [q](SimpleEvent&) { q->select_view_3D("3D"); });
|
|
q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { q->select_view_3D("Preview"); });
|
|
}
|
|
|
|
// Drop target:
|
|
main_frame->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); // if my understanding is right, wxWindow takes the owenership
|
|
q->Layout();
|
|
|
|
set_current_panel(wxGetApp().is_editor() ? static_cast<wxPanel*>(view3D) : static_cast<wxPanel*>(preview));
|
|
if (wxGetApp().is_gcode_viewer())
|
|
preview->hide_layers_slider();
|
|
|
|
// updates camera type from .ini file
|
|
camera.enable_update_config_on_type_change(true);
|
|
camera.set_type(wxGetApp().app_config->get("use_perspective_camera"));
|
|
|
|
// Load the 3DConnexion device database.
|
|
mouse3d_controller.load_config(*wxGetApp().app_config);
|
|
// Start the background thread to detect and connect to a HID device (Windows and Linux).
|
|
// Connect to a 3DConnextion driver (OSX).
|
|
mouse3d_controller.init();
|
|
#ifdef _WIN32
|
|
// Register an USB HID (Human Interface Device) attach event. evt contains Win32 path to the USB device containing VID, PID and other info.
|
|
// This event wakes up the Mouse3DController's background thread to enumerate HID devices, if the VID of the callback event
|
|
// is one of the 3D Mouse vendors (3DConnexion or Logitech).
|
|
this->q->Bind(EVT_HID_DEVICE_ATTACHED, [this](HIDDeviceAttachedEvent &evt) {
|
|
mouse3d_controller.device_attached(evt.data);
|
|
});
|
|
this->q->Bind(EVT_HID_DEVICE_DETACHED, [this](HIDDeviceAttachedEvent& evt) {
|
|
mouse3d_controller.device_detached(evt.data);
|
|
});
|
|
#endif /* _WIN32 */
|
|
|
|
//notification_manager = new NotificationManager(this->q);
|
|
|
|
if (wxGetApp().is_editor()) {
|
|
this->q->Bind(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED, [this](EjectDriveNotificationClickedEvent&) { this->q->eject_drive(); });
|
|
this->q->Bind(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, [this](ExportGcodeNotificationClickedEvent&) { this->q->export_gcode(true); });
|
|
this->q->Bind(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, [](PresetUpdateAvailableClickedEvent&) { wxGetApp().get_preset_updater()->on_update_notification_confirm(); });
|
|
this->q->Bind(EVT_REMOVABLE_DRIVE_EJECTED, [this, q](RemovableDriveEjectEvent &evt) {
|
|
if (evt.data.second) {
|
|
q->show_action_buttons();
|
|
notification_manager->close_notification_of_type(NotificationType::ExportFinished);
|
|
notification_manager->push_notification(NotificationType::CustomNotification,
|
|
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
|
format(_L("Successfully unmounted. The device %s(%s) can now be safely removed from the computer."), evt.data.first.name, evt.data.first.path)
|
|
);
|
|
} else {
|
|
notification_manager->close_notification_of_type(NotificationType::ExportFinished);
|
|
notification_manager->push_notification(NotificationType::CustomNotification,
|
|
NotificationManager::NotificationLevel::ErrorNotificationLevel,
|
|
format(_L("Ejecting of device %s(%s) has failed."), evt.data.first.name, evt.data.first.path)
|
|
);
|
|
}
|
|
});
|
|
this->q->Bind(EVT_REMOVABLE_DRIVES_CHANGED, [this, q](RemovableDrivesChangedEvent &) {
|
|
q->show_action_buttons();
|
|
// Close notification ExportingFinished but only if last export was to removable
|
|
notification_manager->device_ejected();
|
|
});
|
|
|
|
this->q->Bind(EVT_REMOVABLE_DRIVE_ADDED, [this](wxCommandEvent& evt) {
|
|
if (!fs::exists(fs::path(evt.GetString().utf8_string()) / "prusa_printer_settings.ini"))
|
|
return;
|
|
if (evt.GetInt() == 0) { // not at startup, show dialog
|
|
wxGetApp().open_wifi_config_dialog(false, evt.GetString());
|
|
} else { // at startup, show only notification
|
|
notification_manager->push_notification(NotificationType::WifiConfigFileDetected
|
|
, NotificationManager::NotificationLevel::ImportantNotificationLevel
|
|
// TRN Text of notification when Slicer starts and usb stick with printer settings ini file is present
|
|
, _u8L("Printer configuration file detected on removable media.")
|
|
// TRN Text of hypertext of notification when Slicer starts and usb stick with printer settings ini file is present
|
|
, _u8L("Write Wi-Fi credentials."), [evt/*, CONFIG_FILE_NAME*/](wxEvtHandler* evt_hndlr){
|
|
wxGetApp().open_wifi_config_dialog(true, evt.GetString());
|
|
return true;});
|
|
}
|
|
|
|
});
|
|
|
|
// Start the background thread and register this window as a target for update events.
|
|
wxGetApp().removable_drive_manager()->init(this->q);
|
|
#ifdef _WIN32
|
|
// Trigger enumeration of removable media on Win32 notification.
|
|
this->q->Bind(EVT_VOLUME_ATTACHED, [this](VolumeAttachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); });
|
|
this->q->Bind(EVT_VOLUME_DETACHED, [this](VolumeDetachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); });
|
|
#endif /* _WIN32 */
|
|
}
|
|
|
|
// Initialize the Undo / Redo stack with a first snapshot.
|
|
this->take_snapshot(_L("New Project"), UndoRedo::SnapshotType::ProjectSeparator);
|
|
// Reset the "dirty project" flag.
|
|
m_undo_redo_stack_main.mark_current_as_saved();
|
|
dirty_state.update_from_undo_redo_stack(false);
|
|
|
|
this->q->Bind(EVT_LOAD_MODEL_OTHER_INSTANCE, [this](LoadFromOtherInstanceEvent& evt) {
|
|
BOOST_LOG_TRIVIAL(trace) << "Received load from other instance event.";
|
|
wxArrayString input_files;
|
|
for (size_t i = 0; i < evt.data.size(); ++i) {
|
|
input_files.push_back(from_u8(evt.data[i].string()));
|
|
}
|
|
wxGetApp().mainframe->Raise();
|
|
this->q->load_files(input_files);
|
|
});
|
|
|
|
this->q->Bind(EVT_START_DOWNLOAD_OTHER_INSTANCE, [](StartDownloadOtherInstanceEvent& evt) {
|
|
BOOST_LOG_TRIVIAL(trace) << "Received url from other instance event.";
|
|
wxGetApp().mainframe->Raise();
|
|
for (size_t i = 0; i < evt.data.size(); ++i) {
|
|
wxGetApp().start_download(evt.data[i]);
|
|
}
|
|
|
|
});
|
|
this->q->Bind(EVT_LOGIN_OTHER_INSTANCE, [this](LoginOtherInstanceEvent& evt) {
|
|
BOOST_LOG_TRIVIAL(trace) << "Received login from other instance event.";
|
|
user_account->on_login_code_recieved(evt.data);
|
|
});
|
|
|
|
this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent &) {
|
|
bring_instance_forward();
|
|
});
|
|
|
|
this->q->Bind(EVT_OPEN_PRUSAAUTH, [](OpenPrusaAuthEvent& evt) {
|
|
BOOST_LOG_TRIVIAL(info) << "open browser: " << evt.data;
|
|
// first register url to be sure to get the code back
|
|
//auto downloader_worker = new DownloaderUtils::Worker(nullptr);
|
|
DownloaderUtils::Worker::perform_register(wxGetApp().app_config->get("url_downloader_dest"));
|
|
#ifdef __linux__
|
|
if (DownloaderUtils::Worker::perform_registration_linux)
|
|
DesktopIntegrationDialog::perform_downloader_desktop_integration();
|
|
#endif // __linux__
|
|
// than open url
|
|
wxGetApp().open_login_browser_with_dialog(evt.data);
|
|
});
|
|
|
|
this->q->Bind(EVT_UA_LOGGEDOUT, [this](UserAccountSuccessEvent& evt) {
|
|
user_account->clear();
|
|
std::string text = _u8L("Logged out from Prusa Account.");
|
|
this->notification_manager->close_notification_of_type(NotificationType::UserAccountID);
|
|
this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text);
|
|
this->main_frame->remove_connect_webview_tab();
|
|
this->main_frame->refresh_account_menu(true);
|
|
// Update sidebar printer status
|
|
sidebar->update_printer_presets_combobox();
|
|
wxGetApp().update_login_dialog();
|
|
this->show_action_buttons(this->ready_to_slice);
|
|
});
|
|
|
|
this->q->Bind(EVT_UA_ID_USER_SUCCESS, [this](UserAccountSuccessEvent& evt) {
|
|
std::string username;
|
|
if (user_account->on_user_id_success(evt.data, username)) {
|
|
// login notification
|
|
std::string text = format(_u8L("Logged to Prusa Account as %1%."), username);
|
|
this->notification_manager->close_notification_of_type(NotificationType::UserAccountID);
|
|
this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text);
|
|
// show connect tab
|
|
this->main_frame->add_connect_webview_tab();
|
|
// Update User name in TopBar
|
|
this->main_frame->refresh_account_menu();
|
|
wxGetApp().update_login_dialog();
|
|
this->show_action_buttons(this->ready_to_slice);
|
|
} else {
|
|
// data were corrupt and username was not retrieved
|
|
// procced as if EVT_UA_RESET was recieved
|
|
BOOST_LOG_TRIVIAL(error) << "Reseting Prusa Account communication. Recieved data were corrupt.";
|
|
user_account->clear();
|
|
this->notification_manager->close_notification_of_type(NotificationType::UserAccountID);
|
|
this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("Failed to connect to Prusa Account."));
|
|
this->main_frame->remove_connect_webview_tab();
|
|
// Update User name in TopBar
|
|
this->main_frame->refresh_account_menu(true);
|
|
// Update sidebar printer status
|
|
sidebar->update_printer_presets_combobox();
|
|
}
|
|
|
|
});
|
|
this->q->Bind(EVT_UA_RESET, [this](UserAccountFailEvent& evt) {
|
|
BOOST_LOG_TRIVIAL(error) << "Reseting Prusa Account communication. Error message: " << evt.data;
|
|
user_account->clear();
|
|
this->notification_manager->close_notification_of_type(NotificationType::UserAccountID);
|
|
this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("Failed to connect to Prusa Account."));
|
|
this->main_frame->remove_connect_webview_tab();
|
|
// Update User name in TopBar
|
|
this->main_frame->refresh_account_menu(true);
|
|
// Update sidebar printer status
|
|
sidebar->update_printer_presets_combobox();
|
|
});
|
|
this->q->Bind(EVT_UA_FAIL, [this](UserAccountFailEvent& evt) {
|
|
BOOST_LOG_TRIVIAL(error) << "Failed communication with Prusa Account: " << evt.data;
|
|
user_account->on_communication_fail();
|
|
});
|
|
#if 0
|
|
// for debug purposes only
|
|
this->q->Bind(EVT_UA_SUCCESS, [this](UserAccountSuccessEvent& evt) {
|
|
this->notification_manager->close_notification_of_type(NotificationType::UserAccountID);
|
|
this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::ImportantNotificationLevel, evt.data);
|
|
});
|
|
this->q->Bind(EVT_UA_CONNECT_USER_DATA_SUCCESS, [this](UserAccountSuccessEvent& evt) {
|
|
BOOST_LOG_TRIVIAL(error) << evt.data;
|
|
user_account->on_connect_user_data_success(evt.data);
|
|
});
|
|
#endif // 0
|
|
this->q->Bind(EVT_UA_PRUSACONNECT_PRINTERS_SUCCESS, [this](UserAccountSuccessEvent& evt) {
|
|
std::string text;
|
|
bool printers_changed = false;
|
|
if (user_account->on_connect_printers_success(evt.data, wxGetApp().app_config, printers_changed)) {
|
|
if (printers_changed) {
|
|
sidebar->update_printer_presets_combobox();
|
|
}
|
|
} else {
|
|
// message was corrupt, procceed like EVT_UA_FAIL
|
|
user_account->on_communication_fail();
|
|
}
|
|
});
|
|
this->q->Bind(EVT_UA_AVATAR_SUCCESS, [this](UserAccountSuccessEvent& evt) {
|
|
boost::filesystem::path path = user_account->get_avatar_path(true);
|
|
FILE* file;
|
|
file = fopen(path.string().c_str(), "wb");
|
|
fwrite(evt.data.c_str(), 1, evt.data.size(), file);
|
|
fclose(file);
|
|
this->main_frame->refresh_account_menu(true);
|
|
wxGetApp().update_login_dialog();
|
|
});
|
|
|
|
wxGetApp().other_instance_message_handler()->init(this->q);
|
|
|
|
// collapse sidebar according to saved value
|
|
if (wxGetApp().is_editor()) {
|
|
bool is_collapsed = get_config_bool("collapsed_sidebar");
|
|
sidebar->collapse(is_collapsed);
|
|
}
|
|
}
|
|
|
|
Plater::priv::~priv()
|
|
{
|
|
if (config != nullptr)
|
|
delete config;
|
|
// Saves the database of visited (already shown) hints into hints.ini.
|
|
notification_manager->deactivate_loaded_hints();
|
|
}
|
|
|
|
void Plater::priv::update(unsigned int flags)
|
|
{
|
|
// the following line, when enabled, causes flickering on NVIDIA graphics cards
|
|
// wxWindowUpdateLocker freeze_guard(q);
|
|
if (get_config_bool("autocenter"))
|
|
model.center_instances_around_point(this->bed.build_volume().bed_center());
|
|
|
|
unsigned int update_status = 0;
|
|
const bool force_background_processing_restart = this->printer_technology == ptSLA || (flags & (unsigned int)UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE);
|
|
if (force_background_processing_restart)
|
|
// Update the SLAPrint from the current Model, so that the reload_scene()
|
|
// pulls the correct data.
|
|
update_status = this->update_background_process(false, flags & (unsigned int)UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE);
|
|
this->view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH);
|
|
this->preview->reload_print();
|
|
if (force_background_processing_restart)
|
|
this->restart_background_process(update_status);
|
|
else
|
|
this->schedule_background_process();
|
|
|
|
if (get_config_bool("autocenter") && this->sidebar->obj_manipul()->IsShown())
|
|
this->sidebar->obj_manipul()->UpdateAndShow(true);
|
|
}
|
|
|
|
void Plater::priv::select_view(const std::string& direction)
|
|
{
|
|
if (current_panel == view3D)
|
|
view3D->select_view(direction);
|
|
else if (current_panel == preview)
|
|
preview->select_view(direction);
|
|
}
|
|
|
|
void Plater::priv::apply_free_camera_correction(bool apply/* = true*/)
|
|
{
|
|
camera.set_type(wxGetApp().app_config->get("use_perspective_camera"));
|
|
if (apply && !wxGetApp().app_config->get_bool("use_free_camera"))
|
|
camera.recover_from_free_camera();
|
|
}
|
|
|
|
void Plater::priv::select_view_3D(const std::string& name)
|
|
{
|
|
if (name == "3D")
|
|
set_current_panel(view3D);
|
|
else if (name == "Preview")
|
|
set_current_panel(preview);
|
|
|
|
apply_free_camera_correction(false);
|
|
}
|
|
|
|
void Plater::priv::select_next_view_3D()
|
|
{
|
|
if (current_panel == view3D)
|
|
set_current_panel(preview);
|
|
else if (current_panel == preview)
|
|
set_current_panel(view3D);
|
|
}
|
|
|
|
void Plater::priv::collapse_sidebar(bool collapse)
|
|
{
|
|
sidebar->collapse(collapse);
|
|
|
|
// Now update the tooltip in the toolbar.
|
|
std::string new_tooltip = collapse
|
|
? _u8L("Expand sidebar")
|
|
: _u8L("Collapse sidebar");
|
|
new_tooltip += " [Shift+Tab]";
|
|
int id = collapse_toolbar.get_item_id("collapse_sidebar");
|
|
collapse_toolbar.set_tooltip(id, new_tooltip);
|
|
|
|
notification_manager->set_sidebar_collapsed(collapse);
|
|
}
|
|
|
|
|
|
void Plater::priv::reset_all_gizmos()
|
|
{
|
|
view3D->get_canvas3d()->reset_all_gizmos();
|
|
}
|
|
|
|
// Called after the Preferences dialog is closed and the program settings are saved.
|
|
// Update the UI based on the current preferences.
|
|
void Plater::priv::update_ui_from_settings()
|
|
{
|
|
apply_free_camera_correction();
|
|
|
|
view3D->get_canvas3d()->update_ui_from_settings();
|
|
preview->get_canvas3d()->update_ui_from_settings();
|
|
|
|
sidebar->update_ui_from_settings();
|
|
// update Cut gizmo, if it's open
|
|
q->canvas3D()->update_gizmos_on_off_state();
|
|
q->set_current_canvas_as_dirty();
|
|
q->get_current_canvas3D()->request_extra_frame();
|
|
}
|
|
|
|
// Called after the print technology was changed.
|
|
// Update the tooltips for "Switch to Settings" button in maintoolbar
|
|
void Plater::priv::update_main_toolbar_tooltips()
|
|
{
|
|
view3D->get_canvas3d()->update_tooltip_for_settings_item_in_main_toolbar();
|
|
}
|
|
|
|
bool Plater::priv::get_config_bool(const std::string &key) const
|
|
{
|
|
return wxGetApp().app_config->get_bool(key);
|
|
}
|
|
|
|
void Plater::notify_about_installed_presets()
|
|
{
|
|
const auto& names = wxGetApp().preset_bundle->tmp_installed_presets;
|
|
// show notification about temporarily installed presets
|
|
if (!names.empty()) {
|
|
std::string notif_text = into_u8(_L_PLURAL("The preset below was temporarily installed on the active instance of PrusaSlicer",
|
|
"The presets below were temporarily installed on the active instance of PrusaSlicer", names.size())) + ":";
|
|
for (const std::string& name : names)
|
|
notif_text += "\n - " + name;
|
|
get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
|
NotificationManager::NotificationLevel::PrintInfoNotificationLevel, notif_text);
|
|
}
|
|
}
|
|
|
|
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files, bool load_model, bool load_config, bool imperial_units/* = false*/)
|
|
{
|
|
if (input_files.empty()) { return std::vector<size_t>(); }
|
|
|
|
auto *nozzle_dmrs = config->opt<ConfigOptionFloats>("nozzle_diameter");
|
|
|
|
PlaterAfterLoadAutoArrange plater_after_load_auto_arrange;
|
|
|
|
bool one_by_one = input_files.size() == 1 || printer_technology == ptSLA || nozzle_dmrs->values.size() <= 1;
|
|
if (! one_by_one) {
|
|
for (const auto &path : input_files) {
|
|
if (std::regex_match(path.string(), pattern_bundle)) {
|
|
one_by_one = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto loading = _L("Loading") + dots;
|
|
|
|
// The situation with wxProgressDialog is quite interesting here.
|
|
// On Linux (only), there are issues when FDM/SLA is switched during project file loading (disabling of controls,
|
|
// see a comment below). This can be bypassed by creating the wxProgressDialog on heap and destroying it
|
|
// when loading a project file. However, creating the dialog on heap causes issues on macOS, where it does not
|
|
// appear at all. Therefore, we create the dialog on stack on Win and macOS, and on heap on Linux, which
|
|
// is the only system that needed the workarounds in the first place.
|
|
#ifdef __linux__
|
|
auto progress_dlg = new wxProgressDialog(loading, "", 100, find_toplevel_parent(q), wxPD_APP_MODAL | wxPD_AUTO_HIDE);
|
|
Slic3r::ScopeGuard([&progress_dlg](){ if (progress_dlg) progress_dlg->Destroy(); progress_dlg = nullptr; });
|
|
#else
|
|
wxProgressDialog progress_dlg_stack(loading, "", 100, find_toplevel_parent(q), wxPD_APP_MODAL | wxPD_AUTO_HIDE);
|
|
wxProgressDialog* progress_dlg = &progress_dlg_stack;
|
|
#endif
|
|
|
|
|
|
wxBusyCursor busy;
|
|
|
|
auto *new_model = (!load_model || one_by_one) ? nullptr : new Slic3r::Model();
|
|
std::vector<size_t> obj_idxs;
|
|
|
|
int answer_convert_from_meters = wxOK_DEFAULT;
|
|
int answer_convert_from_imperial_units = wxOK_DEFAULT;
|
|
int answer_consider_as_multi_part_objects = wxOK_DEFAULT;
|
|
|
|
bool in_temp = false;
|
|
const fs::path temp_path = wxStandardPaths::Get().GetTempDir().utf8_str().data();
|
|
|
|
size_t input_files_size = input_files.size();
|
|
for (size_t i = 0; i < input_files_size; ++i) {
|
|
#ifdef _WIN32
|
|
auto path = input_files[i];
|
|
// On Windows, we swap slashes to back slashes, see GH #6803 as read_from_file() does not understand slashes on Windows thus it assignes full path to names of loaded objects.
|
|
path.make_preferred();
|
|
#else // _WIN32
|
|
// Don't make a copy on Posix. Slash is a path separator, back slashes are not accepted as a substitute.
|
|
const auto &path = input_files[i];
|
|
#endif // _WIN32
|
|
in_temp = (path.parent_path() == temp_path);
|
|
const auto filename = path.filename();
|
|
if (progress_dlg) {
|
|
progress_dlg->Update(static_cast<int>(100.0f * static_cast<float>(i) / static_cast<float>(input_files.size())), _L("Loading file") + ": " + from_path(filename));
|
|
progress_dlg->Fit();
|
|
}
|
|
|
|
const bool type_3mf = std::regex_match(path.string(), pattern_3mf) || std::regex_match(path.string(), pattern_zip);
|
|
const bool type_zip_amf = !type_3mf && std::regex_match(path.string(), pattern_zip_amf);
|
|
const bool type_any_amf = !type_3mf && std::regex_match(path.string(), pattern_any_amf);
|
|
const bool type_prusa = std::regex_match(path.string(), pattern_prusa);
|
|
|
|
Slic3r::Model model;
|
|
bool is_project_file = type_prusa;
|
|
try {
|
|
if (type_3mf || type_zip_amf) {
|
|
#ifdef __linux__
|
|
// On Linux Constructor of the ProgressDialog calls DisableOtherWindows() function which causes a disabling of all children of the find_toplevel_parent(q)
|
|
// And a destructor of the ProgressDialog calls ReenableOtherWindows() function which revert previously disabled children.
|
|
// But if printer technology will be changes during project loading,
|
|
// then related SLA Print and Materials Settings or FFF Print and Filaments Settings will be unparent from the wxNoteBook
|
|
// and that is why they will never be enabled after destruction of the ProgressDialog.
|
|
// So, distroy progress_gialog if we are loading project file
|
|
if (input_files_size == 1 && progress_dlg) {
|
|
progress_dlg->Destroy();
|
|
progress_dlg = nullptr;
|
|
}
|
|
#endif
|
|
DynamicPrintConfig config;
|
|
PrinterTechnology loaded_printer_technology {ptFFF};
|
|
{
|
|
DynamicPrintConfig config_loaded;
|
|
ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::Enable };
|
|
model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, &config_substitutions, only_if(load_config, Model::LoadAttribute::CheckVersion));
|
|
if (load_config && !config_loaded.empty()) {
|
|
// Based on the printer technology field found in the loaded config, select the base for the config,
|
|
loaded_printer_technology = Preset::printer_technology(config_loaded);
|
|
|
|
// We can't to load SLA project if there is at least one multi-part object on the bed
|
|
if (loaded_printer_technology == ptSLA) {
|
|
const ModelObjectPtrs& objects = q->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 obj_idxs;
|
|
}
|
|
}
|
|
|
|
config.apply(loaded_printer_technology == ptFFF ?
|
|
static_cast<const ConfigBase&>(FullPrintConfig::defaults()) :
|
|
static_cast<const ConfigBase&>(SLAFullPrintConfig::defaults()));
|
|
// Set all the nullable values in defaults to nils.
|
|
config.null_nullables();
|
|
// and place the loaded config over the base.
|
|
config += std::move(config_loaded);
|
|
}
|
|
if (! config_substitutions.empty())
|
|
show_substitutions_info(config_substitutions.substitutions, filename.string());
|
|
|
|
this->model.custom_gcode_per_print_z = model.custom_gcode_per_print_z;
|
|
}
|
|
|
|
if (load_config) {
|
|
if (!config.empty()) {
|
|
const auto* post_process = config.opt<ConfigOptionStrings>("post_process");
|
|
if (post_process != nullptr && !post_process->values.empty()) {
|
|
// TRN The placeholder is either "3MF" or "AMF"
|
|
wxString msg = GUI::format_wxstr(_L("The selected %1% file contains a post-processing script.\n"
|
|
"Please review the script carefully before exporting G-code."), type_3mf ? "3MF" : "AMF" );
|
|
std::string text;
|
|
for (const std::string& s : post_process->values)
|
|
text += s;
|
|
|
|
InfoDialog msg_dlg(nullptr, msg, from_u8(text), true, wxOK | wxICON_WARNING);
|
|
msg_dlg.set_caption(wxString(SLIC3R_APP_NAME " - ") + _L("Attention!"));
|
|
msg_dlg.ShowModal();
|
|
}
|
|
|
|
Preset::normalize(config);
|
|
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
|
preset_bundle->load_config_model(filename.string(), std::move(config));
|
|
q->notify_about_installed_presets();
|
|
|
|
if (loaded_printer_technology == ptFFF)
|
|
CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, &preset_bundle->project_config);
|
|
|
|
// For exporting from the amf/3mf we shouldn't check printer_presets for the containing information about "Print Host upload"
|
|
wxGetApp().load_current_presets(false);
|
|
// Update filament colors for the MM-printer profile in the full config
|
|
// to avoid black (default) colors for Extruders in the ObjectList,
|
|
// when for extruder colors are used filament colors
|
|
q->update_filament_colors_in_full_config();
|
|
is_project_file = true;
|
|
}
|
|
if (!in_temp)
|
|
wxGetApp().app_config->update_config_dir(path.parent_path().string());
|
|
}
|
|
}
|
|
else {
|
|
model = Slic3r::Model::read_from_file(path.string(), nullptr, nullptr, only_if(load_config, Model::LoadAttribute::CheckVersion));
|
|
for (auto obj : model.objects)
|
|
if (obj->name.empty())
|
|
obj->name = fs::path(obj->input_file).filename().string();
|
|
}
|
|
} catch (const ConfigurationError &e) {
|
|
std::string message = GUI::format(_L("Failed loading file \"%1%\" due to an invalid configuration."), filename.string()) + "\n\n" + e.what();
|
|
GUI::show_error(q, message);
|
|
continue;
|
|
} catch (const std::exception &e) {
|
|
GUI::show_error(q, e.what());
|
|
continue;
|
|
}
|
|
|
|
if (load_model) {
|
|
// The model should now be initialized
|
|
|
|
auto convert_from_imperial_units = [](Model& model, bool only_small_volumes) {
|
|
model.convert_from_imperial_units(only_small_volumes);
|
|
// wxGetApp().app_config->set("use_inches", "1");
|
|
// wxGetApp().sidebar().update_ui_from_settings();
|
|
};
|
|
|
|
if (!is_project_file) {
|
|
if (int deleted_objects = model.removed_objects_with_zero_volume(); deleted_objects > 0) {
|
|
MessageDialog(q, format_wxstr(_L_PLURAL(
|
|
"Object size from file %s appears to be zero.\n"
|
|
"This object has been removed from the model",
|
|
"Objects size from file %s appears to be zero.\n"
|
|
"These objects have been removed from the model", deleted_objects), from_path(filename)) + "\n",
|
|
_L("The size of the object is zero"), wxICON_INFORMATION | wxOK).ShowModal();
|
|
}
|
|
if (imperial_units)
|
|
// Convert even if the object is big.
|
|
convert_from_imperial_units(model, false);
|
|
else if (!type_3mf && model.looks_like_saved_in_meters()) {
|
|
auto convert_model_if = [](Model& model, bool condition) {
|
|
if (condition)
|
|
//FIXME up-scale only the small parts?
|
|
model.convert_from_meters(true);
|
|
};
|
|
if (answer_convert_from_meters == wxOK_DEFAULT) {
|
|
RichMessageDialog dlg(q, format_wxstr(_L_PLURAL(
|
|
"The dimensions of the object from file %s seem to be defined in meters.\n"
|
|
"The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of the object?",
|
|
"The dimensions of some objects from file %s seem to be defined in meters.\n"
|
|
"The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of these objects?", model.objects.size()), from_path(filename)) + "\n",
|
|
_L("The object is too small"), wxICON_QUESTION | wxYES_NO);
|
|
dlg.ShowCheckBox(_L("Apply to all the remaining small objects being loaded."));
|
|
int answer = dlg.ShowModal();
|
|
if (dlg.IsCheckBoxChecked())
|
|
answer_convert_from_meters = answer;
|
|
else
|
|
convert_model_if(model, answer == wxID_YES);
|
|
}
|
|
convert_model_if(model, answer_convert_from_meters == wxID_YES);
|
|
}
|
|
else if (!type_3mf && model.looks_like_imperial_units()) {
|
|
auto convert_model_if = [convert_from_imperial_units](Model& model, bool condition) {
|
|
if (condition)
|
|
//FIXME up-scale only the small parts?
|
|
convert_from_imperial_units(model, true);
|
|
};
|
|
if (answer_convert_from_imperial_units == wxOK_DEFAULT) {
|
|
RichMessageDialog dlg(q, format_wxstr(_L_PLURAL(
|
|
"The dimensions of the object from file %s seem to be defined in inches.\n"
|
|
"The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of the object?",
|
|
"The dimensions of some objects from file %s seem to be defined in inches.\n"
|
|
"The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of these objects?", model.objects.size()), from_path(filename)) + "\n",
|
|
_L("The object is too small"), wxICON_QUESTION | wxYES_NO);
|
|
dlg.ShowCheckBox(_L("Apply to all the remaining small objects being loaded."));
|
|
int answer = dlg.ShowModal();
|
|
if (dlg.IsCheckBoxChecked())
|
|
answer_convert_from_imperial_units = answer;
|
|
else
|
|
convert_model_if(model, answer == wxID_YES);
|
|
}
|
|
convert_model_if(model, answer_convert_from_imperial_units == wxID_YES);
|
|
}
|
|
|
|
if (model.looks_like_multipart_object()) {
|
|
if (answer_consider_as_multi_part_objects == wxOK_DEFAULT) {
|
|
RichMessageDialog dlg(q, _L(
|
|
"This file contains several objects positioned at multiple heights.\n"
|
|
"Instead of considering them as multiple objects, should \n"
|
|
"the file be loaded as a single object having multiple parts?") + "\n",
|
|
_L("Multi-part object detected"), wxICON_QUESTION | wxYES_NO);
|
|
dlg.ShowCheckBox(_L("Apply to all objects being loaded."));
|
|
int answer = dlg.ShowModal();
|
|
if (dlg.IsCheckBoxChecked())
|
|
answer_consider_as_multi_part_objects = answer;
|
|
if (answer == wxID_YES)
|
|
model.convert_multipart_object(nozzle_dmrs->size());
|
|
}
|
|
else if (answer_consider_as_multi_part_objects == wxID_YES)
|
|
model.convert_multipart_object(nozzle_dmrs->size());
|
|
}
|
|
}
|
|
if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) {
|
|
MessageDialog msg_dlg(q, _L("This file cannot be loaded in a simple mode. Do you want to switch to an advanced mode?")+"\n",
|
|
_L("Detected advanced data"), wxICON_WARNING | wxOK | wxCANCEL);
|
|
if (msg_dlg.ShowModal() == wxID_OK) {
|
|
if (wxGetApp().save_mode(comAdvanced))
|
|
view3D->set_as_dirty();
|
|
}
|
|
else
|
|
return obj_idxs;
|
|
}
|
|
|
|
for (ModelObject* model_object : model.objects) {
|
|
if (!type_3mf && !type_zip_amf) {
|
|
model_object->center_around_origin(false);
|
|
if (type_any_amf && model_object->instances.empty()) {
|
|
ModelInstance* instance = model_object->add_instance();
|
|
instance->set_offset(-model_object->origin_translation);
|
|
}
|
|
}
|
|
if (!model_object->instances.empty())
|
|
model_object->ensure_on_bed(is_project_file);
|
|
}
|
|
|
|
if (one_by_one) {
|
|
if ((type_3mf && !is_project_file) || (type_any_amf && !type_zip_amf))
|
|
model.center_instances_around_point(this->bed.build_volume().bed_center());
|
|
auto loaded_idxs = load_model_objects(model.objects, is_project_file);
|
|
obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end());
|
|
} else {
|
|
// This must be an .stl or .obj file, which may contain a maximum of one volume.
|
|
for (const ModelObject* model_object : model.objects) {
|
|
new_model->add_object(*model_object);
|
|
}
|
|
}
|
|
|
|
if (is_project_file)
|
|
plater_after_load_auto_arrange.disable();
|
|
}
|
|
}
|
|
|
|
if (new_model != nullptr && new_model->objects.size() > 1) {
|
|
//wxMessageDialog msg_dlg(q, _L(
|
|
MessageDialog msg_dlg(q, _L(
|
|
"Multiple objects were loaded for a multi-material printer.\n"
|
|
"Instead of considering them as multiple objects, should I consider\n"
|
|
"these files to represent a single object having multiple parts?") + "\n",
|
|
_L("Multi-part object detected"), wxICON_WARNING | wxYES | wxNO);
|
|
if (msg_dlg.ShowModal() == wxID_YES) {
|
|
new_model->convert_multipart_object(nozzle_dmrs->values.size());
|
|
}
|
|
|
|
auto loaded_idxs = load_model_objects(new_model->objects);
|
|
obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end());
|
|
}
|
|
|
|
if (load_model && !in_temp) {
|
|
wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().make_preferred().string());
|
|
// XXX: Plater.pm had @loaded_files, but didn't seem to fill them with the filenames...
|
|
}
|
|
|
|
// automatic selection of added objects
|
|
if (!obj_idxs.empty() && view3D != nullptr) {
|
|
// update printable state for new volumes on canvas3D
|
|
wxGetApp().plater()->canvas3D()->update_instance_printable_state_for_objects(obj_idxs);
|
|
|
|
Selection& selection = view3D->get_canvas3d()->get_selection();
|
|
selection.clear();
|
|
for (size_t idx : obj_idxs) {
|
|
selection.add_object((unsigned int)idx, false);
|
|
}
|
|
|
|
if (view3D->get_canvas3d()->get_gizmos_manager().is_enabled())
|
|
// this is required because the selected object changed and the flatten on face an sla support gizmos need to be updated accordingly
|
|
view3D->get_canvas3d()->update_gizmos_on_off_state();
|
|
}
|
|
|
|
GLGizmoSimplify::add_simplify_suggestion_notification(
|
|
obj_idxs, model.objects, *notification_manager);
|
|
|
|
return obj_idxs;
|
|
}
|
|
|
|
// #define AUTOPLACEMENT_ON_LOAD
|
|
|
|
std::vector<size_t> Plater::priv::load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z, bool call_selection_changed /*= true*/)
|
|
{
|
|
const Vec3d bed_size = Slic3r::to_3d(this->bed.build_volume().bounding_volume2d().size(), 1.0) - 2.0 * Vec3d::Ones();
|
|
|
|
#ifndef AUTOPLACEMENT_ON_LOAD
|
|
// bool need_arrange = false;
|
|
#endif /* AUTOPLACEMENT_ON_LOAD */
|
|
bool scaled_down = false;
|
|
std::vector<size_t> obj_idxs;
|
|
unsigned int obj_count = model.objects.size();
|
|
|
|
#ifdef AUTOPLACEMENT_ON_LOAD
|
|
ModelInstancePtrs new_instances;
|
|
#endif /* AUTOPLACEMENT_ON_LOAD */
|
|
for (ModelObject *model_object : model_objects) {
|
|
auto *object = model.add_object(*model_object);
|
|
object->sort_volumes(get_config_bool("order_volumes"));
|
|
std::string object_name = object->name.empty() ? fs::path(object->input_file).filename().string() : object->name;
|
|
obj_idxs.push_back(obj_count++);
|
|
|
|
if (model_object->instances.empty()) {
|
|
#ifdef AUTOPLACEMENT_ON_LOAD
|
|
object->center_around_origin();
|
|
new_instances.emplace_back(object->add_instance());
|
|
#else /* AUTOPLACEMENT_ON_LOAD */
|
|
// if object has no defined position(s) we need to rearrange everything after loading
|
|
// need_arrange = true;
|
|
// add a default instance and center object around origin
|
|
object->center_around_origin(); // also aligns object to Z = 0
|
|
ModelInstance* instance = object->add_instance();
|
|
instance->set_offset(Slic3r::to_3d(this->bed.build_volume().bed_center(), -object->origin_translation(2)));
|
|
#endif /* AUTOPLACEMENT_ON_LOAD */
|
|
}
|
|
|
|
for (size_t i = 0; i < object->instances.size()
|
|
&& !object->is_cut() // don't apply scaled_down functionality to cut objects
|
|
; ++i) {
|
|
ModelInstance* instance = object->instances[i];
|
|
const Vec3d size = object->instance_bounding_box(i).size();
|
|
const Vec3d ratio = size.cwiseQuotient(bed_size);
|
|
const double max_ratio = std::max(ratio(0), ratio(1));
|
|
if (max_ratio > 10000) {
|
|
// the size of the object is too big -> this could lead to overflow when moving to clipper coordinates,
|
|
// so scale down the mesh
|
|
object->scale_mesh_after_creation(1. / max_ratio);
|
|
object->origin_translation = Vec3d::Zero();
|
|
object->center_around_origin();
|
|
scaled_down = true;
|
|
break;
|
|
}
|
|
else if (max_ratio > 5) {
|
|
instance->set_scaling_factor(instance->get_scaling_factor() / max_ratio);
|
|
scaled_down = true;
|
|
}
|
|
}
|
|
|
|
object->ensure_on_bed(allow_negative_z);
|
|
}
|
|
|
|
#ifdef AUTOPLACEMENT_ON_LOAD
|
|
// FIXME distance should be a config value /////////////////////////////////
|
|
auto min_obj_distance = static_cast<coord_t>(6/SCALING_FACTOR);
|
|
const auto *bed_shape_opt = config->opt<ConfigOptionPoints>("bed_shape");
|
|
assert(bed_shape_opt);
|
|
auto& bedpoints = bed_shape_opt->values;
|
|
Polyline bed; bed.points.reserve(bedpoints.size());
|
|
for(auto& v : bedpoints) bed.append(Point::new_scale(v(0), v(1)));
|
|
|
|
std::pair<bool, GLCanvas3D::WipeTowerInfo> wti = view3D->get_canvas3d()->get_wipe_tower_info();
|
|
|
|
arr::find_new_position(model, new_instances, min_obj_distance, bed, wti);
|
|
|
|
// it remains to move the wipe tower:
|
|
view3D->get_canvas3d()->arrange_wipe_tower(wti);
|
|
|
|
#endif /* AUTOPLACEMENT_ON_LOAD */
|
|
|
|
if (scaled_down) {
|
|
GUI::show_info(q,
|
|
_L("Your object appears to be too large, so it was automatically scaled down to fit your print bed."),
|
|
_L("Object too large?"));
|
|
}
|
|
|
|
notification_manager->close_notification_of_type(NotificationType::UpdatedItemsInfo);
|
|
for (const size_t idx : obj_idxs) {
|
|
wxGetApp().obj_list()->add_object_to_list(idx, call_selection_changed);
|
|
}
|
|
|
|
if (call_selection_changed) {
|
|
update();
|
|
// Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(),
|
|
// which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call
|
|
for (const size_t idx : obj_idxs)
|
|
wxGetApp().obj_list()->update_info_items(idx);
|
|
|
|
object_list_changed();
|
|
}
|
|
this->schedule_background_process();
|
|
|
|
return obj_idxs;
|
|
}
|
|
|
|
fs::path Plater::priv::get_export_file_path(GUI::FileType file_type)
|
|
{
|
|
// Update printbility state of each of the ModelInstances.
|
|
this->update_print_volume_state();
|
|
|
|
const Selection& selection = get_selection();
|
|
int obj_idx = selection.get_object_idx();
|
|
|
|
fs::path output_file;
|
|
if (file_type == FT_3MF)
|
|
// for 3mf take the path from the project filename, if any
|
|
output_file = into_path(get_project_filename(".3mf"));
|
|
|
|
if (output_file.empty())
|
|
{
|
|
// first try to get the file name from the current selection
|
|
if ((0 <= obj_idx) && (obj_idx < (int)this->model.objects.size()))
|
|
output_file = this->model.objects[obj_idx]->get_export_filename();
|
|
|
|
if (output_file.empty())
|
|
// Find the file name of the first printable object.
|
|
output_file = this->model.propose_export_file_name_and_path();
|
|
|
|
if (output_file.empty() && !model.objects.empty())
|
|
// Find the file name of the first object.
|
|
output_file = this->model.objects[0]->get_export_filename();
|
|
|
|
if (output_file.empty())
|
|
// Use _L("Untitled") name
|
|
output_file = into_path(_L("Untitled"));
|
|
}
|
|
return output_file;
|
|
}
|
|
|
|
wxString Plater::priv::get_export_file(GUI::FileType file_type)
|
|
{
|
|
wxString wildcard;
|
|
switch (file_type) {
|
|
case FT_STL:
|
|
case FT_AMF:
|
|
case FT_3MF:
|
|
case FT_GCODE:
|
|
case FT_OBJ:
|
|
case FT_OBJECT:
|
|
wildcard = file_wildcards(file_type);
|
|
break;
|
|
default:
|
|
wildcard = file_wildcards(FT_MODEL);
|
|
break;
|
|
}
|
|
|
|
fs::path output_file = get_export_file_path(file_type);
|
|
|
|
wxString dlg_title;
|
|
switch (file_type) {
|
|
case FT_STL:
|
|
{
|
|
output_file.replace_extension("stl");
|
|
dlg_title = _L("Export STL file:");
|
|
break;
|
|
}
|
|
case FT_AMF:
|
|
{
|
|
// XXX: Problem on OS X with double extension?
|
|
output_file.replace_extension("zip.amf");
|
|
dlg_title = _L("Export AMF file:");
|
|
break;
|
|
}
|
|
case FT_3MF:
|
|
{
|
|
output_file.replace_extension("3mf");
|
|
dlg_title = _L("Save file as:");
|
|
break;
|
|
}
|
|
case FT_OBJ:
|
|
{
|
|
output_file.replace_extension("obj");
|
|
dlg_title = _L("Export OBJ file:");
|
|
break;
|
|
}
|
|
default: break;
|
|
}
|
|
|
|
std::string out_dir = (boost::filesystem::path(output_file).parent_path()).string();
|
|
std::string temp_dir = wxStandardPaths::Get().GetTempDir().utf8_str().data();
|
|
|
|
wxFileDialog dlg(q, dlg_title,
|
|
out_dir == temp_dir ? from_u8(wxGetApp().app_config->get("last_output_path")) : (is_shapes_dir(out_dir) ? from_u8(wxGetApp().app_config->get_last_dir()) : from_path(output_file.parent_path())), from_path(output_file.filename()),
|
|
wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
|
|
|
|
if (dlg.ShowModal() != wxID_OK)
|
|
return wxEmptyString;
|
|
|
|
wxString out_path = dlg.GetPath();
|
|
fs::path path(into_path(out_path));
|
|
wxGetApp().app_config->update_last_output_dir(path.parent_path().string());
|
|
|
|
return out_path;
|
|
}
|
|
|
|
const Selection& Plater::priv::get_selection() const
|
|
{
|
|
return view3D->get_canvas3d()->get_selection();
|
|
}
|
|
|
|
Selection& Plater::priv::get_selection()
|
|
{
|
|
return view3D->get_canvas3d()->get_selection();
|
|
}
|
|
|
|
int Plater::priv::get_selected_object_idx() const
|
|
{
|
|
const int idx = get_selection().get_object_idx();
|
|
return (0 <= idx && idx < int(model.objects.size())) ? idx : -1;
|
|
}
|
|
|
|
int Plater::priv::get_selected_instance_idx() const
|
|
{
|
|
const int obj_idx = get_selected_object_idx();
|
|
if (obj_idx >= 0) {
|
|
const int inst_idx = get_selection().get_instance_idx();
|
|
return (0 <= inst_idx && inst_idx < int(model.objects[obj_idx]->instances.size())) ? inst_idx : -1;
|
|
}
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
int Plater::priv::get_selected_volume_idx() const
|
|
{
|
|
auto& selection = get_selection();
|
|
const int idx = selection.get_object_idx();
|
|
if (idx < 0 || int(model.objects.size()) <= idx)
|
|
return-1;
|
|
const GLVolume* v = selection.get_first_volume();
|
|
if (model.objects[idx]->volumes.size() > 1)
|
|
return v->volume_idx();
|
|
return -1;
|
|
}
|
|
|
|
void Plater::priv::selection_changed()
|
|
{
|
|
// if the selection is not valid to allow for layer editing, we need to turn off the tool if it is running
|
|
if (!layers_height_allowed() && view3D->is_layers_editing_enabled()) {
|
|
SimpleEvent evt(EVT_GLTOOLBAR_LAYERSEDITING);
|
|
on_action_layersediting(evt);
|
|
}
|
|
|
|
// forces a frame render to update the view (to avoid a missed update if, for example, the context menu appears)
|
|
view3D->render();
|
|
}
|
|
|
|
void Plater::priv::object_list_changed()
|
|
{
|
|
const bool export_in_progress = this->background_process.is_export_scheduled(); // || ! send_gcode_file.empty());
|
|
// XXX: is this right?
|
|
const bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() == ModelInstancePVS_Inside;
|
|
|
|
sidebar->enable_buttons(!model.objects.empty() && !export_in_progress && model_fits);
|
|
}
|
|
|
|
void Plater::priv::select_all()
|
|
{
|
|
view3D->select_all();
|
|
this->sidebar->obj_list()->update_selections();
|
|
}
|
|
|
|
void Plater::priv::deselect_all()
|
|
{
|
|
view3D->deselect_all();
|
|
}
|
|
|
|
void Plater::priv::remove(size_t obj_idx)
|
|
{
|
|
if (view3D->is_layers_editing_enabled())
|
|
view3D->enable_layers_editing(false);
|
|
|
|
m_worker.cancel_all();
|
|
model.delete_object(obj_idx);
|
|
update();
|
|
// Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model.
|
|
sidebar->obj_list()->delete_object_from_list(obj_idx);
|
|
object_list_changed();
|
|
}
|
|
|
|
|
|
bool Plater::priv::delete_object_from_model(size_t obj_idx)
|
|
{
|
|
// check if object isn't cut
|
|
// show warning message that "cut consistancy" will not be supported any more
|
|
ModelObject* obj = model.objects[obj_idx];
|
|
if (obj->is_cut()) {
|
|
InfoDialog dialog(q, _L("Delete object which is a part of cut object"),
|
|
_L("You try to delete an object which is a part of a cut object.") + "\n" +
|
|
_L("This action will break a cut information.\n"
|
|
"After that PrusaSlicer can't guarantee model consistency"),
|
|
false, wxYES | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
|
|
dialog.SetButtonLabel(wxID_YES, _L("Delete object"));
|
|
if (dialog.ShowModal() == wxID_CANCEL)
|
|
return false;
|
|
}
|
|
|
|
wxString snapshot_label = _L("Delete Object");
|
|
if (!obj->name.empty())
|
|
snapshot_label += ": " + wxString::FromUTF8(obj->name.c_str());
|
|
Plater::TakeSnapshot snapshot(q, snapshot_label);
|
|
m_worker.cancel_all();
|
|
|
|
if (obj->is_cut())
|
|
sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx);
|
|
|
|
model.delete_object(obj_idx);
|
|
|
|
update();
|
|
object_list_changed();
|
|
|
|
return true;
|
|
}
|
|
|
|
void Plater::priv::delete_all_objects_from_model()
|
|
{
|
|
Plater::TakeSnapshot snapshot(q, _L("Delete All Objects"));
|
|
|
|
if (view3D->is_layers_editing_enabled())
|
|
view3D->enable_layers_editing(false);
|
|
|
|
reset_gcode_toolpaths();
|
|
gcode_result.reset();
|
|
|
|
view3D->get_canvas3d()->reset_sequential_print_clearance();
|
|
view3D->get_canvas3d()->reset_all_gizmos();
|
|
|
|
m_worker.cancel_all();
|
|
|
|
// Stop and reset the Print content.
|
|
background_process.reset();
|
|
model.clear_objects();
|
|
update();
|
|
// Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model.
|
|
sidebar->obj_list()->delete_all_objects_from_list();
|
|
object_list_changed();
|
|
|
|
// The hiding of the slicing results, if shown, is not taken care by the background process, so we do it here
|
|
sidebar->show_sliced_info_sizer(false);
|
|
|
|
model.custom_gcode_per_print_z.gcodes.clear();
|
|
}
|
|
|
|
void Plater::priv::reset()
|
|
{
|
|
Plater::TakeSnapshot snapshot(q, _L("Reset Project"), UndoRedo::SnapshotType::ProjectSeparator);
|
|
|
|
clear_warnings();
|
|
|
|
set_project_filename(wxEmptyString);
|
|
|
|
if (view3D->is_layers_editing_enabled())
|
|
view3D->enable_layers_editing(false);
|
|
|
|
reset_gcode_toolpaths();
|
|
gcode_result.reset();
|
|
|
|
view3D->get_canvas3d()->reset_sequential_print_clearance();
|
|
|
|
m_worker.cancel_all();
|
|
|
|
// Stop and reset the Print content.
|
|
this->background_process.reset();
|
|
model.clear_objects();
|
|
update();
|
|
// Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model.
|
|
sidebar->obj_list()->delete_all_objects_from_list();
|
|
object_list_changed();
|
|
|
|
// The hiding of the slicing results, if shown, is not taken care by the background process, so we do it here
|
|
this->sidebar->show_sliced_info_sizer(false);
|
|
|
|
model.custom_gcode_per_print_z.gcodes.clear();
|
|
}
|
|
|
|
void Plater::priv::mirror(Axis axis)
|
|
{
|
|
view3D->mirror_selection(axis);
|
|
}
|
|
|
|
void Plater::priv::split_object()
|
|
{
|
|
int obj_idx = get_selected_object_idx();
|
|
if (obj_idx == -1)
|
|
return;
|
|
|
|
// we clone model object because split_object() adds the split volumes
|
|
// into the same model object, thus causing duplicates when we call load_model_objects()
|
|
Model new_model = model;
|
|
ModelObject* current_model_object = new_model.objects[obj_idx];
|
|
|
|
// Before splitting object we have to remove all custom supports, seams, and multimaterial painting.
|
|
wxGetApp().plater()->clear_before_change_mesh(obj_idx, _u8L("Custom supports, seams and multimaterial painting were "
|
|
"removed after splitting the object."));
|
|
|
|
wxBusyCursor wait;
|
|
ModelObjectPtrs new_objects;
|
|
current_model_object->split(&new_objects);
|
|
if (new_objects.size() == 1)
|
|
// #ysFIXME use notification
|
|
Slic3r::GUI::warning_catcher(q, _L("The selected object couldn't be split because it contains only one solid part."));
|
|
else
|
|
{
|
|
// If we splited object which is contain some parts/modifiers then all non-solid parts (modifiers) were deleted
|
|
if (current_model_object->volumes.size() > 1 && current_model_object->volumes.size() != new_objects.size())
|
|
notification_manager->push_notification(NotificationType::CustomNotification,
|
|
NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
|
|
_u8L("All non-solid parts (modifiers) were deleted"));
|
|
|
|
Plater::TakeSnapshot snapshot(q, _L("Split to Objects"));
|
|
|
|
remove(obj_idx);
|
|
|
|
// load all model objects at once, otherwise the plate would be rearranged after each one
|
|
// causing original positions not to be kept
|
|
std::vector<size_t> idxs = load_model_objects(new_objects);
|
|
|
|
// clear previosli selection
|
|
get_selection().clear();
|
|
// select newly added objects
|
|
for (size_t idx : idxs)
|
|
{
|
|
get_selection().add_object((unsigned int)idx, false);
|
|
// update printable state for new volumes on canvas3D
|
|
q->canvas3D()->update_instance_printable_state_for_object(idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Plater::priv::split_volume()
|
|
{
|
|
wxGetApp().obj_list()->split();
|
|
}
|
|
|
|
void Plater::priv::scale_selection_to_fit_print_volume()
|
|
{
|
|
this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(this->bed.build_volume());
|
|
}
|
|
|
|
void Plater::priv::schedule_background_process()
|
|
{
|
|
delayed_error_message.clear();
|
|
// Trigger the timer event after 0.5s
|
|
this->background_process_timer.Start(500, wxTIMER_ONE_SHOT);
|
|
// Notify the Canvas3D that something has changed, so it may invalidate some of the layer editing stuff.
|
|
this->view3D->get_canvas3d()->set_config(this->config);
|
|
}
|
|
|
|
void Plater::priv::update_print_volume_state()
|
|
{
|
|
this->q->model().update_print_volume_state(this->bed.build_volume());
|
|
}
|
|
|
|
void Plater::priv::process_validation_warning(const std::vector<std::string>& warnings) const
|
|
{
|
|
if (warnings.empty())
|
|
notification_manager->close_notification_of_type(NotificationType::ValidateWarning);
|
|
|
|
for (std::string text : warnings) {
|
|
std::string hypertext = "";
|
|
std::function<bool(wxEvtHandler*)> action_fn = [](wxEvtHandler*){ return false; };
|
|
|
|
if (text == "_SUPPORTS_OFF") {
|
|
text = _u8L("An object has custom support enforcers which will not be used "
|
|
"because supports are disabled.")+"\n";
|
|
hypertext = _u8L("Enable supports for enforcers only");
|
|
action_fn = [](wxEvtHandler*) {
|
|
Tab* print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT);
|
|
assert(print_tab);
|
|
DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
|
config.set_key_value("support_material", new ConfigOptionBool(true));
|
|
config.set_key_value("support_material_auto", new ConfigOptionBool(false));
|
|
print_tab->on_value_change("support_material", config.opt_bool("support_material"));
|
|
print_tab->on_value_change("support_material_auto", config.opt_bool("support_material_auto"));
|
|
return true;
|
|
};
|
|
}
|
|
if (text == "_BED_TEMPS_DIFFER")
|
|
text = _u8L("Bed temperatures for the used filaments differ significantly.");
|
|
|
|
notification_manager->push_notification(
|
|
NotificationType::ValidateWarning,
|
|
NotificationManager::NotificationLevel::WarningNotificationLevel,
|
|
_u8L("WARNING:") + "\n" + text, hypertext, action_fn
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
// Update background processing thread from the current config and Model.
|
|
// Returns a bitmask of UpdateBackgroundProcessReturnState.
|
|
unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages)
|
|
{
|
|
// bitmap of enum UpdateBackgroundProcessReturnState
|
|
unsigned int return_state = 0;
|
|
|
|
// Get the config ready. The binary gcode flag depends on Preferences, which the backend has no access to.
|
|
DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config();
|
|
if (full_config.has("binary_gcode")) // needed for SLA
|
|
full_config.set("binary_gcode", bool(full_config.opt_bool("binary_gcode") & wxGetApp().app_config->get_bool("use_binary_gcode_when_supported")));
|
|
|
|
// If the update_background_process() was not called by the timer, kill the timer,
|
|
// so the update_restart_background_process() will not be called again in vain.
|
|
background_process_timer.Stop();
|
|
// Update the "out of print bed" state of ModelInstances.
|
|
update_print_volume_state();
|
|
// Apply new config to the possibly running background task.
|
|
bool was_running = background_process.running();
|
|
Print::ApplyStatus invalidated = background_process.apply(q->model(), full_config);
|
|
|
|
// Just redraw the 3D canvas without reloading the scene to consume the update of the layer height profile.
|
|
if (view3D->is_layers_editing_enabled())
|
|
view3D->get_wxglcanvas()->Refresh();
|
|
|
|
if (invalidated == Print::APPLY_STATUS_CHANGED || background_process.empty())
|
|
view3D->get_canvas3d()->reset_sequential_print_clearance();
|
|
|
|
if (invalidated == Print::APPLY_STATUS_INVALIDATED) {
|
|
// Some previously calculated data on the Print was invalidated.
|
|
// Hide the slicing results, as the current slicing status is no more valid.
|
|
sidebar->show_sliced_info_sizer(false);
|
|
// Reset preview canvases. If the print has been invalidated, the preview canvases will be cleared.
|
|
// Otherwise they will be just refreshed.
|
|
if (preview != nullptr) {
|
|
// If the preview is not visible, the following line just invalidates the preview,
|
|
// but the G-code paths or SLA preview are calculated first once the preview is made visible.
|
|
reset_gcode_toolpaths();
|
|
preview->reload_print();
|
|
}
|
|
// In FDM mode, we need to reload the 3D scene because of the wipe tower preview box.
|
|
// In SLA mode, we need to reload the 3D scene every time to show the support structures.
|
|
if (printer_technology == ptSLA || (printer_technology == ptFFF && config->opt_bool("wipe_tower")))
|
|
return_state |= UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE;
|
|
|
|
notification_manager->set_slicing_progress_hidden();
|
|
}
|
|
|
|
if ((invalidated != Print::APPLY_STATUS_UNCHANGED || force_validation) && ! background_process.empty()) {
|
|
// The delayed error message is no more valid.
|
|
delayed_error_message.clear();
|
|
// The state of the Print changed, and it is non-zero. Let's validate it and give the user feedback on errors.
|
|
std::vector<std::string> warnings;
|
|
std::string err = background_process.validate(&warnings);
|
|
if (err.empty()) {
|
|
notification_manager->set_all_slicing_errors_gray(true);
|
|
notification_manager->close_notification_of_type(NotificationType::ValidateError);
|
|
if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled())
|
|
return_state |= UPDATE_BACKGROUND_PROCESS_RESTART;
|
|
|
|
// Pass a warning from validation and either show a notification,
|
|
// or hide the old one.
|
|
process_validation_warning(warnings);
|
|
if (printer_technology == ptFFF) {
|
|
GLCanvas3D* canvas = view3D->get_canvas3d();
|
|
canvas->reset_sequential_print_clearance();
|
|
canvas->set_as_dirty();
|
|
canvas->request_extra_frame();
|
|
}
|
|
}
|
|
else {
|
|
// The print is not valid.
|
|
// Show error as notification.
|
|
notification_manager->push_validate_error_notification(err);
|
|
return_state |= UPDATE_BACKGROUND_PROCESS_INVALID;
|
|
if (printer_technology == ptFFF) {
|
|
GLCanvas3D* canvas = view3D->get_canvas3d();
|
|
if (canvas->is_sequential_print_clearance_empty() || canvas->is_sequential_print_clearance_evaluating()) {
|
|
GLCanvas3D::ContoursList contours;
|
|
contours.contours = background_process.fff_print()->get_sequential_print_clearance_contours();
|
|
canvas->set_sequential_print_clearance_contours(contours, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (invalidated == Print::APPLY_STATUS_UNCHANGED && !background_process.empty()) {
|
|
if (printer_technology == ptFFF) {
|
|
// Object manipulation with gizmos may end up in a null transformation.
|
|
// In this case, we need to trigger the completion of the sequential print clearance contours evaluation
|
|
GLCanvas3D* canvas = view3D->get_canvas3d();
|
|
if (canvas->is_sequential_print_clearance_evaluating()) {
|
|
GLCanvas3D::ContoursList contours;
|
|
contours.contours = background_process.fff_print()->get_sequential_print_clearance_contours();
|
|
canvas->set_sequential_print_clearance_contours(contours, true);
|
|
}
|
|
}
|
|
std::vector<std::string> warnings;
|
|
std::string err = background_process.validate(&warnings);
|
|
if (!err.empty())
|
|
return return_state;
|
|
}
|
|
|
|
if (! this->delayed_error_message.empty())
|
|
// Reusing the old state.
|
|
return_state |= UPDATE_BACKGROUND_PROCESS_INVALID;
|
|
}
|
|
|
|
//actualizate warnings
|
|
if (invalidated != Print::APPLY_STATUS_UNCHANGED || background_process.empty()) {
|
|
if (background_process.empty())
|
|
process_validation_warning(std::vector<std::string>());
|
|
actualize_slicing_warnings(*this->background_process.current_print());
|
|
actualize_object_warnings(*this->background_process.current_print());
|
|
show_warning_dialog = false;
|
|
process_completed_with_error = false;
|
|
}
|
|
|
|
if (invalidated != Print::APPLY_STATUS_UNCHANGED && was_running && ! this->background_process.running() &&
|
|
(return_state & UPDATE_BACKGROUND_PROCESS_RESTART) == 0) {
|
|
// The background processing was killed and it will not be restarted.
|
|
// Post the "canceled" callback message, so that it will be processed after any possible pending status bar update messages.
|
|
wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new SlicingProcessCompletedEvent(EVT_PROCESS_COMPLETED, 0, SlicingProcessCompletedEvent::Cancelled, std::exception_ptr{}));
|
|
}
|
|
|
|
if ((return_state & UPDATE_BACKGROUND_PROCESS_INVALID) != 0)
|
|
{
|
|
// Validation of the background data failed.
|
|
const wxString invalid_str = _L("Invalid data");
|
|
for (auto btn : {ActionButtonType::Reslice, ActionButtonType::SendGCode, ActionButtonType::Export})
|
|
sidebar->set_btn_label(btn, invalid_str);
|
|
process_completed_with_error = true;
|
|
}
|
|
else
|
|
{
|
|
// Background data is valid.
|
|
if ((return_state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 ||
|
|
(return_state & UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) != 0 )
|
|
notification_manager->set_slicing_progress_hidden();
|
|
|
|
sidebar->set_btn_label(ActionButtonType::Export, _(label_btn_export));
|
|
sidebar->set_btn_label(ActionButtonType::SendGCode, _(label_btn_send));
|
|
dirty_state.update_from_preview();
|
|
|
|
const wxString slice_string = background_process.running() && wxGetApp().get_mode() == comSimple ?
|
|
_L("Slicing") + dots : _L("Slice now");
|
|
sidebar->set_btn_label(ActionButtonType::Reslice, slice_string);
|
|
|
|
if (background_process.finished())
|
|
show_action_buttons(false);
|
|
else if (!background_process.empty() &&
|
|
!background_process.running()) /* Do not update buttons if background process is running
|
|
* This condition is important for SLA mode especially,
|
|
* when this function is called several times during calculations
|
|
* */
|
|
show_action_buttons(true);
|
|
}
|
|
|
|
return return_state;
|
|
}
|
|
|
|
// Restart background processing thread based on a bitmask of UpdateBackgroundProcessReturnState.
|
|
bool Plater::priv::restart_background_process(unsigned int state)
|
|
{
|
|
if (!m_worker.is_idle()) {
|
|
// Avoid a race condition
|
|
return false;
|
|
}
|
|
|
|
if ( ! this->background_process.empty() &&
|
|
(state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) == 0 &&
|
|
( ((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0 && ! this->background_process.finished()) ||
|
|
(state & UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT) != 0 ||
|
|
(state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 ) ) {
|
|
// The print is valid and it can be started.
|
|
if (this->background_process.start()) {
|
|
if (!show_warning_dialog)
|
|
on_slicing_began();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_removable_media, PrintHostJob upload_job)
|
|
{
|
|
wxCHECK_RET(!(output_path.empty() && upload_job.empty()), "export_gcode: output_path and upload_job empty");
|
|
|
|
if (model.objects.empty())
|
|
return;
|
|
|
|
if (background_process.is_export_scheduled()) {
|
|
GUI::show_error(q, _L("Another export job is currently running."));
|
|
return;
|
|
}
|
|
|
|
// bitmask of UpdateBackgroundProcessReturnState
|
|
unsigned int state = update_background_process(true);
|
|
if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE)
|
|
view3D->reload_scene(false);
|
|
|
|
if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0)
|
|
return;
|
|
|
|
show_warning_dialog = true;
|
|
if (! output_path.empty()) {
|
|
background_process.schedule_export(output_path.string(), output_path_on_removable_media);
|
|
notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() {return true; }, 1000, 0);
|
|
} else {
|
|
background_process.schedule_upload(std::move(upload_job));
|
|
}
|
|
|
|
// If the SLA processing of just a single object's supports is running, restart slicing for the whole object.
|
|
this->background_process.set_task(PrintBase::TaskParams());
|
|
this->restart_background_process(priv::UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT);
|
|
}
|
|
|
|
unsigned int Plater::priv::update_restart_background_process(bool force_update_scene, bool force_update_preview)
|
|
{
|
|
// bitmask of UpdateBackgroundProcessReturnState
|
|
unsigned int state = this->update_background_process(false);
|
|
if (force_update_scene || (state & UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) != 0)
|
|
view3D->reload_scene(false);
|
|
|
|
if (force_update_preview)
|
|
this->preview->reload_print();
|
|
this->restart_background_process(state);
|
|
return state;
|
|
}
|
|
|
|
void Plater::priv::update_fff_scene()
|
|
{
|
|
if (this->preview != nullptr)
|
|
this->preview->reload_print();
|
|
// In case this was MM print, wipe tower bounding box on 3D tab might need redrawing with exact depth:
|
|
view3D->reload_scene(true);
|
|
}
|
|
|
|
void Plater::priv::update_sla_scene()
|
|
{
|
|
// Update the SLAPrint from the current Model, so that the reload_scene()
|
|
// pulls the correct data.
|
|
delayed_scene_refresh = false;
|
|
this->update_restart_background_process(true, true);
|
|
}
|
|
|
|
// class used to show a wxBusyCursor and a wxBusyInfo
|
|
// and hide them on demand
|
|
class Busy
|
|
{
|
|
wxWindow* m_parent{ nullptr };
|
|
std::unique_ptr<wxBusyCursor> m_cursor;
|
|
std::unique_ptr<wxBusyInfo> m_dlg;
|
|
|
|
public:
|
|
Busy(const wxString& message, wxWindow* parent = nullptr) {
|
|
m_parent = parent;
|
|
m_cursor = std::make_unique<wxBusyCursor>();
|
|
m_dlg = std::make_unique<wxBusyInfo>(message, m_parent);
|
|
}
|
|
|
|
~Busy() { reset(); }
|
|
|
|
void update(const wxString& message) {
|
|
// this is ugly but necessary because the call to wxBusyInfo::UpdateLabel() is not working [WX 3.1.4]
|
|
m_dlg = std::make_unique<wxBusyInfo>(message, m_parent);
|
|
// m_dlg->UpdateLabel(message);
|
|
}
|
|
|
|
void reset() {
|
|
m_cursor.reset();
|
|
m_dlg.reset();
|
|
}
|
|
};
|
|
|
|
bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const fs::path& new_path, const wxString& snapshot)
|
|
{
|
|
const std::string path = new_path.string();
|
|
Busy busy(_L("Replace from:") + " " + from_u8(path), q->get_current_canvas3D()->get_wxglcanvas());
|
|
|
|
Model new_model;
|
|
try {
|
|
new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances);
|
|
for (ModelObject* model_object : new_model.objects) {
|
|
model_object->center_around_origin();
|
|
model_object->ensure_on_bed();
|
|
}
|
|
}
|
|
catch (std::exception& e) {
|
|
// error while loading
|
|
busy.reset();
|
|
GUI::show_error(q, e.what());
|
|
return false;
|
|
}
|
|
|
|
if (new_model.objects.size() > 1 || new_model.objects.front()->volumes.size() > 1) {
|
|
MessageDialog dlg(q, _L("Unable to replace with more than one volume"), _L("Error during replace"), wxOK | wxOK_DEFAULT | wxICON_WARNING);
|
|
dlg.ShowModal();
|
|
return false;
|
|
}
|
|
|
|
if (!snapshot.empty())
|
|
q->take_snapshot(snapshot);
|
|
|
|
ModelObject* old_model_object = model.objects[object_idx];
|
|
ModelVolume* old_volume = old_model_object->volumes[volume_idx];
|
|
|
|
bool sinking = old_model_object->min_z() < SINKING_Z_THRESHOLD;
|
|
|
|
ModelObject* new_model_object = new_model.objects.front();
|
|
old_model_object->add_volume(*new_model_object->volumes.front());
|
|
ModelVolume* new_volume = old_model_object->volumes.back();
|
|
new_volume->set_new_unique_id();
|
|
new_volume->config.apply(old_volume->config);
|
|
new_volume->set_type(old_volume->type());
|
|
new_volume->set_material_id(old_volume->material_id());
|
|
new_volume->set_transformation(old_volume->get_transformation());
|
|
new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset));
|
|
assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters);
|
|
if (old_volume->source.is_converted_from_inches)
|
|
new_volume->convert_from_imperial_units();
|
|
else if (old_volume->source.is_converted_from_meters)
|
|
new_volume->convert_from_meters();
|
|
|
|
if (old_volume->mesh().its == new_volume->mesh().its) {
|
|
// This function is called both from reload_from_disk and replace_with_stl.
|
|
// We need to make sure that the painted data point to existing triangles.
|
|
new_volume->supported_facets.assign(old_volume->supported_facets);
|
|
new_volume->seam_facets.assign(old_volume->seam_facets);
|
|
new_volume->mm_segmentation_facets.assign(old_volume->mm_segmentation_facets);
|
|
}
|
|
std::swap(old_model_object->volumes[volume_idx], old_model_object->volumes.back());
|
|
old_model_object->delete_volume(old_model_object->volumes.size() - 1);
|
|
if (!sinking)
|
|
old_model_object->ensure_on_bed();
|
|
old_model_object->sort_volumes(get_config_bool("order_volumes"));
|
|
|
|
// if object has just one volume, rename object too
|
|
if (old_model_object->volumes.size() == 1)
|
|
old_model_object->name = old_model_object->volumes.front()->name;
|
|
|
|
// update new name in ObjectList
|
|
sidebar->obj_list()->update_name_in_list(object_idx, volume_idx);
|
|
sidebar->obj_list()->update_item_error_icon(object_idx, volume_idx);
|
|
|
|
sla::reproject_points_and_holes(old_model_object);
|
|
|
|
return true;
|
|
}
|
|
|
|
void Plater::priv::replace_with_stl()
|
|
{
|
|
if (! q->canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined))
|
|
return;
|
|
|
|
const Selection& selection = get_selection();
|
|
|
|
if (selection.is_wipe_tower() || get_selection().get_volume_idxs().size() != 1)
|
|
return;
|
|
|
|
const GLVolume* v = selection.get_first_volume();
|
|
int object_idx = v->object_idx();
|
|
int volume_idx = v->volume_idx();
|
|
|
|
// collects paths of files to load
|
|
|
|
const ModelObject* object = model.objects[object_idx];
|
|
const ModelVolume* volume = object->volumes[volume_idx];
|
|
|
|
fs::path input_path;
|
|
if (!volume->source.input_file.empty() && fs::exists(volume->source.input_file))
|
|
input_path = volume->source.input_file;
|
|
|
|
wxString title = _L("Select the new file");
|
|
title += ":";
|
|
wxFileDialog dialog(q, title, "", from_u8(input_path.filename().string()), file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
|
if (dialog.ShowModal() != wxID_OK)
|
|
return;
|
|
|
|
fs::path out_path = dialog.GetPath().ToUTF8().data();
|
|
if (out_path.empty()) {
|
|
MessageDialog dlg(q, _L("File for the replace wasn't selected"), _L("Error during replace"), wxOK | wxOK_DEFAULT | wxICON_WARNING);
|
|
dlg.ShowModal();
|
|
return;
|
|
}
|
|
|
|
if (!replace_volume_with_stl(object_idx, volume_idx, out_path, _L("Replace with STL")))
|
|
return;
|
|
|
|
// update 3D scene
|
|
update();
|
|
|
|
// new GLVolumes have been created at this point, so update their printable state
|
|
for (size_t i = 0; i < model.objects.size(); ++i) {
|
|
view3D->get_canvas3d()->update_instance_printable_state_for_object(i);
|
|
}
|
|
}
|
|
|
|
static std::vector<std::pair<int, int>> reloadable_volumes(const Model& model, const Selection& selection)
|
|
{
|
|
std::vector<std::pair<int, int>> ret;
|
|
const std::set<unsigned int>& selected_volumes_idxs = selection.get_volume_idxs();
|
|
for (unsigned int idx : selected_volumes_idxs) {
|
|
const GLVolume& v = *selection.get_volume(idx);
|
|
const int o_idx = v.object_idx();
|
|
if (0 <= o_idx && o_idx < int(model.objects.size())) {
|
|
const ModelObject* obj = model.objects[o_idx];
|
|
const int v_idx = v.volume_idx();
|
|
if (0 <= v_idx && v_idx < int(obj->volumes.size())) {
|
|
const ModelVolume* vol = obj->volumes[v_idx];
|
|
if (!vol->source.is_from_builtin_objects && !vol->source.input_file.empty() &&
|
|
!fs::path(vol->source.input_file).extension().string().empty())
|
|
ret.push_back({ o_idx, v_idx });
|
|
}
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void Plater::priv::reload_from_disk()
|
|
{
|
|
// collect selected reloadable ModelVolumes
|
|
std::vector<std::pair<int, int>> selected_volumes = reloadable_volumes(model, get_selection());
|
|
|
|
// nothing to reload, return
|
|
if (selected_volumes.empty())
|
|
return;
|
|
|
|
std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair<int, int>& v1, const std::pair<int, int>& v2) {
|
|
return (v1.first < v2.first) || (v1.first == v2.first && v1.second < v2.second);
|
|
});
|
|
|
|
selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), [](const std::pair<int, int>& v1, const std::pair<int, int>& v2) {
|
|
return (v1.first == v2.first) && (v1.second == v2.second); }), selected_volumes.end());
|
|
|
|
// collects paths of files to load
|
|
std::vector<fs::path> input_paths;
|
|
std::vector<fs::path> missing_input_paths;
|
|
std::vector<std::pair<fs::path, fs::path>> replace_paths;
|
|
for (auto [obj_idx, vol_idx] : selected_volumes) {
|
|
const ModelObject* object = model.objects[obj_idx];
|
|
const ModelVolume* volume = object->volumes[vol_idx];
|
|
if (fs::exists(volume->source.input_file))
|
|
input_paths.push_back(volume->source.input_file);
|
|
else {
|
|
// searches the source in the same folder containing the object
|
|
bool found = false;
|
|
if (!object->input_file.empty()) {
|
|
fs::path object_path = fs::path(object->input_file).remove_filename();
|
|
if (!object_path.empty()) {
|
|
object_path /= fs::path(volume->source.input_file).filename();
|
|
if (fs::exists(object_path)) {
|
|
input_paths.push_back(object_path);
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
if (!found)
|
|
missing_input_paths.push_back(volume->source.input_file);
|
|
}
|
|
}
|
|
|
|
std::sort(missing_input_paths.begin(), missing_input_paths.end());
|
|
missing_input_paths.erase(std::unique(missing_input_paths.begin(), missing_input_paths.end()), missing_input_paths.end());
|
|
|
|
while (!missing_input_paths.empty()) {
|
|
// ask user to select the missing file
|
|
fs::path search = missing_input_paths.back();
|
|
wxString title = _L("Please select the file to reload");
|
|
#if defined(__APPLE__)
|
|
title += " (" + from_u8(search.filename().string()) + ")";
|
|
#endif // __APPLE__
|
|
title += ":";
|
|
wxFileDialog dialog(q, title, "", from_u8(search.filename().string()), file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
|
if (dialog.ShowModal() != wxID_OK)
|
|
return;
|
|
|
|
std::string sel_filename_path = dialog.GetPath().ToUTF8().data();
|
|
std::string sel_filename = fs::path(sel_filename_path).filename().string();
|
|
if (boost::algorithm::iequals(search.filename().string(), sel_filename)) {
|
|
input_paths.push_back(sel_filename_path);
|
|
missing_input_paths.pop_back();
|
|
|
|
fs::path sel_path = fs::path(sel_filename_path).remove_filename().string();
|
|
|
|
std::vector<fs::path>::iterator it = missing_input_paths.begin();
|
|
while (it != missing_input_paths.end()) {
|
|
// try to use the path of the selected file with all remaining missing files
|
|
fs::path repathed_filename = sel_path;
|
|
repathed_filename /= it->filename();
|
|
if (fs::exists(repathed_filename)) {
|
|
input_paths.push_back(repathed_filename.string());
|
|
it = missing_input_paths.erase(it);
|
|
}
|
|
else
|
|
++it;
|
|
}
|
|
}
|
|
else {
|
|
wxString message = _L("The selected file") + " (" + from_u8(sel_filename) + ") " +
|
|
_L("differs from the original file") + " (" + from_u8(search.filename().string()) + ").\n" + _L("Do you want to replace it") + " ?";
|
|
//wxMessageDialog dlg(q, message, wxMessageBoxCaptionStr, wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION);
|
|
MessageDialog dlg(q, message, wxMessageBoxCaptionStr, wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION);
|
|
if (dlg.ShowModal() == wxID_YES)
|
|
replace_paths.emplace_back(search, sel_filename_path);
|
|
|
|
missing_input_paths.pop_back();
|
|
}
|
|
}
|
|
|
|
std::sort(input_paths.begin(), input_paths.end());
|
|
input_paths.erase(std::unique(input_paths.begin(), input_paths.end()), input_paths.end());
|
|
|
|
std::sort(replace_paths.begin(), replace_paths.end());
|
|
replace_paths.erase(std::unique(replace_paths.begin(), replace_paths.end()), replace_paths.end());
|
|
|
|
Plater::TakeSnapshot snapshot(q, _L("Reload from disk"));
|
|
|
|
std::vector<wxString> fail_list;
|
|
|
|
Busy busy(_L("Reload from:"), q->get_current_canvas3D()->get_wxglcanvas());
|
|
|
|
// load one file at a time
|
|
for (size_t i = 0; i < input_paths.size(); ++i) {
|
|
const auto& path = input_paths[i].string();
|
|
|
|
busy.update(_L("Reload from:") + " " + from_u8(path));
|
|
|
|
Model new_model;
|
|
try
|
|
{
|
|
new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances);
|
|
for (ModelObject* model_object : new_model.objects) {
|
|
model_object->center_around_origin();
|
|
model_object->ensure_on_bed();
|
|
}
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
// error while loading
|
|
busy.reset();
|
|
GUI::show_error(q, e.what());
|
|
return;
|
|
}
|
|
|
|
// update the selected volumes whose source is the current file
|
|
for (auto [obj_idx, vol_idx] : selected_volumes) {
|
|
ModelObject* old_model_object = model.objects[obj_idx];
|
|
ModelVolume* old_volume = old_model_object->volumes[vol_idx];
|
|
|
|
bool sinking = old_model_object->min_z() < SINKING_Z_THRESHOLD;
|
|
|
|
bool has_source = !old_volume->source.input_file.empty() && boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string());
|
|
bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string());
|
|
if (has_source || has_name) {
|
|
int new_volume_idx = -1;
|
|
int new_object_idx = -1;
|
|
bool match_found = false;
|
|
// take idxs from the matching volume
|
|
if (has_source && old_volume->source.object_idx < int(new_model.objects.size())) {
|
|
const ModelObject* obj = new_model.objects[old_volume->source.object_idx];
|
|
if (old_volume->source.volume_idx < int(obj->volumes.size())) {
|
|
if (obj->volumes[old_volume->source.volume_idx]->name == old_volume->name) {
|
|
new_volume_idx = old_volume->source.volume_idx;
|
|
new_object_idx = old_volume->source.object_idx;
|
|
match_found = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!match_found && has_name) {
|
|
// take idxs from the 1st matching volume
|
|
for (size_t o = 0; o < new_model.objects.size(); ++o) {
|
|
ModelObject* obj = new_model.objects[o];
|
|
bool found = false;
|
|
for (size_t v = 0; v < obj->volumes.size(); ++v) {
|
|
if (obj->volumes[v]->name == old_volume->name) {
|
|
new_volume_idx = (int)v;
|
|
new_object_idx = (int)o;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (new_object_idx < 0 || int(new_model.objects.size()) <= new_object_idx) {
|
|
fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name));
|
|
continue;
|
|
}
|
|
ModelObject* new_model_object = new_model.objects[new_object_idx];
|
|
if (new_volume_idx < 0 || int(new_model_object->volumes.size()) <= new_volume_idx) {
|
|
fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name));
|
|
continue;
|
|
}
|
|
|
|
old_model_object->add_volume(*new_model_object->volumes[new_volume_idx]);
|
|
ModelVolume* new_volume = old_model_object->volumes.back();
|
|
new_volume->set_new_unique_id();
|
|
new_volume->config.apply(old_volume->config);
|
|
new_volume->set_type(old_volume->type());
|
|
new_volume->set_material_id(old_volume->material_id());
|
|
new_volume->set_transformation(
|
|
old_volume->get_transformation().get_matrix() *
|
|
old_volume->source.transform.get_matrix_no_offset() *
|
|
Geometry::translation_transform(new_volume->source.mesh_offset - old_volume->source.mesh_offset) *
|
|
new_volume->source.transform.get_matrix_no_offset().inverse()
|
|
);
|
|
new_volume->source.object_idx = old_volume->source.object_idx;
|
|
new_volume->source.volume_idx = old_volume->source.volume_idx;
|
|
assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters);
|
|
if (old_volume->source.is_converted_from_inches)
|
|
new_volume->convert_from_imperial_units();
|
|
else if (old_volume->source.is_converted_from_meters)
|
|
new_volume->convert_from_meters();
|
|
std::swap(old_model_object->volumes[vol_idx], old_model_object->volumes.back());
|
|
old_model_object->delete_volume(old_model_object->volumes.size() - 1);
|
|
if (!sinking)
|
|
old_model_object->ensure_on_bed();
|
|
old_model_object->sort_volumes(get_config_bool("order_volumes"));
|
|
|
|
sla::reproject_points_and_holes(old_model_object);
|
|
|
|
// Fix warning icon in object list
|
|
wxGetApp().obj_list()->update_item_error_icon(obj_idx, vol_idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
busy.reset();
|
|
|
|
for (auto [src, dest] : replace_paths) {
|
|
for (auto [obj_idx, vol_idx] : selected_volumes) {
|
|
if (boost::algorithm::iequals(model.objects[obj_idx]->volumes[vol_idx]->source.input_file, src.string()))
|
|
replace_volume_with_stl(obj_idx, vol_idx, dest, "");
|
|
}
|
|
}
|
|
|
|
if (!fail_list.empty()) {
|
|
wxString message = _L("Unable to reload:") + "\n";
|
|
for (const wxString& s : fail_list) {
|
|
message += s + "\n";
|
|
}
|
|
//wxMessageDialog dlg(q, message, _L("Error during reload"), wxOK | wxOK_DEFAULT | wxICON_WARNING);
|
|
MessageDialog dlg(q, message, _L("Error during reload"), wxOK | wxOK_DEFAULT | wxICON_WARNING);
|
|
dlg.ShowModal();
|
|
}
|
|
|
|
// update 3D scene
|
|
update();
|
|
|
|
// new GLVolumes have been created at this point, so update their printable state
|
|
for (size_t i = 0; i < model.objects.size(); ++i) {
|
|
view3D->get_canvas3d()->update_instance_printable_state_for_object(i);
|
|
}
|
|
}
|
|
|
|
void Plater::priv::reload_all_from_disk()
|
|
{
|
|
if (model.objects.empty())
|
|
return;
|
|
|
|
Plater::TakeSnapshot snapshot(q, _L("Reload all from disk"));
|
|
Plater::SuppressSnapshots suppress(q);
|
|
|
|
Selection& selection = get_selection();
|
|
Selection::IndicesList curr_idxs = selection.get_volume_idxs();
|
|
// reload from disk uses selection
|
|
select_all();
|
|
reload_from_disk();
|
|
// restore previous selection
|
|
selection.clear();
|
|
for (unsigned int idx : curr_idxs) {
|
|
selection.add(idx, false);
|
|
}
|
|
}
|
|
|
|
void Plater::priv::set_current_panel(wxPanel* panel)
|
|
{
|
|
if (std::find(panels.begin(), panels.end(), panel) == panels.end())
|
|
return;
|
|
|
|
#ifdef __WXMAC__
|
|
bool force_render = (current_panel != nullptr);
|
|
#endif // __WXMAC__
|
|
|
|
if (current_panel == panel)
|
|
return;
|
|
|
|
wxPanel* old_panel = current_panel;
|
|
current_panel = panel;
|
|
// to reduce flickering when changing view, first set as visible the new current panel
|
|
for (wxPanel* p : panels) {
|
|
if (p == current_panel) {
|
|
#ifdef __WXMAC__
|
|
// On Mac we need also to force a render to avoid flickering when changing view
|
|
if (force_render) {
|
|
if (p == view3D)
|
|
dynamic_cast<View3D*>(p)->get_canvas3d()->render();
|
|
else if (p == preview)
|
|
dynamic_cast<Preview*>(p)->get_canvas3d()->render();
|
|
}
|
|
#endif // __WXMAC__
|
|
p->Show();
|
|
}
|
|
}
|
|
// then set to invisible the other
|
|
for (wxPanel* p : panels) {
|
|
if (p != current_panel)
|
|
p->Hide();
|
|
}
|
|
|
|
panel_sizer->Layout();
|
|
|
|
if (current_panel == view3D) {
|
|
if (old_panel == preview)
|
|
preview->get_canvas3d()->unbind_event_handlers();
|
|
|
|
view3D->get_canvas3d()->bind_event_handlers();
|
|
|
|
if (view3D->is_reload_delayed()) {
|
|
// Delayed loading of the 3D scene.
|
|
if (printer_technology == ptSLA) {
|
|
// Update the SLAPrint from the current Model, so that the reload_scene()
|
|
// pulls the correct data.
|
|
update_restart_background_process(true, false);
|
|
} else
|
|
view3D->reload_scene(true);
|
|
}
|
|
|
|
// sets the canvas as dirty to force a render at the 1st idle event (wxWidgets IsShownOnScreen() is buggy and cannot be used reliably)
|
|
view3D->set_as_dirty();
|
|
// reset cached size to force a resize on next call to render() to keep imgui in synch with canvas size
|
|
view3D->get_canvas3d()->reset_old_size();
|
|
view_toolbar.select_item("3D");
|
|
if (notification_manager != nullptr)
|
|
notification_manager->set_in_preview(false);
|
|
}
|
|
else if (current_panel == preview) {
|
|
if (old_panel == view3D)
|
|
view3D->get_canvas3d()->unbind_event_handlers();
|
|
|
|
preview->get_canvas3d()->bind_event_handlers();
|
|
|
|
if (wxGetApp().is_editor()) {
|
|
// see: Plater::priv::object_list_changed()
|
|
// FIXME: it may be better to have a single function making this check and let it be called wherever needed
|
|
bool export_in_progress = this->background_process.is_export_scheduled();
|
|
bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside;
|
|
if (!model.objects.empty() && !export_in_progress && model_fits) {
|
|
preview->get_canvas3d()->init_gcode_viewer();
|
|
if (! this->background_process.finished())
|
|
preview->load_gcode_shells();
|
|
q->reslice();
|
|
}
|
|
// keeps current gcode preview, if any
|
|
preview->reload_print(true);
|
|
}
|
|
|
|
preview->set_as_dirty();
|
|
// reset cached size to force a resize on next call to render() to keep imgui in synch with canvas size
|
|
preview->get_canvas3d()->reset_old_size();
|
|
view_toolbar.select_item("Preview");
|
|
if (notification_manager != nullptr)
|
|
notification_manager->set_in_preview(true);
|
|
}
|
|
|
|
current_panel->SetFocusFromKbd();
|
|
}
|
|
|
|
void Plater::priv::on_slicing_update(SlicingStatusEvent &evt)
|
|
{
|
|
if (evt.status.percent >= -1) {
|
|
if (!m_worker.is_idle()) {
|
|
// Avoid a race condition
|
|
return;
|
|
}
|
|
notification_manager->set_slicing_progress_percentage(evt.status.text, (float)evt.status.percent / 100.0f);
|
|
}
|
|
|
|
// Check template filaments and add warning
|
|
// This is more convinient to do here than in slicing backend, so it happens on "Slicing complete".
|
|
if (evt.status.percent >= 100 && this->printer_technology == ptFFF) {
|
|
size_t templ_cnt = 0;
|
|
const auto& preset_bundle = wxGetApp().preset_bundle;
|
|
std::string names;
|
|
for (const auto& extruder_filaments : preset_bundle->extruders_filaments) {
|
|
const Preset* preset = extruder_filaments.get_selected_preset();
|
|
if (preset && preset->vendor && preset->vendor->templates_profile) {
|
|
names += "\n" + preset->name;
|
|
templ_cnt++;
|
|
}
|
|
}
|
|
if (templ_cnt > 0) {
|
|
const std::string message_notif = GUI::format("%1%\n%2%\n\n%3%\n\n%4% "
|
|
, _L_PLURAL("You are using template filament preset.", "You are using template filament presets.", templ_cnt)
|
|
, names
|
|
, _u8L("Please note that template presets are not customized for specific printer and should only be used as a starting point for creating your own user presets.")
|
|
,_u8L("More info at"));
|
|
// warning dialog proccessing cuts text at first '/n' - pass the text without new lines (and without filament names).
|
|
const std::string message_dial = GUI::format("%1% %2% %3%"
|
|
, _L_PLURAL("You are using template filament preset.", "You are using template filament presets.", templ_cnt)
|
|
, _u8L("Please note that template presets are not customized for specific printer and should only be used as a starting point for creating your own user presets.")
|
|
, "<a href=https://help.prusa3d.com/article/template-filaments_467599>https://help.prusa3d.com/</a>"
|
|
);
|
|
BOOST_LOG_TRIVIAL(warning) << message_notif;
|
|
notification_manager->push_slicing_warning_notification(message_notif, false, 0, 0, "https://help.prusa3d.com/",
|
|
[](wxEvtHandler* evnthndlr) { wxGetApp().open_browser_with_warning_dialog("https://help.prusa3d.com/article/template-filaments_467599"); return false; }
|
|
);
|
|
add_warning({ PrintStateBase::WarningLevel::CRITICAL, true, message_dial, 0}, 0);
|
|
}
|
|
}
|
|
|
|
if (evt.status.flags & (PrintBase::SlicingStatus::RELOAD_SCENE | PrintBase::SlicingStatus::RELOAD_SLA_SUPPORT_POINTS)) {
|
|
switch (this->printer_technology) {
|
|
case ptFFF:
|
|
this->update_fff_scene();
|
|
break;
|
|
case ptSLA:
|
|
// If RELOAD_SLA_SUPPORT_POINTS, then the SLA gizmo is updated (reload_scene calls update_gizmos_data)
|
|
if (view3D->is_dragging())
|
|
delayed_scene_refresh = true;
|
|
else {
|
|
view3D->get_canvas3d()->enable_sla_view_type_detection();
|
|
this->update_sla_scene();
|
|
}
|
|
break;
|
|
default: break;
|
|
}
|
|
} else if (evt.status.flags & PrintBase::SlicingStatus::RELOAD_SLA_PREVIEW) {
|
|
// Update the SLA preview. Only called if not RELOAD_SLA_SUPPORT_POINTS, as the block above will refresh the preview anyways.
|
|
this->preview->reload_print();
|
|
}
|
|
|
|
if ((evt.status.flags & PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS) &&
|
|
static_cast<PrintStep>(evt.status.warning_step) == psAlertWhenSupportsNeeded &&
|
|
!get_app_config()->get_bool("alert_when_supports_needed")) {
|
|
// This alerts are from psAlertWhenSupportsNeeded and the respective app settings is not Enabled, so discard the alerts.
|
|
} else if (evt.status.flags &
|
|
(PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS | PrintBase::SlicingStatus::UPDATE_PRINT_OBJECT_STEP_WARNINGS)) {
|
|
// Update notification center with warnings of object_id and its warning_step.
|
|
ObjectID object_id = evt.status.warning_object_id;
|
|
int warning_step = evt.status.warning_step;
|
|
PrintStateBase::StateWithWarnings state;
|
|
if (evt.status.flags & PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS) {
|
|
state = this->printer_technology == ptFFF ?
|
|
this->fff_print.step_state_with_warnings(static_cast<PrintStep>(warning_step)) :
|
|
this->sla_print.step_state_with_warnings(static_cast<SLAPrintStep>(warning_step));
|
|
} else if (this->printer_technology == ptFFF) {
|
|
const PrintObject *print_object = this->fff_print.get_object(object_id);
|
|
if (print_object)
|
|
state = print_object->step_state_with_warnings(static_cast<PrintObjectStep>(warning_step));
|
|
} else {
|
|
const SLAPrintObject *print_object = this->sla_print.get_object(object_id);
|
|
if (print_object)
|
|
state = print_object->step_state_with_warnings(static_cast<SLAPrintObjectStep>(warning_step));
|
|
}
|
|
// Now process state.warnings.
|
|
for (auto const& warning : state.warnings) {
|
|
if (warning.current) {
|
|
notification_manager->push_slicing_warning_notification(warning.message, false, object_id, warning_step);
|
|
add_warning(warning, object_id.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Plater::priv::on_slicing_completed(wxCommandEvent & evt)
|
|
{
|
|
if (view3D->is_dragging()) // updating scene now would interfere with the gizmo dragging
|
|
delayed_scene_refresh = true;
|
|
else {
|
|
if (this->printer_technology == ptFFF)
|
|
this->update_fff_scene();
|
|
else
|
|
this->update_sla_scene();
|
|
}
|
|
}
|
|
|
|
void Plater::priv::on_export_began(wxCommandEvent& evt)
|
|
{
|
|
if (show_warning_dialog)
|
|
warnings_dialog();
|
|
}
|
|
void Plater::priv::on_slicing_began()
|
|
{
|
|
clear_warnings();
|
|
notification_manager->close_notification_of_type(NotificationType::SignDetected);
|
|
notification_manager->close_notification_of_type(NotificationType::ExportFinished);
|
|
notification_manager->set_slicing_progress_began();
|
|
}
|
|
void Plater::priv::add_warning(const Slic3r::PrintStateBase::Warning& warning, size_t oid)
|
|
{
|
|
for (auto const& it : current_warnings) {
|
|
if (warning.message_id == it.first.message_id) {
|
|
if (warning.message_id != 0 || (warning.message_id == 0 && warning.message == it.first.message))
|
|
return;
|
|
}
|
|
}
|
|
current_warnings.emplace_back(std::pair<Slic3r::PrintStateBase::Warning, size_t>(warning, oid));
|
|
}
|
|
void Plater::priv::actualize_slicing_warnings(const PrintBase &print)
|
|
{
|
|
std::vector<ObjectID> ids = print.print_object_ids();
|
|
if (ids.empty()) {
|
|
clear_warnings();
|
|
return;
|
|
}
|
|
ids.emplace_back(print.id());
|
|
std::sort(ids.begin(), ids.end());
|
|
notification_manager->remove_slicing_warnings_of_released_objects(ids);
|
|
notification_manager->set_all_slicing_warnings_gray(true);
|
|
}
|
|
void Plater::priv::actualize_object_warnings(const PrintBase& print)
|
|
{
|
|
std::vector<ObjectID> ids;
|
|
for (const ModelObject* object : print.model().objects )
|
|
{
|
|
ids.push_back(object->id());
|
|
}
|
|
std::sort(ids.begin(), ids.end());
|
|
notification_manager->remove_simplify_suggestion_of_released_objects(ids);
|
|
}
|
|
void Plater::priv::clear_warnings()
|
|
{
|
|
notification_manager->close_slicing_errors_and_warnings();
|
|
this->current_warnings.clear();
|
|
}
|
|
bool Plater::priv::warnings_dialog()
|
|
{
|
|
std::vector<std::pair<Slic3r::PrintStateBase::Warning, size_t>> current_critical_warnings{};
|
|
for (const auto& w : current_warnings) {
|
|
if (w.first.level == PrintStateBase::WarningLevel::CRITICAL) {
|
|
current_critical_warnings.push_back(w);
|
|
}
|
|
}
|
|
|
|
if (current_critical_warnings.empty())
|
|
return true;
|
|
std::string text = _u8L("There are active warnings concerning sliced models:") + "\n";
|
|
for (auto const& it : current_critical_warnings) {
|
|
size_t next_n = it.first.message.find_first_of('\n', 0);
|
|
text += "\n";
|
|
if (next_n != std::string::npos)
|
|
text += it.first.message.substr(0, next_n);
|
|
else
|
|
text += it.first.message;
|
|
}
|
|
//MessageDialog msg_wingow(this->q, from_u8(text), wxString(SLIC3R_APP_NAME " ") + _L("generated warnings"), wxOK);
|
|
// Changed to InfoDialog so it can show hyperlinks
|
|
InfoDialog msg_wingow(this->q, format_wxstr("%1% %2%", SLIC3R_APP_NAME, _L("generated warnings")), from_u8(text), true);
|
|
const auto res = msg_wingow.ShowModal();
|
|
return res == wxID_OK;
|
|
|
|
}
|
|
void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
|
{
|
|
// Stop the background task, wait until the thread goes into the "Idle" state.
|
|
// At this point of time the thread should be either finished or canceled,
|
|
// so the following call just confirms, that the produced data were consumed.
|
|
this->background_process.stop();
|
|
notification_manager->set_slicing_progress_export_possible();
|
|
|
|
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
|
|
this->background_process.reset_export();
|
|
// This bool stops showing export finished notification even when process_completed_with_error is false
|
|
bool has_error = false;
|
|
if (evt.error()) {
|
|
std::pair<std::string, bool> message = evt.format_error_message();
|
|
if (evt.critical_error()) {
|
|
if (q->m_tracking_popup_menu) {
|
|
// We don't want to pop-up a message box when tracking a pop-up menu.
|
|
// We postpone the error message instead.
|
|
q->m_tracking_popup_menu_error_message = message.first;
|
|
} else {
|
|
show_error(q, message.first, message.second);
|
|
notification_manager->set_slicing_progress_hidden();
|
|
notification_manager->stop_delayed_notifications_of_type(NotificationType::ExportOngoing);
|
|
}
|
|
} else
|
|
notification_manager->push_slicing_error_notification(message.first);
|
|
if (evt.invalidate_plater())
|
|
{
|
|
const wxString invalid_str = _L("Invalid data");
|
|
for (auto btn : { ActionButtonType::Reslice, ActionButtonType::SendGCode, ActionButtonType::Export })
|
|
sidebar->set_btn_label(btn, invalid_str);
|
|
process_completed_with_error = true;
|
|
}
|
|
has_error = true;
|
|
}
|
|
if (evt.cancelled()) {
|
|
this->notification_manager->set_slicing_progress_canceled(_u8L("Slicing Cancelled."));
|
|
}
|
|
|
|
this->sidebar->show_sliced_info_sizer(evt.success());
|
|
|
|
// This updates the "Slice now", "Export G-code", "Arrange" buttons status.
|
|
// Namely, it refreshes the "Out of print bed" property of all the ModelObjects, and it enables
|
|
// the "Slice now" and "Export G-code" buttons based on their "out of bed" status.
|
|
this->object_list_changed();
|
|
|
|
// refresh preview
|
|
if (view3D->is_dragging()) // updating scene now would interfere with the gizmo dragging
|
|
delayed_scene_refresh = true;
|
|
else {
|
|
if (this->printer_technology == ptFFF)
|
|
this->update_fff_scene();
|
|
else
|
|
this->update_sla_scene();
|
|
}
|
|
|
|
if (evt.cancelled()) {
|
|
if (wxGetApp().get_mode() == comSimple)
|
|
sidebar->set_btn_label(ActionButtonType::Reslice, "Slice now");
|
|
show_action_buttons(true);
|
|
} else {
|
|
if(wxGetApp().get_mode() == comSimple) {
|
|
show_action_buttons(false);
|
|
}
|
|
if (exporting_status != ExportingStatus::NOT_EXPORTING && !has_error) {
|
|
notification_manager->stop_delayed_notifications_of_type(NotificationType::ExportOngoing);
|
|
notification_manager->close_notification_of_type(NotificationType::ExportOngoing);
|
|
}
|
|
// If writing to removable drive was scheduled, show notification with eject button
|
|
if (exporting_status == ExportingStatus::EXPORTING_TO_REMOVABLE && !has_error) {
|
|
show_action_buttons(false);
|
|
notification_manager->push_exporting_finished_notification(last_output_path, last_output_dir_path,
|
|
// Don't offer the "Eject" button on ChromeOS, the Linux side has no control over it.
|
|
platform_flavor() != PlatformFlavor::LinuxOnChromium);
|
|
wxGetApp().removable_drive_manager()->set_exporting_finished(true);
|
|
}else if (exporting_status == ExportingStatus::EXPORTING_TO_LOCAL && !has_error)
|
|
notification_manager->push_exporting_finished_notification(last_output_path, last_output_dir_path, false);
|
|
}
|
|
exporting_status = ExportingStatus::NOT_EXPORTING;
|
|
}
|
|
|
|
void Plater::priv::on_layer_editing_toggled(bool enable)
|
|
{
|
|
view3D->enable_layers_editing(enable);
|
|
view3D->set_as_dirty();
|
|
}
|
|
|
|
void Plater::priv::on_action_add(SimpleEvent&)
|
|
{
|
|
if (q != nullptr)
|
|
q->add_model();
|
|
}
|
|
|
|
void Plater::priv::on_action_split_objects(SimpleEvent&)
|
|
{
|
|
split_object();
|
|
}
|
|
|
|
void Plater::priv::on_action_split_volumes(SimpleEvent&)
|
|
{
|
|
split_volume();
|
|
}
|
|
|
|
void Plater::priv::on_action_layersediting(SimpleEvent&)
|
|
{
|
|
const bool enable_layersediting = !view3D->is_layers_editing_enabled();
|
|
view3D->enable_layers_editing(enable_layersediting);
|
|
if (enable_layersediting)
|
|
view3D->get_canvas3d()->reset_all_gizmos();
|
|
notification_manager->set_move_from_overlay(view3D->is_layers_editing_enabled());
|
|
}
|
|
|
|
void Plater::priv::on_object_select(SimpleEvent& evt)
|
|
{
|
|
if (auto obj_list = wxGetApp().obj_list())
|
|
obj_list->update_selections();
|
|
else
|
|
return;
|
|
selection_changed();
|
|
}
|
|
|
|
void Plater::priv::on_right_click(RBtnEvent& evt)
|
|
{
|
|
int obj_idx = get_selected_object_idx();
|
|
|
|
wxMenu* menu = nullptr;
|
|
|
|
if (obj_idx == -1) { // no one or several object are selected
|
|
if (evt.data.second) { // right button was clicked on empty space
|
|
if (!get_selection().is_empty()) // several objects are selected in 3DScene
|
|
return;
|
|
menu = menus.default_menu();
|
|
}
|
|
else
|
|
menu = menus.multi_selection_menu();
|
|
}
|
|
else {
|
|
// If in 3DScene is(are) selected volume(s), but right button was clicked on empty space
|
|
if (evt.data.second)
|
|
return;
|
|
|
|
// Each context menu respects to the selected item in ObjectList,
|
|
// so this selection should be updated before menu creation
|
|
wxGetApp().obj_list()->update_selections();
|
|
|
|
// if (printer_technology == ptSLA)
|
|
// menu = menus.sla_object_menu();
|
|
// else {
|
|
const Selection& selection = get_selection();
|
|
// show "Object menu" for each one or several FullInstance instead of FullObject
|
|
const bool is_some_full_instances = selection.is_single_full_instance() ||
|
|
selection.is_single_full_object() ||
|
|
selection.is_multiple_full_instance();
|
|
const bool is_part = selection.is_single_volume_or_modifier() && ! selection.is_any_connector();
|
|
if (is_some_full_instances)
|
|
menu = printer_technology == ptSLA ? menus.sla_object_menu() : menus.object_menu();
|
|
else if (is_part) {
|
|
const GLVolume* gl_volume = selection.get_first_volume();
|
|
const ModelVolume *model_volume = get_model_volume(*gl_volume, selection.get_model()->objects);
|
|
menu = (model_volume != nullptr && model_volume->is_text()) ? menus.text_part_menu() :
|
|
(model_volume != nullptr && model_volume->is_svg()) ? menus.svg_part_menu() :
|
|
menus.part_menu();
|
|
} else
|
|
menu = menus.multi_selection_menu();
|
|
// }
|
|
}
|
|
|
|
if (q != nullptr && menu) {
|
|
Vec2d mouse_position = evt.data.first;
|
|
wxPoint position(static_cast<int>(mouse_position.x()),
|
|
static_cast<int>(mouse_position.y()));
|
|
#ifdef __linux__
|
|
// For some reason on Linux the menu isn't displayed if position is
|
|
// specified (even though the position is sane).
|
|
position = wxDefaultPosition;
|
|
#endif
|
|
GLCanvas3D &canvas = *q->canvas3D();
|
|
canvas.apply_retina_scale(mouse_position);
|
|
canvas.set_popup_menu_position(mouse_position);
|
|
q->PopupMenu(menu, position);
|
|
canvas.clear_popup_menu_position();
|
|
}
|
|
}
|
|
|
|
void Plater::priv::on_wipetower_moved(Vec3dEvent &evt)
|
|
{
|
|
DynamicPrintConfig cfg;
|
|
cfg.opt<ConfigOptionFloat>("wipe_tower_x", true)->value = evt.data(0);
|
|
cfg.opt<ConfigOptionFloat>("wipe_tower_y", true)->value = evt.data(1);
|
|
wxGetApp().get_tab(Preset::TYPE_PRINT)->load_config(cfg);
|
|
}
|
|
|
|
void Plater::priv::on_wipetower_rotated(Vec3dEvent& evt)
|
|
{
|
|
DynamicPrintConfig cfg;
|
|
cfg.opt<ConfigOptionFloat>("wipe_tower_x", true)->value = evt.data(0);
|
|
cfg.opt<ConfigOptionFloat>("wipe_tower_y", true)->value = evt.data(1);
|
|
cfg.opt<ConfigOptionFloat>("wipe_tower_rotation_angle", true)->value = Geometry::rad2deg(evt.data(2));
|
|
wxGetApp().get_tab(Preset::TYPE_PRINT)->load_config(cfg);
|
|
}
|
|
|
|
void Plater::priv::on_update_geometry(Vec3dsEvent<2>&)
|
|
{
|
|
// TODO
|
|
}
|
|
|
|
void Plater::priv::on_3dcanvas_mouse_dragging_started(SimpleEvent&)
|
|
{
|
|
}
|
|
|
|
// Update the scene from the background processing,
|
|
// if the update message was received during mouse manipulation.
|
|
void Plater::priv::on_3dcanvas_mouse_dragging_finished(SimpleEvent&)
|
|
{
|
|
if (delayed_scene_refresh) {
|
|
delayed_scene_refresh = false;
|
|
update_sla_scene();
|
|
}
|
|
}
|
|
|
|
void Plater::priv::generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, Camera::EType camera_type)
|
|
{
|
|
view3D->get_canvas3d()->render_thumbnail(data, w, h, thumbnail_params, camera_type);
|
|
}
|
|
|
|
ThumbnailsList Plater::priv::generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type)
|
|
{
|
|
ThumbnailsList thumbnails;
|
|
for (const Vec2d& size : params.sizes) {
|
|
thumbnails.push_back(ThumbnailData());
|
|
Point isize(size); // round to ints
|
|
generate_thumbnail(thumbnails.back(), isize.x(), isize.y(), params, camera_type);
|
|
if (!thumbnails.back().is_valid())
|
|
thumbnails.pop_back();
|
|
}
|
|
return thumbnails;
|
|
}
|
|
|
|
wxString Plater::priv::get_project_filename(const wxString& extension) const
|
|
{
|
|
return m_project_filename.empty() ? "" : m_project_filename + extension;
|
|
}
|
|
|
|
void Plater::priv::set_project_filename(const wxString& filename)
|
|
{
|
|
boost::filesystem::path full_path = into_path(filename);
|
|
boost::filesystem::path ext = full_path.extension();
|
|
if (boost::iequals(ext.string(), ".amf")) {
|
|
// Remove the first extension.
|
|
full_path.replace_extension("");
|
|
// It may be ".zip.amf".
|
|
if (boost::iequals(full_path.extension().string(), ".zip"))
|
|
// Remove the 2nd extension.
|
|
full_path.replace_extension("");
|
|
} else {
|
|
// Remove just one extension.
|
|
full_path.replace_extension("");
|
|
}
|
|
|
|
m_project_filename = from_path(full_path);
|
|
wxGetApp().mainframe->update_title();
|
|
|
|
const fs::path temp_path = wxStandardPaths::Get().GetTempDir().utf8_str().data();
|
|
bool in_temp = (temp_path == full_path.parent_path().make_preferred());
|
|
|
|
if (!filename.empty() && !in_temp)
|
|
wxGetApp().mainframe->add_to_recent_projects(filename);
|
|
}
|
|
|
|
void Plater::priv::init_notification_manager()
|
|
{
|
|
if (!notification_manager)
|
|
return;
|
|
notification_manager->init();
|
|
|
|
auto cancel_callback = [this]() {
|
|
if (this->background_process.idle())
|
|
return false;
|
|
this->background_process.stop();
|
|
return true;
|
|
};
|
|
notification_manager->init_slicing_progress_notification(cancel_callback);
|
|
notification_manager->set_fff(printer_technology == ptFFF);
|
|
notification_manager->init_progress_indicator();
|
|
}
|
|
|
|
void Plater::priv::set_current_canvas_as_dirty()
|
|
{
|
|
if (current_panel == view3D)
|
|
view3D->set_as_dirty();
|
|
else if (current_panel == preview)
|
|
preview->set_as_dirty();
|
|
}
|
|
|
|
GLCanvas3D* Plater::priv::get_current_canvas3D()
|
|
{
|
|
return (current_panel == view3D) ? view3D->get_canvas3d() : ((current_panel == preview) ? preview->get_canvas3d() : nullptr);
|
|
}
|
|
|
|
void Plater::priv::unbind_canvas_event_handlers()
|
|
{
|
|
if (view3D != nullptr)
|
|
view3D->get_canvas3d()->unbind_event_handlers();
|
|
|
|
if (preview != nullptr)
|
|
preview->get_canvas3d()->unbind_event_handlers();
|
|
}
|
|
|
|
void Plater::priv::reset_canvas_volumes()
|
|
{
|
|
if (view3D != nullptr)
|
|
view3D->get_canvas3d()->reset_volumes();
|
|
|
|
if (preview != nullptr)
|
|
preview->get_canvas3d()->reset_volumes();
|
|
}
|
|
|
|
bool Plater::priv::init_view_toolbar()
|
|
{
|
|
if (wxGetApp().is_gcode_viewer())
|
|
return true;
|
|
|
|
if (view_toolbar.get_items_count() > 0)
|
|
// already initialized
|
|
return true;
|
|
|
|
BackgroundTexture::Metadata background_data;
|
|
background_data.filename = "toolbar_background.png";
|
|
background_data.left = 16;
|
|
background_data.top = 16;
|
|
background_data.right = 16;
|
|
background_data.bottom = 16;
|
|
|
|
if (!view_toolbar.init(background_data))
|
|
return false;
|
|
|
|
view_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Left);
|
|
view_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Bottom);
|
|
view_toolbar.set_border(5.0f);
|
|
view_toolbar.set_gap_size(1.0f);
|
|
|
|
GLToolbarItem::Data item;
|
|
|
|
item.name = "3D";
|
|
item.icon_filename = "editor.svg";
|
|
item.tooltip = _u8L("3D editor view") + " [" + GUI::shortkey_ctrl_prefix() + "5]";
|
|
item.sprite_id = 0;
|
|
item.left.action_callback = [this]() { if (this->q != nullptr) wxPostEvent(this->q, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); };
|
|
if (!view_toolbar.add_item(item))
|
|
return false;
|
|
|
|
item.name = "Preview";
|
|
item.icon_filename = "preview.svg";
|
|
item.tooltip = _u8L("Preview") + " [" + GUI::shortkey_ctrl_prefix() + "6]";
|
|
item.sprite_id = 1;
|
|
item.left.action_callback = [this]() { if (this->q != nullptr) wxPostEvent(this->q, SimpleEvent(EVT_GLVIEWTOOLBAR_PREVIEW)); };
|
|
if (!view_toolbar.add_item(item))
|
|
return false;
|
|
|
|
view_toolbar.select_item("3D");
|
|
view_toolbar.set_enabled(true);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Plater::priv::init_collapse_toolbar()
|
|
{
|
|
if (wxGetApp().is_gcode_viewer())
|
|
return true;
|
|
|
|
if (collapse_toolbar.get_items_count() > 0)
|
|
// already initialized
|
|
return true;
|
|
|
|
BackgroundTexture::Metadata background_data;
|
|
background_data.filename = "toolbar_background.png";
|
|
background_data.left = 16;
|
|
background_data.top = 16;
|
|
background_data.right = 16;
|
|
background_data.bottom = 16;
|
|
|
|
if (!collapse_toolbar.init(background_data))
|
|
return false;
|
|
|
|
collapse_toolbar.set_layout_type(GLToolbar::Layout::Vertical);
|
|
collapse_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Right);
|
|
collapse_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top);
|
|
collapse_toolbar.set_border(5.0f);
|
|
collapse_toolbar.set_separator_size(5);
|
|
collapse_toolbar.set_gap_size(2);
|
|
|
|
GLToolbarItem::Data item;
|
|
|
|
item.name = "collapse_sidebar";
|
|
item.icon_filename = "collapse.svg";
|
|
item.sprite_id = 0;
|
|
item.left.action_callback = []() {
|
|
wxGetApp().plater()->collapse_sidebar(!wxGetApp().plater()->is_sidebar_collapsed());
|
|
};
|
|
|
|
if (!collapse_toolbar.add_item(item))
|
|
return false;
|
|
|
|
// Now "collapse" sidebar to current state. This is done so the tooltip
|
|
// is updated before the toolbar is first used.
|
|
wxGetApp().plater()->collapse_sidebar(wxGetApp().plater()->is_sidebar_collapsed());
|
|
return true;
|
|
}
|
|
|
|
void Plater::priv::set_preview_layers_slider_values_range(int bottom, int top)
|
|
{
|
|
preview->set_layers_slider_values_range(bottom, top);
|
|
}
|
|
|
|
void Plater::priv::update_preview_moves_slider()
|
|
{
|
|
preview->update_moves_slider();
|
|
}
|
|
|
|
void Plater::priv::enable_preview_moves_slider(bool enable)
|
|
{
|
|
preview->enable_moves_slider(enable);
|
|
}
|
|
|
|
void Plater::priv::reset_gcode_toolpaths()
|
|
{
|
|
preview->get_canvas3d()->reset_gcode_toolpaths();
|
|
}
|
|
|
|
bool Plater::priv::can_set_instance_to_object() const
|
|
{
|
|
const int obj_idx = get_selected_object_idx();
|
|
return 0 <= obj_idx && obj_idx < (int)model.objects.size() && model.objects[obj_idx]->instances.size() > 1;
|
|
}
|
|
|
|
bool Plater::priv::can_split(bool to_objects) const
|
|
{
|
|
return sidebar->obj_list()->is_splittable(to_objects);
|
|
}
|
|
|
|
bool Plater::priv::can_scale_to_print_volume() const
|
|
{
|
|
const BuildVolume::Type type = this->bed.build_volume().type();
|
|
return !sidebar->obj_list()->has_selected_cut_object() &&
|
|
!view3D->get_canvas3d()->get_selection().is_empty() && (type == BuildVolume::Type::Rectangle || type == BuildVolume::Type::Circle);
|
|
}
|
|
|
|
bool Plater::priv::layers_height_allowed() const
|
|
{
|
|
if (printer_technology != ptFFF)
|
|
return false;
|
|
|
|
int obj_idx = get_selected_object_idx();
|
|
return 0 <= obj_idx && obj_idx < (int)model.objects.size() && model.objects[obj_idx]->max_z() > SINKING_Z_THRESHOLD &&
|
|
config->opt_bool("variable_layer_height") && view3D->is_layers_editing_allowed();
|
|
}
|
|
|
|
bool Plater::priv::can_mirror() const
|
|
{
|
|
return !sidebar->obj_list()->has_selected_cut_object();
|
|
}
|
|
|
|
|
|
bool Plater::priv::can_replace_with_stl() const
|
|
{
|
|
return !sidebar->obj_list()->has_selected_cut_object() && get_selection().get_volume_idxs().size() == 1;
|
|
}
|
|
|
|
bool Plater::priv::can_reload_from_disk() const
|
|
{
|
|
if (sidebar->obj_list()->has_selected_cut_object())
|
|
return false;
|
|
|
|
// collect selected reloadable ModelVolumes
|
|
std::vector<std::pair<int, int>> selected_volumes = reloadable_volumes(model, get_selection());
|
|
// nothing to reload, return
|
|
if (selected_volumes.empty())
|
|
return false;
|
|
|
|
std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair<int, int>& v1, const std::pair<int, int>& v2) {
|
|
return (v1.first < v2.first) || (v1.first == v2.first && v1.second < v2.second);
|
|
});
|
|
selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), [](const std::pair<int, int>& v1, const std::pair<int, int>& v2) {
|
|
return (v1.first == v2.first) && (v1.second == v2.second); }), selected_volumes.end());
|
|
|
|
// collects paths of files to load
|
|
std::vector<fs::path> paths;
|
|
for (auto [obj_idx, vol_idx] : selected_volumes) {
|
|
paths.push_back(model.objects[obj_idx]->volumes[vol_idx]->source.input_file);
|
|
}
|
|
|
|
std::sort(paths.begin(), paths.end());
|
|
paths.erase(std::unique(paths.begin(), paths.end()), paths.end());
|
|
|
|
return !paths.empty();
|
|
}
|
|
|
|
void Plater::priv::set_bed_shape(const Pointfs& shape, const double max_print_height, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom)
|
|
{
|
|
bool new_shape = bed.set_shape(shape, max_print_height, custom_texture, custom_model, force_as_custom);
|
|
if (new_shape) {
|
|
if (view3D) view3D->bed_shape_changed();
|
|
if (preview) preview->bed_shape_changed();
|
|
}
|
|
}
|
|
|
|
bool Plater::priv::can_delete() const
|
|
{
|
|
return !get_selection().is_empty() && !get_selection().is_wipe_tower() && !sidebar->obj_list()->is_editing();
|
|
}
|
|
|
|
bool Plater::priv::can_delete_all() const
|
|
{
|
|
return !model.objects.empty() && !sidebar->obj_list()->is_editing();
|
|
}
|
|
|
|
bool Plater::priv::can_fix_through_winsdk() const
|
|
{
|
|
std::vector<int> obj_idxs, vol_idxs;
|
|
sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs);
|
|
|
|
#if FIX_THROUGH_WINSDK_ALWAYS
|
|
// Fixing always.
|
|
return ! obj_idxs.empty() || ! vol_idxs.empty();
|
|
#else // FIX_THROUGH_WINSDK_ALWAYS
|
|
// Fixing only if the model is not manifold.
|
|
if (vol_idxs.empty()) {
|
|
for (auto obj_idx : obj_idxs)
|
|
if (model.objects[obj_idx]->get_repaired_errors_count() > 0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
int obj_idx = obj_idxs.front();
|
|
for (auto vol_idx : vol_idxs)
|
|
if (model.objects[obj_idx]->get_repaired_errors_count(vol_idx) > 0)
|
|
return true;
|
|
return false;
|
|
#endif // FIX_THROUGH_WINSDK_ALWAYS
|
|
}
|
|
|
|
bool Plater::priv::can_simplify() const
|
|
{
|
|
const int obj_idx = get_selected_object_idx();
|
|
// is object for simplification selected
|
|
// cut object can't be simplify
|
|
if (obj_idx < 0 || model.objects[obj_idx]->is_cut())
|
|
return false;
|
|
|
|
// is already opened?
|
|
if (q->canvas3D()->get_gizmos_manager().get_current_type() ==
|
|
GLGizmosManager::EType::Simplify)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool Plater::priv::can_increase_instances() const
|
|
{
|
|
if (!m_worker.is_idle()
|
|
|| q->canvas3D()->get_gizmos_manager().is_in_editing_mode())
|
|
return false;
|
|
|
|
// Disallow arrange and add instance when emboss gizmo is opend
|
|
// Prevent strobo effect during editing emboss parameters.
|
|
if (q->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Emboss) return false;
|
|
|
|
const auto obj_idxs = get_selection().get_object_idxs();
|
|
return !obj_idxs.empty() && !get_selection().is_wipe_tower() && !sidebar->obj_list()->has_selected_cut_object();
|
|
}
|
|
|
|
bool Plater::priv::can_decrease_instances(int obj_idx /*= -1*/) const
|
|
{
|
|
if (!m_worker.is_idle()
|
|
|| q->canvas3D()->get_gizmos_manager().is_in_editing_mode())
|
|
return false;
|
|
|
|
if (obj_idx < 0)
|
|
obj_idx = get_selected_object_idx();
|
|
|
|
if (obj_idx < 0) {
|
|
if (const auto obj_ids = get_selection().get_object_idxs(); !obj_ids.empty())
|
|
for (const size_t obj_id : obj_ids)
|
|
if (can_decrease_instances(obj_id))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
return obj_idx < (int)model.objects.size() &&
|
|
(model.objects[obj_idx]->instances.size() > 1) &&
|
|
!sidebar->obj_list()->has_selected_cut_object();
|
|
}
|
|
|
|
bool Plater::priv::can_split_to_objects() const
|
|
{
|
|
return q->can_split(true);
|
|
}
|
|
|
|
bool Plater::priv::can_split_to_volumes() const
|
|
{
|
|
return q->can_split(false);
|
|
}
|
|
|
|
bool Plater::priv::can_arrange() const
|
|
{
|
|
if (model.objects.empty() || !m_worker.is_idle()) return false;
|
|
return q->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Undefined;
|
|
}
|
|
|
|
bool Plater::priv::can_layers_editing() const
|
|
{
|
|
return layers_height_allowed();
|
|
}
|
|
|
|
void Plater::priv::show_action_buttons(const bool ready_to_slice_) const
|
|
{
|
|
// Cache this value, so that the callbacks from the RemovableDriveManager may repeat that value when calling show_action_buttons().
|
|
this->ready_to_slice = ready_to_slice_;
|
|
|
|
wxWindowUpdateLocker noUpdater(sidebar);
|
|
|
|
DynamicPrintConfig* selected_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config();
|
|
const auto print_host_opt = selected_printer_config ? selected_printer_config->option<ConfigOptionString>("print_host") : nullptr;
|
|
const bool send_gcode_shown = print_host_opt != nullptr && !print_host_opt->value.empty();
|
|
const bool connect_gcode_shown = print_host_opt == nullptr && user_account->is_logged();
|
|
// when a background processing is ON, export_btn and/or send_btn are showing
|
|
if (get_config_bool("background_processing"))
|
|
{
|
|
RemovableDriveManager::RemovableDrivesStatus removable_media_status = wxGetApp().removable_drive_manager()->status();
|
|
if (sidebar->show_reslice(false) |
|
|
sidebar->show_export(true) |
|
|
sidebar->show_send(send_gcode_shown) |
|
|
sidebar->show_connect(connect_gcode_shown) |
|
|
sidebar->show_export_removable(removable_media_status.has_removable_drives))
|
|
sidebar->Layout();
|
|
}
|
|
else
|
|
{
|
|
RemovableDriveManager::RemovableDrivesStatus removable_media_status;
|
|
if (! ready_to_slice)
|
|
removable_media_status = wxGetApp().removable_drive_manager()->status();
|
|
if (sidebar->show_reslice(ready_to_slice) |
|
|
sidebar->show_export(!ready_to_slice) |
|
|
sidebar->show_send(send_gcode_shown && !ready_to_slice) |
|
|
sidebar->show_connect(connect_gcode_shown && !ready_to_slice) |
|
|
sidebar->show_export_removable(!ready_to_slice && removable_media_status.has_removable_drives))
|
|
sidebar->Layout();
|
|
}
|
|
}
|
|
|
|
void Plater::priv::enter_gizmos_stack()
|
|
{
|
|
assert(m_undo_redo_stack_active == &m_undo_redo_stack_main);
|
|
if (m_undo_redo_stack_active == &m_undo_redo_stack_main) {
|
|
m_undo_redo_stack_active = &m_undo_redo_stack_gizmos;
|
|
assert(m_undo_redo_stack_active->empty());
|
|
// Take the initial snapshot of the gizmos.
|
|
// Not localized on purpose, the text will never be shown to the user.
|
|
this->take_snapshot(std::string("Gizmos-Initial"));
|
|
}
|
|
}
|
|
|
|
void Plater::priv::leave_gizmos_stack()
|
|
{
|
|
assert(m_undo_redo_stack_active == &m_undo_redo_stack_gizmos);
|
|
if (m_undo_redo_stack_active == &m_undo_redo_stack_gizmos) {
|
|
assert(! m_undo_redo_stack_active->empty());
|
|
m_undo_redo_stack_active->clear();
|
|
m_undo_redo_stack_active = &m_undo_redo_stack_main;
|
|
}
|
|
}
|
|
|
|
int Plater::priv::get_active_snapshot_index()
|
|
{
|
|
const size_t active_snapshot_time = this->undo_redo_stack().active_snapshot_time();
|
|
const std::vector<UndoRedo::Snapshot>& ss_stack = this->undo_redo_stack().snapshots();
|
|
const auto it = std::lower_bound(ss_stack.begin(), ss_stack.end(), UndoRedo::Snapshot(active_snapshot_time));
|
|
return it - ss_stack.begin();
|
|
}
|
|
|
|
void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRedo::SnapshotType snapshot_type)
|
|
{
|
|
if (m_prevent_snapshots > 0)
|
|
return;
|
|
assert(m_prevent_snapshots >= 0);
|
|
UndoRedo::SnapshotData snapshot_data;
|
|
snapshot_data.snapshot_type = snapshot_type;
|
|
snapshot_data.printer_technology = this->printer_technology;
|
|
if (this->view3D->is_layers_editing_enabled())
|
|
snapshot_data.flags |= UndoRedo::SnapshotData::VARIABLE_LAYER_EDITING_ACTIVE;
|
|
if (this->sidebar->obj_list()->is_selected(itSettings)) {
|
|
snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR;
|
|
snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx();
|
|
}
|
|
else if (this->sidebar->obj_list()->is_selected(itLayer)) {
|
|
snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR;
|
|
snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx();
|
|
}
|
|
else if (this->sidebar->obj_list()->is_selected(itLayerRoot))
|
|
snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR;
|
|
|
|
// If SLA gizmo is active, ask it if it wants to trigger support generation
|
|
// on loading this snapshot.
|
|
if (view3D->get_canvas3d()->get_gizmos_manager().wants_reslice_supports_on_undo())
|
|
snapshot_data.flags |= UndoRedo::SnapshotData::RECALCULATE_SLA_SUPPORTS;
|
|
|
|
//FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config.
|
|
// This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config.
|
|
if (this->printer_technology == ptFFF) {
|
|
const DynamicPrintConfig &config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
|
model.wipe_tower.position = Vec2d(config.opt_float("wipe_tower_x"), config.opt_float("wipe_tower_y"));
|
|
model.wipe_tower.rotation = config.opt_float("wipe_tower_rotation_angle");
|
|
}
|
|
const GLGizmosManager& gizmos = view3D->get_canvas3d()->get_gizmos_manager();
|
|
|
|
if (snapshot_type == UndoRedo::SnapshotType::ProjectSeparator && get_config_bool("clear_undo_redo_stack_on_new_project"))
|
|
this->undo_redo_stack().clear();
|
|
this->undo_redo_stack().take_snapshot(snapshot_name, model, view3D->get_canvas3d()->get_selection(), gizmos, snapshot_data);
|
|
if (snapshot_type == UndoRedo::SnapshotType::LeavingGizmoWithAction) {
|
|
// Filter all but the last UndoRedo::SnapshotType::GizmoAction in a row between the last UndoRedo::SnapshotType::EnteringGizmo and UndoRedo::SnapshotType::LeavingGizmoWithAction.
|
|
// The remaining snapshot will be renamed to a more generic name,
|
|
// depending on what gizmo is being left.
|
|
assert(gizmos.get_current() != nullptr);
|
|
std::string new_name = gizmos.get_current()->get_action_snapshot_name();
|
|
this->undo_redo_stack().reduce_noisy_snapshots(new_name);
|
|
} else if (snapshot_type == UndoRedo::SnapshotType::ProjectSeparator) {
|
|
// Reset the "dirty project" flag.
|
|
m_undo_redo_stack_main.mark_current_as_saved();
|
|
}
|
|
this->undo_redo_stack().release_least_recently_used();
|
|
|
|
dirty_state.update_from_undo_redo_stack(m_undo_redo_stack_main.project_modified());
|
|
|
|
// Save the last active preset name of a particular printer technology.
|
|
((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name();
|
|
BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info();
|
|
}
|
|
|
|
void Plater::priv::undo()
|
|
{
|
|
const std::vector<UndoRedo::Snapshot> &snapshots = this->undo_redo_stack().snapshots();
|
|
auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time()));
|
|
if (-- it_current != snapshots.begin())
|
|
this->undo_redo_to(it_current);
|
|
}
|
|
|
|
void Plater::priv::redo()
|
|
{
|
|
const std::vector<UndoRedo::Snapshot> &snapshots = this->undo_redo_stack().snapshots();
|
|
auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time()));
|
|
if (++ it_current != snapshots.end())
|
|
this->undo_redo_to(it_current);
|
|
}
|
|
|
|
void Plater::priv::undo_redo_to(size_t time_to_load)
|
|
{
|
|
const std::vector<UndoRedo::Snapshot> &snapshots = this->undo_redo_stack().snapshots();
|
|
auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(time_to_load));
|
|
assert(it_current != snapshots.end());
|
|
this->undo_redo_to(it_current);
|
|
}
|
|
|
|
void Plater::priv::undo_redo_to(std::vector<UndoRedo::Snapshot>::const_iterator it_snapshot)
|
|
{
|
|
// Make sure that no updating function calls take_snapshot until we are done.
|
|
SuppressSnapshots snapshot_supressor(q);
|
|
|
|
bool temp_snapshot_was_taken = this->undo_redo_stack().temp_snapshot_active();
|
|
PrinterTechnology new_printer_technology = it_snapshot->snapshot_data.printer_technology;
|
|
bool printer_technology_changed = this->printer_technology != new_printer_technology;
|
|
if (printer_technology_changed) {
|
|
// Switching the printer technology when jumping forwards / backwards in time. Switch to the last active printer profile of the other type.
|
|
std::string s_pt = (it_snapshot->snapshot_data.printer_technology == ptFFF) ? "FFF" : "SLA";
|
|
if (!wxGetApp().check_and_save_current_preset_changes(_L("Undo / Redo is processing"),
|
|
// format_wxstr(_L("%1% printer was active at the time the target Undo / Redo snapshot was taken. Switching to %1% printer requires reloading of %1% presets."), s_pt)))
|
|
format_wxstr(_L("Switching the printer technology from %1% to %2%.\n"
|
|
"Some %1% presets were modified, which will be lost after switching the printer technology."), s_pt =="FFF" ? "SLA" : "FFF", s_pt), false))
|
|
// Don't switch the profiles.
|
|
return;
|
|
}
|
|
// Save the last active preset name of a particular printer technology.
|
|
((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name();
|
|
//FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config.
|
|
// This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config.
|
|
if (this->printer_technology == ptFFF) {
|
|
const DynamicPrintConfig &config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
|
model.wipe_tower.position = Vec2d(config.opt_float("wipe_tower_x"), config.opt_float("wipe_tower_y"));
|
|
model.wipe_tower.rotation = config.opt_float("wipe_tower_rotation_angle");
|
|
}
|
|
const int layer_range_idx = it_snapshot->snapshot_data.layer_range_idx;
|
|
// Flags made of Snapshot::Flags enum values.
|
|
unsigned int new_flags = it_snapshot->snapshot_data.flags;
|
|
UndoRedo::SnapshotData top_snapshot_data;
|
|
top_snapshot_data.printer_technology = this->printer_technology;
|
|
if (this->view3D->is_layers_editing_enabled())
|
|
top_snapshot_data.flags |= UndoRedo::SnapshotData::VARIABLE_LAYER_EDITING_ACTIVE;
|
|
if (this->sidebar->obj_list()->is_selected(itSettings)) {
|
|
top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR;
|
|
top_snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx();
|
|
}
|
|
else if (this->sidebar->obj_list()->is_selected(itLayer)) {
|
|
top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR;
|
|
top_snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx();
|
|
}
|
|
else if (this->sidebar->obj_list()->is_selected(itLayerRoot))
|
|
top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR;
|
|
bool new_variable_layer_editing_active = (new_flags & UndoRedo::SnapshotData::VARIABLE_LAYER_EDITING_ACTIVE) != 0;
|
|
bool new_selected_settings_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR) != 0;
|
|
bool new_selected_layer_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR) != 0;
|
|
bool new_selected_layerroot_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR) != 0;
|
|
|
|
if (this->view3D->get_canvas3d()->get_gizmos_manager().wants_reslice_supports_on_undo())
|
|
top_snapshot_data.flags |= UndoRedo::SnapshotData::RECALCULATE_SLA_SUPPORTS;
|
|
|
|
// Disable layer editing before the Undo / Redo jump.
|
|
if (!new_variable_layer_editing_active && view3D->is_layers_editing_enabled())
|
|
view3D->get_canvas3d()->force_main_toolbar_left_action(view3D->get_canvas3d()->get_main_toolbar_item_id("layersediting"));
|
|
|
|
// Make a copy of the snapshot, undo/redo could invalidate the iterator
|
|
const UndoRedo::Snapshot snapshot_copy = *it_snapshot;
|
|
// Do the jump in time.
|
|
if (it_snapshot->timestamp < this->undo_redo_stack().active_snapshot_time() ?
|
|
this->undo_redo_stack().undo(model, this->view3D->get_canvas3d()->get_selection(), this->view3D->get_canvas3d()->get_gizmos_manager(), top_snapshot_data, it_snapshot->timestamp) :
|
|
this->undo_redo_stack().redo(model, this->view3D->get_canvas3d()->get_gizmos_manager(), it_snapshot->timestamp)) {
|
|
if (printer_technology_changed) {
|
|
// Switch to the other printer technology. Switch to the last printer active for that particular technology.
|
|
AppConfig *app_config = wxGetApp().app_config;
|
|
app_config->set("presets", "printer", (new_printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name);
|
|
//FIXME Why are we reloading the whole preset bundle here? Please document. This is fishy and it is unnecessarily expensive.
|
|
// Anyways, don't report any config value substitutions, they have been already reported to the user at application start up.
|
|
wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent);
|
|
// load_current_presets() calls Tab::load_current_preset() -> TabPrint::update() -> Object_list::update_and_show_object_settings_item(),
|
|
// but the Object list still keeps pointer to the old Model. Avoid a crash by removing selection first.
|
|
this->sidebar->obj_list()->unselect_objects();
|
|
// Load the currently selected preset into the GUI, update the preset selection box.
|
|
// This also switches the printer technology based on the printer technology of the active printer profile.
|
|
wxGetApp().load_current_presets();
|
|
}
|
|
//FIXME updating the Print config from the Wipe tower config values at the ModelWipeTower.
|
|
// This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config.
|
|
if (this->printer_technology == ptFFF) {
|
|
const DynamicPrintConfig ¤t_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
|
Vec2d current_position(current_config.opt_float("wipe_tower_x"), current_config.opt_float("wipe_tower_y"));
|
|
double current_rotation = current_config.opt_float("wipe_tower_rotation_angle");
|
|
if (current_position != model.wipe_tower.position || current_rotation != model.wipe_tower.rotation) {
|
|
DynamicPrintConfig new_config;
|
|
new_config.set_key_value("wipe_tower_x", new ConfigOptionFloat(model.wipe_tower.position.x()));
|
|
new_config.set_key_value("wipe_tower_y", new ConfigOptionFloat(model.wipe_tower.position.y()));
|
|
new_config.set_key_value("wipe_tower_rotation_angle", new ConfigOptionFloat(model.wipe_tower.rotation));
|
|
Tab *tab_print = wxGetApp().get_tab(Preset::TYPE_PRINT);
|
|
tab_print->load_config(new_config);
|
|
tab_print->update_dirty();
|
|
}
|
|
}
|
|
// set selection mode for ObjectList on sidebar
|
|
this->sidebar->obj_list()->set_selection_mode(new_selected_settings_on_sidebar ? ObjectList::SELECTION_MODE::smSettings :
|
|
new_selected_layer_on_sidebar ? ObjectList::SELECTION_MODE::smLayer :
|
|
new_selected_layerroot_on_sidebar ? ObjectList::SELECTION_MODE::smLayerRoot :
|
|
ObjectList::SELECTION_MODE::smUndef);
|
|
if (new_selected_settings_on_sidebar || new_selected_layer_on_sidebar)
|
|
this->sidebar->obj_list()->set_selected_layers_range_idx(layer_range_idx);
|
|
|
|
this->update_after_undo_redo(snapshot_copy, temp_snapshot_was_taken);
|
|
// Enable layer editing after the Undo / Redo jump.
|
|
if (! view3D->is_layers_editing_enabled() && this->layers_height_allowed() && new_variable_layer_editing_active)
|
|
view3D->get_canvas3d()->force_main_toolbar_left_action(view3D->get_canvas3d()->get_main_toolbar_item_id("layersediting"));
|
|
}
|
|
|
|
dirty_state.update_from_undo_redo_stack(m_undo_redo_stack_main.project_modified());
|
|
}
|
|
|
|
void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bool /* temp_snapshot_was_taken */)
|
|
{
|
|
this->view3D->get_canvas3d()->get_selection().clear();
|
|
// Update volumes from the deserializd model, always stop / update the background processing (for both the SLA and FFF technologies).
|
|
this->update((unsigned int)UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE | (unsigned int)UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE);
|
|
// Release old snapshots if the memory allocated is excessive. This may remove the top most snapshot if jumping to the very first snapshot.
|
|
//if (temp_snapshot_was_taken)
|
|
// Release the old snapshots always, as it may have happened, that some of the triangle meshes got deserialized from the snapshot, while some
|
|
// triangle meshes may have gotten released from the scene or the background processing, therefore now being calculated into the Undo / Redo stack size.
|
|
this->undo_redo_stack().release_least_recently_used();
|
|
//YS_FIXME update obj_list from the deserialized model (maybe store ObjectIDs into the tree?) (no selections at this point of time)
|
|
this->view3D->get_canvas3d()->get_selection().set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), this->undo_redo_stack().selection_deserialized().volumes_and_instances);
|
|
this->view3D->get_canvas3d()->get_gizmos_manager().update_after_undo_redo(snapshot);
|
|
|
|
wxGetApp().obj_list()->update_after_undo_redo();
|
|
|
|
if (wxGetApp().get_mode() == comSimple && model_has_advanced_features(this->model)) {
|
|
// If the user jumped to a snapshot that require user interface with advanced features, switch to the advanced mode without asking.
|
|
// There is a little risk of surprising the user, as he already must have had the advanced or expert mode active for such a snapshot to be taken.
|
|
if (wxGetApp().save_mode(comAdvanced))
|
|
view3D->set_as_dirty();
|
|
}
|
|
|
|
// this->update() above was called with POSTPONE_VALIDATION_ERROR_MESSAGE, so that if an error message was generated when updating the back end, it would not open immediately,
|
|
// but it would be saved to be show later. Let's do it now. We do not want to display the message box earlier, because on Windows & OSX the message box takes over the message
|
|
// queue pump, which in turn executes the rendering function before a full update after the Undo / Redo jump.
|
|
this->show_delayed_error_message();
|
|
|
|
//FIXME what about the state of the manipulators?
|
|
//FIXME what about the focus? Cursor in the side panel?
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot reloaded. Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info();
|
|
}
|
|
|
|
void Plater::priv::bring_instance_forward() const
|
|
{
|
|
#ifdef __APPLE__
|
|
wxGetApp().other_instance_message_handler()->bring_instance_forward();
|
|
return;
|
|
#endif //__APPLE__
|
|
if (main_frame == nullptr) {
|
|
BOOST_LOG_TRIVIAL(debug) << "Couldnt bring instance forward - mainframe is null";
|
|
return;
|
|
}
|
|
BOOST_LOG_TRIVIAL(debug) << "prusaslicer window going forward";
|
|
//this code maximize app window on Fedora
|
|
{
|
|
main_frame->Iconize(false);
|
|
if (main_frame->IsMaximized())
|
|
main_frame->Maximize(true);
|
|
else
|
|
main_frame->Maximize(false);
|
|
}
|
|
//this code maximize window on Ubuntu
|
|
{
|
|
main_frame->Restore();
|
|
wxGetApp().GetTopWindow()->SetFocus(); // focus on my window
|
|
wxGetApp().GetTopWindow()->Raise(); // bring window to front
|
|
wxGetApp().GetTopWindow()->Show(true); // show the window
|
|
}
|
|
}
|
|
|
|
// Plater / Public
|
|
|
|
Plater::Plater(wxWindow *parent, MainFrame *main_frame)
|
|
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxGetApp().get_min_size(parent))
|
|
, p(new priv(this, main_frame))
|
|
{
|
|
// Initialization performed in the private c-tor
|
|
}
|
|
|
|
bool Plater::is_project_dirty() const { return p->is_project_dirty(); }
|
|
bool Plater::is_presets_dirty() const { return p->is_presets_dirty(); }
|
|
void Plater::update_project_dirty_from_presets() { p->update_project_dirty_from_presets(); }
|
|
int Plater::save_project_if_dirty(const wxString& reason) { return p->save_project_if_dirty(reason); }
|
|
void Plater::reset_project_dirty_after_save() { p->reset_project_dirty_after_save(); }
|
|
void Plater::reset_project_dirty_initial_presets() { p->reset_project_dirty_initial_presets(); }
|
|
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
|
|
void Plater::render_project_state_debug_window() const { p->render_project_state_debug_window(); }
|
|
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
|
|
|
|
Sidebar& Plater::sidebar() { return *p->sidebar; }
|
|
const Model& Plater::model() const { return p->model; }
|
|
Model& Plater::model() { return p->model; }
|
|
const Print& Plater::fff_print() const { return p->fff_print; }
|
|
Print& Plater::fff_print() { return p->fff_print; }
|
|
const SLAPrint& Plater::sla_print() const { return p->sla_print; }
|
|
SLAPrint& Plater::sla_print() { return p->sla_print; }
|
|
|
|
bool Plater::is_project_temp() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void Plater::new_project()
|
|
{
|
|
if (int saved_project = p->save_project_if_dirty(_L("Creating a new project while the current project is modified.")); saved_project == wxID_CANCEL)
|
|
return;
|
|
else {
|
|
wxString header = _L("Creating a new project while some presets are modified.") + "\n" +
|
|
(saved_project == wxID_YES ? _L("You can keep presets modifications to the new project or discard them") :
|
|
_L("You can keep presets modifications to the new project, discard them or save changes as new presets.\n"
|
|
"Note, if changes will be saved then new project wouldn't keep them"));
|
|
int act_buttons = ActionButtons::KEEP;
|
|
if (saved_project == wxID_NO)
|
|
act_buttons |= ActionButtons::SAVE;
|
|
if (!wxGetApp().check_and_keep_current_preset_changes(_L("Creating a new project"), header, act_buttons))
|
|
return;
|
|
}
|
|
|
|
p->select_view_3D("3D");
|
|
take_snapshot(_L("New Project"), UndoRedo::SnapshotType::ProjectSeparator);
|
|
Plater::SuppressSnapshots suppress(this);
|
|
reset();
|
|
// Save the names of active presets and project specific config into ProjectDirtyStateManager.
|
|
reset_project_dirty_initial_presets();
|
|
// Make a copy of the active presets for detecting changes in preset values.
|
|
wxGetApp().update_saved_preset_from_current_preset();
|
|
// Update Project dirty state, update application title bar.
|
|
update_project_dirty_from_presets();
|
|
}
|
|
|
|
void Plater::load_project()
|
|
{
|
|
if (!wxGetApp().can_load_project())
|
|
return;
|
|
|
|
// Ask user for a project file name.
|
|
wxString input_file;
|
|
wxGetApp().load_project(this, input_file);
|
|
// And finally load the new project.
|
|
load_project(input_file);
|
|
}
|
|
|
|
void Plater::load_project(const wxString& filename)
|
|
{
|
|
if (filename.empty())
|
|
return;
|
|
|
|
// Take the Undo / Redo snapshot.
|
|
Plater::TakeSnapshot snapshot(this, _L("Load Project") + ": " + wxString::FromUTF8(into_path(filename).stem().string().c_str()), UndoRedo::SnapshotType::ProjectSeparator);
|
|
|
|
p->reset();
|
|
|
|
if (! load_files({ into_path(filename) }).empty()) {
|
|
// At least one file was loaded.
|
|
p->set_project_filename(filename);
|
|
// Save the names of active presets and project specific config into ProjectDirtyStateManager.
|
|
reset_project_dirty_initial_presets();
|
|
// Make a copy of the active presets for detecting changes in preset values.
|
|
wxGetApp().update_saved_preset_from_current_preset();
|
|
// Update Project dirty state, update application title bar.
|
|
update_project_dirty_from_presets();
|
|
}
|
|
}
|
|
|
|
void Plater::add_model(bool imperial_units/* = false*/)
|
|
{
|
|
wxArrayString input_files;
|
|
wxGetApp().import_model(this, input_files);
|
|
if (input_files.empty())
|
|
return;
|
|
|
|
std::vector<fs::path> paths;
|
|
for (const auto &file : input_files)
|
|
paths.emplace_back(into_path(file));
|
|
|
|
wxString snapshot_label;
|
|
assert(! paths.empty());
|
|
if (paths.size() == 1) {
|
|
snapshot_label = _L("Import Object");
|
|
snapshot_label += ": ";
|
|
snapshot_label += wxString::FromUTF8(paths.front().filename().string().c_str());
|
|
} else {
|
|
snapshot_label = _L("Import Objects");
|
|
snapshot_label += ": ";
|
|
snapshot_label += wxString::FromUTF8(paths.front().filename().string().c_str());
|
|
for (size_t i = 1; i < paths.size(); ++ i) {
|
|
snapshot_label += ", ";
|
|
snapshot_label += wxString::FromUTF8(paths[i].filename().string().c_str());
|
|
}
|
|
}
|
|
|
|
Plater::TakeSnapshot snapshot(this, snapshot_label);
|
|
if (! load_files(paths, true, false, imperial_units).empty())
|
|
wxGetApp().mainframe->update_title();
|
|
}
|
|
|
|
void Plater::import_zip_archive()
|
|
{
|
|
wxString input_file;
|
|
wxGetApp().import_zip(this, input_file);
|
|
if (input_file.empty())
|
|
return;
|
|
|
|
wxArrayString arr;
|
|
arr.Add(input_file);
|
|
load_files(arr, false);
|
|
}
|
|
|
|
void Plater::import_sl1_archive()
|
|
{
|
|
auto &w = get_ui_job_worker();
|
|
if (w.is_idle() && p->m_sla_import_dlg->ShowModal() == wxID_OK) {
|
|
p->take_snapshot(_L("Import SLA archive"));
|
|
replace_job(w, std::make_unique<SLAImportJob>(p->m_sla_import_dlg));
|
|
}
|
|
}
|
|
|
|
void Plater::extract_config_from_project()
|
|
{
|
|
wxString input_file;
|
|
wxGetApp().load_project(this, input_file);
|
|
|
|
if (! input_file.empty())
|
|
load_files({ into_path(input_file) }, false, true);
|
|
}
|
|
|
|
void Plater::load_gcode()
|
|
{
|
|
// Ask user for a gcode file name.
|
|
wxString input_file;
|
|
wxGetApp().load_gcode(this, input_file);
|
|
// And finally load the gcode file.
|
|
load_gcode(input_file);
|
|
}
|
|
|
|
void Plater::load_gcode(const wxString& filename)
|
|
{
|
|
if (! is_gcode_file(into_u8(filename)) || m_last_loaded_gcode == filename)
|
|
return;
|
|
|
|
m_last_loaded_gcode = filename;
|
|
|
|
// cleanup view before to start loading/processing
|
|
p->gcode_result.reset();
|
|
reset_gcode_toolpaths();
|
|
p->preview->reload_print(false);
|
|
p->get_current_canvas3D()->render();
|
|
|
|
wxBusyCursor wait;
|
|
|
|
// process gcode
|
|
GCodeProcessor processor;
|
|
try
|
|
{
|
|
processor.process_file(filename.ToUTF8().data());
|
|
}
|
|
catch (const std::exception& ex)
|
|
{
|
|
show_error(this, ex.what());
|
|
return;
|
|
}
|
|
p->gcode_result = std::move(processor.extract_result());
|
|
|
|
// show results
|
|
try
|
|
{
|
|
p->preview->reload_print(false);
|
|
}
|
|
catch (const std::exception&)
|
|
{
|
|
wxEndBusyCursor();
|
|
p->gcode_result.reset();
|
|
reset_gcode_toolpaths();
|
|
set_default_bed_shape();
|
|
p->preview->reload_print(false);
|
|
p->get_current_canvas3D()->render();
|
|
MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("does not contain valid gcode."),
|
|
wxString(GCODEVIEWER_APP_NAME) + " - " + _L("Error while loading .gcode file"), wxOK | wxICON_WARNING | wxCENTRE).ShowModal();
|
|
set_project_filename(wxEmptyString);
|
|
return;
|
|
}
|
|
p->preview->get_canvas3d()->zoom_to_gcode();
|
|
|
|
if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) {
|
|
wxEndBusyCursor();
|
|
//wxMessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("does not contain valid gcode."),
|
|
MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("does not contain valid gcode."),
|
|
wxString(GCODEVIEWER_APP_NAME) + " - " + _L("Error while loading .gcode file"), wxOK | wxICON_WARNING | wxCENTRE).ShowModal();
|
|
set_project_filename(wxEmptyString);
|
|
}
|
|
else
|
|
set_project_filename(filename);
|
|
}
|
|
|
|
void Plater::reload_gcode_from_disk()
|
|
{
|
|
wxString filename(m_last_loaded_gcode);
|
|
m_last_loaded_gcode.clear();
|
|
load_gcode(filename);
|
|
}
|
|
|
|
static std::string rename_file(const std::string& filename, const std::string& extension)
|
|
{
|
|
const boost::filesystem::path src_path(filename);
|
|
std::string src_stem = src_path.stem().string();
|
|
int value = 0;
|
|
if (src_stem.back() == ')') {
|
|
const size_t pos = src_stem.find_last_of('(');
|
|
if (pos != std::string::npos) {
|
|
const std::string value_str = src_stem.substr(pos + 1, src_stem.length() - pos);
|
|
try
|
|
{
|
|
value = std::stoi(value_str);
|
|
src_stem = src_stem.substr(0, pos);
|
|
}
|
|
catch (...)
|
|
{
|
|
// do nothing
|
|
}
|
|
}
|
|
}
|
|
|
|
boost::filesystem::path dst_path(filename);
|
|
dst_path.remove_filename();
|
|
dst_path /= src_stem + "(" + std::to_string(value + 1) + ")" + extension;
|
|
return dst_path.string();
|
|
}
|
|
|
|
void Plater::convert_gcode_to_ascii()
|
|
{
|
|
// Ask user for a gcode file name.
|
|
wxString input_file;
|
|
wxGetApp().load_gcode(this, input_file);
|
|
if (input_file.empty())
|
|
return;
|
|
|
|
// Open source file
|
|
FilePtr in_file{ boost::nowide::fopen(into_u8(input_file).c_str(), "rb") };
|
|
if (in_file.f == nullptr) {
|
|
MessageDialog msg_dlg(this, _L("Unable to open the selected file."), _L("Error"), wxICON_ERROR | wxOK);
|
|
msg_dlg.ShowModal();
|
|
return;
|
|
}
|
|
|
|
// Set out filename
|
|
const boost::filesystem::path input_path(into_u8(input_file));
|
|
boost::filesystem::path output_path(into_u8(input_file));
|
|
std::string output_file = output_path.replace_extension("gcode").string();
|
|
|
|
if (input_file == output_file) {
|
|
using namespace bgcode::core;
|
|
EResult res = is_valid_binary_gcode(*in_file.f);
|
|
if (res == EResult::InvalidMagicNumber) {
|
|
MessageDialog msg_dlg(this, _L("The selected file is already in ASCII format."), _L("Warning"), wxOK);
|
|
msg_dlg.ShowModal();
|
|
return;
|
|
}
|
|
else {
|
|
output_file = rename_file(output_file, ".gcode");
|
|
wxString msg = GUI::format_wxstr("The converted binary G-code file has '.gcode' extension.\n"
|
|
"The exported file will be renamed to:\n\n%1%\n\nDo you want to continue?", output_file);
|
|
MessageDialog msg_dlg(this, msg, _L("Warning"), wxYES_NO);
|
|
if (msg_dlg.ShowModal() != wxID_YES)
|
|
return;
|
|
}
|
|
}
|
|
|
|
const bool exists = boost::filesystem::exists(output_file);
|
|
if (exists) {
|
|
MessageDialog msg_dlg(this, GUI::format_wxstr(_L("File %1% already exists. Do you wish to overwrite it?"), output_file), _L("Notice"), wxYES_NO);
|
|
if (msg_dlg.ShowModal() != wxID_YES)
|
|
return;
|
|
}
|
|
|
|
// Open destination file
|
|
FilePtr out_file{ boost::nowide::fopen(output_file.c_str(), "wb") };
|
|
if (out_file.f == nullptr) {
|
|
MessageDialog msg_dlg(this, _L("Unable to open output file."), _L("Error"), wxICON_ERROR | wxOK);
|
|
msg_dlg.ShowModal();
|
|
return;
|
|
}
|
|
|
|
// Perform conversion
|
|
{
|
|
wxBusyCursor busy;
|
|
using namespace bgcode::core;
|
|
EResult res = bgcode::convert::from_binary_to_ascii(*in_file.f, *out_file.f, true);
|
|
if (res == EResult::InvalidMagicNumber) {
|
|
in_file.close();
|
|
out_file.close();
|
|
boost::filesystem::copy_file(input_path, output_path, boost::filesystem::copy_options::overwrite_existing);
|
|
}
|
|
else if (res != EResult::Success) {
|
|
MessageDialog msg_dlg(this, _L(std::string(translate_result(res))), _L("Error converting G-code file"), wxICON_INFORMATION | wxOK);
|
|
msg_dlg.ShowModal();
|
|
out_file.close();
|
|
boost::nowide::remove(output_file.c_str());
|
|
return;
|
|
}
|
|
}
|
|
|
|
MessageDialog msg_dlg(this, Slic3r::GUI::format_wxstr("%1%\n%2%", _L("Successfully created G-code ASCII file"), output_file),
|
|
_L("Convert G-code file to ASCII format"), wxICON_ERROR | wxOK);
|
|
msg_dlg.ShowModal();
|
|
}
|
|
|
|
void Plater::convert_gcode_to_binary()
|
|
{
|
|
// Ask user for a gcode file name.
|
|
wxString input_file;
|
|
wxGetApp().load_gcode(this, input_file);
|
|
if (input_file.empty())
|
|
return;
|
|
|
|
// Open source file
|
|
FilePtr in_file{ boost::nowide::fopen(into_u8(input_file).c_str(), "rb") };
|
|
if (in_file.f == nullptr) {
|
|
MessageDialog msg_dlg(this, _L("Unable to open the selected file."), _L("Error"), wxICON_ERROR | wxOK);
|
|
msg_dlg.ShowModal();
|
|
return;
|
|
}
|
|
|
|
// Set out filename
|
|
const boost::filesystem::path input_path(into_u8(input_file));
|
|
boost::filesystem::path output_path(into_u8(input_file));
|
|
std::string output_file = output_path.replace_extension("bgcode").string();
|
|
|
|
if (input_file == output_file) {
|
|
using namespace bgcode::core;
|
|
EResult res = is_valid_binary_gcode(*in_file.f);
|
|
if (res == EResult::Success) {
|
|
MessageDialog msg_dlg(this, _L("The selected file is already in binary format."), _L("Warning"), wxOK);
|
|
msg_dlg.ShowModal();
|
|
return;
|
|
}
|
|
else {
|
|
output_file = rename_file(output_file, ".bgcode");
|
|
wxString msg = GUI::format_wxstr("The converted ASCII G-code file has '.bgcode' extension.\n"
|
|
"The exported file will be renamed to:\n\n%1%\n\nDo you want to continue?", output_file);
|
|
MessageDialog msg_dlg(this, msg, _L("Warning"), wxYES_NO);
|
|
if (msg_dlg.ShowModal() != wxID_YES)
|
|
return;
|
|
}
|
|
}
|
|
|
|
const bool exists = boost::filesystem::exists(output_file);
|
|
if (exists) {
|
|
MessageDialog msg_dlg(this, GUI::format_wxstr(_L("File %1% already exists. Do you wish to overwrite it?"), output_file), _L("Notice"), wxYES_NO);
|
|
if (msg_dlg.ShowModal() != wxID_YES)
|
|
return;
|
|
}
|
|
|
|
// Open destination file
|
|
FilePtr out_file{ boost::nowide::fopen(output_file.c_str(), "wb") };
|
|
if (out_file.f == nullptr) {
|
|
MessageDialog msg_dlg(this, _L("Unable to open output file."), _L("Error"), wxICON_ERROR | wxOK);
|
|
msg_dlg.ShowModal();
|
|
return;
|
|
}
|
|
|
|
// Perform conversion
|
|
{
|
|
wxBusyCursor busy;
|
|
using namespace bgcode::core;
|
|
const bgcode::binarize::BinarizerConfig& binarizer_config = GCodeProcessor::get_binarizer_config();
|
|
const EResult res = bgcode::convert::from_ascii_to_binary(*in_file.f, *out_file.f, binarizer_config);
|
|
if (res == EResult::AlreadyBinarized) {
|
|
in_file.close();
|
|
out_file.close();
|
|
boost::filesystem::copy_file(input_path, output_path, boost::filesystem::copy_options::overwrite_existing);
|
|
}
|
|
else if (res != EResult::Success) {
|
|
MessageDialog msg_dlg(this, _L(std::string(translate_result(res))), _L("Error converting G-code file"), wxICON_INFORMATION | wxOK);
|
|
msg_dlg.ShowModal();
|
|
out_file.close();
|
|
boost::nowide::remove(output_file.c_str());
|
|
return;
|
|
}
|
|
}
|
|
|
|
MessageDialog msg_dlg(this, Slic3r::GUI::format_wxstr("%1%\n%2%", _L("Successfully created G-code binary file"), output_file),
|
|
_L("Convert G-code file to binary format"), wxICON_ERROR | wxOK);
|
|
msg_dlg.ShowModal();
|
|
}
|
|
|
|
void Plater::refresh_print()
|
|
{
|
|
p->preview->refresh_print();
|
|
}
|
|
|
|
std::vector<size_t> Plater::load_files(const std::vector<fs::path>& input_files, bool load_model, bool load_config, bool imperial_units /*= false*/) { return p->load_files(input_files, load_model, load_config, imperial_units); }
|
|
|
|
// To be called when providing a list of files to the GUI slic3r on command line.
|
|
std::vector<size_t> Plater::load_files(const std::vector<std::string>& input_files, bool load_model, bool load_config, bool imperial_units /*= false*/)
|
|
{
|
|
std::vector<fs::path> paths;
|
|
paths.reserve(input_files.size());
|
|
for (const std::string& path : input_files)
|
|
paths.emplace_back(path);
|
|
return p->load_files(paths, load_model, load_config, imperial_units);
|
|
}
|
|
|
|
|
|
class LoadProjectsDialog : public DPIDialog
|
|
{
|
|
int m_action{ 0 };
|
|
bool m_all { false };
|
|
wxComboBox* m_combo_project { nullptr };
|
|
wxComboBox* m_combo_config { nullptr };
|
|
public:
|
|
enum class LoadProjectOption : unsigned char
|
|
{
|
|
Unknown,
|
|
AllGeometry,
|
|
AllNewWindow,
|
|
OneProject,
|
|
OneConfig
|
|
};
|
|
|
|
LoadProjectsDialog(const std::vector<fs::path>& paths);
|
|
|
|
int get_action() const { return m_action + 1; }
|
|
bool get_all() const { return m_all; }
|
|
int get_selected() const
|
|
{
|
|
if (m_combo_project && m_combo_project->IsEnabled())
|
|
return m_combo_project->GetSelection();
|
|
else if (m_combo_config && m_combo_config->IsEnabled())
|
|
return m_combo_config->GetSelection();
|
|
else
|
|
return -1;
|
|
}
|
|
protected:
|
|
void on_dpi_changed(const wxRect& suggested_rect) override;
|
|
};
|
|
|
|
LoadProjectsDialog::LoadProjectsDialog(const std::vector<fs::path>& paths)
|
|
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY,
|
|
format_wxstr(_L("%1% - Multiple projects file"), SLIC3R_APP_NAME), wxDefaultPosition,
|
|
wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
|
|
{
|
|
SetFont(wxGetApp().normal_font());
|
|
|
|
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
|
bool contains_projects = !paths.empty();
|
|
bool instances_allowed = !wxGetApp().app_config->get_bool("single_instance");
|
|
if (contains_projects)
|
|
main_sizer->Add(new wxStaticText(this, wxID_ANY,
|
|
get_wraped_wxString(_L("There are several files being loaded, including Project files.") + "\n" + _L("Select an action to apply to all files."))), 0, wxEXPAND | wxALL, 10);
|
|
else
|
|
main_sizer->Add(new wxStaticText(this, wxID_ANY,
|
|
get_wraped_wxString(_L("There are several files being loaded.") + "\n" + _L("Select an action to apply to all files."))), 0, wxEXPAND | wxALL, 10);
|
|
|
|
wxStaticBox* action_stb = new wxStaticBox(this, wxID_ANY, _L("Action"));
|
|
if (!wxOSX) action_stb->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
action_stb->SetFont(wxGetApp().normal_font());
|
|
|
|
if (contains_projects) {
|
|
wxArrayString filenames;
|
|
for (const fs::path& path : paths) {
|
|
filenames.push_back(from_u8(path.filename().string()));
|
|
}
|
|
m_combo_project = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, filenames, wxCB_READONLY);
|
|
m_combo_project->SetValue(filenames.front());
|
|
m_combo_project->Enable(false);
|
|
|
|
m_combo_config = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, filenames, wxCB_READONLY);
|
|
m_combo_config->SetValue(filenames.front());
|
|
m_combo_config->Enable(false);
|
|
}
|
|
wxStaticBoxSizer* stb_sizer = new wxStaticBoxSizer(action_stb, wxVERTICAL);
|
|
int id = 0;
|
|
|
|
// all geometry
|
|
wxRadioButton* btn = new wxRadioButton(this, wxID_ANY, _L("Import 3D models"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0);
|
|
btn->SetValue(id == m_action);
|
|
btn->Bind(wxEVT_RADIOBUTTON, [this, id, contains_projects](wxCommandEvent&) {
|
|
m_action = id;
|
|
if (contains_projects) {
|
|
m_combo_project->Enable(false);
|
|
m_combo_config->Enable(false);
|
|
}
|
|
});
|
|
stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5);
|
|
id++;
|
|
// all new window
|
|
if (instances_allowed) {
|
|
btn = new wxRadioButton(this, wxID_ANY, _L("Start a new instance of PrusaSlicer"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0);
|
|
btn->SetValue(id == m_action);
|
|
btn->Bind(wxEVT_RADIOBUTTON, [this, id, contains_projects](wxCommandEvent&) {
|
|
m_action = id;
|
|
if (contains_projects) {
|
|
m_combo_project->Enable(false);
|
|
m_combo_config->Enable(false);
|
|
}
|
|
});
|
|
stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5);
|
|
}
|
|
id++; // IMPORTANT TO ALWAYS UP THE ID EVEN IF OPTION IS NOT ADDED!
|
|
if (contains_projects) {
|
|
// one project
|
|
btn = new wxRadioButton(this, wxID_ANY, _L("Select one to load as project"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0);
|
|
btn->SetValue(false);
|
|
btn->Bind(wxEVT_RADIOBUTTON, [this, id](wxCommandEvent&) {
|
|
m_action = id;
|
|
m_combo_project->Enable(true);
|
|
m_combo_config->Enable(false);
|
|
});
|
|
stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5);
|
|
stb_sizer->Add(m_combo_project, 0, wxEXPAND | wxTOP, 5);
|
|
// one config
|
|
id++;
|
|
btn = new wxRadioButton(this, wxID_ANY, _L("Select only one file to load the configuration."), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0);
|
|
btn->SetValue(id == m_action);
|
|
btn->Bind(wxEVT_RADIOBUTTON, [this, id, instances_allowed](wxCommandEvent&) {
|
|
m_action = id;
|
|
if (instances_allowed)
|
|
m_combo_project->Enable(false);
|
|
m_combo_config->Enable(true);
|
|
});
|
|
stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5);
|
|
stb_sizer->Add(m_combo_config, 0, wxEXPAND | wxTOP, 5);
|
|
}
|
|
|
|
|
|
main_sizer->Add(stb_sizer, 1, wxEXPAND | wxRIGHT | wxLEFT, 10);
|
|
wxBoxSizer* bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
bottom_sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxLEFT, 5);
|
|
main_sizer->Add(bottom_sizer, 0, wxEXPAND | wxALL, 10);
|
|
SetSizer(main_sizer);
|
|
main_sizer->SetSizeHints(this);
|
|
|
|
// Update DarkUi just for buttons
|
|
wxGetApp().UpdateDlgDarkUI(this, true);
|
|
}
|
|
|
|
void LoadProjectsDialog::on_dpi_changed(const wxRect& suggested_rect)
|
|
{
|
|
const int em = em_unit();
|
|
SetMinSize(wxSize(65 * em, 30 * em));
|
|
Fit();
|
|
Refresh();
|
|
}
|
|
|
|
|
|
|
|
|
|
bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path)
|
|
{
|
|
//std::vector<fs::path> unzipped_paths;
|
|
std::vector<fs::path> non_project_paths;
|
|
std::vector<fs::path> project_paths;
|
|
try
|
|
{
|
|
mz_zip_archive archive;
|
|
mz_zip_zero_struct(&archive);
|
|
|
|
if (!open_zip_reader(&archive, archive_path.string())) {
|
|
// TRN %1% is archive path
|
|
std::string err_msg = GUI::format(_u8L("Loading of a ZIP archive on path %1% has failed."), archive_path.string());
|
|
throw Slic3r::FileIOError(err_msg);
|
|
}
|
|
mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
|
|
mz_zip_archive_file_stat stat;
|
|
// selected_paths contains paths and its uncompressed size. The size is used to distinguish between files with same path.
|
|
std::vector<std::pair<fs::path, size_t>> selected_paths;
|
|
FileArchiveDialog dlg(static_cast<wxWindow*>(wxGetApp().mainframe), &archive, selected_paths);
|
|
if (dlg.ShowModal() == wxID_OK)
|
|
{
|
|
std::string archive_path_string = archive_path.string();
|
|
archive_path_string = archive_path_string.substr(0, archive_path_string.size() - 4);
|
|
fs::path archive_dir(wxStandardPaths::Get().GetTempDir().utf8_str().data());
|
|
|
|
for (auto& path_w_size : selected_paths) {
|
|
const fs::path& path = path_w_size.first;
|
|
size_t size = path_w_size.second;
|
|
// find path in zip archive
|
|
for (mz_uint i = 0; i < num_entries; ++i) {
|
|
if (mz_zip_reader_file_stat(&archive, i, &stat)) {
|
|
if (size != stat.m_uncomp_size) // size must fit
|
|
continue;
|
|
wxString wname = boost::nowide::widen(stat.m_filename);
|
|
std::string name = into_u8(wname);
|
|
fs::path archive_path(name);
|
|
|
|
std::string extra(1024, 0);
|
|
size_t extra_size = mz_zip_reader_get_filename_from_extra(&archive, i, extra.data(), extra.size());
|
|
if (extra_size > 0) {
|
|
archive_path = fs::path(extra.substr(0, extra_size));
|
|
name = archive_path.string();
|
|
}
|
|
|
|
if (archive_path.empty())
|
|
continue;
|
|
if (path != archive_path)
|
|
continue;
|
|
// decompressing
|
|
try
|
|
{
|
|
std::replace(name.begin(), name.end(), '\\', '/');
|
|
// rename if file exists
|
|
std::string filename = path.filename().string();
|
|
std::string extension = path.extension().string();
|
|
std::string just_filename = filename.substr(0, filename.size() - extension.size());
|
|
std::string final_filename = just_filename;
|
|
|
|
size_t version = 0;
|
|
while (fs::exists(archive_dir / (final_filename + extension)))
|
|
{
|
|
++version;
|
|
final_filename = just_filename + "(" + std::to_string(version) + ")";
|
|
}
|
|
filename = final_filename + extension;
|
|
fs::path final_path = archive_dir / filename;
|
|
std::string buffer((size_t)stat.m_uncomp_size, 0);
|
|
// Decompress action. We already has correct file index in stat structure.
|
|
mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0);
|
|
if (res == 0) {
|
|
// TRN: First argument = path to file, second argument = error description
|
|
wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2%"), final_path.string(), mz_zip_get_error_string(mz_zip_get_last_error(&archive)));
|
|
BOOST_LOG_TRIVIAL(error) << error_log;
|
|
show_error(nullptr, error_log);
|
|
break;
|
|
}
|
|
// write buffer to file
|
|
fs::fstream file(final_path, std::ios::out | std::ios::binary | std::ios::trunc);
|
|
file.write(buffer.c_str(), buffer.size());
|
|
file.close();
|
|
if (!fs::exists(final_path)) {
|
|
wxString error_log = GUI::format_wxstr(_L("Failed to find unzipped file at %1%. Unzipping of file has failed."), final_path.string());
|
|
BOOST_LOG_TRIVIAL(error) << error_log;
|
|
show_error(nullptr, error_log);
|
|
break;
|
|
}
|
|
BOOST_LOG_TRIVIAL(info) << "Unzipped " << final_path;
|
|
if (!boost::algorithm::iends_with(filename, ".3mf") && !boost::algorithm::iends_with(filename, ".amf")) {
|
|
non_project_paths.emplace_back(final_path);
|
|
break;
|
|
}
|
|
// if 3mf - read archive headers to find project file
|
|
if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(final_path.string())) ||
|
|
(boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) {
|
|
non_project_paths.emplace_back(final_path);
|
|
break;
|
|
}
|
|
|
|
project_paths.emplace_back(final_path);
|
|
break;
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
// ensure the zip archive is closed and rethrow the exception
|
|
close_zip_reader(&archive);
|
|
throw Slic3r::FileIOError(e.what());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
close_zip_reader(&archive);
|
|
if (non_project_paths.size() + project_paths.size() != selected_paths.size())
|
|
BOOST_LOG_TRIVIAL(error) << "Decompresing of archive did not retrieve all files. Expected files: "
|
|
<< selected_paths.size()
|
|
<< " Decopressed files: "
|
|
<< non_project_paths.size() + project_paths.size();
|
|
} else {
|
|
close_zip_reader(&archive);
|
|
return false;
|
|
}
|
|
|
|
}
|
|
catch (const Slic3r::FileIOError& e) {
|
|
// zip reader should be already closed or not even opened
|
|
GUI::show_error(this, e.what());
|
|
return false;
|
|
}
|
|
// none selected
|
|
if (project_paths.empty() && non_project_paths.empty())
|
|
{
|
|
return false;
|
|
}
|
|
#if 0
|
|
// 1 project, 0 models - behave like drag n drop
|
|
if (project_paths.size() == 1 && non_project_paths.empty())
|
|
{
|
|
wxArrayString aux;
|
|
aux.Add(from_u8(project_paths.front().string()));
|
|
load_files(aux);
|
|
//load_files(project_paths, true, true);
|
|
boost::system::error_code ec;
|
|
fs::remove(project_paths.front(), ec);
|
|
if (ec)
|
|
BOOST_LOG_TRIVIAL(error) << ec.message();
|
|
return true;
|
|
}
|
|
// 1 model (or more and other instances are not allowed), 0 projects - open geometry
|
|
if (project_paths.empty() && (non_project_paths.size() == 1 || wxGetApp().app_config->get_bool("single_instance")))
|
|
{
|
|
load_files(non_project_paths, true, false);
|
|
boost::system::error_code ec;
|
|
fs::remove(non_project_paths.front(), ec);
|
|
if (ec)
|
|
BOOST_LOG_TRIVIAL(error) << ec.message();
|
|
return true;
|
|
}
|
|
|
|
bool delete_after = true;
|
|
|
|
LoadProjectsDialog dlg(project_paths);
|
|
if (dlg.ShowModal() == wxID_OK) {
|
|
LoadProjectsDialog::LoadProjectOption option = static_cast<LoadProjectsDialog::LoadProjectOption>(dlg.get_action());
|
|
switch (option)
|
|
{
|
|
case LoadProjectsDialog::LoadProjectOption::AllGeometry: {
|
|
load_files(project_paths, true, false);
|
|
load_files(non_project_paths, true, false);
|
|
break;
|
|
}
|
|
case LoadProjectsDialog::LoadProjectOption::AllNewWindow: {
|
|
delete_after = false;
|
|
for (const fs::path& path : project_paths) {
|
|
wxString f = from_path(path);
|
|
start_new_slicer(&f, false);
|
|
}
|
|
for (const fs::path& path : non_project_paths) {
|
|
wxString f = from_path(path);
|
|
start_new_slicer(&f, false);
|
|
}
|
|
break;
|
|
}
|
|
case LoadProjectsDialog::LoadProjectOption::OneProject: {
|
|
int pos = dlg.get_selected();
|
|
assert(pos >= 0 && pos < project_paths.size());
|
|
if (wxGetApp().can_load_project())
|
|
load_project(from_path(project_paths[pos]));
|
|
project_paths.erase(project_paths.begin() + pos);
|
|
load_files(project_paths, true, false);
|
|
load_files(non_project_paths, true, false);
|
|
break;
|
|
}
|
|
case LoadProjectsDialog::LoadProjectOption::OneConfig: {
|
|
int pos = dlg.get_selected();
|
|
assert(pos >= 0 && pos < project_paths.size());
|
|
std::vector<fs::path> aux;
|
|
aux.push_back(project_paths[pos]);
|
|
load_files(aux, false, true);
|
|
project_paths.erase(project_paths.begin() + pos);
|
|
load_files(project_paths, true, false);
|
|
load_files(non_project_paths, true, false);
|
|
break;
|
|
}
|
|
case LoadProjectsDialog::LoadProjectOption::Unknown:
|
|
default:
|
|
assert(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!delete_after)
|
|
return true;
|
|
#else
|
|
// 1 project file and some models - behave like drag n drop of 3mf and then load models
|
|
if (project_paths.size() == 1)
|
|
{
|
|
wxArrayString aux;
|
|
aux.Add(from_u8(project_paths.front().string()));
|
|
bool loaded3mf = load_files(aux, true);
|
|
load_files(non_project_paths, true, false);
|
|
boost::system::error_code ec;
|
|
if (loaded3mf) {
|
|
fs::remove(project_paths.front(), ec);
|
|
if (ec)
|
|
BOOST_LOG_TRIVIAL(error) << ec.message();
|
|
}
|
|
for (const fs::path& path : non_project_paths) {
|
|
// Delete file from temp file (path variable), it will stay only in app memory.
|
|
boost::system::error_code ec;
|
|
fs::remove(path, ec);
|
|
if (ec)
|
|
BOOST_LOG_TRIVIAL(error) << ec.message();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// load all projects and all models as geometry
|
|
load_files(project_paths, true, false);
|
|
load_files(non_project_paths, true, false);
|
|
#endif // 0
|
|
|
|
|
|
for (const fs::path& path : project_paths) {
|
|
// Delete file from temp file (path variable), it will stay only in app memory.
|
|
boost::system::error_code ec;
|
|
fs::remove(path, ec);
|
|
if (ec)
|
|
BOOST_LOG_TRIVIAL(error) << ec.message();
|
|
}
|
|
for (const fs::path& path : non_project_paths) {
|
|
// Delete file from temp file (path variable), it will stay only in app memory.
|
|
boost::system::error_code ec;
|
|
fs::remove(path, ec);
|
|
if (ec)
|
|
BOOST_LOG_TRIVIAL(error) << ec.message();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
class ProjectDropDialog : public DPIDialog
|
|
{
|
|
int m_action { 0 };
|
|
public:
|
|
enum class LoadType : unsigned char
|
|
{
|
|
Unknown,
|
|
OpenProject,
|
|
LoadGeometry,
|
|
LoadConfig,
|
|
OpenWindow
|
|
};
|
|
ProjectDropDialog(const std::string& filename);
|
|
|
|
int get_action() const { return m_action + 1; }
|
|
|
|
protected:
|
|
void on_dpi_changed(const wxRect& suggested_rect) override;
|
|
};
|
|
|
|
ProjectDropDialog::ProjectDropDialog(const std::string& filename)
|
|
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY,
|
|
format_wxstr("%1% - %2%", SLIC3R_APP_NAME, _L("Load project file")), wxDefaultPosition,
|
|
wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
|
|
{
|
|
SetFont(wxGetApp().normal_font());
|
|
|
|
bool single_instance_only = wxGetApp().app_config->get_bool("single_instance");
|
|
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
|
wxArrayString choices;
|
|
choices.reserve(4);
|
|
choices.Add(_L("Open as project"));
|
|
choices.Add(_L("Import 3D models only"));
|
|
choices.Add(_L("Import config only"));
|
|
if (!single_instance_only)
|
|
choices.Add(_L("Start new PrusaSlicer instance"));
|
|
|
|
main_sizer->Add(new wxStaticText(this, wxID_ANY,
|
|
get_wraped_wxString(_L("Select an action to apply to the file") + ": " + from_u8(filename))), 0, wxEXPAND | wxALL, 10);
|
|
|
|
m_action = std::clamp(std::stoi(wxGetApp().app_config->get("drop_project_action")),
|
|
static_cast<int>(LoadType::OpenProject), single_instance_only? static_cast<int>(LoadType::LoadConfig) : static_cast<int>(LoadType::OpenWindow)) - 1;
|
|
|
|
wxStaticBox* action_stb = new wxStaticBox(this, wxID_ANY, _L("Action"));
|
|
if (!wxOSX) action_stb->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
action_stb->SetFont(wxGetApp().normal_font());
|
|
|
|
wxStaticBoxSizer* stb_sizer = new wxStaticBoxSizer(action_stb, wxVERTICAL);
|
|
int id = 0;
|
|
for (const wxString& label : choices) {
|
|
wxRadioButton* btn = new wxRadioButton(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0);
|
|
btn->SetValue(id == m_action);
|
|
btn->Bind(wxEVT_RADIOBUTTON, [this, id](wxCommandEvent&) { m_action = id; });
|
|
stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5);
|
|
id++;
|
|
}
|
|
main_sizer->Add(stb_sizer, 1, wxEXPAND | wxRIGHT | wxLEFT, 10);
|
|
|
|
wxBoxSizer* bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
::CheckBox* check = new ::CheckBox(this, _L("Don't show again"));
|
|
check->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& evt) {
|
|
wxGetApp().app_config->set("show_drop_project_dialog", evt.IsChecked() ? "0" : "1");
|
|
});
|
|
|
|
bottom_sizer->Add(check, 0, wxEXPAND | wxRIGHT, 5);
|
|
bottom_sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxLEFT, 5);
|
|
main_sizer->Add(bottom_sizer, 0, wxEXPAND | wxALL, 10);
|
|
|
|
SetSizer(main_sizer);
|
|
main_sizer->SetSizeHints(this);
|
|
|
|
// Update DarkUi just for buttons
|
|
wxGetApp().UpdateDlgDarkUI(this, true);
|
|
}
|
|
|
|
void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect)
|
|
{
|
|
const int em = em_unit();
|
|
SetMinSize(wxSize(65 * em, 30 * em));
|
|
Fit();
|
|
Refresh();
|
|
}
|
|
|
|
bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/*=false*/)
|
|
{
|
|
const std::regex pattern_drop(".*[.](stl|obj|amf|3mf|prusa|step|stp|zip)", std::regex::icase);
|
|
const std::regex pattern_gcode_drop(".*[.](gcode|g|bgcode|bgc)", std::regex::icase);
|
|
|
|
std::vector<fs::path> paths;
|
|
|
|
// gcode viewer section
|
|
if (wxGetApp().is_gcode_viewer()) {
|
|
for (const auto& filename : filenames) {
|
|
fs::path path(into_path(filename));
|
|
if (std::regex_match(path.string(), pattern_gcode_drop))
|
|
paths.push_back(std::move(path));
|
|
}
|
|
|
|
if (paths.size() > 1) {
|
|
//wxMessageDialog(static_cast<wxWindow*>(this), _L("You can open only one .gcode file at a time."),
|
|
MessageDialog(static_cast<wxWindow*>(this), _L("You can open only one .gcode file at a time."),
|
|
wxString(SLIC3R_APP_NAME) + " - " + _L("Drag and drop G-code file"), wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal();
|
|
return false;
|
|
}
|
|
else if (paths.size() == 1) {
|
|
load_gcode(from_path(paths.front()));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// editor section
|
|
for (const auto& filename : filenames) {
|
|
fs::path path(into_path(filename));
|
|
if (std::regex_match(path.string(), pattern_drop))
|
|
paths.push_back(std::move(path));
|
|
else if (std::regex_match(path.string(), pattern_gcode_drop))
|
|
start_new_gcodeviewer(&filename);
|
|
else
|
|
continue;
|
|
}
|
|
if (paths.empty())
|
|
// Likely all paths processed were gcodes, for which a G-code viewer instance has hopefully been started.
|
|
return false;
|
|
|
|
// searches for project files
|
|
for (std::vector<fs::path>::const_reverse_iterator it = paths.rbegin(); it != paths.rend(); ++it) {
|
|
std::string filename = (*it).filename().string();
|
|
|
|
bool handle_as_project = (boost::algorithm::iends_with(filename, ".3mf") || boost::algorithm::iends_with(filename, ".amf"));
|
|
if (boost::algorithm::iends_with(filename, ".zip") && is_project_3mf(it->string())) {
|
|
BOOST_LOG_TRIVIAL(warning) << "File with .zip extension is 3mf project, opening as it would have .3mf extension: " << *it;
|
|
handle_as_project = true;
|
|
}
|
|
if (handle_as_project) {
|
|
ProjectDropDialog::LoadType load_type = ProjectDropDialog::LoadType::Unknown;
|
|
{
|
|
if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(it->string())) ||
|
|
(boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf")))
|
|
load_type = ProjectDropDialog::LoadType::LoadGeometry;
|
|
else {
|
|
if (wxGetApp().app_config->get_bool("show_drop_project_dialog")) {
|
|
ProjectDropDialog dlg(filename);
|
|
if (dlg.ShowModal() == wxID_OK) {
|
|
int choice = dlg.get_action();
|
|
load_type = static_cast<ProjectDropDialog::LoadType>(choice);
|
|
wxGetApp().app_config->set("drop_project_action", std::to_string(choice));
|
|
}
|
|
}
|
|
else
|
|
load_type = static_cast<ProjectDropDialog::LoadType>(std::clamp(std::stoi(wxGetApp().app_config->get("drop_project_action")),
|
|
static_cast<int>(ProjectDropDialog::LoadType::OpenProject), static_cast<int>(ProjectDropDialog::LoadType::LoadConfig)));
|
|
}
|
|
}
|
|
|
|
if (load_type == ProjectDropDialog::LoadType::Unknown)
|
|
return false;
|
|
|
|
switch (load_type) {
|
|
case ProjectDropDialog::LoadType::OpenProject: {
|
|
if (wxGetApp().can_load_project())
|
|
load_project(from_path(*it));
|
|
break;
|
|
}
|
|
case ProjectDropDialog::LoadType::LoadGeometry: {
|
|
// Plater::TakeSnapshot snapshot(this, _L("Import Object"));
|
|
load_files({ *it }, true, false);
|
|
break;
|
|
}
|
|
case ProjectDropDialog::LoadType::LoadConfig: {
|
|
load_files({ *it }, false, true);
|
|
break;
|
|
}
|
|
case ProjectDropDialog::LoadType::OpenWindow: {
|
|
wxString f = from_path(*it);
|
|
start_new_slicer(&f, false, delete_after_load);
|
|
return false; // did not load anything to this instance
|
|
}
|
|
case ProjectDropDialog::LoadType::Unknown : {
|
|
assert(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} else if (boost::algorithm::iends_with(filename, ".zip")) {
|
|
return preview_zip_archive(*it);
|
|
|
|
}
|
|
}
|
|
|
|
// other files
|
|
wxString snapshot_label;
|
|
assert(!paths.empty());
|
|
if (paths.size() == 1) {
|
|
snapshot_label = _L("Load File");
|
|
snapshot_label += ": ";
|
|
snapshot_label += wxString::FromUTF8(paths.front().filename().string().c_str());
|
|
}
|
|
else {
|
|
snapshot_label = _L("Load Files");
|
|
snapshot_label += ": ";
|
|
snapshot_label += wxString::FromUTF8(paths.front().filename().string().c_str());
|
|
for (size_t i = 1; i < paths.size(); ++i) {
|
|
snapshot_label += ", ";
|
|
snapshot_label += wxString::FromUTF8(paths[i].filename().string().c_str());
|
|
}
|
|
}
|
|
Plater::TakeSnapshot snapshot(this, snapshot_label);
|
|
load_files(paths);
|
|
|
|
return true;
|
|
}
|
|
|
|
void Plater::update(unsigned int flags) { p->update(flags); }
|
|
|
|
Worker &Plater::get_ui_job_worker() { return p->m_worker; }
|
|
|
|
const Worker &Plater::get_ui_job_worker() const { return p->m_worker; }
|
|
|
|
void Plater::update_ui_from_settings() { p->update_ui_from_settings(); }
|
|
|
|
void Plater::select_view(const std::string& direction) { p->select_view(direction); }
|
|
|
|
void Plater::select_view_3D(const std::string& name) { p->select_view_3D(name); }
|
|
|
|
bool Plater::is_preview_shown() const { return p->is_preview_shown(); }
|
|
bool Plater::is_preview_loaded() const { return p->is_preview_loaded(); }
|
|
bool Plater::is_view3D_shown() const { return p->is_view3D_shown(); }
|
|
|
|
bool Plater::are_view3D_labels_shown() const { return p->are_view3D_labels_shown(); }
|
|
void Plater::show_view3D_labels(bool show) { p->show_view3D_labels(show); }
|
|
|
|
bool Plater::is_legend_shown() const { return p->is_legend_shown(); }
|
|
void Plater::show_legend(bool show) { p->show_legend(show); }
|
|
|
|
bool Plater::is_sidebar_collapsed() const { return p->is_sidebar_collapsed(); }
|
|
void Plater::collapse_sidebar(bool show) { p->collapse_sidebar(show); }
|
|
|
|
bool Plater::is_view3D_layers_editing_enabled() const { return p->is_view3D_layers_editing_enabled(); }
|
|
|
|
void Plater::select_all() { p->select_all(); }
|
|
void Plater::deselect_all() { p->deselect_all(); }
|
|
|
|
void Plater::remove(size_t obj_idx) { p->remove(obj_idx); }
|
|
void Plater::reset() { p->reset(); }
|
|
void Plater::reset_with_confirm()
|
|
{
|
|
if (p->model.objects.empty() ||
|
|
//wxMessageDialog(static_cast<wxWindow*>(this), _L("All objects will be removed, continue?"), wxString(SLIC3R_APP_NAME) + " - " + _L("Delete all"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE).ShowModal() == wxID_YES)
|
|
MessageDialog(static_cast<wxWindow*>(this), _L("All objects will be removed, continue?"), wxString(SLIC3R_APP_NAME) + " - " + _L("Delete all"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE).ShowModal() == wxID_YES)
|
|
reset();
|
|
}
|
|
|
|
bool Plater::delete_object_from_model(size_t obj_idx) { return p->delete_object_from_model(obj_idx); }
|
|
|
|
void Plater::remove_selected()
|
|
{
|
|
if (p->get_selection().is_empty())
|
|
return;
|
|
|
|
Plater::TakeSnapshot snapshot(this, _L("Delete Selected Objects"));
|
|
get_ui_job_worker().cancel_all();
|
|
p->view3D->delete_selected();
|
|
}
|
|
|
|
void Plater::increase_instances(size_t num, int obj_idx, int inst_idx)
|
|
{
|
|
if (! can_increase_instances()) { return; }
|
|
|
|
Plater::TakeSnapshot snapshot(this, _L("Increase Instances"));
|
|
|
|
if (obj_idx < 0) {
|
|
obj_idx = p->get_selected_object_idx();
|
|
if (obj_idx < 0) {
|
|
// It's a case of increasing per 1 instance, when multiple objects are selected
|
|
if (const auto obj_idxs = get_selection().get_object_idxs(); !obj_idxs.empty()) {
|
|
// we need a copy made here because the selection changes at every call of increase_instances()
|
|
const Selection::ObjectIdxsToInstanceIdxsMap content = p->get_selection().get_content();
|
|
for (const unsigned int obj_id : obj_idxs) {
|
|
if (auto obj_it = content.find(int(obj_id)); obj_it != content.end())
|
|
increase_instances(1, int(obj_id), *obj_it->second.rbegin());
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
assert(obj_idx >= 0);
|
|
|
|
ModelObject* model_object = p->model.objects[obj_idx];
|
|
|
|
if (inst_idx < 0 && get_selected_object_idx() >= 0) {
|
|
inst_idx = get_selection().get_instance_idx();
|
|
if (0 > inst_idx || inst_idx >= int(model_object->instances.size()))
|
|
inst_idx = -1;
|
|
}
|
|
ModelInstance* model_instance = (inst_idx >= 0) ? model_object->instances[inst_idx] : model_object->instances.back();
|
|
|
|
bool was_one_instance = model_object->instances.size()==1;
|
|
|
|
double offset_base = canvas3D()->get_size_proportional_to_max_bed_size(0.05);
|
|
double offset = offset_base;
|
|
for (size_t i = 0; i < num; i++, offset += offset_base) {
|
|
Vec3d offset_vec = model_instance->get_offset() + Vec3d(offset, offset, 0.0);
|
|
Geometry::Transformation trafo = model_instance->get_transformation();
|
|
trafo.set_offset(offset_vec);
|
|
model_object->add_instance(trafo);
|
|
}
|
|
|
|
if (p->get_config_bool("autocenter"))
|
|
arrange();
|
|
|
|
p->update();
|
|
|
|
p->get_selection().add_instance(obj_idx, (int)model_object->instances.size() - 1);
|
|
|
|
sidebar().obj_list()->increase_object_instances(obj_idx, was_one_instance ? num + 1 : num);
|
|
|
|
p->selection_changed();
|
|
this->p->schedule_background_process();
|
|
}
|
|
|
|
void Plater::decrease_instances(size_t num, int obj_idx/* = -1*/)
|
|
{
|
|
if (! can_decrease_instances(obj_idx)) { return; }
|
|
|
|
Plater::TakeSnapshot snapshot(this, _L("Decrease Instances"));
|
|
|
|
if (obj_idx < 0)
|
|
obj_idx = p->get_selected_object_idx();
|
|
|
|
if (obj_idx < 0) {
|
|
if (const auto obj_ids = get_selection().get_object_idxs(); !obj_ids.empty())
|
|
for (const size_t obj_id : obj_ids)
|
|
decrease_instances(1, int(obj_id));
|
|
return;
|
|
}
|
|
|
|
ModelObject* model_object = p->model.objects[obj_idx];
|
|
if (model_object->instances.size() > num) {
|
|
for (size_t i = 0; i < num; ++ i)
|
|
model_object->delete_last_instance();
|
|
p->update();
|
|
// Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model.
|
|
sidebar().obj_list()->decrease_object_instances(obj_idx, num);
|
|
}
|
|
else {
|
|
remove(obj_idx);
|
|
}
|
|
|
|
if (!model_object->instances.empty())
|
|
p->get_selection().add_instance(obj_idx, (int)model_object->instances.size() - 1);
|
|
|
|
p->selection_changed();
|
|
this->p->schedule_background_process();
|
|
}
|
|
|
|
static long GetNumberFromUser( const wxString& msg,
|
|
const wxString& prompt,
|
|
const wxString& title,
|
|
long value,
|
|
long min,
|
|
long max,
|
|
wxWindow* parent)
|
|
{
|
|
#ifdef _WIN32
|
|
wxNumberEntryDialog dialog(parent, msg, prompt, title, value, min, max, wxDefaultPosition);
|
|
wxGetApp().UpdateDlgDarkUI(&dialog);
|
|
if (dialog.ShowModal() == wxID_OK)
|
|
return dialog.GetValue();
|
|
|
|
return -1;
|
|
#else
|
|
return wxGetNumberFromUser(msg, prompt, title, value, min, max, parent);
|
|
#endif
|
|
}
|
|
|
|
void Plater::set_number_of_copies()
|
|
{
|
|
const auto obj_idxs = get_selection().get_object_idxs();
|
|
if (obj_idxs.empty())
|
|
return;
|
|
|
|
const size_t init_cnt = obj_idxs.size() == 1 ? p->model.objects[*obj_idxs.begin()]->instances.size() : 1;
|
|
const int num = GetNumberFromUser( " ", _L("Enter the number of copies:"),
|
|
_L("Copies of the selected object"), init_cnt, 0, 1000, this );
|
|
if (num < 0)
|
|
return;
|
|
TakeSnapshot snapshot(this, wxString::Format(_L("Set numbers of copies to %d"), num));
|
|
|
|
// we need a copy made here because the selection changes at every call of increase_instances()
|
|
Selection::ObjectIdxsToInstanceIdxsMap content = p->get_selection().get_content();
|
|
|
|
for (const auto& obj_idx : obj_idxs) {
|
|
ModelObject* model_object = p->model.objects[obj_idx];
|
|
const int diff = num - (int)model_object->instances.size();
|
|
if (diff > 0) {
|
|
if (auto obj_it = content.find(int(obj_idx)); obj_it != content.end())
|
|
increase_instances(diff, int(obj_idx), *obj_it->second.rbegin());
|
|
}
|
|
else if (diff < 0)
|
|
decrease_instances(-diff, int(obj_idx));
|
|
}
|
|
}
|
|
|
|
void Plater::fill_bed_with_instances()
|
|
{
|
|
auto &w = get_ui_job_worker();
|
|
if (w.is_idle()) {
|
|
|
|
FillBedJob2::Callbacks cbs;
|
|
cbs.on_processed = [this](arr2::ArrangeTaskBase &t) {
|
|
p->take_snapshot(_L("Fill bed"));
|
|
};
|
|
|
|
auto scene = arr2::Scene{
|
|
build_scene(*this, ArrangeSelectionMode::SelectionOnly)};
|
|
|
|
cbs.on_finished = [this](arr2::FillBedTaskResult &result) {
|
|
auto [prototype_mi, pos] = arr2::find_instance_by_id(model(), result.prototype_id);
|
|
|
|
if (!prototype_mi)
|
|
return;
|
|
|
|
ModelObject *model_object = prototype_mi->get_object();
|
|
assert(model_object);
|
|
|
|
model_object->ensure_on_bed();
|
|
|
|
size_t inst_cnt = model_object->instances.size();
|
|
if (inst_cnt == 0)
|
|
return;
|
|
|
|
int object_idx = pos.obj_idx;
|
|
|
|
if (object_idx < 0 || object_idx >= int(model().objects.size()))
|
|
return;
|
|
|
|
update(static_cast<unsigned int>(UpdateParams::FORCE_FULL_SCREEN_REFRESH));
|
|
|
|
if (!result.to_add.empty()) {
|
|
auto added_cnt = result.to_add.size();
|
|
|
|
// FIXME: somebody explain why this is needed for
|
|
// increase_object_instances
|
|
if (result.arranged_items.size() == 1)
|
|
added_cnt++;
|
|
|
|
sidebar().obj_list()->increase_object_instances(object_idx, added_cnt);
|
|
}
|
|
};
|
|
|
|
replace_job(w, std::make_unique<FillBedJob2>(std::move(scene), cbs));
|
|
}
|
|
}
|
|
|
|
bool Plater::is_selection_empty() const
|
|
{
|
|
return p->get_selection().is_empty() || p->get_selection().is_wipe_tower();
|
|
}
|
|
|
|
void Plater::scale_selection_to_fit_print_volume()
|
|
{
|
|
p->scale_selection_to_fit_print_volume();
|
|
}
|
|
|
|
void Plater::convert_unit(ConversionType conv_type)
|
|
{
|
|
std::vector<int> obj_idxs, volume_idxs;
|
|
wxGetApp().obj_list()->get_selection_indexes(obj_idxs, volume_idxs);
|
|
if (obj_idxs.empty() && volume_idxs.empty())
|
|
return;
|
|
|
|
// We will remove object indexes after convertion
|
|
// So, resort object indexes descending to avoid the crash after remove
|
|
std::sort(obj_idxs.begin(), obj_idxs.end(), std::greater<int>());
|
|
|
|
TakeSnapshot snapshot(this, conv_type == ConversionType::CONV_FROM_INCH ? _L("Convert from imperial units") :
|
|
conv_type == ConversionType::CONV_TO_INCH ? _L("Revert conversion from imperial units") :
|
|
conv_type == ConversionType::CONV_FROM_METER ? _L("Convert from meters") : _L("Revert conversion from meters"));
|
|
wxBusyCursor wait;
|
|
|
|
ModelObjectPtrs objects;
|
|
for (int obj_idx : obj_idxs) {
|
|
ModelObject *object = p->model.objects[obj_idx];
|
|
object->convert_units(objects, conv_type, volume_idxs);
|
|
remove(obj_idx);
|
|
}
|
|
p->load_model_objects(objects);
|
|
|
|
Selection& selection = p->view3D->get_canvas3d()->get_selection();
|
|
size_t last_obj_idx = p->model.objects.size() - 1;
|
|
|
|
if (volume_idxs.empty()) {
|
|
for (size_t i = 0; i < objects.size(); ++i)
|
|
selection.add_object((unsigned int)(last_obj_idx - i), i == 0);
|
|
}
|
|
else {
|
|
for (int vol_idx : volume_idxs)
|
|
selection.add_volume(last_obj_idx, vol_idx, 0, false);
|
|
}
|
|
}
|
|
|
|
void Plater::toggle_layers_editing(bool enable)
|
|
{
|
|
if (canvas3D()->is_layers_editing_enabled() != enable)
|
|
canvas3D()->force_main_toolbar_left_action(canvas3D()->get_main_toolbar_item_id("layersediting"));
|
|
}
|
|
|
|
void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs& new_objects)
|
|
{
|
|
model().delete_object(obj_idx);
|
|
sidebar().obj_list()->delete_object_from_list(obj_idx);
|
|
|
|
// suppress to call selection update for Object List to avoid call of early Gizmos on/off update
|
|
p->load_model_objects(new_objects, false, false);
|
|
|
|
// now process all updates of the 3d scene
|
|
update();
|
|
// Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(),
|
|
// which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call
|
|
for (size_t idx = 0; idx < p->model.objects.size(); idx++)
|
|
wxGetApp().obj_list()->update_info_items(idx);
|
|
|
|
Selection& selection = p->get_selection();
|
|
size_t last_id = p->model.objects.size() - 1;
|
|
for (size_t i = 0; i < new_objects.size(); ++i)
|
|
selection.add_object((unsigned int)(last_id - i), i == 0);
|
|
|
|
UIThreadWorker w;
|
|
arrange(w, true);
|
|
w.wait_for_idle();
|
|
}
|
|
|
|
|
|
|
|
static wxString check_binary_vs_ascii_gcode_extension(PrinterTechnology pt, const std::string& ext, bool binary_output)
|
|
{
|
|
wxString err_out;
|
|
if (pt == ptFFF) {
|
|
const bool binary_extension = (ext == ".bgcode" || ext == ".bgc");
|
|
const bool ascii_extension = (ext == ".gcode" || ext == ".g" || ext == ".gco");
|
|
if (binary_output && ascii_extension) {
|
|
// TRN The placeholder %1% is the file extension the user has selected.
|
|
err_out = format_wxstr(_L("Cannot save binary G-code with %1% extension.\n\n"
|
|
"Use a different extension or disable <a href=%2%>binary G-code export</a> "
|
|
"in Printer Settings."), ext, "binary_gcode;printer");
|
|
}
|
|
if (!binary_output && binary_extension) {
|
|
// TRN The placeholder %1% is the file extension the user has selected.
|
|
err_out = format_wxstr(_L("Cannot save ASCII G-code with %1% extension.\n\n"
|
|
"Use a different extension or enable <a href=%2%>binary G-code export</a> "
|
|
"in Printer Settings."), ext, "binary_gcode;printer");
|
|
}
|
|
}
|
|
return err_out;
|
|
}
|
|
|
|
|
|
|
|
// This function should be deleted when binary G-codes become more common. The dialog is there to make the
|
|
// transition period easier for the users, because bgcode files are not recognized by older firmwares
|
|
// without any error message.
|
|
static void alert_when_exporting_binary_gcode(bool binary_output, const std::string& printer_notes)
|
|
{
|
|
if (binary_output
|
|
&& (boost::algorithm::contains(printer_notes, "PRINTER_MODEL_XL")
|
|
|| boost::algorithm::contains(printer_notes, "PRINTER_MODEL_MINI")
|
|
|| boost::algorithm::contains(printer_notes, "PRINTER_MODEL_MK4")
|
|
|| boost::algorithm::contains(printer_notes, "PRINTER_MODEL_MK3.9")))
|
|
{
|
|
AppConfig* app_config = wxGetApp().app_config;
|
|
wxWindow* parent = wxGetApp().mainframe;
|
|
const std::string option_key = "dont_warn_about_firmware_version_when_exporting_binary_gcode";
|
|
|
|
if (app_config->get(option_key) != "1") {
|
|
const wxString url = "https://prusa.io/binary-gcode";
|
|
HtmlCapableRichMessageDialog dialog(parent,
|
|
format_wxstr(_L("You are exporting binary G-code for a Prusa printer. "
|
|
"Binary G-code enables significantly faster uploads. "
|
|
"Ensure that your printer is running firmware version 5.1.0 or newer, as older versions do not support binary G-codes.\n\n"
|
|
"To learn more about binary G-code, visit <a href=%1%>%1%</a>."),
|
|
url),
|
|
_L("Warning"), wxOK,
|
|
[&url](const std::string&) { wxGetApp().open_browser_with_warning_dialog(url); });
|
|
dialog.ShowCheckBox(_L("Don't show again"));
|
|
if (dialog.ShowModal() == wxID_OK && dialog.IsCheckBoxChecked())
|
|
app_config->set(option_key, "1");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void Plater::export_gcode(bool prefer_removable)
|
|
{
|
|
if (p->model.objects.empty())
|
|
return;
|
|
|
|
if (canvas3D()->get_gizmos_manager().is_in_editing_mode(true))
|
|
return;
|
|
|
|
|
|
if (p->process_completed_with_error)
|
|
return;
|
|
|
|
// If possible, remove accents from accented latin characters.
|
|
// This function is useful for generating file names to be processed by legacy firmwares.
|
|
fs::path default_output_file;
|
|
try {
|
|
// Update the background processing, so that the placeholder parser will get the correct values for the ouput file template.
|
|
// Also if there is something wrong with the current configuration, a pop-up dialog will be shown and the export will not be performed.
|
|
unsigned int state = this->p->update_restart_background_process(false, false);
|
|
if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID)
|
|
return;
|
|
default_output_file = this->p->background_process.output_filepath_for_project(into_path(get_project_filename(".3mf")));
|
|
} catch (const Slic3r::PlaceholderParserError &ex) {
|
|
// Show the error with monospaced font.
|
|
show_error(this, ex.what(), true);
|
|
return;
|
|
} catch (const std::exception &ex) {
|
|
show_error(this, ex.what(), false);
|
|
return;
|
|
}
|
|
default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string()));
|
|
AppConfig &appconfig = *wxGetApp().app_config;
|
|
RemovableDriveManager &removable_drive_manager = *wxGetApp().removable_drive_manager();
|
|
// Get a last save path, either to removable media or to an internal media.
|
|
std::string start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), prefer_removable);
|
|
if (prefer_removable) {
|
|
// Returns a path to a removable media if it exists, prefering start_dir. Update the internal removable drives database.
|
|
start_dir = removable_drive_manager.get_removable_drive_path(start_dir);
|
|
if (start_dir.empty())
|
|
// Direct user to the last internal media.
|
|
start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), false);
|
|
}
|
|
|
|
fs::path output_path;
|
|
{
|
|
std::string ext = default_output_file.extension().string();
|
|
wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _L("Save G-code file as:") : _L("Save SL1 / SL1S file as:"),
|
|
start_dir,
|
|
from_path(default_output_file.filename()),
|
|
printer_technology() == ptFFF ? GUI::file_wildcards(FT_GCODE, ext) :
|
|
GUI::sla_wildcards(p->sla_print.printer_config().sla_archive_format.value.c_str(), ext),
|
|
wxFD_SAVE | wxFD_OVERWRITE_PROMPT
|
|
);
|
|
if (dlg.ShowModal() == wxID_OK) {
|
|
output_path = into_path(dlg.GetPath());
|
|
|
|
auto check_for_error = [this](const boost::filesystem::path& path, wxString& err_out) -> bool {
|
|
const std::string filename = path.filename().string();
|
|
const std::string ext = boost::algorithm::to_lower_copy(path.extension().string());
|
|
if (has_illegal_characters(filename)) {
|
|
err_out = _L("The provided file name is not valid.") + "\n" +
|
|
_L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\"";
|
|
return true;
|
|
}
|
|
if (this->printer_technology() == ptFFF) {
|
|
bool supports_binary = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_bool("binary_gcode");
|
|
bool uses_binary = wxGetApp().app_config->get_bool("use_binary_gcode_when_supported");
|
|
err_out = check_binary_vs_ascii_gcode_extension(printer_technology(), ext, supports_binary && uses_binary);
|
|
}
|
|
return !err_out.IsEmpty();
|
|
};
|
|
|
|
wxString error_str;
|
|
if (check_for_error(output_path, error_str)) {
|
|
const t_link_clicked on_link_clicked = [](const std::string& key) -> void { wxGetApp().jump_to_option(key); };
|
|
ErrorDialog(this, error_str, on_link_clicked).ShowModal();
|
|
output_path.clear();
|
|
} else if (printer_technology() == ptFFF) {
|
|
bool supports_binary = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_bool("binary_gcode");
|
|
bool uses_binary = wxGetApp().app_config->get_bool("use_binary_gcode_when_supported");
|
|
alert_when_exporting_binary_gcode(supports_binary && uses_binary,
|
|
wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_string("printer_notes"));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! output_path.empty()) {
|
|
bool path_on_removable_media = removable_drive_manager.set_and_verify_last_save_path(output_path.string());
|
|
p->notification_manager->new_export_began(path_on_removable_media);
|
|
p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL;
|
|
p->last_output_path = output_path.string();
|
|
p->last_output_dir_path = output_path.parent_path().string();
|
|
p->export_gcode(output_path, path_on_removable_media, PrintHostJob());
|
|
// Storing a path to AppConfig either as path to removable media or a path to internal media.
|
|
// is_path_on_removable_drive() is called with the "true" parameter to update its internal database as the user may have shuffled the external drives
|
|
// while the dialog was open.
|
|
appconfig.update_last_output_dir(output_path.parent_path().string(), path_on_removable_media);
|
|
|
|
}
|
|
}
|
|
|
|
void Plater::export_stl_obj(bool extended, bool selection_only)
|
|
{
|
|
if (p->model.objects.empty()) { return; }
|
|
|
|
wxString path = p->get_export_file(FT_OBJECT);
|
|
if (path.empty()) { return; }
|
|
const std::string path_u8 = into_u8(path);
|
|
|
|
wxBusyCursor wait;
|
|
|
|
const auto &selection = p->get_selection();
|
|
const auto obj_idx = selection.get_object_idx();
|
|
if (selection_only && (obj_idx == -1 || selection.is_wipe_tower()))
|
|
return;
|
|
|
|
// Following lambda generates a combined mesh for export with normals pointing outwards.
|
|
auto mesh_to_export_fff = [this](const ModelObject& mo, int instance_id) {
|
|
TriangleMesh mesh;
|
|
|
|
std::vector<csg::CSGPart> csgmesh;
|
|
csgmesh.reserve(2 * mo.volumes.size());
|
|
csg::model_to_csgmesh(mo, Transform3d::Identity(), std::back_inserter(csgmesh),
|
|
csg::mpartsPositive | csg::mpartsNegative | csg::mpartsDoSplits);
|
|
|
|
auto csgrange = range(csgmesh);
|
|
if (csg::is_all_positive(csgrange)) {
|
|
mesh = TriangleMesh{csg::csgmesh_merge_positive_parts(csgrange)};
|
|
} else if (csg::check_csgmesh_booleans(csgrange) == csgrange.end()) {
|
|
try {
|
|
auto cgalm = csg::perform_csgmesh_booleans(csgrange);
|
|
mesh = MeshBoolean::cgal::cgal_to_triangle_mesh(*cgalm);
|
|
} catch (...) {}
|
|
}
|
|
|
|
if (mesh.empty()) {
|
|
get_notification_manager()->push_plater_error_notification(
|
|
_u8L("Unable to perform boolean operation on model meshes. "
|
|
"Only positive parts will be exported."));
|
|
|
|
for (const ModelVolume* v : mo.volumes)
|
|
if (v->is_model_part()) {
|
|
TriangleMesh vol_mesh(v->mesh());
|
|
vol_mesh.transform(v->get_matrix(), true);
|
|
mesh.merge(vol_mesh);
|
|
}
|
|
}
|
|
|
|
if (instance_id == -1) {
|
|
TriangleMesh vols_mesh(mesh);
|
|
mesh = TriangleMesh();
|
|
for (const ModelInstance* i : mo.instances) {
|
|
TriangleMesh m = vols_mesh;
|
|
m.transform(i->get_matrix(), true);
|
|
mesh.merge(m);
|
|
}
|
|
}
|
|
else if (0 <= instance_id && instance_id < int(mo.instances.size()))
|
|
mesh.transform(mo.instances[instance_id]->get_matrix(), true);
|
|
return mesh;
|
|
};
|
|
|
|
auto mesh_to_export_sla = [&, this](const ModelObject& mo, int instance_id) {
|
|
TriangleMesh mesh;
|
|
|
|
const SLAPrintObject *object = this->p->sla_print.get_print_object_by_model_object_id(mo.id());
|
|
|
|
if (!object || !object->get_mesh_to_print() || object->get_mesh_to_print()->empty())
|
|
mesh = mesh_to_export_fff(mo, instance_id);
|
|
else {
|
|
const Transform3d mesh_trafo_inv = object->trafo().inverse();
|
|
const bool is_left_handed = object->is_left_handed();
|
|
|
|
auto pad_mesh = extended? object->pad_mesh() : TriangleMesh{};
|
|
pad_mesh.transform(mesh_trafo_inv);
|
|
|
|
auto supports_mesh = extended ? object->support_mesh() : TriangleMesh{};
|
|
supports_mesh.transform(mesh_trafo_inv);
|
|
|
|
const std::vector<SLAPrintObject::Instance>& obj_instances = object->instances();
|
|
for (const SLAPrintObject::Instance& obj_instance : obj_instances) {
|
|
auto it = std::find_if(object->model_object()->instances.begin(), object->model_object()->instances.end(),
|
|
[&obj_instance](const ModelInstance *mi) { return mi->id() == obj_instance.instance_id; });
|
|
assert(it != object->model_object()->instances.end());
|
|
|
|
if (it != object->model_object()->instances.end()) {
|
|
const bool one_inst_only = selection_only && ! selection.is_single_full_object();
|
|
|
|
const int instance_idx = it - object->model_object()->instances.begin();
|
|
const Transform3d& inst_transform = one_inst_only
|
|
? Transform3d::Identity()
|
|
: object->model_object()->instances[instance_idx]->get_transformation().get_matrix();
|
|
|
|
TriangleMesh inst_mesh;
|
|
|
|
if (!pad_mesh.empty()) {
|
|
TriangleMesh inst_pad_mesh = pad_mesh;
|
|
inst_pad_mesh.transform(inst_transform, is_left_handed);
|
|
inst_mesh.merge(inst_pad_mesh);
|
|
}
|
|
|
|
if (!supports_mesh.empty()) {
|
|
TriangleMesh inst_supports_mesh = supports_mesh;
|
|
inst_supports_mesh.transform(inst_transform, is_left_handed);
|
|
inst_mesh.merge(inst_supports_mesh);
|
|
}
|
|
|
|
std::shared_ptr<const indexed_triangle_set> m = object->get_mesh_to_print();
|
|
TriangleMesh inst_object_mesh;
|
|
if (m)
|
|
inst_object_mesh = TriangleMesh{*m};
|
|
|
|
inst_object_mesh.transform(mesh_trafo_inv);
|
|
inst_object_mesh.transform(inst_transform, is_left_handed);
|
|
|
|
inst_mesh.merge(inst_object_mesh);
|
|
|
|
// ensure that the instance lays on the bed
|
|
inst_mesh.translate(0.0f, 0.0f, -inst_mesh.bounding_box().min.z());
|
|
|
|
// merge instance with global mesh
|
|
mesh.merge(inst_mesh);
|
|
|
|
if (one_inst_only)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return mesh;
|
|
};
|
|
|
|
std::function<TriangleMesh(const ModelObject& mo, int instance_id)>
|
|
mesh_to_export;
|
|
|
|
if (p->printer_technology == ptFFF )
|
|
mesh_to_export = mesh_to_export_fff;
|
|
else
|
|
mesh_to_export = mesh_to_export_sla;
|
|
|
|
TriangleMesh mesh;
|
|
if (selection_only) {
|
|
const ModelObject* model_object = p->model.objects[obj_idx];
|
|
if (selection.get_mode() == Selection::Instance)
|
|
mesh = mesh_to_export(*model_object, (selection.is_single_full_object() && model_object->instances.size() > 1) ? -1 : selection.get_instance_idx());
|
|
else {
|
|
const GLVolume* volume = selection.get_first_volume();
|
|
mesh = model_object->volumes[volume->volume_idx()]->mesh();
|
|
mesh.transform(volume->get_volume_transformation().get_matrix(), true);
|
|
}
|
|
|
|
if (!selection.is_single_full_object() || model_object->instances.size() == 1)
|
|
mesh.translate(-model_object->origin_translation.cast<float>());
|
|
}
|
|
else {
|
|
for (const ModelObject* o : p->model.objects) {
|
|
mesh.merge(mesh_to_export(*o, -1));
|
|
}
|
|
}
|
|
|
|
if (path.Lower().EndsWith(".stl"))
|
|
Slic3r::store_stl(path_u8.c_str(), &mesh, true);
|
|
else if (path.Lower().EndsWith(".obj"))
|
|
Slic3r::store_obj(path_u8.c_str(), &mesh);
|
|
}
|
|
|
|
namespace {
|
|
std::string get_file_name(const std::string &file_path)
|
|
{
|
|
size_t pos_last_delimiter = file_path.find_last_of("/\\");
|
|
size_t pos_point = file_path.find_last_of('.');
|
|
size_t offset = pos_last_delimiter + 1;
|
|
size_t count = pos_point - pos_last_delimiter - 1;
|
|
return file_path.substr(offset, count);
|
|
}
|
|
using SvgFile = EmbossShape::SvgFile;
|
|
using SvgFiles = std::vector<SvgFile*>;
|
|
std::string create_unique_3mf_filepath(const std::string &file, const SvgFiles svgs)
|
|
{
|
|
// const std::string MODEL_FOLDER = "3D/"; // copy from file 3mf.cpp
|
|
std::string path_in_3mf = "3D/" + file + ".svg";
|
|
size_t suffix_number = 0;
|
|
bool is_unique = false;
|
|
do{
|
|
is_unique = true;
|
|
path_in_3mf = "3D/" + file + ((suffix_number++)? ("_" + std::to_string(suffix_number)) : "") + ".svg";
|
|
for (SvgFile *svgfile : svgs) {
|
|
if (svgfile->path_in_3mf.empty())
|
|
continue;
|
|
if (svgfile->path_in_3mf.compare(path_in_3mf) == 0) {
|
|
is_unique = false;
|
|
break;
|
|
}
|
|
}
|
|
} while (!is_unique);
|
|
return path_in_3mf;
|
|
}
|
|
|
|
bool set_by_local_path(SvgFile &svg, const SvgFiles& svgs)
|
|
{
|
|
// Try to find already used svg file
|
|
for (SvgFile *svg_ : svgs) {
|
|
if (svg_->path_in_3mf.empty())
|
|
continue;
|
|
if (svg.path.compare(svg_->path) == 0) {
|
|
svg.path_in_3mf = svg_->path_in_3mf;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Function to secure private data before store to 3mf
|
|
/// </summary>
|
|
/// <param name="model">Data(also private) to clean before publishing</param>
|
|
void publish(Model &model) {
|
|
|
|
// SVG file publishing
|
|
bool exist_new = false;
|
|
SvgFiles svgfiles;
|
|
for (ModelObject *object: model.objects){
|
|
for (ModelVolume *volume : object->volumes) {
|
|
if (!volume->emboss_shape.has_value())
|
|
continue;
|
|
if (volume->text_configuration.has_value())
|
|
continue; // text dosen't have svg path
|
|
|
|
assert(volume->emboss_shape->svg_file.has_value());
|
|
if (!volume->emboss_shape->svg_file.has_value())
|
|
continue;
|
|
|
|
SvgFile* svg = &(*volume->emboss_shape->svg_file);
|
|
if (svg->path_in_3mf.empty())
|
|
exist_new = true;
|
|
svgfiles.push_back(svg);
|
|
}
|
|
}
|
|
|
|
if (exist_new){
|
|
MessageDialog dialog(nullptr,
|
|
_L("Are you sure you want to store original SVGs with their local paths into the 3MF file?\n"
|
|
"If you hit 'NO', all SVGs in the project will not be editable any more."),
|
|
_L("Private protection"), wxYES_NO | wxICON_QUESTION);
|
|
if (dialog.ShowModal() == wxID_NO){
|
|
for (ModelObject *object : model.objects)
|
|
for (ModelVolume *volume : object->volumes)
|
|
if (volume->emboss_shape.has_value())
|
|
volume->emboss_shape.reset();
|
|
}
|
|
}
|
|
|
|
for (SvgFile* svgfile : svgfiles){
|
|
if (!svgfile->path_in_3mf.empty())
|
|
continue; // already suggested path (previous save)
|
|
|
|
// create unique name for svgs, when local path differ
|
|
std::string filename = "unknown";
|
|
if (!svgfile->path.empty()) {
|
|
if (set_by_local_path(*svgfile, svgfiles))
|
|
continue;
|
|
// check whether original filename is already in:
|
|
filename = get_file_name(svgfile->path);
|
|
}
|
|
svgfile->path_in_3mf = create_unique_3mf_filepath(filename, svgfiles);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Plater::export_3mf(const boost::filesystem::path& output_path)
|
|
{
|
|
if (p->model.objects.empty()) {
|
|
MessageDialog dialog(nullptr, _L("The plater is empty.\nDo you want to save the project?"), _L("Save project"), wxYES_NO);
|
|
if (dialog.ShowModal() != wxID_YES)
|
|
return false;
|
|
}
|
|
|
|
wxString path;
|
|
bool export_config = true;
|
|
if (output_path.empty()) {
|
|
path = p->get_export_file(FT_3MF);
|
|
if (path.empty()) { return false; }
|
|
}
|
|
else
|
|
path = from_path(output_path);
|
|
|
|
if (!path.Lower().EndsWith(".3mf"))
|
|
return false;
|
|
|
|
// take care about private data stored into .3mf
|
|
// modify model
|
|
publish(p->model);
|
|
|
|
DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure();
|
|
const std::string path_u8 = into_u8(path);
|
|
wxBusyCursor wait;
|
|
bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames");
|
|
ThumbnailData thumbnail_data;
|
|
ThumbnailsParams thumbnail_params = { {}, false, true, true, true };
|
|
p->generate_thumbnail(thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho);
|
|
bool ret = false;
|
|
try
|
|
{
|
|
ret = Slic3r::store_3mf(path_u8.c_str(), &p->model, export_config ? &cfg : nullptr, full_pathnames, &thumbnail_data);
|
|
}
|
|
catch (boost::filesystem::filesystem_error& e)
|
|
{
|
|
const wxString what = _("Unable to save file") + ": " + path_u8 + "\n" + e.code().message();
|
|
MessageDialog dlg(this, what, _("Error saving 3mf file"), wxOK | wxICON_ERROR);
|
|
dlg.ShowModal();
|
|
}
|
|
if (ret) {
|
|
// Success
|
|
BOOST_LOG_TRIVIAL(info) << "3MF file exported to " << path;
|
|
p->set_project_filename(path);
|
|
}
|
|
else {
|
|
// Failure
|
|
const wxString what = GUI::format_wxstr("%1%: %2%", _L("Unable to save file") , path_u8);
|
|
show_error(this, what);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void Plater::reload_from_disk()
|
|
{
|
|
p->reload_from_disk();
|
|
}
|
|
|
|
void Plater::replace_with_stl()
|
|
{
|
|
p->replace_with_stl();
|
|
}
|
|
|
|
void Plater::reload_all_from_disk()
|
|
{
|
|
p->reload_all_from_disk();
|
|
}
|
|
|
|
bool Plater::has_toolpaths_to_export() const
|
|
{
|
|
return p->preview->get_canvas3d()->has_toolpaths_to_export();
|
|
}
|
|
|
|
void Plater::export_toolpaths_to_obj() const
|
|
{
|
|
if ((printer_technology() != ptFFF) || !is_preview_loaded())
|
|
return;
|
|
|
|
wxString path = p->get_export_file(FT_OBJ);
|
|
if (path.empty())
|
|
return;
|
|
|
|
wxBusyCursor wait;
|
|
p->preview->get_canvas3d()->export_toolpaths_to_obj(into_u8(path).c_str());
|
|
}
|
|
|
|
void Plater::reslice()
|
|
{
|
|
// There is "invalid data" button instead "slice now"
|
|
if (p->process_completed_with_error)
|
|
return;
|
|
|
|
// In case SLA gizmo is in editing mode, refuse to continue
|
|
// and notify user that he should leave it first.
|
|
if (canvas3D()->get_gizmos_manager().is_in_editing_mode(true))
|
|
return;
|
|
|
|
// Stop the running (and queued) UI jobs and only proceed if they actually
|
|
// get stopped.
|
|
unsigned timeout_ms = 10000;
|
|
if (!stop_queue(this->get_ui_job_worker(), timeout_ms)) {
|
|
BOOST_LOG_TRIVIAL(error) << "Could not stop UI job within "
|
|
<< timeout_ms << " milliseconds timeout!";
|
|
return;
|
|
}
|
|
|
|
if (printer_technology() == ptSLA) {
|
|
for (auto& object : model().objects)
|
|
if (object->sla_points_status == sla::PointsStatus::NoPoints)
|
|
object->sla_points_status = sla::PointsStatus::Generating;
|
|
}
|
|
|
|
//FIXME Don't reslice if export of G-code or sending to OctoPrint is running.
|
|
// bitmask of UpdateBackgroundProcessReturnState
|
|
unsigned int state = this->p->update_background_process(true);
|
|
if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE)
|
|
this->p->view3D->reload_scene(false);
|
|
// If the SLA processing of just a single object's supports is running, restart slicing for the whole object.
|
|
this->p->background_process.set_task(PrintBase::TaskParams());
|
|
// Only restarts if the state is valid.
|
|
this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART);
|
|
|
|
if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0)
|
|
return;
|
|
|
|
bool clean_gcode_toolpaths = true;
|
|
if (p->background_process.running())
|
|
{
|
|
if (wxGetApp().get_mode() == comSimple)
|
|
p->sidebar->set_btn_label(ActionButtonType::Reslice, _L("Slicing") + dots);
|
|
else
|
|
{
|
|
p->sidebar->set_btn_label(ActionButtonType::Reslice, _L("Slice now"));
|
|
p->show_action_buttons(false);
|
|
}
|
|
}
|
|
else if (!p->background_process.empty() && !p->background_process.idle())
|
|
p->show_action_buttons(true);
|
|
else
|
|
clean_gcode_toolpaths = false;
|
|
|
|
if (clean_gcode_toolpaths)
|
|
reset_gcode_toolpaths();
|
|
|
|
p->preview->reload_print(!clean_gcode_toolpaths);
|
|
}
|
|
|
|
void Plater::reslice_until_step_inner(int step, const ModelObject &object, bool postpone_error_messages)
|
|
{
|
|
//FIXME Don't reslice if export of G-code or sending to OctoPrint is running.
|
|
// bitmask of UpdateBackgroundProcessReturnState
|
|
unsigned int state = this->p->update_background_process(true, postpone_error_messages);
|
|
if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE)
|
|
this->p->view3D->reload_scene(false);
|
|
|
|
if (this->p->background_process.empty() || (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID))
|
|
// Nothing to do on empty input or invalid configuration.
|
|
return;
|
|
|
|
// Limit calculation to the single object only.
|
|
PrintBase::TaskParams task;
|
|
task.single_model_object = object.id();
|
|
// If the background processing is not enabled, calculate supports just for the single instance.
|
|
// Otherwise calculate everything, but start with the provided object.
|
|
if (!this->p->background_processing_enabled()) {
|
|
task.single_model_instance_only = true;
|
|
task.to_object_step = step;
|
|
}
|
|
this->p->background_process.set_task(task);
|
|
// and let the background processing start.
|
|
this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART);
|
|
}
|
|
|
|
void Plater::reslice_FFF_until_step(PrintObjectStep step, const ModelObject &object, bool postpone_error_messages)
|
|
{
|
|
this->reslice_until_step_inner(PrintObjectStep(step), object, postpone_error_messages);
|
|
}
|
|
|
|
void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages)
|
|
{
|
|
this->reslice_until_step_inner(SLAPrintObjectStep(step), object, postpone_error_messages);
|
|
}
|
|
|
|
namespace {
|
|
bool load_secret(const std::string& id, const std::string& opt, std::string& usr, std::string& psswd)
|
|
{
|
|
#if wxUSE_SECRETSTORE
|
|
wxSecretStore store = wxSecretStore::GetDefault();
|
|
wxString errmsg;
|
|
if (!store.IsOk(&errmsg)) {
|
|
std::string msg = GUI::format("%1% (%2%).", _u8L("This system doesn't support storing passwords securely"), errmsg);
|
|
BOOST_LOG_TRIVIAL(error) << msg;
|
|
show_error(nullptr, msg);
|
|
return false;
|
|
}
|
|
const wxString service = GUI::format_wxstr(L"%1%/PhysicalPrinter/%2%/%3%", SLIC3R_APP_NAME, id, opt);
|
|
wxString username;
|
|
wxSecretValue password;
|
|
if (!store.Load(service, username, password)) {
|
|
std::string msg(_u8L("Failed to load credentials from the system secret store."));
|
|
BOOST_LOG_TRIVIAL(error) << msg;
|
|
show_error(nullptr, msg);
|
|
return false;
|
|
}
|
|
usr = into_u8(username);
|
|
psswd = into_u8(password.GetAsString());
|
|
return true;
|
|
#else
|
|
BOOST_LOG_TRIVIAL(error) << "wxUSE_SECRETSTORE not supported. Cannot load password from the system store.";
|
|
return false;
|
|
#endif // wxUSE_SECRETSTORE
|
|
}
|
|
}
|
|
void Plater::connect_gcode()
|
|
{
|
|
assert(p->user_account->is_logged());
|
|
std::string dialog_msg;
|
|
if(PrinterPickWebViewDialog(this, dialog_msg).ShowModal() != wxID_OK) {
|
|
return;
|
|
}
|
|
if (dialog_msg.empty()) {
|
|
show_error(this, _L("Failed to select a printer. PrusaConnect did not return a value."));
|
|
return;
|
|
}
|
|
BOOST_LOG_TRIVIAL(debug) << "Message from Printer pick webview: " << dialog_msg;
|
|
|
|
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
|
// Connect data
|
|
std::vector<std::string> compatible_printers;
|
|
p->user_account->fill_compatible_printers_from_json(dialog_msg, compatible_printers);
|
|
std::string connect_nozzle = p->user_account->get_nozzle_from_json(dialog_msg);
|
|
std::string connect_filament = p->user_account->get_keyword_from_json(dialog_msg, "filament_type");
|
|
std::vector<const Preset*> compatible_printer_presets;
|
|
for (const std::string& cp : compatible_printers) {
|
|
compatible_printer_presets.emplace_back(preset_bundle->printers.find_system_preset_by_model_and_variant(cp, connect_nozzle));
|
|
}
|
|
// Selected profiles
|
|
const Preset* selected_printer_preset = &preset_bundle->printers.get_selected_preset();
|
|
const Preset* selected_filament_preset = &preset_bundle->filaments.get_selected_preset();
|
|
const std::string selected_nozzle_serialized = dynamic_cast<const ConfigOptionFloats*>(selected_printer_preset->config.option("nozzle_diameter"))->serialize();
|
|
const std::string selected_filament_serialized = selected_filament_preset->config.option("filament_type")->serialize();
|
|
const std::string selected_printer_model_serialized = selected_printer_preset->config.option("printer_model")->serialize();
|
|
|
|
bool is_first = compatible_printer_presets.front()->name == selected_printer_preset->name;
|
|
bool found = false;
|
|
for (const Preset* connect_preset : compatible_printer_presets) {
|
|
if (!connect_preset) {
|
|
continue;
|
|
}
|
|
if (selected_printer_preset->name == connect_preset->name) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
//
|
|
if (!found) {
|
|
wxString line1 = _L("The printer profile you've selected for upload is not compatible with profiles selected for slicing.");
|
|
wxString line2 = GUI::format_wxstr(_L("PrusaSlicer Profile:\n%1%"), selected_printer_preset->name);
|
|
wxString line3 = _L("Known profiles compatible with printer selected for upload:");
|
|
wxString printers_line;
|
|
for (const Preset* connect_preset : compatible_printer_presets) {
|
|
if (!connect_preset) {
|
|
continue;
|
|
}
|
|
printers_line += GUI::format_wxstr(_L("\n%1%"), connect_preset->name);
|
|
}
|
|
wxString line4 = _L("Do you still wish to upload?");
|
|
wxString message = GUI::format_wxstr("%1%\n\n%2%\n\n%3%%4%\n\n%5%", line1, line2, line3, printers_line,line4);
|
|
MessageDialog msg_dialog(this, message, _L("Do you wish to upload?"), wxYES_NO);
|
|
auto modal_res = msg_dialog.ShowModal();
|
|
if (modal_res != wxID_YES) {
|
|
return;
|
|
}
|
|
} else if (!is_first) {
|
|
wxString line1 = _L("The printer profile you've selected for upload might not be compatible with profiles selected for slicing.");
|
|
wxString line2 = GUI::format_wxstr(_L("PrusaSlicer Profile:\n%1%"), selected_printer_preset->name);
|
|
wxString line3 = _L("Known profiles compatible with printer selected for upload:");
|
|
wxString printers_line;
|
|
for (const Preset* connect_preset : compatible_printer_presets) {
|
|
if (!connect_preset) {
|
|
continue;
|
|
}
|
|
printers_line += GUI::format_wxstr(_L("\n%1%"), connect_preset->name);
|
|
}
|
|
wxString line4 = _L("Do you still wish to upload?");
|
|
wxString message = GUI::format_wxstr("%1%\n\n%2%\n\n%3%%4%\n\n%5%", line1, line2, line3, printers_line, line4);
|
|
MessageDialog msg_dialog(this, message, _L("Do you wish to upload?"), wxYES_NO);
|
|
auto modal_res = msg_dialog.ShowModal();
|
|
if (modal_res != wxID_YES) {
|
|
return;
|
|
}
|
|
}
|
|
// Commented code with selecting printers in plater
|
|
/*
|
|
// if selected (in connect) preset is not visible, make it visible and selected
|
|
if (!connect_printer_preset->is_visible) {
|
|
size_t preset_id = preset_bundle->printers.get_preset_idx_by_name(connect_printer_preset->name);
|
|
assert(preset_id != size_t(-1));
|
|
preset_bundle->printers.select_preset(preset_id);
|
|
wxGetApp().get_tab(Preset::Type::TYPE_PRINTER)->select_preset(connect_printer_preset->name);
|
|
p->notification_manager->close_notification_of_type(NotificationType::PrusaConnectPrinters);
|
|
p->notification_manager->push_notification(NotificationType::PrusaConnectPrinters, NotificationManager::NotificationLevel::ImportantNotificationLevel, format(_u8L("Changed Printer to %1%."), connect_printer_preset->name));
|
|
select_view_3D("3D");
|
|
}
|
|
// if selected (in connect) preset is not selected in slicer, select it
|
|
if (preset_bundle->printers.get_selected_preset_name() != connect_printer_preset->name) {
|
|
size_t preset_id = preset_bundle->printers.get_preset_idx_by_name(connect_printer_preset->name);
|
|
assert(preset_id != size_t(-1));
|
|
preset_bundle->printers.select_preset(preset_id);
|
|
wxGetApp().get_tab(Preset::Type::TYPE_PRINTER)->select_preset(connect_printer_preset->name);
|
|
p->notification_manager->close_notification_of_type(NotificationType::PrusaConnectPrinters);
|
|
p->notification_manager->push_notification(NotificationType::PrusaConnectPrinters, NotificationManager::NotificationLevel::ImportantNotificationLevel, format(_u8L("Changed Printer to %1%."), connect_printer_preset->name));
|
|
select_view_3D("3D");
|
|
}
|
|
*/
|
|
|
|
const std::string connect_state = p->user_account->get_keyword_from_json(dialog_msg, "connect_state");
|
|
const std::string printer_state = p->user_account->get_keyword_from_json(dialog_msg, "printer_state");
|
|
const std::map<std::string, ConnectPrinterState>& printer_state_table = p->user_account->get_printer_state_table();
|
|
const auto state = printer_state_table.find(connect_state);
|
|
assert(state != printer_state_table.end());
|
|
// TODO: all states that does not allow to upload
|
|
if (state->second == ConnectPrinterState::CONNECT_PRINTER_OFFLINE) {
|
|
show_error(this, _L("Failed to select a printer. Chosen printer is in offline state."));
|
|
return;
|
|
}
|
|
|
|
const std::string uuid = p->user_account->get_keyword_from_json(dialog_msg, "uuid");
|
|
const std::string team_id = p->user_account->get_keyword_from_json(dialog_msg, "team_id");
|
|
if (uuid.empty() || team_id.empty()) {
|
|
show_error(this, _L("Failed to select a printer. Missing data (uuid and team id) for chosen printer."));
|
|
return;
|
|
}
|
|
PhysicalPrinter ph_printer("connect_temp_printer", wxGetApp().preset_bundle->physical_printers.default_config(), /**connect_printer_preset*/*selected_printer_preset);
|
|
ph_printer.config.set_key_value("host_type", new ConfigOptionEnum<PrintHostType>(htPrusaConnectNew));
|
|
// use existing structures to pass data
|
|
ph_printer.config.opt_string("printhost_apikey") = team_id;
|
|
ph_printer.config.opt_string("print_host") = uuid;
|
|
DynamicPrintConfig* physical_printer_config = &ph_printer.config;
|
|
|
|
send_gcode_inner(physical_printer_config);
|
|
}
|
|
|
|
void Plater::send_gcode()
|
|
{
|
|
// if physical_printer is selected, send gcode for this printer
|
|
DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config();
|
|
if (! physical_printer_config || p->model.objects.empty())
|
|
return;
|
|
|
|
// Passwords and API keys
|
|
// "stored" indicates data are stored secretly, load them from store.
|
|
std::string printer_name = wxGetApp().preset_bundle->physical_printers.get_selected_printer().name;
|
|
if (physical_printer_config->opt_string("printhost_password") == "stored" && physical_printer_config->opt_string("printhost_password") == "stored") {
|
|
std::string username;
|
|
std::string password;
|
|
if (load_secret(printer_name, "printhost_password", username, password)) {
|
|
if (!username.empty())
|
|
physical_printer_config->opt_string("printhost_user") = username;
|
|
if (!password.empty())
|
|
physical_printer_config->opt_string("printhost_password") = password;
|
|
}
|
|
else {
|
|
physical_printer_config->opt_string("printhost_user") = std::string();
|
|
physical_printer_config->opt_string("printhost_password") = std::string();
|
|
}
|
|
}
|
|
/*
|
|
if (physical_printer_config->opt_string("printhost_apikey") == "stored") {
|
|
std::string username;
|
|
std::string password;
|
|
if (load_secret(printer_name, "printhost_apikey", username, password) && !password.empty())
|
|
physical_printer_config->opt_string("printhost_apikey") = password;
|
|
else
|
|
physical_printer_config->opt_string("printhost_apikey") = std::string();
|
|
}
|
|
*/
|
|
send_gcode_inner(physical_printer_config);
|
|
}
|
|
|
|
void Plater::send_gcode_inner(DynamicPrintConfig* physical_printer_config)
|
|
{
|
|
PrintHostJob upload_job(physical_printer_config);
|
|
if (upload_job.empty())
|
|
return;
|
|
|
|
// Obtain default output path
|
|
fs::path default_output_file;
|
|
try {
|
|
// Update the background processing, so that the placeholder parser will get the correct values for the ouput file template.
|
|
// Also if there is something wrong with the current configuration, a pop-up dialog will be shown and the export will not be performed.
|
|
unsigned int state = this->p->update_restart_background_process(false, false);
|
|
if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID)
|
|
return;
|
|
default_output_file = this->p->background_process.output_filepath_for_project(into_path(get_project_filename(".3mf")));
|
|
} catch (const Slic3r::PlaceholderParserError& ex) {
|
|
// Show the error with monospaced font.
|
|
show_error(this, ex.what(), true);
|
|
return;
|
|
} catch (const std::exception& ex) {
|
|
show_error(this, ex.what(), false);
|
|
return;
|
|
}
|
|
default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string()));
|
|
|
|
// Repetier specific: Query the server for the list of file groups.
|
|
wxArrayString groups;
|
|
{
|
|
wxBusyCursor wait;
|
|
upload_job.printhost->get_groups(groups);
|
|
}
|
|
// PrusaLink specific: Query the server for the list of file groups.
|
|
wxArrayString storage_paths;
|
|
wxArrayString storage_names;
|
|
{
|
|
wxBusyCursor wait;
|
|
try {
|
|
upload_job.printhost->get_storage(storage_paths, storage_names);
|
|
} catch (const Slic3r::IOError& ex) {
|
|
show_error(this, ex.what(), false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
PrintHostSendDialog dlg(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, storage_paths, storage_names);
|
|
if (dlg.ShowModal() == wxID_OK) {
|
|
|
|
if (printer_technology() == ptFFF) {
|
|
const std::string ext = boost::algorithm::to_lower_copy(dlg.filename().extension().string());
|
|
const bool binary_output = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_bool("binary_gcode") &&
|
|
wxGetApp().app_config->get_bool("use_binary_gcode_when_supported");
|
|
const wxString error_str = check_binary_vs_ascii_gcode_extension(printer_technology(), ext, binary_output);
|
|
if (! error_str.IsEmpty()) {
|
|
ErrorDialog(this, error_str, t_kill_focus([](const std::string& key) -> void { wxGetApp().jump_to_option(key); })).ShowModal();
|
|
return;
|
|
}
|
|
|
|
bool supports_binary = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_bool("binary_gcode");
|
|
bool uses_binary = wxGetApp().app_config->get_bool("use_binary_gcode_when_supported");
|
|
alert_when_exporting_binary_gcode(supports_binary && uses_binary,
|
|
wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_string("printer_notes"));
|
|
}
|
|
|
|
upload_job.upload_data.upload_path = dlg.filename();
|
|
upload_job.upload_data.post_action = dlg.post_action();
|
|
upload_job.upload_data.group = dlg.group();
|
|
upload_job.upload_data.storage = dlg.storage();
|
|
|
|
// Show "Is printer clean" dialog for PrusaConnect - Upload and print.
|
|
if (std::string(upload_job.printhost->get_name()) == "PrusaConnect" && upload_job.upload_data.post_action == PrintHostPostUploadAction::StartPrint) {
|
|
GUI::MessageDialog dlg(nullptr, _L("Is the printer ready? Is the print sheet in place, empty and clean?"), _L("Upload and Print"), wxOK | wxCANCEL);
|
|
if (dlg.ShowModal() != wxID_OK)
|
|
return;
|
|
}
|
|
|
|
p->export_gcode(fs::path(), false, std::move(upload_job));
|
|
}
|
|
|
|
}
|
|
|
|
// Called when the Eject button is pressed.
|
|
void Plater::eject_drive()
|
|
{
|
|
wxBusyCursor wait;
|
|
wxGetApp().removable_drive_manager()->eject_drive();
|
|
}
|
|
|
|
void Plater::take_snapshot(const std::string &snapshot_name) { p->take_snapshot(snapshot_name); }
|
|
void Plater::take_snapshot(const wxString &snapshot_name) { p->take_snapshot(snapshot_name); }
|
|
void Plater::take_snapshot(const std::string &snapshot_name, UndoRedo::SnapshotType snapshot_type) { p->take_snapshot(snapshot_name, snapshot_type); }
|
|
void Plater::take_snapshot(const wxString &snapshot_name, UndoRedo::SnapshotType snapshot_type) { p->take_snapshot(snapshot_name, snapshot_type); }
|
|
void Plater::suppress_snapshots() { p->suppress_snapshots(); }
|
|
void Plater::allow_snapshots() { p->allow_snapshots(); }
|
|
void Plater::undo() { p->undo(); }
|
|
void Plater::redo() { p->redo(); }
|
|
void Plater::undo_to(int selection)
|
|
{
|
|
if (selection == 0) {
|
|
p->undo();
|
|
return;
|
|
}
|
|
|
|
const int idx = p->get_active_snapshot_index() - selection - 1;
|
|
p->undo_redo_to(p->undo_redo_stack().snapshots()[idx].timestamp);
|
|
}
|
|
void Plater::redo_to(int selection)
|
|
{
|
|
if (selection == 0) {
|
|
p->redo();
|
|
return;
|
|
}
|
|
|
|
const int idx = p->get_active_snapshot_index() + selection + 1;
|
|
p->undo_redo_to(p->undo_redo_stack().snapshots()[idx].timestamp);
|
|
}
|
|
bool Plater::undo_redo_string_getter(const bool is_undo, int idx, const char** out_text)
|
|
{
|
|
const std::vector<UndoRedo::Snapshot>& ss_stack = p->undo_redo_stack().snapshots();
|
|
const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -(++idx) : idx);
|
|
|
|
if (0 < idx_in_ss_stack && (size_t)idx_in_ss_stack < ss_stack.size() - 1) {
|
|
*out_text = ss_stack[idx_in_ss_stack].name.c_str();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Plater::undo_redo_topmost_string_getter(const bool is_undo, std::string& out_text)
|
|
{
|
|
const std::vector<UndoRedo::Snapshot>& ss_stack = p->undo_redo_stack().snapshots();
|
|
const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -1 : 0);
|
|
|
|
if (0 < idx_in_ss_stack && (size_t)idx_in_ss_stack < ss_stack.size() - 1) {
|
|
out_text = ss_stack[idx_in_ss_stack].name;
|
|
return;
|
|
}
|
|
|
|
out_text = "";
|
|
}
|
|
|
|
bool Plater::update_filament_colors_in_full_config()
|
|
{
|
|
// There is a case, when we use filament_color instead of extruder_color (when extruder_color == "").
|
|
// Thus plater config option "filament_colour" should be filled with filament_presets values.
|
|
// Otherwise, on 3dScene will be used last edited filament color for all volumes with extruder_color == "".
|
|
const auto& extruders_filaments = wxGetApp().preset_bundle->extruders_filaments;
|
|
if (extruders_filaments.size() == 1 || !p->config->has("filament_colour"))
|
|
return false;
|
|
|
|
const PresetCollection& filaments = wxGetApp().preset_bundle->filaments;
|
|
std::vector<std::string> filament_colors;
|
|
filament_colors.reserve(extruders_filaments.size());
|
|
|
|
for (const auto& extr_filaments : extruders_filaments)
|
|
filament_colors.push_back(filaments.find_preset(extr_filaments.get_selected_preset_name(), true)->config.opt_string("filament_colour", (unsigned)0));
|
|
|
|
p->config->option<ConfigOptionStrings>("filament_colour")->values = filament_colors;
|
|
return true;
|
|
}
|
|
|
|
void Plater::on_config_change(const DynamicPrintConfig &config)
|
|
{
|
|
bool update_scheduled = false;
|
|
bool bed_shape_changed = false;
|
|
for (auto opt_key : p->config->diff(config)) {
|
|
if (opt_key == "filament_colour") {
|
|
update_scheduled = true; // update should be scheduled (for update 3DScene) #2738
|
|
|
|
if (update_filament_colors_in_full_config()) {
|
|
p->sidebar->obj_list()->update_extruder_colors();
|
|
continue;
|
|
}
|
|
}
|
|
if (opt_key == "material_colour") {
|
|
update_scheduled = true; // update should be scheduled (for update 3DScene)
|
|
}
|
|
|
|
p->config->set_key_value(opt_key, config.option(opt_key)->clone());
|
|
if (opt_key == "printer_technology") {
|
|
const PrinterTechnology printer_technology = config.opt_enum<PrinterTechnology>(opt_key);
|
|
this->set_printer_technology(printer_technology);
|
|
p->sidebar->show_sliced_info_sizer(false);
|
|
p->reset_gcode_toolpaths();
|
|
p->view3D->get_canvas3d()->reset_sequential_print_clearance();
|
|
p->view3D->get_canvas3d()->set_sla_view_type(GLCanvas3D::ESLAViewType::Original);
|
|
}
|
|
else if (opt_key == "bed_shape" || opt_key == "bed_custom_texture" || opt_key == "bed_custom_model") {
|
|
bed_shape_changed = true;
|
|
update_scheduled = true;
|
|
}
|
|
else if (boost::starts_with(opt_key, "wipe_tower") ||
|
|
// opt_key == "filament_minimal_purge_on_wipe_tower" // ? #ys_FIXME
|
|
opt_key == "single_extruder_multi_material") {
|
|
update_scheduled = true;
|
|
}
|
|
else if(opt_key == "variable_layer_height") {
|
|
if (p->config->opt_bool("variable_layer_height") != true) {
|
|
p->view3D->enable_layers_editing(false);
|
|
p->view3D->set_as_dirty();
|
|
}
|
|
}
|
|
else if(opt_key == "extruder_colour") {
|
|
update_scheduled = true;
|
|
p->sidebar->obj_list()->update_extruder_colors();
|
|
}
|
|
else if (opt_key == "max_print_height") {
|
|
bed_shape_changed = true;
|
|
update_scheduled = true;
|
|
}
|
|
else if (opt_key == "printer_model") {
|
|
p->reset_gcode_toolpaths();
|
|
// update to force bed selection(for texturing)
|
|
bed_shape_changed = true;
|
|
update_scheduled = true;
|
|
}
|
|
}
|
|
|
|
if (bed_shape_changed)
|
|
set_bed_shape();
|
|
|
|
if (update_scheduled)
|
|
update();
|
|
|
|
if (p->main_frame->is_loaded())
|
|
this->p->schedule_background_process();
|
|
}
|
|
|
|
void Plater::set_bed_shape() const
|
|
{
|
|
set_bed_shape(p->config->option<ConfigOptionPoints>("bed_shape")->values,
|
|
p->config->option<ConfigOptionFloat>("max_print_height")->value,
|
|
p->config->option<ConfigOptionString>("bed_custom_texture")->value,
|
|
p->config->option<ConfigOptionString>("bed_custom_model")->value);
|
|
}
|
|
|
|
void Plater::set_bed_shape(const Pointfs& shape, const double max_print_height, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) const
|
|
{
|
|
p->set_bed_shape(shape, max_print_height, custom_texture, custom_model, force_as_custom);
|
|
}
|
|
|
|
void Plater::set_default_bed_shape() const
|
|
{
|
|
set_bed_shape({ { 0.0, 0.0 }, { 200.0, 0.0 }, { 200.0, 200.0 }, { 0.0, 200.0 } }, 0.0, {}, {}, true);
|
|
}
|
|
|
|
void Plater::force_filament_colors_update()
|
|
{
|
|
bool update_scheduled = false;
|
|
DynamicPrintConfig* config = p->config;
|
|
|
|
const auto& extruders_filaments = wxGetApp().preset_bundle->extruders_filaments;
|
|
if (extruders_filaments.size() > 1 &&
|
|
p->config->option<ConfigOptionStrings>("filament_colour")->values.size() == extruders_filaments.size())
|
|
{
|
|
std::vector<std::string> filament_colors;
|
|
filament_colors.reserve(extruders_filaments.size());
|
|
|
|
for (const auto& extr_filaments : extruders_filaments)
|
|
filament_colors.push_back(extr_filaments.get_selected_preset()->config.opt_string("filament_colour", (unsigned)0));
|
|
|
|
if (config->option<ConfigOptionStrings>("filament_colour")->values != filament_colors) {
|
|
config->option<ConfigOptionStrings>("filament_colour")->values = filament_colors;
|
|
update_scheduled = true;
|
|
}
|
|
}
|
|
|
|
if (update_scheduled) {
|
|
update();
|
|
p->sidebar->obj_list()->update_extruder_colors();
|
|
}
|
|
|
|
if (p->main_frame->is_loaded())
|
|
this->p->schedule_background_process();
|
|
}
|
|
|
|
void Plater::force_filament_cb_update()
|
|
{
|
|
// Update visibility for templates presets according to app_config
|
|
PresetCollection& filaments = wxGetApp().preset_bundle->filaments;
|
|
AppConfig& config = *wxGetApp().app_config;
|
|
for (Preset& preset : filaments)
|
|
preset.set_visible_from_appconfig(config);
|
|
wxGetApp().preset_bundle->update_compatible(PresetSelectCompatibleType::Never, PresetSelectCompatibleType::OnlyIfWasCompatible);
|
|
|
|
// Update preset comboboxes on sidebar and filaments tab
|
|
p->sidebar->update_presets(Preset::TYPE_FILAMENT);
|
|
|
|
TabFilament* tab = dynamic_cast<TabFilament*>(wxGetApp().get_tab(Preset::TYPE_FILAMENT));
|
|
tab->select_preset(wxGetApp().preset_bundle->extruders_filaments[tab->get_active_extruder()].get_selected_preset_name());
|
|
}
|
|
|
|
void Plater::force_print_bed_update()
|
|
{
|
|
// Fill in the printer model key with something which cannot possibly be valid, so that Plater::on_config_change() will update the print bed
|
|
// once a new Printer profile config is loaded.
|
|
p->config->opt_string("printer_model", true) = "\x01\x00\x01";
|
|
}
|
|
|
|
void Plater::on_activate()
|
|
{
|
|
this->p->show_delayed_error_message();
|
|
}
|
|
|
|
// Get vector of extruder colors considering filament color, if extruder color is undefined.
|
|
std::vector<std::string> Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result) const
|
|
{
|
|
if (wxGetApp().is_gcode_viewer() && result != nullptr)
|
|
return result->extruder_colors;
|
|
else {
|
|
const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
|
std::vector<std::string> extruder_colors;
|
|
if (!config->has("extruder_colour")) // in case of a SLA print
|
|
return extruder_colors;
|
|
|
|
extruder_colors = (config->option<ConfigOptionStrings>("extruder_colour"))->values;
|
|
if (!wxGetApp().plater())
|
|
return extruder_colors;
|
|
|
|
const std::vector<std::string>& filament_colours = (p->config->option<ConfigOptionStrings>("filament_colour"))->values;
|
|
for (size_t i = 0; i < extruder_colors.size(); ++i)
|
|
if (extruder_colors[i] == "" && i < filament_colours.size())
|
|
extruder_colors[i] = filament_colours[i];
|
|
|
|
return extruder_colors;
|
|
}
|
|
}
|
|
|
|
/* Get vector of colors used for rendering of a Preview scene in "Color print" mode
|
|
* It consists of extruder colors and colors, saved in model.custom_gcode_per_print_z
|
|
*/
|
|
std::vector<std::string> Plater::get_colors_for_color_print(const GCodeProcessorResult* const result) const
|
|
{
|
|
std::vector<std::string> colors = get_extruder_colors_from_plater_config(result);
|
|
colors.reserve(colors.size() + p->model.custom_gcode_per_print_z.gcodes.size());
|
|
|
|
if (wxGetApp().is_gcode_viewer() && result != nullptr) {
|
|
for (const CustomGCode::Item& code : result->custom_gcode_per_print_z) {
|
|
if (code.type == CustomGCode::ColorChange)
|
|
colors.emplace_back(code.color);
|
|
}
|
|
}
|
|
else {
|
|
for (const CustomGCode::Item& code : p->model.custom_gcode_per_print_z.gcodes) {
|
|
if (code.type == CustomGCode::ColorChange)
|
|
colors.emplace_back(code.color);
|
|
}
|
|
}
|
|
|
|
return colors;
|
|
}
|
|
|
|
wxString Plater::get_project_filename(const wxString& extension) const
|
|
{
|
|
return p->get_project_filename(extension);
|
|
}
|
|
|
|
void Plater::set_project_filename(const wxString& filename)
|
|
{
|
|
p->set_project_filename(filename);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
bool Plater::is_single_full_object_selection() const
|
|
{
|
|
return p->get_selection().is_single_full_object();
|
|
}
|
|
|
|
GLCanvas3D* Plater::canvas3D()
|
|
{
|
|
return p->view3D->get_canvas3d();
|
|
}
|
|
|
|
const GLCanvas3D* Plater::canvas3D() const
|
|
{
|
|
return p->view3D->get_canvas3d();
|
|
}
|
|
|
|
GLCanvas3D* Plater::get_current_canvas3D()
|
|
{
|
|
return p->get_current_canvas3D();
|
|
}
|
|
|
|
static std::string concat_strings(const std::set<std::string> &strings,
|
|
const std::string &delim = "\n")
|
|
{
|
|
return std::accumulate(
|
|
strings.begin(), strings.end(), std::string(""),
|
|
[delim](const std::string &s, const std::string &name) {
|
|
return s + name + delim;
|
|
});
|
|
}
|
|
|
|
void Plater::arrange()
|
|
{
|
|
if (p->can_arrange()) {
|
|
auto &w = get_ui_job_worker();
|
|
arrange(w, wxGetKeyState(WXK_SHIFT));
|
|
}
|
|
}
|
|
|
|
void Plater::arrange(Worker &w, bool selected)
|
|
{
|
|
ArrangeSelectionMode mode = selected ?
|
|
ArrangeSelectionMode::SelectionOnly :
|
|
ArrangeSelectionMode::Full;
|
|
|
|
arr2::Scene arrscene{build_scene(*this, mode)};
|
|
|
|
ArrangeJob2::Callbacks cbs;
|
|
|
|
cbs.on_processed = [this](arr2::ArrangeTaskBase &t) {
|
|
p->take_snapshot(_L("Arrange"));
|
|
};
|
|
|
|
cbs.on_finished = [this](arr2::ArrangeTaskResult &t) {
|
|
std::set<std::string> names;
|
|
|
|
auto collect_unarranged = [this, &names](const arr2::TrafoOnlyArrangeItem &itm) {
|
|
if (!arr2::is_arranged(itm)) {
|
|
std::optional<ObjectID> id = arr2::retrieve_id(itm);
|
|
if (id) {
|
|
auto [mi, pos] = arr2::find_instance_by_id(p->model, *id);
|
|
if (mi && mi->get_object()) {
|
|
names.insert(mi->get_object()->name);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
for (const arr2::TrafoOnlyArrangeItem &itm : t.items)
|
|
collect_unarranged(itm);
|
|
|
|
if (!names.empty()) {
|
|
get_notification_manager()->push_notification(
|
|
GUI::format(_L("Arrangement ignored the following objects which "
|
|
"can't fit into a single bed:\n%s"),
|
|
concat_strings(names, "\n")));
|
|
}
|
|
|
|
update(static_cast<unsigned int>(UpdateParams::FORCE_FULL_SCREEN_REFRESH));
|
|
wxGetApp().obj_manipul()->set_dirty();
|
|
};
|
|
|
|
replace_job(w, std::make_unique<ArrangeJob2>(std::move(arrscene), cbs));
|
|
}
|
|
|
|
void Plater::set_current_canvas_as_dirty()
|
|
{
|
|
p->set_current_canvas_as_dirty();
|
|
}
|
|
|
|
void Plater::unbind_canvas_event_handlers()
|
|
{
|
|
p->unbind_canvas_event_handlers();
|
|
}
|
|
|
|
void Plater::reset_canvas_volumes()
|
|
{
|
|
p->reset_canvas_volumes();
|
|
}
|
|
|
|
PrinterTechnology Plater::printer_technology() const
|
|
{
|
|
return p->printer_technology;
|
|
}
|
|
|
|
const DynamicPrintConfig * Plater::config() const { return p->config; }
|
|
|
|
bool Plater::set_printer_technology(PrinterTechnology printer_technology)
|
|
{
|
|
p->printer_technology = printer_technology;
|
|
bool ret = p->background_process.select_technology(printer_technology);
|
|
if (ret) {
|
|
// Update the active presets.
|
|
}
|
|
//FIXME for SLA synchronize
|
|
//p->background_process.apply(Model)!
|
|
|
|
if (printer_technology == ptSLA) {
|
|
for (ModelObject* model_object : p->model.objects) {
|
|
model_object->ensure_on_bed();
|
|
}
|
|
}
|
|
|
|
p->label_btn_export = printer_technology == ptFFF ? L("Export G-code") : L("Export");
|
|
p->label_btn_send = printer_technology == ptFFF ? L("Send G-code") : L("Send to printer");
|
|
|
|
if (wxGetApp().mainframe != nullptr)
|
|
wxGetApp().mainframe->update_menubar();
|
|
|
|
p->update_main_toolbar_tooltips();
|
|
|
|
p->notification_manager->set_fff(printer_technology == ptFFF);
|
|
p->notification_manager->set_slicing_progress_hidden();
|
|
|
|
return ret;
|
|
}
|
|
|
|
void Plater::clear_before_change_mesh(int obj_idx, const std::string ¬ification_msg)
|
|
{
|
|
ModelObject* mo = model().objects[obj_idx];
|
|
|
|
// If there are custom supports/seams/mmu segmentation, remove them. Fixed mesh
|
|
// may be different and they would make no sense.
|
|
bool paint_removed = false;
|
|
for (ModelVolume* mv : mo->volumes) {
|
|
paint_removed |= ! mv->supported_facets.empty() || ! mv->seam_facets.empty() || ! mv->mm_segmentation_facets.empty();
|
|
mv->supported_facets.reset();
|
|
mv->seam_facets.reset();
|
|
mv->mm_segmentation_facets.reset();
|
|
}
|
|
if (paint_removed) {
|
|
// snapshot_time is captured by copy so the lambda knows where to undo/redo to.
|
|
get_notification_manager()->push_notification(
|
|
NotificationType::CustomSupportsAndSeamRemovedAfterRepair,
|
|
NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
|
|
notification_msg);
|
|
// _u8L("Undo the repair"),
|
|
// [this, snapshot_time](wxEvtHandler*){
|
|
// // Make sure the snapshot is still available and that
|
|
// // we are in the main stack and not in a gizmo-stack.
|
|
// if (undo_redo_stack().has_undo_snapshot(snapshot_time)
|
|
// && q->canvas3D()->get_gizmos_manager().get_current() == nullptr)
|
|
// undo_redo_to(snapshot_time);
|
|
// else
|
|
// notification_manager->push_notification(
|
|
// NotificationType::CustomSupportsAndSeamRemovedAfterRepair,
|
|
// NotificationManager::NotificationLevel::RegularNotificationLevel,
|
|
// _u8L("Cannot undo to before the mesh repair!"));
|
|
// return true;
|
|
// });
|
|
}
|
|
}
|
|
|
|
void Plater::changed_mesh(int obj_idx)
|
|
{
|
|
ModelObject* mo = model().objects[obj_idx];
|
|
if (p->printer_technology == ptSLA)
|
|
sla::reproject_points_and_holes(mo);
|
|
update();
|
|
p->object_list_changed();
|
|
p->schedule_background_process();
|
|
}
|
|
|
|
void Plater::changed_object(ModelObject &object){
|
|
assert(object.get_model() == &p->model); // is object from same model?
|
|
object.invalidate_bounding_box();
|
|
|
|
// recenter and re - align to Z = 0
|
|
object.ensure_on_bed(p->printer_technology != ptSLA);
|
|
|
|
if (p->printer_technology == ptSLA) {
|
|
// Update the SLAPrint from the current Model, so that the reload_scene()
|
|
// pulls the correct data, update the 3D scene.
|
|
p->update_restart_background_process(true, false);
|
|
} else
|
|
p->view3D->reload_scene(false);
|
|
|
|
// update print
|
|
p->schedule_background_process();
|
|
|
|
// Check outside bed
|
|
get_current_canvas3D()->requires_check_outside_state();
|
|
}
|
|
|
|
void Plater::changed_object(int obj_idx)
|
|
{
|
|
if (obj_idx < 0)
|
|
return;
|
|
ModelObject *object = p->model.objects[obj_idx];
|
|
if (object == nullptr)
|
|
return;
|
|
changed_object(*object);
|
|
}
|
|
|
|
void Plater::changed_objects(const std::vector<size_t>& object_idxs)
|
|
{
|
|
if (object_idxs.empty())
|
|
return;
|
|
|
|
for (size_t obj_idx : object_idxs) {
|
|
if (obj_idx < p->model.objects.size()) {
|
|
if (p->model.objects[obj_idx]->min_z() >= SINKING_Z_THRESHOLD)
|
|
// re - align to Z = 0
|
|
p->model.objects[obj_idx]->ensure_on_bed();
|
|
}
|
|
}
|
|
if (this->p->printer_technology == ptSLA) {
|
|
// Update the SLAPrint from the current Model, so that the reload_scene()
|
|
// pulls the correct data, update the 3D scene.
|
|
this->p->update_restart_background_process(true, false);
|
|
}
|
|
else {
|
|
p->view3D->reload_scene(false);
|
|
p->view3D->get_canvas3d()->update_instance_printable_state_for_objects(object_idxs);
|
|
}
|
|
|
|
// update print
|
|
this->p->schedule_background_process();
|
|
}
|
|
|
|
void Plater::schedule_background_process(bool schedule/* = true*/)
|
|
{
|
|
if (schedule)
|
|
this->p->schedule_background_process();
|
|
|
|
this->p->suppressed_backround_processing_update = false;
|
|
}
|
|
|
|
bool Plater::is_background_process_update_scheduled() const
|
|
{
|
|
return this->p->background_process_timer.IsRunning();
|
|
}
|
|
|
|
void Plater::suppress_background_process(const bool stop_background_process)
|
|
{
|
|
if (stop_background_process)
|
|
this->p->background_process_timer.Stop();
|
|
|
|
this->p->suppressed_backround_processing_update = true;
|
|
}
|
|
|
|
void Plater::mirror(Axis axis) { p->mirror(axis); }
|
|
void Plater::split_object() { p->split_object(); }
|
|
void Plater::split_volume() { p->split_volume(); }
|
|
void Plater::update_menus() { p->menus.update(); }
|
|
void Plater::show_action_buttons(const bool ready_to_slice) const { p->show_action_buttons(ready_to_slice); }
|
|
void Plater::show_action_buttons() const { p->show_action_buttons(p->ready_to_slice); }
|
|
|
|
void Plater::copy_selection_to_clipboard()
|
|
{
|
|
// At first try to copy selected values to the ObjectList's clipboard
|
|
// to check if Settings or Layers are selected in the list
|
|
// and then copy to 3DCanvas's clipboard if not
|
|
if (can_copy_to_clipboard() && !p->sidebar->obj_list()->copy_to_clipboard())
|
|
p->view3D->get_canvas3d()->get_selection().copy_to_clipboard();
|
|
}
|
|
|
|
void Plater::paste_from_clipboard()
|
|
{
|
|
if (!can_paste_from_clipboard())
|
|
return;
|
|
|
|
Plater::TakeSnapshot snapshot(this, _L("Paste From Clipboard"));
|
|
|
|
// At first try to paste values from the ObjectList's clipboard
|
|
// to check if Settings or Layers were copied
|
|
// and then paste from the 3DCanvas's clipboard if not
|
|
if (!p->sidebar->obj_list()->paste_from_clipboard())
|
|
p->view3D->get_canvas3d()->get_selection().paste_from_clipboard();
|
|
}
|
|
|
|
void Plater::msw_rescale()
|
|
{
|
|
p->preview->msw_rescale();
|
|
|
|
p->view3D->get_canvas3d()->msw_rescale();
|
|
|
|
p->sidebar->msw_rescale();
|
|
|
|
Layout();
|
|
GetParent()->Layout();
|
|
}
|
|
|
|
void Plater::sys_color_changed()
|
|
{
|
|
p->preview->sys_color_changed();
|
|
p->sidebar->sys_color_changed();
|
|
|
|
p->menus.sys_color_changed();
|
|
|
|
Layout();
|
|
GetParent()->Layout();
|
|
}
|
|
|
|
bool Plater::init_view_toolbar()
|
|
{
|
|
return p->init_view_toolbar();
|
|
}
|
|
|
|
void Plater::enable_view_toolbar(bool enable)
|
|
{
|
|
p->view_toolbar.set_enabled(enable);
|
|
}
|
|
|
|
bool Plater::init_collapse_toolbar()
|
|
{
|
|
return p->init_collapse_toolbar();
|
|
}
|
|
|
|
void Plater::enable_collapse_toolbar(bool enable)
|
|
{
|
|
p->collapse_toolbar.set_enabled(enable);
|
|
}
|
|
|
|
const Camera& Plater::get_camera() const
|
|
{
|
|
return p->camera;
|
|
}
|
|
|
|
Camera& Plater::get_camera()
|
|
{
|
|
return p->camera;
|
|
}
|
|
|
|
#if ENABLE_ENVIRONMENT_MAP
|
|
void Plater::init_environment_texture()
|
|
{
|
|
if (p->environment_texture.get_id() == 0)
|
|
p->environment_texture.load_from_file(resources_dir() + "/icons/Pmetal_001.png", false, GLTexture::SingleThreaded, false);
|
|
}
|
|
|
|
unsigned int Plater::get_environment_texture_id() const
|
|
{
|
|
return p->environment_texture.get_id();
|
|
}
|
|
#endif // ENABLE_ENVIRONMENT_MAP
|
|
|
|
const BuildVolume& Plater::build_volume() const
|
|
{
|
|
return p->bed.build_volume();
|
|
}
|
|
|
|
const GLToolbar& Plater::get_view_toolbar() const
|
|
{
|
|
return p->view_toolbar;
|
|
}
|
|
|
|
GLToolbar& Plater::get_view_toolbar()
|
|
{
|
|
return p->view_toolbar;
|
|
}
|
|
|
|
const GLToolbar& Plater::get_collapse_toolbar() const
|
|
{
|
|
return p->collapse_toolbar;
|
|
}
|
|
|
|
GLToolbar& Plater::get_collapse_toolbar()
|
|
{
|
|
return p->collapse_toolbar;
|
|
}
|
|
|
|
void Plater::set_preview_layers_slider_values_range(int bottom, int top)
|
|
{
|
|
p->set_preview_layers_slider_values_range(bottom, top);
|
|
}
|
|
|
|
void Plater::update_preview_moves_slider()
|
|
{
|
|
p->update_preview_moves_slider();
|
|
}
|
|
|
|
void Plater::enable_preview_moves_slider(bool enable)
|
|
{
|
|
p->enable_preview_moves_slider(enable);
|
|
}
|
|
|
|
void Plater::reset_gcode_toolpaths()
|
|
{
|
|
p->reset_gcode_toolpaths();
|
|
}
|
|
|
|
const Mouse3DController& Plater::get_mouse3d_controller() const
|
|
{
|
|
return p->mouse3d_controller;
|
|
}
|
|
|
|
Mouse3DController& Plater::get_mouse3d_controller()
|
|
{
|
|
return p->mouse3d_controller;
|
|
}
|
|
|
|
NotificationManager * Plater::get_notification_manager()
|
|
{
|
|
return p->notification_manager.get();
|
|
}
|
|
|
|
const NotificationManager * Plater::get_notification_manager() const
|
|
{
|
|
return p->notification_manager.get();
|
|
}
|
|
|
|
UserAccount* Plater::get_user_account()
|
|
{
|
|
return p->user_account.get();
|
|
}
|
|
|
|
const UserAccount* Plater::get_user_account() const
|
|
{
|
|
return p->user_account.get();
|
|
}
|
|
|
|
void Plater::init_notification_manager()
|
|
{
|
|
p->init_notification_manager();
|
|
}
|
|
|
|
bool Plater::can_delete() const { return p->can_delete(); }
|
|
bool Plater::can_delete_all() const { return p->can_delete_all(); }
|
|
bool Plater::can_increase_instances() const { return p->can_increase_instances(); }
|
|
bool Plater::can_decrease_instances(int obj_idx/* = -1*/) const { return p->can_decrease_instances(obj_idx); }
|
|
bool Plater::can_set_instance_to_object() const { return p->can_set_instance_to_object(); }
|
|
bool Plater::can_fix_through_winsdk() const { return p->can_fix_through_winsdk(); }
|
|
bool Plater::can_simplify() const { return p->can_simplify(); }
|
|
bool Plater::can_split_to_objects() const { return p->can_split_to_objects(); }
|
|
bool Plater::can_split_to_volumes() const { return p->can_split_to_volumes(); }
|
|
bool Plater::can_arrange() const { return p->can_arrange(); }
|
|
bool Plater::can_layers_editing() const { return p->can_layers_editing(); }
|
|
bool Plater::can_paste_from_clipboard() const
|
|
{
|
|
const Selection& selection = p->view3D->get_canvas3d()->get_selection();
|
|
const Selection::Clipboard& clipboard = selection.get_clipboard();
|
|
|
|
if (clipboard.is_empty() && p->sidebar->obj_list()->clipboard_is_empty())
|
|
return false;
|
|
|
|
if ((wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) && !clipboard.is_sla_compliant())
|
|
return false;
|
|
|
|
Selection::EMode mode = clipboard.get_mode();
|
|
if ((mode == Selection::Volume) && !selection.is_from_single_instance())
|
|
return false;
|
|
|
|
if ((mode == Selection::Instance) && (selection.get_mode() != Selection::Instance))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Plater::can_copy_to_clipboard() const
|
|
{
|
|
if (is_selection_empty())
|
|
return false;
|
|
|
|
const Selection& selection = p->view3D->get_canvas3d()->get_selection();
|
|
if ((wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) && !selection.is_sla_compliant())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Plater::can_undo() const { return p->undo_redo_stack().has_undo_snapshot(); }
|
|
bool Plater::can_redo() const { return p->undo_redo_stack().has_redo_snapshot(); }
|
|
bool Plater::can_reload_from_disk() const { return p->can_reload_from_disk(); }
|
|
bool Plater::can_replace_with_stl() const { return p->can_replace_with_stl(); }
|
|
bool Plater::can_mirror() const { return p->can_mirror(); }
|
|
bool Plater::can_split(bool to_objects) const { return p->can_split(to_objects); }
|
|
bool Plater::can_scale_to_print_volume() const { return p->can_scale_to_print_volume(); }
|
|
|
|
const UndoRedo::Stack& Plater::undo_redo_stack_main() const { return p->undo_redo_stack_main(); }
|
|
void Plater::clear_undo_redo_stack_main() { p->undo_redo_stack_main().clear(); }
|
|
void Plater::enter_gizmos_stack() { p->enter_gizmos_stack(); }
|
|
void Plater::leave_gizmos_stack() { p->leave_gizmos_stack(); }
|
|
bool Plater::inside_snapshot_capture() { return p->inside_snapshot_capture(); }
|
|
|
|
void Plater::toggle_render_statistic_dialog()
|
|
{
|
|
p->show_render_statistic_dialog = !p->show_render_statistic_dialog;
|
|
}
|
|
|
|
bool Plater::is_render_statistic_dialog_visible() const
|
|
{
|
|
return p->show_render_statistic_dialog;
|
|
}
|
|
|
|
void Plater::set_keep_current_preview_type(bool value)
|
|
{
|
|
p->preview->set_keep_current_preview_type(value);
|
|
}
|
|
|
|
Plater::TakeSnapshot::TakeSnapshot(Plater *plater, const std::string &snapshot_name)
|
|
: TakeSnapshot(plater, from_u8(snapshot_name)) {}
|
|
Plater::TakeSnapshot::TakeSnapshot(Plater* plater, const std::string& snapshot_name, UndoRedo::SnapshotType snapshot_type)
|
|
: TakeSnapshot(plater, from_u8(snapshot_name), snapshot_type) {}
|
|
|
|
|
|
// Wrapper around wxWindow::PopupMenu to suppress error messages popping out while tracking the popup menu.
|
|
bool Plater::PopupMenu(wxMenu *menu, const wxPoint& pos)
|
|
{
|
|
// Don't want to wake up and trigger reslicing while tracking the pop-up menu.
|
|
SuppressBackgroundProcessingUpdate sbpu;
|
|
// When tracking a pop-up menu, postpone error messages from the slicing result.
|
|
m_tracking_popup_menu = true;
|
|
bool out = this->wxPanel::PopupMenu(menu, pos);
|
|
m_tracking_popup_menu = false;
|
|
if (! m_tracking_popup_menu_error_message.empty()) {
|
|
// Don't know whether the CallAfter is necessary, but it should not hurt.
|
|
// The menus likely sends out some commands, so we may be safer if the dialog is shown after the menu command is processed.
|
|
wxString message = std::move(m_tracking_popup_menu_error_message);
|
|
wxTheApp->CallAfter([message, this]() { show_error(this, message); });
|
|
m_tracking_popup_menu_error_message.clear();
|
|
}
|
|
return out;
|
|
}
|
|
void Plater::bring_instance_forward()
|
|
{
|
|
p->bring_instance_forward();
|
|
}
|
|
|
|
wxMenu* Plater::object_menu() { return p->menus.object_menu(); }
|
|
wxMenu* Plater::part_menu() { return p->menus.part_menu(); }
|
|
wxMenu* Plater::text_part_menu() { return p->menus.text_part_menu(); }
|
|
wxMenu* Plater::svg_part_menu() { return p->menus.svg_part_menu(); }
|
|
wxMenu* Plater::sla_object_menu() { return p->menus.sla_object_menu(); }
|
|
wxMenu* Plater::default_menu() { return p->menus.default_menu(); }
|
|
wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); }
|
|
wxMenu* Plater::layer_menu() { return p->menus.layer_menu(); }
|
|
wxMenu* Plater::multi_selection_menu() { return p->menus.multi_selection_menu(); }
|
|
|
|
SuppressBackgroundProcessingUpdate::SuppressBackgroundProcessingUpdate() :
|
|
m_was_scheduled(wxGetApp().plater()->is_background_process_update_scheduled())
|
|
{
|
|
wxGetApp().plater()->suppress_background_process(m_was_scheduled);
|
|
}
|
|
|
|
SuppressBackgroundProcessingUpdate::~SuppressBackgroundProcessingUpdate()
|
|
{
|
|
wxGetApp().plater()->schedule_background_process(m_was_scheduled);
|
|
}
|
|
|
|
PlaterAfterLoadAutoArrange::PlaterAfterLoadAutoArrange()
|
|
{
|
|
Plater* plater = wxGetApp().plater();
|
|
m_enabled = plater->model().objects.empty() &&
|
|
plater->printer_technology() == ptFFF &&
|
|
is_XL_printer(plater->fff_print().config());
|
|
}
|
|
|
|
PlaterAfterLoadAutoArrange::~PlaterAfterLoadAutoArrange()
|
|
{
|
|
if (m_enabled)
|
|
wxGetApp().plater()->arrange();
|
|
}
|
|
|
|
}} // namespace Slic3r::GUI
|