diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index dfc5521..d2d8e72 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -247,6 +247,8 @@ set(SLIC3R_GUI_SOURCES GUI/Jobs/PlaterWorker.hpp GUI/Jobs/ArrangeJob2.hpp GUI/Jobs/ArrangeJob2.cpp + GUI/Jobs/SeqArrangeJob.hpp + GUI/Jobs/SeqArrangeJob.cpp GUI/Jobs/CreateFontNameImageJob.cpp GUI/Jobs/CreateFontNameImageJob.hpp GUI/Jobs/CreateFontStyleImagesJob.cpp diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 2470606..136f898 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -169,6 +169,9 @@ void Bed3D::render(GLCanvas3D& canvas, const Transform3d& view_matrix, const Tra shader->set_uniform("projection_matrix", projection_matrix); glsafe(::glEnable(GL_BLEND)); glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glDepthMask(GL_FALSE)); + const bool old_cullface = ::glIsEnabled(GL_CULL_FACE); + glsafe(::glDisable(GL_CULL_FACE)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); glsafe(::glBindTexture(GL_TEXTURE_2D, m_digits_texture->get_id())); @@ -185,13 +188,16 @@ void Bed3D::render(GLCanvas3D& canvas, const Transform3d& view_matrix, const Tra mat.translate(s_multiple_beds.get_bed_translation(i)); if (build_volume().type() != BuildVolume::Type::Circle) mat.translate(Vec3d(0.3 * size_x, 0.3 * size_x, 0.)); - mat.translate(Vec3d(0., 0., 0.5)); + mat.translate(Vec3d(0., 0., 0.5 * GROUND_Z)); mat.scale(Vec3d(size_x, size_x * aspect, 1.)); shader->set_uniform("view_model_matrix", mat); m_digits_models[i + 1]->render(); } glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + if (old_cullface) + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glDepthMask(GL_TRUE)); glsafe(::glDisable(GL_DEPTH_TEST)); shader->stop_using(); } diff --git a/src/slic3r/GUI/ArrangeSettingsDialogImgui.cpp b/src/slic3r/GUI/ArrangeSettingsDialogImgui.cpp index 45102ae..87a79a4 100644 --- a/src/slic3r/GUI/ArrangeSettingsDialogImgui.cpp +++ b/src/slic3r/GUI/ArrangeSettingsDialogImgui.cpp @@ -4,6 +4,11 @@ #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/GUI.hpp" +// These two should not be here. 2.9.1 is getting near and we need a quick +// way of detecting if complete_objects is used. +#include "slic3r/GUI/GUI_App.hpp" +#include "libslic3r/PresetBundle.hpp" + namespace Slic3r { namespace GUI { struct Settings { @@ -41,93 +46,100 @@ void ArrangeSettingsDialogImgui::render(float pos_x, float pos_y, bool current_b ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); - Settings settings; - read_settings(settings, m_db.get()); + if (! wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_bool("complete_objects") || wxGetApp().preset_bundle->prints.get_edited_preset().printer_technology() == ptSLA) { + Settings settings; + read_settings(settings, m_db.get()); - ImGuiPureWrap::text(GUI::format( - _L("Press %1%left mouse button to enter the exact value"), - shortkey_ctrl_prefix())); + ImGuiPureWrap::text(GUI::format( + _L("Press %1%left mouse button to enter the exact value"), + shortkey_ctrl_prefix())); - float dobj_min, dobj_max; - float dbed_min, dbed_max; + float dobj_min, dobj_max; + float dbed_min, dbed_max; - m_db->distance_from_obj_range(dobj_min, dobj_max); - m_db->distance_from_bed_range(dbed_min, dbed_max); + m_db->distance_from_obj_range(dobj_min, dobj_max); + m_db->distance_from_bed_range(dbed_min, dbed_max); - if(dobj_min > settings.d_obj) { - settings.d_obj = std::max(dobj_min, settings.d_obj); - m_db->set_distance_from_objects(settings.d_obj); + if (dobj_min > settings.d_obj) { + settings.d_obj = std::max(dobj_min, settings.d_obj); + m_db->set_distance_from_objects(settings.d_obj); + } + + if (dbed_min > settings.d_bed) { + settings.d_bed = std::max(dbed_min, settings.d_bed); + m_db->set_distance_from_bed(settings.d_bed); + } + + if (m_imgui->slider_float(_L("Spacing"), &settings.d_obj, dobj_min, + dobj_max, "%5.2f")) { + settings.d_obj = std::max(dobj_min, settings.d_obj); + m_db->set_distance_from_objects(settings.d_obj); + } + + if (m_imgui->slider_float(_L("Spacing from bed"), &settings.d_bed, + dbed_min, dbed_max, "%5.2f")) { + settings.d_bed = std::max(dbed_min, settings.d_bed); + m_db->set_distance_from_bed(settings.d_bed); + } + + if (ImGuiPureWrap::checkbox(_u8L("Enable rotations (slow)"), settings.rotations)) { + m_db->set_rotation_enabled(settings.rotations); + } + + if (m_show_xl_combo_predicate() && + settings.xl_align >= 0 && + ImGuiPureWrap::combo(_u8L("Alignment"), + { _u8L("Center"), _u8L("Rear left"), _u8L("Front left"), + _u8L("Front right"), _u8L("Rear right"), + _u8L("Random") }, + settings.xl_align)) { + if (settings.xl_align >= 0 && + settings.xl_align < ArrangeSettingsView::xlpCount) + m_db->set_xl_alignment(static_cast( + settings.xl_align)); + } + + // TRN ArrangeDialog + if (ImGuiPureWrap::combo(_u8L("Geometry handling"), + // TRN ArrangeDialog: Type of "Geometry handling" + { _u8L("Fast"), + // TRN ArrangeDialog: Type of "Geometry handling" + _u8L("Balanced"), + // TRN ArrangeDialog: Type of "Geometry handling" + _u8L("Accurate") }, + settings.geom_handling)) { + if (settings.geom_handling >= 0 && + settings.geom_handling < ArrangeSettingsView::ghCount) + m_db->set_geometry_handling( + static_cast( + settings.geom_handling)); + } + + ImGui::Separator(); + + if (ImGuiPureWrap::button(_u8L("Reset defaults"))) { + arr2::ArrangeSettingsDb::Values df = m_db->get_defaults(); + m_db->set_distance_from_objects(df.d_obj); + m_db->set_distance_from_bed(df.d_bed); + m_db->set_rotation_enabled(df.rotations); + if (m_show_xl_combo_predicate()) + m_db->set_xl_alignment(df.xl_align); + + m_db->set_geometry_handling(df.geom_handling); + m_db->set_arrange_strategy(df.arr_strategy); + + if (m_on_reset_btn) + m_on_reset_btn(); + } + ImGui::SameLine(); + } else { + ImGui::PushTextWrapPos(350.f); + ImGuiPureWrap::text(_u8L("Sequential printing is active. Arrange algorithm will use geometry of the printer " + "to optimize objects placement and avoid collisions with the gantry.")); + ImGui::PopTextWrapPos(); + ImGui::Separator(); } - if (dbed_min > settings.d_bed) { - settings.d_bed = std::max(dbed_min, settings.d_bed); - m_db->set_distance_from_bed(settings.d_bed); - } - - if (m_imgui->slider_float(_L("Spacing"), &settings.d_obj, dobj_min, - dobj_max, "%5.2f")) { - settings.d_obj = std::max(dobj_min, settings.d_obj); - m_db->set_distance_from_objects(settings.d_obj); - } - - if (m_imgui->slider_float(_L("Spacing from bed"), &settings.d_bed, - dbed_min, dbed_max, "%5.2f")) { - settings.d_bed = std::max(dbed_min, settings.d_bed); - m_db->set_distance_from_bed(settings.d_bed); - } - - if (ImGuiPureWrap::checkbox(_u8L("Enable rotations (slow)"), settings.rotations)) { - m_db->set_rotation_enabled(settings.rotations); - } - - if (m_show_xl_combo_predicate() && - settings.xl_align >= 0 && - ImGuiPureWrap::combo(_u8L("Alignment"), - {_u8L("Center"), _u8L("Rear left"), _u8L("Front left"), - _u8L("Front right"), _u8L("Rear right"), - _u8L("Random")}, - settings.xl_align)) { - if (settings.xl_align >= 0 && - settings.xl_align < ArrangeSettingsView::xlpCount) - m_db->set_xl_alignment(static_cast( - settings.xl_align)); - } - - // TRN ArrangeDialog - if (ImGuiPureWrap::combo(_u8L("Geometry handling"), - // TRN ArrangeDialog: Type of "Geometry handling" - {_u8L("Fast"), - // TRN ArrangeDialog: Type of "Geometry handling" - _u8L("Balanced"), - // TRN ArrangeDialog: Type of "Geometry handling" - _u8L("Accurate")}, - settings.geom_handling)) { - if (settings.geom_handling >= 0 && - settings.geom_handling < ArrangeSettingsView::ghCount) - m_db->set_geometry_handling( - static_cast( - settings.geom_handling)); - } - - ImGui::Separator(); - - if (ImGuiPureWrap::button(_u8L("Reset defaults"))) { - arr2::ArrangeSettingsDb::Values df = m_db->get_defaults(); - m_db->set_distance_from_objects(df.d_obj); - m_db->set_distance_from_bed(df.d_bed); - m_db->set_rotation_enabled(df.rotations); - if (m_show_xl_combo_predicate()) - m_db->set_xl_alignment(df.xl_align); - - m_db->set_geometry_handling(df.geom_handling); - m_db->set_arrange_strategy(df.arr_strategy); - - if (m_on_reset_btn) - m_on_reset_btn(); - } - - ImGui::SameLine(); - if (!current_bed && ImGuiPureWrap::button(_u8L("Arrange")) && m_on_arrange_btn) { m_on_arrange_btn(); } diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 2905f76..64cab90 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -587,11 +587,11 @@ std::string BackgroundSlicingProcess::validate(std::vector* warning // Apply config over the print. Returns false, if the new config values caused any of the already // processed steps to be invalidated, therefore the task will need to be restarted. -Print::ApplyStatus BackgroundSlicingProcess::apply(const Model &model, const DynamicPrintConfig &config) +Print::ApplyStatus BackgroundSlicingProcess::apply(const Model &model, const DynamicPrintConfig &config, std::vector *warnings) { assert(m_print != nullptr); assert(config.opt_enum("printer_technology") == m_print->technology()); - Print::ApplyStatus invalidated = m_print->apply(model, config); + Print::ApplyStatus invalidated = m_print->apply(model, config, warnings); if ((invalidated & PrintBase::APPLY_STATUS_INVALIDATED) != 0 && m_print->technology() == ptFFF && !m_fff_print->is_step_done(psGCodeExport)) { // Some FFF status was invalidated, and the G-code was not exported yet. diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp index 8ae1d07..573fc66 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.hpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp @@ -128,7 +128,7 @@ public: // Apply config over the print. Returns false, if the new config values caused any of the already // processed steps to be invalidated, therefore the task will need to be restarted. - PrintBase::ApplyStatus apply(const Model &model, const DynamicPrintConfig &config); + PrintBase::ApplyStatus apply(const Model &model, const DynamicPrintConfig &config, std::vector *warnings = nullptr); // After calling the apply() function, set_task() may be called to limit the task to be processed by process(). // This is useful for calculating SLA supports for a single object only. void set_task(const PrintBase::TaskParams ¶ms); diff --git a/src/slic3r/GUI/BedShapeDialog.cpp b/src/slic3r/GUI/BedShapeDialog.cpp index 600867d..07b345d 100644 --- a/src/slic3r/GUI/BedShapeDialog.cpp +++ b/src/slic3r/GUI/BedShapeDialog.cpp @@ -9,8 +9,9 @@ #include #include "libslic3r/BoundingBox.hpp" -#include "libslic3r/Model.hpp" #include "libslic3r/Polygon.hpp" +#include "libslic3r/FileReader.hpp" +#include "libslic3r/TriangleMesh.hpp" #include #include @@ -699,17 +700,15 @@ void BedShapePanel::load_stl() } wxBusyCursor wait; - - Model model; - try { - model = Model::read_from_file(file_name); - } - catch (std::exception &) { - show_error(this, _L("Error! Invalid model")); + TriangleMesh mesh; + try { + mesh = FileReader::load_mesh(file_name); + } + catch (std::exception& e) { + show_error(this, e.what()); return; } - auto mesh = model.mesh(); auto expolygons = mesh.horizontal_projection(); if (expolygons.size() == 0) { diff --git a/src/slic3r/GUI/BulkExportDialog.cpp b/src/slic3r/GUI/BulkExportDialog.cpp index e117f32..53436a9 100644 --- a/src/slic3r/GUI/BulkExportDialog.cpp +++ b/src/slic3r/GUI/BulkExportDialog.cpp @@ -90,6 +90,7 @@ constexpr int max_path_length = 255; struct PathValidator { std::reference_wrapper>> items; + std::string unusable_symbols; using ItemStatus = BulkExportDialog::ItemStatus; bool is_duplicate(const fs::path &path) { @@ -108,8 +109,7 @@ struct PathValidator { const fs::path &path, const std::string &filename ) { - const char* unusable_symbols = "<>[]:/\\|?*\""; - for (size_t i = 0; i < std::strlen(unusable_symbols); i++) { + for (size_t i = 0; i < std::strlen(unusable_symbols.c_str()); i++) { if (filename.find_first_of(unusable_symbols[i]) != std::string::npos) { return { ItemStatus::NoValid, @@ -196,15 +196,17 @@ void BulkExportDialog::Item::update_valid_bmp() m_valid_bmp->SetBitmap(*get_bmp_bundle(get_bmp_name(m_status))); } -BulkExportDialog::BulkExportDialog(const std::vector>> &paths): +BulkExportDialog::BulkExportDialog(const std::vector>> &paths, const wxString& title, const std::string& unusable_symbols): DPIDialog( nullptr, wxID_ANY, - _L("Export beds"), + title, wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING ) + , m_title(title) + , m_unusable_symbols(unusable_symbols) { this->SetFont(wxGetApp().normal_font()); @@ -242,7 +244,7 @@ BulkExportDialog::BulkExportDialog(const std::vector& path, int bed_index) { - m_items.push_back(std::make_unique(this, m_sizer, path, bed_index, PathValidator{m_items})); + m_items.push_back(std::make_unique(this, m_sizer, path, bed_index, PathValidator{m_items, m_unusable_symbols})); } void BulkExportDialog::accept() @@ -250,7 +252,7 @@ void BulkExportDialog::accept() if (has_warnings()) { MessageDialog dialog(nullptr, _L("Some of the selected files already exist. Do you want to replace them?"), - _L("Export beds"), wxYES_NO | wxICON_QUESTION); + m_title, wxYES_NO | wxICON_QUESTION); if (dialog.ShowModal() == wxID_NO) return; } diff --git a/src/slic3r/GUI/BulkExportDialog.hpp b/src/slic3r/GUI/BulkExportDialog.hpp index c4c9a8c..510897b 100644 --- a/src/slic3r/GUI/BulkExportDialog.hpp +++ b/src/slic3r/GUI/BulkExportDialog.hpp @@ -75,14 +75,17 @@ private: // This must be a unique ptr, because Item does not have copy nor move constructors. std::vector> m_items; wxFlexGridSizer*m_sizer{nullptr}; - + wxString m_title; + std::string m_unusable_symbols; public: - BulkExportDialog(const std::vector>> &paths); + BulkExportDialog(const std::vector>> &paths, const wxString& title, const std::string& unusable_symbols); std::vector>> get_paths() const; bool has_warnings() const; protected: + /// Called when the DPI settings of the system/display have changed + /// @param rect The new display rectangle after DPI change void on_dpi_changed(const wxRect &) override; void on_sys_color_changed() override {} diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 46363c9..c6aa389 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -348,7 +348,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) toggle_field(el, has_solid_infill); for (auto el : { "fill_angle", "bridge_angle", "infill_extrusion_width", - "infill_speed", "bridge_speed" }) + "infill_speed", "bridge_speed", "over_bridge_speed" }) toggle_field(el, have_infill || has_solid_infill); const bool has_ensure_vertical_shell_thickness = config->opt_enum("ensure_vertical_shell_thickness") != EnsureVerticalShellThickness::Disabled; @@ -424,10 +424,6 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) for (auto el : { "ironing_type", "ironing_flowrate", "ironing_spacing", "ironing_speed", "ironing_pattern"}) toggle_field(el, has_ironing); - bool have_sequential_printing = config->opt_bool("complete_objects"); - for (auto el : { "extruder_clearance_radius", "extruder_clearance_height" }) - toggle_field(el, have_sequential_printing); - bool have_ooze_prevention = config->opt_bool("ooze_prevention"); toggle_field("standby_temperature_delta", have_ooze_prevention); @@ -436,9 +432,6 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_bridging", "wipe_tower_no_sparse_layers", "single_extruder_multi_material_priming" }) toggle_field(el, have_wipe_tower); - bool have_non_zero_mmu_segmented_region_max_width = config->opt_float("mmu_segmented_region_max_width") > 0.; - toggle_field("mmu_segmented_region_interlocking_depth", have_non_zero_mmu_segmented_region_max_width); - toggle_field("avoid_crossing_curled_overhangs", !config->opt_bool("avoid_crossing_perimeters")); toggle_field("avoid_crossing_perimeters", !config->opt_bool("avoid_crossing_curled_overhangs")); @@ -463,6 +456,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) toggle_field("scarf_seam_length", uses_scarf_seam); toggle_field("scarf_seam_max_segment_length", uses_scarf_seam); toggle_field("scarf_seam_on_inner_perimeters", uses_scarf_seam); + + bool use_beam_interlocking = config->opt_bool("interlocking_beam"); + toggle_field("interlocking_beam_width", use_beam_interlocking); + toggle_field("interlocking_orientation", use_beam_interlocking); + toggle_field("interlocking_beam_layer_count", use_beam_interlocking); + toggle_field("interlocking_depth", use_beam_interlocking); + toggle_field("interlocking_boundary_avoidance", use_beam_interlocking); + toggle_field("mmu_segmented_region_max_width", !use_beam_interlocking); + + bool have_non_zero_mmu_segmented_region_max_width = !use_beam_interlocking && config->opt_float("mmu_segmented_region_max_width") > 0.; + toggle_field("mmu_segmented_region_interlocking_depth", have_non_zero_mmu_segmented_region_max_width); } void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) @@ -510,7 +514,6 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) toggle_field("branchingsupport_max_weight_on_model", supports_en && is_branching_tree); toggle_field("support_points_density_relative", supports_en); - toggle_field("support_points_minimal_distance", supports_en); bool pad_en = config->opt_bool("pad_enable"); diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 5671a6c..494250d 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -322,7 +322,7 @@ PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxSt % model.thumbnail % vendor.id % model.id; - load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width); + load_bitmap(GUI::from_u8(Slic3r::var(PRINTER_PLACEHOLDER)), bitmap, bitmap_width); } wxStaticText* title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); diff --git a/src/slic3r/GUI/DoubleSliderForLayers.cpp b/src/slic3r/GUI/DoubleSliderForLayers.cpp index 658cbd5..ba1edad 100644 --- a/src/slic3r/GUI/DoubleSliderForLayers.cpp +++ b/src/slic3r/GUI/DoubleSliderForLayers.cpp @@ -767,6 +767,11 @@ void DSForLayers::render_cog_menu() if (m_cb_change_app_config) m_cb_change_app_config("show_estimated_times_in_dbl_slider", m_show_estimated_times ? "1" : "0"); } + if (ImGuiPureWrap::menu_item_with_icon(_u8L("Sequential slider applied only to top layer").c_str(), "", icon_sz, 0, m_seq_top_layer_only)) { + m_seq_top_layer_only = !m_seq_top_layer_only; + if (m_cb_change_app_config) + m_cb_change_app_config("seq_top_layer_only", m_seq_top_layer_only ? "1" : "0"); + } if (m_mode == MultiAsSingle && m_draw_mode == dmRegular && ImGuiPureWrap::menu_item_with_icon(_u8L("Set extruder sequence for the entire print").c_str(), "")) { if (m_ticks.edit_extruder_sequence(m_ctrl.GetMaxPos(), m_mode)) diff --git a/src/slic3r/GUI/DoubleSliderForLayers.hpp b/src/slic3r/GUI/DoubleSliderForLayers.hpp index 4cb7375..b0b92f2 100644 --- a/src/slic3r/GUI/DoubleSliderForLayers.hpp +++ b/src/slic3r/GUI/DoubleSliderForLayers.hpp @@ -87,6 +87,7 @@ public: void set_imgui_wrapper(Slic3r::GUI::ImGuiWrapper* imgui) { m_imgui = imgui; } void show_estimated_times(bool show) { m_show_estimated_times = show; } void show_ruler(bool show, bool show_bg) { m_show_ruler = show; m_show_ruler_bg = show_bg; } + void seq_top_layer_only(bool show) { m_seq_top_layer_only = show; } // manipulation with slider from keyboard @@ -147,6 +148,7 @@ private: bool m_show_ruler_bg { true }; bool m_show_cog_menu { false }; bool m_show_edit_menu { false }; + bool m_seq_top_layer_only { false }; int m_pos_on_move { -1 }; DrawMode m_draw_mode { dmRegular }; diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index dae1c51..7fadf66 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -298,9 +298,11 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true } } else { - show_error(m_parent, _L("Input value is out of range")); - if (m_opt.min > val) val = m_opt.min; - if (val > m_opt.max) val = m_opt.max; + if (val < (m_opt.min - EPSILON) || val > (m_opt.max + EPSILON)) { + show_error(m_parent, _L("Input value is out of range")); + } + + val = std::clamp(static_cast(val), m_opt.min, m_opt.max); set_value(double_to_string(val), true); } } diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 4d35804..36545b0 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -211,46 +211,164 @@ int GCodeViewer::SequentialView::ActualSpeedImguiWidget::plot(const char* label, } #endif // ENABLE_ACTUAL_SPEED_DEBUG -void GCodeViewer::SequentialView::Marker::init() -{ - m_model.init_from(stilized_arrow(16, 2.0f, 4.0f, 1.0f, 8.0f)); - m_model.set_color({ 1.0f, 1.0f, 1.0f, 0.5f }); +void GCodeViewer::SequentialView::Marker::init(std::optional>& model_opt, bool is_ht90) + { + if (! model_opt.has_value()) + return; + + m_model.reset(); + m_is_ht90 = is_ht90; + + m_generic_marker = (model_opt->get() == nullptr); + if (m_generic_marker) + m_model.init_from(stilized_arrow(16, 2.0f, 4.0f, 1.0f, 8.0f)); + else + m_model = **model_opt; + + m_model.set_color({ 1.0f, 1.0f, 1.0f, 0.5f }); + } + + + + +// This does not do any scaling! +static Transform3d align_cylinder(const Vec3d& p1, const Vec3d& p2, double r) { + Vec3d direction = p2 - p1; + Vec3d axis = direction.normalized(); + Vec3d z0(0, 0, 1); + Matrix3d rotation; + if (axis.isApprox(z0)) + rotation = Eigen::Matrix3d::Identity(); // Already aligned, use identity rotation + else if (axis.isApprox(-z0)) { + // 180-degree rotation around x or y (choose any perpendicular axis) + rotation = Eigen::AngleAxisd(M_PI, Vec3d(1, 0, 0)).toRotationMatrix(); + } else { + Vec3d rotationAxis = z0.cross(axis); + double angle = acos(z0.dot(axis)); + rotation = Eigen::AngleAxisd(angle, rotationAxis.normalized()).toRotationMatrix(); + } + Transform3d transform = Transform3d::Identity(); + transform.linear() = rotation;// * Geometry::scale_transform(Vec3d(2*r,2*r,length)).matrix().block<3,3>(0,0); + transform.translation() = p1; + return transform; } +static void render_ht90_rods(const Vec3d& pos, GLShaderProgram* shader, const Transform3d& view_matrix, const Vec3d& bed_offset, GLModel& model) +{ + Vec3d start(30.06, 27.70, 35); // Position of the back-right ball bearing, on the extruder head. + Vec3d end(32.33, 212.92, 338.8); // The other ball bearing, on the printer. + double r=4.725; // Diameter of the rod. + double rods_spacing = 60.; + double height = (end-start).norm(); + + + if (!model.is_initialized()) { + auto t0 = its_make_sphere(r, 0.5); + auto t1 = its_make_sphere(r, 0.5); + auto t2 = its_make_cylinder(r, height); + its_translate(t1, Vec3f(0., 0., height)); + its_merge(t2, t0); + its_merge(t2, t1); + model.init_from(t2); + model.set_color({ 1.0f, 1.0f, 1.0f, 0.5f }); + } + + + // trans transforms extruder to world coord system of the first bed + Transform3d trans = Geometry::translation_transform(pos); + + Vec3d p1 = trans * start; + Vec3d p2 = end; // already in world coords + + for (int j=0; j<6; ++j) { + if (j == 3) { + p1.x() = p1.x() - rods_spacing; + p2.x() = p2.x() - rods_spacing; + } + + p1 = trans * Geometry::rotation_transform(Vec3d(0,0,2*M_PI/3)) * trans.inverse() * p1; + p2 = Geometry::rotation_transform(Vec3d(0,0,2*M_PI/3)) * p2; + + double dx = p2.x() - p1.x(); + double dy = p2.y() - p1.y(); + double dz = std::sqrt(height * height - dx * dx - dy * dy); + p2.z() = p1.z() + dz; + + Transform3d wm = align_cylinder(p1, p2, r); + shader->set_uniform("view_model_matrix", view_matrix * wm); + shader->set_uniform("volume_world_matrix", Geometry::translation_transform(bed_offset) * wm); + model.render(); + } + +} + + + void GCodeViewer::SequentialView::Marker::render() { if (!m_visible) return; - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + GLShaderProgram* shader = wxGetApp().get_shader("tool_marker"); if (shader == nullptr) return; glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + const bool curr_cull_face = glIsEnabled(GL_CULL_FACE); + glsafe(::glDisable(GL_CULL_FACE)); + shader->start_using(); - shader->set_uniform("emission_factor", 0.0f); const Camera& camera = wxGetApp().plater()->get_camera(); Transform3d view_matrix = camera.get_view_matrix(); - view_matrix.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); + Vec3d bed_inst_offset = s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()); + view_matrix.translate(bed_inst_offset); + + std::array, 2> clip_planes; + if (m_generic_marker) + // dummy values, generic marker does not need clipping + clip_planes = {{ { 1.0f, 0.0f, 0.0f, FLT_MAX }, { 1.0f, 0.0f, 0.0f, FLT_MAX } }}; + else { + BoundingBoxf box = s_multiple_beds.get_build_volume_box(); + box.translate(to_2d(bed_inst_offset)); + // add a bit on both sides + box = box.inflated(m_is_ht90 ? 60.f : 40.f); + clip_planes = {{ { 1.0f, 0.0f, 0.0f, -box.min.cast().x() } , { -1.0f, 0.0f, 0.0f, box.max.cast().x() }}}; + } float scale_factor = m_scale_factor; if (m_fixed_screen_size) scale_factor *= 10.0f * camera.get_inv_zoom(); - const Transform3d model_matrix = (Geometry::translation_transform((m_world_position + m_model_z_offset * Vec3f::UnitZ()).cast()) * - Geometry::translation_transform(scale_factor * m_model.get_bounding_box().size().z() * Vec3d::UnitZ()) * Geometry::rotation_transform({ M_PI, 0.0, 0.0 })) * - Geometry::scale_transform(scale_factor); + const Transform3d model_matrix = m_generic_marker + ? Geometry::translation_transform((m_world_position + m_model_z_offset * Vec3f::UnitZ()).cast()) * + Geometry::translation_transform(scale_factor * m_model.get_bounding_box().size().z() * Vec3d::UnitZ()) * + Geometry::rotation_transform({ M_PI, 0.0, 0.0 }) * + Geometry::scale_transform(scale_factor) + : Geometry::translation_transform(m_world_position.cast()); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); shader->set_uniform("view_normal_matrix", view_normal_matrix); + Transform3d volume_world_matrix = model_matrix; + if (!m_generic_marker) + volume_world_matrix = Geometry::translation_transform(bed_inst_offset) * volume_world_matrix; + shader->set_uniform("volume_world_matrix", volume_world_matrix); + shader->set_uniform("clipping_planes[0]", clip_planes[0]); + shader->set_uniform("clipping_planes[1]", clip_planes[1]); + + shader->set_uniform("volume_world_matrix", volume_world_matrix); m_model.render(); + if (m_is_ht90 && ! m_generic_marker) + render_ht90_rods(m_world_position.cast(), shader, view_matrix, bed_inst_offset, m_model_ht90_rod); + shader->stop_using(); + if (curr_cull_face) + glsafe(::glEnable(GL_CULL_FACE)); glsafe(::glDisable(GL_BLEND)); } @@ -836,9 +954,6 @@ void GCodeViewer::init() if (m_gl_data_initialized) return; - // initializes tool marker - m_sequential_view.marker.init(); - m_gl_data_initialized = true; try @@ -1097,6 +1212,8 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_conflict_result = gcode_result.conflict_result; if (m_conflict_result.has_value()) m_conflict_result->layer = m_viewer.get_layer_id_at(static_cast(m_conflict_result->_height)); + + m_sequential_collision_detected = gcode_result.sequential_collision_detected; } void GCodeViewer::load_as_preview(libvgcode::GCodeInputData&& data) @@ -1161,6 +1278,12 @@ void GCodeViewer::render() const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex(); m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position)); m_sequential_view.marker.set_z_offset(m_z_offset); + + // Following just makes sure that the shown marker is correct. + auto [marker_model_opt, is_ht90] = wxGetApp().plater()->get_current_canvas3D()->get_current_marker_model(); + m_sequential_view.marker.init(marker_model_opt, is_ht90); + if (marker_model_opt.has_value()) + m_max_bounding_box.reset(); m_sequential_view.render(legend_height, &m_viewer, curr_vertex.gcode_id); } } diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 9d552cd..946119a 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -109,6 +109,7 @@ public: class Marker { GLModel m_model; + GLModel m_model_ht90_rod; Vec3f m_world_position; // For seams, the position of the marker is on the last endpoint of the toolpath containing it. // This offset is used to show the correct value of tool position in the "ToolPosition" window. @@ -121,14 +122,21 @@ public: bool m_visible{ true }; bool m_fixed_screen_size{ false }; float m_scale_factor{ 1.0f }; + bool m_generic_marker{ true }; + bool m_is_ht90{false}; #if ENABLE_ACTUAL_SPEED_DEBUG ActualSpeedImguiWidget m_actual_speed_imgui_widget; #endif // ENABLE_ACTUAL_SPEED_DEBUG public: - void init(); + void init(std::optional>& model_opt, bool is_ht90); - const BoundingBoxf3& get_bounding_box() const { return m_model.get_bounding_box(); } + BoundingBoxf3 get_bounding_box() const { + auto bb = m_model.get_bounding_box(); + if (m_is_ht90) + bb.max.z() += scale_(400); + return bb; + } void set_world_position(const Vec3f& position) { m_world_position = position; } void set_world_offset(const Vec3f& offset) { m_world_offset = offset; } @@ -254,6 +262,7 @@ private: bool m_contained_in_bed{ true }; ConflictResultOpt m_conflict_result; + std::optional> m_sequential_collision_detected; libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; @@ -350,6 +359,7 @@ public: void invalidate_legend() { m_legend_resizer.reset(); } const ConflictResultOpt& get_conflict_result() const { return m_conflict_result; } + std::optional> get_sequential_collision_detected() const { return m_sequential_collision_detected; } void load_shells(const Print& print); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index a0928d0..3e562a5 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1,6 +1,11 @@ #include "libslic3r/libslic3r.h" #include "GLCanvas3D.hpp" +#include + +#include +#include + #include // IWYU pragma: keep #include #include @@ -34,6 +39,7 @@ #include "I18N.hpp" #include "NotificationManager.hpp" #include "format.hpp" +#include "libslic3r/ArrangeHelper.hpp" #include "slic3r/GUI/BitmapCache.hpp" #include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp" @@ -66,6 +72,7 @@ #include #include #include +#include #include #include @@ -119,8 +126,6 @@ void GLCanvas3D::select_bed(int i, bool triggered_by_user) } } wxGetApp().plater()->canvas3D()->m_process->stop(); - m_sequential_print_clearance.m_evaluating = true; - reset_sequential_print_clearance(); post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, is_sliceable(s_print_statuses[i]))); @@ -150,6 +155,72 @@ void GLCanvas3D::select_bed(int i, bool triggered_by_user) }); } +// Returns extruder model to visualize in the GCodeViewer: +// - nullopt = same as before +// - nullptr = none available, use generic +// - GLModel = the model to use +// The other field is whether the printer is HT90. +std::pair>, bool> GLCanvas3D::get_current_marker_model() const +{ + auto out = std::make_pair>, bool>(std::nullopt, false); + + static std::string last_printer_notes; + static double old_r = 0.; + static double old_h = 0.; + static bool old_seq = false; + + std::string printer_notes = m_config->opt_string("printer_notes"); + double r = m_config->opt_float("extruder_clearance_radius"); + double h = m_config->opt_float("extruder_clearance_height"); + bool seq = m_config->opt_bool("complete_objects"); + + if (last_printer_notes != printer_notes || r != old_r || h != old_h || seq != old_seq) { + last_printer_notes = printer_notes; + old_r = r; + old_h = h; + old_seq = seq; + + out.second = (printer_notes.find("PRINTER_MODEL_HT90") != std::string::npos); + + out.first = std::make_optional(nullptr); + if (! seq) + return out; + try { + boost::nowide::ifstream in(resources_dir() + "/data/printer_gantries/geometries.json"); + boost::property_tree::ptree pt; + boost::property_tree::read_json(in, pt); + for (const auto& printer : pt.get_child("printers")) { + std::string printer_notes_match = printer.second.get("printer_notes_regex"); + boost::regex rgx(printer_notes_match); + if (boost::regex_match(printer_notes, rgx)) { + std::string filename = resources_dir() + "/data/printer_gantries/" + printer.second.get("gantry_model_filename"); + if (boost::filesystem::exists(filename)) { + std::unique_ptr m = std::make_unique(); + if (m->init_from_file(filename)) + out.first = std::make_optional(std::move(m)); + } + break; + } + } + } catch (...) { + // Whatever happened, ignore it. We will return nullptr. + } + if (*(out.first) == nullptr && seq) { + // Generic sequential extruder model. + double gantry_height = 10; + auto mesh = its_make_cylinder(r, h + gantry_height - 0.001); + double d = 3 * wxGetApp().plater()->build_volume().bounding_volume2d().size().x(); + auto mesh2 = its_make_cube(d,2*r, gantry_height); + its_translate(mesh2, Vec3f(-d/2, -r, h)); + its_merge(mesh, mesh2); + std::unique_ptr m = std::make_unique(); + m->init_from(mesh); + out.first = std::make_optional(std::move(m)); + } + } + return out; +} + #ifdef __WXGTK3__ // wxGTK3 seems to simulate OSX behavior in regard to HiDPI scaling support. RetinaHelper::RetinaHelper(wxWindow* window) : m_window(window), m_self(nullptr) {} @@ -921,141 +992,6 @@ void GLCanvas3D::Tooltip::render(const Vec2d& mouse_position, GLCanvas3D& canvas ImGui::PopStyleVar(2); } -void GLCanvas3D::SequentialPrintClearance::set_contours(const ContoursList& contours, bool generate_fill) -{ - m_contours.clear(); - m_instances.clear(); - m_fill.reset(); - - if (contours.empty()) - return; - - const Vec3d bed_offset = generate_fill ? s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()) : Vec3d::Zero(); - - if (generate_fill) { - GLModel::Geometry fill_data; - fill_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - fill_data.color = { 0.3333f, 0.0f, 0.0f, 0.5f }; - - // vertices + indices - const ExPolygons polygons_union = union_ex(contours.contours); - unsigned int vertices_counter = 0; - for (const ExPolygon& poly : polygons_union) { - const std::vector triangulation = triangulate_expolygon_3d(poly); - fill_data.reserve_vertices(fill_data.vertices_count() + triangulation.size()); - fill_data.reserve_indices(fill_data.indices_count() + triangulation.size()); - for (const Vec3d& v : triangulation) { - fill_data.add_vertex((Vec3f)((bed_offset + v).cast() + 0.0125f * Vec3f::UnitZ())); // add a small positive z to avoid z-fighting - ++vertices_counter; - if (vertices_counter % 3 == 0) - fill_data.add_triangle(vertices_counter - 3, vertices_counter - 2, vertices_counter - 1); - } - } - m_fill.init_from(std::move(fill_data)); - } - - const Transform3d bed_transform = Geometry::translation_transform(bed_offset); - - for (size_t i = 0; i < contours.contours.size(); ++i) { - GLModel& model = m_contours.emplace_back(GLModel()); - model.init_from(contours.contours[i], 0.025f); // add a small positive z to avoid z-fighting - } - - if (contours.trafos.has_value()) { - // create the requested instances - for (const auto& instance : *contours.trafos) { - m_instances.emplace_back(instance.first, bed_transform * instance.second); - } - } - else { - // no instances have been specified - // create one instance for every polygon - for (size_t i = 0; i < contours.contours.size(); ++i) { - m_instances.emplace_back(i, bed_transform); - } - } -} - -void GLCanvas3D::SequentialPrintClearance::update_instances_trafos(const std::vector& trafos) -{ - if (trafos.size() == m_instances.size()) { - for (size_t i = 0; i < trafos.size(); ++i) { - m_instances[i].second = trafos[i]; - } - } - else - assert(false); -} - -void GLCanvas3D::SequentialPrintClearance::render() -{ - static const ColorRGBA FILL_COLOR = { 1.0f, 0.0f, 0.0f, 0.5f }; - static const ColorRGBA NO_FILL_COLOR = { 1.0f, 1.0f, 1.0f, 0.75f }; - static const ColorRGBA NO_FILL_EVALUATING_COLOR = { 1.0f, 1.0f, 0.0f, 1.0f }; - - if (m_contours.empty() || m_instances.empty()) - return; - - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glDisable(GL_CULL_FACE)); - glsafe(::glEnable(GL_BLEND)); - glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - - if (!m_evaluating && !m_dragging) - m_fill.render(); - -#if !SLIC3R_OPENGL_ES - if (OpenGLManager::get_gl_info().is_core_profile()) { -#endif // !SLIC3R_OPENGL_ES - shader->stop_using(); - -#if SLIC3R_OPENGL_ES - shader = wxGetApp().get_shader("dashed_lines"); -#else - shader = wxGetApp().get_shader("dashed_thick_lines"); -#endif // SLIC3R_OPENGL_ES - if (shader == nullptr) - return; - - shader->start_using(); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 1.0f); - shader->set_uniform("gap_size", 0.0f); -#if !SLIC3R_OPENGL_ES - } - else - glsafe(::glLineWidth(2.0f)); -#endif // !SLIC3R_OPENGL_ES - - const ColorRGBA color = (!m_evaluating && !m_dragging && m_fill.is_initialized()) ? FILL_COLOR : - m_evaluating ? NO_FILL_EVALUATING_COLOR : NO_FILL_COLOR; - - for (const auto& [id, trafo] : m_instances) { - shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo); - assert(id < m_contours.size()); - m_contours[id].set_color(color); - m_contours[id].render(); - } - - glsafe(::glDisable(GL_BLEND)); - glsafe(::glEnable(GL_CULL_FACE)); - glsafe(::glDisable(GL_DEPTH_TEST)); - - shader->stop_using(); -} - wxDEFINE_EVENT(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_OBJECT_SELECT, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, RBtnEvent); @@ -1411,10 +1347,10 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas *canvas, Bed3D &bed) return this->is_arrange_alignment_enabled(); }); m_arrange_settings_dialog.on_arrange_btn([]{ - wxGetApp().plater()->arrange(); + wxGetApp().plater()->arrange(false); }); m_arrange_settings_dialog.on_arrange_bed_btn([]{ - wxGetApp().plater()->arrange_current_bed(); + wxGetApp().plater()->arrange(true); }); } @@ -2235,10 +2171,6 @@ void GLCanvas3D::render() if (show_imgui_demo_window) ImGui::ShowDemoWindow(); #endif // SHOW_IMGUI_DEMO_WINDOW - - - - const bool is_looking_downward = camera.is_looking_downward(); // draw scene @@ -2256,7 +2188,6 @@ void GLCanvas3D::render() _render_gcode(); _render_objects(GLVolumeCollection::ERenderType::Transparent); - _render_sequential_clearance(); #if ENABLE_RENDER_SELECTION_CENTER _render_selection_center(); #endif // ENABLE_RENDER_SELECTION_CENTER @@ -3017,6 +2948,7 @@ void GLCanvas3D::load_gcode_preview(const GCodeProcessorResult& gcode_result, co isToolpathOutside = false; _set_warning_notification_if_needed(EWarning::ToolpathOutside); _set_warning_notification_if_needed(EWarning::GCodeConflict); + _set_warning_notification_if_needed(EWarning::SequentialCollision); } set_as_dirty(); @@ -3907,8 +3839,6 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) c == GLGizmosManager::EType::Scale || c == GLGizmosManager::EType::Rotate) { show_sinking_contours(); - if (_is_sequential_print_enabled()) - update_sequential_clearance(true); } } else if (evt.LeftUp() && @@ -4067,10 +3997,6 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_selection.setup_cache(); if (!evt.CmdDown()) m_mouse.drag.start_position_3D = m_mouse.scene_position; - m_sequential_print_clearance.m_first_displacement = true; - if (_is_sequential_print_enabled()) - update_sequential_clearance(true); - m_sequential_print_clearance.start_dragging(); } } } @@ -4119,8 +4045,6 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) TransformationType trafo_type; trafo_type.set_relative(); m_selection.translate(cur_pos - m_mouse.drag.start_position_3D, trafo_type); - if (_is_sequential_print_enabled()) - update_sequential_clearance(false); wxGetApp().obj_manipul()->set_dirty(); m_dirty = true; @@ -4198,8 +4122,6 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) else if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) { m_mouse.position = pos.cast(); - if (evt.LeftUp() && m_sequential_print_clearance.is_dragging()) - m_sequential_print_clearance.stop_dragging(); if (evt.RightUp() && m_mouse.is_start_position_2D_defined()) { // forces camera target to be on the plane z = 0 Camera& camera = wxGetApp().plater()->get_camera(); @@ -4479,11 +4401,6 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) post_event(SimpleEvent(EVT_GLCANVAS_WIPETOWER_TOUCHED)); } - if (_is_sequential_print_enabled()) { - update_sequential_clearance(true); - m_sequential_print_clearance.m_evaluating = true; - } - m_dirty = true; } @@ -4577,11 +4494,6 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) if (!done.empty()) post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); - if (_is_sequential_print_enabled()) { - update_sequential_clearance(true); - m_sequential_print_clearance.m_evaluating = true; - } - m_dirty = true; } @@ -4654,11 +4566,6 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) if (!done.empty()) post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED)); - if (_is_sequential_print_enabled()) { - update_sequential_clearance(true); - m_sequential_print_clearance.m_evaluating = true; - } - m_dirty = true; } @@ -4912,137 +4819,6 @@ void GLCanvas3D::mouse_up_cleanup() m_canvas->ReleaseMouse(); } -void GLCanvas3D::update_sequential_clearance(bool force_contours_generation) -{ - if (!_is_sequential_print_enabled()) - return; - - if (m_layers_editing.is_enabled()) - return; - - auto instance_transform_from_volumes = [this](int object_idx, int instance_idx) { - for (const GLVolume* v : m_volumes.volumes) { - if (v->object_idx() == object_idx && v->instance_idx() == instance_idx) - return v->get_instance_transformation(); - } - assert(false); - return Geometry::Transformation(); - }; - - auto is_object_outside_printbed = [this](int object_idx) { - for (const GLVolume* v : m_volumes.volumes) { - if (v->object_idx() == object_idx && v->is_outside) - return true; - } - return false; - }; - - // collects instance transformations from volumes - // first: define temporary cache - unsigned int instances_count = 0; - std::vector>> instance_transforms; - for (size_t obj = 0; obj < m_model->objects.size(); ++obj) { - instance_transforms.emplace_back(std::vector>()); - const ModelObject* model_object = m_model->objects[obj]; - for (size_t i = 0; i < model_object->instances.size(); ++i) { - instance_transforms[obj].emplace_back(std::optional()); - ++instances_count; - } - } - - if (instances_count == 1) - return; - - // second: fill temporary cache with data from volumes - for (const GLVolume* v : m_volumes.volumes) { - if (v->is_wipe_tower()) - continue; - - const int object_idx = v->object_idx(); - const int instance_idx = v->instance_idx(); - auto& transform = instance_transforms[object_idx][instance_idx]; - if (!transform.has_value()) - transform = instance_transform_from_volumes(object_idx, instance_idx); - } - - // helper function to calculate the transformation to be applied to the sequential print clearance contours - auto instance_trafo = [](const Transform3d& hull_trafo, const Geometry::Transformation& inst_trafo) { - Vec3d offset = inst_trafo.get_offset() - hull_trafo.translation(); - offset.z() = 0.0; - return Geometry::translation_transform(offset) * - Geometry::rotation_transform(Geometry::rotation_diff_z(hull_trafo, inst_trafo.get_matrix()) * Vec3d::UnitZ()); - }; - - // calculates objects 2d hulls (see also: Print::sequential_print_horizontal_clearance_valid()) - // this is done only the first time this method is called while moving the mouse, - // the results are then cached for following displacements - if (force_contours_generation || m_sequential_print_clearance.m_first_displacement) { - m_sequential_print_clearance.m_evaluating = false; - m_sequential_print_clearance.m_hulls_2d_cache.clear(); - const float shrink_factor = static_cast(scale_(0.5 * fff_print()->config().extruder_clearance_radius.value - EPSILON)); - const double mitter_limit = scale_(0.1); - m_sequential_print_clearance.m_hulls_2d_cache.reserve(m_model->objects.size()); - for (size_t i = 0; i < m_model->objects.size(); ++i) { - ModelObject* model_object = m_model->objects[i]; - Geometry::Transformation trafo = instance_transform_from_volumes((int)i, 0); - trafo.set_offset({ 0.0, 0.0, trafo.get_offset().z() }); - Pointf3s& new_hull_2d = m_sequential_print_clearance.m_hulls_2d_cache.emplace_back(std::make_pair(Pointf3s(), trafo.get_matrix())).first; - if (is_object_outside_printbed((int)i)) - continue; - - Polygon hull_2d = model_object->convex_hull_2d(trafo.get_matrix()); - if (!hull_2d.empty()) { - // Shrink the extruder_clearance_radius a tiny bit, so that if the object arrangement algorithm placed the objects - // exactly by satisfying the extruder_clearance_radius, this test will not trigger collision. - const Polygons offset_res = offset(hull_2d, shrink_factor, jtRound, mitter_limit); - if (!offset_res.empty()) - hull_2d = offset_res.front(); - } - - new_hull_2d.reserve(hull_2d.points.size()); - for (const Point& p : hull_2d.points) { - new_hull_2d.emplace_back(Vec3d(unscale(p.x()), unscale(p.y()), 0.0)); - } - } - - ContoursList contours; - contours.contours.reserve(instance_transforms.size()); - contours.trafos = std::vector>(); - (*contours.trafos).reserve(instances_count); - for (size_t i = 0; i < instance_transforms.size(); ++i) { - const auto& [hull, hull_trafo] = m_sequential_print_clearance.m_hulls_2d_cache[i]; - Points hull_pts; - hull_pts.reserve(hull.size()); - for (size_t j = 0; j < hull.size(); ++j) { - hull_pts.emplace_back(scaled(hull[j].x()), scaled(hull[j].y())); - } - contours.contours.emplace_back(Geometry::convex_hull(std::move(hull_pts))); - - const auto& instances = instance_transforms[i]; - for (const auto& instance : instances) { - (*contours.trafos).emplace_back(i, instance_trafo(hull_trafo, *instance)); - } - } - - set_sequential_print_clearance_contours(contours, false); - m_sequential_print_clearance.m_first_displacement = false; - } - else { - if (!m_sequential_print_clearance.empty()) { - std::vector trafos; - trafos.reserve(instances_count); - for (size_t i = 0; i < instance_transforms.size(); ++i) { - const auto& [hull, hull_trafo] = m_sequential_print_clearance.m_hulls_2d_cache[i]; - const auto& instances = instance_transforms[i]; - for (const auto& instance : instances) { - trafos.emplace_back(instance_trafo(hull_trafo, *instance)); - } - } - m_sequential_print_clearance.update_instances_trafos(trafos); - } - } -} - bool GLCanvas3D::is_object_sinking(int object_idx) const { for (const GLVolume* v : m_volumes.volumes) { @@ -6542,32 +6318,6 @@ void GLCanvas3D::_render_selection() #endif // ENABLE_MATRICES_DEBUG } -void GLCanvas3D::_render_sequential_clearance() -{ - if (!_is_sequential_print_enabled()) - return; - - if (m_layers_editing.is_enabled()) - return; - - switch (m_gizmos.get_current_type()) - { - case GLGizmosManager::EType::Flatten: - case GLGizmosManager::EType::Cut: - case GLGizmosManager::EType::MmSegmentation: - case GLGizmosManager::EType::Measure: - case GLGizmosManager::EType::Emboss: - case GLGizmosManager::EType::Simplify: - case GLGizmosManager::EType::FdmSupports: - case GLGizmosManager::EType::Seam: - case GLGizmosManager::EType::FuzzySkin: { return; } - default: { break; } - } - - m_sequential_print_clearance.render(); -} - - bool GLCanvas3D::check_toolbar_icon_size(float init_scale, float& new_scale_to_save, bool is_custom, int counter/* = 3*/) { const Size cnv_size = get_canvas_size(); @@ -6671,7 +6421,11 @@ void GLCanvas3D::_render_overlays() if (_is_sequential_print_enabled()) { for (ModelObject* model_object : m_model->objects) for (ModelInstance* model_instance : model_object->instances) { - sorted_instances.emplace_back(model_instance); + if (auto it = s_multiple_beds.get_inst_map().find(model_instance->id()); + it != s_multiple_beds.get_inst_map().end() + && it->second == s_multiple_beds.get_active_bed() + ) + sorted_instances.emplace_back(model_instance); } } m_labels.render(sorted_instances); @@ -7655,6 +7409,8 @@ void GLCanvas3D::_set_warning_notification_if_needed(EWarning warning) show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed(); else if (warning == EWarning::GCodeConflict) show = m_gcode_viewer.has_data() && m_gcode_viewer.is_contained_in_bed() && m_gcode_viewer.get_conflict_result().has_value(); + else if (warning == EWarning::SequentialCollision) + show = m_gcode_viewer.has_data() && m_gcode_viewer.get_sequential_collision_detected().has_value(); } } } @@ -7698,6 +7454,16 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state) error = ErrorType::SLICING_ERROR; break; } + case EWarning::SequentialCollision: { + auto conflict = m_gcode_viewer.get_sequential_collision_detected(); + if (! conflict.has_value()) + break; + // TRN: Placeholders contain names of the colliding objects. + text = format(_u8L("Extruder will crash into %1% while printing %2%."), + conflict->first, conflict->second); + error = ErrorType::SLICING_ERROR; + break; + } } auto& notification_manager = *wxGetApp().plater()->get_notification_manager(); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 34b3dd7..1722cd9 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -370,7 +370,8 @@ class GLCanvas3D SlaSupportsOutside, SomethingNotShown, ObjectClashed, - GCodeConflict + GCodeConflict, + SequentialCollision }; class RenderStats @@ -621,35 +622,6 @@ public: }; private: - - class SequentialPrintClearance - { - GLModel m_fill; - // list of unique contours - std::vector m_contours; - // list of transforms used to render the contours - std::vector> m_instances; - bool m_evaluating{ false }; - bool m_dragging{ false }; - bool m_first_displacement{ true }; - - std::vector> m_hulls_2d_cache; - - public: - void set_contours(const ContoursList& contours, bool generate_fill); - void update_instances_trafos(const std::vector& trafos); - void render(); - bool empty() const { return m_contours.empty(); } - - void start_dragging() { m_dragging = true; } - bool is_dragging() const { return m_dragging; } - void stop_dragging() { m_dragging = false; } - - friend class GLCanvas3D; - }; - - SequentialPrintClearance m_sequential_print_clearance; - struct ToolbarHighlighter { void set_timer_owner(wxEvtHandler* owner, int timerid = wxID_ANY) { m_timer.SetOwner(owner, timerid); } @@ -754,6 +726,8 @@ public: const libvgcode::Interval& get_gcode_view_visible_range() const { return m_gcode_viewer.get_gcode_view_visible_range(); } const libvgcode::PathVertex& get_gcode_vertex_at(size_t id) const { return m_gcode_viewer.get_gcode_vertex_at(id); } + std::pair>, bool> get_current_marker_model() const; + void toggle_sla_auxiliaries_visibility(bool visible, const ModelObject* mo = nullptr, int instance_idx = -1); void toggle_model_objects_visibility(bool visible, const ModelObject* mo = nullptr, int instance_idx = -1, const ModelVolume* mv = nullptr); void update_instance_printable_state_for_object(size_t obj_idx); @@ -987,39 +961,6 @@ public: #endif } - void reset_sequential_print_clearance() { - m_sequential_print_clearance.m_evaluating = false; - if (m_sequential_print_clearance.is_dragging()) - m_sequential_print_clearance.m_first_displacement = true; - else - m_sequential_print_clearance.set_contours(ContoursList(), false); - set_as_dirty(); - request_extra_frame(); - } - - void set_sequential_print_clearance_contours(const ContoursList& contours, bool generate_fill) { - m_sequential_print_clearance.set_contours(contours, generate_fill); - if (generate_fill) - m_sequential_print_clearance.m_evaluating = false; - set_as_dirty(); - request_extra_frame(); - } - - bool is_sequential_print_clearance_empty() const { - return m_sequential_print_clearance.empty(); - } - - bool is_sequential_print_clearance_evaluating() const { - return m_sequential_print_clearance.m_evaluating; - } - - void update_sequential_clearance(bool force_contours_generation); - void set_sequential_clearance_as_evaluating() { - m_sequential_print_clearance.m_evaluating = true; - set_as_dirty(); - request_extra_frame(); - } - const Print* fff_print() const; const SLAPrint* sla_print() const; @@ -1065,7 +1006,6 @@ private: void _render_gcode() { m_gcode_viewer.render(); } void _render_gcode_cog() { m_gcode_viewer.render_cog(); } void _render_selection(); - void _render_sequential_clearance(); bool check_toolbar_icon_size(float init_scale, float& new_scale_to_save, bool is_custom, int counter = 3); #if ENABLE_RENDER_SELECTION_CENTER void _render_selection_center() { m_selection.render_center(m_gizmos.is_dragging()); } diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp index a854041..247e288 100644 --- a/src/slic3r/GUI/GLModel.cpp +++ b/src/slic3r/GUI/GLModel.cpp @@ -10,7 +10,7 @@ #endif // ENABLE_GLMODEL_STATISTICS #include "libslic3r/TriangleMesh.hpp" -#include "libslic3r/Model.hpp" +#include "libslic3r/FileReader.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/BuildVolume.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" @@ -649,15 +649,14 @@ bool GLModel::init_from_file(const std::string& filename) if (!boost::algorithm::iends_with(filename, ".stl")) return false; - Model model; + TriangleMesh mesh; try { - model = Model::read_from_file(filename); + mesh = FileReader::load_mesh(filename); } catch (std::exception&) { return false; } - - init_from(model.mesh()); + init_from(mesh); m_filename = filename; diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp index b90792d..30d43fb 100644 --- a/src/slic3r/GUI/GLShadersManager.cpp +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -63,6 +63,8 @@ std::pair GLShadersManager::init() #endif // SLIC3R_OPENGL_ES // used to render toolpaths center of gravity valid &= append_shader("toolpaths_cog", { prefix + "toolpaths_cog.vs", prefix + "toolpaths_cog.fs" }); + // used to render tool marker + valid &= append_shader("tool_marker", { prefix + "tool_marker.vs", prefix + "tool_marker.fs" }); // used to render bed axes and model, selection hints, gcode sequential view marker model, preview shells, options in gcode preview valid &= append_shader("gouraud_light", { prefix + "gouraud_light.vs", prefix + "gouraud_light.fs" }); // extend "gouraud_light" by adding clipping, used in sla gizmos diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index f42aa83..7501745 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -793,9 +793,7 @@ void GUI_App::post_init() if (plater()->load_files(fns) && this->init_params->input_files.size() == 1) { // Update application titlebar when opening a project file const std::string& filename = this->init_params->input_files.front(); - if (boost::algorithm::iends_with(filename, ".amf") || - boost::algorithm::iends_with(filename, ".amf.xml") || - boost::algorithm::iends_with(filename, ".3mf")) + if (boost::algorithm::iends_with(filename, ".3mf")) this->plater()->set_project_filename(from_u8(filename)); } if (this->init_params->delete_after_load) { @@ -903,7 +901,7 @@ wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) return m_opengl_mgr.init_glcontext(canvas); #else return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0), - init_params != nullptr ? init_params->opengl_compatibiity_profile : false, init_params != nullptr ? init_params->opengl_debug : false); + init_params != nullptr ? init_params->opengl_compatibility_profile : false, init_params != nullptr ? init_params->opengl_debug : false); #endif // SLIC3R_OPENGL_ES } @@ -1537,6 +1535,15 @@ bool GUI_App::on_init_inner() this->check_updates(false); }); + Bind(EVT_CONFIG_UPDATER_FAILED_ARCHIVE, [this](const wxCommandEvent& evt) { + assert(!evt.GetString().empty()); + // TRN Notification text, %1% is list of vendors. + std::string notification_text = format(_u8L("Update check failed for the following vendors:\n\n%1%\nThis may be due to an account logout or a lost connection. Please verify your account status and internet connection. Then select \"Check for Configuration Updates\" to repeat."), evt.GetString()); + notification_manager()->push_notification(NotificationType::FailedSecretVendorUpdateSync, + NotificationManager::NotificationLevel::WarningNotificationLevel, + notification_text); + }); + Bind(wxEVT_ACTIVATE_APP, [this](const wxActivateEvent &evt) { if (plater_) { if (auto user_account = plater_->get_user_account()) @@ -1632,6 +1639,10 @@ bool GUI_App::on_init_inner() //y15 // show_printer_webview_tab(); +#ifdef _WIN32 + mainframe->update_title(); // To ensure taskbar icons is updated. +#endif + #ifdef __APPLE__ other_instance_message_handler()->bring_instance_forward(); #endif //__APPLE__ @@ -2666,19 +2677,19 @@ bool GUI_App::load_language(wxString language, bool initial) #endif if (! wxLocale::IsAvailable(language_info->Language)) { - // Loading the language dictionary failed. - wxString message = "Switching QIDISlicer to language " + language_info->CanonicalName + " failed."; + // Loading the language dictionary failed. + wxString message = "Switching QIDISlicer to language " + language_info->CanonicalName + " failed."; #if !defined(_WIN32) && !defined(__APPLE__) // likely some linux system message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; #endif if (initial) - message + "\n\nApplication will close."; + message + "\n\nApplication will close."; wxMessageBox(message, "QIDISlicer - Switching language failed", wxOK | wxICON_ERROR); if (initial) - std::exit(EXIT_FAILURE); - else - return false; + std::exit(EXIT_FAILURE); + else + return false; } // Release the old locales, create new locales. @@ -4211,7 +4222,7 @@ void GUI_App::select_filament_from_connect(const std::string& msg) } // test if currently selected is same type size_t extruder_count = preset_bundle->extruders_filaments.size(); - if (extruder_count != materials.size()) { + if (extruder_count < materials.size()) { BOOST_LOG_TRIVIAL(error) << format("Failed to select filament from Connect. Selected printer has %1% extruders while data from Connect contains %2% materials.", extruder_count, materials.size()); plater()->get_notification_manager()->close_notification_of_type(NotificationType::SelectFilamentFromConnect); // TRN: Notification text. @@ -4219,7 +4230,7 @@ void GUI_App::select_filament_from_connect(const std::string& msg) return; } std::string notification_text; - for (size_t i = 0; i < extruder_count; i++) { + for (size_t i = 0; i < materials.size(); i++) { search_and_select_filaments(materials[i], avoid_abrasive.size() > i ? avoid_abrasive[i] : false, i, notification_text); } @@ -4308,6 +4319,14 @@ void GUI_App::open_link_in_printables(const std::string& url) mainframe->show_printables_tab(url); } +bool GUI_App::is_account_logged_in() const +{ + if (!plater_ || !plater_->get_user_account()) { + return false; + } + return plater_->get_user_account()->is_logged(); +} + bool LogGui::ignorred_message(const wxString& msg) { for(const wxString& err : std::initializer_list{ wxString("cHRM chunk does not match sRGB"), diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 025eb43..3464bbd 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -467,6 +467,7 @@ public: void printables_slice_request(const std::string& download_url, const std::string& model_url); void printables_login_request(); void open_link_in_printables(const std::string& url); + bool is_account_logged_in() const; private: bool on_init_inner(); void init_app_config(); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index ca461d5..40870f8 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -1,6 +1,7 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/ModelProcessing.hpp" #include "GUI_Factories.hpp" #include "GUI_ObjectList.hpp" @@ -1160,7 +1161,7 @@ void MenuFactory::create_default_menu() []() {return (plater() != nullptr) && !plater()->model().objects.empty(); }, m_parent); append_menu_item(menu, wxID_ANY, _L("Arrange"), _L("Arrange all objects"), - [](wxCommandEvent&) { plater()->arrange(); }, "", nullptr, + [](wxCommandEvent&) { plater()->arrange(true); }, "", nullptr, []() {return plater()->can_arrange(); }, m_parent); m_default_menu.SetFirstSeparator(); diff --git a/src/slic3r/GUI/GUI_Init.hpp b/src/slic3r/GUI/GUI_Init.hpp index 05544fe..d150d27 100644 --- a/src/slic3r/GUI/GUI_Init.hpp +++ b/src/slic3r/GUI/GUI_Init.hpp @@ -35,16 +35,16 @@ struct GUI_InitParams std::vector input_files; CLISelectedProfiles selected_presets; - bool start_as_gcodeviewer; - bool start_downloader; - bool delete_after_load; + bool start_as_gcodeviewer { false }; + bool start_downloader { false }; + bool delete_after_load { false }; std::string download_url; #if !SLIC3R_OPENGL_ES - std::pair opengl_version; - bool opengl_debug; - bool opengl_compatibiity_profile; + std::pair opengl_version { 0, 0 }; + bool opengl_debug { false }; + bool opengl_compatibility_profile { false }; #endif // !SLIC3R_OPENGL_ES - bool opengl_aa; + bool opengl_aa { false }; }; int GUI_Run(GUI_InitParams ¶ms); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 81932b9..da01358 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2,6 +2,8 @@ #include "libslic3r/PresetBundle.hpp" #include "libslic3r/TextConfiguration.hpp" #include "libslic3r/BuildVolume.hpp" // IWYU pragma: keep +#include "libslic3r/ModelProcessing.hpp" +#include "libslic3r/FileReader.hpp" #include "GUI_ObjectList.hpp" #include "GUI_Factories.hpp" #include "GUI_ObjectManipulation.hpp" @@ -428,7 +430,7 @@ void ObjectList::get_selection_indexes(std::vector& obj_idxs, std::vector= 0 ? (*m_objects)[obj_idx]->get_repaired_errors_count(vol_idx) : 0; + return obj_idx >= 0 ? ModelProcessing::get_repaired_errors_count(object(obj_idx), vol_idx) : 0; } static std::string get_warning_icon_name(const TriangleMeshStats& stats) @@ -449,7 +451,7 @@ MeshErrorsInfo ObjectList::get_mesh_errors_info(const int obj_idx, const int vol } const TriangleMeshStats& stats = vol_idx == -1 ? - (*m_objects)[obj_idx]->get_object_stl_stats() : + ModelProcessing::get_object_mesh_stats((*m_objects)[obj_idx]) : (*m_objects)[obj_idx]->volumes[vol_idx]->mesh().stats(); if (!stats.repaired() && stats.manifold()) { @@ -1600,10 +1602,10 @@ void ObjectList::load_from_files(const wxArrayString& input_files, ModelObject& Model model; try { - model = Model::read_from_file(input_file); + model = FileReader::load_model(input_file); } catch (std::exception& e) { - auto msg = _L("Error!") + " " + input_file + " : " + e.what() + "."; + auto msg = _L("Error!") + " " + input_file + " : " + _(e.what()) + "."; show_error(parent, msg); exit(1); } @@ -1879,7 +1881,7 @@ bool ObjectList::del_subobject_item(wxDataViewItem& item) // If last volume item with warning was deleted, unmark object item if (type & itVolume) { - const std::string& icon_name = get_warning_icon_name(object(obj_idx)->get_object_stl_stats()); + const std::string& icon_name = get_warning_icon_name(ModelProcessing::get_object_mesh_stats(object(obj_idx))); m_objects_model->UpdateWarningIcon(parent, icon_name); } @@ -2141,7 +2143,7 @@ void ObjectList::split() wxGetApp().plater()->clear_before_change_mesh(obj_idx, _u8L("Custom supports, seams, fuzzy skin and multi-material painting were " "removed after splitting the object.")); - volume->split(nozzle_dmrs_cnt); + ModelProcessing::split(volume, nozzle_dmrs_cnt); (*m_objects)[obj_idx]->input_file.clear(); @@ -2336,7 +2338,7 @@ void ObjectList::merge(bool to_multipart_object) Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Merge all parts to the one single object")); ModelObject* model_object = (*m_objects)[obj_idx]; - model_object->merge(); + ModelProcessing::merge(model_object); m_objects_model->DeleteVolumeChildren(item); @@ -3166,7 +3168,7 @@ bool ObjectList::delete_from_model_and_list(const std::vector& it m_objects_model->SetExtruder(extruder, parent); } // If last volume item with warning was deleted, unmark object item - m_objects_model->UpdateWarningIcon(parent, get_warning_icon_name(obj->get_object_stl_stats())); + m_objects_model->UpdateWarningIcon(parent, get_warning_icon_name(ModelProcessing::get_object_mesh_stats(obj))); } wxGetApp().plater()->canvas3D()->ensure_on_bed(item->obj_idx, printer_technology() != ptSLA); } @@ -4615,8 +4617,8 @@ void ObjectList::fix_through_winsdk() if (vol_idxs.empty()) { #if !FIX_THROUGH_WINSDK_ALWAYS for (int i = int(obj_idxs.size())-1; i >= 0; --i) - if (object(obj_idxs[i])->get_repaired_errors_count() == 0) - obj_idxs.erase(obj_idxs.begin()+i); + if (ModelProcessing::get_repaired_errors_count(object(obj_idxs[i])) == 0) + obj_idxs.erase(obj_idxs.begin()+i); #endif // FIX_THROUGH_WINSDK_ALWAYS for (int obj_idx : obj_idxs) model_names.push_back(object(obj_idx)->name); @@ -4625,7 +4627,7 @@ void ObjectList::fix_through_winsdk() ModelObject* obj = object(obj_idxs.front()); #if !FIX_THROUGH_WINSDK_ALWAYS for (int i = int(vol_idxs.size()) - 1; i >= 0; --i) - if (obj->get_repaired_errors_count(vol_idxs[i]) == 0) + iif (ModelProcessing::get_repaired_errors_count(obj, vol_idxs[i]) == 0) vol_idxs.erase(vol_idxs.begin() + i); #endif // FIX_THROUGH_WINSDK_ALWAYS for (int vol_idx : vol_idxs) @@ -4681,7 +4683,7 @@ void ObjectList::fix_through_winsdk() int vol_idx{ -1 }; for (int obj_idx : obj_idxs) { #if !FIX_THROUGH_WINSDK_ALWAYS - if (object(obj_idx)->get_repaired_errors_count(vol_idx) == 0) + if (ModelProcessing::get_repaired_errors_count(object(obj_idx), vol_idx) == 0) continue; #endif // FIX_THROUGH_WINSDK_ALWAYS if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models)) @@ -4726,7 +4728,7 @@ void ObjectList::update_item_error_icon(const int obj_idx, const int vol_idx) co { auto obj = object(obj_idx); if (wxDataViewItem obj_item = m_objects_model->GetItemById(obj_idx)) { - const std::string& icon_name = get_warning_icon_name(obj->get_object_stl_stats()); + const std::string& icon_name = get_warning_icon_name(ModelProcessing::get_object_mesh_stats(obj)); m_objects_model->UpdateWarningIcon(obj_item, icon_name); } diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index f935e04..fa3ed75 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -560,6 +560,7 @@ bool ObjectManipulation::IsShown() void ObjectManipulation::UpdateAndShow(const bool show) { if (show) { + wxQueueEvent(wxGetApp().plater(), new SimpleEvent(EVT_REGENERATE_BED_THUMBNAILS)); this->set_dirty(); this->update_if_dirty(); } diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index d233aa5..375e1cb 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -295,6 +295,7 @@ void Preview::reload_print() m_loaded = false; load_print(); + m_layers_slider->seq_top_layer_only(wxGetApp().app_config->get_bool("seq_top_layer_only")); } void Preview::msw_rescale() @@ -397,6 +398,7 @@ void Preview::create_sliders() m_layers_slider->SetEmUnit(wxGetApp().em_unit()); m_layers_slider->set_imgui_wrapper(wxGetApp().imgui()); m_layers_slider->show_estimated_times(wxGetApp().app_config->get_bool("show_estimated_times_in_dbl_slider")); + m_layers_slider->seq_top_layer_only(wxGetApp().app_config->get_bool("seq_top_layer_only")); m_layers_slider->show_ruler(wxGetApp().app_config->get_bool("show_ruler_in_dbl_slider"), wxGetApp().app_config->get_bool("show_ruler_bg_in_dbl_slider")); m_layers_slider->SetDrawMode(wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA, @@ -404,8 +406,10 @@ void Preview::create_sliders() m_layers_slider->set_callback_on_thumb_move( [this]() -> void { Preview::on_layers_slider_scroll_changed(); } ); - m_layers_slider->set_callback_on_change_app_config([](const std::string& key, const std::string& val) -> void { + m_layers_slider->set_callback_on_change_app_config([this](const std::string& key, const std::string& val) -> void { wxGetApp().app_config->set(key, val); + if (key == "seq_top_layer_only") + reload_print(); }); if (wxGetApp().is_editor()) { diff --git a/src/slic3r/GUI/GalleryDialog.cpp b/src/slic3r/GUI/GalleryDialog.cpp index 741a97b..327db8f 100644 --- a/src/slic3r/GUI/GalleryDialog.cpp +++ b/src/slic3r/GUI/GalleryDialog.cpp @@ -29,6 +29,7 @@ #include "libslic3r/AppConfig.hpp" #include "libslic3r/BuildVolume.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/FileReader.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Format/OBJ.hpp" #include "libslic3r/MultipleBeds.hpp" @@ -60,7 +61,7 @@ bool GalleryDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& f // hides the system icon this->MSWUpdateDragImageOnLeave(); #endif // WIN32 - return gallery_dlg ? gallery_dlg->load_files(filenames) : false; + return gallery_dlg ? gallery_dlg->add_files_to_custom_dir(filenames) : false; } @@ -277,7 +278,7 @@ static void generate_thumbnail_from_model(const std::string& filename) Model model; try { - model = Model::read_from_file(filename); + model = FileReader::load_model(filename); } catch (std::exception&) { BOOST_LOG_TRIVIAL(error) << "Error loading model from " << filename << " in generate_thumbnail_from_model()"; @@ -339,10 +340,15 @@ void GalleryDialog::load_label_icon_list() std::vector sorted_names; for (auto& dir_entry : fs::directory_iterator(dir)) { - TriangleMesh mesh; - if ((is_gallery_file(dir_entry, ".stl") && mesh.ReadSTLFile(dir_entry.path().string().c_str())) || - (is_gallery_file(dir_entry, ".obj") && load_obj(dir_entry.path().string().c_str(), &mesh) ) ) + if (is_gallery_file(dir_entry, ".stl") || is_gallery_file(dir_entry, ".obj")) { + try { + Model model = FileReader::load_model(dir_entry.path().string()); + } + catch (std::exception&) { + continue; + } sorted_names.push_back(dir_entry.path().filename().string()); + } } // sort the filename case insensitive @@ -437,7 +443,7 @@ void GalleryDialog::add_custom_shapes(wxEvent& event) if (input_files.IsEmpty()) return; - load_files(input_files); + add_files_to_custom_dir(input_files); } void GalleryDialog::del_custom_shapes() @@ -554,7 +560,7 @@ void GalleryDialog::update() load_label_icon_list(); } -bool GalleryDialog::load_files(const wxArrayString& input_files) +bool GalleryDialog::add_files_to_custom_dir(const wxArrayString& input_files) { auto dest_dir = get_dir(false); @@ -573,16 +579,14 @@ bool GalleryDialog::load_files(const wxArrayString& input_files) // Iterate through the input files for (size_t i = 0; i < input_files.size(); ++i) { std::string input_file = into_u8(input_files.Item(i)); - - TriangleMesh mesh; - if (is_gallery_file(input_file, ".stl") && !mesh.ReadSTLFile(input_file.c_str())) { - show_warning(format_wxstr(_L("Loading of the \"%1%\""), input_file), "STL"); - continue; - } - - if (is_gallery_file(input_file, ".obj") && !load_obj(input_file.c_str(), &mesh)) { - show_warning(format_wxstr(_L("Loading of the \"%1%\""), input_file), "OBJ"); - continue; + if (is_gallery_file(input_file, ".stl") || is_gallery_file(input_file, ".obj")) { + try { + Model model = FileReader::load_model(input_file); + } + catch (std::exception&) { + show_warning(format_wxstr(_L("Loading of the \"%1%\""), input_file), is_gallery_file(input_file, ".obj") ? "OBJ" : "STL"); + continue; + } } try { diff --git a/src/slic3r/GUI/GalleryDialog.hpp b/src/slic3r/GUI/GalleryDialog.hpp index 7d56d1a..442e95a 100644 --- a/src/slic3r/GUI/GalleryDialog.hpp +++ b/src/slic3r/GUI/GalleryDialog.hpp @@ -54,7 +54,7 @@ public: int show(bool show_from_menu = false); void get_input_files(wxArrayString& input_files); - bool load_files(const wxArrayString& input_files); + bool add_files_to_custom_dir(const wxArrayString& input_files); protected: void on_dpi_changed(const wxRect& suggested_rect) override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 6f55d4d..aa4e040 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -18,6 +18,7 @@ #include "slic3r/Utils/FixModelByWin10.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/TriangleMeshSlicer.hpp" +#include "libslic3r/ModelProcessing.hpp" #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS @@ -1894,7 +1895,7 @@ GLGizmoCut3D::PartSelection::PartSelection(const ModelObject* mo, const Transfor // split to parts for (int id = int(volumes.size())-1; id >= 0; id--) if (volumes[id]->is_splittable() && volumes[id]->is_model_part()) // we have to split just solid volumes - volumes[id]->split(1); + ModelProcessing::split(volumes[id], 1); m_parts.clear(); for (const ModelVolume* volume : volumes) { @@ -3289,8 +3290,8 @@ static void check_objects_after_cut(const ModelObjectPtrs& objects) if (connectors_count != connectors_names.size()) err_objects_names.push_back(object->name); - // check manifol/repairs - auto stats = object->get_object_stl_stats(); + // check manifold/repairs + auto stats = ModelProcessing::get_object_mesh_stats(object); if (!stats.manifold() || stats.repaired()) err_objects_idxs.push_back(obj_idx); obj_idx++; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 9b30455..0692f67 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Model.hpp" //#include "slic3r/GUI/3DScene.hpp" +#include "libslic3r/MultipleBeds.hpp" #include "libslic3r/SupportSpotsGenerator.hpp" #include "libslic3r/TriangleSelectorWrapper.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" @@ -531,6 +532,14 @@ void GLGizmoFdmSupports::auto_generate() return; } + const auto first_instance_bed{s_multiple_beds.get_inst_map().find(mo->instances.front()->id())}; + if ( + first_instance_bed != s_multiple_beds.get_inst_map().end() + && s_multiple_beds.get_active_bed() != first_instance_bed->second + ) { + s_multiple_beds.set_active_bed(first_instance_bed->second); + } + bool not_painted = std::all_of(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume* vol){ return vol->type() != ModelVolumeType::MODEL_PART || vol->supported_facets.empty(); }); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index 36621d6..d1fcbb5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -12,21 +12,97 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_ObjectSettings.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/format.hpp" #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/MsgDialog.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/SLAPrint.hpp" +#include "libslic3r/SLA/SupportIslands/SampleConfigFactory.hpp" +#include "imgui/imgui_stdlib.h" // string input for ImGui static const double CONE_RADIUS = 0.25; static const double CONE_HEIGHT = 0.75; -namespace Slic3r { -namespace GUI { +using namespace Slic3r; +using namespace Slic3r::GUI; + +namespace { + +enum class IconType : unsigned { + show_support_points_selected, + show_support_points_unselected, + show_support_points_hovered, + show_support_structure_selected, + show_support_structure_unselected, + show_support_structure_hovered, + // automatic calc of icon's count + _count +}; + +IconManager::Icons init_icons(IconManager &mng, ImVec2 size = ImVec2{50, 50}) { + mng.release(); + + // icon order has to match the enum IconType + IconManager::InitTypes init_types { + {"support_structure_invisible.svg", size, IconManager::RasterType::color}, // show_support_points_selected + {"support_structure_invisible.svg", size, IconManager::RasterType::gray_only_data}, // show_support_points_unselected + {"support_structure_invisible.svg", size, IconManager::RasterType::color}, // show_support_points_hovered + + {"support_structure.svg", size, IconManager::RasterType::color}, // show_support_structure_selected + {"support_structure.svg", size, IconManager::RasterType::gray_only_data}, // show_support_structure_unselected + {"support_structure.svg", size, IconManager::RasterType::color}, // show_support_structure_hovered + }; + + assert(init_types.size() == static_cast(IconType::_count)); + std::string path = resources_dir() + "/icons/"; + for (IconManager::InitType &init_type : init_types) + init_type.filepath = path + init_type.filepath; + + return mng.init(init_types); +} +const IconManager::Icon &get_icon(const IconManager::Icons &icons, IconType type) { + return *icons[static_cast(type)]; } + +/// +/// Draw icon buttons to swap between show structure and only supports points +/// +/// In|Out view mode +/// all loaded icons +/// True when change is made +bool draw_view_mode(bool &show_support_structure, const IconManager::Icons &icons) { + ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 8.); + ScopeGuard sg([] { ImGui::PopStyleVar(); }); + if (show_support_structure) { + draw(get_icon(icons, IconType::show_support_structure_selected)); + if(ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Visible support structure").c_str()); + ImGui::SameLine(); + if (clickable(get_icon(icons, IconType::show_support_points_unselected), + get_icon(icons, IconType::show_support_points_hovered))) { + show_support_structure = false; + return true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Click to show support points without support structure").c_str()); + } else { // !show_support_structure + if (clickable(get_icon(icons, IconType::show_support_structure_unselected), + get_icon(icons, IconType::show_support_structure_hovered))) { + show_support_structure = true; + return true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Click to show support structure with pad").c_str()); + ImGui::SameLine(); + draw(get_icon(icons, IconType::show_support_points_selected)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Visible support points without support structure").c_str()); + } + return false; +} +} // namespace + GLGizmoSlaSupports::GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) -: GLGizmoSlaBase(parent, icon_filename, sprite_id, slaposDrillHoles) -{ - show_sla_supports(true); +: GLGizmoSlaBase(parent, icon_filename, sprite_id, slaposDrillHoles /*slaposSupportPoints*/) { + show_sla_supports(false); } bool GLGizmoSlaSupports::on_init() @@ -39,8 +115,7 @@ bool GLGizmoSlaSupports::on_init() m_desc["remove_all"] = _u8L("Remove all points"); m_desc["apply_changes"] = _u8L("Apply changes"); m_desc["discard_changes"] = _u8L("Discard changes"); - m_desc["minimal_distance"] = _u8L("Minimal points distance") + ": "; - m_desc["points_density"] = _u8L("Support points density") + ": "; + m_desc["points_density"] = _u8L("Support points density"); m_desc["auto_generate"] = _u8L("Auto-generate points"); m_desc["manual_editing"] = _u8L("Manual editing"); m_desc["clipping_of_view"] = _u8L("Clipping of view")+ ": "; @@ -136,8 +211,6 @@ void GLGizmoSlaSupports::on_render() glsafe(::glEnable(GL_BLEND)); glsafe(::glEnable(GL_DEPTH_TEST)); - show_sla_supports(!m_editing_mode); - render_volumes(); render_points(selection); @@ -190,10 +263,15 @@ void GLGizmoSlaSupports::render_points(const Selection& selection) const Transform3d& view_matrix = camera.get_view_matrix(); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + const ColorRGBA selected_color = ColorRGBA::REDISH(); + const ColorRGBA hovered_color = ColorRGBA::CYAN(); + const ColorRGBA island_color = ColorRGBA::BLUEISH(); + const ColorRGBA inactive_color = ColorRGBA::LIGHT_GRAY(); + const ColorRGBA manual_color = ColorRGBA::ORANGE(); + ColorRGBA render_color; for (size_t i = 0; i < cache_size; ++i) { const sla::SupportPoint& support_point = m_editing_mode ? m_editing_cache[i].support_point : m_normal_cache[i]; - const bool point_selected = m_editing_mode ? m_editing_cache[i].selected : false; const bool clipped = is_mesh_point_clipped(support_point.pos.cast()); if (i < m_point_raycasters.size()) { @@ -203,22 +281,16 @@ void GLGizmoSlaSupports::render_points(const Selection& selection) if (clipped) continue; + render_color = + support_point.type == sla::SupportPointType::manual_add ? manual_color : + support_point.type == sla::SupportPointType::island ? island_color : + inactive_color; // First decide about the color of the point. - if (size_t(m_hover_id) == i && m_editing_mode) // ignore hover state unless editing mode is active - render_color = { 0.f, 1.f, 1.f, 1.f }; - else { // neigher hover nor picking - bool supports_new_island = m_lock_unique_islands && support_point.is_new_island; - if (m_editing_mode) { - if (point_selected) - render_color = { 1.f, 0.3f, 0.3f, 1.f}; - else - if (supports_new_island) - render_color = { 0.3f, 0.3f, 1.f, 1.f }; - else - render_color = { 0.7f, 0.7f, 0.7f, 1.f }; - } - else - render_color = { 0.5f, 0.5f, 0.5f, 1.f }; + if (m_editing_mode) { + if (size_t(m_hover_id) == i) // ignore hover state unless editing mode is active + render_color = hovered_color; + else if (m_editing_cache[i].selected) + render_color = selected_color; } m_cone.model.set_color(render_color); @@ -319,7 +391,7 @@ bool GLGizmoSlaSupports::gizmo_event(SLAGizmoEventType action, const Vec2d& mous std::pair pos_and_normal; if (unproject_on_mesh(mouse_position, pos_and_normal)) { // we got an intersection Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Add support point")); - m_editing_cache.emplace_back(sla::SupportPoint(pos_and_normal.first, m_new_point_head_diameter/2.f, false), false, pos_and_normal.second); + m_editing_cache.emplace_back(sla::SupportPoint{pos_and_normal.first, m_new_point_head_diameter/2.f}, false, pos_and_normal.second); m_parent.set_as_dirty(); m_wait_for_up_event = true; unregister_point_raycasters_for_picking(); @@ -474,7 +546,7 @@ void GLGizmoSlaSupports::delete_selected_points(bool force) Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Delete support point")); for (unsigned int idx=0; idx GLGizmoSlaSupports::get_config_options(const st return out; } - - -/* -void GLGizmoSlaSupports::find_intersecting_facets(const igl::AABB* aabb, const Vec3f& normal, double offset, std::vector& idxs) const -{ - if (aabb->is_leaf()) { // this is a facet - // corner.dot(normal) - offset - idxs.push_back(aabb->m_primitive); - } - else { // not a leaf - using CornerType = Eigen::AlignedBox::CornerType; - bool sign = std::signbit(offset - normal.dot(aabb->m_box.corner(CornerType(0)))); - for (unsigned int i=1; i<8; ++i) - if (std::signbit(offset - normal.dot(aabb->m_box.corner(CornerType(i)))) != sign) { - find_intersecting_facets(aabb->m_left, normal, offset, idxs); - find_intersecting_facets(aabb->m_right, normal, offset, idxs); - } - } -} - - - -void GLGizmoSlaSupports::make_line_segments() const -{ - TriangleMeshSlicer tms(&m_c->m_model_object->volumes.front()->mesh); - Vec3f normal(0.f, 1.f, 1.f); - double d = 0.; - - std::vector lines; - find_intersections(&m_AABB, normal, d, lines); - ExPolygons expolys; - tms.make_expolygons_simple(lines, &expolys); - - SVG svg("slice_loops.svg", get_extents(expolys)); - svg.draw(expolys); - //for (const IntersectionLine &l : lines[i]) - // svg.draw(l, "red", 0); - //svg.draw_outline(expolygons, "black", "blue", 0); - svg.Close(); -} -*/ - - void GLGizmoSlaSupports::on_render_input_window(float x, float y, float bottom_limit) { + // Keep resolution of icons for + static float rendered_line_height; + if (float line_height = ImGui::GetTextLineHeightWithSpacing(); + m_icons.empty() || + rendered_line_height != line_height) { // change of view resolution + rendered_line_height = line_height; + + // need regeneration when change resolution(move between monitors) + float width = std::round(line_height / 8 + 1) * 8; + ImVec2 icon_size{width, width}; + m_icons = init_icons(m_icon_manager, icon_size); + } + static float last_y = 0.0f; static float last_h = 0.0f; @@ -590,7 +632,7 @@ RENDER_AGAIN: // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: - const float settings_sliders_left = std::max(ImGuiPureWrap::calc_text_size(m_desc.at("minimal_distance")).x, ImGuiPureWrap::calc_text_size(m_desc.at("points_density")).x) + m_imgui->scaled(1.f); + const float settings_sliders_left = ImGuiPureWrap::calc_text_size(m_desc.at("points_density")).x + m_imgui->scaled(1.f); const float clipping_slider_left = std::max(ImGuiPureWrap::calc_text_size(m_desc.at("clipping_of_view")).x, ImGuiPureWrap::calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f); const float diameter_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("head_diameter")).x + m_imgui->scaled(1.f); const float minimal_slider_width = m_imgui->scaled(4.f); @@ -672,51 +714,91 @@ RENDER_AGAIN: } else { // not in editing mode: m_imgui->disabled_begin(!is_input_enabled()); - - ImGui::AlignTextToFramePadding(); - ImGuiPureWrap::text(m_desc.at("minimal_distance")); - ImGui::SameLine(settings_sliders_left); - ImGui::PushItemWidth(window_width - settings_sliders_left); - - std::vector opts = get_config_options({"support_points_density_relative", "support_points_minimal_distance"}); - float density = static_cast(opts[0])->value; - float minimal_point_distance = static_cast(opts[1])->value; - - m_imgui->slider_float("##minimal_point_distance", &minimal_point_distance, 0.f, 20.f, "%.f mm"); - bool slider_clicked = m_imgui->get_last_slider_status().clicked; // someone clicked the slider - bool slider_edited = m_imgui->get_last_slider_status().edited; // someone is dragging the slider - bool slider_released = m_imgui->get_last_slider_status().deactivated_after_edit; // someone has just released the slider - - ImGui::AlignTextToFramePadding(); ImGuiPureWrap::text(m_desc.at("points_density")); - ImGui::SameLine(settings_sliders_left); + ImGui::SameLine(); - m_imgui->slider_float("##points_density", &density, 0.f, 200.f, "%.f %%"); - slider_clicked |= m_imgui->get_last_slider_status().clicked; - slider_edited |= m_imgui->get_last_slider_status().edited; - slider_released |= m_imgui->get_last_slider_status().deactivated_after_edit; + if (draw_view_mode(m_show_support_structure, m_icons)){ + show_sla_supports(m_show_support_structure); + if (m_show_support_structure) { + if (m_normal_cache.empty()) { + // first click also have to generate point + auto_generate(); + } else { + reslice_until_step(slaposPad); + } + } + } - if (slider_clicked) { // stash the values of the settings so we know what to revert to after undo - m_minimal_point_distance_stash = minimal_point_distance; - m_density_stash = density; + const char *support_points_density = "support_points_density_relative"; + float density = static_cast(get_config_options({support_points_density})[0])->value; + float old_density = density; + wxString tooltip = _L("Change amount of generated support points."); + if (m_imgui->slider_float("##density", &density, 50.f, 200.f, "%.f %%", 1.f, false, tooltip)){ + if (density < 10.f) // not neccessary, but lower value seems pointless. Zero cause issues inside algorithms. + density = 10.f; + mo->config.set(support_points_density, (int) density); } - if (slider_edited) { - mo->config.set("support_points_minimal_distance", minimal_point_distance); - mo->config.set("support_points_density_relative", (int)density); - } - if (slider_released) { - mo->config.set("support_points_minimal_distance", m_minimal_point_distance_stash); - mo->config.set("support_points_density_relative", (int)m_density_stash); + + const ImGuiWrapper::LastSliderStatus &density_status = m_imgui->get_last_slider_status(); + static std::optional density_stash; // Value for undo/redo stack is written on stop dragging + if (!density_stash.has_value() && !is_approx(density, old_density)) // stash the values of the settings so we know what to revert to after undo + density_stash = (int)old_density; + if (density_status.deactivated_after_edit && density_stash.has_value()) { // slider released + // set configuration to value before slide + // to store this value on undo redo snapshot stack + mo->config.set(support_points_density, *density_stash); + density_stash.reset(); Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Support parameter change")); - mo->config.set("support_points_minimal_distance", minimal_point_distance); - mo->config.set("support_points_density_relative", (int)density); + mo->config.set(support_points_density, (int) density); wxGetApp().obj_list()->update_and_show_object_settings_item(); - } - - bool generate = ImGuiPureWrap::button(m_desc.at("auto_generate")); - - if (generate) auto_generate(); + } + + const sla::SupportPoints &supports = m_normal_cache; + int count_user_edited = 0; + int count_island = 0; + for (const sla::SupportPoint &support : supports) + switch (support.type) { + case sla::SupportPointType::manual_add: ++count_user_edited; break; + case sla::SupportPointType::island: ++count_island; break; + //case sla::SupportPointType::slope: + default: assert(support.type == sla::SupportPointType::slope); } + + std::string stats; + if (supports.empty()) { + stats = "No support points generated yet."; + } else if (count_user_edited == 0) { + stats = GUI::format("%d support points generated (%d on islands)", + (int) supports.size(), count_island); + } else { + stats = GUI::format("%d(%d manual) support points (%d on islands)", + (int) supports.size(), count_user_edited, count_island); + } + ImVec4 light_gray{0.4f, 0.4f, 0.4f, 1.0f}; + ImGui::TextColored(light_gray, "%s", stats.c_str()); + + #ifdef USE_ISLAND_GUI_FOR_SETTINGS + ImGui::Separator(); + ImGui::Text("Between delimiters is temporary GUI"); + sla::SampleConfig &sample_config = sla::SampleConfigFactory::get_sample_config(); + if (float overhang_sample_distance = sample_config.prepare_config.discretize_overhang_step; + m_imgui->slider_float("overhang discretization", &overhang_sample_distance, 2e-5f, 10.f, "%.2f mm")){ + sample_config.prepare_config.discretize_overhang_step = overhang_sample_distance; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Smaller will slow down. Step for discretization overhang outline for test of support need"); + + draw_island_config(); + ImGui::Text("Distribution depends on './resources/data/sla_support.svg'\ninstruction for edit are in file"); + ImGui::Separator(); +#endif // USE_ISLAND_GUI_FOR_SETTINGS + + if (ImGuiPureWrap::button(m_desc.at("auto_generate"))) + auto_generate(); + ImGui::SameLine(); + + m_imgui->disabled_begin(!is_input_enabled() || m_normal_cache.empty()); + remove_all = ImGuiPureWrap::button(m_desc.at("remove_all")); + m_imgui->disabled_end(); ImGui::Separator(); if (ImGuiPureWrap::button(m_desc.at("manual_editing"))) @@ -724,10 +806,6 @@ RENDER_AGAIN: m_imgui->disabled_end(); - m_imgui->disabled_begin(!is_input_enabled() || m_normal_cache.empty()); - remove_all = ImGuiPureWrap::button(m_desc.at("remove_all")); - m_imgui->disabled_end(); - // ImGuiPureWrap::text(""); // ImGuiPureWrap::text(m_c->m_model_object->sla_points_status == sla::PointsStatus::NoPoints ? _(L("No points (will be autogenerated)")) : // (m_c->m_model_object->sla_points_status == sla::PointsStatus::AutoGenerated ? _(L("Autogenerated points (no modifications)")) : @@ -793,6 +871,139 @@ RENDER_AGAIN: m_parent.set_as_dirty(); } +#ifdef USE_ISLAND_GUI_FOR_SETTINGS +void GLGizmoSlaSupports::draw_island_config() { + if (!ImGui::TreeNode("Support islands:")) + return; // no need to draw configuration for islands + sla::SampleConfig &sample_config = sla::SampleConfigFactory::get_sample_config(); + + ImGui::SameLine(); + ImGui::Text("head radius %.2f mm", unscale(sample_config.head_radius)); + + bool exist_change = false; + + if (float max_for_one = unscale(sample_config.max_length_for_one_support_point); // [in mm] + ImGui::InputFloat("One support", &max_for_one, .1f, 1.f, "%.2f mm")) { + sample_config.max_length_for_one_support_point = scale_(max_for_one); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal island length (longest voronoi path)\n" + "for support island by exactly one point.\n" + "Point will be on the longest path center"); + + if (float max_for_two = unscale(sample_config.max_length_for_two_support_points); // [in mm] + ImGui::InputFloat("Two supports", &max_for_two, .1f, 1.f, "%.2f mm")) { + sample_config.max_length_for_two_support_points = scale_(max_for_two); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal island length (longest voronoi path)\n" + "for support by 2 points on path sides\n" + "To stretch the island."); + if (float thin_max_width = unscale(sample_config.thin_max_width); // [in mm] + ImGui::InputFloat("Thin max width", &thin_max_width, .1f, 1.f, "%.2f mm")) { + sample_config.thin_max_width = scale_(thin_max_width); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal width of line island supported in the middle of line\n" + "Must be greater than thick min width(to make hysteresis)"); + if (float thick_min_width = unscale(sample_config.thick_min_width); // [in mm] + ImGui::InputFloat("Thick min width", &thick_min_width, .1f, 1.f, "%.2f mm")) { + sample_config.thick_min_width = scale_(thick_min_width); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Minimal width to be supported by outline\n" + "Must be smaller than thin max width(to make hysteresis)"); + if (float max_distance = unscale(sample_config.thin_max_distance); // [in mm] + ImGui::InputFloat("Thin max distance", &max_distance, .1f, 1.f, "%.2f mm")) { + sample_config.thin_max_distance = scale_(max_distance); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal distance of supports on thin island's part"); + if (float max_distance = unscale(sample_config.thick_inner_max_distance); // [in mm] + ImGui::InputFloat("Thick inner max distance", &max_distance, .1f, 1.f, "%.2f mm")) { + sample_config.thick_inner_max_distance = scale_(max_distance); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal distance of supports inside thick island's part"); + if (float max_distance = unscale(sample_config.thick_outline_max_distance); // [in mm] + ImGui::InputFloat("Thick outline max distance", &max_distance, .1f, 1.f, "%.2f mm")) { + sample_config.thick_outline_max_distance = scale_(max_distance); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal distance of supports on thick island's part outline"); + + if (float minimal_distance_from_outline = unscale(sample_config.minimal_distance_from_outline); // [in mm] + ImGui::InputFloat("From outline", &minimal_distance_from_outline, .1f, 1.f, "%.2f mm")) { + sample_config.minimal_distance_from_outline = scale_(minimal_distance_from_outline); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("When it is possible, there will be this minimal distance from outline.\n" + "ZERO mean head center will lay on island outline\n" + "IMHO value should be bigger than head radius"); + ImGui::SameLine(); + if (float maximal_distance_from_outline = unscale(sample_config.maximal_distance_from_outline); // [in mm] + ImGui::InputFloat("Max", &maximal_distance_from_outline, .1f, 1.f, "%.2f mm")) { + sample_config.maximal_distance_from_outline = scale_(maximal_distance_from_outline); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Measured as sum of VD edge length from outline\n" + "Used only when there is no space for outline offset on first/last point\n" + "Must be bigger than value 'From outline'"); + + if (float simplification_tolerance = unscale(sample_config.simplification_tolerance); // [in mm] + ImGui::InputFloat("Simplify", &simplification_tolerance, .1f, 1.f, "%.2f mm")) { + sample_config.simplification_tolerance = scale_(simplification_tolerance); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("There is no need to calculate with precisse island Voronoi\n" + "NOTE: Slice of Cylinder bottom has tip of trinagles on contour\n" + "(neighbor coordinate -> create issue in boost::voronoi)\n" + "Bigger value will speed up"); + ImGui::Text("Aligning termination criteria:"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("After initial support placement on island, supports are aligned\n" + "to more uniformly support area of irregular island shape"); + if (int count = static_cast(sample_config.count_iteration); + ImGui::SliderInt("max iteration", &count, 0, 100, "%d loops" )){ + sample_config.count_iteration = count; + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Align termination condition, max count of aligning calls"); + if (float minimal_move = unscale(sample_config.minimal_move); // [in mm] + ImGui::InputFloat("minimal move", &minimal_move, .1f, 1.f, "%.2f mm")) { + sample_config.minimal_move = scale_(minimal_move); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Align termination condition, when support points after align did not change their position more,\n" + "than this distance it is deduce that supports are aligned enough.\n" + "Bigger value mean speed up of aligning"); + + if (exist_change){ + sla::SampleConfigFactory::verify(sample_config); + } + + +#ifdef OPTION_TO_STORE_ISLAND + bool store_islands = !sample_config.path.empty(); + if (ImGui::Checkbox("StoreIslands", &store_islands)) { + if (store_islands == true) + sample_config.path = "C:/data/temp/island<>.svg"; + else + sample_config.path.clear(); + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Store islands into files\n<> is replaced by island order number"); + if (store_islands) { + ImGui::SameLine(); + std::string path; + ImGui::InputText("path", &sample_config.path); + } +#endif // OPTION_TO_STORE_ISLAND + + // end of tree node + ImGui::TreePop(); +} +#endif // USE_ISLAND_GUI_FOR_SETTINGS + bool GLGizmoSlaSupports::on_is_activable() const { const Selection& selection = m_parent.get_selection(); @@ -909,7 +1120,7 @@ void GLGizmoSlaSupports::on_dragging(const UpdateData &data) { assert(m_hover_id != -1); if (!m_editing_mode) return; - if (m_editing_cache[m_hover_id].support_point.is_new_island && m_lock_unique_islands) + if (m_editing_cache[m_hover_id].support_point.is_island() && m_lock_unique_islands) return; std::pair pos_and_normal; @@ -917,7 +1128,7 @@ void GLGizmoSlaSupports::on_dragging(const UpdateData &data) return; m_editing_cache[m_hover_id].support_point.pos = pos_and_normal.first; - m_editing_cache[m_hover_id].support_point.is_new_island = false; + m_editing_cache[m_hover_id].support_point.type = sla::SupportPointType::manual_add; m_editing_cache[m_hover_id].normal = pos_and_normal.second; } @@ -1016,7 +1227,7 @@ void GLGizmoSlaSupports::editing_mode_apply_changes() mo->sla_support_points.clear(); mo->sla_support_points = m_normal_cache; - reslice_until_step(slaposPad); + reslice_until_step(m_show_support_structure ? slaposPad : slaposSupportPoints); } } @@ -1116,10 +1327,10 @@ void GLGizmoSlaSupports::get_data_from_backend() for (const SLAPrintObject* po : m_parent.sla_print()->objects()) { if (po->model_object()->id() == mo->id()) { m_normal_cache.clear(); - const std::vector& points = po->get_support_points(); - auto mat = po->trafo().inverse().cast(); - for (unsigned int i=0; itrafo().inverse().cast(); // TODO: WTF trafo????? !!!!!! + for (const sla::SupportPoint &p : po->get_support_points()) + m_normal_cache.emplace_back(sla::SupportPoint{mat * p.pos, p.head_front_radius, p.type}); mo->sla_points_status = sla::PointsStatus::AutoGenerated; break; @@ -1133,27 +1344,18 @@ void GLGizmoSlaSupports::get_data_from_backend() void GLGizmoSlaSupports::auto_generate() { - //wxMessageDialog dlg(GUI::wxGetApp().plater(), - MessageDialog dlg(GUI::wxGetApp().plater(), - _L("Autogeneration will erase all manually edited points.") + "\n\n" + - _L("Are you sure you want to do it?") + "\n", - _L("Warning"), wxICON_WARNING | wxYES | wxNO); - + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); + wxGetApp().CallAfter([this]() { reslice_until_step( + m_show_support_structure ? slaposPad : slaposSupportPoints); }); ModelObject* mo = m_c->selection_info()->model_object(); - - if (mo->sla_points_status != sla::PointsStatus::UserModified || m_normal_cache.empty() || dlg.ShowModal() == wxID_YES) { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); - wxGetApp().CallAfter([this]() { reslice_until_step(slaposPad); }); - mo->sla_points_status = sla::PointsStatus::Generating; - } + mo->sla_points_status = sla::PointsStatus::Generating; } - - void GLGizmoSlaSupports::switch_to_editing_mode() { wxGetApp().plater()->enter_gizmos_stack(); m_editing_mode = true; + show_sla_supports(false); m_editing_cache.clear(); for (const sla::SupportPoint& sp : m_normal_cache) m_editing_cache.emplace_back(sp); @@ -1167,6 +1369,7 @@ void GLGizmoSlaSupports::disable_editing_mode() { if (m_editing_mode) { m_editing_mode = false; + show_sla_supports(m_show_support_structure); wxGetApp().plater()->leave_gizmos_stack(); m_parent.set_as_dirty(); unregister_point_raycasters_for_picking(); @@ -1303,11 +1506,20 @@ SlaGizmoHelpDialog::SlaGizmoHelpDialog() gridsizer->Add(desc, -1, wxALIGN_CENTRE_VERTICAL); } + std::vector> point_types; + point_types.push_back(std::make_pair("sphere_lightgray",_L("Generated support point"))); + point_types.push_back(std::make_pair("sphere_redish", _L("Selected support point"))); + point_types.push_back(std::make_pair("sphere_orange", _L("Edited support point"))); + point_types.push_back(std::make_pair("sphere_blueish", _L("Island support point"))); + point_types.push_back(std::make_pair("sphere_cyan", _L("Hovered support point"))); + for (const auto &[icon_name, description] : point_types) { + auto desc = new wxStaticText(this, wxID_ANY, description); + desc->SetFont(font); + gridsizer->Add(new wxStaticBitmap(this, wxID_ANY, ScalableBitmap(this, icon_name).bmp()), + -1, wxALIGN_CENTRE_VERTICAL); + gridsizer->Add(desc, -1, wxALIGN_CENTRE_VERTICAL); + } + SetSizer(hsizer); hsizer->SetSizeHints(this); } - - - -} // namespace GUI -} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp index 7bd1ab4..dc141c6 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp @@ -4,6 +4,7 @@ #include "GLGizmoSlaBase.hpp" #include "slic3r/GUI/GLSelectionRectangle.hpp" #include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/IconManager.hpp" #include "libslic3r/SLA/SupportPoint.hpp" #include "libslic3r/ObjectID.hpp" @@ -87,17 +88,21 @@ private: void unregister_point_raycasters_for_picking(); void update_point_raycasters_for_picking_transform(); + void draw_island_config(); + + bool m_show_support_structure = false; bool m_lock_unique_islands = false; bool m_editing_mode = false; // Is editing mode active? float m_new_point_head_diameter; // Size of a new point. CacheEntry m_point_before_drag; // undo/redo - so we know what state was edited float m_old_point_head_diameter = 0.; // the same - float m_minimal_point_distance_stash = 0.f; // and again - float m_density_stash = 0.f; // and again mutable std::vector m_editing_cache; // a support point and whether it is currently selected std::vector m_normal_cache; // to restore after discarding changes or undo/redo ObjectID m_old_mo_id; + IconManager m_icon_manager; + IconManager::Icons m_icons; + PickingModel m_sphere; PickingModel m_cone; std::vector, std::shared_ptr>> m_point_raycasters; diff --git a/src/slic3r/GUI/IconManager.cpp b/src/slic3r/GUI/IconManager.cpp index 8343530..1adf887 100644 --- a/src/slic3r/GUI/IconManager.cpp +++ b/src/slic3r/GUI/IconManager.cpp @@ -288,7 +288,7 @@ std::vector IconManager::init(const std::vector } void IconManager::release() { - BOOST_LOG_TRIVIAL(error) << "Not implemented yet"; + BOOST_LOG_TRIVIAL(warning) << "Not implemented yet"; } void priv::clear(IconManager::Icons &icons) { diff --git a/src/slic3r/GUI/InstanceCheck.cpp b/src/slic3r/GUI/InstanceCheck.cpp index ea9d5fa..0895896 100644 --- a/src/slic3r/GUI/InstanceCheck.cpp +++ b/src/slic3r/GUI/InstanceCheck.cpp @@ -1,6 +1,7 @@ #include "GUI_App.hpp" #include "InstanceCheck.hpp" #include "Plater.hpp" +#include "format.hpp" #ifdef _WIN32 #include "MainFrame.hpp" @@ -12,12 +13,15 @@ #include "boost/nowide/convert.hpp" #include #include +#include +#include #include #include #include #include #include #include +#include #ifdef _WIN32 #include @@ -62,9 +66,7 @@ namespace instance_check_internal static CommandLineAnalysis process_command_line(int argc, char** argv) { CommandLineAnalysis ret; - //if (argc < 2) - // return ret; - std::vector arguments { argv[0] }; + std::vector arguments { argv[0] }; bool send_if_url = false; bool has_url = false; for (int i = 1; i < argc; ++i) { @@ -86,14 +88,22 @@ namespace instance_check_internal if (send_if_url && has_url) { ret.should_send = true; } - ret.cl_string = escape_strings_cstyle(arguments); + // We do now want escape_strings_cstyle that quotes strings + // It would not be possible to use inside json + for (const std::string& arg : arguments) { + ret.cl_string += escape_string_cstyle(arg); + ret.cl_string += ";"; + } BOOST_LOG_TRIVIAL(info) << "single instance: " << (ret.should_send.has_value() ? (*ret.should_send ? "true" : "false") : "undefined") << ". other params: " << ret.cl_string; return ret; } - + std::string compose_message_json(const std::string& type, const std::string& data) + { + return GUI::format("{ \"type\" : \"%1%\" , \"data\" : \"%2%\"}", type, data); + } #ifdef _WIN32 @@ -164,6 +174,67 @@ namespace instance_check_internal return false; } + static BOOL CALLBACK enum_windows_process_multicast(_In_ HWND hwnd, _In_ LPARAM lParam) + { + if (hwnd == GUI::wxGetApp().mainframe->GetHandle()) { + return true; + } + + TCHAR wndText[1000]; + TCHAR className[1000]; + int err; + err = GetClassName(hwnd, className, 1000); + if (err == 0) + return true; + err = GetWindowText(hwnd, wndText, 1000); + if (err == 0) + return true; + std::wstring classNameString(className); + std::wstring wndTextString(wndText); + if (wndTextString.find(L"QIDISlicer") != std::wstring::npos && classNameString == L"wxWindowNR") { + //check if other instances has same instance hash + //if not it is not same version(binary) as this version + HANDLE handle = GetProp(hwnd, L"Instance_Hash_Minor"); + uint64_t other_instance_hash = PtrToUint(handle); + uint64_t other_instance_hash_major; + uint64_t my_instance_hash = GUI::wxGetApp().get_instance_hash_int(); + handle = GetProp(hwnd, L"Instance_Hash_Major"); + other_instance_hash_major = PtrToUint(handle); + other_instance_hash_major = other_instance_hash_major << 32; + other_instance_hash += other_instance_hash_major; + handle = GetProp(hwnd, L"Instance_Is_Maximized"); + const bool maximized = PtrToUint(handle) == 1; + + if (my_instance_hash == other_instance_hash) + { + BOOST_LOG_TRIVIAL(debug) << "win multicast enum - found instance " << hwnd; + std::wstring multicast_message = *reinterpret_cast(lParam); + std::unique_ptr message = std::make_unique(const_cast(multicast_message.c_str())); + + //Create a COPYDATASTRUCT to send the information + //cbData represents the size of the information we want to send. + //lpData represents the information we want to send. + //dwData is an ID defined by us(this is a type of ID different than WM_COPYDATA). + COPYDATASTRUCT data_to_send = { 0 }; + data_to_send.dwData = 1; + data_to_send.cbData = sizeof(TCHAR) * (wcslen(*message.get()) + 1); + data_to_send.lpData = *message.get(); + SendMessage(hwnd, WM_COPYDATA, 0, (LPARAM)&data_to_send); + + return true; + } + BOOST_LOG_TRIVIAL(trace) << "win enum - found wrong instance"; + } + return true; + } + + static void multicast_message_inner(const std::string& message) + { + // multicast_message must live until EnumWindows is done, it is passed as pointer parameter. + std::wstring multicast_message = boost::nowide::widen(message); + EnumWindows(enum_windows_process_multicast, reinterpret_cast(&multicast_message)); + } + #else static bool get_lock(const std::string& name, const std::string& path) @@ -224,6 +295,11 @@ namespace instance_check_internal #endif //WIN32 #if defined(__APPLE__) + static void multicast_message_inner(const std::string &message_text) + { + multicast_message_mac(message_text); + } + static bool send_message(const std::string &message_text, const std::string &version) { //std::string v(version); @@ -238,6 +314,161 @@ namespace instance_check_internal #elif defined(__linux__) + static void list_matching_objects(const std::string& pattern, std::vector& result) + { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + DBusConnection* connection; + DBusError error; + + // Initialize the D-Bus error. + dbus_error_init(&error); + + // Connect to the session bus. + connection = dbus_bus_get(DBUS_BUS_SESSION, &error); + if (!connection) { + BOOST_LOG_TRIVIAL(error) << "Failed to connect to the D-Bus session bus: " << error.message; + dbus_error_free(&error); + return; + } + + // Request a list of all bus names. + DBusMessage* message = dbus_message_new_method_call( + "org.freedesktop.DBus", // Destination (the D-Bus daemon) + "/org/freedesktop/DBus", // Object path + "org.freedesktop.DBus", // Interface + "ListNames" // Method + ); + + if (!message) { + BOOST_LOG_TRIVIAL(error) << "Failed to create D-Bus message."; + return; + } + + DBusMessage* reply = dbus_connection_send_with_reply_and_block(connection, message, -1, &error); + dbus_message_unref(message); + + if (!reply) { + BOOST_LOG_TRIVIAL(error) << "Failed to send message: " << error.message; + dbus_error_free(&error); + return; + } + + // Parse the reply. + DBusMessageIter args; + if (!dbus_message_iter_init(reply, &args)) { + BOOST_LOG_TRIVIAL(error) << "Reply does not contain arguments."; + dbus_message_unref(reply); + return; + } + + if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_ARRAY) { + BOOST_LOG_TRIVIAL(error) << "Unexpected argument type in reply."; + dbus_message_unref(reply); + return; + } + + DBusMessageIter array_iter; + dbus_message_iter_recurse(&args, &array_iter); + + std::regex instance_regex(pattern); + + while (dbus_message_iter_get_arg_type(&array_iter) == DBUS_TYPE_STRING) { + const char* name; + dbus_message_iter_get_basic(&array_iter, &name); + if (std::regex_match(name, instance_regex)) { + result.push_back(name); + BOOST_LOG_TRIVIAL(debug) << "Matching object found: " << name; + } + dbus_message_iter_next(&array_iter); + } + + dbus_message_unref(reply); + dbus_error_free(&error); + } + + + static bool multicast_one_message(const std::string &message_text, const std::string &interface_name) + { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " to " << interface_name; + DBusMessage* msg; + DBusConnection* conn; + DBusError err; + dbus_uint32_t serial = 0; + const char* sigval = message_text.c_str(); + std::string method_name = "Message"; + std::string object_name = "/" + interface_name; + std::replace(object_name.begin(), object_name.end(), '.', '/'); + + // initialise the error value + dbus_error_init(&err); + // connect to bus, and check for errors (use SESSION bus everywhere!) + conn = dbus_bus_get(DBUS_BUS_SESSION, &err); + if (dbus_error_is_set(&err)) { + BOOST_LOG_TRIVIAL(error) << "DBus Connection Error. Message to another instance wont be send."; + BOOST_LOG_TRIVIAL(error) << "DBus Connection Error: " << err.message; + dbus_error_free(&err); + return false; + } + if (NULL == conn) { + BOOST_LOG_TRIVIAL(error) << "DBus Connection is NULL. Message to another instance wont be send."; + return false; + } + //some sources do request interface ownership before constructing msg but i think its wrong. + //create new method call message + msg = dbus_message_new_method_call(interface_name.c_str(), object_name.c_str(), interface_name.c_str(), method_name.c_str()); + if (NULL == msg) { + BOOST_LOG_TRIVIAL(error) << "DBus Message is NULL. Message to another instance wont be send."; + dbus_connection_unref(conn); + return false; + } + //the Message method is not sending reply. + dbus_message_set_no_reply(msg, TRUE); + //append arguments to message + if (!dbus_message_append_args(msg, DBUS_TYPE_STRING, &sigval, DBUS_TYPE_INVALID)) { + BOOST_LOG_TRIVIAL(error) << "Ran out of memory while constructing args for DBus message. Message to another instance wont be send."; + dbus_message_unref(msg); + dbus_connection_unref(conn); + return false; + } + // send the message and flush the connection + if (!dbus_connection_send(conn, msg, &serial)) { + BOOST_LOG_TRIVIAL(error) << "Ran out of memory while sending DBus message."; + dbus_message_unref(msg); + dbus_connection_unref(conn); + return false; + } + dbus_connection_flush(conn); + BOOST_LOG_TRIVIAL(trace) << "DBus message sent."; + // free the message and close the connection + dbus_message_unref(msg); + dbus_connection_unref(conn); + return true; + } + + static void multicast_message_inner(const std::string &message_text) + { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + std::string pattern = R"(com\.qiditech\.qidislicer\.MulticastListener\.Object\d+)"; + std::vector instances; + std::string my_pid = std::to_string(get_current_pid()); + + list_matching_objects(pattern, instances); + for (const std::string& instance : instances) { + // regex object pid to not send message to myself. + std::regex objectRegex("Object(\\d+)"); + std::smatch match; + if (std::regex_search(instance, match, objectRegex) && match[1] != my_pid) { + if (!multicast_one_message(message_text, instance)) { + BOOST_LOG_TRIVIAL(error) << "Failed send DBUS message to " << instance; + } else { + BOOST_LOG_TRIVIAL(debug) << "Successfully sent DBUS message to " << instance; + } + } + } + + } + + static bool send_message(const std::string &message_text, const std::string &version) { /*std::string v(version); @@ -368,7 +599,7 @@ bool instance_check(int argc, char** argv, bool app_config_single_instance) // get_lock() creates the lockfile therefore *cla.should_send is checked after if (instance_check_internal::get_lock(lock_name + ".lock", data_dir() + "/cache/") && *cla.should_send) { #endif - instance_check_internal::send_message(cla.cl_string, lock_name); + instance_check_internal::send_message(instance_check_internal::compose_message_json("CLI", cla.cl_string), lock_name); BOOST_LOG_TRIVIAL(error) << "Instance check: Another instance found. This instance will terminate. Lock file of current running instance is located at " << data_dir() << #ifdef _WIN32 "\\cache\\" @@ -390,6 +621,7 @@ wxDEFINE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent); wxDEFINE_EVENT(EVT_START_DOWNLOAD_OTHER_INSTANCE, StartDownloadOtherInstanceEvent); wxDEFINE_EVENT(EVT_LOGIN_OTHER_INSTANCE, LoginOtherInstanceEvent); wxDEFINE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent); +wxDEFINE_EVENT(EVT_STORE_READ_REQUEST, SimpleEvent); void OtherInstanceMessageHandler::init(wxEvtHandler* callback_evt_handler) { @@ -406,7 +638,8 @@ void OtherInstanceMessageHandler::init(wxEvtHandler* callback_evt_handler) #endif //__APPLE__ #ifdef BACKGROUND_MESSAGE_LISTENER - m_thread = boost::thread((boost::bind(&OtherInstanceMessageHandler::listen, this))); + m_instance_check_thread = boost::thread((boost::bind(&OtherInstanceMessageHandler::listen_instance_check, this))); + m_multicast_listener_thread = boost::thread((boost::bind(&OtherInstanceMessageHandler::listen_multicast, this))); #endif //BACKGROUND_MESSAGE_LISTENER } void OtherInstanceMessageHandler::shutdown(MainFrame* main_frame) @@ -428,17 +661,30 @@ void OtherInstanceMessageHandler::shutdown(MainFrame* main_frame) this->unregister_for_messages(); #endif //__APPLE__ #ifdef BACKGROUND_MESSAGE_LISTENER - if (m_thread.joinable()) { + if (m_instance_check_thread.joinable()) { // Stop the worker thread, if running. { // Notify the worker thread to cancel wait on detection polling. - std::lock_guard lck(m_thread_stop_mutex); - m_stop = true; + std::lock_guard lck(m_instance_check_thread_stop_mutex); + m_instance_check_thread_stop = true; } - m_thread_stop_condition.notify_all(); + m_instance_check_thread_stop_condition.notify_all(); // Wait for the worker thread to stop. - m_thread.join(); - m_stop = false; + m_instance_check_thread.join(); + m_instance_check_thread_stop = false; + } + + if (m_multicast_listener_thread.joinable()) { + // Stop the worker thread, if running. + { + // Notify the worker thread to cancel wait on detection polling. + std::lock_guard lck(m_multicast_listener_thread_stop_mutex); + m_multicast_listener_thread_stop = true; + } + m_multicast_listener_thread_stop_condition.notify_all(); + // Wait for the worker thread to stop. + m_multicast_listener_thread.join(); + m_multicast_listener_thread_stop = false; } #endif //BACKGROUND_MESSAGE_LISTENER m_callback_evt_handler = nullptr; @@ -521,15 +767,20 @@ namespace MessageHandlerInternal } } //namespace MessageHandlerInternal -void OtherInstanceMessageHandler::handle_message(const std::string& message) +void OtherInstanceMessageHandler::multicast_message(const std::string& message_type, const std::string& message_data) { - BOOST_LOG_TRIVIAL(info) << "message from other instance: " << message; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << message_type; + instance_check_internal::multicast_message_inner(instance_check_internal::compose_message_json(message_type, message_data)); +} - std::vector args; - bool parsed = unescape_strings_cstyle(message, args); + +void OtherInstanceMessageHandler::handle_message_type_cli(const std::string& data) +{ + std::vector args; + bool parsed = unescape_strings_cstyle(data, args); assert(parsed); if (! parsed) { - BOOST_LOG_TRIVIAL(error) << "message from other instance is incorrectly formatted: " << message; + BOOST_LOG_TRIVIAL(error) << "message from other instance is incorrectly formatted: " << data; return; } @@ -538,10 +789,10 @@ void OtherInstanceMessageHandler::handle_message(const std::string& message) // Skip the first argument, it is the path to the slicer executable. auto it = args.begin(); for (++ it; it != args.end(); ++ it) { + BOOST_LOG_TRIVIAL(debug) << *it; boost::filesystem::path p = MessageHandlerInternal::get_path(*it); if (! p.string().empty()) paths.emplace_back(p); -// TODO: There is a misterious slash appearing in recieved msg on windows #ifdef _WIN32 else if (it->rfind("qidislicer://open/?file=", 0) == 0) #else @@ -553,16 +804,45 @@ void OtherInstanceMessageHandler::handle_message(const std::string& message) } } if (! paths.empty()) { - //wxEvtHandler* evt_handler = wxGetApp().plater(); //assert here? - //if (evt_handler) { - wxPostEvent(m_callback_evt_handler, LoadFromOtherInstanceEvent(GUI::EVT_LOAD_MODEL_OTHER_INSTANCE, std::vector(std::move(paths)))); - //} + wxPostEvent(m_callback_evt_handler, LoadFromOtherInstanceEvent(GUI::EVT_LOAD_MODEL_OTHER_INSTANCE, std::vector(std::move(paths)))); } - if (!downloads.empty()) - { + if (!downloads.empty()) { wxPostEvent(m_callback_evt_handler, StartDownloadOtherInstanceEvent(GUI::EVT_START_DOWNLOAD_OTHER_INSTANCE, std::vector(std::move(downloads)))); } } +void OtherInstanceMessageHandler::handle_message_type_store_read(const std::string& data) +{ + wxPostEvent(m_callback_evt_handler, SimpleEvent(GUI::EVT_STORE_READ_REQUEST)); +} + +void OtherInstanceMessageHandler::handle_message(const std::string& message) +{ + BOOST_LOG_TRIVIAL(info) << "message from other instance: " << message; + // message in format { "type" : "TYPE", "data" : "data" } + // types: CLI, STORE_READ + std::string type; + std::string data; + try { + std::stringstream ss(message); + boost::property_tree::ptree ptree; + boost::property_tree::read_json(ss, ptree); + if (const auto action = ptree.get_optional("type"); action) { + type = *action; + } + if (const auto data_opt = ptree.get_optional("data"); data_opt) { + data = *data_opt; + } + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse other instance message: " << e.what(); + return; + } + assert(!type.empty()); + assert(m_message_handlers.find(type) != m_message_handlers.end()); // this assert means there is an message type that has no handling. + if (m_message_handlers.find(type) != m_message_handlers.end()) { + m_message_handlers[type](data); + } +} #ifdef __APPLE__ void OtherInstanceMessageHandler::handle_message_other_closed() @@ -573,7 +853,7 @@ void OtherInstanceMessageHandler::handle_message_other_closed() #ifdef BACKGROUND_MESSAGE_LISTENER -namespace MessageHandlerDBusInternal +namespace InstanceCheckMessageHandlerDBusInternal { //reply to introspect makes our DBus object visible for other programs like D-Feet static void respond_to_introspect(DBusConnection *connection, DBusMessage *request) @@ -634,26 +914,27 @@ namespace MessageHandlerDBusInternal std::string our_interface = "com.qidi3d.qidislicer.InstanceCheck.Object" + wxGetApp().get_instance_hash_string(); BOOST_LOG_TRIVIAL(trace) << "DBus message received: interface: " << interface_name << ", member: " << member_name; if (0 == strcmp("org.freedesktop.DBus.Introspectable", interface_name) && 0 == strcmp("Introspect", member_name)) { - respond_to_introspect(connection, message); + InstanceCheckMessageHandlerDBusInternal::respond_to_introspect(connection, message); return DBUS_HANDLER_RESULT_HANDLED; } else if (0 == strcmp(our_interface.c_str(), interface_name) && 0 == strcmp("AnotherInstance", member_name)) { - handle_method_another_instance(connection, message); + InstanceCheckMessageHandlerDBusInternal::handle_method_another_instance(connection, message); return DBUS_HANDLER_RESULT_HANDLED; } else if (0 == strcmp(our_interface.c_str(), interface_name) && 0 == strcmp("Introspect", member_name)) { - respond_to_introspect(connection, message); + InstanceCheckMessageHandlerDBusInternal::respond_to_introspect(connection, message); return DBUS_HANDLER_RESULT_HANDLED; } return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } -} //namespace MessageHandlerDBusInternal +} //namespace InstanceCheckMessageHandlerDBusInternal -void OtherInstanceMessageHandler::listen() +void OtherInstanceMessageHandler::listen_instance_check() { DBusConnection* conn; DBusError err; int name_req_val; DBusObjectPathVTable vtable; std::string instance_hash = wxGetApp().get_instance_hash_string(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << instance_hash; std::string interface_name = "com.qidi3d.qidislicer.InstanceCheck.Object" + instance_hash; std::string object_name = "/com/qidi3d/qidislicer/InstanceCheck/Object" + instance_hash; @@ -690,7 +971,7 @@ void OtherInstanceMessageHandler::listen() } // Set callbacks. Unregister function should not be nessary. - vtable.message_function = MessageHandlerDBusInternal::handle_dbus_object_message; + vtable.message_function = InstanceCheckMessageHandlerDBusInternal::handle_dbus_object_message; vtable.unregister_function = NULL; // register new object - this is our access to DBus @@ -703,19 +984,170 @@ void OtherInstanceMessageHandler::listen() return; } - BOOST_LOG_TRIVIAL(trace) << "Dbus object "<< object_name <<" registered. Starting listening for messages."; + BOOST_LOG_TRIVIAL(debug) << "Dbus object "<< object_name <<" registered. Starting listening for messages."; for (;;) { // Wait for 1 second // Cancellable. { - std::unique_lock lck(m_thread_stop_mutex); - m_thread_stop_condition.wait_for(lck, std::chrono::seconds(1), [this] { return m_stop; }); + std::unique_lock lck(m_instance_check_thread_stop_mutex); + m_instance_check_thread_stop_condition.wait_for(lck, std::chrono::seconds(1), [this] { return m_instance_check_thread_stop; }); } - if (m_stop) + if (m_instance_check_thread_stop) { // Stop the worker thread. - break; + } + //dispatch should do all the work with incoming messages + //second parameter is blocking time that funciton waits for new messages + //that is handled here with our own event loop above + dbus_connection_read_write_dispatch(conn, 0); + } + + dbus_connection_unref(conn); +} + + +namespace MulticastMessageHandlerDBusInternal +{ + //reply to introspect makes our DBus object visible for other programs like D-Feet + static void respond_to_introspect(DBusConnection *connection, DBusMessage *request) + { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + DBusMessage *reply; + const char *introspection_data = + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " "; + + reply = dbus_message_new_method_return(request); + dbus_message_append_args(reply, DBUS_TYPE_STRING, &introspection_data, DBUS_TYPE_INVALID); + dbus_connection_send(connection, reply, NULL); + dbus_message_unref(reply); + } + + //method Message receives message from another QIDISlicer instance + static void handle_method_message(DBusConnection *connection, DBusMessage *request) + { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + DBusError err; + char* text = nullptr; + wxEvtHandler* evt_handler; + + dbus_error_init(&err); + dbus_message_get_args(request, &err, DBUS_TYPE_STRING, &text, DBUS_TYPE_INVALID); + if (dbus_error_is_set(&err)) { + BOOST_LOG_TRIVIAL(debug) << "Dbus method Message received with wrong arguments."; + dbus_error_free(&err); + return; + } + wxGetApp().other_instance_message_handler()->handle_message(text); + } + + //every dbus message received comes here + static DBusHandlerResult handle_dbus_object_message(DBusConnection *connection, DBusMessage *message, void *user_data) + { + const char* interface_name = dbus_message_get_interface(message); + const char* member_name = dbus_message_get_member(message); + std::string our_interface = "com.qiditech.qidislicer.MulticastListener.Object" + std::to_string(get_current_pid()); + BOOST_LOG_TRIVIAL(debug) << "DBus message received: interface: " << interface_name << ", member: " << member_name; + if (0 == strcmp("org.freedesktop.DBus.Introspectable", interface_name) && 0 == strcmp("Introspect", member_name)) { + MulticastMessageHandlerDBusInternal::respond_to_introspect(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } else if (0 == strcmp(our_interface.c_str(), interface_name) && 0 == strcmp("Message", member_name)) { + MulticastMessageHandlerDBusInternal::handle_method_message(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } else if (0 == strcmp(our_interface.c_str(), interface_name) && 0 == strcmp("Introspect", member_name)) { + MulticastMessageHandlerDBusInternal::respond_to_introspect(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } +} //namespace MulticastMessageHandlerDBusInternal + +void OtherInstanceMessageHandler::listen_multicast() +{ + DBusConnection* conn; + DBusError err; + int name_req_val; + DBusObjectPathVTable vtable; + std::string pid = std::to_string(get_current_pid()); + std::string interface_name = "com.qiditech.qidislicer.MulticastListener.Object" + pid; + std::string object_name = "/com/qiditech/qidislicer/MulticastListener/Object" + pid; + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << interface_name; + dbus_error_init(&err); + + // connect to the bus and check for errors (use SESSION bus everywhere!) + conn = dbus_bus_get(DBUS_BUS_SESSION, &err); + if (dbus_error_is_set(&err)) { + BOOST_LOG_TRIVIAL(error) << "listen_multicast: DBus Connection Error: "<< err.message; + BOOST_LOG_TRIVIAL(error) << "listen_multicast: Dbus Messages listening terminating."; + dbus_error_free(&err); + return; + } + if (NULL == conn) { + BOOST_LOG_TRIVIAL(error) << "listen_multicast: DBus Connection is NULL. Dbus Messages listening terminating."; + return; + } + + // request our name on the bus and check for errors + name_req_val = dbus_bus_request_name(conn, interface_name.c_str(), DBUS_NAME_FLAG_REPLACE_EXISTING , &err); + if (dbus_error_is_set(&err)) { + BOOST_LOG_TRIVIAL(error) << "listen_multicast: DBus Request name Error: "<< err.message; + BOOST_LOG_TRIVIAL(error) << "listen_multicast: Dbus Messages listening terminating."; + dbus_error_free(&err); + dbus_connection_unref(conn); + return; + } + if (DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER != name_req_val) { + BOOST_LOG_TRIVIAL(error) << "listen_multicast: Not primary owner of DBus name - probably another QIDISlicer instance is running."; + BOOST_LOG_TRIVIAL(error) << "listen_multicast: Dbus Messages listening terminating."; + dbus_connection_unref(conn); + return; + } + + // Set callbacks. Unregister function should not be nessary. + vtable.message_function = MulticastMessageHandlerDBusInternal::handle_dbus_object_message; + vtable.unregister_function = NULL; + + // register new object - this is our access to DBus + dbus_connection_try_register_object_path(conn, object_name.c_str(), &vtable, NULL, &err); + if ( dbus_error_is_set(&err) ) { + BOOST_LOG_TRIVIAL(error) << "listen_multicast: DBus Register object Error: "<< err.message; + BOOST_LOG_TRIVIAL(error) << "listen_multicast: Dbus Messages listening terminating."; + dbus_connection_unref(conn); + dbus_error_free(&err); + return; + } + + BOOST_LOG_TRIVIAL(debug) << "listen_multicast: Dbus object "<< object_name <<" registered. Starting listening for messages."; + + for (;;) { + // Wait for 1 second + // Cancellable. + { + std::unique_lock lck(m_multicast_listener_thread_stop_mutex); + m_multicast_listener_thread_stop_condition.wait_for(lck, std::chrono::seconds(1), [this] { return m_multicast_listener_thread_stop; }); + } + if (m_multicast_listener_thread_stop) { + // Stop the worker thread. + break; + } //dispatch should do all the work with incoming messages //second parameter is blocking time that funciton waits for new messages //that is handled here with our own event loop above diff --git a/src/slic3r/GUI/InstanceCheck.hpp b/src/slic3r/GUI/InstanceCheck.hpp index 322cb01..491c56e 100644 --- a/src/slic3r/GUI/InstanceCheck.hpp +++ b/src/slic3r/GUI/InstanceCheck.hpp @@ -8,6 +8,7 @@ #endif //_WIN32 #include +#include #include @@ -28,6 +29,7 @@ bool instance_check(int argc, char** argv, bool app_config_single_instance); // apple implementation of inner functions of instance_check // in InstanceCheckMac.mm void send_message_mac(const std::string& msg, const std::string& version); +void multicast_message_mac(const std::string &msg); void send_message_mac_closing(const std::string& msg, const std::string& version); @@ -50,11 +52,16 @@ wxDECLARE_EVENT(EVT_START_DOWNLOAD_OTHER_INSTANCE, StartDownloadOtherInstanceEve wxDECLARE_EVENT(EVT_LOGIN_OTHER_INSTANCE, LoginOtherInstanceEvent); using InstanceGoToFrontEvent = SimpleEvent; wxDECLARE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent); +wxDECLARE_EVENT(EVT_STORE_READ_REQUEST, SimpleEvent); class OtherInstanceMessageHandler { public: - OtherInstanceMessageHandler() = default; + OtherInstanceMessageHandler() + { + m_message_handlers["CLI"] = std::bind(&OtherInstanceMessageHandler::handle_message_type_cli, this, std::placeholders::_1); + m_message_handlers["STORE_READ"] = std::bind(&OtherInstanceMessageHandler::handle_message_type_store_read, this, std::placeholders::_1); + } OtherInstanceMessageHandler(OtherInstanceMessageHandler const&) = delete; void operator=(OtherInstanceMessageHandler const&) = delete; ~OtherInstanceMessageHandler() { assert(!m_initialized); } @@ -64,13 +71,10 @@ public: // stops listening, on linux stops the background thread void shutdown(MainFrame* main_frame); - //finds paths to models in message(= command line arguments, first should be qidiSlicer executable) - //and sends them to plater via LoadFromOtherInstanceEvent - //security of messages: from message all existing paths are proccesed to load model - // win32 - anybody who has hwnd can send message. - // mac - anybody who posts notification with name:@"OtherQIDISlicerTerminating" - // linux - instrospectable on dbus - void handle_message(const std::string& message); + // message in format { "type" : "TYPE", "data" : "data" } + void handle_message(const std::string& message); + + void multicast_message(const std::string& message_type, const std::string& message_data = std::string()); #ifdef __APPLE__ // Messege form other instance, that it deleted its lockfile - first instance to get it will create its own. void handle_message_other_closed(); @@ -80,19 +84,37 @@ public: void update_windows_properties(MainFrame* main_frame); #endif //WIN32 private: + //finds paths to models in message(= command line arguments, first should be qidiSlicer executable) + //and sends them to plater via LoadFromOtherInstanceEvent + //security of messages: from message all existing paths are proccesed to load model + // win32 - anybody who has hwnd can send message. + // mac - anybody who posts notification with name:@"OtherQIDISlicerTerminating" + // linux - instrospectable on dbus + void handle_message_type_cli(const std::string& data); + // Passes information to UI to perform store read + void handle_message_type_store_read(const std::string& data); + std::map> m_message_handlers; + bool m_initialized { false }; wxEvtHandler* m_callback_evt_handler { nullptr }; #ifdef BACKGROUND_MESSAGE_LISTENER - //worker thread to listen incoming dbus communication - boost::thread m_thread; - std::condition_variable m_thread_stop_condition; - mutable std::mutex m_thread_stop_mutex; - bool m_stop{ false }; - bool m_start{ true }; - - // background thread method - void listen(); + // instance check worker thread to listen incoming dbus communication + // Only one instance has registered dbus object at time + boost::thread m_instance_check_thread; + std::condition_variable m_instance_check_thread_stop_condition; + mutable std::mutex m_instance_check_thread_stop_mutex; + bool m_instance_check_thread_stop{ false }; + //bool m_instance_check_thread_start{ true }; + void listen_instance_check(); + + // "multicast" worker thread to listen incoming dbus communication + // every instance has registered its own object + boost::thread m_multicast_listener_thread; + std::condition_variable m_multicast_listener_thread_stop_condition; + mutable std::mutex m_multicast_listener_thread_stop_mutex; + bool m_multicast_listener_thread_stop{ false }; + void listen_multicast(); #endif //BACKGROUND_MESSAGE_LISTENER #if __APPLE__ diff --git a/src/slic3r/GUI/InstanceCheckMac.h b/src/slic3r/GUI/InstanceCheckMac.h index 0af2737..50188cf 100644 --- a/src/slic3r/GUI/InstanceCheckMac.h +++ b/src/slic3r/GUI/InstanceCheckMac.h @@ -5,6 +5,7 @@ -(instancetype) init; -(void) add_observer:(NSString *)version; -(void) message_update:(NSNotification *)note; +-(void) message_multicast_update:(NSNotification *)note; -(void) closing_update:(NSNotification *)note; -(void) bring_forward; @end diff --git a/src/slic3r/GUI/InstanceCheckMac.mm b/src/slic3r/GUI/InstanceCheckMac.mm index c041caf..da6de89 100644 --- a/src/slic3r/GUI/InstanceCheckMac.mm +++ b/src/slic3r/GUI/InstanceCheckMac.mm @@ -11,12 +11,14 @@ } -(void)add_observer:(NSString *)version_hash { - //NSLog(@"adding observer"); + //NSLog(@"adding observers"); //NSString *nsver = @"OtherQIDISlicerInstanceMessage" + version_hash; NSString *nsver = [NSString stringWithFormat: @"%@%@", @"OtherQIDISlicerInstanceMessage", version_hash]; [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(message_update:) name:nsver object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately]; NSString *nsver2 = [NSString stringWithFormat: @"%@%@", @"OtherQIDISlicerInstanceClosing", version_hash]; [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(closing_update:) name:nsver2 object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately]; + NSString *nsnover = [NSString stringWithFormat: @"%@", @"OtherQIDISlicerMulticastMessage"]; + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(message_multicast_update:) name:nsnover object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately]; } -(void)message_update:(NSNotification *)msg @@ -26,6 +28,13 @@ Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(std::string([msg.userInfo[@"data"] UTF8String])); } +-(void)message_multicast_update:(NSNotification *)msg +{ + //NSLog(@"message_multicast_update"); + //pass message + Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(std::string([msg.userInfo[@"data"] UTF8String])); +} + -(void)closing_update:(NSNotification *)msg { //[self bring_forward]; @@ -51,6 +60,14 @@ namespace Slic3r { +void multicast_message_mac(const std::string &msg) +{ + NSString *nsmsg = [NSString stringWithCString:msg.c_str() encoding:[NSString defaultCStringEncoding]]; + //NSString *nsver = @"OtherQIDISlicerInstanceMessage" + [NSString stringWithCString:version.c_str() encoding:[NSString defaultCStringEncoding]]; + NSString *notifname = [NSString stringWithFormat: @"%@", @"OtherQIDISlicerMulticastMessage"]; + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:notifname object:nil userInfo:[NSDictionary dictionaryWithObject:nsmsg forKey:@"data"] deliverImmediately:YES]; +} + void send_message_mac(const std::string &msg, const std::string &version) { NSString *nsmsg = [NSString stringWithCString:msg.c_str() encoding:[NSString defaultCStringEncoding]]; diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index d33bee1..d1d8b7b 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -117,6 +117,7 @@ void RotoptimizeJob::finalize(bool canceled, std::exception_ptr &eptr) // Correct the z offset of the object which was corrupted be // the rotation + o->invalidate_bounding_box(); o->ensure_on_bed(); } diff --git a/src/slic3r/GUI/Jobs/SeqArrangeJob.cpp b/src/slic3r/GUI/Jobs/SeqArrangeJob.cpp new file mode 100644 index 0000000..bdf9964 --- /dev/null +++ b/src/slic3r/GUI/Jobs/SeqArrangeJob.cpp @@ -0,0 +1,87 @@ +#include "SeqArrangeJob.hpp" + +#include "libslic3r/ArrangeHelper.hpp" + +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/format.hpp" + + + +namespace Slic3r { namespace GUI { + + +SeqArrangeJob::SeqArrangeJob(const Model& model, const DynamicPrintConfig& config, bool current_bed_only) +{ + m_seq_arrange.reset(new SeqArrange(model, config, current_bed_only)); +} + + +void SeqArrangeJob::process(Ctl& ctl) +{ + class SeqArrangeJobException : std::exception {}; + + try { + m_seq_arrange->process_seq_arrange([&](int progress) { + ctl.update_status(progress, _u8L("Arranging for sequential print")); + if (ctl.was_canceled()) + throw SeqArrangeJobException(); + } + ); + } catch (const SeqArrangeJobException&) { + ctl.update_status(100, ""); // Hide progress notification. + } + catch (const std::exception&) { + ctl.update_status(100, ""); // Hide progress notification. + throw; + } +} + + + +void SeqArrangeJob::finalize(bool canceled, std::exception_ptr& eptr) +{ + // If the task was cancelled, the stopping exception was already caught + // in 'process' function. Any other exception propagates through here. + bool error = false; + if (eptr) { + try { + std::rethrow_exception(eptr); + } catch (const ExceptionCannotApplySeqArrange&) { + ErrorDialog dlg(wxGetApp().plater(), _L("The result of the single-bed arrange would scatter " + "instances of a single object between several beds, possibly affecting order of printing " + "of the non-selected beds. Consider using global arrange across all beds."), false); + dlg.ShowModal(); + error = true; + eptr = nullptr; // The exception is handled. + } catch (const Sequential::ObjectTooLargeException&) { + ErrorDialog dlg(wxGetApp().plater(), _L("One of the objects is too large to fit the bed."), false); + dlg.ShowModal(); + error = true; + eptr = nullptr; // The exception is handled. + } catch (const std::runtime_error& ex) { + ErrorDialog dlg(wxGetApp().plater(), GUI::format_wxstr(_L("Internal error: %1%"), ex.what()), false); + dlg.ShowModal(); + error = true; + eptr = nullptr; // The exception is handled. + } + } + + if (! canceled && ! error) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Arrange for sequential print")); + m_seq_arrange->apply_seq_arrange(wxGetApp().model()); + wxGetApp().plater()->canvas3D()->reload_scene(true, true); + wxGetApp().obj_list()->update_after_undo_redo(); + } + m_seq_arrange.reset(); +} + + + + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Jobs/SeqArrangeJob.hpp b/src/slic3r/GUI/Jobs/SeqArrangeJob.hpp new file mode 100644 index 0000000..c604ebb --- /dev/null +++ b/src/slic3r/GUI/Jobs/SeqArrangeJob.hpp @@ -0,0 +1,31 @@ +#ifndef SEQARRANGEJOB_HPP +#define SEQARRANGEJOB_HPP + +#include "Job.hpp" + +namespace Slic3r { + +class Model; + + +class SeqArrange; +class DynamicPrintConfig; + +namespace GUI { + + +class SeqArrangeJob : public Job +{ +public: + explicit SeqArrangeJob(const Model& model, const DynamicPrintConfig& config, bool current_bed_only); + virtual void process(Ctl &ctl) override; + virtual void finalize(bool /*canceled*/, std::exception_ptr&) override; + +private: + std::unique_ptr m_seq_arrange; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // ARRANGEJOB2_HPP diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index e551514..3682f86 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -68,6 +68,8 @@ //B55 #include "../Utils/PrintHost.hpp" +#include + //B64 #if QDT_RELEASE_TO_PUBLIC #include "../QIDI/QIDINetwork.hpp" @@ -275,6 +277,33 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S event.Skip(); }); + //y22 + Bind(wxEVT_ICONIZE, [this](wxIconizeEvent& event) { + if (event.IsIconized()) { + if (m_printer_view->GetHasLoadUrl()) { + printer_view_ip = m_printer_view->GetWebIp(); + printer_view_url = m_printer_view->GetWeburl(); + } + wxString url; + if (m_printer_view->GetNetMode()) { + url = wxString::Format("file://%s/web/qidi/link_missing_connection.html", from_u8(resources_dir())); + } + else { + url = wxString::Format("file://%s/web/qidi/missing_connection.html", from_u8(resources_dir())); + } + m_printer_view->load_disconnect_url(url); + } + else { + if (!printer_view_ip.empty() && new_sel == 4) { + if (is_net_url) + m_printer_view->load_net_url(printer_view_url, printer_view_ip); + else + m_printer_view->load_url(printer_view_url); + } + m_printer_view->Layout(); + } + }); + //FIXME it seems this method is not called on application start-up, at least not on Windows. Why? // The same applies to wxEVT_CREATE, it is not being called on startup on Windows. Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) { @@ -707,6 +736,8 @@ void MainFrame::init_tabpanel() size_t current_selected_tab = m_tabpanel->GetSelection(); Tab* tab = dynamic_cast(panel); + new_sel = e.GetSelection(); + //y15 if (tab != nullptr) { @@ -744,6 +775,32 @@ void MainFrame::init_tabpanel() //y17 else select_tab(size_t(0)); // select Plater + //y22 + if (current_selected_tab != 4) { + if (m_printer_view->GetHasLoadUrl()) { + printer_view_ip = m_printer_view->GetWebIp(); + printer_view_url = m_printer_view->GetWeburl(); + is_net_url = m_printer_view->IsNetUrl(); + } + wxString url; + if (m_printer_view->GetNetMode()) { + url = wxString::Format("file://%s/web/qidi/link_missing_connection.html", from_u8(resources_dir())); + } + else { + url = wxString::Format("file://%s/web/qidi/missing_connection.html", from_u8(resources_dir())); + } + m_printer_view->load_disconnect_url(url); + } + else { + if (!printer_view_ip.empty()) { + if (is_net_url) + m_printer_view->load_net_url(printer_view_url, printer_view_ip); + else + m_printer_view->load_url(printer_view_url); + } + m_printer_view->Layout(); + } + }); m_plater = new Plater(this, this); @@ -927,6 +984,7 @@ void MainFrame::remove_connect_webview_tab() if (!m_connect_webview_added) { return; } + m_connect_webview->prohibit_after_show_func_once(); int n = m_tabpanel->FindPage(m_connect_webview); if (m_tabpanel->GetSelection() == n) m_tabpanel->SetSelection(0); @@ -1453,6 +1511,59 @@ static wxMenu* generate_help_menu() [](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); }); // append_menu_item(helpMenu, wxID_ANY, _L("Report an I&ssue"), wxString::Format(_L("Report an issue on %s"), SLIC3R_APP_NAME), // [](wxCommandEvent&) { wxGetApp().open_browser_with_warning_dialog("https://github.com/qidi3d/slic3r/issues/new", nullptr, false); }); + + append_menu_item(helpMenu, wxID_ANY, _L("Clean the Webview Cache"), _L("Clean the Webview Cache"), + [](wxCommandEvent&) { + CleanCacheDialog* dlg = new CleanCacheDialog(static_cast(wxGetApp().mainframe)); + int res = dlg->ShowModal(); + if (res == wxID_OK) { +#ifdef _WIN32 + wxString local_path = wxStandardPaths::Get().GetUserLocalDataDir(); + wxString command = wxString::Format("explorer %s", local_path); + if (std::filesystem::exists(into_u8(local_path))) { + BOOST_LOG_TRIVIAL(error) << boost::format("The path is Exitsts : %1%") % local_path; + wxExecute(command); + wxPostEvent(wxGetApp().mainframe, wxCloseEvent(wxEVT_CLOSE_WINDOW)); + } + else { + wxMessageBox("The path is not exists", "error", wxICON_ERROR | wxOK); + BOOST_LOG_TRIVIAL(error) << boost::format("The path is not exitsts: %1%") % local_path; + } +#elif defined(__APPLE__) + wxString local_path = wxFileName::GetHomeDir() + "/Library/Caches"; + wxString command = wxString::Format("open \"%s\"", local_path); + wxString local_path_2 = wxFileName::GetHomeDir() + "/Library/WebKit"; + wxString command_2 = wxString::Format("open \"%s\"", local_path_2); + if (std::filesystem::exists(into_u8(local_path)) && std::filesystem::exists(into_u8(local_path_2))) { + BOOST_LOG_TRIVIAL(error) << boost::format("The path is Exitsts : %1%") % local_path; + wxExecute(command); + wxExecute(command_2); + wxPostEvent(wxGetApp().mainframe, wxCloseEvent(wxEVT_CLOSE_WINDOW)); + } + else { + wxMessageBox("The path is not exists", "error", wxICON_ERROR | wxOK); + BOOST_LOG_TRIVIAL(error) << boost::format("The path is not exitsts: %1%") % local_path; + } +#elif defined __linux__ + wxString local_path = wxFileName::GetHomeDir() + "/.local/share"; + wxString command = wxString::Format("xdg-open \"%s\"", local_path); + wxString local_path_2 = wxFileName::GetHomeDir() + "/.cache"; + wxString command_2 = wxString::Format("xdg-open \"%s\"", local_path_2); + if (std::filesystem::exists(into_u8(local_path)) && std::filesystem::exists(into_u8(local_path_2))) { + BOOST_LOG_TRIVIAL(error) << boost::format("The path is Exitsts : %1%") % local_path; + wxExecute(command); + wxExecute(command_2); + wxPostEvent(wxGetApp().mainframe, wxCloseEvent(wxEVT_CLOSE_WINDOW)); + } + else { + wxMessageBox("The path is not exists", "error", wxICON_ERROR | wxOK); + BOOST_LOG_TRIVIAL(error) << boost::format("The path is not exitsts: %1%") % local_path; + } +#endif + } + dlg->Destroy(); + }); + #ifndef __APPLE__ append_about_menu_item(helpMenu); #endif // __APPLE__ diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 4b9549e..f2cb964 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -266,6 +266,12 @@ public: PreferencesDialog* preferences_dialog { nullptr }; PrintHostQueueDialog* m_printhost_queue_dlg; GalleryDialog* m_gallery_dialog{ nullptr }; + + //y22 + wxString printer_view_url = ""; + wxString printer_view_ip = ""; + bool is_net_url = false; + int new_sel; #ifdef __APPLE__ std::unique_ptr m_taskbar_icon; diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 6aa9fd0..ab9b2ac 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -27,6 +27,7 @@ //Y #include "libslic3r/AppConfig.hpp" +#include "Widgets/StateColor.hpp" namespace Slic3r { namespace GUI { @@ -405,5 +406,65 @@ wxString get_wraped_wxString(const wxString& in, size_t line_len /*=80*/) return out; } +//y22 +CleanCacheDialog::CleanCacheDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _L("Clean the Webview Cache"), wxDefaultPosition) +{ + std::string icon_path = (boost::format("%1%/icons/QIDISlicer.ico") % resources_dir()).str(); + SetIcon(wxIcon(icon_path.c_str(), wxBITMAP_TYPE_ICO)); + + wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + + auto m_line_top = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL); + m_line_top->SetBackgroundColour(wxColour(0xA6, 0xa9, 0xAA)); + main_sizer->Add(m_line_top, 0, wxEXPAND, 0); + main_sizer->Add(0, 0, 0, wxTOP, FromDIP(5)); + wxBoxSizer* content_sizer = new wxBoxSizer(wxHORIZONTAL); + wxStaticBitmap* info_bitmap = new wxStaticBitmap(this, wxID_ANY, *get_bmp_bundle("info", 60), wxDefaultPosition, wxSize(FromDIP(70), FromDIP(70)), 0); + content_sizer->Add(info_bitmap, 0, wxEXPAND | wxALL, FromDIP(5)); + + wxBoxSizer* vertical_sizer = new wxBoxSizer(wxVERTICAL); + wxStaticText* message_text = new wxStaticText(this, wxID_ANY, + _L("Click the OK button, the software will open the WebView cache folder.\n" + "You need to manually delete the WebView folder.\n"), + wxDefaultPosition); + vertical_sizer->Add(message_text, 0, wxEXPAND | wxTOP, FromDIP(5)); + + wxString hyperlink_text = "https://wiki.qidi3d.com/en/software/qidi-studio/troubleshooting/blank-page"; + wxHyperlinkCtrl* hyperlink = new wxHyperlinkCtrl(this, wxID_ANY, _L("Learn more"), hyperlink_text, wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE); + vertical_sizer->Add(hyperlink, 0, wxRIGHT, FromDIP(5)); + content_sizer->Add(vertical_sizer, 0, wxEXPAND | wxALL, FromDIP(5)); + main_sizer->Add(content_sizer, 0, wxEXPAND | wxALL, FromDIP(10)); + + auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); + wxGetApp().SetWindowVariantForButton(buttons->GetAffirmativeButton()); + wxGetApp().SetWindowVariantForButton(buttons->GetCancelButton()); + this->Bind(wxEVT_BUTTON, &CleanCacheDialog::OnOK, this, wxID_OK); + this->Bind(wxEVT_BUTTON, &CleanCacheDialog::OnCancel, this, wxID_CANCEL); + + for (int id : {wxID_OK, wxID_CANCEL}) + wxGetApp().UpdateDarkUI(static_cast(FindWindowById(id, this))); + + main_sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM | wxTOP, 10); + + this->SetSizer(main_sizer); + Layout(); + Fit(); + CenterOnParent(); + +} + +CleanCacheDialog::~CleanCacheDialog() {} + +void CleanCacheDialog::OnOK(wxCommandEvent& event) +{ + EndModal(wxID_OK); +} + +void CleanCacheDialog::OnCancel(wxCommandEvent& event) +{ + EndModal(wxID_CANCEL); +} + } } diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp index 9d6f717..3824528 100644 --- a/src/slic3r/GUI/MsgDialog.hpp +++ b/src/slic3r/GUI/MsgDialog.hpp @@ -360,6 +360,18 @@ private: wxString msg; }; +//y22 +class CleanCacheDialog : public wxDialog +{ +#define SELECT_MACHINE_DIALOG_BUTTON_SIZE wxSize(FromDIP(68), FromDIP(23)) +#define SELECT_MACHINE_DIALOG_SIMBOOK_SIZE wxSize(FromDIP(370), FromDIP(64)) +public: + CleanCacheDialog(wxWindow* parent); + ~CleanCacheDialog(); + + void OnOK(wxCommandEvent& event); + void OnCancel(wxCommandEvent& event); +}; } } diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index cf9fad3..635087b 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -140,6 +140,10 @@ enum class NotificationType WipeTowerNozzleDiameterDiffer, // Notification about using supports with different nozzle diameters. SupportNozzleDiameterDiffer, + // Transient error on QIDI Account communication - user is informed and has option to cancel (logout) + AccountTransientRetry, + // Failed to download secret repo archive + FailedSecretVendorUpdateSync }; class NotificationManager diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index d26ff3b..746feb5 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #if wxUSE_SECRETSTORE #include #endif @@ -618,7 +619,8 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr // Always fill in the "printhost_port" combo box from the config and select it. { Choice* choice = dynamic_cast(m_optgroup->get_field("printhost_port")); - choice->set_values({ m_config->opt_string("printhost_port") }); + const std::vector ports = { m_config->opt_string("printhost_port") }; + choice->set_values(ports); choice->set_selection(); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9cec489..ee69546 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -57,6 +57,8 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/miniz_extension.hpp" +#include "libslic3r/ModelProcessing.hpp" +#include "libslic3r/FileReader.hpp" #include "libslic3r/MultipleBeds.hpp" // For stl export @@ -85,6 +87,7 @@ #include "ConfigWizardWebViewPage.hpp" #include "Jobs/RotoptimizeJob.hpp" +#include "Jobs/SeqArrangeJob.hpp" #include "Jobs/SLAImportJob.hpp" #include "Jobs/SLAImportDialog.hpp" #include "Jobs/NotificationProgressIndicator.hpp" @@ -117,6 +120,8 @@ #include "PresetArchiveDatabase.hpp" #include "BulkExportDialog.hpp" +#include "libslic3r/ArrangeHelper.hpp" + //B34 #include "Gizmos/GLGizmoEmboss.hpp" @@ -146,6 +151,7 @@ static const std::pair THUMBNAIL_SIZE_3MF = { 256, 2 static const std::pair THUMBNAIL_SIZE_SEND = {128, 160}; namespace Slic3r { +using namespace ModelProcessing; namespace GUI { // BackgroundSlicingProcess updates UI with slicing progress: Status bar / progress bar has to be updated, possibly scene has to be refreshed, @@ -603,11 +609,9 @@ private: bool show_warning_dialog { false }; }; -const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|zip[.]amf|3mf|qidi)", std::regex::icase); +const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|3mf)", 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_qidi(".*qidi", std::regex::icase); const std::regex Plater::priv::pattern_zip(".*zip", std::regex::icase); const std::regex Plater::priv::pattern_printRequest(".*printRequest", std::regex::icase); @@ -615,7 +619,7 @@ 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", + "bed_shape", "bed_custom_texture", "bed_custom_model", "complete_objects", "duplicate_distance", "extruder_clearance_radius", "extruder_clearance_height", "skirts", "skirt_distance", "brim_width", "brim_separation", "brim_type", "variable_layer_height", "nozzle_diameter", "single_extruder_multi_material", "wipe_tower", "wipe_tower_width", "wipe_tower_brim_width", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_extruder", "extruder_colour", "filament_colour", "material_colour", "max_print_height", "printer_model", "printer_notes", "printer_technology", @@ -689,11 +693,11 @@ void Plater::priv::init() hsizer->Add(sidebar, 0, wxEXPAND | wxLEFT | wxRIGHT, 0); q->SetSizer(hsizer); - menus.init(q); - - // Events: - if (wxGetApp().is_editor()) { + menus.init(q); + + // Events: + // 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(); }); @@ -707,8 +711,8 @@ void Plater::priv::init() 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, [this](SimpleEvent&) { q->remove_selected(); }); - view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent&) { this->q->arrange(); }); - view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE_CURRENT_BED, [this](SimpleEvent&) { this->q->arrange_current_bed(); }); + view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent&) { this->q->arrange(false); }); + view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE_CURRENT_BED, [this](SimpleEvent&) { this->q->arrange(true); }); 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& evt) @@ -741,8 +745,8 @@ void Plater::priv::init() view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE, [this](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_ARRANGE_CURRENT_BED, [this](SimpleEvent&) { this->q->arrange_current_bed(); }); +view3D_canvas->Bind(EVT_GLTOOLBAR_ARRANGE, [this](SimpleEvent&) { this->q->arrange(false); }); +view3D_canvas->Bind(EVT_GLTOOLBAR_ARRANGE_CURRENT_BED, [this](SimpleEvent&) { this->q->arrange(true); }); view3D_canvas->Bind(EVT_GLTOOLBAR_COPY, [this](SimpleEvent&) { q->copy_selection_to_clipboard(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_PASTE, [this](SimpleEvent&) { q->paste_from_clipboard(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_MORE, [this](SimpleEvent&) { q->increase_instances(); }); @@ -832,7 +836,8 @@ void Plater::priv::init() }); this->q->Bind(EVT_REMOVABLE_DRIVE_ADDED, [this](wxCommandEvent& evt) { - if (!fs::exists(fs::path(evt.GetString().utf8_string()) / "qidi_printer_settings.ini")) + boost::system::error_code ec; + if (!fs::exists(fs::path(evt.GetString().utf8_string()) / "qidi_printer_settings.ini", ec) || ec) return; if (evt.GetInt() == 0) { // not at startup, show dialog wxGetApp().open_wifi_config_dialog(false, evt.GetString()); @@ -846,7 +851,6 @@ void Plater::priv::init() wxGetApp().open_wifi_config_dialog(true, evt.GetString()); return true;}); } - }); // Start the background thread and register this window as a target for update events. @@ -890,6 +894,11 @@ void Plater::priv::init() BOOST_LOG_TRIVIAL(trace) << "Received login from other instance event."; user_account->on_login_code_recieved(evt.data); }); + this->q->Bind(EVT_STORE_READ_REQUEST, [this](SimpleEvent& evt) { + BOOST_LOG_TRIVIAL(debug) << "Received store read request from other instance event."; + user_account->on_store_read_request(); + }); + this->q->Bind(EVT_LOGIN_VIA_WIZARD, [this](Event &evt) { BOOST_LOG_TRIVIAL(trace) << "Received login from wizard."; user_account->on_login_code_recieved(evt.data); @@ -947,7 +956,7 @@ void Plater::priv::init() }); //y15 - // this->q->Bind(EVT_UA_ID_USER_SUCCESS, [this](UserAccountSuccessEvent& evt) { + // auto on_id_user_success = [this](UserAccountSuccessEvent& evt, bool after_token_success) { // if (login_dialog != nullptr) { // login_dialog->EndModal(wxID_CANCEL); // } @@ -955,15 +964,19 @@ void Plater::priv::init() // evt.Skip(); // std::string who = user_account->get_username(); // std::string username; - // if (user_account->on_user_id_success(evt.data, username)) { + // if (user_account->on_user_id_success(evt.data, username, after_token_success)) { // if (who != username) { // // show notification only on login (not refresh). // std::string text = format(_u8L("Logged to QIDI Account as %1%."), username); // // login notification // this->notification_manager->close_notification_of_type(NotificationType::UserAccountID); - // // show connect tab + // this->notification_manager->close_notification_of_type(NotificationType::FailedSecretVendorUpdateSync); + // show connect tab // this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); // this->main_frame->on_account_login(user_account->get_access_token()); + + // // notify other instances + // Slic3r::GUI::wxGetApp().other_instance_message_handler()->multicast_message("STORE_READ"); // } else { // // refresh do different operations than on_account_login // this->main_frame->on_account_did_refresh(user_account->get_access_token()); @@ -985,9 +998,16 @@ void Plater::priv::init() // this->main_frame->refresh_account_menu(true); // // Update sidebar printer status // sidebar->update_printer_presets_combobox(); - // } - + // } + // }; + + // this->q->Bind(EVT_UA_ID_USER_SUCCESS, [on_id_user_success](UserAccountSuccessEvent& evt) { + // on_id_user_success(evt, false); // }); + // this->q->Bind(EVT_UA_ID_USER_SUCCESS_AFTER_TOKEN_SUCCESS, [on_id_user_success](UserAccountSuccessEvent& evt) { + // on_id_user_success(evt, true); + // }); + // this->q->Bind(EVT_UA_RESET, [this](UserAccountFailEvent& evt) { // BOOST_LOG_TRIVIAL(error) << "Reseting QIDI Account communication. Error message: " << evt.data; // user_account->clear(); @@ -1003,6 +1023,11 @@ void Plater::priv::init() // BOOST_LOG_TRIVIAL(error) << "Failed communication with Connect Printer endpoint: " << evt.data; // user_account->on_communication_fail(); // }); + // this->q->Bind(EVT_UA_RACE_LOST, [this](UserAccountFailEvent& evt) { + // BOOST_LOG_TRIVIAL(debug) << "Renew token race lost: " << evt.data; + // user_account->on_race_lost(); + // }); + // this->q->Bind(EVT_UA_QIDICONNECT_STATUS_SUCCESS, [this](UserAccountSuccessEvent& evt) { // std::string text; // bool printers_changed = false; @@ -1066,7 +1091,25 @@ void Plater::priv::init() return; } this->q->printables_to_connect_gcode(into_u8(evt.GetString())); - }); + }); + + this->q->Bind(EVT_UA_RETRY_NOTIFY, [this](UserAccountFailEvent& evt) { + this->notification_manager->close_notification_of_type(NotificationType::AccountTransientRetry); + this->notification_manager->push_notification(NotificationType::AccountTransientRetry + , NotificationManager::NotificationLevel::RegularNotificationLevel + , evt.data + // TRN: This is a hyperlink in a notification. It is preceded by a message from QIDIAccount (therefore not in this dictionary) + // saying something like "connection not established, I will keep trying". + , _u8L("Stop now.") + , [this](wxEvtHandler* ) { + this->user_account->do_logout(); + return true; + }); + + }); + this->q->Bind(EVT_UA_CLOSE_RETRY_NOTIFICATION, [this](SimpleEvent& evt) { + this->notification_manager->close_notification_of_type(NotificationType::AccountTransientRetry); + }); } wxGetApp().other_instance_message_handler()->init(this->q); @@ -1202,7 +1245,7 @@ void Plater::notify_about_installed_presets() std::vector Plater::priv::load_files(const std::vector& input_files, bool load_model, bool load_config, bool imperial_units/* = false*/) { - if (input_files.empty()) { return std::vector(); } + if (input_files.empty()) { return std::vector(); } auto *nozzle_dmrs = config->opt("nozzle_diameter"); @@ -1233,11 +1276,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ 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(); + auto *extra_model = (!load_model || one_by_one) ? nullptr : new Slic3r::Model(); std::vector obj_idxs; boost::optional qidislicer_generator_version; @@ -1266,9 +1309,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } 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_qidi = std::regex_match(path.string(), pattern_qidi); const bool type_printRequest = std::regex_match(path.string(), pattern_printRequest); if (type_printRequest && printer_technology != ptSLA) { @@ -1279,108 +1320,37 @@ std::vector Plater::priv::load_files(const std::vector& input_ } Slic3r::Model model; - bool is_project_file = type_qidi; - try { - if (type_3mf || type_zip_amf) { + bool is_project_file = false; + #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; - } + // 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, - qidislicer_generator_version, - 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); + DynamicPrintConfig config; + PrinterTechnology loaded_printer_technology{ ptFFF }; - // 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; - } - } + DynamicPrintConfig config_loaded; + ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::Enable }; - config.apply(loaded_printer_technology == ptFFF ? - static_cast(FullPrintConfig::defaults()) : - static_cast(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()); + if (!type_3mf) + load_config = false; // just 3mf file contains config - if (load_config) { - this->model.get_custom_gcode_per_print_z_vector() = model.get_custom_gcode_per_print_z_vector(); - this->model.get_wipe_tower_vector() = model.get_wipe_tower_vector(); - } - } + FileReader::LoadStats load_stats; - if (load_config) { - if (!config.empty()) { - const auto* post_process = config.opt("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()); - } + try { + if (load_config) { + model = FileReader::load_model_with_config(path.string(), &config_loaded, &config_substitutions, qidislicer_generator_version, FileReader::LoadAttribute::CheckVersion, &load_stats); } - 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(); + else if (load_model) { + model = FileReader::load_model(path.string(), FileReader::LoadAttributes{}, &load_stats); } } 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(); @@ -1391,33 +1361,96 @@ std::vector Plater::priv::load_files(const std::vector& input_ continue; } - if (load_model) { - // The model should now be initialized + if (load_config) { - 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 (!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); + + config.apply(loaded_printer_technology == ptFFF ? + static_cast(FullPrintConfig::defaults()) : + static_cast(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); + } else { // config_loaded.empty() + // Detection of possible breaking change in 3MF configuration loading sometimes in future. + if (qidislicer_generator_version && // set only when loaded with configuration + *qidislicer_generator_version > Semver(SLIC3R_VERSION)) { + wxString title = _L("Configuration was not loaded"); + const wxString url = "https://qidi.io/3mf-transfer"; + // TRN: %1% is filename of the project, %2% is url link. + wxString message = format_wxstr(_L("Unable to load configuration from project\n\nFile: %1%\n\n" + "This project was created in a newer version of QIDISlicer. Only the geometry was loaded.\n" + "Update to the latest version for full compatibility.\nFor more info: %2%"), + from_path(filename), url); + HtmlCapableRichMessageDialog dialog(q, message ,title, wxOK, + [&url](const std::string&) { wxGetApp().open_browser_with_warning_dialog(url); }); + dialog.ShowModal(); + } + } + if (!config_substitutions.empty()) + show_substitutions_info(config_substitutions.substitutions, filename.string()); + + if (!config.empty()) { + const auto* post_process = config.opt("post_process"); + if (post_process != nullptr && !post_process->values.empty()) { + wxString msg = _L("The selected 3MF file contains a post-processing script.\n" + "Please review the script carefully before exporting G-code."); + 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 && load_model) + // CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z(), &preset_bundle->project_config); + + // For exporting from the 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; + } + + this->model.get_custom_gcode_per_print_z_vector() = model.get_custom_gcode_per_print_z_vector(); + this->model.get_wipe_tower_vector() = model.get_wipe_tower_vector(); + + if (!in_temp) + wxGetApp().app_config->update_config_dir(path.parent_path().string()); + } + + if (load_model) { if (!is_project_file) { - if (int deleted_objects = model.removed_objects_with_zero_volume(); deleted_objects > 0) { + + if (load_stats.deleted_objects_cnt > 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", + "These objects have been removed from the model", load_stats.deleted_objects_cnt), from_path(filename)) + "\n", _L("The size of the object is zero"), wxICON_INFORMATION | wxOK).ShowModal(); } - if (imperial_units) + + 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); - }; + // It's a case, when we load model from STL, saved in imperial units + ModelProcessing::convert_from_imperial_units(model, false); + } + else if (load_stats.looks_like_saved_in_meters) { + int answer = answer_convert_from_meters; 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" @@ -1426,20 +1459,16 @@ std::vector Plater::priv::load_files(const std::vector& input_ "The internal unit of QIDISlicer 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(); + 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); + if (answer == wxID_YES) + //FIXME up-scale only the small parts? + ModelProcessing::convert_from_meters(model, true); } - 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); - }; + else if (load_stats.looks_like_imperial_units) { + int answer = answer_convert_from_imperial_units; 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" @@ -1448,16 +1477,17 @@ std::vector Plater::priv::load_files(const std::vector& input_ "The internal unit of QIDISlicer 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(); + 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 (answer == wxID_YES) + //FIXME up-scale only the small parts? + ModelProcessing::convert_from_imperial_units(model, true); } - if (!type_printRequest && model.looks_like_multipart_object()) { + if (load_stats.looks_like_multipart_object) { + int answer = answer_consider_as_multi_part_objects; if (answer_consider_as_multi_part_objects == wxOK_DEFAULT) { RichMessageDialog dlg(q, _L( "This file contains several objects positioned at multiple heights.\n" @@ -1465,17 +1495,16 @@ std::vector Plater::priv::load_files(const std::vector& input_ "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(); + 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 (/*!type_printRequest && */answer == wxID_YES) //! type_printRequest is no need here, SLA allow multipart object now + ModelProcessing::convert_to_multipart_object(model, nozzle_dmrs->size()); } } - if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) { + + if ((wxGetApp().get_mode() == comSimple) && type_3mf && 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) { @@ -1483,11 +1512,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ view3D->set_as_dirty(); } else - return obj_idxs; + continue;// return obj_idxs; } for (ModelObject* model_object : model.objects) { - if (!type_3mf && !type_zip_amf) { + if (!type_3mf) { model_object->center_around_origin(false); if (type_any_amf && model_object->instances.empty()) { ModelInstance* instance = model_object->add_instance(); @@ -1502,12 +1531,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } } - if (type_printRequest) { + + if (type_printRequest ) { assert(model.materials.size()); for (const auto& material : model.materials) { - std::string preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias_invisible(Preset::Type::TYPE_SLA_MATERIAL, - Preset::remove_suffix_modified(material.first)); + std::string preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias_invisible(Preset::Type::TYPE_SLA_MATERIAL, material.first); Preset* prst = wxGetApp().preset_bundle->sla_materials.find_preset(preset_name, false); if (!prst) { //did not find compatible profile // try find alias of material comaptible with another print profile - if exists, use the print profile @@ -1519,8 +1548,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (it->name != edited_print_name) { BOOST_LOG_TRIVIAL(error) << it->name; wxGetApp().get_tab(Preset::Type::TYPE_SLA_PRINT)->select_preset(it->name, false); - preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias_invisible(Preset::Type::TYPE_SLA_MATERIAL, - Preset::remove_suffix_modified(material.first)); + preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias_invisible(Preset::Type::TYPE_SLA_MATERIAL, material.first); prst = wxGetApp().preset_bundle->sla_materials.find_preset(preset_name, false); if (prst) { found = true; @@ -1550,14 +1578,14 @@ std::vector Plater::priv::load_files(const std::vector& input_ } if (one_by_one) { - if ((type_3mf && !is_project_file) || (type_any_amf && !type_zip_amf)) + if ((type_3mf && !is_project_file) || type_any_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); + extra_model->add_object(*model_object); } } @@ -1566,18 +1594,17 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } - if (new_model != nullptr && new_model->objects.size() > 1) { - //wxMessageDialog msg_dlg(q, _L( + if (extra_model != nullptr && extra_model->objects.size() > 1) { 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()); + ModelProcessing::convert_to_multipart_object(*extra_model, nozzle_dmrs->values.size()); } - auto loaded_idxs = load_model_objects(new_model->objects); + auto loaded_idxs = load_model_objects(extra_model->objects); obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); } @@ -1616,7 +1643,6 @@ std::vector Plater::priv::load_files(const std::vector& input_ s_multiple_beds.ensure_wipe_towers_on_beds(model, fff_prints); s_multiple_beds.update_shown_beds(model, q->build_volume()); - s_print_statuses.fill(PrintStatus::idle); update((unsigned int)UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE); return obj_idxs; @@ -1888,9 +1914,14 @@ void Plater::priv::object_list_changed() // if (printer_technology == ptFFF) { for (std::size_t bed_index{}; bed_index < s_multiple_beds.get_number_of_beds(); ++bed_index) { - if ( - wxGetApp().plater()->get_fff_prints()[bed_index]->empty()) { + if ( wxGetApp().plater()->get_fff_prints()[bed_index]->empty()) { s_print_statuses[bed_index] = PrintStatus::empty; + } else if ( + wxGetApp().plater()->get_fff_prints()[bed_index]->finished() + && is_sliceable(s_print_statuses[bed_index]) + && s_print_statuses[bed_index] != PrintStatus::toolpath_outside + ) { + s_print_statuses[bed_index] = PrintStatus::finished; } for (const ModelObject *object : wxGetApp().model().objects) { for (const ModelInstance *instance : object->instances) { @@ -1989,7 +2020,6 @@ void Plater::priv::delete_all_objects_from_model() reset_gcode_toolpaths(); std::for_each(gcode_results.begin(), gcode_results.end(), [](auto& g) { g.reset(); }); - view3D->get_canvas3d()->reset_sequential_print_clearance(); view3D->get_canvas3d()->reset_all_gizmos(); m_worker.cancel_all(); @@ -2023,8 +2053,6 @@ void Plater::priv::reset() reset_gcode_toolpaths(); std::for_each(gcode_results.begin(), gcode_results.end(), [](auto& g) { g.reset(); }); - view3D->get_canvas3d()->reset_sequential_print_clearance(); - m_worker.cancel_all(); // Stop and reset the Print content. @@ -2064,7 +2092,7 @@ void Plater::priv::split_object() wxBusyCursor wait; ModelObjectPtrs new_objects; - current_model_object->split(&new_objects); + ModelProcessing::split(current_model_object, &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.")); @@ -2156,9 +2184,20 @@ void Plater::priv::process_validation_warning(const std::vector& wa print_tab->on_value_change("support_material_auto", config.opt_bool("support_material_auto")); return true; }; - } else if (text == "_BED_TEMPS_DIFFER") { - text = _u8L("Bed temperatures for the used filaments differ significantly."); + } else if (text == "_BED_TEMPS_DIFFER" || text == "_BED_TEMPS_CHANGED") { + // TRN: Text of a notification, followed by (single) hyperlink to two of the config options. The sentence had to be split because of the hyperlink, sorry. + // The hyperlink part of the sentence reads "'Bed temperature by extruder' and 'Wipe tower extruder'", and it is also to be translated. + text = _u8L("Bed temperatures for the used filaments differ significantly.\n" + "For multi-material prints it is recommended to set the "); + // TRN: The other part of the sentence starting "Bed temperatures for the used" (also in the dictionary). Sorry for splitting it, technical reasons - + // this part of the sentence is a hyperlink. + hypertext = _u8L("'Bed temperature by extruder' and 'Wipe tower extruder'"); + multiline = true; notification_type = NotificationType::BedTemperaturesDiffer; + action_fn = [](wxEvtHandler*) { + GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINT)->activate_option("bed_temperature_extruder", boost::nowide::widen("Multiple Extruders"), { "wipe_tower_extruder" }); + return true; + }; } else if (text == "_FILAMENT_SHRINKAGE_DIFFER") { text = _u8L("Filament shrinkage will not be used because filament shrinkage " "for the used filaments differs significantly."); @@ -2321,15 +2360,16 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool std::vector(1) }; - // Apply new config to the possibly running background task. + std::vector warnings; + // Apply new config to the possibly running background task and give the user feedback on warnings. if (printer_technology == ptFFF) { with_single_bed_model_fff(q->model(), s_multiple_beds.get_active_bed(), [&](){ - invalidated = background_process.apply(q->model(), full_config); + invalidated = background_process.apply(q->model(), full_config, &warnings); apply_statuses[s_multiple_beds.get_active_bed()] = invalidated; }); } else if (printer_technology == ptSLA) { with_single_bed_model_sla(q->model(), s_multiple_beds.get_active_bed(), [&](){ - invalidated = background_process.apply(q->model(), full_config); + invalidated = background_process.apply(q->model(), full_config, &warnings); apply_statuses[0] = invalidated; }); } else { @@ -2385,9 +2425,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool 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. @@ -2412,7 +2449,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // 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 warnings; std::string err = background_process.validate(&warnings); if (err.empty()) { notification_manager->set_all_slicing_errors_gray(true); @@ -2425,7 +2461,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool 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(); } @@ -2435,41 +2470,14 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // 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 warnings; std::string err = background_process.validate(&warnings); - if (!err.empty()) { - if (s_multiple_beds.get_number_of_beds() > 1 && printer_technology == ptFFF) { - // user changed bed seletion, - // sequential print clearance contours were changed too - GLCanvas3D* canvas = view3D->get_canvas3d(); - GLCanvas3D::ContoursList contours; - contours.contours = background_process.fff_print()->get_sequential_print_clearance_contours(); - canvas->set_sequential_print_clearance_contours(contours, true); - } + if (!err.empty()) return return_state; - } } if (! this->delayed_error_message.empty()) @@ -2657,7 +2665,7 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const Model new_model; try { - new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances); + new_model = FileReader::load_model(path); for (ModelObject* model_object : new_model.objects) { model_object->center_around_origin(); model_object->ensure_on_bed(); @@ -2695,9 +2703,9 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const 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(); + ModelProcessing::convert_from_imperial_units(new_volume); else if (old_volume->source.is_converted_from_meters) - new_volume->convert_from_meters(); + ModelProcessing::convert_from_meters(new_volume); if (old_volume->mesh().its == new_volume->mesh().its) { // This function is called both from reload_from_disk and replace_with_stl. @@ -2907,7 +2915,7 @@ void Plater::priv::reload_from_disk() Model new_model; try { - new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances); + new_model = FileReader::load_model(path); for (ModelObject* model_object : new_model.objects) { model_object->center_around_origin(); model_object->ensure_on_bed(); @@ -2990,9 +2998,9 @@ void Plater::priv::reload_from_disk() 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(); + ModelProcessing::convert_from_imperial_units(new_volume); else if (old_volume->source.is_converted_from_meters) - new_volume->convert_from_meters(); + ModelProcessing::convert_from_meters(new_volume); 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) @@ -3920,14 +3928,14 @@ bool Plater::priv::can_fix_through_winsdk() const // 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) + if (get_repaired_errors_count(model.objects[obj_idx]) > 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) + if (get_repaired_errors_count(model.objects[obj_idx], vol_idx) > 0) return true; return false; #endif // FIX_THROUGH_WINSDK_ALWAYS @@ -4453,7 +4461,8 @@ void Plater::load_project(const wxString& filename) s_multiple_beds.set_loading_project_flag(true); ScopeGuard guard([](){ s_multiple_beds.set_loading_project_flag(false);}); - if (! load_files({ into_path(filename) }).empty()) { + const std::vector& input_paths = { into_path(filename) }; + if (! load_files(input_paths).empty()) { // At least one file was loaded. p->set_project_filename(filename); // Save the names of active presets and project specific config into ProjectDirtyStateManager. @@ -5621,7 +5630,8 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) break; } // if 3mf - read archive headers to find project file - if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(final_path.string())) || + auto [is_project, ps_version] = is_project_3mf(final_path.string()); + if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project) || (boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) { non_project_paths.emplace_back(final_path); break; @@ -5661,86 +5671,6 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) { 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(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 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) { @@ -5767,7 +5697,6 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) // 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) { @@ -5921,18 +5850,20 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* for (std::vector::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())) { + bool handle_as_project = boost::algorithm::iends_with(filename, ".3mf"); + auto [has_config, ps_version] = is_project_3mf(it->string()); + + if (boost::algorithm::iends_with(filename, ".zip") && has_config) { 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 && load_just_one_file) { 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"))) + if (boost::algorithm::iends_with(filename, ".3mf") && (!has_config && !ps_version.has_value())) { + // Projects generated by QIDISlicer is expected to have config. If absent, it will be reported later. load_type = ProjectDropDialog::LoadType::LoadGeometry; - else { + } else { if (wxGetApp().app_config->get_bool("show_drop_project_dialog")) { ProjectDropDialog dlg(filename); if (dlg.ShowModal() == wxID_OK) { @@ -5979,7 +5910,7 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* return true; } else if (boost::algorithm::iends_with(filename, ".zip")) { if (!load_just_one_file) { - WarningDialog dlg(static_cast(this), + WarningDialog dlg(static_cast(this), format_wxstr(_L("You have several files for loading and \"%1%\" is one of them.\n" "Please note that only one .zip file can be loaded at a time.\n" "In this case we can load just \"%1%\".\n\n" @@ -6293,19 +6224,23 @@ void Plater::convert_unit(ConversionType conv_type) 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); + { + Model tmp_model; + for (int obj_idx : obj_idxs) { + //ModelObject *object = p->model.objects[obj_idx]; + //object->convert_units(objects, conv_type, volume_idxs); + ModelProcessing::convert_units(tmp_model, p->model.objects[obj_idx], conv_type, volume_idxs); + remove(obj_idx); + } + p->load_model_objects(tmp_model.objects); } - 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) + for (size_t i = 0; i < obj_idxs.size(); ++i) selection.add_object((unsigned int)(last_obj_idx - i), i == 0); } else { @@ -6394,50 +6329,6 @@ static wxString check_binary_vs_ascii_gcode_extension(PrinterTechnology pt, cons 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. -void alert_when_exporting_binary_gcode(const std::string& printer_notes) -{ - const bool supports_binary = wxGetApp() - .preset_bundle->printers - .get_edited_preset() - .config.opt_bool("binary_gcode"); - const bool uses_binary = wxGetApp().app_config->get_bool("use_binary_gcode_when_supported"); - const bool binary_output{supports_binary && uses_binary}; - - 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://qidi.io/binary-gcode"; - HtmlCapableRichMessageDialog dialog(parent, - format_wxstr(_L("You are exporting binary G-code for a QIDI 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 %1%."), - 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"); - } - } -} - std::optional Plater::get_default_output_file() { try { // Update the background processing, so that the placeholder parser will get the correct values for the ouput file template. @@ -6571,14 +6462,6 @@ void Plater::export_gcode(bool prefer_removable) } const fs::path &output_path{*optional_output_path}; - if (printer_technology() == ptFFF) { - alert_when_exporting_binary_gcode( - wxGetApp() - .preset_bundle->printers - .get_edited_preset() - .config.opt_string("printer_notes") - ); - } export_gcode_to_path(output_path, [&](const bool path_on_removable_media){ p->export_gcode(output_path, path_on_removable_media, PrintHostJob()); }); @@ -6687,7 +6570,7 @@ void Plater::export_all_gcodes(bool prefer_removable) { paths.emplace_back(print_index, output_file); } - BulkExportDialog dialog{paths}; + BulkExportDialog dialog{paths, _L("Export beds"), "<>[]:/\\|?*\""}; if (dialog.ShowModal() != wxID_OK) { return; } @@ -7242,11 +7125,12 @@ void Plater::printables_to_connect_gcode(const std::string& url) } -std::optional Plater::get_connect_print_host_job() { +std::optional Plater::get_connect_print_host_job(bool multiple_beds) +{ assert(p->user_account->is_logged()); std::string dialog_msg; { - PrinterPickWebViewDialog dialog(this, dialog_msg); + PrinterPickWebViewDialog dialog(this, dialog_msg, multiple_beds); if (dialog.ShowModal() != wxID_OK) { return std::nullopt; } @@ -7300,13 +7184,13 @@ std::optional Plater::get_connect_print_host_job() { void Plater::connect_gcode() { - if (auto upload_job{get_connect_print_host_job()}) { + if (auto upload_job{get_connect_print_host_job(false)}) { p->export_gcode(fs::path(), false, std::move(*upload_job)); } } void Plater::connect_gcode_all() { - auto optional_upload_job{get_connect_print_host_job()}; + auto optional_upload_job{get_connect_print_host_job(true)}; if (!optional_upload_job) { return; } @@ -7318,6 +7202,7 @@ void Plater::connect_gcode_all() { if (print_host_ptr == nullptr) { throw std::runtime_error{"Sending to connect requires QIDIConnectNew host."}; } + const QIDIConnectNew connect{*print_host_ptr}; std::vector >> paths; @@ -7328,16 +7213,34 @@ void Plater::connect_gcode_all() { paths.emplace_back(print_index, std::nullopt); continue; } - - const fs::path filename{upload_job_template.upload_data.upload_path}; - paths.emplace_back(print_index, fs::path{ - filename.stem().string() - + "_bed" + std::to_string(print_index + 1) - + filename.extension().string() - }); + // Prevously, filename from Connect FE was taken and used for each gcode file. + // Now naming is same as in gcode export. + fs::path default_filename{upload_job_template.upload_data.upload_path}; + this->with_mocked_fff_background_process( + *print, + this->p->gcode_results[print_index], + print_index, + [&](){ + const auto optional_file{this->get_default_output_file()}; + if (!optional_file) { + return; + } + if (print_index != s_multiple_beds.get_number_of_beds() - 1 || default_filename.empty()) { + const fs::path &default_file{*optional_file}; + default_filename = default_file.filename(); + } + } + ); + const fs::path filename_fixed{ + default_filename.stem().string() + + "_bed" + + std::to_string(print_index + 1) + + default_filename.extension().string() + }; + paths.emplace_back(print_index, filename_fixed); } - BulkExportDialog dialog{paths}; + BulkExportDialog dialog{paths, _L("Send all to Connect"), connect.get_unusable_symbols()}; if (dialog.ShowModal() != wxID_OK) { return; } @@ -7476,13 +7379,6 @@ void Plater::send_gcode() .ShowModal(); return; } - - alert_when_exporting_binary_gcode( - wxGetApp(). - preset_bundle->printers. - get_edited_preset(). - config.opt_string("printer_notes") - ); } //B53 //B62 //B64 @@ -7587,7 +7483,7 @@ void Plater::send_gcode() } #endif //y16 - bool is_switch_to_device = wxGetApp().app_config->get("switch to device tab after upload") == "1" ? true : false; + bool is_switch_to_device = wxGetApp().app_config->get("switch_to_device_tab_after_upload") == "1" ? true : false; if (is_switch_to_device) wxGetApp().mainframe->select_tab(size_t(4)); } @@ -7696,8 +7592,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) const PrinterTechnology printer_technology = config.opt_enum(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->reset_gcode_toolpaths(); p->view3D->get_canvas3d()->set_sla_view_type(GLCanvas3D::ESLAViewType::Original); p->preview->get_canvas3d()->reset_volumes(); } @@ -7958,30 +7853,29 @@ static std::string concat_strings(const std::set &strings, }); } -void Plater::arrange() +void Plater::arrange(bool current_bed_only) { - const auto mode{ - wxGetKeyState(WXK_SHIFT) ? - ArrangeSelectionMode::SelectionOnly : - ArrangeSelectionMode::Full - }; + ArrangeSelectionMode mode; + if (current_bed_only) + mode = wxGetKeyState(WXK_SHIFT) ? ArrangeSelectionMode::CurrentBedSelectionOnly : ArrangeSelectionMode::CurrentBedFull; + else + mode = wxGetKeyState(WXK_SHIFT) ? ArrangeSelectionMode::SelectionOnly : ArrangeSelectionMode::Full; + + const bool sequential = p->config->has("complete_objects") && p->config->opt_bool("complete_objects") && p->printer_technology == ptFFF; if (p->can_arrange()) { - auto &w = get_ui_job_worker(); - arrange(w, mode); - } -} - -void Plater::arrange_current_bed() -{ - const auto mode{ - wxGetKeyState(WXK_SHIFT) ? - ArrangeSelectionMode::CurrentBedSelectionOnly : - ArrangeSelectionMode::CurrentBedFull - }; - if (p->can_arrange()) { - auto &w = get_ui_job_worker(); - arrange(w, mode); + if (sequential) { + try { + replace_job(this->get_ui_job_worker(), std::make_unique(this->model(), *p->config, current_bed_only)); + } catch (const ExceptionCannotAttemptSeqArrange&) { + ErrorDialog dlg(this, _L("Sequential arrange for a single bed is only allowed when all instances of the affected objects are on the same bed."), false); + dlg.ShowModal(); + } + } + else { + auto& w = get_ui_job_worker(); + arrange(w, mode); + } } } @@ -8569,7 +8463,7 @@ PlaterAfterLoadAutoArrange::PlaterAfterLoadAutoArrange() PlaterAfterLoadAutoArrange::~PlaterAfterLoadAutoArrange() { if (m_enabled) - wxGetApp().plater()->arrange(); + wxGetApp().plater()->arrange(false); } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 055920a..041b16b 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -259,7 +259,7 @@ public: // void send_gcode_inner(DynamicPrintConfig* physical_printer_config); void eject_drive(); - std::optional get_connect_print_host_job(); + std::optional get_connect_print_host_job(bool multiple_beds); void connect_gcode(); void connect_gcode_all(); void printables_to_connect_gcode(const std::string& url); @@ -315,8 +315,7 @@ public: void render_sliders(GLCanvas3D& canvas); - void arrange(); - void arrange_current_bed(); + void arrange(bool current_bed_only); void arrange(Worker &w, const ArrangeSelectionMode &selected); void set_current_canvas_as_dirty(); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 2da3e01..b24adb3 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -138,6 +138,7 @@ void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::strin ,"default_action_on_select_preset" }) m_optgroup_general->set_value(opt_key, app_config->get(opt_key) == "none"); m_optgroup_general->set_value("default_action_on_dirty_project", app_config->get("default_action_on_dirty_project").empty()); + m_optgroup_gui->set_value("seq_top_layer_only", app_config->get_bool("seq_top_layer_only")); // update colors for color pickers of the labels update_color(m_sys_colour, wxGetApp().get_label_clr_sys()); diff --git a/src/slic3r/GUI/PresetArchiveDatabase.cpp b/src/slic3r/GUI/PresetArchiveDatabase.cpp index 26cb499..128b52b 100644 --- a/src/slic3r/GUI/PresetArchiveDatabase.cpp +++ b/src/slic3r/GUI/PresetArchiveDatabase.cpp @@ -748,6 +748,7 @@ void PresetArchiveDatabase::read_server_manifest(const std::string& json_body) assert(it != m_has_installed_printer_repositories_uuid.end()); if (it->second) { ArchiveRepository::RepositoryManifest manifest(repo_ptr->get_manifest()); + manifest.not_in_manifest = true; secret_online_used_repos_cache.emplace_back(std::make_unique(repo_ptr->get_uuid(), std::move(manifest))); } } @@ -809,8 +810,7 @@ SharedArchiveRepositoryVector PresetArchiveDatabase::get_selected_archive_reposi { SharedArchiveRepositoryVector result; result.reserve(m_archive_repositories.size()); - for (const auto &repo_ptr : m_archive_repositories) - { + for (const auto &repo_ptr : m_archive_repositories) { auto it = m_selected_repositories_uuid.find(repo_ptr->get_uuid()); assert(it != m_selected_repositories_uuid.end()); if (it->second) { diff --git a/src/slic3r/GUI/PresetArchiveDatabase.hpp b/src/slic3r/GUI/PresetArchiveDatabase.hpp index 359df8f..ec3108b 100644 --- a/src/slic3r/GUI/PresetArchiveDatabase.hpp +++ b/src/slic3r/GUI/PresetArchiveDatabase.hpp @@ -37,6 +37,7 @@ public: // not read from manifest json boost::filesystem::path tmp_path; // Where archive is unzziped. Created each app run. boost::filesystem::path source_path; // Path given by user. Stored between app runs. + bool not_in_manifest {false}; RepositoryManifest() = default; RepositoryManifest( @@ -47,7 +48,8 @@ public: const std::string &description = "", const std::string &visibility = "", const boost::filesystem::path &tmp_path = "", - const boost::filesystem::path &source_path = "" + const boost::filesystem::path &source_path = "", + bool not_in_manifest = false ) : id(id) , name(name) @@ -57,6 +59,7 @@ public: , visibility(visibility) , tmp_path(tmp_path) , source_path(source_path) + , not_in_manifest(not_in_manifest) {} RepositoryManifest(const RepositoryManifest &other) : id(other.id) @@ -66,7 +69,8 @@ public: , description(other.description) , visibility(other.visibility) , tmp_path(other.tmp_path) - , source_path(other.source_path) + , source_path(other.source_path) + , not_in_manifest(other.not_in_manifest) {} }; // Use std::move when calling constructor. diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 1e9f323..cd9de5e 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -990,39 +990,18 @@ static std::string get_connect_state_suffix_for_printer(const Preset& printer_pr if (auto printer_state_map = wxGetApp().plater()->get_user_account()->get_printer_state_map(); !printer_state_map.empty()) { - for (const auto& [printer_model_nozzle_pair, states] : printer_state_map) { - std::string printer_model = printer_preset.config.opt_string("printer_model"); - const PresetWithVendorProfile& printer_with_vendor = wxGetApp().preset_bundle->printers.get_preset_with_vendor_profile(printer_preset); - printer_model = printer_preset.trim_vendor_repo_prefix(printer_model, printer_with_vendor.vendor); - - - if (printer_preset.config.has("nozzle_diameter")) { - double nozzle_diameter = static_cast(printer_preset.config.option("nozzle_diameter"))->values[0]; - wxString nozzle_diameter_serialized = double_to_string(nozzle_diameter); - nozzle_diameter_serialized.Replace(L",", L"."); - - if (printer_model_nozzle_pair.first == printer_model - && printer_model_nozzle_pair.second == GUI::into_u8(nozzle_diameter_serialized)) - { - PrinterStatesCount states_cnt = get_printe_states_count(states); - - if (states_cnt.available_cnt > 0) - return "_available"; - if (states_cnt.busy_cnt > 0) - return "_busy"; - return "_offline"; - } - } else { - if (printer_model_nozzle_pair.first == printer_model) { - PrinterStatesCount states_cnt = get_printe_states_count(states); - - if (states_cnt.available_cnt > 0) - return "_available"; - if (states_cnt.busy_cnt > 0) - return "_busy"; - return "_offline"; - } + const PresetWithVendorProfile& printer_with_vendor = wxGetApp().preset_bundle->printers.get_preset_with_vendor_profile(printer_preset); + const std::string trimmed_preset_name = printer_preset.trim_vendor_repo_prefix(printer_preset.name, printer_with_vendor.vendor); + for (const auto& [preset_name_from_map, states] : printer_state_map) { + if (trimmed_preset_name != preset_name_from_map) { + continue; } + PrinterStatesCount states_cnt = get_printe_states_count(states); + if (states_cnt.available_cnt > 0) + return "_available"; + if (states_cnt.busy_cnt > 0) + return "_busy"; + return "_offline"; } } @@ -1043,47 +1022,24 @@ static bool fill_data_to_connect_info_line( const Preset& printer_preset, if (auto printer_state_map = wxGetApp().plater()->get_user_account()->get_printer_state_map(); !printer_state_map.empty()) { - for (const auto& [printer_model_nozzle_pair, states] : printer_state_map) { - // get printer_model without repo prefix - std::string printer_model = printer_preset.config.opt_string("printer_model"); - const PresetWithVendorProfile& printer_with_vendor = wxGetApp().preset_bundle->printers.get_preset_with_vendor_profile(printer_preset); - printer_model = printer_preset.trim_vendor_repo_prefix(printer_model, printer_with_vendor.vendor); - - if (printer_preset.config.has("nozzle_diameter")) { - double nozzle_diameter = static_cast(printer_preset.config.option("nozzle_diameter"))->values[0]; - wxString nozzle_diameter_serialized = double_to_string(nozzle_diameter); - nozzle_diameter_serialized.Replace(L",", L"."); - - if (printer_model_nozzle_pair.first == printer_model - && printer_model_nozzle_pair.second == GUI::into_u8(nozzle_diameter_serialized)) - { - PrinterStatesCount states_cnt = get_printe_states_count(states); -#ifdef _WIN32 - connect_available_info->SetLabelMarkup(format_wxstr("%1% %2%", format("%1%", states_cnt.available_cnt), _L("available"))); - connect_offline_info ->SetLabelMarkup(format_wxstr("%1% %2%", format("%1%", states_cnt.offline_cnt), _L("offline"))); - connect_printing_info ->SetLabelMarkup(format_wxstr("%1% %2%", format("%1%", states_cnt.busy_cnt), _L("printing"))); -#else - connect_available_info->SetLabel(format_wxstr("%1% ", states_cnt.available_cnt)); - connect_offline_info ->SetLabel(format_wxstr("%1% ", states_cnt.offline_cnt)); - connect_printing_info ->SetLabel(format_wxstr("%1% ", states_cnt.busy_cnt)); -#endif - return true; - } - } else { - if (printer_model_nozzle_pair.first == printer_model) { - PrinterStatesCount states_cnt = get_printe_states_count(states); -#ifdef _WIN32 - connect_available_info->SetLabelMarkup(format_wxstr("%1% %2%", format("%1%", states_cnt.available_cnt), _L("available"))); - connect_offline_info ->SetLabelMarkup(format_wxstr("%1% %2%", format("%1%", states_cnt.offline_cnt), _L("offline"))); - connect_printing_info ->SetLabelMarkup(format_wxstr("%1% %2%", format("%1%", states_cnt.busy_cnt), _L("printing"))); -#else - connect_available_info->SetLabel(format_wxstr("%1% ", states_cnt.available_cnt)); - connect_offline_info ->SetLabel(format_wxstr("%1% ", states_cnt.offline_cnt)); - connect_printing_info ->SetLabel(format_wxstr("%1% ", states_cnt.busy_cnt)); -#endif - return true; - } + const PresetWithVendorProfile& printer_with_vendor = wxGetApp().preset_bundle->printers.get_preset_with_vendor_profile(printer_preset); + const std::string trimmed_preset_name = printer_preset.trim_vendor_repo_prefix(printer_preset.name, printer_with_vendor.vendor); + for (const auto& [preset_name_from_map, states] : printer_state_map) { + if (trimmed_preset_name != preset_name_from_map) { + continue; } + + PrinterStatesCount states_cnt = get_printe_states_count(states); +#ifdef _WIN32 + connect_available_info->SetLabelMarkup(format_wxstr("%1% %2%", format("%1%", states_cnt.available_cnt), _L("available"))); + connect_offline_info ->SetLabelMarkup(format_wxstr("%1% %2%", format("%1%", states_cnt.offline_cnt), _L("offline"))); + connect_printing_info ->SetLabelMarkup(format_wxstr("%1% %2%", format("%1%", states_cnt.busy_cnt), _L("printing"))); +#else + connect_available_info->SetLabel(format_wxstr("%1% ", states_cnt.available_cnt)); + connect_offline_info ->SetLabel(format_wxstr("%1% ", states_cnt.offline_cnt)); + connect_printing_info ->SetLabel(format_wxstr("%1% ", states_cnt.busy_cnt)); +#endif + return true; } } return false; diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index 81699af..aca1bf6 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -138,14 +138,14 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo //y16 m_switch_to_device = new wxCheckBox(this, wxID_ANY, _L("Switch to Device tab"), wxDefaultPosition); - m_switch_to_device->SetValue((wxGetApp().app_config->get("switch to device tab after upload") == "1") ? true : false); + m_switch_to_device->SetValue((wxGetApp().app_config->get("switch_to_device_tab_after_upload") == "1") ? true : false); wxToolTip* switch_tips = new wxToolTip(_L("Switch to Device tab after upload.")); m_switch_to_device->SetToolTip(switch_tips); m_switch_to_device->Bind(wxEVT_COMMAND_CHECKBOX_CLICKED, [this](wxCommandEvent &event){ if(event.GetInt() == 1) - wxGetApp().app_config->set("switch to device tab after upload", "1"); + wxGetApp().app_config->set("switch_to_device_tab_after_upload", "1"); else - wxGetApp().app_config->set("switch to device tab after upload", "0"); + wxGetApp().app_config->set("switch_to_device_tab_after_upload", "0"); }); wxBoxSizer *max_printer_send = diff --git a/src/slic3r/GUI/PrinterWebView.cpp b/src/slic3r/GUI/PrinterWebView.cpp index e60d9f2..dbe5368 100644 --- a/src/slic3r/GUI/PrinterWebView.cpp +++ b/src/slic3r/GUI/PrinterWebView.cpp @@ -999,6 +999,8 @@ void PrinterWebView::load_disconnect_url(wxString& url) webisNetMode = isDisconnect; m_web = url; m_ip = ""; + //y22 + has_load_url = false; m_browser->LoadURL(url); UpdateState(); } @@ -1008,6 +1010,8 @@ void PrinterWebView::load_url(wxString &url) if (m_browser == nullptr || m_web == url) return; m_web = url; + //y22 + has_load_url = true; m_browser->LoadURL(url); webisNetMode = isLocalWeb; // B55 @@ -1028,11 +1032,13 @@ void PrinterWebView::load_url(wxString &url) } UpdateState(); } -void PrinterWebView::load_net_url(std::string url, std::string ip) +void PrinterWebView::load_net_url(wxString& url, wxString& ip) { if (m_browser == nullptr || m_web == url) return; m_web = url; + //y22 + has_load_url = true; m_ip = ip; webisNetMode = isNetWeb; m_browser->LoadURL(url); @@ -1144,7 +1150,10 @@ void PrinterWebView::RunScript(const wxString &javascript) { formattedHost = "http://" + link_url; } - load_net_url(formattedHost, local_ip); + //y22 + wxString final_host = from_u8(formattedHost); + wxString final_ip = from_u8(local_ip); + load_net_url(final_host, final_ip); } //y16 diff --git a/src/slic3r/GUI/PrinterWebView.hpp b/src/slic3r/GUI/PrinterWebView.hpp index 1a039b8..96ef11c 100644 --- a/src/slic3r/GUI/PrinterWebView.hpp +++ b/src/slic3r/GUI/PrinterWebView.hpp @@ -69,7 +69,8 @@ public: void init_scroll_window(wxPanel *Panel); void CreatThread(); void load_url(wxString& url); - void load_net_url(std::string url, std::string ip); + //y22 + void load_net_url(wxString& url, wxString& ip); void UpdateState(); void OnClose(wxCloseEvent& evt); @@ -137,6 +138,12 @@ public: //y18 bool Local_has_device() { return m_buttons.size() > 0; }; + //y22 + wxString GetWeburl() { return m_web; }; + wxString GetWebIp() { return m_ip; }; + bool GetHasLoadUrl() { return has_load_url; }; + bool IsNetUrl() { return webisNetMode == isNetWeb; }; + private: wxBoxSizer *leftallsizer; @@ -182,6 +189,9 @@ private: //y5 std::string m_user_head_name; bool m_isfluidd_1; + + //y22 + bool has_load_url; }; //y3 diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 7c793d8..5f9fb81 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -608,7 +608,8 @@ bool Selection::contains_any_volume(const std::vector& volume_idxs bool Selection::contains_sinking_volumes(bool ignore_modifiers) const { - for (const GLVolume* v : *m_volumes) { + for (unsigned int i : m_list) { + const GLVolume* v = (*m_volumes)[i]; if (!ignore_modifiers || !v->is_modifier) { if (v->is_sinking()) return true; @@ -1622,8 +1623,6 @@ void Selection::erase() wxGetApp().obj_list()->delete_from_model_and_list(items); ensure_not_below_bed(); } - - wxGetApp().plater()->canvas3D()->set_sequential_clearance_as_evaluating(); } void Selection::render(float scale_factor) diff --git a/src/slic3r/GUI/Sidebar.cpp b/src/slic3r/GUI/Sidebar.cpp index cd3a86f..243c906 100644 --- a/src/slic3r/GUI/Sidebar.cpp +++ b/src/slic3r/GUI/Sidebar.cpp @@ -26,6 +26,7 @@ #include "libslic3r/Print.hpp" #include "libslic3r/SLAPrint.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/ModelProcessing.hpp" #include "GUI.hpp" #include "GUI_App.hpp" @@ -920,7 +921,7 @@ void Sidebar::show_info_sizer() Vec3d size = vol ? vol->mesh().transformed_bounding_box(t).size() : model_object->instance_bounding_box(inst_idx).size(); m_object_info->info_size->SetLabel(wxString::Format("%.2f x %.2f x %.2f", size(0)*koef, size(1)*koef, size(2)*koef)); - const TriangleMeshStats& stats = vol ? vol->mesh().stats() : model_object->get_object_stl_stats(); + const TriangleMeshStats& stats = vol ? vol->mesh().stats() : ModelProcessing::get_object_mesh_stats(model_object); double volume_val = stats.volume; if (vol) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index ced5425..8c34ffc 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -19,6 +19,7 @@ #include "Search.hpp" #include "OG_CustomCtrl.hpp" +#include #include #include #include @@ -1135,7 +1136,7 @@ void Tab::update_wiping_button_visibility() { } } -void Tab::activate_option(const std::string& opt_key, const wxString& category) +void Tab::activate_option(const std::string &opt_key, const wxString &category, const std::vector &another_blinking_opt_keys) { wxString page_title = translate_category(category, m_type); @@ -1183,7 +1184,12 @@ void Tab::activate_option(const std::string& opt_key, const wxString& category) set_focus(field->getWindow()); } - m_highlighter.init(get_custom_ctrl_with_blinking_ptr(opt_key)); + std::vector> custom_blinking_ctrls = { get_custom_ctrl_with_blinking_ptr(opt_key) }; + for (const std::string &another_blinking_opt_key : another_blinking_opt_keys) { + custom_blinking_ctrls.emplace_back(get_custom_ctrl_with_blinking_ptr(another_blinking_opt_key)); + } + + m_highlighter.init(custom_blinking_ctrls); } void Tab::cache_config_diff(const std::vector& selected_options, const DynamicPrintConfig* config/* = nullptr*/) @@ -1430,7 +1436,7 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Horizontal shells")); line = { L("Solid layers"), "" }; - line.label_path = category_path + "solid-layers"; + line.label_path = category_path + "solid-layers-top-bottom"; line.append_option(optgroup->get_option("top_solid_layers")); line.append_option(optgroup->get_option("bottom_solid_layers")); optgroup->append_line(line); @@ -1461,13 +1467,14 @@ void TabPrint::build() optgroup->append_single_option_line("seam_gap_distance", category_path + "seam-gap-distance"); optgroup->append_single_option_line("staggered_inner_seams", category_path + "staggered-inner-seams"); - optgroup->append_single_option_line("scarf_seam_placement", category_path + "scarf-seam-placement"); - optgroup->append_single_option_line("scarf_seam_only_on_smooth", category_path + "scarf-seam-only-on-smooth"); - optgroup->append_single_option_line("scarf_seam_start_height", category_path + "scarf-seam-start-height"); - optgroup->append_single_option_line("scarf_seam_entire_loop", category_path + "scarf-seam-entire-loop"); - optgroup->append_single_option_line("scarf_seam_length", category_path + "scarf-seam-length"); - optgroup->append_single_option_line("scarf_seam_max_segment_length", category_path + "scarf-seam-max-segment-length"); - optgroup->append_single_option_line("scarf_seam_on_inner_perimeters", category_path + "scarf-seam-on-inner-perimeters"); + const std::string scarf_seam_path{"seam-position_151069#"}; + optgroup->append_single_option_line("scarf_seam_placement", scarf_seam_path + "scarf-joint-placement"); + optgroup->append_single_option_line("scarf_seam_only_on_smooth", scarf_seam_path + "scarf-joint-only-on-smooth-perimeters"); + optgroup->append_single_option_line("scarf_seam_start_height", scarf_seam_path + "scarf-start-height"); + optgroup->append_single_option_line("scarf_seam_entire_loop", scarf_seam_path + "scarf-joint-around-entire-perimeter"); + optgroup->append_single_option_line("scarf_seam_length", scarf_seam_path + "scarf-joint-length"); + optgroup->append_single_option_line("scarf_seam_max_segment_length", scarf_seam_path + "max-scarf-joint-segment-length"); + optgroup->append_single_option_line("scarf_seam_on_inner_perimeters", scarf_seam_path + "scarf-joint-on-inner-perimeters"); optgroup->append_single_option_line("external_perimeters_first", category_path + "external-perimeters-first"); optgroup->append_single_option_line("gap_fill_enabled", category_path + "fill-gaps"); @@ -1593,6 +1600,7 @@ void TabPrint::build() optgroup->append_single_option_line("support_material_speed"); optgroup->append_single_option_line("support_material_interface_speed"); optgroup->append_single_option_line("bridge_speed"); + optgroup->append_single_option_line("over_bridge_speed"); optgroup->append_single_option_line("gap_fill_speed"); optgroup->append_single_option_line("ironing_speed"); @@ -1617,7 +1625,6 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Modifiers")); optgroup->append_single_option_line("first_layer_speed"); - //B37 optgroup->append_single_option_line("first_layer_infill_speed"); //B36 optgroup->append_single_option_line("first_layer_travel_speed"); @@ -1655,6 +1662,7 @@ void TabPrint::build() optgroup->append_single_option_line("support_material_extruder"); optgroup->append_single_option_line("support_material_interface_extruder"); optgroup->append_single_option_line("wipe_tower_extruder"); + optgroup->append_single_option_line("bed_temperature_extruder"); optgroup = page->new_optgroup(L("Ooze prevention")); optgroup->append_single_option_line("ooze_prevention"); @@ -1662,7 +1670,7 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Wipe tower")); optgroup->append_single_option_line("wipe_tower"); - optgroup->append_single_option_line("wipe_tower_width"); + optgroup->append_single_option_line("wipe_tower_width"); optgroup->append_single_option_line("wipe_tower_brim_width"); optgroup->append_single_option_line("wipe_tower_bridging"); optgroup->append_single_option_line("wipe_tower_cone_angle"); @@ -1676,6 +1684,13 @@ void TabPrint::build() optgroup->append_single_option_line("mmu_segmented_region_max_width"); optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth"); + optgroup->append_single_option_line("interlocking_beam"); + optgroup->append_single_option_line("interlocking_beam_width"); + optgroup->append_single_option_line("interlocking_orientation"); + optgroup->append_single_option_line("interlocking_beam_layer_count"); + optgroup->append_single_option_line("interlocking_depth"); + optgroup->append_single_option_line("interlocking_boundary_avoidance"); + page = add_options_page(L("Advanced"), "wrench"); optgroup = page->new_optgroup(L("Extrusion width")); optgroup->append_single_option_line("extrusion_width"); @@ -1725,9 +1740,17 @@ void TabPrint::build() page = add_options_page(L("Output options"), "output+page_white"); optgroup = page->new_optgroup(L("Sequential printing")); optgroup->append_single_option_line("complete_objects", "print-settings/sequential-printing"); - line = { L("Extruder clearance"), "" }; - line.append_option(optgroup->get_option("extruder_clearance_radius")); - line.append_option(optgroup->get_option("extruder_clearance_height")); + + line = Line{ "", "" }; + line.full_width = 1; + line.widget = [this](wxWindow* parent) { + ogStaticText* stat_text; // Let the pointer die, we don't need it and the parent will free it. + wxSizer* sizer = description_line_widget(parent, &stat_text); + stat_text->SetText(_L("Note: When using this option, the Arrange function automatically " + "accounts for the printer geometry to prevent collisions. Extruder geometry is built-in for most " + "QIDI printers, the others use generic model defined by values in Printer Settings.")); + return sizer; + }; optgroup->append_line(line); optgroup = page->new_optgroup(L("Output file")); @@ -2721,7 +2744,7 @@ void TabPrinter::build_fff() auto page = add_options_page(L("General"), "printer"); auto optgroup = page->new_optgroup(L("Size and coordinates")); - create_line_with_widget(optgroup.get(), "bed_shape", "", [this](wxWindow* parent) { + create_line_with_widget(optgroup.get(), "bed_shape", "custom-svg-and-png-bed-textures_124612", [this](wxWindow* parent) { return create_bed_shape_widget(parent); }); @@ -2907,6 +2930,10 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("variable_layer_height"); optgroup->append_single_option_line("prefer_clockwise_movements"); + optgroup = page->new_optgroup(L("Sequential printing limits")); + optgroup->append_single_option_line("extruder_clearance_radius"); + optgroup->append_single_option_line("extruder_clearance_height"); + //Y16 optgroup = page->new_optgroup(L("Accessory")); optgroup->append_single_option_line("auxiliary_fan"); @@ -3777,13 +3804,6 @@ void TabPrinter::update_fff() toggle_options(); } -bool Tab::is_qidi_printer() const -{ - const Preset& edited_preset = m_preset_bundle->printers.get_edited_preset(); - std::string printer_model = edited_preset.trim_vendor_repo_prefix(edited_preset.config.opt_string("printer_model")); - return SLAPrint::is_qidi_print(printer_model); -} - void TabPrinter::update_sla() { } @@ -5608,10 +5628,10 @@ static void append_tilt_options_line(ConfigOptionsGroupShp optgroup, const std:: optgroup->append_line(line); } -void TabSLAMaterial::build_tilt_group(Slic3r::GUI::PageShp page) +static void create_tilt_legend(ConfigOptionsGroupShp optgroup) { // Legend - std::vector> legend_columns = { + std::vector> columns = { // TRN: This is a label of a column of parameters in settings to be used when the area is below certain threshold. {L("Below"), L("Values in this column are applied when layer area is smaller than area_fill.")}, @@ -5619,24 +5639,83 @@ void TabSLAMaterial::build_tilt_group(Slic3r::GUI::PageShp page) {L("Above"), L("Values in this column are applied when layer area is larger than area_fill.")}, }; - create_legend(page, legend_columns, comExpert/*, true*/); + auto legend = [columns](wxWindow* parent) { + auto legend_sizer = new wxBoxSizer(wxHORIZONTAL); + legend_sizer->Add(new wxStaticText(parent, wxID_ANY, "", wxDefaultPosition, wxSize(25*em_unit(parent), -1))); + for (auto& [name, tooltip] : columns) { + auto legend_item = new wxStaticText(parent, wxID_ANY, _(name), wxDefaultPosition, wxSize(20*em_unit(parent), -1)); + legend_item->SetToolTip(_(tooltip)); + legend_sizer->Add(legend_item); + } + + return legend_sizer; + }; + + Line line = Line{ "", "" }; + line.full_width = 1; + line.append_widget(legend); + optgroup->append_line(line); +} + +void TabSLAMaterial::build_tilt_group(Slic3r::GUI::PageShp page) +{ // TRN: 'Profile' in this context denotes a group of parameters used to configure // layer separation procedure for SLA printers. auto optgroup = page->new_optgroup(L("Profile settings")); - optgroup->on_change = [this, optgroup](const t_config_option_key& key, boost::any value) + optgroup->on_change = [this](const t_config_option_key& key, boost::any value) { - if (key.find_first_of("use_tilt") == 0) + if (key.find("use_tilt") == 0) toggle_tilt_options(key == "use_tilt#0"); update_dirty(); update(); }; + create_line_with_tilt_defaults(optgroup); + create_tilt_legend(optgroup); + for (const std::string& opt_key : tilt_options()) append_tilt_options_line(optgroup, opt_key); } +std::vector> default_tilt_buttons = { + { _L("Fast"), _L("Set default values for fast print speed"), SLAMaterialSpeed::slamsFast }, + { _L("Slow"), _L("Set default values for slow print speed"), SLAMaterialSpeed::slamsSlow }, + { _L("High viscosity"), _L("Set default values for high viscosity print speed"), SLAMaterialSpeed::slamsHighViscosity } +}; + +void TabSLAMaterial::create_line_with_tilt_defaults(ConfigOptionsGroupShp optgroup) +{ + auto print_speed_btns = [this](wxWindow* parent) { + m_tilt_defaults_sizer = new wxBoxSizer(wxHORIZONTAL); + + auto grid_sizer = new wxGridSizer(3, 0, 0); + for (const auto& [label, tooltip, material_speed] : default_tilt_buttons) { + ScalableButton* btn; + add_scaled_button(parent, &btn, "cog", label + " ", wxBU_EXACTFIT); + btn->SetToolTip(tooltip); + btn->SetFont(Slic3r::GUI::wxGetApp().normal_font()); + + int tilt_mode = int(material_speed); + btn->Bind(wxEVT_BUTTON, [this, tilt_mode](wxCommandEvent&) { + DynamicPrintConfig new_conf = *m_config; + update_tilts_by_mode(new_conf, tilt_mode, false); + load_config(new_conf); + }); + grid_sizer->Add(btn, 1, wxEXPAND | wxRIGHT, 5); + } + + m_tilt_defaults_sizer->Add(grid_sizer, 0, wxALIGN_CENTRE_VERTICAL); + return m_tilt_defaults_sizer; + }; + + Line line = Line{ "", "" }; + line.full_width = 1; + line.append_widget(print_speed_btns); + optgroup->append_line(line); +} + std::vector disable_tilt_options = { "tilt_down_initial_speed" ,"tilt_down_offset_steps" @@ -5704,14 +5783,28 @@ void TabSLAMaterial::update_description_lines() Tab::update_description_lines(); } +std::string Tab::printer_model() const +{ + const Preset& edited_preset = m_preset_bundle->printers.get_edited_preset(); + return edited_preset.trim_vendor_repo_prefix(edited_preset.config.opt_string("printer_model")); +} + +bool Tab::is_qidi_printer() const +{ + return SLAPrint::is_qidi_print(printer_model()); +} + void TabSLAMaterial::update_sla_qidi_specific_visibility() { if (m_active_page && m_active_page->title() == "Material printing profile") { for (auto& title : { "", "Profile settings" }) { auto og_it = std::find_if(m_active_page->m_optgroups.begin(), m_active_page->m_optgroups.end(), [title](const ConfigOptionsGroupShp og) { return og->title == title; }); - if (og_it != m_active_page->m_optgroups.end()) + if (og_it != m_active_page->m_optgroups.end()) { og_it->get()->Show(m_mode >= comAdvanced && is_qidi_printer()); + const std::string pr_model = printer_model(); + m_tilt_defaults_sizer->Show(pr_model == "SL1S" || pr_model == "M1"); + } } auto og_it = std::find_if(m_active_page->m_optgroups.begin(), m_active_page->m_optgroups.end(), @@ -5731,6 +5824,7 @@ void TabSLAMaterial::clear_pages() over_opt.second = nullptr; m_z_correction_to_mm_description = nullptr; + m_tilt_defaults_sizer = nullptr; } void TabSLAMaterial::msw_rescale() @@ -5973,7 +6067,6 @@ void TabSLAPrint::build() optgroup = page->new_optgroup(L("Automatic generation")); optgroup->append_single_option_line("support_points_density_relative"); - optgroup->append_single_option_line("support_points_minimal_distance"); page = add_options_page(L("Pad"), "pad"); optgroup = page->new_optgroup(L("Pad")); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index fdae67c..a379628 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -384,7 +384,7 @@ public: void on_value_change(const std::string& opt_key, const boost::any& value); void update_wiping_button_visibility(); - void activate_option(const std::string& opt_key, const wxString& category); + void activate_option(const std::string &opt_key, const wxString &category, const std::vector &another_blinking_opt_keys = {}); void cache_config_diff(const std::vector& selected_options, const DynamicPrintConfig* config = nullptr); void apply_config_from_cache(); @@ -414,6 +414,7 @@ protected: void update_frequently_filament_changed_parameters(); void fill_icon_descriptions(); void set_tooltips_text(); + std::string printer_model() const; virtual bool select_preset_by_name(const std::string& name_w_suffix, bool force); virtual bool save_current_preset(const std::string& new_name, bool detach); @@ -557,9 +558,11 @@ class TabSLAMaterial : public Tab void update_line_with_near_label_widget(ConfigOptionsGroupShp optgroup, const std::string& opt_key, bool is_checked = true); void add_material_overrides_page(); void update_material_overrides_page(); + void create_line_with_tilt_defaults(ConfigOptionsGroupShp optgroup); std::map m_overrides_options; ogStaticText* m_z_correction_to_mm_description = nullptr; + wxSizer* m_tilt_defaults_sizer { nullptr }; public: TabSLAMaterial(wxBookCtrlBase* parent) : diff --git a/src/slic3r/GUI/TopBarMenus.cpp b/src/slic3r/GUI/TopBarMenus.cpp index 8f2d76a..e46b570 100644 --- a/src/slic3r/GUI/TopBarMenus.cpp +++ b/src/slic3r/GUI/TopBarMenus.cpp @@ -121,7 +121,7 @@ void TopBarMenus::UpdateAccountState(bool state) void TopBarMenus::RemoveHideLoginItem() { if (m_hide_login_item) - account.Remove(m_hide_login_item); + account.Destroy(m_hide_login_item); } void TopBarMenus::Popup(TopBarItemsCtrl* popup_ctrl, wxMenu* menu, wxPoint pos) diff --git a/src/slic3r/GUI/UpdatesUIManager.cpp b/src/slic3r/GUI/UpdatesUIManager.cpp index 67232c7..4091165 100644 --- a/src/slic3r/GUI/UpdatesUIManager.cpp +++ b/src/slic3r/GUI/UpdatesUIManager.cpp @@ -111,7 +111,7 @@ void RepositoryUpdateUIManager::fill_entries(bool init_selection/* = false*/) if (data.source_path.empty()) { // online repo - m_online_entries.push_back({ is_selected, uuid, data.name, data.description, data.visibility }); + m_online_entries.push_back({ is_selected, uuid, data.name, data.description, data.visibility, data.not_in_manifest }); } else { // offline repo @@ -157,12 +157,26 @@ void RepositoryUpdateUIManager::fill_grids() }); add(chb); - if (entry.visibility.empty()) - add(new wxStaticText(m_parent, wxID_ANY, "")); - else { + if (entry.not_in_manifest) { + wxStaticBitmap* bmp = new wxStaticBitmap(m_parent, wxID_ANY, *get_bmp_bundle("notification_warning")); + //wxGetApp().plater()->get_user_account() + if (wxGetApp().is_account_logged_in()) { + // TRN tooltip in Configuration Wizard - Configuration Sources + bmp->SetToolTip(_L("Some vendors were installed from this source, but you do not have the rights to receive updates from it.\n" + "This source may no longer be active, or your account may no longer be subscribed.\n" + "Please consider unsubscribing from this source.")); + } else { + // TRN tooltip in Configuration Wizard - Configuration Sources + bmp->SetToolTip(_L("Some vendors were installed from this source, but you do not have rights to receive updates from it.\n" + "Please log in to restore access to all your subscribed sources or consider unsubscribing from this source.")); + } + add(bmp); + } else if (!entry.visibility.empty()) { wxStaticBitmap* bmp = new wxStaticBitmap(m_parent, wxID_ANY, *get_bmp_bundle("info")); bmp->SetToolTip(from_u8(entry.visibility)); add(bmp); + } else { + add(new wxStaticText(m_parent, wxID_ANY, "")); } add(new wxStaticText(m_parent, wxID_ANY, from_u8(entry.name) + " ")); diff --git a/src/slic3r/GUI/UpdatesUIManager.hpp b/src/slic3r/GUI/UpdatesUIManager.hpp index b32443b..3e6c729 100644 --- a/src/slic3r/GUI/UpdatesUIManager.hpp +++ b/src/slic3r/GUI/UpdatesUIManager.hpp @@ -19,14 +19,15 @@ namespace GUI { class RepositoryUpdateUIManager { struct OnlineEntry { - OnlineEntry(bool use, const std::string &id, const std::string &name, const std::string &description, const std::string &visibility) : - use(use), id(id), name(name), description(description), visibility(visibility) {} + OnlineEntry(bool use, const std::string &id, const std::string &name, const std::string &description, const std::string &visibility, bool not_in_manifest) : + use(use), id(id), name(name), description(description), visibility(visibility) ,not_in_manifest(not_in_manifest) {} bool use; std::string id; std::string name; std::string description; std::string visibility; + bool not_in_manifest; }; struct OfflineEntry { diff --git a/src/slic3r/GUI/UserAccount.cpp b/src/slic3r/GUI/UserAccount.cpp index 17b24e4..446c21e 100644 --- a/src/slic3r/GUI/UserAccount.cpp +++ b/src/slic3r/GUI/UserAccount.cpp @@ -3,14 +3,20 @@ #include "UserAccountUtils.hpp" #include "format.hpp" #include "GUI.hpp" +#include "GUI_App.hpp" #include "libslic3r/Utils.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" #include #include #include #include +#include "InstanceCheck.hpp" +#include "GUI_App.hpp" + #include namespace pt = boost::property_tree; @@ -27,10 +33,10 @@ UserAccount::UserAccount(wxEvtHandler* evt_handler, AppConfig* app_config, const UserAccount::~UserAccount() {} -void UserAccount::set_username(const std::string& username) +void UserAccount::set_username(const std::string& username, bool store) { m_username = username; - m_communication->set_username(username); + m_communication->set_username(username, store); } void UserAccount::clear() @@ -54,7 +60,7 @@ bool UserAccount::get_remember_session() return m_communication->get_remember_session(); } -bool UserAccount::is_logged() +bool UserAccount::is_logged() const { return m_communication->is_logged(); } @@ -64,7 +70,9 @@ void UserAccount::do_login() } void UserAccount::do_logout() { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; m_communication->do_logout(); + Slic3r::GUI::wxGetApp().other_instance_message_handler()->multicast_message("STORE_READ"); } std::string UserAccount::get_access_token() @@ -95,9 +103,13 @@ void UserAccount::enqueue_connect_status_action() { m_communication->enqueue_connect_status_action(); } -void UserAccount::enqueue_avatar_action() +void UserAccount::enqueue_avatar_old_action() { - m_communication->enqueue_avatar_action(m_account_user_data["avatar"]); + m_communication->enqueue_avatar_old_action(m_account_user_data["avatar"]); +} +void UserAccount::enqueue_avatar_new_action(const std::string& url) +{ + m_communication->enqueue_avatar_new_action(url); } void UserAccount::enqueue_printer_data_action(const std::string& uuid) { @@ -114,7 +126,7 @@ bool UserAccount::on_login_code_recieved(const std::string& url_message) return true; } -bool UserAccount::on_user_id_success(const std::string data, std::string& out_username) +bool UserAccount::on_user_id_success(const std::string data, std::string& out_username, bool after_token_success) { boost::property_tree::ptree ptree; try { @@ -129,7 +141,7 @@ bool UserAccount::on_user_id_success(const std::string data, std::string& out_us for (const auto& section : ptree) { const auto opt = ptree.get_optional(section.first); if (opt) { - BOOST_LOG_TRIVIAL(debug) << static_cast(section.first) << " " << *opt; + //BOOST_LOG_TRIVIAL(debug) << static_cast(section.first) << " " << *opt; m_account_user_data[section.first] = *opt; } @@ -139,13 +151,22 @@ bool UserAccount::on_user_id_success(const std::string data, std::string& out_us return false; } std::string public_username = m_account_user_data["public_username"]; - set_username(public_username); + set_username(public_username, after_token_success); out_username = public_username; // enqueue GET with avatar url - if (m_account_user_data.find("avatar") != m_account_user_data.end()) { + + if (m_account_user_data.find("avatar_small") != m_account_user_data.end()) { + const boost::filesystem::path server_file(m_account_user_data["avatar_small"]); + m_avatar_extension = server_file.extension().string(); + enqueue_avatar_new_action(m_account_user_data["avatar_small"]); + } else if (m_account_user_data.find("avatar_large") != m_account_user_data.end()) { + const boost::filesystem::path server_file(m_account_user_data["avatar_large"]); + m_avatar_extension = server_file.extension().string(); + enqueue_avatar_new_action(m_account_user_data["avatar_large"]); + } else if (m_account_user_data.find("avatar") != m_account_user_data.end()) { const boost::filesystem::path server_file(m_account_user_data["avatar"]); m_avatar_extension = server_file.extension().string(); - enqueue_avatar_action(); + enqueue_avatar_old_action(); } else { BOOST_LOG_TRIVIAL(error) << "User ID message from QIDIAuth did not contain avatar."; @@ -165,11 +186,14 @@ void UserAccount::on_communication_fail() } } - +void UserAccount::on_race_lost() +{ + m_communication->on_race_lost(); +} bool UserAccount::on_connect_printers_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed) { - BOOST_LOG_TRIVIAL(debug) << "QIDI Connect printers message: " << data; + BOOST_LOG_TRIVIAL(trace) << "QIDI Connect printers message: " << data; pt::ptree ptree; try { std::stringstream ss(data); @@ -207,18 +231,20 @@ bool UserAccount::on_connect_printers_success(const std::string& data, AppConfig continue; } if (m_printer_uuid_map.find(*printer_uuid) == m_printer_uuid_map.end()) { - BOOST_LOG_TRIVIAL(error) << "Missing printer model for printer uuid: " << *printer_uuid; + BOOST_LOG_TRIVIAL(trace) << "Missing printer model for printer uuid: " << *printer_uuid; continue; } - std::pair model_nozzle_pair = m_printer_uuid_map[*printer_uuid]; + + std::string printer_name = m_printer_uuid_map[*printer_uuid]; - if (new_printer_map.find(model_nozzle_pair) == new_printer_map.end()) { - new_printer_map[model_nozzle_pair].reserve(static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT)); + if (new_printer_map.find(printer_name) == new_printer_map.end()) { + new_printer_map[printer_name].reserve(static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT)); for (size_t i = 0; i < static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT); i++) { - new_printer_map[model_nozzle_pair].push_back(0); + new_printer_map[printer_name].push_back(0); } } - new_printer_map[model_nozzle_pair][static_cast(state)] += 1; + new_printer_map[printer_name][static_cast(state)] += 1; + } // compare new and old printer map and update old map into new @@ -256,27 +282,43 @@ bool UserAccount::on_connect_uiid_map_success(const std::string& data, AppConfig BOOST_LOG_TRIVIAL(error) << "Could not parse qidiconnect message. " << e.what(); return false; } + pt::ptree printers_ptree; + if (auto it = ptree.find("printers"); it != ptree.not_found()) { + printers_ptree = it->second; + } else { + BOOST_LOG_TRIVIAL(error) << "Could not parse qidiconnect message. \"printers\" subtree is missing."; + return false; + } - for (const auto& printer_tree : ptree) { - const auto printer_uuid = printer_tree.second.get_optional("printer_uuid"); + for (const auto& printer_sub : printers_ptree) { + const pt::ptree &printer_ptree = printer_sub.second; + const auto printer_uuid = printer_ptree.get_optional("uuid"); if (!printer_uuid) { continue; } - const auto printer_model = printer_tree.second.get_optional("printer_model"); + const auto printer_model = printer_ptree.get_optional("printer_model"); if (!printer_model) { continue; } - const auto nozzle_diameter_opt = printer_tree.second.get_optional("nozzle_diameter"); - const std::string nozzle_diameter = (nozzle_diameter_opt && *nozzle_diameter_opt != "0.0") ? *nozzle_diameter_opt : std::string(); - std::pair model_nozzle_pair = { *printer_model, nozzle_diameter }; - m_printer_uuid_map[*printer_uuid] = model_nozzle_pair; + + std::map> config_options_to_match; + UserAccountUtils::fill_config_options_from_json(printer_ptree, config_options_to_match); + const Preset* printer_preset = UserAccountUtils::find_preset_by_nozzle_and_options(wxGetApp().preset_bundle->printers, *printer_model, config_options_to_match); + if (printer_preset) { + // Preset can have repo prefix + std::string trimmed_name = printer_preset->name; + const PresetWithVendorProfile& printer_with_vendor = wxGetApp().preset_bundle->printers.get_preset_with_vendor_profile(*printer_preset); + trimmed_name = printer_preset->trim_vendor_repo_prefix(trimmed_name, printer_with_vendor.vendor); + m_printer_uuid_map[*printer_uuid] = trimmed_name; + } else { + BOOST_LOG_TRIVIAL(trace) << "Failed to find preset for printer model: " << *printer_model; + } } m_communication->on_uuid_map_success(); return on_connect_printers_success(data, app_config, out_printers_changed); } -std::string UserAccount::get_current_printer_uuid_from_connect(const std::string &selected_printer_id -) const { +std::string UserAccount::get_current_printer_uuid_from_connect(const std::string &selected_printer_id) const { if (m_current_printer_data_json_from_connect.empty() || m_current_printer_uuid_from_connect.empty()) { return {}; } diff --git a/src/slic3r/GUI/UserAccount.hpp b/src/slic3r/GUI/UserAccount.hpp index 9a01932..2c5c548 100644 --- a/src/slic3r/GUI/UserAccount.hpp +++ b/src/slic3r/GUI/UserAccount.hpp @@ -24,9 +24,9 @@ enum class ConnectPrinterState { CONNECT_PRINTER_ERROR, CONNECT_PRINTER_STATE_COUNT }; -// is pair of printer_model and nozzle_diameter. std::vector is vector of ConnectPrinterState counters -typedef std::map, std::vector> ConnectPrinterStateMap; -typedef std::map< std::string, std::pair> ConnectUUIDToModelNozzleMap; +// printer preset name and std::vector is vector of ConnectPrinterState counters +typedef std::map> ConnectPrinterStateMap; +typedef std::map< std::string, std::string> ConnectUUIDToPresetName; // Class UserAccount should handle every request for entities outside QIDISlicer like QIDIAuth or QIDIConnect. // Outside communication is implemented in class UserAccountCommunication that runs separate thread. Results come back in events to Plater. // All incoming data shoud be stored in UserAccount. @@ -35,7 +35,7 @@ public: UserAccount(wxEvtHandler* evt_handler, Slic3r::AppConfig* app_config, const std::string& instance_hash); ~UserAccount(); - bool is_logged(); + bool is_logged() const; void do_login(); void do_logout(); wxString generate_login_redirect_url() { return m_communication->generate_login_redirect_url(); } @@ -46,7 +46,8 @@ public: bool get_remember_session(); void enqueue_connect_status_action(); void enqueue_connect_printer_models_action(); - void enqueue_avatar_action(); + void enqueue_avatar_old_action(); + void enqueue_avatar_new_action(const std::string& url); void enqueue_printer_data_action(const std::string& uuid); void request_refresh(); // Clears all data and connections, called on logout or EVT_UA_RESET @@ -55,9 +56,10 @@ public: // Functions called from UI where events emmited from UserAccountSession are binded // Returns bool if data were correctly proccessed bool on_login_code_recieved(const std::string& url_message); - bool on_user_id_success(const std::string data, std::string& out_username); + bool on_user_id_success(const std::string data, std::string& out_username, bool after_token_success); // Called on EVT_UA_FAIL, triggers test after several calls void on_communication_fail(); + void on_race_lost(); bool on_connect_printers_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed); bool on_connect_uiid_map_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed); @@ -77,15 +79,17 @@ public: void set_current_printer_data(const std::string& data) { m_current_printer_data_json_from_connect = data; } void set_refresh_time(int seconds) { m_communication->set_refresh_time(seconds); } + + void on_store_read_request() { m_communication->on_store_read_request(); } private: - void set_username(const std::string& username); + void set_username(const std::string& username, bool store); std::string m_instance_hash; // used in avatar path std::unique_ptr m_communication; ConnectPrinterStateMap m_printer_map; - ConnectUUIDToModelNozzleMap m_printer_uuid_map; + ConnectUUIDToPresetName m_printer_uuid_map; std::map m_account_user_data; std::string m_username; size_t m_fail_counter { 0 }; @@ -104,6 +108,7 @@ private: {"READY" , ConnectPrinterState::CONNECT_PRINTER_READY}, {"ATTENTION", ConnectPrinterState::CONNECT_PRINTER_ATTENTION}, {"BUSY" , ConnectPrinterState::CONNECT_PRINTER_BUSY}, + {"ERROR" , ConnectPrinterState::CONNECT_PRINTER_ERROR}, }; }; }} // namespace slic3r::GUI diff --git a/src/slic3r/GUI/UserAccountCommunication.cpp b/src/slic3r/GUI/UserAccountCommunication.cpp index 10ffc8f..0adcf12 100644 --- a/src/slic3r/GUI/UserAccountCommunication.cpp +++ b/src/slic3r/GUI/UserAccountCommunication.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -41,6 +42,8 @@ #include #include #include + +#include #endif // __linux__ @@ -138,7 +141,7 @@ bool load_secret(const std::string& opt, std::string& usr, std::string& psswd) } #ifdef __linux__ -void load_refresh_token_linux(std::string& refresh_token) +void load_tokens_linux(UserAccountCommunication::StoreData& result) { // Load refresh token from UserAccount.dat boost::filesystem::path source(boost::filesystem::path(Slic3r::data_dir()) / "UserAccount.dat") ; @@ -156,18 +159,80 @@ void load_refresh_token_linux(std::string& refresh_token) } boost::nowide::ifstream stream(source.generic_string(), std::ios::in | std::ios::binary); if (!stream) { - BOOST_LOG_TRIVIAL(error) << "UserAccount: Failed to read token from " << source; + BOOST_LOG_TRIVIAL(error) << "UserAccount: Failed to read tokens from " << source; return; } - std::getline(stream, refresh_token); + std::string token_data; + std::getline(stream, token_data); stream.close(); if (delete_after_read) { ec.clear(); if (!boost::filesystem::remove(source, ec) || ec) { BOOST_LOG_TRIVIAL(error) << "UserAccount: Failed to remove file " << source; } - } + + // read data + std::vector token_list; + boost::split(token_list, token_data, boost::is_any_of("|"), boost::token_compress_off); + assert(token_list.empty() || token_list.size() == 5); + if (token_list.size() < 5) { + BOOST_LOG_TRIVIAL(error) << "Size of read secrets is only: " << token_list.size() << " (expected 5). Data: " << token_data; + } + result.access_token = token_list.size() > 0 ? token_list[0] : std::string(); + result.refresh_token = token_list.size() > 1 ? token_list[1] : std::string(); + result.next_timeout = token_list.size() > 2 ? token_list[2] : std::string(); + result.master_pid = token_list.size() > 3 ? token_list[3] : std::string(); + result.shared_session_key = token_list.size() > 4 ? token_list[4] : std::string(); +} +bool concurrent_write_file(const std::string& secret_to_store, const boost::filesystem::path& filename) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + // Open the file + int fd = open(filename.string().c_str(), O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR); + if (fd == -1) { + BOOST_LOG_TRIVIAL(error) << "Unable to open store file " << filename << ": " << strerror(errno); + return false; + } + // Close the file when the guard dies + Slic3r::ScopeGuard sg_fd([fd]() { + close(fd); + BOOST_LOG_TRIVIAL(debug) << "Closed file."; + }); + + // Configure the lock + struct flock lock; + memset(&lock, 0, sizeof(lock)); + lock.l_type = F_WRLCK; // Write lock + lock.l_whence = SEEK_SET; // Lock from the start of the file + lock.l_start = 0; + lock.l_len = 0; // 0 means lock the entire file + + // Try to acquire the lock + BOOST_LOG_TRIVIAL(debug) << "Waiting to acquire lock on file: " << filename; + if (fcntl(fd, F_SETLKW, &lock) == -1) { + BOOST_LOG_TRIVIAL(error) << "Unable to acquire lock: " << strerror(errno); + return false; + } + BOOST_LOG_TRIVIAL(debug) << "Lock acquired on file: " << filename; + + Slic3r::ScopeGuard sg_lock([&lock, fd, filename]() { + // Release the lock when guard dies. + lock.l_type = F_UNLCK; // Unlock the file + if (fcntl(fd, F_SETLK, &lock) == -1) { + BOOST_LOG_TRIVIAL(error) << "Unable to release lock ("<< filename <<"): " << strerror(errno); + } else { + BOOST_LOG_TRIVIAL(debug) << "Lock released on file: " << filename; + } + }); + + // Write content to the file + if (write(fd, secret_to_store.c_str(), strlen(secret_to_store.c_str())) == -1) { + BOOST_LOG_TRIVIAL(error) << "Unable to write to file: " << strerror(errno); + return false; + } + BOOST_LOG_TRIVIAL(debug) << "Content written to file."; + return true; } #endif //__linux__ } @@ -176,44 +241,33 @@ UserAccountCommunication::UserAccountCommunication(wxEvtHandler* evt_handler, Ap : wxEvtHandler() , m_evt_handler(evt_handler) , m_app_config(app_config) - , m_polling_timer(new wxTimer(this)) - , m_token_timer(new wxTimer(this)) + , m_polling_timer(std::make_unique(this)) + , m_token_timer(std::make_unique(this)) + , m_slave_read_timer(std::make_unique(this)) + , m_after_race_lost_timer(std::make_unique(this)) { Bind(wxEVT_TIMER, &UserAccountCommunication::on_token_timer, this, m_token_timer->GetId()); Bind(wxEVT_TIMER, &UserAccountCommunication::on_polling_timer, this, m_polling_timer->GetId()); + Bind(wxEVT_TIMER, &UserAccountCommunication::on_slave_read_timer, this, m_slave_read_timer->GetId()); + Bind(wxEVT_TIMER, &UserAccountCommunication::on_after_race_lost_timer, this, m_after_race_lost_timer->GetId()); - std::string access_token, refresh_token, shared_session_key, next_timeout; - if (is_secret_store_ok()) { - std::string key0, key1, key2, tokens; - if (load_secret("tokens", key0, tokens)) { - std::vector token_list; - boost::split(token_list, tokens, boost::is_any_of("|"), boost::token_compress_off); - assert(token_list.empty() || token_list.size() == 3); - access_token = token_list.size() > 0 ? token_list[0] : std::string(); - refresh_token = token_list.size() > 1 ? token_list[1] : std::string(); - next_timeout = token_list.size() > 2 ? token_list[2] : std::string(); - } else { - load_secret("access_token", key0, access_token); - load_secret("refresh_token", key1, refresh_token); - load_secret("access_token_timeout", key2, next_timeout); - assert(key0 == key1); - } - shared_session_key = key0; + StoreData stored_data; + read_stored_data(stored_data); - } else { -#ifdef __linux__ - load_refresh_token_linux(refresh_token); -#endif - } - long long next = next_timeout.empty() ? 0 : std::stoll(next_timeout); - long long remain_time = next - std::time(nullptr); + long long next_timeout_long = stored_data.next_timeout.empty() ? 0 : std::stoll(stored_data.next_timeout); + long long remain_time = next_timeout_long - std::time(nullptr); if (remain_time <= 0) { - access_token.clear(); + stored_data.access_token.clear(); } else { set_refresh_time((int)remain_time); } - bool has_token = !refresh_token.empty(); - m_session = std::make_unique(evt_handler, access_token, refresh_token, shared_session_key, m_app_config->get_bool("connect_polling")); + if (!stored_data.access_token.empty()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ <<" access_token: " << stored_data.access_token.substr(0,5) << "..." << stored_data.access_token.substr(stored_data.access_token.size()-5); + } else { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ <<" access_token empty!"; + } + bool has_token = !stored_data.refresh_token.empty(); + m_session = std::make_unique(evt_handler, stored_data.access_token, stored_data.refresh_token, stored_data.shared_session_key, next_timeout_long, m_app_config->get_bool("connect_polling")); init_session_thread(); // perform login at the start, but only with tokens if (has_token) { @@ -225,6 +279,7 @@ UserAccountCommunication::~UserAccountCommunication() { m_token_timer->Stop(); m_polling_timer->Stop(); + if (m_thread.joinable()) { // Stop the worker thread, if running. { @@ -238,24 +293,42 @@ UserAccountCommunication::~UserAccountCommunication() } } -void UserAccountCommunication::set_username(const std::string& username) +void UserAccountCommunication::set_username(const std::string& username, bool store) { m_username = username; + if (!store && !username.empty()) { + return; + } { + //BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " empty: " << username.empty(); + std::string at = m_session->get_access_token(); + if (!at.empty()) + at = at.substr(0,5) + "..." + at.substr(at.size()-5); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ <<" access_token: " << (username.empty() ? "" : at); if (is_secret_store_ok()) { - std::string tokens; - if (m_remember_session) { + std::string tokens = "|||"; + if (m_remember_session && !username.empty()) { tokens = m_session->get_access_token() + - "|" + m_session->get_refresh_token() + - "|" + std::to_string(m_session->get_next_token_timeout()); + "|" + m_session->get_refresh_token() + + "|" + std::to_string(m_session->get_next_token_timeout()) + + "|" + std::to_string(get_current_pid()); } - save_secret("tokens", m_session->get_shared_session_key(), tokens); - } - else { + if (!save_secret("tokens", m_session->get_shared_session_key(), tokens)) { + BOOST_LOG_TRIVIAL(error) << "Failed to write tokens to the secret store."; + } + } else { #ifdef __linux__ // If we can't store the tokens in secret store, store them in file with chmod 600 boost::filesystem::path target(boost::filesystem::path(Slic3r::data_dir()) / "UserAccount.dat") ; - std::string data = m_session->get_refresh_token(); + std::string data = "||||"; + if (m_remember_session && !username.empty()) { + data = m_session->get_access_token() + + "|" + m_session->get_refresh_token() + + "|" + std::to_string(m_session->get_next_token_timeout()) + + "|" + std::to_string(get_current_pid()) + + "|" + m_session->get_shared_session_key(); + } + FILE* file; static const auto perms = boost::filesystem::owner_read | boost::filesystem::owner_write; // aka 600 @@ -265,17 +338,15 @@ void UserAccountCommunication::set_username(const std::string& username) BOOST_LOG_TRIVIAL(debug) << "UserAccount: boost::filesystem::permisions before write error message (this could be irrelevant message based on file system): " << ec.message(); ec.clear(); - file = boost::nowide::fopen(target.generic_string().c_str(), "wb"); - if (file == NULL) { - BOOST_LOG_TRIVIAL(error) << "UserAccount: Failed to open file to store token: " << target; - return; + if (!concurrent_write_file(data, target)) { + BOOST_LOG_TRIVIAL(error) << "Failed to store secret."; } - fwrite(data.c_str(), 1, data.size(), file); - fclose(file); boost::filesystem::permissions(target, perms, ec); if (ec) BOOST_LOG_TRIVIAL(debug) << "UserAccount: boost::filesystem::permisions after write error message (this could be irrelevant message based on file system): " << ec.message(); +#else + BOOST_LOG_TRIVIAL(error) << "Failed to write tokens to the secret store: Store is not ok."; #endif } } @@ -283,9 +354,10 @@ void UserAccountCommunication::set_username(const std::string& username) void UserAccountCommunication::set_remember_session(bool b) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; m_remember_session = b; // tokens needs to be stored or deleted - set_username(m_username); + set_username(m_username, true); } std::string UserAccountCommunication::get_access_token() @@ -304,6 +376,8 @@ std::string UserAccountCommunication::get_shared_session_key() void UserAccountCommunication::set_polling_enabled(bool enabled) { + // Here enabled sets to USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS so it gets full list on first, + // than it should change inside session to USER_ACCOUNT_ACTION_CONNECT_STATUS return m_session->set_polling_action(enabled ? UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS : UserAccountActionID::USER_ACCOUNT_ACTION_DUMMY); } @@ -323,7 +397,7 @@ wxString UserAccountCommunication::generate_login_redirect_url() const std::string REDIRECT_URI = "qidislicer://login"; CodeChalengeGenerator ccg; m_code_verifier = ccg.generate_verifier(); - std::string code_challenge = ccg.generate_chalenge(m_code_verifier); + std::string code_challenge = ccg.generate_challenge(m_code_verifier); wxString language = GUI::wxGetApp().current_language_code(); language = language.SubString(0, 1); BOOST_LOG_TRIVIAL(info) << "code verifier: " << m_code_verifier; @@ -339,7 +413,7 @@ wxString UserAccountCommunication::get_login_redirect_url(const std::string& ser const std::string CLIENT_ID = client_id(); const std::string REDIRECT_URI = "qidislicer://login"; CodeChalengeGenerator ccg; - std::string code_challenge = ccg.generate_chalenge(m_code_verifier); + std::string code_challenge = ccg.generate_challenge(m_code_verifier); wxString language = GUI::wxGetApp().current_language_code(); language = language.SubString(0, 1); @@ -355,7 +429,7 @@ void UserAccountCommunication::login_redirect() wxQueueEvent(m_evt_handler,new OpenQIDIAuthEvent(GUI::EVT_OPEN_QIDIAUTH, {std::move(url1), std::move(url2)})); } -bool UserAccountCommunication::is_logged() +bool UserAccountCommunication::is_logged() const { return !m_username.empty(); } @@ -376,9 +450,13 @@ void UserAccountCommunication::do_logout() void UserAccountCommunication::do_clear() { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; m_session->clear(); - set_username({}); + set_username({}, true); m_token_timer->Stop(); + m_slave_read_timer->Stop(); + m_after_race_lost_timer->Stop(); + m_next_token_refresh_at = 0; } void UserAccountCommunication::on_login_code_recieved(const std::string& url_message) @@ -411,20 +489,40 @@ void UserAccountCommunication::enqueue_connect_status_action() void UserAccountCommunication::enqueue_test_connection() { if (!m_session->is_initialized()) { - BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; + BOOST_LOG_TRIVIAL(error) << "Connect test endpoint connection failed - Not Logged in."; return; } m_session->enqueue_test_with_refresh(); wakeup_session_thread(); } -void UserAccountCommunication::enqueue_avatar_action(const std::string& url) +void UserAccountCommunication::enqueue_avatar_old_action(const std::string& url) +{ + if (!m_session->is_initialized()) { + BOOST_LOG_TRIVIAL(error) << "Connect avatar endpoint connection failed - Not Logged in."; + return; + } + m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR_OLD, nullptr, nullptr, url); + wakeup_session_thread(); +} + +void UserAccountCommunication::enqueue_avatar_new_action(const std::string& url) { if (!m_session->is_initialized()) { BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; return; } - m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR, nullptr, nullptr, url); + m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR_NEW, nullptr, nullptr, url); + wakeup_session_thread(); +} + +void UserAccountCommunication::enqueue_id_action() +{ + if (!m_session->is_initialized()) { + BOOST_LOG_TRIVIAL(error) << "Connect id endpoint connection failed - Not Logged in."; + return; + } + m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_USER_ID, nullptr, nullptr, {}); wakeup_session_thread(); } @@ -437,12 +535,6 @@ void UserAccountCommunication::enqueue_printer_data_action(const std::string& uu m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID, nullptr, nullptr, uuid); wakeup_session_thread(); } - -void UserAccountCommunication::request_refresh() -{ - m_token_timer->Stop(); - enqueue_refresh(); -} void UserAccountCommunication::enqueue_refresh() { @@ -488,7 +580,7 @@ void UserAccountCommunication::on_activate_app(bool active) m_window_is_active = active; } auto now = std::time(nullptr); - BOOST_LOG_TRIVIAL(info) << "UserAccountCommunication activate: active " << active; + //BOOST_LOG_TRIVIAL(info) << "UserAccountCommunication activate: active " << active; #ifndef _NDEBUG // constexpr auto refresh_threshold = 110 * 60; constexpr auto refresh_threshold = 60; @@ -496,13 +588,22 @@ void UserAccountCommunication::on_activate_app(bool active) constexpr auto refresh_threshold = 60; #endif if (active && m_next_token_refresh_at > 0 && m_next_token_refresh_at - now < refresh_threshold) { - BOOST_LOG_TRIVIAL(info) << "Enqueue access token refresh on activation"; + // Commented during implementation of sharing access token among instances - TODO + BOOST_LOG_TRIVIAL(debug) << " Requesting refresh when app was activated: next token refresh is at " << m_next_token_refresh_at - now; request_refresh(); + return; + } + // When no token timers are running but we have token -> refresh it. + if (active && m_next_token_refresh_at > 0 && m_token_timer->IsRunning() && m_slave_read_timer->IsRunning() && m_after_race_lost_timer->IsRunning()) { + BOOST_LOG_TRIVIAL(debug) << " Requesting refresh when app was activated when no timers are running, next token refresh is at " << m_next_token_refresh_at - now; + request_refresh(); + return; } } void UserAccountCommunication::wakeup_session_thread() { + //BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; { std::lock_guard lck(m_thread_stop_mutex); m_thread_wakeup = true; @@ -514,6 +615,7 @@ void UserAccountCommunication::set_refresh_time(int seconds) { assert(m_token_timer); m_token_timer->Stop(); + m_last_token_duration_seconds = seconds; const auto prior_expiration_secs = std::max(seconds / 24, 10); int milliseconds = std::max((seconds - prior_expiration_secs) * 1000, 1000); m_next_token_refresh_at = std::time(nullptr) + milliseconds / 1000; @@ -521,21 +623,260 @@ void UserAccountCommunication::set_refresh_time(int seconds) m_token_timer->StartOnce(milliseconds); } +void UserAccountCommunication::read_stored_data(UserAccountCommunication::StoreData& result) +{ + if (is_secret_store_ok()) { + std::string key0, tokens; + if (load_secret("tokens", key0, tokens)) { + std::vector token_list; + boost::split(token_list, tokens, boost::is_any_of("|"), boost::token_compress_off); + assert(token_list.empty() || token_list.size() == 4); + if (token_list.size() < 3) { + BOOST_LOG_TRIVIAL(error) << "Size of read secrets is only: " << token_list.size() << " (expected 4). Data: " << tokens; + } + result.access_token = token_list.size() > 0 ? token_list[0] : std::string(); + result.refresh_token = token_list.size() > 1 ? token_list[1] : std::string(); + result.next_timeout = token_list.size() > 2 ? token_list[2] : std::string(); + result.master_pid = token_list.size() > 3 ? token_list[3] : std::string(); + } + result.shared_session_key = key0; + } else { +#ifdef __linux__ + load_tokens_linux(result); +#endif + } +} + +void UserAccountCommunication::request_refresh() +{ + // This function is called when Printables requests new token - same token as we have now wont do. + // Or from UserAccountCommunication::on_activate_app(true) when current token has too small refresh or is dead + // See if there is different token stored, if not - proceed to T3 (there might be more than 1 app doing this). + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + if (m_token_timer->IsRunning()) { + m_token_timer->Stop(); + } + if (m_slave_read_timer->IsRunning()) { + m_slave_read_timer->Stop(); + } + if (m_after_race_lost_timer->IsRunning()) { + m_after_race_lost_timer->Stop(); + } + + std::string current_access_token = m_session->get_access_token(); + StoreData stored_data; + read_stored_data(stored_data); + if (stored_data.refresh_token.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Store is empty - logging out."; + do_logout(); + return; + } + + // Here we need to count with situation when token was renewed in m_session but was not yet stored. + // Then store token is not valid - it should has erlier expiration + long long expires_in_second = stored_data.next_timeout.empty() ? 0 : std::stoll(stored_data.next_timeout) - std::time(nullptr); + BOOST_LOG_TRIVIAL(debug) << "Compare " << expires_in_second << " vs " << m_next_token_refresh_at - std::time(nullptr) << (stored_data.access_token != current_access_token ? " not same" : " same"); + if (stored_data.access_token != current_access_token && expires_in_second > 0 && expires_in_second > m_next_token_refresh_at - std::time(nullptr)) { + BOOST_LOG_TRIVIAL(debug) << "Found usable token. Expires in " << expires_in_second; + set_tokens(stored_data); + } else { + BOOST_LOG_TRIVIAL(debug) << "No new token"; + enqueue_refresh_race(stored_data.refresh_token); + } +} void UserAccountCommunication::on_token_timer(wxTimerEvent& evt) { - BOOST_LOG_TRIVIAL(info) << "UserAccountCommunication: Token refresh timer fired"; - enqueue_refresh(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " T1"; + // Read PID from current stored token and decide if master / slave + + std::string my_pid = std::to_string(get_current_pid()); + StoreData stored_data; + read_stored_data(stored_data); + + if (stored_data.refresh_token.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Store is empty - logging out."; + do_logout(); + return; + } + + long long expires_in_second = stored_data.next_timeout.empty() ? 0 : std::stoll(stored_data.next_timeout) - std::time(nullptr); + if (my_pid == stored_data.master_pid) { + enqueue_refresh(); + return; + } + // token could be either already new -> we want to start using it now + const auto prior_expiration_secs = std::max(m_last_token_duration_seconds / 24, 10); + if (expires_in_second >= 0 && expires_in_second > prior_expiration_secs) { + BOOST_LOG_TRIVIAL(debug) << "Current token has different PID - expiration is " << expires_in_second << " while longest expected was " << prior_expiration_secs << ". Using this token."; + set_tokens(stored_data); + return; + } + // or yet to be renewed -> we should wait to give time to master to renew it + if (expires_in_second >= 0) { + BOOST_LOG_TRIVIAL(debug) << "Current token has different PID - waiting " << expires_in_second / 2; + m_slave_read_timer->StartOnce((expires_in_second / 2) * 1000); + return; + } + // or expired -> renew now. + BOOST_LOG_TRIVIAL(debug) << "Current token has different PID and is expired."; + enqueue_refresh_race(stored_data.refresh_token); } +void UserAccountCommunication::on_slave_read_timer(wxTimerEvent& evt) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " T2"; + std::string current_access_token = m_session->get_access_token(); + StoreData stored_data; + read_stored_data(stored_data); + + if (stored_data.refresh_token.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Store is empty - logging out."; + do_logout(); + return; + } + + long long expires_in_second = stored_data.next_timeout.empty() ? 0 : std::stoll(stored_data.next_timeout) - std::time(nullptr); + if (stored_data.access_token != current_access_token) { + // consider stored_data as renewed token from master + BOOST_LOG_TRIVIAL(debug) << "Token in store seems to be new - using it."; + set_tokens(stored_data); + return; + } + if (stored_data.access_token != current_access_token) { + // token is expired + BOOST_LOG_TRIVIAL(debug) << "Token in store seems to be new but expired - refreshing now."; + enqueue_refresh_race(stored_data.refresh_token); + return; + } + BOOST_LOG_TRIVIAL(debug) <<"No new token, enqueueing refresh (race expected)."; + enqueue_refresh_race(); +} + +void UserAccountCommunication::enqueue_refresh_race(const std::string refresh_token_from_store/* = std::string()*/) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " T3"; + if (!m_session->is_initialized()) { + BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; + return; + } + if (refresh_token_from_store.empty() && m_session->is_enqueued(UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN)) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Token refresh already enqueued, skipping..."; + return; + } + // At this point, last master did not renew the tokens, behave like master + m_session->enqueue_refresh_race(); + wakeup_session_thread(); +} + +void UserAccountCommunication::on_race_lost() +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " T4"; + // race from on_slave_read_timer has been lost + // other instance was faster to renew tokens so refresh token from this app was denied (invalid grant) + // we should read the other token now. + std::string current_access_token = m_session->get_access_token(); + StoreData stored_data; + read_stored_data(stored_data); + + if (stored_data.refresh_token.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Store is empty - logging out."; + do_logout(); + return; + } + + long long expires_in_second = stored_data.next_timeout.empty() ? 0 : std::stoll(stored_data.next_timeout) - std::time(nullptr); + const auto prior_expiration_secs = std::max(m_last_token_duration_seconds / 24, 10); + if (expires_in_second > 0 && expires_in_second > prior_expiration_secs) { + BOOST_LOG_TRIVIAL(debug) << "Token is alive - using it."; + set_tokens(stored_data); + return; + } + BOOST_LOG_TRIVIAL(debug) << "No suitable token found waiting " << std::max((expires_in_second / 2), (long long)2); + m_after_race_lost_timer->StartOnce(std::max((expires_in_second / 2) * 1000, (long long)2000)); +} + +void UserAccountCommunication::on_after_race_lost_timer(wxTimerEvent& evt) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " T5"; + + std::string current_access_token = m_session->get_access_token(); + StoreData stored_data; + read_stored_data(stored_data); + + if (stored_data.refresh_token.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Store is empty - logging out."; + do_logout(); + return; + } + + long long expires_in_second = stored_data.next_timeout.empty() ? 0 : std::stoll(stored_data.next_timeout) - std::time(nullptr); + const auto prior_expiration_secs = std::max(m_last_token_duration_seconds / 24, 10); + if (expires_in_second > 0 && expires_in_second > prior_expiration_secs) { + BOOST_LOG_TRIVIAL(debug) << "Token is alive - using it."; + set_tokens(stored_data); + return; + } + BOOST_LOG_TRIVIAL(warning) << "No new token is stored - This is error state. Logging out."; + do_logout(); +} + +void UserAccountCommunication::set_tokens(const StoreData store_data) +{ + if (m_token_timer->IsRunning()) { + m_token_timer->Stop(); + } + if (m_slave_read_timer->IsRunning()) { + m_slave_read_timer->Stop(); + } + if (m_after_race_lost_timer->IsRunning()) { + m_after_race_lost_timer->Stop(); + } + + long long next = store_data.next_timeout.empty() ? 0 : std::stoll(store_data.next_timeout); + m_session->set_tokens(store_data.access_token, store_data.refresh_token, store_data.shared_session_key, next); + enqueue_id_action(); +} + +void UserAccountCommunication::on_store_read_request() +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + StoreData stored_data; + read_stored_data(stored_data); + + if (stored_data.refresh_token.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Store is empty - logging out."; + do_logout(); + return; + } + + std::string currrent_access_token = m_session->get_access_token(); + if (currrent_access_token == stored_data.access_token) + { + BOOST_LOG_TRIVIAL(debug) << "Current token is up to date."; + return; + } + + long long expires_in_second = stored_data.next_timeout.empty() ? 0 : std::stoll(stored_data.next_timeout) - std::time(nullptr); + const auto prior_expiration_secs = std::max(m_last_token_duration_seconds / 24, 10); + if (expires_in_second > 0 /*&& expires_in_second > prior_expiration_secs*/) { + BOOST_LOG_TRIVIAL(debug) << "Token is alive - using it."; + set_tokens(stored_data); + return; + } else { + BOOST_LOG_TRIVIAL(warning) << "Token read from store is expired!"; + } +} + void UserAccountCommunication::on_polling_timer(wxTimerEvent& evt) { + if (!m_window_is_active) { return; } wakeup_session_thread(); } -std::string CodeChalengeGenerator::generate_chalenge(const std::string& verifier) +std::string CodeChalengeGenerator::generate_challenge(const std::string& verifier) { std::string code_challenge; try @@ -545,7 +886,7 @@ std::string CodeChalengeGenerator::generate_chalenge(const std::string& verifier } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "Code Chalenge Generator failed: " << e.what(); + BOOST_LOG_TRIVIAL(error) << "Code Challenge Generator failed: " << e.what(); } assert(!code_challenge.empty()); return code_challenge; diff --git a/src/slic3r/GUI/UserAccountCommunication.hpp b/src/slic3r/GUI/UserAccountCommunication.hpp index 0ba2652..1150d67 100644 --- a/src/slic3r/GUI/UserAccountCommunication.hpp +++ b/src/slic3r/GUI/UserAccountCommunication.hpp @@ -22,7 +22,7 @@ class CodeChalengeGenerator public: CodeChalengeGenerator() {} ~CodeChalengeGenerator() {} - std::string generate_chalenge(const std::string& verifier); + std::string generate_challenge(const std::string& verifier); std::string generate_verifier(); private: std::string generate_code_verifier(size_t length); @@ -32,13 +32,21 @@ private: class UserAccountCommunication : public wxEvtHandler { +public: + struct StoreData { + std::string access_token; + std::string refresh_token; + std::string shared_session_key; + std::string next_timeout; + std::string master_pid; + }; public: UserAccountCommunication(wxEvtHandler* evt_handler, AppConfig* app_config); ~UserAccountCommunication(); // UI Session thread Interface // - bool is_logged(); + bool is_logged() const; void do_login(); void do_logout(); void do_clear(); @@ -49,7 +57,9 @@ public: // Trigger function starts various remote operations void enqueue_connect_status_action(); void enqueue_connect_printer_models_action(); - void enqueue_avatar_action(const std::string& url); + void enqueue_avatar_old_action(const std::string& url); + void enqueue_avatar_new_action(const std::string& url); + void enqueue_id_action(); void enqueue_test_connection(); void enqueue_printer_data_action(const std::string& uuid); void enqueue_refresh(); @@ -63,7 +73,7 @@ public: void on_activate_app(bool active); - void set_username(const std::string& username); + void set_username(const std::string& username, bool store); void set_remember_session(bool b); bool get_remember_session() const {return m_remember_session; } @@ -78,6 +88,10 @@ public: void set_refresh_time(int seconds); void on_token_timer(wxTimerEvent& evt); void on_polling_timer(wxTimerEvent& evt); + void set_tokens(const StoreData store_data); + + void on_race_lost(); // T4 + void on_store_read_request(); private: std::unique_ptr m_session; std::thread m_thread; @@ -86,7 +100,7 @@ private: bool m_thread_stop { false }; bool m_thread_wakeup{ false }; bool m_window_is_active{ true }; - wxTimer* m_polling_timer; + std::unique_ptr m_polling_timer; std::string m_code_verifier; wxEvtHandler* m_evt_handler; @@ -95,7 +109,7 @@ private: std::string m_username; bool m_remember_session { true }; // if default is true, on every login Remember me will be checked. - wxTimer* m_token_timer; + std::unique_ptr m_token_timer; std::time_t m_next_token_refresh_at{0}; void wakeup_session_thread(); @@ -103,8 +117,15 @@ private: void login_redirect(); std::string client_id() const { return Utils::ServiceConfig::instance().account_client_id(); } + // master / slave logic + std::unique_ptr m_slave_read_timer; // T2 timer + std::unique_ptr m_after_race_lost_timer; // T5 timer + int m_last_token_duration_seconds {0}; - + void on_slave_read_timer(wxTimerEvent& evt); // T2 + void read_stored_data(StoreData& result); + void enqueue_refresh_race(const std::string refresh_token_from_store = std::string()); // T3 + void on_after_race_lost_timer(wxTimerEvent& evt); // T4 }; } } diff --git a/src/slic3r/GUI/UserAccountSession.cpp b/src/slic3r/GUI/UserAccountSession.cpp index f06e710..bfe8f32 100644 --- a/src/slic3r/GUI/UserAccountSession.cpp +++ b/src/slic3r/GUI/UserAccountSession.cpp @@ -21,6 +21,7 @@ namespace GUI { wxDEFINE_EVENT(EVT_OPEN_QIDIAUTH, OpenQIDIAuthEvent); wxDEFINE_EVENT(EVT_UA_LOGGEDOUT, UserAccountSuccessEvent); wxDEFINE_EVENT(EVT_UA_ID_USER_SUCCESS, UserAccountSuccessEvent); +wxDEFINE_EVENT(EVT_UA_ID_USER_SUCCESS_AFTER_TOKEN_SUCCESS, UserAccountSuccessEvent); wxDEFINE_EVENT(EVT_UA_SUCCESS, UserAccountSuccessEvent); wxDEFINE_EVENT(EVT_UA_QIDICONNECT_STATUS_SUCCESS, UserAccountSuccessEvent); wxDEFINE_EVENT(EVT_UA_QIDICONNECT_PRINTER_MODELS_SUCCESS, UserAccountSuccessEvent); @@ -28,33 +29,45 @@ wxDEFINE_EVENT(EVT_UA_AVATAR_SUCCESS, UserAccountSuccessEvent); wxDEFINE_EVENT(EVT_UA_QIDICONNECT_PRINTER_DATA_SUCCESS, UserAccountSuccessEvent); wxDEFINE_EVENT(EVT_UA_FAIL, UserAccountFailEvent); wxDEFINE_EVENT(EVT_UA_RESET, UserAccountFailEvent); +wxDEFINE_EVENT(EVT_UA_RACE_LOST, UserAccountFailEvent); wxDEFINE_EVENT(EVT_UA_QIDICONNECT_PRINTER_DATA_FAIL, UserAccountFailEvent); wxDEFINE_EVENT(EVT_UA_REFRESH_TIME, UserAccountTimeEvent); wxDEFINE_EVENT(EVT_UA_ENQUEUED_REFRESH, SimpleEvent); +wxDEFINE_EVENT(EVT_UA_RETRY_NOTIFY, UserAccountFailEvent); +wxDEFINE_EVENT(EVT_UA_CLOSE_RETRY_NOTIFICATION, SimpleEvent); void UserActionPost::perform(/*UNUSED*/ wxEvtHandler* evt_handler, /*UNUSED*/ const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const { std::string url = m_url; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << url; + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << " " << url; auto http = Http::post(std::move(url)); if (!input.empty()) http.set_post_body(input); http.header("Content-type", "application/x-www-form-urlencoded"); http.on_error([fail_callback](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(trace) << "UserActionPost::perform on_error"; if (fail_callback) fail_callback(body); }); http.on_complete([success_callback](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(trace) << "UserActionPost::perform on_complete"; if (success_callback) success_callback(body); }); + http.on_retry([&](int attempt, unsigned delay) { + BOOST_LOG_TRIVIAL(trace) << "UserActionPost::perform on_retry " << attempt; + if (attempt > 1) { + wxQueueEvent(evt_handler, new UserAccountFailEvent(EVT_UA_RETRY_NOTIFY, GUI::format(_u8L("Communication with QIDI Account is taking longer than expected. Retrying. Attempt %1%."), std::to_string(attempt)))); + } + return true; + }); http.perform_sync(HttpRetryOpt::default_retry()); } void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const { std::string url = m_url + input; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << url; + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << " " << url; auto http = Http::get(std::move(url)); if (!access_token.empty()) { http.header("Authorization", "Bearer " + access_token); @@ -68,6 +81,7 @@ void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::strin #endif } http.on_error([evt_handler, fail_callback, action_name = &m_action_name, fail_evt_type = m_fail_evt_type](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(trace) << "UserActionGetWithEvent::perform on_error"; if (fail_callback) fail_callback(body); std::string message = GUI::format("%1% action failed (%2%): %3%", action_name, std::to_string(status), body); @@ -75,12 +89,19 @@ void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::strin wxQueueEvent(evt_handler, new UserAccountFailEvent(fail_evt_type, std::move(message))); }); http.on_complete([evt_handler, success_callback, succ_evt_type = m_succ_evt_type](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(trace) << "UserActionGetWithEvent::perform on_complete"; if (success_callback) success_callback(body); if (succ_evt_type != wxEVT_NULL) wxQueueEvent(evt_handler, new UserAccountSuccessEvent(succ_evt_type, body)); }); - + http.on_retry([&](int attempt, unsigned delay) { + BOOST_LOG_TRIVIAL(trace) << "UserActionGetWithEvent::perform on_retry " << attempt; + if (attempt > 1) { + wxQueueEvent(evt_handler, new UserAccountFailEvent(EVT_UA_RETRY_NOTIFY, GUI::format(_u8L("Communication with QIDI Account is taking longer than expected. Retrying. Attempt %1%."), std::to_string(attempt)))); + } + return true; + }); http.perform_sync(HttpRetryOpt::default_retry()); } @@ -102,6 +123,7 @@ void UserAccountSession::process_action_queue() std::lock_guard lock(m_session_mutex); if (!m_proccessing_enabled) return; + BOOST_LOG_TRIVIAL(trace) << "action queue: " << m_priority_action_queue.size() << " " << m_action_queue.size(); if (m_priority_action_queue.empty() && m_action_queue.empty()) { // update printers periodically enqueue_action_inner(m_polling_action, nullptr, nullptr, {}); @@ -132,7 +154,8 @@ void UserAccountSession::process_action_queue_inner() } } if (call_priority || call_standard) { - m_actions[selected_data.action_id]->perform(p_evt_handler, get_access_token(), selected_data.success_callback, selected_data.fail_callback, selected_data.input); + bool use_token = m_actions[selected_data.action_id]->get_requires_auth_token(); + m_actions[selected_data.action_id]->perform(p_evt_handler, use_token ? get_access_token() : std::string(), selected_data.success_callback, selected_data.fail_callback, selected_data.input); process_action_queue_inner(); } } @@ -173,11 +196,38 @@ void UserAccountSession::init_with_code(const std::string& code, const std::stri } } +void UserAccountSession::remove_from_queue(UserAccountActionID action_id) +{ + { + std::lock_guard lock(m_session_mutex); + + auto it = std::find_if( + std::begin(m_priority_action_queue), std::end(m_priority_action_queue), + [action_id](const ActionQueueData& item) { return item.action_id == action_id; } + ); + while (it != m_priority_action_queue.end()) + { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + m_priority_action_queue.erase(it); + it = std::find_if( + std::begin(m_priority_action_queue), std::end(m_priority_action_queue), + [action_id](const ActionQueueData& item) { return item.action_id == action_id; } + ); + } + } + +} + void UserAccountSession::token_success_callback(const std::string& body) { // No need to use lock m_session_mutex here - BOOST_LOG_TRIVIAL(debug) << "Access token refreshed"; + // This is here to prevent performing refresh again until USER_ACCOUNT_ACTION_USER_ID_AFTER_TOKEN_SUCCESS is performed. + // If refresh with stored token was enqueued during performing one we are in its success_callback, + // It would fail and prevent USER_ID to write this tokens to store. + remove_from_queue(UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " Access token refreshed"; // Data we need std::string access_token, refresh_token, shared_session_key; try { @@ -210,14 +260,21 @@ void UserAccountSession::token_success_callback(const std::string& body) m_access_token = std::string(); m_refresh_token = std::string(); m_shared_session_key = std::string(); + m_next_token_timeout = 0; } wxQueueEvent(p_evt_handler, new UserAccountFailEvent(EVT_UA_RESET, std::move(msg))); return; } - //BOOST_LOG_TRIVIAL(info) << "access_token: " << access_token; + if (!access_token.empty()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ <<" access_token: " << access_token.substr(0,5) << "..." << access_token.substr(access_token.size()-5); + } else { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ <<" access_token empty!"; + } + //BOOST_LOG_TRIVIAL(info) << __FUNCTION__ <<" access_token: " << access_token; //BOOST_LOG_TRIVIAL(info) << "refresh_token: " << refresh_token; //BOOST_LOG_TRIVIAL(info) << "shared_session_key: " << shared_session_key; + //BOOST_LOG_TRIVIAL(info) << __FUNCTION__ <<" expires_in: " << std::time(nullptr) + expires_in; { std::lock_guard lock(m_credentials_mutex); m_access_token = access_token; @@ -225,10 +282,29 @@ void UserAccountSession::token_success_callback(const std::string& body) m_shared_session_key = shared_session_key; m_next_token_timeout = std::time(nullptr) + expires_in; } - enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_USER_ID, nullptr, nullptr, {}); + enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_USER_ID_AFTER_TOKEN_SUCCESS, nullptr, nullptr, {}); wxQueueEvent(p_evt_handler, new UserAccountTimeEvent(EVT_UA_REFRESH_TIME, expires_in)); } +void UserAccountSession::set_tokens(const std::string& access_token, const std::string& refresh_token, const std::string& shared_session_key, long long expires_in) +{ + if (!access_token.empty()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ <<" access_token: " << access_token.substr(0,5) << "..." << access_token.substr(access_token.size()-5); + } else { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ <<" access_token empty!"; + } + + { + std::lock_guard lock(m_credentials_mutex); + m_access_token = access_token; + m_refresh_token = refresh_token; + m_shared_session_key = shared_session_key; + m_next_token_timeout = /*std::time(nullptr) +*/ expires_in; + } + long long exp = expires_in - std::time(nullptr); + wxQueueEvent(p_evt_handler, new UserAccountTimeEvent(EVT_UA_REFRESH_TIME, exp)); +} + void UserAccountSession::code_exchange_fail_callback(const std::string& body) { @@ -241,6 +317,7 @@ void UserAccountSession::code_exchange_fail_callback(const std::string& body) void UserAccountSession::enqueue_test_with_refresh() { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; { std::lock_guard lock(m_session_mutex); // on test fail - try refresh @@ -252,6 +329,7 @@ void UserAccountSession::enqueue_test_with_refresh() void UserAccountSession::enqueue_refresh(const std::string& body) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; wxQueueEvent(p_evt_handler, new SimpleEvent(EVT_UA_ENQUEUED_REFRESH)); std::string post_fields; { @@ -280,6 +358,33 @@ void UserAccountSession::refresh_fail_callback(const std::string& body) wxQueueEvent(p_evt_handler, new UserAccountFailEvent(EVT_UA_RESET, body)); } +void UserAccountSession::enqueue_refresh_race(const std::string refresh_token_from_store/* = std::string()*/) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + wxQueueEvent(p_evt_handler, new SimpleEvent(EVT_UA_ENQUEUED_REFRESH)); + std::string post_fields; + { + std::lock_guard lock(m_credentials_mutex); + assert(!m_refresh_token.empty()); + post_fields = "grant_type=refresh_token" + "&client_id=" + client_id() + + "&refresh_token=" + (refresh_token_from_store.empty() ? m_refresh_token :refresh_token_from_store); + } + { + std::lock_guard lock(m_session_mutex); + m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN + , std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1) + , std::bind(&UserAccountSession::refresh_fail_soft_callback, this, std::placeholders::_1) + , post_fields }); + } +} + +void UserAccountSession::refresh_fail_soft_callback(const std::string& body) +{ + cancel_queue(); + wxQueueEvent(p_evt_handler, new UserAccountFailEvent(EVT_UA_RACE_LOST, body)); +} + void UserAccountSession::cancel_queue() { { diff --git a/src/slic3r/GUI/UserAccountSession.hpp b/src/slic3r/GUI/UserAccountSession.hpp index c186c0d..a5af3bc 100644 --- a/src/slic3r/GUI/UserAccountSession.hpp +++ b/src/slic3r/GUI/UserAccountSession.hpp @@ -21,6 +21,7 @@ using UserAccountTimeEvent = Event; wxDECLARE_EVENT(EVT_OPEN_QIDIAUTH, OpenQIDIAuthEvent); wxDECLARE_EVENT(EVT_UA_LOGGEDOUT, UserAccountSuccessEvent); wxDECLARE_EVENT(EVT_UA_ID_USER_SUCCESS, UserAccountSuccessEvent); +wxDECLARE_EVENT(EVT_UA_ID_USER_SUCCESS_AFTER_TOKEN_SUCCESS, UserAccountSuccessEvent); wxDECLARE_EVENT(EVT_UA_SUCCESS, UserAccountSuccessEvent); wxDECLARE_EVENT(EVT_UA_QIDICONNECT_STATUS_SUCCESS, UserAccountSuccessEvent); wxDECLARE_EVENT(EVT_UA_QIDICONNECT_PRINTER_MODELS_SUCCESS, UserAccountSuccessEvent); @@ -28,9 +29,12 @@ wxDECLARE_EVENT(EVT_UA_AVATAR_SUCCESS, UserAccountSuccessEvent); wxDECLARE_EVENT(EVT_UA_QIDICONNECT_PRINTER_DATA_SUCCESS, UserAccountSuccessEvent); wxDECLARE_EVENT(EVT_UA_FAIL, UserAccountFailEvent); // Soft fail - clears only after some number of fails wxDECLARE_EVENT(EVT_UA_RESET, UserAccountFailEvent); // Hard fail - clears all +wxDECLARE_EVENT(EVT_UA_RACE_LOST, UserAccountFailEvent); // Hard fail - clears all wxDECLARE_EVENT(EVT_UA_QIDICONNECT_PRINTER_DATA_FAIL, UserAccountFailEvent); // Failed to get data for printer to select, soft fail, action does not repeat wxDECLARE_EVENT(EVT_UA_REFRESH_TIME, UserAccountTimeEvent); wxDECLARE_EVENT(EVT_UA_ENQUEUED_REFRESH, SimpleEvent); +wxDECLARE_EVENT(EVT_UA_RETRY_NOTIFY, UserAccountFailEvent); // Not fail yet, just retry attempt. string is message to ui. +wxDECLARE_EVENT(EVT_UA_CLOSE_RETRY_NOTIFICATION, SimpleEvent); typedef std::function UserActionSuccessFn; typedef std::function UserActionFailFn; @@ -41,32 +45,35 @@ enum class UserAccountActionID { USER_ACCOUNT_ACTION_REFRESH_TOKEN, USER_ACCOUNT_ACTION_CODE_FOR_TOKEN, USER_ACCOUNT_ACTION_USER_ID, + USER_ACCOUNT_ACTION_USER_ID_AFTER_TOKEN_SUCCESS, USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN, USER_ACCOUNT_ACTION_TEST_CONNECTION, USER_ACCOUNT_ACTION_CONNECT_STATUS, // status of all printers by UUID USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS, // status of all printers by UUID with printer_model. Should be called once to save printer models. - USER_ACCOUNT_ACTION_AVATAR, + USER_ACCOUNT_ACTION_AVATAR_OLD, + USER_ACCOUNT_ACTION_AVATAR_NEW, USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID, }; class UserAction { public: - UserAction(const std::string name, const std::string url) : m_action_name(name), m_url(url){} + UserAction(const std::string name, const std::string url, bool requires_auth_token) : m_action_name(name), m_url(url), m_requires_auth_token(requires_auth_token){} virtual ~UserAction() = default; virtual void perform(wxEvtHandler* evt_handler, const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const = 0; - + bool get_requires_auth_token() { return m_requires_auth_token; } protected: std::string m_action_name; std::string m_url; + bool m_requires_auth_token; }; class UserActionGetWithEvent : public UserAction { public: - UserActionGetWithEvent(const std::string name, const std::string url, wxEventType succ_event_type, wxEventType fail_event_type) + UserActionGetWithEvent(const std::string name, const std::string url, wxEventType succ_event_type, wxEventType fail_event_type, bool requires_auth_token = true) : m_succ_evt_type(succ_event_type) , m_fail_evt_type(fail_event_type) - , UserAction(name, url) + , UserAction(name, url, requires_auth_token) {} ~UserActionGetWithEvent() {} void perform(wxEvtHandler* evt_handler, const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const override; @@ -78,7 +85,7 @@ private: class UserActionPost : public UserAction { public: - UserActionPost(const std::string name, const std::string url) : UserAction(name, url) {} + UserActionPost(const std::string name, const std::string url, bool requires_auth_token = true) : UserAction(name, url, requires_auth_token) {} ~UserActionPost() {} void perform(wxEvtHandler* evt_handler, const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const override; }; @@ -86,7 +93,7 @@ public: class DummyUserAction : public UserAction { public: - DummyUserAction() : UserAction("Dummy", {}) {} + DummyUserAction() : UserAction("Dummy", {}, false) {} ~DummyUserAction() {} void perform(wxEvtHandler* evt_handler, const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const override { } }; @@ -102,11 +109,12 @@ struct ActionQueueData class UserAccountSession { public: - UserAccountSession(wxEvtHandler* evt_handler, const std::string& access_token, const std::string& refresh_token, const std::string& shared_session_key, bool polling_enabled) + UserAccountSession(wxEvtHandler* evt_handler, const std::string& access_token, const std::string& refresh_token, const std::string& shared_session_key, long long next_token_timeout, bool polling_enabled) : p_evt_handler(evt_handler) , m_access_token(access_token) , m_refresh_token(refresh_token) , m_shared_session_key(shared_session_key) + , m_next_token_timeout(next_token_timeout) , m_polling_action(polling_enabled ? UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS : UserAccountActionID::USER_ACCOUNT_ACTION_DUMMY) { @@ -117,11 +125,13 @@ public: m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN] = std::make_unique("EXCHANGE_TOKENS", sc.account_token_url()); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN] = std::make_unique("EXCHANGE_TOKENS", sc.account_token_url()); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_USER_ID] = std::make_unique("USER_ID", sc.account_me_url(), EVT_UA_ID_USER_SUCCESS, EVT_UA_RESET); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_USER_ID_AFTER_TOKEN_SUCCESS] = std::make_unique("USER_ID_AFTER_TOKEN_SUCCESS", sc.account_me_url(), EVT_UA_ID_USER_SUCCESS_AFTER_TOKEN_SUCCESS, EVT_UA_RESET); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN] = std::make_unique("TEST_ACCESS_TOKEN", sc.account_me_url(), EVT_UA_ID_USER_SUCCESS, EVT_UA_FAIL); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_CONNECTION] = std::make_unique("TEST_CONNECTION", sc.account_me_url(), wxEVT_NULL, EVT_UA_RESET); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS] = std::make_unique("CONNECT_STATUS", sc.connect_status_url(), EVT_UA_QIDICONNECT_STATUS_SUCCESS, EVT_UA_FAIL); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS] = std::make_unique("CONNECT_PRINTER_MODELS", sc.connect_printer_list_url(), EVT_UA_QIDICONNECT_PRINTER_MODELS_SUCCESS, EVT_UA_FAIL); - m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR] = std::make_unique("AVATAR", sc.media_url(), EVT_UA_AVATAR_SUCCESS, EVT_UA_FAIL); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR_OLD] = std::make_unique("AVATAR", sc.media_url(), EVT_UA_AVATAR_SUCCESS, EVT_UA_FAIL, false); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR_NEW] = std::make_unique("AVATAR", std::string(), EVT_UA_AVATAR_SUCCESS, EVT_UA_FAIL, false); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID] = std::make_unique("USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID", sc.connect_printers_url(), EVT_UA_QIDICONNECT_PRINTER_DATA_SUCCESS, EVT_UA_QIDICONNECT_PRINTER_DATA_FAIL); } ~UserAccountSession() @@ -133,7 +143,8 @@ public: m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN].reset(nullptr); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_CONNECTION].reset(nullptr); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS].reset(nullptr); - m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR].reset(nullptr); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR_OLD].reset(nullptr); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR_NEW].reset(nullptr); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID].reset(nullptr); } void clear() { @@ -155,6 +166,7 @@ public: // Special enques, that sets callbacks. void enqueue_test_with_refresh(); void enqueue_refresh(const std::string& body); + void enqueue_refresh_race(const std::string refresh_token_from_store = std::string()); void process_action_queue(); bool is_initialized() const { @@ -179,19 +191,23 @@ public: return m_next_token_timeout; } - //void set_polling_enabled(bool enabled) {m_polling_action = enabled ? UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS : UserAccountActionID::USER_ACCOUNT_ACTION_DUMMY; } + void set_tokens(const std::string& access_token, const std::string& refresh_token, const std::string& shared_session_key, long long expires_in); + void set_polling_action(UserAccountActionID action) { std::lock_guard lock(m_session_mutex); m_polling_action = action; } private: void refresh_fail_callback(const std::string& body); + void refresh_fail_soft_callback(const std::string& body); void cancel_queue(); void code_exchange_fail_callback(const std::string& body); void token_success_callback(const std::string& body); std::string client_id() const { return Utils::ServiceConfig::instance().account_client_id(); } void process_action_queue_inner(); + void remove_from_queue(UserAccountActionID action_id); + // called from m_session_mutex protected code only void enqueue_action_inner(UserAccountActionID id, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input); diff --git a/src/slic3r/GUI/UserAccountUtils.cpp b/src/slic3r/GUI/UserAccountUtils.cpp index 8b0b6db..22d8583 100644 --- a/src/slic3r/GUI/UserAccountUtils.cpp +++ b/src/slic3r/GUI/UserAccountUtils.cpp @@ -1,5 +1,8 @@ #include "UserAccountUtils.hpp" +#include "libslic3r/Preset.hpp" +#include "slic3r/GUI/Field.hpp" +#include "slic3r/GUI/GUI.hpp" #include #include #include @@ -38,10 +41,10 @@ pt::ptree parse_tree_for_subtree(const pt::ptree& tree, const std::string& param return pt::ptree(); } -void json_to_ptree(boost::property_tree::ptree& ptree, const std::string& json) { +void json_to_ptree(boost::property_tree::ptree& out_ptree, const std::string& json) { try { std::stringstream ss(json); - pt::read_json(ss, ptree); + pt::read_json(ss, out_ptree); } catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << "Failed to parse json to ptree. " << e.what(); BOOST_LOG_TRIVIAL(error) << "json: " << json; @@ -50,7 +53,7 @@ void json_to_ptree(boost::property_tree::ptree& ptree, const std::string& json) } // namespace -std::string get_nozzle_from_json(boost::property_tree::ptree& ptree) { +std::string get_nozzle_from_json(const boost::property_tree::ptree& ptree) { assert(!ptree.empty()); std::string out = parse_tree_for_param(ptree, "nozzle_diameter"); @@ -74,7 +77,7 @@ std::string get_keyword_from_json(boost::property_tree::ptree& ptree, const std: return parse_tree_for_param(ptree, keyword); } -void fill_supported_printer_models_from_json(boost::property_tree::ptree& ptree, std::vector& result) +void fill_supported_printer_models_from_json(const boost::property_tree::ptree& ptree, std::vector& result) { assert(!ptree.empty()); std::string printer_model = parse_tree_for_param(ptree, "printer_model"); @@ -103,13 +106,20 @@ std::string json_var_to_opt_string(const std::string& json_var) return json_var; } -void fill_config_options_from_json_inner(boost::property_tree::ptree& ptree, std::map>& result, const std::map& parameters) +void fill_config_options_from_json_inner(const boost::property_tree::ptree& ptree, std::map>& result, const std::map& parameters) { - pt::ptree slots = parse_tree_for_subtree(parse_tree_for_subtree(ptree, "slot"), "slots"); + pt::ptree slots = parse_tree_for_subtree(ptree, "tools"); for (const auto &subtree : slots) { size_t slot_id; try { - slot_id = boost::lexical_cast(subtree.first); + // id could "1" for extruder + // or "1.1" for MMU (than we need number after dot as id) + size_t dot_pos = subtree.first.find('.'); + if (dot_pos != std::string::npos) { + slot_id = boost::lexical_cast(subtree.first.substr(dot_pos + 1)); + } else { + slot_id = boost::lexical_cast(subtree.first); + } } catch (const boost::bad_lexical_cast&) { continue; } @@ -139,7 +149,7 @@ void fill_config_options_from_json_inner(boost::property_tree::ptree& ptree, std } } -void fill_config_options_from_json(boost::property_tree::ptree& ptree, std::map>& result) +void fill_config_options_from_json(const boost::property_tree::ptree& ptree, std::map>& result) { assert(!ptree.empty()); /* @@ -232,7 +242,7 @@ void fill_material_from_json(const std::string& json, std::vector& // if not found, find "filament" subtree // find "slot" subtree - pt::ptree slot_subtree = parse_tree_for_subtree(ptree, "slot"); + pt::ptree slot_subtree = parse_tree_for_subtree(ptree, "tools"); if (slot_subtree.empty()) { // if not found, find "filament" subtree pt::ptree filament_subtree = parse_tree_for_subtree(ptree, "filament"); @@ -282,6 +292,12 @@ void fill_material_from_json(const std::string& json, std::vector& for (const std::string& val : result_map["hardened"]) { avoid_abrasive_result.emplace_back(val == "0" ? 1 : 0); } + // MMU has "hardened" only under tool 1 + if (avoid_abrasive_result.size() == 1 && material_result.size() > avoid_abrasive_result.size()) { + for (size_t i = 1; i < material_result.size(); i++) { + avoid_abrasive_result.emplace_back(avoid_abrasive_result[0]); + } + } } } @@ -320,6 +336,68 @@ std::string get_print_data_from_json(const std::string& json, const std::string& return result; } +const Preset* find_preset_by_nozzle_and_options( + const PrinterPresetCollection& collection + , const std::string& model_id + , std::map>& options) +{ + // find all matching presets when repo prefix is omitted + std::vector results; + for (const Preset &preset : collection) { + // trim repo prefix + std::string printer_model = preset.config.opt_string("printer_model"); + const PresetWithVendorProfile& printer_with_vendor = collection.get_preset_with_vendor_profile(preset); + printer_model = preset.trim_vendor_repo_prefix(printer_model, printer_with_vendor.vendor); + + if (!preset.is_system || printer_model != model_id) + continue; + // options (including nozzle_diameter) + bool failed = false; + for (const auto& opt : options) { + assert(preset.config.has(opt.first)); + // We compare only first value now, but options contains data for all (some might be empty tho) + std::string opt_val; + if (preset.config.option(opt.first)->is_scalar()) { + opt_val = preset.config.option(opt.first)->serialize(); + } else { + switch (preset.config.option(opt.first)->type()) { + case coInts: opt_val = std::to_string(static_cast(preset.config.option(opt.first))->values[0]); break; + case coFloats: + opt_val = GUI::into_u8(double_to_string(static_cast(preset.config.option(opt.first))->values[0])); + if (size_t pos = opt_val.find(",") != std::string::npos) + opt_val.replace(pos, 1, 1, '.'); + break; + case coStrings: opt_val = static_cast(preset.config.option(opt.first))->values[0]; break; + case coBools: opt_val = static_cast(preset.config.option(opt.first))->values[0] ? "1" : "0"; break; + default: + assert(false); + continue; + } + } + + if (opt_val != opt.second[0]) + { + failed = true; + break; + } + } + if (!failed) { + results.push_back(&preset); + } + + } + // find one without prefix + for (const Preset* preset : results) { + if (preset->config.opt_string("printer_model") == model_id) { + return preset; + } + } + if (results.size() != 0) { + return results.front(); + } + return nullptr; +} + }}} // Slic3r::GUI::UserAccountUtils diff --git a/src/slic3r/GUI/UserAccountUtils.hpp b/src/slic3r/GUI/UserAccountUtils.hpp index d98e2d0..3678829 100644 --- a/src/slic3r/GUI/UserAccountUtils.hpp +++ b/src/slic3r/GUI/UserAccountUtils.hpp @@ -7,22 +7,26 @@ #include -namespace Slic3r { namespace GUI { +namespace Slic3r { +class Preset; +class PrinterPresetCollection; +namespace GUI { namespace UserAccountUtils { // If ptree parameter is empty, json parameter needs to contain data and ptree is filled. // If ptree is non-epty, json parameter is not used. std::string get_keyword_from_json(boost::property_tree::ptree& ptree, const std::string& json, const std::string& keyword); // Only ptree is passed since these functions are called on places that already has the ptree from get_keyword_from_json call -std::string get_nozzle_from_json(boost::property_tree::ptree &ptree); -void fill_supported_printer_models_from_json(boost::property_tree::ptree& ptree, std::vector& result); -void fill_config_options_from_json(boost::property_tree::ptree& ptree, std::map>& result); +std::string get_nozzle_from_json(const boost::property_tree::ptree &ptree); +void fill_supported_printer_models_from_json(const boost::property_tree::ptree& ptree, std::vector& result); +void fill_config_options_from_json(const boost::property_tree::ptree& ptree, std::map>& result); // Since fill_material_from_json is called only from one place where ptree doesnt need to be shared, it is not always read from json. void fill_material_from_json(const std::string& json, std::vector& material_result, std::vector& avoid_abrasive_result); std::string get_print_data_from_json(const std::string &json, const std::string &keyword); +const Preset* find_preset_by_nozzle_and_options( const PrinterPresetCollection& collection, const std::string& model_id, std::map>& options); }}} // Slic3r::GUI::UserAccountUtils #endif // slic3r_UserAccountUtils_hpp_ diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index 734f391..3545f07 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -451,13 +451,14 @@ void WebViewDialog::EndModal(int retCode) wxDialog::EndModal(retCode); } -PrinterPickWebViewDialog::PrinterPickWebViewDialog(wxWindow* parent, std::string& ret_val) +PrinterPickWebViewDialog::PrinterPickWebViewDialog(wxWindow* parent, std::string& ret_val, bool multiple_beds) : WebViewDialog(parent , GUI::from_u8(Utils::ServiceConfig::instance().connect_select_printer_url()) , _L("Choose a printer") , wxSize(parent->GetClientSize().x / 4 * 3, parent->GetClientSize().y/ 4 * 3) ,{"_qidiSlicer"} , "connect_loading") + , m_multiple_beds(multiple_beds) , m_ret_val(ret_val) { @@ -579,7 +580,9 @@ void PrinterPickWebViewDialog::request_compatible_printers_FFF() { const std::string uuid = wxGetApp().plater()->get_user_account()->get_current_printer_uuid_from_connect(printer_model_serialized); const std::string filename = wxGetApp().plater()->get_upload_filename(); - //filament_abrasive + + const std::string multiple_beds_value = m_multiple_beds ? "true" : "false"; + std::string request = GUI::format( "{" "\"printerUuid\": \"%4%\", " @@ -588,9 +591,10 @@ void PrinterPickWebViewDialog::request_compatible_printers_FFF() { "\"material\": %1%, " "\"filename\": \"%5%\", " "\"filament_abrasive\": %6%," - "\"high_flow\": %7%" + "\"high_flow\": %7%," + "\"multiple_beds\": %8%" "}" - , filament_type_serialized, nozzle_diameter_serialized, printer_model_serialized, uuid, filename, filament_abrasive_serialized, nozzle_high_flow_serialized); + , filament_type_serialized, nozzle_diameter_serialized, printer_model_serialized, uuid, filename, filament_abrasive_serialized, nozzle_high_flow_serialized, multiple_beds_value); wxString script = GUI::format_wxstr("window._qidiConnect_v2.requestCompatiblePrinter(%1%)", request); run_script(script); @@ -608,13 +612,15 @@ void PrinterPickWebViewDialog::request_compatible_printers_SLA() const std::string material_type_serialized = selected_material.config.option("material_type")->serialize(); const std::string uuid = wxGetApp().plater()->get_user_account()->get_current_printer_uuid_from_connect(printer_model_serialized); const std::string filename = wxGetApp().plater()->get_upload_filename(); + const std::string multiple_beds_value = m_multiple_beds ? "true" : "false"; const std::string request = GUI::format( "{" "\"printerUuid\": \"%3%\", " "\"material\": \"%1%\", " "\"printerModel\": \"%2%\", " - "\"filename\": \"%4%\" " - "}", material_type_serialized, printer_model_serialized, uuid, filename); + "\"filename\": \"%4%\", " + "\"multiple_beds\": \"%5%\" " + "}", material_type_serialized, printer_model_serialized, uuid, filename, multiple_beds_value); wxString script = GUI::format_wxstr("window._qidiConnect_v2.requestCompatiblePrinter(%1%)", request); run_script(script); @@ -692,19 +698,28 @@ LoginWebViewDialog::LoginWebViewDialog(wxWindow *parent, std::string &ret_val, c , m_ret_val(ret_val) , p_evt_handler(evt_handler) { + m_force_quit_timer.SetOwner(this, 0); + Bind(wxEVT_TIMER, [this](wxTimerEvent &evt) + { + m_force_quit = true; + }); Centre(); } void LoginWebViewDialog::on_navigation_request(wxWebViewEvent &evt) { wxString url = evt.GetURL(); if (url.starts_with(L"qidislicer")) { - delete_cookies(m_browser, Utils::ServiceConfig::instance().account_url()); - delete_cookies(m_browser, "https://accounts.google.com"); - delete_cookies(m_browser, "https://appleid.apple.com"); - delete_cookies(m_browser, "https://facebook.com"); + m_waiting_for_counters = true; + m_atomic_counter = 0; + m_counter_to_match = 4; + delete_cookies_with_counter(m_browser, Utils::ServiceConfig::instance().account_url(), m_atomic_counter); + delete_cookies_with_counter(m_browser, "https://accounts.google.com", m_atomic_counter); + delete_cookies_with_counter(m_browser, "https://appleid.apple.com", m_atomic_counter); + delete_cookies_with_counter(m_browser, "https://facebook.com", m_atomic_counter); evt.Veto(); m_ret_val = into_u8(url); - EndModal(wxID_OK); + m_force_quit_timer.Start(2000, wxTIMER_ONE_SHOT); + // End modal is moved to on_idle } else if (url.Find(L"accounts.google.com") != wxNOT_FOUND || url.Find(L"appleid.apple.com") != wxNOT_FOUND || url.Find(L"facebook.com") != wxNOT_FOUND) { @@ -725,5 +740,36 @@ void LoginWebViewDialog::on_dpi_changed(const wxRect &suggested_rect) Fit(); Refresh(); } + +void LoginWebViewDialog::on_idle(wxIdleEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + if (m_browser->IsBusy()) { + if constexpr (!is_linux) { + wxSetCursor(wxCURSOR_ARROWWAIT); + } + } else { + if constexpr (!is_linux) { + wxSetCursor(wxNullCursor); + } + if (m_load_error_page) { + m_load_error_page = false; + m_browser->LoadURL(GUI::format_wxstr("file://%1%/web/error_no_reload.html", boost::filesystem::path(resources_dir()).generic_string())); + } + if (m_waiting_for_counters && m_atomic_counter == m_counter_to_match) + { + EndModal(wxID_OK); + } + if (m_force_quit) + { + EndModal(wxID_OK); + } + } +#ifdef DEBUG_URL_PANEL + m_button_stop->Enable(m_browser->IsBusy()); +#endif +} + } // GUI } // Slic3r diff --git a/src/slic3r/GUI/WebViewDialog.hpp b/src/slic3r/GUI/WebViewDialog.hpp index e7d4c52..07de8ba 100644 --- a/src/slic3r/GUI/WebViewDialog.hpp +++ b/src/slic3r/GUI/WebViewDialog.hpp @@ -13,6 +13,8 @@ #include #endif +#include + class wxWebView; class wxWebViewEvent; @@ -30,7 +32,7 @@ public: virtual void on_show(wxShowEvent& evt) {}; virtual void on_script_message(wxWebViewEvent& evt); - void on_idle(wxIdleEvent& evt); + virtual void on_idle(wxIdleEvent& evt); void on_url(wxCommandEvent& evt); void on_back_button(wxCommandEvent& evt); void on_forward_button(wxCommandEvent& evt); @@ -93,7 +95,7 @@ protected: class PrinterPickWebViewDialog : public WebViewDialog, public ConnectRequestHandler { public: - PrinterPickWebViewDialog(wxWindow* parent, std::string& ret_val); + PrinterPickWebViewDialog(wxWindow* parent, std::string& ret_val, bool multiple_beds); void on_show(wxShowEvent& evt) override; void on_script_message(wxWebViewEvent& evt) override; protected: @@ -108,6 +110,7 @@ protected: void on_connect_action_close_dialog(const std::string& message_data) override {assert(false);} private: std::string& m_ret_val; + bool m_multiple_beds; }; class PrintablesConnectUploadDialog : public WebViewDialog, public ConnectRequestHandler @@ -132,10 +135,17 @@ public: LoginWebViewDialog(wxWindow *parent, std::string &ret_val, const wxString& url, wxEvtHandler* evt_handler); void on_navigation_request(wxWebViewEvent &evt) override; void on_dpi_changed(const wxRect &suggested_rect) override; + void on_idle(wxIdleEvent& evt) override; private: std::string& m_ret_val; wxEvtHandler* p_evt_handler; bool m_evt_sent{ false }; + + bool m_waiting_for_counters {false}; + std::atomic m_atomic_counter; + size_t m_counter_to_match; + wxTimer m_force_quit_timer; + bool m_force_quit{false}; }; } // GUI diff --git a/src/slic3r/GUI/WebViewPanel.cpp b/src/slic3r/GUI/WebViewPanel.cpp index 53348df..7e4959d 100644 --- a/src/slic3r/GUI/WebViewPanel.cpp +++ b/src/slic3r/GUI/WebViewPanel.cpp @@ -232,6 +232,10 @@ void WebViewPanel::on_show(wxShowEvent& evt) return; } + if (m_after_show_func_prohibited_once) { + m_after_show_func_prohibited_once = false; + return; + } after_on_show(evt); } @@ -422,7 +426,7 @@ void WebViewPanel::run_script(const wxString& javascript) // Remember the script we run in any case, so the next time the user opens // the "Run Script" dialog box, it is shown there for convenient updating. m_javascript = javascript; - BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; + BOOST_LOG_TRIVIAL(trace) << "RunScript " << javascript << "\n"; m_browser->RunScriptAsync(javascript); } @@ -595,9 +599,10 @@ void WebViewPanel::sys_color_changed() void WebViewPanel::on_app_quit_event(const std::string& message_data) { // MacOS only suplement for cmd+Q - wxGetApp().Exit(); + if (wxGetApp().mainframe) { + wxGetApp().mainframe->Close(); + } } - void WebViewPanel::on_app_minimize_event(const std::string& message_data) { // MacOS only suplement for cmd+M @@ -614,6 +619,7 @@ ConnectWebViewPanel::ConnectWebViewPanel(wxWindow* parent) auto* plater = wxGetApp().plater(); plater->Bind(EVT_UA_LOGGEDOUT, &ConnectWebViewPanel::on_user_logged_out, this); plater->Bind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this); + plater->Bind(EVT_UA_ID_USER_SUCCESS_AFTER_TOKEN_SUCCESS, &ConnectWebViewPanel::on_user_token, this); m_actions["appQuit"] = std::bind(&WebViewPanel::on_app_quit_event, this, std::placeholders::_1); m_actions["appMinimize"] = std::bind(&WebViewPanel::on_app_minimize_event, this, std::placeholders::_1); @@ -640,6 +646,7 @@ void ConnectWebViewPanel::late_create() void ConnectWebViewPanel::on_user_token(UserAccountSuccessEvent& e) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; e.Skip(); if (!m_browser) { return; @@ -659,6 +666,7 @@ ConnectWebViewPanel::~ConnectWebViewPanel() wxString ConnectWebViewPanel::get_login_script(bool refresh) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; Plater* plater = wxGetApp().plater(); const std::string& access_token = plater->get_user_account()->get_access_token(); assert(!access_token.empty()); @@ -809,6 +817,7 @@ void ConnectWebViewPanel::on_page_will_load() if (!m_browser) { return; } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; auto javascript = get_login_script(false); BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; m_browser->AddUserScript(javascript); @@ -858,6 +867,7 @@ void ConnectWebViewPanel::on_navigation_request(wxWebViewEvent &evt) void ConnectWebViewPanel::on_connect_action_error(const std::string &message_data) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; ConnectRequestHandler::on_connect_action_error(message_data); // TODO: make this more user friendly (and make sure only once opened if multiple errors happen) // MessageDialog dialog( @@ -872,6 +882,7 @@ void ConnectWebViewPanel::on_connect_action_error(const std::string &message_dat void ConnectWebViewPanel::on_reload_event(const std::string& message_data) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; // Event from our error page button or keyboard shortcut m_styles_defined = false; try { @@ -890,6 +901,12 @@ void ConnectWebViewPanel::on_reload_event(const std::string& message_data) } } +void ConnectWebViewPanel::after_on_show(wxShowEvent& evt) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + run_script("window.location.reload();"); +} + void ConnectWebViewPanel::logout() { if (!m_browser || m_do_late_webview_create) { @@ -1150,6 +1167,7 @@ PrintablesWebViewPanel::PrintablesWebViewPanel(wxWindow* parent) m_events["reloadHomePage"] = std::bind(&PrintablesWebViewPanel::on_reload_event, this, std::placeholders::_1); m_events["appQuit"] = std::bind(&WebViewPanel::on_app_quit_event, this, std::placeholders::_1); m_events["appMinimize"] = std::bind(&WebViewPanel::on_app_minimize_event, this, std::placeholders::_1); + m_events["ready"] = std::bind(&PrintablesWebViewPanel::on_printables_event_dummy, this, std::placeholders::_1); } void PrintablesWebViewPanel::handle_message(const std::string& message) @@ -1375,6 +1393,7 @@ void PrintablesWebViewPanel::send_refreshed_token(const std::string& access_toke if (m_load_default_url) { return; } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; hide_loading_overlay(); wxString script = GUI::format_wxstr("window.postMessage(JSON.stringify({" "event: 'accessTokenChange'," @@ -1388,6 +1407,7 @@ void PrintablesWebViewPanel::send_will_refresh() if (m_load_default_url) { return; } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; wxString script = "window.postMessage(JSON.stringify({ event: 'accessTokenWillChange' }))"; run_script(script); } diff --git a/src/slic3r/GUI/WebViewPanel.hpp b/src/slic3r/GUI/WebViewPanel.hpp index 4d3326c..eccaab7 100644 --- a/src/slic3r/GUI/WebViewPanel.hpp +++ b/src/slic3r/GUI/WebViewPanel.hpp @@ -76,6 +76,7 @@ public: void on_app_quit_event(const std::string& message_data); void on_app_minimize_event(const std::string& message_data); + void prohibit_after_show_func_once() {m_after_show_func_prohibited_once = true; } protected: virtual void late_create(); virtual void define_css(); @@ -112,6 +113,7 @@ protected: wxString m_response_js; wxString m_default_url; bool m_reached_default_url {false}; + bool m_after_show_func_prohibited_once {false}; std::string m_loading_html; std::string m_error_html; @@ -148,6 +150,7 @@ protected: void on_connect_action_close_dialog(const std::string& message_data) override {assert(false);} void on_user_token(UserAccountSuccessEvent& e); void define_css() override; + void after_on_show(wxShowEvent& evt) override; private: static wxString get_login_script(bool refresh); static wxString get_logout_script(); @@ -210,6 +213,7 @@ protected: void define_css() override; private: void handle_message(const std::string& message); + void on_printables_event_dummy(const std::string& message_data) {} void on_printables_event_access_token_expired(const std::string& message_data); void on_reload_event(const std::string& message_data); void on_printables_event_print_gcode(const std::string& message_data); @@ -217,6 +221,7 @@ private: void on_printables_event_slice_file(const std::string& message_data); void on_printables_event_required_login(const std::string& message_data); void on_printables_event_open_url(const std::string& message_data); + void on_dummy_event(const std::string& message_data) {} void load_default_url() override; std::string get_url_lang_theme(const wxString& url) const; void show_download_notification(const std::string& filename); diff --git a/src/slic3r/GUI/WebViewPlatformUtils.hpp b/src/slic3r/GUI/WebViewPlatformUtils.hpp index f6d5a8b..c484516 100644 --- a/src/slic3r/GUI/WebViewPlatformUtils.hpp +++ b/src/slic3r/GUI/WebViewPlatformUtils.hpp @@ -2,11 +2,13 @@ #include #include +#include namespace Slic3r::GUI { void setup_webview_with_credentials(wxWebView* web_view, const std::string& username, const std::string& password); void remove_webview_credentials(wxWebView* web_view); void delete_cookies(wxWebView* web_view, const std::string& url); + void delete_cookies_with_counter(wxWebView* web_view, const std::string& url, std::atomic& counter); void add_request_authorization(wxWebView* web_view, const wxString& address, const std::string& token); void remove_request_authorization(wxWebView* web_view); void load_request(wxWebView* web_view, const std::string& address, const std::string& token); diff --git a/src/slic3r/GUI/WebViewPlatformUtilsLinux.cpp b/src/slic3r/GUI/WebViewPlatformUtilsLinux.cpp index f5ffd8b..66ad4fc 100644 --- a/src/slic3r/GUI/WebViewPlatformUtilsLinux.cpp +++ b/src/slic3r/GUI/WebViewPlatformUtilsLinux.cpp @@ -109,6 +109,12 @@ void delete_cookies(wxWebView* web_view, const std::string& url) webkit_cookie_manager_get_cookies(cookieManager, uri, nullptr, (GAsyncReadyCallback)Slic3r::GUI::get_cookie_callback, nullptr); } +void delete_cookies_with_counter(wxWebView* web_view, const std::string& url, std::atomic& counter) +{ + delete_cookies(web_view, url); + counter++; +} + void add_request_authorization(wxWebView* web_view, const wxString& address, const std::string& token) { // unused on Linux diff --git a/src/slic3r/GUI/WebViewPlatformUtilsMac.mm b/src/slic3r/GUI/WebViewPlatformUtilsMac.mm index ad04db6..feda9ea 100644 --- a/src/slic3r/GUI/WebViewPlatformUtilsMac.mm +++ b/src/slic3r/GUI/WebViewPlatformUtilsMac.mm @@ -156,6 +156,13 @@ void delete_cookies(wxWebView* web_view, const std::string& url) } }]; } + +void delete_cookies_with_counter(wxWebView* web_view, const std::string& url, std::atomic& counter) +{ + delete_cookies(web_view, url); + counter++; +} + void add_request_authorization(wxWebView* web_view, const wxString& address, const std::string& token) { // unused on MacOS diff --git a/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp b/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp index 819b663..9ae285b 100644 --- a/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp +++ b/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp @@ -150,7 +150,78 @@ void delete_cookies(wxWebView* webview, const std::string& url) ).Get()); } +void delete_cookies_with_counter(wxWebView* webview, const std::string& url, std::atomic& counter) +{ + ICoreWebView2 *webView2 = static_cast(webview->GetNativeBackend()); + if (!webView2) { + BOOST_LOG_TRIVIAL(error) << "delete_cookies Failed: webView2 is null."; + return; + } + /* + "cookies": [{ + "domain": ".google.com", + "expires": 1756464458.304917, + "httpOnly": true, + "name": "__Secure-1PSIDCC", + "path": "/", + "priority": "High", + "sameParty": false, + "secure": true, + "session": false, + "size": 90, + "sourcePort": 443, + "sourceScheme": "Secure", + "value": "AKEyXzUvV_KBqM4aOlsudROI_VZ-ToIH41LRbYJFtFjmKq_rOmx1owoyUGvQHbwr5be380fKuQ" + },...]} + */ + wxString parameters = GUI::format_wxstr(L"{\"urls\": [\"%1%\"]}", url); + webView2->CallDevToolsProtocolMethod(L"Network.getCookies", parameters.c_str(), + Microsoft::WRL::Callback( + [webView2, url, &counter](HRESULT errorCode, LPCWSTR resultJson) -> HRESULT { + if (FAILED(errorCode)) { + return S_OK; + } + // Handle successful call (resultJson contains the list of cookies) + pt::ptree ptree; + try { + std::stringstream ss(GUI::into_u8(resultJson)); + pt::read_json(ss, ptree); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Failed to parse cookies json: " << e.what(); + return S_OK; + } + auto& cookies = ptree.get_child("cookies"); + if (cookies.size() == 0) { + counter++; + return S_OK; + } + for (auto it = cookies.begin(); it != cookies.end(); ++it) { + const auto& cookie = *it; + std::string name = cookie.second.get("name"); + std::string domain = cookie.second.get("domain"); + // Delete cookie by name and domain + wxString name_and_domain = GUI::format_wxstr(L"{\"name\": \"%1%\", \"domain\": \"%2%\"}", name, domain); + BOOST_LOG_TRIVIAL(debug) << "Deleting cookie: " << name_and_domain; + bool last = false; + if (std::next(it) == cookies.end()) { + last = true; + } + webView2->CallDevToolsProtocolMethod(L"Network.deleteCookies", name_and_domain.c_str(), + Microsoft::WRL::Callback( + [last, &counter](HRESULT errorCode, LPCWSTR resultJson) -> HRESULT { + if (last) { + counter++; + } + return S_OK; + }).Get()); + } + return S_OK; + } + ).Get()); + +} static EventRegistrationToken m_webResourceRequestedTokenForImageBlocking = {}; static wxString filter_patern; namespace { @@ -167,7 +238,7 @@ void RequestHeadersToLog(ICoreWebView2HttpRequestHeaders* requestHeaders) wchar_t* value = nullptr; iterator->GetCurrentHeader(&name, &value); - BOOST_LOG_TRIVIAL(debug) <<"name: " << name << L", value: " << value; + BOOST_LOG_TRIVIAL(trace) <<"name: " << name << L", value: " << value; if (name) { CoTaskMemFree(name); } diff --git a/src/slic3r/GUI/WifiConfigDialog.cpp b/src/slic3r/GUI/WifiConfigDialog.cpp index 0e2d5fe..cc89da4 100644 --- a/src/slic3r/GUI/WifiConfigDialog.cpp +++ b/src/slic3r/GUI/WifiConfigDialog.cpp @@ -251,7 +251,7 @@ void WifiConfigDialog::on_ok(wxCommandEvent& e) } if (boost::filesystem::exists(file_path)) { - // TRN placeholder 1 is path to file + // TRN placeholder %1% is path to file wxString msg_text = GUI::format_wxstr(_L("%1% already exists. Do you want to rewrite it?\n(Other items than Wi-Fi credentials will stay unchanged)"), file_path.string()); WarningDialog dialog(m_parent, msg_text, _L("Warning"), wxYES | wxNO); if (dialog.ShowModal() == wxID_NO) { diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 243994e..7b8f900 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -7,6 +7,7 @@ #include #include +#include #include "BitmapCache.hpp" #include "GUI.hpp" @@ -112,6 +113,10 @@ wxMenuItem* append_menu_item(wxMenu* menu, int id, const wxString& string, const entry->SetMenuItem(item); accelerator_entries_cache().push_back(entry); } + else { + delete entry; + entry = nullptr; + } } #endif @@ -510,8 +515,11 @@ ScalableBitmap::ScalableBitmap(wxWindow* parent, boost::filesystem::path& icon_p const std::string ext = icon_path.extension().string(); if (ext == ".png" || ext == ".jpg") { - bitmap.LoadFile(path, ext == ".png" ? wxBITMAP_TYPE_PNG : wxBITMAP_TYPE_JPEG); - + if (!bitmap.LoadFile(path, ext == ".png" ? wxBITMAP_TYPE_PNG : wxBITMAP_TYPE_JPEG)) { + BOOST_LOG_TRIVIAL(error) << "Failed to load bitmap " << path; + return; + } + // check if the bitmap has a square shape if (wxSize sz = bitmap.GetSize(); sz.x != sz.y) { @@ -754,11 +762,33 @@ void HighlighterForWx::init(std::pair params) if (!Highlighter::init(!params.first && !params.second)) return; - m_custom_ctrl = params.first; - m_show_blink_ptr = params.second; + assert(m_blinking_custom_ctrls.empty()); + m_blinking_custom_ctrls.push_back({params.first, params.second}); - *m_show_blink_ptr = true; - m_custom_ctrl->Refresh(); + BlinkingCustomCtrl &blinking_custom_ctrl = m_blinking_custom_ctrls.back(); + *blinking_custom_ctrl.show_blink_ptr = true; + blinking_custom_ctrl.custom_ctrl_ptr->Refresh(); +} + +void HighlighterForWx::init(const std::vector> &blinking_custom_ctrls_params) +{ + this->invalidate(); + + const bool input_failed = blinking_custom_ctrls_params.empty() || + std::any_of(blinking_custom_ctrls_params.cbegin(), blinking_custom_ctrls_params.cend(), + [](auto ¶ms) { return params.first == nullptr || params.second == nullptr; }); + + if (!Highlighter::init(input_failed)) + return; + + assert(m_blinking_custom_ctrls.empty()); + for (const std::pair &blinking_custom_ctrl_params : blinking_custom_ctrls_params) { + m_blinking_custom_ctrls.push_back({blinking_custom_ctrl_params.first, blinking_custom_ctrl_params.second}); + + BlinkingCustomCtrl &blinking_custom_ctrl = m_blinking_custom_ctrls.back(); + *blinking_custom_ctrl.show_blink_ptr = true; + blinking_custom_ctrl.custom_ctrl_ptr->Refresh(); + } } // - using a BlinkingBitmap. Change state of this bitmap @@ -776,13 +806,15 @@ void HighlighterForWx::invalidate() { Highlighter::invalidate(); - if (m_custom_ctrl && m_show_blink_ptr) { - *m_show_blink_ptr = false; - m_custom_ctrl->Refresh(); - m_show_blink_ptr = nullptr; - m_custom_ctrl = nullptr; - } - else if (m_blinking_bitmap) { + if (!m_blinking_custom_ctrls.empty()) { + for (BlinkingCustomCtrl &blinking_custom_ctrl : m_blinking_custom_ctrls) { + assert(blinking_custom_ctrl.is_valid()); + *blinking_custom_ctrl.show_blink_ptr = false; + blinking_custom_ctrl.custom_ctrl_ptr->Refresh(); + } + + m_blinking_custom_ctrls.clear(); + } else if (m_blinking_bitmap) { m_blinking_bitmap->invalidate(); m_blinking_bitmap = nullptr; } @@ -790,14 +822,17 @@ void HighlighterForWx::invalidate() void HighlighterForWx::blink() { - if (m_custom_ctrl && m_show_blink_ptr) { - *m_show_blink_ptr = !*m_show_blink_ptr; - m_custom_ctrl->Refresh(); - } - else if (m_blinking_bitmap) + if (!m_blinking_custom_ctrls.empty()) { + for (BlinkingCustomCtrl &blinking_custom_ctrl : m_blinking_custom_ctrls) { + assert(blinking_custom_ctrl.is_valid()); + *blinking_custom_ctrl.show_blink_ptr = !*blinking_custom_ctrl.show_blink_ptr; + blinking_custom_ctrl.custom_ctrl_ptr->Refresh(); + } + } else if (m_blinking_bitmap) { m_blinking_bitmap->blink(); - else + } else { return; + } Highlighter::blink(); } diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index fed59bb..76993db 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -284,12 +284,24 @@ public: class HighlighterForWx : public Highlighter { + struct BlinkingCustomCtrl + { + OG_CustomCtrl *custom_ctrl_ptr; + bool *show_blink_ptr; + + bool is_valid() const + { + return custom_ctrl_ptr != nullptr && show_blink_ptr != nullptr; + } + }; + + using BlinkingCustomCtrls = std::vector; + // There are 2 possible cases to use HighlighterForWx: // - using a BlinkingBitmap. Change state of this bitmap - BlinkingBitmap* m_blinking_bitmap { nullptr }; + BlinkingBitmap *m_blinking_bitmap { nullptr }; // - using OG_CustomCtrl where arrow will be rendered and flag indicated "show/hide" state of this arrow - OG_CustomCtrl* m_custom_ctrl { nullptr }; - bool* m_show_blink_ptr { nullptr }; + BlinkingCustomCtrls m_blinking_custom_ctrls; public: HighlighterForWx() {} @@ -298,6 +310,7 @@ public: void bind_timer(wxWindow* owner) override; void init(BlinkingBitmap* blinking_bitmap); void init(std::pair); + void init(const std::vector> &blinking_custom_ctrls_params); void blink(); void invalidate(); }; diff --git a/src/slic3r/Utils/OctoPrint.hpp b/src/slic3r/Utils/OctoPrint.hpp index ac11764..ea43a58 100644 --- a/src/slic3r/Utils/OctoPrint.hpp +++ b/src/slic3r/Utils/OctoPrint.hpp @@ -118,6 +118,7 @@ public: PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint | PrintHostPostUploadAction::QueuePrint; } const char* get_name() const override { return "QIDIConnect"; } bool get_storage(wxArrayString& storage_path, wxArrayString& storage_name) const override { return false; } + std::string get_unusable_symbols() const override { return "\\/:*?\"%<>¯°#ˇ|[]"; } protected: void set_http_post_header_args(Http& http, PrintHostPostUploadAction post_action) const override; }; diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index f23b4e8..5f1f577 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -310,7 +310,8 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const ArchiveRep // Download profiles archive zip fs::path archive_path(cache_path / "vendor_indices.zip"); if (!archive_repository->get_archive(archive_path, ui_status)) { - BOOST_LOG_TRIVIAL(error) << "Download of vedor profiles archive zip of " << archive_repository->get_manifest().id << " repository has failed."; + BOOST_LOG_TRIVIAL(error) << "Download of vendor profiles archive zip of " << archive_repository->get_manifest().id << " repository has failed."; + ui_status->add_failed_archive(archive_repository->get_manifest().id); return; } if (ui_status->get_canceled()) { diff --git a/src/slic3r/Utils/PresetUpdaterWrapper.cpp b/src/slic3r/Utils/PresetUpdaterWrapper.cpp index 51123dd..9ad34a9 100644 --- a/src/slic3r/Utils/PresetUpdaterWrapper.cpp +++ b/src/slic3r/Utils/PresetUpdaterWrapper.cpp @@ -1,6 +1,7 @@ #include "PresetUpdaterWrapper.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/MsgDialog.hpp" #include "slic3r/GUI/format.hpp" @@ -15,10 +16,54 @@ namespace Slic3r { wxDEFINE_EVENT(EVT_PRESET_UPDATER_STATUS_END, PresetUpdaterStatusSimpleEvent); wxDEFINE_EVENT(EVT_PRESET_UPDATER_STATUS_PRINT, PresetUpdaterStatusMessageEvent); wxDEFINE_EVENT(EVT_CONFIG_UPDATER_SYNC_DONE, wxCommandEvent); +wxDEFINE_EVENT(EVT_CONFIG_UPDATER_FAILED_ARCHIVE, wxCommandEvent); + +namespace { +// Returns string of vendors that failed archive download. divided by new line +std::string proccess_failed_archives(const std::vector& failed_archives, const VendorMap& vendors, const SharedArchiveRepositoryVector &repos) +{ + std::string failed_vendors; + for (const std::string& failed_archive : failed_archives) { + // find if failed_archive is secret + if (const auto it = + std::find_if(repos.begin(), repos.end(), + [failed_archive](const auto* rep){ + return rep->get_manifest().id == failed_archive; + }) + ; it != repos.end()) + { + // add all installed vendors of failed_archive + for (const auto& pair :vendors) { + if (pair.second.repo_id == failed_archive) { + failed_vendors += pair.second.name + "\n"; + } + } + } + } + return failed_vendors; +} +void display_failed_vendors_dialog(wxWindow *parent, const std::string& failed_vendors, bool logged) +{ + std::string dialog_text; + if (logged) { + // TRN Dialog text, %1% is list of vendors. + dialog_text = format(_u8L("Update check failed for the following vendors:\n\n%1%\n" + "This may be because you are no longer subscribed to some configuration sources.\n" + "Please manage your configuration sources in Configuration Wizard"), failed_vendors); + } else { + // TRN Dialog text, %1% is list of vendors. + dialog_text = format(_u8L("Update check failed for the following vendors:\n\n%1%\n" + "Please log in to restore access to all your subscribed configuration sources."), failed_vendors); + } + GUI::WarningDialog dialog(parent, dialog_text, _L("Warning"), wxOK); + dialog.ShowModal(); +} +} PresetUpdaterWrapper::PresetUpdaterWrapper() : m_preset_updater(std::make_unique()) , m_preset_archive_database(std::make_unique()) + , m_ui_status(std::make_unique()) { } PresetUpdaterWrapper::~PresetUpdaterWrapper() @@ -35,27 +80,28 @@ bool PresetUpdaterWrapper::wizard_sync(const PresetBundle* preset_bundle, const assert(!m_modal_thread.joinable()); // Cancel sync before starting wizard to prevent two downloads at same time. cancel_worker_thread(); - PresetUpdaterUIStatus* ui_status = new PresetUpdaterUIStatus(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_5_TRIES); - Slic3r::ScopeGuard sg([ui_status]() { delete ui_status; }); - GUI::ProgressUpdaterDialog* dialog = new GUI::ProgressUpdaterDialog(ui_status, parent, headline); - ui_status->set_handler(dialog); + + m_ui_status->reset(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_5_TRIES); + + GUI::ProgressUpdaterDialog* dialog = new GUI::ProgressUpdaterDialog(m_ui_status.get(), parent, headline); + m_ui_status->set_handler(dialog); VendorMap vendors_copy = preset_bundle->vendors; - auto worker_body = [ui_status, this, vendors_copy, full_sync]() + auto worker_body = [this, vendors_copy, full_sync]() { - if (!m_preset_archive_database->sync_blocking(ui_status)) { - ui_status->end(); + if (!m_preset_archive_database->sync_blocking(m_ui_status.get())) { + m_ui_status->end(); return; } - if (ui_status->get_canceled()) { ui_status->end(); return; } + if (m_ui_status->get_canceled()) { m_ui_status->end(); return; } if (full_sync) { // Since there might be new repos, we need to sync preset updater const SharedArchiveRepositoryVector &repos = m_preset_archive_database->get_selected_archive_repositories(); - m_preset_updater->sync_blocking(vendors_copy, repos, ui_status); - if (ui_status->get_canceled()) { ui_status->end(); return; } + m_preset_updater->sync_blocking(vendors_copy, repos, m_ui_status.get()); + if (m_ui_status->get_canceled()) { m_ui_status->end(); return; } m_preset_updater->update_index_db(); } - ui_status->end(); + m_ui_status->end(); }; m_modal_thread = std::thread(worker_body); // We need to call ShowModal here instead of prompting it from event callback. @@ -65,28 +111,35 @@ bool PresetUpdaterWrapper::wizard_sync(const PresetBundle* preset_bundle, const m_modal_thread.join(); parent->RemoveChild(dialog); dialog->Destroy(); - ui_status->set_handler(nullptr); + m_ui_status->set_handler(nullptr); // Only now it is possible to work with ui_status, that was previously used in worker thread. - if (std::string s = ui_status->get_error(); !s.empty()) { - std::string err_text = GUI::format(_u8L("Failed to download %1%"), ui_status->get_target()); + if (std::string s = m_ui_status->get_error(); !s.empty()) { + std::string err_text = GUI::format(_u8L("Failed to download %1%"), m_ui_status->get_target()); GUI::ErrorDialog err_msg(nullptr, err_text + "\n\n" + s, false); err_msg.ShowModal(); return false; } // Should m_preset_updater->config_update run even if there is cancel? - if (ui_status->get_canceled() /*&& !full_sync*/) { + if (m_ui_status->get_canceled() /*&& !full_sync*/) { return false; } + // Find secret vendors that failed to download idx in archive + const SharedArchiveRepositoryVector &repos = m_preset_archive_database->get_selected_archive_repositories(); + std::string failed_vendors = proccess_failed_archives(m_ui_status->get_failed_archives(), vendors_copy, repos); + if (!failed_vendors.empty()) { + display_failed_vendors_dialog(parent, failed_vendors, GUI::wxGetApp().is_account_logged_in()); + } + // Offer update installation. if (full_sync) { const SharedArchiveRepositoryVector &repos = m_preset_archive_database->get_selected_archive_repositories(); - m_preset_updater->config_update(old_slic3r_version, PresetUpdater::UpdateParams::SHOW_TEXT_BOX_YES_NO, repos, ui_status); + m_preset_updater->config_update(old_slic3r_version, PresetUpdater::UpdateParams::SHOW_TEXT_BOX_YES_NO, repos, m_ui_status.get()); } - bool res = !ui_status->get_canceled(); + bool res = !m_ui_status->get_canceled(); return res; } @@ -94,28 +147,35 @@ PresetUpdater::UpdateResult PresetUpdaterWrapper::check_updates_on_user_request( { assert(!m_modal_thread.joinable()); cancel_worker_thread(); - PresetUpdaterUIStatus* ui_status = new PresetUpdaterUIStatus(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_5_TRIES); - Slic3r::ScopeGuard sg([ui_status]() { delete ui_status; }); + + m_ui_status->reset(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_5_TRIES); + // TRN: Headline of Progress dialog - GUI::ProgressUpdaterDialog* dialog = new GUI::ProgressUpdaterDialog(ui_status, parent, _L("Checking for Configuration Updates")); - ui_status->set_handler(dialog); + GUI::ProgressUpdaterDialog* dialog = new GUI::ProgressUpdaterDialog(m_ui_status.get(), parent, _L("Checking for Configuration Updates")); + m_ui_status->set_handler(dialog); VendorMap vendors_copy = preset_bundle->vendors; std::string failed_paths; PresetUpdater::UpdateResult updater_result = PresetUpdater::UpdateResult::R_ALL_CANCELED; - auto worker_body = [ui_status, this, vendors_copy, &failed_paths]() + auto worker_body = [this, vendors_copy, &failed_paths]() { - if (!m_preset_archive_database->sync_blocking(ui_status)) { - ui_status->end(); + if (!m_preset_archive_database->sync_blocking(m_ui_status.get())) { + m_ui_status->end(); return; } - if (ui_status->get_canceled()) { ui_status->end(); return; } + if (m_ui_status->get_canceled()) { + m_ui_status->end(); + return; + } m_preset_archive_database->extract_archives_with_check(failed_paths); const SharedArchiveRepositoryVector &repos = m_preset_archive_database->get_selected_archive_repositories(); - m_preset_updater->sync_blocking(vendors_copy, repos, ui_status); - if (ui_status->get_canceled()) { ui_status->end(); return; } + m_preset_updater->sync_blocking(vendors_copy, repos, m_ui_status.get()); + if (m_ui_status->get_canceled()) { + m_ui_status->end(); + return; + } m_preset_updater->update_index_db(); - ui_status->end(); + m_ui_status->end(); }; m_modal_thread = std::thread(worker_body); @@ -126,18 +186,18 @@ PresetUpdater::UpdateResult PresetUpdaterWrapper::check_updates_on_user_request( m_modal_thread.join(); parent->RemoveChild(dialog); dialog->Destroy(); - ui_status->set_handler(nullptr); + m_ui_status->set_handler(nullptr); // Only now it is possible to work with ui_status, that was previously used in worker thread. - if (std::string s = ui_status->get_error(); !s.empty()) { - std::string err_text = GUI::format(_u8L("Failed to download %1%"), ui_status->get_target()); + if (std::string s = m_ui_status->get_error(); !s.empty()) { + std::string err_text = GUI::format(_u8L("Failed to download %1%"), m_ui_status->get_target()); GUI::ErrorDialog err_msg(nullptr, s, false); err_msg.ShowModal(); return PresetUpdater::UpdateResult::R_ALL_CANCELED; } - if (ui_status->get_canceled()) { + if (m_ui_status->get_canceled()) { return PresetUpdater::UpdateResult::R_ALL_CANCELED; } @@ -149,8 +209,16 @@ PresetUpdater::UpdateResult PresetUpdaterWrapper::check_updates_on_user_request( GUI::ErrorDialog err_msg(nullptr, failed_paths, false); err_msg.ShowModal(); } + + // Find secret vendors that failed to download idx in archive + const SharedArchiveRepositoryVector &repos = m_preset_archive_database->get_selected_archive_repositories(); + std::string failed_vendors = proccess_failed_archives(m_ui_status->get_failed_archives(), vendors_copy, repos); + if (!failed_vendors.empty()) { + display_failed_vendors_dialog(parent, failed_vendors, GUI::wxGetApp().is_account_logged_in()); + } + // preset_updater::config_update does show wxDialog - updater_result = m_preset_updater->config_update(old_slic3r_version, PresetUpdater::UpdateParams::SHOW_TEXT_BOX, m_preset_archive_database->get_selected_archive_repositories(), ui_status); + updater_result = m_preset_updater->config_update(old_slic3r_version, PresetUpdater::UpdateParams::SHOW_TEXT_BOX, m_preset_archive_database->get_selected_archive_repositories(), m_ui_status.get()); return updater_result; } @@ -159,10 +227,10 @@ PresetUpdater::UpdateResult PresetUpdaterWrapper::check_updates_on_startup(const if (m_modal_thread.joinable()) { return PresetUpdater::UpdateResult::R_ALL_CANCELED; } - PresetUpdaterUIStatus ui_status(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_NO_RETRY); - // preset_updater::config_update does show wxDialog + m_ui_status->reset(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_NO_RETRY); + m_preset_updater->update_index_db(); - return m_preset_updater->config_update(old_slic3r_version, PresetUpdater::UpdateParams::SHOW_NOTIFICATION, m_preset_archive_database->get_selected_archive_repositories(), &ui_status); + return m_preset_updater->config_update(old_slic3r_version, PresetUpdater::UpdateParams::SHOW_NOTIFICATION, m_preset_archive_database->get_selected_archive_repositories(), m_ui_status.get()); } void PresetUpdaterWrapper::on_update_notification_confirm() @@ -170,32 +238,41 @@ void PresetUpdaterWrapper::on_update_notification_confirm() if (m_modal_thread.joinable()) { return; } - PresetUpdaterUIStatus ui_status(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_NO_RETRY); + m_ui_status->reset(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_NO_RETRY); + // preset_updater::on_update_notification_confirm does show wxDialog const SharedArchiveRepositoryVector &repos = m_preset_archive_database->get_selected_archive_repositories(); - m_preset_updater->on_update_notification_confirm(repos, &ui_status); + m_preset_updater->on_update_notification_confirm(repos, m_ui_status.get()); } bool PresetUpdaterWrapper::install_bundles_rsrc_or_cache_vendor(std::vector bundles, bool snapshot/* = true*/) const { - PresetUpdaterUIStatus ui_status(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_NO_RETRY); - const SharedArchiveRepositoryVector &repos = m_preset_archive_database->get_selected_archive_repositories(); - return m_preset_updater->install_bundles_rsrc_or_cache_vendor(bundles, repos, &ui_status, snapshot); + m_ui_status->reset(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_NO_RETRY); + const SharedArchiveRepositoryVector &repos = m_preset_archive_database->get_selected_archive_repositories(); + return m_preset_updater->install_bundles_rsrc_or_cache_vendor(bundles, repos, m_ui_status.get(), snapshot); } void PresetUpdaterWrapper::sync_preset_updater(wxEvtHandler* end_evt_handler, const PresetBundle* preset_bundle) { cancel_worker_thread(); - m_ui_status = new PresetUpdaterUIStatus(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_NO_RETRY); + m_ui_status->reset(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_NO_RETRY); VendorMap vendors_copy = preset_bundle->vendors; auto worker_body = [ this, vendors_copy, end_evt_handler]() { const SharedArchiveRepositoryVector &repos = m_preset_archive_database->get_selected_archive_repositories(); - m_preset_updater->sync_blocking(vendors_copy, repos, this->m_ui_status); + m_preset_updater->sync_blocking(vendors_copy, repos, m_ui_status.get()); if (this->m_ui_status->get_canceled()) { return; } wxCommandEvent* evt = new wxCommandEvent(EVT_CONFIG_UPDATER_SYNC_DONE); wxQueueEvent(end_evt_handler, evt); + + // Find secret vendors that failed to download idx in archive + std::string failed_vendors = proccess_failed_archives(m_ui_status->get_failed_archives(), vendors_copy, repos); + if (!failed_vendors.empty()) { + wxCommandEvent* evt_arch = new wxCommandEvent(EVT_CONFIG_UPDATER_FAILED_ARCHIVE); + evt_arch->SetString(GUI::from_u8(failed_vendors)); + wxQueueEvent(end_evt_handler, evt_arch); + } }; m_worker_thread = std::thread(worker_body); @@ -209,11 +286,6 @@ void PresetUpdaterWrapper::cancel_worker_thread() } else assert(false); m_worker_thread.join(); - - if (m_ui_status) { - delete m_ui_status; - m_ui_status = nullptr; - } } } @@ -221,7 +293,11 @@ const std::map Pr {PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_5_TRIES, {500ms, 5s, 4}}, {PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_NO_RETRY, {0ms}} }; -PresetUpdaterUIStatus::PresetUpdaterUIStatus(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy policy) +PresetUpdaterUIStatus::PresetUpdaterUIStatus() +{ +} + +void PresetUpdaterUIStatus::reset(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy policy) { if (auto it = policy_map.find(policy); it != policy_map.end()) { m_retry_policy = it->second; @@ -229,7 +305,14 @@ PresetUpdaterUIStatus::PresetUpdaterUIStatus(PresetUpdaterUIStatus::PresetUpdate assert(false); m_retry_policy = {0ms}; } + + m_canceled = false; + m_evt_handler = nullptr; + m_error_msg.clear(); + m_target.clear(); + m_failed_archives.clear(); } + bool PresetUpdaterUIStatus::on_attempt(int attempt, unsigned delay) { if (attempt == 1) { diff --git a/src/slic3r/Utils/PresetUpdaterWrapper.hpp b/src/slic3r/Utils/PresetUpdaterWrapper.hpp index bf21df0..f105537 100644 --- a/src/slic3r/Utils/PresetUpdaterWrapper.hpp +++ b/src/slic3r/Utils/PresetUpdaterWrapper.hpp @@ -25,6 +25,8 @@ using PresetUpdaterStatusMessageEvent = GUI::Event; wxDECLARE_EVENT(EVT_PRESET_UPDATER_STATUS_END, PresetUpdaterStatusSimpleEvent); wxDECLARE_EVENT(EVT_PRESET_UPDATER_STATUS_PRINT, PresetUpdaterStatusMessageEvent); wxDECLARE_EVENT(EVT_CONFIG_UPDATER_SYNC_DONE, wxCommandEvent); +wxDECLARE_EVENT(EVT_CONFIG_UPDATER_FAILED_ARCHIVE, wxCommandEvent); + class PresetBundle; class Semver; @@ -40,10 +42,12 @@ public: PURP_NO_RETRY, }; // called from PresetUpdaterWrapper - PresetUpdaterUIStatus(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy policy); + PresetUpdaterUIStatus(); ~PresetUpdaterUIStatus(){} void set_handler(wxEvtHandler* evt_handler) {m_evt_handler = evt_handler;} + void reset(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy policy); + // called from worker thread bool on_attempt(int attempt, unsigned delay); void set_target(const std::string& target); @@ -53,6 +57,8 @@ public: HttpRetryOpt get_retry_policy() const { return m_retry_policy; } std::string get_error() const { return m_error_msg; } std::string get_target() const { return m_target; } + void add_failed_archive(const std::string& id) { m_failed_archives.emplace_back(id); } + const std::vector& get_failed_archives() { return m_failed_archives; } // called from PresetUpdaterUIStatusCancel (ui thread) void set_canceled(bool val) { m_canceled.store(val); } @@ -66,6 +72,8 @@ private: HttpRetryOpt m_retry_policy; static const std::map policy_map; + + std::vector m_failed_archives; }; // Purpose of this class: @@ -152,7 +160,7 @@ private: // m_worker_thread runs on background while m_modal_thread runs only when modal window exists. std::thread m_worker_thread; - PresetUpdaterUIStatus* m_ui_status {nullptr}; + std::unique_ptr m_ui_status; std::thread m_modal_thread; }; diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp index b4762a8..9d929a7 100644 --- a/src/slic3r/Utils/PrintHost.hpp +++ b/src/slic3r/Utils/PrintHost.hpp @@ -77,6 +77,7 @@ public: // Support for QIDILink uploading to different storage. Not supported by other print hosts. // Returns false if not supported or fail. virtual bool get_storage(wxArrayString& /*storage_path*/, wxArrayString& /*storage_name*/) const { return false; } + virtual std::string get_unusable_symbols() const { return {}; } static PrintHost* get_print_host(DynamicPrintConfig *config); //B64 diff --git a/src/slic3r/Utils/QIDIConnect.cpp b/src/slic3r/Utils/QIDIConnect.cpp index 50c003c..36dfcbe 100644 --- a/src/slic3r/Utils/QIDIConnect.cpp +++ b/src/slic3r/Utils/QIDIConnect.cpp @@ -118,7 +118,8 @@ bool QIDIConnectNew::upload(PrintHostUpload upload_data, ProgressFn progress_fn, std::string json = GUI::format(upload_data.data_json, "", "1"); boost::property_tree::ptree ptree; const std::string printer_uuid = GUI::UserAccountUtils::get_keyword_from_json(ptree, json, "printer_uuid"); - wxString printer_page_url = GUI::format("https://connect.qidi3d.com/printer/%1%/dashboard", printer_uuid); + auto& sc = Utils::ServiceConfig::instance(); + wxString printer_page_url = GUI::format("%1%/printer/%2%/dashboard", sc.connect_url(), printer_uuid); info_fn(L"qidiconnect_printer_address", printer_page_url); std::string init_out; diff --git a/src/slic3r/Utils/QIDIConnect.hpp b/src/slic3r/Utils/QIDIConnect.hpp index 312634c..2d8a142 100644 --- a/src/slic3r/Utils/QIDIConnect.hpp +++ b/src/slic3r/Utils/QIDIConnect.hpp @@ -42,6 +42,7 @@ public: bool get_storage(wxArrayString& storage_path, wxArrayString& storage_name) const override; // const std::string& get_apikey() const { return m_apikey; } // const std::string& get_cafile() const { return m_cafile; } + std::string get_unusable_symbols() const override { return "\\/:*?\"%<>¯°#ˇ|[]"; } private: std::string m_uuid; diff --git a/src/slic3r/Utils/ServiceConfig.hpp b/src/slic3r/Utils/ServiceConfig.hpp index 20c3547..7eab93d 100644 --- a/src/slic3r/Utils/ServiceConfig.hpp +++ b/src/slic3r/Utils/ServiceConfig.hpp @@ -10,7 +10,7 @@ public: const std::string& connect_url() const { return m_connect_url; } std::string connect_status_url() const { return m_connect_url + "/slicer/status"; } - std::string connect_printer_list_url() const { return m_connect_url + "/slicer/printer_list"; } + std::string connect_printer_list_url() const { return m_connect_url + "/slicer/v1/printers"; } std::string connect_select_printer_url() const { return m_connect_url + "/slicer-select-printer"; } std::string connect_printers_url() const { return m_connect_url + "/app/printers/"; } std::string connect_teams_url() const { return m_connect_url + "/app/teams"; } diff --git a/src/slic3r/Utils/WifiScanner.cpp b/src/slic3r/Utils/WifiScanner.cpp index 4c2312e..9eb5b81 100644 --- a/src/slic3r/Utils/WifiScanner.cpp +++ b/src/slic3r/Utils/WifiScanner.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #ifdef _WIN32 #include